diff options
Diffstat (limited to 'core/java')
277 files changed, 16752 insertions, 17512 deletions
diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java index 9d6ee80..811b92a 100644 --- a/core/java/android/accessibilityservice/AccessibilityService.java +++ b/core/java/android/accessibilityservice/AccessibilityService.java @@ -339,7 +339,10 @@ public abstract class AccessibilityService extends Service { private static final String LOG_TAG = "AccessibilityService"; - interface Callbacks { + /** + * @hide + */ + public interface Callbacks { public void onAccessibilityEvent(AccessibilityEvent event); public void onInterrupt(); public void onServiceConnected(); @@ -454,7 +457,7 @@ public abstract class AccessibilityService extends Service { * * @return The accessibility service info. * - * @see AccessibilityNodeInfo + * @see AccessibilityServiceInfo */ public final AccessibilityServiceInfo getServiceInfo() { IAccessibilityServiceConnection connection = @@ -538,8 +541,10 @@ public abstract class AccessibilityService extends Service { /** * Implements the internal {@link IAccessibilityServiceClient} interface to convert * incoming calls to it back to calls on an {@link AccessibilityService}. + * + * @hide */ - static class IAccessibilityServiceClientWrapper extends IAccessibilityServiceClient.Stub + public static class IAccessibilityServiceClientWrapper extends IAccessibilityServiceClient.Stub implements HandlerCaller.Callback { static final int NO_ID = -1; @@ -548,6 +553,7 @@ public abstract class AccessibilityService extends Service { private static final int DO_ON_INTERRUPT = 20; private static final int DO_ON_ACCESSIBILITY_EVENT = 30; private static final int DO_ON_GESTURE = 40; + private static final int DO_CLEAR_ACCESSIBILITY_NODE_INFO_CACHE = 50; private final HandlerCaller mCaller; @@ -580,6 +586,11 @@ public abstract class AccessibilityService extends Service { mCaller.sendMessage(message); } + public void clearAccessibilityNodeInfoCache() { + Message message = mCaller.obtainMessage(DO_CLEAR_ACCESSIBILITY_NODE_INFO_CACHE); + mCaller.sendMessage(message); + } + public void executeMessage(Message message) { switch (message.what) { case DO_ON_ACCESSIBILITY_EVENT : @@ -604,6 +615,7 @@ public abstract class AccessibilityService extends Service { mCallback.onServiceConnected(); } else { AccessibilityInteractionClient.getInstance().removeConnection(connectionId); + AccessibilityInteractionClient.getInstance().clearCache(); mCallback.onSetConnectionId(AccessibilityInteractionClient.NO_ID); } return; @@ -611,6 +623,9 @@ public abstract class AccessibilityService extends Service { final int gestureId = message.arg1; mCallback.onGesture(gestureId); return; + case DO_CLEAR_ACCESSIBILITY_NODE_INFO_CACHE: + AccessibilityInteractionClient.getInstance().clearCache(); + return; default : Log.w(LOG_TAG, "Unknown message type " + message.what); } diff --git a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java index 75a4f83..d82b9a3 100644 --- a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java +++ b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java @@ -33,6 +33,7 @@ import android.util.TypedValue; import android.util.Xml; import android.view.View; import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -148,8 +149,47 @@ public class AccessibilityServiceInfo implements Parcelable { * accessibility service that has this flag set. Hence, clearing this * flag does not guarantee that the device will not be in touch exploration * mode since there may be another enabled service that requested it. + * <p> + * For accessibility services targeting API version higher than + * {@link Build.VERSION_CODES#JELLY_BEAN_MR1} that want to set + * this flag have to request the + * {@link android.Manifest.permission#CAN_REQUEST_TOUCH_EXPLORATION_MODE} + * permission or the flag will be ignored. + * </p> + * <p> + * Services targeting API version equal to or lower than + * {@link Build.VERSION_CODES#JELLY_BEAN_MR1} will work normally, i.e. + * the first time they are run, if this flag is specified, a dialog is + * shown to the user to confirm enabling explore by touch. + * </p> + */ + public static final int FLAG_REQUEST_TOUCH_EXPLORATION_MODE = 0x0000004; + + /** + * This flag requests from the system to enable web accessibility enhancing + * extensions. Such extensions aim to provide improved accessibility support + * for content presented in a {@link android.webkit.WebView}. An example of such + * an extension is injecting JavaScript from a secure source. The system will enable + * enhanced web accessibility if there is at least one accessibility service + * that has this flag set. Hence, clearing this flag does not guarantee that the + * device will not have enhanced web accessibility enabled since there may be + * another enabled service that requested it. + * <p> + * Clients that want to set this flag have to request the + * {@link android.Manifest.permission#CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY} + * permission or the flag will be ignored. + * </p> */ - public static final int FLAG_REQUEST_TOUCH_EXPLORATION_MODE= 0x0000004; + public static final int FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY = 0x00000008; + + /** + * This flag requests that the {@link AccessibilityNodeInfo}s obtained + * by an {@link AccessibilityService} contain the id of the source view. + * The source view id will be a fully qualified resource name of the + * form "package:id/name", for example "foo.bar:id/my_list", and it is + * useful for UI test automation. This flag is not set by default. + */ + public static final int FLAG_REPORT_VIEW_IDS = 0x00000010; /** * The event types an {@link AccessibilityService} is interested in. @@ -359,6 +399,13 @@ public class AccessibilityServiceInfo implements Parcelable { } /** + * @hide + */ + public void setComponentName(ComponentName component) { + mId = component.flattenToShortString(); + } + + /** * The accessibility service id. * <p> * <strong>Generated by the system.</strong> @@ -475,6 +522,33 @@ public class AccessibilityServiceInfo implements Parcelable { } @Override + public int hashCode() { + return 31 * 1 + ((mId == null) ? 0 : mId.hashCode()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + AccessibilityServiceInfo other = (AccessibilityServiceInfo) obj; + if (mId == null) { + if (other.mId != null) { + return false; + } + } else if (!mId.equals(other.mId)) { + return false; + } + return true; + } + + @Override public String toString() { StringBuilder stringBuilder = new StringBuilder(); appendEventTypes(stringBuilder, eventTypes); diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl index d459fd5..5d684e3 100644 --- a/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl +++ b/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl @@ -33,4 +33,6 @@ import android.view.accessibility.AccessibilityEvent; void onInterrupt(); void onGesture(int gesture); + + void clearAccessibilityNodeInfoCache(); } diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl index dd50f3c..7a29f35 100644 --- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl +++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl @@ -18,6 +18,7 @@ package android.accessibilityservice; import android.os.Bundle; import android.accessibilityservice.AccessibilityServiceInfo; +import android.view.MagnificationSpec; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.IAccessibilityInteractionConnectionCallback; @@ -44,9 +45,9 @@ interface IAccessibilityServiceConnection { * @param callback Callback which to receive the result. * @param flags Additional flags. * @param threadId The id of the calling thread. - * @return The current window scale, where zero means a failure. + * @return Whether the call succeeded. */ - float findAccessibilityNodeInfoByAccessibilityId(int accessibilityWindowId, + boolean findAccessibilityNodeInfoByAccessibilityId(int accessibilityWindowId, long accessibilityNodeId, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, long threadId); @@ -66,9 +67,9 @@ interface IAccessibilityServiceConnection { * @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. + * @return Whether the call succeeded. */ - float findAccessibilityNodeInfosByText(int accessibilityWindowId, long accessibilityNodeId, + boolean findAccessibilityNodeInfosByText(int accessibilityWindowId, long accessibilityNodeId, String text, int interactionId, IAccessibilityInteractionConnectionCallback callback, long threadId); @@ -84,15 +85,15 @@ interface IAccessibilityServiceConnection { * where to start the search. Use * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} * to start from the root. - * @param id The id of the node. + * @param viewId The fully qualified resource name of the view id to find. * @param interactionId The id of the interaction for matching with the callback result. * @param callback Callback which to receive the result. * @param threadId The id of the calling thread. - * @return The current window scale, where zero means a failure. + * @return Whether the call succeeded. */ - float findAccessibilityNodeInfoByViewId(int accessibilityWindowId, long accessibilityNodeId, - int viewId, int interactionId, IAccessibilityInteractionConnectionCallback callback, - long threadId); + boolean findAccessibilityNodeInfosByViewId(int accessibilityWindowId, + long accessibilityNodeId, String viewId, int interactionId, + IAccessibilityInteractionConnectionCallback callback, long threadId); /** * Finds the {@link android.view.accessibility.AccessibilityNodeInfo} that has the specified @@ -110,9 +111,9 @@ interface IAccessibilityServiceConnection { * @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. + * @return Whether the call succeeded. */ - float findFocus(int accessibilityWindowId, long accessibilityNodeId, int focusType, + boolean findFocus(int accessibilityWindowId, long accessibilityNodeId, int focusType, int interactionId, IAccessibilityInteractionConnectionCallback callback, long threadId); /** @@ -131,9 +132,9 @@ interface IAccessibilityServiceConnection { * @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. + * @return Whether the call succeeded. */ - float focusSearch(int accessibilityWindowId, long accessibilityNodeId, int direction, + boolean focusSearch(int accessibilityWindowId, long accessibilityNodeId, int direction, int interactionId, IAccessibilityInteractionConnectionCallback callback, long threadId); /** diff --git a/core/java/android/accessibilityservice/UiTestAutomationBridge.java b/core/java/android/accessibilityservice/UiTestAutomationBridge.java deleted file mode 100644 index 6837386..0000000 --- a/core/java/android/accessibilityservice/UiTestAutomationBridge.java +++ /dev/null @@ -1,496 +0,0 @@ -/* - * 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.IAccessibilityServiceClientWrapper; -import android.content.Context; -import android.os.Bundle; -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(); - - private static final int TIMEOUT_REGISTER_SERVICE = 5000; - - public static final int ACTIVE_WINDOW_ID = AccessibilityNodeInfo.ACTIVE_WINDOW_ID; - - public static final long ROOT_NODE_ID = AccessibilityNodeInfo.ROOT_NODE_ID; - - public static final int UNDEFINED = -1; - - private static final int FIND_ACCESSIBILITY_NODE_INFO_PREFETCH_FLAGS = - AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS - | AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS - | AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS; - - private final Object mLock = new Object(); - - private volatile int mConnectionId = AccessibilityInteractionClient.NO_ID; - - private IAccessibilityServiceClientWrapper mListener; - - private AccessibilityEvent mLastEvent; - - private volatile boolean mWaitingForEventDelivery; - - private volatile boolean mUnprocessedEventAvailable; - - private HandlerThread mHandlerThread; - - /** - * 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. - mHandlerThread = new HandlerThread("UiTestAutomationBridge"); - mHandlerThread.setDaemon(true); - mHandlerThread.start(); - Looper looper = mHandlerThread.getLooper(); - - mListener = new IAccessibilityServiceClientWrapper(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) { - mLastEvent = AccessibilityEvent.obtain(event); - if (!mWaitingForEventDelivery) { - mLock.notifyAll(); - break; - } - if (!mUnprocessedEventAvailable) { - mUnprocessedEventAvailable = true; - 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(); - } - } - - @Override - public boolean onGesture(int gestureId) { - return false; - } - }); - - 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; - info.flags |= AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS; - - 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."); - } - - mHandlerThread.quit(); - - 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 { - // TODO: This is broken - remove from here when finalizing this as public APIs. - 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 */ - } - } - } - } - - /** - * Waits for the accessibility event stream to become idle, which is not to - * have received a new accessibility event within <code>idleTimeout</code>, - * and do so within a maximal global timeout as specified by - * <code>globalTimeout</code>. - * - * @param idleTimeout The timeout between two event to consider the device idle. - * @param globalTimeout The maximal global timeout in which to wait for idle. - */ - public void waitForIdle(long idleTimeout, long globalTimeout) { - final long startTimeMillis = SystemClock.uptimeMillis(); - long lastEventTime = (mLastEvent != null) - ? mLastEvent.getEventTime() : SystemClock.uptimeMillis(); - synchronized (mLock) { - while (true) { - final long currentTimeMillis = SystemClock.uptimeMillis(); - final long sinceLastEventTimeMillis = currentTimeMillis - lastEventTime; - if (sinceLastEventTimeMillis > idleTimeout) { - return; - } - if (mLastEvent != null) { - lastEventTime = mLastEvent.getEventTime(); - } - final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; - final long remainingTimeMillis = globalTimeout - elapsedTimeMillis; - if (remainingTimeMillis <= 0) { - return; - } - try { - mLock.wait(idleTimeout); - } catch (InterruptedException e) { - /* 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, - FIND_ACCESSIBILITY_NODE_INFO_PREFETCH_FLAGS); - } - - /** - * Finds an {@link AccessibilityNodeInfo} by View id in the active - * window. The search is performed from the root node. - * - * @param viewId The id of a View. - * @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. - * @param viewId The id of a View. - * @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. - * @param arguments Optional action arguments. - * @return Whether the action was performed. - */ - public boolean performAccessibilityActionInActiveWindow(long accessibilityNodeId, int action, - Bundle arguments) { - return performAccessibilityAction(ACTIVE_WINDOW_ID, accessibilityNodeId, action, arguments); - } - - /** - * 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. - * @param arguments Optional action arguments. - * @return Whether the action was performed. - */ - public boolean performAccessibilityAction(int accessibilityWindowId, long accessibilityNodeId, - int action, Bundle arguments) { - // Cache the id to avoid locking - final int connectionId = mConnectionId; - ensureValidConnection(connectionId); - return AccessibilityInteractionClient.getInstance().performAccessibilityAction(connectionId, - accessibilityWindowId, accessibilityNodeId, action, arguments); - } - - /** - * Gets the root {@link AccessibilityNodeInfo} in the active window. - * - * @return The root info. - */ - public AccessibilityNodeInfo getRootAccessibilityNodeInfoInActiveWindow() { - // Cache the id to avoid locking - final int connectionId = mConnectionId; - ensureValidConnection(connectionId); - return AccessibilityInteractionClient.getInstance() - .findAccessibilityNodeInfoByAccessibilityId(connectionId, ACTIVE_WINDOW_ID, - ROOT_NODE_ID, AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS); - } - - private void ensureValidConnection(int connectionId) { - if (connectionId == UNDEFINED) { - throw new IllegalStateException("UiAutomationService not connected." - + " Did you call #register()?"); - } - } -} diff --git a/core/java/android/accounts/AccountAuthenticatorCache.java b/core/java/android/accounts/AccountAuthenticatorCache.java deleted file mode 100644 index f937cde..0000000 --- a/core/java/android/accounts/AccountAuthenticatorCache.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.accounts; - -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.pm.RegisteredServicesCache; -import android.content.pm.XmlSerializerAndParser; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.text.TextUtils; -import android.util.AttributeSet; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlSerializer; - -import java.io.IOException; - -/** - * A cache of services that export the {@link IAccountAuthenticator} interface. This cache - * is built by interrogating the {@link PackageManager} and is updated as packages are added, - * removed and changed. The authenticators are referred to by their account type and - * are made available via the {@link RegisteredServicesCache#getServiceInfo} method. - * @hide - */ -/* package private */ class AccountAuthenticatorCache - extends RegisteredServicesCache<AuthenticatorDescription> - implements IAccountAuthenticatorCache { - private static final String TAG = "Account"; - private static final MySerializer sSerializer = new MySerializer(); - - public AccountAuthenticatorCache(Context context) { - super(context, AccountManager.ACTION_AUTHENTICATOR_INTENT, - AccountManager.AUTHENTICATOR_META_DATA_NAME, - AccountManager.AUTHENTICATOR_ATTRIBUTES_NAME, sSerializer); - } - - public AuthenticatorDescription parseServiceAttributes(Resources res, - String packageName, AttributeSet attrs) { - TypedArray sa = res.obtainAttributes(attrs, - com.android.internal.R.styleable.AccountAuthenticator); - try { - final String accountType = - sa.getString(com.android.internal.R.styleable.AccountAuthenticator_accountType); - final int labelId = sa.getResourceId( - com.android.internal.R.styleable.AccountAuthenticator_label, 0); - final int iconId = sa.getResourceId( - com.android.internal.R.styleable.AccountAuthenticator_icon, 0); - final int smallIconId = sa.getResourceId( - com.android.internal.R.styleable.AccountAuthenticator_smallIcon, 0); - final int prefId = sa.getResourceId( - com.android.internal.R.styleable.AccountAuthenticator_accountPreferences, 0); - final boolean customTokens = sa.getBoolean( - com.android.internal.R.styleable.AccountAuthenticator_customTokens, false); - if (TextUtils.isEmpty(accountType)) { - return null; - } - return new AuthenticatorDescription(accountType, packageName, labelId, iconId, - smallIconId, prefId, customTokens); - } finally { - sa.recycle(); - } - } - - private static class MySerializer implements XmlSerializerAndParser<AuthenticatorDescription> { - public void writeAsXml(AuthenticatorDescription item, XmlSerializer out) - throws IOException { - out.attribute(null, "type", item.type); - } - - public AuthenticatorDescription createFromXml(XmlPullParser parser) - throws IOException, XmlPullParserException { - return AuthenticatorDescription.newKey(parser.getAttributeValue(null, "type")); - } - } -} diff --git a/core/java/android/accounts/AccountAuthenticatorResponse.java b/core/java/android/accounts/AccountAuthenticatorResponse.java index 614e139..41f26ac 100644 --- a/core/java/android/accounts/AccountAuthenticatorResponse.java +++ b/core/java/android/accounts/AccountAuthenticatorResponse.java @@ -33,7 +33,7 @@ public class AccountAuthenticatorResponse implements Parcelable { /** * @hide */ - /* package private */ AccountAuthenticatorResponse(IAccountAuthenticatorResponse response) { + public AccountAuthenticatorResponse(IAccountAuthenticatorResponse response) { mAccountAuthenticatorResponse = response; } diff --git a/core/java/android/accounts/AccountManager.java b/core/java/android/accounts/AccountManager.java index bcb35d5..6d9bb1d 100644 --- a/core/java/android/accounts/AccountManager.java +++ b/core/java/android/accounts/AccountManager.java @@ -1846,7 +1846,7 @@ public class AccountManager { * Returns an intent to an {@link Activity} that prompts the user to choose from a list of * accounts. * The caller will then typically start the activity by calling - * <code>startActivityWithResult(intent, ...);</code>. + * <code>startActivityForResult(intent, ...);</code>. * <p> * On success the activity returns a Bundle with the account name and type specified using * keys {@link #KEY_ACCOUNT_NAME} and {@link #KEY_ACCOUNT_TYPE}. diff --git a/core/java/android/accounts/AccountManagerService.java b/core/java/android/accounts/AccountManagerService.java deleted file mode 100644 index 2b1a2b2..0000000 --- a/core/java/android/accounts/AccountManagerService.java +++ /dev/null @@ -1,2548 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.accounts; - -import android.Manifest; -import android.app.ActivityManager; -import android.app.ActivityManagerNative; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.ServiceConnection; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.RegisteredServicesCache; -import android.content.pm.RegisteredServicesCacheListener; -import android.content.pm.UserInfo; -import android.database.Cursor; -import android.database.DatabaseUtils; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; -import android.os.Binder; -import android.os.Bundle; -import android.os.Environment; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.IBinder; -import android.os.Looper; -import android.os.Message; -import android.os.RemoteException; -import android.os.SystemClock; -import android.os.UserHandle; -import android.os.UserManager; -import android.text.TextUtils; -import android.util.Log; -import android.util.Pair; -import android.util.Slog; -import android.util.SparseArray; - -import com.android.internal.R; -import com.android.internal.util.IndentingPrintWriter; -import com.google.android.collect.Lists; -import com.google.android.collect.Sets; - -import java.io.File; -import java.io.FileDescriptor; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; - -/** - * A system service that provides account, password, and authtoken management for all - * accounts on the device. Some of these calls are implemented with the help of the corresponding - * {@link IAccountAuthenticator} services. This service is not accessed by users directly, - * instead one uses an instance of {@link AccountManager}, which can be accessed as follows: - * AccountManager accountManager = AccountManager.get(context); - * @hide - */ -public class AccountManagerService - extends IAccountManager.Stub - implements RegisteredServicesCacheListener<AuthenticatorDescription> { - private static final String TAG = "AccountManagerService"; - - private static final int TIMEOUT_DELAY_MS = 1000 * 60; - private static final String DATABASE_NAME = "accounts.db"; - private static final int DATABASE_VERSION = 4; - - private final Context mContext; - - private final PackageManager mPackageManager; - private UserManager mUserManager; - - private HandlerThread mMessageThread; - private final MessageHandler mMessageHandler; - - // Messages that can be sent on mHandler - private static final int MESSAGE_TIMED_OUT = 3; - - private final IAccountAuthenticatorCache mAuthenticatorCache; - - private static final String TABLE_ACCOUNTS = "accounts"; - private static final String ACCOUNTS_ID = "_id"; - private static final String ACCOUNTS_NAME = "name"; - private static final String ACCOUNTS_TYPE = "type"; - private static final String ACCOUNTS_TYPE_COUNT = "count(type)"; - private static final String ACCOUNTS_PASSWORD = "password"; - - private static final String TABLE_AUTHTOKENS = "authtokens"; - private static final String AUTHTOKENS_ID = "_id"; - private static final String AUTHTOKENS_ACCOUNTS_ID = "accounts_id"; - private static final String AUTHTOKENS_TYPE = "type"; - private static final String AUTHTOKENS_AUTHTOKEN = "authtoken"; - - private static final String TABLE_GRANTS = "grants"; - private static final String GRANTS_ACCOUNTS_ID = "accounts_id"; - private static final String GRANTS_AUTH_TOKEN_TYPE = "auth_token_type"; - private static final String GRANTS_GRANTEE_UID = "uid"; - - private static final String TABLE_EXTRAS = "extras"; - private static final String EXTRAS_ID = "_id"; - private static final String EXTRAS_ACCOUNTS_ID = "accounts_id"; - private static final String EXTRAS_KEY = "key"; - private static final String EXTRAS_VALUE = "value"; - - private static final String TABLE_META = "meta"; - private static final String META_KEY = "key"; - private static final String META_VALUE = "value"; - - private static final String[] ACCOUNT_TYPE_COUNT_PROJECTION = - new String[] { ACCOUNTS_TYPE, ACCOUNTS_TYPE_COUNT}; - private static final Intent ACCOUNTS_CHANGED_INTENT; - - private static final String COUNT_OF_MATCHING_GRANTS = "" - + "SELECT COUNT(*) FROM " + TABLE_GRANTS + ", " + TABLE_ACCOUNTS - + " WHERE " + GRANTS_ACCOUNTS_ID + "=" + ACCOUNTS_ID - + " AND " + GRANTS_GRANTEE_UID + "=?" - + " AND " + GRANTS_AUTH_TOKEN_TYPE + "=?" - + " AND " + ACCOUNTS_NAME + "=?" - + " AND " + ACCOUNTS_TYPE + "=?"; - - private static final String SELECTION_AUTHTOKENS_BY_ACCOUNT = - AUTHTOKENS_ACCOUNTS_ID + "=(select _id FROM accounts WHERE name=? AND type=?)"; - private static final String[] COLUMNS_AUTHTOKENS_TYPE_AND_AUTHTOKEN = {AUTHTOKENS_TYPE, - AUTHTOKENS_AUTHTOKEN}; - - private static final String SELECTION_USERDATA_BY_ACCOUNT = - EXTRAS_ACCOUNTS_ID + "=(select _id FROM accounts WHERE name=? AND type=?)"; - private static final String[] COLUMNS_EXTRAS_KEY_AND_VALUE = {EXTRAS_KEY, EXTRAS_VALUE}; - - private final LinkedHashMap<String, Session> mSessions = new LinkedHashMap<String, Session>(); - private final AtomicInteger mNotificationIds = new AtomicInteger(1); - - static class UserAccounts { - private final int userId; - private final DatabaseHelper openHelper; - private final HashMap<Pair<Pair<Account, String>, Integer>, Integer> - credentialsPermissionNotificationIds = - new HashMap<Pair<Pair<Account, String>, Integer>, Integer>(); - private final HashMap<Account, Integer> signinRequiredNotificationIds = - new HashMap<Account, Integer>(); - private final Object cacheLock = new Object(); - /** protected by the {@link #cacheLock} */ - private final HashMap<String, Account[]> accountCache = - new LinkedHashMap<String, Account[]>(); - /** protected by the {@link #cacheLock} */ - private HashMap<Account, HashMap<String, String>> userDataCache = - new HashMap<Account, HashMap<String, String>>(); - /** protected by the {@link #cacheLock} */ - private HashMap<Account, HashMap<String, String>> authTokenCache = - new HashMap<Account, HashMap<String, String>>(); - - UserAccounts(Context context, int userId) { - this.userId = userId; - synchronized (cacheLock) { - openHelper = new DatabaseHelper(context, userId); - } - } - } - - private final SparseArray<UserAccounts> mUsers = new SparseArray<UserAccounts>(); - - private static AtomicReference<AccountManagerService> sThis = - new AtomicReference<AccountManagerService>(); - private static final Account[] EMPTY_ACCOUNT_ARRAY = new Account[]{}; - - static { - ACCOUNTS_CHANGED_INTENT = new Intent(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION); - ACCOUNTS_CHANGED_INTENT.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); - } - - - /** - * This should only be called by system code. One should only call this after the service - * has started. - * @return a reference to the AccountManagerService instance - * @hide - */ - public static AccountManagerService getSingleton() { - return sThis.get(); - } - - public AccountManagerService(Context context) { - this(context, context.getPackageManager(), new AccountAuthenticatorCache(context)); - } - - public AccountManagerService(Context context, PackageManager packageManager, - IAccountAuthenticatorCache authenticatorCache) { - mContext = context; - mPackageManager = packageManager; - - mMessageThread = new HandlerThread("AccountManagerService"); - mMessageThread.start(); - mMessageHandler = new MessageHandler(mMessageThread.getLooper()); - - mAuthenticatorCache = authenticatorCache; - mAuthenticatorCache.setListener(this, null /* Handler */); - - sThis.set(this); - - IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); - intentFilter.addDataScheme("package"); - mContext.registerReceiver(new BroadcastReceiver() { - @Override - public void onReceive(Context context1, Intent intent) { - purgeOldGrantsAll(); - } - }, intentFilter); - - IntentFilter userFilter = new IntentFilter(); - userFilter.addAction(Intent.ACTION_USER_REMOVED); - mContext.registerReceiver(new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - onUserRemoved(intent); - } - }, userFilter); - } - - public void systemReady() { - } - - private UserManager getUserManager() { - if (mUserManager == null) { - mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); - } - return mUserManager; - } - - private UserAccounts initUser(int userId) { - synchronized (mUsers) { - UserAccounts accounts = mUsers.get(userId); - if (accounts == null) { - accounts = new UserAccounts(mContext, userId); - mUsers.append(userId, accounts); - purgeOldGrants(accounts); - validateAccountsInternal(accounts, true /* invalidateAuthenticatorCache */); - } - return accounts; - } - } - - private void purgeOldGrantsAll() { - synchronized (mUsers) { - for (int i = 0; i < mUsers.size(); i++) { - purgeOldGrants(mUsers.valueAt(i)); - } - } - } - - private void purgeOldGrants(UserAccounts accounts) { - synchronized (accounts.cacheLock) { - final SQLiteDatabase db = accounts.openHelper.getWritableDatabase(); - final Cursor cursor = db.query(TABLE_GRANTS, - new String[]{GRANTS_GRANTEE_UID}, - null, null, GRANTS_GRANTEE_UID, null, null); - try { - while (cursor.moveToNext()) { - final int uid = cursor.getInt(0); - final boolean packageExists = mPackageManager.getPackagesForUid(uid) != null; - if (packageExists) { - continue; - } - Log.d(TAG, "deleting grants for UID " + uid - + " because its package is no longer installed"); - db.delete(TABLE_GRANTS, GRANTS_GRANTEE_UID + "=?", - new String[]{Integer.toString(uid)}); - } - } finally { - cursor.close(); - } - } - } - - /** - * Validate internal set of accounts against installed authenticators for - * given user. Clears cached authenticators before validating. - */ - public void validateAccounts(int userId) { - final UserAccounts accounts = getUserAccounts(userId); - - // Invalidate user-specific cache to make sure we catch any - // removed authenticators. - validateAccountsInternal(accounts, true /* invalidateAuthenticatorCache */); - } - - /** - * Validate internal set of accounts against installed authenticators for - * given user. Clear cached authenticators before validating when requested. - */ - private void validateAccountsInternal( - UserAccounts accounts, boolean invalidateAuthenticatorCache) { - if (invalidateAuthenticatorCache) { - mAuthenticatorCache.invalidateCache(accounts.userId); - } - - final HashSet<AuthenticatorDescription> knownAuth = Sets.newHashSet(); - for (RegisteredServicesCache.ServiceInfo<AuthenticatorDescription> service : - mAuthenticatorCache.getAllServices(accounts.userId)) { - knownAuth.add(service.type); - } - - synchronized (accounts.cacheLock) { - final SQLiteDatabase db = accounts.openHelper.getWritableDatabase(); - boolean accountDeleted = false; - Cursor cursor = db.query(TABLE_ACCOUNTS, - new String[]{ACCOUNTS_ID, ACCOUNTS_TYPE, ACCOUNTS_NAME}, - null, null, null, null, null); - try { - accounts.accountCache.clear(); - final HashMap<String, ArrayList<String>> accountNamesByType = - new LinkedHashMap<String, ArrayList<String>>(); - while (cursor.moveToNext()) { - final long accountId = cursor.getLong(0); - final String accountType = cursor.getString(1); - final String accountName = cursor.getString(2); - - if (!knownAuth.contains(AuthenticatorDescription.newKey(accountType))) { - Slog.w(TAG, "deleting account " + accountName + " because type " - + accountType + " no longer has a registered authenticator"); - db.delete(TABLE_ACCOUNTS, ACCOUNTS_ID + "=" + accountId, null); - accountDeleted = true; - final Account account = new Account(accountName, accountType); - accounts.userDataCache.remove(account); - accounts.authTokenCache.remove(account); - } else { - ArrayList<String> accountNames = accountNamesByType.get(accountType); - if (accountNames == null) { - accountNames = new ArrayList<String>(); - accountNamesByType.put(accountType, accountNames); - } - accountNames.add(accountName); - } - } - for (Map.Entry<String, ArrayList<String>> cur - : accountNamesByType.entrySet()) { - final String accountType = cur.getKey(); - final ArrayList<String> accountNames = cur.getValue(); - final Account[] accountsForType = new Account[accountNames.size()]; - int i = 0; - for (String accountName : accountNames) { - accountsForType[i] = new Account(accountName, accountType); - ++i; - } - accounts.accountCache.put(accountType, accountsForType); - } - } finally { - cursor.close(); - if (accountDeleted) { - sendAccountsChangedBroadcast(accounts.userId); - } - } - } - } - - private UserAccounts getUserAccountsForCaller() { - return getUserAccounts(UserHandle.getCallingUserId()); - } - - protected UserAccounts getUserAccounts(int userId) { - synchronized (mUsers) { - UserAccounts accounts = mUsers.get(userId); - if (accounts == null) { - accounts = initUser(userId); - mUsers.append(userId, accounts); - } - return accounts; - } - } - - private void onUserRemoved(Intent intent) { - int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); - if (userId < 1) return; - - UserAccounts accounts; - synchronized (mUsers) { - accounts = mUsers.get(userId); - mUsers.remove(userId); - } - if (accounts == null) { - File dbFile = new File(getDatabaseName(userId)); - dbFile.delete(); - return; - } - - synchronized (accounts.cacheLock) { - accounts.openHelper.close(); - File dbFile = new File(getDatabaseName(userId)); - dbFile.delete(); - } - } - - @Override - public void onServiceChanged(AuthenticatorDescription desc, int userId, boolean removed) { - Slog.d(TAG, "onServiceChanged() for userId " + userId); - validateAccountsInternal(getUserAccounts(userId), false /* invalidateAuthenticatorCache */); - } - - public String getPassword(Account account) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "getPassword: " + account - + ", caller's uid " + Binder.getCallingUid() - + ", pid " + Binder.getCallingPid()); - } - if (account == null) throw new IllegalArgumentException("account is null"); - checkAuthenticateAccountsPermission(account); - - UserAccounts accounts = getUserAccountsForCaller(); - long identityToken = clearCallingIdentity(); - try { - return readPasswordInternal(accounts, account); - } finally { - restoreCallingIdentity(identityToken); - } - } - - private String readPasswordInternal(UserAccounts accounts, Account account) { - if (account == null) { - return null; - } - - synchronized (accounts.cacheLock) { - final SQLiteDatabase db = accounts.openHelper.getReadableDatabase(); - Cursor cursor = db.query(TABLE_ACCOUNTS, new String[]{ACCOUNTS_PASSWORD}, - ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE+ "=?", - new String[]{account.name, account.type}, null, null, null); - try { - if (cursor.moveToNext()) { - return cursor.getString(0); - } - return null; - } finally { - cursor.close(); - } - } - } - - public String getUserData(Account account, String key) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "getUserData: " + account - + ", key " + key - + ", caller's uid " + Binder.getCallingUid() - + ", pid " + Binder.getCallingPid()); - } - if (account == null) throw new IllegalArgumentException("account is null"); - if (key == null) throw new IllegalArgumentException("key is null"); - checkAuthenticateAccountsPermission(account); - UserAccounts accounts = getUserAccountsForCaller(); - long identityToken = clearCallingIdentity(); - try { - return readUserDataInternal(accounts, account, key); - } finally { - restoreCallingIdentity(identityToken); - } - } - - public AuthenticatorDescription[] getAuthenticatorTypes() { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "getAuthenticatorTypes: " - + "caller's uid " + Binder.getCallingUid() - + ", pid " + Binder.getCallingPid()); - } - final int userId = UserHandle.getCallingUserId(); - final long identityToken = clearCallingIdentity(); - try { - Collection<AccountAuthenticatorCache.ServiceInfo<AuthenticatorDescription>> - authenticatorCollection = mAuthenticatorCache.getAllServices(userId); - AuthenticatorDescription[] types = - new AuthenticatorDescription[authenticatorCollection.size()]; - int i = 0; - for (AccountAuthenticatorCache.ServiceInfo<AuthenticatorDescription> authenticator - : authenticatorCollection) { - types[i] = authenticator.type; - i++; - } - return types; - } finally { - restoreCallingIdentity(identityToken); - } - } - - public boolean addAccount(Account account, String password, Bundle extras) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "addAccount: " + account - + ", caller's uid " + Binder.getCallingUid() - + ", pid " + Binder.getCallingPid()); - } - if (account == null) throw new IllegalArgumentException("account is null"); - checkAuthenticateAccountsPermission(account); - - UserAccounts accounts = getUserAccountsForCaller(); - // fails if the account already exists - long identityToken = clearCallingIdentity(); - try { - return addAccountInternal(accounts, account, password, extras); - } finally { - restoreCallingIdentity(identityToken); - } - } - - private boolean addAccountInternal(UserAccounts accounts, Account account, String password, - Bundle extras) { - if (account == null) { - return false; - } - synchronized (accounts.cacheLock) { - final SQLiteDatabase db = accounts.openHelper.getWritableDatabase(); - db.beginTransaction(); - try { - long numMatches = DatabaseUtils.longForQuery(db, - "select count(*) from " + TABLE_ACCOUNTS - + " WHERE " + ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE+ "=?", - new String[]{account.name, account.type}); - if (numMatches > 0) { - Log.w(TAG, "insertAccountIntoDatabase: " + account - + ", skipping since the account already exists"); - return false; - } - ContentValues values = new ContentValues(); - values.put(ACCOUNTS_NAME, account.name); - values.put(ACCOUNTS_TYPE, account.type); - values.put(ACCOUNTS_PASSWORD, password); - long accountId = db.insert(TABLE_ACCOUNTS, ACCOUNTS_NAME, values); - if (accountId < 0) { - Log.w(TAG, "insertAccountIntoDatabase: " + account - + ", skipping the DB insert failed"); - return false; - } - if (extras != null) { - for (String key : extras.keySet()) { - final String value = extras.getString(key); - if (insertExtraLocked(db, accountId, key, value) < 0) { - Log.w(TAG, "insertAccountIntoDatabase: " + account - + ", skipping since insertExtra failed for key " + key); - return false; - } - } - } - db.setTransactionSuccessful(); - insertAccountIntoCacheLocked(accounts, account); - } finally { - db.endTransaction(); - } - sendAccountsChangedBroadcast(accounts.userId); - return true; - } - } - - private long insertExtraLocked(SQLiteDatabase db, long accountId, String key, String value) { - ContentValues values = new ContentValues(); - values.put(EXTRAS_KEY, key); - values.put(EXTRAS_ACCOUNTS_ID, accountId); - values.put(EXTRAS_VALUE, value); - return db.insert(TABLE_EXTRAS, EXTRAS_KEY, values); - } - - public void hasFeatures(IAccountManagerResponse response, - Account account, String[] features) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "hasFeatures: " + account - + ", response " + response - + ", features " + stringArrayToString(features) - + ", caller's uid " + Binder.getCallingUid() - + ", pid " + Binder.getCallingPid()); - } - if (response == null) throw new IllegalArgumentException("response is null"); - if (account == null) throw new IllegalArgumentException("account is null"); - if (features == null) throw new IllegalArgumentException("features is null"); - checkReadAccountsPermission(); - UserAccounts accounts = getUserAccountsForCaller(); - long identityToken = clearCallingIdentity(); - try { - new TestFeaturesSession(accounts, response, account, features).bind(); - } finally { - restoreCallingIdentity(identityToken); - } - } - - private class TestFeaturesSession extends Session { - private final String[] mFeatures; - private final Account mAccount; - - public TestFeaturesSession(UserAccounts accounts, IAccountManagerResponse response, - Account account, String[] features) { - super(accounts, response, account.type, false /* expectActivityLaunch */, - true /* stripAuthTokenFromResult */); - mFeatures = features; - mAccount = account; - } - - public void run() throws RemoteException { - try { - mAuthenticator.hasFeatures(this, mAccount, mFeatures); - } catch (RemoteException e) { - onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, "remote exception"); - } - } - - public void onResult(Bundle result) { - IAccountManagerResponse response = getResponseAndClose(); - if (response != null) { - try { - if (result == null) { - response.onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, "null bundle"); - return; - } - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, getClass().getSimpleName() + " calling onResult() on response " - + response); - } - final Bundle newResult = new Bundle(); - newResult.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, - result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT, false)); - response.onResult(newResult); - } catch (RemoteException e) { - // if the caller is dead then there is no one to care about remote exceptions - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "failure while notifying response", e); - } - } - } - } - - protected String toDebugString(long now) { - return super.toDebugString(now) + ", hasFeatures" - + ", " + mAccount - + ", " + (mFeatures != null ? TextUtils.join(",", mFeatures) : null); - } - } - - public void removeAccount(IAccountManagerResponse response, Account account) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "removeAccount: " + account - + ", response " + response - + ", caller's uid " + Binder.getCallingUid() - + ", pid " + Binder.getCallingPid()); - } - if (response == null) throw new IllegalArgumentException("response is null"); - if (account == null) throw new IllegalArgumentException("account is null"); - checkManageAccountsPermission(); - UserHandle user = Binder.getCallingUserHandle(); - UserAccounts accounts = getUserAccountsForCaller(); - long identityToken = clearCallingIdentity(); - - cancelNotification(getSigninRequiredNotificationId(accounts, account), user); - synchronized(accounts.credentialsPermissionNotificationIds) { - for (Pair<Pair<Account, String>, Integer> pair: - accounts.credentialsPermissionNotificationIds.keySet()) { - if (account.equals(pair.first.first)) { - int id = accounts.credentialsPermissionNotificationIds.get(pair); - cancelNotification(id, user); - } - } - } - - try { - new RemoveAccountSession(accounts, response, account).bind(); - } finally { - restoreCallingIdentity(identityToken); - } - } - - private class RemoveAccountSession extends Session { - final Account mAccount; - public RemoveAccountSession(UserAccounts accounts, IAccountManagerResponse response, - Account account) { - super(accounts, response, account.type, false /* expectActivityLaunch */, - true /* stripAuthTokenFromResult */); - mAccount = account; - } - - protected String toDebugString(long now) { - return super.toDebugString(now) + ", removeAccount" - + ", account " + mAccount; - } - - public void run() throws RemoteException { - mAuthenticator.getAccountRemovalAllowed(this, mAccount); - } - - public void onResult(Bundle result) { - if (result != null && result.containsKey(AccountManager.KEY_BOOLEAN_RESULT) - && !result.containsKey(AccountManager.KEY_INTENT)) { - final boolean removalAllowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT); - if (removalAllowed) { - removeAccountInternal(mAccounts, mAccount); - } - IAccountManagerResponse response = getResponseAndClose(); - if (response != null) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, getClass().getSimpleName() + " calling onResult() on response " - + response); - } - Bundle result2 = new Bundle(); - result2.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, removalAllowed); - try { - response.onResult(result2); - } catch (RemoteException e) { - // ignore - } - } - } - super.onResult(result); - } - } - - /* For testing */ - protected void removeAccountInternal(Account account) { - removeAccountInternal(getUserAccountsForCaller(), account); - } - - private void removeAccountInternal(UserAccounts accounts, Account account) { - synchronized (accounts.cacheLock) { - final SQLiteDatabase db = accounts.openHelper.getWritableDatabase(); - db.delete(TABLE_ACCOUNTS, ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE+ "=?", - new String[]{account.name, account.type}); - removeAccountFromCacheLocked(accounts, account); - sendAccountsChangedBroadcast(accounts.userId); - } - } - - public void invalidateAuthToken(String accountType, String authToken) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "invalidateAuthToken: accountType " + accountType - + ", caller's uid " + Binder.getCallingUid() - + ", pid " + Binder.getCallingPid()); - } - if (accountType == null) throw new IllegalArgumentException("accountType is null"); - if (authToken == null) throw new IllegalArgumentException("authToken is null"); - checkManageAccountsOrUseCredentialsPermissions(); - UserAccounts accounts = getUserAccountsForCaller(); - long identityToken = clearCallingIdentity(); - try { - synchronized (accounts.cacheLock) { - final SQLiteDatabase db = accounts.openHelper.getWritableDatabase(); - db.beginTransaction(); - try { - invalidateAuthTokenLocked(accounts, db, accountType, authToken); - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } - } finally { - restoreCallingIdentity(identityToken); - } - } - - private void invalidateAuthTokenLocked(UserAccounts accounts, SQLiteDatabase db, - String accountType, String authToken) { - if (authToken == null || accountType == null) { - return; - } - Cursor cursor = db.rawQuery( - "SELECT " + TABLE_AUTHTOKENS + "." + AUTHTOKENS_ID - + ", " + TABLE_ACCOUNTS + "." + ACCOUNTS_NAME - + ", " + TABLE_AUTHTOKENS + "." + AUTHTOKENS_TYPE - + " FROM " + TABLE_ACCOUNTS - + " JOIN " + TABLE_AUTHTOKENS - + " ON " + TABLE_ACCOUNTS + "." + ACCOUNTS_ID - + " = " + AUTHTOKENS_ACCOUNTS_ID - + " WHERE " + AUTHTOKENS_AUTHTOKEN + " = ? AND " - + TABLE_ACCOUNTS + "." + ACCOUNTS_TYPE + " = ?", - new String[]{authToken, accountType}); - try { - while (cursor.moveToNext()) { - long authTokenId = cursor.getLong(0); - String accountName = cursor.getString(1); - String authTokenType = cursor.getString(2); - db.delete(TABLE_AUTHTOKENS, AUTHTOKENS_ID + "=" + authTokenId, null); - writeAuthTokenIntoCacheLocked(accounts, db, new Account(accountName, accountType), - authTokenType, null); - } - } finally { - cursor.close(); - } - } - - private boolean saveAuthTokenToDatabase(UserAccounts accounts, Account account, String type, - String authToken) { - if (account == null || type == null) { - return false; - } - cancelNotification(getSigninRequiredNotificationId(accounts, account), - new UserHandle(accounts.userId)); - synchronized (accounts.cacheLock) { - final SQLiteDatabase db = accounts.openHelper.getWritableDatabase(); - db.beginTransaction(); - try { - long accountId = getAccountIdLocked(db, account); - if (accountId < 0) { - return false; - } - db.delete(TABLE_AUTHTOKENS, - AUTHTOKENS_ACCOUNTS_ID + "=" + accountId + " AND " + AUTHTOKENS_TYPE + "=?", - new String[]{type}); - ContentValues values = new ContentValues(); - values.put(AUTHTOKENS_ACCOUNTS_ID, accountId); - values.put(AUTHTOKENS_TYPE, type); - values.put(AUTHTOKENS_AUTHTOKEN, authToken); - if (db.insert(TABLE_AUTHTOKENS, AUTHTOKENS_AUTHTOKEN, values) >= 0) { - db.setTransactionSuccessful(); - writeAuthTokenIntoCacheLocked(accounts, db, account, type, authToken); - return true; - } - return false; - } finally { - db.endTransaction(); - } - } - } - - public String peekAuthToken(Account account, String authTokenType) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "peekAuthToken: " + account - + ", authTokenType " + authTokenType - + ", caller's uid " + Binder.getCallingUid() - + ", pid " + Binder.getCallingPid()); - } - if (account == null) throw new IllegalArgumentException("account is null"); - if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null"); - checkAuthenticateAccountsPermission(account); - UserAccounts accounts = getUserAccountsForCaller(); - long identityToken = clearCallingIdentity(); - try { - return readAuthTokenInternal(accounts, account, authTokenType); - } finally { - restoreCallingIdentity(identityToken); - } - } - - public void setAuthToken(Account account, String authTokenType, String authToken) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "setAuthToken: " + account - + ", authTokenType " + authTokenType - + ", caller's uid " + Binder.getCallingUid() - + ", pid " + Binder.getCallingPid()); - } - if (account == null) throw new IllegalArgumentException("account is null"); - if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null"); - checkAuthenticateAccountsPermission(account); - UserAccounts accounts = getUserAccountsForCaller(); - long identityToken = clearCallingIdentity(); - try { - saveAuthTokenToDatabase(accounts, account, authTokenType, authToken); - } finally { - restoreCallingIdentity(identityToken); - } - } - - public void setPassword(Account account, String password) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "setAuthToken: " + account - + ", caller's uid " + Binder.getCallingUid() - + ", pid " + Binder.getCallingPid()); - } - if (account == null) throw new IllegalArgumentException("account is null"); - checkAuthenticateAccountsPermission(account); - UserAccounts accounts = getUserAccountsForCaller(); - long identityToken = clearCallingIdentity(); - try { - setPasswordInternal(accounts, account, password); - } finally { - restoreCallingIdentity(identityToken); - } - } - - private void setPasswordInternal(UserAccounts accounts, Account account, String password) { - if (account == null) { - return; - } - synchronized (accounts.cacheLock) { - final SQLiteDatabase db = accounts.openHelper.getWritableDatabase(); - db.beginTransaction(); - try { - final ContentValues values = new ContentValues(); - values.put(ACCOUNTS_PASSWORD, password); - final long accountId = getAccountIdLocked(db, account); - if (accountId >= 0) { - final String[] argsAccountId = {String.valueOf(accountId)}; - db.update(TABLE_ACCOUNTS, values, ACCOUNTS_ID + "=?", argsAccountId); - db.delete(TABLE_AUTHTOKENS, AUTHTOKENS_ACCOUNTS_ID + "=?", argsAccountId); - accounts.authTokenCache.remove(account); - db.setTransactionSuccessful(); - } - } finally { - db.endTransaction(); - } - sendAccountsChangedBroadcast(accounts.userId); - } - } - - private void sendAccountsChangedBroadcast(int userId) { - Log.i(TAG, "the accounts changed, sending broadcast of " - + ACCOUNTS_CHANGED_INTENT.getAction()); - mContext.sendBroadcastAsUser(ACCOUNTS_CHANGED_INTENT, new UserHandle(userId)); - } - - public void clearPassword(Account account) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "clearPassword: " + account - + ", caller's uid " + Binder.getCallingUid() - + ", pid " + Binder.getCallingPid()); - } - if (account == null) throw new IllegalArgumentException("account is null"); - checkManageAccountsPermission(); - UserAccounts accounts = getUserAccountsForCaller(); - long identityToken = clearCallingIdentity(); - try { - setPasswordInternal(accounts, account, null); - } finally { - restoreCallingIdentity(identityToken); - } - } - - public void setUserData(Account account, String key, String value) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "setUserData: " + account - + ", key " + key - + ", caller's uid " + Binder.getCallingUid() - + ", pid " + Binder.getCallingPid()); - } - if (key == null) throw new IllegalArgumentException("key is null"); - if (account == null) throw new IllegalArgumentException("account is null"); - checkAuthenticateAccountsPermission(account); - UserAccounts accounts = getUserAccountsForCaller(); - long identityToken = clearCallingIdentity(); - try { - setUserdataInternal(accounts, account, key, value); - } finally { - restoreCallingIdentity(identityToken); - } - } - - private void setUserdataInternal(UserAccounts accounts, Account account, String key, - String value) { - if (account == null || key == null) { - return; - } - synchronized (accounts.cacheLock) { - final SQLiteDatabase db = accounts.openHelper.getWritableDatabase(); - db.beginTransaction(); - try { - long accountId = getAccountIdLocked(db, account); - if (accountId < 0) { - return; - } - long extrasId = getExtrasIdLocked(db, accountId, key); - if (extrasId < 0 ) { - extrasId = insertExtraLocked(db, accountId, key, value); - if (extrasId < 0) { - return; - } - } else { - ContentValues values = new ContentValues(); - values.put(EXTRAS_VALUE, value); - if (1 != db.update(TABLE_EXTRAS, values, EXTRAS_ID + "=" + extrasId, null)) { - return; - } - - } - writeUserDataIntoCacheLocked(accounts, db, account, key, value); - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } - } - - private void onResult(IAccountManagerResponse response, Bundle result) { - if (result == null) { - Log.e(TAG, "the result is unexpectedly null", new Exception()); - } - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, getClass().getSimpleName() + " calling onResult() on response " - + response); - } - try { - response.onResult(result); - } catch (RemoteException e) { - // if the caller is dead then there is no one to care about remote - // exceptions - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "failure while notifying response", e); - } - } - } - - public void getAuthTokenLabel(IAccountManagerResponse response, final String accountType, - final String authTokenType) - throws RemoteException { - if (accountType == null) throw new IllegalArgumentException("accountType is null"); - if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null"); - - final int callingUid = getCallingUid(); - clearCallingIdentity(); - if (callingUid != android.os.Process.SYSTEM_UID) { - throw new SecurityException("can only call from system"); - } - UserAccounts accounts = getUserAccounts(UserHandle.getUserId(callingUid)); - long identityToken = clearCallingIdentity(); - try { - new Session(accounts, response, accountType, false, - false /* stripAuthTokenFromResult */) { - protected String toDebugString(long now) { - return super.toDebugString(now) + ", getAuthTokenLabel" - + ", " + accountType - + ", authTokenType " + authTokenType; - } - - public void run() throws RemoteException { - mAuthenticator.getAuthTokenLabel(this, authTokenType); - } - - public void onResult(Bundle result) { - if (result != null) { - String label = result.getString(AccountManager.KEY_AUTH_TOKEN_LABEL); - Bundle bundle = new Bundle(); - bundle.putString(AccountManager.KEY_AUTH_TOKEN_LABEL, label); - super.onResult(bundle); - return; - } else { - super.onResult(result); - } - } - }.bind(); - } finally { - restoreCallingIdentity(identityToken); - } - } - - public void getAuthToken(IAccountManagerResponse response, final Account account, - final String authTokenType, final boolean notifyOnAuthFailure, - final boolean expectActivityLaunch, Bundle loginOptionsIn) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "getAuthToken: " + account - + ", response " + response - + ", authTokenType " + authTokenType - + ", notifyOnAuthFailure " + notifyOnAuthFailure - + ", expectActivityLaunch " + expectActivityLaunch - + ", caller's uid " + Binder.getCallingUid() - + ", pid " + Binder.getCallingPid()); - } - if (response == null) throw new IllegalArgumentException("response is null"); - if (account == null) throw new IllegalArgumentException("account is null"); - if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null"); - checkBinderPermission(Manifest.permission.USE_CREDENTIALS); - final UserAccounts accounts = getUserAccountsForCaller(); - final RegisteredServicesCache.ServiceInfo<AuthenticatorDescription> authenticatorInfo; - authenticatorInfo = mAuthenticatorCache.getServiceInfo( - AuthenticatorDescription.newKey(account.type), accounts.userId); - final boolean customTokens = - authenticatorInfo != null && authenticatorInfo.type.customTokens; - - // skip the check if customTokens - final int callerUid = Binder.getCallingUid(); - final boolean permissionGranted = customTokens || - permissionIsGranted(account, authTokenType, callerUid); - - final Bundle loginOptions = (loginOptionsIn == null) ? new Bundle() : - loginOptionsIn; - // let authenticator know the identity of the caller - loginOptions.putInt(AccountManager.KEY_CALLER_UID, callerUid); - loginOptions.putInt(AccountManager.KEY_CALLER_PID, Binder.getCallingPid()); - if (notifyOnAuthFailure) { - loginOptions.putBoolean(AccountManager.KEY_NOTIFY_ON_FAILURE, true); - } - - long identityToken = clearCallingIdentity(); - try { - // if the caller has permission, do the peek. otherwise go the more expensive - // route of starting a Session - if (!customTokens && permissionGranted) { - String authToken = readAuthTokenInternal(accounts, account, authTokenType); - if (authToken != null) { - Bundle result = new Bundle(); - result.putString(AccountManager.KEY_AUTHTOKEN, authToken); - result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); - result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type); - onResult(response, result); - return; - } - } - - new Session(accounts, response, account.type, expectActivityLaunch, - false /* stripAuthTokenFromResult */) { - protected String toDebugString(long now) { - if (loginOptions != null) loginOptions.keySet(); - return super.toDebugString(now) + ", getAuthToken" - + ", " + account - + ", authTokenType " + authTokenType - + ", loginOptions " + loginOptions - + ", notifyOnAuthFailure " + notifyOnAuthFailure; - } - - public void run() throws RemoteException { - // If the caller doesn't have permission then create and return the - // "grant permission" intent instead of the "getAuthToken" intent. - if (!permissionGranted) { - mAuthenticator.getAuthTokenLabel(this, authTokenType); - } else { - mAuthenticator.getAuthToken(this, account, authTokenType, loginOptions); - } - } - - public void onResult(Bundle result) { - if (result != null) { - if (result.containsKey(AccountManager.KEY_AUTH_TOKEN_LABEL)) { - Intent intent = newGrantCredentialsPermissionIntent(account, callerUid, - new AccountAuthenticatorResponse(this), - authTokenType, - result.getString(AccountManager.KEY_AUTH_TOKEN_LABEL)); - Bundle bundle = new Bundle(); - bundle.putParcelable(AccountManager.KEY_INTENT, intent); - onResult(bundle); - return; - } - String authToken = result.getString(AccountManager.KEY_AUTHTOKEN); - if (authToken != null) { - String name = result.getString(AccountManager.KEY_ACCOUNT_NAME); - String type = result.getString(AccountManager.KEY_ACCOUNT_TYPE); - if (TextUtils.isEmpty(type) || TextUtils.isEmpty(name)) { - onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, - "the type and name should not be empty"); - return; - } - if (!customTokens) { - saveAuthTokenToDatabase(mAccounts, new Account(name, type), - authTokenType, authToken); - } - } - - Intent intent = result.getParcelable(AccountManager.KEY_INTENT); - if (intent != null && notifyOnAuthFailure && !customTokens) { - doNotification(mAccounts, - account, result.getString(AccountManager.KEY_AUTH_FAILED_MESSAGE), - intent, accounts.userId); - } - } - super.onResult(result); - } - }.bind(); - } finally { - restoreCallingIdentity(identityToken); - } - } - - private void createNoCredentialsPermissionNotification(Account account, Intent intent, - int userId) { - int uid = intent.getIntExtra( - GrantCredentialsPermissionActivity.EXTRAS_REQUESTING_UID, -1); - String authTokenType = intent.getStringExtra( - GrantCredentialsPermissionActivity.EXTRAS_AUTH_TOKEN_TYPE); - String authTokenLabel = intent.getStringExtra( - GrantCredentialsPermissionActivity.EXTRAS_AUTH_TOKEN_LABEL); - - Notification n = new Notification(android.R.drawable.stat_sys_warning, null, - 0 /* when */); - final String titleAndSubtitle = - mContext.getString(R.string.permission_request_notification_with_subtitle, - account.name); - final int index = titleAndSubtitle.indexOf('\n'); - String title = titleAndSubtitle; - String subtitle = ""; - if (index > 0) { - title = titleAndSubtitle.substring(0, index); - subtitle = titleAndSubtitle.substring(index + 1); - } - UserHandle user = new UserHandle(userId); - n.setLatestEventInfo(mContext, title, subtitle, - PendingIntent.getActivityAsUser(mContext, 0, intent, - PendingIntent.FLAG_CANCEL_CURRENT, null, user)); - installNotification(getCredentialPermissionNotificationId( - account, authTokenType, uid), n, user); - } - - private Intent newGrantCredentialsPermissionIntent(Account account, int uid, - AccountAuthenticatorResponse response, String authTokenType, String authTokenLabel) { - - Intent intent = new Intent(mContext, GrantCredentialsPermissionActivity.class); - // See FLAG_ACTIVITY_NEW_TASK docs for limitations and benefits of the flag. - // Since it was set in Eclair+ we can't change it without breaking apps using - // the intent from a non-Activity context. - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.addCategory( - String.valueOf(getCredentialPermissionNotificationId(account, authTokenType, uid))); - - intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_ACCOUNT, account); - intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_AUTH_TOKEN_TYPE, authTokenType); - intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_RESPONSE, response); - intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_REQUESTING_UID, uid); - - return intent; - } - - private Integer getCredentialPermissionNotificationId(Account account, String authTokenType, - int uid) { - Integer id; - UserAccounts accounts = getUserAccounts(UserHandle.getUserId(uid)); - synchronized (accounts.credentialsPermissionNotificationIds) { - final Pair<Pair<Account, String>, Integer> key = - new Pair<Pair<Account, String>, Integer>( - new Pair<Account, String>(account, authTokenType), uid); - id = accounts.credentialsPermissionNotificationIds.get(key); - if (id == null) { - id = mNotificationIds.incrementAndGet(); - accounts.credentialsPermissionNotificationIds.put(key, id); - } - } - return id; - } - - private Integer getSigninRequiredNotificationId(UserAccounts accounts, Account account) { - Integer id; - synchronized (accounts.signinRequiredNotificationIds) { - id = accounts.signinRequiredNotificationIds.get(account); - if (id == null) { - id = mNotificationIds.incrementAndGet(); - accounts.signinRequiredNotificationIds.put(account, id); - } - } - return id; - } - - public void addAcount(final IAccountManagerResponse response, final String accountType, - final String authTokenType, final String[] requiredFeatures, - final boolean expectActivityLaunch, final Bundle optionsIn) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "addAccount: accountType " + accountType - + ", response " + response - + ", authTokenType " + authTokenType - + ", requiredFeatures " + stringArrayToString(requiredFeatures) - + ", expectActivityLaunch " + expectActivityLaunch - + ", caller's uid " + Binder.getCallingUid() - + ", pid " + Binder.getCallingPid()); - } - if (response == null) throw new IllegalArgumentException("response is null"); - if (accountType == null) throw new IllegalArgumentException("accountType is null"); - checkManageAccountsPermission(); - - UserAccounts accounts = getUserAccountsForCaller(); - final int pid = Binder.getCallingPid(); - final int uid = Binder.getCallingUid(); - final Bundle options = (optionsIn == null) ? new Bundle() : optionsIn; - options.putInt(AccountManager.KEY_CALLER_UID, uid); - options.putInt(AccountManager.KEY_CALLER_PID, pid); - - long identityToken = clearCallingIdentity(); - try { - new Session(accounts, response, accountType, expectActivityLaunch, - true /* stripAuthTokenFromResult */) { - public void run() throws RemoteException { - mAuthenticator.addAccount(this, mAccountType, authTokenType, requiredFeatures, - options); - } - - protected String toDebugString(long now) { - return super.toDebugString(now) + ", addAccount" - + ", accountType " + accountType - + ", requiredFeatures " - + (requiredFeatures != null - ? TextUtils.join(",", requiredFeatures) - : null); - } - }.bind(); - } finally { - restoreCallingIdentity(identityToken); - } - } - - @Override - public void confirmCredentialsAsUser(IAccountManagerResponse response, - final Account account, final Bundle options, final boolean expectActivityLaunch, - int userId) { - // Only allow the system process to read accounts of other users - if (userId != UserHandle.getCallingUserId() - && Binder.getCallingUid() != android.os.Process.myUid()) { - throw new SecurityException("User " + UserHandle.getCallingUserId() - + " trying to confirm account credentials for " + userId); - } - - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "confirmCredentials: " + account - + ", response " + response - + ", expectActivityLaunch " + expectActivityLaunch - + ", caller's uid " + Binder.getCallingUid() - + ", pid " + Binder.getCallingPid()); - } - if (response == null) throw new IllegalArgumentException("response is null"); - if (account == null) throw new IllegalArgumentException("account is null"); - checkManageAccountsPermission(); - UserAccounts accounts = getUserAccounts(userId); - long identityToken = clearCallingIdentity(); - try { - new Session(accounts, response, account.type, expectActivityLaunch, - true /* stripAuthTokenFromResult */) { - public void run() throws RemoteException { - mAuthenticator.confirmCredentials(this, account, options); - } - protected String toDebugString(long now) { - return super.toDebugString(now) + ", confirmCredentials" - + ", " + account; - } - }.bind(); - } finally { - restoreCallingIdentity(identityToken); - } - } - - public void updateCredentials(IAccountManagerResponse response, final Account account, - final String authTokenType, final boolean expectActivityLaunch, - final Bundle loginOptions) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "updateCredentials: " + account - + ", response " + response - + ", authTokenType " + authTokenType - + ", expectActivityLaunch " + expectActivityLaunch - + ", caller's uid " + Binder.getCallingUid() - + ", pid " + Binder.getCallingPid()); - } - if (response == null) throw new IllegalArgumentException("response is null"); - if (account == null) throw new IllegalArgumentException("account is null"); - if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null"); - checkManageAccountsPermission(); - UserAccounts accounts = getUserAccountsForCaller(); - long identityToken = clearCallingIdentity(); - try { - new Session(accounts, response, account.type, expectActivityLaunch, - true /* stripAuthTokenFromResult */) { - public void run() throws RemoteException { - mAuthenticator.updateCredentials(this, account, authTokenType, loginOptions); - } - protected String toDebugString(long now) { - if (loginOptions != null) loginOptions.keySet(); - return super.toDebugString(now) + ", updateCredentials" - + ", " + account - + ", authTokenType " + authTokenType - + ", loginOptions " + loginOptions; - } - }.bind(); - } finally { - restoreCallingIdentity(identityToken); - } - } - - public void editProperties(IAccountManagerResponse response, final String accountType, - final boolean expectActivityLaunch) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "editProperties: accountType " + accountType - + ", response " + response - + ", expectActivityLaunch " + expectActivityLaunch - + ", caller's uid " + Binder.getCallingUid() - + ", pid " + Binder.getCallingPid()); - } - if (response == null) throw new IllegalArgumentException("response is null"); - if (accountType == null) throw new IllegalArgumentException("accountType is null"); - checkManageAccountsPermission(); - UserAccounts accounts = getUserAccountsForCaller(); - long identityToken = clearCallingIdentity(); - try { - new Session(accounts, response, accountType, expectActivityLaunch, - true /* stripAuthTokenFromResult */) { - public void run() throws RemoteException { - mAuthenticator.editProperties(this, mAccountType); - } - protected String toDebugString(long now) { - return super.toDebugString(now) + ", editProperties" - + ", accountType " + accountType; - } - }.bind(); - } finally { - restoreCallingIdentity(identityToken); - } - } - - private class GetAccountsByTypeAndFeatureSession extends Session { - private final String[] mFeatures; - private volatile Account[] mAccountsOfType = null; - private volatile ArrayList<Account> mAccountsWithFeatures = null; - private volatile int mCurrentAccount = 0; - - public GetAccountsByTypeAndFeatureSession(UserAccounts accounts, - IAccountManagerResponse response, String type, String[] features) { - super(accounts, response, type, false /* expectActivityLaunch */, - true /* stripAuthTokenFromResult */); - mFeatures = features; - } - - public void run() throws RemoteException { - synchronized (mAccounts.cacheLock) { - mAccountsOfType = getAccountsFromCacheLocked(mAccounts, mAccountType); - } - // check whether each account matches the requested features - mAccountsWithFeatures = new ArrayList<Account>(mAccountsOfType.length); - mCurrentAccount = 0; - - checkAccount(); - } - - public void checkAccount() { - if (mCurrentAccount >= mAccountsOfType.length) { - sendResult(); - return; - } - - final IAccountAuthenticator accountAuthenticator = mAuthenticator; - if (accountAuthenticator == null) { - // It is possible that the authenticator has died, which is indicated by - // mAuthenticator being set to null. If this happens then just abort. - // There is no need to send back a result or error in this case since - // that already happened when mAuthenticator was cleared. - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "checkAccount: aborting session since we are no longer" - + " connected to the authenticator, " + toDebugString()); - } - return; - } - try { - accountAuthenticator.hasFeatures(this, mAccountsOfType[mCurrentAccount], mFeatures); - } catch (RemoteException e) { - onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, "remote exception"); - } - } - - public void onResult(Bundle result) { - mNumResults++; - if (result == null) { - onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, "null bundle"); - return; - } - if (result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT, false)) { - mAccountsWithFeatures.add(mAccountsOfType[mCurrentAccount]); - } - mCurrentAccount++; - checkAccount(); - } - - public void sendResult() { - IAccountManagerResponse response = getResponseAndClose(); - if (response != null) { - try { - Account[] accounts = new Account[mAccountsWithFeatures.size()]; - for (int i = 0; i < accounts.length; i++) { - accounts[i] = mAccountsWithFeatures.get(i); - } - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, getClass().getSimpleName() + " calling onResult() on response " - + response); - } - Bundle result = new Bundle(); - result.putParcelableArray(AccountManager.KEY_ACCOUNTS, accounts); - response.onResult(result); - } catch (RemoteException e) { - // if the caller is dead then there is no one to care about remote exceptions - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "failure while notifying response", e); - } - } - } - } - - - protected String toDebugString(long now) { - return super.toDebugString(now) + ", getAccountsByTypeAndFeatures" - + ", " + (mFeatures != null ? TextUtils.join(",", mFeatures) : null); - } - } - - /** - * Returns the accounts for a specific user - * @hide - */ - public Account[] getAccounts(int userId) { - checkReadAccountsPermission(); - UserAccounts accounts = getUserAccounts(userId); - long identityToken = clearCallingIdentity(); - try { - synchronized (accounts.cacheLock) { - return getAccountsFromCacheLocked(accounts, null); - } - } finally { - restoreCallingIdentity(identityToken); - } - } - - /** - * Returns accounts for all running users. - * - * @hide - */ - public AccountAndUser[] getRunningAccounts() { - final int[] runningUserIds; - try { - runningUserIds = ActivityManagerNative.getDefault().getRunningUserIds(); - } catch (RemoteException e) { - // Running in system_server; should never happen - throw new RuntimeException(e); - } - return getAccounts(runningUserIds); - } - - /** {@hide} */ - public AccountAndUser[] getAllAccounts() { - final List<UserInfo> users = getUserManager().getUsers(); - final int[] userIds = new int[users.size()]; - for (int i = 0; i < userIds.length; i++) { - userIds[i] = users.get(i).id; - } - return getAccounts(userIds); - } - - private AccountAndUser[] getAccounts(int[] userIds) { - final ArrayList<AccountAndUser> runningAccounts = Lists.newArrayList(); - synchronized (mUsers) { - for (int userId : userIds) { - UserAccounts userAccounts = getUserAccounts(userId); - if (userAccounts == null) continue; - synchronized (userAccounts.cacheLock) { - Account[] accounts = getAccountsFromCacheLocked(userAccounts, null); - for (int a = 0; a < accounts.length; a++) { - runningAccounts.add(new AccountAndUser(accounts[a], userId)); - } - } - } - } - - AccountAndUser[] accountsArray = new AccountAndUser[runningAccounts.size()]; - return runningAccounts.toArray(accountsArray); - } - - @Override - public Account[] getAccountsAsUser(String type, int userId) { - // Only allow the system process to read accounts of other users - if (userId != UserHandle.getCallingUserId() - && Binder.getCallingUid() != android.os.Process.myUid()) { - throw new SecurityException("User " + UserHandle.getCallingUserId() - + " trying to get account for " + userId); - } - - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "getAccounts: accountType " + type - + ", caller's uid " + Binder.getCallingUid() - + ", pid " + Binder.getCallingPid()); - } - checkReadAccountsPermission(); - UserAccounts accounts = getUserAccounts(userId); - long identityToken = clearCallingIdentity(); - try { - synchronized (accounts.cacheLock) { - return getAccountsFromCacheLocked(accounts, type); - } - } finally { - restoreCallingIdentity(identityToken); - } - } - - @Override - public Account[] getAccounts(String type) { - return getAccountsAsUser(type, UserHandle.getCallingUserId()); - } - - public void getAccountsByFeatures(IAccountManagerResponse response, - String type, String[] features) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "getAccounts: accountType " + type - + ", response " + response - + ", features " + stringArrayToString(features) - + ", caller's uid " + Binder.getCallingUid() - + ", pid " + Binder.getCallingPid()); - } - if (response == null) throw new IllegalArgumentException("response is null"); - if (type == null) throw new IllegalArgumentException("accountType is null"); - checkReadAccountsPermission(); - UserAccounts userAccounts = getUserAccountsForCaller(); - long identityToken = clearCallingIdentity(); - try { - if (features == null || features.length == 0) { - Account[] accounts; - synchronized (userAccounts.cacheLock) { - accounts = getAccountsFromCacheLocked(userAccounts, type); - } - Bundle result = new Bundle(); - result.putParcelableArray(AccountManager.KEY_ACCOUNTS, accounts); - onResult(response, result); - return; - } - new GetAccountsByTypeAndFeatureSession(userAccounts, response, type, features).bind(); - } finally { - restoreCallingIdentity(identityToken); - } - } - - private long getAccountIdLocked(SQLiteDatabase db, Account account) { - Cursor cursor = db.query(TABLE_ACCOUNTS, new String[]{ACCOUNTS_ID}, - "name=? AND type=?", new String[]{account.name, account.type}, null, null, null); - try { - if (cursor.moveToNext()) { - return cursor.getLong(0); - } - return -1; - } finally { - cursor.close(); - } - } - - private long getExtrasIdLocked(SQLiteDatabase db, long accountId, String key) { - Cursor cursor = db.query(TABLE_EXTRAS, new String[]{EXTRAS_ID}, - EXTRAS_ACCOUNTS_ID + "=" + accountId + " AND " + EXTRAS_KEY + "=?", - new String[]{key}, null, null, null); - try { - if (cursor.moveToNext()) { - return cursor.getLong(0); - } - return -1; - } finally { - cursor.close(); - } - } - - private abstract class Session extends IAccountAuthenticatorResponse.Stub - implements IBinder.DeathRecipient, ServiceConnection { - IAccountManagerResponse mResponse; - final String mAccountType; - final boolean mExpectActivityLaunch; - final long mCreationTime; - - public int mNumResults = 0; - private int mNumRequestContinued = 0; - private int mNumErrors = 0; - - - IAccountAuthenticator mAuthenticator = null; - - private final boolean mStripAuthTokenFromResult; - protected final UserAccounts mAccounts; - - public Session(UserAccounts accounts, IAccountManagerResponse response, String accountType, - boolean expectActivityLaunch, boolean stripAuthTokenFromResult) { - super(); - if (response == null) throw new IllegalArgumentException("response is null"); - if (accountType == null) throw new IllegalArgumentException("accountType is null"); - mAccounts = accounts; - mStripAuthTokenFromResult = stripAuthTokenFromResult; - mResponse = response; - mAccountType = accountType; - mExpectActivityLaunch = expectActivityLaunch; - mCreationTime = SystemClock.elapsedRealtime(); - synchronized (mSessions) { - mSessions.put(toString(), this); - } - try { - response.asBinder().linkToDeath(this, 0 /* flags */); - } catch (RemoteException e) { - mResponse = null; - binderDied(); - } - } - - IAccountManagerResponse getResponseAndClose() { - if (mResponse == null) { - // this session has already been closed - return null; - } - IAccountManagerResponse response = mResponse; - close(); // this clears mResponse so we need to save the response before this call - return response; - } - - private void close() { - synchronized (mSessions) { - if (mSessions.remove(toString()) == null) { - // the session was already closed, so bail out now - return; - } - } - if (mResponse != null) { - // stop listening for response deaths - mResponse.asBinder().unlinkToDeath(this, 0 /* flags */); - - // clear this so that we don't accidentally send any further results - mResponse = null; - } - cancelTimeout(); - unbind(); - } - - public void binderDied() { - mResponse = null; - close(); - } - - protected String toDebugString() { - return toDebugString(SystemClock.elapsedRealtime()); - } - - protected String toDebugString(long now) { - return "Session: expectLaunch " + mExpectActivityLaunch - + ", connected " + (mAuthenticator != null) - + ", stats (" + mNumResults + "/" + mNumRequestContinued - + "/" + mNumErrors + ")" - + ", lifetime " + ((now - mCreationTime) / 1000.0); - } - - void bind() { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "initiating bind to authenticator type " + mAccountType); - } - if (!bindToAuthenticator(mAccountType)) { - Log.d(TAG, "bind attempt failed for " + toDebugString()); - onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, "bind failure"); - } - } - - private void unbind() { - if (mAuthenticator != null) { - mAuthenticator = null; - mContext.unbindService(this); - } - } - - public void scheduleTimeout() { - mMessageHandler.sendMessageDelayed( - mMessageHandler.obtainMessage(MESSAGE_TIMED_OUT, this), TIMEOUT_DELAY_MS); - } - - public void cancelTimeout() { - mMessageHandler.removeMessages(MESSAGE_TIMED_OUT, this); - } - - public void onServiceConnected(ComponentName name, IBinder service) { - mAuthenticator = IAccountAuthenticator.Stub.asInterface(service); - try { - run(); - } catch (RemoteException e) { - onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, - "remote exception"); - } - } - - public void onServiceDisconnected(ComponentName name) { - mAuthenticator = null; - IAccountManagerResponse response = getResponseAndClose(); - if (response != null) { - try { - response.onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, - "disconnected"); - } catch (RemoteException e) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "Session.onServiceDisconnected: " - + "caught RemoteException while responding", e); - } - } - } - } - - public abstract void run() throws RemoteException; - - public void onTimedOut() { - IAccountManagerResponse response = getResponseAndClose(); - if (response != null) { - try { - response.onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, - "timeout"); - } catch (RemoteException e) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "Session.onTimedOut: caught RemoteException while responding", - e); - } - } - } - } - - public void onResult(Bundle result) { - mNumResults++; - if (result != null && !TextUtils.isEmpty(result.getString(AccountManager.KEY_AUTHTOKEN))) { - String accountName = result.getString(AccountManager.KEY_ACCOUNT_NAME); - String accountType = result.getString(AccountManager.KEY_ACCOUNT_TYPE); - if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) { - Account account = new Account(accountName, accountType); - cancelNotification(getSigninRequiredNotificationId(mAccounts, account), - new UserHandle(mAccounts.userId)); - } - } - IAccountManagerResponse response; - if (mExpectActivityLaunch && result != null - && result.containsKey(AccountManager.KEY_INTENT)) { - response = mResponse; - } else { - response = getResponseAndClose(); - } - if (response != null) { - try { - if (result == null) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, getClass().getSimpleName() - + " calling onError() on response " + response); - } - response.onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, - "null bundle returned"); - } else { - if (mStripAuthTokenFromResult) { - result.remove(AccountManager.KEY_AUTHTOKEN); - } - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, getClass().getSimpleName() - + " calling onResult() on response " + response); - } - response.onResult(result); - } - } catch (RemoteException e) { - // if the caller is dead then there is no one to care about remote exceptions - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "failure while notifying response", e); - } - } - } - } - - public void onRequestContinued() { - mNumRequestContinued++; - } - - public void onError(int errorCode, String errorMessage) { - mNumErrors++; - IAccountManagerResponse response = getResponseAndClose(); - if (response != null) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, getClass().getSimpleName() - + " calling onError() on response " + response); - } - try { - response.onError(errorCode, errorMessage); - } catch (RemoteException e) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "Session.onError: caught RemoteException while responding", e); - } - } - } else { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "Session.onError: already closed"); - } - } - } - - /** - * find the component name for the authenticator and initiate a bind - * if no authenticator or the bind fails then return false, otherwise return true - */ - private boolean bindToAuthenticator(String authenticatorType) { - final AccountAuthenticatorCache.ServiceInfo<AuthenticatorDescription> authenticatorInfo; - authenticatorInfo = mAuthenticatorCache.getServiceInfo( - AuthenticatorDescription.newKey(authenticatorType), mAccounts.userId); - if (authenticatorInfo == null) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "there is no authenticator for " + authenticatorType - + ", bailing out"); - } - return false; - } - - Intent intent = new Intent(); - intent.setAction(AccountManager.ACTION_AUTHENTICATOR_INTENT); - intent.setComponent(authenticatorInfo.componentName); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "performing bindService to " + authenticatorInfo.componentName); - } - if (!mContext.bindService(intent, this, Context.BIND_AUTO_CREATE, mAccounts.userId)) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "bindService to " + authenticatorInfo.componentName + " failed"); - } - return false; - } - - - return true; - } - } - - private class MessageHandler extends Handler { - MessageHandler(Looper looper) { - super(looper); - } - - public void handleMessage(Message msg) { - switch (msg.what) { - case MESSAGE_TIMED_OUT: - Session session = (Session)msg.obj; - session.onTimedOut(); - break; - - default: - throw new IllegalStateException("unhandled message: " + msg.what); - } - } - } - - private static String getDatabaseName(int userId) { - File systemDir = Environment.getSystemSecureDirectory(); - File databaseFile = new File(Environment.getUserSystemDirectory(userId), DATABASE_NAME); - if (userId == 0) { - // Migrate old file, if it exists, to the new location. - // Make sure the new file doesn't already exist. A dummy file could have been - // accidentally created in the old location, causing the new one to become corrupted - // as well. - File oldFile = new File(systemDir, DATABASE_NAME); - if (oldFile.exists() && !databaseFile.exists()) { - // Check for use directory; create if it doesn't exist, else renameTo will fail - File userDir = Environment.getUserSystemDirectory(userId); - if (!userDir.exists()) { - if (!userDir.mkdirs()) { - throw new IllegalStateException("User dir cannot be created: " + userDir); - } - } - if (!oldFile.renameTo(databaseFile)) { - throw new IllegalStateException("User dir cannot be migrated: " + databaseFile); - } - } - } - return databaseFile.getPath(); - } - - static class DatabaseHelper extends SQLiteOpenHelper { - - public DatabaseHelper(Context context, int userId) { - super(context, AccountManagerService.getDatabaseName(userId), null, DATABASE_VERSION); - } - - /** - * This call needs to be made while the mCacheLock is held. The way to - * ensure this is to get the lock any time a method is called ont the DatabaseHelper - * @param db The database. - */ - @Override - public void onCreate(SQLiteDatabase db) { - db.execSQL("CREATE TABLE " + TABLE_ACCOUNTS + " ( " - + ACCOUNTS_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " - + ACCOUNTS_NAME + " TEXT NOT NULL, " - + ACCOUNTS_TYPE + " TEXT NOT NULL, " - + ACCOUNTS_PASSWORD + " TEXT, " - + "UNIQUE(" + ACCOUNTS_NAME + "," + ACCOUNTS_TYPE + "))"); - - db.execSQL("CREATE TABLE " + TABLE_AUTHTOKENS + " ( " - + AUTHTOKENS_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " - + AUTHTOKENS_ACCOUNTS_ID + " INTEGER NOT NULL, " - + AUTHTOKENS_TYPE + " TEXT NOT NULL, " - + AUTHTOKENS_AUTHTOKEN + " TEXT, " - + "UNIQUE (" + AUTHTOKENS_ACCOUNTS_ID + "," + AUTHTOKENS_TYPE + "))"); - - createGrantsTable(db); - - db.execSQL("CREATE TABLE " + TABLE_EXTRAS + " ( " - + EXTRAS_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " - + EXTRAS_ACCOUNTS_ID + " INTEGER, " - + EXTRAS_KEY + " TEXT NOT NULL, " - + EXTRAS_VALUE + " TEXT, " - + "UNIQUE(" + EXTRAS_ACCOUNTS_ID + "," + EXTRAS_KEY + "))"); - - db.execSQL("CREATE TABLE " + TABLE_META + " ( " - + META_KEY + " TEXT PRIMARY KEY NOT NULL, " - + META_VALUE + " TEXT)"); - - createAccountsDeletionTrigger(db); - } - - private void createAccountsDeletionTrigger(SQLiteDatabase db) { - db.execSQL("" - + " CREATE TRIGGER " + TABLE_ACCOUNTS + "Delete DELETE ON " + TABLE_ACCOUNTS - + " BEGIN" - + " DELETE FROM " + TABLE_AUTHTOKENS - + " WHERE " + AUTHTOKENS_ACCOUNTS_ID + "=OLD." + ACCOUNTS_ID + " ;" - + " DELETE FROM " + TABLE_EXTRAS - + " WHERE " + EXTRAS_ACCOUNTS_ID + "=OLD." + ACCOUNTS_ID + " ;" - + " DELETE FROM " + TABLE_GRANTS - + " WHERE " + GRANTS_ACCOUNTS_ID + "=OLD." + ACCOUNTS_ID + " ;" - + " END"); - } - - private void createGrantsTable(SQLiteDatabase db) { - db.execSQL("CREATE TABLE " + TABLE_GRANTS + " ( " - + GRANTS_ACCOUNTS_ID + " INTEGER NOT NULL, " - + GRANTS_AUTH_TOKEN_TYPE + " STRING NOT NULL, " - + GRANTS_GRANTEE_UID + " INTEGER NOT NULL, " - + "UNIQUE (" + GRANTS_ACCOUNTS_ID + "," + GRANTS_AUTH_TOKEN_TYPE - + "," + GRANTS_GRANTEE_UID + "))"); - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - Log.e(TAG, "upgrade from version " + oldVersion + " to version " + newVersion); - - if (oldVersion == 1) { - // no longer need to do anything since the work is done - // when upgrading from version 2 - oldVersion++; - } - - if (oldVersion == 2) { - createGrantsTable(db); - db.execSQL("DROP TRIGGER " + TABLE_ACCOUNTS + "Delete"); - createAccountsDeletionTrigger(db); - oldVersion++; - } - - if (oldVersion == 3) { - db.execSQL("UPDATE " + TABLE_ACCOUNTS + " SET " + ACCOUNTS_TYPE + - " = 'com.google' WHERE " + ACCOUNTS_TYPE + " == 'com.google.GAIA'"); - oldVersion++; - } - } - - @Override - public void onOpen(SQLiteDatabase db) { - if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "opened database " + DATABASE_NAME); - } - } - - public IBinder onBind(Intent intent) { - return asBinder(); - } - - /** - * Searches array of arguments for the specified string - * @param args array of argument strings - * @param value value to search for - * @return true if the value is contained in the array - */ - private static boolean scanArgs(String[] args, String value) { - if (args != null) { - for (String arg : args) { - if (value.equals(arg)) { - return true; - } - } - } - return false; - } - - @Override - protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { - if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) - != PackageManager.PERMISSION_GRANTED) { - fout.println("Permission Denial: can't dump AccountsManager from from pid=" - + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid() - + " without permission " + android.Manifest.permission.DUMP); - return; - } - final boolean isCheckinRequest = scanArgs(args, "--checkin") || scanArgs(args, "-c"); - final IndentingPrintWriter ipw = new IndentingPrintWriter(fout, " "); - - final List<UserInfo> users = getUserManager().getUsers(); - for (UserInfo user : users) { - ipw.println("User " + user + ":"); - ipw.increaseIndent(); - dumpUser(getUserAccounts(user.id), fd, ipw, args, isCheckinRequest); - ipw.println(); - ipw.decreaseIndent(); - } - } - - private void dumpUser(UserAccounts userAccounts, FileDescriptor fd, PrintWriter fout, - String[] args, boolean isCheckinRequest) { - synchronized (userAccounts.cacheLock) { - final SQLiteDatabase db = userAccounts.openHelper.getReadableDatabase(); - - if (isCheckinRequest) { - // This is a checkin request. *Only* upload the account types and the count of each. - Cursor cursor = db.query(TABLE_ACCOUNTS, ACCOUNT_TYPE_COUNT_PROJECTION, - null, null, ACCOUNTS_TYPE, null, null); - try { - while (cursor.moveToNext()) { - // print type,count - fout.println(cursor.getString(0) + "," + cursor.getString(1)); - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - } else { - Account[] accounts = getAccountsFromCacheLocked(userAccounts, null /* type */); - fout.println("Accounts: " + accounts.length); - for (Account account : accounts) { - fout.println(" " + account); - } - - fout.println(); - synchronized (mSessions) { - final long now = SystemClock.elapsedRealtime(); - fout.println("Active Sessions: " + mSessions.size()); - for (Session session : mSessions.values()) { - fout.println(" " + session.toDebugString(now)); - } - } - - fout.println(); - mAuthenticatorCache.dump(fd, fout, args, userAccounts.userId); - } - } - } - - private void doNotification(UserAccounts accounts, Account account, CharSequence message, - Intent intent, int userId) { - long identityToken = clearCallingIdentity(); - try { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "doNotification: " + message + " intent:" + intent); - } - - if (intent.getComponent() != null && - GrantCredentialsPermissionActivity.class.getName().equals( - intent.getComponent().getClassName())) { - createNoCredentialsPermissionNotification(account, intent, userId); - } else { - final Integer notificationId = getSigninRequiredNotificationId(accounts, account); - intent.addCategory(String.valueOf(notificationId)); - Notification n = new Notification(android.R.drawable.stat_sys_warning, null, - 0 /* when */); - UserHandle user = new UserHandle(userId); - final String notificationTitleFormat = - mContext.getText(R.string.notification_title).toString(); - n.setLatestEventInfo(mContext, - String.format(notificationTitleFormat, account.name), - message, PendingIntent.getActivityAsUser( - mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT, - null, user)); - installNotification(notificationId, n, user); - } - } finally { - restoreCallingIdentity(identityToken); - } - } - - protected void installNotification(final int notificationId, final Notification n, - UserHandle user) { - ((NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE)) - .notifyAsUser(null, notificationId, n, user); - } - - protected void cancelNotification(int id, UserHandle user) { - long identityToken = clearCallingIdentity(); - try { - ((NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE)) - .cancelAsUser(null, id, user); - } finally { - restoreCallingIdentity(identityToken); - } - } - - /** Succeeds if any of the specified permissions are granted. */ - private void checkBinderPermission(String... permissions) { - final int uid = Binder.getCallingUid(); - - for (String perm : permissions) { - if (mContext.checkCallingOrSelfPermission(perm) == PackageManager.PERMISSION_GRANTED) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, " caller uid " + uid + " has " + perm); - } - return; - } - } - - String msg = "caller uid " + uid + " lacks any of " + TextUtils.join(",", permissions); - Log.w(TAG, " " + msg); - throw new SecurityException(msg); - } - - private boolean inSystemImage(int callingUid) { - final int callingUserId = UserHandle.getUserId(callingUid); - - final PackageManager userPackageManager; - try { - userPackageManager = mContext.createPackageContextAsUser( - "android", 0, new UserHandle(callingUserId)).getPackageManager(); - } catch (NameNotFoundException e) { - return false; - } - - String[] packages = userPackageManager.getPackagesForUid(callingUid); - for (String name : packages) { - try { - PackageInfo packageInfo = userPackageManager.getPackageInfo(name, 0 /* flags */); - if (packageInfo != null - && (packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { - return true; - } - } catch (PackageManager.NameNotFoundException e) { - return false; - } - } - return false; - } - - private boolean permissionIsGranted(Account account, String authTokenType, int callerUid) { - final boolean inSystemImage = inSystemImage(callerUid); - final boolean fromAuthenticator = account != null - && hasAuthenticatorUid(account.type, callerUid); - final boolean hasExplicitGrants = account != null - && hasExplicitlyGrantedPermission(account, authTokenType, callerUid); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "checkGrantsOrCallingUidAgainstAuthenticator: caller uid " - + callerUid + ", " + account - + ": is authenticator? " + fromAuthenticator - + ", has explicit permission? " + hasExplicitGrants); - } - return fromAuthenticator || hasExplicitGrants || inSystemImage; - } - - private boolean hasAuthenticatorUid(String accountType, int callingUid) { - final int callingUserId = UserHandle.getUserId(callingUid); - for (RegisteredServicesCache.ServiceInfo<AuthenticatorDescription> serviceInfo : - mAuthenticatorCache.getAllServices(callingUserId)) { - if (serviceInfo.type.type.equals(accountType)) { - return (serviceInfo.uid == callingUid) || - (mPackageManager.checkSignatures(serviceInfo.uid, callingUid) - == PackageManager.SIGNATURE_MATCH); - } - } - return false; - } - - private boolean hasExplicitlyGrantedPermission(Account account, String authTokenType, - int callerUid) { - if (callerUid == android.os.Process.SYSTEM_UID) { - return true; - } - UserAccounts accounts = getUserAccountsForCaller(); - synchronized (accounts.cacheLock) { - final SQLiteDatabase db = accounts.openHelper.getReadableDatabase(); - String[] args = { String.valueOf(callerUid), authTokenType, - account.name, account.type}; - final boolean permissionGranted = - DatabaseUtils.longForQuery(db, COUNT_OF_MATCHING_GRANTS, args) != 0; - if (!permissionGranted && ActivityManager.isRunningInTestHarness()) { - // TODO: Skip this check when running automated tests. Replace this - // with a more general solution. - Log.d(TAG, "no credentials permission for usage of " + account + ", " - + authTokenType + " by uid " + callerUid - + " but ignoring since device is in test harness."); - return true; - } - return permissionGranted; - } - } - - private void checkCallingUidAgainstAuthenticator(Account account) { - final int uid = Binder.getCallingUid(); - if (account == null || !hasAuthenticatorUid(account.type, uid)) { - String msg = "caller uid " + uid + " is different than the authenticator's uid"; - Log.w(TAG, msg); - throw new SecurityException(msg); - } - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "caller uid " + uid + " is the same as the authenticator's uid"); - } - } - - private void checkAuthenticateAccountsPermission(Account account) { - checkBinderPermission(Manifest.permission.AUTHENTICATE_ACCOUNTS); - checkCallingUidAgainstAuthenticator(account); - } - - private void checkReadAccountsPermission() { - checkBinderPermission(Manifest.permission.GET_ACCOUNTS); - } - - private void checkManageAccountsPermission() { - checkBinderPermission(Manifest.permission.MANAGE_ACCOUNTS); - } - - private void checkManageAccountsOrUseCredentialsPermissions() { - checkBinderPermission(Manifest.permission.MANAGE_ACCOUNTS, - Manifest.permission.USE_CREDENTIALS); - } - - public void updateAppPermission(Account account, String authTokenType, int uid, boolean value) - throws RemoteException { - final int callingUid = getCallingUid(); - - if (callingUid != android.os.Process.SYSTEM_UID) { - throw new SecurityException(); - } - - if (value) { - grantAppPermission(account, authTokenType, uid); - } else { - revokeAppPermission(account, authTokenType, uid); - } - } - - /** - * Allow callers with the given uid permission to get credentials for account/authTokenType. - * <p> - * Although this is public it can only be accessed via the AccountManagerService object - * which is in the system. This means we don't need to protect it with permissions. - * @hide - */ - private void grantAppPermission(Account account, String authTokenType, int uid) { - if (account == null || authTokenType == null) { - Log.e(TAG, "grantAppPermission: called with invalid arguments", new Exception()); - return; - } - UserAccounts accounts = getUserAccounts(UserHandle.getUserId(uid)); - synchronized (accounts.cacheLock) { - final SQLiteDatabase db = accounts.openHelper.getWritableDatabase(); - db.beginTransaction(); - try { - long accountId = getAccountIdLocked(db, account); - if (accountId >= 0) { - ContentValues values = new ContentValues(); - values.put(GRANTS_ACCOUNTS_ID, accountId); - values.put(GRANTS_AUTH_TOKEN_TYPE, authTokenType); - values.put(GRANTS_GRANTEE_UID, uid); - db.insert(TABLE_GRANTS, GRANTS_ACCOUNTS_ID, values); - db.setTransactionSuccessful(); - } - } finally { - db.endTransaction(); - } - cancelNotification(getCredentialPermissionNotificationId(account, authTokenType, uid), - new UserHandle(accounts.userId)); - } - } - - /** - * Don't allow callers with the given uid permission to get credentials for - * account/authTokenType. - * <p> - * Although this is public it can only be accessed via the AccountManagerService object - * which is in the system. This means we don't need to protect it with permissions. - * @hide - */ - private void revokeAppPermission(Account account, String authTokenType, int uid) { - if (account == null || authTokenType == null) { - Log.e(TAG, "revokeAppPermission: called with invalid arguments", new Exception()); - return; - } - UserAccounts accounts = getUserAccounts(UserHandle.getUserId(uid)); - synchronized (accounts.cacheLock) { - final SQLiteDatabase db = accounts.openHelper.getWritableDatabase(); - db.beginTransaction(); - try { - long accountId = getAccountIdLocked(db, account); - if (accountId >= 0) { - db.delete(TABLE_GRANTS, - GRANTS_ACCOUNTS_ID + "=? AND " + GRANTS_AUTH_TOKEN_TYPE + "=? AND " - + GRANTS_GRANTEE_UID + "=?", - new String[]{String.valueOf(accountId), authTokenType, - String.valueOf(uid)}); - db.setTransactionSuccessful(); - } - } finally { - db.endTransaction(); - } - cancelNotification(getCredentialPermissionNotificationId(account, authTokenType, uid), - new UserHandle(accounts.userId)); - } - } - - static final private String stringArrayToString(String[] value) { - return value != null ? ("[" + TextUtils.join(",", value) + "]") : null; - } - - private void removeAccountFromCacheLocked(UserAccounts accounts, Account account) { - final Account[] oldAccountsForType = accounts.accountCache.get(account.type); - if (oldAccountsForType != null) { - ArrayList<Account> newAccountsList = new ArrayList<Account>(); - for (Account curAccount : oldAccountsForType) { - if (!curAccount.equals(account)) { - newAccountsList.add(curAccount); - } - } - if (newAccountsList.isEmpty()) { - accounts.accountCache.remove(account.type); - } else { - Account[] newAccountsForType = new Account[newAccountsList.size()]; - newAccountsForType = newAccountsList.toArray(newAccountsForType); - accounts.accountCache.put(account.type, newAccountsForType); - } - } - accounts.userDataCache.remove(account); - accounts.authTokenCache.remove(account); - } - - /** - * This assumes that the caller has already checked that the account is not already present. - */ - private void insertAccountIntoCacheLocked(UserAccounts accounts, Account account) { - Account[] accountsForType = accounts.accountCache.get(account.type); - int oldLength = (accountsForType != null) ? accountsForType.length : 0; - Account[] newAccountsForType = new Account[oldLength + 1]; - if (accountsForType != null) { - System.arraycopy(accountsForType, 0, newAccountsForType, 0, oldLength); - } - newAccountsForType[oldLength] = account; - accounts.accountCache.put(account.type, newAccountsForType); - } - - protected Account[] getAccountsFromCacheLocked(UserAccounts userAccounts, String accountType) { - if (accountType != null) { - final Account[] accounts = userAccounts.accountCache.get(accountType); - if (accounts == null) { - return EMPTY_ACCOUNT_ARRAY; - } else { - return Arrays.copyOf(accounts, accounts.length); - } - } else { - int totalLength = 0; - for (Account[] accounts : userAccounts.accountCache.values()) { - totalLength += accounts.length; - } - if (totalLength == 0) { - return EMPTY_ACCOUNT_ARRAY; - } - Account[] accounts = new Account[totalLength]; - totalLength = 0; - for (Account[] accountsOfType : userAccounts.accountCache.values()) { - System.arraycopy(accountsOfType, 0, accounts, totalLength, - accountsOfType.length); - totalLength += accountsOfType.length; - } - return accounts; - } - } - - protected void writeUserDataIntoCacheLocked(UserAccounts accounts, final SQLiteDatabase db, - Account account, String key, String value) { - HashMap<String, String> userDataForAccount = accounts.userDataCache.get(account); - if (userDataForAccount == null) { - userDataForAccount = readUserDataForAccountFromDatabaseLocked(db, account); - accounts.userDataCache.put(account, userDataForAccount); - } - if (value == null) { - userDataForAccount.remove(key); - } else { - userDataForAccount.put(key, value); - } - } - - protected void writeAuthTokenIntoCacheLocked(UserAccounts accounts, final SQLiteDatabase db, - Account account, String key, String value) { - HashMap<String, String> authTokensForAccount = accounts.authTokenCache.get(account); - if (authTokensForAccount == null) { - authTokensForAccount = readAuthTokensForAccountFromDatabaseLocked(db, account); - accounts.authTokenCache.put(account, authTokensForAccount); - } - if (value == null) { - authTokensForAccount.remove(key); - } else { - authTokensForAccount.put(key, value); - } - } - - protected String readAuthTokenInternal(UserAccounts accounts, Account account, - String authTokenType) { - synchronized (accounts.cacheLock) { - HashMap<String, String> authTokensForAccount = accounts.authTokenCache.get(account); - if (authTokensForAccount == null) { - // need to populate the cache for this account - final SQLiteDatabase db = accounts.openHelper.getReadableDatabase(); - authTokensForAccount = readAuthTokensForAccountFromDatabaseLocked(db, account); - accounts.authTokenCache.put(account, authTokensForAccount); - } - return authTokensForAccount.get(authTokenType); - } - } - - protected String readUserDataInternal(UserAccounts accounts, Account account, String key) { - synchronized (accounts.cacheLock) { - HashMap<String, String> userDataForAccount = accounts.userDataCache.get(account); - if (userDataForAccount == null) { - // need to populate the cache for this account - final SQLiteDatabase db = accounts.openHelper.getReadableDatabase(); - userDataForAccount = readUserDataForAccountFromDatabaseLocked(db, account); - accounts.userDataCache.put(account, userDataForAccount); - } - return userDataForAccount.get(key); - } - } - - protected HashMap<String, String> readUserDataForAccountFromDatabaseLocked( - final SQLiteDatabase db, Account account) { - HashMap<String, String> userDataForAccount = new HashMap<String, String>(); - Cursor cursor = db.query(TABLE_EXTRAS, - COLUMNS_EXTRAS_KEY_AND_VALUE, - SELECTION_USERDATA_BY_ACCOUNT, - new String[]{account.name, account.type}, - null, null, null); - try { - while (cursor.moveToNext()) { - final String tmpkey = cursor.getString(0); - final String value = cursor.getString(1); - userDataForAccount.put(tmpkey, value); - } - } finally { - cursor.close(); - } - return userDataForAccount; - } - - protected HashMap<String, String> readAuthTokensForAccountFromDatabaseLocked( - final SQLiteDatabase db, Account account) { - HashMap<String, String> authTokensForAccount = new HashMap<String, String>(); - Cursor cursor = db.query(TABLE_AUTHTOKENS, - COLUMNS_AUTHTOKENS_TYPE_AND_AUTHTOKEN, - SELECTION_AUTHTOKENS_BY_ACCOUNT, - new String[]{account.name, account.type}, - null, null, null); - try { - while (cursor.moveToNext()) { - final String type = cursor.getString(0); - final String authToken = cursor.getString(1); - authTokensForAccount.put(type, authToken); - } - } finally { - cursor.close(); - } - return authTokensForAccount; - } -} diff --git a/core/java/android/accounts/IAccountAuthenticatorCache.java b/core/java/android/accounts/IAccountAuthenticatorCache.java deleted file mode 100644 index 06c2106..0000000 --- a/core/java/android/accounts/IAccountAuthenticatorCache.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.accounts; - -import android.content.pm.RegisteredServicesCache; -import android.content.pm.RegisteredServicesCacheListener; -import android.os.Handler; - -import java.io.FileDescriptor; -import java.io.PrintWriter; -import java.util.Collection; - -/** - * An interface to the Authenticator specialization of RegisteredServicesCache. The use of - * this interface by the AccountManagerService makes it easier to unit test it. - * @hide - */ -public interface IAccountAuthenticatorCache { - /** - * Accessor for the {@link android.content.pm.RegisteredServicesCache.ServiceInfo} that - * matched the specified {@link android.accounts.AuthenticatorDescription} or null - * if none match. - * @param type the authenticator type to return - * @return the {@link android.content.pm.RegisteredServicesCache.ServiceInfo} that - * matches the account type or null if none is present - */ - RegisteredServicesCache.ServiceInfo<AuthenticatorDescription> getServiceInfo( - AuthenticatorDescription type, int userId); - - /** - * @return A copy of a Collection of all the current Authenticators. - */ - Collection<RegisteredServicesCache.ServiceInfo<AuthenticatorDescription>> getAllServices( - int userId); - - /** - * Dumps the state of the cache. See - * {@link android.os.Binder#dump(java.io.FileDescriptor, java.io.PrintWriter, String[])} - */ - void dump(FileDescriptor fd, PrintWriter fout, String[] args, int userId); - - /** - * Sets a listener that will be notified whenever the authenticator set changes - * @param listener the listener to notify, or null - * @param handler the {@link Handler} on which the notification will be posted. If null - * the notification will be posted on the main thread. - */ - void setListener(RegisteredServicesCacheListener<AuthenticatorDescription> listener, - Handler handler); - - void invalidateCache(int userId); -} diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index d6ddeb6..87c2d8c 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -1346,6 +1346,20 @@ public class Activity extends ContextThemeWrapper } /** + * This is called when the user is requesting an assist, to build a full + * {@link Intent#ACTION_ASSIST} Intent with all of the context of the current + * application. You can override this method to place into the bundle anything + * you would like to appear in the {@link Intent#EXTRA_ASSIST_CONTEXT} part + * of the assist Intent. The default implementation does nothing. + * + * <p>This function will be called after any global assist callbacks that had + * been registered with {@link Application#registerOnProvideAssistData + * Application.registerOnProvideAssistData}. + */ + public void onProvideAssistData(Bundle data) { + } + + /** * Called when you are no longer visible to the user. You will next * receive either {@link #onRestart}, {@link #onDestroy}, or nothing, * depending on later user activity. @@ -3726,7 +3740,7 @@ public class Activity extends ContextThemeWrapper try { intent.setAllowFds(false); result = ActivityManagerNative.getDefault() - .startActivity(mMainThread.getApplicationThread(), + .startActivity(mMainThread.getApplicationThread(), getBasePackageName(), intent, intent.resolveTypeIfNeeded(getContentResolver()), mToken, mEmbeddedID, requestCode, ActivityManager.START_FLAG_ONLY_IF_NEEDED, null, null, diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 594be68..944a533 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -1915,7 +1915,30 @@ public class ActivityManager { return PackageManager.PERMISSION_DENIED; } - /** @hide */ + /** + * @hide + * Helper for dealing with incoming user arguments to system service calls. + * Takes care of checking permissions and converting USER_CURRENT to the + * actual current user. + * + * @param callingPid The pid of the incoming call, as per Binder.getCallingPid(). + * @param callingUid The uid of the incoming call, as per Binder.getCallingUid(). + * @param userId The user id argument supplied by the caller -- this is the user + * they want to run as. + * @param allowAll If true, we will allow USER_ALL. This means you must be prepared + * to get a USER_ALL returned and deal with it correctly. If false, + * an exception will be thrown if USER_ALL is supplied. + * @param requireFull If true, the caller must hold + * {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL} to be able to run as a + * different user than their current process; otherwise they must hold + * {@link android.Manifest.permission#INTERACT_ACROSS_USERS}. + * @param name Optional textual name of the incoming call; only for generating error messages. + * @param callerPackage Optional package name of caller; only for error messages. + * + * @return Returns the user ID that the call should run as. Will always be a concrete + * user number, unless <var>allowAll</var> is true in which case it could also be + * USER_ALL. + */ public static int handleIncomingUser(int callingPid, int callingUid, int userId, boolean allowAll, boolean requireFull, String name, String callerPackage) { if (UserHandle.getUserId(callingUid) == userId) { diff --git a/core/java/android/app/ActivityManagerNative.java b/core/java/android/app/ActivityManagerNative.java index fe7338b..aca4f9c 100644 --- a/core/java/android/app/ActivityManagerNative.java +++ b/core/java/android/app/ActivityManagerNative.java @@ -39,7 +39,6 @@ import android.os.Parcelable; import android.os.RemoteException; import android.os.ServiceManager; import android.os.StrictMode; -import android.os.UserHandle; import android.text.TextUtils; import android.util.Log; import android.util.Singleton; @@ -93,7 +92,7 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM try { getDefault().broadcastIntent( null, intent, null, null, Activity.RESULT_OK, null, null, - null /*permission*/, false, true, userId); + null /*permission*/, AppOpsManager.OP_NONE, false, true, userId); } catch (RemoteException ex) { } } @@ -117,6 +116,7 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM data.enforceInterface(IActivityManager.descriptor); IBinder b = data.readStrongBinder(); IApplicationThread app = ApplicationThreadNative.asInterface(b); + String callingPackage = data.readString(); Intent intent = Intent.CREATOR.createFromParcel(data); String resolvedType = data.readString(); IBinder resultTo = data.readStrongBinder(); @@ -128,7 +128,7 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM ? data.readFileDescriptor() : null; Bundle options = data.readInt() != 0 ? Bundle.CREATOR.createFromParcel(data) : null; - int result = startActivity(app, intent, resolvedType, + int result = startActivity(app, callingPackage, intent, resolvedType, resultTo, resultWho, requestCode, startFlags, profileFile, profileFd, options); reply.writeNoException(); @@ -141,6 +141,7 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM data.enforceInterface(IActivityManager.descriptor); IBinder b = data.readStrongBinder(); IApplicationThread app = ApplicationThreadNative.asInterface(b); + String callingPackage = data.readString(); Intent intent = Intent.CREATOR.createFromParcel(data); String resolvedType = data.readString(); IBinder resultTo = data.readStrongBinder(); @@ -153,7 +154,7 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM Bundle options = data.readInt() != 0 ? Bundle.CREATOR.createFromParcel(data) : null; int userId = data.readInt(); - int result = startActivityAsUser(app, intent, resolvedType, + int result = startActivityAsUser(app, callingPackage, intent, resolvedType, resultTo, resultWho, requestCode, startFlags, profileFile, profileFd, options, userId); reply.writeNoException(); @@ -166,6 +167,7 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM data.enforceInterface(IActivityManager.descriptor); IBinder b = data.readStrongBinder(); IApplicationThread app = ApplicationThreadNative.asInterface(b); + String callingPackage = data.readString(); Intent intent = Intent.CREATOR.createFromParcel(data); String resolvedType = data.readString(); IBinder resultTo = data.readStrongBinder(); @@ -178,7 +180,7 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM Bundle options = data.readInt() != 0 ? Bundle.CREATOR.createFromParcel(data) : null; int userId = data.readInt(); - WaitResult result = startActivityAndWait(app, intent, resolvedType, + WaitResult result = startActivityAndWait(app, callingPackage, intent, resolvedType, resultTo, resultWho, requestCode, startFlags, profileFile, profileFd, options, userId); reply.writeNoException(); @@ -191,6 +193,7 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM data.enforceInterface(IActivityManager.descriptor); IBinder b = data.readStrongBinder(); IApplicationThread app = ApplicationThreadNative.asInterface(b); + String callingPackage = data.readString(); Intent intent = Intent.CREATOR.createFromParcel(data); String resolvedType = data.readString(); IBinder resultTo = data.readStrongBinder(); @@ -201,7 +204,7 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM Bundle options = data.readInt() != 0 ? Bundle.CREATOR.createFromParcel(data) : null; int userId = data.readInt(); - int result = startActivityWithConfig(app, intent, resolvedType, + int result = startActivityWithConfig(app, callingPackage, intent, resolvedType, resultTo, resultWho, requestCode, startFlags, config, options, userId); reply.writeNoException(); reply.writeInt(result); @@ -341,11 +344,12 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM String resultData = data.readString(); Bundle resultExtras = data.readBundle(); String perm = data.readString(); + int appOp = data.readInt(); boolean serialized = data.readInt() != 0; boolean sticky = data.readInt() != 0; int userId = data.readInt(); int res = broadcastIntent(app, intent, resolvedType, resultTo, - resultCode, resultData, resultExtras, perm, + resultCode, resultData, resultExtras, perm, appOp, serialized, sticky, userId); reply.writeNoException(); reply.writeInt(res); @@ -836,8 +840,10 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM Bundle arguments = data.readBundle(); IBinder b = data.readStrongBinder(); IInstrumentationWatcher w = IInstrumentationWatcher.Stub.asInterface(b); + b = data.readStrongBinder(); + IUiAutomationConnection c = IUiAutomationConnection.Stub.asInterface(b); int userId = data.readInt(); - boolean res = startInstrumentation(className, profileFile, fl, arguments, w, userId); + boolean res = startInstrumentation(className, profileFile, fl, arguments, w, c, userId); reply.writeNoException(); reply.writeInt(res ? 1 : 0); return true; @@ -1526,13 +1532,14 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM data.enforceInterface(IActivityManager.descriptor); IBinder b = data.readStrongBinder(); IApplicationThread app = ApplicationThreadNative.asInterface(b); + String callingPackage = data.readString(); Intent[] intents = data.createTypedArray(Intent.CREATOR); String[] resolvedTypes = data.createStringArray(); IBinder resultTo = data.readStrongBinder(); Bundle options = data.readInt() != 0 ? Bundle.CREATOR.createFromParcel(data) : null; int userId = data.readInt(); - int result = startActivities(app, intents, resolvedTypes, resultTo, + int result = startActivities(app, callingPackage, intents, resolvedTypes, resultTo, options, userId); reply.writeNoException(); reply.writeInt(result); @@ -1784,6 +1791,15 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM return true; } + case GET_LAUNCHED_FROM_PACKAGE_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + IBinder token = data.readStrongBinder(); + String res = getLaunchedFromPackage(token); + reply.writeNoException(); + reply.writeString(res); + return true; + } + case REGISTER_USER_SWITCH_OBSERVER_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); IUserSwitchObserver observer = IUserSwitchObserver.Stub.asInterface( @@ -1819,6 +1835,24 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM return true; } + case GET_TOP_ACTIVITY_EXTRAS_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + int requestType = data.readInt(); + Bundle res = getTopActivityExtras(requestType); + reply.writeNoException(); + reply.writeBundle(res); + return true; + } + + case REPORT_TOP_ACTIVITY_EXTRAS_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + IBinder token = data.readStrongBinder(); + Bundle extras = data.readBundle(); + reportTopActivityExtras(token, extras); + reply.writeNoException(); + return true; + } + } return super.onTransact(code, data, reply, flags); @@ -1855,7 +1889,7 @@ class ActivityManagerProxy implements IActivityManager return mRemote; } - public int startActivity(IApplicationThread caller, Intent intent, + public int startActivity(IApplicationThread caller, String callingPackage, Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode, int startFlags, String profileFile, ParcelFileDescriptor profileFd, Bundle options) throws RemoteException { @@ -1863,6 +1897,7 @@ class ActivityManagerProxy implements IActivityManager Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); data.writeStrongBinder(caller != null ? caller.asBinder() : null); + data.writeString(callingPackage); intent.writeToParcel(data, 0); data.writeString(resolvedType); data.writeStrongBinder(resultTo); @@ -1890,7 +1925,7 @@ class ActivityManagerProxy implements IActivityManager return result; } - public int startActivityAsUser(IApplicationThread caller, Intent intent, + public int startActivityAsUser(IApplicationThread caller, String callingPackage, Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode, int startFlags, String profileFile, ParcelFileDescriptor profileFd, Bundle options, int userId) throws RemoteException { @@ -1898,6 +1933,7 @@ class ActivityManagerProxy implements IActivityManager Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); data.writeStrongBinder(caller != null ? caller.asBinder() : null); + data.writeString(callingPackage); intent.writeToParcel(data, 0); data.writeString(resolvedType); data.writeStrongBinder(resultTo); @@ -1925,14 +1961,15 @@ class ActivityManagerProxy implements IActivityManager data.recycle(); return result; } - public WaitResult startActivityAndWait(IApplicationThread caller, Intent intent, - String resolvedType, IBinder resultTo, String resultWho, + public WaitResult startActivityAndWait(IApplicationThread caller, String callingPackage, + Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode, int startFlags, String profileFile, ParcelFileDescriptor profileFd, Bundle options, int userId) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); data.writeStrongBinder(caller != null ? caller.asBinder() : null); + data.writeString(callingPackage); intent.writeToParcel(data, 0); data.writeString(resolvedType); data.writeStrongBinder(resultTo); @@ -1960,14 +1997,15 @@ class ActivityManagerProxy implements IActivityManager data.recycle(); return result; } - public int startActivityWithConfig(IApplicationThread caller, Intent intent, - String resolvedType, IBinder resultTo, String resultWho, + public int startActivityWithConfig(IApplicationThread caller, String callingPackage, + Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode, int startFlags, Configuration config, Bundle options, int userId) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); data.writeStrongBinder(caller != null ? caller.asBinder() : null); + data.writeString(callingPackage); intent.writeToParcel(data, 0); data.writeString(resolvedType); data.writeStrongBinder(resultTo); @@ -2138,7 +2176,7 @@ class ActivityManagerProxy implements IActivityManager public int broadcastIntent(IApplicationThread caller, Intent intent, String resolvedType, IIntentReceiver resultTo, int resultCode, String resultData, Bundle map, - String requiredPermission, boolean serialized, + String requiredPermission, int appOp, boolean serialized, boolean sticky, int userId) throws RemoteException { Parcel data = Parcel.obtain(); @@ -2152,6 +2190,7 @@ class ActivityManagerProxy implements IActivityManager data.writeString(resultData); data.writeBundle(map); data.writeString(requiredPermission); + data.writeInt(appOp); data.writeInt(serialized ? 1 : 0); data.writeInt(sticky ? 1 : 0); data.writeInt(userId); @@ -2857,8 +2896,8 @@ class ActivityManagerProxy implements IActivityManager } public boolean startInstrumentation(ComponentName className, String profileFile, - int flags, Bundle arguments, IInstrumentationWatcher watcher, int userId) - throws RemoteException { + int flags, Bundle arguments, IInstrumentationWatcher watcher, + IUiAutomationConnection connection, int userId) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); @@ -2867,6 +2906,7 @@ class ActivityManagerProxy implements IActivityManager data.writeInt(flags); data.writeBundle(arguments); data.writeStrongBinder(watcher != null ? watcher.asBinder() : null); + data.writeStrongBinder(connection != null ? connection.asBinder() : null); data.writeInt(userId); mRemote.transact(START_INSTRUMENTATION_TRANSACTION, data, reply, 0); reply.readException(); @@ -3752,13 +3792,14 @@ class ActivityManagerProxy implements IActivityManager return res; } - public int startActivities(IApplicationThread caller, + public int startActivities(IApplicationThread caller, String callingPackage, Intent[] intents, String[] resolvedTypes, IBinder resultTo, Bundle options, int userId) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); data.writeStrongBinder(caller != null ? caller.asBinder() : null); + data.writeString(callingPackage); data.writeTypedArray(intents, 0); data.writeStringArray(resolvedTypes); data.writeStrongBinder(resultTo); @@ -4104,6 +4145,19 @@ class ActivityManagerProxy implements IActivityManager return result; } + public String getLaunchedFromPackage(IBinder activityToken) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeStrongBinder(activityToken); + mRemote.transact(GET_LAUNCHED_FROM_PACKAGE_TRANSACTION, data, reply, 0); + reply.readException(); + String result = reply.readString(); + data.recycle(); + reply.recycle(); + return result; + } + public void registerUserSwitchObserver(IUserSwitchObserver observer) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); @@ -4150,5 +4204,30 @@ class ActivityManagerProxy implements IActivityManager return res; } + public Bundle getTopActivityExtras(int requestType) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeInt(requestType); + mRemote.transact(GET_TOP_ACTIVITY_EXTRAS_TRANSACTION, data, reply, 0); + reply.readException(); + Bundle res = reply.readBundle(); + data.recycle(); + reply.recycle(); + return res; + } + + public void reportTopActivityExtras(IBinder token, Bundle extras) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeStrongBinder(token); + data.writeBundle(extras); + mRemote.transact(REPORT_TOP_ACTIVITY_EXTRAS_TRANSACTION, data, reply, 0); + reply.readException(); + data.recycle(); + reply.recycle(); + } + private IBinder mRemote; } diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index d880817..bb73cf4 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -43,7 +43,6 @@ import android.database.sqlite.SQLiteDebug; import android.database.sqlite.SQLiteDebug.DbStats; import android.graphics.Bitmap; import android.graphics.Canvas; -import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManagerGlobal; import android.net.IConnectivityManager; import android.net.Proxy; @@ -102,7 +101,6 @@ import java.io.IOException; import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.net.InetAddress; -import java.security.Security; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; @@ -187,7 +185,8 @@ public final class ActivityThread { = new ArrayList<Application>(); // set of instantiated backup agents, keyed by package name final HashMap<String, BackupAgent> mBackupAgents = new HashMap<String, BackupAgent>(); - static final ThreadLocal<ActivityThread> sThreadLocal = new ThreadLocal<ActivityThread>(); + /** Reference to singleton {@link ActivityThread} */ + private static ActivityThread sCurrentActivityThread; Instrumentation mInstrumentation; String mInstrumentationAppDir = null; String mInstrumentationAppLibraryDir = null; @@ -419,6 +418,7 @@ public final class ActivityThread { ComponentName instrumentationName; Bundle instrumentationArgs; IInstrumentationWatcher instrumentationWatcher; + IUiAutomationConnection instrumentationUiAutomationConnection; int debugMode; boolean enableOpenGlTrace; boolean restrictedBackupMode; @@ -532,6 +532,12 @@ public final class ActivityThread { String pkg; CompatibilityInfo info; } + + static final class RequestActivityExtras { + IBinder activityToken; + IBinder requestToken; + int requestType; + } private native void dumpGraphicsInfo(FileDescriptor fd); @@ -723,9 +729,10 @@ public final class ActivityThread { ComponentName instrumentationName, String profileFile, ParcelFileDescriptor profileFd, boolean autoStopProfiler, Bundle instrumentationArgs, IInstrumentationWatcher instrumentationWatcher, - int debugMode, boolean enableOpenGlTrace, boolean isRestrictedBackupMode, - boolean persistent, Configuration config, CompatibilityInfo compatInfo, - Map<String, IBinder> services, Bundle coreSettings) { + IUiAutomationConnection instrumentationUiConnection, int debugMode, + boolean enableOpenGlTrace, boolean isRestrictedBackupMode, boolean persistent, + Configuration config, CompatibilityInfo compatInfo, Map<String, IBinder> services, + Bundle coreSettings) { if (services != null) { // Setup the service cache in the ServiceManager @@ -741,6 +748,7 @@ public final class ActivityThread { data.instrumentationName = instrumentationName; data.instrumentationArgs = instrumentationArgs; data.instrumentationWatcher = instrumentationWatcher; + data.instrumentationUiAutomationConnection = instrumentationUiConnection; data.debugMode = debugMode; data.enableOpenGlTrace = enableOpenGlTrace; data.restrictedBackupMode = isRestrictedBackupMode; @@ -1107,6 +1115,16 @@ public final class ActivityThread { queueOrSendMessage(H.UNSTABLE_PROVIDER_DIED, provider); } + @Override + public void requestActivityExtras(IBinder activityToken, IBinder requestToken, + int requestType) { + RequestActivityExtras cmd = new RequestActivityExtras(); + cmd.activityToken = activityToken; + cmd.requestToken = requestToken; + cmd.requestType = requestType; + queueOrSendMessage(H.REQUEST_ACTIVITY_EXTRAS, cmd); + } + private void printRow(PrintWriter pw, String format, Object...objs) { pw.println(String.format(format, objs)); } @@ -1172,6 +1190,7 @@ public final class ActivityThread { public static final int TRIM_MEMORY = 140; public static final int DUMP_PROVIDER = 141; public static final int UNSTABLE_PROVIDER_DIED = 142; + public static final int REQUEST_ACTIVITY_EXTRAS = 143; String codeToString(int code) { if (DEBUG_MESSAGES) { switch (code) { @@ -1218,6 +1237,7 @@ public final class ActivityThread { case TRIM_MEMORY: return "TRIM_MEMORY"; case DUMP_PROVIDER: return "DUMP_PROVIDER"; case UNSTABLE_PROVIDER_DIED: return "UNSTABLE_PROVIDER_DIED"; + case REQUEST_ACTIVITY_EXTRAS: return "REQUEST_ACTIVITY_EXTRAS"; } } return Integer.toString(code); @@ -1429,6 +1449,9 @@ public final class ActivityThread { case UNSTABLE_PROVIDER_DIED: handleUnstableProviderDied((IBinder)msg.obj, false); break; + case REQUEST_ACTIVITY_EXTRAS: + handleRequestActivityExtras((RequestActivityExtras)msg.obj); + break; } if (DEBUG_MESSAGES) Slog.v(TAG, "<<< done: " + codeToString(msg.what)); } @@ -1564,7 +1587,7 @@ public final class ActivityThread { } public static ActivityThread currentActivityThread() { - return sThreadLocal.get(); + return sCurrentActivityThread; } public static String currentPackageName() { @@ -2321,6 +2344,23 @@ public final class ActivityThread { performNewIntents(data.token, data.intents); } + public void handleRequestActivityExtras(RequestActivityExtras cmd) { + Bundle data = new Bundle(); + ActivityClientRecord r = mActivities.get(cmd.activityToken); + if (r != null) { + r.activity.getApplication().dispatchOnProvideAssistData(r.activity, data); + r.activity.onProvideAssistData(data); + } + if (data.isEmpty()) { + data = null; + } + IActivityManager mgr = ActivityManagerNative.getDefault(); + try { + mgr.reportTopActivityExtras(cmd.requestToken, data); + } catch (RemoteException e) { + } + } + private static final ThreadLocal<Intent> sCurrentBroadcastIntent = new ThreadLocal<Intent>(); /** @@ -4277,7 +4317,7 @@ public final class ActivityThread { // Enable OpenGL tracing if required if (data.enableOpenGlTrace) { - GLUtils.enableTracing(); + GLUtils.setTracingLevel(1); } /** @@ -4336,7 +4376,8 @@ public final class ActivityThread { } mInstrumentation.init(this, instrContext, appContext, - new ComponentName(ii.packageName, ii.name), data.instrumentationWatcher); + new ComponentName(ii.packageName, ii.name), data.instrumentationWatcher, + data.instrumentationUiAutomationConnection); if (mProfiler.profileFile != null && !ii.handleProfiling && mProfiler.profileFd == null) { @@ -4894,7 +4935,7 @@ public final class ActivityThread { } private void attach(boolean system) { - sThreadLocal.set(this); + sCurrentActivityThread = this; mSystemThread = system; if (!system) { ViewRootImpl.addFirstDrawHandler(new Runnable() { diff --git a/core/java/android/util/Pool.java b/core/java/android/app/AppOpsManager.aidl index 8cd4f3e..4b97a15 100644 --- a/core/java/android/util/Pool.java +++ b/core/java/android/app/AppOpsManager.aidl @@ -1,11 +1,11 @@ -/* - * Copyright (C) 2009 The Android Open Source Project +/** + * Copyright (c) 2013, The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,12 +14,7 @@ * limitations under the License. */ -package android.util; +package android.app; -/** - * @hide - */ -public interface Pool<T extends Poolable<T>> { - public abstract T acquire(); - public abstract void release(T element); -} +parcelable AppOpsManager.PackageOps; +parcelable AppOpsManager.OpEntry; diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java new file mode 100644 index 0000000..c9776f1 --- /dev/null +++ b/core/java/android/app/AppOpsManager.java @@ -0,0 +1,533 @@ +/* + * 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.app; + +import android.Manifest; +import com.android.internal.app.IAppOpsService; +import com.android.internal.app.IAppOpsCallback; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import android.content.Context; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.Process; +import android.os.RemoteException; + +/** + * API for interacting with "application operation" tracking. Allows you to: + * + * - Note when operations are happening, and find out if they are allowed for the current caller. + * - Disallow specific apps from doing specific operations. + * - Collect all of the current information about operations that have been executed or are not + * being allowed. + * - Monitor for changes in whether an operation is allowed. + * + * Each operation is identified by a single integer; these integers are a fixed set of + * operations, enumerated by the OP_* constants. + * + * When checking operations, the result is a "mode" integer indicating the current setting + * for the operation under that caller: MODE_ALLOWED, MODE_IGNORED (don't execute the operation but + * fake its behavior enough so that the caller doesn't crash), MODE_ERRORED (through a + * SecurityException back to the caller; the normal operation calls will do this for you). + * + * @hide + */ +public class AppOpsManager { + final Context mContext; + final IAppOpsService mService; + final HashMap<Callback, IAppOpsCallback> mModeWatchers + = new HashMap<Callback, IAppOpsCallback>(); + + public static final int MODE_ALLOWED = 0; + public static final int MODE_IGNORED = 1; + public static final int MODE_ERRORED = 2; + + // when adding one of these: + // - increment _NUM_OP + // - add rows to sOpToSwitch, sOpNames, sOpPerms + // - add descriptive strings to Settings/res/values/arrays.xml + public static final int OP_NONE = -1; + public static final int OP_COARSE_LOCATION = 0; + public static final int OP_FINE_LOCATION = 1; + public static final int OP_GPS = 2; + public static final int OP_VIBRATE = 3; + public static final int OP_READ_CONTACTS = 4; + public static final int OP_WRITE_CONTACTS = 5; + public static final int OP_READ_CALL_LOG = 6; + public static final int OP_WRITE_CALL_LOG = 7; + public static final int OP_READ_CALENDAR = 8; + public static final int OP_WRITE_CALENDAR = 9; + public static final int OP_WIFI_SCAN = 10; + public static final int OP_POST_NOTIFICATION = 11; + public static final int OP_NEIGHBORING_CELLS = 12; + public static final int OP_CALL_PHONE = 13; + public static final int OP_READ_SMS = 14; + public static final int OP_WRITE_SMS = 15; + public static final int OP_RECEIVE_SMS = 16; + public static final int OP_RECEIVE_EMERGECY_SMS = 17; + public static final int OP_RECEIVE_MMS = 18; + public static final int OP_RECEIVE_WAP_PUSH = 19; + public static final int OP_SEND_SMS = 20; + public static final int OP_READ_ICC_SMS = 21; + public static final int OP_WRITE_ICC_SMS = 22; + public static final int OP_WRITE_SETTINGS = 23; + public static final int OP_SYSTEM_ALERT_WINDOW = 24; + public static final int OP_ACCESS_NOTIFICATIONS = 25; + public static final int OP_CAMERA = 26; + public static final int OP_RECORD_AUDIO = 27; + public static final int OP_PLAY_AUDIO = 28; + public static final int OP_READ_CLIPBOARD = 29; + public static final int OP_WRITE_CLIPBOARD = 30; + /** @hide */ + public static final int _NUM_OP = 31; + + /** + * This maps each operation to the operation that serves as the + * switch to determine whether it is allowed. Generally this is + * a 1:1 mapping, but for some things (like location) that have + * multiple low-level operations being tracked that should be + * presented to hte user as one switch then this can be used to + * make them all controlled by the same single operation. + */ + private static int[] sOpToSwitch = new int[] { + OP_COARSE_LOCATION, + OP_COARSE_LOCATION, + OP_COARSE_LOCATION, + OP_VIBRATE, + OP_READ_CONTACTS, + OP_WRITE_CONTACTS, + OP_READ_CALL_LOG, + OP_WRITE_CALL_LOG, + OP_READ_CALENDAR, + OP_WRITE_CALENDAR, + OP_COARSE_LOCATION, + OP_POST_NOTIFICATION, + OP_COARSE_LOCATION, + OP_CALL_PHONE, + OP_READ_SMS, + OP_WRITE_SMS, + OP_READ_SMS, + OP_READ_SMS, + OP_READ_SMS, + OP_READ_SMS, + OP_WRITE_SMS, + OP_READ_SMS, + OP_WRITE_SMS, + OP_WRITE_SETTINGS, + OP_SYSTEM_ALERT_WINDOW, + OP_ACCESS_NOTIFICATIONS, + OP_CAMERA, + OP_RECORD_AUDIO, + OP_PLAY_AUDIO, + OP_READ_CLIPBOARD, + OP_WRITE_CLIPBOARD, + }; + + /** + * This provides a simple name for each operation to be used + * in debug output. + */ + private static String[] sOpNames = new String[] { + "COARSE_LOCATION", + "FINE_LOCATION", + "GPS", + "VIBRATE", + "READ_CONTACTS", + "WRITE_CONTACTS", + "READ_CALL_LOG", + "WRITE_CALL_LOG", + "READ_CALENDAR", + "WRITE_CALENDAR", + "WIFI_SCAN", + "POST_NOTIFICATION", + "NEIGHBORING_CELLS", + "CALL_PHONE", + "READ_SMS", + "WRITE_SMS", + "RECEIVE_SMS", + "RECEIVE_EMERGECY_SMS", + "RECEIVE_MMS", + "RECEIVE_WAP_PUSH", + "SEND_SMS", + "READ_ICC_SMS", + "WRITE_ICC_SMS", + "WRITE_SETTINGS", + "SYSTEM_ALERT_WINDOW", + "ACCESS_NOTIFICATIONS", + "CAMERA", + "RECORD_AUDIO", + "PLAY_AUDIO", + "READ_CLIPBOARD", + "WRITE_CLIPBOARD", + }; + + /** + * This optionally maps a permission to an operation. If there + * is no permission associated with an operation, it is null. + */ + private static String[] sOpPerms = new String[] { + android.Manifest.permission.ACCESS_COARSE_LOCATION, + android.Manifest.permission.ACCESS_FINE_LOCATION, + null, + android.Manifest.permission.VIBRATE, + android.Manifest.permission.READ_CONTACTS, + android.Manifest.permission.WRITE_CONTACTS, + android.Manifest.permission.READ_CALL_LOG, + android.Manifest.permission.WRITE_CALL_LOG, + android.Manifest.permission.READ_CALENDAR, + android.Manifest.permission.WRITE_CALENDAR, + null, // no permission required for notifications + android.Manifest.permission.ACCESS_WIFI_STATE, + null, // neighboring cells shares the coarse location perm + android.Manifest.permission.CALL_PHONE, + android.Manifest.permission.READ_SMS, + android.Manifest.permission.WRITE_SMS, + android.Manifest.permission.RECEIVE_SMS, + android.Manifest.permission.RECEIVE_EMERGENCY_BROADCAST, + android.Manifest.permission.RECEIVE_MMS, + android.Manifest.permission.RECEIVE_WAP_PUSH, + android.Manifest.permission.SEND_SMS, + android.Manifest.permission.READ_SMS, + android.Manifest.permission.WRITE_SMS, + android.Manifest.permission.WRITE_SETTINGS, + android.Manifest.permission.SYSTEM_ALERT_WINDOW, + android.Manifest.permission.ACCESS_NOTIFICATIONS, + android.Manifest.permission.CAMERA, + android.Manifest.permission.RECORD_AUDIO, + null, // no permission for playing audio + null, // no permission for reading clipboard + null, // no permission for writing clipboard + }; + + /** + * Retrieve the op switch that controls the given operation. + */ + public static int opToSwitch(int op) { + return sOpToSwitch[op]; + } + + /** + * Retrieve a non-localized name for the operation, for debugging output. + */ + public static String opToName(int op) { + if (op == OP_NONE) return "NONE"; + return op < sOpNames.length ? sOpNames[op] : ("Unknown(" + op + ")"); + } + + /** + * Retrieve the permission associated with an operation, or null if there is not one. + */ + public static String opToPermission(int op) { + return sOpPerms[op]; + } + + /** + * Class holding all of the operation information associated with an app. + */ + public static class PackageOps implements Parcelable { + private final String mPackageName; + private final int mUid; + private final List<OpEntry> mEntries; + + public PackageOps(String packageName, int uid, List<OpEntry> entries) { + mPackageName = packageName; + mUid = uid; + mEntries = entries; + } + + public String getPackageName() { + return mPackageName; + } + + public int getUid() { + return mUid; + } + + public List<OpEntry> getOps() { + return mEntries; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mPackageName); + dest.writeInt(mUid); + dest.writeInt(mEntries.size()); + for (int i=0; i<mEntries.size(); i++) { + mEntries.get(i).writeToParcel(dest, flags); + } + } + + PackageOps(Parcel source) { + mPackageName = source.readString(); + mUid = source.readInt(); + mEntries = new ArrayList<OpEntry>(); + final int N = source.readInt(); + for (int i=0; i<N; i++) { + mEntries.add(OpEntry.CREATOR.createFromParcel(source)); + } + } + + public static final Creator<PackageOps> CREATOR = new Creator<PackageOps>() { + @Override public PackageOps createFromParcel(Parcel source) { + return new PackageOps(source); + } + + @Override public PackageOps[] newArray(int size) { + return new PackageOps[size]; + } + }; + } + + /** + * Class holding the information about one unique operation of an application. + */ + public static class OpEntry implements Parcelable { + private final int mOp; + private final int mMode; + private final long mTime; + private final long mRejectTime; + private final int mDuration; + + public OpEntry(int op, int mode, long time, long rejectTime, int duration) { + mOp = op; + mMode = mode; + mTime = time; + mRejectTime = rejectTime; + mDuration = duration; + } + + public int getOp() { + return mOp; + } + + public int getMode() { + return mMode; + } + + public long getTime() { + return mTime; + } + + public long getRejectTime() { + return mRejectTime; + } + + public boolean isRunning() { + return mDuration == -1; + } + + public int getDuration() { + return mDuration == -1 ? (int)(System.currentTimeMillis()-mTime) : mDuration; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mOp); + dest.writeInt(mMode); + dest.writeLong(mTime); + dest.writeLong(mRejectTime); + dest.writeInt(mDuration); + } + + OpEntry(Parcel source) { + mOp = source.readInt(); + mMode = source.readInt(); + mTime = source.readLong(); + mRejectTime = source.readLong(); + mDuration = source.readInt(); + } + + public static final Creator<OpEntry> CREATOR = new Creator<OpEntry>() { + @Override public OpEntry createFromParcel(Parcel source) { + return new OpEntry(source); + } + + @Override public OpEntry[] newArray(int size) { + return new OpEntry[size]; + } + }; + } + + /** + * Callback for notification of changes to operation state. + */ + public interface Callback { + public void opChanged(int op, String packageName); + } + + public AppOpsManager(Context context, IAppOpsService service) { + mContext = context; + mService = service; + } + + /** + * Retrieve current operation state for all applications. + * + * @param ops The set of operations you are interested in, or null if you want all of them. + */ + public List<AppOpsManager.PackageOps> getPackagesForOps(int[] ops) { + try { + return mService.getPackagesForOps(ops); + } catch (RemoteException e) { + } + return null; + } + + /** + * Retrieve current operation state for one application. + * + * @param uid The uid of the application of interest. + * @param packageName The name of the application of interest. + * @param ops The set of operations you are interested in, or null if you want all of them. + */ + public List<AppOpsManager.PackageOps> getOpsForPackage(int uid, String packageName, int[] ops) { + try { + return mService.getOpsForPackage(uid, packageName, ops); + } catch (RemoteException e) { + } + return null; + } + + public void setMode(int code, int uid, String packageName, int mode) { + try { + mService.setMode(code, uid, packageName, mode); + } catch (RemoteException e) { + } + } + + public void startWatchingMode(int op, String packageName, final Callback callback) { + synchronized (mModeWatchers) { + IAppOpsCallback cb = mModeWatchers.get(callback); + if (cb == null) { + cb = new IAppOpsCallback.Stub() { + public void opChanged(int op, String packageName) { + callback.opChanged(op, packageName); + } + }; + mModeWatchers.put(callback, cb); + } + try { + mService.startWatchingMode(op, packageName, cb); + } catch (RemoteException e) { + } + } + } + + public void stopWatchingMode(Callback callback) { + synchronized (mModeWatchers) { + IAppOpsCallback cb = mModeWatchers.get(callback); + if (cb != null) { + try { + mService.stopWatchingMode(cb); + } catch (RemoteException e) { + } + } + } + } + + public int checkOp(int op, int uid, String packageName) { + try { + int mode = mService.checkOperation(op, uid, packageName); + if (mode == MODE_ERRORED) { + throw new SecurityException("Operation not allowed"); + } + return mode; + } catch (RemoteException e) { + } + return MODE_IGNORED; + } + + public int checkOpNoThrow(int op, int uid, String packageName) { + try { + return mService.checkOperation(op, uid, packageName); + } catch (RemoteException e) { + } + return MODE_IGNORED; + } + + public int noteOp(int op, int uid, String packageName) { + try { + int mode = mService.noteOperation(op, uid, packageName); + if (mode == MODE_ERRORED) { + throw new SecurityException("Operation not allowed"); + } + return mode; + } catch (RemoteException e) { + } + return MODE_IGNORED; + } + + public int noteOpNoThrow(int op, int uid, String packageName) { + try { + return mService.noteOperation(op, uid, packageName); + } catch (RemoteException e) { + } + return MODE_IGNORED; + } + + public int noteOp(int op) { + return noteOp(op, Process.myUid(), mContext.getBasePackageName()); + } + + public int startOp(int op, int uid, String packageName) { + try { + int mode = mService.startOperation(op, uid, packageName); + if (mode == MODE_ERRORED) { + throw new SecurityException("Operation not allowed"); + } + return mode; + } catch (RemoteException e) { + } + return MODE_IGNORED; + } + + public int startOpNoThrow(int op, int uid, String packageName) { + try { + return mService.startOperation(op, uid, packageName); + } catch (RemoteException e) { + } + return MODE_IGNORED; + } + + public int startOp(int op) { + return startOp(op, Process.myUid(), mContext.getBasePackageName()); + } + + public void finishOp(int op, int uid, String packageName) { + try { + mService.finishOperation(op, uid, packageName); + } catch (RemoteException e) { + } + } + + public void finishOp(int op) { + finishOp(op, Process.myUid(), mContext.getBasePackageName()); + } +} diff --git a/core/java/android/app/Application.java b/core/java/android/app/Application.java index 3a67cec..132388e 100644 --- a/core/java/android/app/Application.java +++ b/core/java/android/app/Application.java @@ -22,6 +22,7 @@ import android.content.ComponentCallbacks; import android.content.ComponentCallbacks2; import android.content.Context; import android.content.ContextWrapper; +import android.content.Intent; import android.content.res.Configuration; import android.os.Bundle; @@ -45,6 +46,7 @@ public class Application extends ContextWrapper implements ComponentCallbacks2 { new ArrayList<ComponentCallbacks>(); private ArrayList<ActivityLifecycleCallbacks> mActivityLifecycleCallbacks = new ArrayList<ActivityLifecycleCallbacks>(); + private ArrayList<OnProvideAssistData> mAssistCallbacks = null; /** @hide */ public LoadedApk mLoadedApk; @@ -59,6 +61,21 @@ public class Application extends ContextWrapper implements ComponentCallbacks2 { void onActivityDestroyed(Activity activity); } + /** + * Callback interface for use with {@link Application#registerOnProvideAssistData} + * and {@link Application#unregisterOnProvideAssistData}. + */ + public interface OnProvideAssistData { + /** + * This is called when the user is requesting an assist, to build a full + * {@link Intent#ACTION_ASSIST} Intent with all of the context of the current + * application. You can override this method to place into the bundle anything + * you would like to appear in the {@link Intent#EXTRA_ASSIST_CONTEXT} part + * of the assist Intent. + */ + public void onProvideAssistData(Activity activity, Bundle data); + } + public Application() { super(null); } @@ -137,7 +154,24 @@ public class Application extends ContextWrapper implements ComponentCallbacks2 { mActivityLifecycleCallbacks.remove(callback); } } - + + public void registerOnProvideAssistData(OnProvideAssistData callback) { + synchronized (this) { + if (mAssistCallbacks == null) { + mAssistCallbacks = new ArrayList<OnProvideAssistData>(); + } + mAssistCallbacks.add(callback); + } + } + + public void unregisterOnProvideAssistData(OnProvideAssistData callback) { + synchronized (this) { + if (mAssistCallbacks != null) { + mAssistCallbacks.remove(callback); + } + } + } + // ------------------ Internal API ------------------ /** @@ -232,4 +266,19 @@ public class Application extends ContextWrapper implements ComponentCallbacks2 { } return callbacks; } + + /* package */ void dispatchOnProvideAssistData(Activity activity, Bundle data) { + Object[] callbacks; + synchronized (this) { + if (mAssistCallbacks == null) { + return; + } + callbacks = mAssistCallbacks.toArray(); + } + if (callbacks != null) { + for (int i=0; i<callbacks.length; i++) { + ((OnProvideAssistData)callbacks[i]).onProvideAssistData(activity, data); + } + } + } } diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index 7431765..f09c2fe 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -142,6 +142,21 @@ final class ApplicationPackageManager extends PackageManager { } @Override + public int getPackageUid(String packageName, int userHandle) + throws NameNotFoundException { + try { + int uid = mPM.getPackageUid(packageName, userHandle); + if (uid >= 0) { + return uid; + } + } catch (RemoteException e) { + throw new RuntimeException("Package manager has died", e); + } + + throw new NameNotFoundException(packageName); + } + + @Override public PermissionInfo getPermissionInfo(String name, int flags) throws NameNotFoundException { try { @@ -411,17 +426,22 @@ final class ApplicationPackageManager extends PackageManager { @Override public List<PackageInfo> getInstalledPackages(int flags, int userId) { try { - final List<PackageInfo> packageInfos = new ArrayList<PackageInfo>(); - PackageInfo lastItem = null; - ParceledListSlice<PackageInfo> slice; - - do { - final String lastKey = lastItem != null ? lastItem.packageName : null; - slice = mPM.getInstalledPackages(flags, lastKey, userId); - lastItem = slice.populateList(packageInfos, PackageInfo.CREATOR); - } while (!slice.isLastSlice()); + ParceledListSlice<PackageInfo> slice = mPM.getInstalledPackages(flags, userId); + return slice.getList(); + } catch (RemoteException e) { + throw new RuntimeException("Package manager has died", e); + } + } - return packageInfos; + @SuppressWarnings("unchecked") + @Override + public List<PackageInfo> getPackagesHoldingPermissions( + String[] permissions, int flags) { + final int userId = mContext.getUserId(); + try { + ParceledListSlice<PackageInfo> slice = mPM.getPackagesHoldingPermissions( + permissions, flags, userId); + return slice.getList(); } catch (RemoteException e) { throw new RuntimeException("Package manager has died", e); } @@ -432,17 +452,8 @@ final class ApplicationPackageManager extends PackageManager { public List<ApplicationInfo> getInstalledApplications(int flags) { final int userId = mContext.getUserId(); try { - final List<ApplicationInfo> applicationInfos = new ArrayList<ApplicationInfo>(); - ApplicationInfo lastItem = null; - ParceledListSlice<ApplicationInfo> slice; - - do { - final String lastKey = lastItem != null ? lastItem.packageName : null; - slice = mPM.getInstalledApplications(flags, lastKey, userId); - lastItem = slice.populateList(applicationInfos, ApplicationInfo.CREATOR); - } while (!slice.isLastSlice()); - - return applicationInfos; + ParceledListSlice<ApplicationInfo> slice = mPM.getInstalledApplications(flags, userId); + return slice.getList(); } catch (RemoteException e) { throw new RuntimeException("Package manager has died", e); } diff --git a/core/java/android/app/ApplicationThreadNative.java b/core/java/android/app/ApplicationThreadNative.java index 63aa5f9..b1c58f2 100644 --- a/core/java/android/app/ApplicationThreadNative.java +++ b/core/java/android/app/ApplicationThreadNative.java @@ -267,6 +267,9 @@ public abstract class ApplicationThreadNative extends Binder Bundle testArgs = data.readBundle(); IBinder binder = data.readStrongBinder(); IInstrumentationWatcher testWatcher = IInstrumentationWatcher.Stub.asInterface(binder); + binder = data.readStrongBinder(); + IUiAutomationConnection uiAutomationConnection = + IUiAutomationConnection.Stub.asInterface(binder); int testMode = data.readInt(); boolean openGlTrace = data.readInt() != 0; boolean restrictedBackupMode = (data.readInt() != 0); @@ -277,8 +280,9 @@ public abstract class ApplicationThreadNative extends Binder Bundle coreSettings = data.readBundle(); bindApplication(packageName, info, providers, testName, profileName, profileFd, autoStopProfiler, - testArgs, testWatcher, testMode, openGlTrace, restrictedBackupMode, - persistent, config, compatInfo, services, coreSettings); + testArgs, testWatcher, uiAutomationConnection, testMode, + openGlTrace, restrictedBackupMode, persistent, config, compatInfo, + services, coreSettings); return true; } @@ -587,6 +591,17 @@ public abstract class ApplicationThreadNative extends Binder reply.writeNoException(); return true; } + + case REQUEST_ACTIVITY_EXTRAS_TRANSACTION: + { + data.enforceInterface(IApplicationThread.descriptor); + IBinder activityToken = data.readStrongBinder(); + IBinder requestToken = data.readStrongBinder(); + int requestType = data.readInt(); + requestActivityExtras(activityToken, requestToken, requestType); + reply.writeNoException(); + return true; + } } return super.onTransact(code, data, reply, flags); @@ -863,10 +878,11 @@ class ApplicationThreadProxy implements IApplicationThread { public final void bindApplication(String packageName, ApplicationInfo info, List<ProviderInfo> providers, ComponentName testName, String profileName, ParcelFileDescriptor profileFd, boolean autoStopProfiler, Bundle testArgs, - IInstrumentationWatcher testWatcher, int debugMode, boolean openGlTrace, - boolean restrictedBackupMode, boolean persistent, - Configuration config, CompatibilityInfo compatInfo, - Map<String, IBinder> services, Bundle coreSettings) throws RemoteException { + IInstrumentationWatcher testWatcher, + IUiAutomationConnection uiAutomationConnection, int debugMode, + boolean openGlTrace, boolean restrictedBackupMode, boolean persistent, + Configuration config, CompatibilityInfo compatInfo, Map<String, IBinder> services, + Bundle coreSettings) throws RemoteException { Parcel data = Parcel.obtain(); data.writeInterfaceToken(IApplicationThread.descriptor); data.writeString(packageName); @@ -888,6 +904,7 @@ class ApplicationThreadProxy implements IApplicationThread { data.writeInt(autoStopProfiler ? 1 : 0); data.writeBundle(testArgs); data.writeStrongInterface(testWatcher); + data.writeStrongInterface(uiAutomationConnection); data.writeInt(debugMode); data.writeInt(openGlTrace ? 1 : 0); data.writeInt(restrictedBackupMode ? 1 : 0); @@ -1185,4 +1202,15 @@ class ApplicationThreadProxy implements IApplicationThread { mRemote.transact(UNSTABLE_PROVIDER_DIED_TRANSACTION, data, null, IBinder.FLAG_ONEWAY); data.recycle(); } + + public void requestActivityExtras(IBinder activityToken, IBinder requestToken, int requestType) + throws RemoteException { + Parcel data = Parcel.obtain(); + data.writeInterfaceToken(IApplicationThread.descriptor); + data.writeStrongBinder(activityToken); + data.writeStrongBinder(requestToken); + data.writeInt(requestType); + mRemote.transact(REQUEST_ACTIVITY_EXTRAS_TRANSACTION, data, null, IBinder.FLAG_ONEWAY); + data.recycle(); + } } diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index f895ccc..734d435 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -47,11 +47,9 @@ import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.hardware.ISerialManager; -import android.hardware.SensorManager; import android.hardware.SerialManager; import android.hardware.SystemSensorManager; import android.hardware.display.DisplayManager; -import android.hardware.input.IInputManager; import android.hardware.input.InputManager; import android.hardware.usb.IUsbManager; import android.hardware.usb.UsbManager; @@ -65,8 +63,6 @@ import android.net.ConnectivityManager; import android.net.IConnectivityManager; import android.net.INetworkPolicyManager; import android.net.NetworkPolicyManager; -import android.net.ThrottleManager; -import android.net.IThrottleManager; import android.net.Uri; import android.net.nsd.INsdManager; import android.net.nsd.NsdManager; @@ -109,6 +105,8 @@ import android.view.textservice.TextServicesManager; import android.accounts.AccountManager; import android.accounts.IAccountManager; import android.app.admin.DevicePolicyManager; + +import com.android.internal.app.IAppOpsService; import com.android.internal.os.IDropBoxManagerService; import java.io.File; @@ -462,7 +460,8 @@ class ContextImpl extends Context { registerService(STORAGE_SERVICE, new ServiceFetcher() { public Object createService(ContextImpl ctx) { try { - return new StorageManager(ctx.mMainThread.getHandler().getLooper()); + return new StorageManager( + ctx.getContentResolver(), ctx.mMainThread.getHandler().getLooper()); } catch (RemoteException rex) { Log.e(TAG, "Failed to create StorageManager", rex); return null; @@ -474,12 +473,6 @@ class ContextImpl extends Context { return new TelephonyManager(ctx.getOuterContext()); }}); - registerService(THROTTLE_SERVICE, new StaticServiceFetcher() { - public Object createStaticService() { - IBinder b = ServiceManager.getService(THROTTLE_SERVICE); - return new ThrottleManager(IThrottleManager.Stub.asInterface(b)); - }}); - registerService(UI_MODE_SERVICE, new ServiceFetcher() { public Object createService(ContextImpl ctx) { return new UiModeManager(); @@ -499,7 +492,7 @@ class ContextImpl extends Context { registerService(VIBRATOR_SERVICE, new ServiceFetcher() { public Object createService(ContextImpl ctx) { - return new SystemVibrator(); + return new SystemVibrator(ctx); }}); registerService(WALLPAPER_SERVICE, WALLPAPER_FETCHER); @@ -530,11 +523,18 @@ class ContextImpl extends Context { }}); registerService(USER_SERVICE, new ServiceFetcher() { - public Object getService(ContextImpl ctx) { + public Object createService(ContextImpl ctx) { IBinder b = ServiceManager.getService(USER_SERVICE); IUserManager service = IUserManager.Stub.asInterface(b); return new UserManager(ctx, service); }}); + + registerService(APP_OPS_SERVICE, new ServiceFetcher() { + public Object createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(APP_OPS_SERVICE); + IAppOpsService service = IAppOpsService.Stub.asInterface(b); + return new AppOpsManager(ctx, service); + }}); } static ContextImpl getImpl(Context context) { @@ -624,7 +624,15 @@ class ContextImpl extends Context { if (mPackageInfo != null) { return mPackageInfo.getPackageName(); } - throw new RuntimeException("Not supported in system context"); + // No mPackageInfo means this is a Context for the system itself, + // and this here is its name. + return "android"; + } + + /** @hide */ + @Override + public String getBasePackageName() { + return mBasePackageName != null ? mBasePackageName : getPackageName(); } @Override @@ -956,7 +964,7 @@ class ContextImpl extends Context { public void startActivityAsUser(Intent intent, Bundle options, UserHandle user) { try { ActivityManagerNative.getDefault().startActivityAsUser( - mMainThread.getApplicationThread(), intent, + mMainThread.getApplicationThread(), getBasePackageName(), intent, intent.resolveTypeIfNeeded(getContentResolver()), null, null, 0, Intent.FLAG_ACTIVITY_NEW_TASK, null, null, options, user.getIdentifier()); @@ -1035,7 +1043,7 @@ class ContextImpl extends Context { intent.setAllowFds(false); ActivityManagerNative.getDefault().broadcastIntent( mMainThread.getApplicationThread(), intent, resolvedType, null, - Activity.RESULT_OK, null, null, null, false, false, + Activity.RESULT_OK, null, null, null, AppOpsManager.OP_NONE, false, false, getUserId()); } catch (RemoteException e) { } @@ -1049,7 +1057,21 @@ class ContextImpl extends Context { intent.setAllowFds(false); ActivityManagerNative.getDefault().broadcastIntent( mMainThread.getApplicationThread(), intent, resolvedType, null, - Activity.RESULT_OK, null, null, receiverPermission, false, false, + Activity.RESULT_OK, null, null, receiverPermission, AppOpsManager.OP_NONE, + false, false, getUserId()); + } catch (RemoteException e) { + } + } + + @Override + public void sendBroadcast(Intent intent, String receiverPermission, int appOp) { + warnIfCallingFromSystemProcess(); + String resolvedType = intent.resolveTypeIfNeeded(getContentResolver()); + try { + intent.setAllowFds(false); + ActivityManagerNative.getDefault().broadcastIntent( + mMainThread.getApplicationThread(), intent, resolvedType, null, + Activity.RESULT_OK, null, null, receiverPermission, appOp, false, false, getUserId()); } catch (RemoteException e) { } @@ -1064,7 +1086,7 @@ class ContextImpl extends Context { intent.setAllowFds(false); ActivityManagerNative.getDefault().broadcastIntent( mMainThread.getApplicationThread(), intent, resolvedType, null, - Activity.RESULT_OK, null, null, receiverPermission, true, false, + Activity.RESULT_OK, null, null, receiverPermission, AppOpsManager.OP_NONE, true, false, getUserId()); } catch (RemoteException e) { } @@ -1075,6 +1097,15 @@ class ContextImpl extends Context { String receiverPermission, BroadcastReceiver resultReceiver, Handler scheduler, int initialCode, String initialData, Bundle initialExtras) { + sendOrderedBroadcast(intent, receiverPermission, AppOpsManager.OP_NONE, + resultReceiver, scheduler, initialCode, initialData, initialExtras); + } + + @Override + public void sendOrderedBroadcast(Intent intent, + String receiverPermission, int appOp, BroadcastReceiver resultReceiver, + Handler scheduler, int initialCode, String initialData, + Bundle initialExtras) { warnIfCallingFromSystemProcess(); IIntentReceiver rd = null; if (resultReceiver != null) { @@ -1098,8 +1129,8 @@ class ContextImpl extends Context { intent.setAllowFds(false); ActivityManagerNative.getDefault().broadcastIntent( mMainThread.getApplicationThread(), intent, resolvedType, rd, - initialCode, initialData, initialExtras, receiverPermission, - true, false, getUserId()); + initialCode, initialData, initialExtras, receiverPermission, appOp, + true, false, getUserId()); } catch (RemoteException e) { } } @@ -1110,8 +1141,8 @@ class ContextImpl extends Context { try { intent.setAllowFds(false); ActivityManagerNative.getDefault().broadcastIntent(mMainThread.getApplicationThread(), - intent, resolvedType, null, Activity.RESULT_OK, null, null, null, false, false, - user.getIdentifier()); + intent, resolvedType, null, Activity.RESULT_OK, null, null, null, + AppOpsManager.OP_NONE, false, false, user.getIdentifier()); } catch (RemoteException e) { } } @@ -1124,7 +1155,7 @@ class ContextImpl extends Context { intent.setAllowFds(false); ActivityManagerNative.getDefault().broadcastIntent( mMainThread.getApplicationThread(), intent, resolvedType, null, - Activity.RESULT_OK, null, null, receiverPermission, false, false, + Activity.RESULT_OK, null, null, receiverPermission, AppOpsManager.OP_NONE, false, false, user.getIdentifier()); } catch (RemoteException e) { } @@ -1157,7 +1188,7 @@ class ContextImpl extends Context { ActivityManagerNative.getDefault().broadcastIntent( mMainThread.getApplicationThread(), intent, resolvedType, rd, initialCode, initialData, initialExtras, receiverPermission, - true, false, user.getIdentifier()); + AppOpsManager.OP_NONE, true, false, user.getIdentifier()); } catch (RemoteException e) { } } @@ -1170,7 +1201,7 @@ class ContextImpl extends Context { intent.setAllowFds(false); ActivityManagerNative.getDefault().broadcastIntent( mMainThread.getApplicationThread(), intent, resolvedType, null, - Activity.RESULT_OK, null, null, null, false, true, + Activity.RESULT_OK, null, null, null, AppOpsManager.OP_NONE, false, true, getUserId()); } catch (RemoteException e) { } @@ -1205,7 +1236,7 @@ class ContextImpl extends Context { ActivityManagerNative.getDefault().broadcastIntent( mMainThread.getApplicationThread(), intent, resolvedType, rd, initialCode, initialData, initialExtras, null, - true, true, getUserId()); + AppOpsManager.OP_NONE, true, true, getUserId()); } catch (RemoteException e) { } } @@ -1232,7 +1263,7 @@ class ContextImpl extends Context { intent.setAllowFds(false); ActivityManagerNative.getDefault().broadcastIntent( mMainThread.getApplicationThread(), intent, resolvedType, null, - Activity.RESULT_OK, null, null, null, false, true, user.getIdentifier()); + Activity.RESULT_OK, null, null, null, AppOpsManager.OP_NONE, false, true, user.getIdentifier()); } catch (RemoteException e) { } } @@ -1265,7 +1296,7 @@ class ContextImpl extends Context { ActivityManagerNative.getDefault().broadcastIntent( mMainThread.getApplicationThread(), intent, resolvedType, rd, initialCode, initialData, initialExtras, null, - true, true, user.getIdentifier()); + AppOpsManager.OP_NONE, true, true, user.getIdentifier()); } catch (RemoteException e) { } } @@ -1404,12 +1435,13 @@ class ContextImpl extends Context { public boolean bindService(Intent service, ServiceConnection conn, int flags) { warnIfCallingFromSystemProcess(); - return bindService(service, conn, flags, UserHandle.getUserId(Process.myUid())); + return bindServiceAsUser(service, conn, flags, Process.myUserHandle()); } /** @hide */ @Override - public boolean bindService(Intent service, ServiceConnection conn, int flags, int userHandle) { + public boolean bindServiceAsUser(Intent service, ServiceConnection conn, int flags, + UserHandle user) { IServiceConnection sd; if (conn == null) { throw new IllegalArgumentException("connection is null"); @@ -1431,7 +1463,7 @@ class ContextImpl extends Context { int res = ActivityManagerNative.getDefault().bindService( mMainThread.getApplicationThread(), getActivityToken(), service, service.resolveTypeIfNeeded(getContentResolver()), - sd, flags, userHandle); + sd, flags, user.getIdentifier()); if (res < 0) { throw new SecurityException( "Not allowed to bind to service " + service); @@ -1467,7 +1499,7 @@ class ContextImpl extends Context { arguments.setAllowFds(false); } return ActivityManagerNative.getDefault().startInstrumentation( - className, profileFile, 0, arguments, null, getUserId()); + className, profileFile, 0, arguments, null, null, getUserId()); } catch (RemoteException e) { // System has crashed, nothing we can do. } diff --git a/core/java/android/app/DownloadManager.java b/core/java/android/app/DownloadManager.java index 32e40ee..26dc60d 100644 --- a/core/java/android/app/DownloadManager.java +++ b/core/java/android/app/DownloadManager.java @@ -1085,6 +1085,7 @@ public class DownloadManager { values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, -1); values.putNull(Downloads.Impl._DATA); values.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_PENDING); + values.put(Downloads.Impl.COLUMN_FAILED_CONNECTIONS, 0); mResolver.update(mBaseUri, values, getWhereClauseForIds(ids), getWhereArgsForIds(ids)); } diff --git a/core/java/android/app/IActivityManager.java b/core/java/android/app/IActivityManager.java index 8af17a4..cf4c729 100644 --- a/core/java/android/app/IActivityManager.java +++ b/core/java/android/app/IActivityManager.java @@ -51,19 +51,19 @@ import java.util.List; * {@hide} */ public interface IActivityManager extends IInterface { - public int startActivity(IApplicationThread caller, + public int startActivity(IApplicationThread caller, String callingPackage, Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode, int flags, String profileFile, ParcelFileDescriptor profileFd, Bundle options) throws RemoteException; - public int startActivityAsUser(IApplicationThread caller, + public int startActivityAsUser(IApplicationThread caller, String callingPackage, Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode, int flags, String profileFile, ParcelFileDescriptor profileFd, Bundle options, int userId) throws RemoteException; - public WaitResult startActivityAndWait(IApplicationThread caller, + public WaitResult startActivityAndWait(IApplicationThread caller, String callingPackage, Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode, int flags, String profileFile, ParcelFileDescriptor profileFd, Bundle options, int userId) throws RemoteException; - public int startActivityWithConfig(IApplicationThread caller, + public int startActivityWithConfig(IApplicationThread caller, String callingPackage, Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode, int startFlags, Configuration newConfig, Bundle options, int userId) throws RemoteException; @@ -85,7 +85,7 @@ public interface IActivityManager extends IInterface { public int broadcastIntent(IApplicationThread caller, Intent intent, String resolvedType, IIntentReceiver resultTo, int resultCode, String resultData, Bundle map, String requiredPermission, - boolean serialized, boolean sticky, int userId) throws RemoteException; + int appOp, boolean serialized, boolean sticky, int userId) throws RemoteException; public void unbroadcastIntent(IApplicationThread caller, Intent intent, int userId) throws RemoteException; public void finishReceiver(IBinder who, int resultCode, String resultData, Bundle map, boolean abortBroadcast) throws RemoteException; public void attachApplication(IApplicationThread app) throws RemoteException; @@ -158,8 +158,8 @@ public interface IActivityManager extends IInterface { public void killApplicationProcess(String processName, int uid) throws RemoteException; public boolean startInstrumentation(ComponentName className, String profileFile, - int flags, Bundle arguments, IInstrumentationWatcher watcher, int userId) - throws RemoteException; + int flags, Bundle arguments, IInstrumentationWatcher watcher, + IUiAutomationConnection connection, int userId) throws RemoteException; public void finishInstrumentation(IApplicationThread target, int resultCode, Bundle results) throws RemoteException; @@ -310,7 +310,7 @@ public interface IActivityManager extends IInterface { public boolean dumpHeap(String process, int userId, boolean managed, String path, ParcelFileDescriptor fd) throws RemoteException; - public int startActivities(IApplicationThread caller, + public int startActivities(IApplicationThread caller, String callingPackage, Intent[] intents, String[] resolvedTypes, IBinder resultTo, Bundle options, int userId) throws RemoteException; @@ -357,9 +357,10 @@ public interface IActivityManager extends IInterface { public boolean navigateUpTo(IBinder token, Intent target, int resultCode, Intent resultData) throws RemoteException; - // This is not public because you need to be very careful in how you + // These are not public because you need to be very careful in how you // manage your activity to make sure it is always the uid you expect. public int getLaunchedFromUid(IBinder activityToken) throws RemoteException; + public String getLaunchedFromPackage(IBinder activityToken) throws RemoteException; public void registerUserSwitchObserver(IUserSwitchObserver observer) throws RemoteException; public void unregisterUserSwitchObserver(IUserSwitchObserver observer) throws RemoteException; @@ -368,6 +369,10 @@ public interface IActivityManager extends IInterface { public long inputDispatchingTimedOut(int pid, boolean aboveSystem) throws RemoteException; + public Bundle getTopActivityExtras(int requestType) throws RemoteException; + + public void reportTopActivityExtras(IBinder token, Bundle extras) throws RemoteException; + /* * Private non-Binder interfaces */ @@ -624,4 +629,7 @@ public interface IActivityManager extends IInterface { int INPUT_DISPATCHING_TIMED_OUT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+158; int CLEAR_PENDING_BACKUP_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+159; int GET_INTENT_FOR_INTENT_SENDER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+160; + int GET_TOP_ACTIVITY_EXTRAS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+161; + int REPORT_TOP_ACTIVITY_EXTRAS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+162; + int GET_LAUNCHED_FROM_PACKAGE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+163; } diff --git a/core/java/android/app/IApplicationThread.java b/core/java/android/app/IApplicationThread.java index 03a26d4..3189b31 100644 --- a/core/java/android/app/IApplicationThread.java +++ b/core/java/android/app/IApplicationThread.java @@ -90,7 +90,8 @@ public interface IApplicationThread extends IInterface { void bindApplication(String packageName, ApplicationInfo info, List<ProviderInfo> providers, ComponentName testName, String profileName, ParcelFileDescriptor profileFd, boolean autoStopProfiler, Bundle testArguments, IInstrumentationWatcher testWatcher, - int debugMode, boolean openGlTrace, boolean restrictedBackupMode, boolean persistent, + IUiAutomationConnection uiAutomationConnection, int debugMode, + boolean openGlTrace, boolean restrictedBackupMode, boolean persistent, Configuration config, CompatibilityInfo compatInfo, Map<String, IBinder> services, Bundle coreSettings) throws RemoteException; void scheduleExit() throws RemoteException; @@ -130,6 +131,8 @@ public interface IApplicationThread extends IInterface { void dumpGfxInfo(FileDescriptor fd, String[] args) throws RemoteException; void dumpDbInfo(FileDescriptor fd, String[] args) throws RemoteException; void unstableProviderDied(IBinder provider) throws RemoteException; + void requestActivityExtras(IBinder activityToken, IBinder requestToken, int requestType) + throws RemoteException; String descriptor = "android.app.IApplicationThread"; @@ -179,4 +182,5 @@ public interface IApplicationThread extends IInterface { int DUMP_PROVIDER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+44; int DUMP_DB_INFO_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+45; int UNSTABLE_PROVIDER_DIED_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+46; + int REQUEST_ACTIVITY_EXTRAS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+47; } diff --git a/core/java/android/net/IThrottleManager.aidl b/core/java/android/app/INotificationListener.aidl index a12469d..f010a2a 100644 --- a/core/java/android/net/IThrottleManager.aidl +++ b/core/java/android/app/INotificationListener.aidl @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010, The Android Open Source Project + * Copyright (c) 2013, The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,27 +14,13 @@ * limitations under the License. */ -package android.net; +package android.app; -import android.os.IBinder; +import com.android.internal.statusbar.StatusBarNotification; -/** - * Interface that answers queries about data transfer amounts and throttling - */ -/** {@hide} */ -interface IThrottleManager +/** @hide */ +oneway interface INotificationListener { - long getByteCount(String iface, int dir, int period, int ago); - - int getThrottle(String iface); - - long getResetTime(String iface); - - long getPeriodStartTime(String iface); - - long getCliffThreshold(String iface, int cliff); - - int getCliffLevel(String iface, int cliff); - - String getHelpUri(); -} + void onNotificationPosted(in StatusBarNotification notification); + void onNotificationRemoved(in StatusBarNotification notification); +}
\ No newline at end of file diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index 62d4962..14bcc0d 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -17,10 +17,13 @@ package android.app; +import android.app.INotificationListener; import android.app.ITransientNotification; import android.app.Notification; import android.content.Intent; +import com.android.internal.statusbar.StatusBarNotification; + /** {@hide} */ interface INotificationManager { @@ -28,11 +31,17 @@ interface INotificationManager void enqueueToast(String pkg, ITransientNotification callback, int duration); void cancelToast(String pkg, ITransientNotification callback); - void enqueueNotificationWithTag(String pkg, String tag, int id, + void enqueueNotificationWithTag(String pkg, String basePkg, String tag, int id, in Notification notification, inout int[] idReceived, int userId); void cancelNotificationWithTag(String pkg, String tag, int id, int userId); - void setNotificationsEnabledForPackage(String pkg, boolean enabled); - boolean areNotificationsEnabledForPackage(String pkg); + void setNotificationsEnabledForPackage(String pkg, int uid, boolean enabled); + boolean areNotificationsEnabledForPackage(String pkg, int uid); + + StatusBarNotification[] getActiveNotifications(String callingPkg); + StatusBarNotification[] getHistoricalNotifications(String callingPkg, int count); + + void registerListener(in INotificationListener listener, int userid); + void unregisterListener(in INotificationListener listener, int userid); } diff --git a/core/java/android/app/IUiAutomationConnection.aidl b/core/java/android/app/IUiAutomationConnection.aidl new file mode 100644 index 0000000..09bf829 --- /dev/null +++ b/core/java/android/app/IUiAutomationConnection.aidl @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import android.accessibilityservice.IAccessibilityServiceClient; +import android.graphics.Bitmap; +import android.view.InputEvent; +import android.os.ParcelFileDescriptor; + +/** + * This interface contains privileged operations a shell program can perform + * on behalf of an instrumentation that it runs. These operations require + * special permissions which the shell user has but the instrumentation does + * not. Running privileged operations by the shell user on behalf of an + * instrumentation is needed for running UiTestCases. + * + * {@hide} + */ +interface IUiAutomationConnection { + void connect(IAccessibilityServiceClient client); + void disconnect(); + boolean injectInputEvent(in InputEvent event, boolean sync); + boolean setRotation(int rotation); + Bitmap takeScreenshot(int width, int height); + void shutdown(); +} diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index e0856ae..e7bf305 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -27,6 +27,7 @@ import android.hardware.input.InputManager; import android.os.Bundle; import android.os.Debug; import android.os.IBinder; +import android.os.Looper; import android.os.MessageQueue; import android.os.PerformanceCollector; import android.os.Process; @@ -48,7 +49,6 @@ import java.io.File; import java.util.ArrayList; import java.util.List; - /** * Base class for implementing application instrumentation code. When running * with instrumentation turned on, this class will be instantiated for you @@ -58,6 +58,7 @@ import java.util.List; * <instrumentation> tag. */ public class Instrumentation { + /** * If included in the status or final bundle sent to an IInstrumentationWatcher, this key * identifies the class that is writing the report. This can be used to provide more structured @@ -72,7 +73,7 @@ public class Instrumentation { * instrumentation can also be launched, and results collected, by an automated system. */ public static final String REPORT_KEY_STREAMRESULT = "stream"; - + private static final String TAG = "Instrumentation"; private final Object mSync = new Object(); @@ -85,9 +86,11 @@ public class Instrumentation { private List<ActivityWaiter> mWaitingActivities; private List<ActivityMonitor> mActivityMonitors; private IInstrumentationWatcher mWatcher; + private IUiAutomationConnection mUiAutomationConnection; private boolean mAutomaticPerformanceSnapshots = false; private PerformanceCollector mPerformanceCollector; private Bundle mPerfMetrics = new Bundle(); + private UiAutomation mUiAutomation; public Instrumentation() { } @@ -1410,7 +1413,7 @@ public class Instrumentation { intent.setAllowFds(false); intent.migrateExtraStreamToClipData(); int result = ActivityManagerNative.getDefault() - .startActivity(whoThread, intent, + .startActivity(whoThread, who.getBasePackageName(), intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, target != null ? target.mEmbeddedID : null, requestCode, 0, null, null, options); @@ -1468,15 +1471,16 @@ public class Instrumentation { resolvedTypes[i] = intents[i].resolveTypeIfNeeded(who.getContentResolver()); } int result = ActivityManagerNative.getDefault() - .startActivities(whoThread, intents, resolvedTypes, token, options, - userId); + .startActivities(whoThread, who.getBasePackageName(), intents, resolvedTypes, + token, options, userId); checkStartActivityResult(result, intents[0]); } catch (RemoteException e) { } } /** - * Like {@link #execStartActivity(Context, IBinder, IBinder, Activity, Intent, int)}, + * Like {@link #execStartActivity(android.content.Context, android.os.IBinder, + * android.os.IBinder, Fragment, android.content.Intent, int, android.os.Bundle)}, * but for calls from a {#link Fragment}. * * @param who The Context from which the activity is being started. @@ -1525,7 +1529,7 @@ public class Instrumentation { intent.setAllowFds(false); intent.migrateExtraStreamToClipData(); int result = ActivityManagerNative.getDefault() - .startActivity(whoThread, intent, + .startActivity(whoThread, who.getBasePackageName(), intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, target != null ? target.mWho : null, requestCode, 0, null, null, options); @@ -1585,7 +1589,7 @@ public class Instrumentation { intent.setAllowFds(false); intent.migrateExtraStreamToClipData(); int result = ActivityManagerNative.getDefault() - .startActivityAsUser(whoThread, intent, + .startActivityAsUser(whoThread, who.getBasePackageName(), intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, target != null ? target.mEmbeddedID : null, requestCode, 0, null, null, options, user.getIdentifier()); @@ -1597,13 +1601,14 @@ public class Instrumentation { /*package*/ final void init(ActivityThread thread, Context instrContext, Context appContext, ComponentName component, - IInstrumentationWatcher watcher) { + IInstrumentationWatcher watcher, IUiAutomationConnection uiAutomationConnection) { mThread = thread; mMessageQueue = mThread.getLooper().myQueue(); mInstrContext = instrContext; mAppContext = appContext; mComponent = component; mWatcher = watcher; + mUiAutomationConnection = uiAutomationConnection; } /*package*/ static void checkStartActivityResult(int res, Object intent) { @@ -1637,18 +1642,48 @@ public class Instrumentation { } private final void validateNotAppThread() { - if (ActivityThread.currentActivityThread() != null) { + if (Looper.myLooper() == Looper.getMainLooper()) { throw new RuntimeException( "This method can not be called from the main application thread"); } } + /** + * Gets the {@link UiAutomation} instance. + * <p> + * <strong>Note:</strong> The APIs exposed via the returned {@link UiAutomation} + * work across application boundaries while the APIs exposed by the instrumentation + * do not. For example, {@link Instrumentation#sendPointerSync(MotionEvent)} will + * not allow you to inject the event in an app different from the instrumentation + * target, while {@link UiAutomation#injectInputEvent(android.view.InputEvent, boolean)} + * will work regardless of the current application. + * </p> + * <p> + * A typical test case should be using either the {@link UiAutomation} or + * {@link Instrumentation} APIs. Using both APIs at the same time is not + * a mistake by itself but a client has to be aware of the APIs limitations. + * </p> + * @return The UI automation instance. + * + * @see UiAutomation + */ + public UiAutomation getUiAutomation() { + if (mUiAutomationConnection != null) { + if (mUiAutomation == null) { + mUiAutomation = new UiAutomation(getTargetContext().getMainLooper(), + mUiAutomationConnection); + mUiAutomation.connect(); + } + return mUiAutomation; + } + return null; + } + private final class InstrumentationThread extends Thread { public InstrumentationThread(String name) { super(name); } public void run() { - IActivityManager am = ActivityManagerNative.getDefault(); try { Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_DISPLAY); } catch (RuntimeException e) { @@ -1659,9 +1694,13 @@ public class Instrumentation { startPerformanceSnapshot(); } onStart(); + if (mUiAutomation != null) { + mUiAutomation.disconnect(); + mUiAutomation = null; + } } } - + private static final class EmptyRunnable implements Runnable { public void run() { } diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java index 0a9ed58..2224490 100644 --- a/core/java/android/app/LoadedApk.java +++ b/core/java/android/app/LoadedApk.java @@ -362,7 +362,12 @@ public final class LoadedApk { try { pi = pm.getPackageInfo(mPackageName, 0, UserHandle.myUserId()); } catch (RemoteException e) { - throw new AssertionError(e); + throw new IllegalStateException("Unable to get package info for " + + mPackageName + "; is system dying?", e); + } + if (pi == null) { + throw new IllegalStateException("Unable to get package info for " + + mPackageName + "; is package not installed?"); } /* * Two possible indications that this package could be diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 3f8e16c..ebca041 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -25,15 +25,11 @@ import android.graphics.Bitmap; import android.media.AudioManager; import android.net.Uri; import android.os.Bundle; -import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; import android.os.UserHandle; import android.text.TextUtils; -import android.util.IntProperty; -import android.util.Log; -import android.util.Slog; import android.util.TypedValue; import android.view.View; import android.widget.ProgressBar; @@ -436,14 +432,23 @@ public class Notification implements Parcelable * @hide */ public static final String EXTRA_PEOPLE = "android.people"; - - private Bundle extras; + /** @hide */ + public static final String EXTRA_TITLE = "android.title"; + /** @hide */ + public static final String EXTRA_TEXT = "android.text"; + /** @hide */ + public static final String EXTRA_SUBTEXT = "android.subtext"; + /** @hide */ + public static final String EXTRA_SMALL_ICON = "android.icon"; + + /** @hide */ + public Bundle extras = new Bundle(); /** * Structure to encapsulate an "action", including title and icon, that can be attached to a Notification. * @hide */ - private static class Action implements Parcelable { + public static class Action implements Parcelable { public int icon; public CharSequence title; public PendingIntent actionIntent; @@ -495,7 +500,10 @@ public class Notification implements Parcelable }; } - private Action[] actions; + /** + * @hide + */ + public Action[] actions; /** * Constructs a Notification object with default values. @@ -589,11 +597,10 @@ public class Notification implements Parcelable kind = parcel.createStringArray(); // may set kind to null - if (parcel.readInt() != 0) { - extras = parcel.readBundle(); - } + extras = parcel.readBundle(); // may be null + + actions = parcel.createTypedArray(Action.CREATOR); // may be null - actions = parcel.createTypedArray(Action.CREATOR); if (parcel.readInt() != 0) { bigContentView = RemoteViews.CREATOR.createFromParcel(parcel); } @@ -602,7 +609,11 @@ public class Notification implements Parcelable @Override public Notification clone() { Notification that = new Notification(); + cloneInto(that); + return that; + } + private void cloneInto(Notification that) { that.when = this.when; that.icon = this.icon; that.number = this.number; @@ -656,15 +667,16 @@ public class Notification implements Parcelable } - that.actions = new Action[this.actions.length]; - for(int i=0; i<this.actions.length; i++) { - that.actions[i] = this.actions[i].clone(); + if (this.actions != null) { + that.actions = new Action[this.actions.length]; + for(int i=0; i<this.actions.length; i++) { + that.actions[i] = this.actions[i].clone(); + } } + if (this.bigContentView != null) { that.bigContentView = this.bigContentView.clone(); } - - return that; } public int describeContents() { @@ -745,14 +757,9 @@ public class Notification implements Parcelable parcel.writeStringArray(kind); // ok for null - if (extras != null) { - parcel.writeInt(1); - extras.writeToParcel(parcel, 0); - } else { - parcel.writeInt(0); - } + parcel.writeBundle(extras); // null ok - parcel.writeTypedArray(actions, 0); + parcel.writeTypedArray(actions, 0); // null ok if (bigContentView != null) { parcel.writeInt(1); @@ -800,35 +807,29 @@ public class Notification implements Parcelable @Deprecated public void setLatestEventInfo(Context context, CharSequence contentTitle, CharSequence contentText, PendingIntent contentIntent) { - // TODO: rewrite this to use Builder - RemoteViews contentView = new RemoteViews(context.getPackageName(), - R.layout.notification_template_base); - if (this.icon != 0) { - contentView.setImageViewResource(R.id.icon, this.icon); - } - if (priority < PRIORITY_LOW) { - contentView.setInt(R.id.icon, - "setBackgroundResource", R.drawable.notification_template_icon_low_bg); - contentView.setInt(R.id.status_bar_latest_event_content, - "setBackgroundResource", R.drawable.notification_bg_low); - } + Notification.Builder builder = new Notification.Builder(context); + + // First, ensure that key pieces of information that may have been set directly + // are preserved + builder.setWhen(this.when); + builder.setSmallIcon(this.icon); + builder.setPriority(this.priority); + builder.setTicker(this.tickerText); + builder.setNumber(this.number); + builder.mFlags = this.flags; + builder.setSound(this.sound, this.audioStreamType); + builder.setDefaults(this.defaults); + builder.setVibrate(this.vibrate); + + // now apply the latestEventInfo fields if (contentTitle != null) { - contentView.setTextViewText(R.id.title, contentTitle); + builder.setContentTitle(contentTitle); } if (contentText != null) { - contentView.setTextViewText(R.id.text, contentText); + builder.setContentText(contentText); } - if (this.when != 0) { - contentView.setViewVisibility(R.id.time, View.VISIBLE); - contentView.setLong(R.id.time, "setTime", when); - } - if (this.number != 0) { - NumberFormat f = NumberFormat.getIntegerInstance(); - contentView.setTextViewText(R.id.info, f.format(this.number)); - } - - this.contentView = contentView; - this.contentIntent = contentIntent; + builder.setContentIntent(contentIntent); + builder.buildInto(this); } @Override @@ -1615,11 +1616,20 @@ public class Notification implements Parcelable n.kind = null; } n.priority = mPriority; - n.extras = mExtras != null ? new Bundle(mExtras) : null; if (mActions.size() > 0) { n.actions = new Action[mActions.size()]; mActions.toArray(n.actions); } + + n.extras = mExtras != null ? new Bundle(mExtras) : new Bundle(); + + // Store original information used in the construction of this object + n.extras.putCharSequence(EXTRA_TITLE, mContentTitle); + n.extras.putCharSequence(EXTRA_TEXT, mContentText); + n.extras.putCharSequence(EXTRA_SUBTEXT, mSubText); + n.extras.putInt(EXTRA_SMALL_ICON, mSmallIcon); + //n.extras.putByteArray(EXTRA_LARGE_ICON, ... + return n; } @@ -1642,6 +1652,16 @@ public class Notification implements Parcelable return buildUnstyled(); } } + + /** + * Apply this Builder to an existing {@link Notification} object. + * + * @hide + */ + public Notification buildInto(Notification n) { + build().cloneInto(n); + return n; + } } @@ -1882,6 +1902,9 @@ public class Notification implements Parcelable checkBuilder(); Notification wip = mBuilder.buildUnstyled(); wip.bigContentView = makeBigContentView(); + + wip.extras.putCharSequence(EXTRA_TEXT, mBigText); + return wip; } } @@ -1981,6 +2004,14 @@ public class Notification implements Parcelable checkBuilder(); Notification wip = mBuilder.buildUnstyled(); wip.bigContentView = makeBigContentView(); + + StringBuilder builder = new StringBuilder(); + for (CharSequence str : mTexts) { + builder.append(str); + builder.append("\n"); + } + wip.extras.putCharSequence(EXTRA_TEXT, builder); + return wip; } } diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index 0acad75..5e69128 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -129,8 +129,8 @@ public class NotificationManager } if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")"); try { - service.enqueueNotificationWithTag(pkg, tag, id, notification, idOut, - UserHandle.myUserId()); + service.enqueueNotificationWithTag(pkg, mContext.getBasePackageName(), tag, id, + notification, idOut, UserHandle.myUserId()); if (id != idOut[0]) { Log.w(TAG, "notify: id corrupted: sent " + id + ", got back " + idOut[0]); } @@ -151,8 +151,8 @@ public class NotificationManager } if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")"); try { - service.enqueueNotificationWithTag(pkg, tag, id, notification, idOut, - user.getIdentifier()); + service.enqueueNotificationWithTag(pkg, mContext.getBasePackageName(), tag, id, + notification, idOut, user.getIdentifier()); if (id != idOut[0]) { Log.w(TAG, "notify: id corrupted: sent " + id + ", got back " + idOut[0]); } diff --git a/core/java/android/app/PendingIntent.java b/core/java/android/app/PendingIntent.java index 2897ee0..37804e9 100644 --- a/core/java/android/app/PendingIntent.java +++ b/core/java/android/app/PendingIntent.java @@ -41,7 +41,7 @@ import android.util.AndroidException; * you are granting it the right to perform the operation you have specified * as if the other application was yourself (with the same permissions and * identity). As such, you should be careful about how you build the PendingIntent: - * often, for example, the base Intent you supply will have the component + * almost always, for example, the base Intent you supply should have the component * name explicitly set to one of your own components, to ensure it is ultimately * sent there and nowhere else. * @@ -200,6 +200,11 @@ public final class PendingIntent implements Parcelable { * existing activity, so you must use the {@link Intent#FLAG_ACTIVITY_NEW_TASK * Intent.FLAG_ACTIVITY_NEW_TASK} launch flag in the Intent. * + * <p class="note">For security reasons, the {@link android.content.Intent} + * you supply here should almost always be an <em>explicit intent</em>, + * that is specify an explicit component to be delivered to through + * {@link Intent#setClass(android.content.Context, Class)} Intent.setClass</p> + * * @param context The Context in which this PendingIntent should start * the activity. * @param requestCode Private request code for the sender (currently @@ -227,6 +232,11 @@ public final class PendingIntent implements Parcelable { * existing activity, so you must use the {@link Intent#FLAG_ACTIVITY_NEW_TASK * Intent.FLAG_ACTIVITY_NEW_TASK} launch flag in the Intent. * + * <p class="note">For security reasons, the {@link android.content.Intent} + * you supply here should almost always be an <em>explicit intent</em>, + * that is specify an explicit component to be delivered to through + * {@link Intent#setClass(android.content.Context, Class)} Intent.setClass</p> + * * @param context The Context in which this PendingIntent should start * the activity. * @param requestCode Private request code for the sender (currently @@ -313,6 +323,11 @@ public final class PendingIntent implements Parcelable { * UI the user actually sees when the intents are started. * </p> * + * <p class="note">For security reasons, the {@link android.content.Intent} objects + * you supply here should almost always be <em>explicit intents</em>, + * that is specify an explicit component to be delivered to through + * {@link Intent#setClass(android.content.Context, Class)} Intent.setClass</p> + * * @param context The Context in which this PendingIntent should start * the activity. * @param requestCode Private request code for the sender (currently @@ -359,6 +374,11 @@ public final class PendingIntent implements Parcelable { * UI the user actually sees when the intents are started. * </p> * + * <p class="note">For security reasons, the {@link android.content.Intent} objects + * you supply here should almost always be <em>explicit intents</em>, + * that is specify an explicit component to be delivered to through + * {@link Intent#setClass(android.content.Context, Class)} Intent.setClass</p> + * * @param context The Context in which this PendingIntent should start * the activity. * @param requestCode Private request code for the sender (currently @@ -423,6 +443,11 @@ public final class PendingIntent implements Parcelable { * Retrieve a PendingIntent that will perform a broadcast, like calling * {@link Context#sendBroadcast(Intent) Context.sendBroadcast()}. * + * <p class="note">For security reasons, the {@link android.content.Intent} + * you supply here should almost always be an <em>explicit intent</em>, + * that is specify an explicit component to be delivered to through + * {@link Intent#setClass(android.content.Context, Class)} Intent.setClass</p> + * * @param context The Context in which this PendingIntent should perform * the broadcast. * @param requestCode Private request code for the sender (currently @@ -473,6 +498,11 @@ public final class PendingIntent implements Parcelable { * {@link Context#startService Context.startService()}. The start * arguments given to the service will come from the extras of the Intent. * + * <p class="note">For security reasons, the {@link android.content.Intent} + * you supply here should almost always be an <em>explicit intent</em>, + * that is specify an explicit component to be delivered to through + * {@link Intent#setClass(android.content.Context, Class)} Intent.setClass</p> + * * @param context The Context in which this PendingIntent should start * the service. * @param requestCode Private request code for the sender (currently @@ -707,6 +737,15 @@ public final class PendingIntent implements Parcelable { * sending the Intent. The returned string is supplied by the system, so * that an application can not spoof its package. * + * <p class="note">Be careful about how you use this. All this tells you is + * who created the PendingIntent. It does <strong>not</strong> tell you who + * handed the PendingIntent to you: that is, PendingIntent objects are intended to be + * passed between applications, so the PendingIntent you receive from an application + * could actually be one it received from another application, meaning the result + * you get here will identify the original application. Because of this, you should + * only use this information to identify who you expect to be interacting with + * through a {@link #send} call, not who gave you the PendingIntent.</p> + * * @return The package name of the PendingIntent, or null if there is * none associated with it. */ @@ -726,6 +765,15 @@ public final class PendingIntent implements Parcelable { * sending the Intent. The returned integer is supplied by the system, so * that an application can not spoof its uid. * + * <p class="note">Be careful about how you use this. All this tells you is + * who created the PendingIntent. It does <strong>not</strong> tell you who + * handed the PendingIntent to you: that is, PendingIntent objects are intended to be + * passed between applications, so the PendingIntent you receive from an application + * could actually be one it received from another application, meaning the result + * you get here will identify the original application. Because of this, you should + * only use this information to identify who you expect to be interacting with + * through a {@link #send} call, not who gave you the PendingIntent.</p> + * * @return The uid of the PendingIntent, or -1 if there is * none associated with it. */ @@ -747,6 +795,15 @@ public final class PendingIntent implements Parcelable { * {@link android.os.Process#myUserHandle() Process.myUserHandle()} for * more explanation of user handles. * + * <p class="note">Be careful about how you use this. All this tells you is + * who created the PendingIntent. It does <strong>not</strong> tell you who + * handed the PendingIntent to you: that is, PendingIntent objects are intended to be + * passed between applications, so the PendingIntent you receive from an application + * could actually be one it received from another application, meaning the result + * you get here will identify the original application. Because of this, you should + * only use this information to identify who you expect to be interacting with + * through a {@link #send} call, not who gave you the PendingIntent.</p> + * * @return The user handle of the PendingIntent, or null if there is * none associated with it. */ diff --git a/core/java/android/app/SearchManager.java b/core/java/android/app/SearchManager.java index 6382cee..7dfc589 100644 --- a/core/java/android/app/SearchManager.java +++ b/core/java/android/app/SearchManager.java @@ -846,8 +846,8 @@ public class SearchManager * * @hide */ - public Intent getAssistIntent(Context context) { - return getAssistIntent(context, UserHandle.myUserId()); + public Intent getAssistIntent(Context context, boolean inclContext) { + return getAssistIntent(context, inclContext, UserHandle.myUserId()); } /** @@ -856,7 +856,7 @@ public class SearchManager * * @hide */ - public Intent getAssistIntent(Context context, int userHandle) { + public Intent getAssistIntent(Context context, boolean inclContext, int userHandle) { try { if (mService == null) { return null; @@ -867,6 +867,13 @@ public class SearchManager } Intent intent = new Intent(Intent.ACTION_ASSIST); intent.setComponent(comp); + if (inclContext) { + IActivityManager am = ActivityManagerNative.getDefault(); + Bundle extras = am.getTopActivityExtras(0); + if (extras != null) { + intent.replaceExtras(extras); + } + } return intent; } catch (RemoteException re) { Log.e(TAG, "getAssistIntent() failed: " + re); diff --git a/core/java/android/app/UiAutomation.java b/core/java/android/app/UiAutomation.java new file mode 100644 index 0000000..7d02342 --- /dev/null +++ b/core/java/android/app/UiAutomation.java @@ -0,0 +1,699 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import android.accessibilityservice.AccessibilityService.Callbacks; +import android.accessibilityservice.AccessibilityService.IAccessibilityServiceClientWrapper; +import android.accessibilityservice.AccessibilityServiceInfo; +import android.accessibilityservice.IAccessibilityServiceClient; +import android.accessibilityservice.IAccessibilityServiceConnection; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Point; +import android.hardware.display.DisplayManagerGlobal; +import android.os.Looper; +import android.os.RemoteException; +import android.os.SystemClock; +import android.util.Log; +import android.view.Display; +import android.view.InputEvent; +import android.view.Surface; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityInteractionClient; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.IAccessibilityInteractionConnection; + +import java.util.ArrayList; +import java.util.concurrent.TimeoutException; + +/** + * Class for interacting with the device's UI by simulation user actions and + * introspection of the screen content. It relies on the platform accessibility + * APIs to introspect the screen and to perform some actions on the remote view + * tree. It also allows injecting of arbitrary raw input events simulating user + * interaction with keyboards and touch devices. One can think of a UiAutomation + * as a special type of {@link android.accessibilityservice.AccessibilityService} + * which does not provide hooks for the service life cycle and exposes other + * APIs that are useful for UI test automation. + * <p> + * The APIs exposed by this class are low-level to maximize flexibility when + * developing UI test automation tools and libraries. Generally, a UiAutomation + * client should be using a higher-level library or implement high-level functions. + * For example, performing a tap on the screen requires construction and injecting + * of a touch down and up events which have to be delivered to the system by a + * call to {@link #injectInputEvent(InputEvent, boolean)}. + * </p> + * <p> + * The APIs exposed by this class operate across applications enabling a client + * to write tests that cover use cases spanning over multiple applications. For + * example, going to the settings application to change a setting and then + * interacting with another application whose behavior depends on that setting. + * </p> + */ +public final class UiAutomation { + + private static final String LOG_TAG = UiAutomation.class.getSimpleName(); + + private static final boolean DEBUG = false; + + private static final int CONNECTION_ID_UNDEFINED = -1; + + private static final long CONNECT_TIMEOUT_MILLIS = 5000; + + /** Rotation constant: Unfreeze rotation (rotating the device changes its rotation state). */ + public static final int ROTATION_UNFREEZE = -2; + + /** Rotation constant: Freeze rotation to its current state. */ + public static final int ROTATION_FREEZE_CURRENT = -1; + + /** Rotation constant: Freeze rotation to 0 degrees (natural orientation) */ + public static final int ROTATION_FREEZE_0 = Surface.ROTATION_0; + + /** Rotation constant: Freeze rotation to 90 degrees . */ + public static final int ROTATION_FREEZE_90 = Surface.ROTATION_90; + + /** Rotation constant: Freeze rotation to 180 degrees . */ + public static final int ROTATION_FREEZE_180 = Surface.ROTATION_180; + + /** Rotation constant: Freeze rotation to 270 degrees . */ + public static final int ROTATION_FREEZE_270 = Surface.ROTATION_270; + + private final Object mLock = new Object(); + + private final ArrayList<AccessibilityEvent> mEventQueue = new ArrayList<AccessibilityEvent>(); + + private final IAccessibilityServiceClient mClient; + + private final IUiAutomationConnection mUiAutomationConnection; + + private int mConnectionId = CONNECTION_ID_UNDEFINED; + + private OnAccessibilityEventListener mOnAccessibilityEventListener; + + private boolean mWaitingForEventDelivery; + + private long mLastEventTimeMillis; + + private boolean mIsConnecting; + + /** + * Listener for observing the {@link AccessibilityEvent} stream. + */ + public static interface OnAccessibilityEventListener { + + /** + * Callback for receiving an {@link AccessibilityEvent}. + * <p> + * <strong>Note:</strong> This method is <strong>NOT</strong> executed + * on the main test thread. The client is responsible for proper + * synchronization. + * </p> + * <p> + * <strong>Note:</strong> It is responsibility of the client + * to recycle the received events to minimize object creation. + * </p> + * + * @param event The received event. + */ + public void onAccessibilityEvent(AccessibilityEvent event); + } + + /** + * Listener for filtering accessibility events. + */ + public static interface AccessibilityEventFilter { + + /** + * Callback for determining whether an event is accepted or + * it is filtered out. + * + * @param event The event to process. + * @return True if the event is accepted, false to filter it out. + */ + public boolean accept(AccessibilityEvent event); + } + + /** + * Creates a new instance that will handle callbacks from the accessibility + * layer on the thread of the provided looper and perform requests for privileged + * operations on the provided connection. + * + * @param looper The looper on which to execute accessibility callbacks. + * @param connection The connection for performing privileged operations. + * + * @hide + */ + public UiAutomation(Looper looper, IUiAutomationConnection connection) { + if (looper == null) { + throw new IllegalArgumentException("Looper cannot be null!"); + } + if (connection == null) { + throw new IllegalArgumentException("Connection cannot be null!"); + } + mUiAutomationConnection = connection; + mClient = new IAccessibilityServiceClientImpl(looper); + } + + /** + * Connects this UiAutomation to the accessibility introspection APIs. + * + * @hide + */ + public void connect() { + synchronized (mLock) { + throwIfConnectedLocked(); + if (mIsConnecting) { + return; + } + mIsConnecting = true; + } + + try { + // Calling out without a lock held. + mUiAutomationConnection.connect(mClient); + } catch (RemoteException re) { + throw new RuntimeException("Error while connecting UiAutomation", re); + } + + synchronized (mLock) { + final long startTimeMillis = SystemClock.uptimeMillis(); + try { + while (true) { + if (isConnectedLocked()) { + break; + } + final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; + final long remainingTimeMillis = CONNECT_TIMEOUT_MILLIS - elapsedTimeMillis; + if (remainingTimeMillis <= 0) { + throw new RuntimeException("Error while connecting UiAutomation"); + } + try { + mLock.wait(remainingTimeMillis); + } catch (InterruptedException ie) { + /* ignore */ + } + } + } finally { + mIsConnecting = false; + } + } + } + + /** + * Disconnects this UiAutomation from the accessibility introspection APIs. + * + * @hide + */ + public void disconnect() { + synchronized (mLock) { + if (mIsConnecting) { + throw new IllegalStateException( + "Cannot call disconnect() while connecting!"); + } + throwIfNotConnectedLocked(); + mConnectionId = CONNECTION_ID_UNDEFINED; + } + try { + // Calling out without a lock held. + mUiAutomationConnection.disconnect(); + } catch (RemoteException re) { + throw new RuntimeException("Error while disconnecting UiAutomation", re); + } + } + + /** + * The id of the {@link IAccessibilityInteractionConnection} for querying + * the screen content. This is here for legacy purposes since some tools use + * hidden APIs to introspect the screen. + * + * @hide + */ + public int getConnectionId() { + synchronized (mLock) { + throwIfNotConnectedLocked(); + return mConnectionId; + } + } + + /** + * Sets a callback for observing the stream of {@link AccessibilityEvent}s. + * + * @param listener The callback. + */ + public void setOnAccessibilityEventListener(OnAccessibilityEventListener listener) { + synchronized (mLock) { + mOnAccessibilityEventListener = listener; + } + } + + /** + * Performs a global action. Such an action can be performed at any moment + * regardless of the current application or user location in that application. + * For example going back, going home, opening recents, etc. + * + * @param action The action to perform. + * @return Whether the action was successfully performed. + * + * @see AccessibilityService#GLOBAL_ACTION_BACK + * @see AccessibilityService#GLOBAL_ACTION_HOME + * @see AccessibilityService#GLOBAL_ACTION_NOTIFICATIONS + * @see AccessibilityService#GLOBAL_ACTION_RECENTS + */ + public final boolean performGlobalAction(int action) { + final IAccessibilityServiceConnection connection; + synchronized (mLock) { + throwIfNotConnectedLocked(); + connection = AccessibilityInteractionClient.getInstance() + .getConnection(mConnectionId); + } + // Calling out without a lock held. + if (connection != null) { + try { + return connection.performGlobalAction(action); + } catch (RemoteException re) { + Log.w(LOG_TAG, "Error while calling performGlobalAction", re); + } + } + return false; + } + + /** + * Gets the an {@link AccessibilityServiceInfo} describing this UiAutomation. + * This method is useful if one wants to change some of the dynamically + * configurable properties at runtime. + * + * @return The accessibility service info. + * + * @see AccessibilityServiceInfo + */ + public final AccessibilityServiceInfo getServiceInfo() { + final IAccessibilityServiceConnection connection; + synchronized (mLock) { + throwIfNotConnectedLocked(); + connection = AccessibilityInteractionClient.getInstance() + .getConnection(mConnectionId); + } + // Calling out without a lock held. + if (connection != null) { + try { + return connection.getServiceInfo(); + } catch (RemoteException re) { + Log.w(LOG_TAG, "Error while getting AccessibilityServiceInfo", re); + } + } + return null; + } + + /** + * Sets the {@link AccessibilityServiceInfo} that describes how this + * UiAutomation will be handled by the platform accessibility layer. + * + * @param info The info. + * + * @see AccessibilityServiceInfo + */ + public final void setServiceInfo(AccessibilityServiceInfo info) { + final IAccessibilityServiceConnection connection; + synchronized (mLock) { + throwIfNotConnectedLocked(); + AccessibilityInteractionClient.getInstance().clearCache(); + connection = AccessibilityInteractionClient.getInstance() + .getConnection(mConnectionId); + } + // Calling out without a lock held. + if (connection != null) { + try { + connection.setServiceInfo(info); + } catch (RemoteException re) { + Log.w(LOG_TAG, "Error while setting AccessibilityServiceInfo", re); + } + } + } + + /** + * Gets the root {@link AccessibilityNodeInfo} in the active window. + * + * @return The root info. + */ + public AccessibilityNodeInfo getRootInActiveWindow() { + final int connectionId; + synchronized (mLock) { + throwIfNotConnectedLocked(); + connectionId = mConnectionId; + } + // Calling out without a lock held. + return AccessibilityInteractionClient.getInstance() + .getRootInActiveWindow(connectionId); + } + + /** + * A method for injecting an arbitrary input event. + * <p> + * <strong>Note:</strong> It is caller's responsibility to recycle the event. + * </p> + * @param event The event to inject. + * @param sync Whether to inject the event synchronously. + * @return Whether event injection succeeded. + */ + public boolean injectInputEvent(InputEvent event, boolean sync) { + synchronized (mLock) { + throwIfNotConnectedLocked(); + } + try { + if (DEBUG) { + Log.i(LOG_TAG, "Injecting: " + event + " sync: " + sync); + } + // Calling out without a lock held. + return mUiAutomationConnection.injectInputEvent(event, sync); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while injecting input event!", re); + } + return false; + } + + /** + * Sets the device rotation. A client can freeze the rotation in + * desired state or freeze the rotation to its current state or + * unfreeze the rotation (rotating the device changes its rotation + * state). + * + * @param rotation The desired rotation. + * @return Whether the rotation was set successfully. + * + * @see #ROTATION_FREEZE_0 + * @see #ROTATION_FREEZE_90 + * @see #ROTATION_FREEZE_180 + * @see #ROTATION_FREEZE_270 + * @see #ROTATION_FREEZE_CURRENT + * @see #ROTATION_UNFREEZE + */ + public boolean setRotation(int rotation) { + synchronized (mLock) { + throwIfNotConnectedLocked(); + } + switch (rotation) { + case ROTATION_FREEZE_0: + case ROTATION_FREEZE_90: + case ROTATION_FREEZE_180: + case ROTATION_FREEZE_270: + case ROTATION_UNFREEZE: + case ROTATION_FREEZE_CURRENT: { + try { + // Calling out without a lock held. + mUiAutomationConnection.setRotation(rotation); + return true; + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while setting rotation!", re); + } + } return false; + default: { + throw new IllegalArgumentException("Invalid rotation."); + } + } + } + + /** + * Executes a command and waits for a specific accessibility event up to a + * given wait timeout. To detect a sequence of events one can implement a + * filter that keeps track of seen events of the expected sequence and + * returns true after the last event of that sequence is received. + * <p> + * <strong>Note:</strong> It is caller's responsibility to recycle the returned event. + * </p> + * @param command The command to execute. + * @param filter Filter that recognizes the expected event. + * @param timeoutMillis The wait timeout in milliseconds. + * + * @throws TimeoutException If the expected event is not received within the timeout. + */ + public AccessibilityEvent executeAndWaitForEvent(Runnable command, + AccessibilityEventFilter filter, long timeoutMillis) throws TimeoutException { + synchronized (mLock) { + throwIfNotConnectedLocked(); + + mEventQueue.clear(); + // Prepare to wait for an event. + mWaitingForEventDelivery = true; + + // We will ignore events from previous interactions. + final long executionStartTimeMillis = SystemClock.uptimeMillis(); + + // Execute the command. + command.run(); + try { + // Wait for the event. + final long startTimeMillis = SystemClock.uptimeMillis(); + while (true) { + // Drain the event queue + while (!mEventQueue.isEmpty()) { + AccessibilityEvent event = mEventQueue.remove(0); + // Ignore events from previous interactions. + if (event.getEventTime() <= executionStartTimeMillis) { + continue; + } + if (filter.accept(event)) { + return event; + } + event.recycle(); + } + // Check if timed out and if not wait. + final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; + final long remainingTimeMillis = timeoutMillis - elapsedTimeMillis; + if (remainingTimeMillis <= 0) { + throw new TimeoutException("Expected event not received within: " + + timeoutMillis + " ms."); + } + try { + mLock.wait(remainingTimeMillis); + } catch (InterruptedException ie) { + /* ignore */ + } + } + } finally { + mWaitingForEventDelivery = false; + mEventQueue.clear(); + mLock.notifyAll(); + } + } + } + + /** + * Waits for the accessibility event stream to become idle, which is not to + * have received an accessibility event within <code>idleTimeoutMillis</code>. + * The total time spent to wait for an idle accessibility event stream is bounded + * by the <code>globalTimeoutMillis</code>. + * + * @param idleTimeoutMillis The timeout in milliseconds between two events + * to consider the device idle. + * @param globalTimeoutMillis The maximal global timeout in milliseconds in + * which to wait for an idle state. + * + * @throws TimeoutException If no idle state was detected within + * <code>globalTimeoutMillis.</code> + */ + public void waitForIdle(long idleTimeoutMillis, long globalTimeoutMillis) + throws TimeoutException { + synchronized (mLock) { + throwIfNotConnectedLocked(); + + final long startTimeMillis = SystemClock.uptimeMillis(); + if (mLastEventTimeMillis <= 0) { + mLastEventTimeMillis = startTimeMillis; + } + + while (true) { + final long currentTimeMillis = SystemClock.uptimeMillis(); + // Did we get idle state within the global timeout? + final long elapsedGlobalTimeMillis = currentTimeMillis - startTimeMillis; + final long remainingGlobalTimeMillis = + globalTimeoutMillis - elapsedGlobalTimeMillis; + if (remainingGlobalTimeMillis <= 0) { + throw new TimeoutException("No idle state with idle timeout: " + + idleTimeoutMillis + " within global timeout: " + + globalTimeoutMillis); + } + // Did we get an idle state within the idle timeout? + final long elapsedIdleTimeMillis = currentTimeMillis - mLastEventTimeMillis; + final long remainingIdleTimeMillis = idleTimeoutMillis - elapsedIdleTimeMillis; + if (remainingIdleTimeMillis <= 0) { + return; + } + try { + mLock.wait(remainingIdleTimeMillis); + } catch (InterruptedException ie) { + /* ignore */ + } + } + } + } + + /** + * Takes a screenshot. + * + * @return The screenshot bitmap on success, null otherwise. + */ + public Bitmap takeScreenshot() { + synchronized (mLock) { + throwIfNotConnectedLocked(); + } + Display display = DisplayManagerGlobal.getInstance() + .getRealDisplay(Display.DEFAULT_DISPLAY); + Point displaySize = new Point(); + display.getRealSize(displaySize); + final int displayWidth = displaySize.x; + final int displayHeight = displaySize.y; + + final float screenshotWidth; + final float screenshotHeight; + + final int rotation = display.getRotation(); + switch (rotation) { + case ROTATION_FREEZE_0: { + screenshotWidth = displayWidth; + screenshotHeight = displayHeight; + } break; + case ROTATION_FREEZE_90: { + screenshotWidth = displayHeight; + screenshotHeight = displayWidth; + } break; + case ROTATION_FREEZE_180: { + screenshotWidth = displayWidth; + screenshotHeight = displayHeight; + } break; + case ROTATION_FREEZE_270: { + screenshotWidth = displayHeight; + screenshotHeight = displayWidth; + } break; + default: { + throw new IllegalArgumentException("Invalid rotation: " + + rotation); + } + } + + // Take the screenshot + Bitmap screenShot = null; + try { + // Calling out without a lock held. + screenShot = mUiAutomationConnection.takeScreenshot((int) screenshotWidth, + (int) screenshotHeight); + if (screenShot == null) { + return null; + } + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while taking screnshot!", re); + return null; + } + + // Rotate the screenshot to the current orientation + if (rotation != ROTATION_FREEZE_0) { + Bitmap unrotatedScreenShot = Bitmap.createBitmap(displayWidth, displayHeight, + Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(unrotatedScreenShot); + canvas.translate(unrotatedScreenShot.getWidth() / 2, + unrotatedScreenShot.getHeight() / 2); + canvas.rotate(getDegreesForRotation(rotation)); + canvas.translate(- screenshotWidth / 2, - screenshotHeight / 2); + canvas.drawBitmap(screenShot, 0, 0, null); + canvas.setBitmap(null); + screenShot = unrotatedScreenShot; + } + + // Optimization + screenShot.setHasAlpha(false); + + return screenShot; + } + + private static float getDegreesForRotation(int value) { + switch (value) { + case Surface.ROTATION_90: { + return 360f - 90f; + } + case Surface.ROTATION_180: { + return 360f - 180f; + } + case Surface.ROTATION_270: { + return 360f - 270f; + } default: { + return 0; + } + } + } + + private boolean isConnectedLocked() { + return mConnectionId != CONNECTION_ID_UNDEFINED; + } + + private void throwIfConnectedLocked() { + if (mConnectionId != CONNECTION_ID_UNDEFINED) { + throw new IllegalStateException("UiAutomation not connected!"); + } + } + + private void throwIfNotConnectedLocked() { + if (!isConnectedLocked()) { + throw new IllegalStateException("UiAutomation not connected!"); + } + } + + private class IAccessibilityServiceClientImpl extends IAccessibilityServiceClientWrapper { + + public IAccessibilityServiceClientImpl(Looper looper) { + super(null, looper, new Callbacks() { + @Override + public void onSetConnectionId(int connectionId) { + synchronized (mLock) { + mConnectionId = connectionId; + mLock.notifyAll(); + } + } + + @Override + public void onServiceConnected() { + /* do nothing */ + } + + @Override + public void onInterrupt() { + /* do nothing */ + } + + @Override + public boolean onGesture(int gestureId) { + /* do nothing */ + return false; + } + + @Override + public void onAccessibilityEvent(AccessibilityEvent event) { + synchronized (mLock) { + mLastEventTimeMillis = event.getEventTime(); + if (mWaitingForEventDelivery) { + mEventQueue.add(AccessibilityEvent.obtain(event)); + } + mLock.notifyAll(); + } + // Calling out only without a lock held. + final OnAccessibilityEventListener listener = mOnAccessibilityEventListener; + if (listener != null) { + listener.onAccessibilityEvent(AccessibilityEvent.obtain(event)); + } + } + }); + } + } +} diff --git a/core/java/android/app/UiAutomationConnection.java b/core/java/android/app/UiAutomationConnection.java new file mode 100644 index 0000000..06ef472 --- /dev/null +++ b/core/java/android/app/UiAutomationConnection.java @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.accessibilityservice.IAccessibilityServiceClient; +import android.content.Context; +import android.graphics.Bitmap; +import android.hardware.input.InputManager; +import android.os.Binder; +import android.os.Process; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.view.IWindowManager; +import android.view.InputEvent; +import android.view.SurfaceControl; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.IAccessibilityManager; + +/** + * This is a remote object that is passed from the shell to an instrumentation + * for enabling access to privileged operations which the shell can do and the + * instrumentation cannot. These privileged operations are needed for implementing + * a {@link UiAutomation} that enables across application testing by simulating + * user actions and performing screen introspection. + * + * @hide + */ +public final class UiAutomationConnection extends IUiAutomationConnection.Stub { + + private static final int INITIAL_FROZEN_ROTATION_UNSPECIFIED = -1; + + private final IWindowManager mWindowManager = IWindowManager.Stub.asInterface( + ServiceManager.getService(Service.WINDOW_SERVICE)); + + private final Object mLock = new Object(); + + private int mInitialFrozenRotation = INITIAL_FROZEN_ROTATION_UNSPECIFIED; + + private IAccessibilityServiceClient mClient; + + private boolean mIsShutdown; + + private int mOwningUid; + + public void connect(IAccessibilityServiceClient client) { + if (client == null) { + throw new IllegalArgumentException("Client cannot be null!"); + } + synchronized (mLock) { + throwIfShutdownLocked(); + if (isConnectedLocked()) { + throw new IllegalStateException("Already connected."); + } + mOwningUid = Binder.getCallingUid(); + registerUiTestAutomationServiceLocked(client); + storeRotationStateLocked(); + } + } + + @Override + public void disconnect() { + synchronized (mLock) { + throwIfCalledByNotTrustedUidLocked(); + throwIfShutdownLocked(); + if (!isConnectedLocked()) { + throw new IllegalStateException("Already disconnected."); + } + mOwningUid = -1; + unregisterUiTestAutomationServiceLocked(); + restoreRotationStateLocked(); + } + } + + @Override + public boolean injectInputEvent(InputEvent event, boolean sync) { + synchronized (mLock) { + throwIfCalledByNotTrustedUidLocked(); + throwIfShutdownLocked(); + throwIfNotConnectedLocked(); + } + final int mode = (sync) ? InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH + : InputManager.INJECT_INPUT_EVENT_MODE_ASYNC; + final long identity = Binder.clearCallingIdentity(); + try { + return InputManager.getInstance().injectInputEvent(event, mode); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public boolean setRotation(int rotation) { + synchronized (mLock) { + throwIfCalledByNotTrustedUidLocked(); + throwIfShutdownLocked(); + throwIfNotConnectedLocked(); + } + final long identity = Binder.clearCallingIdentity(); + try { + if (rotation == UiAutomation.ROTATION_UNFREEZE) { + mWindowManager.thawRotation(); + } else { + mWindowManager.freezeRotation(rotation); + } + return true; + } catch (RemoteException re) { + /* ignore */ + } finally { + Binder.restoreCallingIdentity(identity); + } + return false; + } + + @Override + public Bitmap takeScreenshot(int width, int height) { + synchronized (mLock) { + throwIfCalledByNotTrustedUidLocked(); + throwIfShutdownLocked(); + throwIfNotConnectedLocked(); + } + final long identity = Binder.clearCallingIdentity(); + try { + return SurfaceControl.screenshot(width, height); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void shutdown() { + synchronized (mLock) { + throwIfCalledByNotTrustedUidLocked(); + throwIfShutdownLocked(); + mIsShutdown = true; + if (isConnectedLocked()) { + disconnect(); + } + } + } + + private void registerUiTestAutomationServiceLocked(IAccessibilityServiceClient client) { + IAccessibilityManager manager = IAccessibilityManager.Stub.asInterface( + ServiceManager.getService(Context.ACCESSIBILITY_SERVICE)); + AccessibilityServiceInfo info = new AccessibilityServiceInfo(); + info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK; + info.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC; + info.flags |= AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS + | AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS; + try { + // Calling out with a lock held is fine since if the system + // process is gone the client calling in will be killed. + manager.registerUiTestAutomationService(client, info); + mClient = client; + } catch (RemoteException re) { + throw new IllegalStateException("Error while registering UiTestAutomationService.", re); + } + } + + private void unregisterUiTestAutomationServiceLocked() { + IAccessibilityManager manager = IAccessibilityManager.Stub.asInterface( + ServiceManager.getService(Context.ACCESSIBILITY_SERVICE)); + try { + // Calling out with a lock held is fine since if the system + // process is gone the client calling in will be killed. + manager.unregisterUiTestAutomationService(mClient); + mClient = null; + } catch (RemoteException re) { + throw new IllegalStateException("Error while unregistering UiTestAutomationService", + re); + } + } + + private void storeRotationStateLocked() { + try { + if (mWindowManager.isRotationFrozen()) { + // Calling out with a lock held is fine since if the system + // process is gone the client calling in will be killed. + mInitialFrozenRotation = mWindowManager.getRotation(); + } + } catch (RemoteException re) { + /* ignore */ + } + } + + private void restoreRotationStateLocked() { + try { + if (mInitialFrozenRotation != INITIAL_FROZEN_ROTATION_UNSPECIFIED) { + // Calling out with a lock held is fine since if the system + // process is gone the client calling in will be killed. + mWindowManager.freezeRotation(mInitialFrozenRotation); + } else { + // Calling out with a lock held is fine since if the system + // process is gone the client calling in will be killed. + mWindowManager.thawRotation(); + } + } catch (RemoteException re) { + /* ignore */ + } + } + + private boolean isConnectedLocked() { + return mClient != null; + } + + private void throwIfShutdownLocked() { + if (mIsShutdown) { + throw new IllegalStateException("Connection shutdown!"); + } + } + + private void throwIfNotConnectedLocked() { + if (!isConnectedLocked()) { + throw new IllegalStateException("Not connected!"); + } + } + + private void throwIfCalledByNotTrustedUidLocked() { + final int callingUid = Binder.getCallingUid(); + if (callingUid != mOwningUid && mOwningUid != Process.SYSTEM_UID + && callingUid != 0 /*root*/) { + throw new SecurityException("Calling from not trusted UID!"); + } + } +} diff --git a/core/java/android/app/backup/BackupAgent.java b/core/java/android/app/backup/BackupAgent.java index 9ad33a5..44aa06f 100644 --- a/core/java/android/app/backup/BackupAgent.java +++ b/core/java/android/app/backup/BackupAgent.java @@ -24,6 +24,7 @@ import android.content.pm.ApplicationInfo; import android.os.Binder; import android.os.IBinder; import android.os.ParcelFileDescriptor; +import android.os.Process; import android.os.RemoteException; import android.util.Log; @@ -254,6 +255,21 @@ public abstract class BackupAgent extends ContextWrapper { filterSet.add(databaseDir); filterSet.remove(sharedPrefsDir); fullBackupFileTree(packageName, FullBackup.SHAREDPREFS_TREE_TOKEN, sharedPrefsDir, filterSet, data); + + // getExternalFilesDir() location associated with this app. Technically there should + // not be any files here if the app does not properly have permission to access + // external storage, but edge cases happen. fullBackupFileTree() catches + // IOExceptions and similar, and treats them as non-fatal, so we rely on that; and + // we know a priori that processes running as the system UID are not permitted to + // access external storage, so we check for that as well to avoid nastygrams in + // the log. + if (Process.myUid() != Process.SYSTEM_UID) { + File efLocation = getExternalFilesDir(null); + if (efLocation != null) { + fullBackupFileTree(packageName, FullBackup.MANAGED_EXTERNAL_TREE_TOKEN, + efLocation.getCanonicalPath(), null, data); + } + } } /** @@ -274,6 +290,7 @@ public abstract class BackupAgent extends ContextWrapper { String spDir; String cacheDir; String libDir; + String efDir = null; String filePath; ApplicationInfo appInfo = getApplicationInfo(); @@ -288,6 +305,14 @@ public abstract class BackupAgent extends ContextWrapper { ? null : new File(appInfo.nativeLibraryDir).getCanonicalPath(); + // may or may not have external files access to attempt backup/restore there + if (Process.myUid() != Process.SYSTEM_UID) { + File efLocation = getExternalFilesDir(null); + if (efLocation != null) { + efDir = efLocation.getCanonicalPath(); + } + } + // Now figure out which well-defined tree the file is placed in, working from // most to least specific. We also specifically exclude the lib and cache dirs. filePath = file.getCanonicalPath(); @@ -315,6 +340,9 @@ public abstract class BackupAgent extends ContextWrapper { } else if (filePath.startsWith(mainDir)) { domain = FullBackup.ROOT_TREE_TOKEN; rootpath = mainDir; + } else if ((efDir != null) && filePath.startsWith(efDir)) { + domain = FullBackup.MANAGED_EXTERNAL_TREE_TOKEN; + rootpath = efDir; } else { Log.w(TAG, "File " + filePath + " is in an unsupported location; skipping"); return; @@ -438,6 +466,14 @@ public abstract class BackupAgent extends ContextWrapper { basePath = getSharedPrefsFile("foo").getParentFile().getCanonicalPath(); } else if (domain.equals(FullBackup.CACHE_TREE_TOKEN)) { basePath = getCacheDir().getCanonicalPath(); + } else if (domain.equals(FullBackup.MANAGED_EXTERNAL_TREE_TOKEN)) { + // make sure we can try to restore here before proceeding + if (Process.myUid() != Process.SYSTEM_UID) { + File efLocation = getExternalFilesDir(null); + if (efLocation != null) { + basePath = getExternalFilesDir(null).getCanonicalPath(); + } + } } else { // Not a supported location Log.i(TAG, "Data restored from non-app domain " + domain + ", ignoring"); diff --git a/core/java/android/app/backup/FullBackup.java b/core/java/android/app/backup/FullBackup.java index f859599..2fe08f3 100644 --- a/core/java/android/app/backup/FullBackup.java +++ b/core/java/android/app/backup/FullBackup.java @@ -46,6 +46,7 @@ public class FullBackup { public static final String DATA_TREE_TOKEN = "f"; public static final String DATABASE_TREE_TOKEN = "db"; public static final String SHAREDPREFS_TREE_TOKEN = "sp"; + public static final String MANAGED_EXTERNAL_TREE_TOKEN = "ef"; public static final String CACHE_TREE_TOKEN = "c"; public static final String SHARED_STORAGE_TOKEN = "shared"; diff --git a/core/java/android/appwidget/AppWidgetHost.java b/core/java/android/appwidget/AppWidgetHost.java index fa3bf4d..a470e70 100644 --- a/core/java/android/appwidget/AppWidgetHost.java +++ b/core/java/android/appwidget/AppWidgetHost.java @@ -31,6 +31,7 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; import android.util.DisplayMetrics; +import android.util.Log; import android.util.TypedValue; import android.widget.RemoteViews; import android.widget.RemoteViews.OnClickHandler; @@ -55,38 +56,39 @@ public class AppWidgetHost { Context mContext; String mPackageName; + Handler mHandler; + int mHostId; + Callbacks mCallbacks = new Callbacks(); + final HashMap<Integer,AppWidgetHostView> mViews = new HashMap<Integer, AppWidgetHostView>(); + private OnClickHandler mOnClickHandler; class Callbacks extends IAppWidgetHost.Stub { - public void updateAppWidget(int appWidgetId, RemoteViews views) { + public void updateAppWidget(int appWidgetId, RemoteViews views, int userId) { if (isLocalBinder() && views != null) { views = views.clone(); - views.setUser(mUser); + views.setUser(new UserHandle(userId)); } - Message msg = mHandler.obtainMessage(HANDLE_UPDATE); - msg.arg1 = appWidgetId; - msg.obj = views; + Message msg = mHandler.obtainMessage(HANDLE_UPDATE, appWidgetId, userId, views); msg.sendToTarget(); } - public void providerChanged(int appWidgetId, AppWidgetProviderInfo info) { + public void providerChanged(int appWidgetId, AppWidgetProviderInfo info, int userId) { if (isLocalBinder() && info != null) { info = info.clone(); } - Message msg = mHandler.obtainMessage(HANDLE_PROVIDER_CHANGED); - msg.arg1 = appWidgetId; - msg.obj = info; + Message msg = mHandler.obtainMessage(HANDLE_PROVIDER_CHANGED, + appWidgetId, userId, info); msg.sendToTarget(); } - public void providersChanged() { - Message msg = mHandler.obtainMessage(HANDLE_PROVIDERS_CHANGED); + public void providersChanged(int userId) { + Message msg = mHandler.obtainMessage(HANDLE_PROVIDERS_CHANGED, userId, 0); msg.sendToTarget(); } - public void viewDataChanged(int appWidgetId, int viewId) { - Message msg = mHandler.obtainMessage(HANDLE_VIEW_DATA_CHANGED); - msg.arg1 = appWidgetId; - msg.arg2 = viewId; + public void viewDataChanged(int appWidgetId, int viewId, int userId) { + Message msg = mHandler.obtainMessage(HANDLE_VIEW_DATA_CHANGED, + appWidgetId, viewId, userId); msg.sendToTarget(); } } @@ -99,7 +101,7 @@ public class AppWidgetHost { public void handleMessage(Message msg) { switch (msg.what) { case HANDLE_UPDATE: { - updateAppWidgetView(msg.arg1, (RemoteViews)msg.obj); + updateAppWidgetView(msg.arg1, (RemoteViews)msg.obj, msg.arg2); break; } case HANDLE_PROVIDER_CHANGED: { @@ -107,26 +109,17 @@ public class AppWidgetHost { break; } case HANDLE_PROVIDERS_CHANGED: { - onProvidersChanged(); + onProvidersChanged(msg.arg1); break; } case HANDLE_VIEW_DATA_CHANGED: { - viewDataChanged(msg.arg1, msg.arg2); + viewDataChanged(msg.arg1, msg.arg2, (Integer) msg.obj); break; } } } } - Handler mHandler; - - int mHostId; - Callbacks mCallbacks = new Callbacks(); - final HashMap<Integer,AppWidgetHostView> mViews = new HashMap<Integer, AppWidgetHostView>(); - private OnClickHandler mOnClickHandler; - // Optionally set by lockscreen - private UserHandle mUser; - public AppWidgetHost(Context context, int hostId) { this(context, hostId, null, context.getMainLooper()); } @@ -140,14 +133,9 @@ public class AppWidgetHost { mOnClickHandler = handler; mHandler = new UpdateHandler(looper); mDisplayMetrics = context.getResources().getDisplayMetrics(); - mUser = Process.myUserHandle(); bindService(); } - /** @hide */ - public void setUserId(int userId) { - mUser = new UserHandle(userId); - } private static void bindService() { synchronized (sServiceLock) { @@ -163,23 +151,15 @@ public class AppWidgetHost { * becomes visible, i.e. from onStart() in your Activity. */ public void startListening() { - startListeningAsUser(UserHandle.myUserId()); - } - - /** - * Start receiving onAppWidgetChanged calls for your AppWidgets. Call this when your activity - * becomes visible, i.e. from onStart() in your Activity. - * @hide - */ - public void startListeningAsUser(int userId) { int[] updatedIds; ArrayList<RemoteViews> updatedViews = new ArrayList<RemoteViews>(); + final int userId = mContext.getUserId(); try { if (mPackageName == null) { mPackageName = mContext.getPackageName(); } - updatedIds = sService.startListeningAsUser( + updatedIds = sService.startListening( mCallbacks, mPackageName, mHostId, updatedViews, userId); } catch (RemoteException e) { @@ -191,7 +171,7 @@ public class AppWidgetHost { if (updatedViews.get(i) != null) { updatedViews.get(i).setUser(new UserHandle(userId)); } - updateAppWidgetView(updatedIds[i], updatedViews.get(i)); + updateAppWidgetView(updatedIds[i], updatedViews.get(i), userId); } } @@ -201,26 +181,14 @@ public class AppWidgetHost { */ public void stopListening() { try { - sService.stopListeningAsUser(mHostId, UserHandle.myUserId()); + sService.stopListening(mHostId, mContext.getUserId()); } catch (RemoteException e) { throw new RuntimeException("system server dead?", e); } - } - /** - * Stop receiving onAppWidgetChanged calls for your AppWidgets. Call this when your activity is - * no longer visible, i.e. from onStop() in your Activity. - * @hide - */ - public void stopListeningAsUser(int userId) { - try { - sService.stopListeningAsUser(mHostId, userId); - } - catch (RemoteException e) { - throw new RuntimeException("system server dead?", e); - } - // Also clear the views + // This is here because keyguard needs it since it'll be switching users after this call. + // If it turns out other apps need to call this often, we should re-think how this works. clearViews(); } @@ -230,11 +198,12 @@ public class AppWidgetHost { * @return a appWidgetId */ public int allocateAppWidgetId() { + try { if (mPackageName == null) { mPackageName = mContext.getPackageName(); } - return sService.allocateAppWidgetId(mPackageName, mHostId); + return sService.allocateAppWidgetId(mPackageName, mHostId, mContext.getUserId()); } catch (RemoteException e) { throw new RuntimeException("system server dead?", e); @@ -247,7 +216,7 @@ public class AppWidgetHost { * @return a appWidgetId * @hide */ - public static int allocateAppWidgetIdForSystem(int hostId) { + public static int allocateAppWidgetIdForSystem(int hostId, int userId) { checkCallerIsSystem(); try { if (sService == null) { @@ -256,7 +225,7 @@ public class AppWidgetHost { Context systemContext = (Context) ActivityThread.currentActivityThread().getSystemContext(); String packageName = systemContext.getPackageName(); - return sService.allocateAppWidgetId(packageName, hostId); + return sService.allocateAppWidgetId(packageName, hostId, userId); } catch (RemoteException e) { throw new RuntimeException("system server dead?", e); } @@ -272,7 +241,7 @@ public class AppWidgetHost { if (sService == null) { bindService(); } - return sService.getAppWidgetIdsForHost(mHostId); + return sService.getAppWidgetIdsForHost(mHostId, mContext.getUserId()); } catch (RemoteException e) { throw new RuntimeException("system server dead?", e); } @@ -297,7 +266,7 @@ public class AppWidgetHost { synchronized (mViews) { mViews.remove(appWidgetId); try { - sService.deleteAppWidgetId(appWidgetId); + sService.deleteAppWidgetId(appWidgetId, mContext.getUserId()); } catch (RemoteException e) { throw new RuntimeException("system server dead?", e); @@ -309,13 +278,13 @@ public class AppWidgetHost { * Stop listening to changes for this AppWidget. * @hide */ - public static void deleteAppWidgetIdForSystem(int appWidgetId) { + public static void deleteAppWidgetIdForSystem(int appWidgetId, int userId) { checkCallerIsSystem(); try { if (sService == null) { bindService(); } - sService.deleteAppWidgetId(appWidgetId); + sService.deleteAppWidgetId(appWidgetId, userId); } catch (RemoteException e) { throw new RuntimeException("system server dead?", e); } @@ -331,7 +300,7 @@ public class AppWidgetHost { */ public void deleteHost() { try { - sService.deleteHost(mHostId); + sService.deleteHost(mHostId, mContext.getUserId()); } catch (RemoteException e) { throw new RuntimeException("system server dead?", e); @@ -347,8 +316,16 @@ public class AppWidgetHost { * </ul> */ public static void deleteAllHosts() { + deleteAllHosts(UserHandle.myUserId()); + } + + /** + * Private method containing a userId + * @hide + */ + public static void deleteAllHosts(int userId) { try { - sService.deleteAllHosts(); + sService.deleteAllHosts(userId); } catch (RemoteException e) { throw new RuntimeException("system server dead?", e); @@ -361,8 +338,9 @@ public class AppWidgetHost { */ public final AppWidgetHostView createView(Context context, int appWidgetId, AppWidgetProviderInfo appWidget) { + final int userId = context.getUserId(); AppWidgetHostView view = onCreateView(context, appWidgetId, appWidget); - view.setUserId(mUser.getIdentifier()); + view.setUserId(userId); view.setOnClickHandler(mOnClickHandler); view.setAppWidget(appWidgetId, appWidget); synchronized (mViews) { @@ -370,9 +348,9 @@ public class AppWidgetHost { } RemoteViews views; try { - views = sService.getAppWidgetViews(appWidgetId); + views = sService.getAppWidgetViews(appWidgetId, userId); if (views != null) { - views.setUser(mUser); + views.setUser(new UserHandle(mContext.getUserId())); } } catch (RemoteException e) { throw new RuntimeException("system server dead?", e); @@ -422,10 +400,20 @@ public class AppWidgetHost { * are added, updated or removed, or widget components are enabled or disabled.) */ protected void onProvidersChanged() { - // Do nothing + onProvidersChanged(mContext.getUserId()); } - void updateAppWidgetView(int appWidgetId, RemoteViews views) { + /** + * Private method containing a userId + * @hide + */ + protected void onProvidersChanged(int userId) { + checkUserMatch(userId); + // Does nothing + } + + void updateAppWidgetView(int appWidgetId, RemoteViews views, int userId) { + checkUserMatch(userId); AppWidgetHostView v; synchronized (mViews) { v = mViews.get(appWidgetId); @@ -435,7 +423,8 @@ public class AppWidgetHost { } } - void viewDataChanged(int appWidgetId, int viewId) { + void viewDataChanged(int appWidgetId, int viewId, int userId) { + checkUserMatch(userId); AppWidgetHostView v; synchronized (mViews) { v = mViews.get(appWidgetId); @@ -445,6 +434,16 @@ public class AppWidgetHost { } } + // Ensure that the userId passed to us agrees with the one associated with this instance + // of AppWidgetHost. + // TODO: This should be removed in production code. + private void checkUserMatch(int userId) { + if (userId != mContext.getUserId()) { + throw new IllegalStateException( + "User ids don't match, userId=" + userId + ", mUserId=" + mContext.getUserId()); + } + } + /** * Clear the list of Views that have been created by this AppWidgetHost. */ diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java index 6b1c3e2..e68d23a 100644 --- a/core/java/android/appwidget/AppWidgetManager.java +++ b/core/java/android/appwidget/AppWidgetManager.java @@ -16,6 +16,7 @@ package android.appwidget; +import android.app.ActivityManagerNative; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -268,8 +269,8 @@ public class AppWidgetManager { /** * Sent when the custom extras for an AppWidget change. * - * @see AppWidgetProvider#onAppWidgetOptionsChanged - * AppWidgetProvider.onAppWidgetOptionsChanged(Context context, + * @see AppWidgetProvider#onAppWidgetOptionsChanged + * AppWidgetProvider.onAppWidgetOptionsChanged(Context context, * AppWidgetManager appWidgetManager, int appWidgetId, Bundle newExtras) */ public static final String ACTION_APPWIDGET_OPTIONS_CHANGED = "android.appwidget.action.APPWIDGET_UPDATE_OPTIONS"; @@ -352,7 +353,7 @@ public class AppWidgetManager { * It is okay to call this method both inside an {@link #ACTION_APPWIDGET_UPDATE} broadcast, * and outside of the handler. * This method will only work when called from the uid that owns the AppWidget provider. - * + * * <p> * The total Bitmap memory used by the RemoteViews object cannot exceed that required to * fill the screen 1.5 times, ie. (screen width x screen height x 4 x 1.5) bytes. @@ -362,7 +363,7 @@ public class AppWidgetManager { */ public void updateAppWidget(int[] appWidgetIds, RemoteViews views) { try { - sService.updateAppWidgetIds(appWidgetIds, views); + sService.updateAppWidgetIds(appWidgetIds, views, mContext.getUserId()); } catch (RemoteException e) { throw new RuntimeException("system server dead?", e); @@ -382,7 +383,7 @@ public class AppWidgetManager { */ public void updateAppWidgetOptions(int appWidgetId, Bundle options) { try { - sService.updateAppWidgetOptions(appWidgetId, options); + sService.updateAppWidgetOptions(appWidgetId, options, mContext.getUserId()); } catch (RemoteException e) { throw new RuntimeException("system server dead?", e); @@ -402,7 +403,7 @@ public class AppWidgetManager { */ public Bundle getAppWidgetOptions(int appWidgetId) { try { - return sService.getAppWidgetOptions(appWidgetId); + return sService.getAppWidgetOptions(appWidgetId, mContext.getUserId()); } catch (RemoteException e) { throw new RuntimeException("system server dead?", e); @@ -436,7 +437,7 @@ public class AppWidgetManager { * Perform an incremental update or command on the widget(s) specified by appWidgetIds. * * This update differs from {@link #updateAppWidget(int[], RemoteViews)} in that the - * RemoteViews object which is passed is understood to be an incomplete representation of the + * RemoteViews object which is passed is understood to be an incomplete representation of the * widget, and hence does not replace the cached representation of the widget. As of API * level 17, the new properties set within the views objects will be appended to the cached * representation of the widget, and hence will persist. @@ -458,7 +459,7 @@ public class AppWidgetManager { */ public void partiallyUpdateAppWidget(int[] appWidgetIds, RemoteViews views) { try { - sService.partiallyUpdateAppWidgetIds(appWidgetIds, views); + sService.partiallyUpdateAppWidgetIds(appWidgetIds, views, mContext.getUserId()); } catch (RemoteException e) { throw new RuntimeException("system server dead?", e); } @@ -507,7 +508,7 @@ public class AppWidgetManager { */ public void updateAppWidget(ComponentName provider, RemoteViews views) { try { - sService.updateAppWidgetProvider(provider, views); + sService.updateAppWidgetProvider(provider, views, mContext.getUserId()); } catch (RemoteException e) { throw new RuntimeException("system server dead?", e); @@ -523,7 +524,7 @@ public class AppWidgetManager { */ public void notifyAppWidgetViewDataChanged(int[] appWidgetIds, int viewId) { try { - sService.notifyAppWidgetViewDataChanged(appWidgetIds, viewId); + sService.notifyAppWidgetViewDataChanged(appWidgetIds, viewId, mContext.getUserId()); } catch (RemoteException e) { throw new RuntimeException("system server dead?", e); @@ -557,7 +558,8 @@ public class AppWidgetManager { */ public List<AppWidgetProviderInfo> getInstalledProviders(int categoryFilter) { try { - List<AppWidgetProviderInfo> providers = sService.getInstalledProviders(categoryFilter); + List<AppWidgetProviderInfo> providers = sService.getInstalledProviders(categoryFilter, + mContext.getUserId()); for (AppWidgetProviderInfo info : providers) { // Converting complex to dp. info.minWidth = @@ -584,7 +586,8 @@ public class AppWidgetManager { */ public AppWidgetProviderInfo getAppWidgetInfo(int appWidgetId) { try { - AppWidgetProviderInfo info = sService.getAppWidgetInfo(appWidgetId); + AppWidgetProviderInfo info = sService.getAppWidgetInfo(appWidgetId, + mContext.getUserId()); if (info != null) { // Converting complex to dp. info.minWidth = @@ -617,7 +620,7 @@ public class AppWidgetManager { */ public void bindAppWidgetId(int appWidgetId, ComponentName provider) { try { - sService.bindAppWidgetId(appWidgetId, provider, null); + sService.bindAppWidgetId(appWidgetId, provider, null, mContext.getUserId()); } catch (RemoteException e) { throw new RuntimeException("system server dead?", e); @@ -641,7 +644,7 @@ public class AppWidgetManager { */ public void bindAppWidgetId(int appWidgetId, ComponentName provider, Bundle options) { try { - sService.bindAppWidgetId(appWidgetId, provider, options); + sService.bindAppWidgetId(appWidgetId, provider, options, mContext.getUserId()); } catch (RemoteException e) { throw new RuntimeException("system server dead?", e); @@ -667,7 +670,7 @@ public class AppWidgetManager { } try { return sService.bindAppWidgetIdIfAllowed( - mContext.getPackageName(), appWidgetId, provider, null); + mContext.getPackageName(), appWidgetId, provider, null, mContext.getUserId()); } catch (RemoteException e) { throw new RuntimeException("system server dead?", e); @@ -696,8 +699,8 @@ public class AppWidgetManager { return false; } try { - return sService.bindAppWidgetIdIfAllowed( - mContext.getPackageName(), appWidgetId, provider, options); + return sService.bindAppWidgetIdIfAllowed(mContext.getPackageName(), appWidgetId, + provider, options, mContext.getUserId()); } catch (RemoteException e) { throw new RuntimeException("system server dead?", e); @@ -715,7 +718,7 @@ public class AppWidgetManager { */ public boolean hasBindAppWidgetPermission(String packageName) { try { - return sService.hasBindAppWidgetPermission(packageName); + return sService.hasBindAppWidgetPermission(packageName, mContext.getUserId()); } catch (RemoteException e) { throw new RuntimeException("system server dead?", e); @@ -733,7 +736,7 @@ public class AppWidgetManager { */ public void setBindAppWidgetPermission(String packageName, boolean permission) { try { - sService.setBindAppWidgetPermission(packageName, permission); + sService.setBindAppWidgetPermission(packageName, permission, mContext.getUserId()); } catch (RemoteException e) { throw new RuntimeException("system server dead?", e); @@ -794,7 +797,7 @@ public class AppWidgetManager { */ public int[] getAppWidgetIds(ComponentName provider) { try { - return sService.getAppWidgetIds(provider); + return sService.getAppWidgetIds(provider, mContext.getUserId()); } catch (RemoteException e) { throw new RuntimeException("system server dead?", e); diff --git a/core/java/android/bluetooth/BluetoothTetheringDataTracker.java b/core/java/android/bluetooth/BluetoothTetheringDataTracker.java index 063e5a8..3ba4f26 100644 --- a/core/java/android/bluetooth/BluetoothTetheringDataTracker.java +++ b/core/java/android/bluetooth/BluetoothTetheringDataTracker.java @@ -21,7 +21,6 @@ import android.os.ServiceManager; import android.os.INetworkManagementService; import android.content.Context; import android.net.ConnectivityManager; -import android.net.DhcpInfoInternal; import android.net.LinkCapabilities; import android.net.LinkProperties; import android.net.NetworkInfo; diff --git a/core/java/android/content/AsyncTaskLoader.java b/core/java/android/content/AsyncTaskLoader.java index f9025d9..188c786 100644 --- a/core/java/android/content/AsyncTaskLoader.java +++ b/core/java/android/content/AsyncTaskLoader.java @@ -78,7 +78,7 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { // So we treat this case as an unhandled exception. throw ex; } - if (DEBUG) Slog.v(TAG, this + " <<< doInBackground (was canceled)"); + if (DEBUG) Slog.v(TAG, this + " <<< doInBackground (was canceled)", ex); return null; } } diff --git a/core/java/android/content/ClipboardManager.java b/core/java/android/content/ClipboardManager.java index dfd1820..88a4229 100644 --- a/core/java/android/content/ClipboardManager.java +++ b/core/java/android/content/ClipboardManager.java @@ -118,7 +118,7 @@ public class ClipboardManager extends android.text.ClipboardManager { */ public void setPrimaryClip(ClipData clip) { try { - getService().setPrimaryClip(clip); + getService().setPrimaryClip(clip, mContext.getBasePackageName()); } catch (RemoteException e) { } } @@ -128,7 +128,7 @@ public class ClipboardManager extends android.text.ClipboardManager { */ public ClipData getPrimaryClip() { try { - return getService().getPrimaryClip(mContext.getPackageName()); + return getService().getPrimaryClip(mContext.getBasePackageName()); } catch (RemoteException e) { return null; } @@ -140,7 +140,7 @@ public class ClipboardManager extends android.text.ClipboardManager { */ public ClipDescription getPrimaryClipDescription() { try { - return getService().getPrimaryClipDescription(); + return getService().getPrimaryClipDescription(mContext.getBasePackageName()); } catch (RemoteException e) { return null; } @@ -151,7 +151,7 @@ public class ClipboardManager extends android.text.ClipboardManager { */ public boolean hasPrimaryClip() { try { - return getService().hasPrimaryClip(); + return getService().hasPrimaryClip(mContext.getBasePackageName()); } catch (RemoteException e) { return false; } @@ -162,7 +162,7 @@ public class ClipboardManager extends android.text.ClipboardManager { if (mPrimaryClipChangedListeners.size() == 0) { try { getService().addPrimaryClipChangedListener( - mPrimaryClipChangedServiceListener); + mPrimaryClipChangedServiceListener, mContext.getBasePackageName()); } catch (RemoteException e) { } } @@ -209,7 +209,7 @@ public class ClipboardManager extends android.text.ClipboardManager { */ public boolean hasText() { try { - return getService().hasClipboardText(); + return getService().hasClipboardText(mContext.getBasePackageName()); } catch (RemoteException e) { return false; } diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java index 23d8f46..612f1af 100644 --- a/core/java/android/content/ContentProvider.java +++ b/core/java/android/content/ContentProvider.java @@ -16,8 +16,10 @@ package android.content; +import static android.content.pm.PackageManager.GET_PROVIDERS; import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import android.app.AppOpsManager; import android.content.pm.PackageManager; import android.content.pm.PathPermission; import android.content.pm.ProviderInfo; @@ -100,6 +102,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 { private String mWritePermission; private PathPermission[] mPathPermissions; private boolean mExported; + private boolean mNoPerms; private Transport mTransport = new Transport(); @@ -154,8 +157,8 @@ public abstract class ContentProvider implements ComponentCallbacks2 { * * @param abstractInterface The ContentProvider interface that is to be * coerced. - * @return If the IContentProvider is non-null and local, returns its actual - * ContentProvider instance. Otherwise returns null. + * @return If the IContentProvider is non-{@code null} and local, returns its actual + * ContentProvider instance. Otherwise returns {@code null}. * @hide */ public static ContentProvider coerceToLocalContentProvider( @@ -172,6 +175,10 @@ public abstract class ContentProvider implements ComponentCallbacks2 { * @hide */ class Transport extends ContentProviderNative { + AppOpsManager mAppOpsManager = null; + int mReadOp = AppOpsManager.OP_NONE; + int mWriteOp = AppOpsManager.OP_NONE; + ContentProvider getContentProvider() { return ContentProvider.this; } @@ -182,10 +189,13 @@ public abstract class ContentProvider implements ComponentCallbacks2 { } @Override - public Cursor query(Uri uri, String[] projection, + public Cursor query(String callingPkg, Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder, ICancellationSignal cancellationSignal) { - enforceReadPermission(uri); + if (enforceReadPermission(callingPkg, uri) != AppOpsManager.MODE_ALLOWED) { + return rejectQuery(uri, projection, selection, selectionArgs, sortOrder, + CancellationSignal.fromTransport(cancellationSignal)); + } return ContentProvider.this.query(uri, projection, selection, selectionArgs, sortOrder, CancellationSignal.fromTransport(cancellationSignal)); } @@ -196,64 +206,77 @@ public abstract class ContentProvider implements ComponentCallbacks2 { } @Override - public Uri insert(Uri uri, ContentValues initialValues) { - enforceWritePermission(uri); + public Uri insert(String callingPkg, Uri uri, ContentValues initialValues) { + if (enforceWritePermission(callingPkg, uri) != AppOpsManager.MODE_ALLOWED) { + return rejectInsert(uri, initialValues); + } return ContentProvider.this.insert(uri, initialValues); } @Override - public int bulkInsert(Uri uri, ContentValues[] initialValues) { - enforceWritePermission(uri); + public int bulkInsert(String callingPkg, Uri uri, ContentValues[] initialValues) { + if (enforceWritePermission(callingPkg, uri) != AppOpsManager.MODE_ALLOWED) { + return 0; + } return ContentProvider.this.bulkInsert(uri, initialValues); } @Override - public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) + public ContentProviderResult[] applyBatch(String callingPkg, + ArrayList<ContentProviderOperation> operations) throws OperationApplicationException { for (ContentProviderOperation operation : operations) { if (operation.isReadOperation()) { - enforceReadPermission(operation.getUri()); + if (enforceReadPermission(callingPkg, operation.getUri()) + != AppOpsManager.MODE_ALLOWED) { + throw new OperationApplicationException("App op not allowed", 0); + } } if (operation.isWriteOperation()) { - enforceWritePermission(operation.getUri()); + if (enforceWritePermission(callingPkg, operation.getUri()) + != AppOpsManager.MODE_ALLOWED) { + throw new OperationApplicationException("App op not allowed", 0); + } } } return ContentProvider.this.applyBatch(operations); } @Override - public int delete(Uri uri, String selection, String[] selectionArgs) { - enforceWritePermission(uri); + public int delete(String callingPkg, Uri uri, String selection, String[] selectionArgs) { + if (enforceWritePermission(callingPkg, uri) != AppOpsManager.MODE_ALLOWED) { + return 0; + } return ContentProvider.this.delete(uri, selection, selectionArgs); } @Override - public int update(Uri uri, ContentValues values, String selection, + public int update(String callingPkg, Uri uri, ContentValues values, String selection, String[] selectionArgs) { - enforceWritePermission(uri); + if (enforceWritePermission(callingPkg, uri) != AppOpsManager.MODE_ALLOWED) { + return 0; + } return ContentProvider.this.update(uri, values, selection, selectionArgs); } @Override - public ParcelFileDescriptor openFile(Uri uri, String mode) + public ParcelFileDescriptor openFile(String callingPkg, Uri uri, String mode) throws FileNotFoundException { - if (mode != null && mode.startsWith("rw")) enforceWritePermission(uri); - else enforceReadPermission(uri); + enforceFilePermission(callingPkg, uri, mode); return ContentProvider.this.openFile(uri, mode); } @Override - public AssetFileDescriptor openAssetFile(Uri uri, String mode) + public AssetFileDescriptor openAssetFile(String callingPkg, Uri uri, String mode) throws FileNotFoundException { - if (mode != null && mode.startsWith("rw")) enforceWritePermission(uri); - else enforceReadPermission(uri); + enforceFilePermission(callingPkg, uri, mode); return ContentProvider.this.openAssetFile(uri, mode); } @Override - public Bundle call(String method, String arg, Bundle extras) { - return ContentProvider.this.call(method, arg, extras); + public Bundle call(String callingPkg, String method, String arg, Bundle extras) { + return ContentProvider.this.callFromPackage(callingPkg, method, arg, extras); } @Override @@ -262,9 +285,9 @@ public abstract class ContentProvider implements ComponentCallbacks2 { } @Override - public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeType, Bundle opts) - throws FileNotFoundException { - enforceReadPermission(uri); + public AssetFileDescriptor openTypedAssetFile(String callingPkg, Uri uri, String mimeType, + Bundle opts) throws FileNotFoundException { + enforceFilePermission(callingPkg, uri, "r"); return ContentProvider.this.openTypedAssetFile(uri, mimeType, opts); } @@ -273,7 +296,28 @@ public abstract class ContentProvider implements ComponentCallbacks2 { return CancellationSignal.createTransport(); } - private void enforceReadPermission(Uri uri) throws SecurityException { + private void enforceFilePermission(String callingPkg, Uri uri, String mode) + throws FileNotFoundException, SecurityException { + if (mode != null && mode.startsWith("rw")) { + if (enforceWritePermission(callingPkg, uri) != AppOpsManager.MODE_ALLOWED) { + throw new FileNotFoundException("App op not allowed"); + } + } else { + if (enforceReadPermission(callingPkg, uri) != AppOpsManager.MODE_ALLOWED) { + throw new FileNotFoundException("App op not allowed"); + } + } + } + + private int enforceReadPermission(String callingPkg, Uri uri) throws SecurityException { + enforceReadPermissionInner(uri); + if (mReadOp != AppOpsManager.OP_NONE) { + return mAppOpsManager.noteOp(mReadOp, Binder.getCallingUid(), callingPkg); + } + return AppOpsManager.MODE_ALLOWED; + } + + private void enforceReadPermissionInner(Uri uri) throws SecurityException { final Context context = getContext(); final int pid = Binder.getCallingPid(); final int uid = Binder.getCallingUid(); @@ -334,7 +378,15 @@ public abstract class ContentProvider implements ComponentCallbacks2 { + ", uid=" + uid + failReason); } - private void enforceWritePermission(Uri uri) throws SecurityException { + private int enforceWritePermission(String callingPkg, Uri uri) throws SecurityException { + enforceWritePermissionInner(uri); + if (mWriteOp != AppOpsManager.OP_NONE) { + return mAppOpsManager.noteOp(mWriteOp, Binder.getCallingUid(), callingPkg); + } + return AppOpsManager.MODE_ALLOWED; + } + + private void enforceWritePermissionInner(Uri uri) throws SecurityException { final Context context = getContext(); final int pid = Binder.getCallingPid(); final int uid = Binder.getCallingUid(); @@ -398,7 +450,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 { /** * Retrieves the Context this provider is running in. Only available once - * {@link #onCreate} has been called -- this will return null in the + * {@link #onCreate} has been called -- this will return {@code null} in the * constructor. */ public final Context getContext() { @@ -471,6 +523,21 @@ public abstract class ContentProvider implements ComponentCallbacks2 { return mPathPermissions; } + /** @hide */ + public final void setAppOps(int readOp, int writeOp) { + if (!mNoPerms) { + mTransport.mAppOpsManager = (AppOpsManager)mContext.getSystemService( + Context.APP_OPS_SERVICE); + mTransport.mReadOp = readOp; + mTransport.mWriteOp = writeOp; + } + } + + /** @hide */ + public AppOpsManager getAppOpsManager() { + return mTransport.mAppOpsManager; + } + /** * Implement this to initialize your content provider on startup. * This method is called for all registered content providers on the @@ -526,6 +593,31 @@ public abstract class ContentProvider implements ComponentCallbacks2 { } /** + * @hide + * Implementation when a caller has performed a query on the content + * provider, but that call has been rejected for the operation given + * to {@link #setAppOps(int, int)}. The default implementation + * rewrites the <var>selection</var> argument to include a condition + * that is never true (so will always result in an empty cursor) + * and calls through to {@link #query(android.net.Uri, String[], String, String[], + * String, android.os.CancellationSignal)} with that. + */ + public Cursor rejectQuery(Uri uri, String[] projection, + String selection, String[] selectionArgs, String sortOrder, + CancellationSignal cancellationSignal) { + // The read is not allowed... to fake it out, we replace the given + // selection statement with a dummy one that will always be false. + // This way we will get a cursor back that has the correct structure + // but contains no rows. + if (selection == null) { + selection = "'A' = 'B'"; + } else { + selection = "'A' = 'B' AND (" + selection + ")"; + } + return query(uri, projection, selection, selectionArgs, sortOrder, cancellationSignal); + } + + /** * Implement this to handle query requests from clients. * This method can be called from multiple threads, as described in * <a href="{@docRoot}guide/topics/fundamentals/processes-and-threads.html#Threads">Processes @@ -570,15 +662,15 @@ public abstract class ContentProvider implements ComponentCallbacks2 { * that the implementation should parse and add to a WHERE or HAVING clause, specifying * that _id value. * @param projection The list of columns to put into the cursor. If - * null all columns are included. + * {@code null} all columns are included. * @param selection A selection criteria to apply when filtering rows. - * If null then all rows are included. + * If {@code null} then all rows are included. * @param selectionArgs You may include ?s in selection, which will be replaced by * the values from selectionArgs, in order that they appear in the selection. * The values will be bound as Strings. * @param sortOrder How the rows in the cursor should be sorted. - * If null then the provider is free to define the sort order. - * @return a Cursor or null. + * If {@code null} then the provider is free to define the sort order. + * @return a Cursor or {@code null}. */ public abstract Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder); @@ -633,18 +725,18 @@ public abstract class ContentProvider implements ComponentCallbacks2 { * that the implementation should parse and add to a WHERE or HAVING clause, specifying * that _id value. * @param projection The list of columns to put into the cursor. If - * null all columns are included. + * {@code null} all columns are included. * @param selection A selection criteria to apply when filtering rows. - * If null then all rows are included. + * If {@code null} then all rows are included. * @param selectionArgs You may include ?s in selection, which will be replaced by * the values from selectionArgs, in order that they appear in the selection. * The values will be bound as Strings. * @param sortOrder How the rows in the cursor should be sorted. - * If null then the provider is free to define the sort order. - * @param cancellationSignal A signal to cancel the operation in progress, or null if none. + * If {@code null} then the provider is free to define the sort order. + * @param cancellationSignal A signal to cancel the operation in progress, or {@code null} if none. * If the operation is canceled, then {@link OperationCanceledException} will be thrown * when the query is executed. - * @return a Cursor or null. + * @return a Cursor or {@code null}. */ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder, @@ -668,19 +760,37 @@ public abstract class ContentProvider implements ComponentCallbacks2 { * to retrieve the MIME type for a URI when dispatching intents. * * @param uri the URI to query. - * @return a MIME type string, or null if there is no type. + * @return a MIME type string, or {@code null} if there is no type. */ public abstract String getType(Uri uri); /** + * @hide + * Implementation when a caller has performed an insert on the content + * provider, but that call has been rejected for the operation given + * to {@link #setAppOps(int, int)}. The default implementation simply + * returns a dummy URI that is the base URI with a 0 path element + * appended. + */ + public Uri rejectInsert(Uri uri, ContentValues values) { + // If not allowed, we need to return some reasonable URI. Maybe the + // content provider should be responsible for this, but for now we + // will just return the base URI with a dummy '0' tagged on to it. + // You shouldn't be able to read if you can't write, anyway, so it + // shouldn't matter much what is returned. + return uri.buildUpon().appendPath("0").build(); + } + + /** * Implement this to handle requests to insert a new row. * As a courtesy, call {@link ContentResolver#notifyChange(android.net.Uri ,android.database.ContentObserver) notifyChange()} * after inserting. * This method can be called from multiple threads, as described in * <a href="{@docRoot}guide/topics/fundamentals/processes-and-threads.html#Threads">Processes * and Threads</a>. - * @param uri The content:// URI of the insertion request. + * @param uri The content:// URI of the insertion request. This must not be {@code null}. * @param values A set of column_name/value pairs to add to the database. + * This must not be {@code null}. * @return The URI for the newly inserted item. */ public abstract Uri insert(Uri uri, ContentValues values); @@ -697,6 +807,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 { * * @param uri The content:// URI of the insertion request. * @param values An array of sets of column_name/value pairs to add to the database. + * This must not be {@code null}. * @return The number of values that were inserted. */ public int bulkInsert(Uri uri, ContentValues[] values) { @@ -741,8 +852,8 @@ public abstract class ContentProvider implements ComponentCallbacks2 { * * @param uri The URI to query. This can potentially have a record ID if this * is an update request for a specific record. - * @param values A Bundle mapping from column names to new column values (NULL is a - * valid value). + * @param values A set of column_name/value pairs to update in the database. + * This must not be {@code null}. * @param selection An optional filter to match rows to update. * @return the number of rows affected. */ @@ -764,6 +875,18 @@ public abstract class ContentProvider implements ComponentCallbacks2 { * their responsibility to close it when done. That is, the implementation * of this method should create a new ParcelFileDescriptor for each call. * + * <p class="note">For use in Intents, you will want to implement {@link #getType} + * to return the appropriate MIME type for the data returned here with + * the same URI. This will allow intent resolution to automatically determine the data MIME + * type and select the appropriate matching targets as part of its operation.</p> + * + * <p class="note">For better interoperability with other applications, it is recommended + * that for any URIs that can be opened, you also support queries on them + * containing at least the columns specified by {@link android.provider.OpenableColumns}. + * You may also want to support other common columns if you have additional meta-data + * to supply, such as {@link android.provider.MediaStore.MediaColumns#DATE_ADDED} + * in {@link android.provider.MediaStore.MediaColumns}.</p> + * * @param uri The URI whose file is to be opened. * @param mode Access mode for the file. May be "r" for read-only access, * "rw" for read and write access, or "rwt" for read and write access @@ -779,6 +902,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 { * * @see #openAssetFile(Uri, String) * @see #openFileHelper(Uri, String) + * @see #getType(android.net.Uri) */ public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { @@ -806,6 +930,15 @@ public abstract class ContentProvider implements ComponentCallbacks2 { * {@link AssetFileDescriptor#UNKNOWN_LENGTH} to be compatible with * applications that can not handle sub-sections of files.</p> * + * <p class="note">For use in Intents, you will want to implement {@link #getType} + * to return the appropriate MIME type for the data returned here with + * the same URI. This will allow intent resolution to automatically determine the data MIME + * type and select the appropriate matching targets as part of its operation.</p> + * + * <p class="note">For better interoperability with other applications, it is recommended + * that for any URIs that can be opened, you also support queries on them + * containing at least the columns specified by {@link android.provider.OpenableColumns}.</p> + * * @param uri The URI whose file is to be opened. * @param mode Access mode for the file. May be "r" for read-only access, * "w" for write-only access (erasing whatever data is currently in @@ -823,6 +956,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 { * * @see #openFile(Uri, String) * @see #openFileHelper(Uri, String) + * @see #getType(android.net.Uri) */ public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException { @@ -875,7 +1009,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 { /** * Called by a client to determine the types of data streams that this * content provider supports for the given URI. The default implementation - * returns null, meaning no types. If your content provider stores data + * returns {@code null}, meaning no types. If your content provider stores data * of a particular type, return that MIME type if it matches the given * mimeTypeFilter. If it can perform type conversions, return an array * of all supported MIME types that match mimeTypeFilter. @@ -883,7 +1017,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 { * @param uri The data in the content provider being queried. * @param mimeTypeFilter The type of data the client desires. May be * a pattern, such as *\/* to retrieve all possible data types. - * @return Returns null if there are no possible data streams for the + * @return Returns {@code null} if there are no possible data streams for the * given mimeTypeFilter. Otherwise returns an array of all available * concrete MIME types. * @@ -902,12 +1036,19 @@ public abstract class ContentProvider implements ComponentCallbacks2 { * perform data conversions to generate data of the desired type. * * <p>The default implementation compares the given mimeType against the - * result of {@link #getType(Uri)} and, if the match, simple calls + * result of {@link #getType(Uri)} and, if they match, simply calls * {@link #openAssetFile(Uri, String)}. * * <p>See {@link ClipData} for examples of the use and implementation * of this method. * + * <p class="note">For better interoperability with other applications, it is recommended + * that for any URIs that can be opened, you also support queries on them + * containing at least the columns specified by {@link android.provider.OpenableColumns}. + * You may also want to support other common columns if you have additional meta-data + * to supply, such as {@link android.provider.MediaStore.MediaColumns#DATE_ADDED} + * in {@link android.provider.MediaStore.MediaColumns}.</p> + * * @param uri The data in the content provider being queried. * @param mimeTypeFilter The type of data the client desires. May be * a pattern, such as *\/*, if the caller does not have specific type @@ -1029,6 +1170,15 @@ public abstract class ContentProvider implements ComponentCallbacks2 { } /** + * Like {@link #attachInfo(Context, android.content.pm.ProviderInfo)}, but for use + * when directly instantiating the provider for testing. + * @hide + */ + public void attachInfoForTesting(Context context, ProviderInfo info) { + attachInfo(context, info, true); + } + + /** * After being instantiated, this is called to tell the content provider * about itself. * @@ -1036,12 +1186,18 @@ public abstract class ContentProvider implements ComponentCallbacks2 { * @param info Registered information about this content provider */ public void attachInfo(Context context, ProviderInfo info) { + attachInfo(context, info, false); + } + + private void attachInfo(Context context, ProviderInfo info, boolean testing) { /* * We may be using AsyncTask from binder threads. Make it init here * so its static handler is on the main thread. */ AsyncTask.init(); + mNoPerms = testing; + /* * Only allow it to be set once, so after the content service gives * this to us clients can't change it. @@ -1087,14 +1243,23 @@ public abstract class ContentProvider implements ComponentCallbacks2 { } /** + * @hide + * Front-end to {@link #call(String, String, android.os.Bundle)} that provides the name + * of the calling package. + */ + public Bundle callFromPackage(String callingPackag, String method, String arg, Bundle extras) { + return call(method, arg, extras); + } + + /** * Call a provider-defined method. This can be used to implement * interfaces that are cheaper and/or unnatural for a table-like * model. * - * @param method method name to call. Opaque to framework, but should not be null. - * @param arg provider-defined String argument. May be null. - * @param extras provider-defined Bundle argument. May be null. - * @return provider-defined return value. May be null. Null is also + * @param method method name to call. Opaque to framework, but should not be {@code null}. + * @param arg provider-defined String argument. May be {@code null}. + * @param extras provider-defined Bundle argument. May be {@code null}. + * @return provider-defined return value. May be {@code null}, which is also * the default for providers which don't implement any call methods. */ public Bundle call(String method, String arg, Bundle extras) { @@ -1132,12 +1297,10 @@ public abstract class ContentProvider implements ComponentCallbacks2 { * Print the Provider's state into the given stream. This gets invoked if * you run "adb shell dumpsys activity provider <provider_component_name>". * - * @param prefix Desired prefix to prepend at each line of output. * @param fd The raw file descriptor that the dump is being sent to. * @param writer The PrintWriter to which you should dump your state. This will be * closed for you after you return. * @param args additional arguments to the dump request. - * @hide */ public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { writer.println("nothing to dump"); diff --git a/core/java/android/content/ContentProviderClient.java b/core/java/android/content/ContentProviderClient.java index 204f963..8dffac7 100644 --- a/core/java/android/content/ContentProviderClient.java +++ b/core/java/android/content/ContentProviderClient.java @@ -45,6 +45,7 @@ import java.util.ArrayList; public class ContentProviderClient { private final IContentProvider mContentProvider; private final ContentResolver mContentResolver; + private final String mPackageName; private final boolean mStable; private boolean mReleased; @@ -55,6 +56,7 @@ public class ContentProviderClient { IContentProvider contentProvider, boolean stable) { mContentProvider = contentProvider; mContentResolver = contentResolver; + mPackageName = contentResolver.mPackageName; mStable = stable; } @@ -81,8 +83,8 @@ public class ContentProviderClient { cancellationSignal.setRemote(remoteCancellationSignal); } try { - return mContentProvider.query(url, projection, selection, selectionArgs, sortOrder, - remoteCancellationSignal); + return mContentProvider.query(mPackageName, url, projection, selection, selectionArgs, + sortOrder, remoteCancellationSignal); } catch (DeadObjectException e) { if (!mStable) { mContentResolver.unstableProviderDied(mContentProvider); @@ -119,7 +121,7 @@ public class ContentProviderClient { public Uri insert(Uri url, ContentValues initialValues) throws RemoteException { try { - return mContentProvider.insert(url, initialValues); + return mContentProvider.insert(mPackageName, url, initialValues); } catch (DeadObjectException e) { if (!mStable) { mContentResolver.unstableProviderDied(mContentProvider); @@ -131,7 +133,7 @@ public class ContentProviderClient { /** See {@link ContentProvider#bulkInsert ContentProvider.bulkInsert} */ public int bulkInsert(Uri url, ContentValues[] initialValues) throws RemoteException { try { - return mContentProvider.bulkInsert(url, initialValues); + return mContentProvider.bulkInsert(mPackageName, url, initialValues); } catch (DeadObjectException e) { if (!mStable) { mContentResolver.unstableProviderDied(mContentProvider); @@ -144,7 +146,7 @@ public class ContentProviderClient { public int delete(Uri url, String selection, String[] selectionArgs) throws RemoteException { try { - return mContentProvider.delete(url, selection, selectionArgs); + return mContentProvider.delete(mPackageName, url, selection, selectionArgs); } catch (DeadObjectException e) { if (!mStable) { mContentResolver.unstableProviderDied(mContentProvider); @@ -157,7 +159,7 @@ public class ContentProviderClient { public int update(Uri url, ContentValues values, String selection, String[] selectionArgs) throws RemoteException { try { - return mContentProvider.update(url, values, selection, selectionArgs); + return mContentProvider.update(mPackageName, url, values, selection, selectionArgs); } catch (DeadObjectException e) { if (!mStable) { mContentResolver.unstableProviderDied(mContentProvider); @@ -176,7 +178,7 @@ public class ContentProviderClient { public ParcelFileDescriptor openFile(Uri url, String mode) throws RemoteException, FileNotFoundException { try { - return mContentProvider.openFile(url, mode); + return mContentProvider.openFile(mPackageName, url, mode); } catch (DeadObjectException e) { if (!mStable) { mContentResolver.unstableProviderDied(mContentProvider); @@ -195,7 +197,7 @@ public class ContentProviderClient { public AssetFileDescriptor openAssetFile(Uri url, String mode) throws RemoteException, FileNotFoundException { try { - return mContentProvider.openAssetFile(url, mode); + return mContentProvider.openAssetFile(mPackageName, url, mode); } catch (DeadObjectException e) { if (!mStable) { mContentResolver.unstableProviderDied(mContentProvider); @@ -209,7 +211,7 @@ public class ContentProviderClient { String mimeType, Bundle opts) throws RemoteException, FileNotFoundException { try { - return mContentProvider.openTypedAssetFile(uri, mimeType, opts); + return mContentProvider.openTypedAssetFile(mPackageName, uri, mimeType, opts); } catch (DeadObjectException e) { if (!mStable) { mContentResolver.unstableProviderDied(mContentProvider); @@ -222,7 +224,7 @@ public class ContentProviderClient { public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) throws RemoteException, OperationApplicationException { try { - return mContentProvider.applyBatch(operations); + return mContentProvider.applyBatch(mPackageName, operations); } catch (DeadObjectException e) { if (!mStable) { mContentResolver.unstableProviderDied(mContentProvider); @@ -235,7 +237,7 @@ public class ContentProviderClient { public Bundle call(String method, String arg, Bundle extras) throws RemoteException { try { - return mContentProvider.call(method, arg, extras); + return mContentProvider.call(mPackageName, method, arg, extras); } catch (DeadObjectException e) { if (!mStable) { mContentResolver.unstableProviderDied(mContentProvider); diff --git a/core/java/android/content/ContentProviderNative.java b/core/java/android/content/ContentProviderNative.java index 550a1c9..6f822c1 100644 --- a/core/java/android/content/ContentProviderNative.java +++ b/core/java/android/content/ContentProviderNative.java @@ -81,6 +81,7 @@ abstract public class ContentProviderNative extends Binder implements IContentPr { data.enforceInterface(IContentProvider.descriptor); + String callingPkg = data.readString(); Uri url = Uri.CREATOR.createFromParcel(data); // String[] projection @@ -110,16 +111,24 @@ abstract public class ContentProviderNative extends Binder implements IContentPr ICancellationSignal cancellationSignal = ICancellationSignal.Stub.asInterface( data.readStrongBinder()); - Cursor cursor = query(url, projection, selection, selectionArgs, sortOrder, - cancellationSignal); + Cursor cursor = query(callingPkg, url, projection, selection, selectionArgs, + sortOrder, cancellationSignal); if (cursor != null) { - CursorToBulkCursorAdaptor adaptor = new CursorToBulkCursorAdaptor( - cursor, observer, getProviderName()); - BulkCursorDescriptor d = adaptor.getBulkCursorDescriptor(); - - reply.writeNoException(); - reply.writeInt(1); - d.writeToParcel(reply, Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + try { + CursorToBulkCursorAdaptor adaptor = new CursorToBulkCursorAdaptor( + cursor, observer, getProviderName()); + BulkCursorDescriptor d = adaptor.getBulkCursorDescriptor(); + cursor = null; + + reply.writeNoException(); + reply.writeInt(1); + d.writeToParcel(reply, Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + } finally { + // Close cursor if an exception was thrown while constructing the adaptor. + if (cursor != null) { + cursor.close(); + } + } } else { reply.writeNoException(); reply.writeInt(0); @@ -142,10 +151,11 @@ abstract public class ContentProviderNative extends Binder implements IContentPr case INSERT_TRANSACTION: { data.enforceInterface(IContentProvider.descriptor); + String callingPkg = data.readString(); Uri url = Uri.CREATOR.createFromParcel(data); ContentValues values = ContentValues.CREATOR.createFromParcel(data); - Uri out = insert(url, values); + Uri out = insert(callingPkg, url, values); reply.writeNoException(); Uri.writeToParcel(reply, out); return true; @@ -154,10 +164,11 @@ abstract public class ContentProviderNative extends Binder implements IContentPr case BULK_INSERT_TRANSACTION: { data.enforceInterface(IContentProvider.descriptor); + String callingPkg = data.readString(); Uri url = Uri.CREATOR.createFromParcel(data); ContentValues[] values = data.createTypedArray(ContentValues.CREATOR); - int count = bulkInsert(url, values); + int count = bulkInsert(callingPkg, url, values); reply.writeNoException(); reply.writeInt(count); return true; @@ -166,13 +177,14 @@ abstract public class ContentProviderNative extends Binder implements IContentPr case APPLY_BATCH_TRANSACTION: { data.enforceInterface(IContentProvider.descriptor); + String callingPkg = data.readString(); final int numOperations = data.readInt(); final ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>(numOperations); for (int i = 0; i < numOperations; i++) { operations.add(i, ContentProviderOperation.CREATOR.createFromParcel(data)); } - final ContentProviderResult[] results = applyBatch(operations); + final ContentProviderResult[] results = applyBatch(callingPkg, operations); reply.writeNoException(); reply.writeTypedArray(results, 0); return true; @@ -181,11 +193,12 @@ abstract public class ContentProviderNative extends Binder implements IContentPr case DELETE_TRANSACTION: { data.enforceInterface(IContentProvider.descriptor); + String callingPkg = data.readString(); Uri url = Uri.CREATOR.createFromParcel(data); String selection = data.readString(); String[] selectionArgs = data.readStringArray(); - int count = delete(url, selection, selectionArgs); + int count = delete(callingPkg, url, selection, selectionArgs); reply.writeNoException(); reply.writeInt(count); @@ -195,12 +208,13 @@ abstract public class ContentProviderNative extends Binder implements IContentPr case UPDATE_TRANSACTION: { data.enforceInterface(IContentProvider.descriptor); + String callingPkg = data.readString(); Uri url = Uri.CREATOR.createFromParcel(data); ContentValues values = ContentValues.CREATOR.createFromParcel(data); String selection = data.readString(); String[] selectionArgs = data.readStringArray(); - int count = update(url, values, selection, selectionArgs); + int count = update(callingPkg, url, values, selection, selectionArgs); reply.writeNoException(); reply.writeInt(count); @@ -210,11 +224,12 @@ abstract public class ContentProviderNative extends Binder implements IContentPr case OPEN_FILE_TRANSACTION: { data.enforceInterface(IContentProvider.descriptor); + String callingPkg = data.readString(); Uri url = Uri.CREATOR.createFromParcel(data); String mode = data.readString(); ParcelFileDescriptor fd; - fd = openFile(url, mode); + fd = openFile(callingPkg, url, mode); reply.writeNoException(); if (fd != null) { reply.writeInt(1); @@ -229,11 +244,12 @@ abstract public class ContentProviderNative extends Binder implements IContentPr case OPEN_ASSET_FILE_TRANSACTION: { data.enforceInterface(IContentProvider.descriptor); + String callingPkg = data.readString(); Uri url = Uri.CREATOR.createFromParcel(data); String mode = data.readString(); AssetFileDescriptor fd; - fd = openAssetFile(url, mode); + fd = openAssetFile(callingPkg, url, mode); reply.writeNoException(); if (fd != null) { reply.writeInt(1); @@ -249,11 +265,12 @@ abstract public class ContentProviderNative extends Binder implements IContentPr { data.enforceInterface(IContentProvider.descriptor); + String callingPkg = data.readString(); String method = data.readString(); String stringArg = data.readString(); Bundle args = data.readBundle(); - Bundle responseBundle = call(method, stringArg, args); + Bundle responseBundle = call(callingPkg, method, stringArg, args); reply.writeNoException(); reply.writeBundle(responseBundle); @@ -275,12 +292,13 @@ abstract public class ContentProviderNative extends Binder implements IContentPr case OPEN_TYPED_ASSET_FILE_TRANSACTION: { data.enforceInterface(IContentProvider.descriptor); + String callingPkg = data.readString(); Uri url = Uri.CREATOR.createFromParcel(data); String mimeType = data.readString(); Bundle opts = data.readBundle(); AssetFileDescriptor fd; - fd = openTypedAssetFile(url, mimeType, opts); + fd = openTypedAssetFile(callingPkg, url, mimeType, opts); reply.writeNoException(); if (fd != null) { reply.writeInt(1); @@ -329,7 +347,7 @@ final class ContentProviderProxy implements IContentProvider return mRemote; } - public Cursor query(Uri url, String[] projection, String selection, + public Cursor query(String callingPkg, Uri url, String[] projection, String selection, String[] selectionArgs, String sortOrder, ICancellationSignal cancellationSignal) throws RemoteException { BulkCursorToCursorAdaptor adaptor = new BulkCursorToCursorAdaptor(); @@ -338,6 +356,7 @@ final class ContentProviderProxy implements IContentProvider try { data.writeInterfaceToken(IContentProvider.descriptor); + data.writeString(callingPkg); url.writeToParcel(data, 0); int length = 0; if (projection != null) { @@ -405,13 +424,14 @@ final class ContentProviderProxy implements IContentProvider } } - public Uri insert(Uri url, ContentValues values) throws RemoteException + public Uri insert(String callingPkg, Uri url, ContentValues values) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); try { data.writeInterfaceToken(IContentProvider.descriptor); + data.writeString(callingPkg); url.writeToParcel(data, 0); values.writeToParcel(data, 0); @@ -426,12 +446,13 @@ final class ContentProviderProxy implements IContentProvider } } - public int bulkInsert(Uri url, ContentValues[] values) throws RemoteException { + public int bulkInsert(String callingPkg, Uri url, ContentValues[] values) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); try { data.writeInterfaceToken(IContentProvider.descriptor); + data.writeString(callingPkg); url.writeToParcel(data, 0); data.writeTypedArray(values, 0); @@ -446,12 +467,14 @@ final class ContentProviderProxy implements IContentProvider } } - public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) - throws RemoteException, OperationApplicationException { + public ContentProviderResult[] applyBatch(String callingPkg, + ArrayList<ContentProviderOperation> operations) + throws RemoteException, OperationApplicationException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); try { data.writeInterfaceToken(IContentProvider.descriptor); + data.writeString(callingPkg); data.writeInt(operations.size()); for (ContentProviderOperation operation : operations) { operation.writeToParcel(data, 0); @@ -468,13 +491,14 @@ final class ContentProviderProxy implements IContentProvider } } - public int delete(Uri url, String selection, String[] selectionArgs) + public int delete(String callingPkg, Uri url, String selection, String[] selectionArgs) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); try { data.writeInterfaceToken(IContentProvider.descriptor); + data.writeString(callingPkg); url.writeToParcel(data, 0); data.writeString(selection); data.writeStringArray(selectionArgs); @@ -490,13 +514,14 @@ final class ContentProviderProxy implements IContentProvider } } - public int update(Uri url, ContentValues values, String selection, + public int update(String callingPkg, Uri url, ContentValues values, String selection, String[] selectionArgs) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); try { data.writeInterfaceToken(IContentProvider.descriptor); + data.writeString(callingPkg); url.writeToParcel(data, 0); values.writeToParcel(data, 0); data.writeString(selection); @@ -513,13 +538,14 @@ final class ContentProviderProxy implements IContentProvider } } - public ParcelFileDescriptor openFile(Uri url, String mode) + public ParcelFileDescriptor openFile(String callingPkg, Uri url, String mode) throws RemoteException, FileNotFoundException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); try { data.writeInterfaceToken(IContentProvider.descriptor); + data.writeString(callingPkg); url.writeToParcel(data, 0); data.writeString(mode); @@ -535,13 +561,14 @@ final class ContentProviderProxy implements IContentProvider } } - public AssetFileDescriptor openAssetFile(Uri url, String mode) + public AssetFileDescriptor openAssetFile(String callingPkg, Uri url, String mode) throws RemoteException, FileNotFoundException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); try { data.writeInterfaceToken(IContentProvider.descriptor); + data.writeString(callingPkg); url.writeToParcel(data, 0); data.writeString(mode); @@ -558,13 +585,14 @@ final class ContentProviderProxy implements IContentProvider } } - public Bundle call(String method, String request, Bundle args) + public Bundle call(String callingPkg, String method, String request, Bundle args) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); try { data.writeInterfaceToken(IContentProvider.descriptor); + data.writeString(callingPkg); data.writeString(method); data.writeString(request); data.writeBundle(args); @@ -601,13 +629,14 @@ final class ContentProviderProxy implements IContentProvider } } - public AssetFileDescriptor openTypedAssetFile(Uri url, String mimeType, Bundle opts) - throws RemoteException, FileNotFoundException { + public AssetFileDescriptor openTypedAssetFile(String callingPkg, Uri url, String mimeType, + Bundle opts) throws RemoteException, FileNotFoundException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); try { data.writeInterfaceToken(IContentProvider.descriptor); + data.writeString(callingPkg); url.writeToParcel(data, 0); data.writeString(mimeType); data.writeBundle(opts); diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index bde4d2b..fefd343 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -20,6 +20,7 @@ import dalvik.system.CloseGuard; import android.accounts.Account; import android.app.ActivityManagerNative; +import android.app.ActivityThread; import android.app.AppGlobals; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.AssetFileDescriptor; @@ -116,6 +117,10 @@ public abstract class ContentResolver { */ public static final String SYNC_EXTRAS_INITIALIZE = "initialize"; + /** @hide */ + public static final Intent ACTION_SYNC_CONN_STATUS_CHANGED = + new Intent("com.android.sync.SYNC_CONN_STATUS_CHANGED"); + public static final String SCHEME_CONTENT = "content"; public static final String SCHEME_ANDROID_RESOURCE = "android.resource"; public static final String SCHEME_FILE = "file"; @@ -169,6 +174,42 @@ public abstract class ContentResolver { /** @hide */ public static final int SYNC_ERROR_INTERNAL = 8; + private static final String[] SYNC_ERROR_NAMES = new String[] { + "already-in-progress", + "authentication-error", + "io-error", + "parse-error", + "conflict", + "too-many-deletions", + "too-many-retries", + "internal-error", + }; + + /** @hide */ + public static String syncErrorToString(int error) { + if (error < 1 || error > SYNC_ERROR_NAMES.length) { + return String.valueOf(error); + } + return SYNC_ERROR_NAMES[error - 1]; + } + + /** @hide */ + public static int syncErrorStringToInt(String error) { + for (int i = 0, n = SYNC_ERROR_NAMES.length; i < n; i++) { + if (SYNC_ERROR_NAMES[i].equals(error)) { + return i + 1; + } + } + if (error != null) { + try { + return Integer.parseInt(error); + } catch (NumberFormatException e) { + Log.d(TAG, "error parsing sync error: " + error); + } + } + return 0; + } + public static final int SYNC_OBSERVER_TYPE_SETTINGS = 1<<0; public static final int SYNC_OBSERVER_TYPE_PENDING = 1<<1; public static final int SYNC_OBSERVER_TYPE_ACTIVE = 1<<2; @@ -183,7 +224,8 @@ public abstract class ContentResolver { private final Random mRandom = new Random(); // guarded by itself public ContentResolver(Context context) { - mContext = context; + mContext = context != null ? context : ActivityThread.currentApplication(); + mPackageName = mContext.getBasePackageName(); } /** @hide */ @@ -358,6 +400,7 @@ public abstract class ContentResolver { return null; } IContentProvider stableProvider = null; + Cursor qCursor = null; try { long startTime = SystemClock.uptimeMillis(); @@ -367,9 +410,8 @@ public abstract class ContentResolver { remoteCancellationSignal = unstableProvider.createCancellationSignal(); cancellationSignal.setRemote(remoteCancellationSignal); } - Cursor qCursor; try { - qCursor = unstableProvider.query(uri, projection, + qCursor = unstableProvider.query(mPackageName, uri, projection, selection, selectionArgs, sortOrder, remoteCancellationSignal); } catch (DeadObjectException e) { // The remote process has died... but we only hold an unstable @@ -380,26 +422,32 @@ public abstract class ContentResolver { if (stableProvider == null) { return null; } - qCursor = stableProvider.query(uri, projection, + qCursor = stableProvider.query(mPackageName, uri, projection, selection, selectionArgs, sortOrder, remoteCancellationSignal); } if (qCursor == null) { return null; } - // force query execution + + // Force query execution. Might fail and throw a runtime exception here. qCursor.getCount(); long durationMillis = SystemClock.uptimeMillis() - startTime; maybeLogQueryToEventLog(durationMillis, uri, projection, selection, sortOrder); - // Wrap the cursor object into CursorWrapperInner object + + // Wrap the cursor object into CursorWrapperInner object. CursorWrapperInner wrapper = new CursorWrapperInner(qCursor, stableProvider != null ? stableProvider : acquireProvider(uri)); stableProvider = null; + qCursor = null; return wrapper; } catch (RemoteException e) { // Arbitrary and not worth documenting, as Activity // Manager will kill this process shortly anyway. return null; } finally { + if (qCursor != null) { + qCursor.close(); + } if (unstableProvider != null) { releaseUnstableProvider(unstableProvider); } @@ -622,7 +670,7 @@ public abstract class ContentResolver { try { try { - fd = unstableProvider.openAssetFile(uri, mode); + fd = unstableProvider.openAssetFile(mPackageName, uri, mode); if (fd == null) { // The provider will be released by the finally{} clause return null; @@ -636,7 +684,7 @@ public abstract class ContentResolver { if (stableProvider == null) { throw new FileNotFoundException("No content provider: " + uri); } - fd = stableProvider.openAssetFile(uri, mode); + fd = stableProvider.openAssetFile(mPackageName, uri, mode); if (fd == null) { // The provider will be released by the finally{} clause return null; @@ -714,7 +762,7 @@ public abstract class ContentResolver { try { try { - fd = unstableProvider.openTypedAssetFile(uri, mimeType, opts); + fd = unstableProvider.openTypedAssetFile(mPackageName, uri, mimeType, opts); if (fd == null) { // The provider will be released by the finally{} clause return null; @@ -728,7 +776,7 @@ public abstract class ContentResolver { if (stableProvider == null) { throw new FileNotFoundException("No content provider: " + uri); } - fd = stableProvider.openTypedAssetFile(uri, mimeType, opts); + fd = stableProvider.openTypedAssetFile(mPackageName, uri, mimeType, opts); if (fd == null) { // The provider will be released by the finally{} clause return null; @@ -863,7 +911,7 @@ public abstract class ContentResolver { } try { long startTime = SystemClock.uptimeMillis(); - Uri createdRow = provider.insert(url, values); + Uri createdRow = provider.insert(mPackageName, url, values); long durationMillis = SystemClock.uptimeMillis() - startTime; maybeLogUpdateToEventLog(durationMillis, url, "insert", null /* where */); return createdRow; @@ -924,7 +972,7 @@ public abstract class ContentResolver { } try { long startTime = SystemClock.uptimeMillis(); - int rowsCreated = provider.bulkInsert(url, values); + int rowsCreated = provider.bulkInsert(mPackageName, url, values); long durationMillis = SystemClock.uptimeMillis() - startTime; maybeLogUpdateToEventLog(durationMillis, url, "bulkinsert", null /* where */); return rowsCreated; @@ -955,7 +1003,7 @@ public abstract class ContentResolver { } try { long startTime = SystemClock.uptimeMillis(); - int rowsDeleted = provider.delete(url, where, selectionArgs); + int rowsDeleted = provider.delete(mPackageName, url, where, selectionArgs); long durationMillis = SystemClock.uptimeMillis() - startTime; maybeLogUpdateToEventLog(durationMillis, url, "delete", where); return rowsDeleted; @@ -989,7 +1037,7 @@ public abstract class ContentResolver { } try { long startTime = SystemClock.uptimeMillis(); - int rowsUpdated = provider.update(uri, values, where, selectionArgs); + int rowsUpdated = provider.update(mPackageName, uri, values, where, selectionArgs); long durationMillis = SystemClock.uptimeMillis() - startTime; maybeLogUpdateToEventLog(durationMillis, uri, "update", where); return rowsUpdated; @@ -1028,7 +1076,7 @@ public abstract class ContentResolver { throw new IllegalArgumentException("Unknown URI " + uri); } try { - return provider.call(method, arg, extras); + return provider.call(mPackageName, method, arg, extras); } catch (RemoteException e) { // Arbitrary and not worth documenting, as Activity // Manager will kill this process shortly anyway. @@ -1926,7 +1974,13 @@ public abstract class ContentResolver { return sContentService; } + /** @hide */ + public String getPackageName() { + return mPackageName; + } + private static IContentService sContentService; private final Context mContext; + final String mPackageName; private static final String TAG = "ContentResolver"; } diff --git a/core/java/android/content/ContentService.java b/core/java/android/content/ContentService.java deleted file mode 100644 index 4512e82..0000000 --- a/core/java/android/content/ContentService.java +++ /dev/null @@ -1,838 +0,0 @@ -/* - * Copyright (C) 2006 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.content; - -import android.accounts.Account; -import android.app.ActivityManager; -import android.database.IContentObserver; -import android.database.sqlite.SQLiteException; -import android.net.Uri; -import android.os.Binder; -import android.os.Bundle; -import android.os.IBinder; -import android.os.Parcel; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.UserHandle; -import android.util.Log; -import android.util.SparseIntArray; -import android.Manifest; - -import java.io.FileDescriptor; -import java.io.PrintWriter; -import java.security.InvalidParameterException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -/** - * {@hide} - */ -public final class ContentService extends IContentService.Stub { - private static final String TAG = "ContentService"; - private Context mContext; - private boolean mFactoryTest; - private final ObserverNode mRootNode = new ObserverNode(""); - private SyncManager mSyncManager = null; - private final Object mSyncManagerLock = new Object(); - - private SyncManager getSyncManager() { - synchronized(mSyncManagerLock) { - try { - // Try to create the SyncManager, return null if it fails (e.g. the disk is full). - if (mSyncManager == null) mSyncManager = new SyncManager(mContext, mFactoryTest); - } catch (SQLiteException e) { - Log.e(TAG, "Can't create SyncManager", e); - } - return mSyncManager; - } - } - - @Override - protected synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - mContext.enforceCallingOrSelfPermission(Manifest.permission.DUMP, - "caller doesn't have the DUMP permission"); - - // This makes it so that future permission checks will be in the context of this - // process rather than the caller's process. We will restore this before returning. - long identityToken = clearCallingIdentity(); - try { - if (mSyncManager == null) { - pw.println("No SyncManager created! (Disk full?)"); - } else { - mSyncManager.dump(fd, pw); - } - pw.println(); - pw.println("Observer tree:"); - synchronized (mRootNode) { - int[] counts = new int[2]; - final SparseIntArray pidCounts = new SparseIntArray(); - mRootNode.dumpLocked(fd, pw, args, "", " ", counts, pidCounts); - pw.println(); - ArrayList<Integer> sorted = new ArrayList<Integer>(); - for (int i=0; i<pidCounts.size(); i++) { - sorted.add(pidCounts.keyAt(i)); - } - Collections.sort(sorted, new Comparator<Integer>() { - @Override - public int compare(Integer lhs, Integer rhs) { - int lc = pidCounts.get(lhs); - int rc = pidCounts.get(rhs); - if (lc < rc) { - return 1; - } else if (lc > rc) { - return -1; - } - return 0; - } - - }); - for (int i=0; i<sorted.size(); i++) { - int pid = sorted.get(i); - pw.print(" pid "); pw.print(pid); pw.print(": "); - pw.print(pidCounts.get(pid)); pw.println(" observers"); - } - pw.println(); - pw.print(" Total number of nodes: "); pw.println(counts[0]); - pw.print(" Total number of observers: "); pw.println(counts[1]); - } - } finally { - restoreCallingIdentity(identityToken); - } - } - - @Override - public boolean onTransact(int code, Parcel data, Parcel reply, int flags) - throws RemoteException { - try { - return super.onTransact(code, data, reply, flags); - } catch (RuntimeException e) { - // The content service only throws security exceptions, so let's - // log all others. - if (!(e instanceof SecurityException)) { - Log.e(TAG, "Content Service Crash", e); - } - throw e; - } - } - - /*package*/ ContentService(Context context, boolean factoryTest) { - mContext = context; - mFactoryTest = factoryTest; - } - - public void systemReady() { - getSyncManager(); - } - - /** - * Register a content observer tied to a specific user's view of the provider. - * @param userHandle the user whose view of the provider is to be observed. May be - * the calling user without requiring any permission, otherwise the caller needs to - * hold the INTERACT_ACROSS_USERS_FULL permission. Pseudousers USER_ALL and - * USER_CURRENT are properly handled; all other pseudousers are forbidden. - */ - @Override - public void registerContentObserver(Uri uri, boolean notifyForDescendants, - IContentObserver observer, int userHandle) { - if (observer == null || uri == null) { - throw new IllegalArgumentException("You must pass a valid uri and observer"); - } - - final int callingUser = UserHandle.getCallingUserId(); - if (callingUser != userHandle) { - mContext.enforceCallingOrSelfPermission(Manifest.permission.INTERACT_ACROSS_USERS_FULL, - "no permission to observe other users' provider view"); - } - - if (userHandle < 0) { - if (userHandle == UserHandle.USER_CURRENT) { - userHandle = ActivityManager.getCurrentUser(); - } else if (userHandle != UserHandle.USER_ALL) { - throw new InvalidParameterException("Bad user handle for registerContentObserver: " - + userHandle); - } - } - - synchronized (mRootNode) { - mRootNode.addObserverLocked(uri, observer, notifyForDescendants, mRootNode, - Binder.getCallingUid(), Binder.getCallingPid(), userHandle); - if (false) Log.v(TAG, "Registered observer " + observer + " at " + uri + - " with notifyForDescendants " + notifyForDescendants); - } - } - - public void registerContentObserver(Uri uri, boolean notifyForDescendants, - IContentObserver observer) { - registerContentObserver(uri, notifyForDescendants, observer, - UserHandle.getCallingUserId()); - } - - public void unregisterContentObserver(IContentObserver observer) { - if (observer == null) { - throw new IllegalArgumentException("You must pass a valid observer"); - } - synchronized (mRootNode) { - mRootNode.removeObserverLocked(observer); - if (false) Log.v(TAG, "Unregistered observer " + observer); - } - } - - /** - * Notify observers of a particular user's view of the provider. - * @param userHandle the user whose view of the provider is to be notified. May be - * the calling user without requiring any permission, otherwise the caller needs to - * hold the INTERACT_ACROSS_USERS_FULL permission. Pseudousers USER_ALL and - * USER_CURRENT are properly interpreted; no other pseudousers are allowed. - */ - @Override - public void notifyChange(Uri uri, IContentObserver observer, - boolean observerWantsSelfNotifications, boolean syncToNetwork, - int userHandle) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "Notifying update of " + uri + " for user " + userHandle - + " from observer " + observer + ", syncToNetwork " + syncToNetwork); - } - - // Notify for any user other than the caller's own requires permission. - final int callingUserHandle = UserHandle.getCallingUserId(); - if (userHandle != callingUserHandle) { - mContext.enforceCallingOrSelfPermission(Manifest.permission.INTERACT_ACROSS_USERS_FULL, - "no permission to notify other users"); - } - - // We passed the permission check; resolve pseudouser targets as appropriate - if (userHandle < 0) { - if (userHandle == UserHandle.USER_CURRENT) { - userHandle = ActivityManager.getCurrentUser(); - } else if (userHandle != UserHandle.USER_ALL) { - throw new InvalidParameterException("Bad user handle for notifyChange: " - + userHandle); - } - } - - // This makes it so that future permission checks will be in the context of this - // process rather than the caller's process. We will restore this before returning. - long identityToken = clearCallingIdentity(); - try { - ArrayList<ObserverCall> calls = new ArrayList<ObserverCall>(); - synchronized (mRootNode) { - mRootNode.collectObserversLocked(uri, 0, observer, observerWantsSelfNotifications, - userHandle, calls); - } - final int numCalls = calls.size(); - for (int i=0; i<numCalls; i++) { - ObserverCall oc = calls.get(i); - try { - oc.mObserver.onChange(oc.mSelfChange, uri); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "Notified " + oc.mObserver + " of " + "update at " + uri); - } - } catch (RemoteException ex) { - synchronized (mRootNode) { - Log.w(TAG, "Found dead observer, removing"); - IBinder binder = oc.mObserver.asBinder(); - final ArrayList<ObserverNode.ObserverEntry> list - = oc.mNode.mObservers; - int numList = list.size(); - for (int j=0; j<numList; j++) { - ObserverNode.ObserverEntry oe = list.get(j); - if (oe.observer.asBinder() == binder) { - list.remove(j); - j--; - numList--; - } - } - } - } - } - if (syncToNetwork) { - SyncManager syncManager = getSyncManager(); - if (syncManager != null) { - syncManager.scheduleLocalSync(null /* all accounts */, callingUserHandle, - uri.getAuthority()); - } - } - } finally { - restoreCallingIdentity(identityToken); - } - } - - public void notifyChange(Uri uri, IContentObserver observer, - boolean observerWantsSelfNotifications, boolean syncToNetwork) { - notifyChange(uri, observer, observerWantsSelfNotifications, syncToNetwork, - UserHandle.getCallingUserId()); - } - - /** - * Hide this class since it is not part of api, - * but current unittest framework requires it to be public - * @hide - * - */ - public static final class ObserverCall { - final ObserverNode mNode; - final IContentObserver mObserver; - final boolean mSelfChange; - - ObserverCall(ObserverNode node, IContentObserver observer, boolean selfChange) { - mNode = node; - mObserver = observer; - mSelfChange = selfChange; - } - } - - public void requestSync(Account account, String authority, Bundle extras) { - ContentResolver.validateSyncExtrasBundle(extras); - int userId = UserHandle.getCallingUserId(); - - // This makes it so that future permission checks will be in the context of this - // process rather than the caller's process. We will restore this before returning. - long identityToken = clearCallingIdentity(); - try { - SyncManager syncManager = getSyncManager(); - if (syncManager != null) { - syncManager.scheduleSync(account, userId, authority, extras, 0 /* no delay */, - false /* onlyThoseWithUnkownSyncableState */); - } - } finally { - restoreCallingIdentity(identityToken); - } - } - - /** - * Clear all scheduled sync operations that match the uri and cancel the active sync - * if they match the authority and account, if they are present. - * @param account filter the pending and active syncs to cancel using this account - * @param authority filter the pending and active syncs to cancel using this authority - */ - public void cancelSync(Account account, String authority) { - int userId = UserHandle.getCallingUserId(); - - // This makes it so that future permission checks will be in the context of this - // process rather than the caller's process. We will restore this before returning. - long identityToken = clearCallingIdentity(); - try { - SyncManager syncManager = getSyncManager(); - if (syncManager != null) { - syncManager.clearScheduledSyncOperations(account, userId, authority); - syncManager.cancelActiveSync(account, userId, authority); - } - } finally { - restoreCallingIdentity(identityToken); - } - } - - /** - * Get information about the SyncAdapters that are known to the system. - * @return an array of SyncAdapters that have registered with the system - */ - public SyncAdapterType[] getSyncAdapterTypes() { - // This makes it so that future permission checks will be in the context of this - // process rather than the caller's process. We will restore this before returning. - final int userId = UserHandle.getCallingUserId(); - final long identityToken = clearCallingIdentity(); - try { - SyncManager syncManager = getSyncManager(); - return syncManager.getSyncAdapterTypes(userId); - } finally { - restoreCallingIdentity(identityToken); - } - } - - public boolean getSyncAutomatically(Account account, String providerName) { - mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS, - "no permission to read the sync settings"); - int userId = UserHandle.getCallingUserId(); - - long identityToken = clearCallingIdentity(); - try { - SyncManager syncManager = getSyncManager(); - if (syncManager != null) { - return syncManager.getSyncStorageEngine().getSyncAutomatically( - account, userId, providerName); - } - } finally { - restoreCallingIdentity(identityToken); - } - return false; - } - - public void setSyncAutomatically(Account account, String providerName, boolean sync) { - mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS, - "no permission to write the sync settings"); - int userId = UserHandle.getCallingUserId(); - - long identityToken = clearCallingIdentity(); - try { - SyncManager syncManager = getSyncManager(); - if (syncManager != null) { - syncManager.getSyncStorageEngine().setSyncAutomatically( - account, userId, providerName, sync); - } - } finally { - restoreCallingIdentity(identityToken); - } - } - - public void addPeriodicSync(Account account, String authority, Bundle extras, - long pollFrequency) { - mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS, - "no permission to write the sync settings"); - int userId = UserHandle.getCallingUserId(); - - long identityToken = clearCallingIdentity(); - try { - getSyncManager().getSyncStorageEngine().addPeriodicSync( - account, userId, authority, extras, pollFrequency); - } finally { - restoreCallingIdentity(identityToken); - } - } - - public void removePeriodicSync(Account account, String authority, Bundle extras) { - mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS, - "no permission to write the sync settings"); - int userId = UserHandle.getCallingUserId(); - - long identityToken = clearCallingIdentity(); - try { - getSyncManager().getSyncStorageEngine().removePeriodicSync(account, userId, authority, - extras); - } finally { - restoreCallingIdentity(identityToken); - } - } - - public List<PeriodicSync> getPeriodicSyncs(Account account, String providerName) { - mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS, - "no permission to read the sync settings"); - int userId = UserHandle.getCallingUserId(); - - long identityToken = clearCallingIdentity(); - try { - return getSyncManager().getSyncStorageEngine().getPeriodicSyncs( - account, userId, providerName); - } finally { - restoreCallingIdentity(identityToken); - } - } - - public int getIsSyncable(Account account, String providerName) { - mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS, - "no permission to read the sync settings"); - int userId = UserHandle.getCallingUserId(); - - long identityToken = clearCallingIdentity(); - try { - SyncManager syncManager = getSyncManager(); - if (syncManager != null) { - return syncManager.getSyncStorageEngine().getIsSyncable( - account, userId, providerName); - } - } finally { - restoreCallingIdentity(identityToken); - } - return -1; - } - - public void setIsSyncable(Account account, String providerName, int syncable) { - mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS, - "no permission to write the sync settings"); - int userId = UserHandle.getCallingUserId(); - - long identityToken = clearCallingIdentity(); - try { - SyncManager syncManager = getSyncManager(); - if (syncManager != null) { - syncManager.getSyncStorageEngine().setIsSyncable( - account, userId, providerName, syncable); - } - } finally { - restoreCallingIdentity(identityToken); - } - } - - public boolean getMasterSyncAutomatically() { - mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS, - "no permission to read the sync settings"); - int userId = UserHandle.getCallingUserId(); - - long identityToken = clearCallingIdentity(); - try { - SyncManager syncManager = getSyncManager(); - if (syncManager != null) { - return syncManager.getSyncStorageEngine().getMasterSyncAutomatically(userId); - } - } finally { - restoreCallingIdentity(identityToken); - } - return false; - } - - public void setMasterSyncAutomatically(boolean flag) { - mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS, - "no permission to write the sync settings"); - int userId = UserHandle.getCallingUserId(); - - long identityToken = clearCallingIdentity(); - try { - SyncManager syncManager = getSyncManager(); - if (syncManager != null) { - syncManager.getSyncStorageEngine().setMasterSyncAutomatically(flag, userId); - } - } finally { - restoreCallingIdentity(identityToken); - } - } - - public boolean isSyncActive(Account account, String authority) { - mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS, - "no permission to read the sync stats"); - int userId = UserHandle.getCallingUserId(); - - long identityToken = clearCallingIdentity(); - try { - SyncManager syncManager = getSyncManager(); - if (syncManager != null) { - return syncManager.getSyncStorageEngine().isSyncActive( - account, userId, authority); - } - } finally { - restoreCallingIdentity(identityToken); - } - return false; - } - - public List<SyncInfo> getCurrentSyncs() { - mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS, - "no permission to read the sync stats"); - int userId = UserHandle.getCallingUserId(); - - long identityToken = clearCallingIdentity(); - try { - return getSyncManager().getSyncStorageEngine().getCurrentSyncs(userId); - } finally { - restoreCallingIdentity(identityToken); - } - } - - public SyncStatusInfo getSyncStatus(Account account, String authority) { - mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS, - "no permission to read the sync stats"); - int userId = UserHandle.getCallingUserId(); - - long identityToken = clearCallingIdentity(); - try { - SyncManager syncManager = getSyncManager(); - if (syncManager != null) { - return syncManager.getSyncStorageEngine().getStatusByAccountAndAuthority( - account, userId, authority); - } - } finally { - restoreCallingIdentity(identityToken); - } - return null; - } - - public boolean isSyncPending(Account account, String authority) { - mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS, - "no permission to read the sync stats"); - int userId = UserHandle.getCallingUserId(); - - long identityToken = clearCallingIdentity(); - try { - SyncManager syncManager = getSyncManager(); - if (syncManager != null) { - return syncManager.getSyncStorageEngine().isSyncPending(account, userId, authority); - } - } finally { - restoreCallingIdentity(identityToken); - } - return false; - } - - public void addStatusChangeListener(int mask, ISyncStatusObserver callback) { - long identityToken = clearCallingIdentity(); - try { - SyncManager syncManager = getSyncManager(); - if (syncManager != null && callback != null) { - syncManager.getSyncStorageEngine().addStatusChangeListener(mask, callback); - } - } finally { - restoreCallingIdentity(identityToken); - } - } - - public void removeStatusChangeListener(ISyncStatusObserver callback) { - long identityToken = clearCallingIdentity(); - try { - SyncManager syncManager = getSyncManager(); - if (syncManager != null && callback != null) { - syncManager.getSyncStorageEngine().removeStatusChangeListener(callback); - } - } finally { - restoreCallingIdentity(identityToken); - } - } - - public static ContentService main(Context context, boolean factoryTest) { - ContentService service = new ContentService(context, factoryTest); - ServiceManager.addService(ContentResolver.CONTENT_SERVICE_NAME, service); - return service; - } - - /** - * Hide this class since it is not part of api, - * but current unittest framework requires it to be public - * @hide - */ - public static final class ObserverNode { - private class ObserverEntry implements IBinder.DeathRecipient { - public final IContentObserver observer; - public final int uid; - public final int pid; - public final boolean notifyForDescendants; - private final int userHandle; - private final Object observersLock; - - public ObserverEntry(IContentObserver o, boolean n, Object observersLock, - int _uid, int _pid, int _userHandle) { - this.observersLock = observersLock; - observer = o; - uid = _uid; - pid = _pid; - userHandle = _userHandle; - notifyForDescendants = n; - try { - observer.asBinder().linkToDeath(this, 0); - } catch (RemoteException e) { - binderDied(); - } - } - - public void binderDied() { - synchronized (observersLock) { - removeObserverLocked(observer); - } - } - - public void dumpLocked(FileDescriptor fd, PrintWriter pw, String[] args, - String name, String prefix, SparseIntArray pidCounts) { - pidCounts.put(pid, pidCounts.get(pid)+1); - pw.print(prefix); pw.print(name); pw.print(": pid="); - pw.print(pid); pw.print(" uid="); - pw.print(uid); pw.print(" user="); - pw.print(userHandle); pw.print(" target="); - pw.println(Integer.toHexString(System.identityHashCode( - observer != null ? observer.asBinder() : null))); - } - } - - public static final int INSERT_TYPE = 0; - public static final int UPDATE_TYPE = 1; - public static final int DELETE_TYPE = 2; - - private String mName; - private ArrayList<ObserverNode> mChildren = new ArrayList<ObserverNode>(); - private ArrayList<ObserverEntry> mObservers = new ArrayList<ObserverEntry>(); - - public ObserverNode(String name) { - mName = name; - } - - public void dumpLocked(FileDescriptor fd, PrintWriter pw, String[] args, - String name, String prefix, int[] counts, SparseIntArray pidCounts) { - String innerName = null; - if (mObservers.size() > 0) { - if ("".equals(name)) { - innerName = mName; - } else { - innerName = name + "/" + mName; - } - for (int i=0; i<mObservers.size(); i++) { - counts[1]++; - mObservers.get(i).dumpLocked(fd, pw, args, innerName, prefix, - pidCounts); - } - } - if (mChildren.size() > 0) { - if (innerName == null) { - if ("".equals(name)) { - innerName = mName; - } else { - innerName = name + "/" + mName; - } - } - for (int i=0; i<mChildren.size(); i++) { - counts[0]++; - mChildren.get(i).dumpLocked(fd, pw, args, innerName, prefix, - counts, pidCounts); - } - } - } - - private String getUriSegment(Uri uri, int index) { - if (uri != null) { - if (index == 0) { - return uri.getAuthority(); - } else { - return uri.getPathSegments().get(index - 1); - } - } else { - return null; - } - } - - private int countUriSegments(Uri uri) { - if (uri == null) { - return 0; - } - return uri.getPathSegments().size() + 1; - } - - // Invariant: userHandle is either a hard user number or is USER_ALL - public void addObserverLocked(Uri uri, IContentObserver observer, - boolean notifyForDescendants, Object observersLock, - int uid, int pid, int userHandle) { - addObserverLocked(uri, 0, observer, notifyForDescendants, observersLock, - uid, pid, userHandle); - } - - private void addObserverLocked(Uri uri, int index, IContentObserver observer, - boolean notifyForDescendants, Object observersLock, - int uid, int pid, int userHandle) { - // If this is the leaf node add the observer - if (index == countUriSegments(uri)) { - mObservers.add(new ObserverEntry(observer, notifyForDescendants, observersLock, - uid, pid, userHandle)); - return; - } - - // Look to see if the proper child already exists - String segment = getUriSegment(uri, index); - if (segment == null) { - throw new IllegalArgumentException("Invalid Uri (" + uri + ") used for observer"); - } - int N = mChildren.size(); - for (int i = 0; i < N; i++) { - ObserverNode node = mChildren.get(i); - if (node.mName.equals(segment)) { - node.addObserverLocked(uri, index + 1, observer, notifyForDescendants, - observersLock, uid, pid, userHandle); - return; - } - } - - // No child found, create one - ObserverNode node = new ObserverNode(segment); - mChildren.add(node); - node.addObserverLocked(uri, index + 1, observer, notifyForDescendants, - observersLock, uid, pid, userHandle); - } - - public boolean removeObserverLocked(IContentObserver observer) { - int size = mChildren.size(); - for (int i = 0; i < size; i++) { - boolean empty = mChildren.get(i).removeObserverLocked(observer); - if (empty) { - mChildren.remove(i); - i--; - size--; - } - } - - IBinder observerBinder = observer.asBinder(); - size = mObservers.size(); - for (int i = 0; i < size; i++) { - ObserverEntry entry = mObservers.get(i); - if (entry.observer.asBinder() == observerBinder) { - mObservers.remove(i); - // We no longer need to listen for death notifications. Remove it. - observerBinder.unlinkToDeath(entry, 0); - break; - } - } - - if (mChildren.size() == 0 && mObservers.size() == 0) { - return true; - } - return false; - } - - private void collectMyObserversLocked(boolean leaf, IContentObserver observer, - boolean observerWantsSelfNotifications, int targetUserHandle, - ArrayList<ObserverCall> calls) { - int N = mObservers.size(); - IBinder observerBinder = observer == null ? null : observer.asBinder(); - for (int i = 0; i < N; i++) { - ObserverEntry entry = mObservers.get(i); - - // Don't notify the observer if it sent the notification and isn't interested - // in self notifications - boolean selfChange = (entry.observer.asBinder() == observerBinder); - if (selfChange && !observerWantsSelfNotifications) { - continue; - } - - // Does this observer match the target user? - if (targetUserHandle == UserHandle.USER_ALL - || entry.userHandle == UserHandle.USER_ALL - || targetUserHandle == entry.userHandle) { - // Make sure the observer is interested in the notification - if (leaf || (!leaf && entry.notifyForDescendants)) { - calls.add(new ObserverCall(this, entry.observer, selfChange)); - } - } - } - } - - /** - * targetUserHandle is either a hard user handle or is USER_ALL - */ - public void collectObserversLocked(Uri uri, int index, IContentObserver observer, - boolean observerWantsSelfNotifications, int targetUserHandle, - ArrayList<ObserverCall> calls) { - String segment = null; - int segmentCount = countUriSegments(uri); - if (index >= segmentCount) { - // This is the leaf node, notify all observers - collectMyObserversLocked(true, observer, observerWantsSelfNotifications, - targetUserHandle, calls); - } else if (index < segmentCount){ - segment = getUriSegment(uri, index); - // Notify any observers at this level who are interested in descendants - collectMyObserversLocked(false, observer, observerWantsSelfNotifications, - targetUserHandle, calls); - } - - int N = mChildren.size(); - for (int i = 0; i < N; i++) { - ObserverNode node = mChildren.get(i); - if (segment == null || node.mName.equals(segment)) { - // We found the child, - node.collectObserversLocked(uri, index + 1, - observer, observerWantsSelfNotifications, targetUserHandle, calls); - if (segment != null) { - break; - } - } - } - } - } -} diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 7aa2507..8a9eed2 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -418,6 +418,9 @@ public abstract class Context { /** Return the name of this application's package. */ public abstract String getPackageName(); + /** @hide Return the name of the base context this context is derived from. */ + public abstract String getBasePackageName(); + /** Return the full application info for this context's package. */ public abstract ApplicationInfo getApplicationInfo(); @@ -1135,6 +1138,14 @@ public abstract class Context { String receiverPermission); /** + * Like {@link #sendBroadcast(Intent, String)}, but also allows specification + * of an assocated app op as per {@link android.app.AppOpsManager}. + * @hide + */ + public abstract void sendBroadcast(Intent intent, + String receiverPermission, int appOp); + + /** * Broadcast the given intent to all interested BroadcastReceivers, delivering * them one at a time to allow more preferred receivers to consume the * broadcast before it is delivered to less preferred receivers. This @@ -1205,6 +1216,17 @@ public abstract class Context { Bundle initialExtras); /** + * Like {@link #sendOrderedBroadcast(Intent, String, BroadcastReceiver, android.os.Handler, + * int, String, android.os.Bundle)}, but also allows specification + * of an assocated app op as per {@link android.app.AppOpsManager}. + * @hide + */ + public abstract void sendOrderedBroadcast(Intent intent, + String receiverPermission, int appOp, BroadcastReceiver resultReceiver, + Handler scheduler, int initialCode, String initialData, + Bundle initialExtras); + + /** * Version of {@link #sendBroadcast(Intent)} that allows you to specify the * user the broadcast will be sent to. This is not available to applications * that are not pre-installed on the system image. Using it requires holding @@ -1677,7 +1699,7 @@ public abstract class Context { * argument for use by system server and other multi-user aware code. * @hide */ - public boolean bindService(Intent service, ServiceConnection conn, int flags, int userHandle) { + public boolean bindServiceAsUser(Intent service, ServiceConnection conn, int flags, UserHandle user) { throw new RuntimeException("Not implemented. Must override in a subclass."); } @@ -1993,17 +2015,6 @@ public abstract class Context { /** * Use with {@link #getSystemService} to retrieve a {@link - * android.net.ThrottleManager} for handling management of - * throttling. - * - * @hide - * @see #getSystemService - * @see android.net.ThrottleManager - */ - public static final String THROTTLE_SERVICE = "throttle"; - - /** - * Use with {@link #getSystemService} to retrieve a {@link * android.os.IUpdateLock} for managing runtime sequences that * must not be interrupted by headless OTA application or similar. * @@ -2255,6 +2266,18 @@ public abstract class Context { public static final String USER_SERVICE = "user"; /** + * Use with {@link #getSystemService} to retrieve a + * {@link android.app.AppOpsManager} for tracking application operations + * on the device. + * + * @see #getSystemService + * @see android.app.AppOpsManager + * + * @hide + */ + public static final String APP_OPS_SERVICE = "appops"; + + /** * Determine whether the given permission is allowed for a particular * process and user ID running in the system. * @@ -2668,6 +2691,14 @@ public abstract class Context { throws PackageManager.NameNotFoundException; /** + * Get the userId associated with this context + * @return user id + * + * @hide + */ + public abstract int getUserId(); + + /** * Return a new Context object for the current Context but whose resources * are adjusted to match the given Configuration. Each call to this method * returns a new instance of a Context object; Context objects are not diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java index 84ad667..2f1bf8c 100644 --- a/core/java/android/content/ContextWrapper.java +++ b/core/java/android/content/ContextWrapper.java @@ -135,6 +135,12 @@ public class ContextWrapper extends Context { return mBase.getPackageName(); } + /** @hide */ + @Override + public String getBasePackageName() { + return mBase.getBasePackageName(); + } + @Override public ApplicationInfo getApplicationInfo() { return mBase.getApplicationInfo(); @@ -343,6 +349,12 @@ public class ContextWrapper extends Context { mBase.sendBroadcast(intent, receiverPermission); } + /** @hide */ + @Override + public void sendBroadcast(Intent intent, String receiverPermission, int appOp) { + mBase.sendBroadcast(intent, receiverPermission, appOp); + } + @Override public void sendOrderedBroadcast(Intent intent, String receiverPermission) { @@ -359,6 +371,17 @@ public class ContextWrapper extends Context { initialData, initialExtras); } + /** @hide */ + @Override + public void sendOrderedBroadcast( + Intent intent, String receiverPermission, int appOp, BroadcastReceiver resultReceiver, + Handler scheduler, int initialCode, String initialData, + Bundle initialExtras) { + mBase.sendOrderedBroadcast(intent, receiverPermission, appOp, + resultReceiver, scheduler, initialCode, + initialData, initialExtras); + } + @Override public void sendBroadcastAsUser(Intent intent, UserHandle user) { mBase.sendBroadcastAsUser(intent, user); @@ -475,8 +498,9 @@ public class ContextWrapper extends Context { /** @hide */ @Override - public boolean bindService(Intent service, ServiceConnection conn, int flags, int userHandle) { - return mBase.bindService(service, conn, flags, userHandle); + public boolean bindServiceAsUser(Intent service, ServiceConnection conn, int flags, + UserHandle user) { + return mBase.bindServiceAsUser(service, conn, flags, user); } @Override @@ -599,6 +623,12 @@ public class ContextWrapper extends Context { return mBase.createPackageContextAsUser(packageName, flags, user); } + /** @hide */ + @Override + public int getUserId() { + return mBase.getUserId(); + } + @Override public Context createConfigurationContext(Configuration overrideConfiguration) { return mBase.createConfigurationContext(overrideConfiguration); diff --git a/core/java/android/content/CursorLoader.java b/core/java/android/content/CursorLoader.java index 9f7a104..4e89dec 100644 --- a/core/java/android/content/CursorLoader.java +++ b/core/java/android/content/CursorLoader.java @@ -65,9 +65,14 @@ public class CursorLoader extends AsyncTaskLoader<Cursor> { Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection, mSelectionArgs, mSortOrder, mCancellationSignal); if (cursor != null) { - // Ensure the cursor window is filled - cursor.getCount(); - registerContentObserver(cursor, mObserver); + try { + // Ensure the cursor window is filled. + cursor.getCount(); + registerContentObserver(cursor, mObserver); + } catch (RuntimeException ex) { + cursor.close(); + throw ex; + } } return cursor; } finally { diff --git a/core/java/android/content/IClipboard.aidl b/core/java/android/content/IClipboard.aidl index 254901b..af0b8f0 100644 --- a/core/java/android/content/IClipboard.aidl +++ b/core/java/android/content/IClipboard.aidl @@ -26,15 +26,16 @@ import android.content.IOnPrimaryClipChangedListener; * {@hide} */ interface IClipboard { - void setPrimaryClip(in ClipData clip); + void setPrimaryClip(in ClipData clip, String callingPackage); ClipData getPrimaryClip(String pkg); - ClipDescription getPrimaryClipDescription(); - boolean hasPrimaryClip(); - void addPrimaryClipChangedListener(in IOnPrimaryClipChangedListener listener); + ClipDescription getPrimaryClipDescription(String callingPackage); + boolean hasPrimaryClip(String callingPackage); + void addPrimaryClipChangedListener(in IOnPrimaryClipChangedListener listener, + String callingPackage); void removePrimaryClipChangedListener(in IOnPrimaryClipChangedListener listener); /** * Returns true if the clipboard contains text; false otherwise. */ - boolean hasClipboardText(); + boolean hasClipboardText(String callingPackage); } diff --git a/core/java/android/content/IContentProvider.java b/core/java/android/content/IContentProvider.java index eeba1e0..62b79f0 100644 --- a/core/java/android/content/IContentProvider.java +++ b/core/java/android/content/IContentProvider.java @@ -34,30 +34,33 @@ import java.util.ArrayList; * @hide */ public interface IContentProvider extends IInterface { - public Cursor query(Uri url, String[] projection, String selection, + public Cursor query(String callingPkg, Uri url, String[] projection, String selection, String[] selectionArgs, String sortOrder, ICancellationSignal cancellationSignal) throws RemoteException; public String getType(Uri url) throws RemoteException; - public Uri insert(Uri url, ContentValues initialValues) + public Uri insert(String callingPkg, Uri url, ContentValues initialValues) throws RemoteException; - public int bulkInsert(Uri url, ContentValues[] initialValues) throws RemoteException; - public int delete(Uri url, String selection, String[] selectionArgs) + public int bulkInsert(String callingPkg, Uri url, ContentValues[] initialValues) throws RemoteException; - public int update(Uri url, ContentValues values, String selection, + public int delete(String callingPkg, Uri url, String selection, String[] selectionArgs) + throws RemoteException; + public int update(String callingPkg, Uri url, ContentValues values, String selection, String[] selectionArgs) throws RemoteException; - public ParcelFileDescriptor openFile(Uri url, String mode) + public ParcelFileDescriptor openFile(String callingPkg, Uri url, String mode) throws RemoteException, FileNotFoundException; - public AssetFileDescriptor openAssetFile(Uri url, String mode) + public AssetFileDescriptor openAssetFile(String callingPkg, Uri url, String mode) throws RemoteException, FileNotFoundException; - public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) - throws RemoteException, OperationApplicationException; - public Bundle call(String method, String arg, Bundle extras) throws RemoteException; + public ContentProviderResult[] applyBatch(String callingPkg, + ArrayList<ContentProviderOperation> operations) + throws RemoteException, OperationApplicationException; + public Bundle call(String callingPkg, String method, String arg, Bundle extras) + throws RemoteException; public ICancellationSignal createCancellationSignal() throws RemoteException; // Data interchange. public String[] getStreamTypes(Uri url, String mimeTypeFilter) throws RemoteException; - public AssetFileDescriptor openTypedAssetFile(Uri url, String mimeType, Bundle opts) - throws RemoteException, FileNotFoundException; + public AssetFileDescriptor openTypedAssetFile(String callingPkg, Uri url, String mimeType, + Bundle opts) throws RemoteException, FileNotFoundException; /* IPC constants */ static final String descriptor = "android.content.IContentProvider"; diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 89b1bbd..f8ff8d1 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -883,7 +883,7 @@ public class Intent implements Parcelable, Cloneable { * Activity Action: Allow the user to select a particular kind of data and * return it. This is different than {@link #ACTION_PICK} in that here we * just say what kind of data is desired, not a URI of existing data from - * which the user can pick. A ACTION_GET_CONTENT could allow the user to + * which the user can pick. An ACTION_GET_CONTENT could allow the user to * create the data as it runs (for example taking a picture or recording a * sound), let them browse over the web and download the desired data, * etc. @@ -917,12 +917,17 @@ public class Intent implements Parcelable, Cloneable { * from a remote server but not already on the local device (thus requiring * they be downloaded when opened). * <p> + * If the caller can handle multiple returned items (the user performing + * multiple selection), then it can specify {@link #EXTRA_ALLOW_MULTIPLE} + * to indicate this. + * <p> * Input: {@link #getType} is the desired MIME type to retrieve. Note * that no URI is supplied in the intent, as there are no constraints on * where the returned data originally comes from. You may also include the * {@link #CATEGORY_OPENABLE} if you can only accept data that can be * opened as a stream. You may use {@link #EXTRA_LOCAL_ONLY} to limit content - * selection to local data. + * selection to local data. You may use {@link #EXTRA_ALLOW_MULTIPLE} to + * allow the user to select multiple items. * <p> * Output: The URI of the item that was picked. This must be a content: * URI so that any receiver can access it. @@ -1140,14 +1145,33 @@ public class Intent implements Parcelable, Cloneable { */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_WEB_SEARCH = "android.intent.action.WEB_SEARCH"; + /** * Activity Action: Perform assist action. * <p> - * Input: nothing + * Input: {@link #EXTRA_ASSIST_PACKAGE} and {@link #EXTRA_ASSIST_CONTEXT} can provide + * additional optional contextual information about where the user was when they requested + * the assist. * Output: nothing. */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_ASSIST = "android.intent.action.ASSIST"; + + /** + * An optional field on {@link #ACTION_ASSIST} containing the name of the current + * foreground application package at the time the assist was invoked. + */ + public static final String EXTRA_ASSIST_PACKAGE + = "android.intent.extra.ASSIST_PACKAGE"; + + /** + * An optional field on {@link #ACTION_ASSIST} containing additional contextual + * information supplied by the current foreground app at the time of the assist + * request. This is a {@link Bundle} of additional data. + */ + public static final String EXTRA_ASSIST_CONTEXT + = "android.intent.extra.ASSIST_CONTEXT"; + /** * Activity Action: List all available applications * <p>Input: Nothing. @@ -1588,7 +1612,7 @@ public class Intent implements Parcelable, Cloneable { * <ul> * <li> {@link #EXTRA_UID} containing the integer uid assigned to the package. * <li> {@link #EXTRA_CHANGED_COMPONENT_NAME_LIST} containing the class name - * of the changed components. + * of the changed components (or the package name itself). * <li> {@link #EXTRA_DONT_KILL_APP} containing boolean field to override the * default action of restarting the application. * </ul> @@ -2295,6 +2319,61 @@ public class Intent implements Parcelable, Cloneable { "android.intent.action.DOCK_EVENT"; /** + * Broadcast Action: A broadcast when idle maintenance can be started. + * This means that the user is not interacting with the device and is + * not expected to do so soon. Typical use of the idle maintenance is + * to perform somehow expensive tasks that can be postponed at a moment + * when they will not degrade user experience. + * <p> + * <p class="note">In order to keep the device responsive in case of an + * unexpected user interaction, implementations of a maintenance task + * should be interruptible. In such a scenario a broadcast with action + * {@link #ACTION_IDLE_MAINTENANCE_END} will be sent. In other words, you + * should not do the maintenance work in + * {@link BroadcastReceiver#onReceive(Context, Intent)}, rather start a + * maintenance service by {@link Context#startService(Intent)}. Also + * you should hold a wake lock while your maintenance service is running + * to prevent the device going to sleep. + * </p> + * <p> + * <p class="note">This is a protected intent that can only be sent by + * the system. + * </p> + * + * @see #ACTION_IDLE_MAINTENANCE_END + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_IDLE_MAINTENANCE_START = + "android.intent.action.ACTION_IDLE_MAINTENANCE_START"; + + /** + * Broadcast Action: A broadcast when idle maintenance should be stopped. + * This means that the user was not interacting with the device as a result + * of which a broadcast with action {@link #ACTION_IDLE_MAINTENANCE_START} + * was sent and now the user started interacting with the device. Typical + * use of the idle maintenance is to perform somehow expensive tasks that + * can be postponed at a moment when they will not degrade user experience. + * <p> + * <p class="note">In order to keep the device responsive in case of an + * unexpected user interaction, implementations of a maintenance task + * should be interruptible. Hence, on receiving a broadcast with this + * action, the maintenance task should be interrupted as soon as possible. + * In other words, you should not do the maintenance work in + * {@link BroadcastReceiver#onReceive(Context, Intent)}, rather stop the + * maintenance service that was started on receiving of + * {@link #ACTION_IDLE_MAINTENANCE_START}.Also you should release the wake + * lock you acquired when your maintenance service started. + * </p> + * <p class="note">This is a protected intent that can only be sent + * by the system. + * + * @see #ACTION_IDLE_MAINTENANCE_START + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_IDLE_MAINTENANCE_END = + "android.intent.action.ACTION_IDLE_MAINTENANCE_END"; + + /** * Broadcast Action: a remote intent is to be broadcasted. * * A remote intent is used for remote RPC between devices. The remote intent @@ -2465,6 +2544,14 @@ public class Intent implements Parcelable, Cloneable { public static final String ACTION_QUICK_CLOCK = "android.intent.action.QUICK_CLOCK"; + /** + * Broadcast Action: This is broadcast when a user action should request the + * brightness setting dialog. + * @hide + */ + public static final String ACTION_SHOW_BRIGHTNESS_DIALOG = + "android.intent.action.SHOW_BRIGHTNESS_DIALOG"; + // --------------------------------------------------------------------- // --------------------------------------------------------------------- // Standard intent categories (see addCategory()). @@ -2586,7 +2673,8 @@ public class Intent implements Parcelable, Cloneable { public static final String CATEGORY_SAMPLE_CODE = "android.intent.category.SAMPLE_CODE"; /** * Used to indicate that a GET_CONTENT intent only wants URIs that can be opened with - * ContentResolver.openInputStream. Openable URIs must support the columns in OpenableColumns + * ContentResolver.openInputStream. Openable URIs must support the columns in + * {@link android.provider.OpenableColumns} * when queried, though it is allowable for those columns to be blank. */ @SdkConstant(SdkConstantType.INTENT_CATEGORY) @@ -2969,7 +3057,9 @@ public class Intent implements Parcelable, Cloneable { /** * This field is part of {@link android.content.Intent#ACTION_PACKAGE_CHANGED}, - * and contains a string array of all of the components that have changed. + * and contains a string array of all of the components that have changed. If + * the state of the overall package has changed, then it will contain an entry + * with the package name itself. */ public static final String EXTRA_CHANGED_COMPONENT_NAME_LIST = "android.intent.extra.changed_component_name_list"; @@ -3024,6 +3114,17 @@ public class Intent implements Parcelable, Cloneable { "android.intent.extra.LOCAL_ONLY"; /** + * Used to indicate that a {@link #ACTION_GET_CONTENT} intent can allow the + * user to select and return multiple items. This is a boolean extra; the default + * is false. If true, an implementation of ACTION_GET_CONTENT is allowed to + * present the user with a UI where they can pick multiple items that are all + * returned to the caller. When this happens, they should be returned as + * the {@link #getClipData()} part of the result Intent. + */ + public static final String EXTRA_ALLOW_MULTIPLE = + "android.intent.extra.ALLOW_MULTIPLE"; + + /** * The userHandle carried with broadcast intents related to addition, removal and switching of users * - {@link #ACTION_USER_ADDED}, {@link #ACTION_USER_REMOVED} and {@link #ACTION_USER_SWITCHED}. * @hide diff --git a/core/java/android/content/IntentFilter.java b/core/java/android/content/IntentFilter.java index 3b0d846..5e65b59 100644 --- a/core/java/android/content/IntentFilter.java +++ b/core/java/android/content/IntentFilter.java @@ -86,7 +86,8 @@ import java.util.Set; * <strong>data scheme+authority+path</strong> if specified) must match. * * <p><strong>Action</strong> matches if any of the given values match the - * Intent action, <em>or</em> if no actions were specified in the filter. + * Intent action; if the filter specifies no actions, then it will only match + * Intents that do not contain an action. * * <p><strong>Data Type</strong> matches if any of the given values match the * Intent type. The Intent diff --git a/core/java/android/content/PeriodicSync.java b/core/java/android/content/PeriodicSync.java index 17813ec..513a556 100644 --- a/core/java/android/content/PeriodicSync.java +++ b/core/java/android/content/PeriodicSync.java @@ -79,6 +79,25 @@ public class PeriodicSync implements Parcelable { return account.equals(other.account) && authority.equals(other.authority) && period == other.period - && SyncStorageEngine.equals(extras, other.extras); + && syncExtrasEquals(extras, other.extras); + } + + /** {@hide} */ + public static boolean syncExtrasEquals(Bundle b1, Bundle b2) { + if (b1.size() != b2.size()) { + return false; + } + if (b1.isEmpty()) { + return true; + } + for (String key : b1.keySet()) { + if (!b2.containsKey(key)) { + return false; + } + if (!b1.get(key).equals(b2.get(key))) { + return false; + } + } + return true; } } diff --git a/core/java/android/content/SyncAdaptersCache.java b/core/java/android/content/SyncAdaptersCache.java index 7b643a0..8bb3ee7 100644 --- a/core/java/android/content/SyncAdaptersCache.java +++ b/core/java/android/content/SyncAdaptersCache.java @@ -31,7 +31,7 @@ import java.io.IOException; * A cache of services that export the {@link android.content.ISyncAdapter} interface. * @hide */ -/* package private */ class SyncAdaptersCache extends RegisteredServicesCache<SyncAdapterType> { +public class SyncAdaptersCache extends RegisteredServicesCache<SyncAdapterType> { private static final String TAG = "Account"; private static final String SERVICE_INTERFACE = "android.content.SyncAdapter"; @@ -39,7 +39,7 @@ import java.io.IOException; private static final String ATTRIBUTES_NAME = "sync-adapter"; private static final MySerializer sSerializer = new MySerializer(); - SyncAdaptersCache(Context context) { + public SyncAdaptersCache(Context context) { super(context, SERVICE_INTERFACE, SERVICE_META_DATA, ATTRIBUTES_NAME, sSerializer); } diff --git a/core/java/android/content/SyncInfo.java b/core/java/android/content/SyncInfo.java index abfe964..0284882 100644 --- a/core/java/android/content/SyncInfo.java +++ b/core/java/android/content/SyncInfo.java @@ -46,7 +46,7 @@ public class SyncInfo implements Parcelable { public final long startTime; /** @hide */ - SyncInfo(int authorityId, Account account, String authority, + public SyncInfo(int authorityId, Account account, String authority, long startTime) { this.authorityId = authorityId; this.account = account; diff --git a/core/java/android/content/SyncManager.java b/core/java/android/content/SyncManager.java deleted file mode 100644 index e4b4b97..0000000 --- a/core/java/android/content/SyncManager.java +++ /dev/null @@ -1,2632 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.content; - -import android.accounts.Account; -import android.accounts.AccountAndUser; -import android.accounts.AccountManager; -import android.accounts.AccountManagerService; -import android.app.ActivityManager; -import android.app.AlarmManager; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.SyncStorageEngine.OnSyncRequestListener; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.ProviderInfo; -import android.content.pm.RegisteredServicesCache; -import android.content.pm.RegisteredServicesCacheListener; -import android.content.pm.ResolveInfo; -import android.content.pm.UserInfo; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.os.Bundle; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.IBinder; -import android.os.Looper; -import android.os.Message; -import android.os.PowerManager; -import android.os.Process; -import android.os.RemoteException; -import android.os.SystemClock; -import android.os.SystemProperties; -import android.os.UserHandle; -import android.os.UserManager; -import android.os.WorkSource; -import android.provider.Settings; -import android.text.format.DateUtils; -import android.text.format.Time; -import android.util.EventLog; -import android.util.Log; -import android.util.Pair; -import android.util.Slog; - -import com.android.internal.R; -import com.android.internal.annotations.GuardedBy; -import com.android.internal.util.IndentingPrintWriter; -import com.google.android.collect.Lists; -import com.google.android.collect.Maps; -import com.google.android.collect.Sets; - -import java.io.FileDescriptor; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.Set; -import java.util.concurrent.CountDownLatch; - -/** - * @hide - */ -public class SyncManager { - private static final String TAG = "SyncManager"; - - /** Delay a sync due to local changes this long. In milliseconds */ - private static final long LOCAL_SYNC_DELAY; - - /** - * If a sync takes longer than this and the sync queue is not empty then we will - * cancel it and add it back to the end of the sync queue. In milliseconds. - */ - private static final long MAX_TIME_PER_SYNC; - - static { - final boolean isLargeRAM = ActivityManager.isLargeRAM(); - int defaultMaxInitSyncs = isLargeRAM ? 5 : 2; - int defaultMaxRegularSyncs = isLargeRAM ? 2 : 1; - MAX_SIMULTANEOUS_INITIALIZATION_SYNCS = - SystemProperties.getInt("sync.max_init_syncs", defaultMaxInitSyncs); - MAX_SIMULTANEOUS_REGULAR_SYNCS = - SystemProperties.getInt("sync.max_regular_syncs", defaultMaxRegularSyncs); - LOCAL_SYNC_DELAY = - SystemProperties.getLong("sync.local_sync_delay", 30 * 1000 /* 30 seconds */); - MAX_TIME_PER_SYNC = - SystemProperties.getLong("sync.max_time_per_sync", 5 * 60 * 1000 /* 5 minutes */); - SYNC_NOTIFICATION_DELAY = - SystemProperties.getLong("sync.notification_delay", 30 * 1000 /* 30 seconds */); - } - - private static final long SYNC_NOTIFICATION_DELAY; - - /** - * When retrying a sync for the first time use this delay. After that - * the retry time will double until it reached MAX_SYNC_RETRY_TIME. - * In milliseconds. - */ - private static final long INITIAL_SYNC_RETRY_TIME_IN_MS = 30 * 1000; // 30 seconds - - /** - * Default the max sync retry time to this value. - */ - private static final long DEFAULT_MAX_SYNC_RETRY_TIME_IN_SECONDS = 60 * 60; // one hour - - /** - * How long to wait before retrying a sync that failed due to one already being in progress. - */ - private static final int DELAY_RETRY_SYNC_IN_PROGRESS_IN_SECONDS = 10; - - private static final int INITIALIZATION_UNBIND_DELAY_MS = 5000; - - private static final String SYNC_WAKE_LOCK_PREFIX = "*sync*"; - private static final String HANDLE_SYNC_ALARM_WAKE_LOCK = "SyncManagerHandleSyncAlarm"; - private static final String SYNC_LOOP_WAKE_LOCK = "SyncLoopWakeLock"; - - private static final int MAX_SIMULTANEOUS_REGULAR_SYNCS; - private static final int MAX_SIMULTANEOUS_INITIALIZATION_SYNCS; - - private Context mContext; - - private static final AccountAndUser[] INITIAL_ACCOUNTS_ARRAY = new AccountAndUser[0]; - - // TODO: add better locking around mRunningAccounts - private volatile AccountAndUser[] mRunningAccounts = INITIAL_ACCOUNTS_ARRAY; - - volatile private PowerManager.WakeLock mHandleAlarmWakeLock; - volatile private PowerManager.WakeLock mSyncManagerWakeLock; - volatile private boolean mDataConnectionIsConnected = false; - volatile private boolean mStorageIsLow = false; - - private final NotificationManager mNotificationMgr; - private AlarmManager mAlarmService = null; - - private SyncStorageEngine mSyncStorageEngine; - - @GuardedBy("mSyncQueue") - private final SyncQueue mSyncQueue; - - protected final ArrayList<ActiveSyncContext> mActiveSyncContexts = Lists.newArrayList(); - - // set if the sync active indicator should be reported - private boolean mNeedSyncActiveNotification = false; - - private final PendingIntent mSyncAlarmIntent; - // Synchronized on "this". Instead of using this directly one should instead call - // its accessor, getConnManager(). - private ConnectivityManager mConnManagerDoNotUseDirectly; - - protected SyncAdaptersCache mSyncAdapters; - - private BroadcastReceiver mStorageIntentReceiver = - new BroadcastReceiver() { - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(action)) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "Internal storage is low."); - } - mStorageIsLow = true; - cancelActiveSync(null /* any account */, UserHandle.USER_ALL, - null /* any authority */); - } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "Internal storage is ok."); - } - mStorageIsLow = false; - sendCheckAlarmsMessage(); - } - } - }; - - private BroadcastReceiver mBootCompletedReceiver = new BroadcastReceiver() { - public void onReceive(Context context, Intent intent) { - mSyncHandler.onBootCompleted(); - } - }; - - private BroadcastReceiver mBackgroundDataSettingChanged = new BroadcastReceiver() { - public void onReceive(Context context, Intent intent) { - if (getConnectivityManager().getBackgroundDataSetting()) { - scheduleSync(null /* account */, UserHandle.USER_ALL, null /* authority */, - new Bundle(), 0 /* delay */, - false /* onlyThoseWithUnknownSyncableState */); - } - } - }; - - private BroadcastReceiver mAccountsUpdatedReceiver = new BroadcastReceiver() { - public void onReceive(Context context, Intent intent) { - updateRunningAccounts(); - - // Kick off sync for everyone, since this was a radical account change - scheduleSync(null, UserHandle.USER_ALL, null, null, 0 /* no delay */, false); - } - }; - - private final PowerManager mPowerManager; - - // Use this as a random offset to seed all periodic syncs - private int mSyncRandomOffsetMillis; - - private final UserManager mUserManager; - - private static final long SYNC_ALARM_TIMEOUT_MIN = 30 * 1000; // 30 seconds - private static final long SYNC_ALARM_TIMEOUT_MAX = 2 * 60 * 60 * 1000; // two hours - - private List<UserInfo> getAllUsers() { - return mUserManager.getUsers(); - } - - private boolean containsAccountAndUser(AccountAndUser[] accounts, Account account, int userId) { - boolean found = false; - for (int i = 0; i < accounts.length; i++) { - if (accounts[i].userId == userId - && accounts[i].account.equals(account)) { - found = true; - break; - } - } - return found; - } - - public void updateRunningAccounts() { - mRunningAccounts = AccountManagerService.getSingleton().getRunningAccounts(); - - if (mBootCompleted) { - doDatabaseCleanup(); - } - - for (ActiveSyncContext currentSyncContext : mActiveSyncContexts) { - if (!containsAccountAndUser(mRunningAccounts, - currentSyncContext.mSyncOperation.account, - currentSyncContext.mSyncOperation.userId)) { - Log.d(TAG, "canceling sync since the account is no longer running"); - sendSyncFinishedOrCanceledMessage(currentSyncContext, - null /* no result since this is a cancel */); - } - } - - // we must do this since we don't bother scheduling alarms when - // the accounts are not set yet - sendCheckAlarmsMessage(); - } - - private void doDatabaseCleanup() { - for (UserInfo user : mUserManager.getUsers(true)) { - // Skip any partially created/removed users - if (user.partial) continue; - Account[] accountsForUser = AccountManagerService.getSingleton().getAccounts(user.id); - mSyncStorageEngine.doDatabaseCleanup(accountsForUser, user.id); - } - } - - private BroadcastReceiver mConnectivityIntentReceiver = - new BroadcastReceiver() { - public void onReceive(Context context, Intent intent) { - final boolean wasConnected = mDataConnectionIsConnected; - - // don't use the intent to figure out if network is connected, just check - // ConnectivityManager directly. - mDataConnectionIsConnected = readDataConnectionState(); - if (mDataConnectionIsConnected) { - if (!wasConnected) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "Reconnection detected: clearing all backoffs"); - } - mSyncStorageEngine.clearAllBackoffs(mSyncQueue); - } - sendCheckAlarmsMessage(); - } - } - }; - - private boolean readDataConnectionState() { - NetworkInfo networkInfo = getConnectivityManager().getActiveNetworkInfo(); - return (networkInfo != null) && networkInfo.isConnected(); - } - - private BroadcastReceiver mShutdownIntentReceiver = - new BroadcastReceiver() { - public void onReceive(Context context, Intent intent) { - Log.w(TAG, "Writing sync state before shutdown..."); - getSyncStorageEngine().writeAllState(); - } - }; - - private BroadcastReceiver mUserIntentReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL); - if (userId == UserHandle.USER_NULL) return; - - if (Intent.ACTION_USER_REMOVED.equals(action)) { - onUserRemoved(userId); - } else if (Intent.ACTION_USER_STARTING.equals(action)) { - onUserStarting(userId); - } else if (Intent.ACTION_USER_STOPPING.equals(action)) { - onUserStopping(userId); - } - } - }; - - private static final String ACTION_SYNC_ALARM = "android.content.syncmanager.SYNC_ALARM"; - private final SyncHandler mSyncHandler; - - private volatile boolean mBootCompleted = false; - - private ConnectivityManager getConnectivityManager() { - synchronized (this) { - if (mConnManagerDoNotUseDirectly == null) { - mConnManagerDoNotUseDirectly = (ConnectivityManager)mContext.getSystemService( - Context.CONNECTIVITY_SERVICE); - } - return mConnManagerDoNotUseDirectly; - } - } - - /** - * Should only be created after {@link ContentService#systemReady()} so that - * {@link PackageManager} is ready to query. - */ - public SyncManager(Context context, boolean factoryTest) { - // Initialize the SyncStorageEngine first, before registering observers - // and creating threads and so on; it may fail if the disk is full. - mContext = context; - - SyncStorageEngine.init(context); - mSyncStorageEngine = SyncStorageEngine.getSingleton(); - mSyncStorageEngine.setOnSyncRequestListener(new OnSyncRequestListener() { - public void onSyncRequest(Account account, int userId, String authority, - Bundle extras) { - scheduleSync(account, userId, authority, extras, 0, false); - } - }); - - mSyncAdapters = new SyncAdaptersCache(mContext); - mSyncQueue = new SyncQueue(mSyncStorageEngine, mSyncAdapters); - - HandlerThread syncThread = new HandlerThread("SyncHandlerThread", - Process.THREAD_PRIORITY_BACKGROUND); - syncThread.start(); - mSyncHandler = new SyncHandler(syncThread.getLooper()); - - mSyncAdapters.setListener(new RegisteredServicesCacheListener<SyncAdapterType>() { - @Override - public void onServiceChanged(SyncAdapterType type, int userId, boolean removed) { - if (!removed) { - scheduleSync(null, UserHandle.USER_ALL, type.authority, null, 0 /* no delay */, - false /* onlyThoseWithUnkownSyncableState */); - } - } - }, mSyncHandler); - - mSyncAlarmIntent = PendingIntent.getBroadcast( - mContext, 0 /* ignored */, new Intent(ACTION_SYNC_ALARM), 0); - - IntentFilter intentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); - context.registerReceiver(mConnectivityIntentReceiver, intentFilter); - - if (!factoryTest) { - intentFilter = new IntentFilter(Intent.ACTION_BOOT_COMPLETED); - context.registerReceiver(mBootCompletedReceiver, intentFilter); - } - - intentFilter = new IntentFilter(ConnectivityManager.ACTION_BACKGROUND_DATA_SETTING_CHANGED); - context.registerReceiver(mBackgroundDataSettingChanged, intentFilter); - - intentFilter = new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW); - intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); - context.registerReceiver(mStorageIntentReceiver, intentFilter); - - intentFilter = new IntentFilter(Intent.ACTION_SHUTDOWN); - intentFilter.setPriority(100); - context.registerReceiver(mShutdownIntentReceiver, intentFilter); - - intentFilter = new IntentFilter(); - intentFilter.addAction(Intent.ACTION_USER_REMOVED); - intentFilter.addAction(Intent.ACTION_USER_STARTING); - intentFilter.addAction(Intent.ACTION_USER_STOPPING); - mContext.registerReceiverAsUser( - mUserIntentReceiver, UserHandle.ALL, intentFilter, null, null); - - if (!factoryTest) { - mNotificationMgr = (NotificationManager) - context.getSystemService(Context.NOTIFICATION_SERVICE); - context.registerReceiver(new SyncAlarmIntentReceiver(), - new IntentFilter(ACTION_SYNC_ALARM)); - } else { - mNotificationMgr = null; - } - mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); - mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); - - // This WakeLock is used to ensure that we stay awake between the time that we receive - // a sync alarm notification and when we finish processing it. We need to do this - // because we don't do the work in the alarm handler, rather we do it in a message - // handler. - mHandleAlarmWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, - HANDLE_SYNC_ALARM_WAKE_LOCK); - mHandleAlarmWakeLock.setReferenceCounted(false); - - // This WakeLock is used to ensure that we stay awake while running the sync loop - // message handler. Normally we will hold a sync adapter wake lock while it is being - // synced but during the execution of the sync loop it might finish a sync for - // one sync adapter before starting the sync for the other sync adapter and we - // don't want the device to go to sleep during that window. - mSyncManagerWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, - SYNC_LOOP_WAKE_LOCK); - mSyncManagerWakeLock.setReferenceCounted(false); - - mSyncStorageEngine.addStatusChangeListener( - ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, new ISyncStatusObserver.Stub() { - public void onStatusChanged(int which) { - // force the sync loop to run if the settings change - sendCheckAlarmsMessage(); - } - }); - - if (!factoryTest) { - // Register for account list updates for all users - mContext.registerReceiverAsUser(mAccountsUpdatedReceiver, - UserHandle.ALL, - new IntentFilter(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION), - null, null); - } - - // Pick a random second in a day to seed all periodic syncs - mSyncRandomOffsetMillis = mSyncStorageEngine.getSyncRandomOffset() * 1000; - } - - /** - * Return a random value v that satisfies minValue <= v < maxValue. The difference between - * maxValue and minValue must be less than Integer.MAX_VALUE. - */ - private long jitterize(long minValue, long maxValue) { - Random random = new Random(SystemClock.elapsedRealtime()); - long spread = maxValue - minValue; - if (spread > Integer.MAX_VALUE) { - throw new IllegalArgumentException("the difference between the maxValue and the " - + "minValue must be less than " + Integer.MAX_VALUE); - } - return minValue + random.nextInt((int)spread); - } - - public SyncStorageEngine getSyncStorageEngine() { - return mSyncStorageEngine; - } - - private void ensureAlarmService() { - if (mAlarmService == null) { - mAlarmService = (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE); - } - } - - /** - * Initiate a sync. This can start a sync for all providers - * (pass null to url, set onlyTicklable to false), only those - * providers that are marked as ticklable (pass null to url, - * set onlyTicklable to true), or a specific provider (set url - * to the content url of the provider). - * - * <p>If the ContentResolver.SYNC_EXTRAS_UPLOAD boolean in extras is - * true then initiate a sync that just checks for local changes to send - * to the server, otherwise initiate a sync that first gets any - * changes from the server before sending local changes back to - * the server. - * - * <p>If a specific provider is being synced (the url is non-null) - * then the extras can contain SyncAdapter-specific information - * to control what gets synced (e.g. which specific feed to sync). - * - * <p>You'll start getting callbacks after this. - * - * @param requestedAccount the account to sync, may be null to signify all accounts - * @param userId the id of the user whose accounts are to be synced. If userId is USER_ALL, - * then all users' accounts are considered. - * @param requestedAuthority the authority to sync, may be null to indicate all authorities - * @param extras a Map of SyncAdapter-specific information to control - * syncs of a specific provider. Can be null. Is ignored - * if the url is null. - * @param delay how many milliseconds in the future to wait before performing this - * @param onlyThoseWithUnkownSyncableState - */ - public void scheduleSync(Account requestedAccount, int userId, String requestedAuthority, - Bundle extras, long delay, boolean onlyThoseWithUnkownSyncableState) { - boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE); - - final boolean backgroundDataUsageAllowed = !mBootCompleted || - getConnectivityManager().getBackgroundDataSetting(); - - if (extras == null) extras = new Bundle(); - - Boolean expedited = extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false); - if (expedited) { - delay = -1; // this means schedule at the front of the queue - } - - AccountAndUser[] accounts; - if (requestedAccount != null && userId != UserHandle.USER_ALL) { - accounts = new AccountAndUser[] { new AccountAndUser(requestedAccount, userId) }; - } else { - // if the accounts aren't configured yet then we can't support an account-less - // sync request - accounts = mRunningAccounts; - if (accounts.length == 0) { - if (isLoggable) { - Log.v(TAG, "scheduleSync: no accounts configured, dropping"); - } - return; - } - } - - final boolean uploadOnly = extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false); - final boolean manualSync = extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false); - if (manualSync) { - extras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true); - extras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true); - } - final boolean ignoreSettings = - extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, false); - - int source; - if (uploadOnly) { - source = SyncStorageEngine.SOURCE_LOCAL; - } else if (manualSync) { - source = SyncStorageEngine.SOURCE_USER; - } else if (requestedAuthority == null) { - source = SyncStorageEngine.SOURCE_POLL; - } else { - // this isn't strictly server, since arbitrary callers can (and do) request - // a non-forced two-way sync on a specific url - source = SyncStorageEngine.SOURCE_SERVER; - } - - for (AccountAndUser account : accounts) { - // Compile a list of authorities that have sync adapters. - // For each authority sync each account that matches a sync adapter. - final HashSet<String> syncableAuthorities = new HashSet<String>(); - for (RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapter : - mSyncAdapters.getAllServices(account.userId)) { - syncableAuthorities.add(syncAdapter.type.authority); - } - - // if the url was specified then replace the list of authorities - // with just this authority or clear it if this authority isn't - // syncable - if (requestedAuthority != null) { - final boolean hasSyncAdapter = syncableAuthorities.contains(requestedAuthority); - syncableAuthorities.clear(); - if (hasSyncAdapter) syncableAuthorities.add(requestedAuthority); - } - - for (String authority : syncableAuthorities) { - int isSyncable = mSyncStorageEngine.getIsSyncable(account.account, account.userId, - authority); - if (isSyncable == 0) { - continue; - } - final RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapterInfo; - syncAdapterInfo = mSyncAdapters.getServiceInfo( - SyncAdapterType.newKey(authority, account.account.type), account.userId); - if (syncAdapterInfo == null) { - continue; - } - final boolean allowParallelSyncs = syncAdapterInfo.type.allowParallelSyncs(); - final boolean isAlwaysSyncable = syncAdapterInfo.type.isAlwaysSyncable(); - if (isSyncable < 0 && isAlwaysSyncable) { - mSyncStorageEngine.setIsSyncable(account.account, account.userId, authority, 1); - isSyncable = 1; - } - if (onlyThoseWithUnkownSyncableState && isSyncable >= 0) { - continue; - } - if (!syncAdapterInfo.type.supportsUploading() && uploadOnly) { - continue; - } - - // always allow if the isSyncable state is unknown - boolean syncAllowed = - (isSyncable < 0) - || ignoreSettings - || (backgroundDataUsageAllowed - && mSyncStorageEngine.getMasterSyncAutomatically(account.userId) - && mSyncStorageEngine.getSyncAutomatically(account.account, - account.userId, authority)); - if (!syncAllowed) { - if (isLoggable) { - Log.d(TAG, "scheduleSync: sync of " + account + ", " + authority - + " is not allowed, dropping request"); - } - continue; - } - - Pair<Long, Long> backoff = mSyncStorageEngine - .getBackoff(account.account, account.userId, authority); - long delayUntil = mSyncStorageEngine.getDelayUntilTime(account.account, - account.userId, authority); - final long backoffTime = backoff != null ? backoff.first : 0; - if (isSyncable < 0) { - Bundle newExtras = new Bundle(); - newExtras.putBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, true); - if (isLoggable) { - Log.v(TAG, "scheduleSync:" - + " delay " + delay - + ", source " + source - + ", account " + account - + ", authority " + authority - + ", extras " + newExtras); - } - scheduleSyncOperation( - new SyncOperation(account.account, account.userId, source, authority, - newExtras, 0, backoffTime, delayUntil, allowParallelSyncs)); - } - if (!onlyThoseWithUnkownSyncableState) { - if (isLoggable) { - Log.v(TAG, "scheduleSync:" - + " delay " + delay - + ", source " + source - + ", account " + account - + ", authority " + authority - + ", extras " + extras); - } - scheduleSyncOperation( - new SyncOperation(account.account, account.userId, source, authority, - extras, delay, backoffTime, delayUntil, allowParallelSyncs)); - } - } - } - } - - public void scheduleLocalSync(Account account, int userId, String authority) { - final Bundle extras = new Bundle(); - extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, true); - scheduleSync(account, userId, authority, extras, LOCAL_SYNC_DELAY, - false /* onlyThoseWithUnkownSyncableState */); - } - - public SyncAdapterType[] getSyncAdapterTypes(int userId) { - final Collection<RegisteredServicesCache.ServiceInfo<SyncAdapterType>> serviceInfos; - serviceInfos = mSyncAdapters.getAllServices(userId); - SyncAdapterType[] types = new SyncAdapterType[serviceInfos.size()]; - int i = 0; - for (RegisteredServicesCache.ServiceInfo<SyncAdapterType> serviceInfo : serviceInfos) { - types[i] = serviceInfo.type; - ++i; - } - return types; - } - - private void sendSyncAlarmMessage() { - if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "sending MESSAGE_SYNC_ALARM"); - mSyncHandler.sendEmptyMessage(SyncHandler.MESSAGE_SYNC_ALARM); - } - - private void sendCheckAlarmsMessage() { - if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "sending MESSAGE_CHECK_ALARMS"); - mSyncHandler.removeMessages(SyncHandler.MESSAGE_CHECK_ALARMS); - mSyncHandler.sendEmptyMessage(SyncHandler.MESSAGE_CHECK_ALARMS); - } - - private void sendSyncFinishedOrCanceledMessage(ActiveSyncContext syncContext, - SyncResult syncResult) { - if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "sending MESSAGE_SYNC_FINISHED"); - Message msg = mSyncHandler.obtainMessage(); - msg.what = SyncHandler.MESSAGE_SYNC_FINISHED; - msg.obj = new SyncHandlerMessagePayload(syncContext, syncResult); - mSyncHandler.sendMessage(msg); - } - - private void sendCancelSyncsMessage(final Account account, final int userId, - final String authority) { - if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "sending MESSAGE_CANCEL"); - Message msg = mSyncHandler.obtainMessage(); - msg.what = SyncHandler.MESSAGE_CANCEL; - msg.obj = Pair.create(account, authority); - msg.arg1 = userId; - mSyncHandler.sendMessage(msg); - } - - class SyncHandlerMessagePayload { - public final ActiveSyncContext activeSyncContext; - public final SyncResult syncResult; - - SyncHandlerMessagePayload(ActiveSyncContext syncContext, SyncResult syncResult) { - this.activeSyncContext = syncContext; - this.syncResult = syncResult; - } - } - - class SyncAlarmIntentReceiver extends BroadcastReceiver { - public void onReceive(Context context, Intent intent) { - mHandleAlarmWakeLock.acquire(); - sendSyncAlarmMessage(); - } - } - - private void clearBackoffSetting(SyncOperation op) { - mSyncStorageEngine.setBackoff(op.account, op.userId, op.authority, - SyncStorageEngine.NOT_IN_BACKOFF_MODE, SyncStorageEngine.NOT_IN_BACKOFF_MODE); - synchronized (mSyncQueue) { - mSyncQueue.onBackoffChanged(op.account, op.userId, op.authority, 0); - } - } - - private void increaseBackoffSetting(SyncOperation op) { - // TODO: Use this function to align it to an already scheduled sync - // operation in the specified window - final long now = SystemClock.elapsedRealtime(); - - final Pair<Long, Long> previousSettings = - mSyncStorageEngine.getBackoff(op.account, op.userId, op.authority); - long newDelayInMs = -1; - if (previousSettings != null) { - // don't increase backoff before current backoff is expired. This will happen for op's - // with ignoreBackoff set. - if (now < previousSettings.first) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "Still in backoff, do not increase it. " - + "Remaining: " + ((previousSettings.first - now) / 1000) + " seconds."); - } - return; - } - // Subsequent delays are the double of the previous delay - newDelayInMs = previousSettings.second * 2; - } - if (newDelayInMs <= 0) { - // The initial delay is the jitterized INITIAL_SYNC_RETRY_TIME_IN_MS - newDelayInMs = jitterize(INITIAL_SYNC_RETRY_TIME_IN_MS, - (long)(INITIAL_SYNC_RETRY_TIME_IN_MS * 1.1)); - } - - // Cap the delay - long maxSyncRetryTimeInSeconds = Settings.Global.getLong(mContext.getContentResolver(), - Settings.Global.SYNC_MAX_RETRY_DELAY_IN_SECONDS, - DEFAULT_MAX_SYNC_RETRY_TIME_IN_SECONDS); - if (newDelayInMs > maxSyncRetryTimeInSeconds * 1000) { - newDelayInMs = maxSyncRetryTimeInSeconds * 1000; - } - - final long backoff = now + newDelayInMs; - - mSyncStorageEngine.setBackoff(op.account, op.userId, op.authority, - backoff, newDelayInMs); - - op.backoff = backoff; - op.updateEffectiveRunTime(); - - synchronized (mSyncQueue) { - mSyncQueue.onBackoffChanged(op.account, op.userId, op.authority, backoff); - } - } - - private void setDelayUntilTime(SyncOperation op, long delayUntilSeconds) { - final long delayUntil = delayUntilSeconds * 1000; - final long absoluteNow = System.currentTimeMillis(); - long newDelayUntilTime; - if (delayUntil > absoluteNow) { - newDelayUntilTime = SystemClock.elapsedRealtime() + (delayUntil - absoluteNow); - } else { - newDelayUntilTime = 0; - } - mSyncStorageEngine - .setDelayUntilTime(op.account, op.userId, op.authority, newDelayUntilTime); - synchronized (mSyncQueue) { - mSyncQueue.onDelayUntilTimeChanged(op.account, op.authority, newDelayUntilTime); - } - } - - /** - * Cancel the active sync if it matches the authority and account. - * @param account limit the cancelations to syncs with this account, if non-null - * @param authority limit the cancelations to syncs with this authority, if non-null - */ - public void cancelActiveSync(Account account, int userId, String authority) { - sendCancelSyncsMessage(account, userId, authority); - } - - /** - * Create and schedule a SyncOperation. - * - * @param syncOperation the SyncOperation to schedule - */ - public void scheduleSyncOperation(SyncOperation syncOperation) { - boolean queueChanged; - synchronized (mSyncQueue) { - queueChanged = mSyncQueue.add(syncOperation); - } - - if (queueChanged) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "scheduleSyncOperation: enqueued " + syncOperation); - } - sendCheckAlarmsMessage(); - } else { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "scheduleSyncOperation: dropping duplicate sync operation " - + syncOperation); - } - } - } - - /** - * Remove scheduled sync operations. - * @param account limit the removals to operations with this account, if non-null - * @param authority limit the removals to operations with this authority, if non-null - */ - public void clearScheduledSyncOperations(Account account, int userId, String authority) { - synchronized (mSyncQueue) { - mSyncQueue.remove(account, userId, authority); - } - mSyncStorageEngine.setBackoff(account, userId, authority, - SyncStorageEngine.NOT_IN_BACKOFF_MODE, SyncStorageEngine.NOT_IN_BACKOFF_MODE); - } - - void maybeRescheduleSync(SyncResult syncResult, SyncOperation operation) { - boolean isLoggable = Log.isLoggable(TAG, Log.DEBUG); - if (isLoggable) { - Log.d(TAG, "encountered error(s) during the sync: " + syncResult + ", " + operation); - } - - operation = new SyncOperation(operation); - - // The SYNC_EXTRAS_IGNORE_BACKOFF only applies to the first attempt to sync a given - // request. Retries of the request will always honor the backoff, so clear the - // flag in case we retry this request. - if (operation.extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false)) { - operation.extras.remove(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF); - } - - // If this sync aborted because the internal sync loop retried too many times then - // don't reschedule. Otherwise we risk getting into a retry loop. - // If the operation succeeded to some extent then retry immediately. - // If this was a two-way sync then retry soft errors with an exponential backoff. - // If this was an upward sync then schedule a two-way sync immediately. - // Otherwise do not reschedule. - if (operation.extras.getBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, false)) { - Log.d(TAG, "not retrying sync operation because SYNC_EXTRAS_DO_NOT_RETRY was specified " - + operation); - } else if (operation.extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false) - && !syncResult.syncAlreadyInProgress) { - operation.extras.remove(ContentResolver.SYNC_EXTRAS_UPLOAD); - Log.d(TAG, "retrying sync operation as a two-way sync because an upload-only sync " - + "encountered an error: " + operation); - scheduleSyncOperation(operation); - } else if (syncResult.tooManyRetries) { - Log.d(TAG, "not retrying sync operation because it retried too many times: " - + operation); - } else if (syncResult.madeSomeProgress()) { - if (isLoggable) { - Log.d(TAG, "retrying sync operation because even though it had an error " - + "it achieved some success"); - } - scheduleSyncOperation(operation); - } else if (syncResult.syncAlreadyInProgress) { - if (isLoggable) { - Log.d(TAG, "retrying sync operation that failed because there was already a " - + "sync in progress: " + operation); - } - scheduleSyncOperation(new SyncOperation(operation.account, operation.userId, - operation.syncSource, - operation.authority, operation.extras, - DELAY_RETRY_SYNC_IN_PROGRESS_IN_SECONDS * 1000, - operation.backoff, operation.delayUntil, operation.allowParallelSyncs)); - } else if (syncResult.hasSoftError()) { - if (isLoggable) { - Log.d(TAG, "retrying sync operation because it encountered a soft error: " - + operation); - } - scheduleSyncOperation(operation); - } else { - Log.d(TAG, "not retrying sync operation because the error is a hard error: " - + operation); - } - } - - private void onUserStarting(int userId) { - // Make sure that accounts we're about to use are valid - AccountManagerService.getSingleton().validateAccounts(userId); - - mSyncAdapters.invalidateCache(userId); - - updateRunningAccounts(); - - synchronized (mSyncQueue) { - mSyncQueue.addPendingOperations(userId); - } - - // Schedule sync for any accounts under started user - final Account[] accounts = AccountManagerService.getSingleton().getAccounts(userId); - for (Account account : accounts) { - scheduleSync(account, userId, null, null, 0 /* no delay */, - true /* onlyThoseWithUnknownSyncableState */); - } - - sendCheckAlarmsMessage(); - } - - private void onUserStopping(int userId) { - updateRunningAccounts(); - - cancelActiveSync( - null /* any account */, - userId, - null /* any authority */); - } - - private void onUserRemoved(int userId) { - updateRunningAccounts(); - - // Clean up the storage engine database - mSyncStorageEngine.doDatabaseCleanup(new Account[0], userId); - synchronized (mSyncQueue) { - mSyncQueue.removeUser(userId); - } - } - - /** - * @hide - */ - class ActiveSyncContext extends ISyncContext.Stub - implements ServiceConnection, IBinder.DeathRecipient { - final SyncOperation mSyncOperation; - final long mHistoryRowId; - ISyncAdapter mSyncAdapter; - final long mStartTime; - long mTimeoutStartTime; - boolean mBound; - final PowerManager.WakeLock mSyncWakeLock; - final int mSyncAdapterUid; - SyncInfo mSyncInfo; - boolean mIsLinkedToDeath = false; - - /** - * Create an ActiveSyncContext for an impending sync and grab the wakelock for that - * sync adapter. Since this grabs the wakelock you need to be sure to call - * close() when you are done with this ActiveSyncContext, whether the sync succeeded - * or not. - * @param syncOperation the SyncOperation we are about to sync - * @param historyRowId the row in which to record the history info for this sync - * @param syncAdapterUid the UID of the application that contains the sync adapter - * for this sync. This is used to attribute the wakelock hold to that application. - */ - public ActiveSyncContext(SyncOperation syncOperation, long historyRowId, - int syncAdapterUid) { - super(); - mSyncAdapterUid = syncAdapterUid; - mSyncOperation = syncOperation; - mHistoryRowId = historyRowId; - mSyncAdapter = null; - mStartTime = SystemClock.elapsedRealtime(); - mTimeoutStartTime = mStartTime; - mSyncWakeLock = mSyncHandler.getSyncWakeLock( - mSyncOperation.account, mSyncOperation.authority); - mSyncWakeLock.setWorkSource(new WorkSource(syncAdapterUid)); - mSyncWakeLock.acquire(); - } - - public void sendHeartbeat() { - // heartbeats are no longer used - } - - public void onFinished(SyncResult result) { - if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "onFinished: " + this); - // include "this" in the message so that the handler can ignore it if this - // ActiveSyncContext is no longer the mActiveSyncContext at message handling - // time - sendSyncFinishedOrCanceledMessage(this, result); - } - - public void toString(StringBuilder sb) { - sb.append("startTime ").append(mStartTime) - .append(", mTimeoutStartTime ").append(mTimeoutStartTime) - .append(", mHistoryRowId ").append(mHistoryRowId) - .append(", syncOperation ").append(mSyncOperation); - } - - public void onServiceConnected(ComponentName name, IBinder service) { - Message msg = mSyncHandler.obtainMessage(); - msg.what = SyncHandler.MESSAGE_SERVICE_CONNECTED; - msg.obj = new ServiceConnectionData(this, ISyncAdapter.Stub.asInterface(service)); - mSyncHandler.sendMessage(msg); - } - - public void onServiceDisconnected(ComponentName name) { - Message msg = mSyncHandler.obtainMessage(); - msg.what = SyncHandler.MESSAGE_SERVICE_DISCONNECTED; - msg.obj = new ServiceConnectionData(this, null); - mSyncHandler.sendMessage(msg); - } - - boolean bindToSyncAdapter(RegisteredServicesCache.ServiceInfo info, int userId) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.d(TAG, "bindToSyncAdapter: " + info.componentName + ", connection " + this); - } - Intent intent = new Intent(); - intent.setAction("android.content.SyncAdapter"); - intent.setComponent(info.componentName); - intent.putExtra(Intent.EXTRA_CLIENT_LABEL, - com.android.internal.R.string.sync_binding_label); - intent.putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivityAsUser( - mContext, 0, new Intent(Settings.ACTION_SYNC_SETTINGS), 0, - null, new UserHandle(userId))); - mBound = true; - final boolean bindResult = mContext.bindService(intent, this, - Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND - | Context.BIND_ALLOW_OOM_MANAGEMENT, - mSyncOperation.userId); - if (!bindResult) { - mBound = false; - } - return bindResult; - } - - /** - * Performs the required cleanup, which is the releasing of the wakelock and - * unbinding from the sync adapter (if actually bound). - */ - protected void close() { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.d(TAG, "unBindFromSyncAdapter: connection " + this); - } - if (mBound) { - mBound = false; - mContext.unbindService(this); - } - mSyncWakeLock.release(); - mSyncWakeLock.setWorkSource(null); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - toString(sb); - return sb.toString(); - } - - @Override - public void binderDied() { - sendSyncFinishedOrCanceledMessage(this, null); - } - } - - protected void dump(FileDescriptor fd, PrintWriter pw) { - final IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " "); - dumpSyncState(ipw); - dumpSyncHistory(ipw); - dumpSyncAdapters(ipw); - } - - static String formatTime(long time) { - Time tobj = new Time(); - tobj.set(time); - return tobj.format("%Y-%m-%d %H:%M:%S"); - } - - protected void dumpSyncState(PrintWriter pw) { - pw.print("data connected: "); pw.println(mDataConnectionIsConnected); - pw.print("auto sync: "); - List<UserInfo> users = getAllUsers(); - if (users != null) { - for (UserInfo user : users) { - pw.print("u" + user.id + "=" - + mSyncStorageEngine.getMasterSyncAutomatically(user.id) + " "); - } - pw.println(); - } - pw.print("memory low: "); pw.println(mStorageIsLow); - - final AccountAndUser[] accounts = AccountManagerService.getSingleton().getAllAccounts(); - - pw.print("accounts: "); - if (accounts != INITIAL_ACCOUNTS_ARRAY) { - pw.println(accounts.length); - } else { - pw.println("not known yet"); - } - final long now = SystemClock.elapsedRealtime(); - pw.print("now: "); pw.print(now); - pw.println(" (" + formatTime(System.currentTimeMillis()) + ")"); - pw.print("offset: "); pw.print(DateUtils.formatElapsedTime(mSyncRandomOffsetMillis/1000)); - pw.println(" (HH:MM:SS)"); - pw.print("uptime: "); pw.print(DateUtils.formatElapsedTime(now/1000)); - pw.println(" (HH:MM:SS)"); - pw.print("time spent syncing: "); - pw.print(DateUtils.formatElapsedTime( - mSyncHandler.mSyncTimeTracker.timeSpentSyncing() / 1000)); - pw.print(" (HH:MM:SS), sync "); - pw.print(mSyncHandler.mSyncTimeTracker.mLastWasSyncing ? "" : "not "); - pw.println("in progress"); - if (mSyncHandler.mAlarmScheduleTime != null) { - pw.print("next alarm time: "); pw.print(mSyncHandler.mAlarmScheduleTime); - pw.print(" ("); - pw.print(DateUtils.formatElapsedTime((mSyncHandler.mAlarmScheduleTime-now)/1000)); - pw.println(" (HH:MM:SS) from now)"); - } else { - pw.println("no alarm is scheduled (there had better not be any pending syncs)"); - } - - pw.print("notification info: "); - final StringBuilder sb = new StringBuilder(); - mSyncHandler.mSyncNotificationInfo.toString(sb); - pw.println(sb.toString()); - - pw.println(); - pw.println("Active Syncs: " + mActiveSyncContexts.size()); - for (SyncManager.ActiveSyncContext activeSyncContext : mActiveSyncContexts) { - final long durationInSeconds = (now - activeSyncContext.mStartTime) / 1000; - pw.print(" "); - pw.print(DateUtils.formatElapsedTime(durationInSeconds)); - pw.print(" - "); - pw.print(activeSyncContext.mSyncOperation.dump(false)); - pw.println(); - } - - synchronized (mSyncQueue) { - sb.setLength(0); - mSyncQueue.dump(sb); - } - pw.println(); - pw.print(sb.toString()); - - // join the installed sync adapter with the accounts list and emit for everything - pw.println(); - pw.println("Sync Status"); - for (AccountAndUser account : accounts) { - pw.print(" Account "); pw.print(account.account.name); - pw.print(" u"); pw.print(account.userId); - pw.print(" "); pw.print(account.account.type); - pw.println(":"); - for (RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapterType : - mSyncAdapters.getAllServices(account.userId)) { - if (!syncAdapterType.type.accountType.equals(account.account.type)) { - continue; - } - - SyncStorageEngine.AuthorityInfo settings = - mSyncStorageEngine.getOrCreateAuthority( - account.account, account.userId, syncAdapterType.type.authority); - SyncStatusInfo status = mSyncStorageEngine.getOrCreateSyncStatus(settings); - pw.print(" "); pw.print(settings.authority); - pw.println(":"); - pw.print(" settings:"); - pw.print(" " + (settings.syncable > 0 - ? "syncable" - : (settings.syncable == 0 ? "not syncable" : "not initialized"))); - pw.print(", " + (settings.enabled ? "enabled" : "disabled")); - if (settings.delayUntil > now) { - pw.print(", delay for " - + ((settings.delayUntil - now) / 1000) + " sec"); - } - if (settings.backoffTime > now) { - pw.print(", backoff for " - + ((settings.backoffTime - now) / 1000) + " sec"); - } - if (settings.backoffDelay > 0) { - pw.print(", the backoff increment is " + settings.backoffDelay / 1000 - + " sec"); - } - pw.println(); - for (int periodicIndex = 0; - periodicIndex < settings.periodicSyncs.size(); - periodicIndex++) { - Pair<Bundle, Long> info = settings.periodicSyncs.get(periodicIndex); - long lastPeriodicTime = status.getPeriodicSyncTime(periodicIndex); - long nextPeriodicTime = lastPeriodicTime + info.second * 1000; - pw.println(" periodic period=" + info.second - + ", extras=" + info.first - + ", next=" + formatTime(nextPeriodicTime)); - } - pw.print(" count: local="); pw.print(status.numSourceLocal); - pw.print(" poll="); pw.print(status.numSourcePoll); - pw.print(" periodic="); pw.print(status.numSourcePeriodic); - pw.print(" server="); pw.print(status.numSourceServer); - pw.print(" user="); pw.print(status.numSourceUser); - pw.print(" total="); pw.print(status.numSyncs); - pw.println(); - pw.print(" total duration: "); - pw.println(DateUtils.formatElapsedTime(status.totalElapsedTime/1000)); - if (status.lastSuccessTime != 0) { - pw.print(" SUCCESS: source="); - pw.print(SyncStorageEngine.SOURCES[status.lastSuccessSource]); - pw.print(" time="); - pw.println(formatTime(status.lastSuccessTime)); - } - if (status.lastFailureTime != 0) { - pw.print(" FAILURE: source="); - pw.print(SyncStorageEngine.SOURCES[ - status.lastFailureSource]); - pw.print(" initialTime="); - pw.print(formatTime(status.initialFailureTime)); - pw.print(" lastTime="); - pw.println(formatTime(status.lastFailureTime)); - int errCode = status.getLastFailureMesgAsInt(0); - pw.print(" message: "); pw.println( - getLastFailureMessage(errCode) + " (" + errCode + ")"); - } - } - } - } - - private String getLastFailureMessage(int code) { - switch (code) { - case ContentResolver.SYNC_ERROR_SYNC_ALREADY_IN_PROGRESS: - return "sync already in progress"; - - case ContentResolver.SYNC_ERROR_AUTHENTICATION: - return "authentication error"; - - case ContentResolver.SYNC_ERROR_IO: - return "I/O error"; - - case ContentResolver.SYNC_ERROR_PARSE: - return "parse error"; - - case ContentResolver.SYNC_ERROR_CONFLICT: - return "conflict error"; - - case ContentResolver.SYNC_ERROR_TOO_MANY_DELETIONS: - return "too many deletions error"; - - case ContentResolver.SYNC_ERROR_TOO_MANY_RETRIES: - return "too many retries error"; - - case ContentResolver.SYNC_ERROR_INTERNAL: - return "internal error"; - - default: - return "unknown"; - } - } - - private void dumpTimeSec(PrintWriter pw, long time) { - pw.print(time/1000); pw.print('.'); pw.print((time/100)%10); - pw.print('s'); - } - - private void dumpDayStatistic(PrintWriter pw, SyncStorageEngine.DayStats ds) { - pw.print("Success ("); pw.print(ds.successCount); - if (ds.successCount > 0) { - pw.print(" for "); dumpTimeSec(pw, ds.successTime); - pw.print(" avg="); dumpTimeSec(pw, ds.successTime/ds.successCount); - } - pw.print(") Failure ("); pw.print(ds.failureCount); - if (ds.failureCount > 0) { - pw.print(" for "); dumpTimeSec(pw, ds.failureTime); - pw.print(" avg="); dumpTimeSec(pw, ds.failureTime/ds.failureCount); - } - pw.println(")"); - } - - protected void dumpSyncHistory(PrintWriter pw) { - dumpRecentHistory(pw); - dumpDayStatistics(pw); - } - - private void dumpRecentHistory(PrintWriter pw) { - final ArrayList<SyncStorageEngine.SyncHistoryItem> items - = mSyncStorageEngine.getSyncHistory(); - if (items != null && items.size() > 0) { - final Map<String, AuthoritySyncStats> authorityMap = Maps.newHashMap(); - long totalElapsedTime = 0; - long totalTimes = 0; - final int N = items.size(); - - int maxAuthority = 0; - int maxAccount = 0; - for (SyncStorageEngine.SyncHistoryItem item : items) { - SyncStorageEngine.AuthorityInfo authority - = mSyncStorageEngine.getAuthority(item.authorityId); - final String authorityName; - final String accountKey; - if (authority != null) { - authorityName = authority.authority; - accountKey = authority.account.name + "/" + authority.account.type - + " u" + authority.userId; - } else { - authorityName = "Unknown"; - accountKey = "Unknown"; - } - - int length = authorityName.length(); - if (length > maxAuthority) { - maxAuthority = length; - } - length = accountKey.length(); - if (length > maxAccount) { - maxAccount = length; - } - - final long elapsedTime = item.elapsedTime; - totalElapsedTime += elapsedTime; - totalTimes++; - AuthoritySyncStats authoritySyncStats = authorityMap.get(authorityName); - if (authoritySyncStats == null) { - authoritySyncStats = new AuthoritySyncStats(authorityName); - authorityMap.put(authorityName, authoritySyncStats); - } - authoritySyncStats.elapsedTime += elapsedTime; - authoritySyncStats.times++; - final Map<String, AccountSyncStats> accountMap = authoritySyncStats.accountMap; - AccountSyncStats accountSyncStats = accountMap.get(accountKey); - if (accountSyncStats == null) { - accountSyncStats = new AccountSyncStats(accountKey); - accountMap.put(accountKey, accountSyncStats); - } - accountSyncStats.elapsedTime += elapsedTime; - accountSyncStats.times++; - - } - - if (totalElapsedTime > 0) { - pw.println(); - pw.printf("Detailed Statistics (Recent history): " - + "%d (# of times) %ds (sync time)\n", - totalTimes, totalElapsedTime / 1000); - - final List<AuthoritySyncStats> sortedAuthorities = - new ArrayList<AuthoritySyncStats>(authorityMap.values()); - Collections.sort(sortedAuthorities, new Comparator<AuthoritySyncStats>() { - @Override - public int compare(AuthoritySyncStats lhs, AuthoritySyncStats rhs) { - // reverse order - int compare = Integer.compare(rhs.times, lhs.times); - if (compare == 0) { - compare = Long.compare(rhs.elapsedTime, lhs.elapsedTime); - } - return compare; - } - }); - - final int maxLength = Math.max(maxAuthority, maxAccount + 3); - final int padLength = 2 + 2 + maxLength + 2 + 10 + 11; - final char chars[] = new char[padLength]; - Arrays.fill(chars, '-'); - final String separator = new String(chars); - - final String authorityFormat = - String.format(" %%-%ds: %%-9s %%-11s\n", maxLength + 2); - final String accountFormat = - String.format(" %%-%ds: %%-9s %%-11s\n", maxLength); - - pw.println(separator); - for (AuthoritySyncStats authoritySyncStats : sortedAuthorities) { - String name = authoritySyncStats.name; - long elapsedTime; - int times; - String timeStr; - String timesStr; - - elapsedTime = authoritySyncStats.elapsedTime; - times = authoritySyncStats.times; - timeStr = String.format("%ds/%d%%", - elapsedTime / 1000, - elapsedTime * 100 / totalElapsedTime); - timesStr = String.format("%d/%d%%", - times, - times * 100 / totalTimes); - pw.printf(authorityFormat, name, timesStr, timeStr); - - final List<AccountSyncStats> sortedAccounts = - new ArrayList<AccountSyncStats>( - authoritySyncStats.accountMap.values()); - Collections.sort(sortedAccounts, new Comparator<AccountSyncStats>() { - @Override - public int compare(AccountSyncStats lhs, AccountSyncStats rhs) { - // reverse order - int compare = Integer.compare(rhs.times, lhs.times); - if (compare == 0) { - compare = Long.compare(rhs.elapsedTime, lhs.elapsedTime); - } - return compare; - } - }); - for (AccountSyncStats stats: sortedAccounts) { - elapsedTime = stats.elapsedTime; - times = stats.times; - timeStr = String.format("%ds/%d%%", - elapsedTime / 1000, - elapsedTime * 100 / totalElapsedTime); - timesStr = String.format("%d/%d%%", - times, - times * 100 / totalTimes); - pw.printf(accountFormat, stats.name, timesStr, timeStr); - } - pw.println(separator); - } - } - - pw.println(); - pw.println("Recent Sync History"); - final String format = " %-" + maxAccount + "s %s\n"; - final Map<String, Long> lastTimeMap = Maps.newHashMap(); - - for (int i = 0; i < N; i++) { - SyncStorageEngine.SyncHistoryItem item = items.get(i); - SyncStorageEngine.AuthorityInfo authority - = mSyncStorageEngine.getAuthority(item.authorityId); - final String authorityName; - final String accountKey; - if (authority != null) { - authorityName = authority.authority; - accountKey = authority.account.name + "/" + authority.account.type - + " u" + authority.userId; - } else { - authorityName = "Unknown"; - accountKey = "Unknown"; - } - final long elapsedTime = item.elapsedTime; - final Time time = new Time(); - final long eventTime = item.eventTime; - time.set(eventTime); - - final String key = authorityName + "/" + accountKey; - final Long lastEventTime = lastTimeMap.get(key); - final String diffString; - if (lastEventTime == null) { - diffString = ""; - } else { - final long diff = (lastEventTime - eventTime) / 1000; - if (diff < 60) { - diffString = String.valueOf(diff); - } else if (diff < 3600) { - diffString = String.format("%02d:%02d", diff / 60, diff % 60); - } else { - final long sec = diff % 3600; - diffString = String.format("%02d:%02d:%02d", - diff / 3600, sec / 60, sec % 60); - } - } - lastTimeMap.put(key, eventTime); - - pw.printf(" #%-3d: %s %8s %5.1fs %8s", - i + 1, - formatTime(eventTime), - SyncStorageEngine.SOURCES[item.source], - ((float) elapsedTime) / 1000, - diffString); - pw.printf(format, accountKey, authorityName); - - if (item.event != SyncStorageEngine.EVENT_STOP - || item.upstreamActivity != 0 - || item.downstreamActivity != 0) { - pw.printf(" event=%d upstreamActivity=%d downstreamActivity=%d\n", - item.event, - item.upstreamActivity, - item.downstreamActivity); - } - if (item.mesg != null - && !SyncStorageEngine.MESG_SUCCESS.equals(item.mesg)) { - pw.printf(" mesg=%s\n", item.mesg); - } - } - } - } - - private void dumpDayStatistics(PrintWriter pw) { - SyncStorageEngine.DayStats dses[] = mSyncStorageEngine.getDayStatistics(); - if (dses != null && dses[0] != null) { - pw.println(); - pw.println("Sync Statistics"); - pw.print(" Today: "); dumpDayStatistic(pw, dses[0]); - int today = dses[0].day; - int i; - SyncStorageEngine.DayStats ds; - - // Print each day in the current week. - for (i=1; i<=6 && i < dses.length; i++) { - ds = dses[i]; - if (ds == null) break; - int delta = today-ds.day; - if (delta > 6) break; - - pw.print(" Day-"); pw.print(delta); pw.print(": "); - dumpDayStatistic(pw, ds); - } - - // Aggregate all following days into weeks and print totals. - int weekDay = today; - while (i < dses.length) { - SyncStorageEngine.DayStats aggr = null; - weekDay -= 7; - while (i < dses.length) { - ds = dses[i]; - if (ds == null) { - i = dses.length; - break; - } - int delta = weekDay-ds.day; - if (delta > 6) break; - i++; - - if (aggr == null) { - aggr = new SyncStorageEngine.DayStats(weekDay); - } - aggr.successCount += ds.successCount; - aggr.successTime += ds.successTime; - aggr.failureCount += ds.failureCount; - aggr.failureTime += ds.failureTime; - } - if (aggr != null) { - pw.print(" Week-"); pw.print((today-weekDay)/7); pw.print(": "); - dumpDayStatistic(pw, aggr); - } - } - } - } - - private void dumpSyncAdapters(IndentingPrintWriter pw) { - pw.println(); - final List<UserInfo> users = getAllUsers(); - if (users != null) { - for (UserInfo user : users) { - pw.println("Sync adapters for " + user + ":"); - pw.increaseIndent(); - for (RegisteredServicesCache.ServiceInfo<?> info : - mSyncAdapters.getAllServices(user.id)) { - pw.println(info); - } - pw.decreaseIndent(); - pw.println(); - } - } - } - - private static class AuthoritySyncStats { - String name; - long elapsedTime; - int times; - Map<String, AccountSyncStats> accountMap = Maps.newHashMap(); - - private AuthoritySyncStats(String name) { - this.name = name; - } - } - - private static class AccountSyncStats { - String name; - long elapsedTime; - int times; - - private AccountSyncStats(String name) { - this.name = name; - } - } - - /** - * A helper object to keep track of the time we have spent syncing since the last boot - */ - private class SyncTimeTracker { - /** True if a sync was in progress on the most recent call to update() */ - boolean mLastWasSyncing = false; - /** Used to track when lastWasSyncing was last set */ - long mWhenSyncStarted = 0; - /** The cumulative time we have spent syncing */ - private long mTimeSpentSyncing; - - /** Call to let the tracker know that the sync state may have changed */ - public synchronized void update() { - final boolean isSyncInProgress = !mActiveSyncContexts.isEmpty(); - if (isSyncInProgress == mLastWasSyncing) return; - final long now = SystemClock.elapsedRealtime(); - if (isSyncInProgress) { - mWhenSyncStarted = now; - } else { - mTimeSpentSyncing += now - mWhenSyncStarted; - } - mLastWasSyncing = isSyncInProgress; - } - - /** Get how long we have been syncing, in ms */ - public synchronized long timeSpentSyncing() { - if (!mLastWasSyncing) return mTimeSpentSyncing; - - final long now = SystemClock.elapsedRealtime(); - return mTimeSpentSyncing + (now - mWhenSyncStarted); - } - } - - class ServiceConnectionData { - public final ActiveSyncContext activeSyncContext; - public final ISyncAdapter syncAdapter; - ServiceConnectionData(ActiveSyncContext activeSyncContext, ISyncAdapter syncAdapter) { - this.activeSyncContext = activeSyncContext; - this.syncAdapter = syncAdapter; - } - } - - /** - * Handles SyncOperation Messages that are posted to the associated - * HandlerThread. - */ - class SyncHandler extends Handler { - // Messages that can be sent on mHandler - private static final int MESSAGE_SYNC_FINISHED = 1; - private static final int MESSAGE_SYNC_ALARM = 2; - private static final int MESSAGE_CHECK_ALARMS = 3; - private static final int MESSAGE_SERVICE_CONNECTED = 4; - private static final int MESSAGE_SERVICE_DISCONNECTED = 5; - private static final int MESSAGE_CANCEL = 6; - - public final SyncNotificationInfo mSyncNotificationInfo = new SyncNotificationInfo(); - private Long mAlarmScheduleTime = null; - public final SyncTimeTracker mSyncTimeTracker = new SyncTimeTracker(); - private final HashMap<Pair<Account, String>, PowerManager.WakeLock> mWakeLocks = - Maps.newHashMap(); - - private volatile CountDownLatch mReadyToRunLatch = new CountDownLatch(1); - - public void onBootCompleted() { - mBootCompleted = true; - - doDatabaseCleanup(); - - if (mReadyToRunLatch != null) { - mReadyToRunLatch.countDown(); - } - } - - private PowerManager.WakeLock getSyncWakeLock(Account account, String authority) { - final Pair<Account, String> wakeLockKey = Pair.create(account, authority); - PowerManager.WakeLock wakeLock = mWakeLocks.get(wakeLockKey); - if (wakeLock == null) { - final String name = SYNC_WAKE_LOCK_PREFIX + "_" + authority + "_" + account; - wakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, name); - wakeLock.setReferenceCounted(false); - mWakeLocks.put(wakeLockKey, wakeLock); - } - return wakeLock; - } - - private void waitUntilReadyToRun() { - CountDownLatch latch = mReadyToRunLatch; - if (latch != null) { - while (true) { - try { - latch.await(); - mReadyToRunLatch = null; - return; - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - } - } - /** - * Used to keep track of whether a sync notification is active and who it is for. - */ - class SyncNotificationInfo { - // true iff the notification manager has been asked to send the notification - public boolean isActive = false; - - // Set when we transition from not running a sync to running a sync, and cleared on - // the opposite transition. - public Long startTime = null; - - public void toString(StringBuilder sb) { - sb.append("isActive ").append(isActive).append(", startTime ").append(startTime); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - toString(sb); - return sb.toString(); - } - } - - public SyncHandler(Looper looper) { - super(looper); - } - - public void handleMessage(Message msg) { - long earliestFuturePollTime = Long.MAX_VALUE; - long nextPendingSyncTime = Long.MAX_VALUE; - - // Setting the value here instead of a method because we want the dumpsys logs - // to have the most recent value used. - try { - waitUntilReadyToRun(); - mDataConnectionIsConnected = readDataConnectionState(); - mSyncManagerWakeLock.acquire(); - // Always do this first so that we be sure that any periodic syncs that - // are ready to run have been converted into pending syncs. This allows the - // logic that considers the next steps to take based on the set of pending syncs - // to also take into account the periodic syncs. - earliestFuturePollTime = scheduleReadyPeriodicSyncs(); - switch (msg.what) { - case SyncHandler.MESSAGE_CANCEL: { - Pair<Account, String> payload = (Pair<Account, String>)msg.obj; - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.d(TAG, "handleSyncHandlerMessage: MESSAGE_SERVICE_CANCEL: " - + payload.first + ", " + payload.second); - } - cancelActiveSyncLocked(payload.first, msg.arg1, payload.second); - nextPendingSyncTime = maybeStartNextSyncLocked(); - break; - } - - case SyncHandler.MESSAGE_SYNC_FINISHED: - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "handleSyncHandlerMessage: MESSAGE_SYNC_FINISHED"); - } - SyncHandlerMessagePayload payload = (SyncHandlerMessagePayload)msg.obj; - if (!isSyncStillActive(payload.activeSyncContext)) { - Log.d(TAG, "handleSyncHandlerMessage: dropping since the " - + "sync is no longer active: " - + payload.activeSyncContext); - break; - } - runSyncFinishedOrCanceledLocked(payload.syncResult, payload.activeSyncContext); - - // since a sync just finished check if it is time to start a new sync - nextPendingSyncTime = maybeStartNextSyncLocked(); - break; - - case SyncHandler.MESSAGE_SERVICE_CONNECTED: { - ServiceConnectionData msgData = (ServiceConnectionData)msg.obj; - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.d(TAG, "handleSyncHandlerMessage: MESSAGE_SERVICE_CONNECTED: " - + msgData.activeSyncContext); - } - // check that this isn't an old message - if (isSyncStillActive(msgData.activeSyncContext)) { - runBoundToSyncAdapter(msgData.activeSyncContext, msgData.syncAdapter); - } - break; - } - - case SyncHandler.MESSAGE_SERVICE_DISCONNECTED: { - final ActiveSyncContext currentSyncContext = - ((ServiceConnectionData)msg.obj).activeSyncContext; - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.d(TAG, "handleSyncHandlerMessage: MESSAGE_SERVICE_DISCONNECTED: " - + currentSyncContext); - } - // check that this isn't an old message - if (isSyncStillActive(currentSyncContext)) { - // cancel the sync if we have a syncadapter, which means one is - // outstanding - if (currentSyncContext.mSyncAdapter != null) { - try { - currentSyncContext.mSyncAdapter.cancelSync(currentSyncContext); - } catch (RemoteException e) { - // we don't need to retry this in this case - } - } - - // pretend that the sync failed with an IOException, - // which is a soft error - SyncResult syncResult = new SyncResult(); - syncResult.stats.numIoExceptions++; - runSyncFinishedOrCanceledLocked(syncResult, currentSyncContext); - - // since a sync just finished check if it is time to start a new sync - nextPendingSyncTime = maybeStartNextSyncLocked(); - } - - break; - } - - case SyncHandler.MESSAGE_SYNC_ALARM: { - boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE); - if (isLoggable) { - Log.v(TAG, "handleSyncHandlerMessage: MESSAGE_SYNC_ALARM"); - } - mAlarmScheduleTime = null; - try { - nextPendingSyncTime = maybeStartNextSyncLocked(); - } finally { - mHandleAlarmWakeLock.release(); - } - break; - } - - case SyncHandler.MESSAGE_CHECK_ALARMS: - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "handleSyncHandlerMessage: MESSAGE_CHECK_ALARMS"); - } - nextPendingSyncTime = maybeStartNextSyncLocked(); - break; - } - } finally { - manageSyncNotificationLocked(); - manageSyncAlarmLocked(earliestFuturePollTime, nextPendingSyncTime); - mSyncTimeTracker.update(); - mSyncManagerWakeLock.release(); - } - } - - /** - * Turn any periodic sync operations that are ready to run into pending sync operations. - * @return the desired start time of the earliest future periodic sync operation, - * in milliseconds since boot - */ - private long scheduleReadyPeriodicSyncs() { - final boolean backgroundDataUsageAllowed = - getConnectivityManager().getBackgroundDataSetting(); - long earliestFuturePollTime = Long.MAX_VALUE; - if (!backgroundDataUsageAllowed) { - return earliestFuturePollTime; - } - - AccountAndUser[] accounts = mRunningAccounts; - - final long nowAbsolute = System.currentTimeMillis(); - final long shiftedNowAbsolute = (0 < nowAbsolute - mSyncRandomOffsetMillis) - ? (nowAbsolute - mSyncRandomOffsetMillis) : 0; - - ArrayList<SyncStorageEngine.AuthorityInfo> infos = mSyncStorageEngine.getAuthorities(); - for (SyncStorageEngine.AuthorityInfo info : infos) { - // skip the sync if the account of this operation no longer exists - if (!containsAccountAndUser(accounts, info.account, info.userId)) { - continue; - } - - if (!mSyncStorageEngine.getMasterSyncAutomatically(info.userId) - || !mSyncStorageEngine.getSyncAutomatically(info.account, info.userId, - info.authority)) { - continue; - } - - if (mSyncStorageEngine.getIsSyncable(info.account, info.userId, info.authority) - == 0) { - continue; - } - - SyncStatusInfo status = mSyncStorageEngine.getOrCreateSyncStatus(info); - for (int i = 0, N = info.periodicSyncs.size(); i < N; i++) { - final Bundle extras = info.periodicSyncs.get(i).first; - final Long periodInMillis = info.periodicSyncs.get(i).second * 1000; - // find when this periodic sync was last scheduled to run - final long lastPollTimeAbsolute = status.getPeriodicSyncTime(i); - - long remainingMillis - = periodInMillis - (shiftedNowAbsolute % periodInMillis); - - /* - * Sync scheduling strategy: - * Set the next periodic sync based on a random offset (in seconds). - * - * Also sync right now if any of the following cases hold - * and mark it as having been scheduled - * - * Case 1: This sync is ready to run now. - * Case 2: If the lastPollTimeAbsolute is in the future, - * sync now and reinitialize. This can happen for - * example if the user changed the time, synced and - * changed back. - * Case 3: If we failed to sync at the last scheduled time - */ - if (remainingMillis == periodInMillis // Case 1 - || lastPollTimeAbsolute > nowAbsolute // Case 2 - || (nowAbsolute - lastPollTimeAbsolute - >= periodInMillis)) { // Case 3 - // Sync now - final Pair<Long, Long> backoff = mSyncStorageEngine.getBackoff( - info.account, info.userId, info.authority); - final RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapterInfo; - syncAdapterInfo = mSyncAdapters.getServiceInfo( - SyncAdapterType.newKey(info.authority, info.account.type), - info.userId); - if (syncAdapterInfo == null) { - continue; - } - scheduleSyncOperation( - new SyncOperation(info.account, info.userId, - SyncStorageEngine.SOURCE_PERIODIC, - info.authority, extras, 0 /* delay */, - backoff != null ? backoff.first : 0, - mSyncStorageEngine.getDelayUntilTime( - info.account, info.userId, info.authority), - syncAdapterInfo.type.allowParallelSyncs())); - status.setPeriodicSyncTime(i, nowAbsolute); - } - // Compute when this periodic sync should next run - final long nextPollTimeAbsolute = nowAbsolute + remainingMillis; - - // remember this time if it is earlier than earliestFuturePollTime - if (nextPollTimeAbsolute < earliestFuturePollTime) { - earliestFuturePollTime = nextPollTimeAbsolute; - } - } - } - - if (earliestFuturePollTime == Long.MAX_VALUE) { - return Long.MAX_VALUE; - } - - // convert absolute time to elapsed time - return SystemClock.elapsedRealtime() - + ((earliestFuturePollTime < nowAbsolute) - ? 0 - : (earliestFuturePollTime - nowAbsolute)); - } - - private long maybeStartNextSyncLocked() { - final boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE); - if (isLoggable) Log.v(TAG, "maybeStartNextSync"); - - // If we aren't ready to run (e.g. the data connection is down), get out. - if (!mDataConnectionIsConnected) { - if (isLoggable) { - Log.v(TAG, "maybeStartNextSync: no data connection, skipping"); - } - return Long.MAX_VALUE; - } - - if (mStorageIsLow) { - if (isLoggable) { - Log.v(TAG, "maybeStartNextSync: memory low, skipping"); - } - return Long.MAX_VALUE; - } - - // If the accounts aren't known yet then we aren't ready to run. We will be kicked - // when the account lookup request does complete. - AccountAndUser[] accounts = mRunningAccounts; - if (accounts == INITIAL_ACCOUNTS_ARRAY) { - if (isLoggable) { - Log.v(TAG, "maybeStartNextSync: accounts not known, skipping"); - } - return Long.MAX_VALUE; - } - - // Otherwise consume SyncOperations from the head of the SyncQueue until one is - // found that is runnable (not disabled, etc). If that one is ready to run then - // start it, otherwise just get out. - final boolean backgroundDataUsageAllowed = - getConnectivityManager().getBackgroundDataSetting(); - - final long now = SystemClock.elapsedRealtime(); - - // will be set to the next time that a sync should be considered for running - long nextReadyToRunTime = Long.MAX_VALUE; - - // order the sync queue, dropping syncs that are not allowed - ArrayList<SyncOperation> operations = new ArrayList<SyncOperation>(); - synchronized (mSyncQueue) { - if (isLoggable) { - Log.v(TAG, "build the operation array, syncQueue size is " - + mSyncQueue.getOperations().size()); - } - final Iterator<SyncOperation> operationIterator = mSyncQueue.getOperations() - .iterator(); - - final ActivityManager activityManager - = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); - final Set<Integer> removedUsers = Sets.newHashSet(); - while (operationIterator.hasNext()) { - final SyncOperation op = operationIterator.next(); - - // drop the sync if the account of this operation no longer exists - if (!containsAccountAndUser(accounts, op.account, op.userId)) { - operationIterator.remove(); - mSyncStorageEngine.deleteFromPending(op.pendingOperation); - continue; - } - - // drop this sync request if it isn't syncable - int syncableState = mSyncStorageEngine.getIsSyncable( - op.account, op.userId, op.authority); - if (syncableState == 0) { - operationIterator.remove(); - mSyncStorageEngine.deleteFromPending(op.pendingOperation); - continue; - } - - // if the user in not running, drop the request - if (!activityManager.isUserRunning(op.userId)) { - final UserInfo userInfo = mUserManager.getUserInfo(op.userId); - if (userInfo == null) { - removedUsers.add(op.userId); - } - continue; - } - - // if the next run time is in the future, meaning there are no syncs ready - // to run, return the time - if (op.effectiveRunTime > now) { - if (nextReadyToRunTime > op.effectiveRunTime) { - nextReadyToRunTime = op.effectiveRunTime; - } - continue; - } - - final RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapterInfo; - syncAdapterInfo = mSyncAdapters.getServiceInfo( - SyncAdapterType.newKey(op.authority, op.account.type), op.userId); - - // only proceed if network is connected for requesting UID - final boolean uidNetworkConnected; - if (syncAdapterInfo != null) { - final NetworkInfo networkInfo = getConnectivityManager() - .getActiveNetworkInfoForUid(syncAdapterInfo.uid); - uidNetworkConnected = networkInfo != null && networkInfo.isConnected(); - } else { - uidNetworkConnected = false; - } - - // skip the sync if it isn't manual, and auto sync or - // background data usage is disabled or network is - // disconnected for the target UID. - if (!op.extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, false) - && (syncableState > 0) - && (!mSyncStorageEngine.getMasterSyncAutomatically(op.userId) - || !backgroundDataUsageAllowed - || !uidNetworkConnected - || !mSyncStorageEngine.getSyncAutomatically( - op.account, op.userId, op.authority))) { - operationIterator.remove(); - mSyncStorageEngine.deleteFromPending(op.pendingOperation); - continue; - } - - operations.add(op); - } - for (Integer user : removedUsers) { - // if it's still removed - if (mUserManager.getUserInfo(user) == null) { - onUserRemoved(user); - } - } - } - - // find the next operation to dispatch, if one is ready - // iterate from the top, keep issuing (while potentially cancelling existing syncs) - // until the quotas are filled. - // once the quotas are filled iterate once more to find when the next one would be - // (also considering pre-emption reasons). - if (isLoggable) Log.v(TAG, "sort the candidate operations, size " + operations.size()); - Collections.sort(operations); - if (isLoggable) Log.v(TAG, "dispatch all ready sync operations"); - for (int i = 0, N = operations.size(); i < N; i++) { - final SyncOperation candidate = operations.get(i); - final boolean candidateIsInitialization = candidate.isInitialization(); - - int numInit = 0; - int numRegular = 0; - ActiveSyncContext conflict = null; - ActiveSyncContext longRunning = null; - ActiveSyncContext toReschedule = null; - ActiveSyncContext oldestNonExpeditedRegular = null; - - for (ActiveSyncContext activeSyncContext : mActiveSyncContexts) { - final SyncOperation activeOp = activeSyncContext.mSyncOperation; - if (activeOp.isInitialization()) { - numInit++; - } else { - numRegular++; - if (!activeOp.isExpedited()) { - if (oldestNonExpeditedRegular == null - || (oldestNonExpeditedRegular.mStartTime - > activeSyncContext.mStartTime)) { - oldestNonExpeditedRegular = activeSyncContext; - } - } - } - if (activeOp.account.type.equals(candidate.account.type) - && activeOp.authority.equals(candidate.authority) - && activeOp.userId == candidate.userId - && (!activeOp.allowParallelSyncs - || activeOp.account.name.equals(candidate.account.name))) { - conflict = activeSyncContext; - // don't break out since we want to do a full count of the varieties - } else { - if (candidateIsInitialization == activeOp.isInitialization() - && activeSyncContext.mStartTime + MAX_TIME_PER_SYNC < now) { - longRunning = activeSyncContext; - // don't break out since we want to do a full count of the varieties - } - } - } - - if (isLoggable) { - Log.v(TAG, "candidate " + (i + 1) + " of " + N + ": " + candidate); - Log.v(TAG, " numActiveInit=" + numInit + ", numActiveRegular=" + numRegular); - Log.v(TAG, " longRunning: " + longRunning); - Log.v(TAG, " conflict: " + conflict); - Log.v(TAG, " oldestNonExpeditedRegular: " + oldestNonExpeditedRegular); - } - - final boolean roomAvailable = candidateIsInitialization - ? numInit < MAX_SIMULTANEOUS_INITIALIZATION_SYNCS - : numRegular < MAX_SIMULTANEOUS_REGULAR_SYNCS; - - if (conflict != null) { - if (candidateIsInitialization && !conflict.mSyncOperation.isInitialization() - && numInit < MAX_SIMULTANEOUS_INITIALIZATION_SYNCS) { - toReschedule = conflict; - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "canceling and rescheduling sync since an initialization " - + "takes higher priority, " + conflict); - } - } else if (candidate.expedited && !conflict.mSyncOperation.expedited - && (candidateIsInitialization - == conflict.mSyncOperation.isInitialization())) { - toReschedule = conflict; - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "canceling and rescheduling sync since an expedited " - + "takes higher priority, " + conflict); - } - } else { - continue; - } - } else if (roomAvailable) { - // dispatch candidate - } else if (candidate.isExpedited() && oldestNonExpeditedRegular != null - && !candidateIsInitialization) { - // We found an active, non-expedited regular sync. We also know that the - // candidate doesn't conflict with this active sync since conflict - // is null. Reschedule the active sync and start the candidate. - toReschedule = oldestNonExpeditedRegular; - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "canceling and rescheduling sync since an expedited is ready to run, " - + oldestNonExpeditedRegular); - } - } else if (longRunning != null - && (candidateIsInitialization - == longRunning.mSyncOperation.isInitialization())) { - // We found an active, long-running sync. Reschedule the active - // sync and start the candidate. - toReschedule = longRunning; - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "canceling and rescheduling sync since it ran roo long, " - + longRunning); - } - } else { - // we were unable to find or make space to run this candidate, go on to - // the next one - continue; - } - - if (toReschedule != null) { - runSyncFinishedOrCanceledLocked(null, toReschedule); - scheduleSyncOperation(toReschedule.mSyncOperation); - } - synchronized (mSyncQueue) { - mSyncQueue.remove(candidate); - } - dispatchSyncOperation(candidate); - } - - return nextReadyToRunTime; - } - - private boolean dispatchSyncOperation(SyncOperation op) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "dispatchSyncOperation: we are going to sync " + op); - Log.v(TAG, "num active syncs: " + mActiveSyncContexts.size()); - for (ActiveSyncContext syncContext : mActiveSyncContexts) { - Log.v(TAG, syncContext.toString()); - } - } - - // connect to the sync adapter - SyncAdapterType syncAdapterType = SyncAdapterType.newKey(op.authority, op.account.type); - final RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapterInfo; - syncAdapterInfo = mSyncAdapters.getServiceInfo(syncAdapterType, op.userId); - if (syncAdapterInfo == null) { - Log.d(TAG, "can't find a sync adapter for " + syncAdapterType - + ", removing settings for it"); - mSyncStorageEngine.removeAuthority(op.account, op.userId, op.authority); - return false; - } - - ActiveSyncContext activeSyncContext = - new ActiveSyncContext(op, insertStartSyncEvent(op), syncAdapterInfo.uid); - activeSyncContext.mSyncInfo = mSyncStorageEngine.addActiveSync(activeSyncContext); - mActiveSyncContexts.add(activeSyncContext); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "dispatchSyncOperation: starting " + activeSyncContext); - } - if (!activeSyncContext.bindToSyncAdapter(syncAdapterInfo, op.userId)) { - Log.e(TAG, "Bind attempt failed to " + syncAdapterInfo); - closeActiveSyncContext(activeSyncContext); - return false; - } - - return true; - } - - private void runBoundToSyncAdapter(final ActiveSyncContext activeSyncContext, - ISyncAdapter syncAdapter) { - activeSyncContext.mSyncAdapter = syncAdapter; - final SyncOperation syncOperation = activeSyncContext.mSyncOperation; - try { - activeSyncContext.mIsLinkedToDeath = true; - syncAdapter.asBinder().linkToDeath(activeSyncContext, 0); - - syncAdapter.startSync(activeSyncContext, syncOperation.authority, - syncOperation.account, syncOperation.extras); - } catch (RemoteException remoteExc) { - Log.d(TAG, "maybeStartNextSync: caught a RemoteException, rescheduling", remoteExc); - closeActiveSyncContext(activeSyncContext); - increaseBackoffSetting(syncOperation); - scheduleSyncOperation(new SyncOperation(syncOperation)); - } catch (RuntimeException exc) { - closeActiveSyncContext(activeSyncContext); - Log.e(TAG, "Caught RuntimeException while starting the sync " + syncOperation, exc); - } - } - - private void cancelActiveSyncLocked(Account account, int userId, String authority) { - ArrayList<ActiveSyncContext> activeSyncs = - new ArrayList<ActiveSyncContext>(mActiveSyncContexts); - for (ActiveSyncContext activeSyncContext : activeSyncs) { - if (activeSyncContext != null) { - // if an account was specified then only cancel the sync if it matches - if (account != null) { - if (!account.equals(activeSyncContext.mSyncOperation.account)) { - continue; - } - } - // if an authority was specified then only cancel the sync if it matches - if (authority != null) { - if (!authority.equals(activeSyncContext.mSyncOperation.authority)) { - continue; - } - } - // check if the userid matches - if (userId != UserHandle.USER_ALL - && userId != activeSyncContext.mSyncOperation.userId) { - continue; - } - runSyncFinishedOrCanceledLocked(null /* no result since this is a cancel */, - activeSyncContext); - } - } - } - - private void runSyncFinishedOrCanceledLocked(SyncResult syncResult, - ActiveSyncContext activeSyncContext) { - boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE); - - if (activeSyncContext.mIsLinkedToDeath) { - activeSyncContext.mSyncAdapter.asBinder().unlinkToDeath(activeSyncContext, 0); - activeSyncContext.mIsLinkedToDeath = false; - } - closeActiveSyncContext(activeSyncContext); - - final SyncOperation syncOperation = activeSyncContext.mSyncOperation; - - final long elapsedTime = SystemClock.elapsedRealtime() - activeSyncContext.mStartTime; - - String historyMessage; - int downstreamActivity; - int upstreamActivity; - if (syncResult != null) { - if (isLoggable) { - Log.v(TAG, "runSyncFinishedOrCanceled [finished]: " - + syncOperation + ", result " + syncResult); - } - - if (!syncResult.hasError()) { - historyMessage = SyncStorageEngine.MESG_SUCCESS; - // TODO: set these correctly when the SyncResult is extended to include it - downstreamActivity = 0; - upstreamActivity = 0; - clearBackoffSetting(syncOperation); - } else { - Log.d(TAG, "failed sync operation " + syncOperation + ", " + syncResult); - // the operation failed so increase the backoff time - if (!syncResult.syncAlreadyInProgress) { - increaseBackoffSetting(syncOperation); - } - // reschedule the sync if so indicated by the syncResult - maybeRescheduleSync(syncResult, syncOperation); - historyMessage = Integer.toString(syncResultToErrorNumber(syncResult)); - // TODO: set these correctly when the SyncResult is extended to include it - downstreamActivity = 0; - upstreamActivity = 0; - } - - setDelayUntilTime(syncOperation, syncResult.delayUntil); - } else { - if (isLoggable) { - Log.v(TAG, "runSyncFinishedOrCanceled [canceled]: " + syncOperation); - } - if (activeSyncContext.mSyncAdapter != null) { - try { - activeSyncContext.mSyncAdapter.cancelSync(activeSyncContext); - } catch (RemoteException e) { - // we don't need to retry this in this case - } - } - historyMessage = SyncStorageEngine.MESG_CANCELED; - downstreamActivity = 0; - upstreamActivity = 0; - } - - stopSyncEvent(activeSyncContext.mHistoryRowId, syncOperation, historyMessage, - upstreamActivity, downstreamActivity, elapsedTime); - - if (syncResult != null && syncResult.tooManyDeletions) { - installHandleTooManyDeletesNotification(syncOperation.account, - syncOperation.authority, syncResult.stats.numDeletes, - syncOperation.userId); - } else { - mNotificationMgr.cancelAsUser(null, - syncOperation.account.hashCode() ^ syncOperation.authority.hashCode(), - new UserHandle(syncOperation.userId)); - } - - if (syncResult != null && syncResult.fullSyncRequested) { - scheduleSyncOperation(new SyncOperation(syncOperation.account, syncOperation.userId, - syncOperation.syncSource, syncOperation.authority, new Bundle(), 0, - syncOperation.backoff, syncOperation.delayUntil, - syncOperation.allowParallelSyncs)); - } - // no need to schedule an alarm, as that will be done by our caller. - } - - private void closeActiveSyncContext(ActiveSyncContext activeSyncContext) { - activeSyncContext.close(); - mActiveSyncContexts.remove(activeSyncContext); - mSyncStorageEngine.removeActiveSync(activeSyncContext.mSyncInfo, - activeSyncContext.mSyncOperation.userId); - } - - /** - * Convert the error-containing SyncResult into the Sync.History error number. Since - * the SyncResult may indicate multiple errors at once, this method just returns the - * most "serious" error. - * @param syncResult the SyncResult from which to read - * @return the most "serious" error set in the SyncResult - * @throws IllegalStateException if the SyncResult does not indicate any errors. - * If SyncResult.error() is true then it is safe to call this. - */ - private int syncResultToErrorNumber(SyncResult syncResult) { - if (syncResult.syncAlreadyInProgress) - return ContentResolver.SYNC_ERROR_SYNC_ALREADY_IN_PROGRESS; - if (syncResult.stats.numAuthExceptions > 0) - return ContentResolver.SYNC_ERROR_AUTHENTICATION; - if (syncResult.stats.numIoExceptions > 0) - return ContentResolver.SYNC_ERROR_IO; - if (syncResult.stats.numParseExceptions > 0) - return ContentResolver.SYNC_ERROR_PARSE; - if (syncResult.stats.numConflictDetectedExceptions > 0) - return ContentResolver.SYNC_ERROR_CONFLICT; - if (syncResult.tooManyDeletions) - return ContentResolver.SYNC_ERROR_TOO_MANY_DELETIONS; - if (syncResult.tooManyRetries) - return ContentResolver.SYNC_ERROR_TOO_MANY_RETRIES; - if (syncResult.databaseError) - return ContentResolver.SYNC_ERROR_INTERNAL; - throw new IllegalStateException("we are not in an error state, " + syncResult); - } - - private void manageSyncNotificationLocked() { - boolean shouldCancel; - boolean shouldInstall; - - if (mActiveSyncContexts.isEmpty()) { - mSyncNotificationInfo.startTime = null; - - // we aren't syncing. if the notification is active then remember that we need - // to cancel it and then clear out the info - shouldCancel = mSyncNotificationInfo.isActive; - shouldInstall = false; - } else { - // we are syncing - final long now = SystemClock.elapsedRealtime(); - if (mSyncNotificationInfo.startTime == null) { - mSyncNotificationInfo.startTime = now; - } - - // there are three cases: - // - the notification is up: do nothing - // - the notification is not up but it isn't time yet: don't install - // - the notification is not up and it is time: need to install - - if (mSyncNotificationInfo.isActive) { - shouldInstall = shouldCancel = false; - } else { - // it isn't currently up, so there is nothing to cancel - shouldCancel = false; - - final boolean timeToShowNotification = - now > mSyncNotificationInfo.startTime + SYNC_NOTIFICATION_DELAY; - if (timeToShowNotification) { - shouldInstall = true; - } else { - // show the notification immediately if this is a manual sync - shouldInstall = false; - for (ActiveSyncContext activeSyncContext : mActiveSyncContexts) { - final boolean manualSync = activeSyncContext.mSyncOperation.extras - .getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false); - if (manualSync) { - shouldInstall = true; - break; - } - } - } - } - } - - if (shouldCancel && !shouldInstall) { - mNeedSyncActiveNotification = false; - sendSyncStateIntent(); - mSyncNotificationInfo.isActive = false; - } - - if (shouldInstall) { - mNeedSyncActiveNotification = true; - sendSyncStateIntent(); - mSyncNotificationInfo.isActive = true; - } - } - - private void manageSyncAlarmLocked(long nextPeriodicEventElapsedTime, - long nextPendingEventElapsedTime) { - // in each of these cases the sync loop will be kicked, which will cause this - // method to be called again - if (!mDataConnectionIsConnected) return; - if (mStorageIsLow) return; - - // When the status bar notification should be raised - final long notificationTime = - (!mSyncHandler.mSyncNotificationInfo.isActive - && mSyncHandler.mSyncNotificationInfo.startTime != null) - ? mSyncHandler.mSyncNotificationInfo.startTime + SYNC_NOTIFICATION_DELAY - : Long.MAX_VALUE; - - // When we should consider canceling an active sync - long earliestTimeoutTime = Long.MAX_VALUE; - for (ActiveSyncContext currentSyncContext : mActiveSyncContexts) { - final long currentSyncTimeoutTime = - currentSyncContext.mTimeoutStartTime + MAX_TIME_PER_SYNC; - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "manageSyncAlarm: active sync, mTimeoutStartTime + MAX is " - + currentSyncTimeoutTime); - } - if (earliestTimeoutTime > currentSyncTimeoutTime) { - earliestTimeoutTime = currentSyncTimeoutTime; - } - } - - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "manageSyncAlarm: notificationTime is " + notificationTime); - } - - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "manageSyncAlarm: earliestTimeoutTime is " + earliestTimeoutTime); - } - - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "manageSyncAlarm: nextPeriodicEventElapsedTime is " - + nextPeriodicEventElapsedTime); - } - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "manageSyncAlarm: nextPendingEventElapsedTime is " - + nextPendingEventElapsedTime); - } - - long alarmTime = Math.min(notificationTime, earliestTimeoutTime); - alarmTime = Math.min(alarmTime, nextPeriodicEventElapsedTime); - alarmTime = Math.min(alarmTime, nextPendingEventElapsedTime); - - // Bound the alarm time. - final long now = SystemClock.elapsedRealtime(); - if (alarmTime < now + SYNC_ALARM_TIMEOUT_MIN) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "manageSyncAlarm: the alarmTime is too small, " - + alarmTime + ", setting to " + (now + SYNC_ALARM_TIMEOUT_MIN)); - } - alarmTime = now + SYNC_ALARM_TIMEOUT_MIN; - } else if (alarmTime > now + SYNC_ALARM_TIMEOUT_MAX) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "manageSyncAlarm: the alarmTime is too large, " - + alarmTime + ", setting to " + (now + SYNC_ALARM_TIMEOUT_MIN)); - } - alarmTime = now + SYNC_ALARM_TIMEOUT_MAX; - } - - // determine if we need to set or cancel the alarm - boolean shouldSet = false; - boolean shouldCancel = false; - final boolean alarmIsActive = mAlarmScheduleTime != null; - final boolean needAlarm = alarmTime != Long.MAX_VALUE; - if (needAlarm) { - if (!alarmIsActive || alarmTime < mAlarmScheduleTime) { - shouldSet = true; - } - } else { - shouldCancel = alarmIsActive; - } - - // set or cancel the alarm as directed - ensureAlarmService(); - if (shouldSet) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "requesting that the alarm manager wake us up at elapsed time " - + alarmTime + ", now is " + now + ", " + ((alarmTime - now) / 1000) - + " secs from now"); - } - mAlarmScheduleTime = alarmTime; - mAlarmService.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, alarmTime, - mSyncAlarmIntent); - } else if (shouldCancel) { - mAlarmScheduleTime = null; - mAlarmService.cancel(mSyncAlarmIntent); - } - } - - private void sendSyncStateIntent() { - Intent syncStateIntent = new Intent(Intent.ACTION_SYNC_STATE_CHANGED); - syncStateIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); - syncStateIntent.putExtra("active", mNeedSyncActiveNotification); - syncStateIntent.putExtra("failing", false); - mContext.sendBroadcastAsUser(syncStateIntent, UserHandle.OWNER); - } - - private void installHandleTooManyDeletesNotification(Account account, String authority, - long numDeletes, int userId) { - if (mNotificationMgr == null) return; - - final ProviderInfo providerInfo = mContext.getPackageManager().resolveContentProvider( - authority, 0 /* flags */); - if (providerInfo == null) { - return; - } - CharSequence authorityName = providerInfo.loadLabel(mContext.getPackageManager()); - - Intent clickIntent = new Intent(mContext, SyncActivityTooManyDeletes.class); - clickIntent.putExtra("account", account); - clickIntent.putExtra("authority", authority); - clickIntent.putExtra("provider", authorityName.toString()); - clickIntent.putExtra("numDeletes", numDeletes); - - if (!isActivityAvailable(clickIntent)) { - Log.w(TAG, "No activity found to handle too many deletes."); - return; - } - - final PendingIntent pendingIntent = PendingIntent - .getActivityAsUser(mContext, 0, clickIntent, - PendingIntent.FLAG_CANCEL_CURRENT, null, new UserHandle(userId)); - - CharSequence tooManyDeletesDescFormat = mContext.getResources().getText( - R.string.contentServiceTooManyDeletesNotificationDesc); - - Notification notification = - new Notification(R.drawable.stat_notify_sync_error, - mContext.getString(R.string.contentServiceSync), - System.currentTimeMillis()); - notification.setLatestEventInfo(mContext, - mContext.getString(R.string.contentServiceSyncNotificationTitle), - String.format(tooManyDeletesDescFormat.toString(), authorityName), - pendingIntent); - notification.flags |= Notification.FLAG_ONGOING_EVENT; - mNotificationMgr.notifyAsUser(null, account.hashCode() ^ authority.hashCode(), - notification, new UserHandle(userId)); - } - - /** - * Checks whether an activity exists on the system image for the given intent. - * - * @param intent The intent for an activity. - * @return Whether or not an activity exists. - */ - private boolean isActivityAvailable(Intent intent) { - PackageManager pm = mContext.getPackageManager(); - List<ResolveInfo> list = pm.queryIntentActivities(intent, 0); - int listSize = list.size(); - for (int i = 0; i < listSize; i++) { - ResolveInfo resolveInfo = list.get(i); - if ((resolveInfo.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) - != 0) { - return true; - } - } - - return false; - } - - public long insertStartSyncEvent(SyncOperation syncOperation) { - final int source = syncOperation.syncSource; - final long now = System.currentTimeMillis(); - - EventLog.writeEvent(2720, syncOperation.authority, - SyncStorageEngine.EVENT_START, source, - syncOperation.account.name.hashCode()); - - return mSyncStorageEngine.insertStartSyncEvent( - syncOperation.account, syncOperation.userId, syncOperation.authority, - now, source, syncOperation.isInitialization()); - } - - public void stopSyncEvent(long rowId, SyncOperation syncOperation, String resultMessage, - int upstreamActivity, int downstreamActivity, long elapsedTime) { - EventLog.writeEvent(2720, syncOperation.authority, - SyncStorageEngine.EVENT_STOP, syncOperation.syncSource, - syncOperation.account.name.hashCode()); - - mSyncStorageEngine.stopSyncEvent(rowId, elapsedTime, - resultMessage, downstreamActivity, upstreamActivity); - } - } - - private boolean isSyncStillActive(ActiveSyncContext activeSyncContext) { - for (ActiveSyncContext sync : mActiveSyncContexts) { - if (sync == activeSyncContext) { - return true; - } - } - return false; - } -} diff --git a/core/java/android/content/SyncOperation.java b/core/java/android/content/SyncOperation.java deleted file mode 100644 index 6611fcd..0000000 --- a/core/java/android/content/SyncOperation.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.content; - -import android.accounts.Account; -import android.os.Bundle; -import android.os.SystemClock; - -/** - * Value type that represents a sync operation. - * @hide - */ -public class SyncOperation implements Comparable { - public final Account account; - public final int userId; - public int syncSource; - public String authority; - public final boolean allowParallelSyncs; - public Bundle extras; - public final String key; - public long earliestRunTime; - public boolean expedited; - public SyncStorageEngine.PendingOperation pendingOperation; - public Long backoff; - public long delayUntil; - public long effectiveRunTime; - - public SyncOperation(Account account, int userId, int source, String authority, Bundle extras, - long delayInMs, long backoff, long delayUntil, boolean allowParallelSyncs) { - this.account = account; - this.userId = userId; - this.syncSource = source; - this.authority = authority; - this.allowParallelSyncs = allowParallelSyncs; - this.extras = new Bundle(extras); - removeFalseExtra(ContentResolver.SYNC_EXTRAS_UPLOAD); - removeFalseExtra(ContentResolver.SYNC_EXTRAS_MANUAL); - removeFalseExtra(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS); - removeFalseExtra(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF); - removeFalseExtra(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY); - removeFalseExtra(ContentResolver.SYNC_EXTRAS_DISCARD_LOCAL_DELETIONS); - removeFalseExtra(ContentResolver.SYNC_EXTRAS_EXPEDITED); - removeFalseExtra(ContentResolver.SYNC_EXTRAS_OVERRIDE_TOO_MANY_DELETIONS); - this.delayUntil = delayUntil; - this.backoff = backoff; - final long now = SystemClock.elapsedRealtime(); - if (delayInMs < 0) { - this.expedited = true; - this.earliestRunTime = now; - } else { - this.expedited = false; - this.earliestRunTime = now + delayInMs; - } - updateEffectiveRunTime(); - this.key = toKey(); - } - - private void removeFalseExtra(String extraName) { - if (!extras.getBoolean(extraName, false)) { - extras.remove(extraName); - } - } - - SyncOperation(SyncOperation other) { - this.account = other.account; - this.userId = other.userId; - this.syncSource = other.syncSource; - this.authority = other.authority; - this.extras = new Bundle(other.extras); - this.expedited = other.expedited; - this.earliestRunTime = SystemClock.elapsedRealtime(); - this.backoff = other.backoff; - this.delayUntil = other.delayUntil; - this.allowParallelSyncs = other.allowParallelSyncs; - this.updateEffectiveRunTime(); - this.key = toKey(); - } - - public String toString() { - return dump(true); - } - - public String dump(boolean useOneLine) { - StringBuilder sb = new StringBuilder() - .append(account.name) - .append(" u") - .append(userId).append(" (") - .append(account.type) - .append(")") - .append(", ") - .append(authority) - .append(", ") - .append(SyncStorageEngine.SOURCES[syncSource]) - .append(", earliestRunTime ") - .append(earliestRunTime); - if (expedited) { - sb.append(", EXPEDITED"); - } - if (!useOneLine && !extras.keySet().isEmpty()) { - sb.append("\n "); - extrasToStringBuilder(extras, sb); - } - return sb.toString(); - } - - public boolean isInitialization() { - return extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false); - } - - public boolean isExpedited() { - return extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false); - } - - public boolean ignoreBackoff() { - return extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false); - } - - private String toKey() { - StringBuilder sb = new StringBuilder(); - sb.append("authority: ").append(authority); - sb.append(" account {name=" + account.name + ", user=" + userId + ", type=" + account.type - + "}"); - sb.append(" extras: "); - extrasToStringBuilder(extras, sb); - return sb.toString(); - } - - public static void extrasToStringBuilder(Bundle bundle, StringBuilder sb) { - sb.append("["); - for (String key : bundle.keySet()) { - sb.append(key).append("=").append(bundle.get(key)).append(" "); - } - sb.append("]"); - } - - public void updateEffectiveRunTime() { - effectiveRunTime = ignoreBackoff() - ? earliestRunTime - : Math.max( - Math.max(earliestRunTime, delayUntil), - backoff); - } - - public int compareTo(Object o) { - SyncOperation other = (SyncOperation)o; - - if (expedited != other.expedited) { - return expedited ? -1 : 1; - } - - if (effectiveRunTime == other.effectiveRunTime) { - return 0; - } - - return effectiveRunTime < other.effectiveRunTime ? -1 : 1; - } -} diff --git a/core/java/android/content/SyncQueue.java b/core/java/android/content/SyncQueue.java deleted file mode 100644 index c9a325e..0000000 --- a/core/java/android/content/SyncQueue.java +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.content; - -import android.accounts.Account; -import android.content.pm.RegisteredServicesCache.ServiceInfo; -import android.os.SystemClock; -import android.os.UserHandle; -import android.text.format.DateUtils; -import android.util.Log; -import android.util.Pair; - -import com.google.android.collect.Maps; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -/** - * Queue of pending sync operations. Not inherently thread safe, external - * callers are responsible for locking. - * - * @hide - */ -public class SyncQueue { - private static final String TAG = "SyncManager"; - - private final SyncStorageEngine mSyncStorageEngine; - private final SyncAdaptersCache mSyncAdapters; - - // A Map of SyncOperations operationKey -> SyncOperation that is designed for - // quick lookup of an enqueued SyncOperation. - private final HashMap<String, SyncOperation> mOperationsMap = Maps.newHashMap(); - - public SyncQueue(SyncStorageEngine syncStorageEngine, final SyncAdaptersCache syncAdapters) { - mSyncStorageEngine = syncStorageEngine; - mSyncAdapters = syncAdapters; - } - - public void addPendingOperations(int userId) { - for (SyncStorageEngine.PendingOperation op : mSyncStorageEngine.getPendingOperations()) { - if (op.userId != userId) continue; - - final Pair<Long, Long> backoff = mSyncStorageEngine.getBackoff( - op.account, op.userId, op.authority); - final ServiceInfo<SyncAdapterType> syncAdapterInfo = mSyncAdapters.getServiceInfo( - SyncAdapterType.newKey(op.authority, op.account.type), op.userId); - if (syncAdapterInfo == null) { - Log.w(TAG, "Missing sync adapter info for authority " + op.authority + ", userId " - + op.userId); - continue; - } - SyncOperation syncOperation = new SyncOperation( - op.account, op.userId, op.syncSource, op.authority, op.extras, 0 /* delay */, - backoff != null ? backoff.first : 0, - mSyncStorageEngine.getDelayUntilTime(op.account, op.userId, op.authority), - syncAdapterInfo.type.allowParallelSyncs()); - syncOperation.expedited = op.expedited; - syncOperation.pendingOperation = op; - add(syncOperation, op); - } - } - - public boolean add(SyncOperation operation) { - return add(operation, null /* this is not coming from the database */); - } - - private boolean add(SyncOperation operation, - SyncStorageEngine.PendingOperation pop) { - // - if an operation with the same key exists and this one should run earlier, - // update the earliestRunTime of the existing to the new time - // - if an operation with the same key exists and if this one should run - // later, ignore it - // - if no operation exists then add the new one - final String operationKey = operation.key; - final SyncOperation existingOperation = mOperationsMap.get(operationKey); - - if (existingOperation != null) { - boolean changed = false; - if (existingOperation.expedited == operation.expedited) { - final long newRunTime = - Math.min(existingOperation.earliestRunTime, operation.earliestRunTime); - if (existingOperation.earliestRunTime != newRunTime) { - existingOperation.earliestRunTime = newRunTime; - changed = true; - } - } else { - if (operation.expedited) { - existingOperation.expedited = true; - changed = true; - } - } - return changed; - } - - operation.pendingOperation = pop; - if (operation.pendingOperation == null) { - pop = new SyncStorageEngine.PendingOperation( - operation.account, operation.userId, operation.syncSource, - operation.authority, operation.extras, operation.expedited); - pop = mSyncStorageEngine.insertIntoPending(pop); - if (pop == null) { - throw new IllegalStateException("error adding pending sync operation " - + operation); - } - operation.pendingOperation = pop; - } - - mOperationsMap.put(operationKey, operation); - return true; - } - - public void removeUser(int userId) { - ArrayList<SyncOperation> opsToRemove = new ArrayList<SyncOperation>(); - for (SyncOperation op : mOperationsMap.values()) { - if (op.userId == userId) { - opsToRemove.add(op); - } - } - - for (SyncOperation op : opsToRemove) { - remove(op); - } - } - - /** - * Remove the specified operation if it is in the queue. - * @param operation the operation to remove - */ - public void remove(SyncOperation operation) { - SyncOperation operationToRemove = mOperationsMap.remove(operation.key); - if (operationToRemove == null) { - return; - } - if (!mSyncStorageEngine.deleteFromPending(operationToRemove.pendingOperation)) { - final String errorMessage = "unable to find pending row for " + operationToRemove; - Log.e(TAG, errorMessage, new IllegalStateException(errorMessage)); - } - } - - public void onBackoffChanged(Account account, int userId, String providerName, long backoff) { - // for each op that matches the account and provider update its - // backoff and effectiveStartTime - for (SyncOperation op : mOperationsMap.values()) { - if (op.account.equals(account) && op.authority.equals(providerName) - && op.userId == userId) { - op.backoff = backoff; - op.updateEffectiveRunTime(); - } - } - } - - public void onDelayUntilTimeChanged(Account account, String providerName, long delayUntil) { - // for each op that matches the account and provider update its - // delayUntilTime and effectiveStartTime - for (SyncOperation op : mOperationsMap.values()) { - if (op.account.equals(account) && op.authority.equals(providerName)) { - op.delayUntil = delayUntil; - op.updateEffectiveRunTime(); - } - } - } - - public void remove(Account account, int userId, String authority) { - Iterator<Map.Entry<String, SyncOperation>> entries = mOperationsMap.entrySet().iterator(); - while (entries.hasNext()) { - Map.Entry<String, SyncOperation> entry = entries.next(); - SyncOperation syncOperation = entry.getValue(); - if (account != null && !syncOperation.account.equals(account)) { - continue; - } - if (authority != null && !syncOperation.authority.equals(authority)) { - continue; - } - if (userId != syncOperation.userId) { - continue; - } - entries.remove(); - if (!mSyncStorageEngine.deleteFromPending(syncOperation.pendingOperation)) { - final String errorMessage = "unable to find pending row for " + syncOperation; - Log.e(TAG, errorMessage, new IllegalStateException(errorMessage)); - } - } - } - - public Collection<SyncOperation> getOperations() { - return mOperationsMap.values(); - } - - public void dump(StringBuilder sb) { - final long now = SystemClock.elapsedRealtime(); - sb.append("SyncQueue: ").append(mOperationsMap.size()).append(" operation(s)\n"); - for (SyncOperation operation : mOperationsMap.values()) { - sb.append(" "); - if (operation.effectiveRunTime <= now) { - sb.append("READY"); - } else { - sb.append(DateUtils.formatElapsedTime((operation.effectiveRunTime - now) / 1000)); - } - sb.append(" - "); - sb.append(operation.dump(false)).append("\n"); - } - } -} diff --git a/core/java/android/content/SyncStatusInfo.java b/core/java/android/content/SyncStatusInfo.java index bb2b2da..ff628d9 100644 --- a/core/java/android/content/SyncStatusInfo.java +++ b/core/java/android/content/SyncStatusInfo.java @@ -46,19 +46,18 @@ public class SyncStatusInfo implements Parcelable { private static final String TAG = "Sync"; - SyncStatusInfo(int authorityId) { + public SyncStatusInfo(int authorityId) { this.authorityId = authorityId; } public int getLastFailureMesgAsInt(int def) { - try { - if (lastFailureMesg != null) { - return Integer.parseInt(lastFailureMesg); - } - } catch (NumberFormatException e) { - Log.d(TAG, "error parsing lastFailureMesg of " + lastFailureMesg, e); + final int i = ContentResolver.syncErrorStringToInt(lastFailureMesg); + if (i > 0) { + return i; + } else { + Log.d(TAG, "Unknown lastFailureMesg:" + lastFailureMesg); + return def; } - return def; } public int describeContents() { @@ -92,7 +91,7 @@ public class SyncStatusInfo implements Parcelable { } } - SyncStatusInfo(Parcel parcel) { + public SyncStatusInfo(Parcel parcel) { int version = parcel.readInt(); if (version != VERSION && version != 1) { Log.w("SyncStatusInfo", "Unknown version: " + version); diff --git a/core/java/android/content/SyncStorageEngine.java b/core/java/android/content/SyncStorageEngine.java deleted file mode 100644 index 1ecab09..0000000 --- a/core/java/android/content/SyncStorageEngine.java +++ /dev/null @@ -1,2303 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.content; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.util.ArrayUtils; -import com.android.internal.util.FastXmlSerializer; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlSerializer; - -import android.accounts.Account; -import android.accounts.AccountAndUser; -import android.content.res.Resources; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteException; -import android.database.sqlite.SQLiteQueryBuilder; -import android.os.Bundle; -import android.os.Environment; -import android.os.Handler; -import android.os.Message; -import android.os.Parcel; -import android.os.RemoteCallbackList; -import android.os.RemoteException; -import android.util.AtomicFile; -import android.util.Log; -import android.util.SparseArray; -import android.util.Xml; -import android.util.Pair; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Random; -import java.util.TimeZone; -import java.util.List; - -/** - * Singleton that tracks the sync data and overall sync - * history on the device. - * - * @hide - */ -public class SyncStorageEngine extends Handler { - - private static final String TAG = "SyncManager"; - private static final boolean DEBUG = false; - private static final boolean DEBUG_FILE = false; - - private static final String XML_ATTR_NEXT_AUTHORITY_ID = "nextAuthorityId"; - private static final String XML_ATTR_LISTEN_FOR_TICKLES = "listen-for-tickles"; - private static final String XML_ATTR_SYNC_RANDOM_OFFSET = "offsetInSeconds"; - private static final String XML_ATTR_ENABLED = "enabled"; - private static final String XML_ATTR_USER = "user"; - private static final String XML_TAG_LISTEN_FOR_TICKLES = "listenForTickles"; - - private static final long DEFAULT_POLL_FREQUENCY_SECONDS = 60 * 60 * 24; // One day - - @VisibleForTesting - static final long MILLIS_IN_4WEEKS = 1000L * 60 * 60 * 24 * 7 * 4; - - /** Enum value for a sync start event. */ - public static final int EVENT_START = 0; - - /** Enum value for a sync stop event. */ - public static final int EVENT_STOP = 1; - - // TODO: i18n -- grab these out of resources. - /** String names for the sync event types. */ - public static final String[] EVENTS = { "START", "STOP" }; - - /** Enum value for a server-initiated sync. */ - public static final int SOURCE_SERVER = 0; - - /** Enum value for a local-initiated sync. */ - public static final int SOURCE_LOCAL = 1; - /** - * Enum value for a poll-based sync (e.g., upon connection to - * network) - */ - public static final int SOURCE_POLL = 2; - - /** Enum value for a user-initiated sync. */ - public static final int SOURCE_USER = 3; - - /** Enum value for a periodic sync. */ - public static final int SOURCE_PERIODIC = 4; - - public static final long NOT_IN_BACKOFF_MODE = -1; - - public static final Intent SYNC_CONNECTION_SETTING_CHANGED_INTENT = - new Intent("com.android.sync.SYNC_CONN_STATUS_CHANGED"); - - // TODO: i18n -- grab these out of resources. - /** String names for the sync source types. */ - public static final String[] SOURCES = { "SERVER", - "LOCAL", - "POLL", - "USER", - "PERIODIC" }; - - // The MESG column will contain one of these or one of the Error types. - public static final String MESG_SUCCESS = "success"; - public static final String MESG_CANCELED = "canceled"; - - public static final int MAX_HISTORY = 100; - - private static final int MSG_WRITE_STATUS = 1; - private static final long WRITE_STATUS_DELAY = 1000*60*10; // 10 minutes - - private static final int MSG_WRITE_STATISTICS = 2; - private static final long WRITE_STATISTICS_DELAY = 1000*60*30; // 1/2 hour - - private static final boolean SYNC_ENABLED_DEFAULT = false; - - // the version of the accounts xml file format - private static final int ACCOUNTS_VERSION = 2; - - private static HashMap<String, String> sAuthorityRenames; - - static { - sAuthorityRenames = new HashMap<String, String>(); - sAuthorityRenames.put("contacts", "com.android.contacts"); - sAuthorityRenames.put("calendar", "com.android.calendar"); - } - - public static class PendingOperation { - final Account account; - final int userId; - final int syncSource; - final String authority; - final Bundle extras; // note: read-only. - final boolean expedited; - - int authorityId; - byte[] flatExtras; - - PendingOperation(Account account, int userId, int source, - String authority, Bundle extras, boolean expedited) { - this.account = account; - this.userId = userId; - this.syncSource = source; - this.authority = authority; - this.extras = extras != null ? new Bundle(extras) : extras; - this.expedited = expedited; - this.authorityId = -1; - } - - PendingOperation(PendingOperation other) { - this.account = other.account; - this.userId = other.userId; - this.syncSource = other.syncSource; - this.authority = other.authority; - this.extras = other.extras; - this.authorityId = other.authorityId; - this.expedited = other.expedited; - } - } - - static class AccountInfo { - final AccountAndUser accountAndUser; - final HashMap<String, AuthorityInfo> authorities = - new HashMap<String, AuthorityInfo>(); - - AccountInfo(AccountAndUser accountAndUser) { - this.accountAndUser = accountAndUser; - } - } - - public static class AuthorityInfo { - final Account account; - final int userId; - final String authority; - final int ident; - boolean enabled; - int syncable; - long backoffTime; - long backoffDelay; - long delayUntil; - final ArrayList<Pair<Bundle, Long>> periodicSyncs; - - /** - * Copy constructor for making deep-ish copies. Only the bundles stored - * in periodic syncs can make unexpected changes. - * - * @param toCopy AuthorityInfo to be copied. - */ - AuthorityInfo(AuthorityInfo toCopy) { - account = toCopy.account; - userId = toCopy.userId; - authority = toCopy.authority; - ident = toCopy.ident; - enabled = toCopy.enabled; - syncable = toCopy.syncable; - backoffTime = toCopy.backoffTime; - backoffDelay = toCopy.backoffDelay; - delayUntil = toCopy.delayUntil; - periodicSyncs = new ArrayList<Pair<Bundle, Long>>(); - for (Pair<Bundle, Long> sync : toCopy.periodicSyncs) { - // Still not a perfect copy, because we are just copying the mappings. - periodicSyncs.add(Pair.create(new Bundle(sync.first), sync.second)); - } - } - - AuthorityInfo(Account account, int userId, String authority, int ident) { - this.account = account; - this.userId = userId; - this.authority = authority; - this.ident = ident; - enabled = SYNC_ENABLED_DEFAULT; - syncable = -1; // default to "unknown" - backoffTime = -1; // if < 0 then we aren't in backoff mode - backoffDelay = -1; // if < 0 then we aren't in backoff mode - periodicSyncs = new ArrayList<Pair<Bundle, Long>>(); - periodicSyncs.add(Pair.create(new Bundle(), DEFAULT_POLL_FREQUENCY_SECONDS)); - } - } - - public static class SyncHistoryItem { - int authorityId; - int historyId; - long eventTime; - long elapsedTime; - int source; - int event; - long upstreamActivity; - long downstreamActivity; - String mesg; - boolean initialization; - } - - public static class DayStats { - public final int day; - public int successCount; - public long successTime; - public int failureCount; - public long failureTime; - - public DayStats(int day) { - this.day = day; - } - } - - interface OnSyncRequestListener { - /** - * Called when a sync is needed on an account(s) due to some change in state. - * @param account - * @param userId - * @param authority - * @param extras - */ - public void onSyncRequest(Account account, int userId, String authority, Bundle extras); - } - - // Primary list of all syncable authorities. Also our global lock. - private final SparseArray<AuthorityInfo> mAuthorities = - new SparseArray<AuthorityInfo>(); - - private final HashMap<AccountAndUser, AccountInfo> mAccounts - = new HashMap<AccountAndUser, AccountInfo>(); - - private final ArrayList<PendingOperation> mPendingOperations = - new ArrayList<PendingOperation>(); - - private final SparseArray<ArrayList<SyncInfo>> mCurrentSyncs - = new SparseArray<ArrayList<SyncInfo>>(); - - private final SparseArray<SyncStatusInfo> mSyncStatus = - new SparseArray<SyncStatusInfo>(); - - private final ArrayList<SyncHistoryItem> mSyncHistory = - new ArrayList<SyncHistoryItem>(); - - private final RemoteCallbackList<ISyncStatusObserver> mChangeListeners - = new RemoteCallbackList<ISyncStatusObserver>(); - - private int mNextAuthorityId = 0; - - // We keep 4 weeks of stats. - private final DayStats[] mDayStats = new DayStats[7*4]; - private final Calendar mCal; - private int mYear; - private int mYearInDays; - - private final Context mContext; - - private static volatile SyncStorageEngine sSyncStorageEngine = null; - - private int mSyncRandomOffset; - - /** - * This file contains the core engine state: all accounts and the - * settings for them. It must never be lost, and should be changed - * infrequently, so it is stored as an XML file. - */ - private final AtomicFile mAccountInfoFile; - - /** - * This file contains the current sync status. We would like to retain - * it across boots, but its loss is not the end of the world, so we store - * this information as binary data. - */ - private final AtomicFile mStatusFile; - - /** - * This file contains sync statistics. This is purely debugging information - * so is written infrequently and can be thrown away at any time. - */ - private final AtomicFile mStatisticsFile; - - /** - * This file contains the pending sync operations. It is a binary file, - * which must be updated every time an operation is added or removed, - * so we have special handling of it. - */ - private final AtomicFile mPendingFile; - private static final int PENDING_FINISH_TO_WRITE = 4; - private int mNumPendingFinished = 0; - - private int mNextHistoryId = 0; - private SparseArray<Boolean> mMasterSyncAutomatically = new SparseArray<Boolean>(); - private boolean mDefaultMasterSyncAutomatically; - - private OnSyncRequestListener mSyncRequestListener; - - private SyncStorageEngine(Context context, File dataDir) { - mContext = context; - sSyncStorageEngine = this; - - mCal = Calendar.getInstance(TimeZone.getTimeZone("GMT+0")); - - mDefaultMasterSyncAutomatically = mContext.getResources().getBoolean( - com.android.internal.R.bool.config_syncstorageengine_masterSyncAutomatically); - - File systemDir = new File(dataDir, "system"); - File syncDir = new File(systemDir, "sync"); - syncDir.mkdirs(); - mAccountInfoFile = new AtomicFile(new File(syncDir, "accounts.xml")); - mStatusFile = new AtomicFile(new File(syncDir, "status.bin")); - mPendingFile = new AtomicFile(new File(syncDir, "pending.bin")); - mStatisticsFile = new AtomicFile(new File(syncDir, "stats.bin")); - - readAccountInfoLocked(); - readStatusLocked(); - readPendingOperationsLocked(); - readStatisticsLocked(); - readAndDeleteLegacyAccountInfoLocked(); - writeAccountInfoLocked(); - writeStatusLocked(); - writePendingOperationsLocked(); - writeStatisticsLocked(); - } - - public static SyncStorageEngine newTestInstance(Context context) { - return new SyncStorageEngine(context, context.getFilesDir()); - } - - public static void init(Context context) { - if (sSyncStorageEngine != null) { - return; - } - // This call will return the correct directory whether Encrypted File Systems is - // enabled or not. - File dataDir = Environment.getSecureDataDirectory(); - sSyncStorageEngine = new SyncStorageEngine(context, dataDir); - } - - public static SyncStorageEngine getSingleton() { - if (sSyncStorageEngine == null) { - throw new IllegalStateException("not initialized"); - } - return sSyncStorageEngine; - } - - protected void setOnSyncRequestListener(OnSyncRequestListener listener) { - if (mSyncRequestListener == null) { - mSyncRequestListener = listener; - } - } - - @Override public void handleMessage(Message msg) { - if (msg.what == MSG_WRITE_STATUS) { - synchronized (mAuthorities) { - writeStatusLocked(); - } - } else if (msg.what == MSG_WRITE_STATISTICS) { - synchronized (mAuthorities) { - writeStatisticsLocked(); - } - } - } - - public int getSyncRandomOffset() { - return mSyncRandomOffset; - } - - public void addStatusChangeListener(int mask, ISyncStatusObserver callback) { - synchronized (mAuthorities) { - mChangeListeners.register(callback, mask); - } - } - - public void removeStatusChangeListener(ISyncStatusObserver callback) { - synchronized (mAuthorities) { - mChangeListeners.unregister(callback); - } - } - - private void reportChange(int which) { - ArrayList<ISyncStatusObserver> reports = null; - synchronized (mAuthorities) { - int i = mChangeListeners.beginBroadcast(); - while (i > 0) { - i--; - Integer mask = (Integer)mChangeListeners.getBroadcastCookie(i); - if ((which & mask.intValue()) == 0) { - continue; - } - if (reports == null) { - reports = new ArrayList<ISyncStatusObserver>(i); - } - reports.add(mChangeListeners.getBroadcastItem(i)); - } - mChangeListeners.finishBroadcast(); - } - - if (DEBUG) { - Log.v(TAG, "reportChange " + which + " to: " + reports); - } - - if (reports != null) { - int i = reports.size(); - while (i > 0) { - i--; - try { - reports.get(i).onStatusChanged(which); - } catch (RemoteException e) { - // The remote callback list will take care of this for us. - } - } - } - } - - public boolean getSyncAutomatically(Account account, int userId, String providerName) { - synchronized (mAuthorities) { - if (account != null) { - AuthorityInfo authority = getAuthorityLocked(account, userId, providerName, - "getSyncAutomatically"); - return authority != null && authority.enabled; - } - - int i = mAuthorities.size(); - while (i > 0) { - i--; - AuthorityInfo authority = mAuthorities.valueAt(i); - if (authority.authority.equals(providerName) - && authority.userId == userId - && authority.enabled) { - return true; - } - } - return false; - } - } - - public void setSyncAutomatically(Account account, int userId, String providerName, - boolean sync) { - if (DEBUG) { - Log.d(TAG, "setSyncAutomatically: " + /* account + */" provider " + providerName - + ", user " + userId + " -> " + sync); - } - synchronized (mAuthorities) { - AuthorityInfo authority = getOrCreateAuthorityLocked(account, userId, providerName, -1, - false); - if (authority.enabled == sync) { - if (DEBUG) { - Log.d(TAG, "setSyncAutomatically: already set to " + sync + ", doing nothing"); - } - return; - } - authority.enabled = sync; - writeAccountInfoLocked(); - } - - if (sync) { - requestSync(account, userId, providerName, new Bundle()); - } - reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS); - } - - public int getIsSyncable(Account account, int userId, String providerName) { - synchronized (mAuthorities) { - if (account != null) { - AuthorityInfo authority = getAuthorityLocked(account, userId, providerName, - "getIsSyncable"); - if (authority == null) { - return -1; - } - return authority.syncable; - } - - int i = mAuthorities.size(); - while (i > 0) { - i--; - AuthorityInfo authority = mAuthorities.valueAt(i); - if (authority.authority.equals(providerName)) { - return authority.syncable; - } - } - return -1; - } - } - - public void setIsSyncable(Account account, int userId, String providerName, int syncable) { - if (syncable > 1) { - syncable = 1; - } else if (syncable < -1) { - syncable = -1; - } - if (DEBUG) { - Log.d(TAG, "setIsSyncable: " + account + ", provider " + providerName - + ", user " + userId + " -> " + syncable); - } - synchronized (mAuthorities) { - AuthorityInfo authority = getOrCreateAuthorityLocked(account, userId, providerName, -1, - false); - if (authority.syncable == syncable) { - if (DEBUG) { - Log.d(TAG, "setIsSyncable: already set to " + syncable + ", doing nothing"); - } - return; - } - authority.syncable = syncable; - writeAccountInfoLocked(); - } - - if (syncable > 0) { - requestSync(account, userId, providerName, new Bundle()); - } - reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS); - } - - public Pair<Long, Long> getBackoff(Account account, int userId, String providerName) { - synchronized (mAuthorities) { - AuthorityInfo authority = getAuthorityLocked(account, userId, providerName, - "getBackoff"); - if (authority == null || authority.backoffTime < 0) { - return null; - } - return Pair.create(authority.backoffTime, authority.backoffDelay); - } - } - - public void setBackoff(Account account, int userId, String providerName, - long nextSyncTime, long nextDelay) { - if (DEBUG) { - Log.v(TAG, "setBackoff: " + account + ", provider " + providerName - + ", user " + userId - + " -> nextSyncTime " + nextSyncTime + ", nextDelay " + nextDelay); - } - boolean changed = false; - synchronized (mAuthorities) { - if (account == null || providerName == null) { - for (AccountInfo accountInfo : mAccounts.values()) { - if (account != null && !account.equals(accountInfo.accountAndUser.account) - && userId != accountInfo.accountAndUser.userId) { - continue; - } - for (AuthorityInfo authorityInfo : accountInfo.authorities.values()) { - if (providerName != null && !providerName.equals(authorityInfo.authority)) { - continue; - } - if (authorityInfo.backoffTime != nextSyncTime - || authorityInfo.backoffDelay != nextDelay) { - authorityInfo.backoffTime = nextSyncTime; - authorityInfo.backoffDelay = nextDelay; - changed = true; - } - } - } - } else { - AuthorityInfo authority = - getOrCreateAuthorityLocked(account, userId, providerName, -1 /* ident */, - true); - if (authority.backoffTime == nextSyncTime && authority.backoffDelay == nextDelay) { - return; - } - authority.backoffTime = nextSyncTime; - authority.backoffDelay = nextDelay; - changed = true; - } - } - - if (changed) { - reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS); - } - } - - public void clearAllBackoffs(SyncQueue syncQueue) { - boolean changed = false; - synchronized (mAuthorities) { - synchronized (syncQueue) { - for (AccountInfo accountInfo : mAccounts.values()) { - for (AuthorityInfo authorityInfo : accountInfo.authorities.values()) { - if (authorityInfo.backoffTime != NOT_IN_BACKOFF_MODE - || authorityInfo.backoffDelay != NOT_IN_BACKOFF_MODE) { - if (DEBUG) { - Log.v(TAG, "clearAllBackoffs:" - + " authority:" + authorityInfo.authority - + " account:" + accountInfo.accountAndUser.account.name - + " user:" + accountInfo.accountAndUser.userId - + " backoffTime was: " + authorityInfo.backoffTime - + " backoffDelay was: " + authorityInfo.backoffDelay); - } - authorityInfo.backoffTime = NOT_IN_BACKOFF_MODE; - authorityInfo.backoffDelay = NOT_IN_BACKOFF_MODE; - syncQueue.onBackoffChanged(accountInfo.accountAndUser.account, - accountInfo.accountAndUser.userId, authorityInfo.authority, 0); - changed = true; - } - } - } - } - } - - if (changed) { - reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS); - } - } - - public void setDelayUntilTime(Account account, int userId, String providerName, - long delayUntil) { - if (DEBUG) { - Log.v(TAG, "setDelayUntil: " + account + ", provider " + providerName - + ", user " + userId + " -> delayUntil " + delayUntil); - } - synchronized (mAuthorities) { - AuthorityInfo authority = getOrCreateAuthorityLocked( - account, userId, providerName, -1 /* ident */, true); - if (authority.delayUntil == delayUntil) { - return; - } - authority.delayUntil = delayUntil; - } - - reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS); - } - - public long getDelayUntilTime(Account account, int userId, String providerName) { - synchronized (mAuthorities) { - AuthorityInfo authority = getAuthorityLocked(account, userId, providerName, - "getDelayUntil"); - if (authority == null) { - return 0; - } - return authority.delayUntil; - } - } - - private void updateOrRemovePeriodicSync(Account account, int userId, String providerName, - Bundle extras, - long period, boolean add) { - if (period <= 0) { - period = 0; - } - if (extras == null) { - extras = new Bundle(); - } - if (DEBUG) { - Log.v(TAG, "addOrRemovePeriodicSync: " + account + ", user " + userId - + ", provider " + providerName - + " -> period " + period + ", extras " + extras); - } - synchronized (mAuthorities) { - try { - AuthorityInfo authority = - getOrCreateAuthorityLocked(account, userId, providerName, -1, false); - if (add) { - // add this periodic sync if one with the same extras doesn't already - // exist in the periodicSyncs array - boolean alreadyPresent = false; - for (int i = 0, N = authority.periodicSyncs.size(); i < N; i++) { - Pair<Bundle, Long> syncInfo = authority.periodicSyncs.get(i); - final Bundle existingExtras = syncInfo.first; - if (equals(existingExtras, extras)) { - if (syncInfo.second == period) { - return; - } - authority.periodicSyncs.set(i, Pair.create(extras, period)); - alreadyPresent = true; - break; - } - } - // if we added an entry to the periodicSyncs array also add an entry to - // the periodic syncs status to correspond to it - if (!alreadyPresent) { - authority.periodicSyncs.add(Pair.create(extras, period)); - SyncStatusInfo status = getOrCreateSyncStatusLocked(authority.ident); - status.setPeriodicSyncTime(authority.periodicSyncs.size() - 1, 0); - } - } else { - // remove any periodic syncs that match the authority and extras - SyncStatusInfo status = mSyncStatus.get(authority.ident); - boolean changed = false; - Iterator<Pair<Bundle, Long>> iterator = authority.periodicSyncs.iterator(); - int i = 0; - while (iterator.hasNext()) { - Pair<Bundle, Long> syncInfo = iterator.next(); - if (equals(syncInfo.first, extras)) { - iterator.remove(); - changed = true; - // if we removed an entry from the periodicSyncs array also - // remove the corresponding entry from the status - if (status != null) { - status.removePeriodicSyncTime(i); - } - } else { - i++; - } - } - if (!changed) { - return; - } - } - } finally { - writeAccountInfoLocked(); - writeStatusLocked(); - } - } - - reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS); - } - - public void addPeriodicSync(Account account, int userId, String providerName, Bundle extras, - long pollFrequency) { - updateOrRemovePeriodicSync(account, userId, providerName, extras, pollFrequency, - true /* add */); - } - - public void removePeriodicSync(Account account, int userId, String providerName, - Bundle extras) { - updateOrRemovePeriodicSync(account, userId, providerName, extras, 0 /* period, ignored */, - false /* remove */); - } - - public List<PeriodicSync> getPeriodicSyncs(Account account, int userId, String providerName) { - ArrayList<PeriodicSync> syncs = new ArrayList<PeriodicSync>(); - synchronized (mAuthorities) { - AuthorityInfo authority = getAuthorityLocked(account, userId, providerName, - "getPeriodicSyncs"); - if (authority != null) { - for (Pair<Bundle, Long> item : authority.periodicSyncs) { - syncs.add(new PeriodicSync(account, providerName, item.first, - item.second)); - } - } - } - return syncs; - } - - public void setMasterSyncAutomatically(boolean flag, int userId) { - synchronized (mAuthorities) { - Boolean auto = mMasterSyncAutomatically.get(userId); - if (auto != null && (boolean) auto == flag) { - return; - } - mMasterSyncAutomatically.put(userId, flag); - writeAccountInfoLocked(); - } - if (flag) { - requestSync(null, userId, null, new Bundle()); - } - reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS); - mContext.sendBroadcast(SYNC_CONNECTION_SETTING_CHANGED_INTENT); - } - - public boolean getMasterSyncAutomatically(int userId) { - synchronized (mAuthorities) { - Boolean auto = mMasterSyncAutomatically.get(userId); - return auto == null ? mDefaultMasterSyncAutomatically : auto; - } - } - - public AuthorityInfo getOrCreateAuthority(Account account, int userId, String authority) { - synchronized (mAuthorities) { - return getOrCreateAuthorityLocked(account, userId, authority, - -1 /* assign a new identifier if creating a new authority */, - true /* write to storage if this results in a change */); - } - } - - public void removeAuthority(Account account, int userId, String authority) { - synchronized (mAuthorities) { - removeAuthorityLocked(account, userId, authority, true /* doWrite */); - } - } - - public AuthorityInfo getAuthority(int authorityId) { - synchronized (mAuthorities) { - return mAuthorities.get(authorityId); - } - } - - /** - * Returns true if there is currently a sync operation for the given - * account or authority actively being processed. - */ - public boolean isSyncActive(Account account, int userId, String authority) { - synchronized (mAuthorities) { - for (SyncInfo syncInfo : getCurrentSyncs(userId)) { - AuthorityInfo ainfo = getAuthority(syncInfo.authorityId); - if (ainfo != null && ainfo.account.equals(account) - && ainfo.authority.equals(authority) - && ainfo.userId == userId) { - return true; - } - } - } - - return false; - } - - public PendingOperation insertIntoPending(PendingOperation op) { - synchronized (mAuthorities) { - if (DEBUG) { - Log.v(TAG, "insertIntoPending: account=" + op.account - + " user=" + op.userId - + " auth=" + op.authority - + " src=" + op.syncSource - + " extras=" + op.extras); - } - - AuthorityInfo authority = getOrCreateAuthorityLocked(op.account, op.userId, - op.authority, - -1 /* desired identifier */, - true /* write accounts to storage */); - if (authority == null) { - return null; - } - - op = new PendingOperation(op); - op.authorityId = authority.ident; - mPendingOperations.add(op); - appendPendingOperationLocked(op); - - SyncStatusInfo status = getOrCreateSyncStatusLocked(authority.ident); - status.pending = true; - } - - reportChange(ContentResolver.SYNC_OBSERVER_TYPE_PENDING); - return op; - } - - public boolean deleteFromPending(PendingOperation op) { - boolean res = false; - synchronized (mAuthorities) { - if (DEBUG) { - Log.v(TAG, "deleteFromPending: account=" + op.account - + " user=" + op.userId - + " auth=" + op.authority - + " src=" + op.syncSource - + " extras=" + op.extras); - } - if (mPendingOperations.remove(op)) { - if (mPendingOperations.size() == 0 - || mNumPendingFinished >= PENDING_FINISH_TO_WRITE) { - writePendingOperationsLocked(); - mNumPendingFinished = 0; - } else { - mNumPendingFinished++; - } - - AuthorityInfo authority = getAuthorityLocked(op.account, op.userId, op.authority, - "deleteFromPending"); - if (authority != null) { - if (DEBUG) Log.v(TAG, "removing - " + authority); - final int N = mPendingOperations.size(); - boolean morePending = false; - for (int i=0; i<N; i++) { - PendingOperation cur = mPendingOperations.get(i); - if (cur.account.equals(op.account) - && cur.authority.equals(op.authority) - && cur.userId == op.userId) { - morePending = true; - break; - } - } - - if (!morePending) { - if (DEBUG) Log.v(TAG, "no more pending!"); - SyncStatusInfo status = getOrCreateSyncStatusLocked(authority.ident); - status.pending = false; - } - } - - res = true; - } - } - - reportChange(ContentResolver.SYNC_OBSERVER_TYPE_PENDING); - return res; - } - - /** - * Return a copy of the current array of pending operations. The - * PendingOperation objects are the real objects stored inside, so that - * they can be used with deleteFromPending(). - */ - public ArrayList<PendingOperation> getPendingOperations() { - synchronized (mAuthorities) { - return new ArrayList<PendingOperation>(mPendingOperations); - } - } - - /** - * Return the number of currently pending operations. - */ - public int getPendingOperationCount() { - synchronized (mAuthorities) { - return mPendingOperations.size(); - } - } - - /** - * Called when the set of account has changed, given the new array of - * active accounts. - */ - public void doDatabaseCleanup(Account[] accounts, int userId) { - synchronized (mAuthorities) { - if (DEBUG) Log.v(TAG, "Updating for new accounts..."); - SparseArray<AuthorityInfo> removing = new SparseArray<AuthorityInfo>(); - Iterator<AccountInfo> accIt = mAccounts.values().iterator(); - while (accIt.hasNext()) { - AccountInfo acc = accIt.next(); - if (!ArrayUtils.contains(accounts, acc.accountAndUser.account) - && acc.accountAndUser.userId == userId) { - // This account no longer exists... - if (DEBUG) { - Log.v(TAG, "Account removed: " + acc.accountAndUser); - } - for (AuthorityInfo auth : acc.authorities.values()) { - removing.put(auth.ident, auth); - } - accIt.remove(); - } - } - - // Clean out all data structures. - int i = removing.size(); - if (i > 0) { - while (i > 0) { - i--; - int ident = removing.keyAt(i); - mAuthorities.remove(ident); - int j = mSyncStatus.size(); - while (j > 0) { - j--; - if (mSyncStatus.keyAt(j) == ident) { - mSyncStatus.remove(mSyncStatus.keyAt(j)); - } - } - j = mSyncHistory.size(); - while (j > 0) { - j--; - if (mSyncHistory.get(j).authorityId == ident) { - mSyncHistory.remove(j); - } - } - } - writeAccountInfoLocked(); - writeStatusLocked(); - writePendingOperationsLocked(); - writeStatisticsLocked(); - } - } - } - - /** - * Called when a sync is starting. Supply a valid ActiveSyncContext with information - * about the sync. - */ - public SyncInfo addActiveSync(SyncManager.ActiveSyncContext activeSyncContext) { - final SyncInfo syncInfo; - synchronized (mAuthorities) { - if (DEBUG) { - Log.v(TAG, "setActiveSync: account=" - + activeSyncContext.mSyncOperation.account - + " auth=" + activeSyncContext.mSyncOperation.authority - + " src=" + activeSyncContext.mSyncOperation.syncSource - + " extras=" + activeSyncContext.mSyncOperation.extras); - } - AuthorityInfo authority = getOrCreateAuthorityLocked( - activeSyncContext.mSyncOperation.account, - activeSyncContext.mSyncOperation.userId, - activeSyncContext.mSyncOperation.authority, - -1 /* assign a new identifier if creating a new authority */, - true /* write to storage if this results in a change */); - syncInfo = new SyncInfo(authority.ident, - authority.account, authority.authority, - activeSyncContext.mStartTime); - getCurrentSyncs(authority.userId).add(syncInfo); - } - - reportActiveChange(); - return syncInfo; - } - - /** - * Called to indicate that a previously active sync is no longer active. - */ - public void removeActiveSync(SyncInfo syncInfo, int userId) { - synchronized (mAuthorities) { - if (DEBUG) { - Log.v(TAG, "removeActiveSync: account=" + syncInfo.account - + " user=" + userId - + " auth=" + syncInfo.authority); - } - getCurrentSyncs(userId).remove(syncInfo); - } - - reportActiveChange(); - } - - /** - * To allow others to send active change reports, to poke clients. - */ - public void reportActiveChange() { - reportChange(ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE); - } - - /** - * Note that sync has started for the given account and authority. - */ - public long insertStartSyncEvent(Account accountName, int userId, String authorityName, - long now, int source, boolean initialization) { - long id; - synchronized (mAuthorities) { - if (DEBUG) { - Log.v(TAG, "insertStartSyncEvent: account=" + accountName + "user=" + userId - + " auth=" + authorityName + " source=" + source); - } - AuthorityInfo authority = getAuthorityLocked(accountName, userId, authorityName, - "insertStartSyncEvent"); - if (authority == null) { - return -1; - } - SyncHistoryItem item = new SyncHistoryItem(); - item.initialization = initialization; - item.authorityId = authority.ident; - item.historyId = mNextHistoryId++; - if (mNextHistoryId < 0) mNextHistoryId = 0; - item.eventTime = now; - item.source = source; - item.event = EVENT_START; - mSyncHistory.add(0, item); - while (mSyncHistory.size() > MAX_HISTORY) { - mSyncHistory.remove(mSyncHistory.size()-1); - } - id = item.historyId; - if (DEBUG) Log.v(TAG, "returning historyId " + id); - } - - reportChange(ContentResolver.SYNC_OBSERVER_TYPE_STATUS); - return id; - } - - public static boolean equals(Bundle b1, Bundle b2) { - if (b1.size() != b2.size()) { - return false; - } - if (b1.isEmpty()) { - return true; - } - for (String key : b1.keySet()) { - if (!b2.containsKey(key)) { - return false; - } - if (!b1.get(key).equals(b2.get(key))) { - return false; - } - } - return true; - } - - public void stopSyncEvent(long historyId, long elapsedTime, String resultMessage, - long downstreamActivity, long upstreamActivity) { - synchronized (mAuthorities) { - if (DEBUG) { - Log.v(TAG, "stopSyncEvent: historyId=" + historyId); - } - SyncHistoryItem item = null; - int i = mSyncHistory.size(); - while (i > 0) { - i--; - item = mSyncHistory.get(i); - if (item.historyId == historyId) { - break; - } - item = null; - } - - if (item == null) { - Log.w(TAG, "stopSyncEvent: no history for id " + historyId); - return; - } - - item.elapsedTime = elapsedTime; - item.event = EVENT_STOP; - item.mesg = resultMessage; - item.downstreamActivity = downstreamActivity; - item.upstreamActivity = upstreamActivity; - - SyncStatusInfo status = getOrCreateSyncStatusLocked(item.authorityId); - - status.numSyncs++; - status.totalElapsedTime += elapsedTime; - switch (item.source) { - case SOURCE_LOCAL: - status.numSourceLocal++; - break; - case SOURCE_POLL: - status.numSourcePoll++; - break; - case SOURCE_USER: - status.numSourceUser++; - break; - case SOURCE_SERVER: - status.numSourceServer++; - break; - case SOURCE_PERIODIC: - status.numSourcePeriodic++; - break; - } - - boolean writeStatisticsNow = false; - int day = getCurrentDayLocked(); - if (mDayStats[0] == null) { - mDayStats[0] = new DayStats(day); - } else if (day != mDayStats[0].day) { - System.arraycopy(mDayStats, 0, mDayStats, 1, mDayStats.length-1); - mDayStats[0] = new DayStats(day); - writeStatisticsNow = true; - } else if (mDayStats[0] == null) { - } - final DayStats ds = mDayStats[0]; - - final long lastSyncTime = (item.eventTime + elapsedTime); - boolean writeStatusNow = false; - if (MESG_SUCCESS.equals(resultMessage)) { - // - if successful, update the successful columns - if (status.lastSuccessTime == 0 || status.lastFailureTime != 0) { - writeStatusNow = true; - } - status.lastSuccessTime = lastSyncTime; - status.lastSuccessSource = item.source; - status.lastFailureTime = 0; - status.lastFailureSource = -1; - status.lastFailureMesg = null; - status.initialFailureTime = 0; - ds.successCount++; - ds.successTime += elapsedTime; - } else if (!MESG_CANCELED.equals(resultMessage)) { - if (status.lastFailureTime == 0) { - writeStatusNow = true; - } - status.lastFailureTime = lastSyncTime; - status.lastFailureSource = item.source; - status.lastFailureMesg = resultMessage; - if (status.initialFailureTime == 0) { - status.initialFailureTime = lastSyncTime; - } - ds.failureCount++; - ds.failureTime += elapsedTime; - } - - if (writeStatusNow) { - writeStatusLocked(); - } else if (!hasMessages(MSG_WRITE_STATUS)) { - sendMessageDelayed(obtainMessage(MSG_WRITE_STATUS), - WRITE_STATUS_DELAY); - } - if (writeStatisticsNow) { - writeStatisticsLocked(); - } else if (!hasMessages(MSG_WRITE_STATISTICS)) { - sendMessageDelayed(obtainMessage(MSG_WRITE_STATISTICS), - WRITE_STATISTICS_DELAY); - } - } - - reportChange(ContentResolver.SYNC_OBSERVER_TYPE_STATUS); - } - - /** - * Return a list of the currently active syncs. Note that the returned items are the - * real, live active sync objects, so be careful what you do with it. - */ - public List<SyncInfo> getCurrentSyncs(int userId) { - synchronized (mAuthorities) { - ArrayList<SyncInfo> syncs = mCurrentSyncs.get(userId); - if (syncs == null) { - syncs = new ArrayList<SyncInfo>(); - mCurrentSyncs.put(userId, syncs); - } - return syncs; - } - } - - /** - * Return an array of the current sync status for all authorities. Note - * that the objects inside the array are the real, live status objects, - * so be careful what you do with them. - */ - public ArrayList<SyncStatusInfo> getSyncStatus() { - synchronized (mAuthorities) { - final int N = mSyncStatus.size(); - ArrayList<SyncStatusInfo> ops = new ArrayList<SyncStatusInfo>(N); - for (int i=0; i<N; i++) { - ops.add(mSyncStatus.valueAt(i)); - } - return ops; - } - } - - /** - * Return an array of the current authorities. Note - * that the objects inside the array are the real, live objects, - * so be careful what you do with them. - */ - public ArrayList<AuthorityInfo> getAuthorities() { - synchronized (mAuthorities) { - final int N = mAuthorities.size(); - ArrayList<AuthorityInfo> infos = new ArrayList<AuthorityInfo>(N); - for (int i=0; i<N; i++) { - // Make deep copy because AuthorityInfo syncs are liable to change. - infos.add(new AuthorityInfo(mAuthorities.valueAt(i))); - } - return infos; - } - } - - /** - * Returns the status that matches the authority and account. - * - * @param account the account we want to check - * @param authority the authority whose row should be selected - * @return the SyncStatusInfo for the authority - */ - public SyncStatusInfo getStatusByAccountAndAuthority(Account account, int userId, - String authority) { - if (account == null || authority == null) { - throw new IllegalArgumentException(); - } - synchronized (mAuthorities) { - final int N = mSyncStatus.size(); - for (int i=0; i<N; i++) { - SyncStatusInfo cur = mSyncStatus.valueAt(i); - AuthorityInfo ainfo = mAuthorities.get(cur.authorityId); - - if (ainfo != null && ainfo.authority.equals(authority) - && ainfo.userId == userId - && account.equals(ainfo.account)) { - return cur; - } - } - return null; - } - } - - /** - * Return true if the pending status is true of any matching authorities. - */ - public boolean isSyncPending(Account account, int userId, String authority) { - synchronized (mAuthorities) { - final int N = mSyncStatus.size(); - for (int i=0; i<N; i++) { - SyncStatusInfo cur = mSyncStatus.valueAt(i); - AuthorityInfo ainfo = mAuthorities.get(cur.authorityId); - if (ainfo == null) { - continue; - } - if (userId != ainfo.userId) { - continue; - } - if (account != null && !ainfo.account.equals(account)) { - continue; - } - if (ainfo.authority.equals(authority) && cur.pending) { - return true; - } - } - return false; - } - } - - /** - * Return an array of the current sync status for all authorities. Note - * that the objects inside the array are the real, live status objects, - * so be careful what you do with them. - */ - public ArrayList<SyncHistoryItem> getSyncHistory() { - synchronized (mAuthorities) { - final int N = mSyncHistory.size(); - ArrayList<SyncHistoryItem> items = new ArrayList<SyncHistoryItem>(N); - for (int i=0; i<N; i++) { - items.add(mSyncHistory.get(i)); - } - return items; - } - } - - /** - * Return an array of the current per-day statistics. Note - * that the objects inside the array are the real, live status objects, - * so be careful what you do with them. - */ - public DayStats[] getDayStatistics() { - synchronized (mAuthorities) { - DayStats[] ds = new DayStats[mDayStats.length]; - System.arraycopy(mDayStats, 0, ds, 0, ds.length); - return ds; - } - } - - private int getCurrentDayLocked() { - mCal.setTimeInMillis(System.currentTimeMillis()); - final int dayOfYear = mCal.get(Calendar.DAY_OF_YEAR); - if (mYear != mCal.get(Calendar.YEAR)) { - mYear = mCal.get(Calendar.YEAR); - mCal.clear(); - mCal.set(Calendar.YEAR, mYear); - mYearInDays = (int)(mCal.getTimeInMillis()/86400000); - } - return dayOfYear + mYearInDays; - } - - /** - * Retrieve an authority, returning null if one does not exist. - * - * @param accountName The name of the account for the authority. - * @param authorityName The name of the authority itself. - * @param tag If non-null, this will be used in a log message if the - * requested authority does not exist. - */ - private AuthorityInfo getAuthorityLocked(Account accountName, int userId, String authorityName, - String tag) { - AccountAndUser au = new AccountAndUser(accountName, userId); - AccountInfo accountInfo = mAccounts.get(au); - if (accountInfo == null) { - if (tag != null) { - if (DEBUG) { - Log.v(TAG, tag + ": unknown account " + au); - } - } - return null; - } - AuthorityInfo authority = accountInfo.authorities.get(authorityName); - if (authority == null) { - if (tag != null) { - if (DEBUG) { - Log.v(TAG, tag + ": unknown authority " + authorityName); - } - } - return null; - } - - return authority; - } - - private AuthorityInfo getOrCreateAuthorityLocked(Account accountName, int userId, - String authorityName, int ident, boolean doWrite) { - AccountAndUser au = new AccountAndUser(accountName, userId); - AccountInfo account = mAccounts.get(au); - if (account == null) { - account = new AccountInfo(au); - mAccounts.put(au, account); - } - AuthorityInfo authority = account.authorities.get(authorityName); - if (authority == null) { - if (ident < 0) { - ident = mNextAuthorityId; - mNextAuthorityId++; - doWrite = true; - } - if (DEBUG) { - Log.v(TAG, "created a new AuthorityInfo for " + accountName - + ", user " + userId - + ", provider " + authorityName); - } - authority = new AuthorityInfo(accountName, userId, authorityName, ident); - account.authorities.put(authorityName, authority); - mAuthorities.put(ident, authority); - if (doWrite) { - writeAccountInfoLocked(); - } - } - - return authority; - } - - private void removeAuthorityLocked(Account account, int userId, String authorityName, - boolean doWrite) { - AccountInfo accountInfo = mAccounts.get(new AccountAndUser(account, userId)); - if (accountInfo != null) { - final AuthorityInfo authorityInfo = accountInfo.authorities.remove(authorityName); - if (authorityInfo != null) { - mAuthorities.remove(authorityInfo.ident); - if (doWrite) { - writeAccountInfoLocked(); - } - } - } - } - - public SyncStatusInfo getOrCreateSyncStatus(AuthorityInfo authority) { - synchronized (mAuthorities) { - return getOrCreateSyncStatusLocked(authority.ident); - } - } - - private SyncStatusInfo getOrCreateSyncStatusLocked(int authorityId) { - SyncStatusInfo status = mSyncStatus.get(authorityId); - if (status == null) { - status = new SyncStatusInfo(authorityId); - mSyncStatus.put(authorityId, status); - } - return status; - } - - public void writeAllState() { - synchronized (mAuthorities) { - // Account info is always written so no need to do it here. - - if (mNumPendingFinished > 0) { - // Only write these if they are out of date. - writePendingOperationsLocked(); - } - - // Just always write these... they are likely out of date. - writeStatusLocked(); - writeStatisticsLocked(); - } - } - - /** - * public for testing - */ - public void clearAndReadState() { - synchronized (mAuthorities) { - mAuthorities.clear(); - mAccounts.clear(); - mPendingOperations.clear(); - mSyncStatus.clear(); - mSyncHistory.clear(); - - readAccountInfoLocked(); - readStatusLocked(); - readPendingOperationsLocked(); - readStatisticsLocked(); - readAndDeleteLegacyAccountInfoLocked(); - writeAccountInfoLocked(); - writeStatusLocked(); - writePendingOperationsLocked(); - writeStatisticsLocked(); - } - } - - /** - * Read all account information back in to the initial engine state. - */ - private void readAccountInfoLocked() { - int highestAuthorityId = -1; - FileInputStream fis = null; - try { - fis = mAccountInfoFile.openRead(); - if (DEBUG_FILE) Log.v(TAG, "Reading " + mAccountInfoFile.getBaseFile()); - XmlPullParser parser = Xml.newPullParser(); - parser.setInput(fis, null); - int eventType = parser.getEventType(); - while (eventType != XmlPullParser.START_TAG) { - eventType = parser.next(); - } - String tagName = parser.getName(); - if ("accounts".equals(tagName)) { - String listen = parser.getAttributeValue(null, XML_ATTR_LISTEN_FOR_TICKLES); - String versionString = parser.getAttributeValue(null, "version"); - int version; - try { - version = (versionString == null) ? 0 : Integer.parseInt(versionString); - } catch (NumberFormatException e) { - version = 0; - } - String nextIdString = parser.getAttributeValue(null, XML_ATTR_NEXT_AUTHORITY_ID); - try { - int id = (nextIdString == null) ? 0 : Integer.parseInt(nextIdString); - mNextAuthorityId = Math.max(mNextAuthorityId, id); - } catch (NumberFormatException e) { - // don't care - } - String offsetString = parser.getAttributeValue(null, XML_ATTR_SYNC_RANDOM_OFFSET); - try { - mSyncRandomOffset = (offsetString == null) ? 0 : Integer.parseInt(offsetString); - } catch (NumberFormatException e) { - mSyncRandomOffset = 0; - } - if (mSyncRandomOffset == 0) { - Random random = new Random(System.currentTimeMillis()); - mSyncRandomOffset = random.nextInt(86400); - } - mMasterSyncAutomatically.put(0, listen == null || Boolean.parseBoolean(listen)); - eventType = parser.next(); - AuthorityInfo authority = null; - Pair<Bundle, Long> periodicSync = null; - do { - if (eventType == XmlPullParser.START_TAG) { - tagName = parser.getName(); - if (parser.getDepth() == 2) { - if ("authority".equals(tagName)) { - authority = parseAuthority(parser, version); - periodicSync = null; - if (authority.ident > highestAuthorityId) { - highestAuthorityId = authority.ident; - } - } else if (XML_TAG_LISTEN_FOR_TICKLES.equals(tagName)) { - parseListenForTickles(parser); - } - } else if (parser.getDepth() == 3) { - if ("periodicSync".equals(tagName) && authority != null) { - periodicSync = parsePeriodicSync(parser, authority); - } - } else if (parser.getDepth() == 4 && periodicSync != null) { - if ("extra".equals(tagName)) { - parseExtra(parser, periodicSync); - } - } - } - eventType = parser.next(); - } while (eventType != XmlPullParser.END_DOCUMENT); - } - } catch (XmlPullParserException e) { - Log.w(TAG, "Error reading accounts", e); - return; - } catch (java.io.IOException e) { - if (fis == null) Log.i(TAG, "No initial accounts"); - else Log.w(TAG, "Error reading accounts", e); - return; - } finally { - mNextAuthorityId = Math.max(highestAuthorityId + 1, mNextAuthorityId); - if (fis != null) { - try { - fis.close(); - } catch (java.io.IOException e1) { - } - } - } - - maybeMigrateSettingsForRenamedAuthorities(); - } - - /** - * some authority names have changed. copy over their settings and delete the old ones - * @return true if a change was made - */ - private boolean maybeMigrateSettingsForRenamedAuthorities() { - boolean writeNeeded = false; - - ArrayList<AuthorityInfo> authoritiesToRemove = new ArrayList<AuthorityInfo>(); - final int N = mAuthorities.size(); - for (int i=0; i<N; i++) { - AuthorityInfo authority = mAuthorities.valueAt(i); - // skip this authority if it isn't one of the renamed ones - final String newAuthorityName = sAuthorityRenames.get(authority.authority); - if (newAuthorityName == null) { - continue; - } - - // remember this authority so we can remove it later. we can't remove it - // now without messing up this loop iteration - authoritiesToRemove.add(authority); - - // this authority isn't enabled, no need to copy it to the new authority name since - // the default is "disabled" - if (!authority.enabled) { - continue; - } - - // if we already have a record of this new authority then don't copy over the settings - if (getAuthorityLocked(authority.account, authority.userId, newAuthorityName, "cleanup") - != null) { - continue; - } - - AuthorityInfo newAuthority = getOrCreateAuthorityLocked(authority.account, - authority.userId, newAuthorityName, -1 /* ident */, false /* doWrite */); - newAuthority.enabled = true; - writeNeeded = true; - } - - for (AuthorityInfo authorityInfo : authoritiesToRemove) { - removeAuthorityLocked(authorityInfo.account, authorityInfo.userId, - authorityInfo.authority, false /* doWrite */); - writeNeeded = true; - } - - return writeNeeded; - } - - private void parseListenForTickles(XmlPullParser parser) { - String user = parser.getAttributeValue(null, XML_ATTR_USER); - int userId = 0; - try { - userId = Integer.parseInt(user); - } catch (NumberFormatException e) { - Log.e(TAG, "error parsing the user for listen-for-tickles", e); - } catch (NullPointerException e) { - Log.e(TAG, "the user in listen-for-tickles is null", e); - } - String enabled = parser.getAttributeValue(null, XML_ATTR_ENABLED); - boolean listen = enabled == null || Boolean.parseBoolean(enabled); - mMasterSyncAutomatically.put(userId, listen); - } - - private AuthorityInfo parseAuthority(XmlPullParser parser, int version) { - AuthorityInfo authority = null; - int id = -1; - try { - id = Integer.parseInt(parser.getAttributeValue( - null, "id")); - } catch (NumberFormatException e) { - Log.e(TAG, "error parsing the id of the authority", e); - } catch (NullPointerException e) { - Log.e(TAG, "the id of the authority is null", e); - } - if (id >= 0) { - String authorityName = parser.getAttributeValue(null, "authority"); - String enabled = parser.getAttributeValue(null, XML_ATTR_ENABLED); - String syncable = parser.getAttributeValue(null, "syncable"); - String accountName = parser.getAttributeValue(null, "account"); - String accountType = parser.getAttributeValue(null, "type"); - String user = parser.getAttributeValue(null, XML_ATTR_USER); - int userId = user == null ? 0 : Integer.parseInt(user); - if (accountType == null) { - accountType = "com.google"; - syncable = "unknown"; - } - authority = mAuthorities.get(id); - if (DEBUG_FILE) Log.v(TAG, "Adding authority: account=" - + accountName + " auth=" + authorityName - + " user=" + userId - + " enabled=" + enabled - + " syncable=" + syncable); - if (authority == null) { - if (DEBUG_FILE) Log.v(TAG, "Creating entry"); - authority = getOrCreateAuthorityLocked( - new Account(accountName, accountType), userId, authorityName, id, false); - // If the version is 0 then we are upgrading from a file format that did not - // know about periodic syncs. In that case don't clear the list since we - // want the default, which is a daily periodioc sync. - // Otherwise clear out this default list since we will populate it later with - // the periodic sync descriptions that are read from the configuration file. - if (version > 0) { - authority.periodicSyncs.clear(); - } - } - if (authority != null) { - authority.enabled = enabled == null || Boolean.parseBoolean(enabled); - if ("unknown".equals(syncable)) { - authority.syncable = -1; - } else { - authority.syncable = - (syncable == null || Boolean.parseBoolean(syncable)) ? 1 : 0; - } - } else { - Log.w(TAG, "Failure adding authority: account=" - + accountName + " auth=" + authorityName - + " enabled=" + enabled - + " syncable=" + syncable); - } - } - - return authority; - } - - private Pair<Bundle, Long> parsePeriodicSync(XmlPullParser parser, AuthorityInfo authority) { - Bundle extras = new Bundle(); - String periodValue = parser.getAttributeValue(null, "period"); - final long period; - try { - period = Long.parseLong(periodValue); - } catch (NumberFormatException e) { - Log.e(TAG, "error parsing the period of a periodic sync", e); - return null; - } catch (NullPointerException e) { - Log.e(TAG, "the period of a periodic sync is null", e); - return null; - } - final Pair<Bundle, Long> periodicSync = Pair.create(extras, period); - authority.periodicSyncs.add(periodicSync); - - return periodicSync; - } - - private void parseExtra(XmlPullParser parser, Pair<Bundle, Long> periodicSync) { - final Bundle extras = periodicSync.first; - String name = parser.getAttributeValue(null, "name"); - String type = parser.getAttributeValue(null, "type"); - String value1 = parser.getAttributeValue(null, "value1"); - String value2 = parser.getAttributeValue(null, "value2"); - - try { - if ("long".equals(type)) { - extras.putLong(name, Long.parseLong(value1)); - } else if ("integer".equals(type)) { - extras.putInt(name, Integer.parseInt(value1)); - } else if ("double".equals(type)) { - extras.putDouble(name, Double.parseDouble(value1)); - } else if ("float".equals(type)) { - extras.putFloat(name, Float.parseFloat(value1)); - } else if ("boolean".equals(type)) { - extras.putBoolean(name, Boolean.parseBoolean(value1)); - } else if ("string".equals(type)) { - extras.putString(name, value1); - } else if ("account".equals(type)) { - extras.putParcelable(name, new Account(value1, value2)); - } - } catch (NumberFormatException e) { - Log.e(TAG, "error parsing bundle value", e); - } catch (NullPointerException e) { - Log.e(TAG, "error parsing bundle value", e); - } - } - - /** - * Write all account information to the account file. - */ - private void writeAccountInfoLocked() { - if (DEBUG_FILE) Log.v(TAG, "Writing new " + mAccountInfoFile.getBaseFile()); - FileOutputStream fos = null; - - try { - fos = mAccountInfoFile.startWrite(); - XmlSerializer out = new FastXmlSerializer(); - out.setOutput(fos, "utf-8"); - out.startDocument(null, true); - out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); - - out.startTag(null, "accounts"); - out.attribute(null, "version", Integer.toString(ACCOUNTS_VERSION)); - out.attribute(null, XML_ATTR_NEXT_AUTHORITY_ID, Integer.toString(mNextAuthorityId)); - out.attribute(null, XML_ATTR_SYNC_RANDOM_OFFSET, Integer.toString(mSyncRandomOffset)); - - // Write the Sync Automatically flags for each user - final int M = mMasterSyncAutomatically.size(); - for (int m = 0; m < M; m++) { - int userId = mMasterSyncAutomatically.keyAt(m); - Boolean listen = mMasterSyncAutomatically.valueAt(m); - out.startTag(null, XML_TAG_LISTEN_FOR_TICKLES); - out.attribute(null, XML_ATTR_USER, Integer.toString(userId)); - out.attribute(null, XML_ATTR_ENABLED, Boolean.toString(listen)); - out.endTag(null, XML_TAG_LISTEN_FOR_TICKLES); - } - - final int N = mAuthorities.size(); - for (int i=0; i<N; i++) { - AuthorityInfo authority = mAuthorities.valueAt(i); - out.startTag(null, "authority"); - out.attribute(null, "id", Integer.toString(authority.ident)); - out.attribute(null, "account", authority.account.name); - out.attribute(null, XML_ATTR_USER, Integer.toString(authority.userId)); - out.attribute(null, "type", authority.account.type); - out.attribute(null, "authority", authority.authority); - out.attribute(null, XML_ATTR_ENABLED, Boolean.toString(authority.enabled)); - if (authority.syncable < 0) { - out.attribute(null, "syncable", "unknown"); - } else { - out.attribute(null, "syncable", Boolean.toString(authority.syncable != 0)); - } - for (Pair<Bundle, Long> periodicSync : authority.periodicSyncs) { - out.startTag(null, "periodicSync"); - out.attribute(null, "period", Long.toString(periodicSync.second)); - final Bundle extras = periodicSync.first; - for (String key : extras.keySet()) { - out.startTag(null, "extra"); - out.attribute(null, "name", key); - final Object value = extras.get(key); - if (value instanceof Long) { - out.attribute(null, "type", "long"); - out.attribute(null, "value1", value.toString()); - } else if (value instanceof Integer) { - out.attribute(null, "type", "integer"); - out.attribute(null, "value1", value.toString()); - } else if (value instanceof Boolean) { - out.attribute(null, "type", "boolean"); - out.attribute(null, "value1", value.toString()); - } else if (value instanceof Float) { - out.attribute(null, "type", "float"); - out.attribute(null, "value1", value.toString()); - } else if (value instanceof Double) { - out.attribute(null, "type", "double"); - out.attribute(null, "value1", value.toString()); - } else if (value instanceof String) { - out.attribute(null, "type", "string"); - out.attribute(null, "value1", value.toString()); - } else if (value instanceof Account) { - out.attribute(null, "type", "account"); - out.attribute(null, "value1", ((Account)value).name); - out.attribute(null, "value2", ((Account)value).type); - } - out.endTag(null, "extra"); - } - out.endTag(null, "periodicSync"); - } - out.endTag(null, "authority"); - } - - out.endTag(null, "accounts"); - - out.endDocument(); - - mAccountInfoFile.finishWrite(fos); - } catch (java.io.IOException e1) { - Log.w(TAG, "Error writing accounts", e1); - if (fos != null) { - mAccountInfoFile.failWrite(fos); - } - } - } - - static int getIntColumn(Cursor c, String name) { - return c.getInt(c.getColumnIndex(name)); - } - - static long getLongColumn(Cursor c, String name) { - return c.getLong(c.getColumnIndex(name)); - } - - /** - * Load sync engine state from the old syncmanager database, and then - * erase it. Note that we don't deal with pending operations, active - * sync, or history. - */ - private void readAndDeleteLegacyAccountInfoLocked() { - // Look for old database to initialize from. - File file = mContext.getDatabasePath("syncmanager.db"); - if (!file.exists()) { - return; - } - String path = file.getPath(); - SQLiteDatabase db = null; - try { - db = SQLiteDatabase.openDatabase(path, null, - SQLiteDatabase.OPEN_READONLY); - } catch (SQLiteException e) { - } - - if (db != null) { - final boolean hasType = db.getVersion() >= 11; - - // Copy in all of the status information, as well as accounts. - if (DEBUG_FILE) Log.v(TAG, "Reading legacy sync accounts db"); - SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); - qb.setTables("stats, status"); - HashMap<String,String> map = new HashMap<String,String>(); - map.put("_id", "status._id as _id"); - map.put("account", "stats.account as account"); - if (hasType) { - map.put("account_type", "stats.account_type as account_type"); - } - map.put("authority", "stats.authority as authority"); - map.put("totalElapsedTime", "totalElapsedTime"); - map.put("numSyncs", "numSyncs"); - map.put("numSourceLocal", "numSourceLocal"); - map.put("numSourcePoll", "numSourcePoll"); - map.put("numSourceServer", "numSourceServer"); - map.put("numSourceUser", "numSourceUser"); - map.put("lastSuccessSource", "lastSuccessSource"); - map.put("lastSuccessTime", "lastSuccessTime"); - map.put("lastFailureSource", "lastFailureSource"); - map.put("lastFailureTime", "lastFailureTime"); - map.put("lastFailureMesg", "lastFailureMesg"); - map.put("pending", "pending"); - qb.setProjectionMap(map); - qb.appendWhere("stats._id = status.stats_id"); - Cursor c = qb.query(db, null, null, null, null, null, null); - while (c.moveToNext()) { - String accountName = c.getString(c.getColumnIndex("account")); - String accountType = hasType - ? c.getString(c.getColumnIndex("account_type")) : null; - if (accountType == null) { - accountType = "com.google"; - } - String authorityName = c.getString(c.getColumnIndex("authority")); - AuthorityInfo authority = this.getOrCreateAuthorityLocked( - new Account(accountName, accountType), 0 /* legacy is single-user */, - authorityName, -1, false); - if (authority != null) { - int i = mSyncStatus.size(); - boolean found = false; - SyncStatusInfo st = null; - while (i > 0) { - i--; - st = mSyncStatus.valueAt(i); - if (st.authorityId == authority.ident) { - found = true; - break; - } - } - if (!found) { - st = new SyncStatusInfo(authority.ident); - mSyncStatus.put(authority.ident, st); - } - st.totalElapsedTime = getLongColumn(c, "totalElapsedTime"); - st.numSyncs = getIntColumn(c, "numSyncs"); - st.numSourceLocal = getIntColumn(c, "numSourceLocal"); - st.numSourcePoll = getIntColumn(c, "numSourcePoll"); - st.numSourceServer = getIntColumn(c, "numSourceServer"); - st.numSourceUser = getIntColumn(c, "numSourceUser"); - st.numSourcePeriodic = 0; - st.lastSuccessSource = getIntColumn(c, "lastSuccessSource"); - st.lastSuccessTime = getLongColumn(c, "lastSuccessTime"); - st.lastFailureSource = getIntColumn(c, "lastFailureSource"); - st.lastFailureTime = getLongColumn(c, "lastFailureTime"); - st.lastFailureMesg = c.getString(c.getColumnIndex("lastFailureMesg")); - st.pending = getIntColumn(c, "pending") != 0; - } - } - - c.close(); - - // Retrieve the settings. - qb = new SQLiteQueryBuilder(); - qb.setTables("settings"); - c = qb.query(db, null, null, null, null, null, null); - while (c.moveToNext()) { - String name = c.getString(c.getColumnIndex("name")); - String value = c.getString(c.getColumnIndex("value")); - if (name == null) continue; - if (name.equals("listen_for_tickles")) { - setMasterSyncAutomatically(value == null || Boolean.parseBoolean(value), 0); - } else if (name.startsWith("sync_provider_")) { - String provider = name.substring("sync_provider_".length(), - name.length()); - int i = mAuthorities.size(); - while (i > 0) { - i--; - AuthorityInfo authority = mAuthorities.valueAt(i); - if (authority.authority.equals(provider)) { - authority.enabled = value == null || Boolean.parseBoolean(value); - authority.syncable = 1; - } - } - } - } - - c.close(); - - db.close(); - - (new File(path)).delete(); - } - } - - public static final int STATUS_FILE_END = 0; - public static final int STATUS_FILE_ITEM = 100; - - /** - * Read all sync status back in to the initial engine state. - */ - private void readStatusLocked() { - if (DEBUG_FILE) Log.v(TAG, "Reading " + mStatusFile.getBaseFile()); - try { - byte[] data = mStatusFile.readFully(); - Parcel in = Parcel.obtain(); - in.unmarshall(data, 0, data.length); - in.setDataPosition(0); - int token; - while ((token=in.readInt()) != STATUS_FILE_END) { - if (token == STATUS_FILE_ITEM) { - SyncStatusInfo status = new SyncStatusInfo(in); - if (mAuthorities.indexOfKey(status.authorityId) >= 0) { - status.pending = false; - if (DEBUG_FILE) Log.v(TAG, "Adding status for id " - + status.authorityId); - mSyncStatus.put(status.authorityId, status); - } - } else { - // Ooops. - Log.w(TAG, "Unknown status token: " + token); - break; - } - } - } catch (java.io.IOException e) { - Log.i(TAG, "No initial status"); - } - } - - /** - * Write all sync status to the sync status file. - */ - private void writeStatusLocked() { - if (DEBUG_FILE) Log.v(TAG, "Writing new " + mStatusFile.getBaseFile()); - - // The file is being written, so we don't need to have a scheduled - // write until the next change. - removeMessages(MSG_WRITE_STATUS); - - FileOutputStream fos = null; - try { - fos = mStatusFile.startWrite(); - Parcel out = Parcel.obtain(); - final int N = mSyncStatus.size(); - for (int i=0; i<N; i++) { - SyncStatusInfo status = mSyncStatus.valueAt(i); - out.writeInt(STATUS_FILE_ITEM); - status.writeToParcel(out, 0); - } - out.writeInt(STATUS_FILE_END); - fos.write(out.marshall()); - out.recycle(); - - mStatusFile.finishWrite(fos); - } catch (java.io.IOException e1) { - Log.w(TAG, "Error writing status", e1); - if (fos != null) { - mStatusFile.failWrite(fos); - } - } - } - - public static final int PENDING_OPERATION_VERSION = 2; - - /** - * Read all pending operations back in to the initial engine state. - */ - private void readPendingOperationsLocked() { - if (DEBUG_FILE) Log.v(TAG, "Reading " + mPendingFile.getBaseFile()); - try { - byte[] data = mPendingFile.readFully(); - Parcel in = Parcel.obtain(); - in.unmarshall(data, 0, data.length); - in.setDataPosition(0); - final int SIZE = in.dataSize(); - while (in.dataPosition() < SIZE) { - int version = in.readInt(); - if (version != PENDING_OPERATION_VERSION && version != 1) { - Log.w(TAG, "Unknown pending operation version " - + version + "; dropping all ops"); - break; - } - int authorityId = in.readInt(); - int syncSource = in.readInt(); - byte[] flatExtras = in.createByteArray(); - boolean expedited; - if (version == PENDING_OPERATION_VERSION) { - expedited = in.readInt() != 0; - } else { - expedited = false; - } - AuthorityInfo authority = mAuthorities.get(authorityId); - if (authority != null) { - Bundle extras; - if (flatExtras != null) { - extras = unflattenBundle(flatExtras); - } else { - // if we are unable to parse the extras for whatever reason convert this - // to a regular sync by creating an empty extras - extras = new Bundle(); - } - PendingOperation op = new PendingOperation( - authority.account, authority.userId, syncSource, - authority.authority, extras, expedited); - op.authorityId = authorityId; - op.flatExtras = flatExtras; - if (DEBUG_FILE) Log.v(TAG, "Adding pending op: account=" + op.account - + " auth=" + op.authority - + " src=" + op.syncSource - + " expedited=" + op.expedited - + " extras=" + op.extras); - mPendingOperations.add(op); - } - } - } catch (java.io.IOException e) { - Log.i(TAG, "No initial pending operations"); - } - } - - private void writePendingOperationLocked(PendingOperation op, Parcel out) { - out.writeInt(PENDING_OPERATION_VERSION); - out.writeInt(op.authorityId); - out.writeInt(op.syncSource); - if (op.flatExtras == null && op.extras != null) { - op.flatExtras = flattenBundle(op.extras); - } - out.writeByteArray(op.flatExtras); - out.writeInt(op.expedited ? 1 : 0); - } - - /** - * Write all currently pending ops to the pending ops file. - */ - private void writePendingOperationsLocked() { - final int N = mPendingOperations.size(); - FileOutputStream fos = null; - try { - if (N == 0) { - if (DEBUG_FILE) Log.v(TAG, "Truncating " + mPendingFile.getBaseFile()); - mPendingFile.truncate(); - return; - } - - if (DEBUG_FILE) Log.v(TAG, "Writing new " + mPendingFile.getBaseFile()); - fos = mPendingFile.startWrite(); - - Parcel out = Parcel.obtain(); - for (int i=0; i<N; i++) { - PendingOperation op = mPendingOperations.get(i); - writePendingOperationLocked(op, out); - } - fos.write(out.marshall()); - out.recycle(); - - mPendingFile.finishWrite(fos); - } catch (java.io.IOException e1) { - Log.w(TAG, "Error writing pending operations", e1); - if (fos != null) { - mPendingFile.failWrite(fos); - } - } - } - - /** - * Append the given operation to the pending ops file; if unable to, - * write all pending ops. - */ - private void appendPendingOperationLocked(PendingOperation op) { - if (DEBUG_FILE) Log.v(TAG, "Appending to " + mPendingFile.getBaseFile()); - FileOutputStream fos = null; - try { - fos = mPendingFile.openAppend(); - } catch (java.io.IOException e) { - if (DEBUG_FILE) Log.v(TAG, "Failed append; writing full file"); - writePendingOperationsLocked(); - return; - } - - try { - Parcel out = Parcel.obtain(); - writePendingOperationLocked(op, out); - fos.write(out.marshall()); - out.recycle(); - } catch (java.io.IOException e1) { - Log.w(TAG, "Error writing pending operations", e1); - } finally { - try { - fos.close(); - } catch (java.io.IOException e2) { - } - } - } - - static private byte[] flattenBundle(Bundle bundle) { - byte[] flatData = null; - Parcel parcel = Parcel.obtain(); - try { - bundle.writeToParcel(parcel, 0); - flatData = parcel.marshall(); - } finally { - parcel.recycle(); - } - return flatData; - } - - static private Bundle unflattenBundle(byte[] flatData) { - Bundle bundle; - Parcel parcel = Parcel.obtain(); - try { - parcel.unmarshall(flatData, 0, flatData.length); - parcel.setDataPosition(0); - bundle = parcel.readBundle(); - } catch (RuntimeException e) { - // A RuntimeException is thrown if we were unable to parse the parcel. - // Create an empty parcel in this case. - bundle = new Bundle(); - } finally { - parcel.recycle(); - } - return bundle; - } - - private void requestSync(Account account, int userId, String authority, Bundle extras) { - // If this is happening in the system process, then call the syncrequest listener - // to make a request back to the SyncManager directly. - // If this is probably a test instance, then call back through the ContentResolver - // which will know which userId to apply based on the Binder id. - if (android.os.Process.myUid() == android.os.Process.SYSTEM_UID - && mSyncRequestListener != null) { - mSyncRequestListener.onSyncRequest(account, userId, authority, extras); - } else { - ContentResolver.requestSync(account, authority, extras); - } - } - - public static final int STATISTICS_FILE_END = 0; - public static final int STATISTICS_FILE_ITEM_OLD = 100; - public static final int STATISTICS_FILE_ITEM = 101; - - /** - * Read all sync statistics back in to the initial engine state. - */ - private void readStatisticsLocked() { - try { - byte[] data = mStatisticsFile.readFully(); - Parcel in = Parcel.obtain(); - in.unmarshall(data, 0, data.length); - in.setDataPosition(0); - int token; - int index = 0; - while ((token=in.readInt()) != STATISTICS_FILE_END) { - if (token == STATISTICS_FILE_ITEM - || token == STATISTICS_FILE_ITEM_OLD) { - int day = in.readInt(); - if (token == STATISTICS_FILE_ITEM_OLD) { - day = day - 2009 + 14245; // Magic! - } - DayStats ds = new DayStats(day); - ds.successCount = in.readInt(); - ds.successTime = in.readLong(); - ds.failureCount = in.readInt(); - ds.failureTime = in.readLong(); - if (index < mDayStats.length) { - mDayStats[index] = ds; - index++; - } - } else { - // Ooops. - Log.w(TAG, "Unknown stats token: " + token); - break; - } - } - } catch (java.io.IOException e) { - Log.i(TAG, "No initial statistics"); - } - } - - /** - * Write all sync statistics to the sync status file. - */ - private void writeStatisticsLocked() { - if (DEBUG_FILE) Log.v(TAG, "Writing new " + mStatisticsFile.getBaseFile()); - - // The file is being written, so we don't need to have a scheduled - // write until the next change. - removeMessages(MSG_WRITE_STATISTICS); - - FileOutputStream fos = null; - try { - fos = mStatisticsFile.startWrite(); - Parcel out = Parcel.obtain(); - final int N = mDayStats.length; - for (int i=0; i<N; i++) { - DayStats ds = mDayStats[i]; - if (ds == null) { - break; - } - out.writeInt(STATISTICS_FILE_ITEM); - out.writeInt(ds.day); - out.writeInt(ds.successCount); - out.writeLong(ds.successTime); - out.writeInt(ds.failureCount); - out.writeLong(ds.failureTime); - } - out.writeInt(STATISTICS_FILE_END); - fos.write(out.marshall()); - out.recycle(); - - mStatisticsFile.finishWrite(fos); - } catch (java.io.IOException e1) { - Log.w(TAG, "Error writing stats", e1); - if (fos != null) { - mStatisticsFile.failWrite(fos); - } - } - } -} diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index b9e432c..a368451 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -127,7 +127,16 @@ interface IPackageManager { * limit that kicks in when flags are included that bloat up the data * returned. */ - ParceledListSlice getInstalledPackages(int flags, in String lastRead, in int userId); + ParceledListSlice getInstalledPackages(int flags, in int userId); + + /** + * This implements getPackagesHoldingPermissions via a "last returned row" + * mechanism that is not exposed in the API. This is to get around the IPC + * limit that kicks in when flags are included that bloat up the data + * returned. + */ + ParceledListSlice getPackagesHoldingPermissions(in String[] permissions, + int flags, int userId); /** * This implements getInstalledApplications via a "last returned row" @@ -135,7 +144,7 @@ interface IPackageManager { * limit that kicks in when flags are included that bloat up the data * returned. */ - ParceledListSlice getInstalledApplications(int flags, in String lastRead, int userId); + ParceledListSlice getInstalledApplications(int flags, int userId); /** * Retrieve all applications that are marked as persistent. @@ -201,6 +210,8 @@ interface IPackageManager { List<PackageInfo> getPreferredPackages(int flags); + void resetPreferredActivities(int userId); + void addPreferredActivity(in IntentFilter filter, int match, in ComponentName[] set, in ComponentName activity, int userId); diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 8ba1988..c507245 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -175,6 +175,14 @@ public abstract class PackageManager { public static final int GET_CONFIGURATIONS = 0x00004000; /** + * {@link PackageInfo} flag: include disabled components which are in + * that state only because of {@link #COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED} + * in the returned info. Note that if you set this flag, applications + * that are in this disabled state will be reported as enabled. + */ + public static final int GET_DISABLED_UNTIL_USED_COMPONENTS = 0x00008000; + + /** * Resolution and querying flag: if set, only filters that support the * {@link android.content.Intent#CATEGORY_DEFAULT} will be considered for * matching. This is a synonym for including the CATEGORY_DEFAULT in your @@ -265,6 +273,19 @@ public abstract class PackageManager { public static final int COMPONENT_ENABLED_STATE_DISABLED_USER = 3; /** + * Flag for {@link #setApplicationEnabledSetting(String, int, int)} only: This + * application should be considered, until the point where the user actually + * wants to use it. This means that it will not normally show up to the user + * (such as in the launcher), but various parts of the user interface can + * use {@link #GET_DISABLED_UNTIL_USED_COMPONENTS} to still see it and allow + * the user to select it (as for example an IME, device admin, etc). Such code, + * once the user has selected the app, should at that point also make it enabled. + * This option currently <strong>can not</strong> be used with + * {@link #setComponentEnabledSetting(ComponentName, int, int)}. + */ + public static final int COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED = 4; + + /** * Flag parameter for {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} to * indicate that this package should be installed as forward locked, i.e. only the app itself * should have access to its code and non-resource assets. @@ -645,6 +666,15 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_INTERNAL_ERROR = -110; /** + * Installation failed return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} + * if the system failed to install the package because the user is restricted from installing + * apps. + * @hide + */ + public static final int INSTALL_FAILED_USER_RESTRICTED = -111; + + /** * Flag parameter for {@link #deletePackage} to indicate that you don't want to delete the * package's data directory. * @@ -689,6 +719,15 @@ public abstract class PackageManager { public static final int DELETE_FAILED_DEVICE_POLICY_MANAGER = -2; /** + * Deletion failed return code: this is passed to the + * {@link IPackageDeleteObserver} by {@link #deletePackage()} if the system + * failed to delete the package since the user is restricted. + * + * @hide + */ + public static final int DELETE_FAILED_USER_RESTRICTED = -3; + + /** * Return code that is passed to the {@link IPackageMoveObserver} by * {@link #movePackage(android.net.Uri, IPackageMoveObserver)} when the * package has been successfully moved by the system. @@ -1280,6 +1319,22 @@ public abstract class PackageManager { throws NameNotFoundException; /** + * @hide Return the uid associated with the given package name for the + * given user. + * + * <p>Throws {@link NameNotFoundException} if a package with the given + * name can not be found on the system. + * + * @param packageName The full name (i.e. com.google.apps.contacts) of the + * desired package. + * @param userHandle The user handle identifier to look up the package under. + * + * @return Returns an integer uid who owns the given package name. + */ + public abstract int getPackageUid(String packageName, int userHandle) + throws NameNotFoundException; + + /** * Retrieve all of the information we know about a particular permission. * * <p>Throws {@link NameNotFoundException} if a permission with the given @@ -1496,11 +1551,43 @@ public abstract class PackageManager { * @see #GET_SERVICES * @see #GET_SIGNATURES * @see #GET_UNINSTALLED_PACKAGES - * */ public abstract List<PackageInfo> getInstalledPackages(int flags); /** + * Return a List of all installed packages that are currently + * holding any of the given permissions. + * + * @param flags Additional option flags. Use any combination of + * {@link #GET_ACTIVITIES}, + * {@link #GET_GIDS}, + * {@link #GET_CONFIGURATIONS}, + * {@link #GET_INSTRUMENTATION}, + * {@link #GET_PERMISSIONS}, + * {@link #GET_PROVIDERS}, + * {@link #GET_RECEIVERS}, + * {@link #GET_SERVICES}, + * {@link #GET_SIGNATURES}, + * {@link #GET_UNINSTALLED_PACKAGES} to modify the data returned. + * + * @return Returns a List of PackageInfo objects, one for each installed + * application that is holding any of the permissions that were provided. + * + * @see #GET_ACTIVITIES + * @see #GET_GIDS + * @see #GET_CONFIGURATIONS + * @see #GET_INSTRUMENTATION + * @see #GET_PERMISSIONS + * @see #GET_PROVIDERS + * @see #GET_RECEIVERS + * @see #GET_SERVICES + * @see #GET_SIGNATURES + * @see #GET_UNINSTALLED_PACKAGES + */ + public abstract List<PackageInfo> getPackagesHoldingPermissions( + String[] permissions, int flags); + + /** * Return a List of all packages that are installed on the device, for a specific user. * Requesting a list of installed packages for another user * will require the permission INTERACT_ACROSS_USERS_FULL. @@ -1726,14 +1813,14 @@ public abstract class PackageManager { /** * Return a List of all application packages that are installed on the * device. If flag GET_UNINSTALLED_PACKAGES has been set, a list of all - * applications including those deleted with DONT_DELETE_DATA(partially + * applications including those deleted with DONT_DELETE_DATA (partially * installed apps with data directory) will be returned. * * @param flags Additional option flags. Use any combination of * {@link #GET_META_DATA}, {@link #GET_SHARED_LIBRARY_FILES}, * {@link #GET_UNINSTALLED_PACKAGES} to modify the data returned. * - * @return A List of ApplicationInfo objects, one for each application that + * @return Returns a List of ApplicationInfo objects, one for each application that * is installed on the device. In the unlikely case of there being * no installed applications, an empty list is returned. * If flag GET_UNINSTALLED_PACKAGES is set, a list of all diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index 3e8c2a8..e1887bc 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -3527,29 +3527,45 @@ public class PackageParser { return generateApplicationInfo(p, flags, state, UserHandle.getCallingUserId()); } + private static void updateApplicationInfo(ApplicationInfo ai, int flags, + PackageUserState state) { + // CompatibilityMode is global state. + if (!sCompatibilityModeEnabled) { + ai.disableCompatibilityMode(); + } + if (state.installed) { + ai.flags |= ApplicationInfo.FLAG_INSTALLED; + } else { + ai.flags &= ~ApplicationInfo.FLAG_INSTALLED; + } + if (state.enabled == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { + ai.enabled = true; + } else if (state.enabled == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) { + ai.enabled = (flags&PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS) != 0; + } else if (state.enabled == PackageManager.COMPONENT_ENABLED_STATE_DISABLED + || state.enabled == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER) { + ai.enabled = false; + } + ai.enabledSetting = state.enabled; + } + public static ApplicationInfo generateApplicationInfo(Package p, int flags, PackageUserState state, int userId) { if (p == null) return null; if (!checkUseInstalled(flags, state)) { return null; } - if (!copyNeeded(flags, p, state, null, userId)) { - // CompatibilityMode is global state. It's safe to modify the instance - // of the package. - if (!sCompatibilityModeEnabled) { - p.applicationInfo.disableCompatibilityMode(); - } - // Make sure we report as installed. Also safe to do, since the - // default state should be installed (we will always copy if we - // need to report it is not installed). - p.applicationInfo.flags |= ApplicationInfo.FLAG_INSTALLED; - if (state.enabled == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { - p.applicationInfo.enabled = true; - } else if (state.enabled == PackageManager.COMPONENT_ENABLED_STATE_DISABLED - || state.enabled == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER) { - p.applicationInfo.enabled = false; - } - p.applicationInfo.enabledSetting = state.enabled; + if (!copyNeeded(flags, p, state, null, userId) + && ((flags&PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS) == 0 + || state.enabled != PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED)) { + // In this case it is safe to directly modify the internal ApplicationInfo state: + // - CompatibilityMode is global state, so will be the same for every call. + // - We only come in to here if the app should reported as installed; this is the + // default state, and we will do a copy otherwise. + // - The enable state will always be reported the same for the application across + // calls; the only exception is for the UNTIL_USED mode, and in that case we will + // be doing a copy. + updateApplicationInfo(p.applicationInfo, flags, state); return p.applicationInfo; } @@ -3565,26 +3581,12 @@ public class PackageParser { if ((flags & PackageManager.GET_SHARED_LIBRARY_FILES) != 0) { ai.sharedLibraryFiles = p.usesLibraryFiles; } - if (!sCompatibilityModeEnabled) { - ai.disableCompatibilityMode(); - } if (state.stopped) { ai.flags |= ApplicationInfo.FLAG_STOPPED; } else { ai.flags &= ~ApplicationInfo.FLAG_STOPPED; } - if (state.installed) { - ai.flags |= ApplicationInfo.FLAG_INSTALLED; - } else { - ai.flags &= ~ApplicationInfo.FLAG_INSTALLED; - } - if (state.enabled == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { - ai.enabled = true; - } else if (state.enabled == PackageManager.COMPONENT_ENABLED_STATE_DISABLED - || state.enabled == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER) { - ai.enabled = false; - } - ai.enabledSetting = state.enabled; + updateApplicationInfo(ai, flags, state); return ai; } diff --git a/core/java/android/content/pm/ParceledListSlice.java b/core/java/android/content/pm/ParceledListSlice.java index f3a98db..8a43472 100644 --- a/core/java/android/content/pm/ParceledListSlice.java +++ b/core/java/android/content/pm/ParceledListSlice.java @@ -16,44 +16,92 @@ package android.content.pm; +import android.os.Binder; +import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; +import android.os.RemoteException; +import android.util.Log; +import java.util.ArrayList; import java.util.List; /** - * Builds up a parcel that is discarded when written to another parcel or - * written to a list. This is useful for API that sends huge lists across a - * Binder that may be larger than the IPC limit. + * Transfer a large list of Parcelable objects across an IPC. Splits into + * multiple transactions if needed. * * @hide */ public class ParceledListSlice<T extends Parcelable> implements Parcelable { + private static String TAG = "ParceledListSlice"; + private static boolean DEBUG = false; + /* * TODO get this number from somewhere else. For now set it to a quarter of * the 1MB limit. */ private static final int MAX_IPC_SIZE = 256 * 1024; + private static final int MAX_FIRST_IPC_SIZE = MAX_IPC_SIZE / 2; - private Parcel mParcel; - - private int mNumItems; + private final List<T> mList; - private boolean mIsLastSlice; + public ParceledListSlice(List<T> list) { + mList = list; + } - public ParceledListSlice() { - mParcel = Parcel.obtain(); + private ParceledListSlice(Parcel p, ClassLoader loader) { + final int N = p.readInt(); + mList = new ArrayList<T>(N); + if (DEBUG) Log.d(TAG, "Retrieving " + N + " items"); + if (N <= 0) { + return; + } + Parcelable.Creator<T> creator = p.readParcelableCreator(loader); + int i = 0; + while (i < N) { + if (p.readInt() == 0) { + break; + } + mList.add(p.readCreator(creator, loader)); + if (DEBUG) Log.d(TAG, "Read inline #" + i + ": " + mList.get(mList.size()-1)); + i++; + } + if (i >= N) { + return; + } + final IBinder retriever = p.readStrongBinder(); + while (i < N) { + if (DEBUG) Log.d(TAG, "Reading more @" + i + " of " + N + ": retriever=" + retriever); + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInt(i); + try { + retriever.transact(IBinder.FIRST_CALL_TRANSACTION, data, reply, 0); + } catch (RemoteException e) { + Log.w(TAG, "Failure retrieving array; only received " + i + " of " + N, e); + return; + } + while (i < N && reply.readInt() != 0) { + mList.add(reply.readCreator(creator, loader)); + if (DEBUG) Log.d(TAG, "Read extra #" + i + ": " + mList.get(mList.size()-1)); + i++; + } + reply.recycle(); + data.recycle(); + } } - private ParceledListSlice(Parcel p, int numItems, boolean lastSlice) { - mParcel = p; - mNumItems = numItems; - mIsLastSlice = lastSlice; + public List<T> getList() { + return mList; } @Override public int describeContents() { - return 0; + int contents = 0; + for (int i=0; i<mList.size(); i++) { + contents |= mList.get(i).describeContents(); + } + return contents; } /** @@ -63,104 +111,59 @@ public class ParceledListSlice<T extends Parcelable> implements Parcelable { */ @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(mNumItems); - dest.writeInt(mIsLastSlice ? 1 : 0); - - if (mNumItems > 0) { - final int parcelSize = mParcel.dataSize(); - dest.writeInt(parcelSize); - dest.appendFrom(mParcel, 0, parcelSize); - } - - mNumItems = 0; - mParcel.recycle(); - mParcel = null; - } - - /** - * Appends a parcel to this list slice. - * - * @param item Parcelable item to append to this list slice - * @return true when the list slice is full and should not be appended to - * anymore - */ - public boolean append(T item) { - if (mParcel == null) { - throw new IllegalStateException("ParceledListSlice has already been recycled"); - } - - item.writeToParcel(mParcel, PARCELABLE_WRITE_RETURN_VALUE); - mNumItems++; - - return mParcel.dataSize() > MAX_IPC_SIZE; - } - - /** - * Populates a list and discards the internal state of the - * ParceledListSlice in the process. The instance should - * not be used anymore. - * - * @param list list to insert items from this slice. - * @param creator creator that knows how to unparcel the - * target object type. - * @return the last item inserted into the list or null if none. - */ - public T populateList(List<T> list, Creator<T> creator) { - mParcel.setDataPosition(0); - - T item = null; - for (int i = 0; i < mNumItems; i++) { - item = creator.createFromParcel(mParcel); - list.add(item); + final int N = mList.size(); + final int callFlags = flags; + dest.writeInt(N); + if (DEBUG) Log.d(TAG, "Writing " + N + " items"); + if (N > 0) { + dest.writeParcelableCreator(mList.get(0)); + int i = 0; + while (i < N && dest.dataSize() < MAX_FIRST_IPC_SIZE) { + dest.writeInt(1); + mList.get(i).writeToParcel(dest, callFlags); + if (DEBUG) Log.d(TAG, "Wrote inline #" + i + ": " + mList.get(i)); + i++; + } + if (i < N) { + dest.writeInt(0); + Binder retriever = new Binder() { + @Override + protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) + throws RemoteException { + if (code != FIRST_CALL_TRANSACTION) { + return super.onTransact(code, data, reply, flags); + } + int i = data.readInt(); + if (DEBUG) Log.d(TAG, "Writing more @" + i + " of " + N); + while (i < N && reply.dataSize() < MAX_IPC_SIZE) { + reply.writeInt(1); + mList.get(i).writeToParcel(reply, callFlags); + if (DEBUG) Log.d(TAG, "Wrote extra #" + i + ": " + mList.get(i)); + i++; + } + if (i < N) { + if (DEBUG) Log.d(TAG, "Breaking @" + i + " of " + N); + reply.writeInt(0); + } + return true; + } + }; + if (DEBUG) Log.d(TAG, "Breaking @" + i + " of " + N + ": retriever=" + retriever); + dest.writeStrongBinder(retriever); + } } - - mParcel.recycle(); - mParcel = null; - - return item; - } - - /** - * Sets whether this is the last list slice in the series. - * - * @param lastSlice - */ - public void setLastSlice(boolean lastSlice) { - mIsLastSlice = lastSlice; - } - - /** - * Returns whether this is the last slice in a series of slices. - * - * @return true if this is the last slice in the series. - */ - public boolean isLastSlice() { - return mIsLastSlice; } @SuppressWarnings("unchecked") - public static final Parcelable.Creator<ParceledListSlice> CREATOR = - new Parcelable.Creator<ParceledListSlice>() { + public static final Parcelable.ClassLoaderCreator<ParceledListSlice> CREATOR = + new Parcelable.ClassLoaderCreator<ParceledListSlice>() { public ParceledListSlice createFromParcel(Parcel in) { - final int numItems = in.readInt(); - final boolean lastSlice = in.readInt() == 1; - - if (numItems > 0) { - final int parcelSize = in.readInt(); - - // Advance within this Parcel - int offset = in.dataPosition(); - in.setDataPosition(offset + parcelSize); - - Parcel p = Parcel.obtain(); - p.setDataPosition(0); - p.appendFrom(in, offset, parcelSize); - p.setDataPosition(0); + return new ParceledListSlice(in, null); + } - return new ParceledListSlice(p, numItems, lastSlice); - } else { - return new ParceledListSlice(); - } + @Override + public ParceledListSlice createFromParcel(Parcel in, ClassLoader loader) { + return new ParceledListSlice(in, loader); } public ParceledListSlice[] newArray(int size) { diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index b316f23..24a0bb5 100644 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -77,7 +77,7 @@ public class Resources { private static final int ID_OTHER = 0x01000004; - private static final Object mSync = new Object(); + private static final Object sSync = new Object(); /*package*/ static Resources mSystem = null; // Information about preloaded resources. Note that they are not @@ -92,17 +92,18 @@ public class Resources { private static boolean sPreloaded; private static int sPreloadedDensity; - /*package*/ final TypedValue mTmpValue = new TypedValue(); - /*package*/ final Configuration mTmpConfig = new Configuration(); + // These are protected by mAccessLock. - // These are protected by the mTmpValue lock. - private final LongSparseArray<WeakReference<Drawable.ConstantState> > mDrawableCache + /*package*/ final Object mAccessLock = new Object(); + /*package*/ final Configuration mTmpConfig = new Configuration(); + /*package*/ TypedValue mTmpValue = new TypedValue(); + /*package*/ final LongSparseArray<WeakReference<Drawable.ConstantState> > mDrawableCache = new LongSparseArray<WeakReference<Drawable.ConstantState> >(); - private final LongSparseArray<WeakReference<ColorStateList> > mColorStateListCache + /*package*/ final LongSparseArray<WeakReference<ColorStateList> > mColorStateListCache = new LongSparseArray<WeakReference<ColorStateList> >(); - private final LongSparseArray<WeakReference<Drawable.ConstantState> > mColorDrawableCache + /*package*/ final LongSparseArray<WeakReference<Drawable.ConstantState> > mColorDrawableCache = new LongSparseArray<WeakReference<Drawable.ConstantState> >(); - private boolean mPreloading; + /*package*/ boolean mPreloading; /*package*/ TypedArray mCachedStyledAttributes = null; RuntimeException mLastRetrievedAttrs = null; @@ -196,7 +197,7 @@ public class Resources { * on orientation, etc). */ public static Resources getSystem() { - synchronized (mSync) { + synchronized (sSync) { Resources ret = mSystem; if (ret == null) { ret = new Resources(); @@ -266,7 +267,7 @@ public class Resources { } private NativePluralRules getPluralRule() { - synchronized (mSync) { + synchronized (sSync) { if (mPluralRule == null) { mPluralRule = NativePluralRules.forLocale(mConfiguration.locale); } @@ -517,8 +518,11 @@ public class Resources { * @see #getDimensionPixelSize */ public float getDimension(int id) throws NotFoundException { - synchronized (mTmpValue) { + synchronized (mAccessLock) { TypedValue value = mTmpValue; + if (value == null) { + mTmpValue = value = new TypedValue(); + } getValue(id, value, true); if (value.type == TypedValue.TYPE_DIMENSION) { return TypedValue.complexToDimension(value.data, mMetrics); @@ -549,8 +553,11 @@ public class Resources { * @see #getDimensionPixelSize */ public int getDimensionPixelOffset(int id) throws NotFoundException { - synchronized (mTmpValue) { + synchronized (mAccessLock) { TypedValue value = mTmpValue; + if (value == null) { + mTmpValue = value = new TypedValue(); + } getValue(id, value, true); if (value.type == TypedValue.TYPE_DIMENSION) { return TypedValue.complexToDimensionPixelOffset( @@ -583,8 +590,11 @@ public class Resources { * @see #getDimensionPixelOffset */ public int getDimensionPixelSize(int id) throws NotFoundException { - synchronized (mTmpValue) { + synchronized (mAccessLock) { TypedValue value = mTmpValue; + if (value == null) { + mTmpValue = value = new TypedValue(); + } getValue(id, value, true); if (value.type == TypedValue.TYPE_DIMENSION) { return TypedValue.complexToDimensionPixelSize( @@ -614,8 +624,11 @@ public class Resources { * @throws NotFoundException Throws NotFoundException if the given ID does not exist. */ public float getFraction(int id, int base, int pbase) { - synchronized (mTmpValue) { + synchronized (mAccessLock) { TypedValue value = mTmpValue; + if (value == null) { + mTmpValue = value = new TypedValue(); + } getValue(id, value, true); if (value.type == TypedValue.TYPE_FRACTION) { return TypedValue.complexToFraction(value.data, base, pbase); @@ -654,11 +667,23 @@ public class Resources { * @return Drawable An object that can be used to draw this resource. */ public Drawable getDrawable(int id) throws NotFoundException { - synchronized (mTmpValue) { - TypedValue value = mTmpValue; + TypedValue value; + synchronized (mAccessLock) { + value = mTmpValue; + if (value == null) { + value = new TypedValue(); + } else { + mTmpValue = null; + } getValue(id, value, true); - return loadDrawable(value, id); } + Drawable res = loadDrawable(value, id); + synchronized (mAccessLock) { + if (mTmpValue == null) { + mTmpValue = value; + } + } + return res; } /** @@ -681,8 +706,14 @@ public class Resources { * @return Drawable An object that can be used to draw this resource. */ public Drawable getDrawableForDensity(int id, int density) throws NotFoundException { - synchronized (mTmpValue) { - TypedValue value = mTmpValue; + TypedValue value; + synchronized (mAccessLock) { + value = mTmpValue; + if (value == null) { + value = new TypedValue(); + } else { + mTmpValue = null; + } getValueForDensity(id, density, value, true); /* @@ -699,9 +730,15 @@ public class Resources { value.density = (value.density * mMetrics.densityDpi) / density; } } + } - return loadDrawable(value, id); + Drawable res = loadDrawable(value, id); + synchronized (mAccessLock) { + if (mTmpValue == null) { + mTmpValue = value; + } } + return res; } /** @@ -739,20 +776,31 @@ public class Resources { * @return Returns a single color value in the form 0xAARRGGBB. */ public int getColor(int id) throws NotFoundException { - synchronized (mTmpValue) { - TypedValue value = mTmpValue; + TypedValue value; + synchronized (mAccessLock) { + value = mTmpValue; + if (value == null) { + value = new TypedValue(); + } getValue(id, value, true); if (value.type >= TypedValue.TYPE_FIRST_INT && value.type <= TypedValue.TYPE_LAST_INT) { + mTmpValue = value; return value.data; - } else if (value.type == TypedValue.TYPE_STRING) { - ColorStateList csl = loadColorStateList(mTmpValue, id); - return csl.getDefaultColor(); + } else if (value.type != TypedValue.TYPE_STRING) { + throw new NotFoundException( + "Resource ID #0x" + Integer.toHexString(id) + " type #0x" + + Integer.toHexString(value.type) + " is not valid"); + } + mTmpValue = null; + } + ColorStateList csl = loadColorStateList(value, id); + synchronized (mAccessLock) { + if (mTmpValue == null) { + mTmpValue = value; } - throw new NotFoundException( - "Resource ID #0x" + Integer.toHexString(id) + " type #0x" - + Integer.toHexString(value.type) + " is not valid"); } + return csl.getDefaultColor(); } /** @@ -770,11 +818,23 @@ public class Resources { * solid color or multiple colors that can be selected based on a state. */ public ColorStateList getColorStateList(int id) throws NotFoundException { - synchronized (mTmpValue) { - TypedValue value = mTmpValue; + TypedValue value; + synchronized (mAccessLock) { + value = mTmpValue; + if (value == null) { + value = new TypedValue(); + } else { + mTmpValue = null; + } getValue(id, value, true); - return loadColorStateList(value, id); } + ColorStateList res = loadColorStateList(value, id); + synchronized (mAccessLock) { + if (mTmpValue == null) { + mTmpValue = value; + } + } + return res; } /** @@ -791,8 +851,11 @@ public class Resources { * @return Returns the boolean value contained in the resource. */ public boolean getBoolean(int id) throws NotFoundException { - synchronized (mTmpValue) { + synchronized (mAccessLock) { TypedValue value = mTmpValue; + if (value == null) { + mTmpValue = value = new TypedValue(); + } getValue(id, value, true); if (value.type >= TypedValue.TYPE_FIRST_INT && value.type <= TypedValue.TYPE_LAST_INT) { @@ -816,8 +879,11 @@ public class Resources { * @return Returns the integer value contained in the resource. */ public int getInteger(int id) throws NotFoundException { - synchronized (mTmpValue) { + synchronized (mAccessLock) { TypedValue value = mTmpValue; + if (value == null) { + mTmpValue = value = new TypedValue(); + } getValue(id, value, true); if (value.type >= TypedValue.TYPE_FIRST_INT && value.type <= TypedValue.TYPE_LAST_INT) { @@ -917,9 +983,22 @@ public class Resources { * */ public InputStream openRawResource(int id) throws NotFoundException { - synchronized (mTmpValue) { - return openRawResource(id, mTmpValue); + TypedValue value; + synchronized (mAccessLock) { + value = mTmpValue; + if (value == null) { + value = new TypedValue(); + } else { + mTmpValue = null; + } + } + InputStream res = openRawResource(id, value); + synchronized (mAccessLock) { + if (mTmpValue == null) { + mTmpValue = value; + } } + return res; } /** @@ -971,22 +1050,32 @@ public class Resources { * */ public AssetFileDescriptor openRawResourceFd(int id) throws NotFoundException { - synchronized (mTmpValue) { - TypedValue value = mTmpValue; + TypedValue value; + synchronized (mAccessLock) { + value = mTmpValue; + if (value == null) { + value = new TypedValue(); + } else { + mTmpValue = null; + } getValue(id, value, true); - - try { - return mAssets.openNonAssetFd( - value.assetCookie, value.string.toString()); - } catch (Exception e) { - NotFoundException rnf = new NotFoundException( - "File " + value.string.toString() - + " from drawable resource ID #0x" - + Integer.toHexString(id)); - rnf.initCause(e); - throw rnf; + } + try { + return mAssets.openNonAssetFd( + value.assetCookie, value.string.toString()); + } catch (Exception e) { + NotFoundException rnf = new NotFoundException( + "File " + value.string.toString() + + " from drawable resource ID #0x" + + Integer.toHexString(id)); + rnf.initCause(e); + throw rnf; + } finally { + synchronized (mAccessLock) { + if (mTmpValue == null) { + mTmpValue = value; + } } - } } @@ -1118,7 +1207,7 @@ public class Resources { } /** - * Return a StyledAttributes holding the values defined by + * Return a TypedArray holding the values defined by * <var>Theme</var> which are listed in <var>attrs</var>. * * <p>Be sure to call {@link TypedArray#recycle() TypedArray.recycle()} when you are done @@ -1146,7 +1235,7 @@ public class Resources { } /** - * Return a StyledAttributes holding the values defined by the style + * Return a TypedArray holding the values defined by the style * resource <var>resid</var> which are listed in <var>attrs</var>. * * <p>Be sure to call {@link TypedArray#recycle() TypedArray.recycle()} when you are done @@ -1203,7 +1292,7 @@ public class Resources { } /** - * Return a StyledAttributes holding the attribute values in + * Return a TypedArray holding the attribute values in * <var>set</var> * that are listed in <var>attrs</var>. In addition, if the given * AttributeSet specifies a style class (through the "style" attribute), @@ -1235,10 +1324,10 @@ public class Resources { * @param attrs The desired attributes to be retrieved. * @param defStyleAttr An attribute in the current theme that contains a * reference to a style resource that supplies - * defaults values for the StyledAttributes. Can be + * defaults values for the TypedArray. Can be * 0 to not look for defaults. * @param defStyleRes A resource identifier of a style resource that - * supplies default values for the StyledAttributes, + * supplies default values for the TypedArray, * used only if defStyleAttr is 0 or can not be found * in the theme. Can be 0 to not look for defaults. * @@ -1407,7 +1496,7 @@ public class Resources { */ public void updateConfiguration(Configuration config, DisplayMetrics metrics, CompatibilityInfo compat) { - synchronized (mTmpValue) { + synchronized (mAccessLock) { if (false) { Slog.i(TAG, "**** Updating config of " + this + ": old config is " + mConfiguration + " old compat is " + mCompatibilityInfo); @@ -1497,21 +1586,21 @@ public class Resources { + " final compat is " + mCompatibilityInfo); } - clearDrawableCache(mDrawableCache, configChanges); - clearDrawableCache(mColorDrawableCache, configChanges); + clearDrawableCacheLocked(mDrawableCache, configChanges); + clearDrawableCacheLocked(mColorDrawableCache, configChanges); mColorStateListCache.clear(); flushLayoutCache(); } - synchronized (mSync) { + synchronized (sSync) { if (mPluralRule != null) { mPluralRule = NativePluralRules.forLocale(config.locale); } } } - private void clearDrawableCache( + private void clearDrawableCacheLocked( LongSparseArray<WeakReference<ConstantState>> cache, int configChanges) { int N = cache.size(); @@ -1631,6 +1720,9 @@ public class Resources { * resource was found. (0 is not a valid resource ID.) */ public int getIdentifier(String name, String defType, String defPackage) { + if (name == null) { + throw new NullPointerException("name is null"); + } try { return Integer.parseInt(name); } catch (Exception e) { @@ -1846,7 +1938,7 @@ public class Resources { * {@hide} */ public final void startPreloading() { - synchronized (mSync) { + synchronized (sSync) { if (sPreloaded) { throw new IllegalStateException("Resources already preloaded"); } @@ -1990,7 +2082,7 @@ public class Resources { } } } else { - synchronized (mTmpValue) { + synchronized (mAccessLock) { //Log.i(TAG, "Saving cached drawable @ #" + // Integer.toHexString(key.intValue()) // + " in " + this + ": " + cs); @@ -2010,7 +2102,7 @@ public class Resources { private Drawable getCachedDrawable( LongSparseArray<WeakReference<ConstantState>> drawableCache, long key) { - synchronized (mTmpValue) { + synchronized (mAccessLock) { WeakReference<Drawable.ConstantState> wr = drawableCache.get(key); if (wr != null) { // we have the key Drawable.ConstantState entry = wr.get(); @@ -2102,7 +2194,7 @@ public class Resources { sPreloadedColorStateLists.put(key, csl); } } else { - synchronized (mTmpValue) { + synchronized (mAccessLock) { //Log.i(TAG, "Saving cached color state list @ #" + // Integer.toHexString(key.intValue()) // + " in " + this + ": " + csl); @@ -2115,7 +2207,7 @@ public class Resources { } private ColorStateList getCachedColorStateList(long key) { - synchronized (mTmpValue) { + synchronized (mAccessLock) { WeakReference<ColorStateList> wr = mColorStateListCache.get(key); if (wr != null) { // we have the key ColorStateList entry = wr.get(); @@ -2134,8 +2226,11 @@ public class Resources { /*package*/ XmlResourceParser loadXmlResourceParser(int id, String type) throws NotFoundException { - synchronized (mTmpValue) { + synchronized (mAccessLock) { TypedValue value = mTmpValue; + if (value == null) { + mTmpValue = value = new TypedValue(); + } getValue(id, value, true); if (value.type == TypedValue.TYPE_STRING) { return loadXmlResourceParser(value.string.toString(), id, @@ -2197,7 +2292,7 @@ public class Resources { } private TypedArray getCachedStyledAttributes(int len) { - synchronized (mTmpValue) { + synchronized (mAccessLock) { TypedArray attrs = mCachedStyledAttributes; if (attrs != null) { mCachedStyledAttributes = null; diff --git a/core/java/android/content/res/StringBlock.java b/core/java/android/content/res/StringBlock.java index 63e33ce..78180b1 100644 --- a/core/java/android/content/res/StringBlock.java +++ b/core/java/android/content/res/StringBlock.java @@ -16,6 +16,7 @@ package android.content.res; +import android.graphics.Color; import android.text.*; import android.text.style.*; import android.util.Log; @@ -24,7 +25,7 @@ import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Typeface; -import com.android.internal.util.XmlUtils; +import java.util.Arrays; /** * Conveniences for retrieving data out of a compiled string resource. @@ -33,7 +34,7 @@ import com.android.internal.util.XmlUtils; */ final class StringBlock { private static final String TAG = "AssetManager"; - private static final boolean localLOGV = false || false; + private static final boolean localLOGV = false; private final int mNative; private final boolean mUseSparse; @@ -82,7 +83,7 @@ final class StringBlock { CharSequence res = str; int[] style = nativeGetStyle(mNative, idx); if (localLOGV) Log.v(TAG, "Got string: " + str); - if (localLOGV) Log.v(TAG, "Got styles: " + style); + if (localLOGV) Log.v(TAG, "Got styles: " + Arrays.toString(style)); if (style != null) { if (mStyleIDs == null) { mStyleIDs = new StyleIDs(); @@ -139,8 +140,12 @@ final class StringBlock { } protected void finalize() throws Throwable { - if (mOwnsNative) { - nativeDestroy(mNative); + try { + super.finalize(); + } finally { + if (mOwnsNative) { + nativeDestroy(mNative); + } } } @@ -236,19 +241,31 @@ final class StringBlock { sub = subtag(tag, ";fgcolor="); if (sub != null) { - int color = XmlUtils.convertValueToUnsignedInt(sub, -1); - buffer.setSpan(new ForegroundColorSpan(color), + buffer.setSpan(getColor(sub, true), style[i+1], style[i+2]+1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } + sub = subtag(tag, ";color="); + if (sub != null) { + buffer.setSpan(getColor(sub, true), + style[i+1], style[i+2]+1, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + sub = subtag(tag, ";bgcolor="); if (sub != null) { - int color = XmlUtils.convertValueToUnsignedInt(sub, -1); - buffer.setSpan(new BackgroundColorSpan(color), + buffer.setSpan(getColor(sub, false), style[i+1], style[i+2]+1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } + + sub = subtag(tag, ";face="); + if (sub != null) { + buffer.setSpan(new TypefaceSpan(sub), + style[i+1], style[i+2]+1, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } } else if (tag.startsWith("a;")) { String sub; @@ -289,6 +306,48 @@ final class StringBlock { } /** + * Returns a span for the specified color string representation. + * If the specified string does not represent a color (null, empty, etc.) + * the color black is returned instead. + * + * @param color The color as a string. Can be a resource reference, + * HTML hexadecimal, octal or a name + * @param foreground True if the color will be used as the foreground color, + * false otherwise + * + * @return A CharacterStyle + * + * @see Color#getHtmlColor(String) + */ + private static CharacterStyle getColor(String color, boolean foreground) { + int c = 0xff000000; + + if (!TextUtils.isEmpty(color)) { + if (color.startsWith("@")) { + Resources res = Resources.getSystem(); + String name = color.substring(1); + int colorRes = res.getIdentifier(name, "color", "android"); + if (colorRes != 0) { + ColorStateList colors = res.getColorStateList(colorRes); + if (foreground) { + return new TextAppearanceSpan(null, 0, 0, colors, null); + } else { + c = colors.getDefaultColor(); + } + } + } else { + c = Color.getHtmlColor(color); + } + } + + if (foreground) { + return new ForegroundColorSpan(c); + } else { + return new BackgroundColorSpan(c); + } + } + + /** * If a translator has messed up the edges of paragraph-level markup, * fix it to actually cover the entire paragraph that it is attached to * instead of just whatever range they put it on. @@ -423,11 +482,11 @@ final class StringBlock { + ": " + nativeGetSize(mNative)); } - private static final native int nativeCreate(byte[] data, + private static native int nativeCreate(byte[] data, int offset, int size); - private static final native int nativeGetSize(int obj); - private static final native String nativeGetString(int obj, int idx); - private static final native int[] nativeGetStyle(int obj, int idx); - private static final native void nativeDestroy(int obj); + private static native int nativeGetSize(int obj); + private static native String nativeGetString(int obj, int idx); + private static native int[] nativeGetStyle(int obj, int idx); + private static native void nativeDestroy(int obj); } diff --git a/core/java/android/content/res/TypedArray.java b/core/java/android/content/res/TypedArray.java index 2968fbb..27dddd4 100644 --- a/core/java/android/content/res/TypedArray.java +++ b/core/java/android/content/res/TypedArray.java @@ -687,7 +687,7 @@ public class TypedArray { * Give back a previously retrieved array, for later re-use. */ public void recycle() { - synchronized (mResources.mTmpValue) { + synchronized (mResources.mAccessLock) { TypedArray cached = mResources.mCachedStyledAttributes; if (cached == null || cached.mData.length < mData.length) { mXml = null; diff --git a/core/java/android/database/CursorToBulkCursorAdaptor.java b/core/java/android/database/CursorToBulkCursorAdaptor.java index 525be96..82a61d4 100644 --- a/core/java/android/database/CursorToBulkCursorAdaptor.java +++ b/core/java/android/database/CursorToBulkCursorAdaptor.java @@ -132,6 +132,11 @@ public final class CursorToBulkCursorAdaptor extends BulkCursorNative } } + /** + * Returns an object that contains sufficient metadata to reconstruct + * the cursor remotely. May throw if an error occurs when executing the query + * and obtaining the row count. + */ public BulkCursorDescriptor getBulkCursorDescriptor() { synchronized (mLock) { throwIfCursorIsClosed(); diff --git a/core/java/android/database/sqlite/SQLiteCursor.java b/core/java/android/database/sqlite/SQLiteCursor.java index b29897e..5a1a8e2 100644 --- a/core/java/android/database/sqlite/SQLiteCursor.java +++ b/core/java/android/database/sqlite/SQLiteCursor.java @@ -138,17 +138,26 @@ public class SQLiteCursor extends AbstractWindowedCursor { private void fillWindow(int requiredPos) { clearOrCreateWindow(getDatabase().getPath()); - if (mCount == NO_COUNT) { - int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, 0); - mCount = mQuery.fillWindow(mWindow, startPos, requiredPos, true); - mCursorWindowCapacity = mWindow.getNumRows(); - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "received count(*) from native_fill_window: " + mCount); + try { + if (mCount == NO_COUNT) { + int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, 0); + mCount = mQuery.fillWindow(mWindow, startPos, requiredPos, true); + mCursorWindowCapacity = mWindow.getNumRows(); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "received count(*) from native_fill_window: " + mCount); + } + } else { + int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, + mCursorWindowCapacity); + mQuery.fillWindow(mWindow, startPos, requiredPos, false); } - } else { - int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, - mCursorWindowCapacity); - mQuery.fillWindow(mWindow, startPos, requiredPos, false); + } catch (RuntimeException ex) { + // Close the cursor window if the query failed and therefore will + // not produce any results. This helps to avoid accidentally leaking + // the cursor window if the client does not correctly handle exceptions + // and fails to close the cursor. + closeWindow(); + throw ex; } } diff --git a/core/java/android/ddm/DdmHandleHello.java b/core/java/android/ddm/DdmHandleHello.java index e99fa92..842a482 100644 --- a/core/java/android/ddm/DdmHandleHello.java +++ b/core/java/android/ddm/DdmHandleHello.java @@ -36,6 +36,10 @@ public class DdmHandleHello extends ChunkHandler { private static DdmHandleHello mInstance = new DdmHandleHello(); + private static final String[] FRAMEWORK_FEATURES = new String[] { + "opengl-tracing", + "view-hierarchy", + }; /* singleton, do not instantiate */ private DdmHandleHello() {} @@ -149,21 +153,27 @@ public class DdmHandleHello extends ChunkHandler { private Chunk handleFEAT(Chunk request) { // TODO: query the VM to ensure that support for these features // is actually compiled in - final String[] features = Debug.getVmFeatureList(); + final String[] vmFeatures = Debug.getVmFeatureList(); if (false) Log.v("ddm-heap", "Got feature list request"); - int size = 4 + 4 * features.length; - for (int i = features.length-1; i >= 0; i--) - size += features[i].length() * 2; + int size = 4 + 4 * (vmFeatures.length + FRAMEWORK_FEATURES.length); + for (int i = vmFeatures.length-1; i >= 0; i--) + size += vmFeatures[i].length() * 2; + for (int i = FRAMEWORK_FEATURES.length-1; i>= 0; i--) + size += FRAMEWORK_FEATURES[i].length() * 2; ByteBuffer out = ByteBuffer.allocate(size); out.order(ChunkHandler.CHUNK_ORDER); - out.putInt(features.length); - for (int i = features.length-1; i >= 0; i--) { - out.putInt(features[i].length()); - putString(out, features[i]); + out.putInt(vmFeatures.length + FRAMEWORK_FEATURES.length); + for (int i = vmFeatures.length-1; i >= 0; i--) { + out.putInt(vmFeatures[i].length()); + putString(out, vmFeatures[i]); + } + for (int i = FRAMEWORK_FEATURES.length-1; i >= 0; i--) { + out.putInt(FRAMEWORK_FEATURES[i].length()); + putString(out, FRAMEWORK_FEATURES[i]); } return new Chunk(CHUNK_FEAT, out); diff --git a/core/java/android/ddm/DdmHandleViewDebug.java b/core/java/android/ddm/DdmHandleViewDebug.java new file mode 100644 index 0000000..ce83796 --- /dev/null +++ b/core/java/android/ddm/DdmHandleViewDebug.java @@ -0,0 +1,416 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.ddm; + +import android.opengl.GLUtils; +import android.util.Log; +import android.view.View; +import android.view.ViewDebug; +import android.view.ViewRootImpl; +import android.view.WindowManagerGlobal; + +import org.apache.harmony.dalvik.ddmc.Chunk; +import org.apache.harmony.dalvik.ddmc.ChunkHandler; +import org.apache.harmony.dalvik.ddmc.DdmServer; + +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.lang.reflect.Method; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; + +/** + * Handle various requests related to profiling / debugging of the view system. + * Support for these features are advertised via {@link DdmHandleHello}. + */ +public class DdmHandleViewDebug extends ChunkHandler { + /** Enable/Disable tracing of OpenGL calls. */ + public static final int CHUNK_VUGL = type("VUGL"); + + /** List {@link ViewRootImpl}'s of this process. */ + private static final int CHUNK_VULW = type("VULW"); + + /** Operation on view root, first parameter in packet should be one of VURT_* constants */ + private static final int CHUNK_VURT = type("VURT"); + + /** Dump view hierarchy. */ + private static final int VURT_DUMP_HIERARCHY = 1; + + /** Capture View Layers. */ + private static final int VURT_CAPTURE_LAYERS = 2; + + /** + * Generic View Operation, first parameter in the packet should be one of the + * VUOP_* constants below. + */ + private static final int CHUNK_VUOP = type("VUOP"); + + /** Capture View. */ + private static final int VUOP_CAPTURE_VIEW = 1; + + /** Obtain the Display List corresponding to the view. */ + private static final int VUOP_DUMP_DISPLAYLIST = 2; + + /** Profile a view. */ + private static final int VUOP_PROFILE_VIEW = 3; + + /** Invoke a method on the view. */ + private static final int VUOP_INVOKE_VIEW_METHOD = 4; + + /** Set layout parameter. */ + private static final int VUOP_SET_LAYOUT_PARAMETER = 5; + + /** Error code indicating operation specified in chunk is invalid. */ + private static final int ERR_INVALID_OP = -1; + + /** Error code indicating that the parameters are invalid. */ + private static final int ERR_INVALID_PARAM = -2; + + /** Error code indicating an exception while performing operation. */ + private static final int ERR_EXCEPTION = -3; + + private static final String TAG = "DdmViewDebug"; + + private static final DdmHandleViewDebug sInstance = new DdmHandleViewDebug(); + + /** singleton, do not instantiate. */ + private DdmHandleViewDebug() {} + + public static void register() { + DdmServer.registerHandler(CHUNK_VUGL, sInstance); + DdmServer.registerHandler(CHUNK_VULW, sInstance); + DdmServer.registerHandler(CHUNK_VURT, sInstance); + DdmServer.registerHandler(CHUNK_VUOP, sInstance); + } + + @Override + public void connected() { + } + + @Override + public void disconnected() { + } + + @Override + public Chunk handleChunk(Chunk request) { + int type = request.type; + + if (type == CHUNK_VUGL) { + return handleOpenGlTrace(request); + } else if (type == CHUNK_VULW) { + return listWindows(); + } + + ByteBuffer in = wrapChunk(request); + int op = in.getInt(); + + View rootView = getRootView(in); + if (rootView == null) { + return createFailChunk(ERR_INVALID_PARAM, "Invalid View Root"); + } + + if (type == CHUNK_VURT) { + if (op == VURT_DUMP_HIERARCHY) + return dumpHierarchy(rootView, in); + else if (op == VURT_CAPTURE_LAYERS) + return captureLayers(rootView); + else + return createFailChunk(ERR_INVALID_OP, "Unknown view root operation: " + op); + } + + final View targetView = getTargetView(rootView, in); + if (targetView == null) { + return createFailChunk(ERR_INVALID_PARAM, "Invalid target view"); + } + + if (type == CHUNK_VUOP) { + switch (op) { + case VUOP_CAPTURE_VIEW: + return captureView(rootView, targetView); + case VUOP_DUMP_DISPLAYLIST: + return dumpDisplayLists(rootView, targetView); + case VUOP_PROFILE_VIEW: + return profileView(rootView, targetView); + case VUOP_INVOKE_VIEW_METHOD: + return invokeViewMethod(rootView, targetView, in); + case VUOP_SET_LAYOUT_PARAMETER: + return setLayoutParameter(rootView, targetView, in); + default: + return createFailChunk(ERR_INVALID_OP, "Unknown view operation: " + op); + } + } else { + throw new RuntimeException("Unknown packet " + ChunkHandler.name(type)); + } + } + + private Chunk handleOpenGlTrace(Chunk request) { + ByteBuffer in = wrapChunk(request); + GLUtils.setTracingLevel(in.getInt()); + return null; // empty response + } + + /** Returns the list of windows owned by this client. */ + private Chunk listWindows() { + String[] windowNames = WindowManagerGlobal.getInstance().getViewRootNames(); + + int responseLength = 4; // # of windows + for (String name : windowNames) { + responseLength += 4; // length of next window name + responseLength += name.length() * 2; // window name + } + + ByteBuffer out = ByteBuffer.allocate(responseLength); + out.order(ChunkHandler.CHUNK_ORDER); + + out.putInt(windowNames.length); + for (String name : windowNames) { + out.putInt(name.length()); + putString(out, name); + } + + return new Chunk(CHUNK_VULW, out); + } + + private View getRootView(ByteBuffer in) { + try { + int viewRootNameLength = in.getInt(); + String viewRootName = getString(in, viewRootNameLength); + return WindowManagerGlobal.getInstance().getRootView(viewRootName); + } catch (BufferUnderflowException e) { + return null; + } + } + + private View getTargetView(View root, ByteBuffer in) { + int viewLength; + String viewName; + + try { + viewLength = in.getInt(); + viewName = getString(in, viewLength); + } catch (BufferUnderflowException e) { + return null; + } + + return ViewDebug.findView(root, viewName); + } + + /** + * Returns the view hierarchy and/or view properties starting at the provided view. + * Based on the input options, the return data may include: + * - just the view hierarchy + * - view hierarchy & the properties for each of the views + * - just the view properties for a specific view. + * TODO: Currently this only returns views starting at the root, need to fix so that + * it can return properties of any view. + */ + private Chunk dumpHierarchy(View rootView, ByteBuffer in) { + boolean skipChildren = in.getInt() > 0; + boolean includeProperties = in.getInt() > 0; + + ByteArrayOutputStream b = new ByteArrayOutputStream(1024); + try { + ViewDebug.dump(rootView, skipChildren, includeProperties, b); + } catch (IOException e) { + return createFailChunk(1, "Unexpected error while obtaining view hierarchy: " + + e.getMessage()); + } + + byte[] data = b.toByteArray(); + return new Chunk(CHUNK_VURT, data, 0, data.length); + } + + /** Returns a buffer with region details & bitmap of every single view. */ + private Chunk captureLayers(View rootView) { + ByteArrayOutputStream b = new ByteArrayOutputStream(1024); + DataOutputStream dos = new DataOutputStream(b); + try { + ViewDebug.captureLayers(rootView, dos); + } catch (IOException e) { + return createFailChunk(1, "Unexpected error while obtaining view hierarchy: " + + e.getMessage()); + } finally { + try { + dos.close(); + } catch (IOException e) { + // ignore + } + } + + byte[] data = b.toByteArray(); + return new Chunk(CHUNK_VURT, data, 0, data.length); + } + + private Chunk captureView(View rootView, View targetView) { + ByteArrayOutputStream b = new ByteArrayOutputStream(1024); + try { + ViewDebug.capture(rootView, b, targetView); + } catch (IOException e) { + return createFailChunk(1, "Unexpected error while capturing view: " + + e.getMessage()); + } + + byte[] data = b.toByteArray(); + return new Chunk(CHUNK_VUOP, data, 0, data.length); + } + + /** Returns the display lists corresponding to the provided view. */ + private Chunk dumpDisplayLists(final View rootView, final View targetView) { + rootView.post(new Runnable() { + @Override + public void run() { + ViewDebug.outputDisplayList(rootView, targetView); + } + }); + return null; + } + + /** + * Invokes provided method on the view. + * The method name and its arguments are passed in as inputs via the byte buffer. + * The buffer contains:<ol> + * <li> len(method name) </li> + * <li> method name </li> + * <li> # of args </li> + * <li> arguments: Each argument comprises of a type specifier followed by the actual argument. + * The type specifier is a single character as used in JNI: + * (Z - boolean, B - byte, C - char, S - short, I - int, J - long, + * F - float, D - double). <p> + * The type specifier is followed by the actual value of argument. + * Booleans are encoded via bytes with 0 indicating false.</li> + * </ol> + * Methods that take no arguments need only specify the method name. + */ + private Chunk invokeViewMethod(final View rootView, final View targetView, ByteBuffer in) { + int l = in.getInt(); + String methodName = getString(in, l); + + Class<?>[] argTypes; + Object[] args; + if (!in.hasRemaining()) { + argTypes = new Class<?>[0]; + args = new Object[0]; + } else { + int nArgs = in.getInt(); + + argTypes = new Class<?>[nArgs]; + args = new Object[nArgs]; + + for (int i = 0; i < nArgs; i++) { + char c = in.getChar(); + switch (c) { + case 'Z': + argTypes[i] = boolean.class; + args[i] = in.get() == 0 ? false : true; + break; + case 'B': + argTypes[i] = byte.class; + args[i] = in.get(); + break; + case 'C': + argTypes[i] = char.class; + args[i] = in.getChar(); + break; + case 'S': + argTypes[i] = short.class; + args[i] = in.getShort(); + break; + case 'I': + argTypes[i] = int.class; + args[i] = in.getInt(); + break; + case 'J': + argTypes[i] = long.class; + args[i] = in.getLong(); + break; + case 'F': + argTypes[i] = float.class; + args[i] = in.getFloat(); + break; + case 'D': + argTypes[i] = double.class; + args[i] = in.getDouble(); + break; + default: + Log.e(TAG, "arg " + i + ", unrecognized type: " + c); + return createFailChunk(ERR_INVALID_PARAM, + "Unsupported parameter type (" + c + ") to invoke view method."); + } + } + } + + Method method = null; + try { + method = targetView.getClass().getMethod(methodName, argTypes); + } catch (NoSuchMethodException e) { + Log.e(TAG, "No such method: " + e.getMessage()); + return createFailChunk(ERR_INVALID_PARAM, + "No such method: " + e.getMessage()); + } + + try { + ViewDebug.invokeViewMethod(targetView, method, args); + } catch (Exception e) { + Log.e(TAG, "Exception while invoking method: " + e.getCause().getMessage()); + String msg = e.getCause().getMessage(); + if (msg == null) { + msg = e.getCause().toString(); + } + return createFailChunk(ERR_EXCEPTION, msg); + } + + return null; + } + + private Chunk setLayoutParameter(final View rootView, final View targetView, ByteBuffer in) { + int l = in.getInt(); + String param = getString(in, l); + int value = in.getInt(); + try { + ViewDebug.setLayoutParameter(targetView, param, value); + } catch (Exception e) { + Log.e(TAG, "Exception setting layout parameter: " + e); + return createFailChunk(ERR_EXCEPTION, "Error accessing field " + + param + ":" + e.getMessage()); + } + + return null; + } + + /** Profiles provided view. */ + private Chunk profileView(View rootView, final View targetView) { + ByteArrayOutputStream b = new ByteArrayOutputStream(32 * 1024); + BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(b), 32 * 1024); + try { + ViewDebug.profileViewAndChildren(targetView, bw); + } catch (IOException e) { + return createFailChunk(1, "Unexpected error while profiling view: " + e.getMessage()); + } finally { + try { + bw.close(); + } catch (IOException e) { + // ignore + } + } + + byte[] data = b.toByteArray(); + return new Chunk(CHUNK_VUOP, data, 0, data.length); + } +} diff --git a/core/java/android/ddm/DdmRegister.java b/core/java/android/ddm/DdmRegister.java index ecd450d..e0faa51 100644 --- a/core/java/android/ddm/DdmRegister.java +++ b/core/java/android/ddm/DdmRegister.java @@ -51,6 +51,7 @@ public class DdmRegister { DdmHandleNativeHeap.register(); DdmHandleProfiling.register(); DdmHandleExit.register(); + DdmHandleViewDebug.register(); DdmServer.registrationComplete(); } diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java index 483e9de..4e51080 100644 --- a/core/java/android/hardware/Camera.java +++ b/core/java/android/hardware/Camera.java @@ -16,6 +16,7 @@ package android.hardware; +import android.app.ActivityThread; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.content.Context; @@ -337,7 +338,9 @@ public class Camera { mEventHandler = null; } - native_setup(new WeakReference<Camera>(this), cameraId); + String packageName = ActivityThread.currentPackageName(); + + native_setup(new WeakReference<Camera>(this), cameraId, packageName); } /** @@ -350,7 +353,9 @@ public class Camera { release(); } - private native final void native_setup(Object camera_this, int cameraId); + private native final void native_setup(Object camera_this, int cameraId, + String packageName); + private native final void native_release(); diff --git a/core/java/android/hardware/Sensor.java b/core/java/android/hardware/Sensor.java index e0c9d2c..41384d2 100644 --- a/core/java/android/hardware/Sensor.java +++ b/core/java/android/hardware/Sensor.java @@ -194,7 +194,8 @@ public final class Sensor { return mMinDelay; } - int getHandle() { + /** @hide */ + public int getHandle() { return mHandle; } diff --git a/core/java/android/hardware/SensorManager.java b/core/java/android/hardware/SensorManager.java index b8ad818..c0d2fae 100644 --- a/core/java/android/hardware/SensorManager.java +++ b/core/java/android/hardware/SensorManager.java @@ -1314,56 +1314,4 @@ public abstract class SensorManager { return mLegacySensorManager; } } - - /** - * Sensor event pool implementation. - * @hide - */ - protected static final class SensorEventPool { - private final int mPoolSize; - private final SensorEvent mPool[]; - private int mNumItemsInPool; - - private SensorEvent createSensorEvent() { - // maximal size for all legacy events is 3 - return new SensorEvent(3); - } - - SensorEventPool(int poolSize) { - mPoolSize = poolSize; - mNumItemsInPool = poolSize; - mPool = new SensorEvent[poolSize]; - } - - SensorEvent getFromPool() { - SensorEvent t = null; - synchronized (this) { - if (mNumItemsInPool > 0) { - // remove the "top" item from the pool - final int index = mPoolSize - mNumItemsInPool; - t = mPool[index]; - mPool[index] = null; - mNumItemsInPool--; - } - } - if (t == null) { - // the pool was empty or this item was removed from the pool for - // the first time. In any case, we need to create a new item. - t = createSensorEvent(); - } - return t; - } - - void returnToPool(SensorEvent t) { - synchronized (this) { - // is there space left in the pool? - if (mNumItemsInPool < mPoolSize) { - // if so, return the item to the pool - mNumItemsInPool++; - final int index = mPoolSize - mNumItemsInPool; - mPool[index] = t; - } - } - } - } } diff --git a/core/java/android/hardware/SerialPort.java b/core/java/android/hardware/SerialPort.java index 5ef122b..f50cdef 100644 --- a/core/java/android/hardware/SerialPort.java +++ b/core/java/android/hardware/SerialPort.java @@ -82,7 +82,9 @@ public class SerialPort { } /** - * Reads data into the provided buffer + * Reads data into the provided buffer. + * Note that the value returned by {@link java.nio.Buffer#position()} on this buffer is + * unchanged after a call to this method. * * @param buffer to read into * @return number of bytes read @@ -98,7 +100,9 @@ public class SerialPort { } /** - * Writes data from provided buffer + * Writes data from provided buffer. + * Note that the value returned by {@link java.nio.Buffer#position()} on this buffer is + * unchanged after a call to this method. * * @param buffer to write * @param length number of bytes to write diff --git a/core/java/android/hardware/SystemSensorManager.java b/core/java/android/hardware/SystemSensorManager.java index 7375e7d..9591631 100644 --- a/core/java/android/hardware/SystemSensorManager.java +++ b/core/java/android/hardware/SystemSensorManager.java @@ -16,18 +16,19 @@ package android.hardware; -import android.os.Looper; -import android.os.Process; +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; + +import dalvik.system.CloseGuard; + import android.os.Handler; -import android.os.Message; -import android.util.Log; +import android.os.Looper; +import android.os.MessageQueue; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.SparseIntArray; -import java.util.ArrayList; -import java.util.List; - /** * Sensor manager implementation that communicates with the built-in * system sensors. @@ -35,236 +36,43 @@ import java.util.List; * @hide */ public class SystemSensorManager extends SensorManager { - private static final int SENSOR_DISABLE = -1; + private static native void nativeClassInit(); + private static native int nativeGetNextSensor(Sensor sensor, int next); + private static boolean sSensorModuleInitialized = false; - private static ArrayList<Sensor> sFullSensorsList = new ArrayList<Sensor>(); - /* The thread and the sensor list are global to the process - * but the actual thread is spawned on demand */ - private static SensorThread sSensorThread; - private static int sQueue; + private static final Object sSensorModuleLock = new Object(); + private static final ArrayList<Sensor> sFullSensorsList = new ArrayList<Sensor>(); + private static final SparseArray<Sensor> sHandleToSensor = new SparseArray<Sensor>(); - // Used within this module from outside SensorManager, don't make private - static SparseArray<Sensor> sHandleToSensor = new SparseArray<Sensor>(); - static final ArrayList<ListenerDelegate> sListeners = - new ArrayList<ListenerDelegate>(); + // Listener list + private final ArrayList<SensorEventListenerSensorPair> mListenerDelegates = new ArrayList<SensorEventListenerSensorPair>(); // Common pool of sensor events. - static SensorEventPool sPool; + private static SensorEventPool sPool; // Looper associated with the context in which this instance was created. - final Looper mMainLooper; - - /*-----------------------------------------------------------------------*/ - - static private class SensorThread { - - Thread mThread; - boolean mSensorsReady; - - SensorThread() { - } - - @Override - protected void finalize() { - } - - // must be called with sListeners lock - boolean startLocked() { - try { - if (mThread == null) { - mSensorsReady = false; - SensorThreadRunnable runnable = new SensorThreadRunnable(); - Thread thread = new Thread(runnable, SensorThread.class.getName()); - thread.start(); - synchronized (runnable) { - while (mSensorsReady == false) { - runnable.wait(); - } - } - mThread = thread; - } - } catch (InterruptedException e) { - } - return mThread == null ? false : true; - } - - private class SensorThreadRunnable implements Runnable { - SensorThreadRunnable() { - } + private final Looper mMainLooper; - private boolean open() { - // NOTE: this cannot synchronize on sListeners, since - // it's held in the main thread at least until we - // return from here. - sQueue = sensors_create_queue(); - return true; - } + // maps a SensorEventListener to a SensorEventQueue + private final Hashtable<SensorEventListener, SensorEventQueue> mSensorEventQueueMap; - public void run() { - //Log.d(TAG, "entering main sensor thread"); - final float[] values = new float[3]; - final int[] status = new int[1]; - final long timestamp[] = new long[1]; - Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_DISPLAY); - - if (!open()) { - return; - } - - synchronized (this) { - // we've open the driver, we're ready to open the sensors - mSensorsReady = true; - this.notify(); - } - - while (true) { - // wait for an event - final int sensor = sensors_data_poll(sQueue, values, status, timestamp); - - int accuracy = status[0]; - synchronized (sListeners) { - if (sensor == -1 || sListeners.isEmpty()) { - // we lost the connection to the event stream. this happens - // when the last listener is removed or if there is an error - if (sensor == -1 && !sListeners.isEmpty()) { - // log a warning in case of abnormal termination - Log.e(TAG, "_sensors_data_poll() failed, we bail out: sensors=" + sensor); - } - // we have no more listeners or polling failed, terminate the thread - sensors_destroy_queue(sQueue); - sQueue = 0; - mThread = null; - break; - } - final Sensor sensorObject = sHandleToSensor.get(sensor); - if (sensorObject != null) { - // report the sensor event to all listeners that - // care about it. - final int size = sListeners.size(); - for (int i=0 ; i<size ; i++) { - ListenerDelegate listener = sListeners.get(i); - if (listener.hasSensor(sensorObject)) { - // this is asynchronous (okay to call - // with sListeners lock held). - listener.onSensorChangedLocked(sensorObject, - values, timestamp, accuracy); - } - } - } - } - } - //Log.d(TAG, "exiting main sensor thread"); - } - } - } - - /*-----------------------------------------------------------------------*/ - - private class ListenerDelegate { - private final SensorEventListener mSensorEventListener; - private final ArrayList<Sensor> mSensorList = new ArrayList<Sensor>(); - private final Handler mHandler; - public SparseBooleanArray mSensors = new SparseBooleanArray(); - public SparseBooleanArray mFirstEvent = new SparseBooleanArray(); - public SparseIntArray mSensorAccuracies = new SparseIntArray(); - - ListenerDelegate(SensorEventListener listener, Sensor sensor, Handler handler) { - mSensorEventListener = listener; - Looper looper = (handler != null) ? handler.getLooper() : mMainLooper; - // currently we create one Handler instance per listener, but we could - // have one per looper (we'd need to pass the ListenerDelegate - // instance to handleMessage and keep track of them separately). - mHandler = new Handler(looper) { - @Override - public void handleMessage(Message msg) { - final SensorEvent t = (SensorEvent)msg.obj; - final int handle = t.sensor.getHandle(); - - switch (t.sensor.getType()) { - // Only report accuracy for sensors that support it. - case Sensor.TYPE_MAGNETIC_FIELD: - case Sensor.TYPE_ORIENTATION: - // call onAccuracyChanged() only if the value changes - final int accuracy = mSensorAccuracies.get(handle); - if ((t.accuracy >= 0) && (accuracy != t.accuracy)) { - mSensorAccuracies.put(handle, t.accuracy); - mSensorEventListener.onAccuracyChanged(t.sensor, t.accuracy); - } - break; - default: - // For other sensors, just report the accuracy once - if (mFirstEvent.get(handle) == false) { - mFirstEvent.put(handle, true); - mSensorEventListener.onAccuracyChanged( - t.sensor, SENSOR_STATUS_ACCURACY_HIGH); - } - break; - } - - mSensorEventListener.onSensorChanged(t); - sPool.returnToPool(t); - } - }; - addSensor(sensor); - } - - Object getListener() { - return mSensorEventListener; - } - - void addSensor(Sensor sensor) { - mSensors.put(sensor.getHandle(), true); - mSensorList.add(sensor); - } - int removeSensor(Sensor sensor) { - mSensors.delete(sensor.getHandle()); - mSensorList.remove(sensor); - return mSensors.size(); - } - boolean hasSensor(Sensor sensor) { - return mSensors.get(sensor.getHandle()); - } - List<Sensor> getSensors() { - return mSensorList; - } - - void onSensorChangedLocked(Sensor sensor, float[] values, long[] timestamp, int accuracy) { - SensorEvent t = sPool.getFromPool(); - final float[] v = t.values; - v[0] = values[0]; - v[1] = values[1]; - v[2] = values[2]; - t.timestamp = timestamp[0]; - t.accuracy = accuracy; - t.sensor = sensor; - Message msg = Message.obtain(); - msg.what = 0; - msg.obj = t; - msg.setAsynchronous(true); - mHandler.sendMessage(msg); - } - } - - /** - * {@hide} - */ + /** {@hide} */ public SystemSensorManager(Looper mainLooper) { mMainLooper = mainLooper; + mSensorEventQueueMap = new Hashtable<SensorEventListener, SensorEventQueue>(); - synchronized(sListeners) { + synchronized(sSensorModuleLock) { if (!sSensorModuleInitialized) { sSensorModuleInitialized = true; nativeClassInit(); // initialize the sensor list - sensors_module_init(); final ArrayList<Sensor> fullList = sFullSensorsList; int i = 0; do { Sensor sensor = new Sensor(); - i = sensors_module_get_next_sensor(sensor, i); - + i = nativeGetNextSensor(sensor, i); if (i>=0) { //Log.d(TAG, "found sensor: " + sensor.getName() + // ", handle=" + sensor.getHandle()); @@ -274,126 +82,304 @@ public class SystemSensorManager extends SensorManager { } while (i>0); sPool = new SensorEventPool( sFullSensorsList.size()*2 ); - sSensorThread = new SensorThread(); } } } + /** @hide */ @Override protected List<Sensor> getFullSensorList() { return sFullSensorsList; } - private boolean enableSensorLocked(Sensor sensor, int delay) { - boolean result = false; - for (ListenerDelegate i : sListeners) { - if (i.hasSensor(sensor)) { - String name = sensor.getName(); - int handle = sensor.getHandle(); - result = sensors_enable_sensor(sQueue, name, handle, delay); - break; + + /** @hide */ + @Override + protected boolean registerListenerImpl(SensorEventListener listener, Sensor sensor, + int delay, Handler handler) + { + // Invariants to preserve: + // - one Looper per SensorEventListener + // - one Looper per SensorEventQueue + // We map SensorEventListeners to a SensorEventQueue, which holds the looper + + if (sensor == null) throw new NullPointerException("sensor cannot be null"); + + boolean result; + synchronized (mSensorEventQueueMap) { + // check if we already have this SensorEventListener, Sensor pair + // registered -- if so, we ignore the register. This is not ideal + // but this is what the implementation has always been doing. + for (SensorEventListenerSensorPair l : mListenerDelegates) { + if (l.isSameListenerSensorPair(listener, sensor)) { + // already added, just return silently. + return true; + } } - } - return result; - } - private boolean disableSensorLocked(Sensor sensor) { - for (ListenerDelegate i : sListeners) { - if (i.hasSensor(sensor)) { - // not an error, it's just that this sensor is still in use - return true; + // now find the SensorEventQueue associated to this listener + SensorEventQueue queue = mSensorEventQueueMap.get(listener); + if (queue != null) { + result = queue.addSensor(sensor, delay); + if (result) { + // create a new ListenerDelegate for this pair + mListenerDelegates.add(new SensorEventListenerSensorPair(listener, sensor)); + } + } else { + Looper looper = (handler != null) ? handler.getLooper() : mMainLooper; + queue = new SensorEventQueue(listener, looper.getQueue()); + result = queue.addSensor(sensor, delay); + if (result) { + // create a new ListenerDelegate for this pair + mListenerDelegates.add(new SensorEventListenerSensorPair(listener, sensor)); + mSensorEventQueueMap.put(listener, queue); + } else { + queue.dispose(); + } } } - String name = sensor.getName(); - int handle = sensor.getHandle(); - return sensors_enable_sensor(sQueue, name, handle, SENSOR_DISABLE); + return result; } /** @hide */ @Override - protected boolean registerListenerImpl(SensorEventListener listener, Sensor sensor, - int delay, Handler handler) { - boolean result = true; - synchronized (sListeners) { - // look for this listener in our list - ListenerDelegate l = null; - for (ListenerDelegate i : sListeners) { - if (i.getListener() == listener) { - l = i; - break; + protected void unregisterListenerImpl(SensorEventListener listener, Sensor sensor) { + synchronized (mSensorEventQueueMap) { + + // remove this listener/sensor from our list + final ArrayList<SensorEventListenerSensorPair> copy = + new ArrayList<SensorEventListenerSensorPair>(mListenerDelegates); + int lastIndex = copy.size()-1; + for (int i=lastIndex ; i>= 0 ; i--) { + if (copy.get(i).isSameListenerSensorPair(listener, sensor)) { + mListenerDelegates.remove(i); } } - // if we don't find it, add it to the list - if (l == null) { - l = new ListenerDelegate(listener, sensor, handler); - sListeners.add(l); - // if the list is not empty, start our main thread - if (!sListeners.isEmpty()) { - if (sSensorThread.startLocked()) { - if (!enableSensorLocked(sensor, delay)) { - // oops. there was an error - sListeners.remove(l); - result = false; - } - } else { - // there was an error, remove the listener - sListeners.remove(l); - result = false; - } + // find the SensorEventQueue associated to this SensorEventListener + SensorEventQueue queue = mSensorEventQueueMap.get(listener); + if (queue != null) { + if (sensor != null) { + queue.removeSensor(sensor); } else { - // weird, we couldn't add the listener - result = false; + queue.removeAllSensors(); } - } else if (!l.hasSensor(sensor)) { - l.addSensor(sensor); - if (!enableSensorLocked(sensor, delay)) { - // oops. there was an error - l.removeSensor(sensor); - result = false; + if (!queue.hasSensors()) { + mSensorEventQueueMap.remove(listener); + queue.dispose(); } } } + } - return result; + + /* + * ListenerDelegate is essentially a SensorEventListener, Sensor pair + * and is associated with a single SensorEventQueue. + */ + private static final class SensorEventListenerSensorPair { + private final SensorEventListener mSensorEventListener; + private final Sensor mSensor; + public SensorEventListenerSensorPair(SensorEventListener listener, Sensor sensor) { + mSensorEventListener = listener; + mSensor = sensor; + } + public boolean isSameListenerSensorPair(SensorEventListener listener, Sensor sensor) { + // if sensor is null, we match only on the listener + if (sensor != null) { + return (listener == mSensorEventListener) && + (sensor.getHandle() == mSensor.getHandle()); + } else { + return (listener == mSensorEventListener); + } + } } - /** @hide */ - @Override - protected void unregisterListenerImpl(SensorEventListener listener, Sensor sensor) { - synchronized (sListeners) { - final int size = sListeners.size(); - for (int i=0 ; i<size ; i++) { - ListenerDelegate l = sListeners.get(i); - if (l.getListener() == listener) { - if (sensor == null) { - sListeners.remove(i); - // disable all sensors for this listener - for (Sensor s : l.getSensors()) { - disableSensorLocked(s); - } - // Check if the ListenerDelegate has the sensor it is trying to unregister. - } else if (l.hasSensor(sensor) && l.removeSensor(sensor) == 0) { - // if we have no more sensors enabled on this listener, - // take it off the list. - sListeners.remove(i); - disableSensorLocked(sensor); + /* + * SensorEventQueue is the communication channel with the sensor service, + * there is a one-to-one mapping between SensorEventQueue and + * SensorEventListener. + */ + private static final class SensorEventQueue { + private static native int nativeInitSensorEventQueue(SensorEventQueue eventQ, MessageQueue msgQ, float[] scratch); + private static native int nativeEnableSensor(int eventQ, int handle, int us); + private static native int nativeDisableSensor(int eventQ, int handle); + private static native void nativeDestroySensorEventQueue(int eventQ); + private int nSensorEventQueue; + private final SensorEventListener mListener; + private final SparseBooleanArray mActiveSensors = new SparseBooleanArray(); + private final SparseIntArray mSensorAccuracies = new SparseIntArray(); + private final SparseBooleanArray mFirstEvent = new SparseBooleanArray(); + private final CloseGuard mCloseGuard = CloseGuard.get(); + private final float[] mScratch = new float[16]; + + public SensorEventQueue(SensorEventListener listener, MessageQueue msgQ) { + nSensorEventQueue = nativeInitSensorEventQueue(this, msgQ, mScratch); + mListener = listener; + mCloseGuard.open("dispose"); + } + public void dispose() { + dispose(false); + } + + public boolean addSensor(Sensor sensor, int delay) { + if (enableSensor(sensor, delay) == 0) { + mActiveSensors.put(sensor.getHandle(), true); + return true; + } + return false; + } + + public void removeAllSensors() { + for (int i=0 ; i<mActiveSensors.size(); i++) { + if (mActiveSensors.valueAt(i) == true) { + int handle = mActiveSensors.keyAt(i); + Sensor sensor = sHandleToSensor.get(handle); + if (sensor != null) { + disableSensor(sensor); + mActiveSensors.put(handle, false); + } else { + // it should never happen -- just ignore. } - break; } } } + + public void removeSensor(Sensor sensor) { + final int handle = sensor.getHandle(); + if (mActiveSensors.get(handle)) { + disableSensor(sensor); + mActiveSensors.put(sensor.getHandle(), false); + } + } + + public boolean hasSensors() { + // no more sensors are set + return mActiveSensors.indexOfValue(true) >= 0; + } + + @Override + protected void finalize() throws Throwable { + try { + dispose(true); + } finally { + super.finalize(); + } + } + + private void dispose(boolean finalized) { + if (mCloseGuard != null) { + if (finalized) { + mCloseGuard.warnIfOpen(); + } + mCloseGuard.close(); + } + if (nSensorEventQueue != 0) { + nativeDestroySensorEventQueue(nSensorEventQueue); + nSensorEventQueue = 0; + } + } + + private int enableSensor(Sensor sensor, int us) { + if (nSensorEventQueue == 0) throw new NullPointerException(); + if (sensor == null) throw new NullPointerException(); + return nativeEnableSensor(nSensorEventQueue, sensor.getHandle(), us); + } + private int disableSensor(Sensor sensor) { + if (nSensorEventQueue == 0) throw new NullPointerException(); + if (sensor == null) throw new NullPointerException(); + return nativeDisableSensor(nSensorEventQueue, sensor.getHandle()); + } + + // Called from native code. + @SuppressWarnings("unused") + private void dispatchSensorEvent(int handle, float[] values, int inAccuracy, long timestamp) { + // this is always called on the same thread. + final SensorEvent t = sPool.getFromPool(); + try { + final Sensor sensor = sHandleToSensor.get(handle); + final SensorEventListener listener = mListener; + // FIXME: handle more than 3 values + System.arraycopy(values, 0, t.values, 0, 3); + t.timestamp = timestamp; + t.accuracy = inAccuracy; + t.sensor = sensor; + switch (t.sensor.getType()) { + // Only report accuracy for sensors that support it. + case Sensor.TYPE_MAGNETIC_FIELD: + case Sensor.TYPE_ORIENTATION: + // call onAccuracyChanged() only if the value changes + final int accuracy = mSensorAccuracies.get(handle); + if ((t.accuracy >= 0) && (accuracy != t.accuracy)) { + mSensorAccuracies.put(handle, t.accuracy); + listener.onAccuracyChanged(t.sensor, t.accuracy); + } + break; + default: + // For other sensors, just report the accuracy once + if (mFirstEvent.get(handle) == false) { + mFirstEvent.put(handle, true); + listener.onAccuracyChanged( + t.sensor, SENSOR_STATUS_ACCURACY_HIGH); + } + break; + } + listener.onSensorChanged(t); + } finally { + sPool.returnToPool(t); + } + } } - private static native void nativeClassInit(); + /* + * A dumb pool of SensorEvent + */ + private static final class SensorEventPool { + private final int mPoolSize; + private final SensorEvent mPool[]; + private int mNumItemsInPool; + + private SensorEvent createSensorEvent() { + // maximal size for all legacy events is 3 + return new SensorEvent(3); + } - private static native int sensors_module_init(); - private static native int sensors_module_get_next_sensor(Sensor sensor, int next); + SensorEventPool(int poolSize) { + mPoolSize = poolSize; + mNumItemsInPool = poolSize; + mPool = new SensorEvent[poolSize]; + } - // Used within this module from outside SensorManager, don't make private - static native int sensors_create_queue(); - static native void sensors_destroy_queue(int queue); - static native boolean sensors_enable_sensor(int queue, String name, int sensor, int enable); - static native int sensors_data_poll(int queue, float[] values, int[] status, long[] timestamp); + SensorEvent getFromPool() { + SensorEvent t = null; + synchronized (this) { + if (mNumItemsInPool > 0) { + // remove the "top" item from the pool + final int index = mPoolSize - mNumItemsInPool; + t = mPool[index]; + mPool[index] = null; + mNumItemsInPool--; + } + } + if (t == null) { + // the pool was empty or this item was removed from the pool for + // the first time. In any case, we need to create a new item. + t = createSensorEvent(); + } + return t; + } + + void returnToPool(SensorEvent t) { + synchronized (this) { + // is there space left in the pool? + if (mNumItemsInPool < mPoolSize) { + // if so, return the item to the pool + mNumItemsInPool++; + final int index = mPoolSize - mNumItemsInPool; + mPool[index] = t; + } + } + } + } } diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index 262d87d..761faaf 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -212,8 +212,10 @@ public final class InputManager { } catch (RemoteException ex) { throw new RuntimeException("Could not get input device information.", ex); } + if (inputDevice != null) { + mInputDevices.setValueAt(index, inputDevice); + } } - mInputDevices.setValueAt(index, inputDevice); return inputDevice; } } @@ -241,6 +243,8 @@ public final class InputManager { inputDevice = mIm.getInputDevice(id); } catch (RemoteException ex) { // Ignore the problem for the purposes of this method. + } + if (inputDevice == null) { continue; } mInputDevices.setValueAt(i, inputDevice); @@ -809,6 +813,22 @@ public final class InputManager { } } + /** + * @hide + */ + @Override + public void vibrate(int owningUid, String owningPackage, long milliseconds) { + vibrate(milliseconds); + } + + /** + * @hide + */ + @Override + public void vibrate(int owningUid, String owningPackage, long[] pattern, int repeat) { + vibrate(pattern, repeat); + } + @Override public void cancel() { try { diff --git a/core/java/android/hardware/usb/IUsbManager.aidl b/core/java/android/hardware/usb/IUsbManager.aidl index 8286686..9bc967f 100644 --- a/core/java/android/hardware/usb/IUsbManager.aidl +++ b/core/java/android/hardware/usb/IUsbManager.aidl @@ -95,4 +95,7 @@ interface IUsbManager /* Deny USB debugging from the attached host */ void denyUsbDebugging(); + + /* Clear public keys installed for secure USB debugging */ + void clearUsbDebuggingKeys(); } diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 6f1cc94..99624cc 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -1955,7 +1955,7 @@ public class InputMethodService extends AbstractInputMethodService { ic.sendKeyEvent(new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, keyEventCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_SOFT_KEYBOARD|KeyEvent.FLAG_KEEP_TOUCH_MODE)); - ic.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime, + ic.sendKeyEvent(new KeyEvent(eventTime, SystemClock.uptimeMillis(), KeyEvent.ACTION_UP, keyEventCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_SOFT_KEYBOARD|KeyEvent.FLAG_KEEP_TOUCH_MODE)); } diff --git a/core/java/android/net/CaptivePortalTracker.java b/core/java/android/net/CaptivePortalTracker.java index ce71e6b..21995c0 100644 --- a/core/java/android/net/CaptivePortalTracker.java +++ b/core/java/android/net/CaptivePortalTracker.java @@ -25,14 +25,15 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Resources; +import android.database.ContentObserver; import android.net.ConnectivityManager; import android.net.IConnectivityManager; +import android.os.Handler; import android.os.UserHandle; import android.os.Message; import android.os.RemoteException; import android.provider.Settings; import android.telephony.TelephonyManager; -import android.util.Log; import com.android.internal.util.State; import com.android.internal.util.StateMachine; @@ -81,15 +82,21 @@ public class CaptivePortalTracker extends StateMachine { private State mActiveNetworkState = new ActiveNetworkState(); private State mDelayedCaptiveCheckState = new DelayedCaptiveCheckState(); + private static final String SETUP_WIZARD_PACKAGE = "com.google.android.setupwizard"; + private boolean mDeviceProvisioned = false; + private ProvisioningObserver mProvisioningObserver; + private CaptivePortalTracker(Context context, IConnectivityManager cs) { super(TAG); mContext = context; mConnService = cs; mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + mProvisioningObserver = new ProvisioningObserver(); IntentFilter filter = new IntentFilter(); filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); + filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE); mContext.registerReceiver(mReceiver, filter); mServer = Settings.Global.getString(mContext.getContentResolver(), @@ -106,11 +113,31 @@ public class CaptivePortalTracker extends StateMachine { setInitialState(mNoActiveNetworkState); } + private class ProvisioningObserver extends ContentObserver { + ProvisioningObserver() { + super(new Handler()); + mContext.getContentResolver().registerContentObserver(Settings.Global.getUriFor( + Settings.Global.DEVICE_PROVISIONED), false, this); + onChange(false); // load initial value + } + + @Override + public void onChange(boolean selfChange) { + mDeviceProvisioned = Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.DEVICE_PROVISIONED, 0) != 0; + } + } + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); - if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) { + // Normally, we respond to CONNECTIVITY_ACTION, allowing time for the change in + // connectivity to stabilize, but if the device is not yet provisioned, respond + // immediately to speed up transit through the setup wizard. + if ((mDeviceProvisioned && action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) + || (!mDeviceProvisioned + && action.equals(ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE))) { NetworkInfo info = intent.getParcelableExtra( ConnectivityManager.EXTRA_NETWORK_INFO); sendMessage(obtainMessage(CMD_CONNECTIVITY_CHANGE, info)); @@ -222,8 +249,12 @@ public class CaptivePortalTracker extends StateMachine { @Override public void enter() { if (DBG) log(getName() + "\n"); - sendMessageDelayed(obtainMessage(CMD_DELAYED_CAPTIVE_CHECK, - ++mDelayedCheckToken, 0), DELAYED_CHECK_INTERVAL_MS); + Message message = obtainMessage(CMD_DELAYED_CAPTIVE_CHECK, ++mDelayedCheckToken, 0); + if (mDeviceProvisioned) { + sendMessageDelayed(message, DELAYED_CHECK_INTERVAL_MS); + } else { + sendMessage(message); + } } @Override @@ -233,13 +264,26 @@ public class CaptivePortalTracker extends StateMachine { case CMD_DELAYED_CAPTIVE_CHECK: if (message.arg1 == mDelayedCheckToken) { InetAddress server = lookupHost(mServer); - if (server != null) { - if (isCaptivePortal(server)) { - if (DBG) log("Captive network " + mNetworkInfo); + boolean captive = server != null && isCaptivePortal(server); + if (captive) { + if (DBG) log("Captive network " + mNetworkInfo); + } else { + if (DBG) log("Not captive network " + mNetworkInfo); + } + if (mDeviceProvisioned) { + if (captive) { + // Setup Wizard will assist the user in connecting to a captive + // portal, so make the notification visible unless during setup setNotificationVisible(true); } + } else { + Intent intent = new Intent( + ConnectivityManager.ACTION_CAPTIVE_PORTAL_TEST_COMPLETED); + intent.putExtra(ConnectivityManager.EXTRA_IS_CAPTIVE_PORTAL, captive); + intent.setPackage(SETUP_WIZARD_PACKAGE); + mContext.sendBroadcast(intent); } - if (DBG) log("Not captive network " + mNetworkInfo); + transitionTo(mActiveNetworkState); } break; @@ -370,13 +414,4 @@ public class CaptivePortalTracker extends StateMachine { } mNotificationShown = visible; } - - private static void log(String s) { - Log.d(TAG, s); - } - - private static void loge(String s) { - Log.e(TAG, s); - } - } diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index 6ff1a33..3a04c27 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -62,7 +62,7 @@ public class ConnectivityManager { * NetworkInfo for the new network is also passed as an extra. This lets * any receivers of the broadcast know that they should not necessarily * tell the user that no data traffic will be possible. Instead, the - * reciever should expect another broadcast soon, indicating either that + * receiver should expect another broadcast soon, indicating either that * the failover attempt succeeded (and so there is still overall data * connectivity), or that the failover attempt failed, meaning that all * connectivity has been lost. @@ -70,6 +70,7 @@ public class ConnectivityManager { * For a disconnect event, the boolean extra EXTRA_NO_CONNECTIVITY * is set to {@code true} if there are no connected networks at all. */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String CONNECTIVITY_ACTION = "android.net.conn.CONNECTIVITY_CHANGE"; /** @@ -78,6 +79,7 @@ public class ConnectivityManager { * * @hide */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String CONNECTIVITY_ACTION_IMMEDIATE = "android.net.conn.CONNECTIVITY_CHANGE_IMMEDIATE"; @@ -149,8 +151,8 @@ public class ConnectivityManager { /** * Broadcast action to indicate the change of data activity status * (idle or active) on a network in a recent period. - * The network becomes active when data transimission is started, or - * idle if there is no data transimition for a period of time. + * The network becomes active when data transmission is started, or + * idle if there is no data transmission for a period of time. * {@hide} */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) @@ -198,91 +200,119 @@ public class ConnectivityManager { * the network and it's condition. * @hide */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String INET_CONDITION_ACTION = "android.net.conn.INET_CONDITION_ACTION"; /** - * Broadcast Action: A tetherable connection has come or gone - * TODO - finish the doc + * Broadcast Action: A tetherable connection has come or gone. + * Uses {@code ConnectivityManager.EXTRA_AVAILABLE_TETHER}, + * {@code ConnectivityManager.EXTRA_ACTIVE_TETHER} and + * {@code ConnectivityManager.EXTRA_ERRORED_TETHER} to indicate + * the current state of tethering. Each include a list of + * interface names in that state (may be empty). * @hide */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_TETHER_STATE_CHANGED = "android.net.conn.TETHER_STATE_CHANGED"; /** * @hide - * gives a String[] + * gives a String[] listing all the interfaces configured for + * tethering and currently available for tethering. */ public static final String EXTRA_AVAILABLE_TETHER = "availableArray"; /** * @hide - * gives a String[] + * gives a String[] listing all the interfaces currently tethered + * (ie, has dhcp support and packets potentially forwarded/NATed) */ public static final String EXTRA_ACTIVE_TETHER = "activeArray"; /** * @hide - * gives a String[] + * gives a String[] listing all the interfaces we tried to tether and + * failed. Use {@link #getLastTetherError} to find the error code + * for any interfaces listed here. */ public static final String EXTRA_ERRORED_TETHER = "erroredArray"; /** - * The absence of APN.. + * Broadcast Action: The captive portal tracker has finished its test. + * Sent only while running Setup Wizard, in lieu of showing a user + * notification. + * @hide + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_CAPTIVE_PORTAL_TEST_COMPLETED = + "android.net.conn.CAPTIVE_PORTAL_TEST_COMPLETED"; + /** + * The lookup key for a boolean that indicates whether a captive portal was detected. + * Retrieve it with {@link android.content.Intent#getBooleanExtra(String,boolean)}. + * @hide + */ + public static final String EXTRA_IS_CAPTIVE_PORTAL = "captivePortal"; + + /** + * The absence of a connection type. * @hide */ public static final int TYPE_NONE = -1; /** - * The Default Mobile data connection. When active, all data traffic - * will use this connection by default. + * The Mobile data connection. When active, all data traffic + * will use this network type's interface by default + * (it has a default route) */ public static final int TYPE_MOBILE = 0; /** - * The Default WIFI data connection. When active, all data traffic - * will use this connection by default. + * The WIFI data connection. When active, all data traffic + * will use this network type's interface by default + * (it has a default route). */ public static final int TYPE_WIFI = 1; /** - * An MMS-specific Mobile data connection. This connection may be the - * same as {@link #TYPE_MOBILE} but it may be different. This is used - * by applications needing to talk to the carrier's Multimedia Messaging - * Service servers. It may coexist with default data connections. + * An MMS-specific Mobile data connection. This network type may use the + * same network interface as {@link #TYPE_MOBILE} or it may use a different + * one. This is used by applications needing to talk to the carrier's + * Multimedia Messaging Service servers. */ public static final int TYPE_MOBILE_MMS = 2; /** - * A SUPL-specific Mobile data connection. This connection may be the - * same as {@link #TYPE_MOBILE} but it may be different. This is used - * by applications needing to talk to the carrier's Secure User Plane - * Location servers for help locating the device. It may coexist with - * default data connections. + * A SUPL-specific Mobile data connection. This network type may use the + * same network interface as {@link #TYPE_MOBILE} or it may use a different + * one. This is used by applications needing to talk to the carrier's + * Secure User Plane Location servers for help locating the device. */ public static final int TYPE_MOBILE_SUPL = 3; /** - * A DUN-specific Mobile data connection. This connection may be the - * same as {@link #TYPE_MOBILE} but it may be different. This is used - * by applicaitons performing a Dial Up Networking bridge so that - * the carrier is aware of DUN traffic. It may coexist with default data - * connections. + * A DUN-specific Mobile data connection. This network type may use the + * same network interface as {@link #TYPE_MOBILE} or it may use a different + * one. This is sometimes by the system when setting up an upstream connection + * for tethering so that the carrier is aware of DUN traffic. */ public static final int TYPE_MOBILE_DUN = 4; /** - * A High Priority Mobile data connection. This connection is typically - * the same as {@link #TYPE_MOBILE} but the routing setup is different. - * Only requesting processes will have access to the Mobile DNS servers - * and only IP's explicitly requested via {@link #requestRouteToHost} - * will route over this interface if a default route exists. + * A High Priority Mobile data connection. This network type uses the + * same network interface as {@link #TYPE_MOBILE} but the routing setup + * is different. Only requesting processes will have access to the + * Mobile DNS servers and only IP's explicitly requested via {@link #requestRouteToHost} + * will route over this interface if no default route exists. */ public static final int TYPE_MOBILE_HIPRI = 5; /** - * The Default WiMAX data connection. When active, all data traffic - * will use this connection by default. + * The WiMAX data connection. When active, all data traffic + * will use this network type's interface by default + * (it has a default route). */ public static final int TYPE_WIMAX = 6; /** - * The Default Bluetooth data connection. When active, all data traffic - * will use this connection by default. + * The Bluetooth data connection. When active, all data traffic + * will use this network type's interface by default + * (it has a default route). */ public static final int TYPE_BLUETOOTH = 7; @@ -292,25 +322,26 @@ public class ConnectivityManager { public static final int TYPE_DUMMY = 8; /** - * The Default Ethernet data connection. When active, all data traffic - * will use this connection by default. + * The Ethernet data connection. When active, all data traffic + * will use this network type's interface by default + * (it has a default route). */ public static final int TYPE_ETHERNET = 9; /** - * Over the air Adminstration. + * Over the air Administration. * {@hide} */ public static final int TYPE_MOBILE_FOTA = 10; /** - * IP Multimedia Subsystem + * IP Multimedia Subsystem. * {@hide} */ public static final int TYPE_MOBILE_IMS = 11; /** - * Carrier Branded Services + * Carrier Branded Services. * {@hide} */ public static final int TYPE_MOBILE_CBS = 12; @@ -328,11 +359,27 @@ public class ConnectivityManager { /** {@hide} */ public static final int MAX_NETWORK_TYPE = TYPE_WIFI_P2P; + /** + * If you want to set the default network preference,you can directly + * change the networkAttributes array in framework's config.xml. + * + * @deprecated Since we support so many more networks now, the single + * network default network preference can't really express + * the hierarchy. Instead, the default is defined by the + * networkAttributes in config.xml. You can determine + * the current value by calling {@link #getNetworkPreference()} + * from an App. + */ + @Deprecated public static final int DEFAULT_NETWORK_PREFERENCE = TYPE_WIFI; /** * Default value for {@link Settings.Global#CONNECTIVITY_CHANGE_DELAY} in - * milliseconds. + * milliseconds. This was introduced because IPv6 routes seem to take a + * moment to settle - trying network activity before the routes are adjusted + * can lead to packets using the wrong interface or having the wrong IP address. + * This delay is a bit crude, but in the future hopefully we will have kernel + * notifications letting us know when it's safe to use the new network. * * @hide */ @@ -340,11 +387,23 @@ public class ConnectivityManager { private final IConnectivityManager mService; + /** + * Tests if a given integer represents a valid network type. + * @param networkType the type to be tested + * @return a boolean. {@code true} if the type is valid, else {@code false} + */ public static boolean isNetworkTypeValid(int networkType) { return networkType >= 0 && networkType <= MAX_NETWORK_TYPE; } - /** {@hide} */ + /** + * Returns a non-localized string representing a given network type. + * ONLY used for debugging output. + * @param type the type needing naming + * @return a String for the given type, or a string version of the type ("87") + * if no name is known. + * {@hide} + */ public static String getNetworkTypeName(int type) { switch (type) { case TYPE_MOBILE: @@ -380,7 +439,13 @@ public class ConnectivityManager { } } - /** {@hide} */ + /** + * Checks if a given type uses the cellular data connection. + * This should be replaced in the future by a network property. + * @param networkType the type to check + * @return a boolean - {@code true} if uses cellular network, else {@code false} + * {@hide} + */ public static boolean isNetworkTypeMobile(int networkType) { switch (networkType) { case TYPE_MOBILE: @@ -397,6 +462,17 @@ public class ConnectivityManager { } } + /** + * Specifies the preferred network type. When the device has more + * than one type available the preferred network type will be used. + * Note that this made sense when we only had 2 network types, + * but with more and more default networks we need an array to list + * their ordering. This will be deprecated soon. + * + * @param preference the network type to prefer over all others. It is + * unspecified what happens to the old preferred network in the + * overall ordering. + */ public void setNetworkPreference(int preference) { try { mService.setNetworkPreference(preference); @@ -404,6 +480,17 @@ public class ConnectivityManager { } } + /** + * Retrieves the current preferred network type. + * Note that this made sense when we only had 2 network types, + * but with more and more default networks we need an array to list + * their ordering. This will be deprecated soon. + * + * @return an integer representing the preferred network type + * + * <p>This method requires the caller to hold the permission + * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}. + */ public int getNetworkPreference() { try { return mService.getNetworkPreference(); @@ -413,11 +500,16 @@ public class ConnectivityManager { } /** - * Returns details about the currently active data network. When connected, - * this network is the default route for outgoing connections. You should - * always check {@link NetworkInfo#isConnected()} before initiating network - * traffic. This may return {@code null} when no networks are available. - * <p>This method requires the caller to hold the permission + * Returns details about the currently active default data network. When + * connected, this network is the default route for outgoing connections. + * You should always check {@link NetworkInfo#isConnected()} before initiating + * network traffic. This may return {@code null} when there is no default + * network. + * + * @return a {@link NetworkInfo} object for the current default network + * or {@code null} if no network default network is currently active + * + * <p>This method requires the call to hold the permission * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}. */ public NetworkInfo getActiveNetworkInfo() { @@ -428,7 +520,19 @@ public class ConnectivityManager { } } - /** {@hide} */ + /** + * Returns details about the currently active default data network + * for a given uid. This is for internal use only to avoid spying + * other apps. + * + * @return a {@link NetworkInfo} object for the current default network + * for the given uid or {@code null} if no default network is + * available for the specified uid. + * + * <p>This method requires the caller to hold the permission + * {@link android.Manifest.permission#CONNECTIVITY_INTERNAL} + * {@hide} + */ public NetworkInfo getActiveNetworkInfoForUid(int uid) { try { return mService.getActiveNetworkInfoForUid(uid); @@ -437,6 +541,19 @@ public class ConnectivityManager { } } + /** + * Returns connection status information about a particular + * network type. + * + * @param networkType integer specifying which networkType in + * which you're interested. + * @return a {@link NetworkInfo} object for the requested + * network type or {@code null} if the type is not + * supported by the device. + * + * <p>This method requires the call to hold the permission + * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}. + */ public NetworkInfo getNetworkInfo(int networkType) { try { return mService.getNetworkInfo(networkType); @@ -445,6 +562,16 @@ public class ConnectivityManager { } } + /** + * Returns connection status information about all network + * types supported by the device. + * + * @return an array of {@link NetworkInfo} objects. Check each + * {@link NetworkInfo#getType} for which type each applies. + * + * <p>This method requires the call to hold the permission + * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}. + */ public NetworkInfo[] getAllNetworkInfo() { try { return mService.getAllNetworkInfo(); @@ -453,7 +580,17 @@ public class ConnectivityManager { } } - /** {@hide} */ + /** + * Returns the IP information for the current default network. + * + * @return a {@link LinkProperties} object describing the IP info + * for the current default network, or {@code null} if there + * is no current default network. + * + * <p>This method requires the call to hold the permission + * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}. + * {@hide} + */ public LinkProperties getActiveLinkProperties() { try { return mService.getActiveLinkProperties(); @@ -462,7 +599,18 @@ public class ConnectivityManager { } } - /** {@hide} */ + /** + * Returns the IP information for a given network type. + * + * @param networkType the network type of interest. + * @return a {@link LinkProperties} object describing the IP info + * for the given networkType, or {@code null} if there is + * no current default network. + * + * <p>This method requires the call to hold the permission + * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}. + * {@hide} + */ public LinkProperties getLinkProperties(int networkType) { try { return mService.getLinkProperties(networkType); @@ -471,7 +619,18 @@ public class ConnectivityManager { } } - /** {@hide} */ + /** + * Tells each network type to set its radio power state as directed. + * + * @param turnOn a boolean, {@code true} to turn the radios on, + * {@code false} to turn them off. + * @return a boolean, {@code true} indicating success. All network types + * will be tried, even if some fail. + * + * <p>This method requires the call to hold the permission + * {@link android.Manifest.permission#CHANGE_NETWORK_STATE}. + * {@hide} + */ public boolean setRadios(boolean turnOn) { try { return mService.setRadios(turnOn); @@ -480,7 +639,18 @@ public class ConnectivityManager { } } - /** {@hide} */ + /** + * Tells a given networkType to set its radio power state as directed. + * + * @param networkType the int networkType of interest. + * @param turnOn a boolean, {@code true} to turn the radio on, + * {@code} false to turn it off. + * @return a boolean, {@code true} indicating success. + * + * <p>This method requires the call to hold the permission + * {@link android.Manifest.permission#CHANGE_NETWORK_STATE}. + * {@hide} + */ public boolean setRadio(int networkType, boolean turnOn) { try { return mService.setRadio(networkType, turnOn); @@ -615,6 +785,9 @@ public class ConnectivityManager { * network is active. Quota status can change rapidly, so these values * shouldn't be cached. * + * <p>This method requires the call to hold the permission + * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}. + * * @hide */ public NetworkQuotaInfo getActiveNetworkQuotaInfo() { @@ -629,6 +802,9 @@ public class ConnectivityManager { * Gets the value of the setting for enabling Mobile data. * * @return Whether mobile data is enabled. + * + * <p>This method requires the call to hold the permission + * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}. * @hide */ public boolean getMobileDataEnabled() { @@ -642,8 +818,8 @@ public class ConnectivityManager { /** * Sets the persisted value for enabling/disabling Mobile data. * - * @param enabled Whether the mobile data connection should be - * used or not. + * @param enabled Whether the user wants the mobile data connection used + * or not. * @hide */ public void setMobileDataEnabled(boolean enabled) { @@ -666,6 +842,13 @@ public class ConnectivityManager { } /** + * Get the set of tetherable, available interfaces. This list is limited by + * device configuration and current interface existence. + * + * @return an array of 0 or more Strings of tetherable interface names. + * + * <p>This method requires the call to hold the permission + * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}. * {@hide} */ public String[] getTetherableIfaces() { @@ -677,6 +860,12 @@ public class ConnectivityManager { } /** + * Get the set of tethered interfaces. + * + * @return an array of 0 or more String of currently tethered interface names. + * + * <p>This method requires the call to hold the permission + * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}. * {@hide} */ public String[] getTetheredIfaces() { @@ -688,6 +877,18 @@ public class ConnectivityManager { } /** + * Get the set of interface names which attempted to tether but + * failed. Re-attempting to tether may cause them to reset to the Tethered + * state. Alternatively, causing the interface to be destroyed and recreated + * may cause them to reset to the available state. + * {@link ConnectivityManager#getLastTetherError} can be used to get more + * information on the cause of the errors. + * + * @return an array of 0 or more String indicating the interface names + * which failed to tether. + * + * <p>This method requires the call to hold the permission + * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}. * {@hide} */ public String[] getTetheringErroredIfaces() { @@ -699,7 +900,19 @@ public class ConnectivityManager { } /** - * @return error A TETHER_ERROR value indicating success or failure type + * Attempt to tether the named interface. This will setup a dhcp server + * on the interface, forward and NAT IP packets and forward DNS requests + * to the best active upstream network interface. Note that if no upstream + * IP network interface is available, dhcp will still run and traffic will be + * allowed between the tethered devices and this device, though upstream net + * access will of course fail until an upstream network interface becomes + * active. + * + * @param iface the interface name to tether. + * @return error a {@code TETHER_ERROR} value indicating success or failure type + * + * <p>This method requires the call to hold the permission + * {@link android.Manifest.permission#CHANGE_NETWORK_STATE}. * {@hide} */ public int tether(String iface) { @@ -711,7 +924,13 @@ public class ConnectivityManager { } /** - * @return error A TETHER_ERROR value indicating success or failure type + * Stop tethering the named interface. + * + * @param iface the interface name to untether. + * @return error a {@code TETHER_ERROR} value indicating success or failure type + * + * <p>This method requires the call to hold the permission + * {@link android.Manifest.permission#CHANGE_NETWORK_STATE}. * {@hide} */ public int untether(String iface) { @@ -723,6 +942,14 @@ public class ConnectivityManager { } /** + * Check if the device allows for tethering. It may be disabled via + * {@code ro.tether.denied} system property, {@link Settings#TETHER_SUPPORTED} or + * due to device configuration. + * + * @return a boolean - {@code true} indicating Tethering is supported. + * + * <p>This method requires the call to hold the permission + * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}. * {@hide} */ public boolean isTetheringSupported() { @@ -734,6 +961,15 @@ public class ConnectivityManager { } /** + * Get the list of regular expressions that define any tetherable + * USB network interfaces. If USB tethering is not supported by the + * device, this list should be empty. + * + * @return an array of 0 or more regular expression Strings defining + * what interfaces are considered tetherable usb interfaces. + * + * <p>This method requires the call to hold the permission + * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}. * {@hide} */ public String[] getTetherableUsbRegexs() { @@ -745,6 +981,15 @@ public class ConnectivityManager { } /** + * Get the list of regular expressions that define any tetherable + * Wifi network interfaces. If Wifi tethering is not supported by the + * device, this list should be empty. + * + * @return an array of 0 or more regular expression Strings defining + * what interfaces are considered tetherable wifi interfaces. + * + * <p>This method requires the call to hold the permission + * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}. * {@hide} */ public String[] getTetherableWifiRegexs() { @@ -756,6 +1001,15 @@ public class ConnectivityManager { } /** + * Get the list of regular expressions that define any tetherable + * Bluetooth network interfaces. If Bluetooth tethering is not supported by the + * device, this list should be empty. + * + * @return an array of 0 or more regular expression Strings defining + * what interfaces are considered tetherable bluetooth interfaces. + * + * <p>This method requires the call to hold the permission + * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}. * {@hide} */ public String[] getTetherableBluetoothRegexs() { @@ -767,6 +1021,17 @@ public class ConnectivityManager { } /** + * Attempt to both alter the mode of USB and Tethering of USB. A + * utility method to deal with some of the complexity of USB - will + * attempt to switch to Rndis and subsequently tether the resulting + * interface on {@code true} or turn off tethering and switch off + * Rndis on {@code false}. + * + * @param enable a boolean - {@code true} to enable tethering + * @return error a {@code TETHER_ERROR} value indicating success or failure type + * + * <p>This method requires the call to hold the permission + * {@link android.Manifest.permission#CHANGE_NETWORK_STATE}. * {@hide} */ public int setUsbTethering(boolean enable) { @@ -801,9 +1066,15 @@ public class ConnectivityManager { public static final int TETHER_ERROR_IFACE_CFG_ERROR = 10; /** - * @param iface The name of the interface we're interested in + * Get a more detailed error code after a Tethering or Untethering + * request asynchronously failed. + * + * @param iface The name of the interface of interest * @return error The error code of the last error tethering or untethering the named * interface + * + * <p>This method requires the call to hold the permission + * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}. * {@hide} */ public int getLastTetherError(String iface) { @@ -815,9 +1086,16 @@ public class ConnectivityManager { } /** - * Ensure the device stays awake until we connect with the next network - * @param forWhome The name of the network going down for logging purposes + * Try to ensure the device stays awake until we connect with the next network. + * Actually just holds a wakelock for a number of seconds while we try to connect + * to any default networks. This will expire if the timeout passes or if we connect + * to a default after this is called. For internal use only. + * + * @param forWhom the name of the network going down for logging purposes * @return {@code true} on success, {@code false} on failure + * + * <p>This method requires the call to hold the permission + * {@link android.Manifest.permission#CONNECTIVITY_INTERNAL}. * {@hide} */ public boolean requestNetworkTransitionWakelock(String forWhom) { @@ -830,8 +1108,14 @@ public class ConnectivityManager { } /** + * Report network connectivity status. This is currently used only + * to alter status bar UI. + * * @param networkType The type of network you want to report on * @param percentage The quality of the connection 0 is bad, 100 is good + * + * <p>This method requires the call to hold the permission + * {@link android.Manifest.permission#STATUS_BAR}. * {@hide} */ public void reportInetCondition(int networkType, int percentage) { @@ -842,7 +1126,16 @@ public class ConnectivityManager { } /** - * @param proxyProperties The definition for the new global http proxy + * Set a network-independent global http proxy. This is not normally what you want + * for typical HTTP proxies - they are general network dependent. However if you're + * doing something unusual like general internal filtering this may be useful. On + * a private network where the proxy is not accessible, you may break HTTP using this. + * + * @param proxyProperties The a {@link ProxyProperites} object defining the new global + * HTTP proxy. A {@code null} value will clear the global HTTP proxy. + * + * <p>This method requires the call to hold the permission + * {@link android.Manifest.permission#CHANGE_NETWORK_STATE}. * {@hide} */ public void setGlobalProxy(ProxyProperties p) { @@ -853,7 +1146,13 @@ public class ConnectivityManager { } /** - * @return proxyProperties for the current global proxy + * Retrieve any network-independent global HTTP proxy. + * + * @return {@link ProxyProperties} for the current global HTTP proxy or {@code null} + * if no global HTTP proxy is set. + * + * <p>This method requires the call to hold the permission + * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}. * {@hide} */ public ProxyProperties getGlobalProxy() { @@ -865,7 +1164,14 @@ public class ConnectivityManager { } /** - * @return proxyProperties for the current proxy (global if set, network specific if not) + * Get the HTTP proxy settings for the current default network. Note that + * if a global proxy is set, it will override any per-network setting. + * + * @return the {@link ProxyProperties} for the current HTTP proxy, or {@code null} if no + * HTTP proxy is active. + * + * <p>This method requires the call to hold the permission + * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}. * {@hide} */ public ProxyProperties getProxy() { @@ -877,8 +1183,15 @@ public class ConnectivityManager { } /** + * Sets a secondary requirement bit for the given networkType. + * This requirement bit is generally under the control of the carrier + * or its agents and is not directly controlled by the user. + * * @param networkType The network who's dependence has changed - * @param met Boolean - true if network use is ok, false if not + * @param met Boolean - true if network use is OK, false if not + * + * <p>This method requires the call to hold the permission + * {@link android.Manifest.permission#CONNECTIVITY_INTERNAL}. * {@hide} */ public void setDataDependency(int networkType, boolean met) { @@ -892,11 +1205,15 @@ public class ConnectivityManager { * Returns true if the hardware supports the given network type * else it returns false. This doesn't indicate we have coverage * or are authorized onto a network, just whether or not the - * hardware supports it. For example a gsm phone without a sim - * should still return true for mobile data, but a wifi only tablet - * would return false. - * @param networkType The nework type we'd like to check - * @return true if supported, else false + * hardware supports it. For example a GSM phone without a SIM + * should still return {@code true} for mobile data, but a wifi only + * tablet would return {@code false}. + * + * @param networkType The network type we'd like to check + * @return {@code true} if supported, else {@code false} + * + * <p>This method requires the call to hold the permission + * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}. * @hide */ public boolean isNetworkSupported(int networkType) { @@ -909,9 +1226,16 @@ public class ConnectivityManager { /** * Returns if the currently active data network is metered. A network is * classified as metered when the user is sensitive to heavy data usage on - * that connection. You should check this before doing large data transfers, - * and warn the user or delay the operation until another network is - * available. + * that connection due to monetary costs, data limitations or + * battery/performance issues. You should check this before doing large + * data transfers, and warn the user or delay the operation until another + * network is available. + * + * @return {@code true} if large transfers should be avoided, otherwise + * {@code false}. + * + * <p>This method requires the call to hold the permission + * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}. */ public boolean isActiveNetworkMetered() { try { @@ -921,7 +1245,15 @@ public class ConnectivityManager { } } - /** {@hide} */ + /** + * If the LockdownVpn mechanism is enabled, updates the vpn + * with a reload of its profile. + * + * @return a boolean with {@code} indicating success + * + * <p>This method can only be called by the system UID + * {@hide} + */ public boolean updateLockdownVpn() { try { return mService.updateLockdownVpn(); @@ -931,6 +1263,14 @@ public class ConnectivityManager { } /** + * Signal that the captive portal check on the indicated network + * is complete and we can turn the network on for general use. + * + * @param info the {@link NetworkInfo} object for the networkType + * in question. + * + * <p>This method requires the call to hold the permission + * {@link android.Manifest.permission#CONNECTIVITY_INTERNAL}. * {@hide} */ public void captivePortalCheckComplete(NetworkInfo info) { diff --git a/core/java/android/net/DhcpInfo.java b/core/java/android/net/DhcpInfo.java index e2660e4..2b359eb 100644 --- a/core/java/android/net/DhcpInfo.java +++ b/core/java/android/net/DhcpInfo.java @@ -22,16 +22,17 @@ import java.net.InetAddress; /** * A simple object for retrieving the results of a DHCP request. + * @deprecated - use LinkProperties - To be removed 11/2013 + * STOPSHIP - make sure we expose LinkProperties through ConnectivityManager */ public class DhcpInfo implements Parcelable { public int ipAddress; public int gateway; public int netmask; - public int dns1; public int dns2; - public int serverAddress; + public int leaseDuration; public DhcpInfo() { diff --git a/core/java/android/net/DhcpInfoInternal.java b/core/java/android/net/DhcpInfoInternal.java deleted file mode 100644 index f3508c1..0000000 --- a/core/java/android/net/DhcpInfoInternal.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.net; - -import android.text.TextUtils; -import android.util.Log; - -import java.net.Inet4Address; -import java.net.InetAddress; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; - -/** - * A simple object for retrieving the results of a DHCP request. - * Replaces (internally) the IPv4-only DhcpInfo class. - * @hide - */ -public class DhcpInfoInternal { - private final static String TAG = "DhcpInfoInternal"; - public String ipAddress; - public int prefixLength; - - public String dns1; - public String dns2; - - public String serverAddress; - public int leaseDuration; - - /** - * Vendor specific information (from RFC 2132). - */ - public String vendorInfo; - - private Collection<RouteInfo> mRoutes; - - public DhcpInfoInternal() { - mRoutes = new ArrayList<RouteInfo>(); - } - - public void addRoute(RouteInfo routeInfo) { - mRoutes.add(routeInfo); - } - - public Collection<RouteInfo> getRoutes() { - return Collections.unmodifiableCollection(mRoutes); - } - - private int convertToInt(String addr) { - if (addr != null) { - try { - InetAddress inetAddress = NetworkUtils.numericToInetAddress(addr); - if (inetAddress instanceof Inet4Address) { - return NetworkUtils.inetAddressToInt(inetAddress); - } - } catch (IllegalArgumentException e) {} - } - return 0; - } - - public DhcpInfo makeDhcpInfo() { - DhcpInfo info = new DhcpInfo(); - info.ipAddress = convertToInt(ipAddress); - for (RouteInfo route : mRoutes) { - if (route.isDefaultRoute()) { - info.gateway = convertToInt(route.getGateway().getHostAddress()); - break; - } - } - try { - InetAddress inetAddress = NetworkUtils.numericToInetAddress(ipAddress); - info.netmask = NetworkUtils.prefixLengthToNetmaskInt(prefixLength); - } catch (IllegalArgumentException e) {} - info.dns1 = convertToInt(dns1); - info.dns2 = convertToInt(dns2); - info.serverAddress = convertToInt(serverAddress); - info.leaseDuration = leaseDuration; - return info; - } - - public LinkAddress makeLinkAddress() { - if (TextUtils.isEmpty(ipAddress)) { - Log.e(TAG, "makeLinkAddress with empty ipAddress"); - return null; - } - return new LinkAddress(NetworkUtils.numericToInetAddress(ipAddress), prefixLength); - } - - public LinkProperties makeLinkProperties() { - LinkProperties p = new LinkProperties(); - p.addLinkAddress(makeLinkAddress()); - for (RouteInfo route : mRoutes) { - p.addRoute(route); - } - //if empty, connectivity configures default DNS - if (TextUtils.isEmpty(dns1) == false) { - p.addDns(NetworkUtils.numericToInetAddress(dns1)); - } else { - Log.d(TAG, "makeLinkProperties with empty dns1!"); - } - if (TextUtils.isEmpty(dns2) == false) { - p.addDns(NetworkUtils.numericToInetAddress(dns2)); - } else { - Log.d(TAG, "makeLinkProperties with empty dns2!"); - } - return p; - } - - /* Updates the DHCP fields that need to be retained from - * original DHCP request if the DHCP renewal shows them as - * being empty - */ - public void updateFromDhcpRequest(DhcpInfoInternal orig) { - if (orig == null) return; - - if (TextUtils.isEmpty(dns1)) { - dns1 = orig.dns1; - } - - if (TextUtils.isEmpty(dns2)) { - dns2 = orig.dns2; - } - - if (mRoutes.size() == 0) { - for (RouteInfo route : orig.getRoutes()) { - addRoute(route); - } - } - } - - /** - * Test if this DHCP lease includes vendor hint that network link is - * metered, and sensitive to heavy data transfers. - */ - public boolean hasMeteredHint() { - if (vendorInfo != null) { - return vendorInfo.contains("ANDROID_METERED"); - } else { - return false; - } - } - - public String toString() { - String routeString = ""; - for (RouteInfo route : mRoutes) routeString += route.toString() + " | "; - return "addr: " + ipAddress + "/" + prefixLength + - " mRoutes: " + routeString + - " dns: " + dns1 + "," + dns2 + - " dhcpServer: " + serverAddress + - " leaseDuration: " + leaseDuration; - } -} diff --git a/core/java/android/util/PoolableManager.java b/core/java/android/net/DhcpResults.aidl index 8773e63..f4db3c3 100644 --- a/core/java/android/util/PoolableManager.java +++ b/core/java/android/net/DhcpResults.aidl @@ -1,11 +1,11 @@ -/* - * Copyright (C) 2009 The Android Open Source Project +/** + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,14 +14,6 @@ * limitations under the License. */ -package android.util; - -/** - * @hide - */ -public interface PoolableManager<T extends Poolable<T>> { - T newInstance(); +package android.net; - void onAcquired(T element); - void onReleased(T element); -} +parcelable DhcpResults; diff --git a/core/java/android/net/DhcpResults.java b/core/java/android/net/DhcpResults.java new file mode 100644 index 0000000..a3f70da --- /dev/null +++ b/core/java/android/net/DhcpResults.java @@ -0,0 +1,247 @@ +/* + * 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.net; + +import android.os.Parcelable; +import android.os.Parcel; +import android.text.TextUtils; +import android.util.Log; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; + +/** + * A simple object for retrieving the results of a DHCP request. + * Optimized (attempted) for that jni interface + * TODO - remove when DhcpInfo is deprecated. Move the remaining api to LinkProperties. + * @hide + */ +public class DhcpResults implements Parcelable { + private static final String TAG = "DhcpResults"; + + public final LinkProperties linkProperties; + + public InetAddress serverAddress; + + /** + * Vendor specific information (from RFC 2132). + */ + public String vendorInfo; + + public int leaseDuration; + + public DhcpResults() { + linkProperties = new LinkProperties(); + } + + /** copy constructor */ + public DhcpResults(DhcpResults source) { + if (source != null) { + linkProperties = new LinkProperties(source.linkProperties); + serverAddress = source.serverAddress; + leaseDuration = source.leaseDuration; + vendorInfo = source.vendorInfo; + } else { + linkProperties = new LinkProperties(); + } + } + + public DhcpResults(LinkProperties lp) { + linkProperties = new LinkProperties(lp); + } + + /** + * Updates the DHCP fields that need to be retained from + * original DHCP request if the current renewal shows them + * being empty. + */ + public void updateFromDhcpRequest(DhcpResults orig) { + if (orig == null || orig.linkProperties == null) return; + if (linkProperties.getRoutes().size() == 0) { + for (RouteInfo r : orig.linkProperties.getRoutes()) linkProperties.addRoute(r); + } + if (linkProperties.getDnses().size() == 0) { + for (InetAddress d : orig.linkProperties.getDnses()) linkProperties.addDns(d); + } + } + + /** + * Test if this DHCP lease includes vendor hint that network link is + * metered, and sensitive to heavy data transfers. + */ + public boolean hasMeteredHint() { + if (vendorInfo != null) { + return vendorInfo.contains("ANDROID_METERED"); + } else { + return false; + } + } + + public void clear() { + linkProperties.clear(); + serverAddress = null; + vendorInfo = null; + leaseDuration = 0; + } + + @Override + public String toString() { + StringBuffer str = new StringBuffer(linkProperties.toString()); + + str.append(" DHCP server ").append(serverAddress); + str.append(" Vendor info ").append(vendorInfo); + str.append(" lease ").append(leaseDuration).append(" seconds"); + + return str.toString(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + + if (!(obj instanceof DhcpResults)) return false; + + DhcpResults target = (DhcpResults)obj; + + if (linkProperties == null) { + if (target.linkProperties != null) return false; + } else if (!linkProperties.equals(target.linkProperties)) return false; + if (serverAddress == null) { + if (target.serverAddress != null) return false; + } else if (!serverAddress.equals(target.serverAddress)) return false; + if (vendorInfo == null) { + if (target.vendorInfo != null) return false; + } else if (!vendorInfo.equals(target.vendorInfo)) return false; + if (leaseDuration != target.leaseDuration) return false; + + return true; + } + + /** Implement the Parcelable interface */ + public int describeContents() { + return 0; + } + + /** Implement the Parcelable interface */ + public void writeToParcel(Parcel dest, int flags) { + linkProperties.writeToParcel(dest, flags); + + dest.writeInt(leaseDuration); + + if (serverAddress != null) { + dest.writeByte((byte)1); + dest.writeByteArray(serverAddress.getAddress()); + } else { + dest.writeByte((byte)0); + } + + dest.writeString(vendorInfo); + } + + /** Implement the Parcelable interface */ + public static final Creator<DhcpResults> CREATOR = + new Creator<DhcpResults>() { + public DhcpResults createFromParcel(Parcel in) { + DhcpResults prop = new DhcpResults((LinkProperties)in.readParcelable(null)); + + prop.leaseDuration = in.readInt(); + + if (in.readByte() == 1) { + try { + prop.serverAddress = InetAddress.getByAddress(in.createByteArray()); + } catch (UnknownHostException e) {} + } + + prop.vendorInfo = in.readString(); + + return prop; + } + + public DhcpResults[] newArray(int size) { + return new DhcpResults[size]; + } + }; + + // Utils for jni population - false on success + public void setInterfaceName(String interfaceName) { + linkProperties.setInterfaceName(interfaceName); + } + + public boolean addLinkAddress(String addrString, int prefixLength) { + InetAddress addr; + try { + addr = NetworkUtils.numericToInetAddress(addrString); + } catch (IllegalArgumentException e) { + Log.e(TAG, "addLinkAddress failed with addrString " + addrString); + return true; + } + + LinkAddress linkAddress = new LinkAddress(addr, prefixLength); + linkProperties.addLinkAddress(linkAddress); + + RouteInfo routeInfo = new RouteInfo(linkAddress); + linkProperties.addRoute(routeInfo); + return false; + } + + public boolean addGateway(String addrString) { + try { + linkProperties.addRoute(new RouteInfo(NetworkUtils.numericToInetAddress(addrString))); + } catch (IllegalArgumentException e) { + Log.e(TAG, "addGateway failed with addrString " + addrString); + return true; + } + return false; + } + + public boolean addDns(String addrString) { + if (TextUtils.isEmpty(addrString) == false) { + try { + linkProperties.addDns(NetworkUtils.numericToInetAddress(addrString)); + } catch (IllegalArgumentException e) { + Log.e(TAG, "addDns failed with addrString " + addrString); + return true; + } + } + return false; + } + + public boolean setServerAddress(String addrString) { + try { + serverAddress = NetworkUtils.numericToInetAddress(addrString); + } catch (IllegalArgumentException e) { + Log.e(TAG, "setServerAddress failed with addrString " + addrString); + return true; + } + return false; + } + + public void setLeaseDuration(int duration) { + leaseDuration = duration; + } + + public void setVendorInfo(String info) { + vendorInfo = info; + } + + public void setDomains(String domains) { + linkProperties.setDomains(domains); + } +} diff --git a/core/java/android/net/DhcpStateMachine.java b/core/java/android/net/DhcpStateMachine.java index 8dc900e..fd22b10 100644 --- a/core/java/android/net/DhcpStateMachine.java +++ b/core/java/android/net/DhcpStateMachine.java @@ -26,7 +26,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.net.DhcpInfoInternal; +import android.net.DhcpResults; import android.net.NetworkUtils; import android.os.Message; import android.os.PowerManager; @@ -64,7 +64,7 @@ public class DhcpStateMachine extends StateMachine { private static final String WAKELOCK_TAG = "DHCP"; //Remember DHCP configuration from first request - private DhcpInfoInternal mDhcpInfo; + private DhcpResults mDhcpResults; private static final int DHCP_RENEW = 0; private static final String ACTION_DHCP_RENEW = "android.net.wifi.DHCP_RENEW"; @@ -348,23 +348,21 @@ public class DhcpStateMachine extends StateMachine { private boolean runDhcp(DhcpAction dhcpAction) { boolean success = false; - DhcpInfoInternal dhcpInfoInternal = new DhcpInfoInternal(); + DhcpResults dhcpResults = new DhcpResults(); if (dhcpAction == DhcpAction.START) { /* Stop any existing DHCP daemon before starting new */ NetworkUtils.stopDhcp(mInterfaceName); if (DBG) Log.d(TAG, "DHCP request on " + mInterfaceName); - success = NetworkUtils.runDhcp(mInterfaceName, dhcpInfoInternal); - mDhcpInfo = dhcpInfoInternal; + success = NetworkUtils.runDhcp(mInterfaceName, dhcpResults); } else if (dhcpAction == DhcpAction.RENEW) { if (DBG) Log.d(TAG, "DHCP renewal on " + mInterfaceName); - success = NetworkUtils.runDhcpRenew(mInterfaceName, dhcpInfoInternal); - dhcpInfoInternal.updateFromDhcpRequest(mDhcpInfo); + success = NetworkUtils.runDhcpRenew(mInterfaceName, dhcpResults); + dhcpResults.updateFromDhcpRequest(mDhcpResults); } - if (success) { if (DBG) Log.d(TAG, "DHCP succeeded on " + mInterfaceName); - long leaseDuration = dhcpInfoInternal.leaseDuration; //int to long conversion + long leaseDuration = dhcpResults.leaseDuration; //int to long conversion //Sanity check for renewal if (leaseDuration >= 0) { @@ -384,7 +382,8 @@ public class DhcpStateMachine extends StateMachine { //infinite lease time, no renewal needed } - mController.obtainMessage(CMD_POST_DHCP_ACTION, DHCP_SUCCESS, 0, dhcpInfoInternal) + mDhcpResults = dhcpResults; + mController.obtainMessage(CMD_POST_DHCP_ACTION, DHCP_SUCCESS, 0, dhcpResults) .sendToTarget(); } else { Log.e(TAG, "DHCP failed on " + mInterfaceName + ": " + diff --git a/core/java/android/net/EthernetDataTracker.java b/core/java/android/net/EthernetDataTracker.java index 37601fc..8947162 100644 --- a/core/java/android/net/EthernetDataTracker.java +++ b/core/java/android/net/EthernetDataTracker.java @@ -170,13 +170,12 @@ public class EthernetDataTracker implements NetworkStateTracker { private void runDhcp() { Thread dhcpThread = new Thread(new Runnable() { public void run() { - DhcpInfoInternal dhcpInfoInternal = new DhcpInfoInternal(); - if (!NetworkUtils.runDhcp(mIface, dhcpInfoInternal)) { + DhcpResults dhcpResults = new DhcpResults(); + if (!NetworkUtils.runDhcp(mIface, dhcpResults)) { Log.e(TAG, "DHCP request error:" + NetworkUtils.getDhcpError()); return; } - mLinkProperties = dhcpInfoInternal.makeLinkProperties(); - mLinkProperties.setInterfaceName(mIface); + mLinkProperties = dhcpResults.linkProperties; mNetworkInfo.setDetailedState(DetailedState.CONNECTED, null, mHwAddr); Message msg = mCsHandler.obtainMessage(EVENT_STATE_CHANGED, mNetworkInfo); diff --git a/core/java/android/net/LinkProperties.java b/core/java/android/net/LinkProperties.java index 75646fd..b9362da 100644 --- a/core/java/android/net/LinkProperties.java +++ b/core/java/android/net/LinkProperties.java @@ -51,9 +51,10 @@ import java.util.Collections; */ public class LinkProperties implements Parcelable { - String mIfaceName; + private String mIfaceName; private Collection<LinkAddress> mLinkAddresses = new ArrayList<LinkAddress>(); private Collection<InetAddress> mDnses = new ArrayList<InetAddress>(); + private String mDomains; private Collection<RouteInfo> mRoutes = new ArrayList<RouteInfo>(); private ProxyProperties mHttpProxy; @@ -82,9 +83,10 @@ public class LinkProperties implements Parcelable { mIfaceName = source.getInterfaceName(); for (LinkAddress l : source.getLinkAddresses()) mLinkAddresses.add(l); for (InetAddress i : source.getDnses()) mDnses.add(i); + mDomains = source.getDomains(); for (RouteInfo r : source.getRoutes()) mRoutes.add(r); mHttpProxy = (source.getHttpProxy() == null) ? - null : new ProxyProperties(source.getHttpProxy()); + null : new ProxyProperties(source.getHttpProxy()); } } @@ -120,6 +122,14 @@ public class LinkProperties implements Parcelable { return Collections.unmodifiableCollection(mDnses); } + public String getDomains() { + return mDomains; + } + + public void setDomains(String domains) { + mDomains = domains; + } + public void addRoute(RouteInfo route) { if (route != null) mRoutes.add(route); } @@ -138,6 +148,7 @@ public class LinkProperties implements Parcelable { mIfaceName = null; mLinkAddresses.clear(); mDnses.clear(); + mDomains = null; mRoutes.clear(); mHttpProxy = null; } @@ -162,12 +173,14 @@ public class LinkProperties implements Parcelable { for (InetAddress addr : mDnses) dns += addr.getHostAddress() + ","; dns += "] "; - String routes = "Routes: ["; + String domainName = "Domains: " + mDomains; + + String routes = " Routes: ["; for (RouteInfo route : mRoutes) routes += route.toString() + ","; routes += "] "; String proxy = (mHttpProxy == null ? "" : "HttpProxy: " + mHttpProxy.toString() + " "); - return ifaceName + linkAddresses + routes + dns + proxy; + return ifaceName + linkAddresses + routes + dns + domainName + proxy; } /** @@ -181,7 +194,7 @@ public class LinkProperties implements Parcelable { } /** - * Compares this {@code LinkProperties} interface name against the target + * Compares this {@code LinkProperties} interface addresses against the target * * @param target LinkProperties to compare. * @return {@code true} if both are identical, {@code false} otherwise. @@ -201,6 +214,12 @@ public class LinkProperties implements Parcelable { */ public boolean isIdenticalDnses(LinkProperties target) { Collection<InetAddress> targetDnses = target.getDnses(); + String targetDomains = target.getDomains(); + if (mDomains == null) { + if (targetDomains != null) return false; + } else { + if (mDomains.equals(targetDomains) == false) return false; + } return (mDnses.size() == targetDnses.size()) ? mDnses.containsAll(targetDnses) : false; } @@ -359,13 +378,13 @@ public class LinkProperties implements Parcelable { return ((null == mIfaceName) ? 0 : mIfaceName.hashCode() + mLinkAddresses.size() * 31 + mDnses.size() * 37 + + ((null == mDomains) ? 0 : mDomains.hashCode()) + mRoutes.size() * 41 + ((null == mHttpProxy) ? 0 : mHttpProxy.hashCode())); } /** * Implement the Parcelable interface. - * @hide */ public void writeToParcel(Parcel dest, int flags) { dest.writeString(getInterfaceName()); @@ -378,6 +397,7 @@ public class LinkProperties implements Parcelable { for(InetAddress d : mDnses) { dest.writeByteArray(d.getAddress()); } + dest.writeString(mDomains); dest.writeInt(mRoutes.size()); for(RouteInfo route : mRoutes) { @@ -394,19 +414,15 @@ public class LinkProperties implements Parcelable { /** * Implement the Parcelable interface. - * @hide */ public static final Creator<LinkProperties> CREATOR = new Creator<LinkProperties>() { public LinkProperties createFromParcel(Parcel in) { LinkProperties netProp = new LinkProperties(); + String iface = in.readString(); if (iface != null) { - try { - netProp.setInterfaceName(iface); - } catch (Exception e) { - return null; - } + netProp.setInterfaceName(iface); } int addressCount = in.readInt(); for (int i=0; i<addressCount; i++) { @@ -418,6 +434,7 @@ public class LinkProperties implements Parcelable { netProp.addDns(InetAddress.getByAddress(in.createByteArray())); } catch (UnknownHostException e) { } } + netProp.setDomains(in.readString()); addressCount = in.readInt(); for (int i=0; i<addressCount; i++) { netProp.addRoute((RouteInfo)in.readParcelable(null)); diff --git a/core/java/android/net/NetworkInfo.java b/core/java/android/net/NetworkInfo.java index 0b23cb7..689dae5 100644 --- a/core/java/android/net/NetworkInfo.java +++ b/core/java/android/net/NetworkInfo.java @@ -19,6 +19,8 @@ package android.net; import android.os.Parcelable; import android.os.Parcel; +import com.android.internal.annotations.VisibleForTesting; + import java.util.EnumMap; /** @@ -312,7 +314,9 @@ public class NetworkInfo implements Parcelable { } } - void setRoaming(boolean isRoaming) { + /** {@hide} */ + @VisibleForTesting + public void setRoaming(boolean isRoaming) { synchronized (this) { mIsRoaming = isRoaming; } diff --git a/core/java/android/net/NetworkStats.java b/core/java/android/net/NetworkStats.java index c757605..9cb904d 100644 --- a/core/java/android/net/NetworkStats.java +++ b/core/java/android/net/NetworkStats.java @@ -135,6 +135,18 @@ public class NetworkStats implements Parcelable { builder.append(" operations=").append(operations); return builder.toString(); } + + @Override + public boolean equals(Object o) { + if (o instanceof Entry) { + final Entry e = (Entry) o; + return uid == e.uid && set == e.set && tag == e.tag && rxBytes == e.rxBytes + && rxPackets == e.rxPackets && txBytes == e.txBytes + && txPackets == e.txPackets && operations == e.operations + && iface.equals(e.iface); + } + return false; + } } public NetworkStats(long elapsedRealtime, int initialSize) { diff --git a/core/java/android/net/NetworkTemplate.java b/core/java/android/net/NetworkTemplate.java index d3839ad..c189ba4 100644 --- a/core/java/android/net/NetworkTemplate.java +++ b/core/java/android/net/NetworkTemplate.java @@ -22,6 +22,7 @@ import static android.net.ConnectivityManager.TYPE_WIFI_P2P; import static android.net.ConnectivityManager.TYPE_WIMAX; import static android.net.NetworkIdentity.COMBINE_SUBTYPE_ENABLED; import static android.net.NetworkIdentity.scrubSubscriberId; +import static android.net.wifi.WifiInfo.removeDoubleQuotes; import static android.telephony.TelephonyManager.NETWORK_CLASS_2_G; import static android.telephony.TelephonyManager.NETWORK_CLASS_3_G; import static android.telephony.TelephonyManager.NETWORK_CLASS_4_G; @@ -279,7 +280,8 @@ public class NetworkTemplate implements Parcelable { private boolean matchesWifi(NetworkIdentity ident) { switch (ident.mType) { case TYPE_WIFI: - return Objects.equal(mNetworkId, ident.mNetworkId); + return Objects.equal( + removeDoubleQuotes(mNetworkId), removeDoubleQuotes(ident.mNetworkId)); default: return false; } diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java index d39e741..4ab479e 100644 --- a/core/java/android/net/NetworkUtils.java +++ b/core/java/android/net/NetworkUtils.java @@ -62,21 +62,21 @@ public class NetworkUtils { * addresses. This call blocks until it obtains a result (either success * or failure) from the daemon. * @param interfaceName the name of the interface to configure - * @param ipInfo if the request succeeds, this object is filled in with + * @param dhcpResults if the request succeeds, this object is filled in with * the IP address information. * @return {@code true} for success, {@code false} for failure */ - public native static boolean runDhcp(String interfaceName, DhcpInfoInternal ipInfo); + public native static boolean runDhcp(String interfaceName, DhcpResults dhcpResults); /** * Initiate renewal on the Dhcp client daemon. This call blocks until it obtains * a result (either success or failure) from the daemon. * @param interfaceName the name of the interface to configure - * @param ipInfo if the request succeeds, this object is filled in with + * @param dhcpResults if the request succeeds, this object is filled in with * the IP address information. * @return {@code true} for success, {@code false} for failure */ - public native static boolean runDhcpRenew(String interfaceName, DhcpInfoInternal ipInfo); + public native static boolean runDhcpRenew(String interfaceName, DhcpResults dhcpResults); /** * Shut down the DHCP client daemon. @@ -124,12 +124,9 @@ public class NetworkUtils { * @param inetAddr is an InetAddress corresponding to the IPv4 address * @return the IP address as an integer in network byte order */ - public static int inetAddressToInt(InetAddress inetAddr) + public static int inetAddressToInt(Inet4Address inetAddr) throws IllegalArgumentException { byte [] addr = inetAddr.getAddress(); - if (addr.length != 4) { - throw new IllegalArgumentException("Not an IPv4 address"); - } return ((addr[3] & 0xff) << 24) | ((addr[2] & 0xff) << 16) | ((addr[1] & 0xff) << 8) | (addr[0] & 0xff); } diff --git a/core/java/android/net/RouteInfo.java b/core/java/android/net/RouteInfo.java index 275f32a..112e143 100644 --- a/core/java/android/net/RouteInfo.java +++ b/core/java/android/net/RouteInfo.java @@ -76,6 +76,10 @@ public class RouteInfo implements Parcelable { this(null, gateway); } + public RouteInfo(LinkAddress host) { + this(host, null); + } + public static RouteInfo makeHostRoute(InetAddress host) { return makeHostRoute(host, null); } diff --git a/core/java/android/net/ThrottleManager.java b/core/java/android/net/ThrottleManager.java deleted file mode 100644 index 5fdac58..0000000 --- a/core/java/android/net/ThrottleManager.java +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.net; - -import android.annotation.SdkConstant; -import android.annotation.SdkConstant.SdkConstantType; -import android.os.Binder; -import android.os.RemoteException; - -/** - * Class that handles throttling. It provides read/write numbers per interface - * and methods to apply throttled rates. - * {@hide} - */ -public class ThrottleManager -{ - /** - * Broadcast each polling period to indicate new data counts. - * - * Includes four extras: - * EXTRA_CYCLE_READ - a long of the read bytecount for the current cycle - * EXTRA_CYCLE_WRITE -a long of the write bytecount for the current cycle - * EXTRA_CYLCE_START -a long of MS for the cycle start time - * EXTRA_CYCLE_END -a long of MS for the cycle stop time - * {@hide} - */ - public static final String THROTTLE_POLL_ACTION = "android.net.thrott.POLL_ACTION"; - /** - * The lookup key for a long for the read bytecount for this period. Retrieve with - * {@link android.content.Intent#getLongExtra(String)}. - * {@hide} - */ - public static final String EXTRA_CYCLE_READ = "cycleRead"; - /** - * contains a long of the number of bytes written in the cycle - * {@hide} - */ - public static final String EXTRA_CYCLE_WRITE = "cycleWrite"; - /** - * contains a long of the number of bytes read in the cycle - * {@hide} - */ - public static final String EXTRA_CYCLE_START = "cycleStart"; - /** - * contains a long of the ms since 1970 used to init a calendar, etc for the end - * of the cycle - * {@hide} - */ - public static final String EXTRA_CYCLE_END = "cycleEnd"; - - /** - * Broadcast when the thottle level changes. - * {@hide} - */ - public static final String THROTTLE_ACTION = "android.net.thrott.THROTTLE_ACTION"; - /** - * int of the current bandwidth in TODO - * {@hide} - */ - public static final String EXTRA_THROTTLE_LEVEL = "level"; - - /** - * Broadcast on boot and whenever the settings change. - * {@hide} - */ - public static final String POLICY_CHANGED_ACTION = "android.net.thrott.POLICY_CHANGED_ACTION"; - - // {@hide} - public static final int DIRECTION_TX = 0; - // {@hide} - public static final int DIRECTION_RX = 1; - - // {@hide} - public static final int PERIOD_CYCLE = 0; - // {@hide} - public static final int PERIOD_YEAR = 1; - // {@hide} - public static final int PERIOD_MONTH = 2; - // {@hide} - public static final int PERIOD_WEEK = 3; - // @hide - public static final int PERIOD_7DAY = 4; - // @hide - public static final int PERIOD_DAY = 5; - // @hide - public static final int PERIOD_24HOUR = 6; - // @hide - public static final int PERIOD_HOUR = 7; - // @hide - public static final int PERIOD_60MIN = 8; - // @hide - public static final int PERIOD_MINUTE = 9; - // @hide - public static final int PERIOD_60SEC = 10; - // @hide - public static final int PERIOD_SECOND = 11; - - - - /** - * returns a long of the ms from the epoch to the time the current cycle ends for the - * named interface - * {@hide} - */ - public long getResetTime(String iface) { - try { - return mService.getResetTime(iface); - } catch (RemoteException e) { - return -1; - } - } - - /** - * returns a long of the ms from the epoch to the time the current cycle started for the - * named interface - * {@hide} - */ - public long getPeriodStartTime(String iface) { - try { - return mService.getPeriodStartTime(iface); - } catch (RemoteException e) { - return -1; - } - } - - /** - * returns a long of the byte count either read or written on the named interface - * for the period described. Direction is either DIRECTION_RX or DIRECTION_TX and - * period may only be PERIOD_CYCLE for the current cycle (other periods may be supported - * in the future). Ago indicates the number of periods in the past to lookup - 0 means - * the current period, 1 is the last one, 2 was two periods ago.. - * {@hide} - */ - public long getByteCount(String iface, int direction, int period, int ago) { - try { - return mService.getByteCount(iface, direction, period, ago); - } catch (RemoteException e) { - return -1; - } - } - - /** - * returns the number of bytes read+written after which a particular cliff - * takes effect on the named iface. Currently only cliff #1 is supported (1 step) - * {@hide} - */ - public long getCliffThreshold(String iface, int cliff) { - try { - return mService.getCliffThreshold(iface, cliff); - } catch (RemoteException e) { - return -1; - } - } - - /** - * returns the thottling bandwidth (bps) for a given cliff # on the named iface. - * only cliff #1 is currently supported. - * {@hide} - */ - public int getCliffLevel(String iface, int cliff) { - try { - return mService.getCliffLevel(iface, cliff); - } catch (RemoteException e) { - return -1; - } - } - - /** - * returns the help URI for throttling - * {@hide} - */ - public String getHelpUri() { - try { - return mService.getHelpUri(); - } catch (RemoteException e) { - return null; - } - } - - - private IThrottleManager mService; - - /** - * Don't allow use of default constructor. - */ - @SuppressWarnings({"UnusedDeclaration"}) - private ThrottleManager() { - } - - /** - * {@hide} - */ - public ThrottleManager(IThrottleManager service) { - if (service == null) { - throw new IllegalArgumentException( - "ThrottleManager() cannot be constructed with null service"); - } - mService = service; - } -} diff --git a/core/java/android/net/TrafficStats.java b/core/java/android/net/TrafficStats.java index e437d2e..ce1276f 100644 --- a/core/java/android/net/TrafficStats.java +++ b/core/java/android/net/TrafficStats.java @@ -315,6 +315,30 @@ public class TrafficStats { return total; } + /** {@hide} */ + public static long getMobileTcpRxPackets() { + long total = 0; + for (String iface : getMobileIfaces()) { + final long stat = nativeGetIfaceStat(iface, TYPE_TCP_RX_PACKETS); + if (stat != UNSUPPORTED) { + total += stat; + } + } + return total; + } + + /** {@hide} */ + public static long getMobileTcpTxPackets() { + long total = 0; + for (String iface : getMobileIfaces()) { + final long stat = nativeGetIfaceStat(iface, TYPE_TCP_TX_PACKETS); + if (stat != UNSUPPORTED) { + total += stat; + } + } + return total; + } + /** * Get the total number of packets transmitted through the specified interface. * @@ -400,161 +424,156 @@ public class TrafficStats { } /** - * Get the number of bytes sent through the network for this UID. - * The statistics are across all interfaces. - * - * {@see android.os.Process#myUid()}. + * Return number of bytes transmitted by the given UID since device boot. + * Counts packets across all network interfaces, and always increases + * monotonically since device boot. Statistics are measured at the network + * layer, so they include both TCP and UDP usage. + * <p> + * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may return + * {@link #UNSUPPORTED} on devices where statistics aren't available. * - * @param uid The UID of the process to examine. - * @return number of bytes. If the statistics are not supported by this device, - * {@link #UNSUPPORTED} will be returned. + * @see android.os.Process#myUid() + * @see android.content.pm.ApplicationInfo#uid */ - public static native long getUidTxBytes(int uid); + public static long getUidTxBytes(int uid) { + return nativeGetUidStat(uid, TYPE_TX_BYTES); + } /** - * Get the number of bytes received through the network for this UID. - * The statistics are across all interfaces. - * - * {@see android.os.Process#myUid()}. + * Return number of bytes received by the given UID since device boot. + * Counts packets across all network interfaces, and always increases + * monotonically since device boot. Statistics are measured at the network + * layer, so they include both TCP and UDP usage. + * <p> + * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may return + * {@link #UNSUPPORTED} on devices where statistics aren't available. * - * @param uid The UID of the process to examine. - * @return number of bytes + * @see android.os.Process#myUid() + * @see android.content.pm.ApplicationInfo#uid */ - public static native long getUidRxBytes(int uid); + public static long getUidRxBytes(int uid) { + return nativeGetUidStat(uid, TYPE_RX_BYTES); + } /** - * Get the number of packets (TCP segments + UDP) sent through - * the network for this UID. - * The statistics are across all interfaces. - * - * {@see android.os.Process#myUid()}. + * Return number of packets transmitted by the given UID since device boot. + * Counts packets across all network interfaces, and always increases + * monotonically since device boot. Statistics are measured at the network + * layer, so they include both TCP and UDP usage. + * <p> + * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may return + * {@link #UNSUPPORTED} on devices where statistics aren't available. * - * @param uid The UID of the process to examine. - * @return number of packets. - * If the statistics are not supported by this device, - * {@link #UNSUPPORTED} will be returned. + * @see android.os.Process#myUid() + * @see android.content.pm.ApplicationInfo#uid */ - public static native long getUidTxPackets(int uid); + public static long getUidTxPackets(int uid) { + return nativeGetUidStat(uid, TYPE_TX_PACKETS); + } /** - * Get the number of packets (TCP segments + UDP) received through - * the network for this UID. - * The statistics are across all interfaces. - * - * {@see android.os.Process#myUid()}. + * Return number of packets received by the given UID since device boot. + * Counts packets across all network interfaces, and always increases + * monotonically since device boot. Statistics are measured at the network + * layer, so they include both TCP and UDP usage. + * <p> + * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may return + * {@link #UNSUPPORTED} on devices where statistics aren't available. * - * @param uid The UID of the process to examine. - * @return number of packets + * @see android.os.Process#myUid() + * @see android.content.pm.ApplicationInfo#uid */ - public static native long getUidRxPackets(int uid); + public static long getUidRxPackets(int uid) { + return nativeGetUidStat(uid, TYPE_RX_PACKETS); + } /** - * Get the number of TCP payload bytes sent for this UID. - * This total does not include protocol and control overheads at - * the transport and the lower layers of the networking stack. - * The statistics are across all interfaces. - * - * {@see android.os.Process#myUid()}. - * - * @param uid The UID of the process to examine. - * @return number of bytes. If the statistics are not supported by this device, - * {@link #UNSUPPORTED} will be returned. + * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, + * transport layer statistics are no longer available, and will + * always return {@link #UNSUPPORTED}. + * @see #getUidTxBytes(int) */ - public static native long getUidTcpTxBytes(int uid); + @Deprecated + public static long getUidTcpTxBytes(int uid) { + return UNSUPPORTED; + } /** - * Get the number of TCP payload bytes received for this UID. - * This total does not include protocol and control overheads at - * the transport and the lower layers of the networking stack. - * The statistics are across all interfaces. - * - * {@see android.os.Process#myUid()}. - * - * @param uid The UID of the process to examine. - * @return number of bytes. If the statistics are not supported by this device, - * {@link #UNSUPPORTED} will be returned. + * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, + * transport layer statistics are no longer available, and will + * always return {@link #UNSUPPORTED}. + * @see #getUidRxBytes(int) */ - public static native long getUidTcpRxBytes(int uid); + @Deprecated + public static long getUidTcpRxBytes(int uid) { + return UNSUPPORTED; + } /** - * Get the number of UDP payload bytes sent for this UID. - * This total does not include protocol and control overheads at - * the transport and the lower layers of the networking stack. - * The statistics are across all interfaces. - * - * {@see android.os.Process#myUid()}. - * - * @param uid The UID of the process to examine. - * @return number of bytes. If the statistics are not supported by this device, - * {@link #UNSUPPORTED} will be returned. + * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, + * transport layer statistics are no longer available, and will + * always return {@link #UNSUPPORTED}. + * @see #getUidTxBytes(int) */ - public static native long getUidUdpTxBytes(int uid); + @Deprecated + public static long getUidUdpTxBytes(int uid) { + return UNSUPPORTED; + } /** - * Get the number of UDP payload bytes received for this UID. - * This total does not include protocol and control overheads at - * the transport and the lower layers of the networking stack. - * The statistics are across all interfaces. - * - * {@see android.os.Process#myUid()}. - * - * @param uid The UID of the process to examine. - * @return number of bytes. If the statistics are not supported by this device, - * {@link #UNSUPPORTED} will be returned. + * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, + * transport layer statistics are no longer available, and will + * always return {@link #UNSUPPORTED}. + * @see #getUidRxBytes(int) */ - public static native long getUidUdpRxBytes(int uid); + @Deprecated + public static long getUidUdpRxBytes(int uid) { + return UNSUPPORTED; + } /** - * Get the number of TCP segments sent for this UID. - * Does not include TCP control packets (SYN/ACKs/FIN/..). - * The statistics are across all interfaces. - * - * {@see android.os.Process#myUid()}. - * - * @param uid The UID of the process to examine. - * @return number of TCP segments. If the statistics are not supported by this device, - * {@link #UNSUPPORTED} will be returned. + * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, + * transport layer statistics are no longer available, and will + * always return {@link #UNSUPPORTED}. + * @see #getUidTxPackets(int) */ - public static native long getUidTcpTxSegments(int uid); + @Deprecated + public static long getUidTcpTxSegments(int uid) { + return UNSUPPORTED; + } /** - * Get the number of TCP segments received for this UID. - * Does not include TCP control packets (SYN/ACKs/FIN/..). - * The statistics are across all interfaces. - * - * {@see android.os.Process#myUid()}. - * - * @param uid The UID of the process to examine. - * @return number of TCP segments. If the statistics are not supported by this device, - * {@link #UNSUPPORTED} will be returned. + * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, + * transport layer statistics are no longer available, and will + * always return {@link #UNSUPPORTED}. + * @see #getUidRxPackets(int) */ - public static native long getUidTcpRxSegments(int uid); + @Deprecated + public static long getUidTcpRxSegments(int uid) { + return UNSUPPORTED; + } /** - * Get the number of UDP packets sent for this UID. - * Includes DNS requests. - * The statistics are across all interfaces. - * - * {@see android.os.Process#myUid()}. - * - * @param uid The UID of the process to examine. - * @return number of packets. If the statistics are not supported by this device, - * {@link #UNSUPPORTED} will be returned. + * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, + * transport layer statistics are no longer available, and will + * always return {@link #UNSUPPORTED}. + * @see #getUidTxPackets(int) */ - public static native long getUidUdpTxPackets(int uid); + @Deprecated + public static long getUidUdpTxPackets(int uid) { + return UNSUPPORTED; + } /** - * Get the number of UDP packets received for this UID. - * Includes DNS responses. - * The statistics are across all interfaces. - * - * {@see android.os.Process#myUid()}. - * - * @param uid The UID of the process to examine. - * @return number of packets. If the statistics are not supported by this device, - * {@link #UNSUPPORTED} will be returned. + * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, + * transport layer statistics are no longer available, and will + * always return {@link #UNSUPPORTED}. + * @see #getUidRxPackets(int) */ - public static native long getUidUdpRxPackets(int uid); + @Deprecated + public static long getUidUdpRxPackets(int uid) { + return UNSUPPORTED; + } /** * Return detailed {@link NetworkStats} for the current UID. Requires no @@ -587,7 +606,10 @@ public class TrafficStats { private static final int TYPE_RX_PACKETS = 1; private static final int TYPE_TX_BYTES = 2; private static final int TYPE_TX_PACKETS = 3; + private static final int TYPE_TCP_RX_PACKETS = 4; + private static final int TYPE_TCP_TX_PACKETS = 5; private static native long nativeGetTotalStat(int type); private static native long nativeGetIfaceStat(String iface, int type); + private static native long nativeGetUidStat(int uid, int type); } diff --git a/core/java/android/net/http/AndroidHttpClient.java b/core/java/android/net/http/AndroidHttpClient.java index fabe018..04f3974 100644 --- a/core/java/android/net/http/AndroidHttpClient.java +++ b/core/java/android/net/http/AndroidHttpClient.java @@ -17,6 +17,7 @@ package android.net.http; import com.android.internal.http.HttpDateTime; + import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpEntityEnclosingRequest; @@ -25,18 +26,18 @@ import org.apache.http.HttpHost; import org.apache.http.HttpRequest; import org.apache.http.HttpRequestInterceptor; import org.apache.http.HttpResponse; -import org.apache.http.entity.AbstractHttpEntity; -import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.ResponseHandler; -import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.protocol.ClientContext; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.params.HttpClientParams; +import org.apache.http.client.protocol.ClientContext; import org.apache.http.conn.ClientConnectionManager; import org.apache.http.conn.scheme.PlainSocketFactory; import org.apache.http.conn.scheme.Scheme; import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.entity.AbstractHttpEntity; +import org.apache.http.entity.ByteArrayEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.client.RequestWrapper; import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; @@ -44,26 +45,26 @@ import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpParams; import org.apache.http.params.HttpProtocolParams; +import org.apache.http.protocol.BasicHttpContext; import org.apache.http.protocol.BasicHttpProcessor; import org.apache.http.protocol.HttpContext; -import org.apache.http.protocol.BasicHttpContext; -import java.io.IOException; -import java.io.InputStream; -import java.io.ByteArrayOutputStream; -import java.io.OutputStream; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; -import java.net.URI; - -import android.content.Context; import android.content.ContentResolver; +import android.content.Context; import android.net.SSLCertificateSocketFactory; import android.net.SSLSessionCache; import android.os.Looper; import android.util.Base64; import android.util.Log; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + /** * Implementation of the Apache {@link DefaultHttpClient} that is configured with * reasonable default settings and registered schemes for Android. @@ -266,7 +267,7 @@ public final class AndroidHttpClient implements HttpClient { return delegate.execute(target, request, context); } - public <T> T execute(HttpUriRequest request, + public <T> T execute(HttpUriRequest request, ResponseHandler<? extends T> responseHandler) throws IOException, ClientProtocolException { return delegate.execute(request, responseHandler); @@ -404,6 +405,11 @@ public final class AndroidHttpClient implements HttpClient { builder.append("curl "); + // add in the method + builder.append("-X "); + builder.append(request.getMethod()); + builder.append(" "); + for (Header header: request.getAllHeaders()) { if (!logAuthToken && (header.getName().equals("Authorization") || diff --git a/core/java/android/nfc/NfcActivityManager.java b/core/java/android/nfc/NfcActivityManager.java index 53b41d5..7c3123f 100644 --- a/core/java/android/nfc/NfcActivityManager.java +++ b/core/java/android/nfc/NfcActivityManager.java @@ -197,7 +197,7 @@ public final class NfcActivityManager extends INdefPushCallback.Stub isResumed = state.resumed; } if (isResumed) { - requestNfcServiceCallback(true); + requestNfcServiceCallback(); } } @@ -211,7 +211,7 @@ public final class NfcActivityManager extends INdefPushCallback.Stub isResumed = state.resumed; } if (isResumed) { - requestNfcServiceCallback(true); + requestNfcServiceCallback(); } } @@ -223,7 +223,7 @@ public final class NfcActivityManager extends INdefPushCallback.Stub isResumed = state.resumed; } if (isResumed) { - requestNfcServiceCallback(true); + requestNfcServiceCallback(); } } @@ -236,7 +236,7 @@ public final class NfcActivityManager extends INdefPushCallback.Stub isResumed = state.resumed; } if (isResumed) { - requestNfcServiceCallback(true); + requestNfcServiceCallback(); } } @@ -249,18 +249,17 @@ public final class NfcActivityManager extends INdefPushCallback.Stub isResumed = state.resumed; } if (isResumed) { - requestNfcServiceCallback(true); + requestNfcServiceCallback(); } } /** * Request or unrequest NFC service callbacks for NDEF push. * Makes IPC call - do not hold lock. - * TODO: Do not do IPC on every onPause/onResume */ - void requestNfcServiceCallback(boolean request) { + void requestNfcServiceCallback() { try { - NfcAdapter.sService.setNdefPushCallback(request ? this : null); + NfcAdapter.sService.setNdefPushCallback(this); } catch (RemoteException e) { mAdapter.attemptDeadServiceRecovery(e); } @@ -355,7 +354,7 @@ public final class NfcActivityManager extends INdefPushCallback.Stub if (state == null) return; state.resumed = true; } - requestNfcServiceCallback(true); + requestNfcServiceCallback(); } /** Callback from Activity life-cycle, on main thread */ @@ -367,7 +366,6 @@ public final class NfcActivityManager extends INdefPushCallback.Stub if (state == null) return; state.resumed = false; } - requestNfcServiceCallback(false); } /** Callback from Activity life-cycle, on main thread */ diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java index 4baceed..6ad382b 100644 --- a/core/java/android/nfc/NfcAdapter.java +++ b/core/java/android/nfc/NfcAdapter.java @@ -174,31 +174,25 @@ public final class NfcAdapter { * Broadcast Action: The state of the local NFC adapter has been * changed. * <p>For example, NFC has been turned on or off. - * <p>Always contains the extra field {@link #EXTRA_STATE} - * @hide + * <p>Always contains the extra field {@link #EXTRA_ADAPTER_STATE} */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_ADAPTER_STATE_CHANGED = "android.nfc.action.ADAPTER_STATE_CHANGED"; /** - * Used as an int extra field in {@link #ACTION_STATE_CHANGED} + * Used as an int extra field in {@link #ACTION_ADAPTER_STATE_CHANGED} * intents to request the current power state. Possible values are: * {@link #STATE_OFF}, * {@link #STATE_TURNING_ON}, * {@link #STATE_ON}, * {@link #STATE_TURNING_OFF}, - * @hide */ public static final String EXTRA_ADAPTER_STATE = "android.nfc.extra.ADAPTER_STATE"; - /** @hide */ public static final int STATE_OFF = 1; - /** @hide */ public static final int STATE_TURNING_ON = 2; - /** @hide */ public static final int STATE_ON = 3; - /** @hide */ public static final int STATE_TURNING_OFF = 4; /** @hide */ diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java index 9821824..499ec77 100644 --- a/core/java/android/os/BatteryStats.java +++ b/core/java/android/os/BatteryStats.java @@ -93,6 +93,11 @@ public abstract class BatteryStats implements Parcelable { public static final int VIDEO_TURNED_ON = 8; /** + * A constant indicating a vibrator on timer + */ + public static final int VIBRATOR_ON = 9; + + /** * Include all of the data in the stats, including previously saved data. */ public static final int STATS_SINCE_CHARGED = 0; @@ -131,6 +136,7 @@ public abstract class BatteryStats implements Parcelable { private static final String APK_DATA = "apk"; private static final String PROCESS_DATA = "pr"; private static final String SENSOR_DATA = "sr"; + private static final String VIBRATOR_DATA = "vib"; private static final String WAKELOCK_DATA = "wl"; private static final String KERNEL_WAKELOCK_DATA = "kwl"; private static final String NETWORK_DATA = "nt"; @@ -277,6 +283,7 @@ public abstract class BatteryStats implements Parcelable { int which); public abstract long getAudioTurnedOnTime(long batteryRealtime, int which); public abstract long getVideoTurnedOnTime(long batteryRealtime, int which); + public abstract Timer getVibratorOnTimer(); /** * Note that these must match the constants in android.os.PowerManager. @@ -294,6 +301,11 @@ public abstract class BatteryStats implements Parcelable { public abstract int getUserActivityCount(int type, int which); public static abstract class Sensor { + /* + * FIXME: it's not correct to use this magic value because it + * could clash with a sensor handle (which are defined by + * the sensor HAL, and therefore out of our control + */ // Magic sensor number for the GPS. public static final int GPS = -10000; @@ -1395,6 +1407,16 @@ public abstract class BatteryStats implements Parcelable { } } + Timer vibTimer = u.getVibratorOnTimer(); + if (vibTimer != null) { + // Convert from microseconds to milliseconds with rounding + long totalTime = (vibTimer.getTotalTimeLocked(batteryRealtime, which) + 500) / 1000; + int count = vibTimer.getCountLocked(which); + if (totalTime != 0) { + dumpLine(pw, uid, category, VIBRATOR_DATA, totalTime, count); + } + } + Map<String, ? extends BatteryStats.Uid.Proc> processStats = u.getProcessStats(); if (processStats.size() > 0) { for (Map.Entry<String, ? extends BatteryStats.Uid.Proc> ent @@ -1919,6 +1941,26 @@ public abstract class BatteryStats implements Parcelable { } } + Timer vibTimer = u.getVibratorOnTimer(); + if (vibTimer != null) { + // Convert from microseconds to milliseconds with rounding + long totalTime = (vibTimer.getTotalTimeLocked( + batteryRealtime, which) + 500) / 1000; + int count = vibTimer.getCountLocked(which); + //timer.logState(); + if (totalTime != 0) { + sb.setLength(0); + sb.append(prefix); + sb.append(" Vibrator: "); + formatTimeMs(sb, totalTime); + sb.append("realtime ("); + sb.append(count); + sb.append(" times)"); + pw.println(sb.toString()); + uidActivity = true; + } + } + Map<String, ? extends BatteryStats.Uid.Proc> processStats = u.getProcessStats(); if (processStats.size() > 0) { for (Map.Entry<String, ? extends BatteryStats.Uid.Proc> ent diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java index a7f39d5..97ac862 100644 --- a/core/java/android/os/Build.java +++ b/core/java/android/os/Build.java @@ -431,6 +431,11 @@ public class Build { * </ul> */ public static final int JELLY_BEAN_MR1 = 17; + + /** + * Android 4.X: Jelly Bean MR2, the revenge of the beans. + */ + public static final int JELLY_BEAN_MR2 = CUR_DEVELOPMENT; } /** The type of build, like "user" or "eng". */ diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java index 460a5fe..18a0018 100644 --- a/core/java/android/os/Bundle.java +++ b/core/java/android/os/Bundle.java @@ -742,6 +742,25 @@ public final class Bundle implements Parcelable, Cloneable { } /** + * Inserts an {@link IBinder} value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * <p class="note">You should be very careful when using this function. In many + * places where Bundles are used (such as inside of Intent objects), the Bundle + * can live longer inside of another process than the process that had originally + * created it. In that case, the IBinder you supply here will become invalid + * when your process goes away, and no longer usable, even if a new process is + * created for you later on.</p> + * + * @param key a String, or null + * @param value an IBinder object, or null + */ + public void putBinder(String key, IBinder value) { + unparcel(); + mMap.put(key, value); + } + + /** * Inserts an IBinder value into the mapping of this Bundle, replacing * any existing value for the given key. Either key or value may be null. * @@ -749,7 +768,7 @@ public final class Bundle implements Parcelable, Cloneable { * @param value an IBinder object, or null * * @deprecated - * @hide + * @hide This is the old name of the function. */ @Deprecated public void putIBinder(String key, IBinder value) { @@ -1061,10 +1080,7 @@ public final class Bundle implements Parcelable, Cloneable { */ public String getString(String key) { unparcel(); - Object o = mMap.get(key); - if (o == null) { - return null; - } + final Object o = mMap.get(key); try { return (String) o; } catch (ClassCastException e) { @@ -1079,20 +1095,12 @@ public final class Bundle implements Parcelable, Cloneable { * * @param key a String, or null * @param defaultValue Value to return if key does not exist - * @return a String value, or null + * @return the String value associated with the given key, or defaultValue + * if no valid String object is currently mapped to that key. */ public String getString(String key, String defaultValue) { - unparcel(); - Object o = mMap.get(key); - if (o == null) { - return defaultValue; - } - try { - return (String) o; - } catch (ClassCastException e) { - typeWarning(key, o, "String", e); - return defaultValue; - } + final String s = getString(key); + return (s == null) ? defaultValue : s; } /** @@ -1105,10 +1113,7 @@ public final class Bundle implements Parcelable, Cloneable { */ public CharSequence getCharSequence(String key) { unparcel(); - Object o = mMap.get(key); - if (o == null) { - return null; - } + final Object o = mMap.get(key); try { return (CharSequence) o; } catch (ClassCastException e) { @@ -1123,20 +1128,12 @@ public final class Bundle implements Parcelable, Cloneable { * * @param key a String, or null * @param defaultValue Value to return if key does not exist - * @return a CharSequence value, or null + * @return the CharSequence value associated with the given key, or defaultValue + * if no valid CharSequence object is currently mapped to that key. */ public CharSequence getCharSequence(String key, CharSequence defaultValue) { - unparcel(); - Object o = mMap.get(key); - if (o == null) { - return defaultValue; - } - try { - return (CharSequence) o; - } catch (ClassCastException e) { - typeWarning(key, o, "CharSequence", e); - return defaultValue; - } + final CharSequence cs = getCharSequence(key); + return (cs == null) ? defaultValue : cs; } /** @@ -1565,9 +1562,31 @@ public final class Bundle implements Parcelable, Cloneable { * * @param key a String, or null * @return an IBinder value, or null + */ + public IBinder getBinder(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (IBinder) o; + } catch (ClassCastException e) { + typeWarning(key, o, "IBinder", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return an IBinder value, or null * * @deprecated - * @hide + * @hide This is the old name of the function. */ @Deprecated public IBinder getIBinder(String key) { diff --git a/core/java/android/os/INetworkManagementService.aidl b/core/java/android/os/INetworkManagementService.aidl index 2179fa1..c765457 100644 --- a/core/java/android/os/INetworkManagementService.aidl +++ b/core/java/android/os/INetworkManagementService.aidl @@ -306,23 +306,6 @@ interface INetworkManagementService boolean isBandwidthControlEnabled(); /** - * Configures bandwidth throttling on an interface. - */ - void setInterfaceThrottle(String iface, int rxKbps, int txKbps); - - /** - * Returns the currently configured RX throttle values - * for the specified interface - */ - int getInterfaceRxThrottle(String iface); - - /** - * Returns the currently configured TX throttle values - * for the specified interface - */ - int getInterfaceTxThrottle(String iface); - - /** * Sets idletimer for an interface. * * This either initializes a new idletimer or increases its @@ -351,7 +334,7 @@ interface INetworkManagementService /** * Bind name servers to an interface in the DNS resolver. */ - void setDnsServersForInterface(String iface, in String[] servers); + void setDnsServersForInterface(String iface, in String[] servers, String domains); /** * Flush the DNS cache associated with the default interface. @@ -369,4 +352,14 @@ interface INetworkManagementService void setFirewallEgressSourceRule(String addr, boolean allow); void setFirewallEgressDestRule(String addr, int port, boolean allow); void setFirewallUidRule(int uid, boolean allow); + + /** + * Set a process (pid) to use the name servers associated with the specified interface. + */ + void setDnsInterfaceForPid(String iface, int pid); + + /** + * Clear a process (pid) from being associated with an interface. + */ + void clearDnsInterfaceForPid(int pid); } diff --git a/core/java/android/os/IUserManager.aidl b/core/java/android/os/IUserManager.aidl index ec02ae0..34c9740 100644 --- a/core/java/android/os/IUserManager.aidl +++ b/core/java/android/os/IUserManager.aidl @@ -17,6 +17,7 @@ package android.os; +import android.os.Bundle; import android.os.ParcelFileDescriptor; import android.content.pm.UserInfo; import android.graphics.Bitmap; @@ -37,4 +38,6 @@ interface IUserManager { void wipeUser(int userHandle); int getUserSerialNumber(int userHandle); int getUserHandle(int userSerialNumber); + Bundle getUserRestrictions(int userHandle); + void setUserRestrictions(in Bundle restrictions, int userHandle); } diff --git a/core/java/android/os/IVibratorService.aidl b/core/java/android/os/IVibratorService.aidl index 2c2fe8a..456ffb1 100644 --- a/core/java/android/os/IVibratorService.aidl +++ b/core/java/android/os/IVibratorService.aidl @@ -20,8 +20,8 @@ package android.os; interface IVibratorService { boolean hasVibrator(); - void vibrate(long milliseconds, IBinder token); - void vibratePattern(in long[] pattern, int repeat, IBinder token); + void vibrate(int uid, String packageName, long milliseconds, IBinder token); + void vibratePattern(int uid, String packageName, in long[] pattern, int repeat, IBinder token); void cancelVibrate(IBinder token); } diff --git a/core/java/android/os/MessageQueue.java b/core/java/android/os/MessageQueue.java index 5ad60ec..222578a 100644 --- a/core/java/android/os/MessageQueue.java +++ b/core/java/android/os/MessageQueue.java @@ -48,10 +48,10 @@ public class MessageQueue { // Barriers are indicated by messages with a null target whose arg1 field carries the token. private int mNextBarrierToken; - private native void nativeInit(); - private native void nativeDestroy(); - private native void nativePollOnce(int ptr, int timeoutMillis); - private native void nativeWake(int ptr); + private native static int nativeInit(); + private native static void nativeDestroy(int ptr); + private native static void nativePollOnce(int ptr, int timeoutMillis); + private native static void nativeWake(int ptr); /** * Callback interface for discovering when a thread is going to block @@ -102,18 +102,25 @@ public class MessageQueue { MessageQueue(boolean quitAllowed) { mQuitAllowed = quitAllowed; - nativeInit(); + mPtr = nativeInit(); } @Override protected void finalize() throws Throwable { try { - nativeDestroy(); + dispose(); } finally { super.finalize(); } } + private void dispose() { + if (mPtr != 0) { + nativeDestroy(mPtr); + mPtr = 0; + } + } + final Message next() { int pendingIdleHandlerCount = -1; // -1 only during first iteration int nextPollTimeoutMillis = 0; @@ -126,6 +133,7 @@ public class MessageQueue { synchronized (this) { if (mQuiting) { + dispose(); return null; } diff --git a/core/java/android/os/NullVibrator.java b/core/java/android/os/NullVibrator.java index 8de4e06..ac6027f 100644 --- a/core/java/android/os/NullVibrator.java +++ b/core/java/android/os/NullVibrator.java @@ -49,6 +49,22 @@ public class NullVibrator extends Vibrator { } } + /** + * @hide + */ + @Override + public void vibrate(int owningUid, String owningPackage, long milliseconds) { + vibrate(milliseconds); + } + + /** + * @hide + */ + @Override + public void vibrate(int owningUid, String owningPackage, long[] pattern, int repeat) { + vibrate(pattern, repeat); + } + @Override public void cancel() { } diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index 788ab74..31d323b 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -1254,6 +1254,12 @@ public final class Parcel { p.writeToParcel(this, parcelableFlags); } + /** @hide */ + public final void writeParcelableCreator(Parcelable p) { + String name = p.getClass().getName(); + writeString(name); + } + /** * Write a generic serializable object in to a Parcel. It is strongly * recommended that this method be avoided, since the serialization @@ -2046,6 +2052,28 @@ public final class Parcel { * was an error trying to instantiate the Parcelable. */ public final <T extends Parcelable> T readParcelable(ClassLoader loader) { + Parcelable.Creator<T> creator = readParcelableCreator(loader); + if (creator == null) { + return null; + } + if (creator instanceof Parcelable.ClassLoaderCreator<?>) { + return ((Parcelable.ClassLoaderCreator<T>)creator).createFromParcel(this, loader); + } + return creator.createFromParcel(this); + } + + /** @hide */ + public final <T extends Parcelable> T readCreator(Parcelable.Creator<T> creator, + ClassLoader loader) { + if (creator instanceof Parcelable.ClassLoaderCreator<?>) { + return ((Parcelable.ClassLoaderCreator<T>)creator).createFromParcel(this, loader); + } + return creator.createFromParcel(this); + } + + /** @hide */ + public final <T extends Parcelable> Parcelable.Creator<T> readParcelableCreator( + ClassLoader loader) { String name = readString(); if (name == null) { return null; @@ -2087,6 +2115,10 @@ public final class Parcel { + "Parcelable.Creator object called " + " CREATOR on class " + name); } + catch (NullPointerException e) { + throw new BadParcelableException("Parcelable protocol requires " + + "the CREATOR object to be static on class " + name); + } if (creator == null) { throw new BadParcelableException("Parcelable protocol requires a " + "Parcelable.Creator object called " @@ -2097,10 +2129,7 @@ public final class Parcel { } } - if (creator instanceof Parcelable.ClassLoaderCreator<?>) { - return ((Parcelable.ClassLoaderCreator<T>)creator).createFromParcel(this, loader); - } - return creator.createFromParcel(this); + return creator; } /** diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java index ec660ee..3de362c 100644 --- a/core/java/android/os/ParcelFileDescriptor.java +++ b/core/java/android/os/ParcelFileDescriptor.java @@ -108,13 +108,6 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { public static ParcelFileDescriptor open(File file, int mode) throws FileNotFoundException { String path = file.getPath(); - SecurityManager security = System.getSecurityManager(); - if (security != null) { - security.checkRead(path); - if ((mode&MODE_WRITE_ONLY) != 0) { - security.checkWrite(path); - } - } if ((mode&MODE_READ_WRITE) == 0) { throw new IllegalArgumentException( diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java index 05099fb..facab4c 100644 --- a/core/java/android/os/Process.java +++ b/core/java/android/os/Process.java @@ -806,7 +806,15 @@ public class Process { */ public static final native void setProcessGroup(int pid, int group) throws IllegalArgumentException, SecurityException; - + + /** + * Return the scheduling group of requested process. + * + * @hide + */ + public static final native int getProcessGroup(int pid) + throws IllegalArgumentException, SecurityException; + /** * Set the priority of the calling thread, based on Linux priorities. See * {@link #setThreadPriority(int, int)} for more information. diff --git a/core/java/android/os/SchedulingPolicyService.java b/core/java/android/os/SchedulingPolicyService.java deleted file mode 100644 index a3fede6..0000000 --- a/core/java/android/os/SchedulingPolicyService.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.os; - -import android.content.Context; -import android.content.pm.PackageManager; -import android.os.Binder; -import android.os.Process; -import android.util.Log; - -/** - * The implementation of the scheduling policy service interface. - * - * @hide - */ -public class SchedulingPolicyService extends ISchedulingPolicyService.Stub { - - private static final String TAG = "SchedulingPolicyService"; - - // Minimum and maximum values allowed for requestPriority parameter prio - private static final int PRIORITY_MIN = 1; - private static final int PRIORITY_MAX = 3; - - public SchedulingPolicyService() { - } - - public int requestPriority(int pid, int tid, int prio) { - //Log.i(TAG, "requestPriority(pid=" + pid + ", tid=" + tid + ", prio=" + prio + ")"); - - // Verify that caller is mediaserver, priority is in range, and that the - // callback thread specified by app belongs to the app that called mediaserver. - // Once we've verified that the caller is mediaserver, we can trust the pid but - // we can't trust the tid. No need to explicitly check for pid == 0 || tid == 0, - // since if not the case then the getThreadGroupLeader() test will also fail. - if (Binder.getCallingUid() != Process.MEDIA_UID || prio < PRIORITY_MIN || - prio > PRIORITY_MAX || Process.getThreadGroupLeader(tid) != pid) { - return PackageManager.PERMISSION_DENIED; - } - try { - // make good use of our CAP_SYS_NICE capability - Process.setThreadGroup(tid, Binder.getCallingPid() == pid ? - Process.THREAD_GROUP_AUDIO_SYS : Process.THREAD_GROUP_AUDIO_APP); - // must be in this order or it fails the schedulability constraint - Process.setThreadScheduler(tid, Process.SCHED_FIFO, prio); - } catch (RuntimeException e) { - return PackageManager.PERMISSION_DENIED; - } - return PackageManager.PERMISSION_GRANTED; - } - -} diff --git a/core/java/android/os/StatFs.java b/core/java/android/os/StatFs.java index ca7fdba..60ec0d7 100644 --- a/core/java/android/os/StatFs.java +++ b/core/java/android/os/StatFs.java @@ -65,6 +65,14 @@ public class StatFs { } /** + * The size, in bytes, of a block on the file system. This corresponds to + * the Unix {@code statfs.f_bsize} field. + */ + public long getBlockSizeLong() { + return mStat.f_bsize; + } + + /** * The total number of blocks on the file system. This corresponds to the * Unix {@code statfs.f_blocks} field. */ @@ -73,6 +81,14 @@ public class StatFs { } /** + * The size, in bytes, of a block on the file system. This corresponds to + * the Unix {@code statfs.f_bsize} field. + */ + public long getBlockCountLong() { + return mStat.f_blocks; + } + + /** * The total number of blocks that are free on the file system, including * reserved blocks (that are not available to normal applications). This * corresponds to the Unix {@code statfs.f_bfree} field. Most applications @@ -83,10 +99,44 @@ public class StatFs { } /** + * The total number of blocks that are free on the file system, including + * reserved blocks (that are not available to normal applications). This + * corresponds to the Unix {@code statfs.f_bfree} field. Most applications + * will want to use {@link #getAvailableBlocks()} instead. + */ + public long getFreeBlocksLong() { + return mStat.f_bfree; + } + + /** + * The number of bytes that are free on the file system, including + * reserved blocks (that are not available to normal applications). + */ + public long getFreeBytes() { + return mStat.f_bfree * mStat.f_bsize; + } + + /** * The number of blocks that are free on the file system and available to * applications. This corresponds to the Unix {@code statfs.f_bavail} field. */ public int getAvailableBlocks() { return (int) mStat.f_bavail; } + + /** + * The number of blocks that are free on the file system and available to + * applications. This corresponds to the Unix {@code statfs.f_bavail} field. + */ + public long getAvailableBlocksLong() { + return mStat.f_bavail; + } + + /** + * The number of bytes that are free on the file system and available to + * applications. + */ + public long getAvailableBytes() { + return mStat.f_bavail * mStat.f_bsize; + } } diff --git a/core/java/android/os/SystemVibrator.java b/core/java/android/os/SystemVibrator.java index 7c5a47e..e66fb28 100644 --- a/core/java/android/os/SystemVibrator.java +++ b/core/java/android/os/SystemVibrator.java @@ -16,6 +16,8 @@ package android.os; +import android.app.ActivityThread; +import android.content.Context; import android.util.Log; /** @@ -26,10 +28,18 @@ import android.util.Log; public class SystemVibrator extends Vibrator { private static final String TAG = "Vibrator"; + private final String mPackageName; private final IVibratorService mService; private final Binder mToken = new Binder(); public SystemVibrator() { + mPackageName = ActivityThread.currentPackageName(); + mService = IVibratorService.Stub.asInterface( + ServiceManager.getService("vibrator")); + } + + public SystemVibrator(Context context) { + mPackageName = context.getBasePackageName(); mService = IVibratorService.Stub.asInterface( ServiceManager.getService("vibrator")); } @@ -49,19 +59,35 @@ public class SystemVibrator extends Vibrator { @Override public void vibrate(long milliseconds) { + vibrate(Process.myUid(), mPackageName, milliseconds); + } + + @Override + public void vibrate(long[] pattern, int repeat) { + vibrate(Process.myUid(), mPackageName, pattern, repeat); + } + + /** + * @hide + */ + @Override + public void vibrate(int owningUid, String owningPackage, long milliseconds) { if (mService == null) { Log.w(TAG, "Failed to vibrate; no vibrator service."); return; } try { - mService.vibrate(milliseconds, mToken); + mService.vibrate(owningUid, owningPackage, milliseconds, mToken); } catch (RemoteException e) { Log.w(TAG, "Failed to vibrate.", e); } } + /** + * @hide + */ @Override - public void vibrate(long[] pattern, int repeat) { + public void vibrate(int owningUid, String owningPackage, long[] pattern, int repeat) { if (mService == null) { Log.w(TAG, "Failed to vibrate; no vibrator service."); return; @@ -71,7 +97,7 @@ public class SystemVibrator extends Vibrator { // anyway if (repeat < pattern.length) { try { - mService.vibratePattern(pattern, repeat, mToken); + mService.vibratePattern(owningUid, owningPackage, pattern, repeat, mToken); } catch (RemoteException e) { Log.w(TAG, "Failed to vibrate.", e); } diff --git a/core/java/android/os/Trace.java b/core/java/android/os/Trace.java index 0ca9183..27ed6b6 100644 --- a/core/java/android/os/Trace.java +++ b/core/java/android/os/Trace.java @@ -31,7 +31,7 @@ import android.util.Log; public final class Trace { private static final String TAG = "Trace"; - // These tags must be kept in sync with frameworks/native/include/utils/Trace.h. + // These tags must be kept in sync with system/core/include/cutils/trace.h. public static final long TRACE_TAG_NEVER = 0; public static final long TRACE_TAG_ALWAYS = 1L << 0; public static final long TRACE_TAG_GRAPHICS = 1L << 1; @@ -44,12 +44,13 @@ public final class Trace { public static final long TRACE_TAG_AUDIO = 1L << 8; public static final long TRACE_TAG_VIDEO = 1L << 9; public static final long TRACE_TAG_CAMERA = 1L << 10; + public static final long TRACE_TAG_HAL = 1L << 11; private static final long TRACE_TAG_NOT_READY = 1L << 63; public static final int TRACE_FLAGS_START_BIT = 1; public static final String[] TRACE_TAGS = { "Graphics", "Input", "View", "WebView", "Window Manager", - "Activity Manager", "Sync Manager", "Audio", "Video", "Camera", + "Activity Manager", "Sync Manager", "Audio", "Video", "Camera", "HAL", }; public static final String PROPERTY_TRACE_TAG_ENABLEFLAGS = "debug.atrace.tags.enableflags"; diff --git a/core/java/android/os/UserHandle.java b/core/java/android/os/UserHandle.java index cc96152..d205253 100644 --- a/core/java/android/os/UserHandle.java +++ b/core/java/android/os/UserHandle.java @@ -16,6 +16,8 @@ package android.os; +import java.io.PrintWriter; + /** * Representation of a user on the device. */ @@ -152,6 +154,50 @@ public final class UserHandle implements Parcelable { } /** + * Generate a text representation of the uid, breaking out its individual + * components -- user, app, isolated, etc. + * @hide + */ + public static void formatUid(StringBuilder sb, int uid) { + if (uid < Process.FIRST_APPLICATION_UID) { + sb.append(uid); + } else { + sb.append('u'); + sb.append(getUserId(uid)); + final int appId = getAppId(uid); + if (appId >= Process.FIRST_ISOLATED_UID && appId <= Process.LAST_ISOLATED_UID) { + sb.append('i'); + sb.append(appId - Process.FIRST_ISOLATED_UID); + } else { + sb.append('a'); + sb.append(appId); + } + } + } + + /** + * Generate a text representation of the uid, breaking out its individual + * components -- user, app, isolated, etc. + * @hide + */ + public static void formatUid(PrintWriter pw, int uid) { + if (uid < Process.FIRST_APPLICATION_UID) { + pw.print(uid); + } else { + pw.print('u'); + pw.print(getUserId(uid)); + final int appId = getAppId(uid); + if (appId >= Process.FIRST_ISOLATED_UID && appId <= Process.LAST_ISOLATED_UID) { + pw.print('i'); + pw.print(appId - Process.FIRST_ISOLATED_UID); + } else { + pw.print('a'); + pw.print(appId); + } + } + } + + /** * Returns the user id of the current process * @return user id of the current process * @hide diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index d73f99a..51e3e7c 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -15,15 +15,15 @@ */ package android.os; -import com.android.internal.R; - import android.app.ActivityManagerNative; import android.content.Context; import android.content.pm.UserInfo; -import android.graphics.Bitmap; import android.content.res.Resources; +import android.graphics.Bitmap; import android.util.Log; +import com.android.internal.R; + import java.util.List; /** @@ -35,6 +35,50 @@ public class UserManager { private final IUserManager mService; private final Context mContext; + /** + * @hide + * Key for user restrictions. Specifies if a user is allowed to add or remove accounts. + * Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String ALLOW_MODIFY_ACCOUNTS = "modify_accounts"; + + /** + * @hide + * Key for user restrictions. Specifies if a user is allowed to change Wi-Fi access points. + * Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String ALLOW_CONFIG_WIFI = "config_wifi"; + + /** + * @hide + * Key for user restrictions. Specifies if a user is allowed to install applications. + * Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String ALLOW_INSTALL_APPS = "install_apps"; + + /** + * @hide + * Key for user restrictions. Specifies if a user is allowed to uninstall applications. + * Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String ALLOW_UNINSTALL_APPS = "uninstall_apps"; + + /** @hide * + * Key for user restrictions. Specifies if a user is allowed to toggle location sharing. + * Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String ALLOW_CONFIG_LOCATION_ACCESS = "config_location_access"; + /** @hide */ public UserManager(Context context, IUserManager service) { mService = service; @@ -50,11 +94,11 @@ public class UserManager { return getMaxSupportedUsers() > 1; } - /** + /** * Returns the user handle for the user that this application is running for. * @return the user handle of the user making this call. * @hide - * */ + */ public int getUserHandle() { return UserHandle.myUserId(); } @@ -132,6 +176,42 @@ public class UserManager { } } + /** @hide */ + public Bundle getUserRestrictions() { + return getUserRestrictions(Process.myUserHandle()); + } + + /** @hide */ + public Bundle getUserRestrictions(UserHandle userHandle) { + try { + return mService.getUserRestrictions(userHandle.getIdentifier()); + } catch (RemoteException re) { + Log.w(TAG, "Could not get user restrictions", re); + return Bundle.EMPTY; + } + } + + /** @hide */ + public void setUserRestrictions(Bundle restrictions) { + setUserRestrictions(restrictions, Process.myUserHandle()); + } + + /** @hide */ + public void setUserRestrictions(Bundle restrictions, UserHandle userHandle) { + try { + mService.setUserRestrictions(restrictions, userHandle.getIdentifier()); + } catch (RemoteException re) { + Log.w(TAG, "Could not set user restrictions", re); + } + } + + /** @hide */ + public void setUserRestriction(String key, boolean value, UserHandle userHandle) { + Bundle bundle = getUserRestrictions(userHandle); + bundle.putBoolean(key, value); + setUserRestrictions(bundle, userHandle); + } + /** * Return the serial number for a user. This is a device-unique * number assigned to that user; if the user is deleted and then a new @@ -368,4 +448,12 @@ public class UserManager { } return -1; } + + /** + * Returns whether the current user is allow to toggle location sharing settings. + * @hide + */ + public boolean isLocationSharingToggleAllowed() { + return getUserRestrictions().getBoolean(ALLOW_CONFIG_LOCATION_ACCESS); + } } diff --git a/core/java/android/os/Vibrator.java b/core/java/android/os/Vibrator.java index b67be4b..6650fca 100644 --- a/core/java/android/os/Vibrator.java +++ b/core/java/android/os/Vibrator.java @@ -73,6 +73,20 @@ public abstract class Vibrator { public abstract void vibrate(long[] pattern, int repeat); /** + * @hide + * Like {@link #vibrate(long)}, but allowing the caller to specify that + * the vibration is owned by someone else. + */ + public abstract void vibrate(int owningUid, String owningPackage, long milliseconds); + + /** + * @hide + * Like {@link #vibrate(long[], int)}, but allowing the caller to specify that + * the vibration is owned by someone else. + */ + public abstract void vibrate(int owningUid, String owningPackage, long[] pattern, int repeat); + + /** * Turn the vibrator off. * <p>This method requires the caller to hold the permission * {@link android.Manifest.permission#VIBRATE}. diff --git a/core/java/android/os/WorkSource.java b/core/java/android/os/WorkSource.java index ba77df7..b79bdee 100644 --- a/core/java/android/os/WorkSource.java +++ b/core/java/android/os/WorkSource.java @@ -1,6 +1,6 @@ package android.os; -import com.android.internal.util.ArrayUtils; +import android.util.Log; import java.util.Arrays; @@ -10,8 +10,12 @@ import java.util.Arrays; * defined; this is an opaque container. */ public class WorkSource implements Parcelable { + static final String TAG = "WorkSource"; + static final boolean DEBUG = false; + int mNum; int[] mUids; + String[] mNames; /** * Internal statics to avoid object allocations in some operations. @@ -47,8 +51,10 @@ public class WorkSource implements Parcelable { mNum = orig.mNum; if (orig.mUids != null) { mUids = orig.mUids.clone(); + mNames = orig.mNames != null ? orig.mNames.clone() : null; } else { mUids = null; + mNames = null; } } @@ -56,11 +62,23 @@ public class WorkSource implements Parcelable { public WorkSource(int uid) { mNum = 1; mUids = new int[] { uid, 0 }; + mNames = null; + } + + /** @hide */ + public WorkSource(int uid, String name) { + if (name == null) { + throw new NullPointerException("Name can't be null"); + } + mNum = 1; + mUids = new int[] { uid, 0 }; + mNames = new String[] { name, null }; } WorkSource(Parcel in) { mNum = in.readInt(); mUids = in.createIntArray(); + mNames = in.createStringArray(); } /** @hide */ @@ -73,6 +91,11 @@ public class WorkSource implements Parcelable { return mUids[index]; } + /** @hide */ + public String getName(int index) { + return mNames != null ? mNames[index] : null; + } + /** * Clear this WorkSource to be empty. */ @@ -91,6 +114,11 @@ public class WorkSource implements Parcelable { for (int i = 0; i < mNum; i++) { result = ((result << 4) | (result >>> 28)) ^ mUids[i]; } + if (mNames != null) { + for (int i = 0; i < mNum; i++) { + result = ((result << 4) | (result >>> 28)) ^ mNames[i].hashCode(); + } + } return result; } @@ -106,10 +134,15 @@ public class WorkSource implements Parcelable { } final int[] uids1 = mUids; final int[] uids2 = other.mUids; + final String[] names1 = mNames; + final String[] names2 = other.mNames; for (int i=0; i<N; i++) { if (uids1[i] != uids2[i]) { return true; } + if (names1 != null && names2 != null && !names1[i].equals(names2[i])) { + return true; + } } return false; } @@ -131,8 +164,18 @@ public class WorkSource implements Parcelable { } else { mUids = other.mUids.clone(); } + if (other.mNames != null) { + if (mNames != null && mNames.length >= mNum) { + System.arraycopy(other.mNames, 0, mNames, 0, mNum); + } else { + mNames = other.mNames.clone(); + } + } else { + mNames = null; + } } else { mUids = null; + mNames = null; } } @@ -141,6 +184,22 @@ public class WorkSource implements Parcelable { mNum = 1; if (mUids == null) mUids = new int[2]; mUids[0] = uid; + mNames = null; + } + + /** @hide */ + public void set(int uid, String name) { + if (name == null) { + throw new NullPointerException("Name can't be null"); + } + mNum = 1; + if (mUids == null) { + mUids = new int[2]; + mNames = new String[2]; + } + mUids[0] = uid; + mNames[0] = name; + mNames = null; } /** @hide */ @@ -182,10 +241,49 @@ public class WorkSource implements Parcelable { /** @hide */ public boolean add(int uid) { - synchronized (sTmpWorkSource) { - sTmpWorkSource.mUids[0] = uid; - return updateLocked(sTmpWorkSource, false, false); + if (mNum <= 0) { + mNames = null; + insert(0, uid); + return true; + } + if (mNames != null) { + throw new IllegalArgumentException("Adding without name to named " + this); + } + int i = Arrays.binarySearch(mUids, 0, mNum, uid); + if (DEBUG) Log.d(TAG, "Adding uid " + uid + " to " + this + ": binsearch res = " + i); + if (i >= 0) { + return false; } + insert(-i-1, uid); + return true; + } + + /** @hide */ + public boolean add(int uid, String name) { + if (mNum <= 0) { + insert(0, uid, name); + return true; + } + if (mNames == null) { + throw new IllegalArgumentException("Adding name to unnamed " + this); + } + int i; + for (i=0; i<mNum; i++) { + if (mUids[i] > uid) { + break; + } + if (mUids[i] == uid) { + int diff = mNames[i].compareTo(name); + if (diff > 0) { + break; + } + if (diff == 0) { + return false; + } + } + } + insert(i, uid, name); + return true; } /** @hide */ @@ -199,19 +297,102 @@ public class WorkSource implements Parcelable { } public boolean remove(WorkSource other) { + if (mNum <= 0 || other.mNum <= 0) { + return false; + } + if (mNames == null && other.mNames == null) { + return removeUids(other); + } else { + if (mNames == null) { + throw new IllegalArgumentException("Other " + other + " has names, but target " + + this + " does not"); + } + if (other.mNames == null) { + throw new IllegalArgumentException("Target " + this + " has names, but other " + + other + " does not"); + } + return removeUidsAndNames(other); + } + } + + /** @hide */ + public WorkSource stripNames() { + if (mNum <= 0) { + return new WorkSource(); + } + WorkSource result = new WorkSource(); + int lastUid = -1; + for (int i=0; i<mNum; i++) { + int uid = mUids[i]; + if (i == 0 || lastUid != uid) { + result.add(uid); + } + } + return result; + } + + private boolean removeUids(WorkSource other) { int N1 = mNum; final int[] uids1 = mUids; final int N2 = other.mNum; final int[] uids2 = other.mUids; boolean changed = false; - int i1 = 0; - for (int i2=0; i2<N2 && i1<N1; i2++) { + int i1 = 0, i2 = 0; + if (DEBUG) Log.d(TAG, "Remove " + other + " from " + this); + while (i1 < N1 && i2 < N2) { + if (DEBUG) Log.d(TAG, "Step: target @ " + i1 + " of " + N1 + ", other @ " + i2 + + " of " + N2); if (uids2[i2] == uids1[i1]) { + if (DEBUG) Log.d(TAG, "i1=" + i1 + " i2=" + i2 + " N1=" + N1 + + ": remove " + uids1[i1]); N1--; + changed = true; if (i1 < N1) System.arraycopy(uids1, i1+1, uids1, i1, N1-i1); + i2++; + } else if (uids2[i2] > uids1[i1]) { + if (DEBUG) Log.d(TAG, "i1=" + i1 + " i2=" + i2 + " N1=" + N1 + ": skip i1"); + i1++; + } else { + if (DEBUG) Log.d(TAG, "i1=" + i1 + " i2=" + i2 + " N1=" + N1 + ": skip i2"); + i2++; } - while (i1 < N1 && uids2[i2] > uids1[i1]) { + } + + mNum = N1; + + return changed; + } + + private boolean removeUidsAndNames(WorkSource other) { + int N1 = mNum; + final int[] uids1 = mUids; + final String[] names1 = mNames; + final int N2 = other.mNum; + final int[] uids2 = other.mUids; + final String[] names2 = other.mNames; + boolean changed = false; + int i1 = 0, i2 = 0; + if (DEBUG) Log.d(TAG, "Remove " + other + " from " + this); + while (i1 < N1 && i2 < N2) { + if (DEBUG) Log.d(TAG, "Step: target @ " + i1 + " of " + N1 + ", other @ " + i2 + + " of " + N2 + ": " + uids1[i1] + " " + names1[i1]); + if (uids2[i2] == uids1[i1] && names2[i2].equals(names1[i1])) { + if (DEBUG) Log.d(TAG, "i1=" + i1 + " i2=" + i2 + " N1=" + N1 + + ": remove " + uids1[i1] + " " + names1[i1]); + N1--; + changed = true; + if (i1 < N1) { + System.arraycopy(uids1, i1+1, uids1, i1, N1-i1); + System.arraycopy(names1, i1+1, names1, i1, N1-i1); + } + i2++; + } else if (uids2[i2] > uids1[i1] + || (uids2[i2] == uids1[i1] && names2[i2].compareTo(names1[i1]) > 0)) { + if (DEBUG) Log.d(TAG, "i1=" + i1 + " i2=" + i2 + " N1=" + N1 + ": skip i1"); i1++; + } else { + if (DEBUG) Log.d(TAG, "i1=" + i1 + " i2=" + i2 + " N1=" + N1 + ": skip i2"); + i2++; } } @@ -221,20 +402,50 @@ public class WorkSource implements Parcelable { } private boolean updateLocked(WorkSource other, boolean set, boolean returnNewbs) { + if (mNames == null && other.mNames == null) { + return updateUidsLocked(other, set, returnNewbs); + } else { + if (mNum > 0 && mNames == null) { + throw new IllegalArgumentException("Other " + other + " has names, but target " + + this + " does not"); + } + if (other.mNum > 0 && other.mNames == null) { + throw new IllegalArgumentException("Target " + this + " has names, but other " + + other + " does not"); + } + return updateUidsAndNamesLocked(other, set, returnNewbs); + } + } + + private static WorkSource addWork(WorkSource cur, int newUid) { + if (cur == null) { + return new WorkSource(newUid); + } + cur.insert(cur.mNum, newUid); + return cur; + } + + private boolean updateUidsLocked(WorkSource other, boolean set, boolean returnNewbs) { int N1 = mNum; int[] uids1 = mUids; final int N2 = other.mNum; final int[] uids2 = other.mUids; boolean changed = false; - int i1 = 0; - for (int i2=0; i2<N2; i2++) { - if (i1 >= N1 || uids2[i2] < uids1[i1]) { + int i1 = 0, i2 = 0; + if (DEBUG) Log.d(TAG, "Update " + this + " with " + other + " set=" + set + + " returnNewbs=" + returnNewbs); + while (i1 < N1 || i2 < N2) { + if (DEBUG) Log.d(TAG, "Step: target @ " + i1 + " of " + N1 + ", other @ " + i2 + + " of " + N2); + if (i1 >= N1 || (i2 < N2 && uids2[i2] < uids1[i1])) { // Need to insert a new uid. + if (DEBUG) Log.d(TAG, "i1=" + i1 + " i2=" + i2 + " N1=" + N1 + + ": insert " + uids2[i2]); changed = true; if (uids1 == null) { uids1 = new int[4]; uids1[0] = uids2[i2]; - } else if (i1 >= uids1.length) { + } else if (N1 >= uids1.length) { int[] newuids = new int[(uids1.length*3)/2]; if (i1 > 0) System.arraycopy(uids1, 0, newuids, 0, i1); if (i1 < N1) System.arraycopy(uids1, i1, newuids, i1+1, N1-i1); @@ -245,39 +456,37 @@ public class WorkSource implements Parcelable { uids1[i1] = uids2[i2]; } if (returnNewbs) { - if (sNewbWork == null) { - sNewbWork = new WorkSource(uids2[i2]); - } else { - sNewbWork.addLocked(uids2[i2]); - } + sNewbWork = addWork(sNewbWork, uids2[i2]); } N1++; i1++; + i2++; } else { if (!set) { // Skip uids that already exist or are not in 'other'. - do { - i1++; - } while (i1 < N1 && uids2[i2] >= uids1[i1]); + if (DEBUG) Log.d(TAG, "i1=" + i1 + " i2=" + i2 + " N1=" + N1 + ": skip"); + if (i2 < N2 && uids2[i2] == uids1[i1]) { + i2++; + } + i1++; } else { // Remove any uids that don't exist in 'other'. int start = i1; - while (i1 < N1 && uids2[i2] > uids1[i1]) { - if (sGoneWork == null) { - sGoneWork = new WorkSource(uids1[i1]); - } else { - sGoneWork.addLocked(uids1[i1]); - } + while (i1 < N1 && (i2 >= N2 || uids2[i2] > uids1[i1])) { + if (DEBUG) Log.d(TAG, "i1=" + i1 + " i2=" + i2 + ": remove " + uids1[i1]); + sGoneWork = addWork(sGoneWork, uids1[i1]); i1++; } if (start < i1) { - System.arraycopy(uids1, i1, uids1, start, i1-start); + System.arraycopy(uids1, i1, uids1, start, N1-i1); N1 -= i1-start; i1 = start; } // If there is a matching uid, skip it. - if (i1 < N1 && uids2[i1] == uids1[i1]) { + if (i1 < N1 && i2 < N2 && uids2[i2] == uids1[i1]) { + if (DEBUG) Log.d(TAG, "i1=" + i1 + " i2=" + i2 + " N1=" + N1 + ": skip"); i1++; + i2++; } } } @@ -289,21 +498,145 @@ public class WorkSource implements Parcelable { return changed; } - private void addLocked(int uid) { + /** + * Returns 0 if equal, negative if 'this' is before 'other', positive if 'this' is after 'other'. + */ + private int compare(WorkSource other, int i1, int i2) { + final int diff = mUids[i1] - other.mUids[i2]; + if (diff != 0) { + return diff; + } + return mNames[i1].compareTo(other.mNames[i2]); + } + + private static WorkSource addWork(WorkSource cur, int newUid, String newName) { + if (cur == null) { + return new WorkSource(newUid, newName); + } + cur.insert(cur.mNum, newUid, newName); + return cur; + } + + private boolean updateUidsAndNamesLocked(WorkSource other, boolean set, boolean returnNewbs) { + final int N2 = other.mNum; + final int[] uids2 = other.mUids; + String[] names2 = other.mNames; + boolean changed = false; + int i1 = 0, i2 = 0; + if (DEBUG) Log.d(TAG, "Update " + this + " with " + other + " set=" + set + + " returnNewbs=" + returnNewbs); + while (i1 < mNum || i2 < N2) { + if (DEBUG) Log.d(TAG, "Step: target @ " + i1 + " of " + mNum + ", other @ " + i2 + + " of " + N2); + int diff = -1; + if (i1 >= mNum || (i2 < N2 && (diff=compare(other, i1, i2)) > 0)) { + // Need to insert a new uid. + changed = true; + if (DEBUG) Log.d(TAG, "i1=" + i1 + " i2=" + i2 + " N1=" + mNum + + ": insert " + uids2[i2] + " " + names2[i2]); + insert(i1, uids2[i2], names2[i2]); + if (returnNewbs) { + sNewbWork = addWork(sNewbWork, uids2[i2], names2[i2]); + } + i1++; + i2++; + } else { + if (!set) { + if (DEBUG) Log.d(TAG, "i1=" + i1 + " i2=" + i2 + " N1=" + mNum + ": skip"); + if (i2 < N2 && diff == 0) { + i2++; + } + i1++; + } else { + // Remove any uids that don't exist in 'other'. + int start = i1; + while (diff < 0) { + if (DEBUG) Log.d(TAG, "i1=" + i1 + " i2=" + i2 + ": remove " + mUids[i1] + + " " + mNames[i1]); + sGoneWork = addWork(sGoneWork, mUids[i1], mNames[i1]); + i1++; + if (i1 >= mNum) { + break; + } + diff = i2 < N2 ? compare(other, i1, i2) : -1; + } + if (start < i1) { + System.arraycopy(mUids, i1, mUids, start, mNum-i1); + System.arraycopy(mNames, i1, mNames, start, mNum-i1); + mNum -= i1-start; + i1 = start; + } + // If there is a matching uid, skip it. + if (i1 < mNum && diff == 0) { + if (DEBUG) Log.d(TAG, "i1=" + i1 + " i2=" + i2 + " N1=" + mNum + ": skip"); + i1++; + i2++; + } + } + } + } + + return changed; + } + + private void insert(int index, int uid) { + if (DEBUG) Log.d(TAG, "Insert in " + this + " @ " + index + " uid " + uid); if (mUids == null) { mUids = new int[4]; mUids[0] = uid; mNum = 1; - return; - } - if (mNum >= mUids.length) { + } else if (mNum >= mUids.length) { int[] newuids = new int[(mNum*3)/2]; - System.arraycopy(mUids, 0, newuids, 0, mNum); + if (index > 0) { + System.arraycopy(mUids, 0, newuids, 0, index); + } + if (index < mNum) { + System.arraycopy(mUids, index, newuids, index+1, mNum-index); + } mUids = newuids; + mUids[index] = uid; + mNum++; + } else { + if (index < mNum) { + System.arraycopy(mUids, index, mUids, index+1, mNum-index); + } + mUids[index] = uid; + mNum++; } + } - mUids[mNum] = uid; - mNum++; + private void insert(int index, int uid, String name) { + if (mUids == null) { + mUids = new int[4]; + mUids[0] = uid; + mNames = new String[4]; + mNames[0] = name; + mNum = 1; + } else if (mNum >= mUids.length) { + int[] newuids = new int[(mNum*3)/2]; + String[] newnames = new String[(mNum*3)/2]; + if (index > 0) { + System.arraycopy(mUids, 0, newuids, 0, index); + System.arraycopy(mNames, 0, newnames, 0, index); + } + if (index < mNum) { + System.arraycopy(mUids, index, newuids, index+1, mNum-index); + System.arraycopy(mNames, index, newnames, index+1, mNum-index); + } + mUids = newuids; + mNames = newnames; + mUids[index] = uid; + mNames[index] = name; + mNum++; + } else { + if (index < mNum) { + System.arraycopy(mUids, index, mUids, index+1, mNum-index); + System.arraycopy(mNames, index, mNames, index+1, mNum-index); + } + mUids[index] = uid; + mNames[index] = name; + mNum++; + } } @Override @@ -315,19 +648,24 @@ public class WorkSource implements Parcelable { public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mNum); dest.writeIntArray(mUids); + dest.writeStringArray(mNames); } @Override public String toString() { StringBuilder result = new StringBuilder(); - result.append("{WorkSource: uids=["); + result.append("WorkSource{"); for (int i = 0; i < mNum; i++) { if (i != 0) { result.append(", "); } result.append(mUids[i]); + if (mNames != null) { + result.append(" "); + result.append(mNames[i]); + } } - result.append("]}"); + result.append("}"); return result.toString(); } diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java index 862a95c..f5e728d 100644 --- a/core/java/android/os/storage/StorageManager.java +++ b/core/java/android/os/storage/StorageManager.java @@ -16,7 +16,9 @@ package android.os.storage; -import android.app.NotificationManager; +import static android.net.TrafficStats.MB_IN_BYTES; + +import android.content.ContentResolver; import android.content.Context; import android.os.Environment; import android.os.Handler; @@ -25,6 +27,7 @@ import android.os.Message; import android.os.Parcelable; import android.os.RemoteException; import android.os.ServiceManager; +import android.provider.Settings; import android.util.Log; import android.util.SparseArray; @@ -54,20 +57,20 @@ import java.util.concurrent.atomic.AtomicInteger; * {@link android.content.Context#getSystemService(java.lang.String)} with an * argument of {@link android.content.Context#STORAGE_SERVICE}. */ - -public class StorageManager -{ +public class StorageManager { private static final String TAG = "StorageManager"; + private final ContentResolver mResolver; + /* * Our internal MountService binder reference */ - final private IMountService mMountService; + private final IMountService mMountService; /* * The looper target for callbacks */ - Looper mTgtLooper; + private final Looper mTgtLooper; /* * Target listener for binder callbacks @@ -308,16 +311,16 @@ public class StorageManager * * @hide */ - public StorageManager(Looper tgtLooper) throws RemoteException { + public StorageManager(ContentResolver resolver, Looper tgtLooper) throws RemoteException { + mResolver = resolver; + mTgtLooper = tgtLooper; mMountService = IMountService.Stub.asInterface(ServiceManager.getService("mount")); if (mMountService == null) { Log.e(TAG, "Unable to connect to mount service! - is it running yet?"); return; } - mTgtLooper = tgtLooper; } - /** * Registers a {@link android.os.storage.StorageEventListener StorageEventListener}. * @@ -610,4 +613,36 @@ public class StorageManager Log.w(TAG, "No primary storage defined"); return null; } + + private static final int DEFAULT_THRESHOLD_PERCENTAGE = 10; + private static final long DEFAULT_THRESHOLD_MAX_BYTES = 500 * MB_IN_BYTES; + private static final long DEFAULT_FULL_THRESHOLD_BYTES = MB_IN_BYTES; + + /** + * Return the number of available bytes at which the given path is + * considered running low on storage. + * + * @hide + */ + public long getStorageLowBytes(File path) { + final long lowPercent = Settings.Global.getInt(mResolver, + Settings.Global.SYS_STORAGE_THRESHOLD_PERCENTAGE, DEFAULT_THRESHOLD_PERCENTAGE); + final long lowBytes = (path.getTotalSpace() * lowPercent) / 100; + + final long maxLowBytes = Settings.Global.getLong(mResolver, + Settings.Global.SYS_STORAGE_THRESHOLD_MAX_BYTES, DEFAULT_THRESHOLD_MAX_BYTES); + + return Math.min(lowBytes, maxLowBytes); + } + + /** + * Return the number of available bytes at which the given path is + * considered full. + * + * @hide + */ + public long getStorageFullBytes(File path) { + return Settings.Global.getLong(mResolver, Settings.Global.SYS_STORAGE_FULL_THRESHOLD_BYTES, + DEFAULT_FULL_THRESHOLD_BYTES); + } } diff --git a/core/java/android/preference/PreferenceActivity.java b/core/java/android/preference/PreferenceActivity.java index 09ff7be..028317f 100644 --- a/core/java/android/preference/PreferenceActivity.java +++ b/core/java/android/preference/PreferenceActivity.java @@ -703,7 +703,13 @@ public abstract class PreferenceActivity extends ListActivity implements * show for the initial UI. */ public Header onGetInitialHeader() { - return mHeaders.get(0); + for (int i=0; i<mHeaders.size(); i++) { + Header h = mHeaders.get(i); + if (h.fragment != null) { + return h; + } + } + throw new IllegalStateException("Must have at least one header with a fragment"); } /** @@ -1167,6 +1173,9 @@ public abstract class PreferenceActivity extends ListActivity implements getFragmentManager().popBackStack(BACK_STACK_PREFS, FragmentManager.POP_BACK_STACK_INCLUSIVE); } else { + if (header.fragment == null) { + throw new IllegalStateException("can't switch to header that has no fragment"); + } int direction = mHeaders.indexOf(header) - mHeaders.indexOf(mCurHeader); switchToHeaderInner(header.fragment, header.fragmentArguments, direction); setSelectedHeader(header); diff --git a/core/java/android/provider/CalendarContract.java b/core/java/android/provider/CalendarContract.java index af6e88e9..2dd27f8 100644 --- a/core/java/android/provider/CalendarContract.java +++ b/core/java/android/provider/CalendarContract.java @@ -302,9 +302,16 @@ public final class CalendarContract { * Used to indicate that local, unsynced, changes are present. * <P>Type: INTEGER (long)</P> */ + public static final String DIRTY = "dirty"; /** + * Used in conjunction with {@link #DIRTY} to indicate what packages wrote local changes. + * <P>Type: TEXT</P> + */ + public static final String MUTATORS = "mutators"; + + /** * Whether the row has been deleted but not synced to the server. A * deleted row should be ignored. * <P> @@ -525,6 +532,7 @@ public final class CalendarContract { DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_ID); DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, DIRTY); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, MUTATORS); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, CAL_SYNC1); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, CAL_SYNC2); @@ -542,6 +550,8 @@ public final class CalendarContract { Calendars.CALENDAR_DISPLAY_NAME); DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, Calendars.CALENDAR_COLOR); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, + Calendars.CALENDAR_COLOR_KEY); DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, CALENDAR_ACCESS_LEVEL); DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, VISIBLE); DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, SYNC_EVENTS); @@ -647,6 +657,7 @@ public final class CalendarContract { * <li>{@link #CALENDAR_COLOR}</li> * <li>{@link #_SYNC_ID}</li> * <li>{@link #DIRTY}</li> + * <li>{@link #MUTATORS}</li> * <li>{@link #OWNER_ACCOUNT}</li> * <li>{@link #MAX_REMINDERS}</li> * <li>{@link #ALLOWED_REMINDERS}</li> @@ -714,6 +725,7 @@ public final class CalendarContract { ACCOUNT_TYPE, _SYNC_ID, DIRTY, + MUTATORS, OWNER_ACCOUNT, MAX_REMINDERS, ALLOWED_REMINDERS, @@ -1368,6 +1380,8 @@ public final class CalendarContract { DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, ALL_DAY); DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, ACCESS_LEVEL); DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, AVAILABILITY); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, EVENT_COLOR); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, EVENT_COLOR_KEY); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, HAS_ALARM); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, HAS_EXTENDED_PROPERTIES); @@ -1393,6 +1407,7 @@ public final class CalendarContract { DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, IS_ORGANIZER); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_ID); DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, DIRTY); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, MUTATORS); DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, LAST_SYNCED); DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, DELETED); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, SYNC_DATA1); @@ -1599,6 +1614,7 @@ public final class CalendarContract { * The following Events columns are writable only by a sync adapter * <ul> * <li>{@link #DIRTY}</li> + * <li>{@link #MUTATORS}</li> * <li>{@link #_SYNC_ID}</li> * <li>{@link #SYNC_DATA1}</li> * <li>{@link #SYNC_DATA2}</li> @@ -1688,6 +1704,7 @@ public final class CalendarContract { public static final String[] SYNC_WRITABLE_COLUMNS = new String[] { _SYNC_ID, DIRTY, + MUTATORS, SYNC_DATA1, SYNC_DATA2, SYNC_DATA3, diff --git a/core/java/android/provider/Downloads.java b/core/java/android/provider/Downloads.java index 31ad12b..9999760 100644 --- a/core/java/android/provider/Downloads.java +++ b/core/java/android/provider/Downloads.java @@ -407,6 +407,9 @@ public final class Downloads { */ public static final String COLUMN_LAST_UPDATESRC = "lastUpdateSrc"; + /** The column that is used to count retries */ + public static final String COLUMN_FAILED_CONNECTIONS = "numfailed"; + /** * default value for {@link #COLUMN_LAST_UPDATESRC}. * This value is used when this column's value is not relevant. diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 4dbc4b4..266d0d3 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -50,7 +50,6 @@ import android.speech.tts.TextToSpeech; import android.text.TextUtils; import android.util.AndroidException; import android.util.Log; -import android.view.WindowOrientationListener; import com.android.internal.widget.ILockSettings; @@ -456,6 +455,18 @@ public final class Settings { "android.settings.APPLICATION_DETAILS_SETTINGS"; /** + * @hide + * Activity Action: Show the "app ops" settings screen. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_APP_OPS_SETTINGS = + "android.settings.APP_OPS_SETTINGS"; + + /** * Activity Action: Show settings for system update functionality. * <p> * In some cases, a matching Activity may not exist, so ensure you @@ -500,6 +511,9 @@ public final class Settings { * extra to the Intent with one or more syncable content provider's authorities. Only account * types which can sync with that content provider will be offered to the user. * <p> + * Account types can also be filtered by adding an {@link #EXTRA_ACCOUNT_TYPES} extra to the + * Intent with one or more account types. + * <p> * Input: Nothing. * <p> * Output: Nothing. @@ -682,8 +696,9 @@ public final class Settings { * Example: The {@link #ACTION_ADD_ACCOUNT} intent restricts the account types available based * on the authority given. */ - public static final String EXTRA_AUTHORITIES = - "authorities"; + public static final String EXTRA_AUTHORITIES = "authorities"; + + public static final String EXTRA_ACCOUNT_TYPES = "account_types"; public static final String EXTRA_INPUT_METHOD_ID = "input_method_id"; @@ -774,7 +789,7 @@ public final class Settings { arg.putString(Settings.NameValueTable.VALUE, value); arg.putInt(CALL_METHOD_USER_KEY, userHandle); IContentProvider cp = lazyGetProvider(cr); - cp.call(mCallSetCommand, name, arg); + cp.call(cr.getPackageName(), mCallSetCommand, name, arg); } catch (RemoteException e) { Log.w(TAG, "Can't set key " + name + " in " + mUri, e); return false; @@ -821,7 +836,7 @@ public final class Settings { args = new Bundle(); args.putInt(CALL_METHOD_USER_KEY, userHandle); } - Bundle b = cp.call(mCallGetCommand, name, args); + Bundle b = cp.call(cr.getPackageName(), mCallGetCommand, name, args); if (b != null) { String value = b.getPairValue(); // Don't update our cache for reads of other users' data @@ -846,7 +861,7 @@ public final class Settings { Cursor c = null; try { - c = cp.query(mUri, SELECT_VALUE, NAME_EQ_PLACEHOLDER, + c = cp.query(cr.getPackageName(), mUri, SELECT_VALUE, NAME_EQ_PLACEHOLDER, new String[]{name}, null, null); if (c == null) { Log.w(TAG, "Can't get key " + name + " from " + mUri); @@ -2610,6 +2625,7 @@ public final class Settings { MOVED_TO_GLOBAL.add(Settings.Global.ADB_ENABLED); MOVED_TO_GLOBAL.add(Settings.Global.ASSISTED_GPS_ENABLED); MOVED_TO_GLOBAL.add(Settings.Global.BLUETOOTH_ON); + MOVED_TO_GLOBAL.add(Settings.Global.BUGREPORT_IN_POWER_MENU); MOVED_TO_GLOBAL.add(Settings.Global.CDMA_CELL_BROADCAST_SMS); MOVED_TO_GLOBAL.add(Settings.Global.CDMA_ROAMING_MODE); MOVED_TO_GLOBAL.add(Settings.Global.CDMA_SUBSCRIPTION_MODE); @@ -2659,13 +2675,6 @@ public final class Settings { MOVED_TO_GLOBAL.add(Settings.Global.TETHER_DUN_APN); MOVED_TO_GLOBAL.add(Settings.Global.TETHER_DUN_REQUIRED); MOVED_TO_GLOBAL.add(Settings.Global.TETHER_SUPPORTED); - MOVED_TO_GLOBAL.add(Settings.Global.THROTTLE_HELP_URI); - MOVED_TO_GLOBAL.add(Settings.Global.THROTTLE_MAX_NTP_CACHE_AGE_SEC); - MOVED_TO_GLOBAL.add(Settings.Global.THROTTLE_NOTIFICATION_TYPE); - MOVED_TO_GLOBAL.add(Settings.Global.THROTTLE_POLLING_SEC); - MOVED_TO_GLOBAL.add(Settings.Global.THROTTLE_RESET_DAY); - MOVED_TO_GLOBAL.add(Settings.Global.THROTTLE_THRESHOLD_BYTES); - MOVED_TO_GLOBAL.add(Settings.Global.THROTTLE_VALUE_KBITSPS); MOVED_TO_GLOBAL.add(Settings.Global.USB_MASS_STORAGE_ENABLED); MOVED_TO_GLOBAL.add(Settings.Global.USE_GOOGLE_MAIL); MOVED_TO_GLOBAL.add(Settings.Global.WEB_AUTOFILL_QUERY_URL); @@ -3081,8 +3090,10 @@ public final class Settings { /** * When the user has enable the option to have a "bug report" command * in the power menu. + * @deprecated Use {@link android.provider.Settings.Global#BUGREPORT_IN_POWER_MENU} instead * @hide */ + @Deprecated public static final String BUGREPORT_IN_POWER_MENU = "bugreport_in_power_menu"; /** @@ -4037,7 +4048,7 @@ public final class Settings { * @hide */ public static final String[] SETTINGS_TO_BACKUP = { - BUGREPORT_IN_POWER_MENU, + BUGREPORT_IN_POWER_MENU, // moved to global ALLOW_MOCK_LOCATION, PARENTAL_CONTROL_ENABLED, PARENTAL_CONTROL_REDIRECT_URL, @@ -4316,6 +4327,13 @@ public final class Settings { public static final String STAY_ON_WHILE_PLUGGED_IN = "stay_on_while_plugged_in"; /** + * When the user has enable the option to have a "bug report" command + * in the power menu. + * @hide + */ + public static final String BUGREPORT_IN_POWER_MENU = "bugreport_in_power_menu"; + + /** * Whether ADB is enabled. */ public static final String ADB_ENABLED = "adb_enabled"; @@ -4685,50 +4703,6 @@ public final class Settings { public static final String TETHER_DUN_APN = "tether_dun_apn"; /** - * The bandwidth throttle polling freqency in seconds - * @hide - */ - public static final String THROTTLE_POLLING_SEC = "throttle_polling_sec"; - - /** - * The bandwidth throttle threshold (long) - * @hide - */ - public static final String THROTTLE_THRESHOLD_BYTES = "throttle_threshold_bytes"; - - /** - * The bandwidth throttle value (kbps) - * @hide - */ - public static final String THROTTLE_VALUE_KBITSPS = "throttle_value_kbitsps"; - - /** - * The bandwidth throttle reset calendar day (1-28) - * @hide - */ - public static final String THROTTLE_RESET_DAY = "throttle_reset_day"; - - /** - * The throttling notifications we should send - * @hide - */ - public static final String THROTTLE_NOTIFICATION_TYPE = "throttle_notification_type"; - - /** - * Help URI for data throttling policy - * @hide - */ - public static final String THROTTLE_HELP_URI = "throttle_help_uri"; - - /** - * The length of time in Sec that we allow our notion of NTP time - * to be cached before we refresh it - * @hide - */ - public static final String THROTTLE_MAX_NTP_CACHE_AGE_SEC = - "throttle_max_ntp_cache_age_sec"; - - /** * USB Mass Storage Enabled */ public static final String USB_MASS_STORAGE_ENABLED = "usb_mass_storage_enabled"; @@ -5359,6 +5333,7 @@ public final class Settings { * @hide */ public static final String[] SETTINGS_TO_BACKUP = { + BUGREPORT_IN_POWER_MENU, STAY_ON_WHILE_PLUGGED_IN, MODE_RINGER, AUTO_TIME, diff --git a/core/java/android/server/package.html b/core/java/android/server/package.html deleted file mode 100644 index c9f96a6..0000000 --- a/core/java/android/server/package.html +++ /dev/null @@ -1,5 +0,0 @@ -<body> - -{@hide} - -</body> diff --git a/core/java/android/server/search/SearchManagerService.java b/core/java/android/server/search/SearchManagerService.java deleted file mode 100644 index 1a10644..0000000 --- a/core/java/android/server/search/SearchManagerService.java +++ /dev/null @@ -1,279 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.server.search; - -import com.android.internal.content.PackageMonitor; -import com.android.internal.util.IndentingPrintWriter; - -import android.app.ActivityManager; -import android.app.ActivityManagerNative; -import android.app.AppGlobals; -import android.app.ISearchManager; -import android.app.SearchManager; -import android.app.SearchableInfo; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.IPackageManager; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.database.ContentObserver; -import android.os.Binder; -import android.os.Process; -import android.os.RemoteException; -import android.os.UserHandle; -import android.os.UserManager; -import android.provider.Settings; -import android.util.Log; -import android.util.Slog; -import android.util.SparseArray; - -import java.io.FileDescriptor; -import java.io.PrintWriter; -import java.util.List; - -/** - * The search manager service handles the search UI, and maintains a registry of searchable - * activities. - */ -public class SearchManagerService extends ISearchManager.Stub { - - // general debugging support - private static final String TAG = "SearchManagerService"; - - // Context that the service is running in. - private final Context mContext; - - // This field is initialized lazily in getSearchables(), and then never modified. - private final SparseArray<Searchables> mSearchables = new SparseArray<Searchables>(); - - /** - * Initializes the Search Manager service in the provided system context. - * Only one instance of this object should be created! - * - * @param context to use for accessing DB, window manager, etc. - */ - public SearchManagerService(Context context) { - mContext = context; - mContext.registerReceiver(new BootCompletedReceiver(), - new IntentFilter(Intent.ACTION_BOOT_COMPLETED)); - mContext.registerReceiver(new UserReceiver(), - new IntentFilter(Intent.ACTION_USER_REMOVED)); - new MyPackageMonitor().register(context, null, UserHandle.ALL, true); - } - - private Searchables getSearchables(int userId) { - long origId = Binder.clearCallingIdentity(); - try { - boolean userExists = ((UserManager) mContext.getSystemService(Context.USER_SERVICE)) - .getUserInfo(userId) != null; - if (!userExists) return null; - } finally { - Binder.restoreCallingIdentity(origId); - } - synchronized (mSearchables) { - Searchables searchables = mSearchables.get(userId); - - if (searchables == null) { - //Log.i(TAG, "Building list of searchable activities for userId=" + userId); - searchables = new Searchables(mContext, userId); - searchables.buildSearchableList(); - mSearchables.append(userId, searchables); - } - return searchables; - } - } - - private void onUserRemoved(int userId) { - if (userId != UserHandle.USER_OWNER) { - synchronized (mSearchables) { - mSearchables.remove(userId); - } - } - } - - /** - * Creates the initial searchables list after boot. - */ - private final class BootCompletedReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - new Thread() { - @Override - public void run() { - Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); - mContext.unregisterReceiver(BootCompletedReceiver.this); - getSearchables(0); - } - }.start(); - } - } - - private final class UserReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - onUserRemoved(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_OWNER)); - } - } - - /** - * Refreshes the "searchables" list when packages are added/removed. - */ - class MyPackageMonitor extends PackageMonitor { - - @Override - public void onSomePackagesChanged() { - updateSearchables(); - } - - @Override - public void onPackageModified(String pkg) { - updateSearchables(); - } - - private void updateSearchables() { - final int changingUserId = getChangingUserId(); - synchronized (mSearchables) { - // Update list of searchable activities - for (int i = 0; i < mSearchables.size(); i++) { - if (changingUserId == mSearchables.keyAt(i)) { - getSearchables(mSearchables.keyAt(i)).buildSearchableList(); - break; - } - } - } - // Inform all listeners that the list of searchables has been updated. - Intent intent = new Intent(SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED); - intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING - | Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); - mContext.sendBroadcastAsUser(intent, new UserHandle(changingUserId)); - } - } - - class GlobalSearchProviderObserver extends ContentObserver { - private final ContentResolver mResolver; - - public GlobalSearchProviderObserver(ContentResolver resolver) { - super(null); - mResolver = resolver; - mResolver.registerContentObserver( - Settings.Secure.getUriFor(Settings.Secure.SEARCH_GLOBAL_SEARCH_ACTIVITY), - false /* notifyDescendants */, - this); - } - - @Override - public void onChange(boolean selfChange) { - synchronized (mSearchables) { - for (int i = 0; i < mSearchables.size(); i++) { - getSearchables(mSearchables.keyAt(i)).buildSearchableList(); - } - } - Intent intent = new Intent(SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED); - intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); - mContext.sendBroadcastAsUser(intent, UserHandle.ALL); - } - - } - - // - // Searchable activities API - // - - /** - * Returns the SearchableInfo for a given activity. - * - * @param launchActivity The activity from which we're launching this search. - * @return Returns a SearchableInfo record describing the parameters of the search, - * or null if no searchable metadata was available. - */ - public SearchableInfo getSearchableInfo(final ComponentName launchActivity) { - if (launchActivity == null) { - Log.e(TAG, "getSearchableInfo(), activity == null"); - return null; - } - return getSearchables(UserHandle.getCallingUserId()).getSearchableInfo(launchActivity); - } - - /** - * Returns a list of the searchable activities that can be included in global search. - */ - public List<SearchableInfo> getSearchablesInGlobalSearch() { - return getSearchables(UserHandle.getCallingUserId()).getSearchablesInGlobalSearchList(); - } - - public List<ResolveInfo> getGlobalSearchActivities() { - return getSearchables(UserHandle.getCallingUserId()).getGlobalSearchActivities(); - } - - /** - * Gets the name of the global search activity. - */ - public ComponentName getGlobalSearchActivity() { - return getSearchables(UserHandle.getCallingUserId()).getGlobalSearchActivity(); - } - - /** - * Gets the name of the web search activity. - */ - public ComponentName getWebSearchActivity() { - return getSearchables(UserHandle.getCallingUserId()).getWebSearchActivity(); - } - - @Override - public ComponentName getAssistIntent(int userHandle) { - try { - userHandle = ActivityManager.handleIncomingUser(Binder.getCallingPid(), - Binder.getCallingUid(), userHandle, true, false, "getAssistIntent", null); - IPackageManager pm = AppGlobals.getPackageManager(); - Intent assistIntent = new Intent(Intent.ACTION_ASSIST); - ResolveInfo info = - pm.resolveIntent(assistIntent, - assistIntent.resolveTypeIfNeeded(mContext.getContentResolver()), - PackageManager.MATCH_DEFAULT_ONLY, userHandle); - if (info != null) { - return new ComponentName( - info.activityInfo.applicationInfo.packageName, - info.activityInfo.name); - } - } catch (RemoteException re) { - // Local call - Log.e(TAG, "RemoteException in getAssistIntent: " + re); - } catch (Exception e) { - Log.e(TAG, "Exception in getAssistIntent: " + e); - } - return null; - } - - @Override - public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG); - - IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " "); - synchronized (mSearchables) { - for (int i = 0; i < mSearchables.size(); i++) { - ipw.print("\nUser: "); ipw.println(mSearchables.keyAt(i)); - ipw.increaseIndent(); - mSearchables.valueAt(i).dump(fd, ipw, args); - ipw.decreaseIndent(); - } - } - } -} diff --git a/core/java/android/server/search/Searchables.java b/core/java/android/server/search/Searchables.java deleted file mode 100644 index a0095d6..0000000 --- a/core/java/android/server/search/Searchables.java +++ /dev/null @@ -1,464 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.server.search; - -import android.app.AppGlobals; -import android.app.SearchManager; -import android.app.SearchableInfo; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.ApplicationInfo; -import android.content.pm.IPackageManager; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.os.Binder; -import android.os.Bundle; -import android.os.RemoteException; -import android.provider.Settings; -import android.text.TextUtils; -import android.util.Log; - -import java.io.FileDescriptor; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; - -/** - * This class maintains the information about all searchable activities. - * This is a hidden class. - */ -public class Searchables { - - private static final String LOG_TAG = "Searchables"; - - // static strings used for XML lookups, etc. - // TODO how should these be documented for the developer, in a more structured way than - // the current long wordy javadoc in SearchManager.java ? - private static final String MD_LABEL_DEFAULT_SEARCHABLE = "android.app.default_searchable"; - private static final String MD_SEARCHABLE_SYSTEM_SEARCH = "*"; - - private Context mContext; - - private HashMap<ComponentName, SearchableInfo> mSearchablesMap = null; - private ArrayList<SearchableInfo> mSearchablesList = null; - private ArrayList<SearchableInfo> mSearchablesInGlobalSearchList = null; - // Contains all installed activities that handle the global search - // intent. - private List<ResolveInfo> mGlobalSearchActivities; - private ComponentName mCurrentGlobalSearchActivity = null; - private ComponentName mWebSearchActivity = null; - - public static String GOOGLE_SEARCH_COMPONENT_NAME = - "com.android.googlesearch/.GoogleSearch"; - public static String ENHANCED_GOOGLE_SEARCH_COMPONENT_NAME = - "com.google.android.providers.enhancedgooglesearch/.Launcher"; - - // Cache the package manager instance - final private IPackageManager mPm; - // User for which this Searchables caches information - private int mUserId; - - /** - * - * @param context Context to use for looking up activities etc. - */ - public Searchables (Context context, int userId) { - mContext = context; - mUserId = userId; - mPm = AppGlobals.getPackageManager(); - } - - /** - * Look up, or construct, based on the activity. - * - * The activities fall into three cases, based on meta-data found in - * the manifest entry: - * <ol> - * <li>The activity itself implements search. This is indicated by the - * presence of a "android.app.searchable" meta-data attribute. - * The value is a reference to an XML file containing search information.</li> - * <li>A related activity implements search. This is indicated by the - * presence of a "android.app.default_searchable" meta-data attribute. - * The value is a string naming the activity implementing search. In this - * case the factory will "redirect" and return the searchable data.</li> - * <li>No searchability data is provided. We return null here and other - * code will insert the "default" (e.g. contacts) search. - * - * TODO: cache the result in the map, and check the map first. - * TODO: it might make sense to implement the searchable reference as - * an application meta-data entry. This way we don't have to pepper each - * and every activity. - * TODO: can we skip the constructor step if it's a non-searchable? - * TODO: does it make sense to plug the default into a slot here for - * automatic return? Probably not, but it's one way to do it. - * - * @param activity The name of the current activity, or null if the - * activity does not define any explicit searchable metadata. - */ - public SearchableInfo getSearchableInfo(ComponentName activity) { - // Step 1. Is the result already hashed? (case 1) - SearchableInfo result; - synchronized (this) { - result = mSearchablesMap.get(activity); - if (result != null) return result; - } - - // Step 2. See if the current activity references a searchable. - // Note: Conceptually, this could be a while(true) loop, but there's - // no point in implementing reference chaining here and risking a loop. - // References must point directly to searchable activities. - - ActivityInfo ai = null; - try { - ai = mPm.getActivityInfo(activity, PackageManager.GET_META_DATA, mUserId); - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error getting activity info " + re); - return null; - } - String refActivityName = null; - - // First look for activity-specific reference - Bundle md = ai.metaData; - if (md != null) { - refActivityName = md.getString(MD_LABEL_DEFAULT_SEARCHABLE); - } - // If not found, try for app-wide reference - if (refActivityName == null) { - md = ai.applicationInfo.metaData; - if (md != null) { - refActivityName = md.getString(MD_LABEL_DEFAULT_SEARCHABLE); - } - } - - // Irrespective of source, if a reference was found, follow it. - if (refActivityName != null) - { - // This value is deprecated, return null - if (refActivityName.equals(MD_SEARCHABLE_SYSTEM_SEARCH)) { - return null; - } - String pkg = activity.getPackageName(); - ComponentName referredActivity; - if (refActivityName.charAt(0) == '.') { - referredActivity = new ComponentName(pkg, pkg + refActivityName); - } else { - referredActivity = new ComponentName(pkg, refActivityName); - } - - // Now try the referred activity, and if found, cache - // it against the original name so we can skip the check - synchronized (this) { - result = mSearchablesMap.get(referredActivity); - if (result != null) { - mSearchablesMap.put(activity, result); - return result; - } - } - } - - // Step 3. None found. Return null. - return null; - - } - - /** - * Builds an entire list (suitable for display) of - * activities that are searchable, by iterating the entire set of - * ACTION_SEARCH & ACTION_WEB_SEARCH intents. - * - * Also clears the hash of all activities -> searches which will - * refill as the user clicks "search". - * - * This should only be done at startup and again if we know that the - * list has changed. - * - * TODO: every activity that provides a ACTION_SEARCH intent should - * also provide searchability meta-data. There are a bunch of checks here - * that, if data is not found, silently skip to the next activity. This - * won't help a developer trying to figure out why their activity isn't - * showing up in the list, but an exception here is too rough. I would - * like to find a better notification mechanism. - * - * TODO: sort the list somehow? UI choice. - */ - public void buildSearchableList() { - // These will become the new values at the end of the method - HashMap<ComponentName, SearchableInfo> newSearchablesMap - = new HashMap<ComponentName, SearchableInfo>(); - ArrayList<SearchableInfo> newSearchablesList - = new ArrayList<SearchableInfo>(); - ArrayList<SearchableInfo> newSearchablesInGlobalSearchList - = new ArrayList<SearchableInfo>(); - - // Use intent resolver to generate list of ACTION_SEARCH & ACTION_WEB_SEARCH receivers. - List<ResolveInfo> searchList; - final Intent intent = new Intent(Intent.ACTION_SEARCH); - - long ident = Binder.clearCallingIdentity(); - try { - searchList = queryIntentActivities(intent, PackageManager.GET_META_DATA); - - List<ResolveInfo> webSearchInfoList; - final Intent webSearchIntent = new Intent(Intent.ACTION_WEB_SEARCH); - webSearchInfoList = queryIntentActivities(webSearchIntent, PackageManager.GET_META_DATA); - - // analyze each one, generate a Searchables record, and record - if (searchList != null || webSearchInfoList != null) { - int search_count = (searchList == null ? 0 : searchList.size()); - int web_search_count = (webSearchInfoList == null ? 0 : webSearchInfoList.size()); - int count = search_count + web_search_count; - for (int ii = 0; ii < count; ii++) { - // for each component, try to find metadata - ResolveInfo info = (ii < search_count) - ? searchList.get(ii) - : webSearchInfoList.get(ii - search_count); - ActivityInfo ai = info.activityInfo; - // Check first to avoid duplicate entries. - if (newSearchablesMap.get(new ComponentName(ai.packageName, ai.name)) == null) { - SearchableInfo searchable = SearchableInfo.getActivityMetaData(mContext, ai, - mUserId); - if (searchable != null) { - newSearchablesList.add(searchable); - newSearchablesMap.put(searchable.getSearchActivity(), searchable); - if (searchable.shouldIncludeInGlobalSearch()) { - newSearchablesInGlobalSearchList.add(searchable); - } - } - } - } - } - - List<ResolveInfo> newGlobalSearchActivities = findGlobalSearchActivities(); - - // Find the global search activity - ComponentName newGlobalSearchActivity = findGlobalSearchActivity( - newGlobalSearchActivities); - - // Find the web search activity - ComponentName newWebSearchActivity = findWebSearchActivity(newGlobalSearchActivity); - - // Store a consistent set of new values - synchronized (this) { - mSearchablesMap = newSearchablesMap; - mSearchablesList = newSearchablesList; - mSearchablesInGlobalSearchList = newSearchablesInGlobalSearchList; - mGlobalSearchActivities = newGlobalSearchActivities; - mCurrentGlobalSearchActivity = newGlobalSearchActivity; - mWebSearchActivity = newWebSearchActivity; - } - } finally { - Binder.restoreCallingIdentity(ident); - } - } - - /** - * Returns a sorted list of installed search providers as per - * the following heuristics: - * - * (a) System apps are given priority over non system apps. - * (b) Among system apps and non system apps, the relative ordering - * is defined by their declared priority. - */ - private List<ResolveInfo> findGlobalSearchActivities() { - // Step 1 : Query the package manager for a list - // of activities that can handle the GLOBAL_SEARCH intent. - Intent intent = new Intent(SearchManager.INTENT_ACTION_GLOBAL_SEARCH); - List<ResolveInfo> activities = - queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); - if (activities != null && !activities.isEmpty()) { - // Step 2: Rank matching activities according to our heuristics. - Collections.sort(activities, GLOBAL_SEARCH_RANKER); - } - - return activities; - } - - /** - * Finds the global search activity. - */ - private ComponentName findGlobalSearchActivity(List<ResolveInfo> installed) { - // Fetch the global search provider from the system settings, - // and if it's still installed, return it. - final String searchProviderSetting = getGlobalSearchProviderSetting(); - if (!TextUtils.isEmpty(searchProviderSetting)) { - final ComponentName globalSearchComponent = ComponentName.unflattenFromString( - searchProviderSetting); - if (globalSearchComponent != null && isInstalled(globalSearchComponent)) { - return globalSearchComponent; - } - } - - return getDefaultGlobalSearchProvider(installed); - } - - /** - * Checks whether the global search provider with a given - * component name is installed on the system or not. This deals with - * cases such as the removal of an installed provider. - */ - private boolean isInstalled(ComponentName globalSearch) { - Intent intent = new Intent(SearchManager.INTENT_ACTION_GLOBAL_SEARCH); - intent.setComponent(globalSearch); - - List<ResolveInfo> activities = queryIntentActivities(intent, - PackageManager.MATCH_DEFAULT_ONLY); - if (activities != null && !activities.isEmpty()) { - return true; - } - - return false; - } - - private static final Comparator<ResolveInfo> GLOBAL_SEARCH_RANKER = - new Comparator<ResolveInfo>() { - @Override - public int compare(ResolveInfo lhs, ResolveInfo rhs) { - if (lhs == rhs) { - return 0; - } - boolean lhsSystem = isSystemApp(lhs); - boolean rhsSystem = isSystemApp(rhs); - - if (lhsSystem && !rhsSystem) { - return -1; - } else if (rhsSystem && !lhsSystem) { - return 1; - } else { - // Either both system engines, or both non system - // engines. - // - // Note, this isn't a typo. Higher priority numbers imply - // higher priority, but are "lower" in the sort order. - return rhs.priority - lhs.priority; - } - } - }; - - /** - * @return true iff. the resolve info corresponds to a system application. - */ - private static final boolean isSystemApp(ResolveInfo res) { - return (res.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; - } - - /** - * Returns the highest ranked search provider as per the - * ranking defined in {@link #getGlobalSearchActivities()}. - */ - private ComponentName getDefaultGlobalSearchProvider(List<ResolveInfo> providerList) { - if (providerList != null && !providerList.isEmpty()) { - ActivityInfo ai = providerList.get(0).activityInfo; - return new ComponentName(ai.packageName, ai.name); - } - - Log.w(LOG_TAG, "No global search activity found"); - return null; - } - - private String getGlobalSearchProviderSetting() { - return Settings.Secure.getString(mContext.getContentResolver(), - Settings.Secure.SEARCH_GLOBAL_SEARCH_ACTIVITY); - } - - /** - * Finds the web search activity. - * - * Only looks in the package of the global search activity. - */ - private ComponentName findWebSearchActivity(ComponentName globalSearchActivity) { - if (globalSearchActivity == null) { - return null; - } - Intent intent = new Intent(Intent.ACTION_WEB_SEARCH); - intent.setPackage(globalSearchActivity.getPackageName()); - List<ResolveInfo> activities = - queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); - - if (activities != null && !activities.isEmpty()) { - ActivityInfo ai = activities.get(0).activityInfo; - // TODO: do some sanity checks here? - return new ComponentName(ai.packageName, ai.name); - } - Log.w(LOG_TAG, "No web search activity found"); - return null; - } - - private List<ResolveInfo> queryIntentActivities(Intent intent, int flags) { - List<ResolveInfo> activities = null; - try { - activities = - mPm.queryIntentActivities(intent, - intent.resolveTypeIfNeeded(mContext.getContentResolver()), - flags, mUserId); - } catch (RemoteException re) { - // Local call - } - return activities; - } - - /** - * Returns the list of searchable activities. - */ - public synchronized ArrayList<SearchableInfo> getSearchablesList() { - ArrayList<SearchableInfo> result = new ArrayList<SearchableInfo>(mSearchablesList); - return result; - } - - /** - * Returns a list of the searchable activities that can be included in global search. - */ - public synchronized ArrayList<SearchableInfo> getSearchablesInGlobalSearchList() { - return new ArrayList<SearchableInfo>(mSearchablesInGlobalSearchList); - } - - /** - * Returns a list of activities that handle the global search intent. - */ - public synchronized ArrayList<ResolveInfo> getGlobalSearchActivities() { - return new ArrayList<ResolveInfo>(mGlobalSearchActivities); - } - - /** - * Gets the name of the global search activity. - */ - public synchronized ComponentName getGlobalSearchActivity() { - return mCurrentGlobalSearchActivity; - } - - /** - * Gets the name of the web search activity. - */ - public synchronized ComponentName getWebSearchActivity() { - return mWebSearchActivity; - } - - void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - pw.println("Searchable authorities:"); - synchronized (this) { - if (mSearchablesList != null) { - for (SearchableInfo info: mSearchablesList) { - pw.print(" "); pw.println(info.getSuggestAuthority()); - } - } - } - } -} diff --git a/core/java/android/server/search/package.html b/core/java/android/server/search/package.html deleted file mode 100644 index c9f96a6..0000000 --- a/core/java/android/server/search/package.html +++ /dev/null @@ -1,5 +0,0 @@ -<body> - -{@hide} - -</body> diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index d1b23e4..71d8fb6 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -84,7 +84,7 @@ public abstract class WallpaperService extends Service { * tag. */ public static final String SERVICE_META_DATA = "android.service.wallpaper"; - + static final String TAG = "WallpaperService"; static final boolean DEBUG = false; @@ -100,7 +100,6 @@ public abstract class WallpaperService extends Service { private static final int MSG_WINDOW_MOVED = 10035; private static final int MSG_TOUCH_EVENT = 10040; - private Looper mCallbackLooper; private final ArrayList<Engine> mActiveEngines = new ArrayList<Engine>(); @@ -154,6 +153,7 @@ public abstract class WallpaperService extends Service { int mCurWindowPrivateFlags = mWindowPrivateFlags; final Rect mVisibleInsets = new Rect(); final Rect mWinFrame = new Rect(); + final Rect mOverscanInsets = new Rect(); final Rect mContentInsets = new Rect(); final Configuration mConfiguration = new Configuration(); @@ -253,7 +253,7 @@ public abstract class WallpaperService extends Service { final BaseIWindow mWindow = new BaseIWindow() { @Override - public void resized(Rect frame, Rect contentInsets, + public void resized(Rect frame, Rect overscanInsets, Rect contentInsets, Rect visibleInsets, boolean reportDraw, Configuration newConfig) { Message msg = mCaller.obtainMessageI(MSG_WINDOW_RESIZED, reportDraw ? 1 : 0); @@ -628,7 +628,7 @@ public abstract class WallpaperService extends Service { final int relayoutResult = mSession.relayout( mWindow, mWindow.mSeq, mLayout, mWidth, mHeight, - View.VISIBLE, 0, mWinFrame, mContentInsets, + View.VISIBLE, 0, mWinFrame, mOverscanInsets, mContentInsets, mVisibleInsets, mConfiguration, mSurfaceHolder.mSurface); if (DEBUG) Log.v(TAG, "New surface: " + mSurfaceHolder.mSurface @@ -1099,13 +1099,14 @@ public abstract class WallpaperService extends Service { mTarget = context; } + @Override public void attach(IWallpaperConnection conn, IBinder windowToken, int windowType, boolean isPreview, int reqWidth, int reqHeight) { new IWallpaperEngineWrapper(mTarget, conn, windowToken, windowType, isPreview, reqWidth, reqHeight); } } - + @Override public void onCreate() { super.onCreate(); @@ -1128,20 +1129,7 @@ public abstract class WallpaperService extends Service { public final IBinder onBind(Intent intent) { return new IWallpaperServiceWrapper(this); } - - /** - * This allows subclasses to change the thread that most callbacks - * occur on. Currently hidden because it is mostly needed for the - * image wallpaper (which runs in the system process and doesn't want - * to get stuck running on that seriously in use main thread). Not - * exposed right now because the semantics of this are not totally - * well defined and some callbacks can still happen on the main thread). - * @hide - */ - public void setCallbackLooper(Looper looper) { - mCallbackLooper = looper; - } - + /** * Must be implemented to return a new instance of the wallpaper's engine. * Note that multiple instances may be active at the same time, such as diff --git a/core/java/android/speech/tts/FileSynthesisCallback.java b/core/java/android/speech/tts/FileSynthesisCallback.java index 3e33e8e..ab8f82f 100644 --- a/core/java/android/speech/tts/FileSynthesisCallback.java +++ b/core/java/android/speech/tts/FileSynthesisCallback.java @@ -20,10 +20,12 @@ import android.os.FileUtils; import android.util.Log; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.nio.channels.FileChannel; /** * Speech synthesis request that writes the audio to a WAV file. @@ -39,16 +41,19 @@ class FileSynthesisCallback extends AbstractSynthesisCallback { private static final short WAV_FORMAT_PCM = 0x0001; private final Object mStateLock = new Object(); - private final File mFileName; + private int mSampleRateInHz; private int mAudioFormat; private int mChannelCount; - private RandomAccessFile mFile; + + private FileChannel mFileChannel; + + private boolean mStarted = false; private boolean mStopped = false; private boolean mDone = false; - FileSynthesisCallback(File fileName) { - mFileName = fileName; + FileSynthesisCallback(FileChannel fileChannel) { + mFileChannel = fileChannel; } @Override @@ -63,54 +68,23 @@ class FileSynthesisCallback extends AbstractSynthesisCallback { * Must be called while holding the monitor on {@link #mStateLock}. */ private void cleanUp() { - closeFileAndWidenPermissions(); - if (mFile != null) { - mFileName.delete(); - } + closeFile(); } /** * Must be called while holding the monitor on {@link #mStateLock}. */ - private void closeFileAndWidenPermissions() { + private void closeFile() { try { - if (mFile != null) { - mFile.close(); - mFile = null; + if (mFileChannel != null) { + mFileChannel.close(); + mFileChannel = null; } } catch (IOException ex) { - Log.e(TAG, "Failed to close " + mFileName + ": " + ex); - } - - try { - // Make the written file readable and writeable by everyone. - // This allows the app that requested synthesis to read the file. - // - // Note that the directory this file was written must have already - // been world writeable in order it to have been - // written to in the first place. - FileUtils.setPermissions(mFileName.getAbsolutePath(), 0666, -1, -1); //-rw-rw-rw - } catch (SecurityException se) { - Log.e(TAG, "Security exception setting rw permissions on : " + mFileName); - } - } - - /** - * Checks whether a given file exists, and deletes it if it does. - */ - private boolean maybeCleanupExistingFile(File file) { - if (file.exists()) { - Log.v(TAG, "File " + file + " exists, deleting."); - if (!file.delete()) { - Log.e(TAG, "Failed to delete " + file); - return false; - } + Log.e(TAG, "Failed to close output file descriptor", ex); } - - return true; } - @Override public int getMaxBufferSize() { return MAX_AUDIO_BUFFER_SIZE; @@ -132,25 +106,20 @@ class FileSynthesisCallback extends AbstractSynthesisCallback { if (DBG) Log.d(TAG, "Request has been aborted."); return TextToSpeech.ERROR; } - if (mFile != null) { + if (mStarted) { cleanUp(); throw new IllegalArgumentException("FileSynthesisRequest.start() called twice"); } - - if (!maybeCleanupExistingFile(mFileName)) { - return TextToSpeech.ERROR; - } - + mStarted = true; mSampleRateInHz = sampleRateInHz; mAudioFormat = audioFormat; mChannelCount = channelCount; + try { - mFile = new RandomAccessFile(mFileName, "rw"); - // Reserve space for WAV header - mFile.write(new byte[WAV_HEADER_LENGTH]); + mFileChannel.write(ByteBuffer.allocate(WAV_HEADER_LENGTH)); return TextToSpeech.SUCCESS; } catch (IOException ex) { - Log.e(TAG, "Failed to open " + mFileName + ": " + ex); + Log.e(TAG, "Failed to write wav header to output file descriptor" + ex); cleanUp(); return TextToSpeech.ERROR; } @@ -168,15 +137,15 @@ class FileSynthesisCallback extends AbstractSynthesisCallback { if (DBG) Log.d(TAG, "Request has been aborted."); return TextToSpeech.ERROR; } - if (mFile == null) { + if (mFileChannel == null) { Log.e(TAG, "File not open"); return TextToSpeech.ERROR; } try { - mFile.write(buffer, offset, length); + mFileChannel.write(ByteBuffer.wrap(buffer, offset, length)); return TextToSpeech.SUCCESS; } catch (IOException ex) { - Log.e(TAG, "Failed to write to " + mFileName + ": " + ex); + Log.e(TAG, "Failed to write to output file descriptor", ex); cleanUp(); return TextToSpeech.ERROR; } @@ -197,21 +166,21 @@ class FileSynthesisCallback extends AbstractSynthesisCallback { if (DBG) Log.d(TAG, "Request has been aborted."); return TextToSpeech.ERROR; } - if (mFile == null) { + if (mFileChannel == null) { Log.e(TAG, "File not open"); return TextToSpeech.ERROR; } try { // Write WAV header at start of file - mFile.seek(0); - int dataLength = (int) (mFile.length() - WAV_HEADER_LENGTH); - mFile.write( + mFileChannel.position(0); + int dataLength = (int) (mFileChannel.size() - WAV_HEADER_LENGTH); + mFileChannel.write( makeWavHeader(mSampleRateInHz, mAudioFormat, mChannelCount, dataLength)); - closeFileAndWidenPermissions(); + closeFile(); mDone = true; return TextToSpeech.SUCCESS; } catch (IOException ex) { - Log.e(TAG, "Failed to write to " + mFileName + ": " + ex); + Log.e(TAG, "Failed to write to output file descriptor", ex); cleanUp(); return TextToSpeech.ERROR; } @@ -226,7 +195,7 @@ class FileSynthesisCallback extends AbstractSynthesisCallback { } } - private byte[] makeWavHeader(int sampleRateInHz, int audioFormat, int channelCount, + private ByteBuffer makeWavHeader(int sampleRateInHz, int audioFormat, int channelCount, int dataLength) { // TODO: is AudioFormat.ENCODING_DEFAULT always the same as ENCODING_PCM_16BIT? int sampleSizeInBytes = (audioFormat == AudioFormat.ENCODING_PCM_8BIT ? 1 : 2); @@ -251,8 +220,9 @@ class FileSynthesisCallback extends AbstractSynthesisCallback { header.putShort(bitsPerSample); header.put(new byte[]{ 'd', 'a', 't', 'a' }); header.putInt(dataLength); + header.flip(); - return headerBuf; + return header; } } diff --git a/core/java/android/speech/tts/ITextToSpeechService.aidl b/core/java/android/speech/tts/ITextToSpeechService.aidl index ab63187..b7bc70c 100644 --- a/core/java/android/speech/tts/ITextToSpeechService.aidl +++ b/core/java/android/speech/tts/ITextToSpeechService.aidl @@ -18,6 +18,7 @@ package android.speech.tts; import android.net.Uri; import android.os.Bundle; +import android.os.ParcelFileDescriptor; import android.speech.tts.ITextToSpeechCallback; /** @@ -44,11 +45,12 @@ interface ITextToSpeechService { * @param callingInstance a binder representing the identity of the calling * TextToSpeech object. * @param text The text to synthesize. - * @param filename The file to write the synthesized audio to. + * @param fileDescriptor The file descriptor to write the synthesized audio to. Has to be + writable. * @param param Request parameters. */ - int synthesizeToFile(in IBinder callingInstance, in String text, - in String filename, in Bundle params); + int synthesizeToFileDescriptor(in IBinder callingInstance, in String text, + in ParcelFileDescriptor fileDescriptor, in Bundle params); /** * Plays an existing audio resource. @@ -97,7 +99,19 @@ interface ITextToSpeechService { * be empty too. */ String[] getLanguage(); - + + /** + * Returns a default TTS language, country and variant as set by the user. + * + * Can be called from multiple threads. + * + * @return A 3-element array, containing language (ISO 3-letter code), + * country (ISO 3-letter code) and variant used by the engine. + * The country and variant may be {@code ""}. If country is empty, then variant must + * be empty too. + */ + String[] getClientDefaultLanguage(); + /** * Checks whether the engine supports a given language. * @@ -131,6 +145,8 @@ interface ITextToSpeechService { /** * Notifies the engine that it should load a speech synthesis language. * + * @param caller a binder representing the identity of the calling + * TextToSpeech object. * @param lang ISO-3 language code. * @param country ISO-3 country code. May be empty or null. * @param variant Language variant. May be empty or null. @@ -141,13 +157,14 @@ interface ITextToSpeechService { * {@link TextToSpeech#LANG_MISSING_DATA} * {@link TextToSpeech#LANG_NOT_SUPPORTED}. */ - int loadLanguage(in String lang, in String country, in String variant); + int loadLanguage(in IBinder caller, in String lang, in String country, in String variant); /** * Sets the callback that will be notified when playback of utterance from the * given app are completed. * - * @param callingApp Package name for the app whose utterance the callback will handle. + * @param caller Instance a binder representing the identity of the calling + * TextToSpeech object. * @param cb The callback. */ void setCallback(in IBinder caller, ITextToSpeechCallback cb); diff --git a/core/java/android/speech/tts/SynthesisCallback.java b/core/java/android/speech/tts/SynthesisCallback.java index d70c371..f98bb09 100644 --- a/core/java/android/speech/tts/SynthesisCallback.java +++ b/core/java/android/speech/tts/SynthesisCallback.java @@ -22,10 +22,11 @@ package android.speech.tts; * {@link #start}, then {@link #audioAvailable} until all audio has been provided, then finally * {@link #done}. * - * * {@link #error} can be called at any stage in the synthesis process to * indicate that an error has occurred, but if the call is made after a call * to {@link #done}, it might be discarded. + * + * After {@link #start} been called, {@link #done} must be called regardless of errors. */ public interface SynthesisCallback { /** @@ -72,6 +73,8 @@ public interface SynthesisCallback { * This method should only be called on the synthesis thread, * while in {@link TextToSpeechService#onSynthesizeText}. * + * This method has to be called if {@link #start} was called. + * * @return {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}. */ public int done(); diff --git a/core/java/android/speech/tts/TextToSpeech.java b/core/java/android/speech/tts/TextToSpeech.java index 5e367cb..73d400e 100644 --- a/core/java/android/speech/tts/TextToSpeech.java +++ b/core/java/android/speech/tts/TextToSpeech.java @@ -24,13 +24,18 @@ import android.content.Intent; import android.content.ServiceConnection; import android.media.AudioManager; import android.net.Uri; +import android.os.AsyncTask; import android.os.Bundle; import android.os.IBinder; +import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.provider.Settings; import android.text.TextUtils; import android.util.Log; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -139,7 +144,10 @@ public class TextToSpeech { * Listener that will be called when the TTS service has * completed synthesizing an utterance. This is only called if the utterance * has an utterance ID (see {@link TextToSpeech.Engine#KEY_PARAM_UTTERANCE_ID}). + * + * @deprecated Use {@link UtteranceProgressListener} instead. */ + @Deprecated public interface OnUtteranceCompletedListener { /** * Called when an utterance has been synthesized. @@ -235,19 +243,28 @@ public class TextToSpeech { /** * Indicates erroneous data when checking the installation status of the resources used by * the TextToSpeech engine with the {@link #ACTION_CHECK_TTS_DATA} intent. + * + * @deprecated Use CHECK_VOICE_DATA_FAIL instead. */ + @Deprecated public static final int CHECK_VOICE_DATA_BAD_DATA = -1; /** * Indicates missing resources when checking the installation status of the resources used * by the TextToSpeech engine with the {@link #ACTION_CHECK_TTS_DATA} intent. + * + * @deprecated Use CHECK_VOICE_DATA_FAIL instead. */ + @Deprecated public static final int CHECK_VOICE_DATA_MISSING_DATA = -2; /** * Indicates missing storage volume when checking the installation status of the resources * used by the TextToSpeech engine with the {@link #ACTION_CHECK_TTS_DATA} intent. + * + * @deprecated Use CHECK_VOICE_DATA_FAIL instead. */ + @Deprecated public static final int CHECK_VOICE_DATA_MISSING_VOLUME = -3; /** @@ -283,9 +300,8 @@ public class TextToSpeech { "android.speech.tts.engine.INSTALL_TTS_DATA"; /** - * Broadcast Action: broadcast to signal the completion of the installation of - * the data files used by the synthesis engine. Success or failure is indicated in the - * {@link #EXTRA_TTS_DATA_INSTALLED} extra. + * Broadcast Action: broadcast to signal the change in the list of available + * languages or/and their features. */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_TTS_DATA_INSTALLED = @@ -298,20 +314,16 @@ public class TextToSpeech { * return one of the following codes: * {@link #CHECK_VOICE_DATA_PASS}, * {@link #CHECK_VOICE_DATA_FAIL}, - * {@link #CHECK_VOICE_DATA_BAD_DATA}, - * {@link #CHECK_VOICE_DATA_MISSING_DATA}, or - * {@link #CHECK_VOICE_DATA_MISSING_VOLUME}. * <p> Moreover, the data received in the activity result will contain the following * fields: * <ul> - * <li>{@link #EXTRA_VOICE_DATA_ROOT_DIRECTORY} which - * indicates the path to the location of the resource files,</li> - * <li>{@link #EXTRA_VOICE_DATA_FILES} which contains - * the list of all the resource files,</li> - * <li>and {@link #EXTRA_VOICE_DATA_FILES_INFO} which - * contains, for each resource file, the description of the language covered by - * the file in the xxx-YYY format, where xxx is the 3-letter ISO language code, - * and YYY is the 3-letter ISO country code.</li> + * <li>{@link #EXTRA_AVAILABLE_VOICES} which contains an ArrayList<String> of all the + * available voices. The format of each voice is: lang-COUNTRY-variant where COUNTRY and + * variant are optional (ie, "eng" or "eng-USA" or "eng-USA-FEMALE").</li> + * <li>{@link #EXTRA_UNAVAILABLE_VOICES} which contains an ArrayList<String> of all the + * unavailable voices (ones that user can install). The format of each voice is: + * lang-COUNTRY-variant where COUNTRY and variant are optional (ie, "eng" or + * "eng-USA" or "eng-USA-FEMALE").</li> * </ul> */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) @@ -319,37 +331,33 @@ public class TextToSpeech { "android.speech.tts.engine.CHECK_TTS_DATA"; /** - * Activity intent for getting some sample text to use for demonstrating TTS. + * Activity intent for getting some sample text to use for demonstrating TTS. Specific + * locale have to be requested by passing following extra parameters: + * <ul> + * <li>language</li> + * <li>country</li> + * <li>variant</li> + * </ul> * - * @hide This intent was used by engines written against the old API. - * Not sure if it should be exposed. + * Upon completion, the activity result may contain the following fields: + * <ul> + * <li>{@link #EXTRA_SAMPLE_TEXT} which contains an String with sample text.</li> + * </ul> */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_GET_SAMPLE_TEXT = "android.speech.tts.engine.GET_SAMPLE_TEXT"; - // extras for a TTS engine's check data activity /** - * Extra information received with the {@link #ACTION_CHECK_TTS_DATA} intent where - * the TextToSpeech engine specifies the path to its resources. + * Extra information received with the {@link #ACTION_GET_SAMPLE_TEXT} intent result where + * the TextToSpeech engine returns an String with sample text for requested voice */ - public static final String EXTRA_VOICE_DATA_ROOT_DIRECTORY = "dataRoot"; + public static final String EXTRA_SAMPLE_TEXT = "sampleText"; - /** - * Extra information received with the {@link #ACTION_CHECK_TTS_DATA} intent where - * the TextToSpeech engine specifies the file names of its resources under the - * resource path. - */ - public static final String EXTRA_VOICE_DATA_FILES = "dataFiles"; - - /** - * Extra information received with the {@link #ACTION_CHECK_TTS_DATA} intent where - * the TextToSpeech engine specifies the locale associated with each resource file. - */ - public static final String EXTRA_VOICE_DATA_FILES_INFO = "dataFilesInfo"; + // extras for a TTS engine's check data activity /** - * Extra information received with the {@link #ACTION_CHECK_TTS_DATA} intent where + * Extra information received with the {@link #ACTION_CHECK_TTS_DATA} intent result where * the TextToSpeech engine returns an ArrayList<String> of all the available voices. * The format of each voice is: lang-COUNTRY-variant where COUNTRY and variant are * optional (ie, "eng" or "eng-USA" or "eng-USA-FEMALE"). @@ -357,7 +365,7 @@ public class TextToSpeech { public static final String EXTRA_AVAILABLE_VOICES = "availableVoices"; /** - * Extra information received with the {@link #ACTION_CHECK_TTS_DATA} intent where + * Extra information received with the {@link #ACTION_CHECK_TTS_DATA} intent result where * the TextToSpeech engine returns an ArrayList<String> of all the unavailable voices. * The format of each voice is: lang-COUNTRY-variant where COUNTRY and variant are * optional (ie, "eng" or "eng-USA" or "eng-USA-FEMALE"). @@ -365,22 +373,63 @@ public class TextToSpeech { public static final String EXTRA_UNAVAILABLE_VOICES = "unavailableVoices"; /** + * Extra information received with the {@link #ACTION_CHECK_TTS_DATA} intent result where + * the TextToSpeech engine specifies the path to its resources. + * + * It may be used by language packages to find out where to put their data. + * + * @deprecated TTS engine implementation detail, this information has no use for + * text-to-speech API client. + */ + @Deprecated + public static final String EXTRA_VOICE_DATA_ROOT_DIRECTORY = "dataRoot"; + + /** + * Extra information received with the {@link #ACTION_CHECK_TTS_DATA} intent result where + * the TextToSpeech engine specifies the file names of its resources under the + * resource path. + * + * @deprecated TTS engine implementation detail, this information has no use for + * text-to-speech API client. + */ + @Deprecated + public static final String EXTRA_VOICE_DATA_FILES = "dataFiles"; + + /** + * Extra information received with the {@link #ACTION_CHECK_TTS_DATA} intent result where + * the TextToSpeech engine specifies the locale associated with each resource file. + * + * @deprecated TTS engine implementation detail, this information has no use for + * text-to-speech API client. + */ + @Deprecated + public static final String EXTRA_VOICE_DATA_FILES_INFO = "dataFilesInfo"; + + /** * Extra information sent with the {@link #ACTION_CHECK_TTS_DATA} intent where the * caller indicates to the TextToSpeech engine which specific sets of voice data to * check for by sending an ArrayList<String> of the voices that are of interest. * The format of each voice is: lang-COUNTRY-variant where COUNTRY and variant are * optional (ie, "eng" or "eng-USA" or "eng-USA-FEMALE"). + * + * @deprecated Redundant functionality, checking for existence of specific sets of voice + * data can be done on client side. */ + @Deprecated public static final String EXTRA_CHECK_VOICE_DATA_FOR = "checkVoiceDataFor"; // extras for a TTS engine's data installation /** - * Extra information received with the {@link #ACTION_TTS_DATA_INSTALLED} intent. + * Extra information received with the {@link #ACTION_TTS_DATA_INSTALLED} intent result. * It indicates whether the data files for the synthesis engine were successfully * installed. The installation was initiated with the {@link #ACTION_INSTALL_TTS_DATA} * intent. The possible values for this extra are * {@link TextToSpeech#SUCCESS} and {@link TextToSpeech#ERROR}. + * + * @deprecated No longer in use. If client ise interested in information about what + * changed, is should send ACTION_CHECK_TTS_DATA intent to discover available voices. */ + @Deprecated public static final String EXTRA_TTS_DATA_INSTALLED = "dataInstalled"; // keys for the parameters passed with speak commands. Hidden keys are used internally @@ -473,11 +522,16 @@ public class TextToSpeech { * for a description of how feature keys work. If set and supported by the engine * as per {@link TextToSpeech#getFeatures(Locale)}, the engine must synthesize * text on-device (without making network requests). + * + * @see TextToSpeech#speak(String, int, java.util.HashMap) + * @see TextToSpeech#synthesizeToFile(String, java.util.HashMap, String) + * @see TextToSpeech#getFeatures(java.util.Locale) */ public static final String KEY_FEATURE_EMBEDDED_SYNTHESIS = "embeddedTts"; } private final Context mContext; + private Connection mConnectingServiceConnection; private Connection mServiceConnection; private OnInitListener mInitListener; // Written from an unspecified application thread, read from @@ -553,21 +607,24 @@ public class TextToSpeech { initTts(); } - private <R> R runActionNoReconnect(Action<R> action, R errorResult, String method) { - return runAction(action, errorResult, method, false); + private <R> R runActionNoReconnect(Action<R> action, R errorResult, String method, + boolean onlyEstablishedConnection) { + return runAction(action, errorResult, method, false, onlyEstablishedConnection); } private <R> R runAction(Action<R> action, R errorResult, String method) { - return runAction(action, errorResult, method, true); + return runAction(action, errorResult, method, true, true); } - private <R> R runAction(Action<R> action, R errorResult, String method, boolean reconnect) { + private <R> R runAction(Action<R> action, R errorResult, String method, + boolean reconnect, boolean onlyEstablishedConnection) { synchronized (mStartLock) { if (mServiceConnection == null) { Log.w(TAG, method + " failed: not bound to TTS engine"); return errorResult; } - return mServiceConnection.runAction(action, errorResult, method, reconnect); + return mServiceConnection.runAction(action, errorResult, method, reconnect, + onlyEstablishedConnection); } } @@ -630,6 +687,7 @@ public class TextToSpeech { return false; } else { Log.i(TAG, "Sucessfully bound to " + engine); + mConnectingServiceConnection = connection; return true; } } @@ -653,6 +711,16 @@ public class TextToSpeech { * so the TextToSpeech engine can be cleanly stopped. */ public void shutdown() { + // Special case, we are asked to shutdown connection that did finalize its connection. + synchronized (mStartLock) { + if (mConnectingServiceConnection != null) { + mContext.unbindService(mConnectingServiceConnection); + mConnectingServiceConnection = null; + return; + } + } + + // Post connection case runActionNoReconnect(new Action<Void>() { @Override public Void run(ITextToSpeechService service) throws RemoteException { @@ -670,7 +738,7 @@ public class TextToSpeech { mCurrentEngine = null; return null; } - }, null, "shutdown"); + }, null, "shutdown", false); } /** @@ -793,10 +861,16 @@ public class TextToSpeech { } /** - * Speaks the string using the specified queuing strategy and speech - * parameters. + * Speaks the string using the specified queuing strategy and speech parameters. + * This method is asynchronous, i.e. the method just adds the request to the queue of TTS + * requests and then returns. The synthesis might not have finished (or even started!) at the + * time when this method returns. In order to reliably detect errors during synthesis, + * we recommend setting an utterance progress listener (see + * {@link #setOnUtteranceProgressListener}) and using the + * {@link Engine#KEY_PARAM_UTTERANCE_ID} parameter. * - * @param text The string of text to be spoken. + * @param text The string of text to be spoken. No longer than + * {@link #getMaxSpeechInputLength()} characters. * @param queueMode The queuing strategy to use, {@link #QUEUE_ADD} or {@link #QUEUE_FLUSH}. * @param params Parameters for the request. Can be null. * Supported parameter names: @@ -809,7 +883,7 @@ public class TextToSpeech { * the keys "com.svox.pico_foo" and "com.svox.pico:bar" will be passed to the * engine named "com.svox.pico" if it is being used. * - * @return {@link #ERROR} or {@link #SUCCESS}. + * @return {@link #ERROR} or {@link #SUCCESS} of <b>queuing</b> the speak operation. */ public int speak(final String text, final int queueMode, final HashMap<String, String> params) { return runAction(new Action<Integer>() { @@ -830,6 +904,12 @@ public class TextToSpeech { * Plays the earcon using the specified queueing mode and parameters. * The earcon must already have been added with {@link #addEarcon(String, String)} or * {@link #addEarcon(String, String, int)}. + * This method is asynchronous, i.e. the method just adds the request to the queue of TTS + * requests and then returns. The synthesis might not have finished (or even started!) at the + * time when this method returns. In order to reliably detect errors during synthesis, + * we recommend setting an utterance progress listener (see + * {@link #setOnUtteranceProgressListener}) and using the + * {@link Engine#KEY_PARAM_UTTERANCE_ID} parameter. * * @param earcon The earcon that should be played * @param queueMode {@link #QUEUE_ADD} or {@link #QUEUE_FLUSH}. @@ -842,7 +922,7 @@ public class TextToSpeech { * the keys "com.svox.pico_foo" and "com.svox.pico:bar" will be passed to the * engine named "com.svox.pico" if it is being used. * - * @return {@link #ERROR} or {@link #SUCCESS}. + * @return {@link #ERROR} or {@link #SUCCESS} of <b>queuing</b> the playEarcon operation. */ public int playEarcon(final String earcon, final int queueMode, final HashMap<String, String> params) { @@ -862,6 +942,12 @@ public class TextToSpeech { /** * Plays silence for the specified amount of time using the specified * queue mode. + * This method is asynchronous, i.e. the method just adds the request to the queue of TTS + * requests and then returns. The synthesis might not have finished (or even started!) at the + * time when this method returns. In order to reliably detect errors during synthesis, + * we recommend setting an utterance progress listener (see + * {@link #setOnUtteranceProgressListener}) and using the + * {@link Engine#KEY_PARAM_UTTERANCE_ID} parameter. * * @param durationInMs The duration of the silence. * @param queueMode {@link #QUEUE_ADD} or {@link #QUEUE_FLUSH}. @@ -873,7 +959,7 @@ public class TextToSpeech { * the keys "com.svox.pico_foo" and "com.svox.pico:bar" will be passed to the * engine named "com.svox.pico" if it is being used. * - * @return {@link #ERROR} or {@link #SUCCESS}. + * @return {@link #ERROR} or {@link #SUCCESS} of <b>queuing</b> the playSilence operation. */ public int playSilence(final long durationInMs, final int queueMode, final HashMap<String, String> params) { @@ -1005,6 +1091,24 @@ public class TextToSpeech { } /** + * Returns a Locale instance describing the language currently being used as the default + * Text-to-speech language. + * + * @return language, country (if any) and variant (if any) used by the client stored in a + * Locale instance, or {@code null} on error. + */ + public Locale getDefaultLanguage() { + return runAction(new Action<Locale>() { + @Override + public Locale run(ITextToSpeechService service) throws RemoteException { + String[] defaultLanguage = service.getClientDefaultLanguage(); + + return new Locale(defaultLanguage[0], defaultLanguage[1], defaultLanguage[2]); + } + }, null, "getDefaultLanguage"); + } + + /** * Sets the text-to-speech language. * The TTS engine will try to use the closest match to the specified * language as represented by the Locale, but there is no guarantee that the exact same Locale @@ -1031,7 +1135,8 @@ public class TextToSpeech { // the available parts. // Note that the language is not actually set here, instead it is cached so it // will be associated with all upcoming utterances. - int result = service.loadLanguage(language, country, variant); + + int result = service.loadLanguage(getCallerIdentity(), language, country, variant); if (result >= LANG_AVAILABLE){ if (result < LANG_COUNTRY_VAR_AVAILABLE) { variant = ""; @@ -1049,21 +1154,30 @@ public class TextToSpeech { } /** - * Returns a Locale instance describing the language currently being used by the TextToSpeech - * engine. + * Returns a Locale instance describing the language currently being used for synthesis + * requests sent to the TextToSpeech engine. + * + * In Android 4.2 and before (API <= 17) this function returns the language that is currently + * being used by the TTS engine. That is the last language set by this or any other + * client by a {@link TextToSpeech#setLanguage} call to the same engine. * - * @return language, country (if any) and variant (if any) used by the engine stored in a Locale - * instance, or {@code null} on error. + * In Android versions after 4.2 this function returns the language that is currently being + * used for the synthesis requests sent from this client. That is the last language set + * by a {@link TextToSpeech#setLanguage} call on this instance. + * + * @return language, country (if any) and variant (if any) used by the client stored in a + * Locale instance, or {@code null} on error. */ public Locale getLanguage() { return runAction(new Action<Locale>() { @Override - public Locale run(ITextToSpeechService service) throws RemoteException { - String[] locStrings = service.getLanguage(); - if (locStrings != null && locStrings.length == 3) { - return new Locale(locStrings[0], locStrings[1], locStrings[2]); - } - return null; + public Locale run(ITextToSpeechService service) { + /* No service call, but we're accessing mParams, hence need for + wrapping it as an Action instance */ + String lang = mParams.getString(Engine.KEY_PARAM_LANGUAGE, ""); + String country = mParams.getString(Engine.KEY_PARAM_COUNTRY, ""); + String variant = mParams.getString(Engine.KEY_PARAM_VARIANT, ""); + return new Locale(lang, country, variant); } }, null, "getLanguage"); } @@ -1089,8 +1203,15 @@ public class TextToSpeech { /** * Synthesizes the given text to a file using the specified parameters. + * This method is asynchronous, i.e. the method just adds the request to the queue of TTS + * requests and then returns. The synthesis might not have finished (or even started!) at the + * time when this method returns. In order to reliably detect errors during synthesis, + * we recommend setting an utterance progress listener (see + * {@link #setOnUtteranceProgressListener}) and using the + * {@link Engine#KEY_PARAM_UTTERANCE_ID} parameter. * - * @param text The text that should be synthesized + * @param text The text that should be synthesized. No longer than + * {@link #getMaxSpeechInputLength()} characters. * @param params Parameters for the request. Can be null. * Supported parameter names: * {@link Engine#KEY_PARAM_UTTERANCE_ID}. @@ -1101,15 +1222,36 @@ public class TextToSpeech { * @param filename Absolute file filename to write the generated audio data to.It should be * something like "/sdcard/myappsounds/mysound.wav". * - * @return {@link #ERROR} or {@link #SUCCESS}. + * @return {@link #ERROR} or {@link #SUCCESS} of <b>queuing</b> the synthesizeToFile operation. */ public int synthesizeToFile(final String text, final HashMap<String, String> params, final String filename) { return runAction(new Action<Integer>() { @Override public Integer run(ITextToSpeechService service) throws RemoteException { - return service.synthesizeToFile(getCallerIdentity(), text, filename, - getParams(params)); + ParcelFileDescriptor fileDescriptor; + int returnValue; + try { + File file = new File(filename); + if(file.exists() && !file.canWrite()) { + Log.e(TAG, "Can't write to " + filename); + return ERROR; + } + fileDescriptor = ParcelFileDescriptor.open(file, + ParcelFileDescriptor.MODE_WRITE_ONLY | + ParcelFileDescriptor.MODE_CREATE | + ParcelFileDescriptor.MODE_TRUNCATE); + returnValue = service.synthesizeToFileDescriptor(getCallerIdentity(), text, + fileDescriptor, getParams(params)); + fileDescriptor.close(); + return returnValue; + } catch (FileNotFoundException e) { + Log.e(TAG, "Opening file " + filename + " failed", e); + return ERROR; + } catch (IOException e) { + Log.e(TAG, "Closing file " + filename + " failed", e); + return ERROR; + } } }, ERROR, "synthesizeToFile"); } @@ -1253,9 +1395,13 @@ public class TextToSpeech { return mEnginesHelper.getEngines(); } - private class Connection implements ServiceConnection { private ITextToSpeechService mService; + + private SetupConnectionAsyncTask mOnSetupConnectionAsyncTask; + + private boolean mEstablished; + private final ITextToSpeechCallback.Stub mCallback = new ITextToSpeechCallback.Stub() { @Override public void onDone(String utteranceId) { @@ -1282,23 +1428,66 @@ public class TextToSpeech { } }; + private class SetupConnectionAsyncTask extends AsyncTask<Void, Void, Integer> { + private final ComponentName mName; + + public SetupConnectionAsyncTask(ComponentName name) { + mName = name; + } + + @Override + protected Integer doInBackground(Void... params) { + synchronized(mStartLock) { + if (isCancelled()) { + return null; + } + + try { + mService.setCallback(getCallerIdentity(), mCallback); + String[] defaultLanguage = mService.getClientDefaultLanguage(); + + mParams.putString(Engine.KEY_PARAM_LANGUAGE, defaultLanguage[0]); + mParams.putString(Engine.KEY_PARAM_COUNTRY, defaultLanguage[1]); + mParams.putString(Engine.KEY_PARAM_VARIANT, defaultLanguage[2]); + + Log.i(TAG, "Set up connection to " + mName); + return SUCCESS; + } catch (RemoteException re) { + Log.e(TAG, "Error connecting to service, setCallback() failed"); + return ERROR; + } + } + } + + @Override + protected void onPostExecute(Integer result) { + synchronized(mStartLock) { + if (mOnSetupConnectionAsyncTask == this) { + mOnSetupConnectionAsyncTask = null; + } + mEstablished = true; + dispatchOnInit(result); + } + } + } + @Override public void onServiceConnected(ComponentName name, IBinder service) { - Log.i(TAG, "Connected to " + name); synchronized(mStartLock) { - if (mServiceConnection != null) { - // Disconnect any previous service connection - mServiceConnection.disconnect(); + mConnectingServiceConnection = null; + + Log.i(TAG, "Connected to " + name); + + if (mOnSetupConnectionAsyncTask != null) { + mOnSetupConnectionAsyncTask.cancel(false); } - mServiceConnection = this; + mService = ITextToSpeechService.Stub.asInterface(service); - try { - mService.setCallback(getCallerIdentity(), mCallback); - dispatchOnInit(SUCCESS); - } catch (RemoteException re) { - Log.e(TAG, "Error connecting to service, setCallback() failed"); - dispatchOnInit(ERROR); - } + mServiceConnection = Connection.this; + + mEstablished = false; + mOnSetupConnectionAsyncTask = new SetupConnectionAsyncTask(name); + mOnSetupConnectionAsyncTask.execute(); } } @@ -1306,37 +1495,63 @@ public class TextToSpeech { return mCallback; } - @Override - public void onServiceDisconnected(ComponentName name) { + /** + * Clear connection related fields and cancel mOnServiceConnectedAsyncTask if set. + * + * @return true if we cancel mOnSetupConnectionAsyncTask in progress. + */ + private boolean clearServiceConnection() { synchronized(mStartLock) { + boolean result = false; + if (mOnSetupConnectionAsyncTask != null) { + result = mOnSetupConnectionAsyncTask.cancel(false); + mOnSetupConnectionAsyncTask = null; + } + mService = null; // If this is the active connection, clear it if (mServiceConnection == this) { mServiceConnection = null; } + return result; + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + Log.i(TAG, "Asked to disconnect from " + name); + if (clearServiceConnection()) { + /* We need to protect against a rare case where engine + * dies just after successful connection - and we process onServiceDisconnected + * before OnServiceConnectedAsyncTask.onPostExecute. onServiceDisconnected cancels + * OnServiceConnectedAsyncTask.onPostExecute and we don't call dispatchOnInit + * with ERROR as argument. + */ + dispatchOnInit(ERROR); } } public void disconnect() { mContext.unbindService(this); + clearServiceConnection(); + } - synchronized (mStartLock) { - mService = null; - // If this is the active connection, clear it - if (mServiceConnection == this) { - mServiceConnection = null; - } - - } + public boolean isEstablished() { + return mService != null && mEstablished; } - public <R> R runAction(Action<R> action, R errorResult, String method, boolean reconnect) { + public <R> R runAction(Action<R> action, R errorResult, String method, + boolean reconnect, boolean onlyEstablishedConnection) { synchronized (mStartLock) { try { if (mService == null) { Log.w(TAG, method + " failed: not connected to TTS engine"); return errorResult; } + if (onlyEstablishedConnection && !isEstablished()) { + Log.w(TAG, method + " failed: TTS engine connection not fully set up"); + return errorResult; + } return action.run(mService); } catch (RemoteException ex) { Log.e(TAG, method + " failed", ex); @@ -1394,4 +1609,13 @@ public class TextToSpeech { } + /** + * Limit of length of input string passed to speak and synthesizeToFile. + * + * @see #speak + * @see #synthesizeToFile + */ + public static int getMaxSpeechInputLength() { + return 4000; + } } diff --git a/core/java/android/speech/tts/TextToSpeechService.java b/core/java/android/speech/tts/TextToSpeechService.java index d124e68..1bcf3e0 100644 --- a/core/java/android/speech/tts/TextToSpeechService.java +++ b/core/java/android/speech/tts/TextToSpeechService.java @@ -26,6 +26,7 @@ import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.MessageQueue; +import android.os.ParcelFileDescriptor; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.provider.Settings; @@ -33,8 +34,8 @@ import android.speech.tts.TextToSpeech.Engine; import android.text.TextUtils; import android.util.Log; -import java.io.File; -import java.io.IOException; +import java.io.FileDescriptor; +import java.io.FileOutputStream; import java.util.HashMap; import java.util.Locale; import java.util.Set; @@ -74,7 +75,7 @@ public abstract class TextToSpeechService extends Service { private static final boolean DBG = false; private static final String TAG = "TextToSpeechService"; - private static final int MAX_SPEECH_ITEM_CHAR_LENGTH = 4000; + private static final String SYNTH_THREAD_NAME = "SynthThread"; private SynthHandler mSynthHandler; @@ -129,6 +130,8 @@ public abstract class TextToSpeechService extends Service { * * Can be called on multiple threads. * + * Its return values HAVE to be consistent with onLoadLanguage. + * * @param lang ISO-3 language code. * @param country ISO-3 country code. May be empty or null. * @param variant Language variant. May be empty or null. @@ -163,6 +166,8 @@ public abstract class TextToSpeechService extends Service { * at some point in the future. * * Can be called on multiple threads. + * In <= Android 4.2 (<= API 17) can be called on main and service binder threads. + * In > Android 4.2 (> API 17) can be called on main and synthesis threads. * * @param lang ISO-3 language code. * @param country ISO-3 country code. May be empty or null. @@ -256,7 +261,6 @@ public abstract class TextToSpeechService extends Service { } private class SynthHandler extends Handler { - private SpeechItem mCurrentSpeechItem = null; public SynthHandler(Looper looper) { @@ -275,7 +279,7 @@ public abstract class TextToSpeechService extends Service { private synchronized SpeechItem maybeRemoveCurrentSpeechItem(Object callerIdentity) { if (mCurrentSpeechItem != null && - mCurrentSpeechItem.getCallerIdentity() == callerIdentity) { + (mCurrentSpeechItem.getCallerIdentity() == callerIdentity)) { SpeechItem current = mCurrentSpeechItem; mCurrentSpeechItem = null; return current; @@ -296,7 +300,6 @@ public abstract class TextToSpeechService extends Service { if (current != null) { current.stop(); } - // The AudioPlaybackHandler will be destroyed by the caller. } @@ -306,8 +309,15 @@ public abstract class TextToSpeechService extends Service { * Called on a service binder thread. */ public int enqueueSpeechItem(int queueMode, final SpeechItem speechItem) { + UtteranceProgressDispatcher utterenceProgress = null; + if (speechItem instanceof UtteranceProgressDispatcher) { + utterenceProgress = (UtteranceProgressDispatcher) speechItem; + } + if (!speechItem.isValid()) { - speechItem.dispatchOnError(); + if (utterenceProgress != null) { + utterenceProgress.dispatchOnError(); + } return TextToSpeech.ERROR; } @@ -325,6 +335,7 @@ public abstract class TextToSpeechService extends Service { } }; Message msg = Message.obtain(this, runnable); + // The obj is used to remove all callbacks from the given app in // stopForApp(String). // @@ -334,7 +345,9 @@ public abstract class TextToSpeechService extends Service { return TextToSpeech.SUCCESS; } else { Log.w(TAG, "SynthThread has quit"); - speechItem.dispatchOnError(); + if (utterenceProgress != null) { + utterenceProgress.dispatchOnError(); + } return TextToSpeech.ERROR; } } @@ -370,7 +383,7 @@ public abstract class TextToSpeechService extends Service { } public int stopAll() { - // Stop the current speech item unconditionally. + // Stop the current speech item unconditionally . SpeechItem current = setCurrentSpeechItem(null); if (current != null) { current.stop(); @@ -393,7 +406,7 @@ public abstract class TextToSpeechService extends Service { /** * An item in the synth thread queue. */ - private abstract class SpeechItem implements UtteranceProgressDispatcher { + private abstract class SpeechItem { private final Object mCallerIdentity; protected final Bundle mParams; private final int mCallerUid; @@ -412,6 +425,15 @@ public abstract class TextToSpeechService extends Service { return mCallerIdentity; } + + public int getCallerUid() { + return mCallerUid; + } + + public int getCallerPid() { + return mCallerPid; + } + /** * Checker whether the item is valid. If this method returns false, the item should not * be played. @@ -436,6 +458,8 @@ public abstract class TextToSpeechService extends Service { return playImpl(); } + protected abstract int playImpl(); + /** * Stops the speech item. * Must not be called more than once. @@ -452,6 +476,23 @@ public abstract class TextToSpeechService extends Service { stopImpl(); } + protected abstract void stopImpl(); + + protected synchronized boolean isStopped() { + return mStopped; + } + } + + /** + * An item in the synth thread queue that process utterance. + */ + private abstract class UtteranceSpeechItem extends SpeechItem + implements UtteranceProgressDispatcher { + + public UtteranceSpeechItem(Object caller, int callerUid, int callerPid, Bundle params) { + super(caller, callerUid, callerPid, params); + } + @Override public void dispatchOnDone() { final String utteranceId = getUtteranceId(); @@ -476,22 +517,6 @@ public abstract class TextToSpeechService extends Service { } } - public int getCallerUid() { - return mCallerUid; - } - - public int getCallerPid() { - return mCallerPid; - } - - protected synchronized boolean isStopped() { - return mStopped; - } - - protected abstract int playImpl(); - - protected abstract void stopImpl(); - public int getStreamType() { return getIntParam(Engine.KEY_PARAM_STREAM, Engine.DEFAULT_STREAM); } @@ -519,9 +544,10 @@ public abstract class TextToSpeechService extends Service { protected float getFloatParam(String key, float defaultValue) { return mParams == null ? defaultValue : mParams.getFloat(key, defaultValue); } + } - class SynthesisSpeechItem extends SpeechItem { + class SynthesisSpeechItem extends UtteranceSpeechItem { // Never null. private final String mText; private final SynthesisRequest mSynthesisRequest; @@ -552,7 +578,7 @@ public abstract class TextToSpeechService extends Service { Log.e(TAG, "null synthesis text"); return false; } - if (mText.length() >= MAX_SPEECH_ITEM_CHAR_LENGTH) { + if (mText.length() >= TextToSpeech.getMaxSpeechInputLength()) { Log.w(TAG, "Text too long: " + mText.length() + " chars"); return false; } @@ -630,19 +656,19 @@ public abstract class TextToSpeechService extends Service { } } - private class SynthesisToFileSpeechItem extends SynthesisSpeechItem { - private final File mFile; + private class SynthesisToFileSpeechDescriptorItem extends SynthesisSpeechItem { + private final FileDescriptor mFileDescriptor; - public SynthesisToFileSpeechItem(Object callerIdentity, int callerUid, int callerPid, - Bundle params, String text, - File file) { + public SynthesisToFileSpeechDescriptorItem(Object callerIdentity, int callerUid, + int callerPid, Bundle params, String text, FileDescriptor fileDescriptor) { super(callerIdentity, callerUid, callerPid, params, text); - mFile = file; + mFileDescriptor = fileDescriptor; } @Override protected AbstractSynthesisCallback createSynthesisCallback() { - return new FileSynthesisCallback(mFile); + FileOutputStream fileOutputStream = new FileOutputStream(mFileDescriptor); + return new FileSynthesisCallback(fileOutputStream.getChannel()); } @Override @@ -658,7 +684,7 @@ public abstract class TextToSpeechService extends Service { } } - private class AudioSpeechItem extends SpeechItem { + private class AudioSpeechItem extends UtteranceSpeechItem { private final AudioPlaybackQueueItem mItem; public AudioSpeechItem(Object callerIdentity, int callerUid, int callerPid, Bundle params, Uri uri) { @@ -684,7 +710,7 @@ public abstract class TextToSpeechService extends Service { } } - private class SilenceSpeechItem extends SpeechItem { + private class SilenceSpeechItem extends UtteranceSpeechItem { private final long mDuration; public SilenceSpeechItem(Object callerIdentity, int callerUid, int callerPid, @@ -711,6 +737,41 @@ public abstract class TextToSpeechService extends Service { } } + private class LoadLanguageItem extends SpeechItem { + private final String mLanguage; + private final String mCountry; + private final String mVariant; + + public LoadLanguageItem(Object callerIdentity, int callerUid, int callerPid, + Bundle params, String language, String country, String variant) { + super(callerIdentity, callerUid, callerPid, params); + mLanguage = language; + mCountry = country; + mVariant = variant; + } + + @Override + public boolean isValid() { + return true; + } + + @Override + protected int playImpl() { + int result = TextToSpeechService.this.onLoadLanguage(mLanguage, mCountry, mVariant); + if (result == TextToSpeech.LANG_AVAILABLE || + result == TextToSpeech.LANG_COUNTRY_AVAILABLE || + result == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) { + return TextToSpeech.SUCCESS; + } + return TextToSpeech.ERROR; + } + + @Override + protected void stopImpl() { + // No-op + } + } + @Override public IBinder onBind(Intent intent) { if (TextToSpeech.Engine.INTENT_ACTION_TTS_SERVICE.equals(intent.getAction())) { @@ -738,15 +799,15 @@ public abstract class TextToSpeechService extends Service { } @Override - public int synthesizeToFile(IBinder caller, String text, String filename, - Bundle params) { - if (!checkNonNull(caller, text, filename, params)) { + public int synthesizeToFileDescriptor(IBinder caller, String text, ParcelFileDescriptor + fileDescriptor, Bundle params) { + if (!checkNonNull(caller, text, fileDescriptor, params)) { return TextToSpeech.ERROR; } - File file = new File(filename); - SpeechItem item = new SynthesisToFileSpeechItem(caller, Binder.getCallingUid(), - Binder.getCallingPid(), params, text, file); + SpeechItem item = new SynthesisToFileSpeechDescriptorItem(caller, Binder.getCallingUid(), + Binder.getCallingPid(), params, text, + fileDescriptor.getFileDescriptor()); return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item); } @@ -791,6 +852,11 @@ public abstract class TextToSpeechService extends Service { return onGetLanguage(); } + @Override + public String[] getClientDefaultLanguage() { + return getSettingsLocale(); + } + /* * If defaults are enforced, then no language is "available" except * perhaps the default language selected by the user. @@ -822,12 +888,25 @@ public abstract class TextToSpeechService extends Service { * are enforced. */ @Override - public int loadLanguage(String lang, String country, String variant) { + public int loadLanguage(IBinder caller, String lang, String country, String variant) { if (!checkNonNull(lang)) { return TextToSpeech.ERROR; } + int retVal = onIsLanguageAvailable(lang, country, variant); + + if (retVal == TextToSpeech.LANG_AVAILABLE || + retVal == TextToSpeech.LANG_COUNTRY_AVAILABLE || + retVal == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) { - return onLoadLanguage(lang, country, variant); + SpeechItem item = new LoadLanguageItem(caller, Binder.getCallingUid(), + Binder.getCallingPid(), null, lang, country, variant); + + if (mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item) != + TextToSpeech.SUCCESS) { + return TextToSpeech.ERROR; + } + } + return retVal; } @Override diff --git a/core/java/android/test/AndroidTestCase.java b/core/java/android/test/AndroidTestCase.java index 0c8cbe6..0635559 100644 --- a/core/java/android/test/AndroidTestCase.java +++ b/core/java/android/test/AndroidTestCase.java @@ -20,9 +20,11 @@ import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.net.Uri; + import junit.framework.TestCase; import java.lang.reflect.Field; +import java.lang.reflect.Modifier; /** * Extend this if you need to access Resources or other things that depend on Activity Context. @@ -152,11 +154,11 @@ public class AndroidTestCase extends TestCase { * @throws IllegalAccessException */ protected void scrubClass(final Class<?> testCaseClass) - throws IllegalAccessException { + throws IllegalAccessException { final Field[] fields = getClass().getDeclaredFields(); for (Field field : fields) { - final Class<?> fieldClass = field.getDeclaringClass(); - if (testCaseClass.isAssignableFrom(fieldClass) && !field.getType().isPrimitive()) { + if (!field.getType().isPrimitive() && + !Modifier.isStatic(field.getModifiers())) { try { field.setAccessible(true); field.set(this, null); @@ -170,6 +172,4 @@ public class AndroidTestCase extends TestCase { } } } - - } diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java index d909362..122f8a1 100644 --- a/core/java/android/text/DynamicLayout.java +++ b/core/java/android/text/DynamicLayout.java @@ -503,8 +503,15 @@ public class DynamicLayout extends Layout mNumberOfBlocks = newNumberOfBlocks; final int deltaLines = newLineCount - (endLine - startLine + 1); - for (int i = firstBlock + numAddedBlocks; i < mNumberOfBlocks; i++) { - mBlockEndLines[i] += deltaLines; + if (deltaLines != 0) { + // Display list whose index is >= mIndexFirstChangedBlock is valid + // but it needs to update its drawing location. + mIndexFirstChangedBlock = firstBlock + numAddedBlocks; + for (int i = mIndexFirstChangedBlock; i < mNumberOfBlocks; i++) { + mBlockEndLines[i] += deltaLines; + } + } else { + mIndexFirstChangedBlock = mNumberOfBlocks; } int blockIndex = firstBlock; @@ -559,6 +566,20 @@ public class DynamicLayout extends Layout return mNumberOfBlocks; } + /** + * @hide + */ + public int getIndexFirstChangedBlock() { + return mIndexFirstChangedBlock; + } + + /** + * @hide + */ + public void setIndexFirstChangedBlock(int i) { + mIndexFirstChangedBlock = i; + } + @Override public int getLineCount() { return mInts.size() - 1; @@ -697,6 +718,8 @@ public class DynamicLayout extends Layout private int[] mBlockIndices; // Number of items actually currently being used in the above 2 arrays private int mNumberOfBlocks; + // The first index of the blocks whose locations are changed + private int mIndexFirstChangedBlock; private int mTopPadding, mBottomPadding; diff --git a/core/java/android/text/GraphicsOperations.java b/core/java/android/text/GraphicsOperations.java index 831ccc5..60545e5 100644 --- a/core/java/android/text/GraphicsOperations.java +++ b/core/java/android/text/GraphicsOperations.java @@ -38,7 +38,7 @@ extends CharSequence * {@hide} */ void drawTextRun(Canvas c, int start, int end, int contextStart, int contextEnd, - float x, float y, int flags, Paint p); + float x, float y, Paint p); /** * Just like {@link Paint#measureText}. @@ -55,19 +55,12 @@ extends CharSequence * @hide */ float getTextRunAdvances(int start, int end, int contextStart, int contextEnd, - int flags, float[] advances, int advancesIndex, Paint paint); - - /** - * Just like {@link Paint#getTextRunAdvances}. - * @hide - */ - float getTextRunAdvances(int start, int end, int contextStart, int contextEnd, - int flags, float[] advances, int advancesIndex, Paint paint, int reserved); + float[] advances, int advancesIndex, Paint paint); /** * Just like {@link Paint#getTextRunCursor}. * @hide */ - int getTextRunCursor(int contextStart, int contextEnd, int flags, int offset, + int getTextRunCursor(int contextStart, int contextEnd, int offset, int cursorOpt, Paint p); } diff --git a/core/java/android/text/Html.java b/core/java/android/text/Html.java index 1aab911..160c630 100644 --- a/core/java/android/text/Html.java +++ b/core/java/android/text/Html.java @@ -16,6 +16,7 @@ package android.text; +import android.graphics.Color; import com.android.internal.util.ArrayUtils; import org.ccil.cowan.tagsoup.HTMLSchema; import org.ccil.cowan.tagsoup.Parser; @@ -168,7 +169,7 @@ public class Html { for(int j = 0; j < style.length; j++) { if (style[j] instanceof AlignmentSpan) { - Layout.Alignment align = + Layout.Alignment align = ((AlignmentSpan) style[j]).getAlignment(); needDiv = true; if (align == Layout.Alignment.ALIGN_CENTER) { @@ -181,7 +182,7 @@ public class Html { } } if (needDiv) { - out.append("<div " + elements + ">"); + out.append("<div ").append(elements).append(">"); } withinDiv(out, text, i, next); @@ -199,13 +200,13 @@ public class Html { next = text.nextSpanTransition(i, end, QuoteSpan.class); QuoteSpan[] quotes = text.getSpans(i, next, QuoteSpan.class); - for (QuoteSpan quote: quotes) { + for (QuoteSpan quote : quotes) { out.append("<blockquote>"); } withinBlockquote(out, text, i, next); - for (QuoteSpan quote: quotes) { + for (QuoteSpan quote : quotes) { out.append("</blockquote>\n"); } } @@ -391,7 +392,7 @@ public class Html { } else if (c == '&') { out.append("&"); } else if (c > 0x7E || c < ' ') { - out.append("&#" + ((int) c) + ";"); + out.append("&#").append((int) c).append(";"); } else if (c == ' ') { while (i + 1 < end && text.charAt(i + 1) == ' ') { out.append(" "); @@ -616,8 +617,6 @@ class HtmlToSpannedConverter implements ContentHandler { if (where != len) { text.setSpan(repl, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } - - return; } private static void startImg(SpannableStringBuilder text, @@ -673,7 +672,7 @@ class HtmlToSpannedConverter implements ContentHandler { Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } else { - int c = getHtmlColor(f.mColor); + int c = Color.getHtmlColor(f.mColor); if (c != -1) { text.setSpan(new ForegroundColorSpan(c | 0xFF000000), where, len, @@ -842,47 +841,4 @@ class HtmlToSpannedConverter implements ContentHandler { mLevel = level; } } - - private static HashMap<String,Integer> COLORS = buildColorMap(); - - private static HashMap<String,Integer> buildColorMap() { - HashMap<String,Integer> map = new HashMap<String,Integer>(); - map.put("aqua", 0x00FFFF); - map.put("black", 0x000000); - map.put("blue", 0x0000FF); - map.put("fuchsia", 0xFF00FF); - map.put("green", 0x008000); - map.put("grey", 0x808080); - map.put("lime", 0x00FF00); - map.put("maroon", 0x800000); - map.put("navy", 0x000080); - map.put("olive", 0x808000); - map.put("purple", 0x800080); - map.put("red", 0xFF0000); - map.put("silver", 0xC0C0C0); - map.put("teal", 0x008080); - map.put("white", 0xFFFFFF); - map.put("yellow", 0xFFFF00); - return map; - } - - /** - * Converts an HTML color (named or numeric) to an integer RGB value. - * - * @param color Non-null color string. - * @return A color value, or {@code -1} if the color string could not be interpreted. - */ - private static int getHtmlColor(String color) { - Integer i = COLORS.get(color.toLowerCase()); - if (i != null) { - return i; - } else { - try { - return XmlUtils.convertValueToInt(color, -1); - } catch (NumberFormatException nfe) { - return -1; - } - } - } - } diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index 123acca..a6e8c70 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -792,8 +792,17 @@ public abstract class Layout { * the paragraph's primary direction. */ public float getPrimaryHorizontal(int offset) { + return getPrimaryHorizontal(offset, false /* not clamped */); + } + + /** + * Get the primary horizontal position for the specified text offset, but + * optionally clamp it so that it doesn't exceed the width of the layout. + * @hide + */ + public float getPrimaryHorizontal(int offset, boolean clamped) { boolean trailing = primaryIsTrailingPrevious(offset); - return getHorizontal(offset, trailing); + return getHorizontal(offset, trailing, clamped); } /** @@ -802,17 +811,26 @@ public abstract class Layout { * the direction other than the paragraph's primary direction. */ public float getSecondaryHorizontal(int offset) { + return getSecondaryHorizontal(offset, false /* not clamped */); + } + + /** + * Get the secondary horizontal position for the specified text offset, but + * optionally clamp it so that it doesn't exceed the width of the layout. + * @hide + */ + public float getSecondaryHorizontal(int offset, boolean clamped) { boolean trailing = primaryIsTrailingPrevious(offset); - return getHorizontal(offset, !trailing); + return getHorizontal(offset, !trailing, clamped); } - private float getHorizontal(int offset, boolean trailing) { + private float getHorizontal(int offset, boolean trailing, boolean clamped) { int line = getLineForOffset(offset); - return getHorizontal(offset, trailing, line); + return getHorizontal(offset, trailing, line, clamped); } - private float getHorizontal(int offset, boolean trailing, int line) { + private float getHorizontal(int offset, boolean trailing, int line, boolean clamped) { int start = getLineStart(line); int end = getLineEnd(line); int dir = getParagraphDirection(line); @@ -834,6 +852,9 @@ public abstract class Layout { float wid = tl.measure(offset - start, trailing, null); TextLine.recycle(tl); + if (clamped && wid > mWidth) { + wid = mWidth; + } int left = getParagraphLeft(line); int right = getParagraphRight(line); @@ -1257,6 +1278,23 @@ public abstract class Layout { } /** + * Determine whether we should clamp cursor position. Currently it's + * only robust for left-aligned displays. + * @hide + */ + public boolean shouldClampCursor(int line) { + // Only clamp cursor position in left-aligned displays. + switch (getParagraphAlignment(line)) { + case ALIGN_LEFT: + return true; + case ALIGN_NORMAL: + return getParagraphDirection(line) > 0; + default: + return false; + } + + } + /** * Fills in the specified Path with a representation of a cursor * at the specified offset. This will often be a vertical line * but can be multiple discontinuous lines in text with multiple @@ -1270,8 +1308,9 @@ public abstract class Layout { int top = getLineTop(line); int bottom = getLineTop(line+1); - float h1 = getPrimaryHorizontal(point) - 0.5f; - float h2 = isLevelBoundary(point) ? getSecondaryHorizontal(point) - 0.5f : h1; + boolean clamped = shouldClampCursor(line); + float h1 = getPrimaryHorizontal(point, clamped) - 0.5f; + float h2 = isLevelBoundary(point) ? getSecondaryHorizontal(point, clamped) - 0.5f : h1; int caps = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SHIFT_ON) | TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SELECTING); @@ -1357,8 +1396,8 @@ public abstract class Layout { int en = Math.min(end, there); if (st != en) { - float h1 = getHorizontal(st, false, line); - float h2 = getHorizontal(en, true, line); + float h1 = getHorizontal(st, false, line, false /* not clamped */); + float h2 = getHorizontal(en, true, line, false /* not clamped */); float left = Math.min(h1, h2); float right = Math.max(h1, h2); diff --git a/core/java/android/text/MeasuredText.java b/core/java/android/text/MeasuredText.java index bd9310c..0c881a4 100644 --- a/core/java/android/text/MeasuredText.java +++ b/core/java/android/text/MeasuredText.java @@ -159,18 +159,15 @@ class MeasuredText { mPos = p + len; if (mEasy) { - int flags = mDir == Layout.DIR_LEFT_TO_RIGHT - ? Canvas.DIRECTION_LTR : Canvas.DIRECTION_RTL; - return paint.getTextRunAdvances(mChars, p, len, p, len, flags, mWidths, p); + return paint.getTextRunAdvances(mChars, p, len, p, len, mWidths, p); } float totalAdvance = 0; int level = mLevels[p]; for (int q = p, i = p + 1, e = p + len;; ++i) { if (i == e || mLevels[i] != level) { - int flags = (level & 0x1) == 0 ? Canvas.DIRECTION_LTR : Canvas.DIRECTION_RTL; totalAdvance += - paint.getTextRunAdvances(mChars, q, i - q, q, i - q, flags, mWidths, q); + paint.getTextRunAdvances(mChars, q, i - q, q, i - q, mWidths, q); if (i == e) { break; } diff --git a/core/java/android/text/SpannableStringBuilder.java b/core/java/android/text/SpannableStringBuilder.java index 0f30d25..9e43671 100644 --- a/core/java/android/text/SpannableStringBuilder.java +++ b/core/java/android/text/SpannableStringBuilder.java @@ -1130,20 +1130,20 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable * {@hide} */ public void drawTextRun(Canvas c, int start, int end, int contextStart, int contextEnd, - float x, float y, int flags, Paint p) { + float x, float y, Paint p) { checkRange("drawTextRun", start, end); int contextLen = contextEnd - contextStart; int len = end - start; if (contextEnd <= mGapStart) { - c.drawTextRun(mText, start, len, contextStart, contextLen, x, y, flags, p); + c.drawTextRun(mText, start, len, contextStart, contextLen, x, y, p); } else if (contextStart >= mGapStart) { c.drawTextRun(mText, start + mGapLength, len, contextStart + mGapLength, - contextLen, x, y, flags, p); + contextLen, x, y, p); } else { char[] buf = TextUtils.obtain(contextLen); getChars(contextStart, contextEnd, buf, 0); - c.drawTextRun(buf, start - contextStart, len, 0, contextLen, x, y, flags, p); + c.drawTextRun(buf, start - contextStart, len, 0, contextLen, x, y, p); TextUtils.recycle(buf); } } @@ -1200,7 +1200,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable * Don't call this yourself -- exists for Paint to use internally. * {@hide} */ - public float getTextRunAdvances(int start, int end, int contextStart, int contextEnd, int flags, + public float getTextRunAdvances(int start, int end, int contextStart, int contextEnd, float[] advances, int advancesPos, Paint p) { float ret; @@ -1210,44 +1210,15 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable if (end <= mGapStart) { ret = p.getTextRunAdvances(mText, start, len, contextStart, contextLen, - flags, advances, advancesPos); + advances, advancesPos); } else if (start >= mGapStart) { ret = p.getTextRunAdvances(mText, start + mGapLength, len, - contextStart + mGapLength, contextLen, flags, advances, advancesPos); + contextStart + mGapLength, contextLen, advances, advancesPos); } else { char[] buf = TextUtils.obtain(contextLen); getChars(contextStart, contextEnd, buf, 0); ret = p.getTextRunAdvances(buf, start - contextStart, len, - 0, contextLen, flags, advances, advancesPos); - TextUtils.recycle(buf); - } - - return ret; - } - - /** - * Don't call this yourself -- exists for Paint to use internally. - * {@hide} - */ - public float getTextRunAdvances(int start, int end, int contextStart, int contextEnd, int flags, - float[] advances, int advancesPos, Paint p, int reserved) { - - float ret; - - int contextLen = contextEnd - contextStart; - int len = end - start; - - if (end <= mGapStart) { - ret = p.getTextRunAdvances(mText, start, len, contextStart, contextLen, - flags, advances, advancesPos, reserved); - } else if (start >= mGapStart) { - ret = p.getTextRunAdvances(mText, start + mGapLength, len, - contextStart + mGapLength, contextLen, flags, advances, advancesPos, reserved); - } else { - char[] buf = TextUtils.obtain(contextLen); - getChars(contextStart, contextEnd, buf, 0); - ret = p.getTextRunAdvances(buf, start - contextStart, len, - 0, contextLen, flags, advances, advancesPos, reserved); + 0, contextLen, advances, advancesPos); TextUtils.recycle(buf); } @@ -1270,7 +1241,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable * * @param contextStart the start index of the context * @param contextEnd the (non-inclusive) end index of the context - * @param flags either DIRECTION_RTL or DIRECTION_LTR + * @param flags reserved * @param offset the cursor position to move from * @param cursorOpt how to move the cursor, one of CURSOR_AFTER, * CURSOR_AT_OR_AFTER, CURSOR_BEFORE, @@ -1281,22 +1252,30 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable */ @Deprecated public int getTextRunCursor(int contextStart, int contextEnd, int flags, int offset, - int cursorOpt, Paint p) { + int cursorOpt, Paint p) { + return getTextRunCursor(contextStart, contextEnd, offset, cursorOpt, p); + } + + /** + * @hide + */ + public int getTextRunCursor(int contextStart, int contextEnd, int offset, + int cursorOpt, Paint p) { int ret; int contextLen = contextEnd - contextStart; if (contextEnd <= mGapStart) { ret = p.getTextRunCursor(mText, contextStart, contextLen, - flags, offset, cursorOpt); + offset, cursorOpt); } else if (contextStart >= mGapStart) { ret = p.getTextRunCursor(mText, contextStart + mGapLength, contextLen, - flags, offset + mGapLength, cursorOpt) - mGapLength; + offset + mGapLength, cursorOpt) - mGapLength; } else { char[] buf = TextUtils.obtain(contextLen); getChars(contextStart, contextEnd, buf, 0); ret = p.getTextRunCursor(buf, 0, contextLen, - flags, offset - contextStart, cursorOpt) + contextStart; + offset - contextStart, cursorOpt) + contextStart; TextUtils.recycle(buf); } diff --git a/core/java/android/text/TextDirectionHeuristic.java b/core/java/android/text/TextDirectionHeuristic.java index 513e11c..8a4ba42 100644 --- a/core/java/android/text/TextDirectionHeuristic.java +++ b/core/java/android/text/TextDirectionHeuristic.java @@ -17,10 +17,30 @@ package android.text; /** - * Interface for objects that guess at the paragraph direction by examining text. - * - * @hide + * Interface for objects that use a heuristic for guessing at the paragraph direction by examining text. */ public interface TextDirectionHeuristic { - boolean isRtl(char[] text, int start, int count); + /** + * Guess if a chars array is in the RTL direction or not. + * + * @param array the char array. + * @param start start index, inclusive. + * @param count the length to check, must not be negative and not greater than + * {@code array.length - start}. + * @return true if all chars in the range are to be considered in a RTL direction, + * false otherwise. + */ + boolean isRtl(char[] array, int start, int count); + + /** + * Guess if a {@code CharSequence} is in the RTL direction or not. + * + * @param cs the CharSequence. + * @param start start index, inclusive. + * @param count the length to check, must not be negative and not greater than + * {@code CharSequence.length() - start}. + * @return true if all chars in the range are to be considered in a RTL direction, + * false otherwise. + */ + boolean isRtl(CharSequence cs, int start, int count); } diff --git a/core/java/android/text/TextDirectionHeuristics.java b/core/java/android/text/TextDirectionHeuristics.java index df8c4c6..7d7e3a9 100644 --- a/core/java/android/text/TextDirectionHeuristics.java +++ b/core/java/android/text/TextDirectionHeuristics.java @@ -19,43 +19,45 @@ package android.text; import android.view.View; +import java.nio.CharBuffer; + /** * Some objects that implement TextDirectionHeuristic. * - * @hide */ public class TextDirectionHeuristics { - /** Always decides that the direction is left to right. */ + /** + * Always decides that the direction is left to right. + */ public static final TextDirectionHeuristic LTR = new TextDirectionHeuristicInternal(null /* no algorithm */, false); - /** Always decides that the direction is right to left. */ + /** + * Always decides that the direction is right to left. + */ public static final TextDirectionHeuristic RTL = new TextDirectionHeuristicInternal(null /* no algorithm */, true); /** - * Determines the direction based on the first strong directional character, - * including bidi format chars, falling back to left to right if it - * finds none. This is the default behavior of the Unicode Bidirectional - * Algorithm. + * Determines the direction based on the first strong directional character, including bidi + * format chars, falling back to left to right if it finds none. This is the default behavior + * of the Unicode Bidirectional Algorithm. */ public static final TextDirectionHeuristic FIRSTSTRONG_LTR = new TextDirectionHeuristicInternal(FirstStrong.INSTANCE, false); /** - * Determines the direction based on the first strong directional character, - * including bidi format chars, falling back to right to left if it - * finds none. This is similar to the default behavior of the Unicode - * Bidirectional Algorithm, just with different fallback behavior. + * Determines the direction based on the first strong directional character, including bidi + * format chars, falling back to right to left if it finds none. This is similar to the default + * behavior of the Unicode Bidirectional Algorithm, just with different fallback behavior. */ public static final TextDirectionHeuristic FIRSTSTRONG_RTL = new TextDirectionHeuristicInternal(FirstStrong.INSTANCE, true); /** - * If the text contains any strong right to left non-format character, determines - * that the direction is right to left, falling back to left to right if it - * finds none. + * If the text contains any strong right to left non-format character, determines that the + * direction is right to left, falling back to left to right if it finds none. */ public static final TextDirectionHeuristic ANYRTL_LTR = new TextDirectionHeuristicInternal(AnyStrong.INSTANCE_RTL, false); @@ -65,8 +67,39 @@ public class TextDirectionHeuristics { */ public static final TextDirectionHeuristic LOCALE = TextDirectionHeuristicLocale.INSTANCE; - private static enum TriState { - TRUE, FALSE, UNKNOWN; + /** + * State constants for taking care about true / false / unknown + */ + private static final int STATE_TRUE = 0; + private static final int STATE_FALSE = 1; + private static final int STATE_UNKNOWN = 2; + + private static int isRtlText(int directionality) { + switch (directionality) { + case Character.DIRECTIONALITY_LEFT_TO_RIGHT: + return STATE_FALSE; + case Character.DIRECTIONALITY_RIGHT_TO_LEFT: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC: + return STATE_TRUE; + default: + return STATE_UNKNOWN; + } + } + + private static int isRtlTextOrFormat(int directionality) { + switch (directionality) { + case Character.DIRECTIONALITY_LEFT_TO_RIGHT: + case Character.DIRECTIONALITY_LEFT_TO_RIGHT_EMBEDDING: + case Character.DIRECTIONALITY_LEFT_TO_RIGHT_OVERRIDE: + return STATE_FALSE; + case Character.DIRECTIONALITY_RIGHT_TO_LEFT: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE: + return STATE_TRUE; + default: + return STATE_UNKNOWN; + } } /** @@ -87,21 +120,26 @@ public class TextDirectionHeuristics { abstract protected boolean defaultIsRtl(); @Override - public boolean isRtl(char[] chars, int start, int count) { - if (chars == null || start < 0 || count < 0 || chars.length - count < start) { + public boolean isRtl(char[] array, int start, int count) { + return isRtl(CharBuffer.wrap(array), start, count); + } + + @Override + public boolean isRtl(CharSequence cs, int start, int count) { + if (cs == null || start < 0 || count < 0 || cs.length() - count < start) { throw new IllegalArgumentException(); } if (mAlgorithm == null) { return defaultIsRtl(); } - return doCheck(chars, start, count); + return doCheck(cs, start, count); } - private boolean doCheck(char[] chars, int start, int count) { - switch(mAlgorithm.checkRtl(chars, start, count)) { - case TRUE: + private boolean doCheck(CharSequence cs, int start, int count) { + switch(mAlgorithm.checkRtl(cs, start, count)) { + case STATE_TRUE: return true; - case FALSE: + case STATE_FALSE: return false; default: return defaultIsRtl(); @@ -124,58 +162,26 @@ public class TextDirectionHeuristics { } } - private static TriState isRtlText(int directionality) { - switch (directionality) { - case Character.DIRECTIONALITY_LEFT_TO_RIGHT: - return TriState.FALSE; - case Character.DIRECTIONALITY_RIGHT_TO_LEFT: - case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC: - return TriState.TRUE; - default: - return TriState.UNKNOWN; - } - } - - private static TriState isRtlTextOrFormat(int directionality) { - switch (directionality) { - case Character.DIRECTIONALITY_LEFT_TO_RIGHT: - case Character.DIRECTIONALITY_LEFT_TO_RIGHT_EMBEDDING: - case Character.DIRECTIONALITY_LEFT_TO_RIGHT_OVERRIDE: - return TriState.FALSE; - case Character.DIRECTIONALITY_RIGHT_TO_LEFT: - case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC: - case Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING: - case Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE: - return TriState.TRUE; - default: - return TriState.UNKNOWN; - } - } - /** * Interface for an algorithm to guess the direction of a paragraph of text. - * */ private static interface TextDirectionAlgorithm { /** * Returns whether the range of text is RTL according to the algorithm. - * */ - TriState checkRtl(char[] text, int start, int count); + int checkRtl(CharSequence cs, int start, int count); } /** - * Algorithm that uses the first strong directional character to determine - * the paragraph direction. This is the standard Unicode Bidirectional - * algorithm. - * + * Algorithm that uses the first strong directional character to determine the paragraph + * direction. This is the standard Unicode Bidirectional algorithm. */ private static class FirstStrong implements TextDirectionAlgorithm { @Override - public TriState checkRtl(char[] text, int start, int count) { - TriState result = TriState.UNKNOWN; - for (int i = start, e = start + count; i < e && result == TriState.UNKNOWN; ++i) { - result = isRtlTextOrFormat(Character.getDirectionality(text[i])); + public int checkRtl(CharSequence cs, int start, int count) { + int result = STATE_UNKNOWN; + for (int i = start, e = start + count; i < e && result == STATE_UNKNOWN; ++i) { + result = isRtlTextOrFormat(Character.getDirectionality(cs.charAt(i))); } return result; } @@ -190,25 +196,24 @@ public class TextDirectionHeuristics { * Algorithm that uses the presence of any strong directional non-format * character (e.g. excludes LRE, LRO, RLE, RLO) to determine the * direction of text. - * */ private static class AnyStrong implements TextDirectionAlgorithm { private final boolean mLookForRtl; @Override - public TriState checkRtl(char[] text, int start, int count) { + public int checkRtl(CharSequence cs, int start, int count) { boolean haveUnlookedFor = false; for (int i = start, e = start + count; i < e; ++i) { - switch (isRtlText(Character.getDirectionality(text[i]))) { - case TRUE: + switch (isRtlText(Character.getDirectionality(cs.charAt(i)))) { + case STATE_TRUE: if (mLookForRtl) { - return TriState.TRUE; + return STATE_TRUE; } haveUnlookedFor = true; break; - case FALSE: + case STATE_FALSE: if (!mLookForRtl) { - return TriState.FALSE; + return STATE_FALSE; } haveUnlookedFor = true; break; @@ -217,9 +222,9 @@ public class TextDirectionHeuristics { } } if (haveUnlookedFor) { - return mLookForRtl ? TriState.FALSE : TriState.TRUE; + return mLookForRtl ? STATE_FALSE : STATE_TRUE; } - return TriState.UNKNOWN; + return STATE_UNKNOWN; } private AnyStrong(boolean lookForRtl) { diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java index 1fecf81..e34a0ef 100644 --- a/core/java/android/text/TextLine.java +++ b/core/java/android/text/TextLine.java @@ -664,14 +664,13 @@ class TextLine { } } - int flags = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR; int cursorOpt = after ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE; if (mCharsValid) { return wp.getTextRunCursor(mChars, spanStart, spanLimit - spanStart, - flags, offset, cursorOpt); + offset, cursorOpt); } else { return wp.getTextRunCursor(mText, mStart + spanStart, - mStart + spanLimit, flags, mStart + offset, cursorOpt) - mStart; + mStart + spanLimit, mStart + offset, cursorOpt) - mStart; } } @@ -738,15 +737,13 @@ class TextLine { int contextLen = contextEnd - contextStart; if (needWidth || (c != null && (wp.bgColor != 0 || wp.underlineColor != 0 || runIsRtl))) { - int flags = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR; if (mCharsValid) { ret = wp.getTextRunAdvances(mChars, start, runLen, - contextStart, contextLen, flags, null, 0); + contextStart, contextLen, null, 0); } else { int delta = mStart; - ret = wp.getTextRunAdvances(mText, delta + start, - delta + end, delta + contextStart, delta + contextEnd, - flags, null, 0); + ret = wp.getTextRunAdvances(mText, delta + start, delta + end, + delta + contextStart, delta + contextEnd, null, 0); } } @@ -786,8 +783,7 @@ class TextLine { wp.setAntiAlias(previousAntiAlias); } - drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl, - x, y + wp.baselineShift); + drawTextRun(c, wp, start, end, contextStart, contextEnd, x, y + wp.baselineShift); } return runIsRtl ? -ret : ret; @@ -970,23 +966,21 @@ class TextLine { * @param end the end of the run * @param contextStart the start of context for the run * @param contextEnd the end of the context for the run - * @param runIsRtl true if the run is right-to-left * @param x the x position of the left edge of the run * @param y the baseline of the run */ private void drawTextRun(Canvas c, TextPaint wp, int start, int end, - int contextStart, int contextEnd, boolean runIsRtl, float x, int y) { + int contextStart, int contextEnd, float x, int y) { - int flags = runIsRtl ? Canvas.DIRECTION_RTL : Canvas.DIRECTION_LTR; if (mCharsValid) { int count = end - start; int contextCount = contextEnd - contextStart; c.drawTextRun(mChars, start, count, contextStart, contextCount, - x, y, flags, wp); + x, y, wp); } else { int delta = mStart; c.drawTextRun(mText, delta + start, delta + end, - delta + contextStart, delta + contextEnd, x, y, flags, wp); + delta + contextStart, delta + contextEnd, x, y, wp); } } diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java index 1508d10..2ab9bf8 100644 --- a/core/java/android/text/TextUtils.java +++ b/core/java/android/text/TextUtils.java @@ -757,7 +757,7 @@ public class TextUtils { break; case EASY_EDIT_SPAN: - readSpan(p, sp, new EasyEditSpan()); + readSpan(p, sp, new EasyEditSpan(p)); break; case LOCALE_SPAN: diff --git a/core/java/android/text/bidi/BidiFormatter.java b/core/java/android/text/bidi/BidiFormatter.java new file mode 100644 index 0000000..370cbf7 --- /dev/null +++ b/core/java/android/text/bidi/BidiFormatter.java @@ -0,0 +1,1123 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.text.bidi; + +import android.text.TextDirectionHeuristic; +import android.text.TextDirectionHeuristics; +import android.text.TextUtils; +import android.view.View; + +import static android.text.TextDirectionHeuristics.FIRSTSTRONG_LTR; + +import java.util.Locale; + + +/** + * Utility class for formatting text for display in a potentially opposite-directionality context + * without garbling. The directionality of the context is set at formatter creation and the + * directionality of the text can be either estimated or passed in when known. Provides the + * following functionality: + * <p> + * 1. Bidi Wrapping + * When text in one language is mixed into a document in another, opposite-directionality language, + * e.g. when an English business name is embedded in a Hebrew web page, both the inserted string + * and the text surrounding it may be displayed incorrectly unless the inserted string is explicitly + * separated from the surrounding text in a "wrapper" that: + * <p> + * - Declares its directionality so that the string is displayed correctly. This can be done in HTML + * markup (e.g. a 'span dir="rtl"' element) by {@link #spanWrap} and similar methods, or - only in + * contexts where markup can't be used - in Unicode bidi formatting codes by {@link #unicodeWrap} + * and similar methods. + * <p> + * - Isolates the string's directionality, so it does not unduly affect the surrounding content. + * Currently, this can only be done using invisible Unicode characters of the same direction as + * the context (LRM or RLM) in addition to the directionality declaration above, thus "resetting" + * the directionality to that of the context. The "reset" may need to be done at both ends of the + * string. Without "reset" after the string, the string will "stick" to a number or logically + * separate opposite-direction text that happens to follow it in-line (even if separated by + * neutral content like spaces and punctuation). Without "reset" before the string, the same can + * happen there, but only with more opposite-direction text, not a number. One approach is to + * "reset" the direction only after each string, on the theory that if the preceding opposite- + * direction text is itself bidi-wrapped, the "reset" after it will prevent the sticking. (Doing + * the "reset" only before each string definitely does not work because we do not want to require + * bidi-wrapping numbers, and a bidi-wrapped opposite-direction string could be followed by a + * number.) Still, the safest policy is to do the "reset" on both ends of each string, since RTL + * message translations often contain untranslated Latin-script brand names and technical terms, + * and one of these can be followed by a bidi-wrapped inserted value. On the other hand, when one + * has such a message, it is best to do the "reset" manually in the message translation itself, + * since the message's opposite-direction text could be followed by an inserted number, which we + * would not bidi-wrap anyway. Thus, "reset" only after the string is the current default. In an + * alternative to "reset", recent additions to the HTML, CSS, and Unicode standards allow the + * isolation to be part of the directionality declaration. This form of isolation is better than + * "reset" because it takes less space, does not require knowing the context directionality, has a + * gentler effect than "reset", and protects both ends of the string. However, we do not yet allow + * using it because required platforms do not yet support it. + * <p> + * Providing these wrapping services is the basic purpose of the bidi formatter. + * <p> + * 2. Directionality estimation + * How does one know whether a string about to be inserted into surrounding text has the same + * directionality? Well, in many cases, one knows that this must be the case when writing the code + * doing the insertion, e.g. when a localized message is inserted into a localized page. In such + * cases there is no need to involve the bidi formatter at all. In some other cases, it need not be + * the same as the context, but is either constant (e.g. urls are always LTR) or otherwise known. + * In the remaining cases, e.g. when the string is user-entered or comes from a database, the + * language of the string (and thus its directionality) is not known a priori, and must be + * estimated at run-time. The bidi formatter can do this automatically using the default + * first-strong estimation algorithm. It can also be configured to use a custom directionality + * estimation object. + * <p> + * 3. Escaping + * When wrapping plain text - i.e. text that is not already HTML or HTML-escaped - in HTML markup, + * the text must first be HTML-escaped to prevent XSS attacks and other nasty business. This of + * course is always true, but the escaping can not be done after the string has already been wrapped + * in markup, so the bidi formatter also serves as a last chance and includes escaping services. + * <p> + * Thus, in a single call, the formatter will escape the input string as specified, determine its + * directionality, and wrap it as necessary. It is then up to the caller to insert the return value + * in the output. + */ +public final class BidiFormatter { + + /** + * The default text direction heuristic. + */ + private static TextDirectionHeuristic DEFAULT_TEXT_DIRECTION_HEURISTIC = FIRSTSTRONG_LTR; + + /** + * Unicode "Left-To-Right Embedding" (LRE) character. + */ + private static final char LRE = '\u202A'; + + /** + * Unicode "Right-To-Left Embedding" (RLE) character. + */ + private static final char RLE = '\u202B'; + + /** + * Unicode "Pop Directional Formatting" (PDF) character. + */ + private static final char PDF = '\u202C'; + + /** + * Unicode "Left-To-Right Mark" (LRM) character. + */ + private static final char LRM = '\u200E'; + + /* + * Unicode "Right-To-Left Mark" (RLM) character. + */ + private static final char RLM = '\u200F'; + + /* + * String representation of LRM + */ + private static final String LRM_STRING = Character.toString(LRM); + + /* + * String representation of RLM + */ + private static final String RLM_STRING = Character.toString(RLM); + + /** + * "ltr" string constant. + */ + private static final String LTR_STRING = "ltr"; + + /** + * "rtl" string constant. + */ + private static final String RTL_STRING = "rtl"; + + /** + * "dir=\"ltr\"" string constant. + */ + private static final String DIR_LTR_STRING = "dir=\"ltr\""; + + /** + * "dir=\"rtl\"" string constant. + */ + private static final String DIR_RTL_STRING = "dir=\"rtl\""; + + /** + * "right" string constant. + */ + private static final String RIGHT = "right"; + + /** + * "left" string constant. + */ + private static final String LEFT = "left"; + + /** + * Empty string constant. + */ + private static final String EMPTY_STRING = ""; + + /** + * A class for building a BidiFormatter with non-default options. + */ + public static final class Builder { + private boolean isRtlContext; + private int flags; + private TextDirectionHeuristic textDirectionHeuristic; + + /** + * Constructor. + * + */ + public Builder() { + initialize(isRtlLocale(Locale.getDefault())); + } + + /** + * Constructor. + * + * @param rtlContext Whether the context directionality is RTL. + */ + public Builder(boolean rtlContext) { + initialize(rtlContext); + } + + /** + * Constructor. + * + * @param locale The context locale. + */ + public Builder(Locale locale) { + initialize(isRtlLocale(locale)); + } + + /** + * Initializes the builder with the given context directionality and default options. + * + * @param isRtlContext Whether the context is RTL or not. + */ + private void initialize(boolean isRtlContext) { + this.isRtlContext = isRtlContext; + textDirectionHeuristic = DEFAULT_TEXT_DIRECTION_HEURISTIC; + this.flags = DEFAULT_FLAGS; + } + + /** + * Specifies whether the BidiFormatter to be built should also "reset" directionality before + * a string being bidi-wrapped, not just after it. The default is false. + */ + public Builder stereoReset(boolean stereoReset) { + if (stereoReset) { + flags |= FLAG_STEREO_RESET; + } else { + flags &= ~FLAG_STEREO_RESET; + } + return this; + } + + /** + * Specifies the default directionality estimation algorithm to be used by the BidiFormatter. + * By default, uses the first-strong heuristic. + * + * @param heuristic the {@code TextDirectionHeuristic} to use. + * @return the builder itself. + */ + public Builder setTextDirectionHeuristic(TextDirectionHeuristic heuristic) { + this.textDirectionHeuristic = heuristic; + return this; + } + + private static BidiFormatter getDefaultInstanceFromContext(boolean isRtlContext) { + return isRtlContext ? DEFAULT_RTL_INSTANCE : DEFAULT_LTR_INSTANCE; + } + + /** + * @return A BidiFormatter with the specified options. + */ + public BidiFormatter build() { + if (flags == DEFAULT_FLAGS && + textDirectionHeuristic == DEFAULT_TEXT_DIRECTION_HEURISTIC) { + return getDefaultInstanceFromContext(isRtlContext); + } + return new BidiFormatter(isRtlContext, flags, textDirectionHeuristic); + } + } + + // + private static final int FLAG_STEREO_RESET = 2; + private static final int DEFAULT_FLAGS = FLAG_STEREO_RESET; + + private static final BidiFormatter DEFAULT_LTR_INSTANCE = new BidiFormatter( + false /* LTR context */, + DEFAULT_FLAGS, + DEFAULT_TEXT_DIRECTION_HEURISTIC); + + private static final BidiFormatter DEFAULT_RTL_INSTANCE = new BidiFormatter( + true /* RTL context */, + DEFAULT_FLAGS, + DEFAULT_TEXT_DIRECTION_HEURISTIC); + + private final boolean isRtlContext; + private final int flags; + private final TextDirectionHeuristic defaultTextDirectionHeuristic; + + /** + * Factory for creating an instance of BidiFormatter given the context directionality. + * + * @param rtlContext Whether the context directionality is RTL. + */ + public static BidiFormatter getInstance(boolean rtlContext) { + return new Builder(rtlContext).build(); + } + + /** + * Factory for creating an instance of BidiFormatter given the context locale. + * + * @param locale The context locale. + */ + public static BidiFormatter getInstance(Locale locale) { + return new Builder(locale).build(); + } + + /** + * @param isRtlContext Whether the context directionality is RTL or not. + * @param flags The option flags. + * @param heuristic The default text direction heuristic. + */ + private BidiFormatter(boolean isRtlContext, int flags, TextDirectionHeuristic heuristic) { + this.isRtlContext = isRtlContext; + this.flags = flags; + this.defaultTextDirectionHeuristic = heuristic; + } + + /** + * @return Whether the context directionality is RTL + */ + public boolean isRtlContext() { + return isRtlContext; + } + + /** + * @return Whether directionality "reset" should also be done before a string being + * bidi-wrapped, not just after it. + */ + public boolean getStereoReset() { + return (flags & FLAG_STEREO_RESET) != 0; + } + + /** + * Returns "rtl" if {@code str}'s estimated directionality is RTL, and "ltr" if it is LTR. + * + * @param str String whose directionality is to be estimated. + * @return "rtl" if {@code str}'s estimated directionality is RTL, and "ltr" otherwise. + */ + public String dirAttrValue(String str) { + return dirAttrValue(isRtl(str)); + } + + /** + * Operates like {@link #dirAttrValue(String)}, but uses a given heuristic to estimate the + * {@code str}'s directionality. + * + * @param str String whose directionality is to be estimated. + * @param heuristic The text direction heuristic that will be used to estimate the {@code str}'s + * directionality. + * @return "rtl" if {@code str}'s estimated directionality is RTL, and "ltr" otherwise. + */ + public String dirAttrValue(String str, TextDirectionHeuristic heuristic) { + return dirAttrValue(heuristic.isRtl(str, 0, str.length())); + } + + /** + * Returns "rtl" if the given directionality is RTL, and "ltr" if it is LTR. + * + * @param isRtl Whether the directionality is RTL or not. + * @return "rtl" if the given directionality is RTL, and "ltr" otherwise. + */ + public String dirAttrValue(boolean isRtl) { + return isRtl ? RTL_STRING : LTR_STRING; + } + + /** + * Returns "dir=\"ltr\"" or "dir=\"rtl\"", depending on {@code str}'s estimated directionality, + * if it is not the same as the context directionality. Otherwise, returns the empty string. + * + * @param str String whose directionality is to be estimated. + * @return "dir=\"rtl\"" for RTL text in non-RTL context; "dir=\"ltr\"" for LTR text in non-LTR + * context; else, the empty string. + */ + public String dirAttr(String str) { + return dirAttr(isRtl(str)); + } + + /** + * Operates like {@link #dirAttr(String)}, but uses a given heuristic to estimate the + * {@code str}'s directionality. + * + * @param str String whose directionality is to be estimated. + * @param heuristic The text direction heuristic that will be used to estimate the {@code str}'s + * directionality. + * @return "dir=\"rtl\"" for RTL text in non-RTL context; "dir=\"ltr\"" for LTR text in non-LTR + * context; else, the empty string. + */ + public String dirAttr(String str, TextDirectionHeuristic heuristic) { + return dirAttr(heuristic.isRtl(str, 0, str.length())); + } + + /** + * Returns "dir=\"ltr\"" or "dir=\"rtl\"", depending on the given directionality, if it is not + * the same as the context directionality. Otherwise, returns the empty string. + * + * @param isRtl Whether the directionality is RTL or not + * @return "dir=\"rtl\"" for RTL text in non-RTL context; "dir=\"ltr\"" for LTR text in non-LTR + * context; else, the empty string. + */ + public String dirAttr(boolean isRtl) { + return (isRtl != isRtlContext) ? (isRtl ? DIR_RTL_STRING : DIR_LTR_STRING) : EMPTY_STRING; + } + + /** + * Returns a Unicode bidi mark matching the context directionality (LRM or RLM) if either the + * overall or the exit directionality of a given string is opposite to the context directionality. + * Putting this after the string (including its directionality declaration wrapping) prevents it + * from "sticking" to other opposite-directionality text or a number appearing after it inline + * with only neutral content in between. Otherwise returns the empty string. While the exit + * directionality is determined by scanning the end of the string, the overall directionality is + * given explicitly in {@code dir}. + * + * @param str String after which the mark may need to appear. + * @return LRM for RTL text in LTR context; RLM for LTR text in RTL context; + * else, the empty string. + */ + public String markAfter(String str) { + return markAfter(str, defaultTextDirectionHeuristic); + } + + /** + * Operates like {@link #markAfter(String)}, but uses a given heuristic to estimate the + * {@code str}'s directionality. + * + * @param str String after which the mark may need to appear. + * @param heuristic The text direction heuristic that will be used to estimate the {@code str}'s + * directionality. + * @return LRM for RTL text in LTR context; RLM for LTR text in RTL context; + * else, the empty string. + */ + public String markAfter(String str, TextDirectionHeuristic heuristic) { + final boolean isRtl = heuristic.isRtl(str, 0, str.length()); + // getExitDir() is called only if needed (short-circuit). + if (!isRtlContext && (isRtl || getExitDir(str) == DIR_RTL)) { + return LRM_STRING; + } + if (isRtlContext && (!isRtl || getExitDir(str) == DIR_LTR)) { + return RLM_STRING; + } + return EMPTY_STRING; + } + + /** + * Returns a Unicode bidi mark matching the context directionality (LRM or RLM) if either the + * overall or the entry directionality of a given string is opposite to the context + * directionality. Putting this before the string (including its directionality declaration + * wrapping) prevents it from "sticking" to other opposite-directionality text appearing before it + * inline with only neutral content in between. Otherwise returns the empty string. While the + * entry directionality is determined by scanning the beginning of the string, the overall + * directionality is given explicitly in {@code dir}. + * + * @param str String before which the mark may need to appear. + * @return LRM for RTL text in LTR context; RLM for LTR text in RTL context; + * else, the empty string. + */ + public String markBefore(String str) { + return markBefore(str, defaultTextDirectionHeuristic); + } + + /** + * Operates like {@link #markBefore(String)}, but uses a given heuristic to estimate the + * {@code str}'s directionality. + * + * @param str String before which the mark may need to appear. + * @param heuristic The text direction heuristic that will be used to estimate the {@code str}'s + * directionality. + * @return LRM for RTL text in LTR context; RLM for LTR text in RTL context; + * else, the empty string. + */ + public String markBefore(String str, TextDirectionHeuristic heuristic) { + final boolean isRtl = heuristic.isRtl(str, 0, str.length()); + // getEntryDir() is called only if needed (short-circuit). + if (!isRtlContext && (isRtl || getEntryDir(str) == DIR_RTL)) { + return LRM_STRING; + } + if (isRtlContext && (!isRtl || getEntryDir(str) == DIR_LTR)) { + return RLM_STRING; + } + return EMPTY_STRING; + } + + /** + * Returns the Unicode bidi mark matching the context directionality (LRM for LTR context + * directionality, RLM for RTL context directionality). + */ + public String mark() { + return isRtlContext ? RLM_STRING : LRM_STRING; + } + + /** + * Returns "right" for RTL context directionality. Otherwise for LTR context directionality + * returns "left". + */ + public String startEdge() { + return isRtlContext ? RIGHT : LEFT; + } + + /** + * Returns "left" for RTL context directionality. Otherwise for LTR context directionality + * returns "right". + */ + public String endEdge() { + return isRtlContext ? LEFT : RIGHT; + } + + /** + * Estimates the directionality of a string using the default text direction heuristic. + * + * @param str String whose directionality is to be estimated. + * @return true if {@code str}'s estimated overall directionality is RTL. Otherwise returns + * false. + */ + public boolean isRtl(String str) { + return defaultTextDirectionHeuristic.isRtl(str, 0, str.length()); + } + + /** + * Formats a given string of unknown directionality for use in HTML output of the context + * directionality, so an opposite-directionality string is neither garbled nor garbles its + * surroundings. + * <p> + * The algorithm: estimates the directionality of the given string using the given heuristic. + * If the directionality is known, pass TextDirectionHeuristics.LTR or RTL for heuristic. + * In case its directionality doesn't match the context directionality, wraps it with a 'span' + * element and adds a "dir" attribute (either 'dir=\"rtl\"' or 'dir=\"ltr\"'). + * <p> + * If {@code isolate}, directionally isolates the string so that it does not garble its + * surroundings. Currently, this is done by "resetting" the directionality after the string by + * appending a trailing Unicode bidi mark matching the context directionality (LRM or RLM) when + * either the overall directionality or the exit directionality of the string is opposite to that + * of the context. If the formatter was built using {@link Builder#stereoReset(boolean)} and + * passing "true" as an argument, also prepends a Unicode bidi mark matching the context + * directionality when either the overall directionality or the entry directionality of the + * string is opposite to that of the context. + * <p> + * + * @param str The input string. + * @param heuristic The algorithm to be used to estimate the string's overall direction. + * @param isolate Whether to directionally isolate the string to prevent it from garbling the + * content around it. + * @return Input string after applying the above processing. + */ + public String spanWrap(String str, TextDirectionHeuristic heuristic, boolean isolate) { + final boolean isRtl = heuristic.isRtl(str, 0, str.length()); + String origStr = str; + str = TextUtils.htmlEncode(str); + + StringBuilder result = new StringBuilder(); + if (getStereoReset() && isolate) { + result.append(markBefore(origStr, + isRtl ? TextDirectionHeuristics.RTL : TextDirectionHeuristics.LTR)); + } + if (isRtl != isRtlContext) { + result.append("<span ").append(dirAttr(isRtl)).append('>').append(str).append("</span>"); + } else { + result.append(str); + } + if (isolate) { + result.append(markAfter(origStr, + isRtl ? TextDirectionHeuristics.RTL : TextDirectionHeuristics.LTR)); + } + return result.toString(); + } + + /** + * Operates like {@link #spanWrap(String, TextDirectionHeuristic, boolean)}, but assumes + * {@code isolate} is true. + * + * @param str The input string. + * @param heuristic The algorithm to be used to estimate the string's overall direction. + * @return Input string after applying the above processing. + */ + public String spanWrap(String str, TextDirectionHeuristic heuristic) { + return spanWrap(str, heuristic, true /* isolate */); + } + + /** + * Operates like {@link #spanWrap(String, TextDirectionHeuristic, boolean)}, but uses the + * formatter's default direction estimation algorithm. + * + * @param str The input string. + * @param isolate Whether to directionally isolate the string to prevent it from garbling the + * content around it + * @return Input string after applying the above processing. + */ + public String spanWrap(String str, boolean isolate) { + return spanWrap(str, defaultTextDirectionHeuristic, isolate); + } + + /** + * Operates like {@link #spanWrap(String, TextDirectionHeuristic, boolean)}, but uses the + * formatter's default direction estimation algorithm and assumes {@code isolate} is true. + * + * @param str The input string. + * @return Input string after applying the above processing. + */ + public String spanWrap(String str) { + return spanWrap(str, defaultTextDirectionHeuristic, true /* isolate */); + } + + /** + * Formats a string of given directionality for use in plain-text output of the context + * directionality, so an opposite-directionality string is neither garbled nor garbles its + * surroundings. As opposed to {@link #spanWrap}, this makes use of Unicode bidi + * formatting characters. In HTML, its *only* valid use is inside of elements that do not allow + * markup, e.g. the 'option' and 'title' elements. + * <p> + * The algorithm: In case the given directionality doesn't match the context directionality, wraps + * the string with Unicode bidi formatting characters: RLE+{@code str}+PDF for RTL text, or + * LRE+{@code str}+PDF for LTR text. + * <p> + * If {@code isolate}, directionally isolates the string so that it does not garble its + * surroundings. Currently, this is done by "resetting" the directionality after the string by + * appending a trailing Unicode bidi mark matching the context directionality (LRM or RLM) when + * either the overall directionality or the exit directionality of the string is opposite to that + * of the context. If the formatter was built using {@link Builder#stereoReset(boolean)} and + * passing "true" as an argument, also prepends a Unicode bidi mark matching the context + * directionality when either the overall directionality or the entry directionality of the + * string is opposite to that of the context. Note that as opposed to the overall + * directionality, the entry and exit directionalities are determined from the string itself. + * <p> + * Does *not* do HTML-escaping. + * + * @param str The input string. + * @param heuristic The algorithm to be used to estimate the string's overall direction. + * @param isolate Whether to directionally isolate the string to prevent it from garbling the + * content around it + * @return Input string after applying the above processing. + */ + public String unicodeWrap(String str, TextDirectionHeuristic heuristic, boolean isolate) { + final boolean isRtl = heuristic.isRtl(str, 0, str.length()); + StringBuilder result = new StringBuilder(); + if (getStereoReset() && isolate) { + result.append(markBefore(str, + isRtl ? TextDirectionHeuristics.RTL : TextDirectionHeuristics.LTR)); + } + if (isRtl != isRtlContext) { + result.append(isRtl ? RLE : LRE); + result.append(str); + result.append(PDF); + } else { + result.append(str); + } + if (isolate) { + result.append(markAfter(str, + isRtl ? TextDirectionHeuristics.RTL : TextDirectionHeuristics.LTR)); + } + return result.toString(); + } + + /** + * Operates like {@link #unicodeWrap(String, TextDirectionHeuristic, boolean)}, but assumes + * {@code isolate} is true. + * + * @param str The input string. + * @param heuristic The algorithm to be used to estimate the string's overall direction. + * @return Input string after applying the above processing. + */ + public String unicodeWrap(String str, TextDirectionHeuristic heuristic) { + return unicodeWrap(str, heuristic, true /* isolate */); + } + + /** + * Operates like {@link #unicodeWrap(String, TextDirectionHeuristic, boolean)}, but uses the + * formatter's default direction estimation algorithm. + * + * @param str The input string. + * @param isolate Whether to directionally isolate the string to prevent it from garbling the + * content around it + * @return Input string after applying the above processing. + */ + public String unicodeWrap(String str, boolean isolate) { + return unicodeWrap(str, defaultTextDirectionHeuristic, isolate); + } + + /** + * Operates like {@link #unicodeWrap(String, TextDirectionHeuristic, boolean)}, but uses the + * formatter's default direction estimation algorithm and assumes {@code isolate} is true. + * + * @param str The input string. + * @return Input string after applying the above processing. + */ + public String unicodeWrap(String str) { + return unicodeWrap(str, defaultTextDirectionHeuristic, true /* isolate */); + } + + /** + * Helper method to return true if the Locale directionality is RTL. + * + * @param locale The Locale whose directionality will be checked to be RTL or LTR + * @return true if the {@code locale} directionality is RTL. False otherwise. + */ + private static boolean isRtlLocale(Locale locale) { + return (TextUtils.getLayoutDirectionFromLocale(locale) == View.LAYOUT_DIRECTION_RTL); + } + + /** + * Enum for directionality type. + */ + private static final int DIR_LTR = -1; + private static final int DIR_UNKNOWN = 0; + private static final int DIR_RTL = +1; + + /** + * Returns the directionality of the last character with strong directionality in the string, or + * DIR_UNKNOWN if none was encountered. For efficiency, actually scans backwards from the end of + * the string. Treats a non-BN character between an LRE/RLE/LRO/RLO and its matching PDF as a + * strong character, LTR after LRE/LRO, and RTL after RLE/RLO. The results are undefined for a + * string containing unbalanced LRE/RLE/LRO/RLO/PDF characters. The intended use is to check + * whether a logically separate item that starts with a number or a character of the string's + * exit directionality and follows this string inline (not counting any neutral characters in + * between) would "stick" to it in an opposite-directionality context, thus being displayed in + * an incorrect position. An LRM or RLM character (the one of the context's directionality) + * between the two will prevent such sticking. + * + * @param str the string to check. + */ + private static int getExitDir(String str) { + return new DirectionalityEstimator(str, false /* isHtml */).getExitDir(); + } + + /** + * Returns the directionality of the first character with strong directionality in the string, + * or DIR_UNKNOWN if none was encountered. Treats a non-BN character between an + * LRE/RLE/LRO/RLO and its matching PDF as a strong character, LTR after LRE/LRO, and RTL after + * RLE/RLO. The results are undefined for a string containing unbalanced LRE/RLE/LRO/RLO/PDF + * characters. The intended use is to check whether a logically separate item that ends with a + * character of the string's entry directionality and precedes the string inline (not counting + * any neutral characters in between) would "stick" to it in an opposite-directionality context, + * thus being displayed in an incorrect position. An LRM or RLM character (the one of the + * context's directionality) between the two will prevent such sticking. + * + * @param str the string to check. + */ + private static int getEntryDir(String str) { + return new DirectionalityEstimator(str, false /* isHtml */).getEntryDir(); + } + + /** + * An object that estimates the directionality of a given string by various methods. + * + */ + private static class DirectionalityEstimator { + + // Internal static variables and constants. + + /** + * Size of the bidi character class cache. The results of the Character.getDirectionality() + * calls on the lowest DIR_TYPE_CACHE_SIZE codepoints are kept in an array for speed. + * The 0x700 value is designed to leave all the European and Near Eastern languages in the + * cache. It can be reduced to 0x180, restricting the cache to the Western European + * languages. + */ + private static final int DIR_TYPE_CACHE_SIZE = 0x700; + + /** + * The bidi character class cache. + */ + private static final byte DIR_TYPE_CACHE[]; + + static { + DIR_TYPE_CACHE = new byte[DIR_TYPE_CACHE_SIZE]; + for (int i = 0; i < DIR_TYPE_CACHE_SIZE; i++) { + DIR_TYPE_CACHE[i] = Character.getDirectionality(i); + } + } + + // Internal instance variables. + + /** + * The text to be scanned. + */ + private final String text; + + /** + * Whether the text to be scanned is to be treated as HTML, i.e. skipping over tags and + * entities when looking for the next / preceding dir type. + */ + private final boolean isHtml; + + /** + * The length of the text in chars. + */ + private final int length; + + /** + * The current position in the text. + */ + private int charIndex; + + /** + * The char encountered by the last dirTypeForward or dirTypeBackward call. If it + * encountered a supplementary codepoint, this contains a char that is not a valid + * codepoint. This is ok, because this member is only used to detect some well-known ASCII + * syntax, e.g. "http://" and the beginning of an HTML tag or entity. + */ + private char lastChar; + + /** + * Constructor. + * + * @param text The string to scan. + * @param isHtml Whether the text to be scanned is to be treated as HTML, i.e. skipping over + * tags and entities. + */ + DirectionalityEstimator(String text, boolean isHtml) { + this.text = text; + this.isHtml = isHtml; + length = text.length(); + } + + /** + * Returns the directionality of the first character with strong directionality in the + * string, or DIR_UNKNOWN if none was encountered. Treats a non-BN character between an + * LRE/RLE/LRO/RLO and its matching PDF as a strong character, LTR after LRE/LRO, and RTL + * after RLE/RLO. The results are undefined for a string containing unbalanced + * LRE/RLE/LRO/RLO/PDF characters. + */ + int getEntryDir() { + // The reason for this method name, as opposed to getFirstStrongDir(), is that + // "first strong" is a commonly used description of Unicode's estimation algorithm, + // but the two must treat formatting characters quite differently. Thus, we are staying + // away from both "first" and "last" in these method names to avoid confusion. + charIndex = 0; + int embeddingLevel = 0; + int embeddingLevelDir = DIR_UNKNOWN; + int firstNonEmptyEmbeddingLevel = 0; + while (charIndex < length && firstNonEmptyEmbeddingLevel == 0) { + switch (dirTypeForward()) { + case Character.DIRECTIONALITY_LEFT_TO_RIGHT_EMBEDDING: + case Character.DIRECTIONALITY_LEFT_TO_RIGHT_OVERRIDE: + ++embeddingLevel; + embeddingLevelDir = DIR_LTR; + break; + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE: + ++embeddingLevel; + embeddingLevelDir = DIR_RTL; + break; + case Character.DIRECTIONALITY_POP_DIRECTIONAL_FORMAT: + --embeddingLevel; + // To restore embeddingLevelDir to its previous value, we would need a + // stack, which we want to avoid. Thus, at this point we do not know the + // current embedding's directionality. + embeddingLevelDir = DIR_UNKNOWN; + break; + case Character.DIRECTIONALITY_BOUNDARY_NEUTRAL: + break; + case Character.DIRECTIONALITY_LEFT_TO_RIGHT: + if (embeddingLevel == 0) { + return DIR_LTR; + } + firstNonEmptyEmbeddingLevel = embeddingLevel; + break; + case Character.DIRECTIONALITY_RIGHT_TO_LEFT: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC: + if (embeddingLevel == 0) { + return DIR_RTL; + } + firstNonEmptyEmbeddingLevel = embeddingLevel; + break; + default: + firstNonEmptyEmbeddingLevel = embeddingLevel; + break; + } + } + + // We have either found a non-empty embedding or scanned the entire string finding + // neither a non-empty embedding nor a strong character outside of an embedding. + if (firstNonEmptyEmbeddingLevel == 0) { + // We have not found a non-empty embedding. Thus, the string contains neither a + // non-empty embedding nor a strong character outside of an embedding. + return DIR_UNKNOWN; + } + + // We have found a non-empty embedding. + if (embeddingLevelDir != DIR_UNKNOWN) { + // We know the directionality of the non-empty embedding. + return embeddingLevelDir; + } + + // We do not remember the directionality of the non-empty embedding we found. So, we go + // backwards to find the start of the non-empty embedding and get its directionality. + while (charIndex > 0) { + switch (dirTypeBackward()) { + case Character.DIRECTIONALITY_LEFT_TO_RIGHT_EMBEDDING: + case Character.DIRECTIONALITY_LEFT_TO_RIGHT_OVERRIDE: + if (firstNonEmptyEmbeddingLevel == embeddingLevel) { + return DIR_LTR; + } + --embeddingLevel; + break; + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE: + if (firstNonEmptyEmbeddingLevel == embeddingLevel) { + return DIR_RTL; + } + --embeddingLevel; + break; + case Character.DIRECTIONALITY_POP_DIRECTIONAL_FORMAT: + ++embeddingLevel; + break; + } + } + // We should never get here. + return DIR_UNKNOWN; + } + + /** + * Returns the directionality of the last character with strong directionality in the + * string, or DIR_UNKNOWN if none was encountered. For efficiency, actually scans backwards + * from the end of the string. Treats a non-BN character between an LRE/RLE/LRO/RLO and its + * matching PDF as a strong character, LTR after LRE/LRO, and RTL after RLE/RLO. The results + * are undefined for a string containing unbalanced LRE/RLE/LRO/RLO/PDF characters. + */ + int getExitDir() { + // The reason for this method name, as opposed to getLastStrongDir(), is that "last + // strong" sounds like the exact opposite of "first strong", which is a commonly used + // description of Unicode's estimation algorithm (getUnicodeDir() above), but the two + // must treat formatting characters quite differently. Thus, we are staying away from + // both "first" and "last" in these method names to avoid confusion. + charIndex = length; + int embeddingLevel = 0; + int lastNonEmptyEmbeddingLevel = 0; + while (charIndex > 0) { + switch (dirTypeBackward()) { + case Character.DIRECTIONALITY_LEFT_TO_RIGHT: + if (embeddingLevel == 0) { + return DIR_LTR; + } + if (lastNonEmptyEmbeddingLevel == 0) { + lastNonEmptyEmbeddingLevel = embeddingLevel; + } + break; + case Character.DIRECTIONALITY_LEFT_TO_RIGHT_EMBEDDING: + case Character.DIRECTIONALITY_LEFT_TO_RIGHT_OVERRIDE: + if (lastNonEmptyEmbeddingLevel == embeddingLevel) { + return DIR_LTR; + } + --embeddingLevel; + break; + case Character.DIRECTIONALITY_RIGHT_TO_LEFT: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC: + if (embeddingLevel == 0) { + return DIR_RTL; + } + if (lastNonEmptyEmbeddingLevel == 0) { + lastNonEmptyEmbeddingLevel = embeddingLevel; + } + break; + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE: + if (lastNonEmptyEmbeddingLevel == embeddingLevel) { + return DIR_RTL; + } + --embeddingLevel; + break; + case Character.DIRECTIONALITY_POP_DIRECTIONAL_FORMAT: + ++embeddingLevel; + break; + case Character.DIRECTIONALITY_BOUNDARY_NEUTRAL: + break; + default: + if (lastNonEmptyEmbeddingLevel == 0) { + lastNonEmptyEmbeddingLevel = embeddingLevel; + } + break; + } + } + return DIR_UNKNOWN; + } + + // Internal methods + + /** + * Gets the bidi character class, i.e. Character.getDirectionality(), of a given char, using + * a cache for speed. Not designed for supplementary codepoints, whose results we do not + * cache. + */ + private static byte getCachedDirectionality(char c) { + return c < DIR_TYPE_CACHE_SIZE ? DIR_TYPE_CACHE[c] : Character.getDirectionality(c); + } + + /** + * Returns the Character.DIRECTIONALITY_... value of the next codepoint and advances + * charIndex. If isHtml, and the codepoint is '<' or '&', advances through the tag/entity, + * and returns Character.DIRECTIONALITY_WHITESPACE. For an entity, it would be best to + * figure out the actual character, and return its dirtype, but treating it as whitespace is + * good enough for our purposes. + * + * @throws java.lang.IndexOutOfBoundsException if called when charIndex >= length or < 0. + */ + byte dirTypeForward() { + lastChar = text.charAt(charIndex); + if (Character.isHighSurrogate(lastChar)) { + int codePoint = Character.codePointAt(text, charIndex); + charIndex += Character.charCount(codePoint); + return Character.getDirectionality(codePoint); + } + charIndex++; + byte dirType = getCachedDirectionality(lastChar); + if (isHtml) { + // Process tags and entities. + if (lastChar == '<') { + dirType = skipTagForward(); + } else if (lastChar == '&') { + dirType = skipEntityForward(); + } + } + return dirType; + } + + /** + * Returns the Character.DIRECTIONALITY_... value of the preceding codepoint and advances + * charIndex backwards. If isHtml, and the codepoint is the end of a complete HTML tag or + * entity, advances over the whole tag/entity and returns + * Character.DIRECTIONALITY_WHITESPACE. For an entity, it would be best to figure out the + * actual character, and return its dirtype, but treating it as whitespace is good enough + * for our purposes. + * + * @throws java.lang.IndexOutOfBoundsException if called when charIndex > length or <= 0. + */ + byte dirTypeBackward() { + lastChar = text.charAt(charIndex - 1); + if (Character.isLowSurrogate(lastChar)) { + int codePoint = Character.codePointBefore(text, charIndex); + charIndex -= Character.charCount(codePoint); + return Character.getDirectionality(codePoint); + } + charIndex--; + byte dirType = getCachedDirectionality(lastChar); + if (isHtml) { + // Process tags and entities. + if (lastChar == '>') { + dirType = skipTagBackward(); + } else if (lastChar == ';') { + dirType = skipEntityBackward(); + } + } + return dirType; + } + + /** + * Advances charIndex forward through an HTML tag (after the opening < has already been + * read) and returns Character.DIRECTIONALITY_WHITESPACE. If there is no matching >, + * does not change charIndex and returns Character.DIRECTIONALITY_OTHER_NEUTRALS (for the + * < that hadn't been part of a tag after all). + */ + private byte skipTagForward() { + int initialCharIndex = charIndex; + while (charIndex < length) { + lastChar = text.charAt(charIndex++); + if (lastChar == '>') { + // The end of the tag. + return Character.DIRECTIONALITY_WHITESPACE; + } + if (lastChar == '"' || lastChar == '\'') { + // Skip over a quoted attribute value inside the tag. + char quote = lastChar; + while (charIndex < length && (lastChar = text.charAt(charIndex++)) != quote) {} + } + } + // The original '<' wasn't the start of a tag after all. + charIndex = initialCharIndex; + lastChar = '<'; + return Character.DIRECTIONALITY_OTHER_NEUTRALS; + } + + /** + * Advances charIndex backward through an HTML tag (after the closing > has already been + * read) and returns Character.DIRECTIONALITY_WHITESPACE. If there is no matching <, does + * not change charIndex and returns Character.DIRECTIONALITY_OTHER_NEUTRALS (for the > + * that hadn't been part of a tag after all). Nevertheless, the running time for calling + * skipTagBackward() in a loop remains linear in the size of the text, even for a text like + * ">>>>", because skipTagBackward() also stops looking for a matching < + * when it encounters another >. + */ + private byte skipTagBackward() { + int initialCharIndex = charIndex; + while (charIndex > 0) { + lastChar = text.charAt(--charIndex); + if (lastChar == '<') { + // The start of the tag. + return Character.DIRECTIONALITY_WHITESPACE; + } + if (lastChar == '>') { + break; + } + if (lastChar == '"' || lastChar == '\'') { + // Skip over a quoted attribute value inside the tag. + char quote = lastChar; + while (charIndex > 0 && (lastChar = text.charAt(--charIndex)) != quote) {} + } + } + // The original '>' wasn't the end of a tag after all. + charIndex = initialCharIndex; + lastChar = '>'; + return Character.DIRECTIONALITY_OTHER_NEUTRALS; + } + + /** + * Advances charIndex forward through an HTML character entity tag (after the opening + * & has already been read) and returns Character.DIRECTIONALITY_WHITESPACE. It would be + * best to figure out the actual character and return its dirtype, but this is good enough. + */ + private byte skipEntityForward() { + while (charIndex < length && (lastChar = text.charAt(charIndex++)) != ';') {} + return Character.DIRECTIONALITY_WHITESPACE; + } + + /** + * Advances charIndex backward through an HTML character entity tag (after the closing ; + * has already been read) and returns Character.DIRECTIONALITY_WHITESPACE. It would be best + * to figure out the actual character and return its dirtype, but this is good enough. + * If there is no matching &, does not change charIndex and returns + * Character.DIRECTIONALITY_OTHER_NEUTRALS (for the ';' that did not start an entity after + * all). Nevertheless, the running time for calling skipEntityBackward() in a loop remains + * linear in the size of the text, even for a text like ";;;;;;;", because skipTagBackward() + * also stops looking for a matching & when it encounters another ;. + */ + private byte skipEntityBackward() { + int initialCharIndex = charIndex; + while (charIndex > 0) { + lastChar = text.charAt(--charIndex); + if (lastChar == '&') { + return Character.DIRECTIONALITY_WHITESPACE; + } + if (lastChar == ';') { + break; + } + } + charIndex = initialCharIndex; + lastChar = ';'; + return Character.DIRECTIONALITY_OTHER_NEUTRALS; + } + } +}
\ No newline at end of file diff --git a/core/java/android/text/format/DateFormat.java b/core/java/android/text/format/DateFormat.java index cc676de..7e230ac 100644 --- a/core/java/android/text/format/DateFormat.java +++ b/core/java/android/text/format/DateFormat.java @@ -447,6 +447,16 @@ public class DateFormat { * @hide */ public static boolean hasSeconds(CharSequence inFormat) { + return hasDesignator(inFormat, SECONDS); + } + + /** + * Test if a format string contains the given designator. Always returns + * {@code false} if the input format is {@code null}. + * + * @hide + */ + public static boolean hasDesignator(CharSequence inFormat, char designator) { if (inFormat == null) return false; final int length = inFormat.length(); @@ -460,7 +470,7 @@ public class DateFormat { if (c == QUOTE) { count = skipQuotedText(inFormat, i, length); - } else if (c == SECONDS) { + } else if (c == designator) { return true; } } diff --git a/core/java/android/text/method/DigitsKeyListener.java b/core/java/android/text/method/DigitsKeyListener.java index 3d9daed..c95df46 100644 --- a/core/java/android/text/method/DigitsKeyListener.java +++ b/core/java/android/text/method/DigitsKeyListener.java @@ -49,13 +49,22 @@ public class DigitsKeyListener extends NumberKeyListener * @see KeyEvent#getMatch * @see #getAcceptedChars */ - private static final char[][] CHARACTERS = new char[][] { - new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }, - new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-' }, - new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.' }, - new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '.' }, + private static final char[][] CHARACTERS = { + { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }, + { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '+' }, + { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.' }, + { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '+', '.' }, }; + private static boolean isSignChar(final char c) { + return c == '-' || c == '+'; + } + + // TODO: Needs internationalization + private static boolean isDecimalPointChar(final char c) { + return c == '.'; + } + /** * Allocates a DigitsKeyListener that accepts the digits 0 through 9. */ @@ -145,32 +154,32 @@ public class DigitsKeyListener extends NumberKeyListener int dlen = dest.length(); /* - * Find out if the existing text has '-' or '.' characters. + * Find out if the existing text has a sign or decimal point characters. */ for (int i = 0; i < dstart; i++) { char c = dest.charAt(i); - if (c == '-') { + if (isSignChar(c)) { sign = i; - } else if (c == '.') { + } else if (isDecimalPointChar(c)) { decimal = i; } } for (int i = dend; i < dlen; i++) { char c = dest.charAt(i); - if (c == '-') { - return ""; // Nothing can be inserted in front of a '-'. - } else if (c == '.') { + if (isSignChar(c)) { + return ""; // Nothing can be inserted in front of a sign character. + } else if (isDecimalPointChar(c)) { decimal = i; } } /* * If it does, we must strip them out from the source. - * In addition, '-' must be the very first character, - * and nothing can be inserted before an existing '-'. + * In addition, a sign character must be the very first character, + * and nothing can be inserted before an existing sign character. * Go in reverse order so the offsets are stable. */ @@ -180,7 +189,7 @@ public class DigitsKeyListener extends NumberKeyListener char c = source.charAt(i); boolean strip = false; - if (c == '-') { + if (isSignChar(c)) { if (i != start || dstart != 0) { strip = true; } else if (sign >= 0) { @@ -188,7 +197,7 @@ public class DigitsKeyListener extends NumberKeyListener } else { sign = i; } - } else if (c == '.') { + } else if (isDecimalPointChar(c)) { if (decimal >= 0) { strip = true; } else { diff --git a/core/java/android/text/method/QwertyKeyListener.java b/core/java/android/text/method/QwertyKeyListener.java index c5261f3..98316ae 100644 --- a/core/java/android/text/method/QwertyKeyListener.java +++ b/core/java/android/text/method/QwertyKeyListener.java @@ -180,6 +180,7 @@ public class QwertyKeyListener extends BaseKeyListener { if (composed != 0) { i = composed; replace = true; + dead = false; } } diff --git a/core/java/android/text/style/EasyEditSpan.java b/core/java/android/text/style/EasyEditSpan.java index 2feb719..03b4f60 100644 --- a/core/java/android/text/style/EasyEditSpan.java +++ b/core/java/android/text/style/EasyEditSpan.java @@ -16,6 +16,7 @@ package android.text.style; +import android.app.PendingIntent; import android.os.Parcel; import android.text.ParcelableSpan; import android.text.TextUtils; @@ -25,12 +26,62 @@ import android.widget.TextView; * Provides an easy way to edit a portion of text. * <p> * The {@link TextView} uses this span to allow the user to delete a chuck of text in one click. - * the text. {@link TextView} removes this span as soon as the text is edited, or the cursor moves. + * <p> + * {@link TextView} removes the span when the user deletes the whole text or modifies it. + * <p> + * This span can be also used to receive notification when the user deletes or modifies the text; */ public class EasyEditSpan implements ParcelableSpan { + /** + * The extra key field in the pending intent that describes how the text changed. + * + * @see #TEXT_DELETED + * @see #TEXT_MODIFIED + * @see #getPendingIntent() + */ + public static final String EXTRA_TEXT_CHANGED_TYPE = + "android.text.style.EXTRA_TEXT_CHANGED_TYPE"; + + /** + * The value of {@link #EXTRA_TEXT_CHANGED_TYPE} when the text wrapped by this span is deleted. + */ + public static final int TEXT_DELETED = 1; + + /** + * The value of {@link #EXTRA_TEXT_CHANGED_TYPE} when the text wrapped by this span is modified. + */ + public static final int TEXT_MODIFIED = 2; + + private final PendingIntent mPendingIntent; + + private boolean mDeleteEnabled; + + /** + * Creates the span. No intent is sent when the wrapped text is modified or + * deleted. + */ public EasyEditSpan() { - // Empty + mPendingIntent = null; + mDeleteEnabled = true; + } + + /** + * @param pendingIntent The intent will be sent when the wrapped text is deleted or modified. + * When the pending intent is sent, {@link #EXTRA_TEXT_CHANGED_TYPE} is + * added in the intent to describe how the text changed. + */ + public EasyEditSpan(PendingIntent pendingIntent) { + mPendingIntent = pendingIntent; + mDeleteEnabled = true; + } + + /** + * Constructor called from {@link TextUtils} to restore the span. + */ + public EasyEditSpan(Parcel source) { + mPendingIntent = source.readParcelable(null); + mDeleteEnabled = (source.readByte() == 1); } @Override @@ -40,11 +91,39 @@ public class EasyEditSpan implements ParcelableSpan { @Override public void writeToParcel(Parcel dest, int flags) { - // Empty + dest.writeParcelable(mPendingIntent, 0); + dest.writeByte((byte) (mDeleteEnabled ? 1 : 0)); } @Override public int getSpanTypeId() { return TextUtils.EASY_EDIT_SPAN; } + + /** + * @return True if the {@link TextView} should offer the ability to delete the text. + * + * @hide + */ + public boolean isDeleteEnabled() { + return mDeleteEnabled; + } + + /** + * Enables or disables the deletion of the text. + * + * @hide + */ + public void setDeleteEnabled(boolean value) { + mDeleteEnabled = value; + } + + /** + * @return the pending intent to send when the wrapped text is deleted or modified. + * + * @hide + */ + public PendingIntent getPendingIntent() { + return mPendingIntent; + } } diff --git a/core/java/android/text/style/SuggestionSpan.java b/core/java/android/text/style/SuggestionSpan.java index 5dc206f..0ec7e84 100644 --- a/core/java/android/text/style/SuggestionSpan.java +++ b/core/java/android/text/style/SuggestionSpan.java @@ -17,6 +17,7 @@ package android.text.style; import android.content.Context; +import android.content.Intent; import android.content.res.TypedArray; import android.graphics.Color; import android.os.Parcel; @@ -26,6 +27,7 @@ import android.text.ParcelableSpan; import android.text.TextPaint; import android.text.TextUtils; import android.util.Log; +import android.view.inputmethod.InputMethodManager; import android.widget.TextView; import java.util.Arrays; @@ -45,6 +47,8 @@ import java.util.Locale; */ public class SuggestionSpan extends CharacterStyle implements ParcelableSpan { + private static final String TAG = "SuggestionSpan"; + /** * Sets this flag if the suggestions should be easily accessible with few interactions. * This flag should be set for every suggestions that the user is likely to use. @@ -82,6 +86,7 @@ public class SuggestionSpan extends CharacterStyle implements ParcelableSpan { private final String[] mSuggestions; private final String mLocaleString; private final String mNotificationTargetClassName; + private final String mNotificationTargetPackageName; private final int mHashCode; private float mEasyCorrectUnderlineThickness; @@ -134,6 +139,12 @@ public class SuggestionSpan extends CharacterStyle implements ParcelableSpan { mLocaleString = ""; } + if (context != null) { + mNotificationTargetPackageName = context.getPackageName(); + } else { + mNotificationTargetPackageName = null; + } + if (notificationTargetClass != null) { mNotificationTargetClassName = notificationTargetClass.getCanonicalName(); } else { @@ -185,6 +196,7 @@ public class SuggestionSpan extends CharacterStyle implements ParcelableSpan { mFlags = src.readInt(); mLocaleString = src.readString(); mNotificationTargetClassName = src.readString(); + mNotificationTargetPackageName = src.readString(); mHashCode = src.readInt(); mEasyCorrectUnderlineColor = src.readInt(); mEasyCorrectUnderlineThickness = src.readFloat(); @@ -240,6 +252,7 @@ public class SuggestionSpan extends CharacterStyle implements ParcelableSpan { dest.writeInt(mFlags); dest.writeString(mLocaleString); dest.writeString(mNotificationTargetClassName); + dest.writeString(mNotificationTargetPackageName); dest.writeInt(mHashCode); dest.writeInt(mEasyCorrectUnderlineColor); dest.writeFloat(mEasyCorrectUnderlineThickness); @@ -325,4 +338,40 @@ public class SuggestionSpan extends CharacterStyle implements ParcelableSpan { } return 0; } + + /** + * Notifies a suggestion selection. + * + * @hide + */ + public void notifySelection(Context context, String original, int index) { + final Intent intent = new Intent(); + + if (context == null || mNotificationTargetClassName == null) { + return; + } + // Ensures that only a class in the original IME package will receive the + // notification. + if (mSuggestions == null || index < 0 || index >= mSuggestions.length) { + Log.w(TAG, "Unable to notify the suggestion as the index is out of range index=" + index + + " length=" + mSuggestions.length); + return; + } + + // The package name is not mandatory (legacy from JB), and if the package name + // is missing, we try to notify the suggestion through the input method manager. + if (mNotificationTargetPackageName != null) { + intent.setClassName(mNotificationTargetPackageName, mNotificationTargetClassName); + intent.setAction(SuggestionSpan.ACTION_SUGGESTION_PICKED); + intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_BEFORE, original); + intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_AFTER, mSuggestions[index]); + intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_HASHCODE, hashCode()); + context.sendBroadcast(intent); + } else { + InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm != null) { + imm.notifySuggestionPicked(this, original, index); + } + } + } } diff --git a/core/java/android/util/DisplayMetrics.java b/core/java/android/util/DisplayMetrics.java index e856501..dae47b8 100644 --- a/core/java/android/util/DisplayMetrics.java +++ b/core/java/android/util/DisplayMetrics.java @@ -74,6 +74,15 @@ public class DisplayMetrics { public static final int DENSITY_XXHIGH = 480; /** + * Standard quantized DPI for extra-extra-extra-high-density screens. Applications + * should not generally worry about this density; relying on XHIGH graphics + * being scaled up to it should be sufficient for almost all cases. A typical + * use of this density would be 4K television screens -- 3840x2160, which + * is 2x a traditional HD 1920x1080 screen which runs at DENSITY_XHIGH. + */ + public static final int DENSITY_XXXHIGH = 640; + + /** * The reference density used throughout the system. */ public static final int DENSITY_DEFAULT = DENSITY_MEDIUM; diff --git a/core/java/android/util/FinitePool.java b/core/java/android/util/FinitePool.java deleted file mode 100644 index b30f2bf..0000000 --- a/core/java/android/util/FinitePool.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.util; - -/** - * @hide - */ -class FinitePool<T extends Poolable<T>> implements Pool<T> { - private static final String LOG_TAG = "FinitePool"; - - /** - * Factory used to create new pool objects - */ - private final PoolableManager<T> mManager; - /** - * Maximum number of objects in the pool - */ - private final int mLimit; - /** - * If true, mLimit is ignored - */ - private final boolean mInfinite; - - /** - * Next object to acquire - */ - private T mRoot; - /** - * Number of objects in the pool - */ - private int mPoolCount; - - FinitePool(PoolableManager<T> manager) { - mManager = manager; - mLimit = 0; - mInfinite = true; - } - - FinitePool(PoolableManager<T> manager, int limit) { - if (limit <= 0) throw new IllegalArgumentException("The pool limit must be > 0"); - - mManager = manager; - mLimit = limit; - mInfinite = false; - } - - public T acquire() { - T element; - - if (mRoot != null) { - element = mRoot; - mRoot = element.getNextPoolable(); - mPoolCount--; - } else { - element = mManager.newInstance(); - } - - if (element != null) { - element.setNextPoolable(null); - element.setPooled(false); - mManager.onAcquired(element); - } - - return element; - } - - public void release(T element) { - if (!element.isPooled()) { - if (mInfinite || mPoolCount < mLimit) { - mPoolCount++; - element.setNextPoolable(mRoot); - element.setPooled(true); - mRoot = element; - } - mManager.onReleased(element); - } else { - Log.w(LOG_TAG, "Element is already in pool: " + element); - } - } -} diff --git a/core/java/android/util/LongSparseLongArray.java b/core/java/android/util/LongSparseLongArray.java new file mode 100644 index 0000000..34b6126 --- /dev/null +++ b/core/java/android/util/LongSparseLongArray.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +import com.android.internal.util.ArrayUtils; + +import java.util.Arrays; + +/** + * Map of {@code long} to {@code long}. Unlike a normal array of longs, there + * can be gaps in the indices. It is intended to be more efficient than using a + * {@code HashMap}. + * + * @hide + */ +public class LongSparseLongArray implements Cloneable { + private long[] mKeys; + private long[] mValues; + private int mSize; + + /** + * Creates a new SparseLongArray containing no mappings. + */ + public LongSparseLongArray() { + this(10); + } + + /** + * Creates a new SparseLongArray containing no mappings that will not + * require any additional memory allocation to store the specified + * number of mappings. + */ + public LongSparseLongArray(int initialCapacity) { + initialCapacity = ArrayUtils.idealLongArraySize(initialCapacity); + + mKeys = new long[initialCapacity]; + mValues = new long[initialCapacity]; + mSize = 0; + } + + @Override + public LongSparseLongArray clone() { + LongSparseLongArray clone = null; + try { + clone = (LongSparseLongArray) super.clone(); + clone.mKeys = mKeys.clone(); + clone.mValues = mValues.clone(); + } catch (CloneNotSupportedException cnse) { + /* ignore */ + } + return clone; + } + + /** + * Gets the long mapped from the specified key, or <code>0</code> + * if no such mapping has been made. + */ + public long get(long key) { + return get(key, 0); + } + + /** + * Gets the long mapped from the specified key, or the specified value + * if no such mapping has been made. + */ + public long get(long key, long valueIfKeyNotFound) { + int i = Arrays.binarySearch(mKeys, 0, mSize, key); + + if (i < 0) { + return valueIfKeyNotFound; + } else { + return mValues[i]; + } + } + + /** + * Removes the mapping from the specified key, if there was any. + */ + public void delete(long key) { + int i = Arrays.binarySearch(mKeys, 0, mSize, key); + + if (i >= 0) { + removeAt(i); + } + } + + /** + * Removes the mapping at the given index. + */ + public void removeAt(int index) { + System.arraycopy(mKeys, index + 1, mKeys, index, mSize - (index + 1)); + System.arraycopy(mValues, index + 1, mValues, index, mSize - (index + 1)); + mSize--; + } + + /** + * Adds a mapping from the specified key to the specified value, + * replacing the previous mapping from the specified key if there + * was one. + */ + public void put(long key, long value) { + int i = Arrays.binarySearch(mKeys, 0, mSize, key); + + if (i >= 0) { + mValues[i] = value; + } else { + i = ~i; + + if (mSize >= mKeys.length) { + growKeyAndValueArrays(mSize + 1); + } + + if (mSize - i != 0) { + System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i); + System.arraycopy(mValues, i, mValues, i + 1, mSize - i); + } + + mKeys[i] = key; + mValues[i] = value; + mSize++; + } + } + + /** + * Returns the number of key-value mappings that this SparseIntArray + * currently stores. + */ + public int size() { + return mSize; + } + + /** + * Given an index in the range <code>0...size()-1</code>, returns + * the key from the <code>index</code>th key-value mapping that this + * SparseLongArray stores. + */ + public long keyAt(int index) { + return mKeys[index]; + } + + /** + * Given an index in the range <code>0...size()-1</code>, returns + * the value from the <code>index</code>th key-value mapping that this + * SparseLongArray stores. + */ + public long valueAt(int index) { + return mValues[index]; + } + + /** + * Returns the index for which {@link #keyAt} would return the + * specified key, or a negative number if the specified + * key is not mapped. + */ + public int indexOfKey(long key) { + return Arrays.binarySearch(mKeys, 0, mSize, key); + } + + /** + * Returns an index for which {@link #valueAt} would return the + * specified key, or a negative number if no keys map to the + * specified value. + * Beware that this is a linear search, unlike lookups by key, + * and that multiple keys can map to the same value and this will + * find only one of them. + */ + public int indexOfValue(long value) { + for (int i = 0; i < mSize; i++) + if (mValues[i] == value) + return i; + + return -1; + } + + /** + * Removes all key-value mappings from this SparseIntArray. + */ + public void clear() { + mSize = 0; + } + + /** + * Puts a key/value pair into the array, optimizing for the case where + * the key is greater than all existing keys in the array. + */ + public void append(long key, long value) { + if (mSize != 0 && key <= mKeys[mSize - 1]) { + put(key, value); + return; + } + + int pos = mSize; + if (pos >= mKeys.length) { + growKeyAndValueArrays(pos + 1); + } + + mKeys[pos] = key; + mValues[pos] = value; + mSize = pos + 1; + } + + private void growKeyAndValueArrays(int minNeededSize) { + int n = ArrayUtils.idealLongArraySize(minNeededSize); + + long[] nkeys = new long[n]; + long[] nvalues = new long[n]; + + System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length); + System.arraycopy(mValues, 0, nvalues, 0, mValues.length); + + mKeys = nkeys; + mValues = nvalues; + } +} diff --git a/core/java/android/util/Pools.java b/core/java/android/util/Pools.java index 8edb3e6..70581be 100644 --- a/core/java/android/util/Pools.java +++ b/core/java/android/util/Pools.java @@ -17,25 +17,149 @@ package android.util; /** + * Helper class for crating pools of objects. An example use looks like this: + * <pre> + * public class MyPooledClass { + * + * private static final SynchronizedPool<MyPooledClass> sPool = + * new SynchronizedPool<MyPooledClass>(10); + * + * public static MyPooledClass obtain() { + * MyPooledClass instance = sPool.acquire(); + * return (instance != null) ? instance : new MyPooledClass(); + * } + * + * public void recycle() { + * // Clear state if needed. + * sPool.release(this); + * } + * + * . . . + * } + * </pre> + * * @hide */ -public class Pools { - private Pools() { - } +public final class Pools { + + /** + * Interface for managing a pool of objects. + * + * @param <T> The pooled type. + */ + public static interface Pool<T> { - public static <T extends Poolable<T>> Pool<T> simplePool(PoolableManager<T> manager) { - return new FinitePool<T>(manager); + /** + * @return An instance from the pool if such, null otherwise. + */ + public T acquire(); + + /** + * Release an instance to the pool. + * + * @param instance The instance to release. + * @return Whether the instance was put in the pool. + * + * @throws IllegalStateException If the instance is already in the pool. + */ + public boolean release(T instance); } - - public static <T extends Poolable<T>> Pool<T> finitePool(PoolableManager<T> manager, int limit) { - return new FinitePool<T>(manager, limit); + + private Pools() { + /* do nothing - hiding constructor */ } - public static <T extends Poolable<T>> Pool<T> synchronizedPool(Pool<T> pool) { - return new SynchronizedPool<T>(pool); + /** + * Simple (non-synchronized) pool of objects. + * + * @param <T> The pooled type. + */ + public static class SimplePool<T> implements Pool<T> { + private final Object[] mPool; + + private int mPoolSize; + + /** + * Creates a new instance. + * + * @param maxPoolSize The max pool size. + * + * @throws IllegalArgumentException If the max pool size is less than zero. + */ + public SimplePool(int maxPoolSize) { + if (maxPoolSize <= 0) { + throw new IllegalArgumentException("The max pool size must be > 0"); + } + mPool = new Object[maxPoolSize]; + } + + @Override + @SuppressWarnings("unchecked") + public T acquire() { + if (mPoolSize > 0) { + final int lastPooledIndex = mPoolSize - 1; + T instance = (T) mPool[lastPooledIndex]; + mPool[lastPooledIndex] = null; + mPoolSize--; + return instance; + } + return null; + } + + @Override + public boolean release(T instance) { + if (isInPool(instance)) { + throw new IllegalStateException("Already in the pool!"); + } + if (mPoolSize < mPool.length) { + mPool[mPoolSize] = instance; + mPoolSize++; + return true; + } + return false; + } + + private boolean isInPool(T instance) { + for (int i = 0; i < mPoolSize; i++) { + if (mPool[i] == instance) { + return true; + } + } + return false; + } } - public static <T extends Poolable<T>> Pool<T> synchronizedPool(Pool<T> pool, Object lock) { - return new SynchronizedPool<T>(pool, lock); + /** + * Synchronized) pool of objects. + * + * @param <T> The pooled type. + */ + public static class SynchronizedPool<T> extends SimplePool<T> { + private final Object mLock = new Object(); + + /** + * Creates a new instance. + * + * @param maxPoolSize The max pool size. + * + * @throws IllegalArgumentException If the max pool size is less than zero. + */ + public SynchronizedPool(int maxPoolSize) { + super(maxPoolSize); + } + + @Override + public T acquire() { + synchronized (mLock) { + return super.acquire(); + } + } + + @Override + public boolean release(T element) { + synchronized (mLock) { + return super.release(element); + } + } } } diff --git a/core/java/android/util/PropertyValueModel.java b/core/java/android/util/PropertyValueModel.java new file mode 100755 index 0000000..eb9c47d --- /dev/null +++ b/core/java/android/util/PropertyValueModel.java @@ -0,0 +1,143 @@ +/* + * 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.util; + +/** + * A value model for a {@link Property property} of a host object. This class can be used for + * both reflective and non-reflective property implementations. + * + * @param <H> the host type, where the host is the object that holds this property + * @param <T> the value type + * + * @see Property + * @see ValueModel + */ +public class PropertyValueModel<H, T> extends ValueModel<T> { + private final H mHost; + private final Property<H, T> mProperty; + + private PropertyValueModel(H host, Property<H, T> property) { + mProperty = property; + mHost = host; + } + + /** + * Returns the host. + * + * @return the host + */ + public H getHost() { + return mHost; + } + + /** + * Returns the property. + * + * @return the property + */ + public Property<H, T> getProperty() { + return mProperty; + } + + @Override + public Class<T> getType() { + return mProperty.getType(); + } + + @Override + public T get() { + return mProperty.get(mHost); + } + + @Override + public void set(T value) { + mProperty.set(mHost, value); + } + + /** + * Return an appropriate PropertyValueModel for this host and property. + * + * @param host the host + * @param property the property + * @return the value model + */ + public static <H, T> PropertyValueModel<H, T> of(H host, Property<H, T> property) { + return new PropertyValueModel<H, T>(host, property); + } + + /** + * Return a PropertyValueModel for this {@code host} and a + * reflective property, constructed from this {@code propertyType} and {@code propertyName}. + * + * @param host + * @param propertyType the property type + * @param propertyName the property name + * @return a value model with this host and a reflective property with this type and name + * + * @see Property#of + */ + public static <H, T> PropertyValueModel<H, T> of(H host, Class<T> propertyType, + String propertyName) { + return of(host, Property.of((Class<H>) host.getClass(), propertyType, propertyName)); + } + + private static Class getNullaryMethodReturnType(Class c, String name) { + try { + return c.getMethod(name).getReturnType(); + } catch (NoSuchMethodException e) { + return null; + } + } + + private static Class getFieldType(Class c, String name) { + try { + return c.getField(name).getType(); + } catch (NoSuchFieldException e) { + return null; + } + } + + private static String capitalize(String name) { + if (name.isEmpty()) { + return name; + } + return Character.toUpperCase(name.charAt(0)) + name.substring(1); + } + + /** + * Return a PropertyValueModel for this {@code host} and and {@code propertyName}. + * + * @param host the host + * @param propertyName the property name + * @return a value model with this host and a reflective property with this name + */ + public static PropertyValueModel of(Object host, String propertyName) { + Class clazz = host.getClass(); + String suffix = capitalize(propertyName); + Class propertyType = getNullaryMethodReturnType(clazz, "get" + suffix); + if (propertyType == null) { + propertyType = getNullaryMethodReturnType(clazz, "is" + suffix); + } + if (propertyType == null) { + propertyType = getFieldType(clazz, propertyName); + } + if (propertyType == null) { + throw new NoSuchPropertyException(propertyName); + } + return of(host, propertyType, propertyName); + } +} diff --git a/core/java/android/util/SparseLongArray.java b/core/java/android/util/SparseLongArray.java index a08d5cb..2f7a6fe 100644 --- a/core/java/android/util/SparseLongArray.java +++ b/core/java/android/util/SparseLongArray.java @@ -22,8 +22,6 @@ import com.android.internal.util.ArrayUtils; * SparseLongArrays map integers to longs. Unlike a normal array of longs, * there can be gaps in the indices. It is intended to be more efficient * than using a HashMap to map Integers to Longs. - * - * @hide */ public class SparseLongArray implements Cloneable { diff --git a/core/java/android/util/SynchronizedPool.java b/core/java/android/util/SynchronizedPool.java deleted file mode 100644 index 651e0c3..0000000 --- a/core/java/android/util/SynchronizedPool.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.util; - -/** - * - * @hide - */ -class SynchronizedPool<T extends Poolable<T>> implements Pool<T> { - private final Pool<T> mPool; - private final Object mLock; - - public SynchronizedPool(Pool<T> pool) { - mPool = pool; - mLock = this; - } - - public SynchronizedPool(Pool<T> pool, Object lock) { - mPool = pool; - mLock = lock; - } - - public T acquire() { - synchronized (mLock) { - return mPool.acquire(); - } - } - - public void release(T element) { - synchronized (mLock) { - mPool.release(element); - } - } -} diff --git a/core/java/android/util/ValueModel.java b/core/java/android/util/ValueModel.java new file mode 100755 index 0000000..4789682 --- /dev/null +++ b/core/java/android/util/ValueModel.java @@ -0,0 +1,74 @@ +/* + * 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.util; + +/** + * A ValueModel is an abstraction for a 'slot' or place in memory in which a value + * may be stored and retrieved. A common implementation of ValueModel is a regular property of + * an object, whose value may be retrieved by calling the appropriate <em>getter</em> + * method and set by calling the corresponding <em>setter</em> method. + * + * @param <T> the value type + * + * @see PropertyValueModel + */ +public abstract class ValueModel<T> { + /** + * The empty model should be used in place of {@code null} to indicate that a + * model has not been set. The empty model has no value and does nothing when it is set. + */ + public static final ValueModel EMPTY = new ValueModel() { + @Override + public Class getType() { + return Object.class; + } + + @Override + public Object get() { + return null; + } + + @Override + public void set(Object value) { + + } + }; + + protected ValueModel() { + } + + /** + * Returns the type of this property. + * + * @return the property type + */ + public abstract Class<T> getType(); + + /** + * Returns the value of this property. + * + * @return the property value + */ + public abstract T get(); + + /** + * Sets the value of this property. + * + * @param value the new value for this property + */ + public abstract void set(T value); +}
\ No newline at end of file diff --git a/core/java/android/view/AccessibilityInteractionController.java b/core/java/android/view/AccessibilityInteractionController.java index 9bee4bf..2d6453e 100644 --- a/core/java/android/view/AccessibilityInteractionController.java +++ b/core/java/android/view/AccessibilityInteractionController.java @@ -16,8 +16,7 @@ package android.view; -import static android.view.accessibility.AccessibilityNodeInfo.INCLUDE_NOT_IMPORTANT_VIEWS; - +import android.graphics.Point; import android.graphics.Rect; import android.os.Bundle; import android.os.Handler; @@ -26,12 +25,14 @@ import android.os.Message; import android.os.Process; import android.os.RemoteException; import android.util.SparseLongArray; +import android.view.View.AttachInfo; import android.view.accessibility.AccessibilityInteractionClient; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeProvider; import android.view.accessibility.IAccessibilityInteractionConnectionCallback; import com.android.internal.os.SomeArgs; +import com.android.internal.util.Predicate; import java.util.ArrayList; import java.util.HashMap; @@ -47,7 +48,7 @@ import java.util.Map; */ final class AccessibilityInteractionController { - private ArrayList<AccessibilityNodeInfo> mTempAccessibilityNodeInfoList = + private final ArrayList<AccessibilityNodeInfo> mTempAccessibilityNodeInfoList = new ArrayList<AccessibilityNodeInfo>(); private final Handler mHandler; @@ -62,7 +63,12 @@ final class AccessibilityInteractionController { private final ArrayList<View> mTempArrayList = new ArrayList<View>(); + private final Point mTempPoint = new Point(); private final Rect mTempRect = new Rect(); + private final Rect mTempRect1 = new Rect(); + private final Rect mTempRect2 = new Rect(); + + private AddNodeInfosForViewId mAddNodeInfosForViewId; public AccessibilityInteractionController(ViewRootImpl viewRootImpl) { Looper looper = viewRootImpl.mHandler.getLooper(); @@ -86,7 +92,7 @@ final class AccessibilityInteractionController { public void findAccessibilityNodeInfoByAccessibilityIdClientThread( long accessibilityNodeId, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, - long interrogatingTid) { + long interrogatingTid, MagnificationSpec spec) { Message message = mHandler.obtainMessage(); message.what = PrivateHandler.MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID; message.arg1 = flags; @@ -96,6 +102,7 @@ final class AccessibilityInteractionController { args.argi2 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId); args.argi3 = interactionId; args.arg1 = callback; + args.arg2 = spec; message.obj = args; // If the interrogation is performed by the same thread as the main UI @@ -119,6 +126,7 @@ final class AccessibilityInteractionController { final int interactionId = args.argi3; final IAccessibilityInteractionConnectionCallback callback = (IAccessibilityInteractionConnectionCallback) args.arg1; + final MagnificationSpec spec = (MagnificationSpec) args.arg2; args.recycle(); @@ -128,8 +136,7 @@ final class AccessibilityInteractionController { if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) { return; } - mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = - (flags & INCLUDE_NOT_IMPORTANT_VIEWS) != 0; + mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = flags; View root = null; if (accessibilityViewId == AccessibilityNodeInfo.UNDEFINED) { root = mViewRootImpl.mView; @@ -141,8 +148,11 @@ final class AccessibilityInteractionController { } } finally { try { - mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = false; - applyApplicationScaleIfNeeded(infos); + mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0; + applyAppScaleAndMagnificationSpecIfNeeded(infos, spec); + if (spec != null) { + spec.recycle(); + } callback.setFindAccessibilityNodeInfosResult(infos, interactionId); infos.clear(); } catch (RemoteException re) { @@ -151,18 +161,19 @@ final class AccessibilityInteractionController { } } - public void findAccessibilityNodeInfoByViewIdClientThread(long accessibilityNodeId, - int viewId, int interactionId, IAccessibilityInteractionConnectionCallback callback, - int flags, int interrogatingPid, long interrogatingTid) { + public void findAccessibilityNodeInfosByViewIdClientThread(long accessibilityNodeId, + String viewId, int interactionId, IAccessibilityInteractionConnectionCallback callback, + int flags, int interrogatingPid, long interrogatingTid, MagnificationSpec spec) { Message message = mHandler.obtainMessage(); - message.what = PrivateHandler.MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID; + message.what = PrivateHandler.MSG_FIND_ACCESSIBLITY_NODE_INFOS_BY_VIEW_ID; message.arg1 = flags; message.arg2 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); SomeArgs args = SomeArgs.obtain(); - args.argi1 = viewId; - args.argi2 = interactionId; + args.argi1 = interactionId; args.arg1 = callback; + args.arg2 = spec; + args.arg3 = viewId; message.obj = args; @@ -178,25 +189,26 @@ final class AccessibilityInteractionController { } } - private void findAccessibilityNodeInfoByViewIdUiThread(Message message) { + private void findAccessibilityNodeInfosByViewIdUiThread(Message message) { final int flags = message.arg1; final int accessibilityViewId = message.arg2; SomeArgs args = (SomeArgs) message.obj; - final int viewId = args.argi1; - final int interactionId = args.argi2; + final int interactionId = args.argi1; final IAccessibilityInteractionConnectionCallback callback = (IAccessibilityInteractionConnectionCallback) args.arg1; + final MagnificationSpec spec = (MagnificationSpec) args.arg2; + final String viewId = (String) args.arg3; args.recycle(); - AccessibilityNodeInfo info = null; + final List<AccessibilityNodeInfo> infos = mTempAccessibilityNodeInfoList; + infos.clear(); try { if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) { return; } - mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = - (flags & INCLUDE_NOT_IMPORTANT_VIEWS) != 0; + mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = flags; View root = null; if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { root = findViewByAccessibilityId(accessibilityViewId); @@ -204,16 +216,26 @@ final class AccessibilityInteractionController { root = mViewRootImpl.mView; } if (root != null) { - View target = root.findViewById(viewId); - if (target != null && isShown(target)) { - info = target.createAccessibilityNodeInfo(); + final int resolvedViewId = root.getContext().getResources() + .getIdentifier(viewId, null, null); + if (resolvedViewId <= 0) { + return; + } + if (mAddNodeInfosForViewId == null) { + mAddNodeInfosForViewId = new AddNodeInfosForViewId(); } + mAddNodeInfosForViewId.init(resolvedViewId, infos); + root.findViewByPredicate(mAddNodeInfosForViewId); + mAddNodeInfosForViewId.reset(); } } finally { try { - mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = false; - applyApplicationScaleIfNeeded(info); - callback.setFindAccessibilityNodeInfoResult(info, interactionId); + mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0; + applyAppScaleAndMagnificationSpecIfNeeded(infos, spec); + if (spec != null) { + spec.recycle(); + } + callback.setFindAccessibilityNodeInfosResult(infos, interactionId); } catch (RemoteException re) { /* ignore - the other side will time out */ } @@ -222,7 +244,7 @@ final class AccessibilityInteractionController { public void findAccessibilityNodeInfosByTextClientThread(long accessibilityNodeId, String text, int interactionId, IAccessibilityInteractionConnectionCallback callback, - int flags, int interrogatingPid, long interrogatingTid) { + int flags, int interrogatingPid, long interrogatingTid, MagnificationSpec spec) { Message message = mHandler.obtainMessage(); message.what = PrivateHandler.MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT; message.arg1 = flags; @@ -230,10 +252,10 @@ final class AccessibilityInteractionController { SomeArgs args = SomeArgs.obtain(); args.arg1 = text; args.arg2 = callback; + args.arg3 = spec; args.argi1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); args.argi2 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId); args.argi3 = interactionId; - message.obj = args; // If the interrogation is performed by the same thread as the main UI @@ -255,6 +277,7 @@ final class AccessibilityInteractionController { final String text = (String) args.arg1; final IAccessibilityInteractionConnectionCallback callback = (IAccessibilityInteractionConnectionCallback) args.arg2; + final MagnificationSpec spec = (MagnificationSpec) args.arg3; final int accessibilityViewId = args.argi1; final int virtualDescendantId = args.argi2; final int interactionId = args.argi3; @@ -265,8 +288,7 @@ final class AccessibilityInteractionController { if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) { return; } - mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = - (flags & INCLUDE_NOT_IMPORTANT_VIEWS) != 0; + mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = flags; View root = null; if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { root = findViewByAccessibilityId(accessibilityViewId); @@ -309,8 +331,11 @@ final class AccessibilityInteractionController { } } finally { try { - mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = false; - applyApplicationScaleIfNeeded(infos); + mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0; + applyAppScaleAndMagnificationSpecIfNeeded(infos, spec); + if (spec != null) { + spec.recycle(); + } callback.setFindAccessibilityNodeInfosResult(infos, interactionId); } catch (RemoteException re) { /* ignore - the other side will time out */ @@ -320,7 +345,7 @@ final class AccessibilityInteractionController { public void findFocusClientThread(long accessibilityNodeId, int focusType, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interogatingPid, - long interrogatingTid) { + long interrogatingTid, MagnificationSpec spec) { Message message = mHandler.obtainMessage(); message.what = PrivateHandler.MSG_FIND_FOCUS; message.arg1 = flags; @@ -331,6 +356,7 @@ final class AccessibilityInteractionController { args.argi2 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); args.argi3 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId); args.arg1 = callback; + args.arg2 = spec; message.obj = args; @@ -356,7 +382,7 @@ final class AccessibilityInteractionController { final int virtualDescendantId = args.argi3; final IAccessibilityInteractionConnectionCallback callback = (IAccessibilityInteractionConnectionCallback) args.arg1; - + final MagnificationSpec spec = (MagnificationSpec) args.arg2; args.recycle(); AccessibilityNodeInfo focused = null; @@ -364,8 +390,7 @@ final class AccessibilityInteractionController { if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) { return; } - mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = - (flags & INCLUDE_NOT_IMPORTANT_VIEWS) != 0; + mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = flags; View root = null; if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { root = findViewByAccessibilityId(accessibilityViewId); @@ -406,8 +431,11 @@ final class AccessibilityInteractionController { } } finally { try { - mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = false; - applyApplicationScaleIfNeeded(focused); + mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0; + applyAppScaleAndMagnificationSpecIfNeeded(focused, spec); + if (spec != null) { + spec.recycle(); + } callback.setFindAccessibilityNodeInfoResult(focused, interactionId); } catch (RemoteException re) { /* ignore - the other side will time out */ @@ -417,7 +445,7 @@ final class AccessibilityInteractionController { public void focusSearchClientThread(long accessibilityNodeId, int direction, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interogatingPid, - long interrogatingTid) { + long interrogatingTid, MagnificationSpec spec) { Message message = mHandler.obtainMessage(); message.what = PrivateHandler.MSG_FOCUS_SEARCH; message.arg1 = flags; @@ -427,6 +455,7 @@ final class AccessibilityInteractionController { args.argi2 = direction; args.argi3 = interactionId; args.arg1 = callback; + args.arg2 = spec; message.obj = args; @@ -451,6 +480,7 @@ final class AccessibilityInteractionController { final int interactionId = args.argi3; final IAccessibilityInteractionConnectionCallback callback = (IAccessibilityInteractionConnectionCallback) args.arg1; + final MagnificationSpec spec = (MagnificationSpec) args.arg2; args.recycle(); @@ -459,8 +489,7 @@ final class AccessibilityInteractionController { if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) { return; } - mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = - (flags & INCLUDE_NOT_IMPORTANT_VIEWS) != 0; + mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = flags; View root = null; if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { root = findViewByAccessibilityId(accessibilityViewId); @@ -475,8 +504,11 @@ final class AccessibilityInteractionController { } } finally { try { - mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = false; - applyApplicationScaleIfNeeded(next); + mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0; + applyAppScaleAndMagnificationSpecIfNeeded(next, spec); + if (spec != null) { + spec.recycle(); + } callback.setFindAccessibilityNodeInfoResult(next, interactionId); } catch (RemoteException re) { /* ignore - the other side will time out */ @@ -533,8 +565,7 @@ final class AccessibilityInteractionController { if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) { return; } - mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = - (flags & INCLUDE_NOT_IMPORTANT_VIEWS) != 0; + mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = flags; View target = null; if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { target = findViewByAccessibilityId(accessibilityViewId); @@ -552,7 +583,7 @@ final class AccessibilityInteractionController { } } finally { try { - mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = false; + mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0; callback.setPerformAccessibilityActionResult(succeeded, interactionId); } catch (RemoteException re) { /* ignore - the other side will time out */ @@ -572,38 +603,84 @@ final class AccessibilityInteractionController { return foundView; } - private void applyApplicationScaleIfNeeded(List<AccessibilityNodeInfo> infos) { + private void applyAppScaleAndMagnificationSpecIfNeeded(List<AccessibilityNodeInfo> infos, + MagnificationSpec spec) { if (infos == null) { return; } final float applicationScale = mViewRootImpl.mAttachInfo.mApplicationScale; - if (applicationScale != 1.0f) { + if (shouldApplyAppScaleAndMagnificationSpec(applicationScale, spec)) { final int infoCount = infos.size(); for (int i = 0; i < infoCount; i++) { AccessibilityNodeInfo info = infos.get(i); - applyApplicationScaleIfNeeded(info); + applyAppScaleAndMagnificationSpecIfNeeded(info, spec); } } } - private void applyApplicationScaleIfNeeded(AccessibilityNodeInfo info) { + private void applyAppScaleAndMagnificationSpecIfNeeded(AccessibilityNodeInfo info, + MagnificationSpec spec) { if (info == null) { return; } + final float applicationScale = mViewRootImpl.mAttachInfo.mApplicationScale; + if (!shouldApplyAppScaleAndMagnificationSpec(applicationScale, spec)) { + return; + } + + Rect boundsInParent = mTempRect; + Rect boundsInScreen = mTempRect1; + + info.getBoundsInParent(boundsInParent); + info.getBoundsInScreen(boundsInScreen); if (applicationScale != 1.0f) { - Rect bounds = mTempRect; + boundsInParent.scale(applicationScale); + boundsInScreen.scale(applicationScale); + } + if (spec != null) { + boundsInParent.scale(spec.scale); + // boundsInParent must not be offset. + boundsInScreen.scale(spec.scale); + boundsInScreen.offset((int) spec.offsetX, (int) spec.offsetY); + } + info.setBoundsInParent(boundsInParent); + info.setBoundsInScreen(boundsInScreen); + + if (spec != null) { + AttachInfo attachInfo = mViewRootImpl.mAttachInfo; + if (attachInfo.mDisplay == null) { + return; + } - info.getBoundsInParent(bounds); - bounds.scale(applicationScale); - info.setBoundsInParent(bounds); + final float scale = attachInfo.mApplicationScale * spec.scale; - info.getBoundsInScreen(bounds); - bounds.scale(applicationScale); - info.setBoundsInScreen(bounds); + Rect visibleWinFrame = mTempRect1; + visibleWinFrame.left = (int) (attachInfo.mWindowLeft * scale + spec.offsetX); + visibleWinFrame.top = (int) (attachInfo.mWindowTop * scale + spec.offsetY); + visibleWinFrame.right = (int) (visibleWinFrame.left + mViewRootImpl.mWidth * scale); + visibleWinFrame.bottom = (int) (visibleWinFrame.top + mViewRootImpl.mHeight * scale); + + attachInfo.mDisplay.getRealSize(mTempPoint); + final int displayWidth = mTempPoint.x; + final int displayHeight = mTempPoint.y; + + Rect visibleDisplayFrame = mTempRect2; + visibleDisplayFrame.set(0, 0, displayWidth, displayHeight); + + visibleWinFrame.intersect(visibleDisplayFrame); + + if (!visibleWinFrame.intersects(boundsInScreen.left, boundsInScreen.top, + boundsInScreen.right, boundsInScreen.bottom)) { + info.setVisibleToUser(false); + } } } + private boolean shouldApplyAppScaleAndMagnificationSpec(float appScale, + MagnificationSpec spec) { + return (appScale != 1.0f || (spec != null && !spec.isNop())); + } /** * This class encapsulates a prefetching strategy for the accessibility APIs for @@ -616,20 +693,20 @@ final class AccessibilityInteractionController { private final ArrayList<View> mTempViewList = new ArrayList<View>(); - public void prefetchAccessibilityNodeInfos(View view, int virtualViewId, int prefetchFlags, + public void prefetchAccessibilityNodeInfos(View view, int virtualViewId, int fetchFlags, List<AccessibilityNodeInfo> outInfos) { AccessibilityNodeProvider provider = view.getAccessibilityNodeProvider(); if (provider == null) { AccessibilityNodeInfo root = view.createAccessibilityNodeInfo(); if (root != null) { outInfos.add(root); - if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) != 0) { + if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) != 0) { prefetchPredecessorsOfRealNode(view, outInfos); } - if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0) { + if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0) { prefetchSiblingsOfRealNode(view, outInfos); } - if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS) != 0) { + if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS) != 0) { prefetchDescendantsOfRealNode(view, outInfos); } } @@ -637,13 +714,13 @@ final class AccessibilityInteractionController { AccessibilityNodeInfo root = provider.createAccessibilityNodeInfo(virtualViewId); if (root != null) { outInfos.add(root); - if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) != 0) { + if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) != 0) { prefetchPredecessorsOfVirtualNode(root, view, provider, outInfos); } - if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0) { + if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0) { prefetchSiblingsOfVirtualNode(root, view, provider, outInfos); } - if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS) != 0) { + if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS) != 0) { prefetchDescendantsOfVirtualNode(root, provider, outInfos); } } @@ -846,7 +923,7 @@ final class AccessibilityInteractionController { private class PrivateHandler extends Handler { private final static int MSG_PERFORM_ACCESSIBILITY_ACTION = 1; private final static int MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID = 2; - private final static int MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID = 3; + private final static int MSG_FIND_ACCESSIBLITY_NODE_INFOS_BY_VIEW_ID = 3; private final static int MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT = 4; private final static int MSG_FIND_FOCUS = 5; private final static int MSG_FOCUS_SEARCH = 6; @@ -863,8 +940,8 @@ final class AccessibilityInteractionController { return "MSG_PERFORM_ACCESSIBILITY_ACTION"; case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID: return "MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID"; - case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID: - return "MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID"; + case MSG_FIND_ACCESSIBLITY_NODE_INFOS_BY_VIEW_ID: + return "MSG_FIND_ACCESSIBLITY_NODE_INFOS_BY_VIEW_ID"; case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT: return "MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT"; case MSG_FIND_FOCUS: @@ -886,8 +963,8 @@ final class AccessibilityInteractionController { case MSG_PERFORM_ACCESSIBILITY_ACTION: { perfromAccessibilityActionUiThread(message); } break; - case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID: { - findAccessibilityNodeInfoByViewIdUiThread(message); + case MSG_FIND_ACCESSIBLITY_NODE_INFOS_BY_VIEW_ID: { + findAccessibilityNodeInfosByViewIdUiThread(message); } break; case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT: { findAccessibilityNodeInfosByTextUiThread(message); @@ -903,4 +980,27 @@ final class AccessibilityInteractionController { } } } + + private final class AddNodeInfosForViewId implements Predicate<View> { + private int mViewId = View.NO_ID; + private List<AccessibilityNodeInfo> mInfos; + + public void init(int viewId, List<AccessibilityNodeInfo> infos) { + mViewId = viewId; + mInfos = infos; + } + + public void reset() { + mViewId = View.NO_ID; + mInfos = null; + } + + @Override + public boolean apply(View view) { + if (view.getId() == mViewId && isShown(view)) { + mInfos.add(view.createAccessibilityNodeInfo()); + } + return false; + } + } } diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java index b661748..f28e4b5 100644 --- a/core/java/android/view/Choreographer.java +++ b/core/java/android/view/Choreographer.java @@ -693,7 +693,7 @@ public final class Choreographer { // At this time Surface Flinger won't send us vsyncs for secondary displays // but that could change in the future so let's log a message to help us remember // that we need to fix this. - if (builtInDisplayId != Surface.BUILT_IN_DISPLAY_ID_MAIN) { + if (builtInDisplayId != SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN) { Log.d(TAG, "Received vsync from secondary display, but we don't support " + "this case yet. Choreographer needs a way to explicitly request " + "vsync for a specific display to ensure it doesn't lose track " diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java index 758abb5..e6a7950 100644 --- a/core/java/android/view/Display.java +++ b/core/java/android/view/Display.java @@ -437,6 +437,20 @@ public final class Display { } /** + * @hide + * Return a rectangle defining the insets of the overscan region of the display. + * Each field of the rectangle is the number of pixels the overscan area extends + * into the display on that side. + */ + public void getOverscanInsets(Rect outRect) { + synchronized (this) { + updateDisplayInfoLocked(); + outRect.set(mDisplayInfo.overscanLeft, mDisplayInfo.overscanTop, + mDisplayInfo.overscanRight, mDisplayInfo.overscanBottom); + } + } + + /** * Returns the rotation of the screen from its "natural" orientation. * The returned value may be {@link Surface#ROTATION_0 Surface.ROTATION_0} * (no rotation), {@link Surface#ROTATION_90 Surface.ROTATION_90}, diff --git a/core/java/android/view/DisplayEventReceiver.java b/core/java/android/view/DisplayEventReceiver.java index a919ffc..4dade20 100644 --- a/core/java/android/view/DisplayEventReceiver.java +++ b/core/java/android/view/DisplayEventReceiver.java @@ -102,7 +102,7 @@ public abstract class DisplayEventReceiver { * @param timestampNanos The timestamp of the pulse, in the {@link System#nanoTime()} * timebase. * @param builtInDisplayId The surface flinger built-in display id such as - * {@link Surface#BUILT_IN_DISPLAY_ID_MAIN}. + * {@link SurfaceControl#BUILT_IN_DISPLAY_ID_MAIN}. * @param frame The frame number. Increases by one for each vertical sync interval. */ public void onVsync(long timestampNanos, int builtInDisplayId, int frame) { @@ -114,7 +114,7 @@ public abstract class DisplayEventReceiver { * @param timestampNanos The timestamp of the event, in the {@link System#nanoTime()} * timebase. * @param builtInDisplayId The surface flinger built-in display id such as - * {@link Surface#BUILT_IN_DISPLAY_ID_HDMI}. + * {@link SurfaceControl#BUILT_IN_DISPLAY_ID_HDMI}. * @param connected True if the display is connected, false if it disconnected. */ public void onHotplug(long timestampNanos, int builtInDisplayId, boolean connected) { diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java index 305fd5c..9fcd9b1 100644 --- a/core/java/android/view/DisplayInfo.java +++ b/core/java/android/view/DisplayInfo.java @@ -109,6 +109,30 @@ public final class DisplayInfo implements Parcelable { public int logicalHeight; /** + * @hide + * Number of overscan pixels on the left side of the display. + */ + public int overscanLeft; + + /** + * @hide + * Number of overscan pixels on the top side of the display. + */ + public int overscanTop; + + /** + * @hide + * Number of overscan pixels on the right side of the display. + */ + public int overscanRight; + + /** + * @hide + * Number of overscan pixels on the bottom side of the display. + */ + public int overscanBottom; + + /** * The rotation of the display relative to its natural orientation. * May be one of {@link android.view.Surface#ROTATION_0}, * {@link android.view.Surface#ROTATION_90}, {@link android.view.Surface#ROTATION_180}, @@ -196,6 +220,10 @@ public final class DisplayInfo implements Parcelable { && largestNominalAppHeight == other.largestNominalAppHeight && logicalWidth == other.logicalWidth && logicalHeight == other.logicalHeight + && overscanLeft == other.overscanLeft + && overscanTop == other.overscanTop + && overscanRight == other.overscanRight + && overscanBottom == other.overscanBottom && rotation == other.rotation && refreshRate == other.refreshRate && logicalDensityDpi == other.logicalDensityDpi @@ -222,6 +250,10 @@ public final class DisplayInfo implements Parcelable { largestNominalAppHeight = other.largestNominalAppHeight; logicalWidth = other.logicalWidth; logicalHeight = other.logicalHeight; + overscanLeft = other.overscanLeft; + overscanTop = other.overscanTop; + overscanRight = other.overscanRight; + overscanBottom = other.overscanBottom; rotation = other.rotation; refreshRate = other.refreshRate; logicalDensityDpi = other.logicalDensityDpi; @@ -243,6 +275,10 @@ public final class DisplayInfo implements Parcelable { largestNominalAppHeight = source.readInt(); logicalWidth = source.readInt(); logicalHeight = source.readInt(); + overscanLeft = source.readInt(); + overscanTop = source.readInt(); + overscanRight = source.readInt(); + overscanBottom = source.readInt(); rotation = source.readInt(); refreshRate = source.readFloat(); logicalDensityDpi = source.readInt(); @@ -265,6 +301,10 @@ public final class DisplayInfo implements Parcelable { dest.writeInt(largestNominalAppHeight); dest.writeInt(logicalWidth); dest.writeInt(logicalHeight); + dest.writeInt(overscanLeft); + dest.writeInt(overscanTop); + dest.writeInt(overscanRight); + dest.writeInt(overscanBottom); dest.writeInt(rotation); dest.writeFloat(refreshRate); dest.writeInt(logicalDensityDpi); @@ -318,18 +358,55 @@ public final class DisplayInfo implements Parcelable { // For debugging purposes @Override public String toString() { - return "DisplayInfo{\"" + name + "\", app " + appWidth + " x " + appHeight - + ", real " + logicalWidth + " x " + logicalHeight - + ", largest app " + largestNominalAppWidth + " x " + largestNominalAppHeight - + ", smallest app " + smallestNominalAppWidth + " x " + smallestNominalAppHeight - + ", " + refreshRate + " fps" - + ", rotation " + rotation - + ", density " + logicalDensityDpi - + ", " + physicalXDpi + " x " + physicalYDpi + " dpi" - + ", layerStack " + layerStack - + ", type " + Display.typeToString(type) - + ", address " + address - + flagsToString(flags) + "}"; + StringBuilder sb = new StringBuilder(); + sb.append("DisplayInfo{\""); + sb.append(name); + sb.append("\", app "); + sb.append(appWidth); + sb.append(" x "); + sb.append(appHeight); + sb.append(", real "); + sb.append(logicalWidth); + sb.append(" x "); + sb.append(logicalHeight); + if (overscanLeft != 0 || overscanTop != 0 || overscanRight != 0 || overscanBottom != 0) { + sb.append(", overscan ("); + sb.append(overscanLeft); + sb.append(","); + sb.append(overscanTop); + sb.append(","); + sb.append(overscanRight); + sb.append(","); + sb.append(overscanBottom); + sb.append(")"); + } + sb.append(", largest app "); + sb.append(largestNominalAppWidth); + sb.append(" x "); + sb.append(largestNominalAppHeight); + sb.append(", smallest app "); + sb.append(smallestNominalAppWidth); + sb.append(" x "); + sb.append(smallestNominalAppHeight); + sb.append(", "); + sb.append(refreshRate); + sb.append(" fps, rotation"); + sb.append(rotation); + sb.append(", density "); + sb.append(logicalDensityDpi); + sb.append(" ("); + sb.append(physicalXDpi); + sb.append(" x "); + sb.append(physicalYDpi); + sb.append(") dpi, layerStack "); + sb.append(layerStack); + sb.append(", type "); + sb.append(Display.typeToString(type)); + sb.append(", address "); + sb.append(address); + sb.append(flagsToString(flags)); + sb.append("}"); + return sb.toString(); } private static String flagsToString(int flags) { diff --git a/core/java/android/view/DisplayList.java b/core/java/android/view/DisplayList.java index 5e34a36..3bad98e 100644 --- a/core/java/android/view/DisplayList.java +++ b/core/java/android/view/DisplayList.java @@ -19,21 +19,121 @@ package android.view; import android.graphics.Matrix; /** - * A display lists records a series of graphics related operation and can replay + * <p>A display list records a series of graphics related operations and can replay * them later. Display lists are usually built by recording operations on a - * {@link android.graphics.Canvas}. Replaying the operations from a display list - * avoids executing views drawing code on every frame, and is thus much more - * efficient. + * {@link HardwareCanvas}. Replaying the operations from a display list avoids + * executing application code on every frame, and is thus much more efficient.</p> * - * @hide + * <p>Display lists are used internally for all views by default, and are not + * typically used directly. One reason to consider using a display is a custom + * {@link View} implementation that needs to issue a large number of drawing commands. + * When the view invalidates, all the drawing commands must be reissued, even if + * large portions of the drawing command stream stay the same frame to frame, which + * can become a performance bottleneck. To solve this issue, a custom View might split + * its content into several display lists. A display list is updated only when its + * content, and only its content, needs to be updated.</p> + * + * <p>A text editor might for instance store each paragraph into its own display list. + * Thus when the user inserts or removes characters, only the display list of the + * affected paragraph needs to be recorded again.</p> + * + * <h3>Hardware acceleration</h3> + * <p>Display lists can only be replayed using a {@link HardwareCanvas}. They are not + * supported in software. Always make sure that the {@link android.graphics.Canvas} + * you are using to render a display list is hardware accelerated using + * {@link android.graphics.Canvas#isHardwareAccelerated()}.</p> + * + * <h3>Creating a display list</h3> + * <pre class="prettyprint"> + * HardwareRenderer renderer = myView.getHardwareRenderer(); + * if (renderer != null) { + * DisplayList displayList = renderer.createDisplayList(); + * HardwareCanvas canvas = displayList.start(width, height); + * try { + * // Draw onto the canvas + * // For instance: canvas.drawBitmap(...); + * } finally { + * displayList.end(); + * } + * } + * </pre> + * + * <h3>Rendering a display list on a View</h3> + * <pre class="prettyprint"> + * protected void onDraw(Canvas canvas) { + * if (canvas.isHardwareAccelerated()) { + * HardwareCanvas hardwareCanvas = (HardwareCanvas) canvas; + * hardwareCanvas.drawDisplayList(mDisplayList); + * } + * } + * </pre> + * + * <h3>Releasing resources</h3> + * <p>This step is not mandatory but recommended if you want to release resources + * held by a display list as soon as possible.</p> + * <pre class="prettyprint"> + * // Mark this display list invalid, it cannot be used for drawing anymore, + * // and release resources held by this display list + * displayList.clear(); + * </pre> + * + * <h3>Properties</h3> + * <p>In addition, a display list offers several properties, such as + * {@link #setScaleX(float)} or {@link #setLeft(int)}, that can be used to affect all + * the drawing commands recorded within. For instance, these properties can be used + * to move around a large number of images without re-issuing all the individual + * <code>drawBitmap()</code> calls.</p> + * + * <pre class="prettyprint"> + * private void createDisplayList() { + * HardwareRenderer renderer = getHardwareRenderer(); + * if (renderer != null) { + * mDisplayList = renderer.createDisplayList(); + * HardwareCanvas canvas = mDisplayList.start(width, height); + * try { + * for (Bitmap b : mBitmaps) { + * canvas.drawBitmap(b, 0.0f, 0.0f, null); + * canvas.translate(0.0f, b.getHeight()); + * } + * } finally { + * displayList.end(); + * } + * } + * } + * + * protected void onDraw(Canvas canvas) { + * if (canvas.isHardwareAccelerated()) { + * HardwareCanvas hardwareCanvas = (HardwareCanvas) canvas; + * hardwareCanvas.drawDisplayList(mDisplayList); + * } + * } + * + * private void moveContentBy(int x) { + * // This will move all the bitmaps recorded inside the display list + * // by x pixels to the right and redraw this view. All the commands + * // recorded in createDisplayList() won't be re-issued, only onDraw() + * // will be invoked and will execute very quickly + * mDisplayList.offsetLeftAndRight(x); + * invalidate(); + * } + * </pre> + * + * <h3>Threading</h3> + * <p>Display lists must be created on and manipulated from the UI thread only.</p> + * + * @hide */ public abstract class DisplayList { + private boolean mDirty; + /** * Flag used when calling * {@link HardwareCanvas#drawDisplayList(DisplayList, android.graphics.Rect, int)} * When this flag is set, draw operations lying outside of the bounds of the * display list will be culled early. It is recommeneded to always set this * flag. + * + * @hide */ public static final int FLAG_CLIP_CHILDREN = 0x1; @@ -42,14 +142,18 @@ public abstract class DisplayList { /** * Indicates that the display list is done drawing. * - * @see HardwareCanvas#drawDisplayList(DisplayList, android.graphics.Rect, int) + * @see HardwareCanvas#drawDisplayList(DisplayList, android.graphics.Rect, int) + * + * @hide */ public static final int STATUS_DONE = 0x0; /** * Indicates that the display list needs another drawing pass. * - * @see HardwareCanvas#drawDisplayList(DisplayList, android.graphics.Rect, int) + * @see HardwareCanvas#drawDisplayList(DisplayList, android.graphics.Rect, int) + * + * @hide */ public static final int STATUS_DRAW = 0x1; @@ -57,7 +161,9 @@ public abstract class DisplayList { * Indicates that the display list needs to re-execute its GL functors. * * @see HardwareCanvas#drawDisplayList(DisplayList, android.graphics.Rect, int) - * @see HardwareCanvas#callDrawGLFunction(int) + * @see HardwareCanvas#callDrawGLFunction(int) + * + * @hide */ public static final int STATUS_INVOKE = 0x2; @@ -65,35 +171,83 @@ public abstract class DisplayList { * Indicates that the display list performed GL drawing operations. * * @see HardwareCanvas#drawDisplayList(DisplayList, android.graphics.Rect, int) + * + * @hide */ public static final int STATUS_DREW = 0x4; /** * Starts recording the display list. All operations performed on the * returned canvas are recorded and stored in this display list. - * + * + * Calling this method will mark the display list invalid until + * {@link #end()} is called. Only valid display lists can be replayed. + * + * @param width The width of the display list's viewport + * @param height The height of the display list's viewport + * * @return A canvas to record drawing operations. + * + * @see #end() + * @see #isValid() */ - public abstract HardwareCanvas start(); + public abstract HardwareCanvas start(int width, int height); /** * Ends the recording for this display list. A display list cannot be - * replayed if recording is not finished. + * replayed if recording is not finished. Calling this method marks + * the display list valid and {@link #isValid()} will return true. + * + * @see #start(int, int) + * @see #isValid() */ public abstract void end(); /** - * Invalidates the display list, indicating that it should be repopulated - * with new drawing commands prior to being used again. Calling this method - * causes calls to {@link #isValid()} to return <code>false</code>. + * Clears resources held onto by this display list. After calling this method + * {@link #isValid()} will return false. + * + * @see #isValid() + */ + public abstract void clear(); + + /** + * Sets the dirty flag. When a display list is dirty, {@link #clear()} should + * be invoked whenever possible. + * + * @see #isDirty() + * @see #clear() + * + * @hide */ - public abstract void invalidate(); + public void markDirty() { + mDirty = true; + } /** - * Clears additional resources held onto by this display list. You should - * only invoke this method after {@link #invalidate()}. + * Removes the dirty flag. This method can be used to cancel a cleanup + * previously scheduled by setting the dirty flag. + * + * @see #isDirty() + * @see #clear() + * + * @hide */ - public abstract void clear(); + protected void clearDirty() { + mDirty = false; + } + + /** + * Indicates whether the display list is dirty. + * + * @see #markDirty() + * @see #clear() + * + * @hide + */ + public boolean isDirty() { + return mDirty; + } /** * Returns whether the display list is currently usable. If this returns false, @@ -107,6 +261,8 @@ public abstract class DisplayList { * Return the amount of memory used by this display list. * * @return The size of this display list in bytes + * + * @hide */ public abstract int getSize(); @@ -115,228 +271,412 @@ public abstract class DisplayList { /////////////////////////////////////////////////////////////////////////// /** - * Set the caching property on the DisplayList, which indicates whether the DisplayList - * holds a layer. Layer DisplayLists should avoid creating an alpha layer, since alpha is + * Set the caching property on the display list, which indicates whether the display list + * holds a layer. Layer display lists should avoid creating an alpha layer, since alpha is * handled in the drawLayer operation directly (and more efficiently). * - * @param caching true if the DisplayList represents a hardware layer, false otherwise. + * @param caching true if the display list represents a hardware layer, false otherwise. + * + * @hide */ public abstract void setCaching(boolean caching); /** - * Set whether the DisplayList should clip itself to its bounds. This property is controlled by + * Set whether the display list should clip itself to its bounds. This property is controlled by * the view's parent. * - * @param clipChildren true if the DisplayList should clip to its bounds + * @param clipChildren true if the display list should clip to its bounds */ public abstract void setClipChildren(boolean clipChildren); /** - * Set the static matrix on the DisplayList. This matrix exists if a custom ViewGroup - * overrides - * {@link ViewGroup#getChildStaticTransformation(View, android.view.animation.Transformation)} - * and also has {@link ViewGroup#setStaticTransformationsEnabled(boolean)} set to true. - * This matrix will be concatenated with any other matrices in the DisplayList to position - * the view appropriately. + * Set the static matrix on the display list. The specified matrix is combined with other + * transforms (such as {@link #setScaleX(float)}, {@link #setRotation(float)}, etc.) + * + * @param matrix A transform matrix to apply to this display list + * + * @see #getMatrix(android.graphics.Matrix) + * @see #getMatrix() + */ + public abstract void setMatrix(Matrix matrix); + + /** + * Returns the static matrix set on this display list. + * + * @return A new {@link Matrix} instance populated with this display list's static + * matrix + * + * @see #getMatrix(android.graphics.Matrix) + * @see #setMatrix(android.graphics.Matrix) + */ + public Matrix getMatrix() { + return getMatrix(new Matrix()); + } + + /** + * Copies this display list's static matrix into the specified matrix. + * + * @param matrix The {@link Matrix} instance in which to copy this display + * list's static matrix. Cannot be null * - * @param matrix The matrix + * @return The <code>matrix</code> parameter, for convenience + * + * @see #getMatrix() + * @see #setMatrix(android.graphics.Matrix) */ - public abstract void setStaticMatrix(Matrix matrix); + public abstract Matrix getMatrix(Matrix matrix); /** - * Set the Animation matrix on the DisplayList. This matrix exists if an Animation is - * currently playing on a View, and is set on the DisplayList during at draw() time. When + * Set the Animation matrix on the display list. This matrix exists if an Animation is + * currently playing on a View, and is set on the display list during at draw() time. When * the Animation finishes, the matrix should be cleared by sending <code>null</code> * for the matrix parameter. * * @param matrix The matrix, null indicates that the matrix should be cleared. + * + * @hide */ public abstract void setAnimationMatrix(Matrix matrix); /** - * Sets the alpha value for the DisplayList + * Sets the translucency level for the display list. + * + * @param alpha The translucency of the display list, must be a value between 0.0f and 1.0f * - * @param alpha The translucency of the DisplayList * @see View#setAlpha(float) + * @see #getAlpha() */ public abstract void setAlpha(float alpha); /** - * Sets whether the DisplayList renders content which overlaps. Non-overlapping rendering - * can use a fast path for alpha that avoids rendering to an offscreen buffer. + * Returns the translucency level of this display list. + * + * @return A value between 0.0f and 1.0f + * + * @see #setAlpha(float) + */ + public abstract float getAlpha(); + + /** + * Sets whether the display list renders content which overlaps. Non-overlapping rendering + * can use a fast path for alpha that avoids rendering to an offscreen buffer. By default + * display lists consider they do not have overlapping content. + * + * @param hasOverlappingRendering False if the content is guaranteed to be non-overlapping, + * true otherwise. * - * @param hasOverlappingRendering * @see android.view.View#hasOverlappingRendering() + * @see #hasOverlappingRendering() */ public abstract void setHasOverlappingRendering(boolean hasOverlappingRendering); /** - * Sets the translationX value for the DisplayList + * Indicates whether the content of this display list overlaps. + * + * @return True if this display list renders content which overlaps, false otherwise. + * + * @see #setHasOverlappingRendering(boolean) + */ + public abstract boolean hasOverlappingRendering(); + + /** + * Sets the translation value for the display list on the X axis + * + * @param translationX The X axis translation value of the display list, in pixels * - * @param translationX The translationX value of the DisplayList * @see View#setTranslationX(float) + * @see #getTranslationX() */ public abstract void setTranslationX(float translationX); /** - * Sets the translationY value for the DisplayList + * Returns the translation value for this display list on the X axis, in pixels. + * + * @see #setTranslationX(float) + */ + public abstract float getTranslationX(); + + /** + * Sets the translation value for the display list on the Y axis + * + * @param translationY The Y axis translation value of the display list, in pixels * - * @param translationY The translationY value of the DisplayList * @see View#setTranslationY(float) + * @see #getTranslationY() */ public abstract void setTranslationY(float translationY); /** - * Sets the rotation value for the DisplayList + * Returns the translation value for this display list on the Y axis, in pixels. + * + * @see #setTranslationY(float) + */ + public abstract float getTranslationY(); + + /** + * Sets the rotation value for the display list around the Z axis + * + * @param rotation The rotation value of the display list, in degrees * - * @param rotation The rotation value of the DisplayList * @see View#setRotation(float) + * @see #getRotation() */ public abstract void setRotation(float rotation); /** - * Sets the rotationX value for the DisplayList + * Returns the rotation value for this display list around the Z axis, in degrees. + * + * @see #setRotation(float) + */ + public abstract float getRotation(); + + /** + * Sets the rotation value for the display list around the X axis + * + * @param rotationX The rotation value of the display list, in degrees * - * @param rotationX The rotationX value of the DisplayList * @see View#setRotationX(float) + * @see #getRotationX() */ public abstract void setRotationX(float rotationX); /** - * Sets the rotationY value for the DisplayList + * Returns the rotation value for this display list around the X axis, in degrees. + * + * @see #setRotationX(float) + */ + public abstract float getRotationX(); + + /** + * Sets the rotation value for the display list around the Y axis + * + * @param rotationY The rotation value of the display list, in degrees * - * @param rotationY The rotationY value of the DisplayList * @see View#setRotationY(float) + * @see #getRotationY() */ public abstract void setRotationY(float rotationY); /** - * Sets the scaleX value for the DisplayList + * Returns the rotation value for this display list around the Y axis, in degrees. + * + * @see #setRotationY(float) + */ + public abstract float getRotationY(); + + /** + * Sets the scale value for the display list on the X axis + * + * @param scaleX The scale value of the display list * - * @param scaleX The scaleX value of the DisplayList * @see View#setScaleX(float) + * @see #getScaleX() */ public abstract void setScaleX(float scaleX); /** - * Sets the scaleY value for the DisplayList + * Returns the scale value for this display list on the X axis. + * + * @see #setScaleX(float) + */ + public abstract float getScaleX(); + + /** + * Sets the scale value for the display list on the Y axis + * + * @param scaleY The scale value of the display list * - * @param scaleY The scaleY value of the DisplayList * @see View#setScaleY(float) + * @see #getScaleY() */ public abstract void setScaleY(float scaleY); /** - * Sets all of the transform-related values of the View onto the DisplayList + * Returns the scale value for this display list on the Y axis. * - * @param alpha The alpha value of the DisplayList - * @param translationX The translationX value of the DisplayList - * @param translationY The translationY value of the DisplayList - * @param rotation The rotation value of the DisplayList - * @param rotationX The rotationX value of the DisplayList - * @param rotationY The rotationY value of the DisplayList - * @param scaleX The scaleX value of the DisplayList - * @param scaleY The scaleY value of the DisplayList + * @see #setScaleY(float) + */ + public abstract float getScaleY(); + + /** + * Sets all of the transform-related values of the display list + * + * @param alpha The alpha value of the display list + * @param translationX The translationX value of the display list + * @param translationY The translationY value of the display list + * @param rotation The rotation value of the display list + * @param rotationX The rotationX value of the display list + * @param rotationY The rotationY value of the display list + * @param scaleX The scaleX value of the display list + * @param scaleY The scaleY value of the display list + * + * @hide */ public abstract void setTransformationInfo(float alpha, float translationX, float translationY, float rotation, float rotationX, float rotationY, float scaleX, float scaleY); /** - * Sets the pivotX value for the DisplayList + * Sets the pivot value for the display list on the X axis + * + * @param pivotX The pivot value of the display list on the X axis, in pixels * - * @param pivotX The pivotX value of the DisplayList * @see View#setPivotX(float) + * @see #getPivotX() */ public abstract void setPivotX(float pivotX); /** - * Sets the pivotY value for the DisplayList + * Returns the pivot value for this display list on the X axis, in pixels. + * + * @see #setPivotX(float) + */ + public abstract float getPivotX(); + + /** + * Sets the pivot value for the display list on the Y axis + * + * @param pivotY The pivot value of the display list on the Y axis, in pixels * - * @param pivotY The pivotY value of the DisplayList * @see View#setPivotY(float) + * @see #getPivotY() */ public abstract void setPivotY(float pivotY); /** - * Sets the camera distance for the DisplayList + * Returns the pivot value for this display list on the Y axis, in pixels. + * + * @see #setPivotY(float) + */ + public abstract float getPivotY(); + + /** + * Sets the camera distance for the display list. Refer to + * {@link View#setCameraDistance(float)} for more information on how to + * use this property. + * + * @param distance The distance in Z of the camera of the display list * - * @param distance The distance in z of the camera of the DisplayList * @see View#setCameraDistance(float) + * @see #getCameraDistance() */ public abstract void setCameraDistance(float distance); /** - * Sets the left value for the DisplayList + * Returns the distance in Z of the camera of the display list. + * + * @see #setCameraDistance(float) + */ + public abstract float getCameraDistance(); + + /** + * Sets the left position for the display list. + * + * @param left The left position, in pixels, of the display list * - * @param left The left value of the DisplayList * @see View#setLeft(int) + * @see #getLeft() */ public abstract void setLeft(int left); /** - * Sets the top value for the DisplayList + * Returns the left position for the display list in pixels. + * + * @see #setLeft(int) + */ + public abstract float getLeft(); + + /** + * Sets the top position for the display list. + * + * @param top The top position, in pixels, of the display list * - * @param top The top value of the DisplayList * @see View#setTop(int) + * @see #getTop() */ public abstract void setTop(int top); /** - * Sets the right value for the DisplayList + * Returns the top position for the display list in pixels. + * + * @see #setTop(int) + */ + public abstract float getTop(); + + /** + * Sets the right position for the display list. + * + * @param right The right position, in pixels, of the display list * - * @param right The right value of the DisplayList * @see View#setRight(int) + * @see #getRight() */ public abstract void setRight(int right); /** - * Sets the bottom value for the DisplayList + * Returns the right position for the display list in pixels. + * + * @see #setRight(int) + */ + public abstract float getRight(); + + /** + * Sets the bottom position for the display list. + * + * @param bottom The bottom position, in pixels, of the display list * - * @param bottom The bottom value of the DisplayList * @see View#setBottom(int) + * @see #getBottom() */ public abstract void setBottom(int bottom); /** - * Sets the left and top values for the DisplayList + * Returns the bottom position for the display list in pixels. * - * @param left The left value of the DisplayList - * @param top The top value of the DisplayList - * @see View#setLeft(int) - * @see View#setTop(int) + * @see #setBottom(int) */ - public abstract void setLeftTop(int left, int top); + public abstract float getBottom(); /** - * Sets the left and top values for the DisplayList + * Sets the left and top positions for the display list + * + * @param left The left position of the display list, in pixels + * @param top The top position of the display list, in pixels + * @param right The right position of the display list, in pixels + * @param bottom The bottom position of the display list, in pixels * - * @param left The left value of the DisplayList - * @param top The top value of the DisplayList * @see View#setLeft(int) * @see View#setTop(int) + * @see View#setRight(int) + * @see View#setBottom(int) */ public abstract void setLeftTopRightBottom(int left, int top, int right, int bottom); /** - * Offsets the left and right values for the DisplayList + * Offsets the left and right positions for the display list + * + * @param offset The amount that the left and right positions of the display + * list are offset, in pixels * - * @param offset The amount that the left and right values of the DisplayList are offset * @see View#offsetLeftAndRight(int) */ - public abstract void offsetLeftRight(int offset); + public abstract void offsetLeftAndRight(float offset); /** - * Offsets the top and bottom values for the DisplayList + * Offsets the top and bottom values for the display list + * + * @param offset The amount that the top and bottom positions of the display + * list are offset, in pixels * - * @param offset The amount that the top and bottom values of the DisplayList are offset * @see View#offsetTopAndBottom(int) */ - public abstract void offsetTopBottom(int offset); + public abstract void offsetTopAndBottom(float offset); /** - * Reset native resources. This is called when cleaning up the state of DisplayLists + * Reset native resources. This is called when cleaning up the state of display lists * during destruction of hardware resources, to ensure that we do not hold onto * obsolete resources after related resources are gone. + * + * @hide */ public abstract void reset(); } diff --git a/core/java/android/view/GLES20Canvas.java b/core/java/android/view/GLES20Canvas.java index b64a06e..6c48e43 100644 --- a/core/java/android/view/GLES20Canvas.java +++ b/core/java/android/view/GLES20Canvas.java @@ -144,6 +144,14 @@ class GLES20Canvas extends HardwareCanvas { } } + @Override + public void setName(String name) { + super.setName(name); + nSetName(mRenderer, name); + } + + private static native void nSetName(int renderer, String name); + /////////////////////////////////////////////////////////////////////////// // Hardware layers /////////////////////////////////////////////////////////////////////////// @@ -369,24 +377,13 @@ class GLES20Canvas extends HardwareCanvas { } private static native int nGetDisplayList(int renderer, int displayList); - - static void destroyDisplayList(int displayList) { - nDestroyDisplayList(displayList); - } - private static native void nDestroyDisplayList(int displayList); - - static int getDisplayListSize(int displayList) { - return nGetDisplayListSize(displayList); - } - - private static native int nGetDisplayListSize(int displayList); - - static void setDisplayListName(int displayList, String name) { - nSetDisplayListName(displayList, name); + @Override + void outputDisplayList(DisplayList displayList) { + nOutputDisplayList(mRenderer, ((GLES20DisplayList) displayList).getNativeDisplayList()); } - private static native void nSetDisplayListName(int displayList, String name); + private static native void nOutputDisplayList(int renderer, int displayList); @Override public int drawDisplayList(DisplayList displayList, Rect dirty, int flags) { @@ -397,13 +394,6 @@ class GLES20Canvas extends HardwareCanvas { private static native int nDrawDisplayList(int renderer, int displayList, Rect dirty, int flags); - @Override - void outputDisplayList(DisplayList displayList) { - nOutputDisplayList(mRenderer, ((GLES20DisplayList) displayList).getNativeDisplayList()); - } - - private static native void nOutputDisplayList(int renderer, int displayList); - /////////////////////////////////////////////////////////////////////////// // Hardware layer /////////////////////////////////////////////////////////////////////////// @@ -433,20 +423,16 @@ class GLES20Canvas extends HardwareCanvas { @Override public boolean clipPath(Path path) { - // TODO: Implement - path.computeBounds(mPathBounds, true); - return nClipRect(mRenderer, mPathBounds.left, mPathBounds.top, - mPathBounds.right, mPathBounds.bottom, Region.Op.INTERSECT.nativeInt); + return nClipPath(mRenderer, path.mNativePath, Region.Op.INTERSECT.nativeInt); } @Override public boolean clipPath(Path path, Region.Op op) { - // TODO: Implement - path.computeBounds(mPathBounds, true); - return nClipRect(mRenderer, mPathBounds.left, mPathBounds.top, - mPathBounds.right, mPathBounds.bottom, op.nativeInt); + return nClipPath(mRenderer, path.mNativePath, op.nativeInt); } + private static native boolean nClipPath(int renderer, int path, int op); + @Override public boolean clipRect(float left, float top, float right, float bottom) { return nClipRect(mRenderer, left, top, right, bottom, Region.Op.INTERSECT.nativeInt); @@ -465,8 +451,8 @@ class GLES20Canvas extends HardwareCanvas { return nClipRect(mRenderer, left, top, right, bottom, Region.Op.INTERSECT.nativeInt); } - private static native boolean nClipRect(int renderer, int left, int top, int right, int bottom, - int op); + private static native boolean nClipRect(int renderer, int left, int top, + int right, int bottom, int op); @Override public boolean clipRect(Rect rect) { @@ -492,20 +478,16 @@ class GLES20Canvas extends HardwareCanvas { @Override public boolean clipRegion(Region region) { - // TODO: Implement - region.getBounds(mClipBounds); - return nClipRect(mRenderer, mClipBounds.left, mClipBounds.top, - mClipBounds.right, mClipBounds.bottom, Region.Op.INTERSECT.nativeInt); + return nClipRegion(mRenderer, region.mNativeRegion, Region.Op.INTERSECT.nativeInt); } @Override public boolean clipRegion(Region region, Region.Op op) { - // TODO: Implement - region.getBounds(mClipBounds); - return nClipRect(mRenderer, mClipBounds.left, mClipBounds.top, - mClipBounds.right, mClipBounds.bottom, op.nativeInt); + return nClipRegion(mRenderer, region.mNativeRegion, op.nativeInt); } + private static native boolean nClipRegion(int renderer, int region, int op); + @Override public boolean getClipBounds(Rect bounds) { return nGetClipBounds(mRenderer, bounds); @@ -515,22 +497,22 @@ class GLES20Canvas extends HardwareCanvas { @Override public boolean quickReject(float left, float top, float right, float bottom, EdgeType type) { - return nQuickReject(mRenderer, left, top, right, bottom, type.nativeInt); + return nQuickReject(mRenderer, left, top, right, bottom); } private static native boolean nQuickReject(int renderer, float left, float top, - float right, float bottom, int edge); + float right, float bottom); @Override public boolean quickReject(Path path, EdgeType type) { path.computeBounds(mPathBounds, true); return nQuickReject(mRenderer, mPathBounds.left, mPathBounds.top, - mPathBounds.right, mPathBounds.bottom, type.nativeInt); + mPathBounds.right, mPathBounds.bottom); } @Override public boolean quickReject(RectF rect, EdgeType type) { - return nQuickReject(mRenderer, rect.left, rect.top, rect.right, rect.bottom, type.nativeInt); + return nQuickReject(mRenderer, rect.left, rect.top, rect.right, rect.bottom); } /////////////////////////////////////////////////////////////////////////// @@ -901,9 +883,9 @@ class GLES20Canvas extends HardwareCanvas { final int count = (meshWidth + 1) * (meshHeight + 1); checkRange(verts.length, vertOffset, count * 2); - // TODO: Colors are ignored for now - colors = null; - colorOffset = 0; + if (colors != null) { + checkRange(colors.length, colorOffset, count); + } int modifiers = paint != null ? setupModifiers(bitmap, paint) : MODIFIER_NONE; try { @@ -955,6 +937,8 @@ class GLES20Canvas extends HardwareCanvas { @Override public void drawLines(float[] pts, int offset, int count, Paint paint) { + if (count < 4) return; + if ((offset | count) < 0 || offset + count > pts.length) { throw new IllegalArgumentException("The lines array must contain 4 elements per line."); } @@ -1013,6 +997,17 @@ class GLES20Canvas extends HardwareCanvas { private static native void nDrawPath(int renderer, int path, int paint); private static native void nDrawRects(int renderer, int region, int paint); + void drawRects(float[] rects, int count, Paint paint) { + int modifiers = setupModifiers(paint, MODIFIER_COLOR_FILTER | MODIFIER_SHADER); + try { + nDrawRects(mRenderer, rects, count, paint.mNativePaint); + } finally { + if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); + } + } + + private static native void nDrawRects(int renderer, float[] rects, int count, int paint); + @Override public void drawPicture(Picture picture) { if (picture.createdFromStream) { @@ -1067,6 +1062,8 @@ class GLES20Canvas extends HardwareCanvas { @Override public void drawPoints(float[] pts, int offset, int count, Paint paint) { + if (count < 2) return; + int modifiers = setupModifiers(paint, MODIFIER_COLOR_FILTER | MODIFIER_SHADER); try { nDrawPoints(mRenderer, pts, offset, count, paint.mNativePaint); @@ -1165,14 +1162,14 @@ class GLES20Canvas extends HardwareCanvas { int modifiers = setupModifiers(paint); try { - nDrawText(mRenderer, text, index, count, x, y, paint.mBidiFlags, paint.mNativePaint); + nDrawText(mRenderer, text, index, count, x, y, paint.mNativePaint); } finally { if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); } } private static native void nDrawText(int renderer, char[] text, int index, int count, - float x, float y, int bidiFlags, int paint); + float x, float y, int paint); @Override public void drawText(CharSequence text, int start, int end, float x, float y, Paint paint) { @@ -1180,16 +1177,14 @@ class GLES20Canvas extends HardwareCanvas { try { if (text instanceof String || text instanceof SpannedString || text instanceof SpannableString) { - nDrawText(mRenderer, text.toString(), start, end, x, y, paint.mBidiFlags, - paint.mNativePaint); + nDrawText(mRenderer, text.toString(), start, end, x, y, paint.mNativePaint); } else if (text instanceof GraphicsOperations) { ((GraphicsOperations) text).drawText(this, start, end, x, y, paint); } else { char[] buf = TemporaryBuffer.obtain(end - start); TextUtils.getChars(text, start, end, buf, 0); - nDrawText(mRenderer, buf, 0, end - start, x, y, - paint.mBidiFlags, paint.mNativePaint); + nDrawText(mRenderer, buf, 0, end - start, x, y, paint.mNativePaint); TemporaryBuffer.recycle(buf); } } finally { @@ -1205,21 +1200,20 @@ class GLES20Canvas extends HardwareCanvas { int modifiers = setupModifiers(paint); try { - nDrawText(mRenderer, text, start, end, x, y, paint.mBidiFlags, paint.mNativePaint); + nDrawText(mRenderer, text, start, end, x, y, paint.mNativePaint); } finally { if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); } } private static native void nDrawText(int renderer, String text, int start, int end, - float x, float y, int bidiFlags, int paint); + float x, float y, int paint); @Override public void drawText(String text, float x, float y, Paint paint) { int modifiers = setupModifiers(paint); try { - nDrawText(mRenderer, text, 0, text.length(), x, y, paint.mBidiFlags, - paint.mNativePaint); + nDrawText(mRenderer, text, 0, text.length(), x, y, paint.mNativePaint); } finally { if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); } @@ -1235,14 +1229,14 @@ class GLES20Canvas extends HardwareCanvas { int modifiers = setupModifiers(paint); try { nDrawTextOnPath(mRenderer, text, index, count, path.mNativePath, hOffset, vOffset, - paint.mBidiFlags, paint.mNativePaint); + paint.mNativePaint); } finally { if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); } } private static native void nDrawTextOnPath(int renderer, char[] text, int index, int count, - int path, float hOffset, float vOffset, int bidiFlags, int nativePaint); + int path, float hOffset, float vOffset, int nativePaint); @Override public void drawTextOnPath(String text, Path path, float hOffset, float vOffset, Paint paint) { @@ -1251,28 +1245,25 @@ class GLES20Canvas extends HardwareCanvas { int modifiers = setupModifiers(paint); try { nDrawTextOnPath(mRenderer, text, 0, text.length(), path.mNativePath, hOffset, vOffset, - paint.mBidiFlags, paint.mNativePaint); + paint.mNativePaint); } finally { if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); } } private static native void nDrawTextOnPath(int renderer, String text, int start, int end, - int path, float hOffset, float vOffset, int bidiFlags, int nativePaint); + int path, float hOffset, float vOffset, int nativePaint); @Override public void drawTextRun(char[] text, int index, int count, int contextIndex, int contextCount, - float x, float y, int dir, Paint paint) { + float x, float y, Paint paint) { if ((index | count | text.length - index - count) < 0) { throw new IndexOutOfBoundsException(); } - if (dir != DIRECTION_LTR && dir != DIRECTION_RTL) { - throw new IllegalArgumentException("Unknown direction: " + dir); - } int modifiers = setupModifiers(paint); try { - nDrawTextRun(mRenderer, text, index, count, contextIndex, contextCount, x, y, dir, + nDrawTextRun(mRenderer, text, index, count, contextIndex, contextCount, x, y, paint.mNativePaint); } finally { if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); @@ -1280,32 +1271,31 @@ class GLES20Canvas extends HardwareCanvas { } private static native void nDrawTextRun(int renderer, char[] text, int index, int count, - int contextIndex, int contextCount, float x, float y, int dir, int nativePaint); + int contextIndex, int contextCount, float x, float y, int nativePaint); @Override public void drawTextRun(CharSequence text, int start, int end, int contextStart, int contextEnd, - float x, float y, int dir, Paint paint) { + float x, float y, Paint paint) { if ((start | end | end - start | text.length() - end) < 0) { throw new IndexOutOfBoundsException(); } int modifiers = setupModifiers(paint); try { - int flags = dir == 0 ? 0 : 1; if (text instanceof String || text instanceof SpannedString || text instanceof SpannableString) { nDrawTextRun(mRenderer, text.toString(), start, end, contextStart, - contextEnd, x, y, flags, paint.mNativePaint); + contextEnd, x, y, paint.mNativePaint); } else if (text instanceof GraphicsOperations) { ((GraphicsOperations) text).drawTextRun(this, start, end, - contextStart, contextEnd, x, y, flags, paint); + contextStart, contextEnd, x, y, paint); } else { int contextLen = contextEnd - contextStart; int len = end - start; char[] buf = TemporaryBuffer.obtain(contextLen); TextUtils.getChars(text, contextStart, contextEnd, buf, 0); nDrawTextRun(mRenderer, buf, start - contextStart, len, 0, contextLen, - x, y, flags, paint.mNativePaint); + x, y, paint.mNativePaint); TemporaryBuffer.recycle(buf); } } finally { @@ -1314,7 +1304,7 @@ class GLES20Canvas extends HardwareCanvas { } private static native void nDrawTextRun(int renderer, String text, int start, int end, - int contextStart, int contextEnd, float x, float y, int flags, int nativePaint); + int contextStart, int contextEnd, float x, float y, int nativePaint); @Override public void drawVertices(VertexMode mode, int vertexCount, float[] verts, int vertOffset, diff --git a/core/java/android/view/GLES20DisplayList.java b/core/java/android/view/GLES20DisplayList.java index e9bd0c4..9c5cdb2 100644 --- a/core/java/android/view/GLES20DisplayList.java +++ b/core/java/android/view/GLES20DisplayList.java @@ -58,7 +58,7 @@ class GLES20DisplayList extends DisplayList { } @Override - public HardwareCanvas start() { + public HardwareCanvas start(int width, int height) { if (mCanvas != null) { throw new IllegalStateException("Recording has already started"); } @@ -66,24 +66,25 @@ class GLES20DisplayList extends DisplayList { mValid = false; mCanvas = GLES20RecordingCanvas.obtain(this); mCanvas.start(); + + mCanvas.setViewport(width, height); + // The dirty rect should always be null for a display list + mCanvas.onPreDraw(null); + return mCanvas; } - @Override - public void invalidate() { + public void clear() { + clearDirty(); + if (mCanvas != null) { mCanvas.recycle(); mCanvas = null; } mValid = false; - } - @Override - public void clear() { - if (!mValid) { - mBitmaps.clear(); - mChildDisplayLists.clear(); - } + mBitmaps.clear(); + mChildDisplayLists.clear(); } @Override @@ -101,11 +102,12 @@ class GLES20DisplayList extends DisplayList { @Override public void end() { if (mCanvas != null) { + mCanvas.onPostDraw(); if (mFinalizer != null) { mCanvas.end(mFinalizer.mNativeDisplayList); } else { mFinalizer = new DisplayListFinalizer(mCanvas.end(0)); - GLES20Canvas.setDisplayListName(mFinalizer.mNativeDisplayList, mName); + nSetDisplayListName(mFinalizer.mNativeDisplayList, mName); } mCanvas.recycle(); mCanvas = null; @@ -116,9 +118,13 @@ class GLES20DisplayList extends DisplayList { @Override public int getSize() { if (mFinalizer == null) return 0; - return GLES20Canvas.getDisplayListSize(mFinalizer.mNativeDisplayList); + return nGetDisplayListSize(mFinalizer.mNativeDisplayList); } + private static native void nDestroyDisplayList(int displayList); + private static native int nGetDisplayListSize(int displayList); + private static native void nSetDisplayListName(int displayList, String name); + /////////////////////////////////////////////////////////////////////////// // Native View Properties /////////////////////////////////////////////////////////////////////////// @@ -138,13 +144,21 @@ class GLES20DisplayList extends DisplayList { } @Override - public void setStaticMatrix(Matrix matrix) { + public void setMatrix(Matrix matrix) { if (hasNativeDisplayList()) { nSetStaticMatrix(mFinalizer.mNativeDisplayList, matrix.native_instance); } } @Override + public Matrix getMatrix(Matrix matrix) { + if (hasNativeDisplayList()) { + nGetMatrix(mFinalizer.mNativeDisplayList, matrix.native_instance); + } + return matrix; + } + + @Override public void setAnimationMatrix(Matrix matrix) { if (hasNativeDisplayList()) { nSetAnimationMatrix(mFinalizer.mNativeDisplayList, @@ -160,6 +174,14 @@ class GLES20DisplayList extends DisplayList { } @Override + public float getAlpha() { + if (hasNativeDisplayList()) { + return nGetAlpha(mFinalizer.mNativeDisplayList); + } + return 1.0f; + } + + @Override public void setHasOverlappingRendering(boolean hasOverlappingRendering) { if (hasNativeDisplayList()) { nSetHasOverlappingRendering(mFinalizer.mNativeDisplayList, hasOverlappingRendering); @@ -167,6 +189,15 @@ class GLES20DisplayList extends DisplayList { } @Override + public boolean hasOverlappingRendering() { + //noinspection SimplifiableIfStatement + if (hasNativeDisplayList()) { + return nHasOverlappingRendering(mFinalizer.mNativeDisplayList); + } + return true; + } + + @Override public void setTranslationX(float translationX) { if (hasNativeDisplayList()) { nSetTranslationX(mFinalizer.mNativeDisplayList, translationX); @@ -174,6 +205,14 @@ class GLES20DisplayList extends DisplayList { } @Override + public float getTranslationX() { + if (hasNativeDisplayList()) { + return nGetTranslationX(mFinalizer.mNativeDisplayList); + } + return 0.0f; + } + + @Override public void setTranslationY(float translationY) { if (hasNativeDisplayList()) { nSetTranslationY(mFinalizer.mNativeDisplayList, translationY); @@ -181,6 +220,14 @@ class GLES20DisplayList extends DisplayList { } @Override + public float getTranslationY() { + if (hasNativeDisplayList()) { + return nGetTranslationY(mFinalizer.mNativeDisplayList); + } + return 0.0f; + } + + @Override public void setRotation(float rotation) { if (hasNativeDisplayList()) { nSetRotation(mFinalizer.mNativeDisplayList, rotation); @@ -188,6 +235,14 @@ class GLES20DisplayList extends DisplayList { } @Override + public float getRotation() { + if (hasNativeDisplayList()) { + return nGetRotation(mFinalizer.mNativeDisplayList); + } + return 0.0f; + } + + @Override public void setRotationX(float rotationX) { if (hasNativeDisplayList()) { nSetRotationX(mFinalizer.mNativeDisplayList, rotationX); @@ -195,6 +250,14 @@ class GLES20DisplayList extends DisplayList { } @Override + public float getRotationX() { + if (hasNativeDisplayList()) { + return nGetRotationX(mFinalizer.mNativeDisplayList); + } + return 0.0f; + } + + @Override public void setRotationY(float rotationY) { if (hasNativeDisplayList()) { nSetRotationY(mFinalizer.mNativeDisplayList, rotationY); @@ -202,6 +265,14 @@ class GLES20DisplayList extends DisplayList { } @Override + public float getRotationY() { + if (hasNativeDisplayList()) { + return nGetRotationY(mFinalizer.mNativeDisplayList); + } + return 0.0f; + } + + @Override public void setScaleX(float scaleX) { if (hasNativeDisplayList()) { nSetScaleX(mFinalizer.mNativeDisplayList, scaleX); @@ -209,6 +280,14 @@ class GLES20DisplayList extends DisplayList { } @Override + public float getScaleX() { + if (hasNativeDisplayList()) { + return nGetScaleX(mFinalizer.mNativeDisplayList); + } + return 1.0f; + } + + @Override public void setScaleY(float scaleY) { if (hasNativeDisplayList()) { nSetScaleY(mFinalizer.mNativeDisplayList, scaleY); @@ -216,6 +295,14 @@ class GLES20DisplayList extends DisplayList { } @Override + public float getScaleY() { + if (hasNativeDisplayList()) { + return nGetScaleY(mFinalizer.mNativeDisplayList); + } + return 1.0f; + } + + @Override public void setTransformationInfo(float alpha, float translationX, float translationY, float rotation, float rotationX, float rotationY, float scaleX, float scaleY) { if (hasNativeDisplayList()) { @@ -232,6 +319,14 @@ class GLES20DisplayList extends DisplayList { } @Override + public float getPivotX() { + if (hasNativeDisplayList()) { + return nGetPivotX(mFinalizer.mNativeDisplayList); + } + return 0.0f; + } + + @Override public void setPivotY(float pivotY) { if (hasNativeDisplayList()) { nSetPivotY(mFinalizer.mNativeDisplayList, pivotY); @@ -239,6 +334,14 @@ class GLES20DisplayList extends DisplayList { } @Override + public float getPivotY() { + if (hasNativeDisplayList()) { + return nGetPivotY(mFinalizer.mNativeDisplayList); + } + return 0.0f; + } + + @Override public void setCameraDistance(float distance) { if (hasNativeDisplayList()) { nSetCameraDistance(mFinalizer.mNativeDisplayList, distance); @@ -246,6 +349,14 @@ class GLES20DisplayList extends DisplayList { } @Override + public float getCameraDistance() { + if (hasNativeDisplayList()) { + return nGetCameraDistance(mFinalizer.mNativeDisplayList); + } + return 0.0f; + } + + @Override public void setLeft(int left) { if (hasNativeDisplayList()) { nSetLeft(mFinalizer.mNativeDisplayList, left); @@ -253,6 +364,14 @@ class GLES20DisplayList extends DisplayList { } @Override + public float getLeft() { + if (hasNativeDisplayList()) { + return nGetLeft(mFinalizer.mNativeDisplayList); + } + return 0.0f; + } + + @Override public void setTop(int top) { if (hasNativeDisplayList()) { nSetTop(mFinalizer.mNativeDisplayList, top); @@ -260,6 +379,14 @@ class GLES20DisplayList extends DisplayList { } @Override + public float getTop() { + if (hasNativeDisplayList()) { + return nGetTop(mFinalizer.mNativeDisplayList); + } + return 0.0f; + } + + @Override public void setRight(int right) { if (hasNativeDisplayList()) { nSetRight(mFinalizer.mNativeDisplayList, right); @@ -267,6 +394,14 @@ class GLES20DisplayList extends DisplayList { } @Override + public float getRight() { + if (hasNativeDisplayList()) { + return nGetRight(mFinalizer.mNativeDisplayList); + } + return 0.0f; + } + + @Override public void setBottom(int bottom) { if (hasNativeDisplayList()) { nSetBottom(mFinalizer.mNativeDisplayList, bottom); @@ -274,10 +409,11 @@ class GLES20DisplayList extends DisplayList { } @Override - public void setLeftTop(int left, int top) { + public float getBottom() { if (hasNativeDisplayList()) { - nSetLeftTop(mFinalizer.mNativeDisplayList, left, top); + return nGetBottom(mFinalizer.mNativeDisplayList); } + return 0.0f; } @Override @@ -288,25 +424,24 @@ class GLES20DisplayList extends DisplayList { } @Override - public void offsetLeftRight(int offset) { + public void offsetLeftAndRight(float offset) { if (hasNativeDisplayList()) { - nOffsetLeftRight(mFinalizer.mNativeDisplayList, offset); + nOffsetLeftAndRight(mFinalizer.mNativeDisplayList, offset); } } @Override - public void offsetTopBottom(int offset) { + public void offsetTopAndBottom(float offset) { if (hasNativeDisplayList()) { - nOffsetTopBottom(mFinalizer.mNativeDisplayList, offset); + nOffsetTopAndBottom(mFinalizer.mNativeDisplayList, offset); } } private static native void nReset(int displayList); - private static native void nOffsetTopBottom(int displayList, int offset); - private static native void nOffsetLeftRight(int displayList, int offset); + private static native void nOffsetTopAndBottom(int displayList, float offset); + private static native void nOffsetLeftAndRight(int displayList, float offset); private static native void nSetLeftTopRightBottom(int displayList, int left, int top, int right, int bottom); - private static native void nSetLeftTop(int displayList, int left, int top); private static native void nSetBottom(int displayList, int bottom); private static native void nSetRight(int displayList, int right); private static native void nSetTop(int displayList, int top); @@ -332,6 +467,23 @@ class GLES20DisplayList extends DisplayList { private static native void nSetStaticMatrix(int displayList, int nativeMatrix); private static native void nSetAnimationMatrix(int displayList, int animationMatrix); + private static native boolean nHasOverlappingRendering(int displayList); + private static native void nGetMatrix(int displayList, int matrix); + private static native float nGetAlpha(int displayList); + private static native float nGetLeft(int displayList); + private static native float nGetTop(int displayList); + private static native float nGetRight(int displayList); + private static native float nGetBottom(int displayList); + private static native float nGetCameraDistance(int displayList); + private static native float nGetScaleX(int displayList); + private static native float nGetScaleY(int displayList); + private static native float nGetTranslationX(int displayList); + private static native float nGetTranslationY(int displayList); + private static native float nGetRotation(int displayList); + private static native float nGetRotationX(int displayList); + private static native float nGetRotationY(int displayList); + private static native float nGetPivotX(int displayList); + private static native float nGetPivotY(int displayList); /////////////////////////////////////////////////////////////////////////// // Finalization @@ -347,7 +499,7 @@ class GLES20DisplayList extends DisplayList { @Override protected void finalize() throws Throwable { try { - GLES20Canvas.destroyDisplayList(mNativeDisplayList); + nDestroyDisplayList(mNativeDisplayList); } finally { super.finalize(); } diff --git a/core/java/android/view/GLES20Layer.java b/core/java/android/view/GLES20Layer.java index 812fb97..7ee628b 100644 --- a/core/java/android/view/GLES20Layer.java +++ b/core/java/android/view/GLES20Layer.java @@ -53,12 +53,12 @@ abstract class GLES20Layer extends HardwareLayer { } @Override - boolean copyInto(Bitmap bitmap) { + public boolean copyInto(Bitmap bitmap) { return GLES20Canvas.nCopyLayer(mLayer, bitmap.mNativeBitmap); } @Override - void destroy() { + public void destroy() { if (mFinalizer != null) { mFinalizer.destroy(); mFinalizer = null; diff --git a/core/java/android/view/GLES20RecordingCanvas.java b/core/java/android/view/GLES20RecordingCanvas.java index d2df45a..947cf44 100644 --- a/core/java/android/view/GLES20RecordingCanvas.java +++ b/core/java/android/view/GLES20RecordingCanvas.java @@ -24,10 +24,7 @@ import android.graphics.Path; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Shader; -import android.util.Pool; -import android.util.Poolable; -import android.util.PoolableManager; -import android.util.Pools; +import android.util.Pools.SynchronizedPool; /** * An implementation of a GL canvas that records drawing operations. @@ -35,35 +32,25 @@ import android.util.Pools; * Bitmap objects that it draws, preventing the backing memory of Bitmaps from being freed while * the DisplayList is still holding a native reference to the memory. */ -class GLES20RecordingCanvas extends GLES20Canvas implements Poolable<GLES20RecordingCanvas> { +class GLES20RecordingCanvas extends GLES20Canvas { // The recording canvas pool should be large enough to handle a deeply nested // view hierarchy because display lists are generated recursively. private static final int POOL_LIMIT = 25; - private static final Pool<GLES20RecordingCanvas> sPool = Pools.synchronizedPool( - Pools.finitePool(new PoolableManager<GLES20RecordingCanvas>() { - public GLES20RecordingCanvas newInstance() { - return new GLES20RecordingCanvas(); - } - @Override - public void onAcquired(GLES20RecordingCanvas element) { - } - @Override - public void onReleased(GLES20RecordingCanvas element) { - } - }, POOL_LIMIT)); - - private GLES20RecordingCanvas mNextPoolable; - private boolean mIsPooled; + private static final SynchronizedPool<GLES20RecordingCanvas> sPool = + new SynchronizedPool<GLES20RecordingCanvas>(POOL_LIMIT); private GLES20DisplayList mDisplayList; private GLES20RecordingCanvas() { - super(true /*record*/, true /*translucent*/); + super(true, true); } static GLES20RecordingCanvas obtain(GLES20DisplayList displayList) { GLES20RecordingCanvas canvas = sPool.acquire(); + if (canvas == null) { + canvas = new GLES20RecordingCanvas(); + } canvas.mDisplayList = displayList; return canvas; } @@ -280,15 +267,15 @@ class GLES20RecordingCanvas extends GLES20Canvas implements Poolable<GLES20Recor @Override public void drawTextRun(char[] text, int index, int count, int contextIndex, int contextCount, - float x, float y, int dir, Paint paint) { - super.drawTextRun(text, index, count, contextIndex, contextCount, x, y, dir, paint); + float x, float y, Paint paint) { + super.drawTextRun(text, index, count, contextIndex, contextCount, x, y, paint); recordShaderBitmap(paint); } @Override public void drawTextRun(CharSequence text, int start, int end, int contextStart, - int contextEnd, float x, float y, int dir, Paint paint) { - super.drawTextRun(text, start, end, contextStart, contextEnd, x, y, dir, paint); + int contextEnd, float x, float y, Paint paint) { + super.drawTextRun(text, start, end, contextStart, contextEnd, x, y, paint); recordShaderBitmap(paint); } @@ -300,24 +287,4 @@ class GLES20RecordingCanvas extends GLES20Canvas implements Poolable<GLES20Recor colorOffset, indices, indexOffset, indexCount, paint); recordShaderBitmap(paint); } - - @Override - public GLES20RecordingCanvas getNextPoolable() { - return mNextPoolable; - } - - @Override - public void setNextPoolable(GLES20RecordingCanvas element) { - mNextPoolable = element; - } - - @Override - public boolean isPooled() { - return mIsPooled; - } - - @Override - public void setPooled(boolean isPooled) { - mIsPooled = isPooled; - } } diff --git a/core/java/android/view/GLES20RenderLayer.java b/core/java/android/view/GLES20RenderLayer.java index 44d4719..086e78c 100644 --- a/core/java/android/view/GLES20RenderLayer.java +++ b/core/java/android/view/GLES20RenderLayer.java @@ -92,6 +92,10 @@ class GLES20RenderLayer extends GLES20Layer { if (currentCanvas instanceof GLES20Canvas) { ((GLES20Canvas) currentCanvas).resume(); } + HardwareCanvas canvas = getCanvas(); + if (canvas != null) { + canvas.onPostDraw(); + } } @Override @@ -99,7 +103,10 @@ class GLES20RenderLayer extends GLES20Layer { if (currentCanvas instanceof GLES20Canvas) { ((GLES20Canvas) currentCanvas).interrupt(); } - return getCanvas(); + HardwareCanvas canvas = getCanvas(); + canvas.setViewport(mWidth, mHeight); + canvas.onPreDraw(null); + return canvas; } /** diff --git a/core/java/android/view/GestureDetector.java b/core/java/android/view/GestureDetector.java index 9ddb32e..28c1058 100644 --- a/core/java/android/view/GestureDetector.java +++ b/core/java/android/view/GestureDetector.java @@ -213,6 +213,7 @@ public class GestureDetector { private OnDoubleTapListener mDoubleTapListener; private boolean mStillDown; + private boolean mDeferConfirmSingleTap; private boolean mInLongPress; private boolean mAlwaysInTapRegion; private boolean mAlwaysInBiggerTapRegion; @@ -267,8 +268,12 @@ public class GestureDetector { case TAP: // If the user's finger is still down, do not count it as a tap - if (mDoubleTapListener != null && !mStillDown) { - mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent); + if (mDoubleTapListener != null) { + if (!mStillDown) { + mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent); + } else { + mDeferConfirmSingleTap = true; + } } break; @@ -533,6 +538,7 @@ public class GestureDetector { mAlwaysInBiggerTapRegion = true; mStillDown = true; mInLongPress = false; + mDeferConfirmSingleTap = false; if (mIsLongpressEnabled) { mHandler.removeMessages(LONG_PRESS); @@ -586,6 +592,9 @@ public class GestureDetector { mInLongPress = false; } else if (mAlwaysInTapRegion) { handled = mListener.onSingleTapUp(ev); + if (mDeferConfirmSingleTap && mDoubleTapListener != null) { + mDoubleTapListener.onSingleTapConfirmed(ev); + } } else { // A fling must travel the minimum tap distance @@ -612,6 +621,7 @@ public class GestureDetector { mVelocityTracker = null; } mIsDoubleTapping = false; + mDeferConfirmSingleTap = false; mHandler.removeMessages(SHOW_PRESS); mHandler.removeMessages(LONG_PRESS); break; @@ -637,6 +647,7 @@ public class GestureDetector { mStillDown = false; mAlwaysInTapRegion = false; mAlwaysInBiggerTapRegion = false; + mDeferConfirmSingleTap = false; if (mInLongPress) { mInLongPress = false; } @@ -649,6 +660,7 @@ public class GestureDetector { mIsDoubleTapping = false; mAlwaysInTapRegion = false; mAlwaysInBiggerTapRegion = false; + mDeferConfirmSingleTap = false; if (mInLongPress) { mInLongPress = false; } @@ -671,6 +683,7 @@ public class GestureDetector { private void dispatchLongPress() { mHandler.removeMessages(TAP); + mDeferConfirmSingleTap = false; mInLongPress = true; mListener.onLongPress(mCurrentDownEvent); } diff --git a/core/java/android/view/HardwareCanvas.java b/core/java/android/view/HardwareCanvas.java index eeae3ed..0dfed69 100644 --- a/core/java/android/view/HardwareCanvas.java +++ b/core/java/android/view/HardwareCanvas.java @@ -23,10 +23,12 @@ import android.graphics.Rect; /** * Hardware accelerated canvas. - * - * @hide + * + * @hide */ public abstract class HardwareCanvas extends Canvas { + private String mName; + @Override public boolean isHardwareAccelerated() { return true; @@ -36,33 +38,76 @@ public abstract class HardwareCanvas extends Canvas { public void setBitmap(Bitmap bitmap) { throw new UnsupportedOperationException(); } - + + /** + * Specifies the name of this canvas. Naming the canvas is entirely + * optional but can be useful for debugging purposes. + * + * @param name The name of the canvas, can be null + * + * @see #getName() + * + * @hide + */ + public void setName(String name) { + mName = name; + } + + /** + * Returns the name of this canvas. + * + * @return The name of the canvas or null + * + * @see #setName(String) + * + * @hide + */ + public String getName() { + return mName; + } + /** * Invoked before any drawing operation is performed in this canvas. * * @param dirty The dirty rectangle to update, can be null. * @return {@link DisplayList#STATUS_DREW} if anything was drawn (such as a call to clear - * the canvas). + * the canvas). + * + * @hide */ public abstract int onPreDraw(Rect dirty); /** * Invoked after all drawing operation have been performed. + * + * @hide */ public abstract void onPostDraw(); /** + * Draws the specified display list onto this canvas. The display list can only + * be drawn if {@link android.view.DisplayList#isValid()} returns true. + * + * @param displayList The display list to replay. + */ + public void drawDisplayList(DisplayList displayList) { + drawDisplayList(displayList, null, DisplayList.FLAG_CLIP_CHILDREN); + } + + /** * Draws the specified display list onto this canvas. * * @param displayList The display list to replay. * @param dirty The dirty region to redraw in the next pass, matters only - * if this method returns true, can be null. + * if this method returns {@link DisplayList#STATUS_DRAW}, can be null. * @param flags Optional flags about drawing, see {@link DisplayList} for * the possible flags. * * @return One of {@link DisplayList#STATUS_DONE}, {@link DisplayList#STATUS_DRAW}, or * {@link DisplayList#STATUS_INVOKE}, or'd with {@link DisplayList#STATUS_DREW} * if anything was drawn. + * + * @hide */ public abstract int drawDisplayList(DisplayList displayList, Rect dirty, int flags); @@ -71,6 +116,8 @@ public abstract class HardwareCanvas extends Canvas { * tools to output display lists for selected nodes to the log. * * @param displayList The display list to be logged. + * + * @hide */ abstract void outputDisplayList(DisplayList displayList); @@ -81,6 +128,8 @@ public abstract class HardwareCanvas extends Canvas { * @param x The left coordinate of the layer * @param y The top coordinate of the layer * @param paint The paint used to draw the layer + * + * @hide */ abstract void drawHardwareLayer(HardwareLayer layer, float x, float y, Paint paint); @@ -93,6 +142,8 @@ public abstract class HardwareCanvas extends Canvas { * * @return One of {@link DisplayList#STATUS_DONE}, {@link DisplayList#STATUS_DRAW} or * {@link DisplayList#STATUS_INVOKE} + * + * @hide */ public int callDrawGLFunction(int drawGLFunction) { // Noop - this is done in the display list recorder subclass @@ -106,6 +157,8 @@ public abstract class HardwareCanvas extends Canvas { * * @return One of {@link DisplayList#STATUS_DONE}, {@link DisplayList#STATUS_DRAW} or * {@link DisplayList#STATUS_INVOKE} + * + * @hide */ public int invokeFunctors(Rect dirty) { return DisplayList.STATUS_DONE; @@ -118,7 +171,9 @@ public abstract class HardwareCanvas extends Canvas { * * @see #invokeFunctors(android.graphics.Rect) * @see #callDrawGLFunction(int) - * @see #detachFunctor(int) + * @see #detachFunctor(int) + * + * @hide */ abstract void detachFunctor(int functor); @@ -129,7 +184,9 @@ public abstract class HardwareCanvas extends Canvas { * * @see #invokeFunctors(android.graphics.Rect) * @see #callDrawGLFunction(int) - * @see #detachFunctor(int) + * @see #detachFunctor(int) + * + * @hide */ abstract void attachFunctor(int functor); @@ -139,13 +196,17 @@ public abstract class HardwareCanvas extends Canvas { * @param layer The layer to update * * @see #clearLayerUpdates() + * + * @hide */ abstract void pushLayerUpdate(HardwareLayer layer); /** * Removes all enqueued layer updates. * - * @see #pushLayerUpdate(HardwareLayer) + * @see #pushLayerUpdate(HardwareLayer) + * + * @hide */ abstract void clearLayerUpdates(); } diff --git a/core/java/android/view/HardwareLayer.java b/core/java/android/view/HardwareLayer.java index d3bc35a..18b838b 100644 --- a/core/java/android/view/HardwareLayer.java +++ b/core/java/android/view/HardwareLayer.java @@ -24,7 +24,7 @@ import android.graphics.Rect; /** * A hardware layer can be used to render graphics operations into a hardware - * friendly buffer. For instance, with an OpenGL backend, a hardware layer + * friendly buffer. For instance, with an OpenGL backend a hardware layer * would use a Frame Buffer Object (FBO.) The hardware layer can be used as * a drawing cache when a complex set of graphics operations needs to be * drawn several times. @@ -68,7 +68,7 @@ abstract class HardwareLayer { * @param paint The paint used when the layer is drawn into the destination canvas. * @see View#setLayerPaint(android.graphics.Paint) */ - void setLayerPaint(Paint paint) {} + void setLayerPaint(Paint paint) { } /** * Returns the minimum width of the layer. @@ -144,6 +144,9 @@ abstract class HardwareLayer { * this layer. * * @return A hardware canvas, or null if a canvas cannot be created + * + * @see #start(android.graphics.Canvas) + * @see #end(android.graphics.Canvas) */ abstract HardwareCanvas getCanvas(); @@ -154,12 +157,14 @@ abstract class HardwareLayer { /** * This must be invoked before drawing onto this layer. + * * @param currentCanvas */ abstract HardwareCanvas start(Canvas currentCanvas); - + /** * This must be invoked after drawing onto this layer. + * * @param currentCanvas */ abstract void end(Canvas currentCanvas); diff --git a/core/java/android/view/HardwareRenderer.java b/core/java/android/view/HardwareRenderer.java index 5b7a5af..e086f5a 100644 --- a/core/java/android/view/HardwareRenderer.java +++ b/core/java/android/view/HardwareRenderer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010 The Android Open Source Project + * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,6 @@ * limitations under the License. */ - package android.view; import android.content.ComponentCallbacks2; @@ -29,6 +28,7 @@ import android.os.Looper; import android.os.SystemClock; import android.os.SystemProperties; import android.os.Trace; +import android.util.DisplayMetrics; import android.util.Log; import com.google.android.gles_jni.EGLImpl; @@ -42,13 +42,14 @@ import javax.microedition.khronos.opengles.GL; import java.io.File; import java.io.PrintWriter; +import java.util.Arrays; import java.util.concurrent.locks.ReentrantLock; import static javax.microedition.khronos.egl.EGL10.*; /** - * Interface for rendering a ViewAncestor using hardware acceleration. - * + * Interface for rendering a view hierarchy using hardware acceleration. + * * @hide */ public abstract class HardwareRenderer { @@ -62,9 +63,9 @@ public abstract class HardwareRenderer { /** * Turn on to only refresh the parts of the screen that need updating. * When turned on the property defined by {@link #RENDER_DIRTY_REGIONS_PROPERTY} - * must also have the value "true". + * must also have the value "true". */ - public static final boolean RENDER_DIRTY_REGIONS = true; + static final boolean RENDER_DIRTY_REGIONS = true; /** * System property used to enable or disable dirty regions invalidation. @@ -76,16 +77,6 @@ public abstract class HardwareRenderer { * "false", to disable partial invalidates */ static final String RENDER_DIRTY_REGIONS_PROPERTY = "debug.hwui.render_dirty_regions"; - - /** - * System property used to enable or disable vsync. - * The default value of this property is assumed to be false. - * - * Possible values: - * "true", to disable vsync - * "false", to enable vsync - */ - static final String DISABLE_VSYNC_PROPERTY = "debug.hwui.disable_vsync"; /** * System property used to enable or disable hardware rendering profiling. @@ -97,13 +88,34 @@ public abstract class HardwareRenderer { * * Possible values: * "true", to enable profiling + * "visual_bars", to enable profiling and visualize the results on screen + * "visual_lines", to enable profiling and visualize the results on screen * "false", to disable profiling - * + * + * @see #PROFILE_PROPERTY_VISUALIZE_BARS + * @see #PROFILE_PROPERTY_VISUALIZE_LINES + * * @hide */ public static final String PROFILE_PROPERTY = "debug.hwui.profile"; /** + * Value for {@link #PROFILE_PROPERTY}. When the property is set to this + * value, profiling data will be visualized on screen as a bar chart. + * + * @hide + */ + public static final String PROFILE_PROPERTY_VISUALIZE_BARS = "visual_bars"; + + /** + * Value for {@link #PROFILE_PROPERTY}. When the property is set to this + * value, profiling data will be visualized on screen as a line chart. + * + * @hide + */ + public static final String PROFILE_PROPERTY_VISUALIZE_LINES = "visual_lines"; + + /** * System property used to specify the number of frames to be used * when doing hardware rendering profiling. * The default value of this property is #PROFILE_MAX_FRAMES. @@ -161,6 +173,19 @@ public abstract class HardwareRenderer { public static final String DEBUG_SHOW_OVERDRAW_PROPERTY = "debug.hwui.show_overdraw"; /** + * Turn on to debug non-rectangular clip operations. + * + * Possible values: + * "hide", to disable this debug mode + * "highlight", highlight drawing commands tested against a non-rectangular clip + * "stencil", renders the clip region on screen when set + * + * @hide + */ + public static final String DEBUG_SHOW_NON_RECTANGULAR_CLIP_PROPERTY = + "debug.hwui.show_non_rect_clip"; + + /** * A process can set this flag to false to prevent the use of hardware * rendering. * @@ -323,10 +348,26 @@ public abstract class HardwareRenderer { abstract long getFrameCount(); /** + * Loads system properties used by the renderer. This method is invoked + * whenever system properties are modified. Implementations can use this + * to trigger live updates of the renderer based on properties. + * + * @param surface The surface to update with the new properties. + * Can be null. + * + * @return True if a property has changed. + */ + abstract boolean loadSystemProperties(Surface surface); + + private static native boolean nLoadProperties(); + + /** * Sets the directory to use as a persistent storage for hardware rendering * resources. * * @param cacheDir A directory the current process can write to + * + * @hide */ public static void setupDiskCache(File cacheDir) { nSetupShadersDiskCache(new File(cacheDir, CACHE_PATH_SHADERS).getAbsolutePath()); @@ -338,7 +379,7 @@ public abstract class HardwareRenderer { * Notifies EGL that the frame is about to be rendered. * @param size */ - private static void beginFrame(int[] size) { + static void beginFrame(int[] size) { nBeginFrame(size); } @@ -373,15 +414,6 @@ public abstract class HardwareRenderer { private static native boolean nIsBackBufferPreserved(); /** - * Disables v-sync. For performance testing only. - */ - static void disableVsync() { - nDisableVsync(); - } - - private static native void nDisableVsync(); - - /** * Indicates that the specified hardware layer needs to be updated * as soon as possible. * @@ -426,10 +458,11 @@ public abstract class HardwareRenderer { * Creates a new display list that can be used to record batches of * drawing operations. * - * @param name The name of the display list, used for debugging purpose. - * May be null + * @param name The name of the display list, used for debugging purpose. May be null. * * @return A new display list. + * + * @hide */ public abstract DisplayList createDisplayList(String name); @@ -457,7 +490,6 @@ public abstract class HardwareRenderer { /** * Creates a new {@link SurfaceTexture} that can be used to render into the * specified hardware layer. - * * * @param layer The layer to render into using a {@link android.graphics.SurfaceTexture} * @@ -526,6 +558,13 @@ public abstract class HardwareRenderer { } /** + * Optional, sets the name of the renderer. Useful for debugging purposes. + * + * @param name The name of this renderer, can be null + */ + abstract void setName(String name); + + /** * Creates a hardware renderer using OpenGL. * * @param glVersion The version of OpenGL to use (1 for OpenGL 1, 11 for OpenGL 1.1, etc.) @@ -612,6 +651,100 @@ public abstract class HardwareRenderer { mRequested = requested; } + /** + * Describes a series of frames that should be drawn on screen as a graph. + * Each frame is composed of 1 or more elements. + */ + abstract class GraphDataProvider { + /** + * Draws the graph as bars. Frame elements are stacked on top of + * each other. + */ + public static final int GRAPH_TYPE_BARS = 0; + /** + * Draws the graph as lines. The number of series drawn corresponds + * to the number of elements. + */ + public static final int GRAPH_TYPE_LINES = 1; + + /** + * Returns the type of graph to render. + * + * @return {@link #GRAPH_TYPE_BARS} or {@link #GRAPH_TYPE_LINES} + */ + abstract int getGraphType(); + + /** + * This method is invoked before the graph is drawn. This method + * can be used to compute sizes, etc. + * + * @param metrics The display metrics + */ + abstract void prepare(DisplayMetrics metrics); + + /** + * @return The size in pixels of a vertical unit. + */ + abstract int getVerticalUnitSize(); + + /** + * @return The size in pixels of a horizontal unit. + */ + abstract int getHorizontalUnitSize(); + + /** + * @return The size in pixels of the margin between horizontal units. + */ + abstract int getHorizontaUnitMargin(); + + /** + * An optional threshold value. + * + * @return A value >= 0 to draw the threshold, a negative value + * to ignore it. + */ + abstract float getThreshold(); + + /** + * The data to draw in the graph. The number of elements in the + * array must be at least {@link #getFrameCount()} * {@link #getElementCount()}. + * If a value is negative the following values will be ignored. + */ + abstract float[] getData(); + + /** + * Returns the number of frames to render in the graph. + */ + abstract int getFrameCount(); + + /** + * Returns the number of elements in each frame. This directly affects + * the number of series drawn in the graph. + */ + abstract int getElementCount(); + + /** + * Returns the current frame, if any. If the returned value is negative + * the current frame is ignored. + */ + abstract int getCurrentFrame(); + + /** + * Prepares the paint to draw the specified element (or series.) + */ + abstract void setupGraphPaint(Paint paint, int elementIndex); + + /** + * Prepares the paint to draw the threshold. + */ + abstract void setupThresholdPaint(Paint paint); + + /** + * Prepares the paint to draw the current frame indicator. + */ + abstract void setupCurrentFramePaint(Paint paint); + } + @SuppressWarnings({"deprecation"}) static abstract class GlRenderer extends HardwareRenderer { static final int SURFACE_STATE_ERROR = 0; @@ -620,6 +753,14 @@ public abstract class HardwareRenderer { static final int FUNCTOR_PROCESS_DELAY = 4; + private static final int PROFILE_DRAW_MARGIN = 0; + private static final int PROFILE_DRAW_WIDTH = 3; + private static final int[] PROFILE_DRAW_COLORS = { 0xcf3e66cc, 0xcfdc3912, 0xcfe69800 }; + private static final int PROFILE_DRAW_CURRENT_FRAME_COLOR = 0xcf5faa4d; + private static final int PROFILE_DRAW_THRESHOLD_COLOR = 0xff5faa4d; + private static final int PROFILE_DRAW_THRESHOLD_STROKE_WIDTH = 2; + private static final int PROFILE_DRAW_DP_PER_MS = 7; + static EGL10 sEgl; static EGLDisplay sEglDisplay; static EGLConfig sEglConfig; @@ -637,6 +778,8 @@ public abstract class HardwareRenderer { GL mGl; HardwareCanvas mCanvas; + String mName; + long mFrameCount; Paint mDebugPaint; @@ -652,15 +795,18 @@ public abstract class HardwareRenderer { boolean mDirtyRegionsEnabled; boolean mUpdateDirtyRegions; - final boolean mVsyncDisabled; - - final boolean mProfileEnabled; - final float[] mProfileData; - final ReentrantLock mProfileLock; + boolean mProfileEnabled; + int mProfileVisualizerType = -1; + float[] mProfileData; + ReentrantLock mProfileLock; int mProfileCurrentFrame = -PROFILE_FRAME_DATA_COUNT; - - final boolean mDebugDirtyRegions; - final boolean mShowOverdraw; + + GraphDataProvider mDebugDataProvider; + float[][] mProfileShapes; + Paint mProfilePaint; + + boolean mDebugDirtyRegions; + boolean mShowOverdraw; final int mGlVersion; final boolean mTranslucent; @@ -675,44 +821,90 @@ public abstract class HardwareRenderer { GlRenderer(int glVersion, boolean translucent) { mGlVersion = glVersion; mTranslucent = translucent; - - String property; - property = SystemProperties.get(DISABLE_VSYNC_PROPERTY, "false"); - mVsyncDisabled = "true".equalsIgnoreCase(property); - if (mVsyncDisabled) { - Log.d(LOG_TAG, "Disabling v-sync"); + loadSystemProperties(null); + } + + private static final String[] VISUALIZERS = { + PROFILE_PROPERTY_VISUALIZE_BARS, + PROFILE_PROPERTY_VISUALIZE_LINES + }; + + @Override + boolean loadSystemProperties(Surface surface) { + boolean value; + boolean changed = false; + + String profiling = SystemProperties.get(PROFILE_PROPERTY); + int graphType = Arrays.binarySearch(VISUALIZERS, profiling); + value = graphType >= 0; + + if (graphType != mProfileVisualizerType) { + changed = true; + mProfileVisualizerType = graphType; + + mProfileShapes = null; + mProfilePaint = null; + + if (value) { + mDebugDataProvider = new DrawPerformanceDataProvider(graphType); + } else { + mDebugDataProvider = null; + } } - property = SystemProperties.get(PROFILE_PROPERTY, "false"); - mProfileEnabled = "true".equalsIgnoreCase(property); - if (mProfileEnabled) { - Log.d(LOG_TAG, "Profiling hardware renderer"); + // If on-screen profiling is not enabled, we need to check whether + // console profiling only is enabled + if (!value) { + value = Boolean.parseBoolean(profiling); } - if (mProfileEnabled) { - property = SystemProperties.get(PROFILE_MAXFRAMES_PROPERTY, - Integer.toString(PROFILE_MAX_FRAMES)); - int maxProfileFrames = Integer.valueOf(property); - mProfileData = new float[maxProfileFrames * PROFILE_FRAME_DATA_COUNT]; - for (int i = 0; i < mProfileData.length; i += PROFILE_FRAME_DATA_COUNT) { - mProfileData[i] = mProfileData[i + 1] = mProfileData[i + 2] = -1; + if (value != mProfileEnabled) { + changed = true; + mProfileEnabled = value; + + if (mProfileEnabled) { + Log.d(LOG_TAG, "Profiling hardware renderer"); + + int maxProfileFrames = SystemProperties.getInt(PROFILE_MAXFRAMES_PROPERTY, + PROFILE_MAX_FRAMES); + mProfileData = new float[maxProfileFrames * PROFILE_FRAME_DATA_COUNT]; + for (int i = 0; i < mProfileData.length; i += PROFILE_FRAME_DATA_COUNT) { + mProfileData[i] = mProfileData[i + 1] = mProfileData[i + 2] = -1; + } + + mProfileLock = new ReentrantLock(); + } else { + mProfileData = null; + mProfileLock = null; + mProfileVisualizerType = -1; } - mProfileLock = new ReentrantLock(); - } else { - mProfileData = null; - mProfileLock = null; + mProfileCurrentFrame = -PROFILE_FRAME_DATA_COUNT; } - property = SystemProperties.get(DEBUG_DIRTY_REGIONS_PROPERTY, "false"); - mDebugDirtyRegions = "true".equalsIgnoreCase(property); - if (mDebugDirtyRegions) { - Log.d(LOG_TAG, "Debugging dirty regions"); + value = SystemProperties.getBoolean(DEBUG_DIRTY_REGIONS_PROPERTY, false); + if (value != mDebugDirtyRegions) { + changed = true; + mDebugDirtyRegions = value; + + if (mDebugDirtyRegions) { + Log.d(LOG_TAG, "Debugging dirty regions"); + } } - mShowOverdraw = SystemProperties.getBoolean( + value = SystemProperties.getBoolean( HardwareRenderer.DEBUG_SHOW_OVERDRAW_PROPERTY, false); + if (value != mShowOverdraw) { + changed = true; + mShowOverdraw = value; + } + + if (nLoadProperties()) { + changed = true; + } + + return changed; } @Override @@ -795,6 +987,7 @@ public abstract class HardwareRenderer { } else { if (mCanvas == null) { mCanvas = createCanvas(); + mCanvas.setName(mName); } if (mCanvas != null) { setEnabled(true); @@ -842,19 +1035,7 @@ public abstract class HardwareRenderer { checkEglErrorsForced(); - sEglConfig = chooseEglConfig(); - if (sEglConfig == null) { - // We tried to use EGL_SWAP_BEHAVIOR_PRESERVED_BIT, try again without - if (sDirtyRegions) { - sDirtyRegions = false; - sEglConfig = chooseEglConfig(); - if (sEglConfig == null) { - throw new RuntimeException("eglConfig not initialized"); - } - } else { - throw new RuntimeException("eglConfig not initialized"); - } - } + sEglConfig = loadEglConfig(); } } @@ -868,6 +1049,23 @@ public abstract class HardwareRenderer { } } + private EGLConfig loadEglConfig() { + EGLConfig eglConfig = chooseEglConfig(); + if (eglConfig == null) { + // We tried to use EGL_SWAP_BEHAVIOR_PRESERVED_BIT, try again without + if (sDirtyRegions) { + sDirtyRegions = false; + eglConfig = chooseEglConfig(); + if (eglConfig == null) { + throw new RuntimeException("eglConfig not initialized"); + } + } else { + throw new RuntimeException("eglConfig not initialized"); + } + } + return eglConfig; + } + abstract ManagedEGLContext createManagedContext(EGLContext eglContext); private EGLConfig chooseEglConfig() { @@ -1104,6 +1302,11 @@ public abstract class HardwareRenderer { return mCanvas; } + @Override + void setName(String name) { + mName = name; + } + boolean canDraw() { return mGl != null && mCanvas != null; } @@ -1154,93 +1357,27 @@ public abstract class HardwareRenderer { mProfileLock.lock(); } - // We had to change the current surface and/or context, redraw everything - if (surfaceState == SURFACE_STATE_UPDATED) { - dirty = null; - beginFrame(null); - } else { - int[] size = mSurfaceSize; - beginFrame(size); - - if (size[1] != mHeight || size[0] != mWidth) { - mWidth = size[0]; - mHeight = size[1]; + dirty = beginFrame(canvas, dirty, surfaceState); - canvas.setViewport(mWidth, mHeight); - - dirty = null; - } - } + DisplayList displayList = buildDisplayList(view, canvas); int saveCount = 0; int status = DisplayList.STATUS_DONE; try { - view.mRecreateDisplayList = (view.mPrivateFlags & View.PFLAG_INVALIDATED) - == View.PFLAG_INVALIDATED; - view.mPrivateFlags &= ~View.PFLAG_INVALIDATED; - - long getDisplayListStartTime = 0; - if (mProfileEnabled) { - mProfileCurrentFrame += PROFILE_FRAME_DATA_COUNT; - if (mProfileCurrentFrame >= mProfileData.length) { - mProfileCurrentFrame = 0; - } + status = prepareFrame(dirty); - getDisplayListStartTime = System.nanoTime(); - } - - canvas.clearLayerUpdates(); - - DisplayList displayList; - Trace.traceBegin(Trace.TRACE_TAG_VIEW, "getDisplayList"); - try { - displayList = view.getDisplayList(); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIEW); - } - - Trace.traceBegin(Trace.TRACE_TAG_VIEW, "prepareFrame"); - try { - status = onPreDraw(dirty); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIEW); - } saveCount = canvas.save(); callbacks.onHardwarePreDraw(canvas); - if (mProfileEnabled) { - long now = System.nanoTime(); - float total = (now - getDisplayListStartTime) * 0.000001f; - //noinspection PointlessArithmeticExpression - mProfileData[mProfileCurrentFrame] = total; - } - if (displayList != null) { - long drawDisplayListStartTime = 0; - if (mProfileEnabled) { - drawDisplayListStartTime = System.nanoTime(); - } - - Trace.traceBegin(Trace.TRACE_TAG_VIEW, "drawDisplayList"); - try { - status |= canvas.drawDisplayList(displayList, mRedrawClip, - DisplayList.FLAG_CLIP_CHILDREN); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIEW); - } - - if (mProfileEnabled) { - long now = System.nanoTime(); - float total = (now - drawDisplayListStartTime) * 0.000001f; - mProfileData[mProfileCurrentFrame + 1] = total; - } - - handleFunctorStatus(attachInfo, status); + status = drawDisplayList(attachInfo, canvas, displayList, status); } else { // Shouldn't reach here view.draw(canvas); } + } catch (Exception e) { + Log.e(LOG_TAG, "An error has occurred while drawing:", e); } finally { callbacks.onHardwarePostDraw(canvas); canvas.restoreToCount(saveCount); @@ -1248,43 +1385,19 @@ public abstract class HardwareRenderer { mFrameCount++; - if (mDebugDirtyRegions) { - if (mDebugPaint == null) { - mDebugPaint = new Paint(); - mDebugPaint.setColor(0x7fff0000); - } - - if (dirty != null && (mFrameCount & 1) == 0) { - canvas.drawRect(dirty, mDebugPaint); - } - } + debugDirtyRegions(dirty, canvas); + drawProfileData(attachInfo); } onPostDraw(); - attachInfo.mIgnoreDirtyState = false; - - if ((status & DisplayList.STATUS_DREW) == DisplayList.STATUS_DREW) { - long eglSwapBuffersStartTime = 0; - if (mProfileEnabled) { - eglSwapBuffersStartTime = System.nanoTime(); - } - - sEgl.eglSwapBuffers(sEglDisplay, mEglSurface); - - if (mProfileEnabled) { - long now = System.nanoTime(); - float total = (now - eglSwapBuffersStartTime) * 0.000001f; - mProfileData[mProfileCurrentFrame + 2] = total; - } - - checkEglErrors(); - } + swapBuffers(status); if (mProfileEnabled) { mProfileLock.unlock(); } + attachInfo.mIgnoreDirtyState = false; return dirty == null; } } @@ -1292,6 +1405,139 @@ public abstract class HardwareRenderer { return false; } + private DisplayList buildDisplayList(View view, HardwareCanvas canvas) { + view.mRecreateDisplayList = (view.mPrivateFlags & View.PFLAG_INVALIDATED) + == View.PFLAG_INVALIDATED; + view.mPrivateFlags &= ~View.PFLAG_INVALIDATED; + + long buildDisplayListStartTime = startBuildDisplayListProfiling(); + canvas.clearLayerUpdates(); + + Trace.traceBegin(Trace.TRACE_TAG_VIEW, "getDisplayList"); + DisplayList displayList = view.getDisplayList(); + Trace.traceEnd(Trace.TRACE_TAG_VIEW); + + endBuildDisplayListProfiling(buildDisplayListStartTime); + + return displayList; + } + + abstract void drawProfileData(View.AttachInfo attachInfo); + + private Rect beginFrame(HardwareCanvas canvas, Rect dirty, int surfaceState) { + // We had to change the current surface and/or context, redraw everything + if (surfaceState == SURFACE_STATE_UPDATED) { + dirty = null; + beginFrame(null); + } else { + int[] size = mSurfaceSize; + beginFrame(size); + + if (size[1] != mHeight || size[0] != mWidth) { + mWidth = size[0]; + mHeight = size[1]; + + canvas.setViewport(mWidth, mHeight); + + dirty = null; + } + } + + if (mDebugDataProvider != null) dirty = null; + + return dirty; + } + + private long startBuildDisplayListProfiling() { + if (mProfileEnabled) { + mProfileCurrentFrame += PROFILE_FRAME_DATA_COUNT; + if (mProfileCurrentFrame >= mProfileData.length) { + mProfileCurrentFrame = 0; + } + + return System.nanoTime(); + } + return 0; + } + + private void endBuildDisplayListProfiling(long getDisplayListStartTime) { + if (mProfileEnabled) { + long now = System.nanoTime(); + float total = (now - getDisplayListStartTime) * 0.000001f; + //noinspection PointlessArithmeticExpression + mProfileData[mProfileCurrentFrame] = total; + } + } + + private int prepareFrame(Rect dirty) { + int status; + Trace.traceBegin(Trace.TRACE_TAG_VIEW, "prepareFrame"); + try { + status = onPreDraw(dirty); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIEW); + } + return status; + } + + private int drawDisplayList(View.AttachInfo attachInfo, HardwareCanvas canvas, + DisplayList displayList, int status) { + + long drawDisplayListStartTime = 0; + if (mProfileEnabled) { + drawDisplayListStartTime = System.nanoTime(); + } + + Trace.traceBegin(Trace.TRACE_TAG_VIEW, "drawDisplayList"); + try { + status |= canvas.drawDisplayList(displayList, mRedrawClip, + DisplayList.FLAG_CLIP_CHILDREN); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIEW); + } + + if (mProfileEnabled) { + long now = System.nanoTime(); + float total = (now - drawDisplayListStartTime) * 0.000001f; + mProfileData[mProfileCurrentFrame + 1] = total; + } + + handleFunctorStatus(attachInfo, status); + return status; + } + + private void swapBuffers(int status) { + if ((status & DisplayList.STATUS_DREW) == DisplayList.STATUS_DREW) { + long eglSwapBuffersStartTime = 0; + if (mProfileEnabled) { + eglSwapBuffersStartTime = System.nanoTime(); + } + + sEgl.eglSwapBuffers(sEglDisplay, mEglSurface); + + if (mProfileEnabled) { + long now = System.nanoTime(); + float total = (now - eglSwapBuffersStartTime) * 0.000001f; + mProfileData[mProfileCurrentFrame + 2] = total; + } + + checkEglErrors(); + } + } + + private void debugDirtyRegions(Rect dirty, HardwareCanvas canvas) { + if (mDebugDirtyRegions) { + if (mDebugPaint == null) { + mDebugPaint = new Paint(); + mDebugPaint.setColor(0x7fff0000); + } + + if (dirty != null && (mFrameCount & 1) == 0) { + canvas.drawRect(dirty, mDebugPaint); + } + } + } + private void handleFunctorStatus(View.AttachInfo attachInfo, int status) { // If the draw flag is set, functors will be invoked while executing // the tree of display lists @@ -1362,6 +1608,96 @@ public abstract class HardwareRenderer { } return SURFACE_STATE_SUCCESS; } + + private static int dpToPx(int dp, float density) { + return (int) (dp * density + 0.5f); + } + + class DrawPerformanceDataProvider extends GraphDataProvider { + private final int mGraphType; + + private int mVerticalUnit; + private int mHorizontalUnit; + private int mHorizontalMargin; + private int mThresholdStroke; + + DrawPerformanceDataProvider(int graphType) { + mGraphType = graphType; + } + + @Override + void prepare(DisplayMetrics metrics) { + final float density = metrics.density; + + mVerticalUnit = dpToPx(PROFILE_DRAW_DP_PER_MS, density); + mHorizontalUnit = dpToPx(PROFILE_DRAW_WIDTH, density); + mHorizontalMargin = dpToPx(PROFILE_DRAW_MARGIN, density); + mThresholdStroke = dpToPx(PROFILE_DRAW_THRESHOLD_STROKE_WIDTH, density); + } + + @Override + int getGraphType() { + return mGraphType; + } + + @Override + int getVerticalUnitSize() { + return mVerticalUnit; + } + + @Override + int getHorizontalUnitSize() { + return mHorizontalUnit; + } + + @Override + int getHorizontaUnitMargin() { + return mHorizontalMargin; + } + + @Override + float[] getData() { + return mProfileData; + } + + @Override + float getThreshold() { + return 16; + } + + @Override + int getFrameCount() { + return mProfileData.length / PROFILE_FRAME_DATA_COUNT; + } + + @Override + int getElementCount() { + return PROFILE_FRAME_DATA_COUNT; + } + + @Override + int getCurrentFrame() { + return mProfileCurrentFrame / PROFILE_FRAME_DATA_COUNT; + } + + @Override + void setupGraphPaint(Paint paint, int elementIndex) { + paint.setColor(PROFILE_DRAW_COLORS[elementIndex]); + if (mGraphType == GRAPH_TYPE_LINES) paint.setStrokeWidth(mThresholdStroke); + } + + @Override + void setupThresholdPaint(Paint paint) { + paint.setColor(PROFILE_DRAW_THRESHOLD_COLOR); + paint.setStrokeWidth(mThresholdStroke); + } + + @Override + void setupCurrentFramePaint(Paint paint) { + paint.setColor(PROFILE_DRAW_CURRENT_FRAME_COLOR); + if (mGraphType == GRAPH_TYPE_LINES) paint.setStrokeWidth(mThresholdStroke); + } + } } /** @@ -1370,6 +1706,8 @@ public abstract class HardwareRenderer { static class Gl20Renderer extends GlRenderer { private GLES20Canvas mGlCanvas; + private DisplayMetrics mDisplayMetrics; + private static EGLSurface sPbuffer; private static final Object[] sPbufferLock = new Object[0]; @@ -1436,6 +1774,10 @@ public abstract class HardwareRenderer { @Override int[] getConfig(boolean dirtyRegions) { + //noinspection PointlessBooleanExpression,ConstantConditions + final int stencilSize = GLES20Canvas.getStencilSize(); + final int swapBehavior = dirtyRegions ? EGL14.EGL_SWAP_BEHAVIOR_PRESERVED_BIT : 0; + return new int[] { EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, EGL_RED_SIZE, 8, @@ -1444,14 +1786,12 @@ public abstract class HardwareRenderer { EGL_ALPHA_SIZE, 8, EGL_DEPTH_SIZE, 0, EGL_CONFIG_CAVEAT, EGL_NONE, - // TODO: Find a better way to choose the stencil size - EGL_STENCIL_SIZE, mShowOverdraw ? GLES20Canvas.getStencilSize() : 0, - EGL_SURFACE_TYPE, EGL_WINDOW_BIT | - (dirtyRegions ? EGL14.EGL_SWAP_BEHAVIOR_PRESERVED_BIT : 0), + EGL_STENCIL_SIZE, stencilSize, + EGL_SURFACE_TYPE, EGL_WINDOW_BIT | swapBehavior, EGL_NONE }; } - + @Override void initCaches() { GLES20Canvas.initCaches(); @@ -1473,6 +1813,153 @@ public abstract class HardwareRenderer { } @Override + void drawProfileData(View.AttachInfo attachInfo) { + if (mDebugDataProvider != null) { + final GraphDataProvider provider = mDebugDataProvider; + initProfileDrawData(attachInfo, provider); + + final int height = provider.getVerticalUnitSize(); + final int margin = provider.getHorizontaUnitMargin(); + final int width = provider.getHorizontalUnitSize(); + + int x = 0; + int count = 0; + int current = 0; + + final float[] data = provider.getData(); + final int elementCount = provider.getElementCount(); + final int graphType = provider.getGraphType(); + + int totalCount = provider.getFrameCount() * elementCount; + if (graphType == GraphDataProvider.GRAPH_TYPE_LINES) { + totalCount -= elementCount; + } + + for (int i = 0; i < totalCount; i += elementCount) { + if (data[i] < 0.0f) break; + + int index = count * 4; + if (i == provider.getCurrentFrame() * elementCount) current = index; + + x += margin; + int x2 = x + width; + + int y2 = mHeight; + int y1 = (int) (y2 - data[i] * height); + + switch (graphType) { + case GraphDataProvider.GRAPH_TYPE_BARS: { + for (int j = 0; j < elementCount; j++) { + //noinspection MismatchedReadAndWriteOfArray + final float[] r = mProfileShapes[j]; + r[index] = x; + r[index + 1] = y1; + r[index + 2] = x2; + r[index + 3] = y2; + + y2 = y1; + if (j < elementCount - 1) { + y1 = (int) (y2 - data[i + j + 1] * height); + } + } + } break; + case GraphDataProvider.GRAPH_TYPE_LINES: { + for (int j = 0; j < elementCount; j++) { + //noinspection MismatchedReadAndWriteOfArray + final float[] r = mProfileShapes[j]; + r[index] = (x + x2) * 0.5f; + r[index + 1] = index == 0 ? y1 : r[index - 1]; + r[index + 2] = r[index] + width; + r[index + 3] = y1; + + y2 = y1; + if (j < elementCount - 1) { + y1 = (int) (y2 - data[i + j + 1] * height); + } + } + } break; + } + + + x += width; + count++; + } + + x += margin; + + drawGraph(graphType, count); + drawCurrentFrame(graphType, current); + drawThreshold(x, height); + } + } + + private void drawGraph(int graphType, int count) { + for (int i = 0; i < mProfileShapes.length; i++) { + mDebugDataProvider.setupGraphPaint(mProfilePaint, i); + switch (graphType) { + case GraphDataProvider.GRAPH_TYPE_BARS: + mGlCanvas.drawRects(mProfileShapes[i], count * 4, mProfilePaint); + break; + case GraphDataProvider.GRAPH_TYPE_LINES: + mGlCanvas.drawLines(mProfileShapes[i], 0, count * 4, mProfilePaint); + break; + } + } + } + + private void drawCurrentFrame(int graphType, int index) { + if (index >= 0) { + mDebugDataProvider.setupCurrentFramePaint(mProfilePaint); + switch (graphType) { + case GraphDataProvider.GRAPH_TYPE_BARS: + mGlCanvas.drawRect(mProfileShapes[2][index], mProfileShapes[2][index + 1], + mProfileShapes[2][index + 2], mProfileShapes[0][index + 3], + mProfilePaint); + break; + case GraphDataProvider.GRAPH_TYPE_LINES: + mGlCanvas.drawLine(mProfileShapes[2][index], mProfileShapes[2][index + 1], + mProfileShapes[2][index], mHeight, mProfilePaint); + break; + } + } + } + + private void drawThreshold(int x, int height) { + float threshold = mDebugDataProvider.getThreshold(); + if (threshold > 0.0f) { + mDebugDataProvider.setupThresholdPaint(mProfilePaint); + int y = (int) (mHeight - threshold * height); + mGlCanvas.drawLine(0.0f, y, x, y, mProfilePaint); + } + } + + private void initProfileDrawData(View.AttachInfo attachInfo, GraphDataProvider provider) { + if (mProfileShapes == null) { + final int elementCount = provider.getElementCount(); + final int frameCount = provider.getFrameCount(); + + mProfileShapes = new float[elementCount][]; + for (int i = 0; i < elementCount; i++) { + mProfileShapes[i] = new float[frameCount * 4]; + } + + mProfilePaint = new Paint(); + } + + mProfilePaint.reset(); + if (provider.getGraphType() == GraphDataProvider.GRAPH_TYPE_LINES) { + mProfilePaint.setAntiAlias(true); + } + + if (mDisplayMetrics == null) { + mDisplayMetrics = new DisplayMetrics(); + } + + attachInfo.mDisplay.getMetrics(mDisplayMetrics); + provider.prepare(mDisplayMetrics); + } + + @Override void destroy(boolean full) { try { super.destroy(full); @@ -1484,14 +1971,6 @@ public abstract class HardwareRenderer { } @Override - void setup(int width, int height) { - super.setup(width, height); - if (mVsyncDisabled) { - disableVsync(); - } - } - - @Override void pushLayerUpdate(HardwareLayer layer) { mGlCanvas.pushLayerUpdate(layer); } @@ -1507,12 +1986,12 @@ public abstract class HardwareRenderer { } @Override - HardwareLayer createHardwareLayer(int width, int height, boolean isOpaque) { + public HardwareLayer createHardwareLayer(int width, int height, boolean isOpaque) { return new GLES20RenderLayer(width, height, isOpaque); } @Override - SurfaceTexture createSurfaceTexture(HardwareLayer layer) { + public SurfaceTexture createSurfaceTexture(HardwareLayer layer) { return ((GLES20TextureLayer) layer).getSurfaceTexture(); } diff --git a/core/java/android/view/IDisplayContentChangeListener.aidl b/core/java/android/view/IMagnificationCallbacks.aidl index ef7edea..032d073 100644 --- a/core/java/android/view/IDisplayContentChangeListener.aidl +++ b/core/java/android/view/IMagnificationCallbacks.aidl @@ -1,7 +1,7 @@ /* ** Copyright 2012, The Android Open Source Project ** -** Licensed under the Apache License, Version 2.0 (the "License") +** Licensed under the Apache License, Version 2.0 (the "License"); ** you may not use this file except in compliance with the License. ** You may obtain a copy of the License at ** @@ -16,18 +16,14 @@ package android.view; -import android.os.IBinder; -import android.view.WindowInfo; -import android.graphics.Rect; +import android.graphics.Region; /** - * Interface for observing content changes on a display. - * * {@hide} */ -oneway interface IDisplayContentChangeListener { - void onWindowTransition(int displayId, int transition, in WindowInfo info); - void onRectangleOnScreenRequested(int displayId, in Rect rectangle, boolean immediate); - void onWindowLayersChanged(int displayId); +oneway interface IMagnificationCallbacks { + void onMagnifedBoundsChanged(in Region bounds); + void onRectangleOnScreenRequested(int left, int top, int right, int bottom); void onRotationChanged(int rotation); + void onUserContextChanged(); } diff --git a/core/java/android/view/IWindow.aidl b/core/java/android/view/IWindow.aidl index 15bd46c..8ec07ef 100644 --- a/core/java/android/view/IWindow.aidl +++ b/core/java/android/view/IWindow.aidl @@ -45,7 +45,7 @@ oneway interface IWindow { */ void executeCommand(String command, String parameters, in ParcelFileDescriptor descriptor); - void resized(in Rect frame, in Rect contentInsets, + void resized(in Rect frame, in Rect overscanInsets, in Rect contentInsets, in Rect visibleInsets, boolean reportDraw, in Configuration newConfig); void moved(int newX, int newY); void dispatchAppVisibility(boolean visible); diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index 2b6cbcf..f0c6241 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -27,17 +27,17 @@ import android.graphics.Rect; import android.os.Bundle; import android.os.IRemoteCallback; import android.view.IApplicationToken; -import android.view.IDisplayContentChangeListener; +import android.view.IMagnificationCallbacks; import android.view.IOnKeyguardExitResult; import android.view.IRotationWatcher; import android.view.IWindowSession; import android.view.KeyEvent; import android.view.InputEvent; +import android.view.MagnificationSpec; import android.view.MotionEvent; import android.view.InputChannel; import android.view.InputDevice; import android.view.IInputFilter; -import android.view.WindowInfo; /** * System private interface to the window manager. @@ -65,6 +65,8 @@ interface IWindowManager void setForcedDisplayDensity(int displayId, int density); void clearForcedDisplayDensity(int displayId); + void setOverscan(int displayId, int left, int top, int right, int bottom); + // Is the device configured to have a full system bar for larger screens? boolean hasSystemNavBar(); @@ -74,7 +76,7 @@ interface IWindowManager void setEventDispatching(boolean enabled); void addWindowToken(IBinder token, int type); void removeWindowToken(IBinder token); - void addAppToken(int addPos, int userId, IApplicationToken token, + void addAppToken(int addPos, IApplicationToken token, int groupId, int requestedOrientation, boolean fullscreen, boolean showWhenLocked); void setAppGroupId(IBinder token, int groupId); void setAppOrientation(IApplicationToken token, int requestedOrientation); @@ -190,6 +192,13 @@ interface IWindowManager void thawRotation(); /** + * Gets whether the rotation is frozen. + * + * @return Whether the rotation is frozen. + */ + boolean isRotationFrozen(); + + /** * Create a screenshot of the applications currently displayed. */ Bitmap screenshotApplications(IBinder appToken, int displayId, int maxWidth, int maxHeight); @@ -221,48 +230,49 @@ interface IWindowManager IBinder getFocusedWindowToken(); /** - * Gets the compatibility scale of e window given its token. - */ - float getWindowCompatibilityScale(IBinder windowToken); - - /** * Sets an input filter for manipulating the input event stream. */ void setInputFilter(in IInputFilter filter); /** - * Sets the scale and offset for implementing accessibility magnification. - */ - void magnifyDisplay(int dipslayId, float scale, float offsetX, float offsetY); - - /** - * Adds a listener for display content changes. + * Gets the frame of a window given its token. */ - void addDisplayContentChangeListener(int displayId, IDisplayContentChangeListener listener); + void getWindowFrame(IBinder token, out Rect outFrame); /** - * Removes a listener for display content changes. + * Device is in safe mode. */ - void removeDisplayContentChangeListener(int displayId, IDisplayContentChangeListener listener); + boolean isSafeModeEnabled(); /** - * Gets the info for a window given its token. + * Tell keyguard to show the assistant (Intent.ACTION_ASSIST) after asking for the user's + * credentials. */ - WindowInfo getWindowInfo(IBinder token); + void showAssistant(); /** - * Gets the infos for all visible windows. + * Sets the display magnification callbacks. These callbacks notify + * the client for contextual changes related to display magnification. + * + * @param callbacks The magnification callbacks. */ - void getVisibleWindowsForDisplay(int displayId, out List<WindowInfo> outInfos); + void setMagnificationCallbacks(IMagnificationCallbacks callbacks); /** - * Device is in safe mode. + * Sets the magnification spec to be applied to all windows that can be + * magnified. + * + * @param spec The current magnification spec. */ - boolean isSafeModeEnabled(); + void setMagnificationSpec(in MagnificationSpec spec); /** - * Tell keyguard to show the assistant (Intent.ACTION_ASSIST) after asking for the user's - * credentials. + * Gets the magnification spec for a window given its token. If the + * window has a compatibility scale it is also folded in the returned + * magnification spec. + * + * @param windowToken The unique window token. + * @return The magnification spec if such or null. */ - void showAssistant(); + MagnificationSpec getCompatibleMagnificationSpecForWindow(in IBinder windowToken); } diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl index ff9dcce..0a8e609 100644 --- a/core/java/android/view/IWindowSession.aidl +++ b/core/java/android/view/IWindowSession.aidl @@ -63,6 +63,9 @@ interface IWindowSession { * {@link WindowManagerGlobal#RELAYOUT_DEFER_SURFACE_DESTROY}. * @param outFrame Rect in which is placed the new position/size on * screen. + * @param outOverscanInsets Rect in which is placed the offsets from + * <var>outFrame</var> in which the content of the window are inside + * of the display's overlay region. * @param outContentInsets Rect in which is placed the offsets from * <var>outFrame</var> in which the content of the window should be * placed. This can be used to modify the window layout to ensure its @@ -84,7 +87,7 @@ interface IWindowSession { */ int relayout(IWindow window, int seq, in WindowManager.LayoutParams attrs, int requestedWidth, int requestedHeight, int viewVisibility, - int flags, out Rect outFrame, + int flags, out Rect outFrame, out Rect outOverscanInsets, out Rect outContentInsets, out Rect outVisibleInsets, out Configuration outConfig, out Surface outSurface); diff --git a/core/java/android/view/KeyCharacterMap.java b/core/java/android/view/KeyCharacterMap.java index 2cb724f..9e5f25a 100644 --- a/core/java/android/view/KeyCharacterMap.java +++ b/core/java/android/view/KeyCharacterMap.java @@ -180,6 +180,8 @@ public class KeyCharacterMap implements Parcelable { private static final int ACCENT_CIRCUMFLEX_LEGACY = '^'; private static final int ACCENT_TILDE_LEGACY = '~'; + private static final int CHAR_SPACE = ' '; + /** * Maps Unicode combining diacritical to display-form dead key. */ @@ -473,14 +475,23 @@ public class KeyCharacterMap implements Parcelable { } /** - * Get the character that is produced by putting accent on the character c. + * Get the character that is produced by combining the dead key producing accent + * with the key producing character c. * For example, getDeadChar('`', 'e') returns è. + * getDeadChar('^', ' ') returns '^' and getDeadChar('^', '^') returns '^'. * * @param accent The accent character. eg. '`' * @param c The basic character. * @return The combined character, or 0 if the characters cannot be combined. */ public static int getDeadChar(int accent, int c) { + if (c == accent || CHAR_SPACE == c) { + // The same dead character typed twice or a dead character followed by a + // space should both produce the non-combining version of the combining char. + // In this case we don't even need to compute the combining character. + return accent; + } + int combining = sAccentToCombining.get(accent); if (combining == 0) { return 0; @@ -495,7 +506,8 @@ public class KeyCharacterMap implements Parcelable { sDeadKeyBuilder.append((char)c); sDeadKeyBuilder.append((char)combining); String result = Normalizer.normalize(sDeadKeyBuilder, Normalizer.Form.NFC); - combined = result.length() == 1 ? result.charAt(0) : 0; + combined = result.codePointCount(0, result.length()) == 1 + ? result.codePointAt(0) : 0; sDeadKeyCache.put(combination, combined); } } diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java index c2a3e58..bb533bf 100644 --- a/core/java/android/view/KeyEvent.java +++ b/core/java/android/view/KeyEvent.java @@ -302,27 +302,27 @@ public class KeyEvent extends InputEvent implements Parcelable { public static final int KEYCODE_SWITCH_CHARSET = 95; // switch char-sets (Kanji,Katakana) /** Key code constant: A Button key. * On a game controller, the A button should be either the button labeled A - * or the first button on the upper row of controller buttons. */ + * or the first button on the bottom row of controller buttons. */ public static final int KEYCODE_BUTTON_A = 96; /** Key code constant: B Button key. * On a game controller, the B button should be either the button labeled B - * or the second button on the upper row of controller buttons. */ + * or the second button on the bottom row of controller buttons. */ public static final int KEYCODE_BUTTON_B = 97; /** Key code constant: C Button key. * On a game controller, the C button should be either the button labeled C - * or the third button on the upper row of controller buttons. */ + * or the third button on the bottom row of controller buttons. */ public static final int KEYCODE_BUTTON_C = 98; /** Key code constant: X Button key. * On a game controller, the X button should be either the button labeled X - * or the first button on the lower row of controller buttons. */ + * or the first button on the upper row of controller buttons. */ public static final int KEYCODE_BUTTON_X = 99; /** Key code constant: Y Button key. * On a game controller, the Y button should be either the button labeled Y - * or the second button on the lower row of controller buttons. */ + * or the second button on the upper row of controller buttons. */ public static final int KEYCODE_BUTTON_Y = 100; /** Key code constant: Z Button key. * On a game controller, the Z button should be either the button labeled Z - * or the third button on the lower row of controller buttons. */ + * or the third button on the upper row of controller buttons. */ public static final int KEYCODE_BUTTON_Z = 101; /** Key code constant: L1 Button key. * On a game controller, the L1 button should be either the button labeled L1 (or L) @@ -623,8 +623,14 @@ public class KeyEvent extends InputEvent implements Parcelable { /** Key code constant: Assist key. * Launches the global assist activity. Not delivered to applications. */ public static final int KEYCODE_ASSIST = 219; + /** Key code constant: Brightness Down key. + * Adjusts the screen brightness down. */ + public static final int KEYCODE_BRIGHTNESS_DOWN = 220; + /** Key code constant: Brightness Up key. + * Adjusts the screen brightness up. */ + public static final int KEYCODE_BRIGHTNESS_UP = 221; - private static final int LAST_KEYCODE = KEYCODE_ASSIST; + private static final int LAST_KEYCODE = KEYCODE_BRIGHTNESS_UP; // NOTE: If you add a new keycode here you must also add it to: // isSystem() @@ -866,6 +872,8 @@ public class KeyEvent extends InputEvent implements Parcelable { names.append(KEYCODE_RO, "KEYCODE_RO"); names.append(KEYCODE_KANA, "KEYCODE_KANA"); names.append(KEYCODE_ASSIST, "KEYCODE_ASSIST"); + names.append(KEYCODE_BRIGHTNESS_DOWN, "KEYCODE_BRIGHTNESS_DOWN"); + names.append(KEYCODE_BRIGHTNESS_UP, "KEYCODE_BRIGHTNESS_UP"); }; // Symbolic names of all metakeys in bit order from least significant to most significant. diff --git a/core/java/android/view/WindowInfo.aidl b/core/java/android/view/MagnificationSpec.aidl index 23e927a..d5fbdef 100644 --- a/core/java/android/view/WindowInfo.aidl +++ b/core/java/android/view/MagnificationSpec.aidl @@ -17,4 +17,4 @@ package android.view; -parcelable WindowInfo; +parcelable MagnificationSpec; diff --git a/core/java/android/view/MagnificationSpec.java b/core/java/android/view/MagnificationSpec.java new file mode 100644 index 0000000..0ee6714 --- /dev/null +++ b/core/java/android/view/MagnificationSpec.java @@ -0,0 +1,123 @@ +/* + * 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.os.Parcel; +import android.os.Parcelable; +import android.util.Pools.SynchronizedPool; + +/** + * This class represents spec for performing screen magnification. + * + * @hide + */ +public class MagnificationSpec implements Parcelable { + private static final int MAX_POOL_SIZE = 20; + private static final SynchronizedPool<MagnificationSpec> sPool = + new SynchronizedPool<MagnificationSpec>(MAX_POOL_SIZE); + + public float scale = 1.0f; + public float offsetX; + public float offsetY; + + private MagnificationSpec() { + /* do nothing - reducing visibility */ + } + + public void initialize(float scale, float offsetX, float offsetY) { + if (scale < 1) { + throw new IllegalArgumentException("Scale must be greater than or equal to one!"); + } + this.scale = scale; + this.offsetX = offsetX; + this.offsetY = offsetY; + } + + public boolean isNop() { + return scale == 1.0f && offsetX == 0 && offsetY == 0; + } + + public static MagnificationSpec obtain(MagnificationSpec other) { + MagnificationSpec info = obtain(); + info.scale = other.scale; + info.offsetX = other.offsetX; + info.offsetY = other.offsetY; + return info; + } + + public static MagnificationSpec obtain() { + MagnificationSpec spec = sPool.acquire(); + return (spec != null) ? spec : new MagnificationSpec(); + } + + public void recycle() { + clear(); + sPool.release(this); + } + + public void clear() { + scale = 1.0f; + offsetX = 0.0f; + offsetY = 0.0f; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeFloat(scale); + parcel.writeFloat(offsetX); + parcel.writeFloat(offsetY); + recycle(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("<scale:"); + builder.append(scale); + builder.append(",offsetX:"); + builder.append(offsetX); + builder.append(",offsetY:"); + builder.append(offsetY); + builder.append(">"); + return builder.toString(); + } + + private void initFromParcel(Parcel parcel) { + scale = parcel.readFloat(); + offsetX = parcel.readFloat(); + offsetY = parcel.readFloat(); + } + + public static final Creator<MagnificationSpec> CREATOR = new Creator<MagnificationSpec>() { + @Override + public MagnificationSpec[] newArray(int size) { + return new MagnificationSpec[size]; + } + + @Override + public MagnificationSpec createFromParcel(Parcel parcel) { + MagnificationSpec spec = MagnificationSpec.obtain(); + spec.initFromParcel(parcel); + return spec; + } + }; +} diff --git a/core/java/android/view/SimulatedDpad.java b/core/java/android/view/SimulatedDpad.java index b03e4c7..883fd49 100644 --- a/core/java/android/view/SimulatedDpad.java +++ b/core/java/android/view/SimulatedDpad.java @@ -186,7 +186,7 @@ class SimulatedDpad { Intent intent = ((SearchManager)mContext.getSystemService(Context.SEARCH_SERVICE)) - .getAssistIntent(mContext, UserHandle.USER_CURRENT_OR_SELF); + .getAssistIntent(mContext, false, UserHandle.USER_CURRENT_OR_SELF); if (intent != null) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); try { diff --git a/core/java/android/view/Surface.java b/core/java/android/view/Surface.java index 0a81a71..03a9b09 100644 --- a/core/java/android/view/Surface.java +++ b/core/java/android/view/Surface.java @@ -16,20 +16,15 @@ package android.view; -import dalvik.system.CloseGuard; - import android.content.res.CompatibilityInfo.Translator; -import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Rect; -import android.graphics.Region; import android.graphics.SurfaceTexture; -import android.os.IBinder; -import android.os.Parcelable; import android.os.Parcel; -import android.os.SystemProperties; +import android.os.Parcelable; import android.util.Log; +import dalvik.system.CloseGuard; /** * Handle onto a raw buffer that is being managed by the screen compositor. @@ -37,8 +32,19 @@ import android.util.Log; public class Surface implements Parcelable { private static final String TAG = "Surface"; - private static final boolean HEADLESS = "1".equals( - SystemProperties.get("ro.config.headless", "0")); + private static native int nativeCreateFromSurfaceTexture(SurfaceTexture surfaceTexture) + throws OutOfResourcesException; + + private native Canvas nativeLockCanvas(int nativeObject, Rect dirty); + private native void nativeUnlockCanvasAndPost(int nativeObject, Canvas canvas); + + private static native void nativeRelease(int nativeObject); + private static native void nativeDestroy(int nativeObject); + private static native boolean nativeIsValid(int nativeObject); + private static native boolean nativeIsConsumerRunningBehind(int nativeObject); + private static native int nativeCopyFrom(int nativeObject, int surfaceControlNativeObject); + private static native int nativeReadFromParcel(int nativeObject, Parcel source); + private static native void nativeWriteToParcel(int nativeObject, Parcel dest); public static final Parcelable.Creator<Surface> CREATOR = new Parcelable.Creator<Surface>() { @@ -52,157 +58,11 @@ public class Surface implements Parcelable { return null; } } - public Surface[] newArray(int size) { return new Surface[size]; } }; - /** - * Rotation constant: 0 degree rotation (natural orientation) - */ - public static final int ROTATION_0 = 0; - - /** - * Rotation constant: 90 degree rotation. - */ - public static final int ROTATION_90 = 1; - - /** - * Rotation constant: 180 degree rotation. - */ - public static final int ROTATION_180 = 2; - - /** - * Rotation constant: 270 degree rotation. - */ - public static final int ROTATION_270 = 3; - - /* built-in physical display ids (keep in sync with ISurfaceComposer.h) - * these are different from the logical display ids used elsewhere in the framework */ - - /** - * Built-in physical display id: Main display. - * Use only with {@link #getBuiltInDisplay()}. - * @hide - */ - public static final int BUILT_IN_DISPLAY_ID_MAIN = 0; - - /** - * Built-in physical display id: Attached HDMI display. - * Use only with {@link #getBuiltInDisplay()}. - * @hide - */ - public static final int BUILT_IN_DISPLAY_ID_HDMI = 1; - - /* flags used in constructor (keep in sync with ISurfaceComposerClient.h) */ - - /** - * Surface creation flag: Surface is created hidden - * @hide */ - public static final int HIDDEN = 0x00000004; - - /** - * Surface creation flag: The surface contains secure content, special - * measures will be taken to disallow the surface's content to be copied - * from another process. In particular, screenshots and VNC servers will - * be disabled, but other measures can take place, for instance the - * surface might not be hardware accelerated. - * @hide - */ - public static final int SECURE = 0x00000080; - - /** - * Surface creation flag: Creates a surface where color components are interpreted - * as "non pre-multiplied" by their alpha channel. Of course this flag is - * meaningless for surfaces without an alpha channel. By default - * surfaces are pre-multiplied, which means that each color component is - * already multiplied by its alpha value. In this case the blending - * equation used is: - * - * DEST = SRC + DEST * (1-SRC_ALPHA) - * - * By contrast, non pre-multiplied surfaces use the following equation: - * - * DEST = SRC * SRC_ALPHA * DEST * (1-SRC_ALPHA) - * - * pre-multiplied surfaces must always be used if transparent pixels are - * composited on top of each-other into the surface. A pre-multiplied - * surface can never lower the value of the alpha component of a given - * pixel. - * - * In some rare situations, a non pre-multiplied surface is preferable. - * @hide - */ - public static final int NON_PREMULTIPLIED = 0x00000100; - - /** - * Surface creation flag: Indicates that the surface must be considered opaque, - * even if its pixel format is set to translucent. This can be useful if an - * application needs full RGBA 8888 support for instance but will - * still draw every pixel opaque. - * @hide - */ - public static final int OPAQUE = 0x00000400; - - /** - * Surface creation flag: Application requires a hardware-protected path to an - * external display sink. If a hardware-protected path is not available, - * then this surface will not be displayed on the external sink. - * @hide - */ - public static final int PROTECTED_APP = 0x00000800; - - // 0x1000 is reserved for an independent DRM protected flag in framework - - /** - * Surface creation flag: Creates a normal surface. - * This is the default. - * @hide - */ - public static final int FX_SURFACE_NORMAL = 0x00000000; - - /** - * Surface creation flag: Creates a Blur surface. - * Everything behind this surface is blurred by some amount. - * The quality and refresh speed of the blur effect is not settable or guaranteed. - * It is an error to lock a Blur surface, since it doesn't have a backing store. - * @hide - * @deprecated - */ - @Deprecated - public static final int FX_SURFACE_BLUR = 0x00010000; - - /** - * Surface creation flag: Creates a Dim surface. - * Everything behind this surface is dimmed by the amount specified - * in {@link #setAlpha}. It is an error to lock a Dim surface, since it - * doesn't have a backing store. - * @hide - */ - public static final int FX_SURFACE_DIM = 0x00020000; - - /** - * @hide - */ - public static final int FX_SURFACE_SCREENSHOT = 0x00030000; - - /** - * Mask used for FX values above. - * @hide - */ - public static final int FX_SURFACE_MASK = 0x000F0000; - - /* flags used with setFlags() (keep in sync with ISurfaceComposer.h) */ - - /** - * Surface flag: Hide the surface. - * Equivalent to calling hide(). - * @hide - */ - public static final int SURFACE_HIDDEN = 0x01; - - private final CloseGuard mCloseGuard = CloseGuard.get(); private String mName; @@ -211,11 +71,10 @@ public class Surface implements Parcelable { // server or system processes. When this class is parceled we defer to the // mSurfaceControl to do the parceling. Otherwise we parcel the // mNativeSurface. - private int mNativeSurface; // Surface* - private int mNativeSurfaceControl; // SurfaceControl* + int mNativeObject; // package scope only for SurfaceControl access + private int mGenerationId; // incremented each time mNativeSurface changes private final Canvas mCanvas = new CompatibleCanvas(); - private int mCanvasSaveCount; // Canvas save count at time of lockCanvas() // The Translator for density compatibility mode. This is used for scaling // the canvas to perform the appropriate density transformation. @@ -225,118 +84,34 @@ public class Surface implements Parcelable { // non compatibility mode. private Matrix mCompatibleMatrix; - private int mWidth; - private int mHeight; - - private native void nativeCreate(SurfaceSession session, String name, - int w, int h, int format, int flags) - throws OutOfResourcesException; - private native void nativeCreateFromSurfaceTexture(SurfaceTexture surfaceTexture) - throws OutOfResourcesException; - private native void nativeRelease(); - private native void nativeDestroy(); - - private native boolean nativeIsValid(); - private native int nativeGetIdentity(); - private native boolean nativeIsConsumerRunningBehind(); - - private native Canvas nativeLockCanvas(Rect dirty); - private native void nativeUnlockCanvasAndPost(Canvas canvas); - - private static native Bitmap nativeScreenshot(IBinder displayToken, - int width, int height, int minLayer, int maxLayer, boolean allLayers); - - private static native void nativeOpenTransaction(); - private static native void nativeCloseTransaction(); - private static native void nativeSetAnimationTransaction(); - - private native void nativeSetLayer(int zorder); - private native void nativeSetPosition(float x, float y); - private native void nativeSetSize(int w, int h); - private native void nativeSetTransparentRegionHint(Region region); - private native void nativeSetAlpha(float alpha); - private native void nativeSetMatrix(float dsdx, float dtdx, float dsdy, float dtdy); - private native void nativeSetFlags(int flags, int mask); - private native void nativeSetWindowCrop(Rect crop); - private native void nativeSetLayerStack(int layerStack); - - private static native IBinder nativeGetBuiltInDisplay(int physicalDisplayId); - private static native IBinder nativeCreateDisplay(String name, boolean secure); - private static native void nativeSetDisplaySurface( - IBinder displayToken, Surface surface); - private static native void nativeSetDisplayLayerStack( - IBinder displayToken, int layerStack); - private static native void nativeSetDisplayProjection( - IBinder displayToken, int orientation, Rect layerStackRect, Rect displayRect); - private static native boolean nativeGetDisplayInfo( - IBinder displayToken, PhysicalDisplayInfo outInfo); - private static native void nativeBlankDisplay(IBinder displayToken); - private static native void nativeUnblankDisplay(IBinder displayToken); - - private native void nativeCopyFrom(Surface other); - private native void nativeTransferFrom(Surface other); - private native void nativeReadFromParcel(Parcel source); - private native void nativeWriteToParcel(Parcel dest); - /** - * Create an empty surface, which will later be filled in by readFromParcel(). - * @hide + * Rotation constant: 0 degree rotation (natural orientation) */ - public Surface() { - checkHeadless(); + public static final int ROTATION_0 = 0; - mCloseGuard.open("release"); - } + /** + * Rotation constant: 90 degree rotation. + */ + public static final int ROTATION_90 = 1; /** - * Create a surface with a name. - * - * The surface creation flags specify what kind of surface to create and - * certain options such as whether the surface can be assumed to be opaque - * and whether it should be initially hidden. Surfaces should always be - * created with the {@link #HIDDEN} flag set to ensure that they are not - * made visible prematurely before all of the surface's properties have been - * configured. - * - * Good practice is to first create the surface with the {@link #HIDDEN} flag - * specified, open a transaction, set the surface layer, layer stack, alpha, - * and position, call {@link #show} if appropriate, and close the transaction. - * - * @param session The surface session, must not be null. - * @param name The surface name, must not be null. - * @param w The surface initial width. - * @param h The surface initial height. - * @param flags The surface creation flags. Should always include {@link #HIDDEN} - * in the creation flags. - * @hide + * Rotation constant: 180 degree rotation. */ - public Surface(SurfaceSession session, - String name, int w, int h, int format, int flags) - throws OutOfResourcesException { - if (session == null) { - throw new IllegalArgumentException("session must not be null"); - } - if (name == null) { - throw new IllegalArgumentException("name must not be null"); - } + public static final int ROTATION_180 = 2; - if ((flags & HIDDEN) == 0) { - Log.w(TAG, "Surfaces should always be created with the HIDDEN flag set " - + "to ensure that they are not made visible prematurely before " - + "all of the surface's properties have been configured. " - + "Set the other properties and make the surface visible within " - + "a transaction. New surface name: " + name, - new Throwable()); - } + /** + * Rotation constant: 270 degree rotation. + */ + public static final int ROTATION_270 = 3; - checkHeadless(); - mName = name; - mWidth = w; - mHeight = h; - nativeCreate(session, name, w, h, format, flags); + /** + * Create an empty surface, which will later be filled in by readFromParcel(). + * @hide + */ + public Surface() { mCloseGuard.open("release"); } @@ -355,11 +130,9 @@ public class Surface implements Parcelable { throw new IllegalArgumentException("surfaceTexture must not be null"); } - checkHeadless(); - mName = surfaceTexture.toString(); try { - nativeCreateFromSurfaceTexture(surfaceTexture); + mNativeObject = nativeCreateFromSurfaceTexture(surfaceTexture); } catch (OutOfResourcesException ex) { // We can't throw OutOfResourcesException because it would be an API change. throw new RuntimeException(ex); @@ -368,13 +141,20 @@ public class Surface implements Parcelable { mCloseGuard.open("release"); } + private Surface(int nativeObject) { + mNativeObject = nativeObject; + mCloseGuard.open("release"); + } + @Override protected void finalize() throws Throwable { try { if (mCloseGuard != null) { mCloseGuard.warnIfOpen(); } - nativeRelease(); + if (mNativeObject != 0) { + nativeRelease(mNativeObject); + } } finally { super.finalize(); } @@ -386,7 +166,10 @@ public class Surface implements Parcelable { * This will make the surface invalid. */ public void release() { - nativeRelease(); + if (mNativeObject != 0) { + nativeRelease(mNativeObject); + mNativeObject = 0; + } mCloseGuard.close(); } @@ -397,7 +180,10 @@ public class Surface implements Parcelable { * @hide */ public void destroy() { - nativeDestroy(); + if (mNativeObject != 0) { + nativeDestroy(mNativeObject); + mNativeObject = 0; + } mCloseGuard.close(); } @@ -408,7 +194,8 @@ public class Surface implements Parcelable { * Otherwise returns false. */ public boolean isValid() { - return nativeIsValid(); + if (mNativeObject == 0) return false; + return nativeIsValid(mNativeObject); } /** @@ -429,7 +216,8 @@ public class Surface implements Parcelable { * @hide */ public boolean isConsumerRunningBehind() { - return nativeIsConsumerRunningBehind(); + checkNotReleased(); + return nativeIsConsumerRunningBehind(mNativeObject); } /** @@ -438,7 +226,7 @@ public class Surface implements Parcelable { * After drawing into the provided {@link Canvas}, the caller should * invoke {@link #unlockCanvasAndPost} to post the new contents to the surface. * - * @param dirty A rectangle that represents the dirty region that the caller wants + * @param inOutDirty A rectangle that represents the dirty region that the caller wants * to redraw. This function may choose to expand the dirty rectangle if for example * the surface has been resized or if the previous contents of the surface were * not available. The caller should redraw the entire dirty region as represented @@ -447,9 +235,10 @@ public class Surface implements Parcelable { * entire surface should be redrawn. * @return A canvas for drawing into the surface. */ - public Canvas lockCanvas(Rect dirty) + public Canvas lockCanvas(Rect inOutDirty) throws OutOfResourcesException, IllegalArgumentException { - return nativeLockCanvas(dirty); + checkNotReleased(); + return nativeLockCanvas(mNativeObject, inOutDirty); } /** @@ -459,7 +248,8 @@ public class Surface implements Parcelable { * @param canvas The canvas previously obtained from {@link #lockCanvas}. */ public void unlockCanvasAndPost(Canvas canvas) { - nativeUnlockCanvasAndPost(canvas); + checkNotReleased(); + nativeUnlockCanvasAndPost(mNativeObject, canvas); } /** @@ -482,202 +272,6 @@ public class Surface implements Parcelable { } } - /** - * Like {@link #screenshot(int, int, int, int)} but includes all - * Surfaces in the screenshot. - * - * @hide - */ - public static Bitmap screenshot(int width, int height) { - // TODO: should take the display as a parameter - IBinder displayToken = getBuiltInDisplay(BUILT_IN_DISPLAY_ID_MAIN); - return nativeScreenshot(displayToken, width, height, 0, 0, true); - } - - /** - * Copy the current screen contents into a bitmap and return it. - * - * @param width The desired width of the returned bitmap; the raw - * screen will be scaled down to this size. - * @param height The desired height of the returned bitmap; the raw - * screen will be scaled down to this size. - * @param minLayer The lowest (bottom-most Z order) surface layer to - * include in the screenshot. - * @param maxLayer The highest (top-most Z order) surface layer to - * include in the screenshot. - * @return Returns a Bitmap containing the screen contents, or null - * if an error occurs. - * - * @hide - */ - public static Bitmap screenshot(int width, int height, int minLayer, int maxLayer) { - // TODO: should take the display as a parameter - IBinder displayToken = getBuiltInDisplay(BUILT_IN_DISPLAY_ID_MAIN); - return nativeScreenshot(displayToken, width, height, minLayer, maxLayer, false); - } - - /* - * set surface parameters. - * needs to be inside open/closeTransaction block - */ - - /** start a transaction @hide */ - public static void openTransaction() { - nativeOpenTransaction(); - } - - /** end a transaction @hide */ - public static void closeTransaction() { - nativeCloseTransaction(); - } - - /** flag the transaction as an animation @hide */ - public static void setAnimationTransaction() { - nativeSetAnimationTransaction(); - } - - /** @hide */ - public void setLayer(int zorder) { - nativeSetLayer(zorder); - } - - /** @hide */ - public void setPosition(int x, int y) { - nativeSetPosition(x, y); - } - - /** @hide */ - public void setPosition(float x, float y) { - nativeSetPosition(x, y); - } - - /** @hide */ - public void setSize(int w, int h) { - mWidth = w; - mHeight = h; - nativeSetSize(w, h); - } - - /** @hide */ - public int getWidth() { - return mWidth; - } - - /** @hide */ - public int getHeight() { - return mHeight; - } - - /** @hide */ - public void hide() { - nativeSetFlags(SURFACE_HIDDEN, SURFACE_HIDDEN); - } - - /** @hide */ - public void show() { - nativeSetFlags(0, SURFACE_HIDDEN); - } - - /** @hide */ - public void setTransparentRegionHint(Region region) { - nativeSetTransparentRegionHint(region); - } - - /** @hide */ - public void setAlpha(float alpha) { - nativeSetAlpha(alpha); - } - - /** @hide */ - public void setMatrix(float dsdx, float dtdx, float dsdy, float dtdy) { - nativeSetMatrix(dsdx, dtdx, dsdy, dtdy); - } - - /** @hide */ - public void setFlags(int flags, int mask) { - nativeSetFlags(flags, mask); - } - - /** @hide */ - public void setWindowCrop(Rect crop) { - nativeSetWindowCrop(crop); - } - - /** @hide */ - public void setLayerStack(int layerStack) { - nativeSetLayerStack(layerStack); - } - - /** @hide */ - public static IBinder getBuiltInDisplay(int builtInDisplayId) { - return nativeGetBuiltInDisplay(builtInDisplayId); - } - - /** @hide */ - public static IBinder createDisplay(String name, boolean secure) { - if (name == null) { - throw new IllegalArgumentException("name must not be null"); - } - return nativeCreateDisplay(name, secure); - } - - /** @hide */ - public static void setDisplaySurface(IBinder displayToken, Surface surface) { - if (displayToken == null) { - throw new IllegalArgumentException("displayToken must not be null"); - } - nativeSetDisplaySurface(displayToken, surface); - } - - /** @hide */ - public static void setDisplayLayerStack(IBinder displayToken, int layerStack) { - if (displayToken == null) { - throw new IllegalArgumentException("displayToken must not be null"); - } - nativeSetDisplayLayerStack(displayToken, layerStack); - } - - /** @hide */ - public static void setDisplayProjection(IBinder displayToken, - int orientation, Rect layerStackRect, Rect displayRect) { - if (displayToken == null) { - throw new IllegalArgumentException("displayToken must not be null"); - } - if (layerStackRect == null) { - throw new IllegalArgumentException("layerStackRect must not be null"); - } - if (displayRect == null) { - throw new IllegalArgumentException("displayRect must not be null"); - } - nativeSetDisplayProjection(displayToken, orientation, layerStackRect, displayRect); - } - - /** @hide */ - public static boolean getDisplayInfo(IBinder displayToken, PhysicalDisplayInfo outInfo) { - if (displayToken == null) { - throw new IllegalArgumentException("displayToken must not be null"); - } - if (outInfo == null) { - throw new IllegalArgumentException("outInfo must not be null"); - } - return nativeGetDisplayInfo(displayToken, outInfo); - } - - /** @hide */ - public static void blankDisplay(IBinder displayToken) { - if (displayToken == null) { - throw new IllegalArgumentException("displayToken must not be null"); - } - nativeBlankDisplay(displayToken); - } - - /** @hide */ - public static void unblankDisplay(IBinder displayToken) { - if (displayToken == null) { - throw new IllegalArgumentException("displayToken must not be null"); - } - nativeUnblankDisplay(displayToken); - } /** * Copy another surface to this one. This surface now holds a reference @@ -688,13 +282,15 @@ public class Surface implements Parcelable { * in to it. * @hide */ - public void copyFrom(Surface other) { + public void copyFrom(SurfaceControl other) { if (other == null) { throw new IllegalArgumentException("other must not be null"); } - if (other != this) { - nativeCopyFrom(other); + if (other.mNativeObject == 0) { + throw new NullPointerException( + "SurfaceControl native object is null. Are you using a released SurfaceControl?"); } + mNativeObject = nativeCopyFrom(mNativeObject, other.mNativeObject); } /** @@ -710,7 +306,13 @@ public class Surface implements Parcelable { throw new IllegalArgumentException("other must not be null"); } if (other != this) { - nativeTransferFrom(other); + if (mNativeObject != 0) { + // release our reference to our native object + nativeRelease(mNativeObject); + } + // transfer the reference from other to us + mNativeObject = other.mNativeObject; + other.mNativeObject = 0; } } @@ -723,9 +325,8 @@ public class Surface implements Parcelable { if (source == null) { throw new IllegalArgumentException("source must not be null"); } - mName = source.readString(); - nativeReadFromParcel(source); + mNativeObject = nativeReadFromParcel(mNativeObject, source); } @Override @@ -733,9 +334,8 @@ public class Surface implements Parcelable { if (dest == null) { throw new IllegalArgumentException("dest must not be null"); } - dest.writeString(mName); - nativeWriteToParcel(dest); + nativeWriteToParcel(mNativeObject, dest); if ((flags & Parcelable.PARCELABLE_WRITE_RETURN_VALUE) != 0) { release(); } @@ -743,13 +343,7 @@ public class Surface implements Parcelable { @Override public String toString() { - return "Surface(name=" + mName + ", identity=" + nativeGetIdentity() + ")"; - } - - private static void checkHeadless() { - if (HEADLESS) { - throw new UnsupportedOperationException("Device is headless"); - } + return "Surface(name=" + mName + ")"; } /** @@ -758,69 +352,36 @@ public class Surface implements Parcelable { public static class OutOfResourcesException extends Exception { public OutOfResourcesException() { } - public OutOfResourcesException(String name) { super(name); } } /** - * Describes the properties of a physical display known to surface flinger. + * Returns a human readable representation of a rotation. + * + * @param rotation The rotation. + * @return The rotation symbolic name. + * * @hide */ - public static final class PhysicalDisplayInfo { - public int width; - public int height; - public float refreshRate; - public float density; - public float xDpi; - public float yDpi; - public boolean secure; - - public PhysicalDisplayInfo() { - } - - public PhysicalDisplayInfo(PhysicalDisplayInfo other) { - copyFrom(other); - } - - @Override - public boolean equals(Object o) { - return o instanceof PhysicalDisplayInfo && equals((PhysicalDisplayInfo)o); - } - - public boolean equals(PhysicalDisplayInfo other) { - return other != null - && width == other.width - && height == other.height - && refreshRate == other.refreshRate - && density == other.density - && xDpi == other.xDpi - && yDpi == other.yDpi - && secure == other.secure; - } - - @Override - public int hashCode() { - return 0; // don't care - } - - public void copyFrom(PhysicalDisplayInfo other) { - width = other.width; - height = other.height; - refreshRate = other.refreshRate; - density = other.density; - xDpi = other.xDpi; - yDpi = other.yDpi; - secure = other.secure; - } - - // For debugging purposes - @Override - public String toString() { - return "PhysicalDisplayInfo{" + width + " x " + height + ", " + refreshRate + " fps, " - + "density " + density + ", " + xDpi + " x " + yDpi + " dpi, secure " + secure - + "}"; + public static String rotationToString(int rotation) { + switch (rotation) { + case Surface.ROTATION_0: { + return "ROTATION_0"; + } + case Surface.ROTATION_90: { + return "ROATATION_90"; + } + case Surface.ROTATION_180: { + return "ROATATION_180"; + } + case Surface.ROTATION_270: { + return "ROATATION_270"; + } + default: { + throw new IllegalArgumentException("Invalid rotation: " + rotation); + } } } @@ -883,4 +444,9 @@ public class Surface implements Parcelable { mOrigMatrix.set(m); } } + + private void checkNotReleased() { + if (mNativeObject == 0) throw new NullPointerException( + "mNativeObject is null. Have you called release() already?"); + } } diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java new file mode 100644 index 0000000..ded2f47 --- /dev/null +++ b/core/java/android/view/SurfaceControl.java @@ -0,0 +1,565 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import dalvik.system.CloseGuard; +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.Region; +import android.os.IBinder; +import android.os.SystemProperties; +import android.util.Log; + +/** + * SurfaceControl + * @hide + */ +public class SurfaceControl { + private static final String TAG = "SurfaceControl"; + + private static native int nativeCreate(SurfaceSession session, String name, + int w, int h, int format, int flags) + throws OutOfResourcesException; + private static native void nativeRelease(int nativeObject); + private static native void nativeDestroy(int nativeObject); + + private static native Bitmap nativeScreenshot(IBinder displayToken, + int width, int height, int minLayer, int maxLayer, boolean allLayers); + + private static native void nativeOpenTransaction(); + private static native void nativeCloseTransaction(); + private static native void nativeSetAnimationTransaction(); + + private static native void nativeSetLayer(int nativeObject, int zorder); + private static native void nativeSetPosition(int nativeObject, float x, float y); + private static native void nativeSetSize(int nativeObject, int w, int h); + private static native void nativeSetTransparentRegionHint(int nativeObject, Region region); + private static native void nativeSetAlpha(int nativeObject, float alpha); + private static native void nativeSetMatrix(int nativeObject, float dsdx, float dtdx, float dsdy, float dtdy); + private static native void nativeSetFlags(int nativeObject, int flags, int mask); + private static native void nativeSetWindowCrop(int nativeObject, int l, int t, int r, int b); + private static native void nativeSetLayerStack(int nativeObject, int layerStack); + + private static native IBinder nativeGetBuiltInDisplay(int physicalDisplayId); + private static native IBinder nativeCreateDisplay(String name, boolean secure); + private static native void nativeSetDisplaySurface( + IBinder displayToken, int nativeSurfaceObject); + private static native void nativeSetDisplayLayerStack( + IBinder displayToken, int layerStack); + private static native void nativeSetDisplayProjection( + IBinder displayToken, int orientation, + int l, int t, int r, int b, + int L, int T, int R, int B); + private static native boolean nativeGetDisplayInfo( + IBinder displayToken, SurfaceControl.PhysicalDisplayInfo outInfo); + private static native void nativeBlankDisplay(IBinder displayToken); + private static native void nativeUnblankDisplay(IBinder displayToken); + + + private final CloseGuard mCloseGuard = CloseGuard.get(); + private String mName; + int mNativeObject; // package visibility only for Surface.java access + + private static final boolean HEADLESS = "1".equals( + SystemProperties.get("ro.config.headless", "0")); + + /** + * Exception thrown when a surface couldn't be created or resized. + */ + public static class OutOfResourcesException extends Exception { + public OutOfResourcesException() { + } + public OutOfResourcesException(String name) { + super(name); + } + } + + /* flags used in constructor (keep in sync with ISurfaceComposerClient.h) */ + + /** + * Surface creation flag: Surface is created hidden + */ + public static final int HIDDEN = 0x00000004; + + /** + * Surface creation flag: The surface contains secure content, special + * measures will be taken to disallow the surface's content to be copied + * from another process. In particular, screenshots and VNC servers will + * be disabled, but other measures can take place, for instance the + * surface might not be hardware accelerated. + * + */ + public static final int SECURE = 0x00000080; + + /** + * Surface creation flag: Creates a surface where color components are interpreted + * as "non pre-multiplied" by their alpha channel. Of course this flag is + * meaningless for surfaces without an alpha channel. By default + * surfaces are pre-multiplied, which means that each color component is + * already multiplied by its alpha value. In this case the blending + * equation used is: + * + * DEST = SRC + DEST * (1-SRC_ALPHA) + * + * By contrast, non pre-multiplied surfaces use the following equation: + * + * DEST = SRC * SRC_ALPHA * DEST * (1-SRC_ALPHA) + * + * pre-multiplied surfaces must always be used if transparent pixels are + * composited on top of each-other into the surface. A pre-multiplied + * surface can never lower the value of the alpha component of a given + * pixel. + * + * In some rare situations, a non pre-multiplied surface is preferable. + * + */ + public static final int NON_PREMULTIPLIED = 0x00000100; + + /** + * Surface creation flag: Indicates that the surface must be considered opaque, + * even if its pixel format is set to translucent. This can be useful if an + * application needs full RGBA 8888 support for instance but will + * still draw every pixel opaque. + * + */ + public static final int OPAQUE = 0x00000400; + + /** + * Surface creation flag: Application requires a hardware-protected path to an + * external display sink. If a hardware-protected path is not available, + * then this surface will not be displayed on the external sink. + * + */ + public static final int PROTECTED_APP = 0x00000800; + + // 0x1000 is reserved for an independent DRM protected flag in framework + + /** + * Surface creation flag: Creates a normal surface. + * This is the default. + * + */ + public static final int FX_SURFACE_NORMAL = 0x00000000; + + /** + * Surface creation flag: Creates a Blur surface. + * Everything behind this surface is blurred by some amount. + * The quality and refresh speed of the blur effect is not settable or guaranteed. + * It is an error to lock a Blur surface, since it doesn't have a backing store. + * + * @deprecated + */ + @Deprecated + public static final int FX_SURFACE_BLUR = 0x00010000; + + /** + * Surface creation flag: Creates a Dim surface. + * Everything behind this surface is dimmed by the amount specified + * in {@link #setAlpha}. It is an error to lock a Dim surface, since it + * doesn't have a backing store. + * + */ + public static final int FX_SURFACE_DIM = 0x00020000; + + /** + * + */ + public static final int FX_SURFACE_SCREENSHOT = 0x00030000; + + /** + * Mask used for FX values above. + * + */ + public static final int FX_SURFACE_MASK = 0x000F0000; + + /* flags used with setFlags() (keep in sync with ISurfaceComposer.h) */ + + /** + * Surface flag: Hide the surface. + * Equivalent to calling hide(). + */ + public static final int SURFACE_HIDDEN = 0x01; + + + /* built-in physical display ids (keep in sync with ISurfaceComposer.h) + * these are different from the logical display ids used elsewhere in the framework */ + + /** + * Built-in physical display id: Main display. + * Use only with {@link SurfaceControl#getBuiltInDisplay()}. + */ + public static final int BUILT_IN_DISPLAY_ID_MAIN = 0; + + /** + * Built-in physical display id: Attached HDMI display. + * Use only with {@link SurfaceControl#getBuiltInDisplay()}. + */ + public static final int BUILT_IN_DISPLAY_ID_HDMI = 1; + + + + /** + * Create a surface with a name. + * + * The surface creation flags specify what kind of surface to create and + * certain options such as whether the surface can be assumed to be opaque + * and whether it should be initially hidden. Surfaces should always be + * created with the {@link #HIDDEN} flag set to ensure that they are not + * made visible prematurely before all of the surface's properties have been + * configured. + * + * Good practice is to first create the surface with the {@link #HIDDEN} flag + * specified, open a transaction, set the surface layer, layer stack, alpha, + * and position, call {@link #show} if appropriate, and close the transaction. + * + * @param session The surface session, must not be null. + * @param name The surface name, must not be null. + * @param w The surface initial width. + * @param h The surface initial height. + * @param flags The surface creation flags. Should always include {@link #HIDDEN} + * in the creation flags. + */ + public SurfaceControl(SurfaceSession session, + String name, int w, int h, int format, int flags) + throws OutOfResourcesException { + if (session == null) { + throw new IllegalArgumentException("session must not be null"); + } + if (name == null) { + throw new IllegalArgumentException("name must not be null"); + } + + if ((flags & SurfaceControl.HIDDEN) == 0) { + Log.w(TAG, "Surfaces should always be created with the HIDDEN flag set " + + "to ensure that they are not made visible prematurely before " + + "all of the surface's properties have been configured. " + + "Set the other properties and make the surface visible within " + + "a transaction. New surface name: " + name, + new Throwable()); + } + + checkHeadless(); + + mName = name; + mNativeObject = nativeCreate(session, name, w, h, format, flags); + if (mNativeObject == 0) { + throw new OutOfResourcesException( + "Couldn't allocate SurfaceControl native object"); + } + + mCloseGuard.open("release"); + } + + @Override + protected void finalize() throws Throwable { + try { + if (mCloseGuard != null) { + mCloseGuard.warnIfOpen(); + } + if (mNativeObject != 0) { + nativeRelease(mNativeObject); + } + } finally { + super.finalize(); + } + } + + @Override + public String toString() { + return "Surface(name=" + mName + ")"; + } + + /** + * Release the local reference to the server-side surface. + * Always call release() when you're done with a Surface. + * This will make the surface invalid. + */ + public void release() { + if (mNativeObject != 0) { + nativeRelease(mNativeObject); + mNativeObject = 0; + } + mCloseGuard.close(); + } + + /** + * Free all server-side state associated with this surface and + * release this object's reference. This method can only be + * called from the process that created the service. + */ + public void destroy() { + if (mNativeObject != 0) { + nativeDestroy(mNativeObject); + mNativeObject = 0; + } + mCloseGuard.close(); + } + + private void checkNotReleased() { + if (mNativeObject == 0) throw new NullPointerException( + "mNativeObject is null. Have you called release() already?"); + } + + /* + * set surface parameters. + * needs to be inside open/closeTransaction block + */ + + /** start a transaction */ + public static void openTransaction() { + nativeOpenTransaction(); + } + + /** end a transaction */ + public static void closeTransaction() { + nativeCloseTransaction(); + } + + /** flag the transaction as an animation */ + public static void setAnimationTransaction() { + nativeSetAnimationTransaction(); + } + + public void setLayer(int zorder) { + checkNotReleased(); + nativeSetLayer(mNativeObject, zorder); + } + + public void setPosition(float x, float y) { + checkNotReleased(); + nativeSetPosition(mNativeObject, x, y); + } + + public void setSize(int w, int h) { + checkNotReleased(); + nativeSetSize(mNativeObject, w, h); + } + + public void hide() { + checkNotReleased(); + nativeSetFlags(mNativeObject, SURFACE_HIDDEN, SURFACE_HIDDEN); + } + + public void show() { + checkNotReleased(); + nativeSetFlags(mNativeObject, 0, SURFACE_HIDDEN); + } + + public void setTransparentRegionHint(Region region) { + checkNotReleased(); + nativeSetTransparentRegionHint(mNativeObject, region); + } + + public void setAlpha(float alpha) { + checkNotReleased(); + nativeSetAlpha(mNativeObject, alpha); + } + + public void setMatrix(float dsdx, float dtdx, float dsdy, float dtdy) { + checkNotReleased(); + nativeSetMatrix(mNativeObject, dsdx, dtdx, dsdy, dtdy); + } + + public void setFlags(int flags, int mask) { + checkNotReleased(); + nativeSetFlags(mNativeObject, flags, mask); + } + + public void setWindowCrop(Rect crop) { + checkNotReleased(); + if (crop != null) { + nativeSetWindowCrop(mNativeObject, + crop.left, crop.top, crop.right, crop.bottom); + } else { + nativeSetWindowCrop(mNativeObject, 0, 0, 0, 0); + } + } + + public void setLayerStack(int layerStack) { + checkNotReleased(); + nativeSetLayerStack(mNativeObject, layerStack); + } + + /* + * set display parameters. + * needs to be inside open/closeTransaction block + */ + + /** + * Describes the properties of a physical display known to surface flinger. + */ + public static final class PhysicalDisplayInfo { + public int width; + public int height; + public float refreshRate; + public float density; + public float xDpi; + public float yDpi; + public boolean secure; + + public PhysicalDisplayInfo() { + } + + public PhysicalDisplayInfo(PhysicalDisplayInfo other) { + copyFrom(other); + } + + @Override + public boolean equals(Object o) { + return o instanceof PhysicalDisplayInfo && equals((PhysicalDisplayInfo)o); + } + + public boolean equals(PhysicalDisplayInfo other) { + return other != null + && width == other.width + && height == other.height + && refreshRate == other.refreshRate + && density == other.density + && xDpi == other.xDpi + && yDpi == other.yDpi + && secure == other.secure; + } + + @Override + public int hashCode() { + return 0; // don't care + } + + public void copyFrom(PhysicalDisplayInfo other) { + width = other.width; + height = other.height; + refreshRate = other.refreshRate; + density = other.density; + xDpi = other.xDpi; + yDpi = other.yDpi; + secure = other.secure; + } + + // For debugging purposes + @Override + public String toString() { + return "PhysicalDisplayInfo{" + width + " x " + height + ", " + refreshRate + " fps, " + + "density " + density + ", " + xDpi + " x " + yDpi + " dpi, secure " + secure + + "}"; + } + } + + public static void unblankDisplay(IBinder displayToken) { + if (displayToken == null) { + throw new IllegalArgumentException("displayToken must not be null"); + } + nativeUnblankDisplay(displayToken); + } + + public static void blankDisplay(IBinder displayToken) { + if (displayToken == null) { + throw new IllegalArgumentException("displayToken must not be null"); + } + nativeBlankDisplay(displayToken); + } + + public static boolean getDisplayInfo(IBinder displayToken, SurfaceControl.PhysicalDisplayInfo outInfo) { + if (displayToken == null) { + throw new IllegalArgumentException("displayToken must not be null"); + } + if (outInfo == null) { + throw new IllegalArgumentException("outInfo must not be null"); + } + return nativeGetDisplayInfo(displayToken, outInfo); + } + + public static void setDisplayProjection(IBinder displayToken, + int orientation, Rect layerStackRect, Rect displayRect) { + if (displayToken == null) { + throw new IllegalArgumentException("displayToken must not be null"); + } + if (layerStackRect == null) { + throw new IllegalArgumentException("layerStackRect must not be null"); + } + if (displayRect == null) { + throw new IllegalArgumentException("displayRect must not be null"); + } + nativeSetDisplayProjection(displayToken, orientation, + layerStackRect.left, layerStackRect.top, layerStackRect.right, layerStackRect.bottom, + displayRect.left, displayRect.top, displayRect.right, displayRect.bottom); + } + + public static void setDisplayLayerStack(IBinder displayToken, int layerStack) { + if (displayToken == null) { + throw new IllegalArgumentException("displayToken must not be null"); + } + nativeSetDisplayLayerStack(displayToken, layerStack); + } + + public static void setDisplaySurface(IBinder displayToken, Surface surface) { + if (displayToken == null) { + throw new IllegalArgumentException("displayToken must not be null"); + } + int nativeSurface = surface != null ? surface.mNativeObject : 0; + nativeSetDisplaySurface(displayToken, nativeSurface); + } + + public static IBinder createDisplay(String name, boolean secure) { + if (name == null) { + throw new IllegalArgumentException("name must not be null"); + } + return nativeCreateDisplay(name, secure); + } + + public static IBinder getBuiltInDisplay(int builtInDisplayId) { + return nativeGetBuiltInDisplay(builtInDisplayId); + } + + + /** + * Copy the current screen contents into a bitmap and return it. + * + * @param width The desired width of the returned bitmap; the raw + * screen will be scaled down to this size. + * @param height The desired height of the returned bitmap; the raw + * screen will be scaled down to this size. + * @param minLayer The lowest (bottom-most Z order) surface layer to + * include in the screenshot. + * @param maxLayer The highest (top-most Z order) surface layer to + * include in the screenshot. + * @return Returns a Bitmap containing the screen contents, or null + * if an error occurs. + * + */ + public static Bitmap screenshot(int width, int height, int minLayer, int maxLayer) { + // TODO: should take the display as a parameter + IBinder displayToken = SurfaceControl.getBuiltInDisplay(SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN); + return nativeScreenshot(displayToken, width, height, minLayer, maxLayer, false); + } + + /** + * Like {@link SurfaceControl#screenshot(int, int, int, int)} but includes all + * Surfaces in the screenshot. + * + */ + public static Bitmap screenshot(int width, int height) { + // TODO: should take the display as a parameter + IBinder displayToken = SurfaceControl.getBuiltInDisplay(SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN); + return nativeScreenshot(displayToken, width, height, 0, 0, true); + } + + private static void checkHeadless() { + if (HEADLESS) { + throw new UnsupportedOperationException("Device is headless"); + } + } +} diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java index 9008521..5d0f523 100644 --- a/core/java/android/view/SurfaceView.java +++ b/core/java/android/view/SurfaceView.java @@ -103,6 +103,7 @@ public class SurfaceView extends View { MyWindow mWindow; final Rect mVisibleInsets = new Rect(); final Rect mWinFrame = new Rect(); + final Rect mOverscanInsets = new Rect(); final Rect mContentInsets = new Rect(); final Configuration mConfiguration = new Configuration(); @@ -507,7 +508,7 @@ public class SurfaceView extends View { mWindow, mWindow.mSeq, mLayout, mWidth, mHeight, visible ? VISIBLE : GONE, WindowManagerGlobal.RELAYOUT_DEFER_SURFACE_DESTROY, - mWinFrame, mContentInsets, + mWinFrame, mOverscanInsets, mContentInsets, mVisibleInsets, mConfiguration, mNewSurface); if ((relayoutResult & WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0) { mReportDrawNeeded = true; @@ -642,7 +643,7 @@ public class SurfaceView extends View { } @Override - public void resized(Rect frame, Rect contentInsets, + public void resized(Rect frame, Rect overscanInsets, Rect contentInsets, Rect visibleInsets, boolean reportDraw, Configuration newConfig) { SurfaceView surfaceView = mSurfaceView.get(); if (surfaceView != null) { diff --git a/core/java/android/view/VelocityTracker.java b/core/java/android/view/VelocityTracker.java index 82b3963..eb81f72 100644 --- a/core/java/android/view/VelocityTracker.java +++ b/core/java/android/view/VelocityTracker.java @@ -16,10 +16,7 @@ package android.view; -import android.util.Poolable; -import android.util.Pool; -import android.util.Pools; -import android.util.PoolableManager; +import android.util.Pools.SynchronizedPool; /** * Helper for tracking the velocity of touch events, for implementing @@ -31,30 +28,15 @@ import android.util.PoolableManager; * {@link #computeCurrentVelocity(int)} and then call {@link #getXVelocity(int)} * and {@link #getYVelocity(int)} to retrieve the velocity for each pointer id. */ -public final class VelocityTracker implements Poolable<VelocityTracker> { - private static final Pool<VelocityTracker> sPool = Pools.synchronizedPool( - Pools.finitePool(new PoolableManager<VelocityTracker>() { - public VelocityTracker newInstance() { - return new VelocityTracker(null); - } - - public void onAcquired(VelocityTracker element) { - // Intentionally empty - } - - public void onReleased(VelocityTracker element) { - element.clear(); - } - }, 2)); +public final class VelocityTracker { + private static final SynchronizedPool<VelocityTracker> sPool = + new SynchronizedPool<VelocityTracker>(2); private static final int ACTIVE_POINTER_ID = -1; private int mPtr; private final String mStrategy; - private VelocityTracker mNext; - private boolean mIsPooled; - private static native int nativeInitialize(String strategy); private static native void nativeDispose(int ptr); private static native void nativeClear(int ptr); @@ -73,7 +55,8 @@ public final class VelocityTracker implements Poolable<VelocityTracker> { * @return Returns a new VelocityTracker. */ static public VelocityTracker obtain() { - return sPool.acquire(); + VelocityTracker instance = sPool.acquire(); + return (instance != null) ? instance : new VelocityTracker(null); } /** @@ -98,38 +81,11 @@ public final class VelocityTracker implements Poolable<VelocityTracker> { */ public void recycle() { if (mStrategy == null) { + clear(); sPool.release(this); } } - /** - * @hide - */ - public void setNextPoolable(VelocityTracker element) { - mNext = element; - } - - /** - * @hide - */ - public VelocityTracker getNextPoolable() { - return mNext; - } - - /** - * @hide - */ - public boolean isPooled() { - return mIsPooled; - } - - /** - * @hide - */ - public void setPooled(boolean isPooled) { - mIsPooled = isPooled; - } - private VelocityTracker(String strategy) { mPtr = nativeInitialize(strategy); mStrategy = strategy; diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 0d2141f..ab8f934 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -40,6 +40,7 @@ import android.graphics.Shader; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.hardware.display.DisplayManagerGlobal; +import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; @@ -52,10 +53,7 @@ import android.text.TextUtils; import android.util.AttributeSet; import android.util.FloatProperty; import android.util.Log; -import android.util.Pool; -import android.util.Poolable; -import android.util.PoolableManager; -import android.util.Pools; +import android.util.Pools.SynchronizedPool; import android.util.Property; import android.util.SparseArray; import android.util.TypedValue; @@ -691,6 +689,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public static final int NO_ID = -1; + private static boolean sUseBrokenMakeMeasureSpec = false; + /** * This view does not want keystrokes. Use with TAKES_FOCUS_MASK when * calling setFlags. @@ -1561,9 +1561,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ int mAccessibilityViewId = NO_ID; - /** - * @hide - */ private int mAccessibilityCursorPosition = ACCESSIBILITY_CURSOR_POSITION_UNDEFINED; /** @@ -1870,6 +1867,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private static final int LAYOUT_DIRECTION_DEFAULT = LAYOUT_DIRECTION_INHERIT; /** + * Default horizontal layout direction. + * @hide + */ + static final int LAYOUT_DIRECTION_RESOLVED_DEFAULT = LAYOUT_DIRECTION_LTR; + + /** * Indicates that the view is tracking some sort of transient state * that the app should not need to be aware of, but that the framework * should take special care to preserve. @@ -1918,6 +1921,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private static final int TEXT_DIRECTION_DEFAULT = TEXT_DIRECTION_INHERIT; /** + * Default resolved text direction + * @hide + */ + static final int TEXT_DIRECTION_RESOLVED_DEFAULT = TEXT_DIRECTION_FIRST_STRONG; + + /** * Bit shift to get the horizontal layout direction. (bits after LAYOUT_DIRECTION_RESOLVED) * @hide */ @@ -1969,7 +1978,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @hide */ static final int PFLAG2_TEXT_DIRECTION_RESOLVED_DEFAULT = - TEXT_DIRECTION_FIRST_STRONG << PFLAG2_TEXT_DIRECTION_RESOLVED_MASK_SHIFT; + TEXT_DIRECTION_RESOLVED_DEFAULT << PFLAG2_TEXT_DIRECTION_RESOLVED_MASK_SHIFT; /* * Default text alignment. The text alignment of this View is inherited from its parent. @@ -2028,6 +2037,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private static final int TEXT_ALIGNMENT_DEFAULT = TEXT_ALIGNMENT_GRAVITY; /** + * Default resolved text alignment + * @hide + */ + static final int TEXT_ALIGNMENT_RESOLVED_DEFAULT = TEXT_ALIGNMENT_GRAVITY; + + /** * Bit shift to get the horizontal layout direction. (bits after DRAG_HOVERED) * @hide */ @@ -2077,7 +2092,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * Indicates whether if the view text alignment has been resolved to gravity */ private static final int PFLAG2_TEXT_ALIGNMENT_RESOLVED_DEFAULT = - TEXT_ALIGNMENT_GRAVITY << PFLAG2_TEXT_ALIGNMENT_RESOLVED_MASK_SHIFT; + TEXT_ALIGNMENT_RESOLVED_DEFAULT << PFLAG2_TEXT_ALIGNMENT_RESOLVED_MASK_SHIFT; // Accessiblity constants for mPrivateFlags2 @@ -2515,8 +2530,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, /** * The undefined cursor position. + * + * @hide */ - private static final int ACCESSIBILITY_CURSOR_POSITION_UNDEFINED = -1; + public static final int ACCESSIBILITY_CURSOR_POSITION_UNDEFINED = -1; /** * Indicates that the screen has changed state and is now off. @@ -3237,6 +3254,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, setOverScrollMode(OVER_SCROLL_IF_CONTENT_SCROLLS); mUserPaddingStart = UNDEFINED_PADDING; mUserPaddingEnd = UNDEFINED_PADDING; + + if (!sUseBrokenMakeMeasureSpec && context.getApplicationInfo().targetSdkVersion <= + Build.VERSION_CODES.JELLY_BEAN_MR1 ) { + // Older apps may need this compatibility hack for measurement. + sUseBrokenMakeMeasureSpec = true; + } } /** @@ -4370,10 +4393,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if ((mPrivateFlags & PFLAG_FOCUSED) == 0) { mPrivateFlags |= PFLAG_FOCUSED; + View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null; + if (mParent != null) { mParent.requestChildFocus(this, this); } + if (mAttachInfo != null) { + mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this); + } + onFocusChanged(true, direction, previouslyFocusedRect); refreshDrawableState(); @@ -4480,7 +4509,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, refreshDrawableState(); - ensureInputFocusOnFirstFocusable(); + if (!rootViewRequestFocus()) { + notifyGlobalFocusCleared(this); + } if (AccessibilityManager.getInstance(mContext).isEnabled()) { notifyAccessibilityStateChanged(); @@ -4488,11 +4519,18 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } } - void ensureInputFocusOnFirstFocusable() { + void notifyGlobalFocusCleared(View oldFocus) { + if (oldFocus != null && mAttachInfo != null) { + mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, null); + } + } + + boolean rootViewRequestFocus() { View root = getRootView(); if (root != null) { - root.requestFocus(); + return root.requestFocus(); } + return false; } /** @@ -4838,13 +4876,25 @@ public class View implements Drawable.Callback, KeyEvent.Callback, event.setEnabled(isEnabled()); event.setContentDescription(mContentDescription); - if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED && mAttachInfo != null) { - ArrayList<View> focusablesTempList = mAttachInfo.mTempArrayList; - getRootView().addFocusables(focusablesTempList, View.FOCUS_FORWARD, - FOCUSABLES_ALL); - event.setItemCount(focusablesTempList.size()); - event.setCurrentItemIndex(focusablesTempList.indexOf(this)); - focusablesTempList.clear(); + switch (event.getEventType()) { + case AccessibilityEvent.TYPE_VIEW_FOCUSED: { + ArrayList<View> focusablesTempList = (mAttachInfo != null) + ? mAttachInfo.mTempArrayList : new ArrayList<View>(); + getRootView().addFocusables(focusablesTempList, View.FOCUS_FORWARD, FOCUSABLES_ALL); + event.setItemCount(focusablesTempList.size()); + event.setCurrentItemIndex(focusablesTempList.indexOf(this)); + if (mAttachInfo != null) { + focusablesTempList.clear(); + } + } break; + case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED: { + CharSequence text = getIterableTextForAccessibility(); + if (text != null && text.length() > 0) { + event.setFromIndex(getAccessibilitySelectionStart()); + event.setToIndex(getAccessibilitySelectionEnd()); + event.setItemCount(text.length()); + } + } break; } } @@ -4986,6 +5036,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (label != null) { info.setLabeledBy(label); } + + if ((mAttachInfo.mAccessibilityFetchFlags + & AccessibilityNodeInfo.FLAG_REPORT_VIEW_IDS) != 0) { + try { + String viewId = getResources().getResourceName(mID); + info.setViewIdResourceName(viewId); + } catch (Resources.NotFoundException nfe) { + /* ignore */ + } + } } if (mLabelForId != View.NO_ID) { @@ -5041,7 +5101,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, info.addAction(AccessibilityNodeInfo.ACTION_LONG_CLICK); } - if (mContentDescription != null && mContentDescription.length() > 0) { + CharSequence text = getIterableTextForAccessibility(); + if (text != null && text.length() > 0) { + info.setTextSelection(getAccessibilitySelectionStart(), getAccessibilitySelectionEnd()); + + info.addAction(AccessibilityNodeInfo.ACTION_SET_SELECTION); info.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY); info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY); info.setMovementGranularities(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER @@ -5615,20 +5679,45 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if ((mViewFlags & FITS_SYSTEM_WINDOWS) == FITS_SYSTEM_WINDOWS) { mUserPaddingStart = UNDEFINED_PADDING; mUserPaddingEnd = UNDEFINED_PADDING; - if ((mViewFlags & OPTIONAL_FITS_SYSTEM_WINDOWS) == 0 - || mAttachInfo == null - || (mAttachInfo.mSystemUiVisibility & SYSTEM_UI_LAYOUT_FLAGS) == 0) { - internalSetPadding(insets.left, insets.top, insets.right, insets.bottom); - return true; - } else { - internalSetPadding(0, 0, 0, 0); - return false; + Rect localInsets = sThreadLocal.get(); + if (localInsets == null) { + localInsets = new Rect(); + sThreadLocal.set(localInsets); } + boolean res = computeFitSystemWindows(insets, localInsets); + internalSetPadding(localInsets.left, localInsets.top, + localInsets.right, localInsets.bottom); + return res; } return false; } /** + * @hide Compute the insets that should be consumed by this view and the ones + * that should propagate to those under it. + */ + protected boolean computeFitSystemWindows(Rect inoutInsets, Rect outLocalInsets) { + if ((mViewFlags & OPTIONAL_FITS_SYSTEM_WINDOWS) == 0 + || mAttachInfo == null + || ((mAttachInfo.mSystemUiVisibility & SYSTEM_UI_LAYOUT_FLAGS) == 0 + && !mAttachInfo.mOverscanRequested)) { + outLocalInsets.set(inoutInsets); + inoutInsets.set(0, 0, 0, 0); + return true; + } else { + // The application wants to take care of fitting system window for + // the content... however we still need to take care of any overscan here. + final Rect overscan = mAttachInfo.mOverscanInsets; + outLocalInsets.set(overscan); + inoutInsets.left -= overscan.left; + inoutInsets.top -= overscan.top; + inoutInsets.right -= overscan.right; + inoutInsets.bottom -= overscan.bottom; + return false; + } + } + + /** * Sets whether or not this view should account for system screen decorations * such as the status bar and inset its content; that is, controlling whether * the default implementation of {@link #fitSystemWindows(Rect)} will be @@ -5923,7 +6012,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion; if (targetSdkVersion < JELLY_BEAN_MR1) { mPrivateFlags2 |= PFLAG2_LAYOUT_DIRECTION_RESOLVED; - return LAYOUT_DIRECTION_LTR; + return LAYOUT_DIRECTION_RESOLVED_DEFAULT; } return ((mPrivateFlags2 & PFLAG2_LAYOUT_DIRECTION_RESOLVED_RTL) == PFLAG2_LAYOUT_DIRECTION_RESOLVED_RTL) ? LAYOUT_DIRECTION_RTL : LAYOUT_DIRECTION_LTR; @@ -6817,7 +6906,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public boolean includeForAccessibility() { if (mAttachInfo != null) { - return mAttachInfo.mIncludeNotImportantViews || isImportantForAccessibility(); + return (mAttachInfo.mAccessibilityFetchFlags + & AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS) != 0 + || isImportantForAccessibility(); } return false; } @@ -6966,21 +7057,43 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (arguments != null) { final int granularity = arguments.getInt( AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT); - return nextAtGranularity(granularity); + final boolean extendSelection = arguments.getBoolean( + AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN); + return nextAtGranularity(granularity, extendSelection); } } break; case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: { if (arguments != null) { final int granularity = arguments.getInt( AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT); - return previousAtGranularity(granularity); + final boolean extendSelection = arguments.getBoolean( + AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN); + return previousAtGranularity(granularity, extendSelection); + } + } break; + case AccessibilityNodeInfo.ACTION_SET_SELECTION: { + CharSequence text = getIterableTextForAccessibility(); + if (text == null) { + return false; + } + final int start = (arguments != null) ? arguments.getInt( + AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT, -1) : -1; + final int end = (arguments != null) ? arguments.getInt( + AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT, -1) : -1; + // Only cursor position can be specified (selection length == 0) + if ((getAccessibilitySelectionStart() != start + || getAccessibilitySelectionEnd() != end) + && (start == end)) { + setAccessibilitySelection(start, end); + notifyAccessibilityStateChanged(); + return true; } } break; } return false; } - private boolean nextAtGranularity(int granularity) { + private boolean nextAtGranularity(int granularity, boolean extendSelection) { CharSequence text = getIterableTextForAccessibility(); if (text == null || text.length() == 0) { return false; @@ -6989,21 +7102,32 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (iterator == null) { return false; } - final int current = getAccessibilityCursorPosition(); + int current = getAccessibilitySelectionEnd(); + if (current == ACCESSIBILITY_CURSOR_POSITION_UNDEFINED) { + current = 0; + } final int[] range = iterator.following(current); if (range == null) { return false; } final int start = range[0]; final int end = range[1]; - setAccessibilityCursorPosition(end); + if (extendSelection && isAccessibilitySelectionExtendable()) { + int selectionStart = getAccessibilitySelectionStart(); + if (selectionStart == ACCESSIBILITY_CURSOR_POSITION_UNDEFINED) { + selectionStart = start; + } + setAccessibilitySelection(selectionStart, end); + } else { + setAccessibilitySelection(end, end); + } sendViewTextTraversedAtGranularityEvent( AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, granularity, start, end); return true; } - private boolean previousAtGranularity(int granularity) { + private boolean previousAtGranularity(int granularity, boolean extendSelection) { CharSequence text = getIterableTextForAccessibility(); if (text == null || text.length() == 0) { return false; @@ -7012,15 +7136,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (iterator == null) { return false; } - int current = getAccessibilityCursorPosition(); + int current = getAccessibilitySelectionStart(); if (current == ACCESSIBILITY_CURSOR_POSITION_UNDEFINED) { current = text.length(); - setAccessibilityCursorPosition(current); - } else if (granularity == AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER) { - // When traversing by character we always put the cursor after the character - // to ease edit and have to compensate before asking the for previous segment. - current--; - setAccessibilityCursorPosition(current); } final int[] range = iterator.preceding(current); if (range == null) { @@ -7028,11 +7146,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } final int start = range[0]; final int end = range[1]; - // Always put the cursor after the character to ease edit. - if (granularity == AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER) { - setAccessibilityCursorPosition(end); + if (extendSelection && isAccessibilitySelectionExtendable()) { + int selectionEnd = getAccessibilitySelectionEnd(); + if (selectionEnd == ACCESSIBILITY_CURSOR_POSITION_UNDEFINED) { + selectionEnd = end; + } + setAccessibilitySelection(start, selectionEnd); } else { - setAccessibilityCursorPosition(start); + setAccessibilitySelection(start, start); } sendViewTextTraversedAtGranularityEvent( AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY, @@ -7052,17 +7173,43 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Gets whether accessibility selection can be extended. + * + * @return If selection is extensible. + * * @hide */ - public int getAccessibilityCursorPosition() { + public boolean isAccessibilitySelectionExtendable() { + return false; + } + + /** + * @hide + */ + public int getAccessibilitySelectionStart() { return mAccessibilityCursorPosition; } /** * @hide */ - public void setAccessibilityCursorPosition(int position) { - mAccessibilityCursorPosition = position; + public int getAccessibilitySelectionEnd() { + return getAccessibilitySelectionStart(); + } + + /** + * @hide + */ + public void setAccessibilitySelection(int start, int end) { + if (start == end && end == mAccessibilityCursorPosition) { + return; + } + if (start >= 0 && start == end && end <= getIterableTextForAccessibility().length()) { + mAccessibilityCursorPosition = start; + } else { + mAccessibilityCursorPosition = ACCESSIBILITY_CURSOR_POSITION_UNDEFINED; + } + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED); } private void sendViewTextTraversedAtGranularityEvent(int action, int granularity, @@ -9806,8 +9953,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, outRect.set(mLeft, mTop, mRight, mBottom); } else { final RectF tmpRect = mAttachInfo.mTmpTransformRect; - tmpRect.set(-info.mPivotX, -info.mPivotY, - getWidth() - info.mPivotX, getHeight() - info.mPivotY); + tmpRect.set(0, 0, getWidth(), getHeight()); info.mMatrix.mapRect(tmpRect); outRect.set((int) tmpRect.left + mLeft, (int) tmpRect.top + mTop, (int) tmpRect.right + mLeft, (int) tmpRect.bottom + mTop); @@ -9928,7 +10074,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mTop += offset; mBottom += offset; if (mDisplayList != null) { - mDisplayList.offsetTopBottom(offset); + mDisplayList.offsetTopAndBottom(offset); invalidateViewProperty(false, false); } else { if (!matrixIsIdentity) { @@ -9976,7 +10122,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mLeft += offset; mRight += offset; if (mDisplayList != null) { - mDisplayList.offsetLeftRight(offset); + mDisplayList.offsetLeftAndRight(offset); invalidateViewProperty(false, false); } else { if (!matrixIsIdentity) { @@ -10749,7 +10895,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // if we are not attached to our window final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { - final AttachInfo.InvalidateInfo info = AttachInfo.InvalidateInfo.acquire(); + final AttachInfo.InvalidateInfo info = AttachInfo.InvalidateInfo.obtain(); info.target = this; info.left = left; info.top = top; @@ -10798,7 +10944,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // if we are not attached to our window final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { - final AttachInfo.InvalidateInfo info = AttachInfo.InvalidateInfo.acquire(); + final AttachInfo.InvalidateInfo info = AttachInfo.InvalidateInfo.obtain(); info.target = this; info.left = left; info.top = top; @@ -11564,8 +11710,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, imm.focusIn(this); } - if (mAttachInfo != null && mDisplayList != null) { - mAttachInfo.mViewRootImpl.dequeueDisplayList(mDisplayList); + if (mDisplayList != null) { + mDisplayList.clearDirty(); } } @@ -11691,11 +11837,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // later to get the correct resolved value if (!canResolveLayoutDirection()) return false; - View parent = ((View) mParent); // Parent has not yet resolved, LTR is still the default - if (!parent.isLayoutDirectionResolved()) return false; + if (!mParent.isLayoutDirectionResolved()) return false; - if (parent.getLayoutDirection() == LAYOUT_DIRECTION_RTL) { + if (mParent.getLayoutDirection() == LAYOUT_DIRECTION_RTL) { mPrivateFlags2 |= PFLAG2_LAYOUT_DIRECTION_RESOLVED_RTL; } break; @@ -11728,8 +11873,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, public boolean canResolveLayoutDirection() { switch (getRawLayoutDirection()) { case LAYOUT_DIRECTION_INHERIT: - return (mParent != null) && (mParent instanceof ViewGroup) && - ((ViewGroup) mParent).canResolveLayoutDirection(); + return (mParent != null) && mParent.canResolveLayoutDirection(); default: return true; } @@ -11757,8 +11901,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, /** * @return true if layout direction has been resolved. + * @hide */ - private boolean isLayoutDirectionResolved() { + public boolean isLayoutDirectionResolved() { return (mPrivateFlags2 & PFLAG2_LAYOUT_DIRECTION_RESOLVED) == PFLAG2_LAYOUT_DIRECTION_RESOLVED; } @@ -11845,6 +11990,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (mAttachInfo != null) { if (mDisplayList != null) { + mDisplayList.markDirty(); mAttachInfo.mViewRootImpl.enqueueDisplayList(mDisplayList); } mAttachInfo.mViewRootImpl.cancelInvalidate(this); @@ -12548,8 +12694,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * @return The HardwareRenderer associated with that view or null if hardware rendering - * is not supported or this this has not been attached to a window. + * @return The {@link HardwareRenderer} associated with that view or null if + * hardware rendering is not supported or this view is not attached + * to a window. * * @hide */ @@ -12604,15 +12751,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } boolean caching = false; - final HardwareCanvas canvas = displayList.start(); int width = mRight - mLeft; int height = mBottom - mTop; + int layerType = getLayerType(); + + final HardwareCanvas canvas = displayList.start(width, height); try { - canvas.setViewport(width, height); - // The dirty rect should always be null for a display list - canvas.onPreDraw(null); - int layerType = getLayerType(); if (!isLayer && layerType != LAYER_TYPE_NONE) { if (layerType == LAYER_TYPE_HARDWARE) { final HardwareLayer layer = getHardwareLayer(); @@ -12650,8 +12795,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } } } finally { - canvas.onPostDraw(); - displayList.end(); displayList.setCaching(caching); if (isLayer) { @@ -12696,7 +12839,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private void clearDisplayList() { if (mDisplayList != null) { - mDisplayList.invalidate(); mDisplayList.clear(); } } @@ -13272,7 +13414,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, alpha = transform.getAlpha(); } if ((transformType & Transformation.TYPE_MATRIX) != 0) { - displayList.setStaticMatrix(transform.getMatrix()); + displayList.setMatrix(transform.getMatrix()); } } } @@ -13347,8 +13489,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } transformToApply = parent.mChildTransformation; } else { - if ((mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_TRANSFORM) == PFLAG3_VIEW_IS_ANIMATING_TRANSFORM && - mDisplayList != null) { + if ((mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_TRANSFORM) == + PFLAG3_VIEW_IS_ANIMATING_TRANSFORM && mDisplayList != null) { // No longer animating: clear out old animation matrix mDisplayList.setAnimationMatrix(null); mPrivateFlags3 &= ~PFLAG3_VIEW_IS_ANIMATING_TRANSFORM; @@ -13545,7 +13687,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } if ((flags & ViewGroup.FLAG_CLIP_CHILDREN) == ViewGroup.FLAG_CLIP_CHILDREN && - !useDisplayListProperties) { + !useDisplayListProperties && layerType == LAYER_TYPE_NONE) { if (offsetForScroll) { canvas.clipRect(sx, sy, sx + (mRight - mLeft), sy + (mBottom - mTop)); } else { @@ -13977,6 +14119,25 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Return true if o is a ViewGroup that is laying out using optical bounds. + * @hide + */ + public static boolean isLayoutModeOptical(Object o) { + return o instanceof ViewGroup && ((ViewGroup) o).isLayoutModeOptical(); + } + + private boolean setOpticalFrame(int left, int top, int right, int bottom) { + Insets parentInsets = mParent instanceof View ? + ((View) mParent).getOpticalInsets() : Insets.NONE; + Insets childInsets = getOpticalInsets(); + return setFrame( + left + parentInsets.left - childInsets.left, + top + parentInsets.top - childInsets.top, + right + parentInsets.left + childInsets.right, + bottom + parentInsets.top + childInsets.bottom); + } + + /** * Assign a size and position to a view and all of its * descendants * @@ -14002,7 +14163,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, int oldT = mTop; int oldB = mBottom; int oldR = mRight; - boolean changed = setFrame(l, t, r, b); + boolean changed = isLayoutModeOptical(mParent) ? + setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { onLayout(changed, l, t, r, b); mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED; @@ -14444,6 +14606,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (mBackground instanceof ColorDrawable) { ((ColorDrawable) mBackground.mutate()).setColor(color); computeOpaqueFlags(); + mBackgroundResource = 0; } else { setBackground(new ColorDrawable(color)); } @@ -14818,6 +14981,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return (mUserPaddingStart != UNDEFINED_PADDING || mUserPaddingEnd != UNDEFINED_PADDING); } + Insets computeOpticalInsets() { + return (mBackground == null) ? Insets.NONE : mBackground.getOpticalInsets(); + } + /** * @hide */ @@ -14841,19 +15008,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public Insets getOpticalInsets() { if (mLayoutInsets == null) { - mLayoutInsets = (mBackground == null) ? Insets.NONE : mBackground.getLayoutInsets(); + mLayoutInsets = computeOpticalInsets(); } return mLayoutInsets; } /** - * @hide - */ - public void setLayoutInsets(Insets layoutInsets) { - mLayoutInsets = layoutInsets; - } - - /** * Changes the selection state of this view. A view can be selected or not. * Note that selection is not the same as focus. Views are typically * selected in the context of an AdapterView like ListView or GridView; @@ -15460,17 +15620,50 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Returns whether the view hierarchy is currently undergoing a layout pass. This + * information is useful to avoid situations such as calling {@link #requestLayout()} during + * a layout pass. + * + * @return whether the view hierarchy is currently undergoing a layout pass + */ + public boolean isInLayout() { + ViewRootImpl viewRoot = getViewRootImpl(); + return (viewRoot != null && viewRoot.isInLayout()); + } + + /** * Call this when something has changed which has invalidated the * layout of this view. This will schedule a layout pass of the view - * tree. + * tree. This should not be called while the view hierarchy is currently in a layout + * pass ({@link #isInLayout()}. If layout is happening, the request may be honored at the + * end of the current layout pass (and then layout will run again) or after the current + * frame is drawn and the next layout occurs. + * + * <p>Subclasses which override this method should call the superclass method to + * handle possible request-during-layout errors correctly.</p> */ public void requestLayout() { + if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) { + // Only trigger request-during-layout logic if this is the view requesting it, + // not the views in its parent hierarchy + ViewRootImpl viewRoot = getViewRootImpl(); + if (viewRoot != null && viewRoot.isInLayout()) { + if (!viewRoot.requestLayoutDuringLayout(this)) { + return; + } + } + mAttachInfo.mViewRequestingLayout = this; + } + mPrivateFlags |= PFLAG_FORCE_LAYOUT; mPrivateFlags |= PFLAG_INVALIDATED; if (mParent != null && !mParent.isLayoutRequested()) { mParent.requestLayout(); } + if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) { + mAttachInfo.mViewRequestingLayout = null; + } } /** @@ -15504,6 +15697,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #onMeasure(int, int) */ public final void measure(int widthMeasureSpec, int heightMeasureSpec) { + boolean optical = isLayoutModeOptical(this); + if (optical != isLayoutModeOptical(mParent)) { + Insets insets = getOpticalInsets(); + int oWidth = insets.left + insets.right; + int oHeight = insets.top + insets.bottom; + widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth); + heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight); + } if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT || widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec) { @@ -15595,6 +15796,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * {@link #MEASURED_STATE_TOO_SMALL}. */ protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { + boolean optical = isLayoutModeOptical(this); + if (optical != isLayoutModeOptical(mParent)) { + Insets insets = getOpticalInsets(); + int opticalWidth = insets.left + insets.right; + int opticalHeight = insets.top + insets.bottom; + + measuredWidth += optical ? opticalWidth : -opticalWidth; + measuredHeight += optical ? opticalHeight : -opticalHeight; + } mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; @@ -16722,16 +16932,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return false; } - View parent = ((View) mParent); // Parent has not yet resolved, so we still return the default - if (!parent.isTextDirectionResolved()) { + if (!mParent.isTextDirectionResolved()) { mPrivateFlags2 |= PFLAG2_TEXT_DIRECTION_RESOLVED_DEFAULT; // Resolution will need to happen again later return false; } // Set current resolved direction to the same value as the parent's one - final int parentResolvedDirection = parent.getTextDirection(); + final int parentResolvedDirection = mParent.getTextDirection(); switch (parentResolvedDirection) { case TEXT_DIRECTION_FIRST_STRONG: case TEXT_DIRECTION_ANY_RTL: @@ -16772,12 +16981,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * Check if text direction resolution can be done. * * @return true if text direction resolution can be done otherwise return false. + * + * @hide */ - private boolean canResolveTextDirection() { + public boolean canResolveTextDirection() { switch (getRawTextDirection()) { case TEXT_DIRECTION_INHERIT: - return (mParent != null) && (mParent instanceof View) && - ((View) mParent).canResolveTextDirection(); + return (mParent != null) && mParent.canResolveTextDirection(); default: return true; } @@ -16807,8 +17017,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, /** * @return true if text direction is resolved. + * + * @hide */ - private boolean isTextDirectionResolved() { + public boolean isTextDirectionResolved() { return (mPrivateFlags2 & PFLAG2_TEXT_DIRECTION_RESOLVED) == PFLAG2_TEXT_DIRECTION_RESOLVED; } @@ -16931,16 +17143,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // Resolution will need to happen again later return false; } - View parent = (View) mParent; // Parent has not yet resolved, so we still return the default - if (!parent.isTextAlignmentResolved()) { + if (!mParent.isTextAlignmentResolved()) { mPrivateFlags2 |= PFLAG2_TEXT_ALIGNMENT_RESOLVED_DEFAULT; // Resolution will need to happen again later return false; } - final int parentResolvedTextAlignment = parent.getTextAlignment(); + final int parentResolvedTextAlignment = mParent.getTextAlignment(); switch (parentResolvedTextAlignment) { case TEXT_ALIGNMENT_GRAVITY: case TEXT_ALIGNMENT_TEXT_START: @@ -16985,12 +17196,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * Check if text alignment resolution can be done. * * @return true if text alignment resolution can be done otherwise return false. + * + * @hide */ - private boolean canResolveTextAlignment() { + public boolean canResolveTextAlignment() { switch (getRawTextAlignment()) { case TEXT_DIRECTION_INHERIT: - return (mParent != null) && (mParent instanceof View) && - ((View) mParent).canResolveTextAlignment(); + return (mParent != null) && mParent.canResolveTextAlignment(); default: return true; } @@ -17020,8 +17232,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, /** * @return true if text alignment is resolved. + * + * @hide */ - private boolean isTextAlignmentResolved() { + public boolean isTextAlignmentResolved() { return (mPrivateFlags2 & PFLAG2_TEXT_ALIGNMENT_RESOLVED) == PFLAG2_TEXT_ALIGNMENT_RESOLVED; } @@ -17266,12 +17480,23 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * <li>{@link android.view.View.MeasureSpec#AT_MOST}</li> * </ul> * + * <p><strong>Note:</strong> On API level 17 and lower, makeMeasureSpec's + * implementation was such that the order of arguments did not matter + * and overflow in either value could impact the resulting MeasureSpec. + * {@link android.widget.RelativeLayout} was affected by this bug. + * Apps targeting API levels greater than 17 will get the fixed, more strict + * behavior.</p> + * * @param size the size of the measure specification * @param mode the mode of the measure specification * @return the measure specification based on size and mode */ public static int makeMeasureSpec(int size, int mode) { - return size + mode; + if (sUseBrokenMakeMeasureSpec) { + return size + mode; + } else { + return (size & ~MODE_MASK) | (mode & MODE_MASK); + } } /** @@ -17296,6 +17521,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return (measureSpec & ~MODE_MASK); } + static int adjust(int measureSpec, int delta) { + return makeMeasureSpec(getSize(measureSpec + delta), getMode(measureSpec)); + } + /** * Returns a String representation of the specified measure * specification. @@ -17633,25 +17862,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * POOL_LIMIT objects that get reused. This reduces memory allocations * whenever possible. */ - static class InvalidateInfo implements Poolable<InvalidateInfo> { + static class InvalidateInfo { private static final int POOL_LIMIT = 10; - private static final Pool<InvalidateInfo> sPool = Pools.synchronizedPool( - Pools.finitePool(new PoolableManager<InvalidateInfo>() { - public InvalidateInfo newInstance() { - return new InvalidateInfo(); - } - - public void onAcquired(InvalidateInfo element) { - } - - public void onReleased(InvalidateInfo element) { - element.target = null; - } - }, POOL_LIMIT) - ); - private InvalidateInfo mNext; - private boolean mIsPooled; + private static final SynchronizedPool<InvalidateInfo> sPool = + new SynchronizedPool<InvalidateInfo>(POOL_LIMIT); View target; @@ -17660,29 +17875,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback, int right; int bottom; - public void setNextPoolable(InvalidateInfo element) { - mNext = element; + public static InvalidateInfo obtain() { + InvalidateInfo instance = sPool.acquire(); + return (instance != null) ? instance : new InvalidateInfo(); } - public InvalidateInfo getNextPoolable() { - return mNext; - } - - static InvalidateInfo acquire() { - return sPool.acquire(); - } - - void release() { + public void recycle() { + target = null; sPool.release(this); } - - public boolean isPooled() { - return mIsPooled; - } - - public void setPooled(boolean isPooled) { - mIsPooled = isPooled; - } } final IWindowSession mSession; @@ -17743,6 +17944,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, /** * For windows that are full-screen but using insets to layout inside + * of the screen areas, these are the current insets to appear inside + * the overscan area of the display. + */ + final Rect mOverscanInsets = new Rect(); + + /** + * For windows that are full-screen but using insets to layout inside * of the screen decorations, these are the current insets for the * content of the window. */ @@ -17844,6 +18052,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, boolean mHasSystemUiListeners; /** + * Set if the window has requested to extend into the overscan region + * via WindowManager.LayoutParams.FLAG_LAYOUT_IN_OVERSCAN. + */ + boolean mOverscanRequested; + + /** * Set if the visibility of any views has changed. */ boolean mViewVisibilityChanged; @@ -17926,10 +18140,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, int mAccessibilityWindowId = View.NO_ID; /** - * Whether to ingore not exposed for accessibility Views when - * reporting the view tree to accessibility services. + * Flags related to accessibility processing. + * + * @see AccessibilityNodeInfo#FLAG_INCLUDE_NOT_IMPORTANT_VIEWS + * @see AccessibilityNodeInfo#FLAG_REPORT_VIEW_IDS */ - boolean mIncludeNotImportantViews; + int mAccessibilityFetchFlags; /** * The drawable for highlighting accessibility focus. @@ -17947,6 +18163,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, final Point mPoint = new Point(); /** + * Used to track which View originated a requestLayout() call, used when + * requestLayout() is called during layout. + */ + View mViewRequestingLayout; + + /** * Creates a new set of attachment information with the specified * events handler and thread. * diff --git a/core/java/android/view/ViewDebug.java b/core/java/android/view/ViewDebug.java index 023e58f..987ff78 100644 --- a/core/java/android/view/ViewDebug.java +++ b/core/java/android/view/ViewDebug.java @@ -45,6 +45,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; /** * Various debugging/tracing tools related to {@link View} and the view hierarchy. @@ -406,7 +407,7 @@ public class ViewDebug { view = view.getRootView(); if (REMOTE_COMMAND_DUMP.equalsIgnoreCase(command)) { - dump(view, clientStream); + dump(view, false, true, clientStream); } else if (REMOTE_COMMAND_CAPTURE_LAYERS.equalsIgnoreCase(command)) { captureLayers(view, new DataOutputStream(clientStream)); } else { @@ -425,7 +426,8 @@ public class ViewDebug { } } - private static View findView(View root, String parameter) { + /** @hide */ + public static View findView(View root, String parameter) { // Look by type/hashcode if (parameter.indexOf('@') != -1) { final String[] ids = parameter.split("@"); @@ -488,7 +490,8 @@ public class ViewDebug { } } - private static void profileViewAndChildren(final View view, BufferedWriter out) + /** @hide */ + public static void profileViewAndChildren(final View view, BufferedWriter out) throws IOException { profileViewAndChildren(view, out, true); } @@ -623,7 +626,8 @@ public class ViewDebug { return duration[0]; } - private static void captureLayers(View root, final DataOutputStream clientStream) + /** @hide */ + public static void captureLayers(View root, final DataOutputStream clientStream) throws IOException { try { @@ -695,10 +699,21 @@ public class ViewDebug { view.getViewRootImpl().outputDisplayList(view); } + /** @hide */ + public static void outputDisplayList(View root, View target) { + root.getViewRootImpl().outputDisplayList(target); + } + private static void capture(View root, final OutputStream clientStream, String parameter) throws IOException { final View captureView = findView(root, parameter); + capture(root, clientStream, captureView); + } + + /** @hide */ + public static void capture(View root, final OutputStream clientStream, View captureView) + throws IOException { Bitmap b = performViewCapture(captureView, false); if (b == null) { @@ -752,14 +767,20 @@ public class ViewDebug { return null; } - private static void dump(View root, OutputStream clientStream) throws IOException { + /** + * Dumps the view hierarchy starting from the given view. + * @hide + */ + public static void dump(View root, boolean skipChildren, boolean includeProperties, + OutputStream clientStream) throws IOException { BufferedWriter out = null; try { out = new BufferedWriter(new OutputStreamWriter(clientStream, "utf-8"), 32 * 1024); View view = root.getRootView(); if (view instanceof ViewGroup) { ViewGroup group = (ViewGroup) view; - dumpViewHierarchyWithProperties(group.getContext(), group, out, 0); + dumpViewHierarchy(group.getContext(), group, out, 0, + skipChildren, includeProperties); } out.write("DONE."); out.newLine(); @@ -804,9 +825,13 @@ public class ViewDebug { return view.getClass().getName().equals(className) && view.hashCode() == hashCode; } - private static void dumpViewHierarchyWithProperties(Context context, ViewGroup group, - BufferedWriter out, int level) { - if (!dumpViewWithProperties(context, group, out, level)) { + private static void dumpViewHierarchy(Context context, ViewGroup group, + BufferedWriter out, int level, boolean skipChildren, boolean includeProperties) { + if (!dumpView(context, group, out, level, includeProperties)) { + return; + } + + if (skipChildren) { return; } @@ -814,9 +839,10 @@ public class ViewDebug { for (int i = 0; i < count; i++) { final View view = group.getChildAt(i); if (view instanceof ViewGroup) { - dumpViewHierarchyWithProperties(context, (ViewGroup) view, out, level + 1); + dumpViewHierarchy(context, (ViewGroup) view, out, level + 1, skipChildren, + includeProperties); } else { - dumpViewWithProperties(context, view, out, level + 1); + dumpView(context, view, out, level + 1, includeProperties); } } if (group instanceof HierarchyHandler) { @@ -824,8 +850,8 @@ public class ViewDebug { } } - private static boolean dumpViewWithProperties(Context context, View view, - BufferedWriter out, int level) { + private static boolean dumpView(Context context, View view, + BufferedWriter out, int level, boolean includeProperties) { try { for (int i = 0; i < level; i++) { @@ -835,7 +861,9 @@ public class ViewDebug { out.write('@'); out.write(Integer.toHexString(view.hashCode())); out.write(' '); - dumpViewProperties(context, view, out); + if (includeProperties) { + dumpViewProperties(context, view, out); + } out.newLine(); } catch (IOException e) { Log.w("View", "Error while dumping hierarchy tree"); @@ -1347,4 +1375,68 @@ public class ViewDebug { sb.append(capturedViewExportMethods(view, klass, "")); Log.d(tag, sb.toString()); } + + /** + * Invoke a particular method on given view. + * The given method is always invoked on the UI thread. The caller thread will stall until the + * method invocation is complete. Returns an object equal to the result of the method + * invocation, null if the method is declared to return void + * @throws Exception if the method invocation caused any exception + * @hide + */ + public static Object invokeViewMethod(final View view, final Method method, + final Object[] args) { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference<Object> result = new AtomicReference<Object>(); + final AtomicReference<Throwable> exception = new AtomicReference<Throwable>(); + + view.post(new Runnable() { + @Override + public void run() { + try { + result.set(method.invoke(view, args)); + } catch (InvocationTargetException e) { + exception.set(e.getCause()); + } catch (Exception e) { + exception.set(e); + } + + latch.countDown(); + } + }); + + try { + latch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + if (exception.get() != null) { + throw new RuntimeException(exception.get()); + } + + return result.get(); + } + + /** + * @hide + */ + public static void setLayoutParameter(final View view, final String param, final int value) + throws NoSuchFieldException, IllegalAccessException { + final ViewGroup.LayoutParams p = view.getLayoutParams(); + final Field f = p.getClass().getField(param); + if (f.getType() != int.class) { + throw new RuntimeException("Only integer layout parameters can be set. Field " + + param + " is of type " + f.getType().getSimpleName()); + } + + f.set(p, Integer.valueOf(value)); + + view.post(new Runnable() { + @Override + public void run() { + view.setLayoutParams(p); + } + }); + } } diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index dbbcde6..5105fda 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -35,6 +35,7 @@ import android.os.Parcelable; import android.os.SystemClock; import android.util.AttributeSet; import android.util.Log; +import android.util.Pools.SynchronizedPool; import android.util.SparseArray; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; @@ -83,6 +84,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager private static final String TAG = "ViewGroup"; private static final boolean DBG = false; + /** @hide */ + public static boolean DEBUG_DRAW = false; /** * Views which have been hidden or removed which need to be animated on @@ -180,10 +183,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager }) protected int mGroupFlags; - /* - * The layout mode: either {@link #CLIP_BOUNDS} or {@link #OPTICAL_BOUNDS} + /** + * Either {@link #LAYOUT_MODE_CLIP_BOUNDS} or {@link #LAYOUT_MODE_OPTICAL_BOUNDS}. */ - private int mLayoutMode = CLIP_BOUNDS; + private int mLayoutMode = DEFAULT_LAYOUT_MODE; /** * NOTE: If you change the flags below make sure to reflect the changes @@ -356,20 +359,19 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * This constant is a {@link #setLayoutMode(int) layoutMode}. * Clip bounds are the raw values of {@link #getLeft() left}, {@link #getTop() top}, * {@link #getRight() right} and {@link #getBottom() bottom}. - * - * @hide */ - public static final int CLIP_BOUNDS = 0; + public static final int LAYOUT_MODE_CLIP_BOUNDS = 0; /** * This constant is a {@link #setLayoutMode(int) layoutMode}. * Optical bounds describe where a widget appears to be. They sit inside the clip * bounds which need to cover a larger area to allow other effects, * such as shadows and glows, to be drawn. - * - * @hide */ - public static final int OPTICAL_BOUNDS = 1; + public static final int LAYOUT_MODE_OPTICAL_BOUNDS = 1; + + /** @hide */ + public static int DEFAULT_LAYOUT_MODE = LAYOUT_MODE_CLIP_BOUNDS; /** * We clip to padding when FLAG_CLIP_TO_PADDING and FLAG_PADDING_NOT_NULL @@ -434,7 +436,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } private boolean debugDraw() { - return mAttachInfo != null && mAttachInfo.mDebugLayout; + return DEBUG_DRAW || mAttachInfo != null && mAttachInfo.mDebugLayout; } private void initViewGroup() { @@ -504,6 +506,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager setLayoutTransition(new LayoutTransition()); } break; + case R.styleable.ViewGroup_layoutMode: + setLayoutMode(a.getInt(attr, DEFAULT_LAYOUT_MODE)); + break; } } @@ -2420,7 +2425,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager for (int i = 0; i < count; i++) { final View child = children[i]; child.dispatchAttachedToWindow(info, - visibility | (child.mViewFlags&VISIBILITY_MASK)); + visibility | (child.mViewFlags & VISIBILITY_MASK)); } } @@ -2682,20 +2687,89 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager return b; } - private static void drawRect(Canvas canvas, int x1, int y1, int x2, int y2, int color) { - Paint paint = getDebugPaint(); - paint.setColor(color); + /** Return true if this ViewGroup is laying out using optical bounds. */ + boolean isLayoutModeOptical() { + return mLayoutMode == LAYOUT_MODE_OPTICAL_BOUNDS; + } + + Insets computeOpticalInsets() { + if (isLayoutModeOptical()) { + int left = 0; + int top = 0; + int right = 0; + int bottom = 0; + for (int i = 0; i < mChildrenCount; i++) { + View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + Insets insets = child.getOpticalInsets(); + left = Math.max(left, insets.left); + top = Math.max(top, insets.top); + right = Math.max(right, insets.right); + bottom = Math.max(bottom, insets.bottom); + } + } + return Insets.of(left, top, right, bottom); + } else { + return Insets.NONE; + } + } + + private static void fillRect(Canvas canvas, Paint paint, int x1, int y1, int x2, int y2) { + if (x1 != x2 && y1 != y2) { + if (x1 > x2) { + int tmp = x1; x1 = x2; x2 = tmp; + } + if (y1 > y2) { + int tmp = y1; y1 = y2; y2 = tmp; + } + canvas.drawRect(x1, y1, x2, y2, paint); + } + } + + private static int sign(int x) { + return (x >= 0) ? 1 : -1; + } + + private static void drawCorner(Canvas c, Paint paint, int x1, int y1, int dx, int dy, int lw) { + fillRect(c, paint, x1, y1, x1 + dx, y1 + lw * sign(dy)); + fillRect(c, paint, x1, y1, x1 + lw * sign(dx), y1 + dy); + } + + private int dipsToPixels(int dips) { + float scale = getContext().getResources().getDisplayMetrics().density; + return (int) (dips * scale + 0.5f); + } + + private void drawRectCorners(Canvas canvas, int x1, int y1, int x2, int y2, Paint paint, + int lineLength, int lineWidth) { + drawCorner(canvas, paint, x1, y1, lineLength, lineLength, lineWidth); + drawCorner(canvas, paint, x1, y2, lineLength, -lineLength, lineWidth); + drawCorner(canvas, paint, x2, y1, -lineLength, lineLength, lineWidth); + drawCorner(canvas, paint, x2, y2, -lineLength, -lineLength, lineWidth); + } + + private static void fillDifference(Canvas canvas, + int x2, int y2, int x3, int y3, + int dx1, int dy1, int dx2, int dy2, Paint paint) { + int x1 = x2 - dx1; + int y1 = y2 - dy1; + + int x4 = x3 + dx2; + int y4 = y3 + dy2; - canvas.drawLines(getDebugLines(x1, y1, x2, y2), paint); + fillRect(canvas, paint, x1, y1, x4, y2); + fillRect(canvas, paint, x1, y2, x2, y3); + fillRect(canvas, paint, x3, y2, x4, y3); + fillRect(canvas, paint, x1, y3, x4, y4); } /** * @hide */ - protected void onDebugDrawMargins(Canvas canvas) { + protected void onDebugDrawMargins(Canvas canvas, Paint paint) { for (int i = 0; i < getChildCount(); i++) { View c = getChildAt(i); - c.getLayoutParams().onDebugDraw(c, canvas); + c.getLayoutParams().onDebugDraw(c, canvas, paint); } } @@ -2703,26 +2777,45 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * @hide */ protected void onDebugDraw(Canvas canvas) { + Paint paint = getDebugPaint(); + // Draw optical bounds - if (getLayoutMode() == OPTICAL_BOUNDS) { + { + paint.setColor(Color.RED); + paint.setStyle(Paint.Style.STROKE); + for (int i = 0; i < getChildCount(); i++) { View c = getChildAt(i); Insets insets = c.getOpticalInsets(); - drawRect(canvas, - c.getLeft() + insets.left, - c.getTop() + insets.top, - c.getRight() - insets.right, - c.getBottom() - insets.bottom, Color.RED); + + drawRect(canvas, paint, + c.getLeft() + insets.left, + c.getTop() + insets.top, + c.getRight() - insets.right - 1, + c.getBottom() - insets.bottom - 1); } } // Draw margins - onDebugDrawMargins(canvas); + { + paint.setColor(Color.argb(63, 255, 0, 255)); + paint.setStyle(Paint.Style.FILL); - // Draw bounds - for (int i = 0; i < getChildCount(); i++) { - View c = getChildAt(i); - drawRect(canvas, c.getLeft(), c.getTop(), c.getRight(), c.getBottom(), Color.BLUE); + onDebugDrawMargins(canvas, paint); + } + + // Draw clip bounds + { + paint.setColor(Color.rgb(63, 127, 255)); + paint.setStyle(Paint.Style.FILL); + + int lineLength = dipsToPixels(8); + int lineWidth = dipsToPixels(1); + for (int i = 0; i < getChildCount(); i++) { + View c = getChildAt(i); + drawRectCorners(canvas, c.getLeft(), c.getTop(), c.getRight(), c.getBottom(), + paint, lineLength, lineWidth); + } } } @@ -3604,7 +3697,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager clearChildFocus = true; } - view.clearAccessibilityFocus(); + if (view.isAccessibilityFocused()) { + view.clearAccessibilityFocus(); + } cancelTouchTarget(view); cancelHoverTarget(view); @@ -3620,20 +3715,18 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager childHasTransientStateChanged(view, false); } - onViewRemoved(view); - needGlobalAttributesUpdate(false); removeFromArray(index); if (clearChildFocus) { clearChildFocus(view); - ensureInputFocusOnFirstFocusable(); + if (!rootViewRequestFocus()) { + notifyGlobalFocusCleared(this); + } } - if (view.isAccessibilityFocused()) { - view.clearAccessibilityFocus(); - } + onViewRemoved(view); } /** @@ -3672,7 +3765,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager private void removeViewsInternal(int start, int count) { final View focused = mFocused; final boolean detach = mAttachInfo != null; - View clearChildFocus = null; + boolean clearChildFocus = false; final View[] children = mChildren; final int end = start + count; @@ -3686,10 +3779,12 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager if (view == focused) { view.unFocus(); - clearChildFocus = view; + clearChildFocus = true; } - view.clearAccessibilityFocus(); + if (view.isAccessibilityFocused()) { + view.clearAccessibilityFocus(); + } cancelTouchTarget(view); cancelHoverTarget(view); @@ -3712,9 +3807,11 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager removeFromArray(start, count); - if (clearChildFocus != null) { - clearChildFocus(clearChildFocus); - ensureInputFocusOnFirstFocusable(); + if (clearChildFocus) { + clearChildFocus(focused); + if (!rootViewRequestFocus()) { + notifyGlobalFocusCleared(focused); + } } } @@ -3756,7 +3853,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final View focused = mFocused; final boolean detach = mAttachInfo != null; - View clearChildFocus = null; + boolean clearChildFocus = false; needGlobalAttributesUpdate(false); @@ -3769,10 +3866,12 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager if (view == focused) { view.unFocus(); - clearChildFocus = view; + clearChildFocus = true; } - view.clearAccessibilityFocus(); + if (view.isAccessibilityFocused()) { + view.clearAccessibilityFocus(); + } cancelTouchTarget(view); cancelHoverTarget(view); @@ -3794,9 +3893,11 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager children[i] = null; } - if (clearChildFocus != null) { - clearChildFocus(clearChildFocus); - ensureInputFocusOnFirstFocusable(); + if (clearChildFocus) { + clearChildFocus(focused); + if (!rootViewRequestFocus()) { + notifyGlobalFocusCleared(focused); + } } } @@ -4312,7 +4413,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager v.mTop += offset; v.mBottom += offset; if (v.mDisplayList != null) { - v.mDisplayList.offsetTopBottom(offset); + v.mDisplayList.offsetTopAndBottom(offset); invalidateViewProperty(false, false); } } @@ -4610,13 +4711,11 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager /** * Returns the basis of alignment during layout operations on this view group: - * either {@link #CLIP_BOUNDS} or {@link #OPTICAL_BOUNDS}. + * either {@link #LAYOUT_MODE_CLIP_BOUNDS} or {@link #LAYOUT_MODE_OPTICAL_BOUNDS}. * * @return the layout mode to use during layout operations * * @see #setLayoutMode(int) - * - * @hide */ public int getLayoutMode() { return mLayoutMode; @@ -4624,15 +4723,14 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager /** * Sets the basis of alignment during the layout of this view group. - * Valid values are either {@link #CLIP_BOUNDS} or {@link #OPTICAL_BOUNDS}. + * Valid values are either {@link #LAYOUT_MODE_CLIP_BOUNDS} or + * {@link #LAYOUT_MODE_OPTICAL_BOUNDS}. * <p> - * The default is {@link #CLIP_BOUNDS}. + * The default is {@link #LAYOUT_MODE_CLIP_BOUNDS}. * * @param layoutMode the layout mode to use during layout operations * * @see #getLayoutMode() - * - * @hide */ public void setLayoutMode(int layoutMode) { if (mLayoutMode != layoutMode) { @@ -5650,7 +5748,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * * @hide */ - public void onDebugDraw(View view, Canvas canvas) { + public void onDebugDraw(View view, Canvas canvas, Paint paint) { } /** @@ -5998,12 +6096,19 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * @hide */ @Override - public void onDebugDraw(View view, Canvas canvas) { - drawRect(canvas, - view.getLeft() - leftMargin, - view.getTop() - topMargin, - view.getRight() + rightMargin, - view.getBottom() + bottomMargin, Color.MAGENTA); + public void onDebugDraw(View view, Canvas canvas, Paint paint) { + Insets oi = isLayoutModeOptical(view.mParent) ? view.getOpticalInsets() : Insets.NONE; + + fillDifference(canvas, + view.getLeft() + oi.left, + view.getTop() + oi.top, + view.getRight() - oi.right, + view.getBottom() - oi.bottom, + leftMargin, + topMargin, + rightMargin, + bottomMargin, + paint); } } @@ -6119,50 +6224,25 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager private static final int MAX_POOL_SIZE = 32; - private static final Object sPoolLock = new Object(); - - private static ChildListForAccessibility sPool; - - private static int sPoolSize; - - private boolean mIsPooled; - - private ChildListForAccessibility mNext; + private static final SynchronizedPool<ChildListForAccessibility> sPool = + new SynchronizedPool<ChildListForAccessibility>(MAX_POOL_SIZE); private final ArrayList<View> mChildren = new ArrayList<View>(); private final ArrayList<ViewLocationHolder> mHolders = new ArrayList<ViewLocationHolder>(); public static ChildListForAccessibility obtain(ViewGroup parent, boolean sort) { - ChildListForAccessibility list = null; - synchronized (sPoolLock) { - if (sPool != null) { - list = sPool; - sPool = list.mNext; - list.mNext = null; - list.mIsPooled = false; - sPoolSize--; - } else { - list = new ChildListForAccessibility(); - } - list.init(parent, sort); - return list; + ChildListForAccessibility list = sPool.acquire(); + if (list == null) { + list = new ChildListForAccessibility(); } + list.init(parent, sort); + return list; } public void recycle() { - if (mIsPooled) { - throw new IllegalStateException("Instance already recycled."); - } clear(); - synchronized (sPoolLock) { - if (sPoolSize < MAX_POOL_SIZE) { - mNext = sPool; - mIsPooled = true; - sPool = this; - sPoolSize++; - } - } + sPool.release(this); } public int getChildCount() { @@ -6216,15 +6296,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager private static final int MAX_POOL_SIZE = 32; - private static final Object sPoolLock = new Object(); - - private static ViewLocationHolder sPool; - - private static int sPoolSize; - - private boolean mIsPooled; - - private ViewLocationHolder mNext; + private static final SynchronizedPool<ViewLocationHolder> sPool = + new SynchronizedPool<ViewLocationHolder>(MAX_POOL_SIZE); private final Rect mLocation = new Rect(); @@ -6233,35 +6306,17 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager private int mLayoutDirection; public static ViewLocationHolder obtain(ViewGroup root, View view) { - ViewLocationHolder holder = null; - synchronized (sPoolLock) { - if (sPool != null) { - holder = sPool; - sPool = holder.mNext; - holder.mNext = null; - holder.mIsPooled = false; - sPoolSize--; - } else { - holder = new ViewLocationHolder(); - } - holder.init(root, view); - return holder; + ViewLocationHolder holder = sPool.acquire(); + if (holder == null) { + holder = new ViewLocationHolder(); } + holder.init(root, view); + return holder; } public void recycle() { - if (mIsPooled) { - throw new IllegalStateException("Instance already recycled."); - } clear(); - synchronized (sPoolLock) { - if (sPoolSize < MAX_POOL_SIZE) { - mNext = sPool; - mIsPooled = true; - sPool = this; - sPoolSize++; - } - } + sPool.release(this); } @Override @@ -6337,14 +6392,11 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager return sDebugPaint; } - private static float[] getDebugLines(int x1, int y1, int x2, int y2) { + private void drawRect(Canvas canvas, Paint paint, int x1, int y1, int x2, int y2) { if (sDebugLines== null) { sDebugLines = new float[16]; } - x2--; - y2--; - sDebugLines[0] = x1; sDebugLines[1] = y1; sDebugLines[2] = x2; @@ -6353,18 +6405,18 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager sDebugLines[4] = x2; sDebugLines[5] = y1; sDebugLines[6] = x2; - sDebugLines[7] = y2 + 1; + sDebugLines[7] = y2; - sDebugLines[8] = x2 + 1; + sDebugLines[8] = x2; sDebugLines[9] = y2; sDebugLines[10] = x1; sDebugLines[11] = y2; - sDebugLines[12] = x1; - sDebugLines[13] = y2; + sDebugLines[12] = x1; + sDebugLines[13] = y2; sDebugLines[14] = x1; sDebugLines[15] = y1; - return sDebugLines; + canvas.drawLines(sDebugLines, paint); } } diff --git a/core/java/android/view/ViewParent.java b/core/java/android/view/ViewParent.java index ddff91d..4b70bc0 100644 --- a/core/java/android/view/ViewParent.java +++ b/core/java/android/view/ViewParent.java @@ -295,4 +295,105 @@ public interface ViewParent { * @hide */ public void childAccessibilityStateChanged(View child); + + /** + * Tells if this view parent can resolve the layout direction. + * See {@link View#setLayoutDirection(int)} + * + * @return True if this view parent can resolve the layout direction. + * + * @hide + */ + public boolean canResolveLayoutDirection(); + + /** + * Tells if this view parent layout direction is resolved. + * See {@link View#setLayoutDirection(int)} + * + * @return True if this view parent layout direction is resolved. + * + * @hide + */ + public boolean isLayoutDirectionResolved(); + + /** + * Return this view parent layout direction. See {@link View#getLayoutDirection()} + * + * @return {@link View#LAYOUT_DIRECTION_RTL} if the layout direction is RTL or returns + * {@link View#LAYOUT_DIRECTION_LTR} if the layout direction is not RTL. + * + * @hide + */ + public int getLayoutDirection(); + + /** + * Tells if this view parent can resolve the text direction. + * See {@link View#setTextDirection(int)} + * + * @return True if this view parent can resolve the text direction. + * + * @hide + */ + public boolean canResolveTextDirection(); + + /** + * Tells if this view parent text direction is resolved. + * See {@link View#setTextDirection(int)} + * + * @return True if this view parent text direction is resolved. + * + * @hide + */ + public boolean isTextDirectionResolved(); + + /** + * Return this view parent text direction. See {@link View#getTextDirection()} + * + * @return the resolved text direction. Returns one of: + * + * {@link View#TEXT_DIRECTION_FIRST_STRONG} + * {@link View#TEXT_DIRECTION_ANY_RTL}, + * {@link View#TEXT_DIRECTION_LTR}, + * {@link View#TEXT_DIRECTION_RTL}, + * {@link View#TEXT_DIRECTION_LOCALE} + * + * @hide + */ + public int getTextDirection(); + + /** + * Tells if this view parent can resolve the text alignment. + * See {@link View#setTextAlignment(int)} + * + * @return True if this view parent can resolve the text alignment. + * + * @hide + */ + public boolean canResolveTextAlignment(); + + /** + * Tells if this view parent text alignment is resolved. + * See {@link View#setTextAlignment(int)} + * + * @return True if this view parent text alignment is resolved. + * + * @hide + */ + public boolean isTextAlignmentResolved(); + + /** + * Return this view parent text alignment. See {@link android.view.View#getTextAlignment()} + * + * @return the resolved text alignment. Returns one of: + * + * {@link View#TEXT_ALIGNMENT_GRAVITY}, + * {@link View#TEXT_ALIGNMENT_CENTER}, + * {@link View#TEXT_ALIGNMENT_TEXT_START}, + * {@link View#TEXT_ALIGNMENT_TEXT_END}, + * {@link View#TEXT_ALIGNMENT_VIEW_START}, + * {@link View#TEXT_ALIGNMENT_VIEW_END} + * + * @hide + */ + public int getTextAlignment(); } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 3b91e00..b8fae86 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -114,7 +114,7 @@ public final class ViewRootImpl implements ViewParent, * Set this system property to true to force the view hierarchy to render * at 60 Hz. This can be used to measure the potential framerate. */ - private static final String PROPERTY_PROFILE_RENDERING = "viewancestor.profile_rendering"; + private static final String PROPERTY_PROFILE_RENDERING = "viewancestor.profile_rendering"; private static final boolean MEASURE_LATENCY = false; private static LatencyTimer lt; @@ -139,6 +139,7 @@ public final class ViewRootImpl implements ViewParent, final IWindowSession mWindowSession; final Display mDisplay; + final String mBasePackageName; long mLastTrackballTime = 0; final TrackballAxis mTrackballAxisX = new TrackballAxis(); @@ -169,9 +170,6 @@ public final class ViewRootImpl implements ViewParent, int mSeq; View mView; - View mFocusedView; - View mRealFocusedView; // this is not set to null in touch mode - View mOldFocusedView; View mAccessibilityFocusedHost; AccessibilityNodeInfo mAccessibilityFocusedVirtualView; @@ -231,6 +229,7 @@ public final class ViewRootImpl implements ViewParent, boolean mIsDrawing; int mLastSystemUiVisibility; int mClientWindowLayoutFlags; + boolean mLastOverscanRequested; /** @hide */ public static final int EVENT_NOT_HANDLED = 0; @@ -264,6 +263,7 @@ public final class ViewRootImpl implements ViewParent, // These are accessed by multiple threads. final Rect mWinFrame; // frame given by window manager. + final Rect mPendingOverscanInsets = new Rect(); final Rect mPendingVisibleInsets = new Rect(); final Rect mPendingContentInsets = new Rect(); final ViewTreeObserver.InternalInsetsInfo mLastGivenInsets @@ -276,7 +276,7 @@ public final class ViewRootImpl implements ViewParent, boolean mScrollMayChange; int mSoftInputMode; - View mLastScrolledFocus; + WeakReference<View> mLastScrolledFocus; int mScrollY; int mCurScrollY; Scroller mScroller; @@ -296,15 +296,15 @@ public final class ViewRootImpl implements ViewParent, final PointF mLastTouchPoint = new PointF(); private boolean mProfileRendering; - private Thread mRenderProfiler; - private volatile boolean mRenderProfilingEnabled; + private Choreographer.FrameCallback mRenderProfiler; + private boolean mRenderProfilingEnabled; // Variables to track frames per second, enabled via DEBUG_FPS flag private long mFpsStartTime = -1; private long mFpsPrevTime = -1; private int mFpsNumFrames; - private final ArrayList<DisplayList> mDisplayLists = new ArrayList<DisplayList>(24); + private final ArrayList<DisplayList> mDisplayLists = new ArrayList<DisplayList>(); /** * see {@link #playSoundEffect(int)} @@ -324,6 +324,10 @@ public final class ViewRootImpl implements ViewParent, private final int mDensity; private final int mNoncompatDensity; + private boolean mInLayout = false; + ArrayList<View> mLayoutRequesters = new ArrayList<View>(); + boolean mHandlingLayoutInLayoutRequest = false; + private int mViewLayoutDirectionInitial; /** @@ -354,6 +358,7 @@ public final class ViewRootImpl implements ViewParent, // allow the spawning of threads. mWindowSession = WindowManagerGlobal.getWindowSession(context.getMainLooper()); mDisplay = display; + mBasePackageName = context.getBasePackageName(); CompatibilityInfoHolder cih = display.getCompatibilityInfo(); mCompatibilityInfo = cih != null ? cih : new CompatibilityInfoHolder(); @@ -385,8 +390,6 @@ public final class ViewRootImpl implements ViewParent, mDensity = context.getResources().getDisplayMetrics().densityDpi; mNoncompatDensity = context.getResources().getDisplayMetrics().noncompatDensityDpi; mFallbackEventHandler = PolicyManager.makeNewFallbackEventHandler(context); - mProfileRendering = Boolean.parseBoolean( - SystemProperties.get(PROPERTY_PROFILE_RENDERING, "false")); mChoreographer = Choreographer.getInstance(); PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); @@ -478,6 +481,9 @@ public final class ViewRootImpl implements ViewParent, mViewLayoutDirectionInitial = mView.getRawLayoutDirection(); mFallbackEventHandler.setView(view); mWindowAttributes.copyFrom(attrs); + if (mWindowAttributes.packageName == null) { + mWindowAttributes.packageName = mBasePackageName; + } attrs = mWindowAttributes; // Keep track of the actual window flags supplied by the client. mClientWindowLayoutFlags = attrs.flags; @@ -562,6 +568,7 @@ public final class ViewRootImpl implements ViewParent, if (mTranslator != null) { mTranslator.translateRectInScreenToAppWindow(mAttachInfo.mContentInsets); } + mPendingOverscanInsets.set(0, 0, 0, 0); mPendingContentInsets.set(mAttachInfo.mContentInsets); mPendingVisibleInsets.set(0, 0, 0, 0); if (DEBUG_LAYOUT) Log.v(TAG, "Added window " + mWindow); @@ -740,6 +747,7 @@ public final class ViewRootImpl implements ViewParent, final boolean translucent = attrs.format != PixelFormat.OPAQUE; mAttachInfo.mHardwareRenderer = HardwareRenderer.createGlRenderer(2, translucent); + mAttachInfo.mHardwareRenderer.setName(attrs.getTitle().toString()); mAttachInfo.mHardwareAccelerated = mAttachInfo.mHardwareAccelerationRequested = mAttachInfo.mHardwareRenderer != null; @@ -774,6 +782,9 @@ public final class ViewRootImpl implements ViewParent, attrs.systemUiVisibility = mWindowAttributes.systemUiVisibility; attrs.subtreeSystemUiVisibility = mWindowAttributes.subtreeSystemUiVisibility; mWindowAttributesChangesFlag = mWindowAttributes.copyFrom(attrs); + if (mWindowAttributes.packageName == null) { + mWindowAttributes.packageName = mBasePackageName; + } mWindowAttributes.flags |= compatibleWindowFlag; applyKeepScreenOnFlag(mWindowAttributes); @@ -830,9 +841,11 @@ public final class ViewRootImpl implements ViewParent, @Override public void requestLayout() { - checkThread(); - mLayoutRequested = true; - scheduleTraversals(); + if (!mHandlingLayoutInLayoutRequest) { + checkThread(); + mLayoutRequested = true; + scheduleTraversals(); + } } @Override @@ -1245,6 +1258,9 @@ public final class ViewRootImpl implements ViewParent, mAttachInfo.mInTouchMode = !mAddedTouchMode; ensureTouchModeLocally(mAddedTouchMode); } else { + if (!mPendingOverscanInsets.equals(mAttachInfo.mOverscanInsets)) { + insetsChanged = true; + } if (!mPendingContentInsets.equals(mAttachInfo.mContentInsets)) { insetsChanged = true; } @@ -1310,15 +1326,20 @@ public final class ViewRootImpl implements ViewParent, } } - if (params != null && (host.mPrivateFlags & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) != 0) { - if (!PixelFormat.formatHasAlpha(params.format)) { - params.format = PixelFormat.TRANSLUCENT; + if (params != null) { + if ((host.mPrivateFlags & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) != 0) { + if (!PixelFormat.formatHasAlpha(params.format)) { + params.format = PixelFormat.TRANSLUCENT; + } } + mAttachInfo.mOverscanRequested = (params.flags + & WindowManager.LayoutParams.FLAG_LAYOUT_IN_OVERSCAN) != 0; } if (mFitSystemWindowsRequested) { mFitSystemWindowsRequested = false; mFitSystemWindowsInsets.set(mAttachInfo.mContentInsets); + mLastOverscanRequested = mAttachInfo.mOverscanRequested; host.fitSystemWindows(mFitSystemWindowsInsets); if (mLayoutRequested) { // Short-circuit catching a new layout request here, so @@ -1373,7 +1394,6 @@ public final class ViewRootImpl implements ViewParent, boolean hwInitialized = false; boolean contentInsetsChanged = false; - boolean visibleInsetsChanged; boolean hadSurface = mSurface.isValid(); try { @@ -1386,6 +1406,7 @@ public final class ViewRootImpl implements ViewParent, relayoutResult = relayoutWindow(params, viewVisibility, insetsPending); if (DEBUG_LAYOUT) Log.v(TAG, "relayout: frame=" + frame.toShortString() + + " overscan=" + mPendingOverscanInsets.toShortString() + " content=" + mPendingContentInsets.toShortString() + " visible=" + mPendingVisibleInsets.toShortString() + " surface=" + mSurface); @@ -1397,9 +1418,11 @@ public final class ViewRootImpl implements ViewParent, mPendingConfiguration.seq = 0; } + final boolean overscanInsetsChanged = !mPendingOverscanInsets.equals( + mAttachInfo.mOverscanInsets); contentInsetsChanged = !mPendingContentInsets.equals( mAttachInfo.mContentInsets); - visibleInsetsChanged = !mPendingVisibleInsets.equals( + final boolean visibleInsetsChanged = !mPendingVisibleInsets.equals( mAttachInfo.mVisibleInsets); if (contentInsetsChanged) { if (mWidth > 0 && mHeight > 0 && lp != null && @@ -1427,8 +1450,6 @@ public final class ViewRootImpl implements ViewParent, } // TODO: should handle create/resize failure layerCanvas = mResizeBuffer.start(hwRendererCanvas); - layerCanvas.setViewport(mWidth, mHeight); - layerCanvas.onPreDraw(null); final int restoreCount = layerCanvas.save(); int yoff; @@ -1465,9 +1486,6 @@ public final class ViewRootImpl implements ViewParent, } catch (OutOfMemoryError e) { Log.w(TAG, "Not enough memory for content change anim buffer", e); } finally { - if (layerCanvas != null) { - layerCanvas.onPostDraw(); - } if (mResizeBuffer != null) { mResizeBuffer.end(hwRendererCanvas); if (!completed) { @@ -1481,9 +1499,18 @@ public final class ViewRootImpl implements ViewParent, if (DEBUG_LAYOUT) Log.v(TAG, "Content insets changing to: " + mAttachInfo.mContentInsets); } + if (overscanInsetsChanged) { + mAttachInfo.mOverscanInsets.set(mPendingOverscanInsets); + if (DEBUG_LAYOUT) Log.v(TAG, "Overscan insets changing to: " + + mAttachInfo.mOverscanInsets); + // Need to relayout with content insets. + contentInsetsChanged = true; + } if (contentInsetsChanged || mLastSystemUiVisibility != - mAttachInfo.mSystemUiVisibility || mFitSystemWindowsRequested) { + mAttachInfo.mSystemUiVisibility || mFitSystemWindowsRequested + || mLastOverscanRequested != mAttachInfo.mOverscanRequested) { mLastSystemUiVisibility = mAttachInfo.mSystemUiVisibility; + mLastOverscanRequested = mAttachInfo.mOverscanRequested; mFitSystemWindowsRequested = false; mFitSystemWindowsInsets.set(mAttachInfo.mContentInsets); host.fitSystemWindows(mFitSystemWindowsInsets); @@ -1512,16 +1539,7 @@ public final class ViewRootImpl implements ViewParent, hwInitialized = mAttachInfo.mHardwareRenderer.initialize( mHolder.getSurface()); } catch (Surface.OutOfResourcesException e) { - Log.e(TAG, "OutOfResourcesException initializing HW surface", e); - try { - if (!mWindowSession.outOfMemory(mWindow) && - Process.myUid() != Process.SYSTEM_UID) { - Slog.w(TAG, "No processes killed for memory; killing self"); - Process.killProcess(Process.myPid()); - } - } catch (RemoteException ex) { - } - mLayoutRequested = true; // ask wm for a new surface next time. + handleOutOfResourcesException(e); return; } } @@ -1529,7 +1547,9 @@ public final class ViewRootImpl implements ViewParent, } else if (!mSurface.isValid()) { // If the surface has been removed, then reset the scroll // positions. - mLastScrolledFocus = null; + if (mLastScrolledFocus != null) { + mLastScrolledFocus.clear(); + } mScrollY = mCurScrollY = 0; if (mScroller != null) { mScroller.abortAnimation(); @@ -1546,15 +1566,7 @@ public final class ViewRootImpl implements ViewParent, try { mAttachInfo.mHardwareRenderer.updateSurface(mHolder.getSurface()); } catch (Surface.OutOfResourcesException e) { - Log.e(TAG, "OutOfResourcesException updating HW surface", e); - try { - if (!mWindowSession.outOfMemory(mWindow)) { - Slog.w(TAG, "No processes killed for memory; killing self"); - Process.killProcess(Process.myPid()); - } - } catch (RemoteException ex) { - } - mLayoutRequested = true; // ask wm for a new surface next time. + handleOutOfResourcesException(e); return; } } @@ -1718,7 +1730,7 @@ public final class ViewRootImpl implements ViewParent, boolean triggerGlobalLayoutListener = didLayout || attachInfo.mRecomputeGlobalAttributes; if (didLayout) { - performLayout(); + performLayout(lp, desiredWindowWidth, desiredWindowHeight); // By this point all views have been sized and positionned // We can compute the transparent area @@ -1805,13 +1817,11 @@ public final class ViewRootImpl implements ViewParent, if (mView != null) { if (!mView.hasFocus()) { mView.requestFocus(View.FOCUS_FORWARD); - mFocusedView = mRealFocusedView = mView.findFocus(); if (DEBUG_INPUT_RESIZE) Log.v(TAG, "First: requested focused view=" - + mFocusedView); + + mView.findFocus()); } else { - mRealFocusedView = mView.findFocus(); if (DEBUG_INPUT_RESIZE) Log.v(TAG, "First: existing focused view=" - + mRealFocusedView); + + mView.findFocus()); } } if ((relayoutResult & WindowManagerGlobal.RELAYOUT_RES_ANIMATING) != 0) { @@ -1878,6 +1888,19 @@ public final class ViewRootImpl implements ViewParent, mIsInTraversal = false; } + private void handleOutOfResourcesException(Surface.OutOfResourcesException e) { + Log.e(TAG, "OutOfResourcesException initializing HW surface", e); + try { + if (!mWindowSession.outOfMemory(mWindow) && + Process.myUid() != Process.SYSTEM_UID) { + Slog.w(TAG, "No processes killed for memory; killing self"); + Process.killProcess(Process.myPid()); + } + } catch (RemoteException ex) { + } + mLayoutRequested = true; // ask wm for a new surface next time. + } + private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure"); try { @@ -1887,9 +1910,66 @@ public final class ViewRootImpl implements ViewParent, } } - private void performLayout() { + /** + * Called by {@link android.view.View#isInLayout()} to determine whether the view hierarchy + * is currently undergoing a layout pass. + * + * @return whether the view hierarchy is currently undergoing a layout pass + */ + boolean isInLayout() { + return mInLayout; + } + + /** + * Called by {@link android.view.View#requestLayout()} if the view hierarchy is currently + * undergoing a layout pass. requestLayout() should not generally be called during layout, + * unless the container hierarchy knows what it is doing (i.e., it is fine as long as + * all children in that container hierarchy are measured and laid out at the end of the layout + * pass for that container). If requestLayout() is called anyway, we handle it correctly + * by registering all requesters during a frame as it proceeds. At the end of the frame, + * we check all of those views to see if any still have pending layout requests, which + * indicates that they were not correctly handled by their container hierarchy. If that is + * the case, we clear all such flags in the tree, to remove the buggy flag state that leads + * to blank containers, and force a second request/measure/layout pass in this frame. If + * more requestLayout() calls are received during that second layout pass, we post those + * requests to the next frame to avoid possible infinite loops. + * + * <p>The return value from this method indicates whether the request should proceed + * (if it is a request during the first layout pass) or should be skipped and posted to the + * next frame (if it is a request during the second layout pass).</p> + * + * @param view the view that requested the layout. + * + * @return true if request should proceed, false otherwise. + */ + boolean requestLayoutDuringLayout(final View view) { + if (view.mParent == null || view.mAttachInfo == null) { + // Would not normally trigger another layout, so just let it pass through as usual + return true; + } + if (!mHandlingLayoutInLayoutRequest) { + if (!mLayoutRequesters.contains(view)) { + mLayoutRequesters.add(view); + } + return true; + } else { + Log.w("View", "requestLayout() called by " + view + " during second layout pass: " + + "posting to next frame"); + view.post(new Runnable() { + @Override + public void run() { + view.requestLayout(); + } + }); + return false; + } + } + + private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, + int desiredWindowHeight) { mLayoutRequested = false; mScrollMayChange = true; + mInLayout = true; final View host = mView; if (DEBUG_ORIENTATION || DEBUG_LAYOUT) { @@ -1900,9 +1980,72 @@ public final class ViewRootImpl implements ViewParent, Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout"); try { host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); + + mInLayout = false; + int numViewsRequestingLayout = mLayoutRequesters.size(); + if (numViewsRequestingLayout > 0) { + // requestLayout() was called during layout. + // If no layout-request flags are set on the requesting views, there is no problem. + // If some requests are still pending, then we need to clear those flags and do + // a full request/measure/layout pass to handle this situation. + + // Check state of layout flags for all requesters + ArrayList<View> mValidLayoutRequesters = null; + for (int i = 0; i < numViewsRequestingLayout; ++i) { + View view = mLayoutRequesters.get(i); + if ((view.mPrivateFlags & View.PFLAG_FORCE_LAYOUT) == View.PFLAG_FORCE_LAYOUT) { + while (view != null && view.mAttachInfo != null && view.mParent != null && + (view.mPrivateFlags & View.PFLAG_FORCE_LAYOUT) != 0) { + if ((view.mViewFlags & View.VISIBILITY_MASK) != View.GONE) { + // Only trigger new requests for non-GONE views + Log.w(TAG, "requestLayout() improperly called during " + + "layout: running second layout pass for " + view); + if (mValidLayoutRequesters == null) { + mValidLayoutRequesters = new ArrayList<View>(); + } + mValidLayoutRequesters.add(view); + break; + } + if (view.mParent instanceof View) { + view = (View) view.mParent; + } else { + view = null; + } + } + } + } + if (mValidLayoutRequesters != null) { + // Clear flags throughout hierarchy, walking up from each flagged requester + for (int i = 0; i < numViewsRequestingLayout; ++i) { + View view = mLayoutRequesters.get(i); + while (view != null && + (view.mPrivateFlags & View.PFLAG_FORCE_LAYOUT) != 0) { + view.mPrivateFlags &= ~View.PFLAG_FORCE_LAYOUT; + if (view.mParent instanceof View) { + view = (View) view.mParent; + } else { + view = null; + } + } + } + // Process fresh layout requests, then measure and layout + mHandlingLayoutInLayoutRequest = true; + int numValidRequests = mValidLayoutRequesters.size(); + for (int i = 0; i < numValidRequests; ++i) { + mValidLayoutRequesters.get(i).requestLayout(); + } + measureHierarchy(host, lp, mView.getContext().getResources(), + desiredWindowWidth, desiredWindowHeight); + mInLayout = true; + host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); + mHandlingLayoutInLayoutRequest = false; + } + mLayoutRequesters.clear(); + } } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } + mInLayout = false; } public void requestTransparentRegion(View child) { @@ -1985,31 +2128,25 @@ public final class ViewRootImpl implements ViewParent, private void profileRendering(boolean enabled) { if (mProfileRendering) { mRenderProfilingEnabled = enabled; - if (mRenderProfiler == null) { - mRenderProfiler = new Thread(new Runnable() { - @Override - public void run() { - Log.d(TAG, "Starting profiling thread"); - while (mRenderProfilingEnabled) { - mAttachInfo.mHandler.post(new Runnable() { - @Override - public void run() { - mDirty.set(0, 0, mWidth, mHeight); - scheduleTraversals(); - } - }); - try { - // TODO: This should use vsync when we get an API - Thread.sleep(15); - } catch (InterruptedException e) { - Log.d(TAG, "Exiting profiling thread"); - } + + if (mRenderProfiler != null) { + mChoreographer.removeFrameCallback(mRenderProfiler); + } + if (mRenderProfilingEnabled) { + if (mRenderProfiler == null) { + mRenderProfiler = new Choreographer.FrameCallback() { + @Override + public void doFrame(long frameTimeNanos) { + mDirty.set(0, 0, mWidth, mHeight); + scheduleTraversals(); + if (mRenderProfilingEnabled) { + mChoreographer.postFrameCallback(mRenderProfiler); + } } - } - }, "Rendering Profiler"); - mRenderProfiler.start(); + }; + } + mChoreographer.postFrameCallback(mRenderProfiler); } else { - mRenderProfiler.interrupt(); mRenderProfiler = null; } } @@ -2085,7 +2222,7 @@ public final class ViewRootImpl implements ViewParent, private void draw(boolean fullRedrawNeeded) { Surface surface = mSurface; - if (surface == null || !surface.isValid()) { + if (!surface.isValid()) { return; } @@ -2166,6 +2303,8 @@ public final class ViewRootImpl implements ViewParent, appScale + ", width=" + mWidth + ", height=" + mHeight); } + invalidateDisplayLists(); + attachInfo.mTreeObserver.dispatchOnDraw(); if (!dirty.isEmpty() || mIsAnimating) { @@ -2184,8 +2323,35 @@ public final class ViewRootImpl implements ViewParent, animating ? null : mCurrentDirty)) { mPreviousDirty.set(0, 0, mWidth, mHeight); } - } else if (!drawSoftware(surface, attachInfo, yoff, scalingRequired, dirty)) { - return; + } else { + // If we get here with a disabled & requested hardware renderer, something went + // wrong (an invalidate posted right before we destroyed the hardware surface + // for instance) so we should just bail out. Locking the surface with software + // rendering at this point would lock it forever and prevent hardware renderer + // from doing its job when it comes back. + // Before we request a new frame we must however attempt to reinitiliaze the + // hardware renderer if it's in requested state. This would happen after an + // eglTerminate() for instance. + if (attachInfo.mHardwareRenderer != null && + !attachInfo.mHardwareRenderer.isEnabled() && + attachInfo.mHardwareRenderer.isRequested()) { + + try { + attachInfo.mHardwareRenderer.initializeIfNeeded(mWidth, mHeight, + mHolder.getSurface()); + } catch (Surface.OutOfResourcesException e) { + handleOutOfResourcesException(e); + return; + } + + mFullRedrawNeeded = true; + scheduleTraversals(); + return; + } + + if (!drawSoftware(surface, attachInfo, yoff, scalingRequired, dirty)) { + return; + } } } @@ -2201,18 +2367,6 @@ public final class ViewRootImpl implements ViewParent, private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int yoff, boolean scalingRequired, Rect dirty) { - // If we get here with a disabled & requested hardware renderer, something went - // wrong (an invalidate posted right before we destroyed the hardware surface - // for instance) so we should just bail out. Locking the surface with software - // rendering at this point would lock it forever and prevent hardware renderer - // from doing its job when it comes back. - if (attachInfo.mHardwareRenderer != null && !attachInfo.mHardwareRenderer.isEnabled() && - attachInfo.mHardwareRenderer.isRequested()) { - mFullRedrawNeeded = true; - scheduleTraversals(); - return false; - } - // Draw with software renderer. Canvas canvas; try { @@ -2231,15 +2385,7 @@ public final class ViewRootImpl implements ViewParent, // TODO: Do this in native canvas.setDensity(mDensity); } catch (Surface.OutOfResourcesException e) { - Log.e(TAG, "OutOfResourcesException locking surface", e); - try { - if (!mWindowSession.outOfMemory(mWindow)) { - Slog.w(TAG, "No processes killed for memory; killing self"); - Process.killProcess(Process.myPid()); - } - } catch (RemoteException ex) { - } - mLayoutRequested = true; // ask wm for a new surface next time. + handleOutOfResourcesException(e); return false; } catch (IllegalArgumentException e) { Log.e(TAG, "Could not lock surface", e); @@ -2376,8 +2522,9 @@ public final class ViewRootImpl implements ViewParent, for (int i = 0; i < count; i++) { final DisplayList displayList = displayLists.get(i); - displayList.invalidate(); - displayList.clear(); + if (displayList.isDirty()) { + displayList.clear(); + } } displayLists.clear(); @@ -2403,17 +2550,12 @@ public final class ViewRootImpl implements ViewParent, // requestChildRectangleOnScreen() call (in which case 'rectangle' // is non-null and we just want to scroll to whatever that // rectangle is). - View focus = mRealFocusedView; - - // When in touch mode, focus points to the previously focused view, - // which may have been removed from the view hierarchy. The following - // line checks whether the view is still in our hierarchy. - if (focus == null || focus.mAttachInfo != mAttachInfo) { - mRealFocusedView = null; + View focus = mView.findFocus(); + if (focus == null) { return false; } - - if (focus != mLastScrolledFocus) { + View lastScrolledFocus = (mLastScrolledFocus != null) ? mLastScrolledFocus.get() : null; + if (lastScrolledFocus != null && focus != lastScrolledFocus) { // If the focus has changed, then ignore any requests to scroll // to a rectangle; first we want to make sure the entire focus // view is visible. @@ -2422,8 +2564,7 @@ public final class ViewRootImpl implements ViewParent, if (DEBUG_INPUT_RESIZE) Log.v(TAG, "Eval scroll: focus=" + focus + " rectangle=" + rectangle + " ci=" + ci + " vi=" + vi); - if (focus == mLastScrolledFocus && !mScrollMayChange - && rectangle == null) { + if (focus == lastScrolledFocus && !mScrollMayChange && rectangle == null) { // Optimization: if the focus hasn't changed since last // time, and no layout has happened, then just leave things // as they are. @@ -2433,7 +2574,7 @@ public final class ViewRootImpl implements ViewParent, // We need to determine if the currently focused view is // within the visible part of the window and, if not, apply // a pan so it can be seen. - mLastScrolledFocus = focus; + mLastScrolledFocus = new WeakReference<View>(focus); mScrollMayChange = false; if (DEBUG_INPUT_RESIZE) Log.v(TAG, "Need to scroll?"); // Try to find the rectangle from the focus view. @@ -2560,33 +2701,19 @@ public final class ViewRootImpl implements ViewParent, } public void requestChildFocus(View child, View focused) { - checkThread(); - if (DEBUG_INPUT_RESIZE) { Log.v(TAG, "Request child focus: focus now " + focused); } - - mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(mOldFocusedView, focused); + checkThread(); scheduleTraversals(); - - mFocusedView = mRealFocusedView = focused; } public void clearChildFocus(View child) { - checkThread(); - if (DEBUG_INPUT_RESIZE) { Log.v(TAG, "Clearing child focus"); } - - mOldFocusedView = mFocusedView; - - // Invoke the listener only if there is no view to take focus - if (focusSearch(null, View.FOCUS_FORWARD) == null) { - mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(mOldFocusedView, null); - } - - mFocusedView = mRealFocusedView = null; + checkThread(); + scheduleTraversals(); } @Override @@ -2603,14 +2730,13 @@ public final class ViewRootImpl implements ViewParent, // the one case where will transfer focus away from the current one // is if the current view is a view group that prefers to give focus // to its children first AND the view is a descendant of it. - mFocusedView = mView.findFocus(); - boolean descendantsHaveDibsOnFocus = - (mFocusedView instanceof ViewGroup) && - (((ViewGroup) mFocusedView).getDescendantFocusability() == - ViewGroup.FOCUS_AFTER_DESCENDANTS); - if (descendantsHaveDibsOnFocus && isViewDescendantOf(v, mFocusedView)) { - // If a view gets the focus, the listener will be invoked from requestChildFocus() - v.requestFocus(); + View focused = mView.findFocus(); + if (focused instanceof ViewGroup) { + ViewGroup group = (ViewGroup) focused; + if (group.getDescendantFocusability() == ViewGroup.FOCUS_AFTER_DESCENDANTS + && isViewDescendantOf(v, focused)) { + v.requestFocus(); + } } } } @@ -2751,11 +2877,10 @@ public final class ViewRootImpl implements ViewParent, private final static int MSG_UPDATE_CONFIGURATION = 18; private final static int MSG_PROCESS_INPUT_EVENTS = 19; private final static int MSG_DISPATCH_SCREEN_STATE = 20; - private final static int MSG_INVALIDATE_DISPLAY_LIST = 21; - private final static int MSG_CLEAR_ACCESSIBILITY_FOCUS_HOST = 22; - private final static int MSG_DISPATCH_DONE_ANIMATING = 23; - private final static int MSG_INVALIDATE_WORLD = 24; - private final static int MSG_WINDOW_MOVED = 25; + private final static int MSG_CLEAR_ACCESSIBILITY_FOCUS_HOST = 21; + private final static int MSG_DISPATCH_DONE_ANIMATING = 22; + private final static int MSG_INVALIDATE_WORLD = 23; + private final static int MSG_WINDOW_MOVED = 24; final class ViewRootHandler extends Handler { @Override @@ -2801,8 +2926,6 @@ public final class ViewRootImpl implements ViewParent, return "MSG_PROCESS_INPUT_EVENTS"; case MSG_DISPATCH_SCREEN_STATE: return "MSG_DISPATCH_SCREEN_STATE"; - case MSG_INVALIDATE_DISPLAY_LIST: - return "MSG_INVALIDATE_DISPLAY_LIST"; case MSG_CLEAR_ACCESSIBILITY_FOCUS_HOST: return "MSG_CLEAR_ACCESSIBILITY_FOCUS_HOST"; case MSG_DISPATCH_DONE_ANIMATING: @@ -2822,7 +2945,7 @@ public final class ViewRootImpl implements ViewParent, case MSG_INVALIDATE_RECT: final View.AttachInfo.InvalidateInfo info = (View.AttachInfo.InvalidateInfo) msg.obj; info.target.invalidate(info.left, info.top, info.right, info.bottom); - info.release(); + info.recycle(); break; case MSG_IME_FINISHED_EVENT: handleImeFinishedEvent(msg.arg1, msg.arg2 != 0); @@ -2841,6 +2964,7 @@ public final class ViewRootImpl implements ViewParent, // Recycled in the fall through... SomeArgs args = (SomeArgs) msg.obj; if (mWinFrame.equals(args.arg1) + && mPendingOverscanInsets.equals(args.arg5) && mPendingContentInsets.equals(args.arg2) && mPendingVisibleInsets.equals(args.arg3) && args.arg4 == null) { @@ -2857,6 +2981,7 @@ public final class ViewRootImpl implements ViewParent, } mWinFrame.set((Rect) args.arg1); + mPendingOverscanInsets.set((Rect) args.arg5); mPendingContentInsets.set((Rect) args.arg2); mPendingVisibleInsets.set((Rect) args.arg3); @@ -2905,10 +3030,8 @@ public final class ViewRootImpl implements ViewParent, mSurface != null && mSurface.isValid()) { mFullRedrawNeeded = true; try { - if (mAttachInfo.mHardwareRenderer.initializeIfNeeded( - mWidth, mHeight, mHolder.getSurface())) { - mFullRedrawNeeded = true; - } + mAttachInfo.mHardwareRenderer.initializeIfNeeded( + mWidth, mHeight, mHolder.getSurface()); } catch (Surface.OutOfResourcesException e) { Log.e(TAG, "OutOfResourcesException locking surface", e); try { @@ -3009,7 +3132,7 @@ public final class ViewRootImpl implements ViewParent, handleDragEvent(event); } break; case MSG_DISPATCH_SYSTEM_UI_VISIBILITY: { - handleDispatchSystemUiVisibilityChanged((SystemUiVisibilityInfo)msg.obj); + handleDispatchSystemUiVisibilityChanged((SystemUiVisibilityInfo) msg.obj); } break; case MSG_UPDATE_CONFIGURATION: { Configuration config = (Configuration)msg.obj; @@ -3023,9 +3146,6 @@ public final class ViewRootImpl implements ViewParent, handleScreenStateChange(msg.arg1 == 1); } } break; - case MSG_INVALIDATE_DISPLAY_LIST: { - invalidateDisplayLists(); - } break; case MSG_CLEAR_ACCESSIBILITY_FOCUS_HOST: { setAccessibilityFocus(null, null); } break; @@ -3094,7 +3214,6 @@ public final class ViewRootImpl implements ViewParent, // set yet. final View focused = mView.findFocus(); if (focused != null && !focused.isFocusableInTouchMode()) { - final ViewGroup ancestorToTakeFocus = findAncestorToTakeFocusInTouchMode(focused); if (ancestorToTakeFocus != null) { @@ -3103,10 +3222,7 @@ public final class ViewRootImpl implements ViewParent, return ancestorToTakeFocus.requestFocus(); } else { // nothing appropriate to have focus in touch mode, clear it out - mView.unFocus(); - mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(focused, null); - mFocusedView = null; - mOldFocusedView = null; + focused.unFocus(); return true; } } @@ -3141,12 +3257,11 @@ public final class ViewRootImpl implements ViewParent, private boolean leaveTouchMode() { if (mView != null) { if (mView.hasFocus()) { - // i learned the hard way to not trust mFocusedView :) - mFocusedView = mView.findFocus(); - if (!(mFocusedView instanceof ViewGroup)) { + View focusedView = mView.findFocus(); + if (!(focusedView instanceof ViewGroup)) { // some view has focus, let it keep it return false; - } else if (((ViewGroup)mFocusedView).getDescendantFocusability() != + } else if (((ViewGroup) focusedView).getDescendantFocusability() != ViewGroup.FOCUS_AFTER_DESCENDANTS) { // some view group has focus, and doesn't prefer its children // over itself for focus, so let them keep it. @@ -3940,7 +4055,7 @@ public final class ViewRootImpl implements ViewParent, (int) (mView.getMeasuredWidth() * appScale + 0.5f), (int) (mView.getMeasuredHeight() * appScale + 0.5f), viewVisibility, insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, - mWinFrame, mPendingContentInsets, mPendingVisibleInsets, + mWinFrame, mPendingOverscanInsets, mPendingContentInsets, mPendingVisibleInsets, mPendingConfiguration, mSurface); //Log.d(TAG, "<<<<<< BACK FROM relayout"); if (restore) { @@ -3949,6 +4064,7 @@ public final class ViewRootImpl implements ViewParent, if (mTranslator != null) { mTranslator.translateRectInScreenToAppWinFrame(mWinFrame); + mTranslator.translateRectInScreenToAppWindow(mPendingOverscanInsets); mTranslator.translateRectInScreenToAppWindow(mPendingContentInsets); mTranslator.translateRectInScreenToAppWindow(mPendingVisibleInsets); } @@ -4066,6 +4182,7 @@ public final class ViewRootImpl implements ViewParent, } if (mAdded && !mFirst) { + invalidateDisplayLists(); destroyHardwareRenderer(); if (mView != null) { @@ -4098,14 +4215,30 @@ public final class ViewRootImpl implements ViewParent, } public void loadSystemProperties() { - boolean layout = SystemProperties.getBoolean( - View.DEBUG_LAYOUT_PROPERTY, false); - if (layout != mAttachInfo.mDebugLayout) { - mAttachInfo.mDebugLayout = layout; - if (!mHandler.hasMessages(MSG_INVALIDATE_WORLD)) { - mHandler.sendEmptyMessageDelayed(MSG_INVALIDATE_WORLD, 200); + mHandler.post(new Runnable() { + @Override + public void run() { + // Profiling + mProfileRendering = SystemProperties.getBoolean(PROPERTY_PROFILE_RENDERING, false); + profileRendering(mAttachInfo.mHasWindowFocus); + + // Hardware rendering + if (mAttachInfo.mHardwareRenderer != null) { + if (mAttachInfo.mHardwareRenderer.loadSystemProperties(mHolder.getSurface())) { + invalidate(); + } + } + + // Layout debugging + boolean layout = SystemProperties.getBoolean(View.DEBUG_LAYOUT_PROPERTY, false); + if (layout != mAttachInfo.mDebugLayout) { + mAttachInfo.mDebugLayout = layout; + if (!mHandler.hasMessages(MSG_INVALIDATE_WORLD)) { + mHandler.sendEmptyMessageDelayed(MSG_INVALIDATE_WORLD, 200); + } + } } - } + }); } private void destroyHardwareRenderer() { @@ -4137,7 +4270,7 @@ public final class ViewRootImpl implements ViewParent, mHandler.sendMessage(msg); } - public void dispatchResized(Rect frame, Rect contentInsets, + public void dispatchResized(Rect frame, Rect overscanInsets, Rect contentInsets, Rect visibleInsets, boolean reportDraw, Configuration newConfig) { if (DEBUG_LAYOUT) Log.v(TAG, "Resizing " + this + ": frame=" + frame.toShortString() + " contentInsets=" + contentInsets.toShortString() @@ -4146,6 +4279,7 @@ public final class ViewRootImpl implements ViewParent, Message msg = mHandler.obtainMessage(reportDraw ? MSG_RESIZED_REPORT : MSG_RESIZED); if (mTranslator != null) { mTranslator.translateRectInScreenToAppWindow(frame); + mTranslator.translateRectInScreenToAppWindow(overscanInsets); mTranslator.translateRectInScreenToAppWindow(contentInsets); mTranslator.translateRectInScreenToAppWindow(visibleInsets); } @@ -4155,6 +4289,7 @@ public final class ViewRootImpl implements ViewParent, args.arg2 = sameProcessCall ? new Rect(contentInsets) : contentInsets; args.arg3 = sameProcessCall ? new Rect(visibleInsets) : visibleInsets; args.arg4 = sameProcessCall && newConfig != null ? new Configuration(newConfig) : newConfig; + args.arg5 = sameProcessCall ? new Rect(overscanInsets) : overscanInsets; msg.obj = args; mHandler.sendMessage(msg); } @@ -4437,7 +4572,7 @@ public final class ViewRootImpl implements ViewParent, AttachInfo.InvalidateInfo info = mViewRects.get(i); if (info.target == view) { mViewRects.remove(i); - info.release(); + info.recycle(); } } @@ -4478,7 +4613,7 @@ public final class ViewRootImpl implements ViewParent, for (int i = 0; i < viewRectCount; i++) { final View.AttachInfo.InvalidateInfo info = mTempViewRects[i]; info.target.invalidate(info.left, info.top, info.right, info.bottom); - info.release(); + info.recycle(); } } @@ -4513,19 +4648,6 @@ public final class ViewRootImpl implements ViewParent, public void enqueueDisplayList(DisplayList displayList) { mDisplayLists.add(displayList); - - mHandler.removeMessages(MSG_INVALIDATE_DISPLAY_LIST); - Message msg = mHandler.obtainMessage(MSG_INVALIDATE_DISPLAY_LIST); - mHandler.sendMessage(msg); - } - - public void dequeueDisplayList(DisplayList displayList) { - if (mDisplayLists.remove(displayList)) { - displayList.invalidate(); - if (mDisplayLists.size() == 0) { - mHandler.removeMessages(MSG_INVALIDATE_DISPLAY_LIST); - } - } } public void cancelInvalidate(View view) { @@ -4727,6 +4849,51 @@ public final class ViewRootImpl implements ViewParent, postSendWindowContentChangedCallback(child); } + @Override + public boolean canResolveLayoutDirection() { + return true; + } + + @Override + public boolean isLayoutDirectionResolved() { + return true; + } + + @Override + public int getLayoutDirection() { + return View.LAYOUT_DIRECTION_RESOLVED_DEFAULT; + } + + @Override + public boolean canResolveTextDirection() { + return true; + } + + @Override + public boolean isTextDirectionResolved() { + return true; + } + + @Override + public int getTextDirection() { + return View.TEXT_DIRECTION_RESOLVED_DEFAULT; + } + + @Override + public boolean canResolveTextAlignment() { + return true; + } + + @Override + public boolean isTextAlignmentResolved() { + return true; + } + + @Override + public int getTextAlignment() { + return View.TEXT_ALIGNMENT_RESOLVED_DEFAULT; + } + private View getCommonPredecessor(View first, View second) { if (mAttachInfo != null) { if (mTempHashSet == null) { @@ -4857,11 +5024,11 @@ public final class ViewRootImpl implements ViewParent, mWindowSession = viewAncestor.mWindowSession; } - public void resized(Rect frame, Rect contentInsets, + public void resized(Rect frame, Rect overscanInsets, Rect contentInsets, Rect visibleInsets, boolean reportDraw, Configuration newConfig) { final ViewRootImpl viewAncestor = mViewAncestor.get(); if (viewAncestor != null) { - viewAncestor.dispatchResized(frame, contentInsets, + viewAncestor.dispatchResized(frame, overscanInsets, contentInsets, visibleInsets, reportDraw, newConfig); } } @@ -5362,12 +5529,13 @@ public final class ViewRootImpl implements ViewParent, @Override public void findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, - int interrogatingPid, long interrogatingTid) { + int interrogatingPid, long interrogatingTid, MagnificationSpec spec) { ViewRootImpl viewRootImpl = mViewRootImpl.get(); if (viewRootImpl != null && viewRootImpl.mView != null) { viewRootImpl.getAccessibilityInteractionController() .findAccessibilityNodeInfoByAccessibilityIdClientThread(accessibilityNodeId, - interactionId, callback, flags, interrogatingPid, interrogatingTid); + interactionId, callback, flags, interrogatingPid, interrogatingTid, + spec); } else { // We cannot make the call and notify the caller so it does not wait. try { @@ -5399,14 +5567,16 @@ public final class ViewRootImpl implements ViewParent, } @Override - public void findAccessibilityNodeInfoByViewId(long accessibilityNodeId, int viewId, - int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, - int interrogatingPid, long interrogatingTid) { + public void findAccessibilityNodeInfosByViewId(long accessibilityNodeId, + String viewId, int interactionId, + IAccessibilityInteractionConnectionCallback callback, int flags, + int interrogatingPid, long interrogatingTid, MagnificationSpec spec) { ViewRootImpl viewRootImpl = mViewRootImpl.get(); if (viewRootImpl != null && viewRootImpl.mView != null) { viewRootImpl.getAccessibilityInteractionController() - .findAccessibilityNodeInfoByViewIdClientThread(accessibilityNodeId, viewId, - interactionId, callback, flags, interrogatingPid, interrogatingTid); + .findAccessibilityNodeInfosByViewIdClientThread(accessibilityNodeId, + viewId, interactionId, callback, flags, interrogatingPid, + interrogatingTid, spec); } else { // We cannot make the call and notify the caller so it does not wait. try { @@ -5420,12 +5590,13 @@ public final class ViewRootImpl implements ViewParent, @Override public void findAccessibilityNodeInfosByText(long accessibilityNodeId, String text, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, - int interrogatingPid, long interrogatingTid) { + int interrogatingPid, long interrogatingTid, MagnificationSpec spec) { ViewRootImpl viewRootImpl = mViewRootImpl.get(); if (viewRootImpl != null && viewRootImpl.mView != null) { viewRootImpl.getAccessibilityInteractionController() .findAccessibilityNodeInfosByTextClientThread(accessibilityNodeId, text, - interactionId, callback, flags, interrogatingPid, interrogatingTid); + interactionId, callback, flags, interrogatingPid, interrogatingTid, + spec); } else { // We cannot make the call and notify the caller so it does not wait. try { @@ -5439,12 +5610,12 @@ public final class ViewRootImpl implements ViewParent, @Override public void findFocus(long accessibilityNodeId, int focusType, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, - int interrogatingPid, long interrogatingTid) { + int interrogatingPid, long interrogatingTid, MagnificationSpec spec) { ViewRootImpl viewRootImpl = mViewRootImpl.get(); if (viewRootImpl != null && viewRootImpl.mView != null) { viewRootImpl.getAccessibilityInteractionController() .findFocusClientThread(accessibilityNodeId, focusType, interactionId, callback, - flags, interrogatingPid, interrogatingTid); + flags, interrogatingPid, interrogatingTid, spec); } else { // We cannot make the call and notify the caller so it does not wait. try { @@ -5458,12 +5629,12 @@ public final class ViewRootImpl implements ViewParent, @Override public void focusSearch(long accessibilityNodeId, int direction, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, - int interrogatingPid, long interrogatingTid) { + int interrogatingPid, long interrogatingTid, MagnificationSpec spec) { ViewRootImpl viewRootImpl = mViewRootImpl.get(); if (viewRootImpl != null && viewRootImpl.mView != null) { viewRootImpl.getAccessibilityInteractionController() .focusSearchClientThread(accessibilityNodeId, direction, interactionId, - callback, flags, interrogatingPid, interrogatingTid); + callback, flags, interrogatingPid, interrogatingTid, spec); } else { // We cannot make the call and notify the caller so it does not wait. try { diff --git a/core/java/android/view/VolumePanel.java b/core/java/android/view/VolumePanel.java index 001d020..e711b94 100644 --- a/core/java/android/view/VolumePanel.java +++ b/core/java/android/view/VolumePanel.java @@ -218,12 +218,14 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie private static class WarningDialogReceiver extends BroadcastReceiver implements DialogInterface.OnDismissListener { - private Context mContext; - private Dialog mDialog; + private final Context mContext; + private final Dialog mDialog; + private final VolumePanel mVolumePanel; - WarningDialogReceiver(Context context, Dialog dialog) { + WarningDialogReceiver(Context context, Dialog dialog, VolumePanel volumePanel) { mContext = context; mDialog = dialog; + mVolumePanel = volumePanel; IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); context.registerReceiver(this, filter); } @@ -231,16 +233,20 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie @Override public void onReceive(Context context, Intent intent) { mDialog.cancel(); - synchronized (sConfirmSafeVolumeLock) { - sConfirmSafeVolumeDialog = null; - } + cleanUp(); } public void onDismiss(DialogInterface unused) { mContext.unregisterReceiver(this); + cleanUp(); + } + + private void cleanUp() { synchronized (sConfirmSafeVolumeLock) { sConfirmSafeVolumeDialog = null; } + mVolumePanel.forceTimeout(); + mVolumePanel.updateStates(); } } @@ -276,7 +282,8 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie mDialog = new Dialog(context, R.style.Theme_Panel_Volume) { public boolean onTouchEvent(MotionEvent event) { - if (isShowing() && event.getAction() == MotionEvent.ACTION_OUTSIDE) { + if (isShowing() && event.getAction() == MotionEvent.ACTION_OUTSIDE && + sConfirmSafeVolumeDialog == null) { forceTimeout(); return true; } @@ -330,6 +337,11 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie listenToRingerMode(); } + public void setLayoutDirection(int layoutDirection) { + mPanel.setLayoutDirection(layoutDirection); + updateStates(); + } + private void listenToRingerMode() { final IntentFilter filter = new IntentFilter(); filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); @@ -453,6 +465,8 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie private void updateSlider(StreamControl sc) { sc.seekbarView.setProgress(getStreamVolume(sc.streamType)); final boolean muted = isMuted(sc.streamType); + // Force reloading the image resource + sc.icon.setImageDrawable(null); sc.icon.setImageResource(muted ? sc.iconMuteRes : sc.iconRes); if (sc.streamType == AudioManager.STREAM_RING && mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE) { @@ -462,7 +476,8 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie // never disable touch interactions for remote playback, the muting is not tied to // the state of the phone. sc.seekbarView.setEnabled(true); - } else if (sc.streamType != mAudioManager.getMasterStreamType() && muted) { + } else if ((sc.streamType != mAudioManager.getMasterStreamType() && muted) || + (sConfirmSafeVolumeDialog != null)) { sc.seekbarView.setEnabled(false); } else { sc.seekbarView.setEnabled(true); @@ -491,7 +506,7 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie } } - private void updateStates() { + public void updateStates() { final int count = mSliderGroup.getChildCount(); for (int i = 0; i < count; i++) { StreamControl sc = (StreamControl) mSliderGroup.getChildAt(i).getTag(); @@ -563,9 +578,9 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie postMuteChanged(STREAM_MASTER, flags); } - public void postDisplaySafeVolumeWarning() { + public void postDisplaySafeVolumeWarning(int flags) { if (hasMessages(MSG_DISPLAY_SAFE_VOLUME_WARNING)) return; - obtainMessage(MSG_DISPLAY_SAFE_VOLUME_WARNING, 0, 0).sendToTarget(); + obtainMessage(MSG_DISPLAY_SAFE_VOLUME_WARNING, flags, 0).sendToTarget(); } /** @@ -599,7 +614,6 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie removeMessages(MSG_FREE_RESOURCES); sendMessageDelayed(obtainMessage(MSG_FREE_RESOURCES), FREE_DELAY); - resetTimeout(); } @@ -705,7 +719,8 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie if (((flags & AudioManager.FLAG_FIXED_VOLUME) != 0) || (streamType != mAudioManager.getMasterStreamType() && streamType != AudioService.STREAM_REMOTE_MUSIC && - isMuted(streamType))) { + isMuted(streamType)) || + sConfirmSafeVolumeDialog != null) { sc.seekbarView.setEnabled(false); } else { sc.seekbarView.setEnabled(true); @@ -803,7 +818,6 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie removeMessages(MSG_FREE_RESOURCES); sendMessageDelayed(obtainMessage(MSG_FREE_RESOURCES), FREE_DELAY); - resetTimeout(); } @@ -839,30 +853,34 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie } } - protected void onDisplaySafeVolumeWarning() { - synchronized (sConfirmSafeVolumeLock) { - if (sConfirmSafeVolumeDialog != null) { - return; + protected void onDisplaySafeVolumeWarning(int flags) { + if ((flags & AudioManager.FLAG_SHOW_UI) != 0 || mDialog.isShowing()) { + synchronized (sConfirmSafeVolumeLock) { + if (sConfirmSafeVolumeDialog != null) { + return; + } + sConfirmSafeVolumeDialog = new AlertDialog.Builder(mContext) + .setMessage(com.android.internal.R.string.safe_media_volume_warning) + .setPositiveButton(com.android.internal.R.string.yes, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + mAudioService.disableSafeMediaVolume(); + } + }) + .setNegativeButton(com.android.internal.R.string.no, null) + .setIconAttribute(android.R.attr.alertDialogIcon) + .create(); + final WarningDialogReceiver warning = new WarningDialogReceiver(mContext, + sConfirmSafeVolumeDialog, this); + + sConfirmSafeVolumeDialog.setOnDismissListener(warning); + sConfirmSafeVolumeDialog.getWindow().setType( + WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); + sConfirmSafeVolumeDialog.show(); } - sConfirmSafeVolumeDialog = new AlertDialog.Builder(mContext) - .setMessage(com.android.internal.R.string.safe_media_volume_warning) - .setPositiveButton(com.android.internal.R.string.yes, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - mAudioService.disableSafeMediaVolume(); - } - }) - .setNegativeButton(com.android.internal.R.string.no, null) - .setIconAttribute(android.R.attr.alertDialogIcon) - .create(); - final WarningDialogReceiver warning = new WarningDialogReceiver(mContext, - sConfirmSafeVolumeDialog); - - sConfirmSafeVolumeDialog.setOnDismissListener(warning); - sConfirmSafeVolumeDialog.getWindow().setType( - WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); - sConfirmSafeVolumeDialog.show(); + updateStates(); } + resetTimeout(); } /** @@ -958,6 +976,11 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie mDialog.dismiss(); mActiveStreamType = -1; } + synchronized (sConfirmSafeVolumeLock) { + if (sConfirmSafeVolumeDialog != null) { + sConfirmSafeVolumeDialog.dismiss(); + } + } break; } case MSG_RINGER_MODE_CHANGED: { @@ -981,7 +1004,7 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie break; case MSG_DISPLAY_SAFE_VOLUME_WARNING: - onDisplaySafeVolumeWarning(); + onDisplaySafeVolumeWarning(msg.arg1); break; } } diff --git a/core/java/android/view/WindowInfo.java b/core/java/android/view/WindowInfo.java deleted file mode 100644 index 7d16e14..0000000 --- a/core/java/android/view/WindowInfo.java +++ /dev/null @@ -1,175 +0,0 @@ -/* - * 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.graphics.Rect; -import android.os.IBinder; -import android.os.Parcel; -import android.os.Parcelable; - -/** - * Information the state of a window. - * - * @hide - */ -public class WindowInfo implements Parcelable { - - private static final int MAX_POOL_SIZE = 20; - - private static int UNDEFINED = -1; - - private static Object sPoolLock = new Object(); - private static WindowInfo sPool; - private static int sPoolSize; - - private WindowInfo mNext; - private boolean mInPool; - - public IBinder token; - - public final Rect frame = new Rect(); - - public final Rect touchableRegion = new Rect(); - - public int type = UNDEFINED; - - public float compatibilityScale = UNDEFINED; - - public boolean visible; - - public int displayId = UNDEFINED; - - public int layer = UNDEFINED; - - private WindowInfo() { - /* do nothing - reduce visibility */ - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel parcel, int flags) { - parcel.writeStrongBinder(token); - parcel.writeParcelable(frame, 0); - parcel.writeParcelable(touchableRegion, 0); - parcel.writeInt(type); - parcel.writeFloat(compatibilityScale); - parcel.writeInt(visible ? 1 : 0); - parcel.writeInt(displayId); - parcel.writeInt(layer); - recycle(); - } - - private void initFromParcel(Parcel parcel) { - token = parcel.readStrongBinder(); - frame.set((Rect) parcel.readParcelable(null)); - touchableRegion.set((Rect) parcel.readParcelable(null)); - type = parcel.readInt(); - compatibilityScale = parcel.readFloat(); - visible = (parcel.readInt() == 1); - displayId = parcel.readInt(); - layer = parcel.readInt(); - } - - public static WindowInfo obtain(WindowInfo other) { - WindowInfo info = obtain(); - info.token = other.token; - info.frame.set(other.frame); - info.touchableRegion.set(other.touchableRegion); - info.type = other.type; - info.compatibilityScale = other.compatibilityScale; - info.visible = other.visible; - info.displayId = other.displayId; - info.layer = other.layer; - return info; - } - - public static WindowInfo obtain() { - synchronized (sPoolLock) { - if (sPoolSize > 0) { - WindowInfo info = sPool; - sPool = info.mNext; - info.mNext = null; - info.mInPool = false; - sPoolSize--; - return info; - } else { - return new WindowInfo(); - } - } - } - - public void recycle() { - if (mInPool) { - throw new IllegalStateException("Already recycled."); - } - clear(); - synchronized (sPoolLock) { - if (sPoolSize < MAX_POOL_SIZE) { - mNext = sPool; - sPool = this; - mInPool = true; - sPoolSize++; - } - } - } - - private void clear() { - token = null; - frame.setEmpty(); - touchableRegion.setEmpty(); - type = UNDEFINED; - compatibilityScale = UNDEFINED; - visible = false; - displayId = UNDEFINED; - layer = UNDEFINED; - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append("Window [token:").append((token != null) ? token.hashCode() : null); - builder.append(", displayId:").append(displayId); - builder.append(", type:").append(type); - builder.append(", visible:").append(visible); - builder.append(", layer:").append(layer); - builder.append(", compatibilityScale:").append(compatibilityScale); - builder.append(", frame:").append(frame); - builder.append(", touchableRegion:").append(touchableRegion); - builder.append("]"); - return builder.toString(); - } - - /** - * @see Parcelable.Creator - */ - public static final Parcelable.Creator<WindowInfo> CREATOR = - new Parcelable.Creator<WindowInfo>() { - public WindowInfo createFromParcel(Parcel parcel) { - WindowInfo info = WindowInfo.obtain(); - info.initFromParcel(parcel); - return info; - } - - public WindowInfo[] newArray(int size) { - return new WindowInfo[size]; - } - }; -} diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 6a67d8b..792188b 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -612,10 +612,23 @@ public interface WindowManager extends ViewManager { /** Window flag: allow window to extend outside of the screen. */ public static final int FLAG_LAYOUT_NO_LIMITS = 0x00000200; - /** Window flag: Hide all screen decorations (e.g. status bar) while + /** + * Window flag: Hide all screen decorations (e.g. status bar) while * this window is displayed. This allows the window to use the entire * display space for itself -- the status bar will be hidden when - * an app window with this flag set is on the top layer. */ + * an app window with this flag set is on the top layer. + * + * <p>This flag can be controlled in your theme through the + * {@link android.R.attr#windowFullscreen} attribute; this attribute + * is automatically set for you in the standard fullscreen themes + * such as {@link android.R.style#Theme_NoTitleBar_Fullscreen}, + * {@link android.R.style#Theme_Black_NoTitleBar_Fullscreen}, + * {@link android.R.style#Theme_Light_NoTitleBar_Fullscreen}, + * {@link android.R.style#Theme_Holo_NoActionBar_Fullscreen}, + * {@link android.R.style#Theme_Holo_Light_NoActionBar_Fullscreen}, + * {@link android.R.style#Theme_DeviceDefault_NoActionBar_Fullscreen}, and + * {@link android.R.style#Theme_DeviceDefault_Light_NoActionBar_Fullscreen}.</p> + */ public static final int FLAG_FULLSCREEN = 0x00000400; /** Window flag: Override {@link #FLAG_FULLSCREEN and force the @@ -697,6 +710,17 @@ public interface WindowManager extends ViewManager { * to actually see the wallpaper behind it; this flag just ensures * that the wallpaper surface will be there if this window actually * has translucent regions. + * + * <p>This flag can be controlled in your theme through the + * {@link android.R.attr#windowShowWallpaper} attribute; this attribute + * is automatically set for you in the standard wallpaper themes + * such as {@link android.R.style#Theme_Wallpaper}, + * {@link android.R.style#Theme_Wallpaper_NoTitleBar}, + * {@link android.R.style#Theme_Wallpaper_NoTitleBar_Fullscreen}, + * {@link android.R.style#Theme_Holo_Wallpaper}, + * {@link android.R.style#Theme_Holo_Wallpaper_NoTitleBar}, + * {@link android.R.style#Theme_DeviceDefault_Wallpaper}, and + * {@link android.R.style#Theme_DeviceDefault_Wallpaper_NoTitleBar}.</p> */ public static final int FLAG_SHOW_WALLPAPER = 0x00100000; @@ -735,20 +759,20 @@ public interface WindowManager extends ViewManager { /** * <p>Indicates whether this window should be hardware accelerated. * Requesting hardware acceleration does not guarantee it will happen.</p> - * + * * <p>This flag can be controlled programmatically <em>only</em> to enable * hardware acceleration. To enable hardware acceleration for a given * window programmatically, do the following:</p> - * + * * <pre> * Window w = activity.getWindow(); // in Activity's onCreate() for instance * w.setFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, * WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); * </pre> - * + * * <p>It is important to remember that this flag <strong>must</strong> * be set before setting the content view of your activity or dialog.</p> - * + * * <p>This flag cannot be used to disable hardware acceleration after it * was enabled in your manifest using * {@link android.R.attr#hardwareAccelerated}. If you need to selectively @@ -756,13 +780,48 @@ public interface WindowManager extends ViewManager { * for instance), make sure it is turned off in your manifest and enable it * on your activity or dialog when you need it instead, using the method * described above.</p> - * + * * <p>This flag is automatically set by the system if the * {@link android.R.attr#hardwareAccelerated android:hardwareAccelerated} * XML attribute is set to true on an activity or on the application.</p> */ public static final int FLAG_HARDWARE_ACCELERATED = 0x01000000; + /** + * Window flag: allow window contents to extend in to the screen's + * overscan area, if there is one. The window should still correctly + * position its contents to take the overscan area into account. + * + * <p>This flag can be controlled in your theme through the + * {@link android.R.attr#windowOverscan} attribute; this attribute + * is automatically set for you in the standard overscan themes + * such as {@link android.R.style#Theme_NoTitleBar_Overscan}, + * {@link android.R.style#Theme_Black_NoTitleBar_Overscan}, + * {@link android.R.style#Theme_Light_NoTitleBar_Overscan}, + * {@link android.R.style#Theme_Holo_NoActionBar_Overscan}, + * {@link android.R.style#Theme_Holo_Light_NoActionBar_Overscan}, + * {@link android.R.style#Theme_DeviceDefault_NoActionBar_Overscan}, and + * {@link android.R.style#Theme_DeviceDefault_Light_NoActionBar_Overscan}.</p> + * + * <p>When this flag is enabled for a window, its normal content may be obscured + * to some degree by the overscan region of the display. To ensure key parts of + * that content are visible to the user, you can use + * {@link View#setFitsSystemWindows(boolean) View.setFitsSystemWindows(boolean)} + * to set the point in the view hierarchy where the appropriate offsets should + * be applied. (This can be done either by directly calling this function, using + * the {@link android.R.attr#fitsSystemWindows} attribute in your view hierarchy, + * or implementing you own {@link View#fitSystemWindows(android.graphics.Rect) + * View.fitSystemWindows(Rect)} method).</p> + * + * <p>This mechanism for positioning content elements is identical to its equivalent + * use with layout and {@link View#setSystemUiVisibility(int) + * View.setSystemUiVisibility(int)}; here is an example layout that will correctly + * position its UI elements with this overscan flag is set:</p> + * + * {@sample development/samples/ApiDemos/res/layout/overscan_activity.xml complete} + */ + public static final int FLAG_LAYOUT_IN_OVERSCAN = 0x02000000; + // ----- HIDDEN FLAGS. // These start at the high bit and go down. @@ -1085,6 +1144,11 @@ public interface WindowManager extends ViewManager { * {@link #SOFT_INPUT_ADJUST_UNSPECIFIED}, * {@link #SOFT_INPUT_ADJUST_RESIZE}, or * {@link #SOFT_INPUT_ADJUST_PAN}. + * </ul> + * + * + * <p>This flag can be controlled in your theme through the + * {@link android.R.attr#windowSoftInputMode} attribute.</p> */ public int softInputMode; @@ -1187,6 +1251,37 @@ public interface WindowManager extends ViewManager { public float buttonBrightness = BRIGHTNESS_OVERRIDE_NONE; /** + * Value for {@link #rotationAnimation} to define the animation used to + * specify that this window will rotate in or out following a rotation. + */ + public static final int ROTATION_ANIMATION_ROTATE = 0; + + /** + * Value for {@link #rotationAnimation} to define the animation used to + * specify that this window will fade in or out following a rotation. + */ + public static final int ROTATION_ANIMATION_CROSSFADE = 1; + + /** + * Value for {@link #rotationAnimation} to define the animation used to + * specify that this window will immediately disappear or appear following + * a rotation. + */ + public static final int ROTATION_ANIMATION_JUMPCUT = 2; + + /** + * Define the animation used on this window for entry or exit following + * a rotation. This only works if the incoming and outgoing topmost + * opaque windows have the #FLAG_FULLSCREEN bit set and are not covered + * by other windows. + * + * @see #ROTATION_ANIMATION_ROTATE + * @see #ROTATION_ANIMATION_CROSSFADE + * @see #ROTATION_ANIMATION_JUMPCUT + */ + public int rotationAnimation = ROTATION_ANIMATION_ROTATE; + + /** * Identifier for this window. This will usually be filled in for * you. */ @@ -1361,6 +1456,7 @@ public interface WindowManager extends ViewManager { out.writeFloat(dimAmount); out.writeFloat(screenBrightness); out.writeFloat(buttonBrightness); + out.writeInt(rotationAnimation); out.writeStrongBinder(token); out.writeString(packageName); TextUtils.writeToParcel(mTitle, out, parcelableFlags); @@ -1402,6 +1498,7 @@ public interface WindowManager extends ViewManager { dimAmount = in.readFloat(); screenBrightness = in.readFloat(); buttonBrightness = in.readFloat(); + rotationAnimation = in.readInt(); token = in.readStrongBinder(); packageName = in.readString(); mTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); @@ -1426,18 +1523,19 @@ public interface WindowManager extends ViewManager { public static final int SOFT_INPUT_MODE_CHANGED = 1<<9; public static final int SCREEN_ORIENTATION_CHANGED = 1<<10; public static final int SCREEN_BRIGHTNESS_CHANGED = 1<<11; + public static final int ROTATION_ANIMATION_CHANGED = 1<<12; /** {@hide} */ - public static final int BUTTON_BRIGHTNESS_CHANGED = 1<<12; + public static final int BUTTON_BRIGHTNESS_CHANGED = 1<<13; /** {@hide} */ - public static final int SYSTEM_UI_VISIBILITY_CHANGED = 1<<13; + public static final int SYSTEM_UI_VISIBILITY_CHANGED = 1<<14; /** {@hide} */ - public static final int SYSTEM_UI_LISTENER_CHANGED = 1<<14; + public static final int SYSTEM_UI_LISTENER_CHANGED = 1<<15; /** {@hide} */ - public static final int INPUT_FEATURES_CHANGED = 1<<15; + public static final int INPUT_FEATURES_CHANGED = 1<<16; /** {@hide} */ - public static final int PRIVATE_FLAGS_CHANGED = 1<<16; + public static final int PRIVATE_FLAGS_CHANGED = 1<<17; /** {@hide} */ - public static final int USER_ACTIVITY_TIMEOUT_CHANGED = 1<<17; + public static final int USER_ACTIVITY_TIMEOUT_CHANGED = 1<<18; /** {@hide} */ public static final int EVERYTHING_CHANGED = 0xffffffff; @@ -1537,6 +1635,10 @@ public interface WindowManager extends ViewManager { buttonBrightness = o.buttonBrightness; changes |= BUTTON_BRIGHTNESS_CHANGED; } + if (rotationAnimation != o.rotationAnimation) { + rotationAnimation = o.rotationAnimation; + changes |= ROTATION_ANIMATION_CHANGED; + } if (screenOrientation != o.screenOrientation) { screenOrientation = o.screenOrientation; @@ -1639,6 +1741,10 @@ public interface WindowManager extends ViewManager { sb.append(" bbrt="); sb.append(buttonBrightness); } + if (rotationAnimation != ROTATION_ANIMATION_ROTATE) { + sb.append(" rotAnim="); + sb.append(rotationAnimation); + } if ((flags & FLAG_COMPATIBLE_WINDOW) != 0) { sb.append(" compatible=true"); } diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java index e8945aa..7eb26fa 100644 --- a/core/java/android/view/WindowManagerGlobal.java +++ b/core/java/android/view/WindowManagerGlobal.java @@ -160,6 +160,29 @@ public final class WindowManagerGlobal { } } + public String[] getViewRootNames() { + synchronized (mLock) { + if (mRoots == null) return new String[0]; + String[] mViewRoots = new String[mRoots.length]; + int i = 0; + for (ViewRootImpl root : mRoots) { + mViewRoots[i++] = getWindowName(root); + } + return mViewRoots; + } + } + + public View getRootView(String name) { + synchronized (mLock) { + if (mRoots == null) return null; + for (ViewRootImpl root : mRoots) { + if (name.equals(getWindowName(root))) return root.getView(); + } + } + + return null; + } + public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { if (view == null) { diff --git a/core/java/android/view/WindowManagerPolicy.java b/core/java/android/view/WindowManagerPolicy.java index 26739b3..c0044b6 100644 --- a/core/java/android/view/WindowManagerPolicy.java +++ b/core/java/android/view/WindowManagerPolicy.java @@ -135,6 +135,16 @@ public interface WindowManagerPolicy { */ public interface WindowState { /** + * Return the uid of the app that owns this window. + */ + int getOwningUid(); + + /** + * Return the package name of the app that owns this window. + */ + String getOwningPackage(); + + /** * Perform standard frame computation. The result can be obtained with * getFrame() if so desired. Must be called with the window manager * lock held. @@ -144,6 +154,8 @@ public interface WindowManagerPolicy { * @param displayFrame The frame of the overall display in which this * window can appear, used for constraining the overall dimensions * of the window. + * @param overlayFrame The frame within the display that is inside + * of the overlay region. * @param contentFrame The frame within the display in which we would * like active content to appear. This will cause windows behind to * be resized to match the given content frame. @@ -155,7 +167,7 @@ public interface WindowManagerPolicy { * are visible. */ public void computeFrameLw(Rect parentFrame, Rect displayFrame, - Rect contentFrame, Rect visibleFrame); + Rect overlayFrame, Rect contentFrame, Rect visibleFrame); /** * Retrieve the current frame of the window that has been assigned by @@ -183,6 +195,15 @@ public interface WindowManagerPolicy { public Rect getDisplayFrameLw(); /** + * Retrieve the frame of the area inside the overscan region of the + * display that this window was last laid out in. Must be called with the + * window manager lock held. + * + * @return Rect The rectangle holding the display overscan frame. + */ + public Rect getOverscanFrameLw(); + + /** * Retrieve the frame of the content area that this window was last * laid out in. This is the area in which the content of the window * should be placed. It will be smaller than the display frame to @@ -401,61 +422,19 @@ public interface WindowManagerPolicy { public void rebootSafeMode(boolean confirm); } - /** - * Bit mask that is set for all enter transition. - */ - public final int TRANSIT_ENTER_MASK = 0x1000; - - /** - * Bit mask that is set for all exit transitions. - */ - public final int TRANSIT_EXIT_MASK = 0x2000; - - /** Not set up for a transition. */ - public final int TRANSIT_UNSET = -1; - /** No animation for transition. */ - public final int TRANSIT_NONE = 0; /** Window has been added to the screen. */ - public final int TRANSIT_ENTER = 1 | TRANSIT_ENTER_MASK; + public static final int TRANSIT_ENTER = 1; /** Window has been removed from the screen. */ - public final int TRANSIT_EXIT = 2 | TRANSIT_EXIT_MASK; + public static final int TRANSIT_EXIT = 2; /** Window has been made visible. */ - public final int TRANSIT_SHOW = 3 | TRANSIT_ENTER_MASK; - /** Window has been made invisible. */ - public final int TRANSIT_HIDE = 4 | TRANSIT_EXIT_MASK; + public static final int TRANSIT_SHOW = 3; + /** Window has been made invisible. + * TODO: Consider removal as this is unused. */ + public static final int TRANSIT_HIDE = 4; /** The "application starting" preview window is no longer needed, and will * animate away to show the real window. */ - public final int TRANSIT_PREVIEW_DONE = 5; - /** A window in a new activity is being opened on top of an existing one - * in the same task. */ - public final int TRANSIT_ACTIVITY_OPEN = 6 | TRANSIT_ENTER_MASK; - /** The window in the top-most activity is being closed to reveal the - * previous activity in the same task. */ - public final int TRANSIT_ACTIVITY_CLOSE = 7 | TRANSIT_EXIT_MASK; - /** A window in a new task is being opened on top of an existing one - * in another activity's task. */ - public final int TRANSIT_TASK_OPEN = 8 | TRANSIT_ENTER_MASK; - /** A window in the top-most activity is being closed to reveal the - * previous activity in a different task. */ - public final int TRANSIT_TASK_CLOSE = 9 | TRANSIT_EXIT_MASK; - /** A window in an existing task is being displayed on top of an existing one - * in another activity's task. */ - public final int TRANSIT_TASK_TO_FRONT = 10 | TRANSIT_ENTER_MASK; - /** A window in an existing task is being put below all other tasks. */ - public final int TRANSIT_TASK_TO_BACK = 11 | TRANSIT_EXIT_MASK; - /** A window in a new activity that doesn't have a wallpaper is being - * opened on top of one that does, effectively closing the wallpaper. */ - public final int TRANSIT_WALLPAPER_CLOSE = 12 | TRANSIT_EXIT_MASK; - /** A window in a new activity that does have a wallpaper is being - * opened on one that didn't, effectively opening the wallpaper. */ - public final int TRANSIT_WALLPAPER_OPEN = 13 | TRANSIT_ENTER_MASK; - /** A window in a new activity is being opened on top of an existing one, - * and both are on top of the wallpaper. */ - public final int TRANSIT_WALLPAPER_INTRA_OPEN = 14 | TRANSIT_ENTER_MASK; - /** The window in the top-most activity is being closed to reveal the - * previous activity, and both are on top of he wallpaper. */ - public final int TRANSIT_WALLPAPER_INTRA_CLOSE = 15 | TRANSIT_EXIT_MASK; - + public static final int TRANSIT_PREVIEW_DONE = 5; + // NOTE: screen off reasons are in order of significance, with more // important ones lower than less important ones. @@ -490,15 +469,23 @@ public interface WindowManagerPolicy { public void setInitialDisplaySize(Display display, int width, int height, int density); /** + * Called by window manager to set the overscan region that should be used for the + * given display. + */ + public void setDisplayOverscan(Display display, int left, int top, int right, int bottom); + + /** * Check permissions when adding a window. * - * @param attrs The window's LayoutParams. + * @param attrs The window's LayoutParams. + * @param outAppOp First element will be filled with the app op corresponding to + * this window, or OP_NONE. * * @return {@link WindowManagerGlobal#ADD_OKAY} if the add can proceed; * else an error code, usually * {@link WindowManagerGlobal#ADD_PERMISSION_DENIED}, to abort the add. */ - public int checkAddPermission(WindowManager.LayoutParams attrs); + public int checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp); /** * Check permissions when adding a window. @@ -708,6 +695,31 @@ public interface WindowManagerPolicy { public int selectAnimationLw(WindowState win, int transit); /** + * Determine the animation to run for a rotation transition based on the + * top fullscreen windows {@link WindowManager.LayoutParams#rotationAnimation} + * and whether it is currently fullscreen and frontmost. + * + * @param anim The exiting animation resource id is stored in anim[0], the + * entering animation resource id is stored in anim[1]. + */ + public void selectRotationAnimationLw(int anim[]); + + /** + * Validate whether the current top fullscreen has specified the same + * {@link WindowManager.LayoutParams#rotationAnimation} value as that + * being passed in from the previous top fullscreen window. + * + * @param exitAnimId exiting resource id from the previous window. + * @param enterAnimId entering resource id from the previous window. + * @param forceDefault For rotation animations only, if true ignore the + * animation values and just return false. + * @return true if the previous values are still valid, false if they + * should be replaced with the default. + */ + public boolean validateRotationAnimationLw(int exitAnimId, int enterAnimId, + boolean forceDefault); + + /** * Create and return an animation to re-display a force hidden window. */ public Animation createForceHideEnterAnimation(boolean onWallpaper); @@ -933,6 +945,7 @@ public interface WindowManagerPolicy { * @see android.app.KeyguardManager.KeyguardLock#disableKeyguard() * @see android.app.KeyguardManager.KeyguardLock#reenableKeyguard() */ + @SuppressWarnings("javadoc") public void enableKeyguard(boolean enabled); /** @@ -948,6 +961,7 @@ public interface WindowManagerPolicy { * @param callback Callback to send the result back. * @see android.app.KeyguardManager#exitKeyguardSecurely(android.app.KeyguardManager.OnKeyguardExitResult) */ + @SuppressWarnings("javadoc") void exitKeyguardSecurely(OnKeyguardExitResult callback); /** @@ -1077,6 +1091,16 @@ public interface WindowManagerPolicy { public void keepScreenOnStoppedLw(); /** + * Gets the current user rotation mode. + * + * @return The rotation mode. + * + * @see WindowManagerPolicy#USER_ROTATION_LOCKED + * @see WindowManagerPolicy#USER_ROTATION_FREE + */ + public int getUserRotationMode(); + + /** * Inform the policy that the user has chosen a preferred orientation ("rotation lock"). * * @param mode One of {@link WindowManagerPolicy#USER_ROTATION_LOCKED} or @@ -1112,14 +1136,6 @@ public interface WindowManagerPolicy { public void setLastInputMethodWindowLw(WindowState ime, WindowState target); /** - * Returns whether magnification can be applied to the given window type. - * - * @param attrs The window's LayoutParams. - * @return Whether magnification can be applied. - */ - public boolean canMagnifyWindowLw(WindowManager.LayoutParams attrs); - - /** * Called when the current user changes. Guaranteed to be called before the broadcast * of the new user id is made to all listeners. * @@ -1142,4 +1158,23 @@ public interface WindowManagerPolicy { * {@link android.content.Intent#ACTION_ASSIST} */ public void showAssistant(); + + /** + * Returns whether a given window type can be magnified. + * + * @param windowType The window type. + * @return True if the window can be magnified. + */ + public boolean canMagnifyWindow(int windowType); + + /** + * Returns whether a given window type is considered a top level one. + * A top level window does not have a container, i.e. attached window, + * or if it has a container it is laid out as a top-level window, not + * as a child of its container. + * + * @param windowType The window type. + * @return True if the window is a top level one. + */ + public boolean isTopLevelWindow(int windowType); } diff --git a/core/java/android/view/WindowOrientationListener.java b/core/java/android/view/WindowOrientationListener.java deleted file mode 100644 index bf77c67..0000000 --- a/core/java/android/view/WindowOrientationListener.java +++ /dev/null @@ -1,715 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.view; - -import android.content.Context; -import android.hardware.Sensor; -import android.hardware.SensorEvent; -import android.hardware.SensorEventListener; -import android.hardware.SensorManager; -import android.os.SystemProperties; -import android.util.FloatMath; -import android.util.Log; -import android.util.Slog; - -/** - * A special helper class used by the WindowManager - * for receiving notifications from the SensorManager when - * the orientation of the device has changed. - * - * NOTE: If changing anything here, please run the API demo - * "App/Activity/Screen Orientation" to ensure that all orientation - * modes still work correctly. - * - * You can also visualize the behavior of the WindowOrientationListener. - * Refer to frameworks/base/tools/orientationplot/README.txt for details. - * - * @hide - */ -public abstract class WindowOrientationListener { - private static final String TAG = "WindowOrientationListener"; - private static final boolean LOG = SystemProperties.getBoolean( - "debug.orientation.log", false); - - private static final boolean USE_GRAVITY_SENSOR = false; - - private SensorManager mSensorManager; - private boolean mEnabled; - private int mRate; - private Sensor mSensor; - private SensorEventListenerImpl mSensorEventListener; - int mCurrentRotation = -1; - - /** - * Creates a new WindowOrientationListener. - * - * @param context for the WindowOrientationListener. - */ - public WindowOrientationListener(Context context) { - this(context, SensorManager.SENSOR_DELAY_UI); - } - - /** - * Creates a new WindowOrientationListener. - * - * @param context for the WindowOrientationListener. - * @param rate at which sensor events are processed (see also - * {@link android.hardware.SensorManager SensorManager}). Use the default - * value of {@link android.hardware.SensorManager#SENSOR_DELAY_NORMAL - * SENSOR_DELAY_NORMAL} for simple screen orientation change detection. - * - * This constructor is private since no one uses it. - */ - private WindowOrientationListener(Context context, int rate) { - mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE); - mRate = rate; - mSensor = mSensorManager.getDefaultSensor(USE_GRAVITY_SENSOR - ? Sensor.TYPE_GRAVITY : Sensor.TYPE_ACCELEROMETER); - if (mSensor != null) { - // Create listener only if sensors do exist - mSensorEventListener = new SensorEventListenerImpl(this); - } - } - - /** - * Enables the WindowOrientationListener so it will monitor the sensor and call - * {@link #onOrientationChanged} when the device orientation changes. - */ - public void enable() { - if (mSensor == null) { - Log.w(TAG, "Cannot detect sensors. Not enabled"); - return; - } - if (mEnabled == false) { - if (LOG) { - Log.d(TAG, "WindowOrientationListener enabled"); - } - mSensorEventListener.reset(); - mSensorManager.registerListener(mSensorEventListener, mSensor, mRate); - mEnabled = true; - } - } - - /** - * Disables the WindowOrientationListener. - */ - public void disable() { - if (mSensor == null) { - Log.w(TAG, "Cannot detect sensors. Invalid disable"); - return; - } - if (mEnabled == true) { - if (LOG) { - Log.d(TAG, "WindowOrientationListener disabled"); - } - mSensorManager.unregisterListener(mSensorEventListener); - mEnabled = false; - } - } - - /** - * Sets the current rotation. - * - * @param rotation The current rotation. - */ - public void setCurrentRotation(int rotation) { - mCurrentRotation = rotation; - } - - /** - * Gets the proposed rotation. - * - * This method only returns a rotation if the orientation listener is certain - * of its proposal. If the rotation is indeterminate, returns -1. - * - * @return The proposed rotation, or -1 if unknown. - */ - public int getProposedRotation() { - if (mEnabled) { - return mSensorEventListener.getProposedRotation(); - } - return -1; - } - - /** - * Returns true if sensor is enabled and false otherwise - */ - public boolean canDetectOrientation() { - return mSensor != null; - } - - /** - * Called when the rotation view of the device has changed. - * - * This method is called whenever the orientation becomes certain of an orientation. - * It is called each time the orientation determination transitions from being - * uncertain to being certain again, even if it is the same orientation as before. - * - * @param rotation The new orientation of the device, one of the Surface.ROTATION_* constants. - * @see Surface - */ - public abstract void onProposedRotationChanged(int rotation); - - /** - * This class filters the raw accelerometer data and tries to detect actual changes in - * orientation. This is a very ill-defined problem so there are a lot of tweakable parameters, - * but here's the outline: - * - * - Low-pass filter the accelerometer vector in cartesian coordinates. We do it in - * cartesian space because the orientation calculations are sensitive to the - * absolute magnitude of the acceleration. In particular, there are singularities - * in the calculation as the magnitude approaches 0. By performing the low-pass - * filtering early, we can eliminate most spurious high-frequency impulses due to noise. - * - * - Convert the acceleromter vector from cartesian to spherical coordinates. - * Since we're dealing with rotation of the device, this is the sensible coordinate - * system to work in. The zenith direction is the Z-axis, the direction the screen - * is facing. The radial distance is referred to as the magnitude below. - * The elevation angle is referred to as the "tilt" below. - * The azimuth angle is referred to as the "orientation" below (and the azimuth axis is - * the Y-axis). - * See http://en.wikipedia.org/wiki/Spherical_coordinate_system for reference. - * - * - If the tilt angle is too close to horizontal (near 90 or -90 degrees), do nothing. - * The orientation angle is not meaningful when the device is nearly horizontal. - * The tilt angle thresholds are set differently for each orientation and different - * limits are applied when the device is facing down as opposed to when it is facing - * forward or facing up. - * - * - When the orientation angle reaches a certain threshold, consider transitioning - * to the corresponding orientation. These thresholds have some hysteresis built-in - * to avoid oscillations between adjacent orientations. - * - * - Wait for the device to settle for a little bit. Once that happens, issue the - * new orientation proposal. - * - * Details are explained inline. - * - * See http://en.wikipedia.org/wiki/Low-pass_filter#Discrete-time_realization for - * signal processing background. - */ - static final class SensorEventListenerImpl implements SensorEventListener { - // We work with all angles in degrees in this class. - private static final float RADIANS_TO_DEGREES = (float) (180 / Math.PI); - - // Number of nanoseconds per millisecond. - private static final long NANOS_PER_MS = 1000000; - - // Indices into SensorEvent.values for the accelerometer sensor. - private static final int ACCELEROMETER_DATA_X = 0; - private static final int ACCELEROMETER_DATA_Y = 1; - private static final int ACCELEROMETER_DATA_Z = 2; - - private final WindowOrientationListener mOrientationListener; - - // The minimum amount of time that a predicted rotation must be stable before it - // is accepted as a valid rotation proposal. This value can be quite small because - // the low-pass filter already suppresses most of the noise so we're really just - // looking for quick confirmation that the last few samples are in agreement as to - // the desired orientation. - private static final long PROPOSAL_SETTLE_TIME_NANOS = 40 * NANOS_PER_MS; - - // The minimum amount of time that must have elapsed since the device last exited - // the flat state (time since it was picked up) before the proposed rotation - // can change. - private static final long PROPOSAL_MIN_TIME_SINCE_FLAT_ENDED_NANOS = 500 * NANOS_PER_MS; - - // The minimum amount of time that must have elapsed since the device stopped - // swinging (time since device appeared to be in the process of being put down - // or put away into a pocket) before the proposed rotation can change. - private static final long PROPOSAL_MIN_TIME_SINCE_SWING_ENDED_NANOS = 300 * NANOS_PER_MS; - - // The minimum amount of time that must have elapsed since the device stopped - // undergoing external acceleration before the proposed rotation can change. - private static final long PROPOSAL_MIN_TIME_SINCE_ACCELERATION_ENDED_NANOS = - 500 * NANOS_PER_MS; - - // If the tilt angle remains greater than the specified angle for a minimum of - // the specified time, then the device is deemed to be lying flat - // (just chillin' on a table). - private static final float FLAT_ANGLE = 75; - private static final long FLAT_TIME_NANOS = 1000 * NANOS_PER_MS; - - // If the tilt angle has increased by at least delta degrees within the specified amount - // of time, then the device is deemed to be swinging away from the user - // down towards flat (tilt = 90). - private static final float SWING_AWAY_ANGLE_DELTA = 20; - private static final long SWING_TIME_NANOS = 300 * NANOS_PER_MS; - - // The maximum sample inter-arrival time in milliseconds. - // If the acceleration samples are further apart than this amount in time, we reset the - // state of the low-pass filter and orientation properties. This helps to handle - // boundary conditions when the device is turned on, wakes from suspend or there is - // a significant gap in samples. - private static final long MAX_FILTER_DELTA_TIME_NANOS = 1000 * NANOS_PER_MS; - - // The acceleration filter time constant. - // - // This time constant is used to tune the acceleration filter such that - // impulses and vibrational noise (think car dock) is suppressed before we - // try to calculate the tilt and orientation angles. - // - // The filter time constant is related to the filter cutoff frequency, which is the - // frequency at which signals are attenuated by 3dB (half the passband power). - // Each successive octave beyond this frequency is attenuated by an additional 6dB. - // - // Given a time constant t in seconds, the filter cutoff frequency Fc in Hertz - // is given by Fc = 1 / (2pi * t). - // - // The higher the time constant, the lower the cutoff frequency, so more noise - // will be suppressed. - // - // Filtering adds latency proportional the time constant (inversely proportional - // to the cutoff frequency) so we don't want to make the time constant too - // large or we can lose responsiveness. Likewise we don't want to make it too - // small or we do a poor job suppressing acceleration spikes. - // Empirically, 100ms seems to be too small and 500ms is too large. - private static final float FILTER_TIME_CONSTANT_MS = 200.0f; - - /* State for orientation detection. */ - - // Thresholds for minimum and maximum allowable deviation from gravity. - // - // If the device is undergoing external acceleration (being bumped, in a car - // that is turning around a corner or a plane taking off) then the magnitude - // may be substantially more or less than gravity. This can skew our orientation - // detection by making us think that up is pointed in a different direction. - // - // Conversely, if the device is in freefall, then there will be no gravity to - // measure at all. This is problematic because we cannot detect the orientation - // without gravity to tell us which way is up. A magnitude near 0 produces - // singularities in the tilt and orientation calculations. - // - // In both cases, we postpone choosing an orientation. - // - // However, we need to tolerate some acceleration because the angular momentum - // of turning the device can skew the observed acceleration for a short period of time. - private static final float NEAR_ZERO_MAGNITUDE = 1; // m/s^2 - private static final float ACCELERATION_TOLERANCE = 4; // m/s^2 - private static final float MIN_ACCELERATION_MAGNITUDE = - SensorManager.STANDARD_GRAVITY - ACCELERATION_TOLERANCE; - private static final float MAX_ACCELERATION_MAGNITUDE = - SensorManager.STANDARD_GRAVITY + ACCELERATION_TOLERANCE; - - // Maximum absolute tilt angle at which to consider orientation data. Beyond this (i.e. - // when screen is facing the sky or ground), we completely ignore orientation data. - private static final int MAX_TILT = 75; - - // The tilt angle range in degrees for each orientation. - // Beyond these tilt angles, we don't even consider transitioning into the - // specified orientation. We place more stringent requirements on unnatural - // orientations than natural ones to make it less likely to accidentally transition - // into those states. - // The first value of each pair is negative so it applies a limit when the device is - // facing down (overhead reading in bed). - // The second value of each pair is positive so it applies a limit when the device is - // facing up (resting on a table). - // The ideal tilt angle is 0 (when the device is vertical) so the limits establish - // how close to vertical the device must be in order to change orientation. - private static final int[][] TILT_TOLERANCE = new int[][] { - /* ROTATION_0 */ { -25, 70 }, - /* ROTATION_90 */ { -25, 65 }, - /* ROTATION_180 */ { -25, 60 }, - /* ROTATION_270 */ { -25, 65 } - }; - - // The gap angle in degrees between adjacent orientation angles for hysteresis. - // This creates a "dead zone" between the current orientation and a proposed - // adjacent orientation. No orientation proposal is made when the orientation - // angle is within the gap between the current orientation and the adjacent - // orientation. - private static final int ADJACENT_ORIENTATION_ANGLE_GAP = 45; - - // Timestamp and value of the last accelerometer sample. - private long mLastFilteredTimestampNanos; - private float mLastFilteredX, mLastFilteredY, mLastFilteredZ; - - // The last proposed rotation, -1 if unknown. - private int mProposedRotation; - - // Value of the current predicted rotation, -1 if unknown. - private int mPredictedRotation; - - // Timestamp of when the predicted rotation most recently changed. - private long mPredictedRotationTimestampNanos; - - // Timestamp when the device last appeared to be flat for sure (the flat delay elapsed). - private long mFlatTimestampNanos; - - // Timestamp when the device last appeared to be swinging. - private long mSwingTimestampNanos; - - // Timestamp when the device last appeared to be undergoing external acceleration. - private long mAccelerationTimestampNanos; - - // History of observed tilt angles. - private static final int TILT_HISTORY_SIZE = 40; - private float[] mTiltHistory = new float[TILT_HISTORY_SIZE]; - private long[] mTiltHistoryTimestampNanos = new long[TILT_HISTORY_SIZE]; - private int mTiltHistoryIndex; - - public SensorEventListenerImpl(WindowOrientationListener orientationListener) { - mOrientationListener = orientationListener; - reset(); - } - - public int getProposedRotation() { - return mProposedRotation; - } - - @Override - public void onAccuracyChanged(Sensor sensor, int accuracy) { - } - - @Override - public void onSensorChanged(SensorEvent event) { - // The vector given in the SensorEvent points straight up (towards the sky) under ideal - // conditions (the phone is not accelerating). I'll call this up vector elsewhere. - float x = event.values[ACCELEROMETER_DATA_X]; - float y = event.values[ACCELEROMETER_DATA_Y]; - float z = event.values[ACCELEROMETER_DATA_Z]; - - if (LOG) { - Slog.v(TAG, "Raw acceleration vector: " - + "x=" + x + ", y=" + y + ", z=" + z - + ", magnitude=" + FloatMath.sqrt(x * x + y * y + z * z)); - } - - // Apply a low-pass filter to the acceleration up vector in cartesian space. - // Reset the orientation listener state if the samples are too far apart in time - // or when we see values of (0, 0, 0) which indicates that we polled the - // accelerometer too soon after turning it on and we don't have any data yet. - final long now = event.timestamp; - final long then = mLastFilteredTimestampNanos; - final float timeDeltaMS = (now - then) * 0.000001f; - final boolean skipSample; - if (now < then - || now > then + MAX_FILTER_DELTA_TIME_NANOS - || (x == 0 && y == 0 && z == 0)) { - if (LOG) { - Slog.v(TAG, "Resetting orientation listener."); - } - reset(); - skipSample = true; - } else { - final float alpha = timeDeltaMS / (FILTER_TIME_CONSTANT_MS + timeDeltaMS); - x = alpha * (x - mLastFilteredX) + mLastFilteredX; - y = alpha * (y - mLastFilteredY) + mLastFilteredY; - z = alpha * (z - mLastFilteredZ) + mLastFilteredZ; - if (LOG) { - Slog.v(TAG, "Filtered acceleration vector: " - + "x=" + x + ", y=" + y + ", z=" + z - + ", magnitude=" + FloatMath.sqrt(x * x + y * y + z * z)); - } - skipSample = false; - } - mLastFilteredTimestampNanos = now; - mLastFilteredX = x; - mLastFilteredY = y; - mLastFilteredZ = z; - - boolean isAccelerating = false; - boolean isFlat = false; - boolean isSwinging = false; - if (!skipSample) { - // Calculate the magnitude of the acceleration vector. - final float magnitude = FloatMath.sqrt(x * x + y * y + z * z); - if (magnitude < NEAR_ZERO_MAGNITUDE) { - if (LOG) { - Slog.v(TAG, "Ignoring sensor data, magnitude too close to zero."); - } - clearPredictedRotation(); - } else { - // Determine whether the device appears to be undergoing external acceleration. - if (isAccelerating(magnitude)) { - isAccelerating = true; - mAccelerationTimestampNanos = now; - } - - // Calculate the tilt angle. - // This is the angle between the up vector and the x-y plane (the plane of - // the screen) in a range of [-90, 90] degrees. - // -90 degrees: screen horizontal and facing the ground (overhead) - // 0 degrees: screen vertical - // 90 degrees: screen horizontal and facing the sky (on table) - final int tiltAngle = (int) Math.round( - Math.asin(z / magnitude) * RADIANS_TO_DEGREES); - addTiltHistoryEntry(now, tiltAngle); - - // Determine whether the device appears to be flat or swinging. - if (isFlat(now)) { - isFlat = true; - mFlatTimestampNanos = now; - } - if (isSwinging(now, tiltAngle)) { - isSwinging = true; - mSwingTimestampNanos = now; - } - - // If the tilt angle is too close to horizontal then we cannot determine - // the orientation angle of the screen. - if (Math.abs(tiltAngle) > MAX_TILT) { - if (LOG) { - Slog.v(TAG, "Ignoring sensor data, tilt angle too high: " - + "tiltAngle=" + tiltAngle); - } - clearPredictedRotation(); - } else { - // Calculate the orientation angle. - // This is the angle between the x-y projection of the up vector onto - // the +y-axis, increasing clockwise in a range of [0, 360] degrees. - int orientationAngle = (int) Math.round( - -Math.atan2(-x, y) * RADIANS_TO_DEGREES); - if (orientationAngle < 0) { - // atan2 returns [-180, 180]; normalize to [0, 360] - orientationAngle += 360; - } - - // Find the nearest rotation. - int nearestRotation = (orientationAngle + 45) / 90; - if (nearestRotation == 4) { - nearestRotation = 0; - } - - // Determine the predicted orientation. - if (isTiltAngleAcceptable(nearestRotation, tiltAngle) - && isOrientationAngleAcceptable(nearestRotation, - orientationAngle)) { - updatePredictedRotation(now, nearestRotation); - if (LOG) { - Slog.v(TAG, "Predicted: " - + "tiltAngle=" + tiltAngle - + ", orientationAngle=" + orientationAngle - + ", predictedRotation=" + mPredictedRotation - + ", predictedRotationAgeMS=" - + ((now - mPredictedRotationTimestampNanos) - * 0.000001f)); - } - } else { - if (LOG) { - Slog.v(TAG, "Ignoring sensor data, no predicted rotation: " - + "tiltAngle=" + tiltAngle - + ", orientationAngle=" + orientationAngle); - } - clearPredictedRotation(); - } - } - } - } - - // Determine new proposed rotation. - final int oldProposedRotation = mProposedRotation; - if (mPredictedRotation < 0 || isPredictedRotationAcceptable(now)) { - mProposedRotation = mPredictedRotation; - } - - // Write final statistics about where we are in the orientation detection process. - if (LOG) { - Slog.v(TAG, "Result: currentRotation=" + mOrientationListener.mCurrentRotation - + ", proposedRotation=" + mProposedRotation - + ", predictedRotation=" + mPredictedRotation - + ", timeDeltaMS=" + timeDeltaMS - + ", isAccelerating=" + isAccelerating - + ", isFlat=" + isFlat - + ", isSwinging=" + isSwinging - + ", timeUntilSettledMS=" + remainingMS(now, - mPredictedRotationTimestampNanos + PROPOSAL_SETTLE_TIME_NANOS) - + ", timeUntilAccelerationDelayExpiredMS=" + remainingMS(now, - mAccelerationTimestampNanos + PROPOSAL_MIN_TIME_SINCE_ACCELERATION_ENDED_NANOS) - + ", timeUntilFlatDelayExpiredMS=" + remainingMS(now, - mFlatTimestampNanos + PROPOSAL_MIN_TIME_SINCE_FLAT_ENDED_NANOS) - + ", timeUntilSwingDelayExpiredMS=" + remainingMS(now, - mSwingTimestampNanos + PROPOSAL_MIN_TIME_SINCE_SWING_ENDED_NANOS)); - } - - // Tell the listener. - if (mProposedRotation != oldProposedRotation && mProposedRotation >= 0) { - if (LOG) { - Slog.v(TAG, "Proposed rotation changed! proposedRotation=" + mProposedRotation - + ", oldProposedRotation=" + oldProposedRotation); - } - mOrientationListener.onProposedRotationChanged(mProposedRotation); - } - } - - /** - * Returns true if the tilt angle is acceptable for a given predicted rotation. - */ - private boolean isTiltAngleAcceptable(int rotation, int tiltAngle) { - return tiltAngle >= TILT_TOLERANCE[rotation][0] - && tiltAngle <= TILT_TOLERANCE[rotation][1]; - } - - /** - * Returns true if the orientation angle is acceptable for a given predicted rotation. - * - * This function takes into account the gap between adjacent orientations - * for hysteresis. - */ - private boolean isOrientationAngleAcceptable(int rotation, int orientationAngle) { - // If there is no current rotation, then there is no gap. - // The gap is used only to introduce hysteresis among advertised orientation - // changes to avoid flapping. - final int currentRotation = mOrientationListener.mCurrentRotation; - if (currentRotation >= 0) { - // If the specified rotation is the same or is counter-clockwise adjacent - // to the current rotation, then we set a lower bound on the orientation angle. - // For example, if currentRotation is ROTATION_0 and proposed is ROTATION_90, - // then we want to check orientationAngle > 45 + GAP / 2. - if (rotation == currentRotation - || rotation == (currentRotation + 1) % 4) { - int lowerBound = rotation * 90 - 45 - + ADJACENT_ORIENTATION_ANGLE_GAP / 2; - if (rotation == 0) { - if (orientationAngle >= 315 && orientationAngle < lowerBound + 360) { - return false; - } - } else { - if (orientationAngle < lowerBound) { - return false; - } - } - } - - // If the specified rotation is the same or is clockwise adjacent, - // then we set an upper bound on the orientation angle. - // For example, if currentRotation is ROTATION_0 and rotation is ROTATION_270, - // then we want to check orientationAngle < 315 - GAP / 2. - if (rotation == currentRotation - || rotation == (currentRotation + 3) % 4) { - int upperBound = rotation * 90 + 45 - - ADJACENT_ORIENTATION_ANGLE_GAP / 2; - if (rotation == 0) { - if (orientationAngle <= 45 && orientationAngle > upperBound) { - return false; - } - } else { - if (orientationAngle > upperBound) { - return false; - } - } - } - } - return true; - } - - /** - * Returns true if the predicted rotation is ready to be advertised as a - * proposed rotation. - */ - private boolean isPredictedRotationAcceptable(long now) { - // The predicted rotation must have settled long enough. - if (now < mPredictedRotationTimestampNanos + PROPOSAL_SETTLE_TIME_NANOS) { - return false; - } - - // The last flat state (time since picked up) must have been sufficiently long ago. - if (now < mFlatTimestampNanos + PROPOSAL_MIN_TIME_SINCE_FLAT_ENDED_NANOS) { - return false; - } - - // The last swing state (time since last movement to put down) must have been - // sufficiently long ago. - if (now < mSwingTimestampNanos + PROPOSAL_MIN_TIME_SINCE_SWING_ENDED_NANOS) { - return false; - } - - // The last acceleration state must have been sufficiently long ago. - if (now < mAccelerationTimestampNanos - + PROPOSAL_MIN_TIME_SINCE_ACCELERATION_ENDED_NANOS) { - return false; - } - - // Looks good! - return true; - } - - private void reset() { - mLastFilteredTimestampNanos = Long.MIN_VALUE; - mProposedRotation = -1; - mFlatTimestampNanos = Long.MIN_VALUE; - mSwingTimestampNanos = Long.MIN_VALUE; - mAccelerationTimestampNanos = Long.MIN_VALUE; - clearPredictedRotation(); - clearTiltHistory(); - } - - private void clearPredictedRotation() { - mPredictedRotation = -1; - mPredictedRotationTimestampNanos = Long.MIN_VALUE; - } - - private void updatePredictedRotation(long now, int rotation) { - if (mPredictedRotation != rotation) { - mPredictedRotation = rotation; - mPredictedRotationTimestampNanos = now; - } - } - - private boolean isAccelerating(float magnitude) { - return magnitude < MIN_ACCELERATION_MAGNITUDE - || magnitude > MAX_ACCELERATION_MAGNITUDE; - } - - private void clearTiltHistory() { - mTiltHistoryTimestampNanos[0] = Long.MIN_VALUE; - mTiltHistoryIndex = 1; - } - - private void addTiltHistoryEntry(long now, float tilt) { - mTiltHistory[mTiltHistoryIndex] = tilt; - mTiltHistoryTimestampNanos[mTiltHistoryIndex] = now; - mTiltHistoryIndex = (mTiltHistoryIndex + 1) % TILT_HISTORY_SIZE; - mTiltHistoryTimestampNanos[mTiltHistoryIndex] = Long.MIN_VALUE; - } - - private boolean isFlat(long now) { - for (int i = mTiltHistoryIndex; (i = nextTiltHistoryIndex(i)) >= 0; ) { - if (mTiltHistory[i] < FLAT_ANGLE) { - break; - } - if (mTiltHistoryTimestampNanos[i] + FLAT_TIME_NANOS <= now) { - // Tilt has remained greater than FLAT_TILT_ANGLE for FLAT_TIME_NANOS. - return true; - } - } - return false; - } - - private boolean isSwinging(long now, float tilt) { - for (int i = mTiltHistoryIndex; (i = nextTiltHistoryIndex(i)) >= 0; ) { - if (mTiltHistoryTimestampNanos[i] + SWING_TIME_NANOS < now) { - break; - } - if (mTiltHistory[i] + SWING_AWAY_ANGLE_DELTA <= tilt) { - // Tilted away by SWING_AWAY_ANGLE_DELTA within SWING_TIME_NANOS. - return true; - } - } - return false; - } - - private int nextTiltHistoryIndex(int index) { - index = (index == 0 ? TILT_HISTORY_SIZE : index) - 1; - return mTiltHistoryTimestampNanos[index] != Long.MIN_VALUE ? index : -1; - } - - private static float remainingMS(long now, long until) { - return now >= until ? 0 : (until - now) * 0.000001f; - } - } -} diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java index 1500905..9603fe5 100644 --- a/core/java/android/view/accessibility/AccessibilityEvent.java +++ b/core/java/android/view/accessibility/AccessibilityEvent.java @@ -19,6 +19,7 @@ package android.view.accessibility; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; +import android.util.Pools.SynchronizedPool; import java.util.ArrayList; import java.util.List; @@ -686,11 +687,8 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par public static final int TYPES_ALL_MASK = 0xFFFFFFFF; private static final int MAX_POOL_SIZE = 10; - private static final Object sPoolLock = new Object(); - private static AccessibilityEvent sPool; - private static int sPoolSize; - private AccessibilityEvent mNext; - private boolean mIsInPool; + private static final SynchronizedPool<AccessibilityEvent> sPool = + new SynchronizedPool<AccessibilityEvent>(MAX_POOL_SIZE); private int mEventType; private CharSequence mPackageName; @@ -916,17 +914,8 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par * @return An instance. */ public static AccessibilityEvent obtain() { - synchronized (sPoolLock) { - if (sPool != null) { - AccessibilityEvent event = sPool; - sPool = sPool.mNext; - sPoolSize--; - event.mNext = null; - event.mIsInPool = false; - return event; - } - return new AccessibilityEvent(); - } + AccessibilityEvent event = sPool.acquire(); + return (event != null) ? event : new AccessibilityEvent(); } /** @@ -939,18 +928,8 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par */ @Override public void recycle() { - if (mIsInPool) { - throw new IllegalStateException("Event already recycled!"); - } clear(); - synchronized (sPoolLock) { - if (sPoolSize <= MAX_POOL_SIZE) { - mNext = sPool; - sPool = this; - mIsInPool = true; - sPoolSize++; - } - } + sPool.release(this); } /** @@ -1137,54 +1116,176 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par * @return The string representation. */ public static String eventTypeToString(int eventType) { - switch (eventType) { - case TYPE_VIEW_CLICKED: - return "TYPE_VIEW_CLICKED"; - case TYPE_VIEW_LONG_CLICKED: - return "TYPE_VIEW_LONG_CLICKED"; - case TYPE_VIEW_SELECTED: - return "TYPE_VIEW_SELECTED"; - case TYPE_VIEW_FOCUSED: - return "TYPE_VIEW_FOCUSED"; - case TYPE_VIEW_TEXT_CHANGED: - return "TYPE_VIEW_TEXT_CHANGED"; - case TYPE_WINDOW_STATE_CHANGED: - return "TYPE_WINDOW_STATE_CHANGED"; - case TYPE_VIEW_HOVER_ENTER: - return "TYPE_VIEW_HOVER_ENTER"; - case TYPE_VIEW_HOVER_EXIT: - return "TYPE_VIEW_HOVER_EXIT"; - case TYPE_NOTIFICATION_STATE_CHANGED: - return "TYPE_NOTIFICATION_STATE_CHANGED"; - case TYPE_TOUCH_EXPLORATION_GESTURE_START: - return "TYPE_TOUCH_EXPLORATION_GESTURE_START"; - case TYPE_TOUCH_EXPLORATION_GESTURE_END: - return "TYPE_TOUCH_EXPLORATION_GESTURE_END"; - case TYPE_WINDOW_CONTENT_CHANGED: - return "TYPE_WINDOW_CONTENT_CHANGED"; - case TYPE_VIEW_TEXT_SELECTION_CHANGED: - return "TYPE_VIEW_TEXT_SELECTION_CHANGED"; - case TYPE_VIEW_SCROLLED: - return "TYPE_VIEW_SCROLLED"; - case TYPE_ANNOUNCEMENT: - return "TYPE_ANNOUNCEMENT"; - case TYPE_VIEW_ACCESSIBILITY_FOCUSED: - return "TYPE_VIEW_ACCESSIBILITY_FOCUSED"; - case TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: - return "TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED"; - case TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY: - return "TYPE_CURRENT_AT_GRANULARITY_MOVEMENT_CHANGED"; - case TYPE_GESTURE_DETECTION_START: - return "TYPE_GESTURE_DETECTION_START"; - case TYPE_GESTURE_DETECTION_END: - return "TYPE_GESTURE_DETECTION_END"; - case TYPE_TOUCH_INTERACTION_START: - return "TYPE_TOUCH_INTERACTION_START"; - case TYPE_TOUCH_INTERACTION_END: - return "TYPE_TOUCH_INTERACTION_END"; - default: - return null; + if (eventType == TYPES_ALL_MASK) { + return "TYPES_ALL_MASK"; } + StringBuilder builder = new StringBuilder(); + int eventTypeCount = 0; + while (eventType != 0) { + final int eventTypeFlag = 1 << Integer.numberOfTrailingZeros(eventType); + eventType &= ~eventTypeFlag; + switch (eventTypeFlag) { + case TYPE_VIEW_CLICKED: { + if (eventTypeCount > 0) { + builder.append(", "); + } + builder.append("TYPE_VIEW_CLICKED"); + eventTypeCount++; + } break; + case TYPE_VIEW_LONG_CLICKED: { + if (eventTypeCount > 0) { + builder.append(", "); + } + builder.append("TYPE_VIEW_LONG_CLICKED"); + eventTypeCount++; + } break; + case TYPE_VIEW_SELECTED: { + if (eventTypeCount > 0) { + builder.append(", "); + } + builder.append("TYPE_VIEW_SELECTED"); + eventTypeCount++; + } break; + case TYPE_VIEW_FOCUSED: { + if (eventTypeCount > 0) { + builder.append(", "); + } + builder.append("TYPE_VIEW_FOCUSED"); + eventTypeCount++; + } break; + case TYPE_VIEW_TEXT_CHANGED: { + if (eventTypeCount > 0) { + builder.append(", "); + } + builder.append("TYPE_VIEW_TEXT_CHANGED"); + eventTypeCount++; + } break; + case TYPE_WINDOW_STATE_CHANGED: { + if (eventTypeCount > 0) { + builder.append(", "); + } + builder.append("TYPE_WINDOW_STATE_CHANGED"); + eventTypeCount++; + } break; + case TYPE_VIEW_HOVER_ENTER: { + if (eventTypeCount > 0) { + builder.append(", "); + } + builder.append("TYPE_VIEW_HOVER_ENTER"); + eventTypeCount++; + } break; + case TYPE_VIEW_HOVER_EXIT: { + if (eventTypeCount > 0) { + builder.append(", "); + } + builder.append("TYPE_VIEW_HOVER_EXIT"); + eventTypeCount++; + } break; + case TYPE_NOTIFICATION_STATE_CHANGED: { + if (eventTypeCount > 0) { + builder.append(", "); + } + builder.append("TYPE_NOTIFICATION_STATE_CHANGED"); + eventTypeCount++; + } break; + case TYPE_TOUCH_EXPLORATION_GESTURE_START: { + if (eventTypeCount > 0) { + builder.append(", "); + } + builder.append("TYPE_TOUCH_EXPLORATION_GESTURE_START"); + eventTypeCount++; + } break; + case TYPE_TOUCH_EXPLORATION_GESTURE_END: { + if (eventTypeCount > 0) { + builder.append(", "); + } + builder.append("TYPE_TOUCH_EXPLORATION_GESTURE_END"); + eventTypeCount++; + } break; + case TYPE_WINDOW_CONTENT_CHANGED: { + if (eventTypeCount > 0) { + builder.append(", "); + } + builder.append("TYPE_WINDOW_CONTENT_CHANGED"); + eventTypeCount++; + } break; + case TYPE_VIEW_TEXT_SELECTION_CHANGED: { + if (eventTypeCount > 0) { + builder.append(", "); + } + builder.append("TYPE_VIEW_TEXT_SELECTION_CHANGED"); + eventTypeCount++; + } break; + case TYPE_VIEW_SCROLLED: { + if (eventTypeCount > 0) { + builder.append(", "); + } + builder.append("TYPE_VIEW_SCROLLED"); + eventTypeCount++; + } break; + case TYPE_ANNOUNCEMENT: { + if (eventTypeCount > 0) { + builder.append(", "); + } + builder.append("TYPE_ANNOUNCEMENT"); + eventTypeCount++; + } break; + case TYPE_VIEW_ACCESSIBILITY_FOCUSED: { + if (eventTypeCount > 0) { + builder.append(", "); + } + builder.append("TYPE_VIEW_ACCESSIBILITY_FOCUSED"); + eventTypeCount++; + } break; + case TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: { + if (eventTypeCount > 0) { + builder.append(", "); + } + builder.append("TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED"); + eventTypeCount++; + } break; + case TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY: { + if (eventTypeCount > 0) { + builder.append(", "); + } + builder.append("TYPE_CURRENT_AT_GRANULARITY_MOVEMENT_CHANGED"); + eventTypeCount++; + } break; + case TYPE_GESTURE_DETECTION_START: { + if (eventTypeCount > 0) { + builder.append(", "); + } + builder.append("TYPE_GESTURE_DETECTION_START"); + eventTypeCount++; + } break; + case TYPE_GESTURE_DETECTION_END: { + if (eventTypeCount > 0) { + builder.append(", "); + } + builder.append("TYPE_GESTURE_DETECTION_END"); + eventTypeCount++; + } break; + case TYPE_TOUCH_INTERACTION_START: { + if (eventTypeCount > 0) { + builder.append(", "); + } + builder.append("TYPE_TOUCH_INTERACTION_START"); + eventTypeCount++; + } break; + case TYPE_TOUCH_INTERACTION_END: { + if (eventTypeCount > 0) { + builder.append(", "); + } + builder.append("TYPE_TOUCH_INTERACTION_END"); + eventTypeCount++; + } break; + } + } + if (eventTypeCount > 1) { + builder.insert(0, '['); + builder.append(']'); + } + return builder.toString(); } /** diff --git a/core/java/android/view/accessibility/AccessibilityInteractionClient.java b/core/java/android/view/accessibility/AccessibilityInteractionClient.java index 20b5f17..84d7e72 100644 --- a/core/java/android/view/accessibility/AccessibilityInteractionClient.java +++ b/core/java/android/view/accessibility/AccessibilityInteractionClient.java @@ -17,7 +17,6 @@ package android.view.accessibility; import android.accessibilityservice.IAccessibilityServiceConnection; -import android.graphics.Rect; import android.os.Binder; import android.os.Build; import android.os.Bundle; @@ -102,8 +101,6 @@ public final class AccessibilityInteractionClient private Message mSameThreadMessage; - private final Rect mTempBounds = new Rect(); - // The connection cache is shared between all interrogating threads. private static final SparseArray<IAccessibilityServiceConnection> sConnectionCache = new SparseArray<IAccessibilityServiceConnection>(); @@ -194,14 +191,14 @@ public final class AccessibilityInteractionClient return cachedInfo; } final int interactionId = mInteractionIdCounter.getAndIncrement(); - final float windowScale = connection.findAccessibilityNodeInfoByAccessibilityId( + final boolean success = connection.findAccessibilityNodeInfoByAccessibilityId( accessibilityWindowId, accessibilityNodeId, interactionId, this, prefetchFlags, Thread.currentThread().getId()); // If the scale is zero the call has failed. - if (windowScale > 0) { + if (success) { List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( interactionId); - finalizeAndCacheAccessibilityNodeInfos(infos, connectionId, windowScale); + finalizeAndCacheAccessibilityNodeInfos(infos, connectionId); if (infos != null && !infos.isEmpty()) { return infos.get(0); } @@ -233,25 +230,25 @@ public final class AccessibilityInteractionClient * where to start the search. Use * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} * to start from the root. - * @param viewId The id of the view. - * @return An {@link AccessibilityNodeInfo} if found, null otherwise. + * @param viewId The fully qualified resource name of the view id to find. + * @return An list of {@link AccessibilityNodeInfo} if found, empty list otherwise. */ - public AccessibilityNodeInfo findAccessibilityNodeInfoByViewId(int connectionId, - int accessibilityWindowId, long accessibilityNodeId, int viewId) { + public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewId(int connectionId, + int accessibilityWindowId, long accessibilityNodeId, String viewId) { try { IAccessibilityServiceConnection connection = getConnection(connectionId); if (connection != null) { final int interactionId = mInteractionIdCounter.getAndIncrement(); - final float windowScale = - 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( + final boolean success = connection.findAccessibilityNodeInfosByViewId( + accessibilityWindowId, accessibilityNodeId, viewId, interactionId, this, + Thread.currentThread().getId()); + if (success) { + List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( interactionId); - finalizeAndCacheAccessibilityNodeInfo(info, connectionId, windowScale); - return info; + if (infos != null) { + finalizeAndCacheAccessibilityNodeInfos(infos, connectionId); + return infos; + } } } else { if (DEBUG) { @@ -264,7 +261,7 @@ public final class AccessibilityInteractionClient + " findAccessibilityNodeInfoByViewIdInActiveWindow", re); } } - return null; + return Collections.emptyList(); } /** @@ -290,15 +287,16 @@ public final class AccessibilityInteractionClient IAccessibilityServiceConnection connection = getConnection(connectionId); if (connection != null) { final int interactionId = mInteractionIdCounter.getAndIncrement(); - final float windowScale = connection.findAccessibilityNodeInfosByText( + final boolean success = connection.findAccessibilityNodeInfosByText( accessibilityWindowId, accessibilityNodeId, text, interactionId, this, Thread.currentThread().getId()); - // If the scale is zero the call has failed. - if (windowScale > 0) { + if (success) { List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( interactionId); - finalizeAndCacheAccessibilityNodeInfos(infos, connectionId, windowScale); - return infos; + if (infos != null) { + finalizeAndCacheAccessibilityNodeInfos(infos, connectionId); + return infos; + } } } else { if (DEBUG) { @@ -336,14 +334,13 @@ public final class AccessibilityInteractionClient IAccessibilityServiceConnection connection = getConnection(connectionId); if (connection != null) { final int interactionId = mInteractionIdCounter.getAndIncrement(); - final float windowScale = connection.findFocus(accessibilityWindowId, + final boolean success = connection.findFocus(accessibilityWindowId, accessibilityNodeId, focusType, interactionId, this, Thread.currentThread().getId()); - // If the scale is zero the call has failed. - if (windowScale > 0) { + if (success) { AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear( interactionId); - finalizeAndCacheAccessibilityNodeInfo(info, connectionId, windowScale); + finalizeAndCacheAccessibilityNodeInfo(info, connectionId); return info; } } else { @@ -381,14 +378,13 @@ public final class AccessibilityInteractionClient IAccessibilityServiceConnection connection = getConnection(connectionId); if (connection != null) { final int interactionId = mInteractionIdCounter.getAndIncrement(); - final float windowScale = connection.focusSearch(accessibilityWindowId, + final boolean success = connection.focusSearch(accessibilityWindowId, accessibilityNodeId, direction, interactionId, this, Thread.currentThread().getId()); - // If the scale is zero the call has failed. - if (windowScale > 0) { + if (success) { AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear( interactionId); - finalizeAndCacheAccessibilityNodeInfo(info, connectionId, windowScale); + finalizeAndCacheAccessibilityNodeInfo(info, connectionId); return info; } } else { @@ -604,36 +600,14 @@ public final class AccessibilityInteractionClient } /** - * Applies compatibility scale to the info bounds if it is not equal to one. - * - * @param info The info whose bounds to scale. - * @param scale The scale to apply. - */ - private void applyCompatibilityScaleIfNeeded(AccessibilityNodeInfo info, float scale) { - if (scale == 1.0f) { - return; - } - Rect bounds = mTempBounds; - info.getBoundsInParent(bounds); - bounds.scale(scale); - info.setBoundsInParent(bounds); - - info.getBoundsInScreen(bounds); - bounds.scale(scale); - info.setBoundsInScreen(bounds); - } - - /** * Finalize an {@link AccessibilityNodeInfo} before passing it to the client. * * @param info The info. * @param connectionId The id of the connection to the system. - * @param windowScale The source window compatibility scale. */ - private void finalizeAndCacheAccessibilityNodeInfo(AccessibilityNodeInfo info, int connectionId, - float windowScale) { + private void finalizeAndCacheAccessibilityNodeInfo(AccessibilityNodeInfo info, + int connectionId) { if (info != null) { - applyCompatibilityScaleIfNeeded(info, windowScale); info.setConnectionId(connectionId); info.setSealed(true); sAccessibilityNodeInfoCache.add(info); @@ -645,15 +619,14 @@ public final class AccessibilityInteractionClient * * @param infos The {@link AccessibilityNodeInfo}s. * @param connectionId The id of the connection to the system. - * @param windowScale The source window compatibility scale. */ private void finalizeAndCacheAccessibilityNodeInfos(List<AccessibilityNodeInfo> infos, - int connectionId, float windowScale) { + int connectionId) { if (infos != null) { final int infosCount = infos.size(); for (int i = 0; i < infosCount; i++) { AccessibilityNodeInfo info = infos.get(i); - finalizeAndCacheAccessibilityNodeInfo(info, connectionId, windowScale); + finalizeAndCacheAccessibilityNodeInfo(info, connectionId); } } } diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java index 1dc2487..ad87fcb 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -16,10 +16,12 @@ package android.view.accessibility; +import android.accessibilityservice.AccessibilityServiceInfo; import android.graphics.Rect; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; +import android.util.Pools.SynchronizedPool; import android.util.SparseLongArray; import android.view.View; @@ -77,7 +79,10 @@ public class AccessibilityNodeInfo implements Parcelable { public static final int FLAG_PREFETCH_DESCENDANTS = 0x00000004; /** @hide */ - public static final int INCLUDE_NOT_IMPORTANT_VIEWS = 0x00000008; + public static final int FLAG_INCLUDE_NOT_IMPORTANT_VIEWS = 0x00000008; + + /** @hide */ + public static final int FLAG_REPORT_VIEW_IDS = 0x00000010; // Actions. @@ -126,16 +131,22 @@ public class AccessibilityNodeInfo implements Parcelable { * at a given movement granularity. For example, move to the next character, * word, etc. * <p> - * <strong>Arguments:</strong> {@link #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT}<br> - * <strong>Example:</strong> + * <strong>Arguments:</strong> {@link #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT}<, + * {@link #ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN}<br> + * <strong>Example:</strong> Move to the previous character and do not extend selection. * <code><pre><p> * Bundle arguments = new Bundle(); * arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT, * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER); + * arguments.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, + * false); * info.performAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, arguments); * </code></pre></p> * </p> * + * @see #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT + * @see #ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN + * * @see #setMovementGranularities(int) * @see #getMovementGranularities() * @@ -152,17 +163,23 @@ public class AccessibilityNodeInfo implements Parcelable { * at a given movement granularity. For example, move to the next character, * word, etc. * <p> - * <strong>Arguments:</strong> {@link #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT}<br> - * <strong>Example:</strong> + * <strong>Arguments:</strong> {@link #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT}<, + * {@link #ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN}<br> + * <strong>Example:</strong> Move to the next character and do not extend selection. * <code><pre><p> * Bundle arguments = new Bundle(); * arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT, * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER); + * arguments.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, + * false); * info.performAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY, * arguments); * </code></pre></p> * </p> * + * @see #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT + * @see #ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN + * * @see #setMovementGranularities(int) * @see #getMovementGranularities() * @@ -215,15 +232,53 @@ public class AccessibilityNodeInfo implements Parcelable { public static final int ACTION_SCROLL_BACKWARD = 0x00002000; /** + * Action to copy the current selection to the clipboard. + */ + public static final int ACTION_COPY = 0x00004000; + + /** + * Action to paste the current clipboard content. + */ + public static final int ACTION_PASTE = 0x00008000; + + /** + * Action to cut the current selection and place it to the clipboard. + */ + public static final int ACTION_CUT = 0x00010000; + + /** + * Action to set the selection. Performing this action with no arguments + * clears the selection. + * <p> + * <strong>Arguments:</strong> {@link #ACTION_ARGUMENT_SELECTION_START_INT}, + * {@link #ACTION_ARGUMENT_SELECTION_END_INT}<br> + * <strong>Example:</strong> + * <code><pre><p> + * Bundle arguments = new Bundle(); + * arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT, 1); + * arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT, 2); + * info.performAction(AccessibilityNodeInfo.ACTION_SET_SELECTION, arguments); + * </code></pre></p> + * </p> + * + * @see #ACTION_ARGUMENT_SELECTION_START_INT + * @see #ACTION_ARGUMENT_SELECTION_END_INT + */ + public static final int ACTION_SET_SELECTION = 0x00020000; + + /** * Argument for which movement granularity to be used when traversing the node text. * <p> * <strong>Type:</strong> int<br> * <strong>Actions:</strong> {@link #ACTION_NEXT_AT_MOVEMENT_GRANULARITY}, * {@link #ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY} * </p> + * + * @see #ACTION_NEXT_AT_MOVEMENT_GRANULARITY + * @see #ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY */ public static final String ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT = - "ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT"; + "ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT"; /** * Argument for which HTML element to get moving to the next/previous HTML element. @@ -232,9 +287,51 @@ public class AccessibilityNodeInfo implements Parcelable { * <strong>Actions:</strong> {@link #ACTION_NEXT_HTML_ELEMENT}, * {@link #ACTION_PREVIOUS_HTML_ELEMENT} * </p> + * + * @see #ACTION_NEXT_HTML_ELEMENT + * @see #ACTION_PREVIOUS_HTML_ELEMENT */ public static final String ACTION_ARGUMENT_HTML_ELEMENT_STRING = - "ACTION_ARGUMENT_HTML_ELEMENT_STRING"; + "ACTION_ARGUMENT_HTML_ELEMENT_STRING"; + + /** + * Argument for whether when moving at granularity to extend the selection + * or to move it otherwise. + * <p> + * <strong>Type:</strong> boolean<br> + * <strong>Actions:</strong> {@link #ACTION_NEXT_AT_MOVEMENT_GRANULARITY}, + * {@link #ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY} + * </p> + * + * @see #ACTION_NEXT_AT_MOVEMENT_GRANULARITY + * @see #ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY + */ + public static final String ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN = + "ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN"; + + /** + * Argument for specifying the selection start. + * <p> + * <strong>Type:</strong> int<br> + * <strong>Actions:</strong> {@link #ACTION_SET_SELECTION} + * </p> + * + * @see #ACTION_SET_SELECTION + */ + public static final String ACTION_ARGUMENT_SELECTION_START_INT = + "ACTION_ARGUMENT_SELECTION_START_INT"; + + /** + * Argument for specifying the selection end. + * <p> + * <strong>Type:</strong> int<br> + * <strong>Actions:</strong> {@link #ACTION_SET_SELECTION} + * </p> + * + * @see #ACTION_SET_SELECTION + */ + public static final String ACTION_ARGUMENT_SELECTION_END_INT = + "ACTION_ARGUMENT_SELECTION_END_INT"; /** * The input focus. @@ -275,29 +372,31 @@ public class AccessibilityNodeInfo implements Parcelable { // Boolean attributes. - private static final int PROPERTY_CHECKABLE = 0x00000001; + private static final int BOOLEAN_PROPERTY_CHECKABLE = 0x00000001; + + private static final int BOOLEAN_PROPERTY_CHECKED = 0x00000002; - private static final int PROPERTY_CHECKED = 0x00000002; + private static final int BOOLEAN_PROPERTY_FOCUSABLE = 0x00000004; - private static final int PROPERTY_FOCUSABLE = 0x00000004; + private static final int BOOLEAN_PROPERTY_FOCUSED = 0x00000008; - private static final int PROPERTY_FOCUSED = 0x00000008; + private static final int BOOLEAN_PROPERTY_SELECTED = 0x00000010; - private static final int PROPERTY_SELECTED = 0x00000010; + private static final int BOOLEAN_PROPERTY_CLICKABLE = 0x00000020; - private static final int PROPERTY_CLICKABLE = 0x00000020; + private static final int BOOLEAN_PROPERTY_LONG_CLICKABLE = 0x00000040; - private static final int PROPERTY_LONG_CLICKABLE = 0x00000040; + private static final int BOOLEAN_PROPERTY_ENABLED = 0x00000080; - private static final int PROPERTY_ENABLED = 0x00000080; + private static final int BOOLEAN_PROPERTY_PASSWORD = 0x00000100; - private static final int PROPERTY_PASSWORD = 0x00000100; + private static final int BOOLEAN_PROPERTY_SCROLLABLE = 0x00000200; - private static final int PROPERTY_SCROLLABLE = 0x00000200; + private static final int BOOLEAN_PROPERTY_ACCESSIBILITY_FOCUSED = 0x00000400; - private static final int PROPERTY_ACCESSIBILITY_FOCUSED = 0x00000400; + private static final int BOOLEAN_PROPERTY_VISIBLE_TO_USER = 0x00000800; - private static final int PROPERTY_VISIBLE_TO_USER = 0x00000800; + private static final int BOOLEAN_PROPERTY_EDITABLE = 0x00001000; /** * Bits that provide the id of a virtual descendant of a view. @@ -354,11 +453,9 @@ public class AccessibilityNodeInfo implements Parcelable { // Housekeeping. private static final int MAX_POOL_SIZE = 50; - private static final Object sPoolLock = new Object(); - private static AccessibilityNodeInfo sPool; - private static int sPoolSize; - private AccessibilityNodeInfo mNext; - private boolean mIsInPool; + private static final SynchronizedPool<AccessibilityNodeInfo> sPool = + new SynchronizedPool<AccessibilityNodeInfo>(MAX_POOL_SIZE); + private boolean mSealed; // Data. @@ -376,12 +473,16 @@ public class AccessibilityNodeInfo implements Parcelable { private CharSequence mClassName; private CharSequence mText; private CharSequence mContentDescription; + private CharSequence mViewIdResourceName; private final SparseLongArray mChildNodeIds = new SparseLongArray(); private int mActions; private int mMovementGranularities; + private int mTextSelectionStart = UNDEFINED; + private int mTextSelectionEnd = UNDEFINED; + private int mConnectionId = UNDEFINED; /** @@ -487,6 +588,31 @@ public class AccessibilityNodeInfo implements Parcelable { } /** + * Refreshes this info with the latest state of the view it represents. + * <p> + * <strong>Note:</strong> If this method returns false this info is obsolete + * since it represents a view that is no longer in the view tree and should + * be recycled. + * </p> + * @return Whether the refresh succeeded. + */ + public boolean refresh() { + enforceSealed(); + if (!canPerformRequestOverConnection(mSourceNodeId)) { + return false; + } + AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); + AccessibilityNodeInfo refreshedInfo = client.findAccessibilityNodeInfoByAccessibilityId( + mConnectionId, mWindowId, mSourceNodeId, 0); + if (refreshedInfo == null) { + return false; + } + init(refreshedInfo); + refreshedInfo.recycle(); + return true; + } + + /** * @return The ids of the children. * * @hide @@ -705,6 +831,37 @@ public class AccessibilityNodeInfo implements Parcelable { } /** + * Finds {@link AccessibilityNodeInfo}s by the fully qualified view id's resource + * name where a fully qualified id is of the from "package:id/id_resource_name". + * For example, if the target application's package is "foo.bar" and the id + * resource name is "baz", the fully qualified resource id is "foo.bar:id/baz". + * + * <p> + * <strong>Note:</strong> It is a client responsibility to recycle the + * received info by calling {@link AccessibilityNodeInfo#recycle()} + * to avoid creating of multiple instances. + * </p> + * <p> + * <strong>Note:</strong> The primary usage of this API is for UI test automation + * and in order to report the fully qualified view id if an {@link AccessibilityNodeInfo} + * the client has to set the {@link AccessibilityServiceInfo#FLAG_REPORT_VIEW_IDS} + * flag when configuring his {@link android.accessibilityservice.AccessibilityService}. + * </p> + * + * @param viewId The fully qualified resource name of the view id to find. + * @return A list of node info. + */ + public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewId(String viewId) { + enforceSealed(); + if (!canPerformRequestOverConnection(mSourceNodeId)) { + return Collections.emptyList(); + } + AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); + return client.findAccessibilityNodeInfosByViewId(mConnectionId, mWindowId, mSourceNodeId, + viewId); + } + + /** * Gets the parent. * <p> * <strong>Note:</strong> It is a client responsibility to recycle the @@ -835,7 +992,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @return True if the node is checkable. */ public boolean isCheckable() { - return getBooleanProperty(PROPERTY_CHECKABLE); + return getBooleanProperty(BOOLEAN_PROPERTY_CHECKABLE); } /** @@ -851,7 +1008,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @throws IllegalStateException If called from an AccessibilityService. */ public void setCheckable(boolean checkable) { - setBooleanProperty(PROPERTY_CHECKABLE, checkable); + setBooleanProperty(BOOLEAN_PROPERTY_CHECKABLE, checkable); } /** @@ -860,7 +1017,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @return True if the node is checked. */ public boolean isChecked() { - return getBooleanProperty(PROPERTY_CHECKED); + return getBooleanProperty(BOOLEAN_PROPERTY_CHECKED); } /** @@ -876,7 +1033,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @throws IllegalStateException If called from an AccessibilityService. */ public void setChecked(boolean checked) { - setBooleanProperty(PROPERTY_CHECKED, checked); + setBooleanProperty(BOOLEAN_PROPERTY_CHECKED, checked); } /** @@ -885,7 +1042,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @return True if the node is focusable. */ public boolean isFocusable() { - return getBooleanProperty(PROPERTY_FOCUSABLE); + return getBooleanProperty(BOOLEAN_PROPERTY_FOCUSABLE); } /** @@ -901,7 +1058,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @throws IllegalStateException If called from an AccessibilityService. */ public void setFocusable(boolean focusable) { - setBooleanProperty(PROPERTY_FOCUSABLE, focusable); + setBooleanProperty(BOOLEAN_PROPERTY_FOCUSABLE, focusable); } /** @@ -910,7 +1067,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @return True if the node is focused. */ public boolean isFocused() { - return getBooleanProperty(PROPERTY_FOCUSED); + return getBooleanProperty(BOOLEAN_PROPERTY_FOCUSED); } /** @@ -926,7 +1083,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @throws IllegalStateException If called from an AccessibilityService. */ public void setFocused(boolean focused) { - setBooleanProperty(PROPERTY_FOCUSED, focused); + setBooleanProperty(BOOLEAN_PROPERTY_FOCUSED, focused); } /** @@ -935,7 +1092,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @return Whether the node is visible to the user. */ public boolean isVisibleToUser() { - return getBooleanProperty(PROPERTY_VISIBLE_TO_USER); + return getBooleanProperty(BOOLEAN_PROPERTY_VISIBLE_TO_USER); } /** @@ -951,7 +1108,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @throws IllegalStateException If called from an AccessibilityService. */ public void setVisibleToUser(boolean visibleToUser) { - setBooleanProperty(PROPERTY_VISIBLE_TO_USER, visibleToUser); + setBooleanProperty(BOOLEAN_PROPERTY_VISIBLE_TO_USER, visibleToUser); } /** @@ -960,7 +1117,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @return True if the node is accessibility focused. */ public boolean isAccessibilityFocused() { - return getBooleanProperty(PROPERTY_ACCESSIBILITY_FOCUSED); + return getBooleanProperty(BOOLEAN_PROPERTY_ACCESSIBILITY_FOCUSED); } /** @@ -976,7 +1133,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @throws IllegalStateException If called from an AccessibilityService. */ public void setAccessibilityFocused(boolean focused) { - setBooleanProperty(PROPERTY_ACCESSIBILITY_FOCUSED, focused); + setBooleanProperty(BOOLEAN_PROPERTY_ACCESSIBILITY_FOCUSED, focused); } /** @@ -985,7 +1142,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @return True if the node is selected. */ public boolean isSelected() { - return getBooleanProperty(PROPERTY_SELECTED); + return getBooleanProperty(BOOLEAN_PROPERTY_SELECTED); } /** @@ -1001,7 +1158,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @throws IllegalStateException If called from an AccessibilityService. */ public void setSelected(boolean selected) { - setBooleanProperty(PROPERTY_SELECTED, selected); + setBooleanProperty(BOOLEAN_PROPERTY_SELECTED, selected); } /** @@ -1010,7 +1167,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @return True if the node is clickable. */ public boolean isClickable() { - return getBooleanProperty(PROPERTY_CLICKABLE); + return getBooleanProperty(BOOLEAN_PROPERTY_CLICKABLE); } /** @@ -1026,7 +1183,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @throws IllegalStateException If called from an AccessibilityService. */ public void setClickable(boolean clickable) { - setBooleanProperty(PROPERTY_CLICKABLE, clickable); + setBooleanProperty(BOOLEAN_PROPERTY_CLICKABLE, clickable); } /** @@ -1035,7 +1192,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @return True if the node is long clickable. */ public boolean isLongClickable() { - return getBooleanProperty(PROPERTY_LONG_CLICKABLE); + return getBooleanProperty(BOOLEAN_PROPERTY_LONG_CLICKABLE); } /** @@ -1051,7 +1208,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @throws IllegalStateException If called from an AccessibilityService. */ public void setLongClickable(boolean longClickable) { - setBooleanProperty(PROPERTY_LONG_CLICKABLE, longClickable); + setBooleanProperty(BOOLEAN_PROPERTY_LONG_CLICKABLE, longClickable); } /** @@ -1060,7 +1217,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @return True if the node is enabled. */ public boolean isEnabled() { - return getBooleanProperty(PROPERTY_ENABLED); + return getBooleanProperty(BOOLEAN_PROPERTY_ENABLED); } /** @@ -1076,7 +1233,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @throws IllegalStateException If called from an AccessibilityService. */ public void setEnabled(boolean enabled) { - setBooleanProperty(PROPERTY_ENABLED, enabled); + setBooleanProperty(BOOLEAN_PROPERTY_ENABLED, enabled); } /** @@ -1085,7 +1242,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @return True if the node is a password. */ public boolean isPassword() { - return getBooleanProperty(PROPERTY_PASSWORD); + return getBooleanProperty(BOOLEAN_PROPERTY_PASSWORD); } /** @@ -1101,7 +1258,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @throws IllegalStateException If called from an AccessibilityService. */ public void setPassword(boolean password) { - setBooleanProperty(PROPERTY_PASSWORD, password); + setBooleanProperty(BOOLEAN_PROPERTY_PASSWORD, password); } /** @@ -1110,7 +1267,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @return True if the node is scrollable, false otherwise. */ public boolean isScrollable() { - return getBooleanProperty(PROPERTY_SCROLLABLE); + return getBooleanProperty(BOOLEAN_PROPERTY_SCROLLABLE); } /** @@ -1127,7 +1284,32 @@ public class AccessibilityNodeInfo implements Parcelable { */ public void setScrollable(boolean scrollable) { enforceNotSealed(); - setBooleanProperty(PROPERTY_SCROLLABLE, scrollable); + setBooleanProperty(BOOLEAN_PROPERTY_SCROLLABLE, scrollable); + } + + /** + * Gets if the node is editable. + * + * @return True if the node is editable, false otherwise. + */ + public boolean isEditable() { + return getBooleanProperty(BOOLEAN_PROPERTY_EDITABLE); + } + + /** + * Sets whether this node is editable. + * <p> + * <strong>Note:</strong> Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * + * @param editable True if the node is editable. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setEditable(boolean editable) { + setBooleanProperty(BOOLEAN_PROPERTY_EDITABLE, editable); } /** @@ -1349,6 +1531,75 @@ public class AccessibilityNodeInfo implements Parcelable { } /** + * Sets the fully qualified resource name of the source view's id. + * + * <p> + * <strong>Note:</strong> Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * + * @param viewIdResName The id resource name. + */ + public void setViewIdResourceName(CharSequence viewIdResName) { + enforceNotSealed(); + mViewIdResourceName = viewIdResName; + } + + /** + * Gets the fully qualified resource name of the source view's id. + * + * <p> + * <strong>Note:</strong> The primary usage of this API is for UI test automation + * and in order to report the source view id of an {@link AccessibilityNodeInfo} the + * client has to set the {@link AccessibilityServiceInfo#FLAG_REPORT_VIEW_IDS} + * flag when configuring his {@link android.accessibilityservice.AccessibilityService}. + * </p> + + * @return The id resource name. + */ + public CharSequence getViewIdResourceName() { + return mViewIdResourceName; + } + + /** + * Gets the text selection start. + * + * @return The text selection start if there is selection or -1. + */ + public int getTextSelectionStart() { + return mTextSelectionStart; + } + + /** + * Gets the text selection end. + * + * @return The text selection end if there is selection or -1. + */ + public int getTextSelectionEnd() { + return mTextSelectionEnd; + } + + /** + * Sets the text selection start and end. + * <p> + * <strong>Note:</strong> Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * + * @param start The text selection start. + * @param end The text selection end. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setTextSelection(int start, int end) { + enforceNotSealed(); + mTextSelectionStart = start; + mTextSelectionEnd = end; + } + + /** * Gets the value of a boolean property. * * @param property The property. @@ -1517,17 +1768,8 @@ public class AccessibilityNodeInfo implements Parcelable { * @return An instance. */ public static AccessibilityNodeInfo obtain() { - synchronized (sPoolLock) { - if (sPool != null) { - AccessibilityNodeInfo info = sPool; - sPool = sPool.mNext; - sPoolSize--; - info.mNext = null; - info.mIsInPool = false; - return info; - } - return new AccessibilityNodeInfo(); - } + AccessibilityNodeInfo info = sPool.acquire(); + return (info != null) ? info : new AccessibilityNodeInfo(); } /** @@ -1552,18 +1794,8 @@ public class AccessibilityNodeInfo implements Parcelable { * @throws IllegalStateException If the info is already recycled. */ public void recycle() { - if (mIsInPool) { - throw new IllegalStateException("Info already recycled!"); - } clear(); - synchronized (sPoolLock) { - if (sPoolSize <= MAX_POOL_SIZE) { - mNext = sPool; - sPool = this; - mIsInPool = true; - sPoolSize++; - } - } + sPool.release(this); } /** @@ -1609,6 +1841,10 @@ public class AccessibilityNodeInfo implements Parcelable { parcel.writeCharSequence(mClassName); parcel.writeCharSequence(mText); parcel.writeCharSequence(mContentDescription); + parcel.writeCharSequence(mViewIdResourceName); + + parcel.writeInt(mTextSelectionStart); + parcel.writeInt(mTextSelectionEnd); // Since instances of this class are fetched via synchronous i.e. blocking // calls in IPCs we always recycle as soon as the instance is marshaled. @@ -1620,7 +1856,6 @@ public class AccessibilityNodeInfo implements Parcelable { * * @param other The other instance. */ - @SuppressWarnings("unchecked") private void init(AccessibilityNodeInfo other) { mSealed = other.mSealed; mSourceNodeId = other.mSourceNodeId; @@ -1635,6 +1870,7 @@ public class AccessibilityNodeInfo implements Parcelable { mClassName = other.mClassName; mText = other.mText; mContentDescription = other.mContentDescription; + mViewIdResourceName = other.mViewIdResourceName; mActions= other.mActions; mBooleanProperties = other.mBooleanProperties; mMovementGranularities = other.mMovementGranularities; @@ -1642,6 +1878,8 @@ public class AccessibilityNodeInfo implements Parcelable { for (int i = 0; i < otherChildIdCount; i++) { mChildNodeIds.put(i, other.mChildNodeIds.valueAt(i)); } + mTextSelectionStart = other.mTextSelectionStart; + mTextSelectionEnd = other.mTextSelectionEnd; } /** @@ -1685,6 +1923,10 @@ public class AccessibilityNodeInfo implements Parcelable { mClassName = parcel.readCharSequence(); mText = parcel.readCharSequence(); mContentDescription = parcel.readCharSequence(); + mViewIdResourceName = parcel.readCharSequence(); + + mTextSelectionStart = parcel.readInt(); + mTextSelectionEnd = parcel.readInt(); } /** @@ -1707,7 +1949,10 @@ public class AccessibilityNodeInfo implements Parcelable { mClassName = null; mText = null; mContentDescription = null; + mViewIdResourceName = null; mActions = 0; + mTextSelectionStart = UNDEFINED; + mTextSelectionEnd = UNDEFINED; } /** @@ -1746,8 +1991,16 @@ public class AccessibilityNodeInfo implements Parcelable { return "ACTION_SCROLL_FORWARD"; case ACTION_SCROLL_BACKWARD: return "ACTION_SCROLL_BACKWARD"; + case ACTION_CUT: + return "ACTION_CUT"; + case ACTION_COPY: + return "ACTION_COPY"; + case ACTION_PASTE: + return "ACTION_PASTE"; + case ACTION_SET_SELECTION: + return "ACTION_SET_SELECTION"; default: - throw new IllegalArgumentException("Unknown action: " + action); + return"ACTION_UNKNOWN"; } } @@ -1851,6 +2104,7 @@ public class AccessibilityNodeInfo implements Parcelable { builder.append("; className: ").append(mClassName); builder.append("; text: ").append(mText); builder.append("; contentDescription: ").append(mContentDescription); + builder.append("; viewIdResName: ").append(mViewIdResourceName); builder.append("; checkable: ").append(isCheckable()); builder.append("; checked: ").append(isChecked()); diff --git a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl index 9b39300..8d15472 100644 --- a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl +++ b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl @@ -17,6 +17,7 @@ package android.view.accessibility; import android.os.Bundle; +import android.view.MagnificationSpec; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.IAccessibilityInteractionConnectionCallback; @@ -30,23 +31,23 @@ oneway interface IAccessibilityInteractionConnection { void findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, - long interrogatingTid); + long interrogatingTid, in MagnificationSpec spec); - void findAccessibilityNodeInfoByViewId(long accessibilityNodeId, int viewId, int interactionId, - IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, - long interrogatingTid); + void findAccessibilityNodeInfosByViewId(long accessibilityNodeId, String viewId, + int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, + int interrogatingPid, long interrogatingTid, in MagnificationSpec spec); void findAccessibilityNodeInfosByText(long accessibilityNodeId, String text, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, - long interrogatingTid); + long interrogatingTid, in MagnificationSpec spec); void findFocus(long accessibilityNodeId, int focusType, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, - long interrogatingTid); + long interrogatingTid, in MagnificationSpec spec); void focusSearch(long accessibilityNodeId, int direction, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, - long interrogatingTid); + long interrogatingTid, in MagnificationSpec spec); void performAccessibilityAction(long accessibilityNodeId, int action, in Bundle arguments, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, diff --git a/core/java/android/view/inputmethod/InputMethodInfo.java b/core/java/android/view/inputmethod/InputMethodInfo.java index 08e30aa..54c2ba5 100644 --- a/core/java/android/view/inputmethod/InputMethodInfo.java +++ b/core/java/android/view/inputmethod/InputMethodInfo.java @@ -81,6 +81,11 @@ public final class InputMethodInfo implements Parcelable { private boolean mIsAuxIme; /** + * Cavert: mForceDefault must be false for production. This flag is only for test. + */ + private final boolean mForceDefault; + + /** * Constructor. * * @param context The Context in which we are parsing the input method. @@ -108,6 +113,7 @@ public final class InputMethodInfo implements Parcelable { ServiceInfo si = service.serviceInfo; mId = new ComponentName(si.packageName, si.name).flattenToShortString(); mIsAuxIme = true; + mForceDefault = false; PackageManager pm = context.getPackageManager(); String settingsActivityComponent = null; @@ -215,13 +221,39 @@ public final class InputMethodInfo implements Parcelable { mIsAuxIme = source.readInt() == 1; mService = ResolveInfo.CREATOR.createFromParcel(source); source.readTypedList(mSubtypes, InputMethodSubtype.CREATOR); + mForceDefault = false; } /** - * Temporary API for creating a built-in input method. + * Temporary API for creating a built-in input method for test. */ public InputMethodInfo(String packageName, String className, CharSequence label, String settingsActivity) { + this(buildDummyResolveInfo(packageName, className, label), false, settingsActivity, null, + 0, false); + } + + /** + * Temporary API for creating a built-in input method for test. + * @hide + */ + public InputMethodInfo(ResolveInfo ri, boolean isAuxIme, + String settingsActivity, List<InputMethodSubtype> subtypes, int isDefaultResId, + boolean forceDefault) { + final ServiceInfo si = ri.serviceInfo; + mService = ri; + mId = new ComponentName(si.packageName, si.name).flattenToShortString(); + mSettingsActivityName = settingsActivity; + mIsDefaultResId = isDefaultResId; + mIsAuxIme = isAuxIme; + if (subtypes != null) { + mSubtypes.addAll(subtypes); + } + mForceDefault = forceDefault; + } + + private static ResolveInfo buildDummyResolveInfo(String packageName, String className, + CharSequence label) { ResolveInfo ri = new ResolveInfo(); ServiceInfo si = new ServiceInfo(); ApplicationInfo ai = new ApplicationInfo(); @@ -234,11 +266,7 @@ public final class InputMethodInfo implements Parcelable { si.exported = true; si.nonLocalizedLabel = label; ri.serviceInfo = si; - mService = ri; - mId = new ComponentName(si.packageName, si.name).flattenToShortString(); - mSettingsActivityName = settingsActivity; - mIsDefaultResId = 0; - mIsAuxIme = false; + return ri; } /** @@ -340,6 +368,22 @@ public final class InputMethodInfo implements Parcelable { return mIsDefaultResId; } + /** + * Return whether or not this ime is a default ime or not. + * @hide + */ + public boolean isDefault(Context context) { + if (mForceDefault) { + return true; + } + try { + final Resources res = context.createPackageContext(getPackageName(), 0).getResources(); + return res.getBoolean(getIsDefaultResourceId()); + } catch (NameNotFoundException e) { + return false; + } + } + public void dump(Printer pw, String prefix) { pw.println(prefix + "mId=" + mId + " mSettingsActivityName=" + mSettingsActivityName); diff --git a/core/java/android/webkit/AccessibilityInjector.java b/core/java/android/webkit/AccessibilityInjector.java index 008a615..8008a6b 100644 --- a/core/java/android/webkit/AccessibilityInjector.java +++ b/core/java/android/webkit/AccessibilityInjector.java @@ -318,12 +318,15 @@ class AccessibilityInjector { /** * Attempts to handle selection change events when accessibility is using a * non-JavaScript method. + * <p> + * This must not be called from the main thread. * - * @param selectionString The selection string. + * @param selection The selection string. + * @param token The selection request token. */ - public void handleSelectionChangedIfNecessary(String selectionString) { + public void onSelectionStringChangedWebCoreThread(String selection, int token) { if (mAccessibilityInjectorFallback != null) { - mAccessibilityInjectorFallback.onSelectionStringChange(selectionString); + mAccessibilityInjectorFallback.onSelectionStringChangedWebCoreThread(selection, token); } } diff --git a/core/java/android/webkit/AccessibilityInjectorFallback.java b/core/java/android/webkit/AccessibilityInjectorFallback.java index 783b3db..6417527 100644 --- a/core/java/android/webkit/AccessibilityInjectorFallback.java +++ b/core/java/android/webkit/AccessibilityInjectorFallback.java @@ -27,8 +27,9 @@ import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; import android.webkit.WebViewCore.EventHub; +import com.android.internal.os.SomeArgs; + import java.util.ArrayList; -import java.util.Stack; /** * This class injects accessibility into WebViews with disabled JavaScript or @@ -48,8 +49,7 @@ import java.util.Stack; * </p> * The possible actions are invocations to * {@link #setCurrentAxis(int, boolean, String)}, or - * {@link #traverseCurrentAxis(int, boolean, String)} - * {@link #traverseGivenAxis(int, int, boolean, String)} + * {@link #traverseGivenAxis(int, int, boolean, String, boolean)} * {@link #performAxisTransition(int, int, boolean, String)} * referred via the values of: * {@link #ACTION_SET_CURRENT_AXIS}, @@ -74,6 +74,9 @@ class AccessibilityInjectorFallback { private static final int ACTION_PERFORM_AXIS_TRANSITION = 3; private static final int ACTION_TRAVERSE_DEFAULT_WEB_VIEW_BEHAVIOR_AXIS = 4; + /** Timeout after which asynchronous granular movement is aborted. */ + private static final int MODIFY_SELECTION_TIMEOUT = 500; + // WebView navigation axes from WebViewCore.h, plus an additional axis for // the default behavior. private static final int NAVIGATION_AXIS_CHARACTER = 0; @@ -81,7 +84,8 @@ class AccessibilityInjectorFallback { private static final int NAVIGATION_AXIS_SENTENCE = 2; @SuppressWarnings("unused") private static final int NAVIGATION_AXIS_HEADING = 3; - private static final int NAVIGATION_AXIS_SIBLING = 5; + @SuppressWarnings("unused") + private static final int NAVIGATION_AXIS_SIBLING = 4; @SuppressWarnings("unused") private static final int NAVIGATION_AXIS_PARENT_FIRST_CHILD = 5; private static final int NAVIGATION_AXIS_DOCUMENT = 6; @@ -99,8 +103,11 @@ class AccessibilityInjectorFallback { private final WebViewClassic mWebView; private final WebView mWebViewInternal; - // events scheduled for sending as soon as we receive the selected text - private final Stack<AccessibilityEvent> mScheduledEventStack = new Stack<AccessibilityEvent>(); + // Event scheduled for sending as soon as we receive the selected text. + private AccessibilityEvent mScheduledEvent; + + // Token required to send the scheduled event. + private int mScheduledToken = 0; // the current traversal axis private int mCurrentAxis = 2; // sentence @@ -114,6 +121,15 @@ class AccessibilityInjectorFallback { // keep track of last direction private int mLastDirection; + // Lock used for asynchronous selection callback. + private final Object mCallbackLock = new Object(); + + // Whether the asynchronous selection callback was received. + private boolean mCallbackReceived; + + // Whether the asynchronous selection callback succeeded. + private boolean mCallbackResult; + /** * Creates a new injector associated with a given {@link WebViewClassic}. * @@ -174,8 +190,8 @@ class AccessibilityInjectorFallback { } mLastDirection = direction; sendEvent = (binding.getSecondArgument(i) == 1); - mLastDownEventHandled = traverseCurrentAxis(direction, sendEvent, - contentDescription); + mLastDownEventHandled = traverseGivenAxis( + direction, mCurrentAxis, sendEvent, contentDescription, false); break; case ACTION_TRAVERSE_GIVEN_AXIS: direction = binding.getFirstArgument(i); @@ -187,7 +203,7 @@ class AccessibilityInjectorFallback { mLastDirection = direction; axis = binding.getSecondArgument(i); sendEvent = (binding.getThirdArgument(i) == 1); - traverseGivenAxis(direction, axis, sendEvent, contentDescription); + traverseGivenAxis(direction, axis, sendEvent, contentDescription, false); mLastDownEventHandled = true; break; case ACTION_PERFORM_AXIS_TRANSITION: @@ -207,7 +223,7 @@ class AccessibilityInjectorFallback { mLastDirection = binding.getFirstArgument(i); sendEvent = (binding.getSecondArgument(i) == 1); traverseGivenAxis(mLastDirection, NAVIGATION_AXIS_DEFAULT_WEB_VIEW_BEHAVIOR, - sendEvent, contentDescription); + sendEvent, contentDescription, false); mLastDownEventHandled = false; } else { mLastDownEventHandled = true; @@ -222,8 +238,7 @@ class AccessibilityInjectorFallback { } /** - * Set the current navigation axis which will be used while - * calling {@link #traverseCurrentAxis(int, boolean, String)}. + * Set the current navigation axis. * * @param axis The axis to set. * @param sendEvent Whether to send an accessibility event to @@ -255,20 +270,6 @@ class AccessibilityInjectorFallback { } } - /** - * Traverse the document along the current navigation axis. - * - * @param direction The direction of traversal. - * @param sendEvent Whether to send an accessibility event to - * announce the change. - * @param contentDescription A description of the performed action. - * @see #setCurrentAxis(int, boolean, String) - */ - private boolean traverseCurrentAxis(int direction, boolean sendEvent, - String contentDescription) { - return traverseGivenAxis(direction, mCurrentAxis, sendEvent, contentDescription); - } - boolean performAccessibilityAction(int action, Bundle arguments) { switch (action) { case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY: @@ -276,14 +277,14 @@ class AccessibilityInjectorFallback { final int direction = getDirectionForAction(action); final int axis = getAxisForGranularity(arguments.getInt( AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT)); - return traverseGivenAxis(direction, axis, true, null); + return traverseGivenAxis(direction, axis, true, null, true); } case AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT: case AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT: { final int direction = getDirectionForAction(action); // TODO: Add support for moving by object. final int axis = NAVIGATION_AXIS_SENTENCE; - return traverseGivenAxis(direction, axis, true, null); + return traverseGivenAxis(direction, axis, true, null, true); } default: return false; @@ -293,7 +294,7 @@ class AccessibilityInjectorFallback { /** * Returns the {@link WebView}-defined direction for the given * {@link AccessibilityNodeInfo}-defined action. - * + * * @param action An accessibility action identifier. * @return A web view navigation direction. */ @@ -313,7 +314,7 @@ class AccessibilityInjectorFallback { /** * Returns the {@link WebView}-defined axis for the given * {@link AccessibilityNodeInfo}-defined granularity. - * + * * @param granularity An accessibility granularity identifier. * @return A web view navigation axis. */ @@ -345,20 +346,20 @@ class AccessibilityInjectorFallback { * @param contentDescription A description of the performed action. */ private boolean traverseGivenAxis(int direction, int axis, boolean sendEvent, - String contentDescription) { - WebViewCore webViewCore = mWebView.getWebViewCore(); + String contentDescription, boolean sychronous) { + final WebViewCore webViewCore = mWebView.getWebViewCore(); if (webViewCore == null) { return false; } - AccessibilityEvent event = null; if (sendEvent) { - event = getPartialyPopulatedAccessibilityEvent( + final AccessibilityEvent event = getPartialyPopulatedAccessibilityEvent( AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY); - // the text will be set upon receiving the selection string + // The text will be set upon receiving the selection string. event.setContentDescription(contentDescription); + mScheduledEvent = event; + mScheduledToken++; } - mScheduledEventStack.push(event); // if the axis is the default let WebView handle the event which will // result in cursor ring movement and selection of its content @@ -366,27 +367,78 @@ class AccessibilityInjectorFallback { return false; } - webViewCore.sendMessage(EventHub.MODIFY_SELECTION, direction, axis); - return true; + final SomeArgs args = SomeArgs.obtain(); + args.argi1 = direction; + args.argi2 = axis; + args.argi3 = mScheduledToken; + + // If we don't need synchronous results, just return true. + if (!sychronous) { + webViewCore.sendMessage(EventHub.MODIFY_SELECTION, args); + return true; + } + + final boolean callbackResult; + + synchronized (mCallbackLock) { + mCallbackReceived = false; + + // Asynchronously changes the selection in WebView, which responds by + // calling onSelectionStringChanged(). + webViewCore.sendMessage(EventHub.MODIFY_SELECTION, args); + + try { + mCallbackLock.wait(MODIFY_SELECTION_TIMEOUT); + } catch (InterruptedException e) { + // Do nothing. + } + + callbackResult = mCallbackResult; + } + + return (mCallbackReceived && callbackResult); } - /** - * Called when the <code>selectionString</code> has changed. - */ - public void onSelectionStringChange(String selectionString) { + /* package */ void onSelectionStringChangedWebCoreThread( + final String selection, final int token) { + synchronized (mCallbackLock) { + mCallbackReceived = true; + mCallbackResult = (selection != null); + mCallbackLock.notifyAll(); + } + + // Managing state and sending events must take place on the UI thread. + mWebViewInternal.post(new Runnable() { + @Override + public void run() { + onSelectionStringChangedMainThread(selection, token); + } + }); + } + + private void onSelectionStringChangedMainThread(String selection, int token) { if (DEBUG) { - Log.d(LOG_TAG, "Selection string: " + selectionString); + Log.d(LOG_TAG, "Selection string: " + selection); } - mIsLastSelectionStringNull = (selectionString == null); - if (mScheduledEventStack.isEmpty()) { + + if (token != mScheduledToken) { + if (DEBUG) { + Log.d(LOG_TAG, "Selection string has incorrect token: " + token); + } return; } - AccessibilityEvent event = mScheduledEventStack.pop(); - if ((event != null) && (selectionString != null)) { - event.getText().add(selectionString); + + mIsLastSelectionStringNull = (selection == null); + + final AccessibilityEvent event = mScheduledEvent; + mScheduledEvent = null; + + if ((event != null) && (selection != null)) { + event.getText().add(selection); event.setFromIndex(0); - event.setToIndex(selectionString.length()); + event.setToIndex(selection.length()); sendAccessibilityEvent(event); + event.recycle(); } } diff --git a/core/java/android/webkit/BrowserFrame.java b/core/java/android/webkit/BrowserFrame.java index c3a1a17..0b7e92f 100644 --- a/core/java/android/webkit/BrowserFrame.java +++ b/core/java/android/webkit/BrowserFrame.java @@ -226,8 +226,6 @@ class BrowserFrame extends Handler { } else { sJavaBridge.setCacheSize(4 * 1024 * 1024); } - // initialize CacheManager - CacheManager.init(appContext); // create CookieSyncManager with current Context CookieSyncManager.createInstance(appContext); // create PluginManager with current Context diff --git a/core/java/android/webkit/CacheManager.java b/core/java/android/webkit/CacheManager.java index 52f41e6..bbd3f2b 100644 --- a/core/java/android/webkit/CacheManager.java +++ b/core/java/android/webkit/CacheManager.java @@ -45,14 +45,6 @@ import java.util.Map; // CacheManager may only be used if your activity contains a WebView. @Deprecated public final class CacheManager { - - private static final String LOGTAG = "cache"; - - static final String HEADER_KEY_IFMODIFIEDSINCE = "if-modified-since"; - static final String HEADER_KEY_IFNONEMATCH = "if-none-match"; - - private static File mBaseDir; - /** * Represents a resource stored in the HTTP cache. Instances of this class * can be obtained by calling @@ -239,39 +231,23 @@ public final class CacheManager { } /** - * Initializes the HTTP cache. This method must be called before any - * CacheManager methods are used. Note that this is called automatically - * when a {@link WebView} is created. - * - * @param context the application context - */ - static void init(Context context) { - // This isn't actually where the real cache lives, but where we put files for the - // purpose of getCacheFile(). - mBaseDir = new File(context.getCacheDir(), "webviewCacheChromiumStaging"); - if (!mBaseDir.exists()) { - mBaseDir.mkdirs(); - } - } - - /** * Gets the base directory in which the files used to store the contents of * cache entries are placed. See * {@link CacheManager.CacheResult#getLocalPath CacheManager.CacheResult.getLocalPath()}. * * @return the base directory of the cache - * @deprecated Access to the HTTP cache will be removed in a future release. + * @deprecated This method no longer has any effect and always returns null. */ @Deprecated public static File getCacheFileBaseDir() { - return mBaseDir; + return null; } /** * Gets whether the HTTP cache is disabled. * * @return true if the HTTP cache is disabled - * @deprecated Access to the HTTP cache will be removed in a future release. + * @deprecated This method no longer has any effect and always returns false. */ @Deprecated public static boolean cacheDisabled() { @@ -314,73 +290,11 @@ public final class CacheManager { * @param headers a map from HTTP header name to value, to be populated * for the returned cache entry * @return the cache entry for the specified URL - * @deprecated Access to the HTTP cache will be removed in a future release. + * @deprecated This method no longer has any effect and always returns null. */ @Deprecated public static CacheResult getCacheFile(String url, Map<String, String> headers) { - return getCacheFile(url, 0, headers); - } - - static CacheResult getCacheFile(String url, long postIdentifier, - Map<String, String> headers) { - CacheResult result = nativeGetCacheResult(url); - if (result == null) { - return null; - } - // A temporary local file will have been created native side and localPath set - // appropriately. - File src = new File(mBaseDir, result.localPath); - try { - // Open the file here so that even if it is deleted, the content - // is still readable by the caller until close() is called. - result.inStream = new FileInputStream(src); - } catch (FileNotFoundException e) { - Log.v(LOGTAG, "getCacheFile(): Failed to open file: " + e); - // TODO: The files in the cache directory can be removed by the - // system. If it is gone, what should we do? - return null; - } - - // A null value for headers is used by CACHE_MODE_CACHE_ONLY to imply - // that we should provide the cache result even if it is expired. - // Note that a negative expires value means a time in the far future. - if (headers != null && result.expires >= 0 - && result.expires <= System.currentTimeMillis()) { - if (result.lastModified == null && result.etag == null) { - return null; - } - // Return HEADER_KEY_IFNONEMATCH or HEADER_KEY_IFMODIFIEDSINCE - // for requesting validation. - if (result.etag != null) { - headers.put(HEADER_KEY_IFNONEMATCH, result.etag); - } - if (result.lastModified != null) { - headers.put(HEADER_KEY_IFMODIFIEDSINCE, result.lastModified); - } - } - - if (DebugFlags.CACHE_MANAGER) { - Log.v(LOGTAG, "getCacheFile for url " + url); - } - - return result; - } - - /** - * Given a URL and its full headers, gets a CacheResult if a local cache - * can be stored. Otherwise returns null. The mimetype is passed in so that - * the function can use the mimetype that will be passed to WebCore which - * could be different from the mimetype defined in the headers. - * forceCache is for out-of-package callers to force creation of a - * CacheResult, and is used to supply surrogate responses for URL - * interception. - * - * @return a CacheResult for a given URL - */ - static CacheResult createCacheFile(String url, int statusCode, - Headers headers, String mimeType, boolean forceCache) { - // This method is public but hidden. We break functionality. return null; } @@ -424,36 +338,4 @@ public final class CacheManager { // use, we should already have thrown an exception above. assert false; } - - /** - * Removes all cache files. - * - * @return whether the removal succeeded - */ - static boolean removeAllCacheFiles() { - // delete cache files in a separate thread to not block UI. - final Runnable clearCache = new Runnable() { - public void run() { - // delete all cache files - try { - String[] files = mBaseDir.list(); - // if mBaseDir doesn't exist, files can be null. - if (files != null) { - for (int i = 0; i < files.length; i++) { - File f = new File(mBaseDir, files[i]); - if (!f.delete()) { - Log.e(LOGTAG, f.getPath() + " delete failed."); - } - } - } - } catch (SecurityException e) { - // Ignore SecurityExceptions. - } - } - }; - new Thread(clearCache).start(); - return true; - } - - private static native CacheResult nativeGetCacheResult(String url); } diff --git a/core/java/android/webkit/EventLogTags.logtags b/core/java/android/webkit/EventLogTags.logtags index 082a437..b0b5493 100644 --- a/core/java/android/webkit/EventLogTags.logtags +++ b/core/java/android/webkit/EventLogTags.logtags @@ -8,4 +8,3 @@ option java_package android.webkit; # 70103- used by the browser app itself 70150 browser_snap_center -70151 browser_text_size_change (oldSize|1|5), (newSize|1|5) diff --git a/core/java/android/webkit/FindActionModeCallback.java b/core/java/android/webkit/FindActionModeCallback.java index 6a627e1..c68b450 100644 --- a/core/java/android/webkit/FindActionModeCallback.java +++ b/core/java/android/webkit/FindActionModeCallback.java @@ -33,12 +33,15 @@ import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.TextView; -class FindActionModeCallback implements ActionMode.Callback, TextWatcher, - View.OnClickListener { +/** + * @hide + */ +public class FindActionModeCallback implements ActionMode.Callback, TextWatcher, + View.OnClickListener, WebView.FindListener { private View mCustomView; private EditText mEditText; private TextView mMatches; - private WebViewClassic mWebView; + private WebView mWebView; private InputMethodManager mInput; private Resources mResources; private boolean mMatchesFound; @@ -46,7 +49,7 @@ class FindActionModeCallback implements ActionMode.Callback, TextWatcher, private int mActiveMatchIndex; private ActionMode mActionMode; - FindActionModeCallback(Context context) { + public FindActionModeCallback(Context context) { mCustomView = LayoutInflater.from(context).inflate( com.android.internal.R.layout.webview_find, null); mEditText = (EditText) mCustomView.findViewById( @@ -61,7 +64,7 @@ class FindActionModeCallback implements ActionMode.Callback, TextWatcher, mResources = context.getResources(); } - void finish() { + public void finish() { mActionMode.finish(); } @@ -69,7 +72,7 @@ class FindActionModeCallback implements ActionMode.Callback, TextWatcher, * Place text in the text field so it can be searched for. Need to press * the find next or find previous button to find all of the matches. */ - void setText(String text) { + public void setText(String text) { mEditText.setText(text); Spannable span = (Spannable) mEditText.getText(); int length = span.length(); @@ -84,15 +87,23 @@ class FindActionModeCallback implements ActionMode.Callback, TextWatcher, } /* - * Set the WebView to search. Must be non null, and set before calling - * startActionMode. + * Set the WebView to search. Must be non null. */ - void setWebView(WebViewClassic webView) { + public void setWebView(WebView webView) { if (null == webView) { throw new AssertionError("WebView supplied to " + "FindActionModeCallback cannot be null"); } mWebView = webView; + mWebView.setFindDialogFindListener(this); + } + + @Override + public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, + boolean isDoneCounting) { + if (isDoneCounting) { + updateMatchCount(activeMatchOrdinal, numberOfMatches, numberOfMatches == 0); + } } /* @@ -121,7 +132,7 @@ class FindActionModeCallback implements ActionMode.Callback, TextWatcher, /* * Highlight all the instances of the string from mEditText in mWebView. */ - void findAll() { + public void findAll() { if (mWebView == null) { throw new AssertionError( "No WebView for FindActionModeCallback::findAll"); @@ -208,7 +219,8 @@ class FindActionModeCallback implements ActionMode.Callback, TextWatcher, public void onDestroyActionMode(ActionMode mode) { mActionMode = null; mWebView.notifyFindDialogDismissed(); - mInput.hideSoftInputFromWindow(mWebView.getWebView().getWindowToken(), 0); + mWebView.setFindDialogFindListener(null); + mInput.hideSoftInputFromWindow(mWebView.getWindowToken(), 0); } @Override @@ -222,7 +234,7 @@ class FindActionModeCallback implements ActionMode.Callback, TextWatcher, throw new AssertionError( "No WebView for FindActionModeCallback::onActionItemClicked"); } - mInput.hideSoftInputFromWindow(mWebView.getWebView().getWindowToken(), 0); + mInput.hideSoftInputFromWindow(mWebView.getWindowToken(), 0); switch(item.getItemId()) { case com.android.internal.R.id.find_prev: findNext(false); diff --git a/core/java/android/webkit/GeolocationPermissions.java b/core/java/android/webkit/GeolocationPermissions.java index 9c0f754..bc3d035 100644 --- a/core/java/android/webkit/GeolocationPermissions.java +++ b/core/java/android/webkit/GeolocationPermissions.java @@ -61,7 +61,8 @@ public class GeolocationPermissions { }; /** - * Gets the singleton instance of this class. + * Gets the singleton instance of this class. This method cannot be + * called before the application instantiates a {@link WebView} instance. * * @return the singleton {@link GeolocationPermissions} instance */ diff --git a/core/java/android/webkit/HttpAuthHandler.java b/core/java/android/webkit/HttpAuthHandler.java index 296d960..ee3b369 100644 --- a/core/java/android/webkit/HttpAuthHandler.java +++ b/core/java/android/webkit/HttpAuthHandler.java @@ -40,7 +40,7 @@ public class HttpAuthHandler extends Handler { * previously been rejected by the server for the current request. * * @return whether the credentials are suitable for use - * @see Webview#getHttpAuthUsernamePassword + * @see WebView#getHttpAuthUsernamePassword */ public boolean useHttpAuthUsernamePassword() { return false; diff --git a/core/java/android/webkit/SslErrorHandler.java b/core/java/android/webkit/SslErrorHandler.java index 3a43950..af31544 100644 --- a/core/java/android/webkit/SslErrorHandler.java +++ b/core/java/android/webkit/SslErrorHandler.java @@ -19,9 +19,11 @@ package android.webkit; import android.os.Handler; /** - * SslErrorHandler: class responsible for handling SSL errors. - * This class is passed as a parameter to BrowserCallback.displaySslErrorDialog - * and is meant to receive the user's response. + * Represents a request for handling an SSL error. Instances of this class are + * created by the WebView and passed to + * {@link WebViewClient#onReceivedSslError}. The host application must call + * either {@link #proceed} or {@link #cancel} to set the WebView's response + * to the request. */ public class SslErrorHandler extends Handler { diff --git a/core/java/android/webkit/ViewStateSerializer.java b/core/java/android/webkit/ViewStateSerializer.java index 096d4cda..1d44b96 100644 --- a/core/java/android/webkit/ViewStateSerializer.java +++ b/core/java/android/webkit/ViewStateSerializer.java @@ -31,7 +31,8 @@ class ViewStateSerializer { private static final int WORKING_STREAM_STORAGE = 16 * 1024; - static final int VERSION = 1; + // VERSION = 1 was for pictures encoded using a previous copy of libskia + static final int VERSION = 2; static boolean serializeViewState(OutputStream stream, DrawData draw) throws IOException { diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java index aa68904..728bcd3 100644 --- a/core/java/android/webkit/WebSettings.java +++ b/core/java/android/webkit/WebSettings.java @@ -37,6 +37,10 @@ public abstract class WebSettings { * <li>SINGLE_COLUMN moves all content into one column that is the width of the * view.</li> * <li>NARROW_COLUMNS makes all columns no wider than the screen if possible.</li> + * <li>TEXT_AUTOSIZING boosts font size of paragraphs based on heuristics to make + * the text readable when viewing a wide-viewport layout in the overview mode. + * It is recommended to enable zoom support {@link #setSupportZoom} when + * using this mode.</li> * </ul> */ // XXX: These must match LayoutAlgorithm in Settings.h in WebCore. @@ -47,7 +51,11 @@ public abstract class WebSettings { */ @Deprecated SINGLE_COLUMN, - NARROW_COLUMNS + NARROW_COLUMNS, + /** + * @hide + */ + TEXT_AUTOSIZING } /** @@ -89,6 +97,14 @@ public abstract class WebSettings { ZoomDensity(int size) { value = size; } + + /** + * @hide Only for use by WebViewProvider implementations + */ + public int getValue() { + return value; + } + int value; } @@ -388,16 +404,14 @@ public abstract class WebSettings { } /** - * Sets whether the WebView should save form data. The default is true, - * unless in private browsing mode, when the value is always false. + * Sets whether the WebView should save form data. The default is true. */ public void setSaveFormData(boolean save) { throw new MustOverrideException(); } /** - * Gets whether the WebView saves form data. Always false in private - * browsing mode. + * Gets whether the WebView saves form data. * * @return whether the WebView saves form data * @see #setSaveFormData @@ -580,18 +594,25 @@ public abstract class WebSettings { } /** - * Tells the WebView to use a wide viewport. The default is false. + * Sets whether the WebView should enable support for the "viewport" + * HTML meta tag or should use a wide viewport. + * When the value of the setting is false, the layout width is always set to the + * width of the WebView control in device-independent (CSS) pixels. + * When the value is true and the page contains the viewport meta tag, the value + * of the width specified in the tag is used. If the page does not contain the tag or + * does not provide a width, then a wide viewport will be used. * - * @param use whether to use a wide viewport + * @param use whether to enable support for the viewport meta tag */ public synchronized void setUseWideViewPort(boolean use) { throw new MustOverrideException(); } /** - * Gets whether the WebView is using a wide viewport. + * Gets whether the WebView supports the "viewport" + * HTML meta tag or will use a wide viewport. * - * @return true if the WebView is using a wide viewport + * @return true if the WebView supports the viewport meta tag * @see #setUseWideViewPort */ public synchronized boolean getUseWideViewPort() { @@ -936,6 +957,9 @@ public abstract class WebSettings { * access to content from other file scheme URLs. See * {@link #setAllowFileAccessFromFileURLs}. To enable the most restrictive, * and therefore secure policy, this setting should be disabled. + * Note that this setting affects only JavaScript access to file scheme + * resources. Other access to such resources, for example, from image HTML + * elements, is unaffected. * <p> * The default value is true for API level * {@link android.os.Build.VERSION_CODES#ICE_CREAM_SANDWICH_MR1} and below, @@ -953,6 +977,9 @@ public abstract class WebSettings { * enable the most restrictive, and therefore secure policy, this setting * should be disabled. Note that the value of this setting is ignored if * the value of {@link #getAllowUniversalAccessFromFileURLs} is true. + * Note too, that this setting affects only JavaScript access to file scheme + * resources. Other access to such resources, for example, from image HTML + * elements, is unaffected. * <p> * The default value is true for API level * {@link android.os.Build.VERSION_CODES#ICE_CREAM_SANDWICH_MR1} and below, @@ -1052,7 +1079,7 @@ public abstract class WebSettings { * * @param appCachePath a String path to the directory containing * Application Caches files. - * @see setAppCacheEnabled + * @see #setAppCacheEnabled */ public synchronized void setAppCachePath(String appCachePath) { throw new MustOverrideException(); @@ -1121,9 +1148,22 @@ public abstract class WebSettings { } /** - * Sets whether Geolocation is enabled. The default is true. See also - * {@link #setGeolocationDatabasePath} for how to correctly set up - * Geolocation. + * Sets whether Geolocation is enabled. The default is true. + * <p> + * Please note that in order for the Geolocation API to be usable + * by a page in the WebView, the following requirements must be met: + * <ul> + * <li>an application must have permission to access the device location, + * see {@link android.Manifest.permission#ACCESS_COARSE_LOCATION}, + * {@link android.Manifest.permission#ACCESS_FINE_LOCATION}; + * <li>an application must provide an implementation of the + * {@link WebChromeClient#onGeolocationPermissionsShowPrompt} callback + * to receive notifications that a page is requesting access to location + * via the JavaScript Geolocation API. + * </ul> + * <p> + * As an option, it is possible to store previous locations and web origin + * permissions in a database. See {@link #setGeolocationDatabasePath}. * * @param flag whether Geolocation should be enabled */ @@ -1295,7 +1335,7 @@ public abstract class WebSettings { * and content is re-validated as needed. When navigating back, content is * not revalidated, instead the content is just retrieved from the cache. * This method allows the client to override this behavior by specifying - * one of {@link #LOAD_DEFAULT}, {@link #LOAD_NORMAL}, + * one of {@link #LOAD_DEFAULT}, * {@link #LOAD_CACHE_ELSE_NETWORK}, {@link #LOAD_NO_CACHE} or * {@link #LOAD_CACHE_ONLY}. The default value is {@link #LOAD_DEFAULT}. * diff --git a/core/java/android/webkit/WebSettingsClassic.java b/core/java/android/webkit/WebSettingsClassic.java index 1bbe7bb..c10a429 100644 --- a/core/java/android/webkit/WebSettingsClassic.java +++ b/core/java/android/webkit/WebSettingsClassic.java @@ -647,10 +647,6 @@ public class WebSettingsClassic extends WebSettings { @Override public synchronized void setTextZoom(int textZoom) { if (mTextSize != textZoom) { - if (WebViewClassic.mLogEvent) { - EventLog.writeEvent(EventLogTags.BROWSER_TEXT_SIZE_CHANGE, - mTextSize, textZoom); - } mTextSize = textZoom; postSync(); } @@ -820,6 +816,10 @@ public class WebSettingsClassic extends WebSettings { */ @Override public synchronized void setLayoutAlgorithm(LayoutAlgorithm l) { + if (l == LayoutAlgorithm.TEXT_AUTOSIZING) { + throw new IllegalArgumentException( + "WebViewClassic does not support TEXT_AUTOSIZING layout mode"); + } // XXX: This will only be affective if libwebcore was built with // ANDROID_LAYOUT defined. if (mLayoutAlgorithm != l) { diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index 6df7820..dcb664e 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -334,7 +334,9 @@ public class WebView extends AbsoluteLayout * See {@link WebView#capturePicture} for details of the picture. * * @param view the WebView that owns the picture - * @param picture the new picture + * @param picture the new picture. Applications targeting + * {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2} or above + * will always receive a null Picture. * @deprecated Deprecated due to internal changes. */ @Deprecated @@ -615,7 +617,7 @@ public class WebView extends AbsoluteLayout * @param realm the realm to which the credentials apply * @param username the username * @param password the password - * @see getHttpAuthUsernamePassword + * @see #getHttpAuthUsernamePassword * @see WebViewDatabase#hasHttpAuthUsernamePassword * @see WebViewDatabase#clearHttpAuthUsernamePassword */ @@ -635,7 +637,7 @@ public class WebView extends AbsoluteLayout * @return the credentials as a String array, if found. The first element * is the username and the second element is the password. Null if * no credentials are found. - * @see setHttpAuthUsernamePassword + * @see #setHttpAuthUsernamePassword * @see WebViewDatabase#hasHttpAuthUsernamePassword * @see WebViewDatabase#clearHttpAuthUsernamePassword */ @@ -1329,7 +1331,8 @@ public class WebView extends AbsoluteLayout */ public void setFindListener(FindListener listener) { checkThread(); - mProvider.setFindListener(listener); + setupFindListenerIfNeeded(); + mFindListener.mUserFindListener = listener; } /** @@ -1850,11 +1853,60 @@ public class WebView extends AbsoluteLayout } //------------------------------------------------------------------------- + // Package-private internal stuff + //------------------------------------------------------------------------- + + // Only used by android.webkit.FindActionModeCallback. + void setFindDialogFindListener(FindListener listener) { + checkThread(); + setupFindListenerIfNeeded(); + mFindListener.mFindDialogFindListener = listener; + } + + // Only used by android.webkit.FindActionModeCallback. + void notifyFindDialogDismissed() { + checkThread(); + mProvider.notifyFindDialogDismissed(); + } + + //------------------------------------------------------------------------- // Private internal stuff //------------------------------------------------------------------------- private WebViewProvider mProvider; + /** + * In addition to the FindListener that the user may set via the WebView.setFindListener + * API, FindActionModeCallback will register it's own FindListener. We keep them separate + * via this class so that that the two FindListeners can potentially exist at once. + */ + private class FindListenerDistributor implements FindListener { + private FindListener mFindDialogFindListener; + private FindListener mUserFindListener; + + @Override + public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, + boolean isDoneCounting) { + if (mFindDialogFindListener != null) { + mFindDialogFindListener.onFindResultReceived(activeMatchOrdinal, numberOfMatches, + isDoneCounting); + } + + if (mUserFindListener != null) { + mUserFindListener.onFindResultReceived(activeMatchOrdinal, numberOfMatches, + isDoneCounting); + } + } + } + private FindListenerDistributor mFindListener; + + private void setupFindListenerIfNeeded() { + if (mFindListener == null) { + mFindListener = new FindListenerDistributor(); + mProvider.setFindListener(mFindListener); + } + } + private void ensureProviderCreated() { checkThread(); if (mProvider == null) { @@ -1865,9 +1917,6 @@ public class WebView extends AbsoluteLayout } private static synchronized WebViewFactoryProvider getFactory() { - // For now the main purpose of this function (and the factory abstration) is to keep - // us honest and minimize usage of WebViewClassic internals when binding the proxy. - checkThread(); return WebViewFactory.getProvider(); } @@ -1910,9 +1959,8 @@ public class WebView extends AbsoluteLayout @Override public void setOverScrollMode(int mode) { super.setOverScrollMode(mode); - // This method may called in the constructor chain, before the WebView provider is - // created. (Fortunately, this is the only method we override that can get called by - // any of the base class constructors). + // This method may be called in the constructor chain, before the WebView provider is + // created. ensureProviderCreated(); mProvider.getViewDelegate().setOverScrollMode(mode); } @@ -2072,6 +2120,9 @@ public class WebView extends AbsoluteLayout @Override protected void onVisibilityChanged(View changedView, int visibility) { super.onVisibilityChanged(changedView, visibility); + // This method may be called in the constructor chain, before the WebView provider is + // created. + ensureProviderCreated(); mProvider.getViewDelegate().onVisibilityChanged(changedView, visibility); } @@ -2137,4 +2188,10 @@ public class WebView extends AbsoluteLayout super.setLayerType(layerType, paint); mProvider.getViewDelegate().setLayerType(layerType, paint); } + + @Override + protected void dispatchDraw(Canvas canvas) { + mProvider.getViewDelegate().preDispatchDraw(canvas); + super.dispatchDraw(canvas); + } } diff --git a/core/java/android/webkit/WebViewClassic.java b/core/java/android/webkit/WebViewClassic.java index 7154f95..6fefcca 100644 --- a/core/java/android/webkit/WebViewClassic.java +++ b/core/java/android/webkit/WebViewClassic.java @@ -1024,30 +1024,26 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc static final int UPDATE_MATCH_COUNT = 126; static final int CENTER_FIT_RECT = 127; static final int SET_SCROLLBAR_MODES = 129; - static final int SELECTION_STRING_CHANGED = 130; - static final int HIT_TEST_RESULT = 131; - static final int SAVE_WEBARCHIVE_FINISHED = 132; - - static final int SET_AUTOFILLABLE = 133; - static final int AUTOFILL_COMPLETE = 134; - - static final int SCREEN_ON = 136; - static final int UPDATE_ZOOM_DENSITY = 139; - static final int EXIT_FULLSCREEN_VIDEO = 140; - - static final int COPY_TO_CLIPBOARD = 141; - static final int INIT_EDIT_FIELD = 142; - static final int REPLACE_TEXT = 143; - static final int CLEAR_CARET_HANDLE = 144; - static final int KEY_PRESS = 145; - static final int RELOCATE_AUTO_COMPLETE_POPUP = 146; - static final int FOCUS_NODE_CHANGED = 147; - static final int AUTOFILL_FORM = 148; - static final int SCROLL_EDIT_TEXT = 149; - static final int EDIT_TEXT_SIZE_CHANGED = 150; - static final int SHOW_CARET_HANDLE = 151; - static final int UPDATE_CONTENT_BOUNDS = 152; - static final int SCROLL_HANDLE_INTO_VIEW = 153; + static final int HIT_TEST_RESULT = 130; + static final int SAVE_WEBARCHIVE_FINISHED = 131; + static final int SET_AUTOFILLABLE = 132; + static final int AUTOFILL_COMPLETE = 133; + static final int SCREEN_ON = 134; + static final int UPDATE_ZOOM_DENSITY = 135; + static final int EXIT_FULLSCREEN_VIDEO = 136; + static final int COPY_TO_CLIPBOARD = 137; + static final int INIT_EDIT_FIELD = 138; + static final int REPLACE_TEXT = 139; + static final int CLEAR_CARET_HANDLE = 140; + static final int KEY_PRESS = 141; + static final int RELOCATE_AUTO_COMPLETE_POPUP = 142; + static final int FOCUS_NODE_CHANGED = 143; + static final int AUTOFILL_FORM = 144; + static final int SCROLL_EDIT_TEXT = 145; + static final int EDIT_TEXT_SIZE_CHANGED = 146; + static final int SHOW_CARET_HANDLE = 147; + static final int UPDATE_CONTENT_BOUNDS = 148; + static final int SCROLL_HANDLE_INTO_VIEW = 149; private static final int FIRST_PACKAGE_MSG_ID = SCROLL_TO_MSG_ID; private static final int LAST_PACKAGE_MSG_ID = HIT_TEST_RESULT; @@ -1800,6 +1796,12 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc event.setMaxScrollY(Math.max(convertedContentHeight - adjustedViewHeight, 0)); } + /* package */ void handleSelectionChangedWebCoreThread(String selection, int token) { + if (isAccessibilityInjectionEnabled()) { + getAccessibilityInjector().onSelectionStringChangedWebCoreThread(selection, token); + } + } + private boolean isAccessibilityInjectionEnabled() { final AccessibilityManager manager = AccessibilityManager.getInstance(mContext); if (!manager.isEnabled()) { @@ -3702,7 +3704,7 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc mCachedOverlappingActionModeHeight = -1; mFindCallback = callback; setFindIsUp(true); - mFindCallback.setWebView(this); + mFindCallback.setWebView(getWebView()); if (showIme) { mFindCallback.showSoftInput(); } else if (text != null) { @@ -3804,7 +3806,8 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc /** * Called when the find ActionMode ends. */ - void notifyFindDialogDismissed() { + @Override + public void notifyFindDialogDismissed() { mFindCallback = null; mCachedOverlappingActionModeHeight = -1; if (mWebViewCore == null) { @@ -7497,13 +7500,6 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc mVerticalScrollBarMode = msg.arg2; break; - case SELECTION_STRING_CHANGED: - if (isAccessibilityInjectionEnabled()) { - getAccessibilityInjector() - .handleSelectionChangedIfNecessary((String) msg.obj); - } - break; - case FOCUS_NODE_CHANGED: mIsEditingText = (msg.arg1 == mFieldPointer); if (mAutoCompletePopup != null && !mIsEditingText) { @@ -7913,7 +7909,9 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc if (mPictureListener != null) { // trigger picture listener for hardware layers. Software layers are // triggered in setNewPicture - mPictureListener.onNewPicture(getWebView(), capturePicture()); + Picture picture = mContext.getApplicationInfo().targetSdkVersion < + Build.VERSION_CODES.JELLY_BEAN_MR2 ? capturePicture() : null; + mPictureListener.onNewPicture(getWebView(), picture); } } @@ -7998,7 +7996,9 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc || mWebView.getLayerType() == View.LAYER_TYPE_SOFTWARE) { // trigger picture listener for software layers. Hardware layers are // triggered in pageSwapCallback - mPictureListener.onNewPicture(getWebView(), capturePicture()); + Picture picture = mContext.getApplicationInfo().targetSdkVersion < + Build.VERSION_CODES.JELLY_BEAN_MR2 ? capturePicture() : null; + mPictureListener.onNewPicture(getWebView(), picture); } } } @@ -8562,6 +8562,11 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc updateHwAccelerated(); } + @Override + public void preDispatchDraw(Canvas canvas) { + // no-op for WebViewClassic. + } + private void updateHwAccelerated() { if (mNativeClass == 0) { return; diff --git a/core/java/android/webkit/WebViewClient.java b/core/java/android/webkit/WebViewClient.java index 08a046a..e8974c6 100644 --- a/core/java/android/webkit/WebViewClient.java +++ b/core/java/android/webkit/WebViewClient.java @@ -31,6 +31,7 @@ public class WebViewClient { * proper handler for the url. If WebViewClient is provided, return true * means the host application handles the url, while return false means the * current WebView handles the url. + * This method is not called for requests using the POST "method". * * @param view The WebView that is initiating the callback. * @param url The url to be loaded. @@ -82,9 +83,9 @@ public class WebViewClient { * Notify the host application of a resource request and allow the * application to return the data. If the return value is null, the WebView * will continue to load the resource as usual. Otherwise, the return - * response and data will be used. NOTE: This method is called by the - * network thread so clients should exercise caution when accessing private - * data. + * response and data will be used. NOTE: This method is called on a thread + * other than the UI thread so clients should exercise caution + * when accessing private data or the view system. * * @param view The {@link android.webkit.WebView} that is requesting the * resource. @@ -213,7 +214,7 @@ public class WebViewClient { * @param handler the HttpAuthHandler used to set the WebView's response * @param host the host requiring authentication * @param realm the realm for which authentication is required - * @see Webview#getHttpAuthUsernamePassword + * @see WebView#getHttpAuthUsernamePassword */ public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) { diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java index c35b768..4a09636 100644 --- a/core/java/android/webkit/WebViewCore.java +++ b/core/java/android/webkit/WebViewCore.java @@ -41,6 +41,8 @@ import android.view.View; import android.webkit.WebViewClassic.FocusNodeHref; import android.webkit.WebViewInputDispatcher.WebKitCallbacks; +import com.android.internal.os.SomeArgs; + import junit.framework.Assert; import java.io.OutputStream; @@ -1545,12 +1547,14 @@ public final class WebViewCore { case MODIFY_SELECTION: mTextSelectionChangeReason = TextSelectionData.REASON_ACCESSIBILITY_INJECTOR; - String modifiedSelectionString = - nativeModifySelection(mNativeClass, msg.arg1, - msg.arg2); - mWebViewClassic.mPrivateHandler.obtainMessage( - WebViewClassic.SELECTION_STRING_CHANGED, - modifiedSelectionString).sendToTarget(); + final SomeArgs args = (SomeArgs) msg.obj; + final String modifiedSelectionString = nativeModifySelection( + mNativeClass, args.argi1, args.argi2); + // If accessibility is on, the main thread may be + // waiting for a response. Send on webcore thread. + mWebViewClassic.handleSelectionChangedWebCoreThread( + modifiedSelectionString, args.argi3); + args.recycle(); mTextSelectionChangeReason = TextSelectionData.REASON_UNKNOWN; break; @@ -2001,9 +2005,6 @@ public final class WebViewCore { private void clearCache(boolean includeDiskFiles) { mBrowserFrame.clearCache(); - if (includeDiskFiles) { - CacheManager.removeAllCacheFiles(); - } } private void loadUrl(String url, Map<String, String> extraHeaders) { diff --git a/core/java/android/webkit/WebViewDatabase.java b/core/java/android/webkit/WebViewDatabase.java index 5597259..e08052a 100644 --- a/core/java/android/webkit/WebViewDatabase.java +++ b/core/java/android/webkit/WebViewDatabase.java @@ -50,7 +50,7 @@ public class WebViewDatabase { * * @return true if there are any saved username/password pairs * @see WebView#savePassword - * @see clearUsernamePassword + * @see #clearUsernamePassword */ public boolean hasUsernamePassword() { throw new MustOverrideException(); @@ -61,7 +61,7 @@ public class WebViewDatabase { * Note that these are unrelated to HTTP authentication credentials. * * @see WebView#savePassword - * @see hasUsernamePassword + * @see #hasUsernamePassword */ public void clearUsernamePassword() { throw new MustOverrideException(); @@ -71,9 +71,9 @@ public class WebViewDatabase { * Gets whether there are any saved credentials for HTTP authentication. * * @return whether there are any saved credentials - * @see Webview#getHttpAuthUsernamePassword - * @see Webview#setHttpAuthUsernamePassword - * @see clearHttpAuthUsernamePassword + * @see WebView#getHttpAuthUsernamePassword + * @see WebView#setHttpAuthUsernamePassword + * @see #clearHttpAuthUsernamePassword */ public boolean hasHttpAuthUsernamePassword() { throw new MustOverrideException(); @@ -82,9 +82,9 @@ public class WebViewDatabase { /** * Clears any saved credentials for HTTP authentication. * - * @see Webview#getHttpAuthUsernamePassword - * @see Webview#setHttpAuthUsernamePassword - * @see hasHttpAuthUsernamePassword + * @see WebView#getHttpAuthUsernamePassword + * @see WebView#setHttpAuthUsernamePassword + * @see #hasHttpAuthUsernamePassword */ public void clearHttpAuthUsernamePassword() { throw new MustOverrideException(); @@ -94,7 +94,7 @@ public class WebViewDatabase { * Gets whether there is any saved data for web forms. * * @return whether there is any saved data for web forms - * @see clearFormData + * @see #clearFormData */ public boolean hasFormData() { throw new MustOverrideException(); @@ -103,7 +103,7 @@ public class WebViewDatabase { /** * Clears any saved data for web forms. * - * @see hasFormData + * @see #hasFormData */ public void clearFormData() { throw new MustOverrideException(); diff --git a/core/java/android/webkit/WebViewFactory.java b/core/java/android/webkit/WebViewFactory.java index b833a01..18df0b1 100644 --- a/core/java/android/webkit/WebViewFactory.java +++ b/core/java/android/webkit/WebViewFactory.java @@ -31,7 +31,7 @@ class WebViewFactory { // TODO: When the Chromium powered WebView is ready, it should be the default factory class. private static final String DEFAULT_WEBVIEW_FACTORY = "android.webkit.WebViewClassic$Factory"; private static final String CHROMIUM_WEBVIEW_FACTORY = - "com.android.webviewchromium.WebViewChromiumFactoryProvider"; + "com.android.webview.chromium.WebViewChromiumFactoryProvider"; private static final String CHROMIUM_WEBVIEW_JAR = "/system/framework/webviewchromium.jar"; private static final String LOGTAG = "WebViewFactory"; diff --git a/core/java/android/webkit/WebViewProvider.java b/core/java/android/webkit/WebViewProvider.java index c9f9fbd..fa17ab9 100644 --- a/core/java/android/webkit/WebViewProvider.java +++ b/core/java/android/webkit/WebViewProvider.java @@ -240,7 +240,7 @@ public interface WebViewProvider { public View findHierarchyView(String className, int hashCode); //------------------------------------------------------------------------- - // Provider glue methods + // Provider internal methods //------------------------------------------------------------------------- /** @@ -255,6 +255,12 @@ public interface WebViewProvider { */ /* package */ ScrollDelegate getScrollDelegate(); + /** + * Only used by FindActionModeCallback to inform providers that the find dialog has + * been dismissed. + */ + public void notifyFindDialogDismissed(); + //------------------------------------------------------------------------- // View / ViewGroup delegation methods //------------------------------------------------------------------------- @@ -341,6 +347,8 @@ public interface WebViewProvider { public void setBackgroundColor(int color); public void setLayerType(int layerType, Paint paint); + + public void preDispatchDraw(Canvas canvas); } interface ScrollDelegate { diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 57bf0d3..396fd68 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -1374,9 +1374,11 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te if (isEnabled()) { if (getFirstVisiblePosition() > 0) { info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); + info.setScrollable(true); } if (getLastVisiblePosition() < getCount() - 1) { info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); + info.setScrollable(true); } } } diff --git a/core/java/android/widget/AbsSeekBar.java b/core/java/android/widget/AbsSeekBar.java index 3b5e75b..7674837 100644 --- a/core/java/android/widget/AbsSeekBar.java +++ b/core/java/android/widget/AbsSeekBar.java @@ -305,7 +305,7 @@ public abstract class AbsSeekBar extends ProgressBar { } // Canvas will be translated, so 0,0 is where we start drawing - final int left = isLayoutRtl() ? available - thumbPos : thumbPos; + final int left = (isLayoutRtl() && mMirrorForRtl) ? available - thumbPos : thumbPos; thumb.setBounds(left, topBound, left + thumbWidth, bottomBound); } @@ -426,7 +426,7 @@ public abstract class AbsSeekBar extends ProgressBar { int x = (int)event.getX(); float scale; float progress = 0; - if (isLayoutRtl()) { + if (isLayoutRtl() && mMirrorForRtl) { if (x > width - mPaddingRight) { scale = 0.0f; } else if (x < mPaddingLeft) { diff --git a/core/java/android/widget/AbsSpinner.java b/core/java/android/widget/AbsSpinner.java index f279f8e..a379157 100644 --- a/core/java/android/widget/AbsSpinner.java +++ b/core/java/android/widget/AbsSpinner.java @@ -375,7 +375,7 @@ public abstract class AbsSpinner extends AdapterView<SpinnerAdapter> { /** * Constructor called from {@link #CREATOR} */ - private SavedState(Parcel in) { + SavedState(Parcel in) { super(in); selectedId = in.readLong(); position = in.readInt(); diff --git a/core/java/android/widget/ArrayAdapter.java b/core/java/android/widget/ArrayAdapter.java index 44f04db..97926a7 100644 --- a/core/java/android/widget/ArrayAdapter.java +++ b/core/java/android/widget/ArrayAdapter.java @@ -97,11 +97,11 @@ public class ArrayAdapter<T> extends BaseAdapter implements Filterable { * Constructor * * @param context The current context. - * @param textViewResourceId The resource ID for a layout file containing a TextView to use when + * @param resource The resource ID for a layout file containing a TextView to use when * instantiating views. */ - public ArrayAdapter(Context context, int textViewResourceId) { - init(context, textViewResourceId, 0, new ArrayList<T>()); + public ArrayAdapter(Context context, int resource) { + init(context, resource, 0, new ArrayList<T>()); } /** @@ -120,12 +120,12 @@ public class ArrayAdapter<T> extends BaseAdapter implements Filterable { * Constructor * * @param context The current context. - * @param textViewResourceId The resource ID for a layout file containing a TextView to use when + * @param resource The resource ID for a layout file containing a TextView to use when * instantiating views. * @param objects The objects to represent in the ListView. */ - public ArrayAdapter(Context context, int textViewResourceId, T[] objects) { - init(context, textViewResourceId, 0, Arrays.asList(objects)); + public ArrayAdapter(Context context, int resource, T[] objects) { + init(context, resource, 0, Arrays.asList(objects)); } /** @@ -145,12 +145,12 @@ public class ArrayAdapter<T> extends BaseAdapter implements Filterable { * Constructor * * @param context The current context. - * @param textViewResourceId The resource ID for a layout file containing a TextView to use when + * @param resource The resource ID for a layout file containing a TextView to use when * instantiating views. * @param objects The objects to represent in the ListView. */ - public ArrayAdapter(Context context, int textViewResourceId, List<T> objects) { - init(context, textViewResourceId, 0, objects); + public ArrayAdapter(Context context, int resource, List<T> objects) { + init(context, resource, 0, objects); } /** diff --git a/core/java/android/widget/CheckBox.java b/core/java/android/widget/CheckBox.java index f1804f8..41ab5f2 100644 --- a/core/java/android/widget/CheckBox.java +++ b/core/java/android/widget/CheckBox.java @@ -20,6 +20,7 @@ import android.content.Context; import android.util.AttributeSet; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; +import android.util.ValueModel; /** @@ -55,7 +56,9 @@ import android.view.accessibility.AccessibilityNodeInfo; * {@link android.R.styleable#View View Attributes} * </p> */ -public class CheckBox extends CompoundButton { +public class CheckBox extends CompoundButton implements ValueEditor<Boolean> { + private ValueModel<Boolean> mValueModel = ValueModel.EMPTY; + public CheckBox(Context context) { this(context, null); } @@ -79,4 +82,22 @@ public class CheckBox extends CompoundButton { super.onInitializeAccessibilityNodeInfo(info); info.setClassName(CheckBox.class.getName()); } + + @Override + public ValueModel<Boolean> getValueModel() { + return mValueModel; + } + + @Override + public void setValueModel(ValueModel<Boolean> valueModel) { + mValueModel = valueModel; + setChecked(mValueModel.get()); + } + + @Override + public boolean performClick() { + boolean handled = super.performClick(); + mValueModel.set(isChecked()); + return handled; + } } diff --git a/core/java/android/widget/EditText.java b/core/java/android/widget/EditText.java index 57e51c2..ec81214 100644 --- a/core/java/android/widget/EditText.java +++ b/core/java/android/widget/EditText.java @@ -17,6 +17,7 @@ package android.widget; import android.content.Context; +import android.graphics.Rect; import android.text.Editable; import android.text.Selection; import android.text.Spannable; @@ -24,6 +25,7 @@ import android.text.TextUtils; import android.text.method.ArrowKeyMovementMethod; import android.text.method.MovementMethod; import android.util.AttributeSet; +import android.util.ValueModel; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; @@ -47,7 +49,9 @@ import android.view.accessibility.AccessibilityNodeInfo; * {@link android.R.styleable#TextView TextView Attributes}, * {@link android.R.styleable#View View Attributes} */ -public class EditText extends TextView { +public class EditText extends TextView implements ValueEditor<CharSequence> { + private ValueModel<CharSequence> mValueModel = ValueModel.EMPTY; + public EditText(Context context) { this(context, null); } @@ -128,4 +132,21 @@ public class EditText extends TextView { super.onInitializeAccessibilityNodeInfo(info); info.setClassName(EditText.class.getName()); } + + @Override + public ValueModel<CharSequence> getValueModel() { + return mValueModel; + } + + @Override + public void setValueModel(ValueModel<CharSequence> valueModel) { + mValueModel = valueModel; + setText(mValueModel.get()); + } + + @Override + void sendAfterTextChanged(Editable text) { + super.sendAfterTextChanged(text); + mValueModel.set(text); + } } diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index 8892316..dc305a5 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -20,6 +20,8 @@ import com.android.internal.util.ArrayUtils; import com.android.internal.widget.EditableInputConnection; import android.R; +import android.app.PendingIntent; +import android.app.PendingIntent.CanceledException; import android.content.ClipData; import android.content.ClipData.Item; import android.content.Context; @@ -124,7 +126,6 @@ public class Editor { InputMethodState mInputMethodState; DisplayList[] mTextDisplayLists; - int mLastLayoutHeight; boolean mFrozenWithFocus; boolean mSelectionMoved; @@ -1289,20 +1290,11 @@ public class Editor { mTextDisplayLists = new DisplayList[ArrayUtils.idealObjectArraySize(0)]; } - // If the height of the layout changes (usually when inserting or deleting a line, - // but could be changes within a span), invalidate everything. We could optimize - // more aggressively (for example, adding offsets to blocks) but it would be more - // complex and we would only get the benefit in some cases. - int layoutHeight = layout.getHeight(); - if (mLastLayoutHeight != layoutHeight) { - invalidateTextDisplayList(); - mLastLayoutHeight = layoutHeight; - } - DynamicLayout dynamicLayout = (DynamicLayout) layout; int[] blockEndLines = dynamicLayout.getBlockEndLines(); int[] blockIndices = dynamicLayout.getBlockIndices(); final int numberOfBlocks = dynamicLayout.getNumberOfBlocks(); + final int indexFirstChangedBlock = dynamicLayout.getIndexFirstChangedBlock(); int endOfPreviousBlock = -1; int searchStartIndex = 0; @@ -1324,10 +1316,11 @@ public class Editor { blockDisplayList = mTextDisplayLists[blockIndex] = mTextView.getHardwareRenderer().createDisplayList("Text " + blockIndex); } else { - if (blockIsInvalid) blockDisplayList.invalidate(); + if (blockIsInvalid) blockDisplayList.clear(); } - if (!blockDisplayList.isValid()) { + final boolean blockDisplayListIsInvalid = !blockDisplayList.isValid(); + if (i >= indexFirstChangedBlock || blockDisplayListIsInvalid) { final int blockBeginLine = endOfPreviousBlock + 1; final int top = layout.getLineTop(blockBeginLine); final int bottom = layout.getLineBottom(blockEndLine); @@ -1344,24 +1337,27 @@ public class Editor { right = (int) (max + 0.5f); } - final HardwareCanvas hardwareCanvas = blockDisplayList.start(); - try { - // Tighten the bounds of the viewport to the actual text size - hardwareCanvas.setViewport(right - left, bottom - top); - // The dirty rect should always be null for a display list - hardwareCanvas.onPreDraw(null); - // drawText is always relative to TextView's origin, this translation brings - // this range of text back to the top left corner of the viewport - hardwareCanvas.translate(-left, -top); - layout.drawText(hardwareCanvas, blockBeginLine, blockEndLine); - // No need to untranslate, previous context is popped after drawDisplayList - } finally { - hardwareCanvas.onPostDraw(); - blockDisplayList.end(); - blockDisplayList.setLeftTopRightBottom(left, top, right, bottom); - // Same as drawDisplayList below, handled by our TextView's parent - blockDisplayList.setClipChildren(false); + // Rebuild display list if it is invalid + if (blockDisplayListIsInvalid) { + final HardwareCanvas hardwareCanvas = blockDisplayList.start( + right - left, bottom - top); + try { + // drawText is always relative to TextView's origin, this translation + // brings this range of text back to the top left corner of the viewport + hardwareCanvas.translate(-left, -top); + layout.drawText(hardwareCanvas, blockBeginLine, blockEndLine); + // No need to untranslate, previous context is popped after + // drawDisplayList + } finally { + blockDisplayList.end(); + // Same as drawDisplayList below, handled by our TextView's parent + blockDisplayList.setClipChildren(false); + } } + + // Valid disply list whose index is >= indexFirstChangedBlock + // only needs to update its drawing location. + blockDisplayList.setLeftTopRightBottom(left, top, right, bottom); } ((HardwareCanvas) canvas).drawDisplayList(blockDisplayList, null, @@ -1369,6 +1365,8 @@ public class Editor { endOfPreviousBlock = blockEndLine; } + + dynamicLayout.setIndexFirstChangedBlock(numberOfBlocks); } else { // Boring layout is used for empty and hint text layout.drawText(canvas, firstLine, lastLine); @@ -1431,7 +1429,7 @@ public class Editor { while (i < numberOfBlocks) { final int blockIndex = blockIndices[i]; if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX) { - mTextDisplayLists[blockIndex].invalidate(); + mTextDisplayLists[blockIndex].clear(); } if (blockEndLines[i] >= lastLine) break; i++; @@ -1442,7 +1440,7 @@ public class Editor { void invalidateTextDisplayList() { if (mTextDisplayLists != null) { for (int i = 0; i < mTextDisplayLists.length; i++) { - if (mTextDisplayLists[i] != null) mTextDisplayLists[i].invalidate(); + if (mTextDisplayLists[i] != null) mTextDisplayLists[i].clear(); } } } @@ -1468,20 +1466,24 @@ public class Editor { middle = (top + bottom) >> 1; } - updateCursorPosition(0, top, middle, getPrimaryHorizontal(layout, hintLayout, offset)); + boolean clamped = layout.shouldClampCursor(line); + updateCursorPosition(0, top, middle, + getPrimaryHorizontal(layout, hintLayout, offset, clamped)); if (mCursorCount == 2) { - updateCursorPosition(1, middle, bottom, layout.getSecondaryHorizontal(offset)); + updateCursorPosition(1, middle, bottom, + layout.getSecondaryHorizontal(offset, clamped)); } } - private float getPrimaryHorizontal(Layout layout, Layout hintLayout, int offset) { + private float getPrimaryHorizontal(Layout layout, Layout hintLayout, int offset, + boolean clamped) { if (TextUtils.isEmpty(layout.getText()) && hintLayout != null && !TextUtils.isEmpty(hintLayout.getText())) { - return hintLayout.getPrimaryHorizontal(offset); + return hintLayout.getPrimaryHorizontal(offset, clamped); } else { - return layout.getPrimaryHorizontal(offset); + return layout.getPrimaryHorizontal(offset, clamped); } } @@ -1890,10 +1892,23 @@ public class Editor { // Make sure there is only at most one EasyEditSpan in the text if (mPopupWindow.mEasyEditSpan != null) { - text.removeSpan(mPopupWindow.mEasyEditSpan); + mPopupWindow.mEasyEditSpan.setDeleteEnabled(false); } mPopupWindow.setEasyEditSpan((EasyEditSpan) span); + mPopupWindow.setOnDeleteListener(new EasyEditDeleteListener() { + @Override + public void onDeleteClick(EasyEditSpan span) { + Editable editable = (Editable) mTextView.getText(); + int start = editable.getSpanStart(span); + int end = editable.getSpanEnd(span); + if (start >= 0 && end >= 0) { + sendNotification(EasyEditSpan.TEXT_DELETED, span); + mTextView.deleteText_internal(start, end); + } + editable.removeSpan(span); + } + }); if (mTextView.getWindowVisibility() != View.VISIBLE) { // The window is not visible yet, ignore the text change. @@ -1927,8 +1942,10 @@ public class Editor { @Override public void onSpanChanged(Spannable text, Object span, int previousStart, int previousEnd, int newStart, int newEnd) { - if (mPopupWindow != null && span == mPopupWindow.mEasyEditSpan) { - text.removeSpan(mPopupWindow.mEasyEditSpan); + if (mPopupWindow != null && span instanceof EasyEditSpan) { + EasyEditSpan easyEditSpan = (EasyEditSpan) span; + sendNotification(EasyEditSpan.TEXT_MODIFIED, easyEditSpan); + text.removeSpan(easyEditSpan); } } @@ -1938,6 +1955,31 @@ public class Editor { mTextView.removeCallbacks(mHidePopup); } } + + private void sendNotification(int textChangedType, EasyEditSpan span) { + try { + PendingIntent pendingIntent = span.getPendingIntent(); + if (pendingIntent != null) { + Intent intent = new Intent(); + intent.putExtra(EasyEditSpan.EXTRA_TEXT_CHANGED_TYPE, textChangedType); + pendingIntent.send(mTextView.getContext(), 0, intent); + } + } catch (CanceledException e) { + // This should not happen, as we should try to send the intent only once. + Log.w(TAG, "PendingIntent for notification cannot be sent", e); + } + } + } + + /** + * Listens for the delete event triggered by {@link EasyEditPopupWindow}. + */ + private interface EasyEditDeleteListener { + + /** + * Clicks the delete pop-up. + */ + void onDeleteClick(EasyEditSpan span); } /** @@ -1950,6 +1992,7 @@ public class Editor { com.android.internal.R.layout.text_edit_action_popup_text; private TextView mDeleteTextView; private EasyEditSpan mEasyEditSpan; + private EasyEditDeleteListener mOnDeleteListener; @Override protected void createPopupWindow() { @@ -1984,16 +2027,26 @@ public class Editor { mEasyEditSpan = easyEditSpan; } + private void setOnDeleteListener(EasyEditDeleteListener listener) { + mOnDeleteListener = listener; + } + @Override public void onClick(View view) { - if (view == mDeleteTextView) { - Editable editable = (Editable) mTextView.getText(); - int start = editable.getSpanStart(mEasyEditSpan); - int end = editable.getSpanEnd(mEasyEditSpan); - if (start >= 0 && end >= 0) { - mTextView.deleteText_internal(start, end); - } + if (view == mDeleteTextView + && mEasyEditSpan != null && mEasyEditSpan.isDeleteEnabled() + && mOnDeleteListener != null) { + mOnDeleteListener.onDeleteClick(mEasyEditSpan); + } + } + + @Override + public void hide() { + if (mEasyEditSpan != null) { + mEasyEditSpan.setDeleteEnabled(false); } + mOnDeleteListener = null; + super.hide(); } @Override @@ -2647,15 +2700,10 @@ public class Editor { suggestionStart, suggestionEnd).toString(); mTextView.replaceText_internal(spanStart, spanEnd, suggestion); - // Notify source IME of the suggestion pick. Do this before swaping texts. - if (!TextUtils.isEmpty( - suggestionInfo.suggestionSpan.getNotificationTargetClassName())) { - InputMethodManager imm = InputMethodManager.peekInstance(); - if (imm != null) { - imm.notifySuggestionPicked(suggestionInfo.suggestionSpan, originalText, - suggestionInfo.suggestionIndex); - } - } + // Notify source IME of the suggestion pick. Do this before + // swaping texts. + suggestionInfo.suggestionSpan.notifySelection( + mTextView.getContext(), originalText, suggestionInfo.suggestionIndex); // Swap text content between actual text and Suggestion span String[] suggestions = suggestionInfo.suggestionSpan.getSuggestions(); diff --git a/core/java/android/widget/Gallery.java b/core/java/android/widget/Gallery.java index e0c5bbd..c4ef11c 100644 --- a/core/java/android/widget/Gallery.java +++ b/core/java/android/widget/Gallery.java @@ -891,7 +891,7 @@ public class Gallery extends AbsSpinner implements GestureDetector.OnGestureList lp = (Gallery.LayoutParams) generateDefaultLayoutParams(); } - addViewInLayout(child, fromLeft != mIsRtl ? -1 : 0, lp); + addViewInLayout(child, fromLeft != mIsRtl ? -1 : 0, lp, true); child.setSelected(offset == 0); diff --git a/core/java/android/widget/GridLayout.java b/core/java/android/widget/GridLayout.java index 772d748..85ed8db 100644 --- a/core/java/android/widget/GridLayout.java +++ b/core/java/android/widget/GridLayout.java @@ -605,7 +605,7 @@ public class GridLayout extends ViewGroup { } private int getDefaultMargin(View c, boolean isAtEdge, boolean horizontal, boolean leading) { - return isAtEdge ? DEFAULT_CONTAINER_MARGIN : getDefaultMargin(c, horizontal, leading); + return /*isAtEdge ? DEFAULT_CONTAINER_MARGIN :*/ getDefaultMargin(c, horizontal, leading); } private int getDefaultMargin(View c, LayoutParams p, boolean horizontal, boolean leading) { @@ -824,13 +824,11 @@ public class GridLayout extends ViewGroup { // Draw grid private void drawLine(Canvas graphics, int x1, int y1, int x2, int y2, Paint paint) { - int dx = getPaddingLeft(); - int dy = getPaddingTop(); if (isLayoutRtl()) { int width = getWidth(); - graphics.drawLine(width - dx - x1, dy + y1, width - dx - x2, dy + y2, paint); + graphics.drawLine(width - x1, y1, width - x2, y2, paint); } else { - graphics.drawLine(dx + x1, dy + y1, dx + x2, dy + y2, paint); + graphics.drawLine(x1, y1, x2, y2, paint); } } @@ -838,18 +836,17 @@ public class GridLayout extends ViewGroup { * @hide */ @Override - protected void onDebugDrawMargins(Canvas canvas) { + protected void onDebugDrawMargins(Canvas canvas, Paint paint) { // Apply defaults, so as to remove UNDEFINED values LayoutParams lp = new LayoutParams(); for (int i = 0; i < getChildCount(); i++) { View c = getChildAt(i); - Insets insets = getLayoutMode() == OPTICAL_BOUNDS ? c.getOpticalInsets() : Insets.NONE; lp.setMargins( - getMargin1(c, true, true) - insets.left, - getMargin1(c, false, true) - insets.top, - getMargin1(c, true, false) - insets.right, - getMargin1(c, false, false) - insets.bottom); - lp.onDebugDraw(c, canvas); + getMargin1(c, true, true), + getMargin1(c, false, true), + getMargin1(c, true, false), + getMargin1(c, false, false)); + lp.onDebugDraw(c, canvas, paint); } } @@ -858,26 +855,30 @@ public class GridLayout extends ViewGroup { */ @Override protected void onDebugDraw(Canvas canvas) { - int height = getHeight() - getPaddingTop() - getPaddingBottom(); - int width = getWidth() - getPaddingLeft() - getPaddingRight(); - Paint paint = new Paint(); paint.setStyle(Paint.Style.STROKE); paint.setColor(Color.argb(50, 255, 255, 255)); + Insets insets = getOpticalInsets(); + + int top = getPaddingTop() + insets.top; + int left = getPaddingLeft() + insets.left; + int right = getWidth() - getPaddingRight() - insets.right; + int bottom = getHeight() - getPaddingBottom() - insets.bottom; + int[] xs = horizontalAxis.locations; if (xs != null) { for (int i = 0, length = xs.length; i < length; i++) { - int x = xs[i]; - drawLine(canvas, x, 0, x, height - 1, paint); + int x = left + xs[i]; + drawLine(canvas, x, top, x, bottom, paint); } } int[] ys = verticalAxis.locations; if (ys != null) { for (int i = 0, length = ys.length; i < length; i++) { - int y = ys[i]; - drawLine(canvas, 0, y, width - 1, y, paint); + int y = top + ys[i]; + drawLine(canvas, left, y, right, y, paint); } } @@ -1013,12 +1014,7 @@ public class GridLayout extends ViewGroup { } private int getMeasurement(View c, boolean horizontal) { - int result = horizontal ? c.getMeasuredWidth() : c.getMeasuredHeight(); - if (getLayoutMode() == OPTICAL_BOUNDS) { - Insets insets = c.getOpticalInsets(); - return result - (horizontal ? insets.left + insets.right : insets.top + insets.bottom); - } - return result; + return horizontal ? c.getMeasuredWidth() : c.getMeasuredHeight(); } final int getMeasurementIncludingMargin(View c, boolean horizontal) { @@ -1124,14 +1120,6 @@ public class GridLayout extends ViewGroup { targetWidth - width - paddingRight - rightMargin - dx; int cy = paddingTop + y1 + gravityOffsetY + alignmentOffsetY + topMargin; - boolean useLayoutBounds = getLayoutMode() == OPTICAL_BOUNDS; - if (useLayoutBounds) { - Insets insets = c.getOpticalInsets(); - cx -= insets.left; - cy -= insets.top; - width += (insets.left + insets.right); - height += (insets.top + insets.bottom); - } if (width != c.getMeasuredWidth() || height != c.getMeasuredHeight()) { c.measure(makeMeasureSpec(width, EXACTLY), makeMeasureSpec(height, EXACTLY)); } @@ -2418,6 +2406,8 @@ public class GridLayout extends ViewGroup { * <li> {@code spec.span = [start, start + size]} </li> * <li> {@code spec.alignment = alignment} </li> * </ul> + * <p> + * To leave the start index undefined, use the value {@link #UNDEFINED}. * * @param start the start * @param size the size @@ -2433,9 +2423,13 @@ public class GridLayout extends ViewGroup { * <li> {@code spec.span = [start, start + 1]} </li> * <li> {@code spec.alignment = alignment} </li> * </ul> + * <p> + * To leave the start index undefined, use the value {@link #UNDEFINED}. * * @param start the start index * @param alignment the alignment + * + * @see #spec(int, int, Alignment) */ public static Spec spec(int start, Alignment alignment) { return spec(start, 1, alignment); @@ -2446,9 +2440,13 @@ public class GridLayout extends ViewGroup { * <ul> * <li> {@code spec.span = [start, start + size]} </li> * </ul> + * <p> + * To leave the start index undefined, use the value {@link #UNDEFINED}. * * @param start the start * @param size the size + * + * @see #spec(int, Alignment) */ public static Spec spec(int start, int size) { return spec(start, size, UNDEFINED_ALIGNMENT); @@ -2459,8 +2457,12 @@ public class GridLayout extends ViewGroup { * <ul> * <li> {@code spec.span = [start, start + 1]} </li> * </ul> + * <p> + * To leave the start index undefined, use the value {@link #UNDEFINED}. * * @param start the start index + * + * @see #spec(int, int) */ public static Spec spec(int start) { return spec(start, 1); @@ -2654,14 +2656,7 @@ public class GridLayout extends ViewGroup { @Override public int getAlignmentValue(View view, int viewSize, int mode) { int baseline = view.getBaseline(); - if (baseline == -1) { - return UNDEFINED; - } else { - if (mode == OPTICAL_BOUNDS) { - return baseline - view.getOpticalInsets().top; - } - return baseline; - } + return baseline == -1 ? UNDEFINED : baseline; } @Override diff --git a/core/java/android/widget/ImageView.java b/core/java/android/widget/ImageView.java index 26c801f..1bbf4eb 100644 --- a/core/java/android/widget/ImageView.java +++ b/core/java/android/widget/ImageView.java @@ -30,6 +30,7 @@ import android.graphics.RectF; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; +import android.os.Build; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; @@ -40,6 +41,9 @@ import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; +import java.io.IOException; +import java.io.InputStream; + /** * Displays an arbitrary image, such as an icon. The ImageView class * can load images from various sources (such as resources or content @@ -90,6 +94,9 @@ public class ImageView extends View { private int mBaseline = -1; private boolean mBaselineAlignBottom = false; + // AdjustViewBounds behavior will be in compatibility mode for older apps. + private boolean mAdjustViewBoundsCompat = false; + private static final ScaleType[] sScaleTypeArray = { ScaleType.MATRIX, ScaleType.FIT_XY, @@ -164,6 +171,8 @@ public class ImageView extends View { private void initImageView() { mMatrix = new Matrix(); mScaleType = ScaleType.FIT_CENTER; + mAdjustViewBoundsCompat = mContext.getApplicationInfo().targetSdkVersion <= + Build.VERSION_CODES.JELLY_BEAN_MR1; } @Override @@ -225,8 +234,15 @@ public class ImageView extends View { /** * Set this to true if you want the ImageView to adjust its bounds * to preserve the aspect ratio of its drawable. + * + * <p><strong>Note:</strong> If the application targets API level 17 or lower, + * adjustViewBounds will allow the drawable to shrink the view bounds, but not grow + * to fill available measured space in all cases. This is for compatibility with + * legacy {@link android.view.View.MeasureSpec MeasureSpec} and + * {@link android.widget.RelativeLayout RelativeLayout} behavior.</p> + * * @param adjustViewBounds Whether to adjust the bounds of this view - * to presrve the original aspect ratio of the drawable + * to preserve the original aspect ratio of the drawable. * * @see #getAdjustViewBounds() * @@ -546,13 +562,14 @@ public class ImageView extends View { /** Return the view's optional matrix. This is applied to the view's drawable when it is drawn. If there is not matrix, - this method will return null. - Do not change this matrix in place. If you want a different matrix - applied to the drawable, be sure to call setImageMatrix(). + this method will return an identity matrix. + Do not change this matrix in place but make a copy. + If you want a different matrix applied to the drawable, + be sure to call setImageMatrix(). */ public Matrix getImageMatrix() { if (mDrawMatrix == null) { - return Matrix.IDENTITY_MATRIX; + return new Matrix(Matrix.IDENTITY_MATRIX); } return mDrawMatrix; } @@ -635,20 +652,27 @@ public class ImageView extends View { } } else if (ContentResolver.SCHEME_CONTENT.equals(scheme) || ContentResolver.SCHEME_FILE.equals(scheme)) { + InputStream stream = null; try { - d = Drawable.createFromStream( - mContext.getContentResolver().openInputStream(mUri), - null); + stream = mContext.getContentResolver().openInputStream(mUri); + d = Drawable.createFromStream(stream, null); } catch (Exception e) { Log.w("ImageView", "Unable to open content: " + mUri, e); + } finally { + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { + Log.w("ImageView", "Unable to close content: " + mUri, e); + } + } } - } else { + } else { d = Drawable.createFromPath(mUri.toString()); } if (d == null) { - System.out.println("resolveUri failed on bad bitmap uri: " - + mUri); + System.out.println("resolveUri failed on bad bitmap uri: " + mUri); // Don't try again. mUri = null; } @@ -792,6 +816,12 @@ public class ImageView extends View { if (resizeWidth) { int newWidth = (int)(desiredAspect * (heightSize - ptop - pbottom)) + pleft + pright; + + // Allow the width to outgrow its original estimate if height is fixed. + if (!resizeHeight && !mAdjustViewBoundsCompat) { + widthSize = resolveAdjustedSize(newWidth, mMaxWidth, widthMeasureSpec); + } + if (newWidth <= widthSize) { widthSize = newWidth; done = true; @@ -802,6 +832,13 @@ public class ImageView extends View { if (!done && resizeHeight) { int newHeight = (int)((widthSize - pleft - pright) / desiredAspect) + ptop + pbottom; + + // Allow the height to outgrow its original estimate if width is fixed. + if (!resizeWidth && !mAdjustViewBoundsCompat) { + heightSize = resolveAdjustedSize(newHeight, mMaxHeight, + heightMeasureSpec); + } + if (newHeight <= heightSize) { heightSize = newHeight; } diff --git a/core/java/android/widget/LinearLayout.java b/core/java/android/widget/LinearLayout.java index 36dd13c..bc57c36 100644 --- a/core/java/android/widget/LinearLayout.java +++ b/core/java/android/widget/LinearLayout.java @@ -1431,9 +1431,9 @@ public class LinearLayout extends ViewGroup { @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (mOrientation == VERTICAL) { - layoutVertical(); + layoutVertical(l, t, r, b); } else { - layoutHorizontal(); + layoutHorizontal(l, t, r, b); } } @@ -1444,15 +1444,19 @@ public class LinearLayout extends ViewGroup { * @see #getOrientation() * @see #setOrientation(int) * @see #onLayout(boolean, int, int, int, int) + * @param left + * @param top + * @param right + * @param bottom */ - void layoutVertical() { + void layoutVertical(int left, int top, int right, int bottom) { final int paddingLeft = mPaddingLeft; int childTop; int childLeft; // Where right end of child should go - final int width = mRight - mLeft; + final int width = right - left; int childRight = width - mPaddingRight; // Space available for child @@ -1466,12 +1470,12 @@ public class LinearLayout extends ViewGroup { switch (majorGravity) { case Gravity.BOTTOM: // mTotalLength contains the padding already - childTop = mPaddingTop + mBottom - mTop - mTotalLength; + childTop = mPaddingTop + bottom - top - mTotalLength; break; // mTotalLength contains the padding already case Gravity.CENTER_VERTICAL: - childTop = mPaddingTop + (mBottom - mTop - mTotalLength) / 2; + childTop = mPaddingTop + (bottom - top - mTotalLength) / 2; break; case Gravity.TOP: @@ -1534,8 +1538,12 @@ public class LinearLayout extends ViewGroup { * @see #getOrientation() * @see #setOrientation(int) * @see #onLayout(boolean, int, int, int, int) + * @param left + * @param top + * @param right + * @param bottom */ - void layoutHorizontal() { + void layoutHorizontal(int left, int top, int right, int bottom) { final boolean isLayoutRtl = isLayoutRtl(); final int paddingTop = mPaddingTop; @@ -1543,7 +1551,7 @@ public class LinearLayout extends ViewGroup { int childLeft; // Where bottom of child should go - final int height = mBottom - mTop; + final int height = bottom - top; int childBottom = height - mPaddingBottom; // Space available for child @@ -1563,12 +1571,12 @@ public class LinearLayout extends ViewGroup { switch (Gravity.getAbsoluteGravity(majorGravity, layoutDirection)) { case Gravity.RIGHT: // mTotalLength contains the padding already - childLeft = mPaddingLeft + mRight - mLeft - mTotalLength; + childLeft = mPaddingLeft + right - left - mTotalLength; break; case Gravity.CENTER_HORIZONTAL: // mTotalLength contains the padding already - childLeft = mPaddingLeft + (mRight - mLeft - mTotalLength) / 2; + childLeft = mPaddingLeft + (right - left - mTotalLength) / 2; break; case Gravity.LEFT: diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java index 4436fbb..69e3177 100644 --- a/core/java/android/widget/ListView.java +++ b/core/java/android/widget/ListView.java @@ -2429,9 +2429,7 @@ public class ListView extends AbsListView { View selectedView = getSelectedView(); int selectedPos = mSelectedPosition; - int nextSelectedPosition = (direction == View.FOCUS_DOWN) ? - lookForSelectablePosition(selectedPos + 1, true) : - lookForSelectablePosition(selectedPos - 1, false); + int nextSelectedPosition = lookForSelectablePositionOnScreen(direction); int amountToScroll = amountToScroll(direction, nextSelectedPosition); // if we are moving focus, we may OVERRIDE the default behavior @@ -2643,18 +2641,14 @@ public class ListView extends AbsListView { final int listBottom = getHeight() - mListPadding.bottom; final int listTop = mListPadding.top; - int numChildren = getChildCount(); + final int numChildren = getChildCount(); if (direction == View.FOCUS_DOWN) { int indexToMakeVisible = numChildren - 1; if (nextSelectedPosition != INVALID_POSITION) { indexToMakeVisible = nextSelectedPosition - mFirstPosition; } - while (numChildren <= indexToMakeVisible) { - // Child to view is not attached yet. - addViewBelow(getChildAt(numChildren - 1), mFirstPosition + numChildren - 1); - numChildren++; - } + final int positionToMakeVisible = mFirstPosition + indexToMakeVisible; final View viewToMakeVisible = getChildAt(indexToMakeVisible); @@ -2688,12 +2682,6 @@ public class ListView extends AbsListView { if (nextSelectedPosition != INVALID_POSITION) { indexToMakeVisible = nextSelectedPosition - mFirstPosition; } - while (indexToMakeVisible < 0) { - // Child to view is not attached yet. - addViewAbove(getChildAt(0), mFirstPosition); - mFirstPosition--; - indexToMakeVisible = nextSelectedPosition - mFirstPosition; - } final int positionToMakeVisible = mFirstPosition + indexToMakeVisible; final View viewToMakeVisible = getChildAt(indexToMakeVisible); int goalTop = listTop; diff --git a/core/java/android/widget/MediaController.java b/core/java/android/widget/MediaController.java index f76ab2b..ee1bf18 100644 --- a/core/java/android/widget/MediaController.java +++ b/core/java/android/widget/MediaController.java @@ -149,7 +149,7 @@ public class MediaController extends FrameLayout { private void initFloatingWindowLayout() { mDecorLayoutParams = new WindowManager.LayoutParams(); WindowManager.LayoutParams p = mDecorLayoutParams; - p.gravity = Gravity.TOP; + p.gravity = Gravity.TOP | Gravity.LEFT; p.height = LayoutParams.WRAP_CONTENT; p.x = 0; p.format = PixelFormat.TRANSLUCENT; @@ -167,9 +167,15 @@ public class MediaController extends FrameLayout { int [] anchorPos = new int[2]; mAnchor.getLocationOnScreen(anchorPos); + // we need to know the size of the controller so we can properly position it + // within its space + mDecor.measure(MeasureSpec.makeMeasureSpec(mAnchor.getWidth(), MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(mAnchor.getHeight(), MeasureSpec.AT_MOST)); + WindowManager.LayoutParams p = mDecorLayoutParams; p.width = mAnchor.getWidth(); - p.y = anchorPos[1] + mAnchor.getHeight(); + p.x = anchorPos[0] + (mAnchor.getWidth() - p.width) / 2; + p.y = anchorPos[1] + mAnchor.getHeight() - mDecor.getMeasuredHeight(); } // This is called whenever mAnchor's layout bound changes @@ -204,6 +210,8 @@ public class MediaController extends FrameLayout { /** * Set the view that acts as the anchor for the control view. * This can for example be a VideoView, or your Activity's main view. + * When VideoView calls this method, it will use the VideoView's parent + * as the anchor. * @param view The view to which to anchor the controller when it is visible. */ public void setAnchorView(View view) { diff --git a/core/java/android/widget/ProgressBar.java b/core/java/android/widget/ProgressBar.java index ea50e2e..d816200 100644 --- a/core/java/android/widget/ProgressBar.java +++ b/core/java/android/widget/ProgressBar.java @@ -38,10 +38,7 @@ import android.graphics.drawable.shapes.Shape; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; -import android.util.Pool; -import android.util.Poolable; -import android.util.PoolableManager; -import android.util.Pools; +import android.util.Pools.SynchronizedPool; import android.view.Gravity; import android.view.RemotableViewMethod; import android.view.View; @@ -226,6 +223,8 @@ public class ProgressBar extends View { private boolean mAttached; private boolean mRefreshIsPosted; + boolean mMirrorForRtl = false; + private final ArrayList<RefreshData> mRefreshData = new ArrayList<RefreshData>(); private AccessibilityEventSender mAccessibilityEventSender; @@ -305,6 +304,8 @@ public class ProgressBar extends View { setIndeterminate(mOnlyIndeterminate || a.getBoolean( R.styleable.ProgressBar_indeterminate, mIndeterminate)); + mMirrorForRtl = a.getBoolean(R.styleable.ProgressBar_mirrorForRtl, mMirrorForRtl); + a.recycle(); } @@ -604,33 +605,20 @@ public class ProgressBar extends View { } } - private static class RefreshData implements Poolable<RefreshData> { + private static class RefreshData { + private static final int POOL_MAX = 24; + private static final SynchronizedPool<RefreshData> sPool = + new SynchronizedPool<RefreshData>(POOL_MAX); + public int id; public int progress; public boolean fromUser; - - private RefreshData mNext; - private boolean mIsPooled; - - private static final int POOL_MAX = 24; - private static final Pool<RefreshData> sPool = Pools.synchronizedPool( - Pools.finitePool(new PoolableManager<RefreshData>() { - @Override - public RefreshData newInstance() { - return new RefreshData(); - } - - @Override - public void onAcquired(RefreshData element) { - } - - @Override - public void onReleased(RefreshData element) { - } - }, POOL_MAX)); public static RefreshData obtain(int id, int progress, boolean fromUser) { RefreshData rd = sPool.acquire(); + if (rd == null) { + rd = new RefreshData(); + } rd.id = id; rd.progress = progress; rd.fromUser = fromUser; @@ -640,28 +628,8 @@ public class ProgressBar extends View { public void recycle() { sPool.release(this); } - - @Override - public void setNextPoolable(RefreshData element) { - mNext = element; - } - - @Override - public RefreshData getNextPoolable() { - return mNext; - } - - @Override - public boolean isPooled() { - return mIsPooled; - } - - @Override - public void setPooled(boolean isPooled) { - mIsPooled = isPooled; - } } - + private synchronized void doRefreshProgress(int id, int progress, boolean fromUser, boolean callBackToApp) { float scale = mMax > 0 ? (float) progress / (float) mMax : 0; @@ -1040,7 +1008,7 @@ public class ProgressBar extends View { } } } - if (isLayoutRtl()) { + if (isLayoutRtl() && mMirrorForRtl) { int tempLeft = left; left = w - right; right = w - tempLeft; @@ -1062,7 +1030,7 @@ public class ProgressBar extends View { // Translate canvas so a indeterminate circular progress bar with padding // rotates properly in its animation canvas.save(); - if(isLayoutRtl()) { + if(isLayoutRtl() && mMirrorForRtl) { canvas.translate(getWidth() - mPaddingRight, mPaddingTop); canvas.scale(-1.0f, 1.0f); } else { diff --git a/core/java/android/widget/QuickContactBadge.java b/core/java/android/widget/QuickContactBadge.java index 786afe2..368f6ad 100644 --- a/core/java/android/widget/QuickContactBadge.java +++ b/core/java/android/widget/QuickContactBadge.java @@ -27,6 +27,7 @@ import android.database.Cursor; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.net.Uri; +import android.os.Bundle; import android.provider.ContactsContract.CommonDataKinds.Email; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Intents; @@ -50,6 +51,7 @@ public class QuickContactBadge extends ImageView implements OnClickListener { private Drawable mOverlay; private QueryHandler mQueryHandler; private Drawable mDefaultAvatar; + private Bundle mExtras = null; protected String[] mExcludeMimes = null; @@ -58,6 +60,8 @@ public class QuickContactBadge extends ImageView implements OnClickListener { static final private int TOKEN_EMAIL_LOOKUP_AND_TRIGGER = 2; static final private int TOKEN_PHONE_LOOKUP_AND_TRIGGER = 3; + static final private String EXTRA_URI_CONTENT = "uri_content"; + static final String[] EMAIL_LOOKUP_PROJECTION = new String[] { RawContacts.CONTACT_ID, Contacts.LOOKUP_KEY, @@ -175,7 +179,26 @@ public class QuickContactBadge extends ImageView implements OnClickListener { * until this view is clicked. */ public void assignContactFromEmail(String emailAddress, boolean lazyLookup) { + assignContactFromEmail(emailAddress, lazyLookup, null); + } + + /** + * Assign a contact based on an email address. This should only be used when + * the contact's URI is not available, as an extra query will have to be + * performed to lookup the URI based on the email. + + @param emailAddress The email address of the contact. + @param lazyLookup If this is true, the lookup query will not be performed + until this view is clicked. + @param extras A bundle of extras to populate the contact edit page with if the contact + is not found and the user chooses to add the email address to an existing contact or + create a new contact. Uses the same string constants as those found in + {@link android.provider.ContactsContract.Intents.Insert} + */ + + public void assignContactFromEmail(String emailAddress, boolean lazyLookup, Bundle extras) { mContactEmail = emailAddress; + mExtras = extras; if (!lazyLookup) { mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP, null, Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mContactEmail)), @@ -186,6 +209,7 @@ public class QuickContactBadge extends ImageView implements OnClickListener { } } + /** * Assign a contact based on a phone number. This should only be used when * the contact's URI is not available, as an extra query will have to be @@ -196,7 +220,25 @@ public class QuickContactBadge extends ImageView implements OnClickListener { * until this view is clicked. */ public void assignContactFromPhone(String phoneNumber, boolean lazyLookup) { + assignContactFromPhone(phoneNumber, lazyLookup, new Bundle()); + } + + /** + * Assign a contact based on a phone number. This should only be used when + * the contact's URI is not available, as an extra query will have to be + * performed to lookup the URI based on the phone number. + * + * @param phoneNumber The phone number of the contact. + * @param lazyLookup If this is true, the lookup query will not be performed + * until this view is clicked. + * @param extras A bundle of extras to populate the contact edit page with if the contact + * is not found and the user chooses to add the phone number to an existing contact or + * create a new contact. Uses the same string constants as those found in + * {@link android.provider.ContactsContract.Intents.Insert} + */ + public void assignContactFromPhone(String phoneNumber, boolean lazyLookup, Bundle extras) { mContactPhone = phoneNumber; + mExtras = extras; if (!lazyLookup) { mQueryHandler.startQuery(TOKEN_PHONE_LOOKUP, null, Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, mContactPhone), @@ -213,15 +255,21 @@ public class QuickContactBadge extends ImageView implements OnClickListener { @Override public void onClick(View v) { + // If contact has been assigned, mExtras should no longer be null, but do a null check + // anyway just in case assignContactFromPhone or Email was called with a null bundle or + // wasn't assigned previously. + final Bundle extras = (mExtras == null) ? new Bundle() : mExtras; if (mContactUri != null) { QuickContact.showQuickContact(getContext(), QuickContactBadge.this, mContactUri, QuickContact.MODE_LARGE, mExcludeMimes); } else if (mContactEmail != null) { - mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP_AND_TRIGGER, mContactEmail, + extras.putString(EXTRA_URI_CONTENT, mContactEmail); + mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP_AND_TRIGGER, extras, Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mContactEmail)), EMAIL_LOOKUP_PROJECTION, null, null, null); } else if (mContactPhone != null) { - mQueryHandler.startQuery(TOKEN_PHONE_LOOKUP_AND_TRIGGER, mContactPhone, + extras.putString(EXTRA_URI_CONTENT, mContactPhone); + mQueryHandler.startQuery(TOKEN_PHONE_LOOKUP_AND_TRIGGER, extras, Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, mContactPhone), PHONE_LOOKUP_PROJECTION, null, null, null); } else { @@ -262,12 +310,12 @@ public class QuickContactBadge extends ImageView implements OnClickListener { Uri lookupUri = null; Uri createUri = null; boolean trigger = false; - + Bundle extras = (cookie != null) ? (Bundle) cookie : new Bundle(); try { switch(token) { case TOKEN_PHONE_LOOKUP_AND_TRIGGER: trigger = true; - createUri = Uri.fromParts("tel", (String)cookie, null); + createUri = Uri.fromParts("tel", extras.getString(EXTRA_URI_CONTENT), null); //$FALL-THROUGH$ case TOKEN_PHONE_LOOKUP: { @@ -281,7 +329,8 @@ public class QuickContactBadge extends ImageView implements OnClickListener { } case TOKEN_EMAIL_LOOKUP_AND_TRIGGER: trigger = true; - createUri = Uri.fromParts("mailto", (String)cookie, null); + createUri = Uri.fromParts("mailto", + extras.getString(EXTRA_URI_CONTENT), null); //$FALL-THROUGH$ case TOKEN_EMAIL_LOOKUP: { @@ -309,6 +358,10 @@ public class QuickContactBadge extends ImageView implements OnClickListener { } else if (createUri != null) { // Prompt user to add this person to contacts final Intent intent = new Intent(Intents.SHOW_OR_CREATE_CONTACT, createUri); + if (extras != null) { + extras.remove(EXTRA_URI_CONTENT); + intent.putExtras(extras); + } getContext().startActivity(intent); } } diff --git a/core/java/android/widget/RelativeLayout.java b/core/java/android/widget/RelativeLayout.java index 27fda24..deec41c 100644 --- a/core/java/android/widget/RelativeLayout.java +++ b/core/java/android/widget/RelativeLayout.java @@ -29,16 +29,15 @@ import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Rect; +import android.os.Build; import android.util.AttributeSet; -import android.util.Pool; -import android.util.Poolable; -import android.util.PoolableManager; -import android.util.Pools; +import android.util.Pools.SimplePool; import android.util.SparseArray; import android.view.Gravity; import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; +import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; @@ -56,6 +55,21 @@ import static android.util.Log.d; * {@link #ALIGN_PARENT_BOTTOM}. * </p> * + * <p><strong>Note:</strong> In platform version 17 and lower, RelativeLayout was affected by + * a measurement bug that could cause child views to be measured with incorrect + * {@link android.view.View.MeasureSpec MeasureSpec} values. (See + * {@link android.view.View.MeasureSpec#makeMeasureSpec(int, int) MeasureSpec.makeMeasureSpec} + * for more details.) This was triggered when a RelativeLayout container was placed in + * a scrolling container, such as a ScrollView or HorizontalScrollView. If a custom view + * not equipped to properly measure with the MeasureSpec mode + * {@link android.view.View.MeasureSpec#UNSPECIFIED UNSPECIFIED} was placed in a RelativeLayout, + * this would silently work anyway as RelativeLayout would pass a very large + * {@link android.view.View.MeasureSpec#AT_MOST AT_MOST} MeasureSpec instead.</p> + * + * <p>This behavior has been preserved for apps that set <code>android:targetSdkVersion="17"</code> + * or older in their manifest's <code>uses-sdk</code> tag for compatibility. Apps targeting SDK + * version 18 or newer will receive the correct behavior</p> + * * <p>See the <a href="{@docRoot}guide/topics/ui/layout/relative.html">Relative * Layout</a> guide.</p> * @@ -202,18 +216,34 @@ public class RelativeLayout extends ViewGroup { private View[] mSortedVerticalChildren = new View[0]; private final DependencyGraph mGraph = new DependencyGraph(); + // Compatibility hack. Old versions of the platform had problems + // with MeasureSpec value overflow and RelativeLayout was one source of them. + // Some apps came to rely on them. :( + private boolean mAllowBrokenMeasureSpecs = false; + + private int mDisplayWidth; + public RelativeLayout(Context context) { super(context); + mAllowBrokenMeasureSpecs = context.getApplicationInfo().targetSdkVersion <= + Build.VERSION_CODES.JELLY_BEAN_MR1; + getDisplayWidth(); } public RelativeLayout(Context context, AttributeSet attrs) { super(context, attrs); initFromAttributes(context, attrs); + mAllowBrokenMeasureSpecs = context.getApplicationInfo().targetSdkVersion <= + Build.VERSION_CODES.JELLY_BEAN_MR1; + getDisplayWidth(); } public RelativeLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initFromAttributes(context, attrs); + mAllowBrokenMeasureSpecs = context.getApplicationInfo().targetSdkVersion <= + Build.VERSION_CODES.JELLY_BEAN_MR1; + getDisplayWidth(); } private void initFromAttributes(Context context, AttributeSet attrs) { @@ -223,6 +253,11 @@ public class RelativeLayout extends ViewGroup { a.recycle(); } + private void getDisplayWidth() { + WindowManager wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE); + mDisplayWidth = wm.getDefaultDisplay().getWidth(); + } + @Override public boolean shouldDelayChildPressedState() { return false; @@ -414,51 +449,28 @@ public class RelativeLayout extends ViewGroup { final boolean isWrapContentWidth = widthMode != MeasureSpec.EXACTLY; final boolean isWrapContentHeight = heightMode != MeasureSpec.EXACTLY; + // We need to know our size for doing the correct computation of children positioning in RTL + // mode but there is no practical way to get it instead of running the code below. + // So, instead of running the code twice, we just set the width to the "display width" + // before the computation and then, as a last pass, we will update their real position with + // an offset equals to "displayWidth - width". + final int layoutDirection = getLayoutDirection(); + if (isLayoutRtl() && myWidth == -1) { + myWidth = mDisplayWidth; + } + View[] views = mSortedHorizontalChildren; int count = views.length; - // We need to know our size for doing the correct computation of positioning in RTL mode - if (isLayoutRtl() && (myWidth == -1 || isWrapContentWidth)) { - int w = getPaddingStart() + getPaddingEnd(); - final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); - for (int i = 0; i < count; i++) { - View child = views[i]; - if (child.getVisibility() != GONE) { - LayoutParams params = (LayoutParams) child.getLayoutParams(); - // Would be similar to a call to measureChildHorizontal(child, params, -1, myHeight) - // but we cannot change for now the behavior of measureChildHorizontal() for - // taking care or a "-1" for "mywidth" so use here our own version of that code. - int childHeightMeasureSpec; - if (params.width == LayoutParams.MATCH_PARENT) { - childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(myHeight, MeasureSpec.EXACTLY); - } else { - childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(myHeight, MeasureSpec.AT_MOST); - } - child.measure(childWidthMeasureSpec, childHeightMeasureSpec); - - w += child.getMeasuredWidth(); - w += params.leftMargin + params.rightMargin; - } - } - if (myWidth == -1) { - // Easy case: "myWidth" was undefined before so use the width we have just computed - myWidth = w; - } else { - // "myWidth" was defined before, so take the min of it and the computed width if it - // is a non null one - if (w > 0) { - myWidth = Math.min(myWidth, w); - } - } - } - for (int i = 0; i < count; i++) { View child = views[i]; if (child.getVisibility() != GONE) { LayoutParams params = (LayoutParams) child.getLayoutParams(); + int[] rules = params.getRules(layoutDirection); - applyHorizontalSizeRules(params, myWidth); + applyHorizontalSizeRules(params, myWidth, rules); measureChildHorizontal(child, params, myWidth, myHeight); + if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) { offsetHorizontalAxis = true; } @@ -480,7 +492,11 @@ public class RelativeLayout extends ViewGroup { } if (isWrapContentWidth) { - width = Math.max(width, params.mRight); + if (isLayoutRtl()) { + width = Math.max(width, myWidth - params.mLeft); + } else { + width = Math.max(width, params.mRight); + } } if (isWrapContentHeight) { @@ -519,8 +535,6 @@ public class RelativeLayout extends ViewGroup { } } - final int layoutDirection = getLayoutDirection(); - if (isWrapContentWidth) { // Width already has left padding in it since it was calculated by looking at // the right of each child view @@ -610,6 +624,19 @@ public class RelativeLayout extends ViewGroup { } } + if (isLayoutRtl()) { + final int offsetWidth = myWidth - width; + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + if (child.getVisibility() != GONE) { + LayoutParams params = (LayoutParams) child.getLayoutParams(); + params.mLeft -= offsetWidth; + params.mRight -= offsetWidth; + } + } + + } + setMeasuredDimension(width, height); } @@ -673,7 +700,17 @@ public class RelativeLayout extends ViewGroup { mPaddingLeft, mPaddingRight, myWidth); int childHeightMeasureSpec; - if (params.width == LayoutParams.MATCH_PARENT) { + if (myHeight < 0 && !mAllowBrokenMeasureSpecs) { + if (params.height >= 0) { + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( + params.height, MeasureSpec.EXACTLY); + } else { + // Negative values in a mySize/myWidth/myWidth value in RelativeLayout measurement + // is code for, "we got an unspecified mode in the RelativeLayout's measurespec." + // Carry it forward. + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + } + } else if (params.width == LayoutParams.MATCH_PARENT) { childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(myHeight, MeasureSpec.EXACTLY); } else { childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(myHeight, MeasureSpec.AT_MOST); @@ -700,6 +737,16 @@ public class RelativeLayout extends ViewGroup { private int getChildMeasureSpec(int childStart, int childEnd, int childSize, int startMargin, int endMargin, int startPadding, int endPadding, int mySize) { + if (mySize < 0 && !mAllowBrokenMeasureSpecs) { + if (childSize >= 0) { + return MeasureSpec.makeMeasureSpec(childSize, MeasureSpec.EXACTLY); + } + // Negative values in a mySize/myWidth/myWidth value in RelativeLayout measurement + // is code for, "we got an unspecified mode in the RelativeLayout's measurespec." + // Carry it forward. + return MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + } + int childSpecMode = 0; int childSpecSize = 0; @@ -826,9 +873,7 @@ public class RelativeLayout extends ViewGroup { return rules[ALIGN_PARENT_BOTTOM] != 0; } - private void applyHorizontalSizeRules(LayoutParams childParams, int myWidth) { - final int layoutDirection = getLayoutDirection(); - int[] rules = childParams.getRules(layoutDirection); + private void applyHorizontalSizeRules(LayoutParams childParams, int myWidth, int[] rules) { RelativeLayout.LayoutParams anchorParams; // -1 indicated a "soft requirement" in that direction. For example: @@ -991,7 +1036,7 @@ public class RelativeLayout extends ViewGroup { return -1; } - private void centerHorizontal(View child, LayoutParams params, int myWidth) { + private static void centerHorizontal(View child, LayoutParams params, int myWidth) { int childWidth = child.getMeasuredWidth(); int left = (myWidth - childWidth) / 2; @@ -999,7 +1044,7 @@ public class RelativeLayout extends ViewGroup { params.mRight = left + childWidth; } - private void centerVertical(View child, LayoutParams params, int myHeight) { + private static void centerVertical(View child, LayoutParams params, int myHeight) { int childHeight = child.getMeasuredHeight(); int top = (myHeight - childHeight) / 2; @@ -1193,6 +1238,7 @@ public class RelativeLayout extends ViewGroup { com.android.internal.R.styleable.RelativeLayout_Layout); final int[] rules = mRules; + //noinspection MismatchedReadAndWriteOfArray final int[] initialRules = mInitialRules; final int N = a.getIndexCount(); @@ -1271,9 +1317,7 @@ public class RelativeLayout extends ViewGroup { } } - for (int n = LEFT_OF; n < VERB_COUNT; n++) { - initialRules[n] = rules[n]; - } + System.arraycopy(rules, LEFT_OF, initialRules, LEFT_OF, VERB_COUNT); a.recycle(); } @@ -1364,9 +1408,7 @@ public class RelativeLayout extends ViewGroup { private void resolveRules(int layoutDirection) { final boolean isLayoutRtl = (layoutDirection == View.LAYOUT_DIRECTION_RTL); // Reset to initial state - for (int n = LEFT_OF; n < VERB_COUNT; n++) { - mRules[n] = mInitialRules[n]; - } + System.arraycopy(mInitialRules, LEFT_OF, mRules, LEFT_OF, VERB_COUNT); // Apply rules depending on direction if (mRules[ALIGN_START] != 0) { mRules[isLayoutRtl ? ALIGN_RIGHT : ALIGN_LEFT] = mRules[ALIGN_START]; @@ -1655,7 +1697,7 @@ public class RelativeLayout extends ViewGroup { * * A node with no dependent is considered a root of the graph. */ - static class Node implements Poolable<Node> { + static class Node { /** * The view representing this node in the layout. */ @@ -1678,43 +1720,14 @@ public class RelativeLayout extends ViewGroup { // The pool is static, so all nodes instances are shared across // activities, that's why we give it a rather high limit private static final int POOL_LIMIT = 100; - private static final Pool<Node> sPool = Pools.synchronizedPool( - Pools.finitePool(new PoolableManager<Node>() { - public Node newInstance() { - return new Node(); - } - - public void onAcquired(Node element) { - } - - public void onReleased(Node element) { - } - }, POOL_LIMIT) - ); - - private Node mNext; - private boolean mIsPooled; - - public void setNextPoolable(Node element) { - mNext = element; - } - - public Node getNextPoolable() { - return mNext; - } - - public boolean isPooled() { - return mIsPooled; - } - - public void setPooled(boolean isPooled) { - mIsPooled = isPooled; - } + private static final SimplePool<Node> sPool = new SimplePool<Node>(POOL_LIMIT); static Node acquire(View view) { - final Node node = sPool.acquire(); + Node node = sPool.acquire(); + if (node == null) { + node = new Node(); + } node.view = view; - return node; } diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 8d1be53..79fc51e 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -53,7 +53,6 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; - /** * A class that describes a view hierarchy that can be displayed in * another process. The hierarchy is inflated from a layout resource @@ -340,7 +339,7 @@ public class RemoteViews implements Parcelable, Filter { if (target == null) return; if (!mIsWidgetCollectionChild) { - Log.e("RemoteViews", "The method setOnClickFillInIntent is available " + + Log.e(LOG_TAG, "The method setOnClickFillInIntent is available " + "only from RemoteViewsFactory (ie. on collection items)."); return; } @@ -359,13 +358,13 @@ public class RemoteViews implements Parcelable, Filter { if (parent instanceof AppWidgetHostView || parent == null) { // Somehow they've managed to get this far without having // and AdapterView as a parent. - Log.e("RemoteViews", "Collection item doesn't have AdapterView parent"); + Log.e(LOG_TAG, "Collection item doesn't have AdapterView parent"); return; } // Insure that a template pending intent has been set on an ancestor if (!(parent.getTag() instanceof PendingIntent)) { - Log.e("RemoteViews", "Attempting setOnClickFillInIntent without" + + Log.e(LOG_TAG, "Attempting setOnClickFillInIntent without" + " calling setPendingIntentTemplate on parent."); return; } @@ -472,7 +471,7 @@ public class RemoteViews implements Parcelable, Filter { av.setOnItemClickListener(listener); av.setTag(pendingIntentTemplate); } else { - Log.e("RemoteViews", "Cannot setPendingIntentTemplate on a view which is not" + + Log.e(LOG_TAG, "Cannot setPendingIntentTemplate on a view which is not" + "an AdapterView (id: " + viewId + ")"); return; } @@ -487,6 +486,88 @@ public class RemoteViews implements Parcelable, Filter { public final static int TAG = 8; } + private class SetRemoteViewsAdapterList extends Action { + public SetRemoteViewsAdapterList(int id, ArrayList<RemoteViews> list, int viewTypeCount) { + this.viewId = id; + this.list = list; + this.viewTypeCount = viewTypeCount; + } + + public SetRemoteViewsAdapterList(Parcel parcel) { + viewId = parcel.readInt(); + viewTypeCount = parcel.readInt(); + int count = parcel.readInt(); + list = new ArrayList<RemoteViews>(); + + for (int i = 0; i < count; i++) { + RemoteViews rv = RemoteViews.CREATOR.createFromParcel(parcel); + list.add(rv); + } + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(TAG); + dest.writeInt(viewId); + dest.writeInt(viewTypeCount); + + if (list == null || list.size() == 0) { + dest.writeInt(0); + } else { + int count = list.size(); + dest.writeInt(count); + for (int i = 0; i < count; i++) { + RemoteViews rv = list.get(i); + rv.writeToParcel(dest, flags); + } + } + } + + @Override + public void apply(View root, ViewGroup rootParent, OnClickHandler handler) { + final View target = root.findViewById(viewId); + if (target == null) return; + + // Ensure that we are applying to an AppWidget root + if (!(rootParent instanceof AppWidgetHostView)) { + Log.e(LOG_TAG, "SetRemoteViewsAdapterIntent action can only be used for " + + "AppWidgets (root id: " + viewId + ")"); + return; + } + // Ensure that we are calling setRemoteAdapter on an AdapterView that supports it + if (!(target instanceof AbsListView) && !(target instanceof AdapterViewAnimator)) { + Log.e(LOG_TAG, "Cannot setRemoteViewsAdapter on a view which is not " + + "an AbsListView or AdapterViewAnimator (id: " + viewId + ")"); + return; + } + + if (target instanceof AbsListView) { + AbsListView v = (AbsListView) target; + Adapter a = v.getAdapter(); + if (a instanceof RemoteViewsListAdapter && viewTypeCount <= a.getViewTypeCount()) { + ((RemoteViewsListAdapter) a).setViewsList(list); + } else { + v.setAdapter(new RemoteViewsListAdapter(v.getContext(), list, viewTypeCount)); + } + } else if (target instanceof AdapterViewAnimator) { + AdapterViewAnimator v = (AdapterViewAnimator) target; + Adapter a = v.getAdapter(); + if (a instanceof RemoteViewsListAdapter && viewTypeCount <= a.getViewTypeCount()) { + ((RemoteViewsListAdapter) a).setViewsList(list); + } else { + v.setAdapter(new RemoteViewsListAdapter(v.getContext(), list, viewTypeCount)); + } + } + } + + public String getActionName() { + return "SetRemoteViewsAdapterList"; + } + + int viewTypeCount; + ArrayList<RemoteViews> list; + public final static int TAG = 15; + } + private class SetRemoteViewsAdapterIntent extends Action { public SetRemoteViewsAdapterIntent(int id, Intent intent) { this.viewId = id; @@ -511,13 +592,13 @@ public class RemoteViews implements Parcelable, Filter { // Ensure that we are applying to an AppWidget root if (!(rootParent instanceof AppWidgetHostView)) { - Log.e("RemoteViews", "SetRemoteViewsAdapterIntent action can only be used for " + + Log.e(LOG_TAG, "SetRemoteViewsAdapterIntent action can only be used for " + "AppWidgets (root id: " + viewId + ")"); return; } // Ensure that we are calling setRemoteAdapter on an AdapterView that supports it if (!(target instanceof AbsListView) && !(target instanceof AdapterViewAnimator)) { - Log.e("RemoteViews", "Cannot setRemoteViewsAdapter on a view which is not " + + Log.e(LOG_TAG, "Cannot setRemoteViewsAdapter on a view which is not " + "an AbsListView or AdapterViewAnimator (id: " + viewId + ")"); return; } @@ -585,7 +666,7 @@ public class RemoteViews implements Parcelable, Filter { // If the view is an AdapterView, setting a PendingIntent on click doesn't make much // sense, do they mean to set a PendingIntent template for the AdapterView's children? if (mIsWidgetCollectionChild) { - Log.w("RemoteViews", "Cannot setOnClickPendingIntent for collection item " + + Log.w(LOG_TAG, "Cannot setOnClickPendingIntent for collection item " + "(id: " + viewId + ")"); ApplicationInfo appInfo = root.getContext().getApplicationInfo(); @@ -772,7 +853,7 @@ public class RemoteViews implements Parcelable, Filter { try { //noinspection ConstantIfStatement if (false) { - Log.d("RemoteViews", "view: " + klass.getName() + " calling method: " + Log.d(LOG_TAG, "view: " + klass.getName() + " calling method: " + this.methodName + "()"); } method.invoke(view); @@ -945,7 +1026,7 @@ public class RemoteViews implements Parcelable, Filter { this.type = in.readInt(); //noinspection ConstantIfStatement if (false) { - Log.d("RemoteViews", "read viewId=0x" + Integer.toHexString(this.viewId) + Log.d(LOG_TAG, "read viewId=0x" + Integer.toHexString(this.viewId) + " methodName=" + this.methodName + " type=" + this.type); } @@ -1012,7 +1093,7 @@ public class RemoteViews implements Parcelable, Filter { out.writeInt(this.type); //noinspection ConstantIfStatement if (false) { - Log.d("RemoteViews", "write viewId=0x" + Integer.toHexString(this.viewId) + Log.d(LOG_TAG, "write viewId=0x" + Integer.toHexString(this.viewId) + " methodName=" + this.methodName + " type=" + this.type); } @@ -1139,7 +1220,7 @@ public class RemoteViews implements Parcelable, Filter { try { //noinspection ConstantIfStatement if (false) { - Log.d("RemoteViews", "view: " + klass.getName() + " calling method: " + Log.d(LOG_TAG, "view: " + klass.getName() + " calling method: " + this.methodName + "(" + param.getName() + ") with " + (this.value == null ? "null" : this.value.getClass().getName())); } @@ -1562,6 +1643,9 @@ public class RemoteViews implements Parcelable, Filter { case BitmapReflectionAction.TAG: mActions.add(new BitmapReflectionAction(parcel)); break; + case SetRemoteViewsAdapterList.TAG: + mActions.add(new SetRemoteViewsAdapterList(parcel)); + break; default: throw new ActionException("Tag " + tag + " not found"); } @@ -1982,7 +2066,7 @@ public class RemoteViews implements Parcelable, Filter { * * @param appWidgetId The id of the app widget which contains the specified view. (This * parameter is ignored in this deprecated method) - * @param viewId The id of the {@link AbsListView} + * @param viewId The id of the {@link AdapterView} * @param intent The intent of the service which will be * providing data to the RemoteViewsAdapter * @deprecated This method has been deprecated. See @@ -1997,7 +2081,7 @@ public class RemoteViews implements Parcelable, Filter { * Equivalent to calling {@link android.widget.AbsListView#setRemoteViewsAdapter(Intent)}. * Can only be used for App Widgets. * - * @param viewId The id of the {@link AbsListView} + * @param viewId The id of the {@link AdapterView} * @param intent The intent of the service which will be * providing data to the RemoteViewsAdapter */ @@ -2006,6 +2090,29 @@ public class RemoteViews implements Parcelable, Filter { } /** + * Creates a simple Adapter for the viewId specified. The viewId must point to an AdapterView, + * ie. {@link ListView}, {@link GridView}, {@link StackView} or {@link AdapterViewAnimator}. + * This is a simpler but less flexible approach to populating collection widgets. Its use is + * encouraged for most scenarios, as long as the total memory within the list of RemoteViews + * is relatively small (ie. doesn't contain large or numerous Bitmaps, see {@link + * RemoteViews#setImageViewBitmap}). In the case of numerous images, the use of API is still + * possible by setting image URIs instead of Bitmaps, see {@link RemoteViews#setImageViewUri}. + * + * This API is supported in the compatibility library for previous API levels, see + * RemoteViewsCompat. + * + * @param viewId The id of the {@link AdapterView} + * @param list The list of RemoteViews which will populate the view specified by viewId. + * @param viewTypeCount The maximum number of unique layout id's used to construct the list of + * RemoteViews. This count cannot change during the life-cycle of a given widget, so this + * parameter should account for the maximum possible number of types that may appear in the + * See {@link Adapter#getViewTypeCount()}. + */ + public void setRemoteAdapter(int viewId, ArrayList<RemoteViews> list, int viewTypeCount) { + addAction(new SetRemoteViewsAdapterList(viewId, list, viewTypeCount)); + } + + /** * Equivalent to calling {@link android.widget.AbsListView#smoothScrollToPosition(int, int)}. * * @param viewId The id of the view to change diff --git a/core/java/android/widget/RemoteViewsListAdapter.java b/core/java/android/widget/RemoteViewsListAdapter.java new file mode 100644 index 0000000..e490458 --- /dev/null +++ b/core/java/android/widget/RemoteViewsListAdapter.java @@ -0,0 +1,121 @@ +/* + * 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.widget; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; + +import java.util.ArrayList; + +/** + * @hide + */ +public class RemoteViewsListAdapter extends BaseAdapter { + + private Context mContext; + private ArrayList<RemoteViews> mRemoteViewsList; + private ArrayList<Integer> mViewTypes = new ArrayList<Integer>(); + private int mViewTypeCount; + + public RemoteViewsListAdapter(Context context, ArrayList<RemoteViews> remoteViews, + int viewTypeCount) { + mContext = context; + mRemoteViewsList = remoteViews; + mViewTypeCount = viewTypeCount; + init(); + } + + public void setViewsList(ArrayList<RemoteViews> remoteViews) { + mRemoteViewsList = remoteViews; + init(); + notifyDataSetChanged(); + } + + private void init() { + if (mRemoteViewsList == null) return; + + mViewTypes.clear(); + for (RemoteViews rv: mRemoteViewsList) { + if (!mViewTypes.contains(rv.getLayoutId())) { + mViewTypes.add(rv.getLayoutId()); + } + } + + if (mViewTypes.size() > mViewTypeCount || mViewTypeCount < 1) { + throw new RuntimeException("Invalid view type count -- view type count must be >= 1" + + "and must be as large as the total number of distinct view types"); + } + } + + @Override + public int getCount() { + if (mRemoteViewsList != null) { + return mRemoteViewsList.size(); + } else { + return 0; + } + } + + @Override + public Object getItem(int position) { + return null; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (position < getCount()) { + RemoteViews rv = mRemoteViewsList.get(position); + rv.setIsWidgetCollectionChild(true); + View v; + if (convertView != null && rv != null && + convertView.getId() == rv.getLayoutId()) { + v = convertView; + rv.reapply(mContext, v); + } else { + v = rv.apply(mContext, parent); + } + return v; + } else { + return null; + } + } + + @Override + public int getItemViewType(int position) { + if (position < getCount()) { + int layoutId = mRemoteViewsList.get(position).getLayoutId(); + return mViewTypes.indexOf(layoutId); + } else { + return 0; + } + } + + public int getViewTypeCount() { + return mViewTypeCount; + } + + @Override + public boolean hasStableIds() { + return false; + } +} diff --git a/core/java/android/widget/SearchView.java b/core/java/android/widget/SearchView.java index cd8638d..0281602 100644 --- a/core/java/android/widget/SearchView.java +++ b/core/java/android/widget/SearchView.java @@ -1247,6 +1247,7 @@ public class SearchView extends LinearLayout implements CollapsibleActionView { */ @Override public void onActionViewCollapsed() { + setQuery("", false); clearFocus(); updateViewsVisibility(true); mQueryTextView.setImeOptions(mCollapsedImeOptions); diff --git a/core/java/android/widget/SeekBar.java b/core/java/android/widget/SeekBar.java index 2737f94..a6486a8 100644 --- a/core/java/android/widget/SeekBar.java +++ b/core/java/android/widget/SeekBar.java @@ -18,6 +18,7 @@ package android.widget; import android.content.Context; import android.util.AttributeSet; +import android.util.ValueModel; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; @@ -33,7 +34,7 @@ import android.view.accessibility.AccessibilityNodeInfo; * * @attr ref android.R.styleable#SeekBar_thumb */ -public class SeekBar extends AbsSeekBar { +public class SeekBar extends AbsSeekBar implements ValueEditor<Integer> { /** * A callback that notifies clients when the progress level has been @@ -69,8 +70,9 @@ public class SeekBar extends AbsSeekBar { void onStopTrackingTouch(SeekBar seekBar); } + private ValueModel<Integer> mValueModel = ValueModel.EMPTY; private OnSeekBarChangeListener mOnSeekBarChangeListener; - + public SeekBar(Context context) { this(context, null); } @@ -89,9 +91,23 @@ public class SeekBar extends AbsSeekBar { if (mOnSeekBarChangeListener != null) { mOnSeekBarChangeListener.onProgressChanged(this, getProgress(), fromUser); + if (fromUser) { + mValueModel.set(getProgress()); + } } } + @Override + public ValueModel<Integer> getValueModel() { + return mValueModel; + } + + @Override + public void setValueModel(ValueModel<Integer> valueModel) { + mValueModel = valueModel; + setProgress(mValueModel.get()); + } + /** * Sets a listener to receive notifications of changes to the SeekBar's progress level. Also * provides notifications of when the user starts and stops a touch gesture within the SeekBar. diff --git a/core/java/android/widget/SpellChecker.java b/core/java/android/widget/SpellChecker.java index 74ea038..9e7f97e 100644 --- a/core/java/android/widget/SpellChecker.java +++ b/core/java/android/widget/SpellChecker.java @@ -109,7 +109,7 @@ public class SpellChecker implements SpellCheckerSessionListener { mIds = new int[size]; mSpellCheckSpans = new SpellCheckSpan[size]; - setLocale(mTextView.getTextServicesLocale()); + setLocale(mTextView.getSpellCheckerLocale()); mCookie = hashCode(); } @@ -120,7 +120,8 @@ public class SpellChecker implements SpellCheckerSessionListener { mTextServicesManager = (TextServicesManager) mTextView.getContext(). getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE); if (!mTextServicesManager.isSpellCheckerEnabled() - || mTextServicesManager.getCurrentSpellCheckerSubtype(true) == null) { + || mCurrentLocale == null + || mTextServicesManager.getCurrentSpellCheckerSubtype(true) == null) { mSpellCheckerSession = null; } else { mSpellCheckerSession = mTextServicesManager.newSpellCheckerSession( @@ -146,8 +147,10 @@ public class SpellChecker implements SpellCheckerSessionListener { resetSession(); - // Change SpellParsers' wordIterator locale - mWordIterator = new WordIterator(locale); + if (locale != null) { + // Change SpellParsers' wordIterator locale + mWordIterator = new WordIterator(locale); + } // This class is the listener for locale change: warn other locale-aware objects mTextView.onLocaleChanged(); @@ -222,9 +225,9 @@ public class SpellChecker implements SpellCheckerSessionListener { if (DBG) { Log.d(TAG, "Start spell-checking: " + start + ", " + end); } - final Locale locale = mTextView.getTextServicesLocale(); + final Locale locale = mTextView.getSpellCheckerLocale(); final boolean isSessionActive = isSessionActive(); - if (mCurrentLocale == null || (!(mCurrentLocale.equals(locale)))) { + if (locale == null || mCurrentLocale == null || (!(mCurrentLocale.equals(locale)))) { setLocale(locale); // Re-check the entire text start = 0; diff --git a/core/java/android/widget/Spinner.java b/core/java/android/widget/Spinner.java index 925864c..fa64fd3 100644 --- a/core/java/android/widget/Spinner.java +++ b/core/java/android/widget/Spinner.java @@ -25,6 +25,8 @@ import android.content.res.TypedArray; import android.database.DataSetObserver; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.os.Parcel; +import android.os.Parcelable; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; @@ -697,6 +699,69 @@ public class Spinner extends AbsSpinner implements OnClickListener { return width; } + @Override + public Parcelable onSaveInstanceState() { + final SavedState ss = new SavedState(super.onSaveInstanceState()); + ss.showDropdown = mPopup != null && mPopup.isShowing(); + return ss; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + + super.onRestoreInstanceState(ss.getSuperState()); + + if (ss.showDropdown) { + ViewTreeObserver vto = getViewTreeObserver(); + if (vto != null) { + final OnGlobalLayoutListener listener = new OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + if (!mPopup.isShowing()) { + mPopup.show(); + } + final ViewTreeObserver vto = getViewTreeObserver(); + if (vto != null) { + vto.removeOnGlobalLayoutListener(this); + } + } + }; + vto.addOnGlobalLayoutListener(listener); + } + } + } + + static class SavedState extends AbsSpinner.SavedState { + boolean showDropdown; + + SavedState(Parcelable superState) { + super(superState); + } + + private SavedState(Parcel in) { + super(in); + showDropdown = in.readByte() != 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeByte((byte) (showDropdown ? 1 : 0)); + } + + public static final Parcelable.Creator<SavedState> CREATOR = + new Parcelable.Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + /** * <p>Wrapper class for an Adapter. Transforms the embedded Adapter instance * into a ListAdapter.</p> @@ -941,8 +1006,7 @@ public class Spinner extends AbsSpinner implements OnClickListener { mHintText = hintText; } - @Override - public void show() { + void computeContentWidth() { final Drawable background = getBackground(); int hOffset = 0; if (background != null) { @@ -955,6 +1019,7 @@ public class Spinner extends AbsSpinner implements OnClickListener { final int spinnerPaddingLeft = Spinner.this.getPaddingLeft(); final int spinnerPaddingRight = Spinner.this.getPaddingRight(); final int spinnerWidth = Spinner.this.getWidth(); + if (mDropDownWidth == WRAP_CONTENT) { int contentWidth = measureContentWidth( (SpinnerAdapter) mAdapter, getBackground()); @@ -977,11 +1042,25 @@ public class Spinner extends AbsSpinner implements OnClickListener { hOffset += spinnerPaddingLeft; } setHorizontalOffset(hOffset); + } + + @Override + public void show() { + final boolean wasShowing = isShowing(); + + computeContentWidth(); + setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); super.show(); getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE); setSelection(Spinner.this.getSelectedItemPosition()); + if (wasShowing) { + // Skip setting up the layout/dismiss listener below. If we were previously + // showing it will still stick around. + return; + } + // Make sure we hide if our anchor goes away. // TODO: This might be appropriate to push all the way down to PopupWindow, // but it may have other side effects to investigate first. (Text editing handles, etc.) @@ -992,6 +1071,12 @@ public class Spinner extends AbsSpinner implements OnClickListener { public void onGlobalLayout() { if (!Spinner.this.isVisibleToUser()) { dismiss(); + } else { + computeContentWidth(); + + // Use super.show here to update; we don't want to move the selected + // position or adjust other things that would be reset otherwise. + DropdownPopup.super.show(); } } }; diff --git a/core/java/android/widget/TableLayout.java b/core/java/android/widget/TableLayout.java index 399b4fa..f4b2ce0 100644 --- a/core/java/android/widget/TableLayout.java +++ b/core/java/android/widget/TableLayout.java @@ -445,7 +445,7 @@ public class TableLayout extends LinearLayout { @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // enforce vertical layout - layoutVertical(); + layoutVertical(l, t, r, b); } /** diff --git a/core/java/android/widget/TableRow.java b/core/java/android/widget/TableRow.java index 35927e0..fe3631a 100644 --- a/core/java/android/widget/TableRow.java +++ b/core/java/android/widget/TableRow.java @@ -120,7 +120,7 @@ public class TableRow extends LinearLayout { @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // enforce horizontal layout - layoutHorizontal(); + layoutHorizontal(l, t, r, b); } /** diff --git a/core/java/android/widget/TextClock.java b/core/java/android/widget/TextClock.java index 290d9b5..5bf21c0 100644 --- a/core/java/android/widget/TextClock.java +++ b/core/java/android/widget/TextClock.java @@ -386,6 +386,16 @@ public class TextClock extends TextView { } /** + * Returns the current format string. Always valid after constructor has + * finished, and will never be {@code null}. + * + * @hide + */ + public CharSequence getFormat() { + return mFormat; + } + + /** * Selects either one of {@link #getFormat12Hour()} or {@link #getFormat24Hour()} * depending on whether the user has selected 24-hour format. * diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 22bfadb..a1ced6e 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -26,6 +26,7 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.graphics.Canvas; +import android.graphics.Insets; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; @@ -510,7 +511,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private InputFilter[] mFilters = NO_FILTERS; - private volatile Locale mCurrentTextServicesLocaleCache; + private volatile Locale mCurrentSpellCheckerLocaleCache; private final ReentrantLock mCurrentTextServicesLocaleLock = new ReentrantLock(); // It is possible to have a selection even when mEditor is null (programmatically set, like when @@ -606,6 +607,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int typefaceIndex = -1; int styleIndex = -1; boolean allCaps = false; + int shadowcolor = 0; + float dx = 0, dy = 0, r = 0; final Resources.Theme theme = context.getTheme(); @@ -666,6 +669,22 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener case com.android.internal.R.styleable.TextAppearance_textAllCaps: allCaps = appearance.getBoolean(attr, false); break; + + case com.android.internal.R.styleable.TextAppearance_shadowColor: + shadowcolor = a.getInt(attr, 0); + break; + + case com.android.internal.R.styleable.TextAppearance_shadowDx: + dx = a.getFloat(attr, 0); + break; + + case com.android.internal.R.styleable.TextAppearance_shadowDy: + dy = a.getFloat(attr, 0); + break; + + case com.android.internal.R.styleable.TextAppearance_shadowRadius: + r = a.getFloat(attr, 0); + break; } } @@ -689,8 +708,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int maxlength = -1; CharSequence text = ""; CharSequence hint = null; - int shadowcolor = 0; - float dx = 0, dy = 0, r = 0; boolean password = false; int inputType = EditorInfo.TYPE_NULL; @@ -2330,6 +2347,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setTypefaceFromAttrs(familyName, typefaceIndex, styleIndex); + final int shadowcolor = appearance.getInt( + com.android.internal.R.styleable.TextAppearance_shadowColor, 0); + if (shadowcolor != 0) { + final float dx = appearance.getFloat( + com.android.internal.R.styleable.TextAppearance_shadowDx, 0); + final float dy = appearance.getFloat( + com.android.internal.R.styleable.TextAppearance_shadowDy, 0); + final float r = appearance.getFloat( + com.android.internal.R.styleable.TextAppearance_shadowRadius, 0); + + setShadowLayer(r, dx, dy, shadowcolor); + } + if (appearance.getBoolean(com.android.internal.R.styleable.TextAppearance_textAllCaps, false)) { setTransformationMethod(new AllCapsTransformationMethod(getContext())); @@ -4349,6 +4379,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener ///////////////////////////////////////////////////////////////////////// + private int getBoxHeight(Layout l) { + Insets opticalInsets = isLayoutModeOptical(mParent) ? getOpticalInsets() : Insets.NONE; + int padding = (l == mHintLayout) ? + getCompoundPaddingTop() + getCompoundPaddingBottom() : + getExtendedPaddingTop() + getExtendedPaddingBottom(); + return getMeasuredHeight() - padding + opticalInsets.top + opticalInsets.bottom; + } + int getVerticalOffset(boolean forceNormal) { int voffset = 0; final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; @@ -4359,15 +4397,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (gravity != Gravity.TOP) { - int boxht; - - if (l == mHintLayout) { - boxht = getMeasuredHeight() - getCompoundPaddingTop() - - getCompoundPaddingBottom(); - } else { - boxht = getMeasuredHeight() - getExtendedPaddingTop() - - getExtendedPaddingBottom(); - } + int boxht = getBoxHeight(l); int textht = l.getHeight(); if (textht < boxht) { @@ -4390,15 +4420,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (gravity != Gravity.BOTTOM) { - int boxht; - - if (l == mHintLayout) { - boxht = getMeasuredHeight() - getCompoundPaddingTop() - - getCompoundPaddingBottom(); - } else { - boxht = getMeasuredHeight() - getExtendedPaddingTop() - - getExtendedPaddingBottom(); - } + int boxht = getBoxHeight(l); int textht = l.getHeight(); if (textht < boxht) { @@ -5144,6 +5166,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener voffset = getVerticalOffset(true); } + if (isLayoutModeOptical(mParent)) { + voffset -= getOpticalInsets().top; + } + return getExtendedPaddingTop() + voffset + mLayout.getLineBaseline(0); } @@ -6462,7 +6488,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mDeferScroll = -1; bringPointIntoView(Math.min(curs, mText.length())); } - if (changed && mEditor != null) mEditor.invalidateTextDisplayList(); } private boolean isShowingHint() { @@ -6556,15 +6581,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int line = layout.getLineForOffset(offset); - // FIXME: Is it okay to truncate this, or should we round? - final int x = (int)layout.getPrimaryHorizontal(offset); - final int top = layout.getLineTop(line); - final int bottom = layout.getLineTop(line + 1); - - int left = (int) FloatMath.floor(layout.getLineLeft(line)); - int right = (int) FloatMath.ceil(layout.getLineRight(line)); - int ht = layout.getHeight(); - int grav; switch (layout.getParagraphAlignment(line)) { @@ -6586,8 +6602,32 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener break; } + // We only want to clamp the cursor to fit within the layout width + // in left-to-right modes, because in a right to left alignment, + // we want to scroll to keep the line-right on the screen, as other + // lines are likely to have text flush with the right margin, which + // we want to keep visible. + // A better long-term solution would probably be to measure both + // the full line and a blank-trimmed version, and, for example, use + // the latter measurement for centering and right alignment, but for + // the time being we only implement the cursor clamping in left to + // right where it is most likely to be annoying. + final boolean clamped = grav > 0; + // FIXME: Is it okay to truncate this, or should we round? + final int x = (int)layout.getPrimaryHorizontal(offset, clamped); + final int top = layout.getLineTop(line); + final int bottom = layout.getLineTop(line + 1); + + int left = (int) FloatMath.floor(layout.getLineLeft(line)); + int right = (int) FloatMath.ceil(layout.getLineRight(line)); + int ht = layout.getHeight(); + int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(); int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom(); + if (!mHorizontallyScrolling && right - left > hspace && right > x) { + // If cursor has been clamped, make sure we don't scroll. + right = Math.max(x, left + hspace); + } int hslack = (bottom - top) / 2; int vslack = hslack; @@ -7154,6 +7194,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ protected void onSelectionChanged(int selStart, int selEnd) { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED); + notifyAccessibilityStateChanged(); } /** @@ -7730,7 +7771,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** * Returns the TextView_textColor attribute from the - * Resources.StyledAttributes, if set, or the TextAppearance_textColor + * TypedArray, if set, or the TextAppearance_textColor * from the TextView_textAppearance attribute, if TextView_textColor * was not set directly. */ @@ -7828,27 +7869,46 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener (isTextSelectable() && mText instanceof Spannable && isEnabled()); } + private Locale getTextServicesLocale(boolean allowNullLocale) { + // Start fetching the text services locale asynchronously. + updateTextServicesLocaleAsync(); + // If !allowNullLocale and there is no cached text services locale, just return the default + // locale. + return (mCurrentSpellCheckerLocaleCache == null && !allowNullLocale) ? Locale.getDefault() + : mCurrentSpellCheckerLocaleCache; + } + /** * This is a temporary method. Future versions may support multi-locale text. * Caveat: This method may not return the latest text services locale, but this should be * acceptable and it's more important to make this method asynchronous. * - * @return The locale that should be used for a word iterator and a spell checker + * @return The locale that should be used for a word iterator * in this TextView, based on the current spell checker settings, * the current IME's locale, or the system default locale. + * Please note that a word iterator in this TextView is different from another word iterator + * used by SpellChecker.java of TextView. This method should be used for the former. * @hide */ // TODO: Support multi-locale // TODO: Update the text services locale immediately after the keyboard locale is switched // by catching intent of keyboard switch event public Locale getTextServicesLocale() { - if (mCurrentTextServicesLocaleCache == null) { - // If there is no cached text services locale, just return the default locale. - mCurrentTextServicesLocaleCache = Locale.getDefault(); - } - // Start fetching the text services locale asynchronously. - updateTextServicesLocaleAsync(); - return mCurrentTextServicesLocaleCache; + return getTextServicesLocale(false /* allowNullLocale */); + } + + /** + * This is a temporary method. Future versions may support multi-locale text. + * Caveat: This method may not return the latest spell checker locale, but this should be + * acceptable and it's more important to make this method asynchronous. + * + * @return The locale that should be used for a spell checker in this TextView, + * based on the current spell checker settings, the current IME's locale, or the system default + * locale. + * @hide + */ + public Locale getSpellCheckerLocale() { + return getTextServicesLocale(true /* allowNullLocale */); } private void updateTextServicesLocaleAsync() { @@ -7867,14 +7927,16 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } private void updateTextServicesLocaleLocked() { - Locale locale = Locale.getDefault(); final TextServicesManager textServicesManager = (TextServicesManager) mContext.getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE); final SpellCheckerSubtype subtype = textServicesManager.getCurrentSpellCheckerSubtype(true); + final Locale locale; if (subtype != null) { locale = SpellCheckerSubtype.constructLocaleFromString(subtype.getLocale()); + } else { + locale = null; } - mCurrentTextServicesLocaleCache = locale; + mCurrentSpellCheckerLocaleCache = locale; } void onLocaleChanged() { @@ -7944,6 +8006,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener info.setText(getTextForAccessibility()); } + if (mBufferType == BufferType.EDITABLE) { + info.setEditable(true); + } + if (TextUtils.isEmpty(getContentDescription()) && !TextUtils.isEmpty(mText)) { info.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY); info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY); @@ -7953,6 +8019,82 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PAGE); } + if (isFocused()) { + if (canSelectText()) { + info.addAction(AccessibilityNodeInfo.ACTION_SET_SELECTION); + } + if (canCopy()) { + info.addAction(AccessibilityNodeInfo.ACTION_COPY); + } + if (canPaste()) { + info.addAction(AccessibilityNodeInfo.ACTION_PASTE); + } + if (canCut()) { + info.addAction(AccessibilityNodeInfo.ACTION_CUT); + } + } + } + + @Override + public boolean performAccessibilityAction(int action, Bundle arguments) { + switch (action) { + case AccessibilityNodeInfo.ACTION_COPY: { + if (isFocused() && canCopy()) { + if (onTextContextMenuItem(ID_COPY)) { + notifyAccessibilityStateChanged(); + return true; + } + } + } return false; + case AccessibilityNodeInfo.ACTION_PASTE: { + if (isFocused() && canPaste()) { + if (onTextContextMenuItem(ID_PASTE)) { + notifyAccessibilityStateChanged(); + return true; + } + } + } return false; + case AccessibilityNodeInfo.ACTION_CUT: { + if (isFocused() && canCut()) { + if (onTextContextMenuItem(ID_CUT)) { + notifyAccessibilityStateChanged(); + return true; + } + } + } return false; + case AccessibilityNodeInfo.ACTION_SET_SELECTION: { + if (isFocused() && canSelectText()) { + CharSequence text = getIterableTextForAccessibility(); + if (text == null) { + return false; + } + final int start = (arguments != null) ? arguments.getInt( + AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT, -1) : -1; + final int end = (arguments != null) ? arguments.getInt( + AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT, -1) : -1; + if ((getSelectionStart() != start || getSelectionEnd() != end)) { + // No arguments clears the selection. + if (start == end && end == -1) { + Selection.removeSelection((Spannable) text); + notifyAccessibilityStateChanged(); + return true; + } + if (start >= 0 && start <= end && end <= text.length()) { + Selection.setSelection((Spannable) text, start, end); + // Make sure selection mode is engaged. + if (mEditor != null) { + mEditor.startSelectionActionMode(); + } + notifyAccessibilityStateChanged(); + return true; + } + } + } + } return false; + default: { + return super.performAccessibilityAction(action, arguments); + } + } } @Override @@ -8522,32 +8664,51 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @hide */ @Override - public int getAccessibilityCursorPosition() { + public int getAccessibilitySelectionStart() { + if (TextUtils.isEmpty(getContentDescription())) { + final int selectionStart = getSelectionStart(); + if (selectionStart >= 0) { + return selectionStart; + } + } + return ACCESSIBILITY_CURSOR_POSITION_UNDEFINED; + } + + /** + * @hide + */ + public boolean isAccessibilitySelectionExtendable() { + return true; + } + + /** + * @hide + */ + @Override + public int getAccessibilitySelectionEnd() { if (TextUtils.isEmpty(getContentDescription())) { final int selectionEnd = getSelectionEnd(); if (selectionEnd >= 0) { return selectionEnd; } } - return super.getAccessibilityCursorPosition(); + return ACCESSIBILITY_CURSOR_POSITION_UNDEFINED; } /** * @hide */ @Override - public void setAccessibilityCursorPosition(int index) { - if (getAccessibilityCursorPosition() == index) { + public void setAccessibilitySelection(int start, int end) { + if (getAccessibilitySelectionStart() == start + && getAccessibilitySelectionEnd() == end) { return; } - if (TextUtils.isEmpty(getContentDescription())) { - if (index >= 0 && index <= mText.length()) { - Selection.setSelection((Spannable) mText, index); - } else { - Selection.removeSelection((Spannable) mText); - } + CharSequence text = getIterableTextForAccessibility(); + if (start >= 0 && start <= end && end <= text.length()) { + Selection.setSelection((Spannable) text, start, end); } else { - super.setAccessibilityCursorPosition(index); + Selection.removeSelection((Spannable) text); } } @@ -8669,11 +8830,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } public void drawTextRun(Canvas c, int start, int end, - int contextStart, int contextEnd, float x, float y, int flags, Paint p) { + int contextStart, int contextEnd, float x, float y, Paint p) { int count = end - start; int contextCount = contextEnd - contextStart; c.drawTextRun(mChars, start + mStart, count, contextStart + mStart, - contextCount, x, y, flags, p); + contextCount, x, y, p); } public float measureText(int start, int end, Paint p) { @@ -8685,30 +8846,20 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } public float getTextRunAdvances(int start, int end, int contextStart, - int contextEnd, int flags, float[] advances, int advancesIndex, + int contextEnd, float[] advances, int advancesIndex, Paint p) { int count = end - start; int contextCount = contextEnd - contextStart; return p.getTextRunAdvances(mChars, start + mStart, count, - contextStart + mStart, contextCount, flags, advances, + contextStart + mStart, contextCount, advances, advancesIndex); } - public float getTextRunAdvances(int start, int end, int contextStart, - int contextEnd, int flags, float[] advances, int advancesIndex, - Paint p, int reserved) { - int count = end - start; - int contextCount = contextEnd - contextStart; - return p.getTextRunAdvances(mChars, start + mStart, count, - contextStart + mStart, contextCount, flags, advances, - advancesIndex, reserved); - } - - public int getTextRunCursor(int contextStart, int contextEnd, int flags, + public int getTextRunCursor(int contextStart, int contextEnd, int offset, int cursorOpt, Paint p) { int contextCount = contextEnd - contextStart; return p.getTextRunCursor(mChars, contextStart + mStart, - contextCount, flags, offset + mStart, cursorOpt); + contextCount, offset + mStart, cursorOpt); } } diff --git a/core/java/android/widget/ValueEditor.java b/core/java/android/widget/ValueEditor.java new file mode 100755 index 0000000..2b91abf --- /dev/null +++ b/core/java/android/widget/ValueEditor.java @@ -0,0 +1,53 @@ +/* + * 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.widget; + +import android.util.ValueModel; + +/** + * An interface for editors of simple values. Classes implementing this interface are normally + * UI controls (subclasses of {@link android.view.View View}) that can provide a suitable + * user interface to display and edit values of the specified type. This interface is + * intended to describe editors for simple types, like {@code boolean}, {@code int} or + * {@code String}, where the values themselves are immutable. + * <p> + * For example, {@link android.widget.CheckBox CheckBox} implements + * this interface for the Boolean type as it is capable of providing an appropriate + * mechanism for displaying and changing the value of a Boolean property. + * + * @param <T> the value type that this editor supports + */ +public interface ValueEditor<T> { + /** + * Return the last value model that was set. If no value model has been set, the editor + * should return the value {@link android.util.ValueModel#EMPTY}. + * + * @return the value model + */ + public ValueModel<T> getValueModel(); + + /** + * Sets the value model for this editor. When the value model is set, the editor should + * retrieve the value from the value model, using {@link android.util.ValueModel#get()}, + * and set its internal state accordingly. Likewise, when the editor's internal state changes + * it should update the value model by calling {@link android.util.ValueModel#set(T)} + * with the appropriate value. + * + * @param valueModel the new value model for this editor. + */ + public void setValueModel(ValueModel<T> valueModel); +} diff --git a/core/java/android/widget/VideoView.java b/core/java/android/widget/VideoView.java index 329b0df..16b6a76 100644 --- a/core/java/android/widget/VideoView.java +++ b/core/java/android/widget/VideoView.java @@ -35,6 +35,7 @@ import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; +import android.view.View.MeasureSpec; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.MediaController.MediaPlayerControl; @@ -107,23 +108,65 @@ public class VideoView extends SurfaceView implements MediaPlayerControl { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - //Log.i("@@@@", "onMeasure"); + //Log.i("@@@@", "onMeasure(" + MeasureSpec.toString(widthMeasureSpec) + ", " + // + MeasureSpec.toString(heightMeasureSpec) + ")"); + int width = getDefaultSize(mVideoWidth, widthMeasureSpec); int height = getDefaultSize(mVideoHeight, heightMeasureSpec); if (mVideoWidth > 0 && mVideoHeight > 0) { - if ( mVideoWidth * height > width * mVideoHeight ) { - //Log.i("@@@", "image too tall, correcting"); + + int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); + + if (widthSpecMode == MeasureSpec.EXACTLY && heightSpecMode == MeasureSpec.EXACTLY) { + // the size is fixed + width = widthSpecSize; + height = heightSpecSize; + + // for compatibility, we adjust size based on aspect ratio + if ( mVideoWidth * height < width * mVideoHeight ) { + //Log.i("@@@", "image too wide, correcting"); + width = height * mVideoWidth / mVideoHeight; + } else if ( mVideoWidth * height > width * mVideoHeight ) { + //Log.i("@@@", "image too tall, correcting"); + height = width * mVideoHeight / mVideoWidth; + } + } else if (widthSpecMode == MeasureSpec.EXACTLY) { + // only the width is fixed, adjust the height to match aspect ratio if possible + width = widthSpecSize; height = width * mVideoHeight / mVideoWidth; - } else if ( mVideoWidth * height < width * mVideoHeight ) { - //Log.i("@@@", "image too wide, correcting"); + if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) { + // couldn't match aspect ratio within the constraints + height = heightSpecSize; + } + } else if (heightSpecMode == MeasureSpec.EXACTLY) { + // only the height is fixed, adjust the width to match aspect ratio if possible + height = heightSpecSize; width = height * mVideoWidth / mVideoHeight; + if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) { + // couldn't match aspect ratio within the constraints + width = widthSpecSize; + } } else { - //Log.i("@@@", "aspect ratio is correct: " + - //width+"/"+height+"="+ - //mVideoWidth+"/"+mVideoHeight); + // neither the width nor the height are fixed, try to use actual video size + width = mVideoWidth; + height = mVideoHeight; + if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) { + // too tall, decrease both width and height + height = heightSpecSize; + width = height * mVideoWidth / mVideoHeight; + } + if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) { + // too wide, decrease both width and height + width = widthSpecSize; + height = width * mVideoHeight / mVideoWidth; + } } + } else { + // no size yet, just adopt the given spec sizes } - //Log.i("@@@@@@@@@@", "setting size: " + width + 'x' + height); setMeasuredDimension(width, height); } @@ -140,33 +183,8 @@ public class VideoView extends SurfaceView implements MediaPlayerControl { } public int resolveAdjustedSize(int desiredSize, int measureSpec) { - int result = desiredSize; - int specMode = MeasureSpec.getMode(measureSpec); - int specSize = MeasureSpec.getSize(measureSpec); - - switch (specMode) { - case MeasureSpec.UNSPECIFIED: - /* Parent says we can be as big as we want. Just don't be larger - * than max size imposed on ourselves. - */ - result = desiredSize; - break; - - case MeasureSpec.AT_MOST: - /* Parent says we can be as big as we want, up to specSize. - * Don't be larger than specSize, and don't be larger than - * the max size imposed on ourselves. - */ - result = Math.min(desiredSize, specSize); - break; - - case MeasureSpec.EXACTLY: - // No choice. Do what we are told. - result = specSize; - break; - } - return result; -} + return getDefaultSize(desiredSize, measureSpec); + } private void initVideoView() { mVideoWidth = 0; diff --git a/core/java/android/util/Poolable.java b/core/java/com/android/internal/app/IAppOpsCallback.aidl index 87e0529..afbc609 100644 --- a/core/java/android/util/Poolable.java +++ b/core/java/com/android/internal/app/IAppOpsCallback.aidl @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009 The Android Open Source Project + * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,14 +14,10 @@ * limitations under the License. */ -package android.util; +package com.android.internal.app; -/** - * @hide - */ -public interface Poolable<T> { - void setNextPoolable(T element); - T getNextPoolable(); - boolean isPooled(); - void setPooled(boolean isPooled); +// This interface is also used by native code, so must +// be kept in sync with frameworks/native/include/binder/IAppOpsCallback.h +oneway interface IAppOpsCallback { + void opChanged(int op, String packageName); } diff --git a/core/java/com/android/internal/app/IAppOpsService.aidl b/core/java/com/android/internal/app/IAppOpsService.aidl new file mode 100644 index 0000000..a9da863 --- /dev/null +++ b/core/java/com/android/internal/app/IAppOpsService.aidl @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.app; + +import android.app.AppOpsManager; +import com.android.internal.app.IAppOpsCallback; + +interface IAppOpsService { + // These first methods are also called by native code, so must + // be kept in sync with frameworks/native/include/binder/IAppOpsService.h + int checkOperation(int code, int uid, String packageName); + int noteOperation(int code, int uid, String packageName); + int startOperation(int code, int uid, String packageName); + void finishOperation(int code, int uid, String packageName); + void startWatchingMode(int op, String packageName, IAppOpsCallback callback); + void stopWatchingMode(IAppOpsCallback callback); + + // Remaining methods are only used in Java. + List<AppOpsManager.PackageOps> getPackagesForOps(in int[] ops); + List<AppOpsManager.PackageOps> getOpsForPackage(int uid, String packageName, in int[] ops); + void setMode(int code, int uid, String packageName, int mode); +} diff --git a/core/java/com/android/internal/app/IBatteryStats.aidl b/core/java/com/android/internal/app/IBatteryStats.aidl index 1a76461f..823e19f 100644 --- a/core/java/com/android/internal/app/IBatteryStats.aidl +++ b/core/java/com/android/internal/app/IBatteryStats.aidl @@ -37,6 +37,8 @@ interface IBatteryStats { void noteStartWakelockFromSource(in WorkSource ws, int pid, String name, int type); void noteStopWakelockFromSource(in WorkSource ws, int pid, String name, int type); + void noteVibratorOn(int uid, long durationMillis); + void noteVibratorOff(int uid); void noteStartGps(int uid); void noteStopGps(int uid); void noteScreenOn(); diff --git a/core/java/com/android/internal/appwidget/IAppWidgetHost.aidl b/core/java/com/android/internal/appwidget/IAppWidgetHost.aidl index 78b4466..6d51d38 100644 --- a/core/java/com/android/internal/appwidget/IAppWidgetHost.aidl +++ b/core/java/com/android/internal/appwidget/IAppWidgetHost.aidl @@ -22,9 +22,9 @@ import android.widget.RemoteViews; /** {@hide} */ oneway interface IAppWidgetHost { - void updateAppWidget(int appWidgetId, in RemoteViews views); - void providerChanged(int appWidgetId, in AppWidgetProviderInfo info); - void providersChanged(); - void viewDataChanged(int appWidgetId, int viewId); + void updateAppWidget(int appWidgetId, in RemoteViews views, int userId); + void providerChanged(int appWidgetId, in AppWidgetProviderInfo info, int userId); + void providersChanged(int userId); + void viewDataChanged(int appWidgetId, int viewId, int userId); } diff --git a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl index e685e63..7ddd5d2 100644 --- a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl +++ b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl @@ -26,42 +26,39 @@ import android.widget.RemoteViews; /** {@hide} */ interface IAppWidgetService { - + // // for AppWidgetHost // int[] startListening(IAppWidgetHost host, String packageName, int hostId, - out List<RemoteViews> updatedViews); - int[] startListeningAsUser(IAppWidgetHost host, String packageName, int hostId, out List<RemoteViews> updatedViews, int userId); - void stopListening(int hostId); - void stopListeningAsUser(int hostId, int userId); - int allocateAppWidgetId(String packageName, int hostId); - void deleteAppWidgetId(int appWidgetId); - void deleteHost(int hostId); - void deleteAllHosts(); - RemoteViews getAppWidgetViews(int appWidgetId); - int[] getAppWidgetIdsForHost(int hostId); + void stopListening(int hostId, int userId); + int allocateAppWidgetId(String packageName, int hostId, int userId); + void deleteAppWidgetId(int appWidgetId, int userId); + void deleteHost(int hostId, int userId); + void deleteAllHosts(int userId); + RemoteViews getAppWidgetViews(int appWidgetId, int userId); + int[] getAppWidgetIdsForHost(int hostId, int userId); // // for AppWidgetManager // - void updateAppWidgetIds(in int[] appWidgetIds, in RemoteViews views); - void updateAppWidgetOptions(int appWidgetId, in Bundle extras); - Bundle getAppWidgetOptions(int appWidgetId); - void partiallyUpdateAppWidgetIds(in int[] appWidgetIds, in RemoteViews views); - void updateAppWidgetProvider(in ComponentName provider, in RemoteViews views); - void notifyAppWidgetViewDataChanged(in int[] appWidgetIds, int viewId); - List<AppWidgetProviderInfo> getInstalledProviders(int categoryFilter); - AppWidgetProviderInfo getAppWidgetInfo(int appWidgetId); - boolean hasBindAppWidgetPermission(in String packageName); - void setBindAppWidgetPermission(in String packageName, in boolean permission); - void bindAppWidgetId(int appWidgetId, in ComponentName provider, in Bundle options); - boolean bindAppWidgetIdIfAllowed( - in String packageName, int appWidgetId, in ComponentName provider, in Bundle options); + void updateAppWidgetIds(in int[] appWidgetIds, in RemoteViews views, int userId); + void updateAppWidgetOptions(int appWidgetId, in Bundle extras, int userId); + Bundle getAppWidgetOptions(int appWidgetId, int userId); + void partiallyUpdateAppWidgetIds(in int[] appWidgetIds, in RemoteViews views, int userId); + void updateAppWidgetProvider(in ComponentName provider, in RemoteViews views, int userId); + void notifyAppWidgetViewDataChanged(in int[] appWidgetIds, int viewId, int userId); + List<AppWidgetProviderInfo> getInstalledProviders(int categoryFilter, int userId); + AppWidgetProviderInfo getAppWidgetInfo(int appWidgetId, int userId); + boolean hasBindAppWidgetPermission(in String packageName, int userId); + void setBindAppWidgetPermission(in String packageName, in boolean permission, int userId); + void bindAppWidgetId(int appWidgetId, in ComponentName provider, in Bundle options, int userId); + boolean bindAppWidgetIdIfAllowed(in String packageName, int appWidgetId, + in ComponentName provider, in Bundle options, int userId); void bindRemoteViewsService(int appWidgetId, in Intent intent, in IBinder connection, int userId); void unbindRemoteViewsService(int appWidgetId, in Intent intent, int userId); - int[] getAppWidgetIds(in ComponentName provider); + int[] getAppWidgetIds(in ComponentName provider, int userId); } diff --git a/core/java/com/android/internal/content/PackageMonitor.java b/core/java/com/android/internal/content/PackageMonitor.java index 20ecace..424c19b 100644 --- a/core/java/com/android/internal/content/PackageMonitor.java +++ b/core/java/com/android/internal/content/PackageMonitor.java @@ -153,8 +153,33 @@ public abstract class PackageMonitor extends android.content.BroadcastReceiver { public void onPackageUpdateFinished(String packageName, int uid) { } - - public void onPackageChanged(String packageName, int uid, String[] components) { + + /** + * Direct reflection of {@link Intent#ACTION_PACKAGE_CHANGED + * Intent.ACTION_PACKAGE_CHANGED} being received, informing you of + * changes to the enabled/disabled state of components in a package + * and/or of the overall package. + * + * @param packageName The name of the package that is changing. + * @param uid The user ID the package runs under. + * @param components Any components in the package that are changing. If + * the overall package is changing, this will contain an entry of the + * package name itself. + * @return Return true to indicate you care about this change, which will + * result in {@link #onSomePackagesChanged()} being called later. If you + * return false, no further callbacks will happen about this change. The + * default implementation returns true if this is a change to the entire + * package. + */ + public boolean onPackageChanged(String packageName, int uid, String[] components) { + if (components != null) { + for (String name : components) { + if (packageName.equals(name)) { + return true; + } + } + } + return false; } public boolean onHandleForceStop(Intent intent, String[] packages, int uid, boolean doit) { @@ -189,7 +214,10 @@ public abstract class PackageMonitor extends android.content.BroadcastReceiver { */ public void onPackageAppeared(String packageName, int reason) { } - + + /** + * Called when an existing package is updated or its disabled state changes. + */ public void onPackageModified(String packageName) { } @@ -328,9 +356,10 @@ public abstract class PackageMonitor extends android.content.BroadcastReceiver { if (pkg != null) { mModifiedPackages = mTempArray; mTempArray[0] = pkg; - onPackageChanged(pkg, uid, components); - // XXX Don't want this to always cause mSomePackagesChanged, - // since it can happen a fair amount. + mChangeType = PACKAGE_PERMANENT_CHANGE; + if (onPackageChanged(pkg, uid, components)) { + mSomePackagesChanged = true; + } onPackageModified(pkg); } } else if (Intent.ACTION_QUERY_PACKAGE_RESTART.equals(action)) { diff --git a/core/java/com/android/internal/inputmethod/InputMethodUtils.java b/core/java/com/android/internal/inputmethod/InputMethodUtils.java new file mode 100644 index 0000000..655d148 --- /dev/null +++ b/core/java/com/android/internal/inputmethod/InputMethodUtils.java @@ -0,0 +1,839 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.inputmethod; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.provider.Settings; +import android.provider.Settings.SettingNotFoundException; +import android.text.TextUtils; +import android.util.Pair; +import android.util.Slog; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodSubtype; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; + +/** + * InputMethodManagerUtils contains some static methods that provides IME informations. + * This methods are supposed to be used in both the framework and the Settings application. + */ +public class InputMethodUtils { + public static final boolean DEBUG = false; + public static final int NOT_A_SUBTYPE_ID = -1; + public static final String SUBTYPE_MODE_KEYBOARD = "keyboard"; + public static final String SUBTYPE_MODE_VOICE = "voice"; + private static final String TAG = "InputMethodUtils"; + private static final Locale ENGLISH_LOCALE = new Locale("en"); + private static final String NOT_A_SUBTYPE_ID_STR = String.valueOf(NOT_A_SUBTYPE_ID); + private static final String TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE = + "EnabledWhenDefaultIsNotAsciiCapable"; + private static final String TAG_ASCII_CAPABLE = "AsciiCapable"; + + private InputMethodUtils() { + // This utility class is not publicly instantiable. + } + + public static boolean isSystemIme(InputMethodInfo inputMethod) { + return (inputMethod.getServiceInfo().applicationInfo.flags + & ApplicationInfo.FLAG_SYSTEM) != 0; + } + + public static boolean isSystemImeThatHasEnglishKeyboardSubtype(InputMethodInfo imi) { + if (!isSystemIme(imi)) { + return false; + } + return containsSubtypeOf(imi, ENGLISH_LOCALE.getLanguage(), SUBTYPE_MODE_KEYBOARD); + } + + private static boolean isSystemAuxilialyImeThatHashAutomaticSubtype(InputMethodInfo imi) { + if (!isSystemIme(imi)) { + return false; + } + if (!imi.isAuxiliaryIme()) { + return false; + } + final int subtypeCount = imi.getSubtypeCount(); + for (int i = 0; i < subtypeCount; ++i) { + final InputMethodSubtype s = imi.getSubtypeAt(i); + if (s.overridesImplicitlyEnabledSubtype()) { + return true; + } + } + return false; + } + + public static ArrayList<InputMethodInfo> getDefaultEnabledImes( + Context context, boolean isSystemReady, ArrayList<InputMethodInfo> imis) { + final ArrayList<InputMethodInfo> retval = new ArrayList<InputMethodInfo>(); + boolean auxilialyImeAdded = false; + for (int i = 0; i < imis.size(); ++i) { + final InputMethodInfo imi = imis.get(i); + if (isDefaultEnabledIme(isSystemReady, imi, context)) { + retval.add(imi); + if (imi.isAuxiliaryIme()) { + auxilialyImeAdded = true; + } + } + } + if (auxilialyImeAdded) { + return retval; + } + for (int i = 0; i < imis.size(); ++i) { + final InputMethodInfo imi = imis.get(i); + if (isSystemAuxilialyImeThatHashAutomaticSubtype(imi)) { + retval.add(imi); + } + } + return retval; + } + + // TODO: Rename isSystemDefaultImeThatHasCurrentLanguageSubtype + public static boolean isValidSystemDefaultIme( + boolean isSystemReady, InputMethodInfo imi, Context context) { + if (!isSystemReady) { + return false; + } + if (!isSystemIme(imi)) { + return false; + } + if (imi.getIsDefaultResourceId() != 0) { + try { + if (imi.isDefault(context) && containsSubtypeOf( + imi, context.getResources().getConfiguration().locale.getLanguage(), + null /* mode */)) { + return true; + } + } catch (Resources.NotFoundException ex) { + } + } + if (imi.getSubtypeCount() == 0) { + Slog.w(TAG, "Found no subtypes in a system IME: " + imi.getPackageName()); + } + return false; + } + + public static boolean isDefaultEnabledIme( + boolean isSystemReady, InputMethodInfo imi, Context context) { + return isValidSystemDefaultIme(isSystemReady, imi, context) + || isSystemImeThatHasEnglishKeyboardSubtype(imi); + } + + private static boolean containsSubtypeOf(InputMethodInfo imi, String language, String mode) { + final int N = imi.getSubtypeCount(); + for (int i = 0; i < N; ++i) { + if (!imi.getSubtypeAt(i).getLocale().startsWith(language)) { + continue; + } + if(!TextUtils.isEmpty(mode) && !imi.getSubtypeAt(i).getMode().equalsIgnoreCase(mode)) { + continue; + } + return true; + } + return false; + } + + public static ArrayList<InputMethodSubtype> getSubtypes(InputMethodInfo imi) { + ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>(); + final int subtypeCount = imi.getSubtypeCount(); + for (int i = 0; i < subtypeCount; ++i) { + subtypes.add(imi.getSubtypeAt(i)); + } + return subtypes; + } + + public static ArrayList<InputMethodSubtype> getOverridingImplicitlyEnabledSubtypes( + InputMethodInfo imi, String mode) { + ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>(); + final int subtypeCount = imi.getSubtypeCount(); + for (int i = 0; i < subtypeCount; ++i) { + final InputMethodSubtype subtype = imi.getSubtypeAt(i); + if (subtype.overridesImplicitlyEnabledSubtype() && subtype.getMode().equals(mode)) { + subtypes.add(subtype); + } + } + return subtypes; + } + + public static InputMethodInfo getMostApplicableDefaultIME( + List<InputMethodInfo> enabledImes) { + if (enabledImes != null && enabledImes.size() > 0) { + // We'd prefer to fall back on a system IME, since that is safer. + int i = enabledImes.size(); + int firstFoundSystemIme = -1; + while (i > 0) { + i--; + final InputMethodInfo imi = enabledImes.get(i); + if (InputMethodUtils.isSystemImeThatHasEnglishKeyboardSubtype(imi) + && !imi.isAuxiliaryIme()) { + return imi; + } + if (firstFoundSystemIme < 0 && InputMethodUtils.isSystemIme(imi) + && !imi.isAuxiliaryIme()) { + firstFoundSystemIme = i; + } + } + return enabledImes.get(Math.max(firstFoundSystemIme, 0)); + } + return null; + } + + public static boolean isValidSubtypeId(InputMethodInfo imi, int subtypeHashCode) { + return getSubtypeIdFromHashCode(imi, subtypeHashCode) != NOT_A_SUBTYPE_ID; + } + + public static int getSubtypeIdFromHashCode(InputMethodInfo imi, int subtypeHashCode) { + if (imi != null) { + final int subtypeCount = imi.getSubtypeCount(); + for (int i = 0; i < subtypeCount; ++i) { + InputMethodSubtype ims = imi.getSubtypeAt(i); + if (subtypeHashCode == ims.hashCode()) { + return i; + } + } + } + return NOT_A_SUBTYPE_ID; + } + + private static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked( + Resources res, InputMethodInfo imi) { + final List<InputMethodSubtype> subtypes = InputMethodUtils.getSubtypes(imi); + final String systemLocale = res.getConfiguration().locale.toString(); + if (TextUtils.isEmpty(systemLocale)) return new ArrayList<InputMethodSubtype>(); + final HashMap<String, InputMethodSubtype> applicableModeAndSubtypesMap = + new HashMap<String, InputMethodSubtype>(); + final int N = subtypes.size(); + for (int i = 0; i < N; ++i) { + // scan overriding implicitly enabled subtypes. + InputMethodSubtype subtype = subtypes.get(i); + if (subtype.overridesImplicitlyEnabledSubtype()) { + final String mode = subtype.getMode(); + if (!applicableModeAndSubtypesMap.containsKey(mode)) { + applicableModeAndSubtypesMap.put(mode, subtype); + } + } + } + if (applicableModeAndSubtypesMap.size() > 0) { + return new ArrayList<InputMethodSubtype>(applicableModeAndSubtypesMap.values()); + } + for (int i = 0; i < N; ++i) { + final InputMethodSubtype subtype = subtypes.get(i); + final String locale = subtype.getLocale(); + final String mode = subtype.getMode(); + // When system locale starts with subtype's locale, that subtype will be applicable + // for system locale + // For instance, it's clearly applicable for cases like system locale = en_US and + // subtype = en, but it is not necessarily considered applicable for cases like system + // locale = en and subtype = en_US. + // We just call systemLocale.startsWith(locale) in this function because there is no + // need to find applicable subtypes aggressively unlike + // findLastResortApplicableSubtypeLocked. + if (systemLocale.startsWith(locale)) { + final InputMethodSubtype applicableSubtype = applicableModeAndSubtypesMap.get(mode); + // If more applicable subtypes are contained, skip. + if (applicableSubtype != null) { + if (systemLocale.equals(applicableSubtype.getLocale())) continue; + if (!systemLocale.equals(locale)) continue; + } + applicableModeAndSubtypesMap.put(mode, subtype); + } + } + final InputMethodSubtype keyboardSubtype + = applicableModeAndSubtypesMap.get(SUBTYPE_MODE_KEYBOARD); + final ArrayList<InputMethodSubtype> applicableSubtypes = new ArrayList<InputMethodSubtype>( + applicableModeAndSubtypesMap.values()); + if (keyboardSubtype != null && !keyboardSubtype.containsExtraValueKey(TAG_ASCII_CAPABLE)) { + for (int i = 0; i < N; ++i) { + final InputMethodSubtype subtype = subtypes.get(i); + final String mode = subtype.getMode(); + if (SUBTYPE_MODE_KEYBOARD.equals(mode) && subtype.containsExtraValueKey( + TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE)) { + applicableSubtypes.add(subtype); + } + } + } + if (keyboardSubtype == null) { + InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtypeLocked( + res, subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true); + if (lastResortKeyboardSubtype != null) { + applicableSubtypes.add(lastResortKeyboardSubtype); + } + } + return applicableSubtypes; + } + + private static List<InputMethodSubtype> getEnabledInputMethodSubtypeList( + Context context, InputMethodInfo imi, List<InputMethodSubtype> enabledSubtypes, + boolean allowsImplicitlySelectedSubtypes) { + if (allowsImplicitlySelectedSubtypes && enabledSubtypes.isEmpty()) { + enabledSubtypes = InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + context.getResources(), imi); + } + return InputMethodSubtype.sort(context, 0, imi, enabledSubtypes); + } + + /** + * If there are no selected subtypes, tries finding the most applicable one according to the + * given locale. + * @param subtypes this function will search the most applicable subtype in subtypes + * @param mode subtypes will be filtered by mode + * @param locale subtypes will be filtered by locale + * @param canIgnoreLocaleAsLastResort if this function can't find the most applicable subtype, + * it will return the first subtype matched with mode + * @return the most applicable subtypeId + */ + public static InputMethodSubtype findLastResortApplicableSubtypeLocked( + Resources res, List<InputMethodSubtype> subtypes, String mode, String locale, + boolean canIgnoreLocaleAsLastResort) { + if (subtypes == null || subtypes.size() == 0) { + return null; + } + if (TextUtils.isEmpty(locale)) { + locale = res.getConfiguration().locale.toString(); + } + final String language = locale.substring(0, 2); + boolean partialMatchFound = false; + InputMethodSubtype applicableSubtype = null; + InputMethodSubtype firstMatchedModeSubtype = null; + final int N = subtypes.size(); + for (int i = 0; i < N; ++i) { + InputMethodSubtype subtype = subtypes.get(i); + final String subtypeLocale = subtype.getLocale(); + // An applicable subtype should match "mode". If mode is null, mode will be ignored, + // and all subtypes with all modes can be candidates. + if (mode == null || subtypes.get(i).getMode().equalsIgnoreCase(mode)) { + if (firstMatchedModeSubtype == null) { + firstMatchedModeSubtype = subtype; + } + if (locale.equals(subtypeLocale)) { + // Exact match (e.g. system locale is "en_US" and subtype locale is "en_US") + applicableSubtype = subtype; + break; + } else if (!partialMatchFound && subtypeLocale.startsWith(language)) { + // Partial match (e.g. system locale is "en_US" and subtype locale is "en") + applicableSubtype = subtype; + partialMatchFound = true; + } + } + } + + if (applicableSubtype == null && canIgnoreLocaleAsLastResort) { + return firstMatchedModeSubtype; + } + + // The first subtype applicable to the system locale will be defined as the most applicable + // subtype. + if (DEBUG) { + if (applicableSubtype != null) { + Slog.d(TAG, "Applicable InputMethodSubtype was found: " + + applicableSubtype.getMode() + "," + applicableSubtype.getLocale()); + } + } + return applicableSubtype; + } + + public static boolean canAddToLastInputMethod(InputMethodSubtype subtype) { + if (subtype == null) return true; + return !subtype.isAuxiliary(); + } + + /** + * Utility class for putting and getting settings for InputMethod + * TODO: Move all putters and getters of settings to this class. + */ + public static class InputMethodSettings { + // The string for enabled input method is saved as follows: + // example: ("ime0;subtype0;subtype1;subtype2:ime1:ime2;subtype0") + private static final char INPUT_METHOD_SEPARATER = ':'; + private static final char INPUT_METHOD_SUBTYPE_SEPARATER = ';'; + private final TextUtils.SimpleStringSplitter mInputMethodSplitter = + new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATER); + + private final TextUtils.SimpleStringSplitter mSubtypeSplitter = + new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATER); + + private final Resources mRes; + private final ContentResolver mResolver; + private final HashMap<String, InputMethodInfo> mMethodMap; + private final ArrayList<InputMethodInfo> mMethodList; + + private String mEnabledInputMethodsStrCache; + private int mCurrentUserId; + + private static void buildEnabledInputMethodsSettingString( + StringBuilder builder, Pair<String, ArrayList<String>> pair) { + String id = pair.first; + ArrayList<String> subtypes = pair.second; + builder.append(id); + // Inputmethod and subtypes are saved in the settings as follows: + // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1 + for (String subtypeId: subtypes) { + builder.append(INPUT_METHOD_SUBTYPE_SEPARATER).append(subtypeId); + } + } + + public InputMethodSettings( + Resources res, ContentResolver resolver, + HashMap<String, InputMethodInfo> methodMap, ArrayList<InputMethodInfo> methodList, + int userId) { + setCurrentUserId(userId); + mRes = res; + mResolver = resolver; + mMethodMap = methodMap; + mMethodList = methodList; + } + + public void setCurrentUserId(int userId) { + if (DEBUG) { + Slog.d(TAG, "--- Swtich the current user from " + mCurrentUserId + " to " + + userId + ", new ime = " + getSelectedInputMethod()); + } + // IMMS settings are kept per user, so keep track of current user + mCurrentUserId = userId; + } + + public List<InputMethodInfo> getEnabledInputMethodListLocked() { + return createEnabledInputMethodListLocked( + getEnabledInputMethodsAndSubtypeListLocked()); + } + + public List<Pair<InputMethodInfo, ArrayList<String>>> + getEnabledInputMethodAndSubtypeHashCodeListLocked() { + return createEnabledInputMethodAndSubtypeHashCodeListLocked( + getEnabledInputMethodsAndSubtypeListLocked()); + } + + public List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked( + Context context, InputMethodInfo imi, boolean allowsImplicitlySelectedSubtypes) { + List<InputMethodSubtype> enabledSubtypes = + getEnabledInputMethodSubtypeListLocked(imi); + if (allowsImplicitlySelectedSubtypes && enabledSubtypes.isEmpty()) { + enabledSubtypes = InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + context.getResources(), imi); + } + return InputMethodSubtype.sort(context, 0, imi, enabledSubtypes); + } + + private List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked( + InputMethodInfo imi) { + List<Pair<String, ArrayList<String>>> imsList = + getEnabledInputMethodsAndSubtypeListLocked(); + ArrayList<InputMethodSubtype> enabledSubtypes = + new ArrayList<InputMethodSubtype>(); + if (imi != null) { + for (Pair<String, ArrayList<String>> imsPair : imsList) { + InputMethodInfo info = mMethodMap.get(imsPair.first); + if (info != null && info.getId().equals(imi.getId())) { + final int subtypeCount = info.getSubtypeCount(); + for (int i = 0; i < subtypeCount; ++i) { + InputMethodSubtype ims = info.getSubtypeAt(i); + for (String s: imsPair.second) { + if (String.valueOf(ims.hashCode()).equals(s)) { + enabledSubtypes.add(ims); + } + } + } + break; + } + } + } + return enabledSubtypes; + } + + // At the initial boot, the settings for input methods are not set, + // so we need to enable IME in that case. + public void enableAllIMEsIfThereIsNoEnabledIME() { + if (TextUtils.isEmpty(getEnabledInputMethodsStr())) { + StringBuilder sb = new StringBuilder(); + final int N = mMethodList.size(); + for (int i = 0; i < N; i++) { + InputMethodInfo imi = mMethodList.get(i); + Slog.i(TAG, "Adding: " + imi.getId()); + if (i > 0) sb.append(':'); + sb.append(imi.getId()); + } + putEnabledInputMethodsStr(sb.toString()); + } + } + + public List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeListLocked() { + ArrayList<Pair<String, ArrayList<String>>> imsList + = new ArrayList<Pair<String, ArrayList<String>>>(); + final String enabledInputMethodsStr = getEnabledInputMethodsStr(); + if (TextUtils.isEmpty(enabledInputMethodsStr)) { + return imsList; + } + mInputMethodSplitter.setString(enabledInputMethodsStr); + while (mInputMethodSplitter.hasNext()) { + String nextImsStr = mInputMethodSplitter.next(); + mSubtypeSplitter.setString(nextImsStr); + if (mSubtypeSplitter.hasNext()) { + ArrayList<String> subtypeHashes = new ArrayList<String>(); + // The first element is ime id. + String imeId = mSubtypeSplitter.next(); + while (mSubtypeSplitter.hasNext()) { + subtypeHashes.add(mSubtypeSplitter.next()); + } + imsList.add(new Pair<String, ArrayList<String>>(imeId, subtypeHashes)); + } + } + return imsList; + } + + public void appendAndPutEnabledInputMethodLocked(String id, boolean reloadInputMethodStr) { + if (reloadInputMethodStr) { + getEnabledInputMethodsStr(); + } + if (TextUtils.isEmpty(mEnabledInputMethodsStrCache)) { + // Add in the newly enabled input method. + putEnabledInputMethodsStr(id); + } else { + putEnabledInputMethodsStr( + mEnabledInputMethodsStrCache + INPUT_METHOD_SEPARATER + id); + } + } + + /** + * Build and put a string of EnabledInputMethods with removing specified Id. + * @return the specified id was removed or not. + */ + public boolean buildAndPutEnabledInputMethodsStrRemovingIdLocked( + StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id) { + boolean isRemoved = false; + boolean needsAppendSeparator = false; + for (Pair<String, ArrayList<String>> ims: imsList) { + String curId = ims.first; + if (curId.equals(id)) { + // We are disabling this input method, and it is + // currently enabled. Skip it to remove from the + // new list. + isRemoved = true; + } else { + if (needsAppendSeparator) { + builder.append(INPUT_METHOD_SEPARATER); + } else { + needsAppendSeparator = true; + } + buildEnabledInputMethodsSettingString(builder, ims); + } + } + if (isRemoved) { + // Update the setting with the new list of input methods. + putEnabledInputMethodsStr(builder.toString()); + } + return isRemoved; + } + + private List<InputMethodInfo> createEnabledInputMethodListLocked( + List<Pair<String, ArrayList<String>>> imsList) { + final ArrayList<InputMethodInfo> res = new ArrayList<InputMethodInfo>(); + for (Pair<String, ArrayList<String>> ims: imsList) { + InputMethodInfo info = mMethodMap.get(ims.first); + if (info != null) { + res.add(info); + } + } + return res; + } + + private List<Pair<InputMethodInfo, ArrayList<String>>> + createEnabledInputMethodAndSubtypeHashCodeListLocked( + List<Pair<String, ArrayList<String>>> imsList) { + final ArrayList<Pair<InputMethodInfo, ArrayList<String>>> res + = new ArrayList<Pair<InputMethodInfo, ArrayList<String>>>(); + for (Pair<String, ArrayList<String>> ims : imsList) { + InputMethodInfo info = mMethodMap.get(ims.first); + if (info != null) { + res.add(new Pair<InputMethodInfo, ArrayList<String>>(info, ims.second)); + } + } + return res; + } + + private void putEnabledInputMethodsStr(String str) { + Settings.Secure.putStringForUser( + mResolver, Settings.Secure.ENABLED_INPUT_METHODS, str, mCurrentUserId); + mEnabledInputMethodsStrCache = str; + if (DEBUG) { + Slog.d(TAG, "putEnabledInputMethodStr: " + str); + } + } + + public String getEnabledInputMethodsStr() { + mEnabledInputMethodsStrCache = Settings.Secure.getStringForUser( + mResolver, Settings.Secure.ENABLED_INPUT_METHODS, mCurrentUserId); + if (DEBUG) { + Slog.d(TAG, "getEnabledInputMethodsStr: " + mEnabledInputMethodsStrCache + + ", " + mCurrentUserId); + } + return mEnabledInputMethodsStrCache; + } + + private void saveSubtypeHistory( + List<Pair<String, String>> savedImes, String newImeId, String newSubtypeId) { + StringBuilder builder = new StringBuilder(); + boolean isImeAdded = false; + if (!TextUtils.isEmpty(newImeId) && !TextUtils.isEmpty(newSubtypeId)) { + builder.append(newImeId).append(INPUT_METHOD_SUBTYPE_SEPARATER).append( + newSubtypeId); + isImeAdded = true; + } + for (Pair<String, String> ime: savedImes) { + String imeId = ime.first; + String subtypeId = ime.second; + if (TextUtils.isEmpty(subtypeId)) { + subtypeId = NOT_A_SUBTYPE_ID_STR; + } + if (isImeAdded) { + builder.append(INPUT_METHOD_SEPARATER); + } else { + isImeAdded = true; + } + builder.append(imeId).append(INPUT_METHOD_SUBTYPE_SEPARATER).append( + subtypeId); + } + // Remove the last INPUT_METHOD_SEPARATER + putSubtypeHistoryStr(builder.toString()); + } + + private void addSubtypeToHistory(String imeId, String subtypeId) { + List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked(); + for (Pair<String, String> ime: subtypeHistory) { + if (ime.first.equals(imeId)) { + if (DEBUG) { + Slog.v(TAG, "Subtype found in the history: " + imeId + ", " + + ime.second); + } + // We should break here + subtypeHistory.remove(ime); + break; + } + } + if (DEBUG) { + Slog.v(TAG, "Add subtype to the history: " + imeId + ", " + subtypeId); + } + saveSubtypeHistory(subtypeHistory, imeId, subtypeId); + } + + private void putSubtypeHistoryStr(String str) { + if (DEBUG) { + Slog.d(TAG, "putSubtypeHistoryStr: " + str); + } + Settings.Secure.putStringForUser( + mResolver, Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, str, mCurrentUserId); + } + + public Pair<String, String> getLastInputMethodAndSubtypeLocked() { + // Gets the first one from the history + return getLastSubtypeForInputMethodLockedInternal(null); + } + + public String getLastSubtypeForInputMethodLocked(String imeId) { + Pair<String, String> ime = getLastSubtypeForInputMethodLockedInternal(imeId); + if (ime != null) { + return ime.second; + } else { + return null; + } + } + + private Pair<String, String> getLastSubtypeForInputMethodLockedInternal(String imeId) { + List<Pair<String, ArrayList<String>>> enabledImes = + getEnabledInputMethodsAndSubtypeListLocked(); + List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked(); + for (Pair<String, String> imeAndSubtype : subtypeHistory) { + final String imeInTheHistory = imeAndSubtype.first; + // If imeId is empty, returns the first IME and subtype in the history + if (TextUtils.isEmpty(imeId) || imeInTheHistory.equals(imeId)) { + final String subtypeInTheHistory = imeAndSubtype.second; + final String subtypeHashCode = + getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked( + enabledImes, imeInTheHistory, subtypeInTheHistory); + if (!TextUtils.isEmpty(subtypeHashCode)) { + if (DEBUG) { + Slog.d(TAG, "Enabled subtype found in the history: " + subtypeHashCode); + } + return new Pair<String, String>(imeInTheHistory, subtypeHashCode); + } + } + } + if (DEBUG) { + Slog.d(TAG, "No enabled IME found in the history"); + } + return null; + } + + private String getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(List<Pair<String, + ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode) { + for (Pair<String, ArrayList<String>> enabledIme: enabledImes) { + if (enabledIme.first.equals(imeId)) { + final ArrayList<String> explicitlyEnabledSubtypes = enabledIme.second; + final InputMethodInfo imi = mMethodMap.get(imeId); + if (explicitlyEnabledSubtypes.size() == 0) { + // If there are no explicitly enabled subtypes, applicable subtypes are + // enabled implicitly. + // If IME is enabled and no subtypes are enabled, applicable subtypes + // are enabled implicitly, so needs to treat them to be enabled. + if (imi != null && imi.getSubtypeCount() > 0) { + List<InputMethodSubtype> implicitlySelectedSubtypes = + getImplicitlyApplicableSubtypesLocked(mRes, imi); + if (implicitlySelectedSubtypes != null) { + final int N = implicitlySelectedSubtypes.size(); + for (int i = 0; i < N; ++i) { + final InputMethodSubtype st = implicitlySelectedSubtypes.get(i); + if (String.valueOf(st.hashCode()).equals(subtypeHashCode)) { + return subtypeHashCode; + } + } + } + } + } else { + for (String s: explicitlyEnabledSubtypes) { + if (s.equals(subtypeHashCode)) { + // If both imeId and subtypeId are enabled, return subtypeId. + try { + final int hashCode = Integer.valueOf(subtypeHashCode); + // Check whether the subtype id is valid or not + if (isValidSubtypeId(imi, hashCode)) { + return s; + } else { + return NOT_A_SUBTYPE_ID_STR; + } + } catch (NumberFormatException e) { + return NOT_A_SUBTYPE_ID_STR; + } + } + } + } + // If imeId was enabled but subtypeId was disabled. + return NOT_A_SUBTYPE_ID_STR; + } + } + // If both imeId and subtypeId are disabled, return null + return null; + } + + private List<Pair<String, String>> loadInputMethodAndSubtypeHistoryLocked() { + ArrayList<Pair<String, String>> imsList = new ArrayList<Pair<String, String>>(); + final String subtypeHistoryStr = getSubtypeHistoryStr(); + if (TextUtils.isEmpty(subtypeHistoryStr)) { + return imsList; + } + mInputMethodSplitter.setString(subtypeHistoryStr); + while (mInputMethodSplitter.hasNext()) { + String nextImsStr = mInputMethodSplitter.next(); + mSubtypeSplitter.setString(nextImsStr); + if (mSubtypeSplitter.hasNext()) { + String subtypeId = NOT_A_SUBTYPE_ID_STR; + // The first element is ime id. + String imeId = mSubtypeSplitter.next(); + while (mSubtypeSplitter.hasNext()) { + subtypeId = mSubtypeSplitter.next(); + break; + } + imsList.add(new Pair<String, String>(imeId, subtypeId)); + } + } + return imsList; + } + + private String getSubtypeHistoryStr() { + if (DEBUG) { + Slog.d(TAG, "getSubtypeHistoryStr: " + Settings.Secure.getStringForUser( + mResolver, Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, mCurrentUserId)); + } + return Settings.Secure.getStringForUser( + mResolver, Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, mCurrentUserId); + } + + public void putSelectedInputMethod(String imeId) { + if (DEBUG) { + Slog.d(TAG, "putSelectedInputMethodStr: " + imeId + ", " + + mCurrentUserId); + } + Settings.Secure.putStringForUser( + mResolver, Settings.Secure.DEFAULT_INPUT_METHOD, imeId, mCurrentUserId); + } + + public void putSelectedSubtype(int subtypeId) { + if (DEBUG) { + Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeId + ", " + + mCurrentUserId); + } + Settings.Secure.putIntForUser(mResolver, Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, + subtypeId, mCurrentUserId); + } + + public String getDisabledSystemInputMethods() { + return Settings.Secure.getStringForUser( + mResolver, Settings.Secure.DISABLED_SYSTEM_INPUT_METHODS, mCurrentUserId); + } + + public String getSelectedInputMethod() { + if (DEBUG) { + Slog.d(TAG, "getSelectedInputMethodStr: " + Settings.Secure.getStringForUser( + mResolver, Settings.Secure.DEFAULT_INPUT_METHOD, mCurrentUserId) + + ", " + mCurrentUserId); + } + return Settings.Secure.getStringForUser( + mResolver, Settings.Secure.DEFAULT_INPUT_METHOD, mCurrentUserId); + } + + public boolean isSubtypeSelected() { + return getSelectedInputMethodSubtypeHashCode() != NOT_A_SUBTYPE_ID; + } + + private int getSelectedInputMethodSubtypeHashCode() { + try { + return Settings.Secure.getIntForUser( + mResolver, Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, mCurrentUserId); + } catch (SettingNotFoundException e) { + return NOT_A_SUBTYPE_ID; + } + } + + public int getCurrentUserId() { + return mCurrentUserId; + } + + public int getSelectedInputMethodSubtypeId(String selectedImiId) { + final InputMethodInfo imi = mMethodMap.get(selectedImiId); + if (imi == null) { + return NOT_A_SUBTYPE_ID; + } + final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode(); + return getSubtypeIdFromHashCode(imi, subtypeHashCode); + } + + public void saveCurrentInputMethodAndSubtypeToHistory( + String curMethodId, InputMethodSubtype currentSubtype) { + String subtypeId = NOT_A_SUBTYPE_ID_STR; + if (currentSubtype != null) { + subtypeId = String.valueOf(currentSubtype.hashCode()); + } + if (canAddToLastInputMethod(currentSubtype)) { + addSubtypeToHistory(curMethodId, subtypeId); + } + } + } +} diff --git a/core/java/com/android/internal/net/NetworkStatsFactory.java b/core/java/com/android/internal/net/NetworkStatsFactory.java index c517a68..8282d23 100644 --- a/core/java/com/android/internal/net/NetworkStatsFactory.java +++ b/core/java/com/android/internal/net/NetworkStatsFactory.java @@ -31,6 +31,7 @@ import com.android.internal.util.ProcFileReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.net.ProtocolException; import libcore.io.IoUtils; @@ -41,7 +42,8 @@ import libcore.io.IoUtils; public class NetworkStatsFactory { private static final String TAG = "NetworkStatsFactory"; - // TODO: consider moving parsing to native code + private static final boolean USE_NATIVE_PARSING = true; + private static final boolean SANITY_CHECK_NATIVE = false; /** Path to {@code /proc/net/xt_qtaguid/iface_stat_all}. */ private final File mStatsXtIfaceAll; @@ -69,7 +71,7 @@ public class NetworkStatsFactory { * * @throws IllegalStateException when problem parsing stats. */ - public NetworkStats readNetworkStatsSummaryDev() throws IllegalStateException { + public NetworkStats readNetworkStatsSummaryDev() throws IOException { final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads(); final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 6); @@ -105,11 +107,9 @@ public class NetworkStatsFactory { reader.finishLine(); } } catch (NullPointerException e) { - throw new IllegalStateException("problem parsing stats: " + e); + throw new ProtocolException("problem parsing stats", e); } catch (NumberFormatException e) { - throw new IllegalStateException("problem parsing stats: " + e); - } catch (IOException e) { - throw new IllegalStateException("problem parsing stats: " + e); + throw new ProtocolException("problem parsing stats", e); } finally { IoUtils.closeQuietly(reader); StrictMode.setThreadPolicy(savedPolicy); @@ -124,7 +124,7 @@ public class NetworkStatsFactory { * * @throws IllegalStateException when problem parsing stats. */ - public NetworkStats readNetworkStatsSummaryXt() throws IllegalStateException { + public NetworkStats readNetworkStatsSummaryXt() throws IOException { final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads(); // return null when kernel doesn't support @@ -154,11 +154,9 @@ public class NetworkStatsFactory { reader.finishLine(); } } catch (NullPointerException e) { - throw new IllegalStateException("problem parsing stats: " + e); + throw new ProtocolException("problem parsing stats", e); } catch (NumberFormatException e) { - throw new IllegalStateException("problem parsing stats: " + e); - } catch (IOException e) { - throw new IllegalStateException("problem parsing stats: " + e); + throw new ProtocolException("problem parsing stats", e); } finally { IoUtils.closeQuietly(reader); StrictMode.setThreadPolicy(savedPolicy); @@ -166,17 +164,33 @@ public class NetworkStatsFactory { return stats; } - public NetworkStats readNetworkStatsDetail() { + public NetworkStats readNetworkStatsDetail() throws IOException { return readNetworkStatsDetail(UID_ALL); } + public NetworkStats readNetworkStatsDetail(int limitUid) throws IOException { + if (USE_NATIVE_PARSING) { + final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 0); + if (nativeReadNetworkStatsDetail(stats, mStatsXtUid.getAbsolutePath(), limitUid) != 0) { + throw new IOException("Failed to parse network stats"); + } + if (SANITY_CHECK_NATIVE) { + final NetworkStats javaStats = javaReadNetworkStatsDetail(mStatsXtUid, limitUid); + assertEquals(javaStats, stats); + } + return stats; + } else { + return javaReadNetworkStatsDetail(mStatsXtUid, limitUid); + } + } + /** - * Parse and return {@link NetworkStats} with UID-level details. Values - * monotonically increase since device boot. - * - * @throws IllegalStateException when problem parsing stats. + * Parse and return {@link NetworkStats} with UID-level details. Values are + * expected to monotonically increase since device boot. */ - public NetworkStats readNetworkStatsDetail(int limitUid) throws IllegalStateException { + @VisibleForTesting + public static NetworkStats javaReadNetworkStatsDetail(File detailPath, int limitUid) + throws IOException { final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads(); final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 24); @@ -188,13 +202,13 @@ public class NetworkStatsFactory { ProcFileReader reader = null; try { // open and consume header line - reader = new ProcFileReader(new FileInputStream(mStatsXtUid)); + reader = new ProcFileReader(new FileInputStream(detailPath)); reader.finishLine(); while (reader.hasMoreData()) { idx = reader.nextInt(); if (idx != lastIdx + 1) { - throw new IllegalStateException( + throw new ProtocolException( "inconsistent idx=" + idx + " after lastIdx=" + lastIdx); } lastIdx = idx; @@ -215,11 +229,9 @@ public class NetworkStatsFactory { reader.finishLine(); } } catch (NullPointerException e) { - throw new IllegalStateException("problem parsing idx " + idx, e); + throw new ProtocolException("problem parsing idx " + idx, e); } catch (NumberFormatException e) { - throw new IllegalStateException("problem parsing idx " + idx, e); - } catch (IOException e) { - throw new IllegalStateException("problem parsing idx " + idx, e); + throw new ProtocolException("problem parsing idx " + idx, e); } finally { IoUtils.closeQuietly(reader); StrictMode.setThreadPolicy(savedPolicy); @@ -227,4 +239,30 @@ public class NetworkStatsFactory { return stats; } + + public void assertEquals(NetworkStats expected, NetworkStats actual) { + if (expected.size() != actual.size()) { + throw new AssertionError( + "Expected size " + expected.size() + ", actual size " + actual.size()); + } + + NetworkStats.Entry expectedRow = null; + NetworkStats.Entry actualRow = null; + for (int i = 0; i < expected.size(); i++) { + expectedRow = expected.getValues(i, expectedRow); + actualRow = actual.getValues(i, actualRow); + if (!expectedRow.equals(actualRow)) { + throw new AssertionError( + "Expected row " + i + ": " + expectedRow + ", actual row " + actualRow); + } + } + } + + /** + * Parse statistics from file into given {@link NetworkStats} object. Values + * are expected to monotonically increase since device boot. + */ + @VisibleForTesting + public static native int nativeReadNetworkStatsDetail( + NetworkStats stats, String path, int limitUid); } diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java index 94e7a06..04b9884 100644 --- a/core/java/com/android/internal/os/BatteryStatsImpl.java +++ b/core/java/com/android/internal/os/BatteryStatsImpl.java @@ -16,14 +16,11 @@ package com.android.internal.os; -import static android.net.NetworkStats.IFACE_ALL; -import static android.net.NetworkStats.UID_ALL; import static android.text.format.DateUtils.SECOND_IN_MILLIS; import static com.android.server.NetworkManagementSocketTagger.PROP_QTAGUID_ENABLED; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadset; -import android.content.res.Resources; import android.net.ConnectivityManager; import android.net.NetworkStats; import android.os.BatteryManager; @@ -49,7 +46,6 @@ import android.util.Slog; import android.util.SparseArray; import android.util.TimeUtils; -import com.android.internal.R; import com.android.internal.net.NetworkStatsFactory; import com.android.internal.util.JournaledFile; import com.google.android.collect.Sets; @@ -87,7 +83,7 @@ public final class BatteryStatsImpl extends BatteryStats { private static final int MAGIC = 0xBA757475; // 'BATSTATS' // Current on-disk Parcel version - private static final int VERSION = 62 + (USE_OLD_HISTORY ? 1000 : 0); + private static final int VERSION = 64 + (USE_OLD_HISTORY ? 1000 : 0); // Maximum number of items we will record in the history. private static final int MAX_HISTORY_ITEMS = 2000; @@ -356,8 +352,8 @@ public final class BatteryStatsImpl extends BatteryStats { } public static interface Unpluggable { - void unplug(long batteryUptime, long batteryRealtime); - void plug(long batteryUptime, long batteryRealtime); + void unplug(long elapsedRealtime, long batteryUptime, long batteryRealtime); + void plug(long elapsedRealtime, long batteryUptime, long batteryRealtime); } /** @@ -392,12 +388,12 @@ public final class BatteryStatsImpl extends BatteryStats { out.writeInt(mUnpluggedCount); } - public void unplug(long batteryUptime, long batteryRealtime) { + public void unplug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { mUnpluggedCount = mPluggedCount; mCount.set(mPluggedCount); } - public void plug(long batteryUptime, long batteryRealtime) { + public void plug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { mPluggedCount = mCount.get(); } @@ -587,7 +583,7 @@ public final class BatteryStatsImpl extends BatteryStats { out.writeLong(mUnpluggedTime); } - public void unplug(long batteryUptime, long batteryRealtime) { + public void unplug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { if (DEBUG && mType < 0) { Log.v(TAG, "unplug #" + mType + ": realtime=" + batteryRealtime + " old mUnpluggedTime=" + mUnpluggedTime @@ -602,7 +598,7 @@ public final class BatteryStatsImpl extends BatteryStats { } } - public void plug(long batteryUptime, long batteryRealtime) { + public void plug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { if (DEBUG && mType < 0) { Log.v(TAG, "plug #" + mType + ": realtime=" + batteryRealtime + " old mTotalTime=" + mTotalTime); @@ -731,7 +727,7 @@ public final class BatteryStatsImpl extends BatteryStats { boolean mTrackingReportedValues; /* - * A sequnce counter, incremented once for each update of the stats. + * A sequence counter, incremented once for each update of the stats. */ int mUpdateVersion; @@ -786,8 +782,8 @@ public final class BatteryStatsImpl extends BatteryStats { mCurrentReportedTotalTime = totalTime; } - public void unplug(long batteryUptime, long batteryRealtime) { - super.unplug(batteryUptime, batteryRealtime); + public void unplug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { + super.unplug(elapsedRealtime, batteryUptime, batteryRealtime); if (mTrackingReportedValues) { mUnpluggedReportedTotalTime = mCurrentReportedTotalTime; mUnpluggedReportedCount = mCurrentReportedCount; @@ -795,8 +791,8 @@ public final class BatteryStatsImpl extends BatteryStats { mInDischarge = true; } - public void plug(long batteryUptime, long batteryRealtime) { - super.plug(batteryUptime, batteryRealtime); + public void plug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { + super.plug(elapsedRealtime, batteryUptime, batteryRealtime); mInDischarge = false; } @@ -849,6 +845,141 @@ public final class BatteryStatsImpl extends BatteryStats { } /** + * A timer that increments in batches. It does not run for durations, but just jumps + * for a pre-determined amount. + */ + public static final class BatchTimer extends Timer { + final Uid mUid; + + /** + * The last time at which we updated the timer. This is in elapsed realtime microseconds. + */ + long mLastAddedTime; + + /** + * The last duration that we added to the timer. This is in microseconds. + */ + long mLastAddedDuration; + + /** + * Whether we are currently in a discharge cycle. + */ + boolean mInDischarge; + + BatchTimer(Uid uid, int type, ArrayList<Unpluggable> unpluggables, + boolean inDischarge, Parcel in) { + super(type, unpluggables, in); + mUid = uid; + mLastAddedTime = in.readLong(); + mLastAddedDuration = in.readLong(); + mInDischarge = inDischarge; + } + + BatchTimer(Uid uid, int type, ArrayList<Unpluggable> unpluggables, + boolean inDischarge) { + super(type, unpluggables); + mUid = uid; + mInDischarge = inDischarge; + } + + @Override + public void writeToParcel(Parcel out, long batteryRealtime) { + super.writeToParcel(out, batteryRealtime); + out.writeLong(mLastAddedTime); + out.writeLong(mLastAddedDuration); + } + + @Override + public void plug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { + recomputeLastDuration(SystemClock.elapsedRealtime() * 1000, false); + mInDischarge = false; + super.plug(elapsedRealtime, batteryUptime, batteryRealtime); + } + + @Override + public void unplug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { + recomputeLastDuration(elapsedRealtime, false); + mInDischarge = true; + // If we are still within the last added duration, then re-added whatever remains. + if (mLastAddedTime == elapsedRealtime) { + mTotalTime += mLastAddedDuration; + } + super.unplug(elapsedRealtime, batteryUptime, batteryRealtime); + } + + @Override + public void logState(Printer pw, String prefix) { + super.logState(pw, prefix); + pw.println(prefix + "mLastAddedTime=" + mLastAddedTime + + " mLastAddedDuration=" + mLastAddedDuration); + } + + private long computeOverage(long curTime) { + if (mLastAddedTime > 0) { + return mLastTime + mLastAddedDuration - curTime; + } + return 0; + } + + private void recomputeLastDuration(long curTime, boolean abort) { + final long overage = computeOverage(curTime); + if (overage > 0) { + // Aborting before the duration ran out -- roll back the remaining + // duration. Only do this if currently discharging; otherwise we didn't + // actually add the time. + if (mInDischarge) { + mTotalTime -= overage; + } + if (abort) { + mLastAddedTime = 0; + } else { + mLastAddedTime = curTime; + mLastAddedDuration -= overage; + } + } + } + + public void addDuration(BatteryStatsImpl stats, long durationMillis) { + final long now = SystemClock.elapsedRealtime() * 1000; + recomputeLastDuration(now, true); + mLastAddedTime = now; + mLastAddedDuration = durationMillis * 1000; + if (mInDischarge) { + mTotalTime += mLastAddedDuration; + mCount++; + } + } + + public void abortLastDuration(BatteryStatsImpl stats) { + final long now = SystemClock.elapsedRealtime() * 1000; + recomputeLastDuration(now, true); + } + + @Override + protected int computeCurrentCountLocked() { + return mCount; + } + + @Override + protected long computeRunTimeLocked(long curBatteryRealtime) { + final long overage = computeOverage(SystemClock.elapsedRealtime() * 1000); + if (overage > 0) { + return mTotalTime = overage; + } + return mTotalTime; + } + + @Override + boolean reset(BatteryStatsImpl stats, boolean detachIfReset) { + final long now = SystemClock.elapsedRealtime() * 1000; + recomputeLastDuration(now, true); + boolean stillActive = mLastAddedTime == now; + super.reset(stats, !stillActive && detachIfReset); + return !stillActive; + } + } + + /** * State for keeping track of timing information. */ public static final class StopwatchTimer extends Timer { @@ -902,12 +1033,12 @@ public final class BatteryStatsImpl extends BatteryStats { out.writeLong(mUpdateTime); } - public void plug(long batteryUptime, long batteryRealtime) { + public void plug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { if (mNesting > 0) { if (DEBUG && mType < 0) { Log.v(TAG, "old mUpdateTime=" + mUpdateTime); } - super.plug(batteryUptime, batteryRealtime); + super.plug(elapsedRealtime, batteryUptime, batteryRealtime); mUpdateTime = batteryRealtime; if (DEBUG && mType < 0) { Log.v(TAG, "new mUpdateTime=" + mUpdateTime); @@ -1443,7 +1574,7 @@ public final class BatteryStatsImpl extends BatteryStats { mHistoryOverflow = false; } - public void doUnplugLocked(long batteryUptime, long batteryRealtime) { + public void doUnplugLocked(long elapsedRealtime, long batteryUptime, long batteryRealtime) { NetworkStats.Entry entry = null; // Track UID data usage @@ -1462,7 +1593,7 @@ public final class BatteryStatsImpl extends BatteryStats { } for (int i = mUnpluggables.size() - 1; i >= 0; i--) { - mUnpluggables.get(i).unplug(batteryUptime, batteryRealtime); + mUnpluggables.get(i).unplug(elapsedRealtime, batteryUptime, batteryRealtime); } // Track both mobile and total overall data @@ -1483,7 +1614,7 @@ public final class BatteryStatsImpl extends BatteryStats { mBluetoothPingCount = 0; } - public void doPlugLocked(long batteryUptime, long batteryRealtime) { + public void doPlugLocked(long elapsedRealtime, long batteryUptime, long batteryRealtime) { NetworkStats.Entry entry = null; for (int iu = mUidStats.size() - 1; iu >= 0; iu--) { @@ -1498,7 +1629,7 @@ public final class BatteryStatsImpl extends BatteryStats { } } for (int i = mUnpluggables.size() - 1; i >= 0; i--) { - mUnpluggables.get(i).plug(batteryUptime, batteryRealtime); + mUnpluggables.get(i).plug(elapsedRealtime, batteryUptime, batteryRealtime); } // Track both mobile and total overall data @@ -2109,6 +2240,14 @@ public final class BatteryStatsImpl extends BatteryStats { getUidStatsLocked(uid).noteVideoTurnedOffLocked(); } + public void noteVibratorOnLocked(int uid, long durationMillis) { + getUidStatsLocked(uid).noteVibratorOnLocked(durationMillis); + } + + public void noteVibratorOffLocked(int uid) { + getUidStatsLocked(uid).noteVibratorOffLocked(); + } + public void noteWifiRunningLocked(WorkSource ws) { if (!mGlobalWifiRunning) { mHistoryCur.states |= HistoryItem.STATE_WIFI_RUNNING_FLAG; @@ -2402,6 +2541,8 @@ public final class BatteryStatsImpl extends BatteryStats { boolean mVideoTurnedOn; StopwatchTimer mVideoTurnedOnTimer; + BatchTimer mVibratorOnTimer; + Counter[] mUserActivityCounters; /** @@ -2439,10 +2580,6 @@ public final class BatteryStatsImpl extends BatteryStats { mWifiScanTimers, mUnpluggables); mWifiMulticastTimer = new StopwatchTimer(Uid.this, WIFI_MULTICAST_ENABLED, mWifiMulticastTimers, mUnpluggables); - mAudioTurnedOnTimer = new StopwatchTimer(Uid.this, AUDIO_TURNED_ON, - null, mUnpluggables); - mVideoTurnedOnTimer = new StopwatchTimer(Uid.this, VIDEO_TURNED_ON, - null, mUnpluggables); } @Override @@ -2587,15 +2724,19 @@ public final class BatteryStatsImpl extends BatteryStats { } } + public StopwatchTimer createAudioTurnedOnTimerLocked() { + if (mAudioTurnedOnTimer == null) { + mAudioTurnedOnTimer = new StopwatchTimer(Uid.this, AUDIO_TURNED_ON, + null, mUnpluggables); + } + return mAudioTurnedOnTimer; + } + @Override public void noteAudioTurnedOnLocked() { if (!mAudioTurnedOn) { mAudioTurnedOn = true; - if (mAudioTurnedOnTimer == null) { - mAudioTurnedOnTimer = new StopwatchTimer(Uid.this, AUDIO_TURNED_ON, - null, mUnpluggables); - } - mAudioTurnedOnTimer.startRunningLocked(BatteryStatsImpl.this); + createAudioTurnedOnTimerLocked().startRunningLocked(BatteryStatsImpl.this); } } @@ -2603,19 +2744,25 @@ public final class BatteryStatsImpl extends BatteryStats { public void noteAudioTurnedOffLocked() { if (mAudioTurnedOn) { mAudioTurnedOn = false; - mAudioTurnedOnTimer.stopRunningLocked(BatteryStatsImpl.this); + if (mAudioTurnedOnTimer != null) { + mAudioTurnedOnTimer.stopRunningLocked(BatteryStatsImpl.this); + } + } + } + + public StopwatchTimer createVideoTurnedOnTimerLocked() { + if (mVideoTurnedOnTimer == null) { + mVideoTurnedOnTimer = new StopwatchTimer(Uid.this, VIDEO_TURNED_ON, + null, mUnpluggables); } + return mVideoTurnedOnTimer; } @Override public void noteVideoTurnedOnLocked() { if (!mVideoTurnedOn) { mVideoTurnedOn = true; - if (mVideoTurnedOnTimer == null) { - mVideoTurnedOnTimer = new StopwatchTimer(Uid.this, VIDEO_TURNED_ON, - null, mUnpluggables); - } - mVideoTurnedOnTimer.startRunningLocked(BatteryStatsImpl.this); + createVideoTurnedOnTimerLocked().startRunningLocked(BatteryStatsImpl.this); } } @@ -2623,7 +2770,27 @@ public final class BatteryStatsImpl extends BatteryStats { public void noteVideoTurnedOffLocked() { if (mVideoTurnedOn) { mVideoTurnedOn = false; - mVideoTurnedOnTimer.stopRunningLocked(BatteryStatsImpl.this); + if (mVideoTurnedOnTimer != null) { + mVideoTurnedOnTimer.stopRunningLocked(BatteryStatsImpl.this); + } + } + } + + public BatchTimer createVibratorOnTimerLocked() { + if (mVibratorOnTimer == null) { + mVibratorOnTimer = new BatchTimer(Uid.this, VIBRATOR_ON, + mUnpluggables, BatteryStatsImpl.this.mOnBatteryInternal); + } + return mVibratorOnTimer; + } + + public void noteVibratorOnLocked(long durationMillis) { + createVibratorOnTimerLocked().addDuration(BatteryStatsImpl.this, durationMillis); + } + + public void noteVibratorOffLocked() { + if (mVibratorOnTimer != null) { + mVibratorOnTimer.abortLastDuration(BatteryStatsImpl.this); } } @@ -2677,6 +2844,11 @@ public final class BatteryStatsImpl extends BatteryStats { } @Override + public Timer getVibratorOnTimer() { + return mVibratorOnTimer; + } + + @Override public void noteUserActivityLocked(int type) { if (mUserActivityCounters == null) { initUserActivityLocked(); @@ -2747,6 +2919,14 @@ public final class BatteryStatsImpl extends BatteryStats { active |= !mVideoTurnedOnTimer.reset(BatteryStatsImpl.this, false); active |= mVideoTurnedOn; } + if (mVibratorOnTimer != null) { + if (mVibratorOnTimer.reset(BatteryStatsImpl.this, false)) { + mVibratorOnTimer.detach(); + mVibratorOnTimer = null; + } else { + active = true; + } + } mLoadedTcpBytesReceived = mLoadedTcpBytesSent = 0; mCurrentTcpBytesReceived = mCurrentTcpBytesSent = 0; @@ -2832,9 +3012,11 @@ public final class BatteryStatsImpl extends BatteryStats { } if (mAudioTurnedOnTimer != null) { mAudioTurnedOnTimer.detach(); + mAudioTurnedOnTimer = null; } if (mVideoTurnedOnTimer != null) { mVideoTurnedOnTimer.detach(); + mVideoTurnedOnTimer = null; } if (mUserActivityCounters != null) { for (int i=0; i<NUM_USER_ACTIVITY_TYPES; i++) { @@ -2917,6 +3099,12 @@ public final class BatteryStatsImpl extends BatteryStats { } else { out.writeInt(0); } + if (mVibratorOnTimer != null) { + out.writeInt(1); + mVibratorOnTimer.writeToParcel(out, batteryRealtime); + } else { + out.writeInt(0); + } if (mUserActivityCounters != null) { out.writeInt(1); for (int i=0; i<NUM_USER_ACTIVITY_TYPES; i++) { @@ -3016,6 +3204,12 @@ public final class BatteryStatsImpl extends BatteryStats { mVideoTurnedOnTimer = null; } if (in.readInt() != 0) { + mVibratorOnTimer = new BatchTimer(Uid.this, VIBRATOR_ON, + mUnpluggables, BatteryStatsImpl.this.mOnBatteryInternal, in); + } else { + mVibratorOnTimer = null; + } + if (in.readInt() != 0) { mUserActivityCounters = new Counter[NUM_USER_ACTIVITY_TYPES]; for (int i=0; i<NUM_USER_ACTIVITY_TYPES; i++) { mUserActivityCounters[i] = new Counter(mUnpluggables, in); @@ -3256,14 +3450,14 @@ public final class BatteryStatsImpl extends BatteryStats { mSpeedBins = new SamplingCounter[getCpuSpeedSteps()]; } - public void unplug(long batteryUptime, long batteryRealtime) { + public void unplug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { mUnpluggedUserTime = mUserTime; mUnpluggedSystemTime = mSystemTime; mUnpluggedStarts = mStarts; mUnpluggedForegroundTime = mForegroundTime; } - public void plug(long batteryUptime, long batteryRealtime) { + public void plug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { } void detach() { @@ -3550,11 +3744,11 @@ public final class BatteryStatsImpl extends BatteryStats { mUnpluggables.add(this); } - public void unplug(long batteryUptime, long batteryRealtime) { + public void unplug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { mUnpluggedWakeups = mWakeups; } - public void plug(long batteryUptime, long batteryRealtime) { + public void plug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { } void detach() { @@ -3712,13 +3906,13 @@ public final class BatteryStatsImpl extends BatteryStats { mUnpluggables.add(this); } - public void unplug(long batteryUptime, long batteryRealtime) { + public void unplug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { mUnpluggedStartTime = getStartTimeToNowLocked(batteryUptime); mUnpluggedStarts = mStarts; mUnpluggedLaunches = mLaunches; } - public void plug(long batteryUptime, long batteryRealtime) { + public void plug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { } void detach() { @@ -4367,7 +4561,7 @@ public final class BatteryStatsImpl extends BatteryStats { } mDischargeAmountScreenOn = 0; mDischargeAmountScreenOff = 0; - doUnplugLocked(mUnpluggedBatteryUptime, mUnpluggedBatteryRealtime); + doUnplugLocked(realtime, mUnpluggedBatteryUptime, mUnpluggedBatteryRealtime); } else { updateKernelWakelocksLocked(); mHistoryCur.batteryLevel = (byte)level; @@ -4383,7 +4577,7 @@ public final class BatteryStatsImpl extends BatteryStats { mHighDischargeAmountSinceCharge += mDischargeUnplugLevel-level; } updateDischargeScreenLevelsLocked(mScreenOn, mScreenOn); - doPlugLocked(getBatteryUptimeLocked(uptime), getBatteryRealtimeLocked(realtime)); + doPlugLocked(realtime, getBatteryUptimeLocked(uptime), getBatteryRealtimeLocked(realtime)); } if (doWrite || (mLastWriteTime + (60 * 1000)) < mSecRealtime) { if (mFile != null) { @@ -5161,11 +5355,14 @@ public final class BatteryStatsImpl extends BatteryStats { } u.mAudioTurnedOn = false; if (in.readInt() != 0) { - u.mAudioTurnedOnTimer.readSummaryFromParcelLocked(in); + u.createAudioTurnedOnTimerLocked().readSummaryFromParcelLocked(in); } u.mVideoTurnedOn = false; if (in.readInt() != 0) { - u.mVideoTurnedOnTimer.readSummaryFromParcelLocked(in); + u.createVideoTurnedOnTimerLocked().readSummaryFromParcelLocked(in); + } + if (in.readInt() != 0) { + u.createVibratorOnTimerLocked().readSummaryFromParcelLocked(in); } if (in.readInt() != 0) { @@ -5367,6 +5564,12 @@ public final class BatteryStatsImpl extends BatteryStats { } else { out.writeInt(0); } + if (u.mVibratorOnTimer != null) { + out.writeInt(1); + u.mVibratorOnTimer.writeSummaryFromParcelLocked(out, NOWREAL); + } else { + out.writeInt(0); + } if (u.mUserActivityCounters == null) { out.writeInt(0); @@ -5753,7 +5956,7 @@ public final class BatteryStatsImpl extends BatteryStats { if (SystemProperties.getBoolean(PROP_QTAGUID_ENABLED, false)) { try { mNetworkSummaryCache = mNetworkStatsFactory.readNetworkStatsSummaryDev(); - } catch (IllegalStateException e) { + } catch (IOException e) { Log.wtf(TAG, "problem reading network stats", e); } } @@ -5777,7 +5980,7 @@ public final class BatteryStatsImpl extends BatteryStats { try { mNetworkDetailCache = mNetworkStatsFactory .readNetworkStatsDetail().groupedByUid(); - } catch (IllegalStateException e) { + } catch (IOException e) { Log.wtf(TAG, "problem reading network stats", e); } } diff --git a/core/java/com/android/internal/os/SomeArgs.java b/core/java/com/android/internal/os/SomeArgs.java index 88e58dc..6fb72f1 100644 --- a/core/java/com/android/internal/os/SomeArgs.java +++ b/core/java/com/android/internal/os/SomeArgs.java @@ -39,6 +39,7 @@ public final class SomeArgs { public Object arg2; public Object arg3; public Object arg4; + public Object arg5; public int argi1; public int argi2; public int argi3; @@ -85,6 +86,7 @@ public final class SomeArgs { arg2 = null; arg3 = null; arg4 = null; + arg5 = null; argi1 = 0; argi2 = 0; argi3 = 0; diff --git a/core/java/com/android/internal/os/ZygoteConnection.java b/core/java/com/android/internal/os/ZygoteConnection.java index d24513a..fd7e3b0 100644 --- a/core/java/com/android/internal/os/ZygoteConnection.java +++ b/core/java/com/android/internal/os/ZygoteConnection.java @@ -76,18 +76,6 @@ class ZygoteConnection { private final String peerSecurityContext; /** - * A long-lived reference to the original command socket used to launch - * this peer. If "peer wait" mode is specified, the process that requested - * the new VM instance intends to track the lifetime of the spawned instance - * via the command socket. In this case, the command socket is closed - * in the Zygote and placed here in the spawned instance so that it will - * not be collected and finalized. This field remains null at all times - * in the original Zygote process, and in all spawned processes where - * "peer-wait" mode was not requested. - */ - private static LocalSocket sPeerWaitSocket = null; - - /** * Constructs instance from connected socket. * * @param socket non-null; connected socket @@ -298,11 +286,6 @@ class ZygoteConnection { * <li> --rlimit=r,c,m<i>tuple of values for setrlimit() call. * <code>r</code> is the resource, <code>c</code> and <code>m</code> * are the settings for current and max value.</i> - * <li> --peer-wait indicates that the command socket should - * be inherited by (and set to close-on-exec in) the spawned process - * and used to track the lifetime of that process. The spawning process - * then exits. Without this flag, it is retained by the spawning process - * (and closed in the child) in expectation of a new spawn request. * <li> --classpath=<i>colon-separated classpath</i> indicates * that the specified class (which must b first non-flag argument) should * be loaded from jar files in the specified classpath. Incompatible with @@ -330,9 +313,6 @@ class ZygoteConnection { /** from --setgroups */ int[] gids; - /** from --peer-wait */ - boolean peerWait; - /** * From --enable-debugger, --enable-checkjni, --enable-assert, * --enable-safemode, and --enable-jni-logging. @@ -437,8 +417,6 @@ class ZygoteConnection { debugFlags |= Zygote.DEBUG_ENABLE_JNI_LOGGING; } else if (arg.equals("--enable-assert")) { debugFlags |= Zygote.DEBUG_ENABLE_ASSERT; - } else if (arg.equals("--peer-wait")) { - peerWait = true; } else if (arg.equals("--runtime-init")) { runtimeInit = true; } else if (arg.startsWith("--seinfo=")) { @@ -897,23 +875,8 @@ class ZygoteConnection { FileDescriptor[] descriptors, FileDescriptor pipeFd, PrintStream newStderr) throws ZygoteInit.MethodAndArgsCaller { - /* - * Close the socket, unless we're in "peer wait" mode, in which - * case it's used to track the liveness of this process. - */ - - if (parsedArgs.peerWait) { - try { - ZygoteInit.setCloseOnExec(mSocket.getFileDescriptor(), true); - sPeerWaitSocket = mSocket; - } catch (IOException ex) { - Log.e(TAG, "Zygote Child: error setting peer wait " - + "socket to be close-on-exec", ex); - } - } else { - closeSocket(); - ZygoteInit.closeServerSocket(); - } + closeSocket(); + ZygoteInit.closeServerSocket(); if (descriptors != null) { try { @@ -1044,18 +1007,6 @@ class ZygoteConnection { return true; } - /* - * If the peer wants to use the socket to wait on the - * newly spawned process, then we're all done. - */ - if (parsedArgs.peerWait) { - try { - mSocket.close(); - } catch (IOException ex) { - Log.e(TAG, "Zygote: error closing sockets", ex); - } - return true; - } return false; } diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java index 9e43749..7eddc9c 100644 --- a/core/java/com/android/internal/os/ZygoteInit.java +++ b/core/java/com/android/internal/os/ZygoteInit.java @@ -19,10 +19,8 @@ package com.android.internal.os; import static libcore.io.OsConstants.S_IRWXG; import static libcore.io.OsConstants.S_IRWXO; -import android.content.pm.ActivityInfo; import android.content.res.Resources; import android.content.res.TypedArray; -import android.graphics.drawable.Drawable; import android.net.LocalServerSocket; import android.os.Debug; import android.os.Process; @@ -88,12 +86,6 @@ public class ZygoteInit { static final int GC_LOOP_COUNT = 10; /** - * If true, zygote forks for each peer. If false, a select loop is used - * inside a single process. The latter is preferred. - */ - private static final boolean ZYGOTE_FORK_MODE = false; - - /** * The name of a resource file that contains classes to preload. */ private static final String PRELOADED_CLASSES = "preloaded-classes"; @@ -549,11 +541,7 @@ public class ZygoteInit { Log.i(TAG, "Accepting command socket connections"); - if (ZYGOTE_FORK_MODE) { - runForkMode(); - } else { - runSelectLoopMode(); - } + runSelectLoop(); closeServerSocket(); } catch (MethodAndArgsCaller caller) { @@ -566,44 +554,6 @@ public class ZygoteInit { } /** - * Runs the zygote in accept-and-fork mode. In this mode, each peer - * gets its own zygote spawner process. This code is retained for - * reference only. - * - * @throws MethodAndArgsCaller in a child process when a main() should - * be executed. - */ - private static void runForkMode() throws MethodAndArgsCaller { - while (true) { - ZygoteConnection peer = acceptCommandPeer(); - - int pid; - - pid = Zygote.fork(); - - if (pid == 0) { - // The child process should handle the peer requests - - // The child does not accept any more connections - try { - sServerSocket.close(); - } catch (IOException ex) { - Log.e(TAG, "Zygote Child: error closing sockets", ex); - } finally { - sServerSocket = null; - } - - peer.run(); - break; - } else if (pid > 0) { - peer.closeSocket(); - } else { - throw new RuntimeException("Error invoking fork()"); - } - } - } - - /** * Runs the zygote process's select loop. Accepts new connections as * they happen, and reads commands from connections one spawn-request's * worth at a time. @@ -611,9 +561,9 @@ public class ZygoteInit { * @throws MethodAndArgsCaller in a child process when a main() should * be executed. */ - private static void runSelectLoopMode() throws MethodAndArgsCaller { - ArrayList<FileDescriptor> fds = new ArrayList(); - ArrayList<ZygoteConnection> peers = new ArrayList(); + private static void runSelectLoop() throws MethodAndArgsCaller { + ArrayList<FileDescriptor> fds = new ArrayList<FileDescriptor>(); + ArrayList<ZygoteConnection> peers = new ArrayList<ZygoteConnection>(); FileDescriptor[] fdArray = new FileDescriptor[4]; fds.add(sServerSocket.getFileDescriptor()); @@ -734,17 +684,6 @@ public class ZygoteInit { throws IOException; /** - * Sets the permitted and effective capability sets of this process. - * - * @param permittedCapabilities permitted set - * @param effectiveCapabilities effective set - * @throws IOException on error - */ - static native void setCapabilities( - long permittedCapabilities, - long effectiveCapabilities) throws IOException; - - /** * Invokes select() on the provider array of file descriptors (selecting * for readability only). Array elements of null are ignored. * diff --git a/core/java/com/android/internal/policy/PolicyManager.java b/core/java/com/android/internal/policy/PolicyManager.java index 5274e54..462b3a9 100644 --- a/core/java/com/android/internal/policy/PolicyManager.java +++ b/core/java/com/android/internal/policy/PolicyManager.java @@ -22,8 +22,6 @@ import android.view.LayoutInflater; import android.view.Window; import android.view.WindowManagerPolicy; -import com.android.internal.policy.IPolicy; - /** * {@hide} */ diff --git a/core/java/com/android/internal/statusbar/StatusBarNotification.java b/core/java/com/android/internal/statusbar/StatusBarNotification.java index a91aa3c..23e87fc 100644 --- a/core/java/com/android/internal/statusbar/StatusBarNotification.java +++ b/core/java/com/android/internal/statusbar/StatusBarNotification.java @@ -21,23 +21,13 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.UserHandle; -/* -boolean clearable = !n.ongoingEvent && ((notification.flags & Notification.FLAG_NO_CLEAR) == 0); - - -// TODO: make this restriction do something smarter like never fill -// more than two screens. "Why would anyone need more than 80 characters." :-/ -final int maxTickerLen = 80; -if (truncatedTicker != null && truncatedTicker.length() > maxTickerLen) { - truncatedTicker = truncatedTicker.subSequence(0, maxTickerLen); -} -*/ - /** - * Class encapsulating a Notification. Sent by the NotificationManagerService to the IStatusBar (in System UI). + * Class encapsulating a Notification. Sent by the NotificationManagerService to clients including + * the IStatusBar (in System UI). */ public class StatusBarNotification implements Parcelable { public final String pkg; + public final String basePkg; public final int id; public final String tag; public final int uid; @@ -47,6 +37,7 @@ public class StatusBarNotification implements Parcelable { public final Notification notification; public final int score; public final UserHandle user; + public final long postTime; /** This is temporarily needed for the JB MR1 PDK. */ @Deprecated @@ -57,10 +48,23 @@ public class StatusBarNotification implements Parcelable { public StatusBarNotification(String pkg, int id, String tag, int uid, int initialPid, int score, Notification notification, UserHandle user) { + this(pkg, null, id, tag, uid, initialPid, score, notification, user); + } + + public StatusBarNotification(String pkg, String basePkg, int id, String tag, int uid, + int initialPid, int score, Notification notification, UserHandle user) { + this(pkg, basePkg, id, tag, uid, initialPid, score, notification, user, + System.currentTimeMillis()); + } + + public StatusBarNotification(String pkg, String basePkg, int id, String tag, int uid, + int initialPid, int score, Notification notification, UserHandle user, + long postTime) { if (pkg == null) throw new NullPointerException(); if (notification == null) throw new NullPointerException(); this.pkg = pkg; + this.basePkg = pkg; this.id = id; this.tag = tag; this.uid = uid; @@ -69,10 +73,13 @@ public class StatusBarNotification implements Parcelable { this.notification = notification; this.user = user; this.notification.setUser(user); + + this.postTime = postTime; } public StatusBarNotification(Parcel in) { this.pkg = in.readString(); + this.basePkg = in.readString(); this.id = in.readInt(); if (in.readInt() != 0) { this.tag = in.readString(); @@ -84,11 +91,13 @@ public class StatusBarNotification implements Parcelable { this.score = in.readInt(); this.notification = new Notification(in); this.user = UserHandle.readFromParcel(in); - this.notification.setUser(user); + this.notification.setUser(this.user); + this.postTime = in.readLong(); } public void writeToParcel(Parcel out, int flags) { out.writeString(this.pkg); + out.writeString(this.basePkg); out.writeInt(this.id); if (this.tag != null) { out.writeInt(1); @@ -101,6 +110,8 @@ public class StatusBarNotification implements Parcelable { out.writeInt(this.score); this.notification.writeToParcel(out, flags); user.writeToParcel(out, flags); + + out.writeLong(this.postTime); } public int describeContents() { @@ -123,14 +134,17 @@ public class StatusBarNotification implements Parcelable { @Override public StatusBarNotification clone() { - return new StatusBarNotification(this.pkg, this.id, this.tag, this.uid, this.initialPid, - this.score, this.notification.clone(), this.user); + return new StatusBarNotification(this.pkg, this.basePkg, + this.id, this.tag, this.uid, this.initialPid, + this.score, this.notification.clone(), this.user, this.postTime); } @Override public String toString() { - return "StatusBarNotification(pkg=" + pkg + " id=" + id + " tag=" + tag + " score=" + score - + " notn=" + notification + " user=" + user + ")"; + return String.format( + "StatusBarNotification(pkg=%s user=%s id=%d tag=%s score=%d: %s)", + this.pkg, this.user, this.id, this.tag, + this.score, this.notification); } public boolean isOngoing() { diff --git a/core/java/com/android/internal/util/FastXmlSerializer.java b/core/java/com/android/internal/util/FastXmlSerializer.java index 592a8fa..7a04080 100644 --- a/core/java/com/android/internal/util/FastXmlSerializer.java +++ b/core/java/com/android/internal/util/FastXmlSerializer.java @@ -50,6 +50,8 @@ public class FastXmlSerializer implements XmlSerializer { private static final int BUFFER_LEN = 8192; + private static String sSpace = " "; + private final char[] mText = new char[BUFFER_LEN]; private int mPos; @@ -59,8 +61,12 @@ public class FastXmlSerializer implements XmlSerializer { private CharsetEncoder mCharset; private ByteBuffer mBytes = ByteBuffer.allocate(BUFFER_LEN); + private boolean mIndent = false; private boolean mInTag; + private int mNesting = 0; + private boolean mLineStart = true; + private void append(char c) throws IOException { int pos = mPos; if (pos >= (BUFFER_LEN-1)) { @@ -113,6 +119,14 @@ public class FastXmlSerializer implements XmlSerializer { append(str, 0, str.length()); } + private void appendIndent(int indent) throws IOException { + indent *= 4; + if (indent > sSpace.length()) { + indent = sSpace.length(); + } + append(sSpace, 0, indent); + } + private void escapeAndAppendString(final String string) throws IOException { final int N = string.length(); final char NE = (char)ESCAPE_TABLE.length; @@ -161,6 +175,7 @@ public class FastXmlSerializer implements XmlSerializer { escapeAndAppendString(value); append('"'); + mLineStart = false; return this; } @@ -185,9 +200,13 @@ public class FastXmlSerializer implements XmlSerializer { public XmlSerializer endTag(String namespace, String name) throws IOException, IllegalArgumentException, IllegalStateException { + mNesting--; if (mInTag) { append(" />\n"); } else { + if (mIndent && mLineStart) { + appendIndent(mNesting); + } append("</"); if (namespace != null) { append(namespace); @@ -196,6 +215,7 @@ public class FastXmlSerializer implements XmlSerializer { append(name); append(">\n"); } + mLineStart = true; mInTag = false; return this; } @@ -278,6 +298,7 @@ public class FastXmlSerializer implements XmlSerializer { public void setFeature(String name, boolean state) throws IllegalArgumentException, IllegalStateException { if (name.equals("http://xmlpull.org/v1/doc/features.html#indent-output")) { + mIndent = true; return; } throw new UnsupportedOperationException(); @@ -325,6 +346,7 @@ public class FastXmlSerializer implements XmlSerializer { IllegalArgumentException, IllegalStateException { append("<?xml version='1.0' encoding='utf-8' standalone='" + (standalone ? "yes" : "no") + "' ?>\n"); + mLineStart = true; } public XmlSerializer startTag(String namespace, String name) throws IOException, @@ -332,6 +354,10 @@ public class FastXmlSerializer implements XmlSerializer { if (mInTag) { append(">\n"); } + if (mIndent) { + appendIndent(mNesting); + } + mNesting++; append('<'); if (namespace != null) { append(namespace); @@ -339,6 +365,7 @@ public class FastXmlSerializer implements XmlSerializer { } append(name); mInTag = true; + mLineStart = false; return this; } @@ -349,6 +376,9 @@ public class FastXmlSerializer implements XmlSerializer { mInTag = false; } escapeAndAppendString(buf, start, len); + if (mIndent) { + mLineStart = buf[start+len-1] == '\n'; + } return this; } @@ -359,6 +389,9 @@ public class FastXmlSerializer implements XmlSerializer { mInTag = false; } escapeAndAppendString(text); + if (mIndent) { + mLineStart = text.length() > 0 && (text.charAt(text.length()-1) == '\n'); + } return this; } diff --git a/core/java/com/android/internal/util/IndentingPrintWriter.java b/core/java/com/android/internal/util/IndentingPrintWriter.java index dd5918b..d01a817 100644 --- a/core/java/com/android/internal/util/IndentingPrintWriter.java +++ b/core/java/com/android/internal/util/IndentingPrintWriter.java @@ -21,29 +21,47 @@ import java.io.Writer; /** * Lightweight wrapper around {@link PrintWriter} that automatically indents - * newlines based on internal state. Delays writing indent until first actual - * write on a newline, enabling indent modification after newline. + * newlines based on internal state. It also automatically wraps long lines + * based on given line length. + * <p> + * Delays writing indent until first actual write on a newline, enabling indent + * modification after newline. */ public class IndentingPrintWriter extends PrintWriter { - private final String mIndent; + private final String mSingleIndent; + private final int mWrapLength; - private StringBuilder mBuilder = new StringBuilder(); - private char[] mCurrent; + /** Mutable version of current indent */ + private StringBuilder mIndentBuilder = new StringBuilder(); + /** Cache of current {@link #mIndentBuilder} value */ + private char[] mCurrentIndent; + /** Length of current line being built, excluding any indent */ + private int mCurrentLength; + + /** + * Flag indicating if we're currently sitting on an empty line, and that + * next write should be prefixed with the current indent. + */ private boolean mEmptyLine = true; - public IndentingPrintWriter(Writer writer, String indent) { + public IndentingPrintWriter(Writer writer, String singleIndent) { + this(writer, singleIndent, -1); + } + + public IndentingPrintWriter(Writer writer, String singleIndent, int wrapLength) { super(writer); - mIndent = indent; + mSingleIndent = singleIndent; + mWrapLength = wrapLength; } public void increaseIndent() { - mBuilder.append(mIndent); - mCurrent = null; + mIndentBuilder.append(mSingleIndent); + mCurrentIndent = null; } public void decreaseIndent() { - mBuilder.delete(0, mIndent.length()); - mCurrent = null; + mIndentBuilder.delete(0, mSingleIndent.length()); + mCurrentIndent = null; } public void printPair(String key, Object value) { @@ -52,33 +70,56 @@ public class IndentingPrintWriter extends PrintWriter { @Override public void write(char[] buf, int offset, int count) { + final int indentLength = mIndentBuilder.length(); final int bufferEnd = offset + count; int lineStart = offset; int lineEnd = offset; + + // March through incoming buffer looking for newlines while (lineEnd < bufferEnd) { char ch = buf[lineEnd++]; + mCurrentLength++; if (ch == '\n') { - writeIndent(); + maybeWriteIndent(); super.write(buf, lineStart, lineEnd - lineStart); lineStart = lineEnd; mEmptyLine = true; + mCurrentLength = 0; + } + + // Wrap if we've pushed beyond line length + if (mWrapLength > 0 && mCurrentLength >= mWrapLength - indentLength) { + if (!mEmptyLine) { + // Give ourselves a fresh line to work with + super.write('\n'); + mEmptyLine = true; + mCurrentLength = lineEnd - lineStart; + } else { + // We need more than a dedicated line, slice it hard + maybeWriteIndent(); + super.write(buf, lineStart, lineEnd - lineStart); + super.write('\n'); + mEmptyLine = true; + lineStart = lineEnd; + mCurrentLength = 0; + } } } if (lineStart != lineEnd) { - writeIndent(); + maybeWriteIndent(); super.write(buf, lineStart, lineEnd - lineStart); } } - private void writeIndent() { + private void maybeWriteIndent() { if (mEmptyLine) { mEmptyLine = false; - if (mBuilder.length() != 0) { - if (mCurrent == null) { - mCurrent = mBuilder.toString().toCharArray(); + if (mIndentBuilder.length() != 0) { + if (mCurrentIndent == null) { + mCurrentIndent = mIndentBuilder.toString().toCharArray(); } - super.write(mCurrent, 0, mCurrent.length); + super.write(mCurrentIndent, 0, mCurrentIndent.length); } } } diff --git a/core/java/com/android/internal/util/ProcFileReader.java b/core/java/com/android/internal/util/ProcFileReader.java index 72e1f0f..81571fe 100644 --- a/core/java/com/android/internal/util/ProcFileReader.java +++ b/core/java/com/android/internal/util/ProcFileReader.java @@ -19,6 +19,7 @@ package com.android.internal.util; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; +import java.net.ProtocolException; import java.nio.charset.Charsets; /** @@ -82,12 +83,15 @@ public class ProcFileReader implements Closeable { } /** - * Find buffer index of next token delimiter, usually space or newline. Will - * fill buffer as needed. + * Find buffer index of next token delimiter, usually space or newline. + * Fills buffer as needed. + * + * @return Index of next delimeter, otherwise -1 if no tokens remain on + * current line. */ private int nextTokenIndex() throws IOException { if (mLineFinished) { - throw new IOException("no tokens remaining on current line"); + return -1; } int i = 0; @@ -105,7 +109,7 @@ public class ProcFileReader implements Closeable { } } while (fillBuf() > 0); - throw new IOException("end of stream while looking for token boundary"); + throw new ProtocolException("End of stream while looking for token boundary"); } /** @@ -136,7 +140,7 @@ public class ProcFileReader implements Closeable { } } while (fillBuf() > 0); - throw new IOException("end of stream while looking for line boundary"); + throw new ProtocolException("End of stream while looking for line boundary"); } /** @@ -144,9 +148,11 @@ public class ProcFileReader implements Closeable { */ public String nextString() throws IOException { final int tokenIndex = nextTokenIndex(); - final String s = new String(mBuffer, 0, tokenIndex, Charsets.US_ASCII); - consumeBuf(tokenIndex + 1); - return s; + if (tokenIndex == -1) { + throw new ProtocolException("Missing required string"); + } else { + return parseAndConsumeString(tokenIndex); + } } /** @@ -154,6 +160,33 @@ public class ProcFileReader implements Closeable { */ public long nextLong() throws IOException { final int tokenIndex = nextTokenIndex(); + if (tokenIndex == -1) { + throw new ProtocolException("Missing required long"); + } else { + return parseAndConsumeLong(tokenIndex); + } + } + + /** + * Parse and return next token as base-10 encoded {@code long}, or return + * the given default value if no remaining tokens on current line. + */ + public long nextOptionalLong(long def) throws IOException { + final int tokenIndex = nextTokenIndex(); + if (tokenIndex == -1) { + return def; + } else { + return parseAndConsumeLong(tokenIndex); + } + } + + private String parseAndConsumeString(int tokenIndex) throws IOException { + final String s = new String(mBuffer, 0, tokenIndex, Charsets.US_ASCII); + consumeBuf(tokenIndex + 1); + return s; + } + + private long parseAndConsumeLong(int tokenIndex) throws IOException { final boolean negative = mBuffer[0] == '-'; // TODO: refactor into something like IntegralToString @@ -193,6 +226,7 @@ public class ProcFileReader implements Closeable { return (int) value; } + @Override public void close() throws IOException { mStream.close(); } diff --git a/core/java/com/android/internal/util/StateMachine.java b/core/java/com/android/internal/util/StateMachine.java index 0ea7b83..58d4aa7 100644 --- a/core/java/com/android/internal/util/StateMachine.java +++ b/core/java/com/android/internal/util/StateMachine.java @@ -27,6 +27,7 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Calendar; +import java.util.Collection; import java.util.HashMap; import java.util.Vector; @@ -35,7 +36,7 @@ import java.util.Vector; * * <p>The state machine defined here is a hierarchical state machine which processes messages * and can have states arranged hierarchically.</p> - * + * * <p>A state is a <code>State</code> object and must implement * <code>processMessage</code> and optionally <code>enter/exit/getName</code>. * The enter/exit methods are equivalent to the construction and destruction @@ -81,8 +82,8 @@ import java.util.Vector; * machine will cause <code>haltedProcessMessage</code> to be invoked.</p> * * <p>If it is desirable to completely stop the state machine call <code>quit</code> or - * <code>abort</code>. These will call <code>exit</code> of the current state and its parents, call - * <code>onQuiting</code> and then exit Thread/Loopers.</p> + * <code>quitNow</code>. These will call <code>exit</code> of the current state and its parents, + * call <code>onQuiting</code> and then exit Thread/Loopers.</p> * * <p>In addition to <code>processMessage</code> each <code>State</code> has * an <code>enter</code> method and <code>exit</exit> method which may be overridden.</p> @@ -148,7 +149,7 @@ class HelloWorld extends StateMachine { class State1 extends State { @Override public boolean processMessage(Message message) { - Log.d(TAG, "Hello World"); + log("Hello World"); return HANDLED; } } @@ -232,8 +233,6 @@ state mP2 { * <p>The implementation is below and also in StateMachineTest:</p> <code> class Hsm1 extends StateMachine { - private static final String TAG = "hsm1"; - public static final int CMD_1 = 1; public static final int CMD_2 = 2; public static final int CMD_3 = 3; @@ -241,16 +240,16 @@ class Hsm1 extends StateMachine { public static final int CMD_5 = 5; public static Hsm1 makeHsm1() { - Log.d(TAG, "makeHsm1 E"); + log("makeHsm1 E"); Hsm1 sm = new Hsm1("hsm1"); sm.start(); - Log.d(TAG, "makeHsm1 X"); + log("makeHsm1 X"); return sm; } Hsm1(String name) { super(name); - Log.d(TAG, "ctor E"); + log("ctor E"); // Add states, use indentation to show hierarchy addState(mP1); @@ -260,16 +259,16 @@ class Hsm1 extends StateMachine { // Set the initial state setInitialState(mS1); - Log.d(TAG, "ctor X"); + log("ctor X"); } class P1 extends State { @Override public void enter() { - Log.d(TAG, "mP1.enter"); + log("mP1.enter"); } @Override public boolean processMessage(Message message) { boolean retVal; - Log.d(TAG, "mP1.processMessage what=" + message.what); + log("mP1.processMessage what=" + message.what); switch(message.what) { case CMD_2: // CMD_2 will arrive in mS2 before CMD_3 @@ -286,16 +285,16 @@ class Hsm1 extends StateMachine { return retVal; } @Override public void exit() { - Log.d(TAG, "mP1.exit"); + log("mP1.exit"); } } class S1 extends State { @Override public void enter() { - Log.d(TAG, "mS1.enter"); + log("mS1.enter"); } @Override public boolean processMessage(Message message) { - Log.d(TAG, "S1.processMessage what=" + message.what); + log("S1.processMessage what=" + message.what); if (message.what == CMD_1) { // Transition to ourself to show that enter/exit is called transitionTo(mS1); @@ -306,17 +305,17 @@ class Hsm1 extends StateMachine { } } @Override public void exit() { - Log.d(TAG, "mS1.exit"); + log("mS1.exit"); } } class S2 extends State { @Override public void enter() { - Log.d(TAG, "mS2.enter"); + log("mS2.enter"); } @Override public boolean processMessage(Message message) { boolean retVal; - Log.d(TAG, "mS2.processMessage what=" + message.what); + log("mS2.processMessage what=" + message.what); switch(message.what) { case(CMD_2): sendMessage(obtainMessage(CMD_4)); @@ -334,17 +333,17 @@ class Hsm1 extends StateMachine { return retVal; } @Override public void exit() { - Log.d(TAG, "mS2.exit"); + log("mS2.exit"); } } class P2 extends State { @Override public void enter() { - Log.d(TAG, "mP2.enter"); + log("mP2.enter"); sendMessage(obtainMessage(CMD_5)); } @Override public boolean processMessage(Message message) { - Log.d(TAG, "P2.processMessage what=" + message.what); + log("P2.processMessage what=" + message.what); switch(message.what) { case(CMD_3): break; @@ -357,13 +356,13 @@ class Hsm1 extends StateMachine { return HANDLED; } @Override public void exit() { - Log.d(TAG, "mP2.exit"); + log("mP2.exit"); } } @Override void onHalting() { - Log.d(TAG, "halting"); + log("halting"); synchronized (this) { this.notifyAll(); } @@ -386,7 +385,7 @@ synchronize(hsm) { // wait for the messages to be handled hsm.wait(); } catch (InterruptedException e) { - Log.e(TAG, "exception while waiting " + e.getMessage()); + loge("exception while waiting " + e.getMessage()); } } </code> @@ -418,8 +417,7 @@ D/hsm1 ( 1999): halting </code> */ public class StateMachine { - - private static final String TAG = "StateMachine"; + // Name of the state machine and used as logging tag private String mName; /** Message.what value when quitting */ @@ -447,36 +445,44 @@ public class StateMachine { * {@hide} */ public static class LogRec { + private StateMachine mSm; private long mTime; private int mWhat; private String mInfo; - private State mState; - private State mOrgState; + private IState mState; + private IState mOrgState; + private IState mDstState; /** * Constructor * * @param msg - * @param state that handled the message + * @param state the state which handled the message * @param orgState is the first state the received the message but * did not processes the message. + * @param transToState is the state that was transitioned to after the message was + * processed. */ - LogRec(Message msg, String info, State state, State orgState) { - update(msg, info, state, orgState); + LogRec(StateMachine sm, Message msg, String info, IState state, IState orgState, + IState transToState) { + update(sm, msg, info, state, orgState, transToState); } /** * Update the information in the record. * @param state that handled the message - * @param orgState is the first state the received the message but - * did not processes the message. + * @param orgState is the first state the received the message + * @param dstState is the state that was the transition target when logging */ - public void update(Message msg, String info, State state, State orgState) { + public void update(StateMachine sm, Message msg, String info, IState state, IState orgState, + IState dstState) { + mSm = sm; mTime = System.currentTimeMillis(); mWhat = (msg != null) ? msg.what : 0; mInfo = info; mState = state; mOrgState = orgState; + mDstState = dstState; } /** @@ -503,32 +509,39 @@ public class StateMachine { /** * @return the state that handled this message */ - public State getState() { + public IState getState() { return mState; } /** - * @return the original state that received the message. + * @return the state destination state if a transition is occurring or null if none. */ - public State getOriginalState() { - return mOrgState; + public IState getDestState() { + return mDstState; } /** - * @return as string + * @return the original state that received the message. */ - public String toString(StateMachine sm) { + public IState getOriginalState() { + return mOrgState; + } + + @Override + public String toString() { StringBuilder sb = new StringBuilder(); sb.append("time="); Calendar c = Calendar.getInstance(); c.setTimeInMillis(mTime); sb.append(String.format("%tm-%td %tH:%tM:%tS.%tL", c, c, c, c, c, c)); - sb.append(" state="); + sb.append(" processed="); sb.append(mState == null ? "<null>" : mState.getName()); - sb.append(" orgState="); + sb.append(" org="); sb.append(mOrgState == null ? "<null>" : mOrgState.getName()); + sb.append(" dest="); + sb.append(mDstState == null ? "<null>" : mDstState.getName()); sb.append(" what="); - String what = sm.getWhatToString(mWhat); + String what = mSm != null ? mSm.getWhatToString(mWhat) : ""; if (TextUtils.isEmpty(what)) { sb.append(mWhat); sb.append("(0x"); @@ -537,7 +550,7 @@ public class StateMachine { } else { sb.append(what); } - if ( ! TextUtils.isEmpty(mInfo)) { + if (!TextUtils.isEmpty(mInfo)) { sb.append(" "); sb.append(mInfo); } @@ -560,10 +573,11 @@ public class StateMachine { private static final int DEFAULT_SIZE = 20; - private Vector<LogRec> mLogRecords = new Vector<LogRec>(); + private Vector<LogRec> mLogRecVector = new Vector<LogRec>(); private int mMaxSize = DEFAULT_SIZE; private int mOldestIndex = 0; private int mCount = 0; + private boolean mLogOnlyTransitions = false; /** * private constructor use add @@ -579,14 +593,22 @@ public class StateMachine { synchronized void setSize(int maxSize) { mMaxSize = maxSize; mCount = 0; - mLogRecords.clear(); + mLogRecVector.clear(); + } + + synchronized void setLogOnlyTransitions(boolean enable) { + mLogOnlyTransitions = enable; + } + + synchronized boolean logOnlyTransitions() { + return mLogOnlyTransitions; } /** * @return the number of recent records. */ synchronized int size() { - return mLogRecords.size(); + return mLogRecVector.size(); } /** @@ -600,7 +622,7 @@ public class StateMachine { * Clear the list of records. */ synchronized void cleanup() { - mLogRecords.clear(); + mLogRecVector.clear(); } /** @@ -616,7 +638,7 @@ public class StateMachine { if (nextIndex >= size()) { return null; } else { - return mLogRecords.get(nextIndex); + return mLogRecVector.get(nextIndex); } } @@ -628,23 +650,26 @@ public class StateMachine { * @param state that handled the message * @param orgState is the first state the received the message but * did not processes the message. + * @param transToState is the state that was transitioned to after the message was + * processed. + * */ - synchronized void add(Message msg, String messageInfo, State state, State orgState) { + synchronized void add(StateMachine sm, Message msg, String messageInfo, IState state, + IState orgState, IState transToState) { mCount += 1; - if (mLogRecords.size() < mMaxSize) { - mLogRecords.add(new LogRec(msg, messageInfo, state, orgState)); + if (mLogRecVector.size() < mMaxSize) { + mLogRecVector.add(new LogRec(sm, msg, messageInfo, state, orgState, transToState)); } else { - LogRec pmi = mLogRecords.get(mOldestIndex); + LogRec pmi = mLogRecVector.get(mOldestIndex); mOldestIndex += 1; if (mOldestIndex >= mMaxSize) { mOldestIndex = 0; } - pmi.update(msg, messageInfo, state, orgState); + pmi.update(sm, msg, messageInfo, state, orgState, transToState); } } } - private static class SmHandler extends Handler { /** The debug flag */ @@ -702,15 +727,13 @@ public class StateMachine { */ @Override public String toString() { - return "state=" + state.getName() + ",active=" + active - + ",parent=" + ((parentStateInfo == null) ? - "null" : parentStateInfo.state.getName()); + return "state=" + state.getName() + ",active=" + active + ",parent=" + + ((parentStateInfo == null) ? "null" : parentStateInfo.state.getName()); } } /** The map of all of the states in the state machine */ - private HashMap<State, StateInfo> mStateInfo = - new HashMap<State, StateInfo>(); + private HashMap<State, StateInfo> mStateInfo = new HashMap<State, StateInfo>(); /** The initial state that will process the first message */ private State mInitialState; @@ -750,66 +773,97 @@ public class StateMachine { */ @Override public final void handleMessage(Message msg) { - if (mDbg) Log.d(TAG, "handleMessage: E msg.what=" + msg.what); + if (mDbg) mSm.log("handleMessage: E msg.what=" + msg.what); /** Save the current message */ mMsg = msg; + /** State that processed the message */ + State msgProcessedState = null; if (mIsConstructionCompleted) { /** Normal path */ - processMsg(msg); - } else if (!mIsConstructionCompleted && - (mMsg.what == SM_INIT_CMD) && (mMsg.obj == mSmHandlerObj)) { + msgProcessedState = processMsg(msg); + } else if (!mIsConstructionCompleted && (mMsg.what == SM_INIT_CMD) + && (mMsg.obj == mSmHandlerObj)) { /** Initial one time path. */ mIsConstructionCompleted = true; invokeEnterMethods(0); } else { - throw new RuntimeException("StateMachine.handleMessage: " + - "The start method not called, received msg: " + msg); + throw new RuntimeException("StateMachine.handleMessage: " + + "The start method not called, received msg: " + msg); } - performTransitions(); + performTransitions(msgProcessedState, msg); - if (mDbg) Log.d(TAG, "handleMessage: X"); + // We need to check if mSm == null here as we could be quitting. + if (mDbg && mSm != null) mSm.log("handleMessage: X"); } /** * Do any transitions + * @param msgProcessedState is the state that processed the message */ - private void performTransitions() { + private void performTransitions(State msgProcessedState, Message msg) { /** * If transitionTo has been called, exit and then enter * the appropriate states. We loop on this to allow * enter and exit methods to use transitionTo. */ - State destState = null; - while (mDestState != null) { - if (mDbg) Log.d(TAG, "handleMessage: new destination call exit"); + State orgState = mStateStack[mStateStackTopIndex].state; - /** - * Save mDestState locally and set to null - * to know if enter/exit use transitionTo. - */ - destState = mDestState; - mDestState = null; + /** + * Record whether message needs to be logged before we transition and + * and we won't log special messages SM_INIT_CMD or SM_QUIT_CMD which + * always set msg.obj to the handler. + */ + boolean recordLogMsg = mSm.recordLogRec(mMsg) && (msg.obj != mSmHandlerObj); + if (mLogRecords.logOnlyTransitions()) { + /** Record only if there is a transition */ + if (mDestState != null) { + mLogRecords.add(mSm, mMsg, mSm.getLogRecString(mMsg), msgProcessedState, + orgState, mDestState); + } + } else if (recordLogMsg) { + /** Record message */ + mLogRecords.add(mSm, mMsg, mSm.getLogRecString(mMsg), msgProcessedState, orgState, + mDestState); + } + + State destState = mDestState; + if (destState != null) { /** - * Determine the states to exit and enter and return the - * common ancestor state of the enter/exit states. Then - * invoke the exit methods then the enter methods. + * Process the transitions including transitions in the enter/exit methods */ - StateInfo commonStateInfo = setupTempStateStackWithStatesToEnter(destState); - invokeExitMethods(commonStateInfo); - int stateStackEnteringIndex = moveTempStateStackToStateStack(); - invokeEnterMethods(stateStackEnteringIndex); + while (true) { + if (mDbg) mSm.log("handleMessage: new destination call exit/enter"); + + /** + * Determine the states to exit and enter and return the + * common ancestor state of the enter/exit states. Then + * invoke the exit methods then the enter methods. + */ + StateInfo commonStateInfo = setupTempStateStackWithStatesToEnter(destState); + invokeExitMethods(commonStateInfo); + int stateStackEnteringIndex = moveTempStateStackToStateStack(); + invokeEnterMethods(stateStackEnteringIndex); + /** + * Since we have transitioned to a new state we need to have + * any deferred messages moved to the front of the message queue + * so they will be processed before any other messages in the + * message queue. + */ + moveDeferredMessageAtFrontOfQueue(); - /** - * Since we have transitioned to a new state we need to have - * any deferred messages moved to the front of the message queue - * so they will be processed before any other messages in the - * message queue. - */ - moveDeferredMessageAtFrontOfQueue(); + if (destState != mDestState) { + // A new mDestState so continue looping + destState = mDestState; + } else { + // No change in mDestState so we're done + break; + } + } + mDestState = null; } /** @@ -860,7 +914,7 @@ public class StateMachine { * Complete the construction of the state machine. */ private final void completeConstruction() { - if (mDbg) Log.d(TAG, "completeConstruction: E"); + if (mDbg) mSm.log("completeConstruction: E"); /** * Determine the maximum depth of the state hierarchy @@ -876,7 +930,7 @@ public class StateMachine { maxDepth = depth; } } - if (mDbg) Log.d(TAG, "completeConstruction: maxDepth=" + maxDepth); + if (mDbg) mSm.log("completeConstruction: maxDepth=" + maxDepth); mStateStack = new StateInfo[maxDepth]; mTempStateStack = new StateInfo[maxDepth]; @@ -885,18 +939,19 @@ public class StateMachine { /** Sending SM_INIT_CMD message to invoke enter methods asynchronously */ sendMessageAtFrontOfQueue(obtainMessage(SM_INIT_CMD, mSmHandlerObj)); - if (mDbg) Log.d(TAG, "completeConstruction: X"); + if (mDbg) mSm.log("completeConstruction: X"); } /** * Process the message. If the current state doesn't handle * it, call the states parent and so on. If it is never handled then * call the state machines unhandledMessage method. + * @return the state that processed the message */ - private final void processMsg(Message msg) { + private final State processMsg(Message msg) { StateInfo curStateInfo = mStateStack[mStateStackTopIndex]; if (mDbg) { - Log.d(TAG, "processMsg: " + curStateInfo.state.getName()); + mSm.log("processMsg: " + curStateInfo.state.getName()); } if (isQuit(msg)) { @@ -915,23 +970,11 @@ public class StateMachine { break; } if (mDbg) { - Log.d(TAG, "processMsg: " + curStateInfo.state.getName()); - } - } - - /** - * Record that we processed the message - */ - if (mSm.recordLogRec(msg)) { - if (curStateInfo != null) { - State orgState = mStateStack[mStateStackTopIndex].state; - mLogRecords.add(msg, mSm.getLogRecString(msg), curStateInfo.state, - orgState); - } else { - mLogRecords.add(msg, mSm.getLogRecString(msg), null, null); + mSm.log("processMsg: " + curStateInfo.state.getName()); } } } + return (curStateInfo != null) ? curStateInfo.state : null; } /** @@ -939,10 +982,10 @@ public class StateMachine { * up to the common ancestor state. */ private final void invokeExitMethods(StateInfo commonStateInfo) { - while ((mStateStackTopIndex >= 0) && - (mStateStack[mStateStackTopIndex] != commonStateInfo)) { + while ((mStateStackTopIndex >= 0) + && (mStateStack[mStateStackTopIndex] != commonStateInfo)) { State curState = mStateStack[mStateStackTopIndex].state; - if (mDbg) Log.d(TAG, "invokeExitMethods: " + curState.getName()); + if (mDbg) mSm.log("invokeExitMethods: " + curState.getName()); curState.exit(); mStateStack[mStateStackTopIndex].active = false; mStateStackTopIndex -= 1; @@ -954,7 +997,7 @@ public class StateMachine { */ private final void invokeEnterMethods(int stateStackEnteringIndex) { for (int i = stateStackEnteringIndex; i <= mStateStackTopIndex; i++) { - if (mDbg) Log.d(TAG, "invokeEnterMethods: " + mStateStack[i].state.getName()); + if (mDbg) mSm.log("invokeEnterMethods: " + mStateStack[i].state.getName()); mStateStack[i].state.enter(); mStateStack[i].active = true; } @@ -970,9 +1013,9 @@ public class StateMachine { * as the most resent message and end with the oldest * messages at the front of the queue. */ - for (int i = mDeferredMessages.size() - 1; i >= 0; i-- ) { + for (int i = mDeferredMessages.size() - 1; i >= 0; i--) { Message curMsg = mDeferredMessages.get(i); - if (mDbg) Log.d(TAG, "moveDeferredMessageAtFrontOfQueue; what=" + curMsg.what); + if (mDbg) mSm.log("moveDeferredMessageAtFrontOfQueue; what=" + curMsg.what); sendMessageAtFrontOfQueue(curMsg); } mDeferredMessages.clear(); @@ -990,7 +1033,7 @@ public class StateMachine { int i = mTempStateStackCount - 1; int j = startingIndex; while (i >= 0) { - if (mDbg) Log.d(TAG, "moveTempStackToStateStack: i=" + i + ",j=" + j); + if (mDbg) mSm.log("moveTempStackToStateStack: i=" + i + ",j=" + j); mStateStack[j] = mTempStateStack[i]; j += 1; i -= 1; @@ -998,9 +1041,9 @@ public class StateMachine { mStateStackTopIndex = j - 1; if (mDbg) { - Log.d(TAG, "moveTempStackToStateStack: X mStateStackTop=" - + mStateStackTopIndex + ",startingIndex=" + startingIndex - + ",Top=" + mStateStack[mStateStackTopIndex].state.getName()); + mSm.log("moveTempStackToStateStack: X mStateStackTop=" + mStateStackTopIndex + + ",startingIndex=" + startingIndex + ",Top=" + + mStateStack[mStateStackTopIndex].state.getName()); } return startingIndex; } @@ -1031,8 +1074,8 @@ public class StateMachine { } while ((curStateInfo != null) && !curStateInfo.active); if (mDbg) { - Log.d(TAG, "setupTempStateStackWithStatesToEnter: X mTempStateStackCount=" - + mTempStateStackCount + ",curStateInfo: " + curStateInfo); + mSm.log("setupTempStateStackWithStatesToEnter: X mTempStateStackCount=" + + mTempStateStackCount + ",curStateInfo: " + curStateInfo); } return curStateInfo; } @@ -1042,8 +1085,7 @@ public class StateMachine { */ private final void setupInitialStateStack() { if (mDbg) { - Log.d(TAG, "setupInitialStateStack: E mInitialState=" - + mInitialState.getName()); + mSm.log("setupInitialStateStack: E mInitialState=" + mInitialState.getName()); } StateInfo curStateInfo = mStateInfo.get(mInitialState); @@ -1083,8 +1125,8 @@ public class StateMachine { */ private final StateInfo addState(State state, State parent) { if (mDbg) { - Log.d(TAG, "addStateInternal: E state=" + state.getName() - + ",parent=" + ((parent == null) ? "" : parent.getName())); + mSm.log("addStateInternal: E state=" + state.getName() + ",parent=" + + ((parent == null) ? "" : parent.getName())); } StateInfo parentStateInfo = null; if (parent != null) { @@ -1101,14 +1143,14 @@ public class StateMachine { } // Validate that we aren't adding the same state in two different hierarchies. - if ((stateInfo.parentStateInfo != null) && - (stateInfo.parentStateInfo != parentStateInfo)) { - throw new RuntimeException("state already added"); + if ((stateInfo.parentStateInfo != null) + && (stateInfo.parentStateInfo != parentStateInfo)) { + throw new RuntimeException("state already added"); } stateInfo.state = state; stateInfo.parentStateInfo = parentStateInfo; stateInfo.active = false; - if (mDbg) Log.d(TAG, "addStateInternal: X stateInfo: " + stateInfo); + if (mDbg) mSm.log("addStateInternal: X stateInfo: " + stateInfo); return stateInfo; } @@ -1128,19 +1170,19 @@ public class StateMachine { /** @see StateMachine#setInitialState(State) */ private final void setInitialState(State initialState) { - if (mDbg) Log.d(TAG, "setInitialState: initialState=" + initialState.getName()); + if (mDbg) mSm.log("setInitialState: initialState=" + initialState.getName()); mInitialState = initialState; } /** @see StateMachine#transitionTo(IState) */ private final void transitionTo(IState destState) { mDestState = (State) destState; - if (mDbg) Log.d(TAG, "transitionTo: destState=" + mDestState.getName()); + if (mDbg) mSm.log("transitionTo: destState=" + mDestState.getName()); } /** @see StateMachine#deferMessage(Message) */ private final void deferMessage(Message msg) { - if (mDbg) Log.d(TAG, "deferMessage: msg=" + msg.what); + if (mDbg) mSm.log("deferMessage: msg=" + msg.what); /* Copy the "msg" to "newMsg" as "msg" will be recycled */ Message newMsg = obtainMessage(); @@ -1151,17 +1193,17 @@ public class StateMachine { /** @see StateMachine#quit() */ private final void quit() { - if (mDbg) Log.d(TAG, "quit:"); + if (mDbg) mSm.log("quit:"); sendMessage(obtainMessage(SM_QUIT_CMD, mSmHandlerObj)); } /** @see StateMachine#quitNow() */ private final void quitNow() { - if (mDbg) Log.d(TAG, "abort:"); + if (mDbg) mSm.log("quitNow:"); sendMessageAtFrontOfQueue(obtainMessage(SM_QUIT_CMD, mSmHandlerObj)); } - /** Validate that the message was sent by quit or abort. */ + /** Validate that the message was sent by quit or quitNow. */ private final boolean isQuit(Message msg) { return (msg.what == SM_QUIT_CMD) && (msg.obj == mSmHandlerObj); } @@ -1224,20 +1266,6 @@ public class StateMachine { } /** - * @return current message - */ - protected final Message getCurrentMessage() { - return mSmHandler.getCurrentMessage(); - } - - /** - * @return current state - */ - protected final IState getCurrentState() { - return mSmHandler.getCurrentState(); - } - - /** * Add a new state to the state machine, parent will be null * @param state to add */ @@ -1256,6 +1284,26 @@ public class StateMachine { } /** + * @return current message + */ + protected final Message getCurrentMessage() { + // mSmHandler can be null if the state machine has quit. + SmHandler smh = mSmHandler; + if (smh == null) return null; + return smh.getCurrentMessage(); + } + + /** + * @return current state + */ + protected final IState getCurrentState() { + // mSmHandler can be null if the state machine has quit. + SmHandler smh = mSmHandler; + if (smh == null) return null; + return smh.getCurrentState(); + } + + /** * transition to destination state. Upon returning * from processMessage the current state's exit will * be executed and upon the next message arriving @@ -1303,7 +1351,7 @@ public class StateMachine { * @param msg that couldn't be handled. */ protected void unhandledMessage(Message msg) { - if (mSmHandler.mDbg) Log.e(TAG, mName + " - unhandledMessage: msg.what=" + msg.what); + if (mSmHandler.mDbg) loge(" - unhandledMessage: msg.what=" + msg.what); } /** @@ -1347,43 +1395,69 @@ public class StateMachine { } /** + * Set to log only messages that cause a state transition + * + * @param enable {@code true} to enable, {@code false} to disable + */ + public final void setLogOnlyTransitions(boolean enable) { + mSmHandler.mLogRecords.setLogOnlyTransitions(enable); + } + + /** * @return number of log records */ public final int getLogRecSize() { - return mSmHandler.mLogRecords.size(); + // mSmHandler can be null if the state machine has quit. + SmHandler smh = mSmHandler; + if (smh == null) return 0; + return smh.mLogRecords.size(); } /** * @return the total number of records processed */ public final int getLogRecCount() { - return mSmHandler.mLogRecords.count(); + // mSmHandler can be null if the state machine has quit. + SmHandler smh = mSmHandler; + if (smh == null) return 0; + return smh.mLogRecords.count(); } /** - * @return a log record + * @return a log record, or null if index is out of range */ public final LogRec getLogRec(int index) { - return mSmHandler.mLogRecords.get(index); + // mSmHandler can be null if the state machine has quit. + SmHandler smh = mSmHandler; + if (smh == null) return null; + return smh.mLogRecords.get(index); } /** - * Add the string to LogRecords. - * - * @param string + * @return a copy of LogRecs as a collection */ - protected void addLogRec(String string) { - mSmHandler.mLogRecords.add(null, string, null, null); + public final Collection<LogRec> copyLogRecs() { + Vector<LogRec> vlr = new Vector<LogRec>(); + SmHandler smh = mSmHandler; + if (smh != null) { + for (LogRec lr : smh.mLogRecords.mLogRecVector) { + vlr.add(lr); + } + } + return vlr; } /** - * Add the string and state to LogRecords + * Add the string to LogRecords. * * @param string - * @param state current state */ - protected void addLogRec(String string, State state) { - mSmHandler.mLogRecords.add(null, string, state, null); + protected void addLogRec(String string) { + // mSmHandler can be null if the state machine has quit. + SmHandler smh = mSmHandler; + if (smh == null) return; + smh.mLogRecords.add(this, smh.getCurrentMessage(), string, smh.getCurrentState(), + smh.mStateStack[smh.mStateStackTopIndex].state, smh.mDestState); } /** @@ -1412,168 +1486,213 @@ public class StateMachine { } /** - * @return Handler + * @return Handler, maybe null if state machine has quit. */ public final Handler getHandler() { return mSmHandler; } /** - * Get a message and set Message.target = this. + * Get a message and set Message.target state machine handler. * - * @return message or null if SM has quit + * Note: The handler can be null if the state machine has quit, + * which means target will be null and may cause a AndroidRuntimeException + * in MessageQueue#enqueMessage if sent directly or if sent using + * StateMachine#sendMessage the message will just be ignored. + * + * @return A Message object from the global pool */ - public final Message obtainMessage() - { - if (mSmHandler == null) return null; - + public final Message obtainMessage() { return Message.obtain(mSmHandler); } /** - * Get a message and set Message.target = this and what + * Get a message and set Message.target state machine handler, what. + * + * Note: The handler can be null if the state machine has quit, + * which means target will be null and may cause a AndroidRuntimeException + * in MessageQueue#enqueMessage if sent directly or if sent using + * StateMachine#sendMessage the message will just be ignored. * * @param what is the assigned to Message.what. - * @return message or null if SM has quit + * @return A Message object from the global pool */ public final Message obtainMessage(int what) { - if (mSmHandler == null) return null; - return Message.obtain(mSmHandler, what); } /** - * Get a message and set Message.target = this, + * Get a message and set Message.target state machine handler, * what and obj. * + * Note: The handler can be null if the state machine has quit, + * which means target will be null and may cause a AndroidRuntimeException + * in MessageQueue#enqueMessage if sent directly or if sent using + * StateMachine#sendMessage the message will just be ignored. + * * @param what is the assigned to Message.what. * @param obj is assigned to Message.obj. - * @return message or null if SM has quit + * @return A Message object from the global pool */ - public final Message obtainMessage(int what, Object obj) - { - if (mSmHandler == null) return null; - + public final Message obtainMessage(int what, Object obj) { return Message.obtain(mSmHandler, what, obj); } /** - * Get a message and set Message.target = this, + * Get a message and set Message.target state machine handler, * what, arg1 and arg2 * + * Note: The handler can be null if the state machine has quit, + * which means target will be null and may cause a AndroidRuntimeException + * in MessageQueue#enqueMessage if sent directly or if sent using + * StateMachine#sendMessage the message will just be ignored. + * * @param what is assigned to Message.what * @param arg1 is assigned to Message.arg1 * @param arg2 is assigned to Message.arg2 - * @return A Message object from the global pool or null if - * SM has quit + * @return A Message object from the global pool */ - public final Message obtainMessage(int what, int arg1, int arg2) - { - if (mSmHandler == null) return null; - + public final Message obtainMessage(int what, int arg1, int arg2) { return Message.obtain(mSmHandler, what, arg1, arg2); } /** - * Get a message and set Message.target = this, + * Get a message and set Message.target state machine handler, * what, arg1, arg2 and obj * + * Note: The handler can be null if the state machine has quit, + * which means target will be null and may cause a AndroidRuntimeException + * in MessageQueue#enqueMessage if sent directly or if sent using + * StateMachine#sendMessage the message will just be ignored. + * * @param what is assigned to Message.what * @param arg1 is assigned to Message.arg1 * @param arg2 is assigned to Message.arg2 * @param obj is assigned to Message.obj - * @return A Message object from the global pool or null if - * SM has quit + * @return A Message object from the global pool */ - public final Message obtainMessage(int what, int arg1, int arg2, Object obj) - { - if (mSmHandler == null) return null; - + public final Message obtainMessage(int what, int arg1, int arg2, Object obj) { return Message.obtain(mSmHandler, what, arg1, arg2, obj); } /** * Enqueue a message to this state machine. + * + * Message is ignored if state machine has quit. */ public final void sendMessage(int what) { // mSmHandler can be null if the state machine has quit. - if (mSmHandler == null) return; + SmHandler smh = mSmHandler; + if (smh == null) return; - mSmHandler.sendMessage(obtainMessage(what)); + smh.sendMessage(obtainMessage(what)); } /** * Enqueue a message to this state machine. + * + * Message is ignored if state machine has quit. */ public final void sendMessage(int what, Object obj) { // mSmHandler can be null if the state machine has quit. - if (mSmHandler == null) return; + SmHandler smh = mSmHandler; + if (smh == null) return; - mSmHandler.sendMessage(obtainMessage(what,obj)); + smh.sendMessage(obtainMessage(what, obj)); } /** * Enqueue a message to this state machine. + * + * Message is ignored if state machine has quit. */ public final void sendMessage(Message msg) { // mSmHandler can be null if the state machine has quit. - if (mSmHandler == null) return; + SmHandler smh = mSmHandler; + if (smh == null) return; - mSmHandler.sendMessage(msg); + smh.sendMessage(msg); } /** * Enqueue a message to this state machine after a delay. + * + * Message is ignored if state machine has quit. */ public final void sendMessageDelayed(int what, long delayMillis) { // mSmHandler can be null if the state machine has quit. - if (mSmHandler == null) return; + SmHandler smh = mSmHandler; + if (smh == null) return; - mSmHandler.sendMessageDelayed(obtainMessage(what), delayMillis); + smh.sendMessageDelayed(obtainMessage(what), delayMillis); } /** * Enqueue a message to this state machine after a delay. + * + * Message is ignored if state machine has quit. */ public final void sendMessageDelayed(int what, Object obj, long delayMillis) { // mSmHandler can be null if the state machine has quit. - if (mSmHandler == null) return; + SmHandler smh = mSmHandler; + if (smh == null) return; - mSmHandler.sendMessageDelayed(obtainMessage(what, obj), delayMillis); + smh.sendMessageDelayed(obtainMessage(what, obj), delayMillis); } /** * Enqueue a message to this state machine after a delay. + * + * Message is ignored if state machine has quit. */ public final void sendMessageDelayed(Message msg, long delayMillis) { // mSmHandler can be null if the state machine has quit. - if (mSmHandler == null) return; + SmHandler smh = mSmHandler; + if (smh == null) return; - mSmHandler.sendMessageDelayed(msg, delayMillis); + smh.sendMessageDelayed(msg, delayMillis); } /** * Enqueue a message to the front of the queue for this state machine. * Protected, may only be called by instances of StateMachine. + * + * Message is ignored if state machine has quit. */ protected final void sendMessageAtFrontOfQueue(int what, Object obj) { - mSmHandler.sendMessageAtFrontOfQueue(obtainMessage(what, obj)); + // mSmHandler can be null if the state machine has quit. + SmHandler smh = mSmHandler; + if (smh == null) return; + + smh.sendMessageAtFrontOfQueue(obtainMessage(what, obj)); } /** * Enqueue a message to the front of the queue for this state machine. * Protected, may only be called by instances of StateMachine. + * + * Message is ignored if state machine has quit. */ protected final void sendMessageAtFrontOfQueue(int what) { - mSmHandler.sendMessageAtFrontOfQueue(obtainMessage(what)); + // mSmHandler can be null if the state machine has quit. + SmHandler smh = mSmHandler; + if (smh == null) return; + + smh.sendMessageAtFrontOfQueue(obtainMessage(what)); } /** * Enqueue a message to the front of the queue for this state machine. * Protected, may only be called by instances of StateMachine. + * + * Message is ignored if state machine has quit. */ protected final void sendMessageAtFrontOfQueue(Message msg) { - mSmHandler.sendMessageAtFrontOfQueue(msg); + // mSmHandler can be null if the state machine has quit. + SmHandler smh = mSmHandler; + if (smh == null) return; + + smh.sendMessageAtFrontOfQueue(msg); } /** @@ -1581,7 +1700,23 @@ public class StateMachine { * Protected, may only be called by instances of StateMachine. */ protected final void removeMessages(int what) { - mSmHandler.removeMessages(what); + // mSmHandler can be null if the state machine has quit. + SmHandler smh = mSmHandler; + if (smh == null) return; + + smh.removeMessages(what); + } + + /** + * Validate that the message was sent by + * {@link StateMachine#quit} or {@link StateMachine#quitNow}. + * */ + protected final boolean isQuit(Message msg) { + // mSmHandler can be null if the state machine has quit. + SmHandler smh = mSmHandler; + if (smh == null) return msg.what == SM_QUIT_CMD; + + return smh.isQuit(msg); } /** @@ -1589,9 +1724,10 @@ public class StateMachine { */ protected final void quit() { // mSmHandler can be null if the state machine is already stopped. - if (mSmHandler == null) return; + SmHandler smh = mSmHandler; + if (smh == null) return; - mSmHandler.quit(); + smh.quit(); } /** @@ -1599,9 +1735,10 @@ public class StateMachine { */ protected final void quitNow() { // mSmHandler can be null if the state machine is already stopped. - if (mSmHandler == null) return; + SmHandler smh = mSmHandler; + if (smh == null) return; - mSmHandler.quitNow(); + smh.quitNow(); } /** @@ -1609,9 +1746,10 @@ public class StateMachine { */ public boolean isDbg() { // mSmHandler can be null if the state machine has quit. - if (mSmHandler == null) return false; + SmHandler smh = mSmHandler; + if (smh == null) return false; - return mSmHandler.isDbg(); + return smh.isDbg(); } /** @@ -1621,9 +1759,10 @@ public class StateMachine { */ public void setDbg(boolean dbg) { // mSmHandler can be null if the state machine has quit. - if (mSmHandler == null) return; + SmHandler smh = mSmHandler; + if (smh == null) return; - mSmHandler.setDbg(dbg); + smh.setDbg(dbg); } /** @@ -1631,10 +1770,11 @@ public class StateMachine { */ public void start() { // mSmHandler can be null if the state machine has quit. - if (mSmHandler == null) return; + SmHandler smh = mSmHandler; + if (smh == null) return; /** Send the complete construction message */ - mSmHandler.completeConstruction(); + smh.completeConstruction(); } /** @@ -1647,10 +1787,84 @@ public class StateMachine { public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println(getName() + ":"); pw.println(" total records=" + getLogRecCount()); - for (int i=0; i < getLogRecSize(); i++) { - pw.printf(" rec[%d]: %s\n", i, getLogRec(i).toString(this)); + for (int i = 0; i < getLogRecSize(); i++) { + pw.printf(" rec[%d]: %s\n", i, getLogRec(i).toString()); pw.flush(); } pw.println("curState=" + getCurrentState().getName()); } + + /** + * Log with debug and add to the LogRecords. + * + * @param s is string log + */ + protected void logAndAddLogRec(String s) { + addLogRec(s); + log(s); + } + + /** + * Log with debug + * + * @param s is string log + */ + protected void log(String s) { + Log.d(mName, s); + } + + /** + * Log with debug attribute + * + * @param s is string log + */ + protected void logd(String s) { + Log.d(mName, s); + } + + /** + * Log with verbose attribute + * + * @param s is string log + */ + protected void logv(String s) { + Log.v(mName, s); + } + + /** + * Log with info attribute + * + * @param s is string log + */ + protected void logi(String s) { + Log.i(mName, s); + } + + /** + * Log with warning attribute + * + * @param s is string log + */ + protected void logw(String s) { + Log.w(mName, s); + } + + /** + * Log with error attribute + * + * @param s is string log + */ + protected void loge(String s) { + Log.e(mName, s); + } + + /** + * Log with error attribute + * + * @param s is string log + * @param e is a Throwable which logs additional information. + */ + protected void loge(String s, Throwable e) { + Log.e(mName, s, e); + } } diff --git a/core/java/com/android/internal/util/XmlUtils.java b/core/java/com/android/internal/util/XmlUtils.java index 6a09fe0..93f6cf6 100644 --- a/core/java/com/android/internal/util/XmlUtils.java +++ b/core/java/com/android/internal/util/XmlUtils.java @@ -123,18 +123,15 @@ public class XmlUtils return Integer.parseInt(nm.substring(index), base) * sign; } - public static final int - convertValueToUnsignedInt(String value, int defaultValue) - { - if (null == value) + public static int convertValueToUnsignedInt(String value, int defaultValue) { + if (null == value) { return defaultValue; + } return parseUnsignedIntAttribute(value); } - public static final int - parseUnsignedIntAttribute(CharSequence charSeq) - { + public static int parseUnsignedIntAttribute(CharSequence charSeq) { String value = charSeq.toString(); long bits; diff --git a/core/java/com/android/internal/view/BaseIWindow.java b/core/java/com/android/internal/view/BaseIWindow.java index b76e89d..02bd4ac 100644 --- a/core/java/com/android/internal/view/BaseIWindow.java +++ b/core/java/com/android/internal/view/BaseIWindow.java @@ -34,7 +34,7 @@ public class BaseIWindow extends IWindow.Stub { } @Override - public void resized(Rect frame, Rect contentInsets, + public void resized(Rect frame, Rect overscanInsets, Rect contentInsets, Rect visibleInsets, boolean reportDraw, Configuration newConfig) { if (reportDraw) { try { diff --git a/core/java/com/android/internal/widget/ActionBarContainer.java b/core/java/com/android/internal/widget/ActionBarContainer.java index 0cfe4fd..6120a09 100644 --- a/core/java/com/android/internal/widget/ActionBarContainer.java +++ b/core/java/com/android/internal/widget/ActionBarContainer.java @@ -84,6 +84,10 @@ public class ActionBarContainer extends FrameLayout { mBackground = bg; if (bg != null) { bg.setCallback(this); + if (mActionBarView != null) { + mBackground.setBounds(mActionBarView.getLeft(), mActionBarView.getTop(), + mActionBarView.getRight(), mActionBarView.getBottom()); + } } setWillNotDraw(mIsSplit ? mSplitBackground == null : mBackground == null && mStackedBackground == null); @@ -98,6 +102,10 @@ public class ActionBarContainer extends FrameLayout { mStackedBackground = bg; if (bg != null) { bg.setCallback(this); + if ((mIsStacked && mStackedBackground != null)) { + mStackedBackground.setBounds(mTabContainer.getLeft(), mTabContainer.getTop(), + mTabContainer.getRight(), mTabContainer.getBottom()); + } } setWillNotDraw(mIsSplit ? mSplitBackground == null : mBackground == null && mStackedBackground == null); @@ -112,6 +120,9 @@ public class ActionBarContainer extends FrameLayout { mSplitBackground = bg; if (bg != null) { bg.setCallback(this); + if (mIsSplit && mSplitBackground != null) { + mSplitBackground.setBounds(0, 0, getMeasuredWidth(), getMeasuredHeight()); + } } setWillNotDraw(mIsSplit ? mSplitBackground == null : mBackground == null && mStackedBackground == null); diff --git a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java index a129496..18a696e 100644 --- a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java +++ b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java @@ -41,7 +41,7 @@ public class ActionBarOverlayLayout extends FrameLayout { private ActionBarView mActionView; private View mActionBarBottom; private int mLastSystemUiVisibility; - private final Rect mZeroRect = new Rect(0, 0, 0, 0); + private final Rect mLocalInsets = new Rect(); static final int[] mActionBarSizeAttr = new int [] { com.android.internal.R.attr.actionBarSize @@ -165,13 +165,8 @@ public class ActionBarOverlayLayout extends FrameLayout { // make sure its content is not being covered by system UI... though it // will still be covered by the action bar since they have requested it to // overlay. - if ((vis & SYSTEM_UI_LAYOUT_FLAGS) == 0) { - changed |= applyInsets(mContent, insets, true, true, true, true); - // The insets are now consumed. - insets.set(0, 0, 0, 0); - } else { - changed |= applyInsets(mContent, mZeroRect, true, true, true, true); - } + boolean res = computeFitSystemWindows(insets, mLocalInsets); + changed |= applyInsets(mContent, mLocalInsets, true, true, true, true); if (stable || mActionBarTop.getVisibility() == VISIBLE) { @@ -190,7 +185,7 @@ public class ActionBarOverlayLayout extends FrameLayout { if (mActionView.isSplitActionBar()) { if (stable || (mActionBarBottom != null && mActionBarBottom.getVisibility() == VISIBLE)) { - // If action bar is split, adjust buttom insets for it. + // If action bar is split, adjust bottom insets for it. insets.bottom += mActionBarHeight; } } @@ -199,7 +194,8 @@ public class ActionBarOverlayLayout extends FrameLayout { requestLayout(); } - return super.fitSystemWindows(insets); + super.fitSystemWindows(insets); + return true; } void pullChildren() { diff --git a/core/java/com/android/internal/widget/ActionBarView.java b/core/java/com/android/internal/widget/ActionBarView.java index 0f964b9..6bb7ac7 100644 --- a/core/java/com/android/internal/widget/ActionBarView.java +++ b/core/java/com/android/internal/widget/ActionBarView.java @@ -719,6 +719,7 @@ public class ActionBarView extends AbsActionBarView { if (mSpinner == null) { mSpinner = new Spinner(mContext, null, com.android.internal.R.attr.actionDropDownStyle); + mSpinner.setId(com.android.internal.R.id.action_bar_spinner); mListNavLayout = new LinearLayout(mContext, null, com.android.internal.R.attr.actionBarTabBarStyle); LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( diff --git a/core/java/com/android/internal/widget/DigitalClock.java b/core/java/com/android/internal/widget/DigitalClock.java deleted file mode 100644 index af3fd42..0000000 --- a/core/java/com/android/internal/widget/DigitalClock.java +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.internal.widget; - -import com.android.internal.R; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.database.ContentObserver; -import android.graphics.Typeface; -import android.os.Handler; -import android.provider.Settings; -import android.text.format.DateFormat; -import android.util.AttributeSet; -import android.view.View; -import android.widget.RelativeLayout; -import android.widget.TextView; - -import java.lang.ref.WeakReference; -import java.text.DateFormatSymbols; -import java.util.Calendar; - -/** - * Displays the time - */ -public class DigitalClock extends RelativeLayout { - - private static final String SYSTEM = "/system/fonts/"; - private static final String SYSTEM_FONT_TIME_BACKGROUND = SYSTEM + "AndroidClock.ttf"; - private static final String SYSTEM_FONT_TIME_FOREGROUND = SYSTEM + "AndroidClock_Highlight.ttf"; - private final static String M12 = "h:mm"; - private final static String M24 = "kk:mm"; - - private Calendar mCalendar; - private String mFormat; - private TextView mTimeDisplayBackground; - private TextView mTimeDisplayForeground; - private AmPm mAmPm; - private ContentObserver mFormatChangeObserver; - private int mAttached = 0; // for debugging - tells us whether attach/detach is unbalanced - - /* called by system on minute ticks */ - private final Handler mHandler = new Handler(); - private BroadcastReceiver mIntentReceiver; - - private static final Typeface sBackgroundFont; - private static final Typeface sForegroundFont; - - static { - sBackgroundFont = Typeface.createFromFile(SYSTEM_FONT_TIME_BACKGROUND); - sForegroundFont = Typeface.createFromFile(SYSTEM_FONT_TIME_FOREGROUND); - } - - private static class TimeChangedReceiver extends BroadcastReceiver { - private WeakReference<DigitalClock> mClock; - private Context mContext; - - public TimeChangedReceiver(DigitalClock clock) { - mClock = new WeakReference<DigitalClock>(clock); - mContext = clock.getContext(); - } - - @Override - public void onReceive(Context context, Intent intent) { - // Post a runnable to avoid blocking the broadcast. - final boolean timezoneChanged = - intent.getAction().equals(Intent.ACTION_TIMEZONE_CHANGED); - final DigitalClock clock = mClock.get(); - if (clock != null) { - clock.mHandler.post(new Runnable() { - public void run() { - if (timezoneChanged) { - clock.mCalendar = Calendar.getInstance(); - } - clock.updateTime(); - } - }); - } else { - try { - mContext.unregisterReceiver(this); - } catch (RuntimeException e) { - // Shouldn't happen - } - } - } - }; - - static class AmPm { - private TextView mAmPmTextView; - private String mAmString, mPmString; - - AmPm(View parent, Typeface tf) { - // No longer used, uncomment if we decide to use AM/PM indicator again - // mAmPmTextView = (TextView) parent.findViewById(R.id.am_pm); - if (mAmPmTextView != null && tf != null) { - mAmPmTextView.setTypeface(tf); - } - - String[] ampm = new DateFormatSymbols().getAmPmStrings(); - mAmString = ampm[0]; - mPmString = ampm[1]; - } - - void setShowAmPm(boolean show) { - if (mAmPmTextView != null) { - mAmPmTextView.setVisibility(show ? View.VISIBLE : View.GONE); - } - } - - void setIsMorning(boolean isMorning) { - if (mAmPmTextView != null) { - mAmPmTextView.setText(isMorning ? mAmString : mPmString); - } - } - } - - private static class FormatChangeObserver extends ContentObserver { - private WeakReference<DigitalClock> mClock; - private Context mContext; - public FormatChangeObserver(DigitalClock clock) { - super(new Handler()); - mClock = new WeakReference<DigitalClock>(clock); - mContext = clock.getContext(); - } - @Override - public void onChange(boolean selfChange) { - DigitalClock digitalClock = mClock.get(); - if (digitalClock != null) { - digitalClock.setDateFormat(); - digitalClock.updateTime(); - } else { - try { - mContext.getContentResolver().unregisterContentObserver(this); - } catch (RuntimeException e) { - // Shouldn't happen - } - } - } - } - - public DigitalClock(Context context) { - this(context, null); - } - - public DigitalClock(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - - /* The time display consists of two tones. That's why we have two overlapping text views. */ - mTimeDisplayBackground = (TextView) findViewById(R.id.timeDisplayBackground); - mTimeDisplayBackground.setTypeface(sBackgroundFont); - mTimeDisplayBackground.setVisibility(View.INVISIBLE); - - mTimeDisplayForeground = (TextView) findViewById(R.id.timeDisplayForeground); - mTimeDisplayForeground.setTypeface(sForegroundFont); - mAmPm = new AmPm(this, null); - mCalendar = Calendar.getInstance(); - - setDateFormat(); - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - - mAttached++; - - /* monitor time ticks, time changed, timezone */ - if (mIntentReceiver == null) { - mIntentReceiver = new TimeChangedReceiver(this); - IntentFilter filter = new IntentFilter(); - filter.addAction(Intent.ACTION_TIME_TICK); - filter.addAction(Intent.ACTION_TIME_CHANGED); - filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); - mContext.registerReceiver(mIntentReceiver, filter); - } - - /* monitor 12/24-hour display preference */ - if (mFormatChangeObserver == null) { - mFormatChangeObserver = new FormatChangeObserver(this); - mContext.getContentResolver().registerContentObserver( - Settings.System.CONTENT_URI, true, mFormatChangeObserver); - } - - updateTime(); - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - - mAttached--; - - if (mIntentReceiver != null) { - mContext.unregisterReceiver(mIntentReceiver); - } - if (mFormatChangeObserver != null) { - mContext.getContentResolver().unregisterContentObserver( - mFormatChangeObserver); - } - - mFormatChangeObserver = null; - mIntentReceiver = null; - } - - void updateTime(Calendar c) { - mCalendar = c; - updateTime(); - } - - public void updateTime() { - mCalendar.setTimeInMillis(System.currentTimeMillis()); - - CharSequence newTime = DateFormat.format(mFormat, mCalendar); - mTimeDisplayBackground.setText(newTime); - mTimeDisplayForeground.setText(newTime); - mAmPm.setIsMorning(mCalendar.get(Calendar.AM_PM) == 0); - } - - private void setDateFormat() { - mFormat = android.text.format.DateFormat.is24HourFormat(getContext()) - ? M24 : M12; - mAmPm.setShowAmPm(mFormat.equals(M12)); - } -} diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index 907b52a..555c7c2 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -123,14 +123,14 @@ public class LockPatternUtils { */ public static final int ID_DEFAULT_STATUS_WIDGET = -2; - protected final static String LOCKOUT_PERMANENT_KEY = "lockscreen.lockedoutpermanently"; - protected final static String LOCKOUT_ATTEMPT_DEADLINE = "lockscreen.lockoutattemptdeadline"; - protected final static String PATTERN_EVER_CHOSEN_KEY = "lockscreen.patterneverchosen"; + public final static String LOCKOUT_PERMANENT_KEY = "lockscreen.lockedoutpermanently"; + public final static String LOCKOUT_ATTEMPT_DEADLINE = "lockscreen.lockoutattemptdeadline"; + public final static String PATTERN_EVER_CHOSEN_KEY = "lockscreen.patterneverchosen"; public final static String PASSWORD_TYPE_KEY = "lockscreen.password_type"; public static final String PASSWORD_TYPE_ALTERNATE_KEY = "lockscreen.password_type_alternate"; - protected final static String LOCK_PASSWORD_SALT_KEY = "lockscreen.password_salt"; - protected final static String DISABLE_LOCKSCREEN_KEY = "lockscreen.disabled"; - protected final static String LOCKSCREEN_OPTIONS = "lockscreen.options"; + public final static String LOCK_PASSWORD_SALT_KEY = "lockscreen.password_salt"; + public final static String DISABLE_LOCKSCREEN_KEY = "lockscreen.disabled"; + public final static String LOCKSCREEN_OPTIONS = "lockscreen.options"; public final static String LOCKSCREEN_BIOMETRIC_WEAK_FALLBACK = "lockscreen.biometric_weak_fallback"; public final static String BIOMETRIC_WEAK_EVER_CHOSEN_KEY @@ -138,7 +138,7 @@ public class LockPatternUtils { public final static String LOCKSCREEN_POWER_BUTTON_INSTANTLY_LOCKS = "lockscreen.power_button_instantly_locks"; - protected final static String PASSWORD_HISTORY_KEY = "lockscreen.passwordhistory"; + public final static String PASSWORD_HISTORY_KEY = "lockscreen.passwordhistory"; private final Context mContext; private final ContentResolver mContentResolver; diff --git a/core/java/com/android/internal/widget/LockSettingsService.java b/core/java/com/android/internal/widget/LockSettingsService.java deleted file mode 100644 index 4ecbd16..0000000 --- a/core/java/com/android/internal/widget/LockSettingsService.java +++ /dev/null @@ -1,405 +0,0 @@ -/* - * 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.internal.widget; - -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; -import android.os.Binder; -import android.os.Environment; -import android.os.RemoteException; -import android.os.SystemProperties; -import android.os.UserHandle; -import android.provider.Settings; -import android.provider.Settings.Secure; -import android.text.TextUtils; -import android.util.Slog; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.util.Arrays; - -/** - * Keeps the lock pattern/password data and related settings for each user. - * Used by LockPatternUtils. Needs to be a service because Settings app also needs - * to be able to save lockscreen information for secondary users. - * @hide - */ -public class LockSettingsService extends ILockSettings.Stub { - - private final DatabaseHelper mOpenHelper; - private static final String TAG = "LockSettingsService"; - - private static final String TABLE = "locksettings"; - private static final String COLUMN_KEY = "name"; - private static final String COLUMN_USERID = "user"; - private static final String COLUMN_VALUE = "value"; - - private static final String[] COLUMNS_FOR_QUERY = { - COLUMN_VALUE - }; - - private static final String SYSTEM_DIRECTORY = "/system/"; - private static final String LOCK_PATTERN_FILE = "gesture.key"; - private static final String LOCK_PASSWORD_FILE = "password.key"; - - private final Context mContext; - - public LockSettingsService(Context context) { - mContext = context; - // Open the database - mOpenHelper = new DatabaseHelper(mContext); - } - - public void systemReady() { - migrateOldData(); - } - - private void migrateOldData() { - try { - if (getString("migrated", null, 0) != null) { - // Already migrated - return; - } - - final ContentResolver cr = mContext.getContentResolver(); - for (String validSetting : VALID_SETTINGS) { - String value = Settings.Secure.getString(cr, validSetting); - if (value != null) { - setString(validSetting, value, 0); - } - } - // No need to move the password / pattern files. They're already in the right place. - setString("migrated", "true", 0); - Slog.i(TAG, "Migrated lock settings to new location"); - } catch (RemoteException re) { - Slog.e(TAG, "Unable to migrate old data"); - } - } - - private static final void checkWritePermission(int userId) { - final int callingUid = Binder.getCallingUid(); - if (UserHandle.getAppId(callingUid) != android.os.Process.SYSTEM_UID) { - throw new SecurityException("uid=" + callingUid - + " not authorized to write lock settings"); - } - } - - private static final void checkPasswordReadPermission(int userId) { - final int callingUid = Binder.getCallingUid(); - if (UserHandle.getAppId(callingUid) != android.os.Process.SYSTEM_UID) { - throw new SecurityException("uid=" + callingUid - + " not authorized to read lock password"); - } - } - - private static final void checkReadPermission(int userId) { - final int callingUid = Binder.getCallingUid(); - if (UserHandle.getAppId(callingUid) != android.os.Process.SYSTEM_UID - && UserHandle.getUserId(callingUid) != userId) { - throw new SecurityException("uid=" + callingUid - + " not authorized to read settings of user " + userId); - } - } - - @Override - public void setBoolean(String key, boolean value, int userId) throws RemoteException { - checkWritePermission(userId); - - writeToDb(key, value ? "1" : "0", userId); - } - - @Override - public void setLong(String key, long value, int userId) throws RemoteException { - checkWritePermission(userId); - - writeToDb(key, Long.toString(value), userId); - } - - @Override - public void setString(String key, String value, int userId) throws RemoteException { - checkWritePermission(userId); - - writeToDb(key, value, userId); - } - - @Override - public boolean getBoolean(String key, boolean defaultValue, int userId) throws RemoteException { - //checkReadPermission(userId); - - String value = readFromDb(key, null, userId); - return TextUtils.isEmpty(value) ? - defaultValue : (value.equals("1") || value.equals("true")); - } - - @Override - public long getLong(String key, long defaultValue, int userId) throws RemoteException { - //checkReadPermission(userId); - - String value = readFromDb(key, null, userId); - return TextUtils.isEmpty(value) ? defaultValue : Long.parseLong(value); - } - - @Override - public String getString(String key, String defaultValue, int userId) throws RemoteException { - //checkReadPermission(userId); - - return readFromDb(key, defaultValue, userId); - } - - private String getLockPatternFilename(int userId) { - String dataSystemDirectory = - android.os.Environment.getDataDirectory().getAbsolutePath() + - SYSTEM_DIRECTORY; - if (userId == 0) { - // Leave it in the same place for user 0 - return dataSystemDirectory + LOCK_PATTERN_FILE; - } else { - return new File(Environment.getUserSystemDirectory(userId), LOCK_PATTERN_FILE) - .getAbsolutePath(); - } - } - - private String getLockPasswordFilename(int userId) { - String dataSystemDirectory = - android.os.Environment.getDataDirectory().getAbsolutePath() + - SYSTEM_DIRECTORY; - if (userId == 0) { - // Leave it in the same place for user 0 - return dataSystemDirectory + LOCK_PASSWORD_FILE; - } else { - return new File(Environment.getUserSystemDirectory(userId), LOCK_PASSWORD_FILE) - .getAbsolutePath(); - } - } - - @Override - public boolean havePassword(int userId) throws RemoteException { - // Do we need a permissions check here? - - return new File(getLockPasswordFilename(userId)).length() > 0; - } - - @Override - public boolean havePattern(int userId) throws RemoteException { - // Do we need a permissions check here? - - return new File(getLockPatternFilename(userId)).length() > 0; - } - - @Override - public void setLockPattern(byte[] hash, int userId) throws RemoteException { - checkWritePermission(userId); - - writeFile(getLockPatternFilename(userId), hash); - } - - @Override - public boolean checkPattern(byte[] hash, int userId) throws RemoteException { - checkPasswordReadPermission(userId); - try { - // Read all the bytes from the file - RandomAccessFile raf = new RandomAccessFile(getLockPatternFilename(userId), "r"); - final byte[] stored = new byte[(int) raf.length()]; - int got = raf.read(stored, 0, stored.length); - raf.close(); - if (got <= 0) { - return true; - } - // Compare the hash from the file with the entered pattern's hash - return Arrays.equals(stored, hash); - } catch (FileNotFoundException fnfe) { - Slog.e(TAG, "Cannot read file " + fnfe); - return true; - } catch (IOException ioe) { - Slog.e(TAG, "Cannot read file " + ioe); - return true; - } - } - - @Override - public void setLockPassword(byte[] hash, int userId) throws RemoteException { - checkWritePermission(userId); - - writeFile(getLockPasswordFilename(userId), hash); - } - - @Override - public boolean checkPassword(byte[] hash, int userId) throws RemoteException { - checkPasswordReadPermission(userId); - - try { - // Read all the bytes from the file - RandomAccessFile raf = new RandomAccessFile(getLockPasswordFilename(userId), "r"); - final byte[] stored = new byte[(int) raf.length()]; - int got = raf.read(stored, 0, stored.length); - raf.close(); - if (got <= 0) { - return true; - } - // Compare the hash from the file with the entered password's hash - return Arrays.equals(stored, hash); - } catch (FileNotFoundException fnfe) { - Slog.e(TAG, "Cannot read file " + fnfe); - return true; - } catch (IOException ioe) { - Slog.e(TAG, "Cannot read file " + ioe); - return true; - } - } - - @Override - public void removeUser(int userId) { - checkWritePermission(userId); - - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - try { - File file = new File(getLockPasswordFilename(userId)); - if (file.exists()) { - file.delete(); - } - file = new File(getLockPatternFilename(userId)); - if (file.exists()) { - file.delete(); - } - - db.beginTransaction(); - db.delete(TABLE, COLUMN_USERID + "='" + userId + "'", null); - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } - - private void writeFile(String name, byte[] hash) { - try { - // Write the hash to file - RandomAccessFile raf = new RandomAccessFile(name, "rw"); - // Truncate the file if pattern is null, to clear the lock - if (hash == null || hash.length == 0) { - raf.setLength(0); - } else { - raf.write(hash, 0, hash.length); - } - raf.close(); - } catch (IOException ioe) { - Slog.e(TAG, "Error writing to file " + ioe); - } - } - - private void writeToDb(String key, String value, int userId) { - writeToDb(mOpenHelper.getWritableDatabase(), key, value, userId); - } - - private void writeToDb(SQLiteDatabase db, String key, String value, int userId) { - ContentValues cv = new ContentValues(); - cv.put(COLUMN_KEY, key); - cv.put(COLUMN_USERID, userId); - cv.put(COLUMN_VALUE, value); - - db.beginTransaction(); - try { - db.delete(TABLE, COLUMN_KEY + "=? AND " + COLUMN_USERID + "=?", - new String[] {key, Integer.toString(userId)}); - db.insert(TABLE, null, cv); - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } - - private String readFromDb(String key, String defaultValue, int userId) { - Cursor cursor; - String result = defaultValue; - SQLiteDatabase db = mOpenHelper.getReadableDatabase(); - if ((cursor = db.query(TABLE, COLUMNS_FOR_QUERY, - COLUMN_USERID + "=? AND " + COLUMN_KEY + "=?", - new String[] { Integer.toString(userId), key }, - null, null, null)) != null) { - if (cursor.moveToFirst()) { - result = cursor.getString(0); - } - cursor.close(); - } - return result; - } - - class DatabaseHelper extends SQLiteOpenHelper { - private static final String TAG = "LockSettingsDB"; - private static final String DATABASE_NAME = "locksettings.db"; - - private static final int DATABASE_VERSION = 1; - - public DatabaseHelper(Context context) { - super(context, DATABASE_NAME, null, DATABASE_VERSION); - setWriteAheadLoggingEnabled(true); - } - - private void createTable(SQLiteDatabase db) { - db.execSQL("CREATE TABLE " + TABLE + " (" + - "_id INTEGER PRIMARY KEY AUTOINCREMENT," + - COLUMN_KEY + " TEXT," + - COLUMN_USERID + " INTEGER," + - COLUMN_VALUE + " TEXT" + - ");"); - } - - @Override - public void onCreate(SQLiteDatabase db) { - createTable(db); - initializeDefaults(db); - } - - private void initializeDefaults(SQLiteDatabase db) { - // Get the lockscreen default from a system property, if available - boolean lockScreenDisable = SystemProperties.getBoolean("ro.lockscreen.disable.default", - false); - if (lockScreenDisable) { - writeToDb(db, LockPatternUtils.DISABLE_LOCKSCREEN_KEY, "1", 0); - } - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) { - // Nothing yet - } - } - - private static final String[] VALID_SETTINGS = new String[] { - LockPatternUtils.LOCKOUT_PERMANENT_KEY, - LockPatternUtils.LOCKOUT_ATTEMPT_DEADLINE, - LockPatternUtils.PATTERN_EVER_CHOSEN_KEY, - LockPatternUtils.PASSWORD_TYPE_KEY, - LockPatternUtils.PASSWORD_TYPE_ALTERNATE_KEY, - LockPatternUtils.LOCK_PASSWORD_SALT_KEY, - LockPatternUtils.DISABLE_LOCKSCREEN_KEY, - LockPatternUtils.LOCKSCREEN_OPTIONS, - LockPatternUtils.LOCKSCREEN_BIOMETRIC_WEAK_FALLBACK, - LockPatternUtils.BIOMETRIC_WEAK_EVER_CHOSEN_KEY, - LockPatternUtils.LOCKSCREEN_POWER_BUTTON_INSTANTLY_LOCKS, - LockPatternUtils.PASSWORD_HISTORY_KEY, - Secure.LOCK_PATTERN_ENABLED, - Secure.LOCK_BIOMETRIC_WEAK_FLAGS, - Secure.LOCK_PATTERN_VISIBLE, - Secure.LOCK_PATTERN_TACTILE_FEEDBACK_ENABLED - }; -} diff --git a/core/java/com/android/internal/widget/SizeAdaptiveLayout.java b/core/java/com/android/internal/widget/SizeAdaptiveLayout.java index 273f6fe..ba113a3 100644 --- a/core/java/com/android/internal/widget/SizeAdaptiveLayout.java +++ b/core/java/com/android/internal/widget/SizeAdaptiveLayout.java @@ -225,13 +225,11 @@ public class SizeAdaptiveLayout extends ViewGroup { if (unboundedView != null) { tallestView = unboundedView; } - if (heightMode == MeasureSpec.UNSPECIFIED) { - return tallestView; - } - if (heightSize > tallestViewSize) { + if (heightMode == MeasureSpec.UNSPECIFIED || heightSize > tallestViewSize) { return tallestView; + } else { + return smallestView; } - return smallestView; } @Override @@ -272,10 +270,10 @@ public class SizeAdaptiveLayout extends ViewGroup { final int childWidth = mActiveChild.getMeasuredWidth(); final int childHeight = mActiveChild.getMeasuredHeight(); // TODO investigate setting LAYER_TYPE_HARDWARE on mLastActive - mActiveChild.layout(0, 0, 0 + childWidth, 0 + childHeight); + mActiveChild.layout(0, 0, childWidth, childHeight); if (DEBUG) Log.d(TAG, "got modesty offset of " + mModestyPanelTop); - mModestyPanel.layout(0, mModestyPanelTop, 0 + childWidth, mModestyPanelTop + childHeight); + mModestyPanel.layout(0, mModestyPanelTop, childWidth, mModestyPanelTop + childHeight); } @Override |