diff options
Diffstat (limited to 'core/java')
97 files changed, 4444 insertions, 2841 deletions
diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl index 7c41082..e53b313 100644 --- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl +++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl @@ -33,14 +33,14 @@ interface IAccessibilityServiceConnection { * Finds an {@link AccessibilityNodeInfo} by accessibility id. * * @param accessibilityWindowId A unique window id. - * @param accessibilityViewId A unique View accessibility id. + * @param accessibilityNodeId A unique view id or virtual descendant id. * @param interactionId The id of the interaction for matching with the callback result. * @param callback Callback which to receive the result. * @param threadId The id of the calling thread. * @return The current window scale, where zero means a failure. */ float findAccessibilityNodeInfoByAccessibilityId(int accessibilityWindowId, - int accessibilityViewId, int interactionId, + long accessibilityNodeId, int interactionId, IAccessibilityInteractionConnectionCallback callback, long threadId); /** @@ -51,15 +51,15 @@ interface IAccessibilityServiceConnection { * * @param text The searched text. * @param accessibilityWindowId A unique window id. - * @param accessibilityViewId A unique View accessibility id from where to start the search. - * Use {@link android.view.View#NO_ID} to start from the root. + * @param accessibilityNodeId A unique view id or virtual descendant id from + * where to start the search. Use {@link android.view.View#NO_ID} to start from the root. * @param interactionId The id of the interaction for matching with the callback result. * @param callback Callback which to receive the result. * @param threadId The id of the calling thread. * @return The current window scale, where zero means a failure. */ - float findAccessibilityNodeInfosByViewText(String text, int accessibilityWindowId, - int accessibilityViewId, int interractionId, + float findAccessibilityNodeInfosByText(String text, int accessibilityWindowId, + long accessibilityNodeId, int interractionId, IAccessibilityInteractionConnectionCallback callback, long threadId); /** @@ -75,7 +75,7 @@ interface IAccessibilityServiceConnection { * @param threadId The id of the calling thread. * @return The current window scale, where zero means a failure. */ - float findAccessibilityNodeInfosByViewTextInActiveWindow(String text, + float findAccessibilityNodeInfosByTextInActiveWindow(String text, int interactionId, IAccessibilityInteractionConnectionCallback callback, long threadId); @@ -96,14 +96,14 @@ interface IAccessibilityServiceConnection { * Performs an accessibility action on an {@link AccessibilityNodeInfo}. * * @param accessibilityWindowId The id of the window. - * @param accessibilityViewId A unique View accessibility id. + * @param accessibilityNodeId A unique view id or virtual descendant id. * @param action The action to perform. * @param interactionId The id of the interaction for matching with the callback result. * @param callback Callback which to receive the result. * @param threadId The id of the calling thread. * @return Whether the action was performed. */ - boolean performAccessibilityAction(int accessibilityWindowId, int accessibilityViewId, + boolean performAccessibilityAction(int accessibilityWindowId, long accessibilityNodeId, int action, int interactionId, IAccessibilityInteractionConnectionCallback callback, long threadId); } diff --git a/core/java/android/accounts/ChooseTypeAndAccountActivity.java b/core/java/android/accounts/ChooseTypeAndAccountActivity.java index c3c9d16..136c68c 100644 --- a/core/java/android/accounts/ChooseTypeAndAccountActivity.java +++ b/core/java/android/accounts/ChooseTypeAndAccountActivity.java @@ -216,7 +216,7 @@ public class ChooseTypeAndAccountActivity extends Activity if (mPendingRequest == REQUEST_NULL) { // If there are no allowable accounts go directly to add account - if (mAccountInfos.isEmpty()) { + if (shouldSkipToChooseAccountTypeFlow()) { startChooseAccountTypeActivity(); return; } @@ -265,6 +265,12 @@ public class ChooseTypeAndAccountActivity extends Activity mPendingRequest = REQUEST_NULL; if (resultCode == RESULT_CANCELED) { + // if cancelling out of addAccount and the original state caused us to skip this, + // finish this activity + if (shouldSkipToChooseAccountTypeFlow()) { + setResult(Activity.RESULT_CANCELED); + finish(); + } return; } @@ -318,6 +324,14 @@ public class ChooseTypeAndAccountActivity extends Activity finish(); } + /** + * convenience method to check if we should skip the accounts list display and immediately + * jump to the flow that asks the user to select from the account type list + */ + private boolean shouldSkipToChooseAccountTypeFlow() { + return mAccountInfos.isEmpty(); + } + protected void runAddAccountForAuthenticator(String type) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "runAddAccountForAuthenticator: " + type); diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java index edd0fa3..9bf1634 100755 --- a/core/java/android/animation/ValueAnimator.java +++ b/core/java/android/animation/ValueAnimator.java @@ -20,6 +20,7 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; import android.util.AndroidRuntimeException; +import android.view.Choreographer; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.AnimationUtils; import android.view.animation.LinearInterpolator; @@ -45,17 +46,10 @@ public class ValueAnimator extends Animator { * Internal constants */ - /* - * The default amount of time in ms between animation frames - */ - private static final long DEFAULT_FRAME_DELAY = 10; - /** - * Messages sent to timing handler: START is sent when an animation first begins, FRAME is sent - * by the handler to itself to process the next animation frame + * Messages sent to timing handler: START is sent when an animation first begins. */ static final int ANIMATION_START = 0; - static final int ANIMATION_FRAME = 1; /** * Values used with internal variable mPlayingState to indicate the current state of an @@ -83,70 +77,15 @@ public class ValueAnimator extends Animator { */ long mSeekTime = -1; - // TODO: We access the following ThreadLocal variables often, some of them on every update. - // If ThreadLocal access is significantly expensive, we may want to put all of these - // fields into a structure sot hat we just access ThreadLocal once to get the reference - // to that structure, then access the structure directly for each field. - // The static sAnimationHandler processes the internal timing loop on which all animations // are based private static ThreadLocal<AnimationHandler> sAnimationHandler = new ThreadLocal<AnimationHandler>(); - // The per-thread list of all active animations - private static final ThreadLocal<ArrayList<ValueAnimator>> sAnimations = - new ThreadLocal<ArrayList<ValueAnimator>>() { - @Override - protected ArrayList<ValueAnimator> initialValue() { - return new ArrayList<ValueAnimator>(); - } - }; - - // The per-thread set of animations to be started on the next animation frame - private static final ThreadLocal<ArrayList<ValueAnimator>> sPendingAnimations = - new ThreadLocal<ArrayList<ValueAnimator>>() { - @Override - protected ArrayList<ValueAnimator> initialValue() { - return new ArrayList<ValueAnimator>(); - } - }; - - /** - * Internal per-thread collections used to avoid set collisions as animations start and end - * while being processed. - */ - private static final ThreadLocal<ArrayList<ValueAnimator>> sDelayedAnims = - new ThreadLocal<ArrayList<ValueAnimator>>() { - @Override - protected ArrayList<ValueAnimator> initialValue() { - return new ArrayList<ValueAnimator>(); - } - }; - - private static final ThreadLocal<ArrayList<ValueAnimator>> sEndingAnims = - new ThreadLocal<ArrayList<ValueAnimator>>() { - @Override - protected ArrayList<ValueAnimator> initialValue() { - return new ArrayList<ValueAnimator>(); - } - }; - - private static final ThreadLocal<ArrayList<ValueAnimator>> sReadyAnims = - new ThreadLocal<ArrayList<ValueAnimator>>() { - @Override - protected ArrayList<ValueAnimator> initialValue() { - return new ArrayList<ValueAnimator>(); - } - }; - // The time interpolator to be used if none is set on the animation private static final TimeInterpolator sDefaultInterpolator = new AccelerateDecelerateInterpolator(); - // type evaluators for the primitive types handled by this implementation - private static final TypeEvaluator sIntEvaluator = new IntEvaluator(); - private static final TypeEvaluator sFloatEvaluator = new FloatEvaluator(); - /** * Used to indicate whether the animation is currently playing in reverse. This causes the * elapsed fraction to be inverted to calculate the appropriate values. @@ -217,9 +156,6 @@ public class ValueAnimator extends Animator { // The amount of time in ms to delay starting the animation after start() is called private long mStartDelay = 0; - // The number of milliseconds between animation frames - private static long sFrameDelay = DEFAULT_FRAME_DELAY; - // The number of times the animation will repeat. The default is 0, which means the animation // will play only once private int mRepeatCount = 0; @@ -566,119 +502,146 @@ public class ValueAnimator extends Animator { * animations possible. * */ - private static class AnimationHandler extends Handler { + private static class AnimationHandler extends Handler + implements Choreographer.OnAnimateListener { + // The per-thread list of all active animations + private final ArrayList<ValueAnimator> mAnimations = new ArrayList<ValueAnimator>(); + + // The per-thread set of animations to be started on the next animation frame + private final ArrayList<ValueAnimator> mPendingAnimations = new ArrayList<ValueAnimator>(); + /** - * There are only two messages that we care about: ANIMATION_START and - * ANIMATION_FRAME. The START message is sent when an animation's start() - * method is called. It cannot start synchronously when start() is called + * Internal per-thread collections used to avoid set collisions as animations start and end + * while being processed. + */ + private final ArrayList<ValueAnimator> mDelayedAnims = new ArrayList<ValueAnimator>(); + private final ArrayList<ValueAnimator> mEndingAnims = new ArrayList<ValueAnimator>(); + private final ArrayList<ValueAnimator> mReadyAnims = new ArrayList<ValueAnimator>(); + + private final Choreographer mChoreographer; + private boolean mIsChoreographed; + + private AnimationHandler() { + mChoreographer = Choreographer.getInstance(); + } + + /** + * The START message is sent when an animation's start() method is called. + * It cannot start synchronously when start() is called * because the call may be on the wrong thread, and it would also not be * synchronized with other animations because it would not start on a common * timing pulse. So each animation sends a START message to the handler, which * causes the handler to place the animation on the active animations queue and * start processing frames for that animation. - * The FRAME message is the one that is sent over and over while there are any - * active animations to process. */ @Override public void handleMessage(Message msg) { - boolean callAgain = true; - ArrayList<ValueAnimator> animations = sAnimations.get(); - ArrayList<ValueAnimator> delayedAnims = sDelayedAnims.get(); switch (msg.what) { - // TODO: should we avoid sending frame message when starting if we - // were already running? case ANIMATION_START: - ArrayList<ValueAnimator> pendingAnimations = sPendingAnimations.get(); - if (animations.size() > 0 || delayedAnims.size() > 0) { - callAgain = false; - } - // pendingAnims holds any animations that have requested to be started - // We're going to clear sPendingAnimations, but starting animation may - // cause more to be added to the pending list (for example, if one animation - // starting triggers another starting). So we loop until sPendingAnimations - // is empty. - while (pendingAnimations.size() > 0) { - ArrayList<ValueAnimator> pendingCopy = - (ArrayList<ValueAnimator>) pendingAnimations.clone(); - pendingAnimations.clear(); - int count = pendingCopy.size(); - for (int i = 0; i < count; ++i) { - ValueAnimator anim = pendingCopy.get(i); - // If the animation has a startDelay, place it on the delayed list - if (anim.mStartDelay == 0) { - anim.startAnimation(); - } else { - delayedAnims.add(anim); - } - } - } - // fall through to process first frame of new animations - case ANIMATION_FRAME: - // currentTime holds the common time for all animations processed - // during this frame - long currentTime = AnimationUtils.currentAnimationTimeMillis(); - ArrayList<ValueAnimator> readyAnims = sReadyAnims.get(); - ArrayList<ValueAnimator> endingAnims = sEndingAnims.get(); - - // First, process animations currently sitting on the delayed queue, adding - // them to the active animations if they are ready - int numDelayedAnims = delayedAnims.size(); - for (int i = 0; i < numDelayedAnims; ++i) { - ValueAnimator anim = delayedAnims.get(i); - if (anim.delayedAnimationFrame(currentTime)) { - readyAnims.add(anim); - } - } - int numReadyAnims = readyAnims.size(); - if (numReadyAnims > 0) { - for (int i = 0; i < numReadyAnims; ++i) { - ValueAnimator anim = readyAnims.get(i); - anim.startAnimation(); - anim.mRunning = true; - delayedAnims.remove(anim); - } - readyAnims.clear(); - } + doAnimationStart(); + break; + } + } - // Now process all active animations. The return value from animationFrame() - // tells the handler whether it should now be ended - int numAnims = animations.size(); - int i = 0; - while (i < numAnims) { - ValueAnimator anim = animations.get(i); - if (anim.animationFrame(currentTime)) { - endingAnims.add(anim); - } - if (animations.size() == numAnims) { - ++i; - } else { - // An animation might be canceled or ended by client code - // during the animation frame. Check to see if this happened by - // seeing whether the current index is the same as it was before - // calling animationFrame(). Another approach would be to copy - // animations to a temporary list and process that list instead, - // but that entails garbage and processing overhead that would - // be nice to avoid. - --numAnims; - endingAnims.remove(anim); - } - } - if (endingAnims.size() > 0) { - for (i = 0; i < endingAnims.size(); ++i) { - endingAnims.get(i).endAnimation(); - } - endingAnims.clear(); + private void doAnimationStart() { + // mPendingAnimations holds any animations that have requested to be started + // We're going to clear mPendingAnimations, but starting animation may + // cause more to be added to the pending list (for example, if one animation + // starting triggers another starting). So we loop until mPendingAnimations + // is empty. + while (mPendingAnimations.size() > 0) { + ArrayList<ValueAnimator> pendingCopy = + (ArrayList<ValueAnimator>) mPendingAnimations.clone(); + mPendingAnimations.clear(); + int count = pendingCopy.size(); + for (int i = 0; i < count; ++i) { + ValueAnimator anim = pendingCopy.get(i); + // If the animation has a startDelay, place it on the delayed list + if (anim.mStartDelay == 0) { + anim.startAnimation(this); + } else { + mDelayedAnims.add(anim); } + } + } + doAnimationFrame(); + } - // If there are still active or delayed animations, call the handler again - // after the frameDelay - if (callAgain && (!animations.isEmpty() || !delayedAnims.isEmpty())) { - sendEmptyMessageDelayed(ANIMATION_FRAME, Math.max(0, sFrameDelay - - (AnimationUtils.currentAnimationTimeMillis() - currentTime))); - } - break; + private void doAnimationFrame() { + // currentTime holds the common time for all animations processed + // during this frame + long currentTime = AnimationUtils.currentAnimationTimeMillis(); + + // First, process animations currently sitting on the delayed queue, adding + // them to the active animations if they are ready + int numDelayedAnims = mDelayedAnims.size(); + for (int i = 0; i < numDelayedAnims; ++i) { + ValueAnimator anim = mDelayedAnims.get(i); + if (anim.delayedAnimationFrame(currentTime)) { + mReadyAnims.add(anim); + } + } + int numReadyAnims = mReadyAnims.size(); + if (numReadyAnims > 0) { + for (int i = 0; i < numReadyAnims; ++i) { + ValueAnimator anim = mReadyAnims.get(i); + anim.startAnimation(this); + anim.mRunning = true; + mDelayedAnims.remove(anim); + } + mReadyAnims.clear(); + } + + // Now process all active animations. The return value from animationFrame() + // tells the handler whether it should now be ended + int numAnims = mAnimations.size(); + int i = 0; + while (i < numAnims) { + ValueAnimator anim = mAnimations.get(i); + if (anim.animationFrame(currentTime)) { + mEndingAnims.add(anim); + } + if (mAnimations.size() == numAnims) { + ++i; + } else { + // An animation might be canceled or ended by client code + // during the animation frame. Check to see if this happened by + // seeing whether the current index is the same as it was before + // calling animationFrame(). Another approach would be to copy + // animations to a temporary list and process that list instead, + // but that entails garbage and processing overhead that would + // be nice to avoid. + --numAnims; + mEndingAnims.remove(anim); + } + } + if (mEndingAnims.size() > 0) { + for (i = 0; i < mEndingAnims.size(); ++i) { + mEndingAnims.get(i).endAnimation(this); + } + mEndingAnims.clear(); + } + + // If there are still active or delayed animations, schedule a future call to + // onAnimate to process the next frame of the animations. + if (!mAnimations.isEmpty() || !mDelayedAnims.isEmpty()) { + if (!mIsChoreographed) { + mIsChoreographed = true; + mChoreographer.addOnAnimateListener(this); + } + mChoreographer.scheduleAnimation(); + } else { + if (mIsChoreographed) { + mIsChoreographed = false; + mChoreographer.removeOnAnimateListener(this); + } } } + + @Override + public void onAnimate() { + doAnimationFrame(); + } } /** @@ -708,10 +671,13 @@ public class ValueAnimator extends Animator { * function because the same delay will be applied to all animations, since they are all * run off of a single timing loop. * + * The frame delay may be ignored when the animation system uses an external timing + * source, such as the display refresh rate (vsync), to govern animations. + * * @return the requested time between frames, in milliseconds */ public static long getFrameDelay() { - return sFrameDelay; + return Choreographer.getFrameDelay(); } /** @@ -721,10 +687,13 @@ public class ValueAnimator extends Animator { * function because the same delay will be applied to all animations, since they are all * run off of a single timing loop. * + * The frame delay may be ignored when the animation system uses an external timing + * source, such as the display refresh rate (vsync), to govern animations. + * * @param frameDelay the requested time between frames, in milliseconds */ public static void setFrameDelay(long frameDelay) { - sFrameDelay = frameDelay; + Choreographer.setFrameDelay(frameDelay); } /** @@ -921,7 +890,8 @@ public class ValueAnimator extends Animator { mPlayingState = STOPPED; mStarted = true; mStartedDelay = false; - sPendingAnimations.get().add(this); + AnimationHandler animationHandler = getOrCreateAnimationHandler(); + animationHandler.mPendingAnimations.add(this); if (mStartDelay == 0) { // This sets the initial value of the animation, prior to actually starting it running setCurrentPlayTime(getCurrentPlayTime()); @@ -937,11 +907,6 @@ public class ValueAnimator extends Animator { } } } - AnimationHandler animationHandler = sAnimationHandler.get(); - if (animationHandler == null) { - animationHandler = new AnimationHandler(); - sAnimationHandler.set(animationHandler); - } animationHandler.sendEmptyMessage(ANIMATION_START); } @@ -954,8 +919,10 @@ public class ValueAnimator extends Animator { public void cancel() { // Only cancel if the animation is actually running or has been started and is about // to run - if (mPlayingState != STOPPED || sPendingAnimations.get().contains(this) || - sDelayedAnims.get().contains(this)) { + AnimationHandler handler = getOrCreateAnimationHandler(); + if (mPlayingState != STOPPED + || handler.mPendingAnimations.contains(this) + || handler.mDelayedAnims.contains(this)) { // Only notify listeners if the animator has actually started if (mRunning && mListeners != null) { ArrayList<AnimatorListener> tmpListeners = @@ -964,16 +931,17 @@ public class ValueAnimator extends Animator { listener.onAnimationCancel(this); } } - endAnimation(); + endAnimation(handler); } } @Override public void end() { - if (!sAnimations.get().contains(this) && !sPendingAnimations.get().contains(this)) { + AnimationHandler handler = getOrCreateAnimationHandler(); + if (!handler.mAnimations.contains(this) && !handler.mPendingAnimations.contains(this)) { // Special case if the animation has not yet started; get it ready for ending mStartedDelay = false; - startAnimation(); + startAnimation(handler); } else if (!mInitialized) { initAnimation(); } @@ -984,7 +952,7 @@ public class ValueAnimator extends Animator { } else { animateValue(1f); } - endAnimation(); + endAnimation(handler); } @Override @@ -1020,10 +988,10 @@ public class ValueAnimator extends Animator { * Called internally to end an animation by removing it from the animations list. Must be * called on the UI thread. */ - private void endAnimation() { - sAnimations.get().remove(this); - sPendingAnimations.get().remove(this); - sDelayedAnims.get().remove(this); + private void endAnimation(AnimationHandler handler) { + handler.mAnimations.remove(this); + handler.mPendingAnimations.remove(this); + handler.mDelayedAnims.remove(this); mPlayingState = STOPPED; if (mRunning && mListeners != null) { ArrayList<AnimatorListener> tmpListeners = @@ -1041,9 +1009,9 @@ public class ValueAnimator extends Animator { * Called internally to start an animation by adding it to the active animations list. Must be * called on the UI thread. */ - private void startAnimation() { + private void startAnimation(AnimationHandler handler) { initAnimation(); - sAnimations.get().add(this); + handler.mAnimations.add(this); if (mStartDelay > 0 && mListeners != null) { // Listeners were already notified in start() if startDelay is 0; this is // just for delayed animations @@ -1229,13 +1197,14 @@ public class ValueAnimator extends Animator { /** * Return the number of animations currently running. * - * Used by StrictMode internally to annotate violations. Only - * called on the main thread. + * Used by StrictMode internally to annotate violations. + * May be called on arbitrary threads! * * @hide */ public static int getCurrentAnimationsCount() { - return sAnimations.get().size(); + AnimationHandler handler = sAnimationHandler.get(); + return handler != null ? handler.mAnimations.size() : 0; } /** @@ -1245,9 +1214,21 @@ public class ValueAnimator extends Animator { * @hide */ public static void clearAllAnimations() { - sAnimations.get().clear(); - sPendingAnimations.get().clear(); - sDelayedAnims.get().clear(); + AnimationHandler handler = sAnimationHandler.get(); + if (handler != null) { + handler.mAnimations.clear(); + handler.mPendingAnimations.clear(); + handler.mDelayedAnims.clear(); + } + } + + private AnimationHandler getOrCreateAnimationHandler() { + AnimationHandler handler = sAnimationHandler.get(); + if (handler == null) { + handler = new AnimationHandler(); + sAnimationHandler.set(handler); + } + return handler; } @Override diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index d7f5c55..c037ffb 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -24,14 +24,14 @@ import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.os.Bundle; -import android.os.PerformanceCollector; -import android.os.RemoteException; import android.os.Debug; import android.os.IBinder; import android.os.MessageQueue; +import android.os.PerformanceCollector; import android.os.Process; -import android.os.SystemClock; +import android.os.RemoteException; import android.os.ServiceManager; +import android.os.SystemClock; import android.util.AndroidRuntimeException; import android.util.Log; import android.view.IWindowManager; @@ -40,7 +40,6 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ViewConfiguration; import android.view.Window; -import android.view.inputmethod.InputMethodManager; import java.io.File; import java.util.ArrayList; @@ -834,16 +833,21 @@ public class Instrumentation { return; } KeyCharacterMap keyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); - + KeyEvent[] events = keyCharacterMap.getEvents(text.toCharArray()); - + if (events != null) { for (int i = 0; i < events.length; i++) { - sendKeySync(events[i]); + // We have to change the time of an event before injecting it because + // all KeyEvents returned by KeyCharacterMap.getEvents() have the same + // time stamp and the system rejects too old events. Hence, it is + // possible for an event to become stale before it is injected if it + // takes too long to inject the preceding ones. + sendKeySync(KeyEvent.changeTimeRepeat(events[i], SystemClock.uptimeMillis(), 0)); } - } + } } - + /** * Send a key event to the currently focused window/view and wait for it to * be processed. Finished at some point after the recipient has returned diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java index 5b8addf..dd9f337 100644 --- a/core/java/android/app/StatusBarManager.java +++ b/core/java/android/app/StatusBarManager.java @@ -56,6 +56,11 @@ public class StatusBarManager { | DISABLE_NOTIFICATION_ALERTS | DISABLE_NOTIFICATION_TICKER | DISABLE_SYSTEM_INFO | DISABLE_RECENT | DISABLE_HOME | DISABLE_BACK | DISABLE_CLOCK; + public static final int NAVIGATION_HINT_BACK_NOP = 1 << 0; + public static final int NAVIGATION_HINT_HOME_NOP = 1 << 1; + public static final int NAVIGATION_HINT_RECENT_NOP = 1 << 2; + public static final int NAVIGATION_HINT_BACK_ALT = 1 << 3; + private Context mContext; private IStatusBarService mService; private IBinder mToken = new Binder(); diff --git a/core/java/android/bluetooth/BluetoothAdapter.java b/core/java/android/bluetooth/BluetoothAdapter.java index 5f5ba50..e420bfd 100644 --- a/core/java/android/bluetooth/BluetoothAdapter.java +++ b/core/java/android/bluetooth/BluetoothAdapter.java @@ -399,6 +399,25 @@ public final class BluetoothAdapter { } /** + * Get a {@link BluetoothDevice} object for the given Bluetooth hardware + * address. + * <p>Valid Bluetooth hardware addresses must be 6 bytes. This method + * expects the address in network byte order (MSB first). + * <p>A {@link BluetoothDevice} will always be returned for a valid + * hardware address, even if this adapter has never seen that device. + * + * @param address Bluetooth MAC address (6 bytes) + * @throws IllegalArgumentException if address is invalid + */ + public BluetoothDevice getRemoteDevice(byte[] address) { + if (address == null || address.length != 6) { + throw new IllegalArgumentException("Bluetooth address must have 6 bytes"); + } + return new BluetoothDevice(String.format("%02X:%02X:%02X:%02X:%02X:%02X", + address[0], address[1], address[2], address[3], address[4], address[5])); + } + + /** * Return true if Bluetooth is currently enabled and ready for use. * <p>Equivalent to: * <code>getBluetoothState() == STATE_ON</code> @@ -1281,7 +1300,7 @@ public final class BluetoothAdapter { } /** - * Validate a Bluetooth address, such as "00:43:A8:23:10:F0" + * Validate a String Bluetooth address, such as "00:43:A8:23:10:F0" * <p>Alphabetic characters must be uppercase to be valid. * * @param address Bluetooth address as string diff --git a/core/java/android/content/pm/ResolveInfo.java b/core/java/android/content/pm/ResolveInfo.java index bcd599b..e3749b4 100644 --- a/core/java/android/content/pm/ResolveInfo.java +++ b/core/java/android/content/pm/ResolveInfo.java @@ -34,8 +34,8 @@ import java.util.Comparator; */ public class ResolveInfo implements Parcelable { /** - * The activity that corresponds to this resolution match, if this - * resolution is for an activity. One and only one of this and + * The activity or broadcast receiver that corresponds to this resolution match, + * if this resolution is for an activity or broadcast receiver. One and only one of this and * serviceInfo must be non-null. */ public ActivityInfo activityInfo; diff --git a/core/java/android/database/BulkCursorNative.java b/core/java/android/database/BulkCursorNative.java index 20a9c67..67cf0f8 100644 --- a/core/java/android/database/BulkCursorNative.java +++ b/core/java/android/database/BulkCursorNative.java @@ -180,13 +180,13 @@ final class BulkCursorProxy implements IBulkCursor { return mRemote; } - public CursorWindow getWindow(int startPos) throws RemoteException + public CursorWindow getWindow(int position) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); try { data.writeInterfaceToken(IBulkCursor.descriptor); - data.writeInt(startPos); + data.writeInt(position); mRemote.transact(GET_CURSOR_WINDOW_TRANSACTION, data, reply, 0); DatabaseUtils.readExceptionFromParcel(reply); diff --git a/core/java/android/database/CrossProcessCursorWrapper.java b/core/java/android/database/CrossProcessCursorWrapper.java index 8c250b8..1b77cb9 100644 --- a/core/java/android/database/CrossProcessCursorWrapper.java +++ b/core/java/android/database/CrossProcessCursorWrapper.java @@ -24,10 +24,10 @@ import android.database.CursorWrapper; /** * Cursor wrapper that implements {@link CrossProcessCursor}. * <p> - * If the wrapper cursor implemented {@link CrossProcessCursor}, then delegates - * {@link #fillWindow}, {@link #getWindow()} and {@link #onMove} to it. Otherwise, - * provides default implementations of these methods that traverse the contents - * of the cursor similar to {@link AbstractCursor#fillWindow}. + * If the wrapped cursor implements {@link CrossProcessCursor}, then the wrapper + * delegates {@link #fillWindow}, {@link #getWindow()} and {@link #onMove} to it. + * Otherwise, the wrapper provides default implementations of these methods that + * traverse the contents of the cursor similar to {@link AbstractCursor#fillWindow}. * </p><p> * This wrapper can be used to adapt an ordinary {@link Cursor} into a * {@link CrossProcessCursor}. diff --git a/core/java/android/database/CursorToBulkCursorAdaptor.java b/core/java/android/database/CursorToBulkCursorAdaptor.java index 215035d..aa0f61e 100644 --- a/core/java/android/database/CursorToBulkCursorAdaptor.java +++ b/core/java/android/database/CursorToBulkCursorAdaptor.java @@ -132,11 +132,11 @@ public final class CursorToBulkCursorAdaptor extends BulkCursorNative } @Override - public CursorWindow getWindow(int startPos) { + public CursorWindow getWindow(int position) { synchronized (mLock) { throwIfCursorIsClosed(); - if (!mCursor.moveToPosition(startPos)) { + if (!mCursor.moveToPosition(position)) { closeFilledWindowLocked(); return null; } @@ -149,12 +149,11 @@ public final class CursorToBulkCursorAdaptor extends BulkCursorNative if (window == null) { mFilledWindow = new CursorWindow(mProviderName); window = mFilledWindow; - mCursor.fillWindow(startPos, window); - } else if (startPos < window.getStartPosition() - || startPos >= window.getStartPosition() + window.getNumRows()) { + } else if (position < window.getStartPosition() + || position >= window.getStartPosition() + window.getNumRows()) { window.clear(); - mCursor.fillWindow(startPos, window); } + mCursor.fillWindow(position, window); } // Acquire a reference before returning from this RPC. diff --git a/core/java/android/database/DatabaseUtils.java b/core/java/android/database/DatabaseUtils.java index a10ca15..a8ba9a3 100644 --- a/core/java/android/database/DatabaseUtils.java +++ b/core/java/android/database/DatabaseUtils.java @@ -726,6 +726,32 @@ public class DatabaseUtils { } /** + * Picks a start position for {@link Cursor#fillWindow} such that the + * window will contain the requested row and a useful range of rows + * around it. + * + * When the data set is too large to fit in a cursor window, seeking the + * cursor can become a very expensive operation since we have to run the + * query again when we move outside the bounds of the current window. + * + * We try to choose a start position for the cursor window such that + * 1/3 of the window's capacity is used to hold rows before the requested + * position and 2/3 of the window's capacity is used to hold rows after the + * requested position. + * + * @param cursorPosition The row index of the row we want to get. + * @param cursorWindowCapacity The estimated number of rows that can fit in + * a cursor window, or 0 if unknown. + * @return The recommended start position, always less than or equal to + * the requested row. + * @hide + */ + public static int cursorPickFillWindowStartPosition( + int cursorPosition, int cursorWindowCapacity) { + return Math.max(cursorPosition - cursorWindowCapacity / 3, 0); + } + + /** * Query the table for the number of rows in the table. * @param db the database the table is in * @param table the name of the table to query diff --git a/core/java/android/database/IBulkCursor.java b/core/java/android/database/IBulkCursor.java index 7c96797..0f4500a 100644 --- a/core/java/android/database/IBulkCursor.java +++ b/core/java/android/database/IBulkCursor.java @@ -30,11 +30,17 @@ import android.os.RemoteException; */ public interface IBulkCursor extends IInterface { /** - * Returns a BulkCursorWindow, which either has a reference to a shared - * memory segment with the rows, or an array of JSON strings. + * Gets a cursor window that contains the specified position. + * The window will contain a range of rows around the specified position. */ - public CursorWindow getWindow(int startPos) throws RemoteException; + public CursorWindow getWindow(int position) throws RemoteException; + /** + * Notifies the cursor that the position has changed. + * Only called when {@link #getWantsAllOnMoveCalls()} returns true. + * + * @param position The new position + */ public void onMove(int position) throws RemoteException; /** diff --git a/core/java/android/database/sqlite/SQLiteCursor.java b/core/java/android/database/sqlite/SQLiteCursor.java index c24acd4..8dcedf2 100644 --- a/core/java/android/database/sqlite/SQLiteCursor.java +++ b/core/java/android/database/sqlite/SQLiteCursor.java @@ -18,6 +18,7 @@ package android.database.sqlite; import android.database.AbstractWindowedCursor; import android.database.CursorWindow; +import android.database.DatabaseUtils; import android.os.StrictMode; import android.util.Log; @@ -48,7 +49,10 @@ public class SQLiteCursor extends AbstractWindowedCursor { private final SQLiteCursorDriver mDriver; /** The number of rows in the cursor */ - private volatile int mCount = NO_COUNT; + private int mCount = NO_COUNT; + + /** The number of rows that can fit in the cursor window, 0 if unknown */ + private int mCursorWindowCapacity; /** A mapping of column names to column indices, to speed up lookups */ private Map<String, Integer> mColumnNameMap; @@ -158,18 +162,20 @@ public class SQLiteCursor extends AbstractWindowedCursor { return mCount; } - private void fillWindow(int startPos) { + private void fillWindow(int requiredPos) { clearOrCreateWindow(getDatabase().getPath()); - mWindow.setStartPosition(startPos); - int count = getQuery().fillWindow(mWindow); - if (startPos == 0) { // fillWindow returns count(*) only for startPos = 0 + + if (mCount == NO_COUNT) { + int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, 0); + mCount = getQuery().fillWindow(mWindow, startPos, requiredPos, true); + mCursorWindowCapacity = mWindow.getNumRows(); if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "received count(*) from native_fill_window: " + count); + Log.d(TAG, "received count(*) from native_fill_window: " + mCount); } - mCount = count; - } else if (mCount <= 0) { - throw new IllegalStateException("Row count should never be zero or negative " - + "when the start position is non-zero"); + } else { + int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, + mCursorWindowCapacity); + getQuery().fillWindow(mWindow, startPos, requiredPos, false); } } diff --git a/core/java/android/database/sqlite/SQLiteQuery.java b/core/java/android/database/sqlite/SQLiteQuery.java index faf6cba..56dd007 100644 --- a/core/java/android/database/sqlite/SQLiteQuery.java +++ b/core/java/android/database/sqlite/SQLiteQuery.java @@ -31,8 +31,8 @@ import android.util.Log; public class SQLiteQuery extends SQLiteProgram { private static final String TAG = "SQLiteQuery"; - private static native int nativeFillWindow(int databasePtr, int statementPtr, int windowPtr, - int startPos, int offsetParam); + private static native long nativeFillWindow(int databasePtr, int statementPtr, int windowPtr, + int offsetParam, int startPos, int requiredPos, boolean countAllRows); private static native int nativeColumnCount(int statementPtr); private static native String nativeColumnName(int statementPtr, int columnIndex); @@ -73,27 +73,38 @@ public class SQLiteQuery extends SQLiteProgram { * Reads rows into a buffer. This method acquires the database lock. * * @param window The window to fill into - * @return number of total rows in the query + * @param startPos The start position for filling the window. + * @param requiredPos The position of a row that MUST be in the window. + * If it won't fit, then the query should discard part of what it filled. + * @param countAllRows True to count all rows that the query would + * return regardless of whether they fit in the window. + * @return Number of rows that were enumerated. Might not be all rows + * unless countAllRows is true. */ - /* package */ int fillWindow(CursorWindow window) { + /* package */ int fillWindow(CursorWindow window, + int startPos, int requiredPos, boolean countAllRows) { mDatabase.lock(mSql); long timeStart = SystemClock.uptimeMillis(); try { acquireReference(); try { window.acquireReference(); - int startPos = window.getStartPosition(); - int numRows = nativeFillWindow(nHandle, nStatement, window.mWindowPtr, - startPos, mOffsetIndex); + long result = nativeFillWindow(nHandle, nStatement, window.mWindowPtr, + mOffsetIndex, startPos, requiredPos, countAllRows); + int actualPos = (int)(result >> 32); + int countedRows = (int)result; + window.setStartPosition(actualPos); if (SQLiteDebug.DEBUG_LOG_SLOW_QUERIES) { long elapsed = SystemClock.uptimeMillis() - timeStart; if (SQLiteDebug.shouldLogSlowQuery(elapsed)) { Log.d(TAG, "fillWindow took " + elapsed + " ms: window=\"" + window + "\", startPos=" + startPos + + ", requiredPos=" + requiredPos + ", offset=" + mOffsetIndex + + ", actualPos=" + actualPos + ", filledRows=" + window.getNumRows() - + ", countedRows=" + numRows + + ", countedRows=" + countedRows + ", query=\"" + mSql + "\"" + ", args=[" + (mBindArgs != null ? TextUtils.join(", ", mBindArgs.values()) : "") @@ -101,7 +112,7 @@ public class SQLiteQuery extends SQLiteProgram { } } mDatabase.logTimeStat(mSql, timeStart); - return numRows; + return countedRows; } catch (IllegalStateException e){ // simply ignore it return 0; diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java index 7ca6155..cca208a 100644 --- a/core/java/android/hardware/Camera.java +++ b/core/java/android/hardware/Camera.java @@ -138,7 +138,7 @@ public class Camera { private static final int CAMERA_MSG_COMPRESSED_IMAGE = 0x100; private static final int CAMERA_MSG_RAW_IMAGE_NOTIFY = 0x200; private static final int CAMERA_MSG_PREVIEW_METADATA = 0x400; - private static final int CAMERA_MSG_ALL_MSGS = 0x4FF; + private static final int CAMERA_MSG_FOCUS_MOVE = 0x800; private int mNativeContext; // accessed by native methods private EventHandler mEventHandler; @@ -148,6 +148,7 @@ public class Camera { private PreviewCallback mPreviewCallback; private PictureCallback mPostviewCallback; private AutoFocusCallback mAutoFocusCallback; + private AutoFocusMoveCallback mAutoFocusMoveCallback; private OnZoomChangeListener mZoomListener; private FaceDetectionListener mFaceListener; private ErrorCallback mErrorCallback; @@ -302,6 +303,12 @@ public class Camera { native_setup(new WeakReference<Camera>(this), cameraId); } + /** + * An empty Camera for testing purpose. + */ + Camera() { + } + protected void finalize() { release(); } @@ -492,6 +499,7 @@ public class Camera { mPostviewCallback = null; mJpegCallback = null; mAutoFocusCallback = null; + mAutoFocusMoveCallback = null; } private native final void _stopPreview(); @@ -737,6 +745,12 @@ public class Camera { } return; + case CAMERA_MSG_FOCUS_MOVE: + if (mAutoFocusMoveCallback != null) { + mAutoFocusMoveCallback.onAutoFocusMoving(msg.arg1 == 0 ? false : true, mCamera); + } + return; + default: Log.e(TAG, "Unknown message type " + msg.what); return; @@ -849,6 +863,39 @@ public class Camera { private native final void native_cancelAutoFocus(); /** + * Callback interface used to notify on auto focus start and stop. + * + * <p>This is useful for continuous autofocus -- {@link Parameters#FOCUS_MODE_CONTINUOUS_VIDEO} + * and {@link Parameters#FOCUS_MODE_CONTINUOUS_PICTURE}. Applications can + * show autofocus animation.</p> + * + * @hide + */ + public interface AutoFocusMoveCallback + { + /** + * Called when the camera auto focus starts or stops. + * + * @param start true if focus starts to move, false if focus stops to move + * @param camera the Camera service object + */ + void onAutoFocusMoving(boolean start, Camera camera); + } + + /** + * Sets camera auto-focus move callback. + * + * @param cb the callback to run + * @hide + */ + public void setAutoFocusMoveCallback(AutoFocusMoveCallback cb) { + mAutoFocusMoveCallback = cb; + enableFocusMoveCallback((mAutoFocusMoveCallback != null) ? 1 : 0); + } + + private native void enableFocusMoveCallback(int enable); + + /** * Callback interface used to signal the moment of actual image capture. * * @see #takePicture(ShutterCallback, PictureCallback, PictureCallback, PictureCallback) @@ -1310,6 +1357,18 @@ public class Camera { } /** + * Returns an empty {@link Parameters} for testing purpose. + * + * @return an Parameter object. + * + * @hide + */ + public static Parameters getEmptyParameters() { + Camera camera = new Camera(); + return camera.new Parameters(); + } + + /** * Image size (width and height dimensions). */ public class Size { diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index 0052dd0..a569317 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -360,6 +360,11 @@ public class ConnectivityManager { } } + /** + * Gets you info about the current data network. + * Call {@link NetworkInfo#isConnected()} on the returned {@link NetworkInfo} + * to check if the device has a data connection. + */ public NetworkInfo getActiveNetworkInfo() { try { return mService.getActiveNetworkInfo(); diff --git a/core/java/android/net/DhcpStateMachine.java b/core/java/android/net/DhcpStateMachine.java index fc6a44a..397a12a 100644 --- a/core/java/android/net/DhcpStateMachine.java +++ b/core/java/android/net/DhcpStateMachine.java @@ -347,21 +347,25 @@ public class DhcpStateMachine extends StateMachine { if (success) { if (DBG) Log.d(TAG, "DHCP succeeded on " + mInterfaceName); - long leaseDuration = dhcpInfoInternal.leaseDuration; //int to long conversion - - //Sanity check for renewal - //TODO: would be good to notify the user that his network configuration is - //bad and that the device cannot renew below MIN_RENEWAL_TIME_SECS - if (leaseDuration < MIN_RENEWAL_TIME_SECS) { - leaseDuration = MIN_RENEWAL_TIME_SECS; - } - //Do it a bit earlier than half the lease duration time - //to beat the native DHCP client and avoid extra packets - //48% for one hour lease time = 29 minutes - mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, - SystemClock.elapsedRealtime() + - leaseDuration * 480, //in milliseconds - mDhcpRenewalIntent); + long leaseDuration = dhcpInfoInternal.leaseDuration; //int to long conversion + + //Sanity check for renewal + if (leaseDuration >= 0) { + //TODO: would be good to notify the user that his network configuration is + //bad and that the device cannot renew below MIN_RENEWAL_TIME_SECS + if (leaseDuration < MIN_RENEWAL_TIME_SECS) { + leaseDuration = MIN_RENEWAL_TIME_SECS; + } + //Do it a bit earlier than half the lease duration time + //to beat the native DHCP client and avoid extra packets + //48% for one hour lease time = 29 minutes + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + + leaseDuration * 480, //in milliseconds + mDhcpRenewalIntent); + } else { + //infinite lease time, no renewal needed + } mController.obtainMessage(CMD_POST_DHCP_ACTION, DHCP_SUCCESS, 0, dhcpInfoInternal) .sendToTarget(); diff --git a/core/java/android/net/InterfaceConfiguration.java b/core/java/android/net/InterfaceConfiguration.java index 89b5915..8cdd153 100644 --- a/core/java/android/net/InterfaceConfiguration.java +++ b/core/java/android/net/InterfaceConfiguration.java @@ -16,34 +16,84 @@ package android.net; -import android.os.Parcelable; import android.os.Parcel; +import android.os.Parcelable; + +import com.google.android.collect.Sets; -import java.net.InetAddress; -import java.net.UnknownHostException; +import java.util.HashSet; /** - * A simple object for retrieving / setting an interfaces configuration + * Configuration details for a network interface. + * * @hide */ public class InterfaceConfiguration implements Parcelable { - public String hwAddr; - public LinkAddress addr; - public String interfaceFlags; + private String mHwAddr; + private LinkAddress mAddr; + private HashSet<String> mFlags = Sets.newHashSet(); - public InterfaceConfiguration() { - super(); - } + private static final String FLAG_UP = "up"; + private static final String FLAG_DOWN = "down"; + @Override public String toString() { - StringBuffer str = new StringBuffer(); + final StringBuilder builder = new StringBuilder(); + builder.append("mHwAddr=").append(mHwAddr); + builder.append(" mAddr=").append(String.valueOf(mAddr)); + builder.append(" mFlags=").append(getFlags()); + return builder.toString(); + } + + public Iterable<String> getFlags() { + return mFlags; + } + + public boolean hasFlag(String flag) { + validateFlag(flag); + return mFlags.contains(flag); + } + + public void clearFlag(String flag) { + validateFlag(flag); + mFlags.remove(flag); + } + + public void setFlag(String flag) { + validateFlag(flag); + mFlags.add(flag); + } + + /** + * Set flags to mark interface as up. + */ + public void setInterfaceUp() { + mFlags.remove(FLAG_DOWN); + mFlags.add(FLAG_UP); + } - str.append("ipddress "); - str.append((addr != null) ? addr.toString() : "NULL"); - str.append(" flags ").append(interfaceFlags); - str.append(" hwaddr ").append(hwAddr); + /** + * Set flags to mark interface as down. + */ + public void setInterfaceDown() { + mFlags.remove(FLAG_UP); + mFlags.add(FLAG_DOWN); + } + + public LinkAddress getLinkAddress() { + return mAddr; + } + + public void setLinkAddress(LinkAddress addr) { + mAddr = addr; + } + + public String getHardwareAddress() { + return mHwAddr; + } - return str.toString(); + public void setHardwareAddress(String hwAddr) { + mHwAddr = hwAddr; } /** @@ -55,8 +105,8 @@ public class InterfaceConfiguration implements Parcelable { */ public boolean isActive() { try { - if(interfaceFlags.contains("up")) { - for (byte b : addr.getAddress().getAddress()) { + if (hasFlag(FLAG_UP)) { + for (byte b : mAddr.getAddress().getAddress()) { if (b != 0) return true; } } @@ -66,38 +116,49 @@ public class InterfaceConfiguration implements Parcelable { return false; } - /** Implement the Parcelable interface {@hide} */ + /** {@inheritDoc} */ public int describeContents() { return 0; } - /** Implement the Parcelable interface {@hide} */ + /** {@inheritDoc} */ public void writeToParcel(Parcel dest, int flags) { - dest.writeString(hwAddr); - if (addr != null) { + dest.writeString(mHwAddr); + if (mAddr != null) { dest.writeByte((byte)1); - dest.writeParcelable(addr, flags); + dest.writeParcelable(mAddr, flags); } else { dest.writeByte((byte)0); } - dest.writeString(interfaceFlags); + dest.writeInt(mFlags.size()); + for (String flag : mFlags) { + dest.writeString(flag); + } } - /** Implement the Parcelable interface {@hide} */ - public static final Creator<InterfaceConfiguration> CREATOR = - new Creator<InterfaceConfiguration>() { - public InterfaceConfiguration createFromParcel(Parcel in) { - InterfaceConfiguration info = new InterfaceConfiguration(); - info.hwAddr = in.readString(); - if (in.readByte() == 1) { - info.addr = in.readParcelable(null); - } - info.interfaceFlags = in.readString(); - return info; + public static final Creator<InterfaceConfiguration> CREATOR = new Creator< + InterfaceConfiguration>() { + public InterfaceConfiguration createFromParcel(Parcel in) { + InterfaceConfiguration info = new InterfaceConfiguration(); + info.mHwAddr = in.readString(); + if (in.readByte() == 1) { + info.mAddr = in.readParcelable(null); } - - public InterfaceConfiguration[] newArray(int size) { - return new InterfaceConfiguration[size]; + final int size = in.readInt(); + for (int i = 0; i < size; i++) { + info.mFlags.add(in.readString()); } - }; + return info; + } + + public InterfaceConfiguration[] newArray(int size) { + return new InterfaceConfiguration[size]; + } + }; + + private static void validateFlag(String flag) { + if (flag.indexOf(' ') >= 0) { + throw new IllegalArgumentException("flag contains space: " + flag); + } + } } diff --git a/core/java/android/net/NetworkInfo.java b/core/java/android/net/NetworkInfo.java index 537750a..7286f0d 100644 --- a/core/java/android/net/NetworkInfo.java +++ b/core/java/android/net/NetworkInfo.java @@ -22,8 +22,9 @@ import android.os.Parcel; import java.util.EnumMap; /** - * Describes the status of a network interface of a given type - * (currently either Mobile or Wifi). + * Describes the status of a network interface. + * <p>Use {@link ConnectivityManager#getActiveNetworkInfo()} to get an instance that represents + * the current network connection. */ public class NetworkInfo implements Parcelable { @@ -38,7 +39,7 @@ public class NetworkInfo implements Parcelable { * <tr><td><code>SCANNING</code></td><td><code>CONNECTING</code></td></tr> * <tr><td><code>CONNECTING</code></td><td><code>CONNECTING</code></td></tr> * <tr><td><code>AUTHENTICATING</code></td><td><code>CONNECTING</code></td></tr> - * <tr><td><code>CONNECTED</code></td><td<code>CONNECTED</code></td></tr> + * <tr><td><code>CONNECTED</code></td><td><code>CONNECTED</code></td></tr> * <tr><td><code>DISCONNECTING</code></td><td><code>DISCONNECTING</code></td></tr> * <tr><td><code>DISCONNECTED</code></td><td><code>DISCONNECTED</code></td></tr> * <tr><td><code>UNAVAILABLE</code></td><td><code>DISCONNECTED</code></td></tr> @@ -159,9 +160,12 @@ public class NetworkInfo implements Parcelable { } /** - * Reports the type of network (currently mobile or Wi-Fi) to which the - * info in this object pertains. - * @return the network type + * Reports the type of network to which the + * info in this {@code NetworkInfo} pertains. + * @return one of {@link ConnectivityManager#TYPE_MOBILE}, {@link + * ConnectivityManager#TYPE_WIFI}, {@link ConnectivityManager#TYPE_WIMAX}, {@link + * ConnectivityManager#TYPE_ETHERNET}, {@link ConnectivityManager#TYPE_BLUETOOTH}, or other + * types defined by {@link ConnectivityManager} */ public int getType() { synchronized (this) { @@ -226,6 +230,7 @@ public class NetworkInfo implements Parcelable { /** * Indicates whether network connectivity exists and it is possible to establish * connections and pass data. + * <p>Always call this before attempting to perform data transactions. * @return {@code true} if network connectivity exists, {@code false} otherwise. */ public boolean isConnected() { diff --git a/core/java/android/net/Uri.java b/core/java/android/net/Uri.java index 9d28eff..0fb49bc 100644 --- a/core/java/android/net/Uri.java +++ b/core/java/android/net/Uri.java @@ -19,12 +19,10 @@ package android.net; import android.os.Parcel; import android.os.Parcelable; import android.util.Log; - import java.io.File; -import java.io.IOException; import java.io.UnsupportedEncodingException; -import java.io.ByteArrayOutputStream; import java.net.URLEncoder; +import java.nio.charset.Charsets; import java.util.AbstractList; import java.util.ArrayList; import java.util.Collections; @@ -32,6 +30,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.RandomAccess; import java.util.Set; +import libcore.net.UriCodec; /** * Immutable URI reference. A URI reference includes a URI and a fragment, the @@ -1305,7 +1304,7 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { * * <p>An opaque URI follows this pattern: * {@code <scheme>:<opaque part>#<fragment>} - * + * * <p>Use {@link Uri#buildUpon()} to obtain a builder representing an existing URI. */ public static final class Builder { @@ -1646,6 +1645,9 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { /** * Searches the query string for the first value with the given key. * + * <p><strong>Warning:</strong> Prior to Ice Cream Sandwich, this decoded + * the '+' character as '+' rather than ' '. + * * @param key which will be encoded * @throws UnsupportedOperationException if this isn't a hierarchical URI * @throws NullPointerException if key is null @@ -1679,9 +1681,10 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { if (separator - start == encodedKey.length() && query.regionMatches(start, encodedKey, 0, encodedKey.length())) { if (separator == end) { - return ""; + return ""; } else { - return decode(query.substring(separator + 1, end)); + String encodedValue = query.substring(separator + 1, end); + return UriCodec.decode(encodedValue, true, Charsets.UTF_8, false); } } @@ -1877,9 +1880,6 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { || (allow != null && allow.indexOf(c) != NOT_FOUND); } - /** Unicode replacement character: \\uFFFD. */ - private static final byte[] REPLACEMENT = { (byte) 0xFF, (byte) 0xFD }; - /** * Decodes '%'-escaped octets in the given string using the UTF-8 scheme. * Replaces invalid octets with the unicode replacement character @@ -1890,104 +1890,10 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { * s is null */ public static String decode(String s) { - /* - Compared to java.net.URLEncoderDecoder.decode(), this method decodes a - chunk at a time instead of one character at a time, and it doesn't - throw exceptions. It also only allocates memory when necessary--if - there's nothing to decode, this method won't do much. - */ - if (s == null) { return null; } - - // Lazily-initialized buffers. - StringBuilder decoded = null; - ByteArrayOutputStream out = null; - - int oldLength = s.length(); - - // This loop alternates between copying over normal characters and - // escaping in chunks. This results in fewer method calls and - // allocations than decoding one character at a time. - int current = 0; - while (current < oldLength) { - // Start in "copying" mode where we copy over normal characters. - - // Find the next escape sequence. - int nextEscape = s.indexOf('%', current); - - if (nextEscape == NOT_FOUND) { - if (decoded == null) { - // We didn't actually decode anything. - return s; - } else { - // Append the remainder and return the decoded string. - decoded.append(s, current, oldLength); - return decoded.toString(); - } - } - - // Prepare buffers. - if (decoded == null) { - // Looks like we're going to need the buffers... - // We know the new string will be shorter. Using the old length - // may overshoot a bit, but it will save us from resizing the - // buffer. - decoded = new StringBuilder(oldLength); - out = new ByteArrayOutputStream(4); - } else { - // Clear decoding buffer. - out.reset(); - } - - // Append characters leading up to the escape. - if (nextEscape > current) { - decoded.append(s, current, nextEscape); - - current = nextEscape; - } else { - // assert current == nextEscape - } - - // Switch to "decoding" mode where we decode a string of escape - // sequences. - - // Decode and append escape sequences. Escape sequences look like - // "%ab" where % is literal and a and b are hex digits. - try { - do { - if (current + 2 >= oldLength) { - // Truncated escape sequence. - out.write(REPLACEMENT); - } else { - int a = Character.digit(s.charAt(current + 1), 16); - int b = Character.digit(s.charAt(current + 2), 16); - - if (a == -1 || b == -1) { - // Non hex digits. - out.write(REPLACEMENT); - } else { - // Combine the hex digits into one byte and write. - out.write((a << 4) + b); - } - } - - // Move passed the escape sequence. - current += 3; - } while (current < oldLength && s.charAt(current) == '%'); - - // Decode UTF-8 bytes into a string and append it. - decoded.append(out.toString(DEFAULT_ENCODING)); - } catch (UnsupportedEncodingException e) { - throw new AssertionError(e); - } catch (IOException e) { - throw new AssertionError(e); - } - } - - // If we don't have a buffer, we didn't have to decode anything. - return decoded == null ? s : decoded.toString(); + return UriCodec.decode(s, false, Charsets.UTF_8, false); } /** @@ -2342,7 +2248,7 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { * * @param baseUri Uri to append path segment to * @param pathSegment encoded path segment to append - * @return a new Uri based on baseUri with the given segment appended to + * @return a new Uri based on baseUri with the given segment appended to * the path * @throws NullPointerException if baseUri is null */ diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java index 2857ac5..02096f2 100644 --- a/core/java/android/nfc/NfcAdapter.java +++ b/core/java/android/nfc/NfcAdapter.java @@ -357,8 +357,11 @@ public final class NfcAdapter { throw new IllegalArgumentException("context cannot be null"); } context = context.getApplicationContext(); - /* use getSystemService() instead of just instantiating to take - * advantage of the context's cached NfcManager & NfcAdapter */ + if (context == null) { + throw new IllegalArgumentException( + "context not associated with any application (using a mock context?)"); + } + /* use getSystemService() for consistency */ NfcManager manager = (NfcManager) context.getSystemService(Context.NFC_SERVICE); if (manager == null) { // NFC not available @@ -442,11 +445,13 @@ public final class NfcAdapter { /** * Return true if this NFC Adapter has any features enabled. * - * <p>Application may use this as a helper to suggest that the user - * should turn on NFC in Settings. * <p>If this method returns false, the NFC hardware is guaranteed not to - * generate or respond to any NFC transactions. + * generate or respond to any NFC communication over its NFC radio. + * <p>Applications can use this to check if NFC is enabled. Applications + * can request Settings UI allowing the user to toggle NFC using: + * <p><pre>startActivity(new Intent(Settings.ACTION_NFC_SETTINGS))</pre> * + * @see android.provider.Settings#ACTION_NFC_SETTINGS * @return true if this NFC Adapter has any features enabled */ public boolean isEnabled() { @@ -798,61 +803,6 @@ public final class NfcAdapter { } /** - * TODO: Remove this once pre-built apk's (Maps, Youtube etc) are updated - * @deprecated use {@link CreateNdefMessageCallback} or {@link OnNdefPushCompleteCallback} - * @hide - */ - @Deprecated - public interface NdefPushCallback { - /** - * @deprecated use {@link CreateNdefMessageCallback} instead - */ - @Deprecated - NdefMessage createMessage(); - /** - * @deprecated use{@link OnNdefPushCompleteCallback} instead - */ - @Deprecated - void onMessagePushed(); - } - - /** - * TODO: Remove this - * Converts new callbacks to old callbacks. - */ - static final class LegacyCallbackWrapper implements CreateNdefMessageCallback, - OnNdefPushCompleteCallback { - final NdefPushCallback mLegacyCallback; - LegacyCallbackWrapper(NdefPushCallback legacyCallback) { - mLegacyCallback = legacyCallback; - } - @Override - public void onNdefPushComplete(NfcEvent event) { - mLegacyCallback.onMessagePushed(); - } - @Override - public NdefMessage createNdefMessage(NfcEvent event) { - return mLegacyCallback.createMessage(); - } - } - - /** - * TODO: Remove this once pre-built apk's (Maps, Youtube etc) are updated - * @deprecated use {@link #setNdefPushMessageCallback} instead - * @hide - */ - @Deprecated - public void enableForegroundNdefPush(Activity activity, final NdefPushCallback callback) { - if (activity == null || callback == null) { - throw new NullPointerException(); - } - enforceResumed(activity); - LegacyCallbackWrapper callbackWrapper = new LegacyCallbackWrapper(callback); - mNfcActivityManager.setNdefPushMessageCallback(activity, callbackWrapper); - mNfcActivityManager.setOnNdefPushCompleteCallback(activity, callbackWrapper); - } - - /** * Enable NDEF Push feature. * <p>This API is for the Settings application. * @hide @@ -881,16 +831,28 @@ public final class NfcAdapter { } /** - * Return true if NDEF Push feature is enabled. - * <p>This function can return true even if NFC is currently turned-off. - * This indicates that NDEF Push is not currently active, but it has - * been requested by the user and will be active as soon as NFC is turned - * on. - * <p>If you want to check if NDEF PUsh sharing is currently active, use - * <code>{@link #isEnabled()} && {@link #isNdefPushEnabled()}</code> + * Return true if the NDEF Push (Android Beam) feature is enabled. + * <p>This function will return true only if both NFC is enabled, and the + * NDEF Push feature is enabled. + * <p>Note that if NFC is enabled but NDEF Push is disabled then this + * device can still <i>receive</i> NDEF messages, it just cannot send them. + * <p>Applications cannot directly toggle the NDEF Push feature, but they + * can request Settings UI allowing the user to toggle NDEF Push using + * <code>startActivity(new Intent(Settings.ACTION_NFCSHARING_SETTINGS))</code> + * <p>Example usage in an Activity that requires NDEF Push: + * <p><pre> + * protected void onResume() { + * super.onResume(); + * if (!nfcAdapter.isEnabled()) { + * startActivity(new Intent(Settings.ACTION_NFC_SETTINGS)); + * } else if (!nfcAdapter.isNdefPushEnabled()) { + * startActivity(new Intent(Settings.ACTION_NFCSHARING_SETTINGS)); + * } + * } + * </pre> * + * @see android.provider.Settings#ACTION_NFCSHARING_SETTINGS * @return true if NDEF Push feature is enabled - * @hide */ public boolean isNdefPushEnabled() { try { diff --git a/core/java/android/nfc/NfcManager.java b/core/java/android/nfc/NfcManager.java index 6ec2e21..2bbed57 100644 --- a/core/java/android/nfc/NfcManager.java +++ b/core/java/android/nfc/NfcManager.java @@ -40,6 +40,10 @@ public final class NfcManager { public NfcManager(Context context) { NfcAdapter adapter; context = context.getApplicationContext(); + if (context == null) { + throw new IllegalArgumentException( + "context not associated with any application (using a mock context?)"); + } try { adapter = NfcAdapter.getNfcAdapter(context); } catch (UnsupportedOperationException e) { diff --git a/core/java/android/os/AsyncTask.java b/core/java/android/os/AsyncTask.java index 9dea4c4..5e9abb7 100644 --- a/core/java/android/os/AsyncTask.java +++ b/core/java/android/os/AsyncTask.java @@ -195,6 +195,7 @@ public abstract class AsyncTask<Params, Progress, Result> { private volatile Status mStatus = Status.PENDING; + private final AtomicBoolean mCancelled = new AtomicBoolean(); private final AtomicBoolean mTaskInvoked = new AtomicBoolean(); private static class SerialExecutor implements Executor { @@ -261,6 +262,7 @@ public abstract class AsyncTask<Params, Progress, Result> { mTaskInvoked.set(true); Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + //noinspection unchecked return postResult(doInBackground(mParams)); } }; @@ -269,9 +271,7 @@ public abstract class AsyncTask<Params, Progress, Result> { @Override protected void done() { try { - final Result result = get(); - - postResultIfNotInvoked(result); + postResultIfNotInvoked(get()); } catch (InterruptedException e) { android.util.Log.w(LOG_TAG, e); } catch (ExecutionException e) { @@ -295,6 +295,7 @@ public abstract class AsyncTask<Params, Progress, Result> { } private Result postResult(Result result) { + @SuppressWarnings("unchecked") Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT, new AsyncTaskResult<Result>(this, result)); message.sendToTarget(); @@ -411,7 +412,7 @@ public abstract class AsyncTask<Params, Progress, Result> { * @see #cancel(boolean) */ public final boolean isCancelled() { - return mFuture.isCancelled(); + return mCancelled.get(); } /** @@ -444,6 +445,7 @@ public abstract class AsyncTask<Params, Progress, Result> { * @see #onCancelled(Object) */ public final boolean cancel(boolean mayInterruptIfRunning) { + mCancelled.set(true); return mFuture.cancel(mayInterruptIfRunning); } diff --git a/core/java/android/os/RecoverySystem.java b/core/java/android/os/RecoverySystem.java index 73e8d98..43cf74e 100644 --- a/core/java/android/os/RecoverySystem.java +++ b/core/java/android/os/RecoverySystem.java @@ -26,6 +26,7 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.FileWriter; import java.io.IOException; +import java.io.InputStream; import java.io.RandomAccessFile; import java.security.GeneralSecurityException; import java.security.PublicKey; @@ -103,7 +104,12 @@ public class RecoverySystem { Enumeration<? extends ZipEntry> entries = zip.entries(); while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); - trusted.add(cf.generateCertificate(zip.getInputStream(entry))); + InputStream is = zip.getInputStream(entry); + try { + trusted.add(cf.generateCertificate(is)); + } finally { + is.close(); + } } } finally { zip.close(); @@ -162,8 +168,6 @@ public class RecoverySystem { int commentSize = (footer[4] & 0xff) | ((footer[5] & 0xff) << 8); int signatureStart = (footer[0] & 0xff) | ((footer[1] & 0xff) << 8); - Log.v(TAG, String.format("comment size %d; signature start %d", - commentSize, signatureStart)); byte[] eocd = new byte[commentSize + 22]; raf.seek(fileLen - (commentSize + 22)); diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java index 83acef8..d724d56 100644 --- a/core/java/android/provider/ContactsContract.java +++ b/core/java/android/provider/ContactsContract.java @@ -6747,6 +6747,39 @@ public final class ContactsContract { */ public static final String NAMESPACE = DataColumns.DATA2; } + + /** + * <p> + * Convenient functionalities for "callable" data. Note that, this is NOT a separate data + * kind. + * </p> + * <p> + * This URI allows the ContactsProvider to return a unified result for "callable" data + * that users can use for calling purposes. {@link Phone} and {@link SipAddress} are the + * current examples for "callable", but may be expanded to the other types. + * </p> + * <p> + * Each returned row may have a different MIMETYPE and thus different interpretation for + * each column. For example the meaning for {@link Phone}'s type is different than + * {@link SipAddress}'s. + * </p> + * + * @hide + */ + public static final class Callable implements DataColumnsWithJoins, CommonColumns { + /** + * Similar to {@link Phone#CONTENT_URI}, but returns callable data instead of only + * phone numbers. + */ + public static final Uri CONTENT_URI = Uri.withAppendedPath(Data.CONTENT_URI, + "callables"); + /** + * Similar to {@link Phone#CONTENT_FILTER_URI}, but allows users to filter callable + * data. + */ + public static final Uri CONTENT_FILTER_URI = Uri.withAppendedPath(CONTENT_URI, + "filter"); + } } /** diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 65b4e7e..c44f23b 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -569,7 +569,26 @@ public final class Settings { "android.settings.DEVICE_INFO_SETTINGS"; /** - * Activity Action: Show NFC sharing settings. + * Activity Action: Show NFC settings. + * <p> + * This shows UI that allows NFC to be turned on or off. + * <p> + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing + * @see android.nfc.NfcAdapter#isEnabled() + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_NFC_SETTINGS = "android.settings.NFC_SETTINGS"; + + /** + * Activity Action: Show NFC Sharing settings. + * <p> + * This shows UI that allows NDEF Push (Android Beam) to be turned on or + * off. * <p> * In some cases, a matching Activity may not exist, so ensure you * safeguard against this. @@ -577,6 +596,7 @@ public final class Settings { * Input: Nothing. * <p> * Output: Nothing + * @see android.nfc.NfcAdapter#isNdefPushEnabled() */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_NFCSHARING_SETTINGS = @@ -2759,10 +2779,10 @@ public final class Settings { public static final String ACCESSIBILITY_SPEAK_PASSWORD = "speak_password"; /** - * If injection of accessibility enhancing JavaScript scripts + * If injection of accessibility enhancing JavaScript screen-reader * is enabled. * <p> - * Note: Accessibility injecting scripts are served by the + * Note: The JavaScript based screen-reader is served by the * Google infrastructure and enable users with disabilities to * efficiantly navigate in and explore web content. * </p> @@ -2775,6 +2795,22 @@ public final class Settings { "accessibility_script_injection"; /** + * The URL for the injected JavaScript based screen-reader used + * for providing accessiblity of content in WebView. + * <p> + * Note: The JavaScript based screen-reader is served by the + * Google infrastructure and enable users with disabilities to + * efficiently navigate in and explore web content. + * </p> + * <p> + * This property represents a string value. + * </p> + * @hide + */ + public static final String ACCESSIBILITY_SCREEN_READER_URL = + "accessibility_script_injection_url"; + + /** * Key bindings for navigation in built-in accessibility support for web content. * <p> * Note: These key bindings are for the built-in accessibility navigation for @@ -4032,6 +4068,28 @@ public final class Settings { public static final String SETUP_PREPAID_DETECTION_REDIR_HOST = "setup_prepaid_detection_redir_host"; + /** + * Whether the screensaver is enabled. + * @hide + */ + public static final String SCREENSAVER_ENABLED = "screensaver_enabled"; + + /** + * The user's chosen screensaver component. + * + * This component will be launched by the PhoneWindowManager after a timeout when not on + * battery, or upon dock insertion (if SCREENSAVER_ACTIVATE_ON_DOCK is set to 1). + * @hide + */ + public static final String SCREENSAVER_COMPONENT = "screensaver_component"; + + /** + * Whether the screensaver should be automatically launched when the device is inserted + * into a (desk) dock. + * @hide + */ + public static final String SCREENSAVER_ACTIVATE_ON_DOCK = "screensaver_activate_on_dock"; + /** {@hide} */ public static final String NETSTATS_ENABLED = "netstats_enabled"; /** {@hide} */ diff --git a/core/java/android/server/BluetoothAdapterStateMachine.java b/core/java/android/server/BluetoothAdapterStateMachine.java index 8ec79e2..ed59b03 100644 --- a/core/java/android/server/BluetoothAdapterStateMachine.java +++ b/core/java/android/server/BluetoothAdapterStateMachine.java @@ -62,6 +62,17 @@ import java.io.PrintWriter; * m1 = TURN_HOT * m2 = Transition to HotOff when number of process wanting BT on is 0. * POWER_STATE_CHANGED will make the transition. + * Note: + * The diagram above shows all the states and messages that trigger normal state changes. + * The diagram above does not capture everything: + * The diagram does not capture following messages. + * - messages that do not trigger state changes + * For example, PER_PROCESS_TURN_ON received in BluetoothOn state + * - unhandled messages + * For example, USER_TURN_ON received in BluetoothOn state + * - timeout messages + * The diagram does not capture error conditions and state recoveries. + * - For example POWER_STATE_CHANGED received in BluetoothOn state */ final class BluetoothAdapterStateMachine extends StateMachine { private static final String TAG = "BluetoothAdapterStateMachine"; diff --git a/core/java/android/server/BluetoothPanProfileHandler.java b/core/java/android/server/BluetoothPanProfileHandler.java index bfad747..41bb87f 100644 --- a/core/java/android/server/BluetoothPanProfileHandler.java +++ b/core/java/android/server/BluetoothPanProfileHandler.java @@ -377,16 +377,16 @@ final class BluetoothPanProfileHandler { try { ifcg = service.getInterfaceConfig(iface); if (ifcg != null) { + final LinkAddress linkAddr = ifcg.getLinkAddress(); InetAddress addr = null; - if (ifcg.addr == null || (addr = ifcg.addr.getAddress()) == null || + if (linkAddr == null || (addr = linkAddr.getAddress()) == null || addr.equals(NetworkUtils.numericToInetAddress("0.0.0.0")) || addr.equals(NetworkUtils.numericToInetAddress("::0"))) { addr = NetworkUtils.numericToInetAddress(address); } - ifcg.interfaceFlags = ifcg.interfaceFlags.replace("down", "up"); - ifcg.addr = new LinkAddress(addr, BLUETOOTH_PREFIX_LENGTH); - ifcg.interfaceFlags = ifcg.interfaceFlags.replace("running", ""); - ifcg.interfaceFlags = ifcg.interfaceFlags.replace(" "," "); + ifcg.setInterfaceUp(); + ifcg.clearFlag("running"); + ifcg.setLinkAddress(new LinkAddress(addr, BLUETOOTH_PREFIX_LENGTH)); service.setInterfaceConfig(iface, ifcg); if (cm.tether(iface) != ConnectivityManager.TETHER_ERROR_NO_ERROR) { Log.e(TAG, "Error tethering "+iface); diff --git a/core/java/android/service/textservice/SpellCheckerService.java b/core/java/android/service/textservice/SpellCheckerService.java index 28251a6..5282e61 100644 --- a/core/java/android/service/textservice/SpellCheckerService.java +++ b/core/java/android/service/textservice/SpellCheckerService.java @@ -138,6 +138,25 @@ public abstract class SpellCheckerService extends Service { } /** + * @hide + * The default implementation returns an array of SuggestionsInfo by simply calling + * onGetSuggestions(). + * When you override this method, make sure that suggestionsLimit is applied to suggestions + * that share the same start position and length. + */ + public SuggestionsInfo[] onGetSuggestionsMultipleForSentence(TextInfo[] textInfos, + int suggestionsLimit) { + final int length = textInfos.length; + final SuggestionsInfo[] retval = new SuggestionsInfo[length]; + for (int i = 0; i < length; ++i) { + retval[i] = onGetSuggestions(textInfos[i], suggestionsLimit); + retval[i].setCookieAndSequence( + textInfos[i].getCookie(), textInfos[i].getSequence()); + } + return retval; + } + + /** * Request to abort all tasks executed in SpellChecker. * This function will run on the incoming IPC thread. * So, this is not called on the main thread, @@ -196,6 +215,16 @@ public abstract class SpellCheckerService extends Service { } @Override + public void onGetSuggestionsMultipleForSentence( + TextInfo[] textInfos, int suggestionsLimit) { + try { + mListener.onGetSuggestionsForSentence( + mSession.onGetSuggestionsMultipleForSentence(textInfos, suggestionsLimit)); + } catch (RemoteException e) { + } + } + + @Override public void onCancel() { mSession.onCancel(); } diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 18167b6..7ce96c0 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -18,7 +18,6 @@ package android.service.wallpaper; import com.android.internal.os.HandlerCaller; import com.android.internal.view.BaseIWindow; -import com.android.internal.view.BaseInputHandler; import com.android.internal.view.BaseSurfaceHolder; import android.annotation.SdkConstant; @@ -45,8 +44,8 @@ import android.view.Gravity; import android.view.IWindowSession; import android.view.InputChannel; import android.view.InputDevice; -import android.view.InputHandler; -import android.view.InputQueue; +import android.view.InputEvent; +import android.view.InputEventReceiver; import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.View; @@ -228,24 +227,29 @@ public abstract class WallpaperService extends Service { } }; - - final InputHandler mInputHandler = new BaseInputHandler() { + + final class WallpaperInputEventReceiver extends InputEventReceiver { + public WallpaperInputEventReceiver(InputChannel inputChannel, Looper looper) { + super(inputChannel, looper); + } + @Override - public void handleMotion(MotionEvent event, - InputQueue.FinishedCallback finishedCallback) { + public void onInputEvent(InputEvent event) { boolean handled = false; try { - int source = event.getSource(); - if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) { - dispatchPointer(event); + if (event instanceof MotionEvent + && (event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { + MotionEvent dup = MotionEvent.obtainNoHistory((MotionEvent)event); + dispatchPointer(dup); handled = true; } } finally { - finishedCallback.finished(handled); + finishInputEvent(event, handled); } } - }; - + } + WallpaperInputEventReceiver mInputEventReceiver; + final BaseIWindow mWindow = new BaseIWindow() { @Override public void resized(int w, int h, Rect coveredInsets, @@ -534,6 +538,8 @@ public abstract class WallpaperService extends Service { } Message msg = mCaller.obtainMessageO(MSG_TOUCH_EVENT, event); mCaller.sendMessage(msg); + } else { + event.recycle(); } } @@ -599,8 +605,8 @@ public abstract class WallpaperService extends Service { } mCreated = true; - InputQueue.registerInputChannel(mInputChannel, mInputHandler, - Looper.myQueue()); + mInputEventReceiver = new WallpaperInputEventReceiver( + mInputChannel, Looper.myLooper()); } mSurfaceHolder.mSurfaceLock.lock(); @@ -902,8 +908,9 @@ public abstract class WallpaperService extends Service { if (DEBUG) Log.v(TAG, "Removing window and destroying surface " + mSurfaceHolder.getSurface() + " of: " + this); - if (mInputChannel != null) { - InputQueue.unregisterInputChannel(mInputChannel); + if (mInputEventReceiver != null) { + mInputEventReceiver.dispose(); + mInputEventReceiver = null; } mSession.remove(mWindow); @@ -970,6 +977,8 @@ public abstract class WallpaperService extends Service { public void dispatchPointer(MotionEvent event) { if (mEngine != null) { mEngine.dispatchPointer(event); + } else { + event.recycle(); } } diff --git a/core/java/android/speech/tts/AudioMessageParams.java b/core/java/android/speech/tts/AudioMessageParams.java deleted file mode 100644 index 29b4367..0000000 --- a/core/java/android/speech/tts/AudioMessageParams.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * 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.speech.tts; - -import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher; - -class AudioMessageParams extends MessageParams { - private final BlockingMediaPlayer mPlayer; - - AudioMessageParams(UtteranceProgressDispatcher dispatcher, - String callingApp, BlockingMediaPlayer player) { - super(dispatcher, callingApp); - mPlayer = player; - } - - BlockingMediaPlayer getPlayer() { - return mPlayer; - } - - @Override - int getType() { - return TYPE_AUDIO; - } - -} diff --git a/core/java/android/speech/tts/AudioPlaybackHandler.java b/core/java/android/speech/tts/AudioPlaybackHandler.java index 46a78dc..d63f605 100644 --- a/core/java/android/speech/tts/AudioPlaybackHandler.java +++ b/core/java/android/speech/tts/AudioPlaybackHandler.java @@ -15,44 +15,20 @@ */ package android.speech.tts; -import android.media.AudioFormat; -import android.media.AudioTrack; -import android.text.TextUtils; import android.util.Log; import java.util.Iterator; -import java.util.concurrent.PriorityBlockingQueue; -import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.LinkedBlockingQueue; class AudioPlaybackHandler { private static final String TAG = "TTS.AudioPlaybackHandler"; - private static final boolean DBG_THREADING = false; private static final boolean DBG = false; - private static final int MIN_AUDIO_BUFFER_SIZE = 8192; - - private static final int SYNTHESIS_START = 1; - private static final int SYNTHESIS_DATA_AVAILABLE = 2; - private static final int SYNTHESIS_DONE = 3; - - private static final int PLAY_AUDIO = 5; - private static final int PLAY_SILENCE = 6; - - private static final int SHUTDOWN = -1; - - private static final int DEFAULT_PRIORITY = 1; - private static final int HIGH_PRIORITY = 0; - - private final PriorityBlockingQueue<ListEntry> mQueue = - new PriorityBlockingQueue<ListEntry>(); + private final LinkedBlockingQueue<PlaybackQueueItem> mQueue = + new LinkedBlockingQueue<PlaybackQueueItem>(); private final Thread mHandlerThread; - private volatile MessageParams mCurrentParams = null; - // Used only for book keeping and error detection. - private volatile SynthesisMessageParams mLastSynthesisRequest = null; - // Used to order incoming messages in our priority queue. - private final AtomicLong mSequenceIdCtr = new AtomicLong(0); - + private volatile PlaybackQueueItem mCurrentWorkItem = null; AudioPlaybackHandler() { mHandlerThread = new Thread(new MessageLoop(), "TTS.AudioPlaybackThread"); @@ -62,82 +38,38 @@ class AudioPlaybackHandler { mHandlerThread.start(); } - /** - * Stops all synthesis for a given {@code token}. If the current token - * is currently being processed, an effort will be made to stop it but - * that is not guaranteed. - * - * NOTE: This assumes that all other messages in the queue with {@code token} - * have been removed already. - * - * NOTE: Must be called synchronized on {@code AudioPlaybackHandler.this}. - */ - private void stop(MessageParams token) { - if (token == null) { + private void stop(PlaybackQueueItem item) { + if (item == null) { return; } - if (DBG) Log.d(TAG, "Stopping token : " + token); + item.stop(false); + } - if (token.getType() == MessageParams.TYPE_SYNTHESIS) { - AudioTrack current = ((SynthesisMessageParams) token).getAudioTrack(); - if (current != null) { - // Stop the current audio track if it's still playing. - // The audio track is thread safe in this regard. The current - // handleSynthesisDataAvailable call will return soon after this - // call. - current.stop(); - } - // This is safe because PlaybackSynthesisCallback#stop would have - // been called before this method, and will no longer enqueue any - // audio for this token. - // - // (Even if it did, all it would result in is a warning message). - mQueue.add(new ListEntry(SYNTHESIS_DONE, token, HIGH_PRIORITY)); - } else if (token.getType() == MessageParams.TYPE_AUDIO) { - ((AudioMessageParams) token).getPlayer().stop(); - // No cleanup required for audio messages. - } else if (token.getType() == MessageParams.TYPE_SILENCE) { - ((SilenceMessageParams) token).getConditionVariable().open(); - // No cleanup required for silence messages. + public void enqueue(PlaybackQueueItem item) { + try { + mQueue.put(item); + } catch (InterruptedException ie) { + // This exception will never be thrown, since we allow our queue + // to be have an unbounded size. put() will therefore never block. } } - // ----------------------------------------------------- - // Methods that add and remove elements from the queue. These do not - // need to be synchronized strictly speaking, but they make the behaviour - // a lot more predictable. (though it would still be correct without - // synchronization). - // ----------------------------------------------------- - - synchronized public void removePlaybackItems(String callingApp) { - if (DBG_THREADING) Log.d(TAG, "Removing all callback items for : " + callingApp); - removeMessages(callingApp); + public void stopForApp(Object callerIdentity) { + if (DBG) Log.d(TAG, "Removing all callback items for : " + callerIdentity); + removeWorkItemsFor(callerIdentity); - final MessageParams current = getCurrentParams(); - if (current != null && TextUtils.equals(callingApp, current.getCallingApp())) { + final PlaybackQueueItem current = mCurrentWorkItem; + if (current != null && (current.getCallerIdentity() == callerIdentity)) { stop(current); } - - final MessageParams lastSynthesis = mLastSynthesisRequest; - - if (lastSynthesis != null && lastSynthesis != current && - TextUtils.equals(callingApp, lastSynthesis.getCallingApp())) { - stop(lastSynthesis); - } } - synchronized public void removeAllItems() { - if (DBG_THREADING) Log.d(TAG, "Removing all items"); + public void stop() { + if (DBG) Log.d(TAG, "Stopping all items"); removeAllMessages(); - final MessageParams current = getCurrentParams(); - final MessageParams lastSynthesis = mLastSynthesisRequest; - stop(current); - - if (lastSynthesis != null && lastSynthesis != current) { - stop(lastSynthesis); - } + stop(mCurrentWorkItem); } /** @@ -145,489 +77,64 @@ class AudioPlaybackHandler { * being handled, true otherwise. */ public boolean isSpeaking() { - return (mQueue.peek() != null) || (mCurrentParams != null); + return (mQueue.peek() != null) || (mCurrentWorkItem != null); } /** * Shut down the audio playback thread. */ - synchronized public void quit() { + public void quit() { removeAllMessages(); - stop(getCurrentParams()); - mQueue.add(new ListEntry(SHUTDOWN, null, HIGH_PRIORITY)); - } - - synchronized void enqueueSynthesisStart(SynthesisMessageParams token) { - if (DBG_THREADING) Log.d(TAG, "Enqueuing synthesis start : " + token); - mQueue.add(new ListEntry(SYNTHESIS_START, token)); - } - - synchronized void enqueueSynthesisDataAvailable(SynthesisMessageParams token) { - if (DBG_THREADING) Log.d(TAG, "Enqueuing synthesis data available : " + token); - mQueue.add(new ListEntry(SYNTHESIS_DATA_AVAILABLE, token)); - } - - synchronized void enqueueSynthesisDone(SynthesisMessageParams token) { - if (DBG_THREADING) Log.d(TAG, "Enqueuing synthesis done : " + token); - mQueue.add(new ListEntry(SYNTHESIS_DONE, token)); - } - - synchronized void enqueueAudio(AudioMessageParams token) { - if (DBG_THREADING) Log.d(TAG, "Enqueuing audio : " + token); - mQueue.add(new ListEntry(PLAY_AUDIO, token)); - } - - synchronized void enqueueSilence(SilenceMessageParams token) { - if (DBG_THREADING) Log.d(TAG, "Enqueuing silence : " + token); - mQueue.add(new ListEntry(PLAY_SILENCE, token)); - } - - // ----------------------------------------- - // End of public API methods. - // ----------------------------------------- - - // ----------------------------------------- - // Methods for managing the message queue. - // ----------------------------------------- - - /* - * The MessageLoop is a handler like implementation that - * processes messages from a priority queue. - */ - private final class MessageLoop implements Runnable { - @Override - public void run() { - while (true) { - ListEntry entry = null; - try { - entry = mQueue.take(); - } catch (InterruptedException ie) { - return; - } - - if (entry.mWhat == SHUTDOWN) { - if (DBG) Log.d(TAG, "MessageLoop : Shutting down"); - return; - } - - if (DBG) { - Log.d(TAG, "MessageLoop : Handling message :" + entry.mWhat - + " ,seqId : " + entry.mSequenceId); - } - - setCurrentParams(entry.mMessage); - handleMessage(entry); - setCurrentParams(null); - } - } + stop(mCurrentWorkItem); + mHandlerThread.interrupt(); } /* * Atomically clear the queue of all messages. */ - synchronized private void removeAllMessages() { + private void removeAllMessages() { mQueue.clear(); } /* * Remove all messages that originate from a given calling app. */ - synchronized private void removeMessages(String callingApp) { - Iterator<ListEntry> it = mQueue.iterator(); + private void removeWorkItemsFor(Object callerIdentity) { + Iterator<PlaybackQueueItem> it = mQueue.iterator(); while (it.hasNext()) { - final ListEntry current = it.next(); - // The null check is to prevent us from removing control messages, - // such as a shutdown message. - if (current.mMessage != null && - callingApp.equals(current.mMessage.getCallingApp())) { + final PlaybackQueueItem item = it.next(); + if (item.getCallerIdentity() == callerIdentity) { it.remove(); } } } /* - * An element of our priority queue of messages. Each message has a priority, - * and a sequence id (defined by the order of enqueue calls). Among messages - * with the same priority, messages that were received earlier win out. + * The MessageLoop is a handler like implementation that + * processes messages from a priority queue. */ - private final class ListEntry implements Comparable<ListEntry> { - final int mWhat; - final MessageParams mMessage; - final int mPriority; - final long mSequenceId; - - private ListEntry(int what, MessageParams message) { - this(what, message, DEFAULT_PRIORITY); - } - - private ListEntry(int what, MessageParams message, int priority) { - mWhat = what; - mMessage = message; - mPriority = priority; - mSequenceId = mSequenceIdCtr.incrementAndGet(); - } - + private final class MessageLoop implements Runnable { @Override - public int compareTo(ListEntry that) { - if (that == this) { - return 0; - } - - // Note that this is always 0, 1 or -1. - int priorityDiff = mPriority - that.mPriority; - if (priorityDiff == 0) { - // The == case cannot occur. - return (mSequenceId < that.mSequenceId) ? -1 : 1; - } - - return priorityDiff; - } - } - - private void setCurrentParams(MessageParams p) { - if (DBG_THREADING) { - if (p != null) { - Log.d(TAG, "Started handling :" + p); - } else { - Log.d(TAG, "End handling : " + mCurrentParams); - } - } - mCurrentParams = p; - } - - private MessageParams getCurrentParams() { - return mCurrentParams; - } - - // ----------------------------------------- - // Methods for dealing with individual messages, the methods - // below do the actual work. - // ----------------------------------------- - - private void handleMessage(ListEntry entry) { - final MessageParams msg = entry.mMessage; - if (entry.mWhat == SYNTHESIS_START) { - handleSynthesisStart(msg); - } else if (entry.mWhat == SYNTHESIS_DATA_AVAILABLE) { - handleSynthesisDataAvailable(msg); - } else if (entry.mWhat == SYNTHESIS_DONE) { - handleSynthesisDone(msg); - } else if (entry.mWhat == PLAY_AUDIO) { - handleAudio(msg); - } else if (entry.mWhat == PLAY_SILENCE) { - handleSilence(msg); - } - } - - // Currently implemented as blocking the audio playback thread for the - // specified duration. If a call to stop() is made, the thread - // unblocks. - private void handleSilence(MessageParams msg) { - if (DBG) Log.d(TAG, "handleSilence()"); - SilenceMessageParams params = (SilenceMessageParams) msg; - params.getDispatcher().dispatchOnStart(); - if (params.getSilenceDurationMs() > 0) { - params.getConditionVariable().block(params.getSilenceDurationMs()); - } - params.getDispatcher().dispatchOnDone(); - if (DBG) Log.d(TAG, "handleSilence() done."); - } - - // Plays back audio from a given URI. No TTS engine involvement here. - private void handleAudio(MessageParams msg) { - if (DBG) Log.d(TAG, "handleAudio()"); - AudioMessageParams params = (AudioMessageParams) msg; - params.getDispatcher().dispatchOnStart(); - // Note that the BlockingMediaPlayer spawns a separate thread. - // - // TODO: This can be avoided. - params.getPlayer().startAndWait(); - params.getDispatcher().dispatchOnDone(); - if (DBG) Log.d(TAG, "handleAudio() done."); - } - - // Denotes the start of a new synthesis request. We create a new - // audio track, and prepare it for incoming data. - // - // Note that since all TTS synthesis happens on a single thread, we - // should ALWAYS see the following order : - // - // handleSynthesisStart -> handleSynthesisDataAvailable(*) -> handleSynthesisDone - // OR - // handleSynthesisCompleteDataAvailable. - private void handleSynthesisStart(MessageParams msg) { - if (DBG) Log.d(TAG, "handleSynthesisStart()"); - final SynthesisMessageParams param = (SynthesisMessageParams) msg; - - // Oops, looks like the engine forgot to call done(). We go through - // extra trouble to clean the data to prevent the AudioTrack resources - // from being leaked. - if (mLastSynthesisRequest != null) { - Log.e(TAG, "Error : Missing call to done() for request : " + - mLastSynthesisRequest); - handleSynthesisDone(mLastSynthesisRequest); - } - - mLastSynthesisRequest = param; - - // Create the audio track. - final AudioTrack audioTrack = createStreamingAudioTrack(param); - - if (DBG) Log.d(TAG, "Created audio track [" + audioTrack.hashCode() + "]"); - - param.setAudioTrack(audioTrack); - msg.getDispatcher().dispatchOnStart(); - } - - // More data available to be flushed to the audio track. - private void handleSynthesisDataAvailable(MessageParams msg) { - final SynthesisMessageParams param = (SynthesisMessageParams) msg; - if (param.getAudioTrack() == null) { - Log.w(TAG, "Error : null audio track in handleDataAvailable : " + param); - return; - } - - if (param != mLastSynthesisRequest) { - Log.e(TAG, "Call to dataAvailable without done() / start()"); - return; - } - - final AudioTrack audioTrack = param.getAudioTrack(); - final SynthesisMessageParams.ListEntry bufferCopy = param.getNextBuffer(); - - if (bufferCopy == null) { - Log.e(TAG, "No buffers available to play."); - return; - } - - int playState = audioTrack.getPlayState(); - if (playState == AudioTrack.PLAYSTATE_STOPPED) { - if (DBG) Log.d(TAG, "AudioTrack stopped, restarting : " + audioTrack.hashCode()); - audioTrack.play(); - } - int count = 0; - while (count < bufferCopy.mBytes.length) { - // Note that we don't take bufferCopy.mOffset into account because - // it is guaranteed to be 0. - int written = audioTrack.write(bufferCopy.mBytes, count, bufferCopy.mBytes.length); - if (written <= 0) { - break; - } - count += written; - } - param.mBytesWritten += count; - param.mLogger.onPlaybackStart(); - } - - // Wait for the audio track to stop playing, and then release its resources. - private void handleSynthesisDone(MessageParams msg) { - final SynthesisMessageParams params = (SynthesisMessageParams) msg; - - if (DBG) Log.d(TAG, "handleSynthesisDone()"); - final AudioTrack audioTrack = params.getAudioTrack(); - - if (audioTrack == null) { - // There was already a call to handleSynthesisDone for - // this token. - return; - } - - if (params.mBytesWritten < params.mAudioBufferSize) { - if (DBG) Log.d(TAG, "Stopping audio track to flush audio, state was : " + - audioTrack.getPlayState()); - params.mIsShortUtterance = true; - audioTrack.stop(); - } - - if (DBG) Log.d(TAG, "Waiting for audio track to complete : " + - audioTrack.hashCode()); - blockUntilDone(params); - if (DBG) Log.d(TAG, "Releasing audio track [" + audioTrack.hashCode() + "]"); - - // The last call to AudioTrack.write( ) will return only after - // all data from the audioTrack has been sent to the mixer, so - // it's safe to release at this point. Make sure release() and the call - // that set the audio track to null are performed atomically. - synchronized (this) { - // Never allow the audioTrack to be observed in a state where - // it is released but non null. The only case this might happen - // is in the various stopFoo methods that call AudioTrack#stop from - // different threads, but they are synchronized on AudioPlayBackHandler#this - // too. - audioTrack.release(); - params.setAudioTrack(null); - } - if (params.isError()) { - params.getDispatcher().dispatchOnError(); - } else { - params.getDispatcher().dispatchOnDone(); - } - mLastSynthesisRequest = null; - params.mLogger.onWriteData(); - } - - /** - * The minimum increment of time to wait for an audiotrack to finish - * playing. - */ - private static final long MIN_SLEEP_TIME_MS = 20; - - /** - * The maximum increment of time to sleep while waiting for an audiotrack - * to finish playing. - */ - private static final long MAX_SLEEP_TIME_MS = 2500; - - /** - * The maximum amount of time to wait for an audio track to make progress while - * it remains in PLAYSTATE_PLAYING. This should never happen in normal usage, but - * could happen in exceptional circumstances like a media_server crash. - */ - private static final long MAX_PROGRESS_WAIT_MS = MAX_SLEEP_TIME_MS; - - private static void blockUntilDone(SynthesisMessageParams params) { - if (params.mAudioTrack == null || params.mBytesWritten <= 0) { - return; - } - - if (params.mIsShortUtterance) { - // In this case we would have called AudioTrack#stop() to flush - // buffers to the mixer. This makes the playback head position - // unobservable and notification markers do not work reliably. We - // have no option but to wait until we think the track would finish - // playing and release it after. - // - // This isn't as bad as it looks because (a) We won't end up waiting - // for much longer than we should because even at 4khz mono, a short - // utterance weighs in at about 2 seconds, and (b) such short utterances - // are expected to be relatively infrequent and in a stream of utterances - // this shows up as a slightly longer pause. - blockUntilEstimatedCompletion(params); - } else { - blockUntilCompletion(params); - } - } - - private static void blockUntilEstimatedCompletion(SynthesisMessageParams params) { - final int lengthInFrames = params.mBytesWritten / params.mBytesPerFrame; - final long estimatedTimeMs = (lengthInFrames * 1000 / params.mSampleRateInHz); - - if (DBG) Log.d(TAG, "About to sleep for: " + estimatedTimeMs + "ms for a short utterance"); - - try { - Thread.sleep(estimatedTimeMs); - } catch (InterruptedException ie) { - // Do nothing. - } - } - - private static void blockUntilCompletion(SynthesisMessageParams params) { - final AudioTrack audioTrack = params.mAudioTrack; - final int lengthInFrames = params.mBytesWritten / params.mBytesPerFrame; - - int previousPosition = -1; - int currentPosition = 0; - long blockedTimeMs = 0; - - while ((currentPosition = audioTrack.getPlaybackHeadPosition()) < lengthInFrames && - audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) { - - final long estimatedTimeMs = ((lengthInFrames - currentPosition) * 1000) / - audioTrack.getSampleRate(); - final long sleepTimeMs = clip(estimatedTimeMs, MIN_SLEEP_TIME_MS, MAX_SLEEP_TIME_MS); - - // Check if the audio track has made progress since the last loop - // iteration. We should then add in the amount of time that was - // spent sleeping in the last iteration. - if (currentPosition == previousPosition) { - // This works only because the sleep time that would have been calculated - // would be the same in the previous iteration too. - blockedTimeMs += sleepTimeMs; - // If we've taken too long to make progress, bail. - if (blockedTimeMs > MAX_PROGRESS_WAIT_MS) { - Log.w(TAG, "Waited unsuccessfully for " + MAX_PROGRESS_WAIT_MS + "ms " + - "for AudioTrack to make progress, Aborting"); - break; + public void run() { + while (true) { + PlaybackQueueItem item = null; + try { + item = mQueue.take(); + } catch (InterruptedException ie) { + if (DBG) Log.d(TAG, "MessageLoop : Shutting down (interrupted)"); + return; } - } else { - blockedTimeMs = 0; - } - previousPosition = currentPosition; - if (DBG) Log.d(TAG, "About to sleep for : " + sleepTimeMs + " ms," + - " Playback position : " + currentPosition + ", Length in frames : " - + lengthInFrames); - try { - Thread.sleep(sleepTimeMs); - } catch (InterruptedException ie) { - break; - } - } - } - - private static final long clip(long value, long min, long max) { - if (value < min) { - return min; - } - - if (value > max) { - return max; - } - - return value; - } - - private static AudioTrack createStreamingAudioTrack(SynthesisMessageParams params) { - final int channelConfig = getChannelConfig(params.mChannelCount); - final int sampleRateInHz = params.mSampleRateInHz; - final int audioFormat = params.mAudioFormat; - - int minBufferSizeInBytes - = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat); - int bufferSizeInBytes = Math.max(MIN_AUDIO_BUFFER_SIZE, minBufferSizeInBytes); - - AudioTrack audioTrack = new AudioTrack(params.mStreamType, sampleRateInHz, channelConfig, - audioFormat, bufferSizeInBytes, AudioTrack.MODE_STREAM); - if (audioTrack.getState() != AudioTrack.STATE_INITIALIZED) { - Log.w(TAG, "Unable to create audio track."); - audioTrack.release(); - return null; - } - params.mAudioBufferSize = bufferSizeInBytes; + // If stop() or stopForApp() are called between mQueue.take() + // returning and mCurrentWorkItem being set, the current work item + // will be run anyway. - setupVolume(audioTrack, params.mVolume, params.mPan); - return audioTrack; - } - - static int getChannelConfig(int channelCount) { - if (channelCount == 1) { - return AudioFormat.CHANNEL_OUT_MONO; - } else if (channelCount == 2){ - return AudioFormat.CHANNEL_OUT_STEREO; - } - - return 0; - } - - private static void setupVolume(AudioTrack audioTrack, float volume, float pan) { - float vol = clip(volume, 0.0f, 1.0f); - float panning = clip(pan, -1.0f, 1.0f); - float volLeft = vol; - float volRight = vol; - if (panning > 0.0f) { - volLeft *= (1.0f - panning); - } else if (panning < 0.0f) { - volRight *= (1.0f + panning); - } - if (DBG) Log.d(TAG, "volLeft=" + volLeft + ",volRight=" + volRight); - if (audioTrack.setStereoVolume(volLeft, volRight) != AudioTrack.SUCCESS) { - Log.e(TAG, "Failed to set volume"); + mCurrentWorkItem = item; + item.run(); + mCurrentWorkItem = null; + } } } - private static float clip(float value, float min, float max) { - return value > max ? max : (value < min ? min : value); - } - } diff --git a/core/java/android/speech/tts/BlockingMediaPlayer.java b/core/java/android/speech/tts/AudioPlaybackQueueItem.java index 3cf60dd..1a1fda8 100644 --- a/core/java/android/speech/tts/BlockingMediaPlayer.java +++ b/core/java/android/speech/tts/AudioPlaybackQueueItem.java @@ -19,93 +19,44 @@ import android.content.Context; import android.media.MediaPlayer; import android.net.Uri; import android.os.ConditionVariable; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; +import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher; import android.util.Log; -/** - * A media player that allows blocking to wait for it to finish. - */ -class BlockingMediaPlayer { - - private static final String TAG = "BlockMediaPlayer"; - - private static final String MEDIA_PLAYER_THREAD_NAME = "TTS-MediaPlayer"; +class AudioPlaybackQueueItem extends PlaybackQueueItem { + private static final String TAG = "TTS.AudioQueueItem"; private final Context mContext; private final Uri mUri; private final int mStreamType; + private final ConditionVariable mDone; - // Only accessed on the Handler thread private MediaPlayer mPlayer; private volatile boolean mFinished; - /** - * Creates a new blocking media player. - * Creating a blocking media player is a cheap operation. - * - * @param context - * @param uri - * @param streamType - */ - public BlockingMediaPlayer(Context context, Uri uri, int streamType) { + AudioPlaybackQueueItem(UtteranceProgressDispatcher dispatcher, + Object callerIdentity, + Context context, Uri uri, int streamType) { + super(dispatcher, callerIdentity); + mContext = context; mUri = uri; mStreamType = streamType; - mDone = new ConditionVariable(); - - } - /** - * Starts playback and waits for it to finish. - * Can be called from any thread. - * - * @return {@code true} if the playback finished normally, {@code false} if the playback - * failed or {@link #stop} was called before the playback finished. - */ - public boolean startAndWait() { - HandlerThread thread = new HandlerThread(MEDIA_PLAYER_THREAD_NAME); - thread.start(); - Handler handler = new Handler(thread.getLooper()); + mDone = new ConditionVariable(); + mPlayer = null; mFinished = false; - handler.post(new Runnable() { - @Override - public void run() { - startPlaying(); - } - }); - mDone.block(); - handler.post(new Runnable() { - @Override - public void run() { - finish(); - // No new messages should get posted to the handler thread after this - Looper.myLooper().quit(); - } - }); - return mFinished; } + @Override + public void run() { + final UtteranceProgressDispatcher dispatcher = getDispatcher(); - /** - * Stops playback. Can be called multiple times. - * Can be called from any thread. - */ - public void stop() { - mDone.open(); - } - - /** - * Starts playback. - * Called on the handler thread. - */ - private void startPlaying() { + dispatcher.dispatchOnStart(); mPlayer = MediaPlayer.create(mContext, mUri); if (mPlayer == null) { - Log.w(TAG, "Failed to play " + mUri); - mDone.open(); + dispatcher.dispatchOnError(); return; } + try { mPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() { @Override @@ -124,16 +75,20 @@ class BlockingMediaPlayer { }); mPlayer.setAudioStreamType(mStreamType); mPlayer.start(); + mDone.block(); + finish(); } catch (IllegalArgumentException ex) { Log.w(TAG, "MediaPlayer failed", ex); mDone.open(); } + + if (mFinished) { + dispatcher.dispatchOnDone(); + } else { + dispatcher.dispatchOnError(); + } } - /** - * Stops playback and release the media player. - * Called on the handler thread. - */ private void finish() { try { mPlayer.stop(); @@ -143,4 +98,8 @@ class BlockingMediaPlayer { mPlayer.release(); } -}
\ No newline at end of file + @Override + void stop(boolean isError) { + mDone.open(); + } +} diff --git a/core/java/android/speech/tts/BlockingAudioTrack.java b/core/java/android/speech/tts/BlockingAudioTrack.java new file mode 100644 index 0000000..fcadad7 --- /dev/null +++ b/core/java/android/speech/tts/BlockingAudioTrack.java @@ -0,0 +1,338 @@ +// Copyright 2011 Google Inc. All Rights Reserved. + +package android.speech.tts; + +import android.media.AudioFormat; +import android.media.AudioTrack; +import android.util.Log; + +/** + * Exposes parts of the {@link AudioTrack} API by delegating calls to an + * underlying {@link AudioTrack}. Additionally, provides methods like + * {@link #waitAndRelease()} that will block until all audiotrack + * data has been flushed to the mixer, and is estimated to have completed + * playback. + */ +class BlockingAudioTrack { + private static final String TAG = "TTS.BlockingAudioTrack"; + private static final boolean DBG = false; + + + /** + * The minimum increment of time to wait for an AudioTrack to finish + * playing. + */ + private static final long MIN_SLEEP_TIME_MS = 20; + + /** + * The maximum increment of time to sleep while waiting for an AudioTrack + * to finish playing. + */ + private static final long MAX_SLEEP_TIME_MS = 2500; + + /** + * The maximum amount of time to wait for an audio track to make progress while + * it remains in PLAYSTATE_PLAYING. This should never happen in normal usage, but + * could happen in exceptional circumstances like a media_server crash. + */ + private static final long MAX_PROGRESS_WAIT_MS = MAX_SLEEP_TIME_MS; + + /** + * Minimum size of the buffer of the underlying {@link android.media.AudioTrack} + * we create. + */ + private static final int MIN_AUDIO_BUFFER_SIZE = 8192; + + + private final int mStreamType; + private final int mSampleRateInHz; + private final int mAudioFormat; + private final int mChannelCount; + private final float mVolume; + private final float mPan; + + private final int mBytesPerFrame; + /** + * A "short utterance" is one that uses less bytes than the audio + * track buffer size (mAudioBufferSize). In this case, we need to call + * {@link AudioTrack#stop()} to send pending buffers to the mixer, and slightly + * different logic is required to wait for the track to finish. + * + * Not volatile, accessed only from the audio playback thread. + */ + private boolean mIsShortUtterance; + /** + * Will be valid after a call to {@link #init()}. + */ + private int mAudioBufferSize; + private int mBytesWritten = 0; + + private AudioTrack mAudioTrack; + private volatile boolean mStopped; + // Locks the initialization / uninitialization of the audio track. + // This is required because stop() will throw an illegal state exception + // if called before init() or after mAudioTrack.release(). + private final Object mAudioTrackLock = new Object(); + + BlockingAudioTrack(int streamType, int sampleRate, + int audioFormat, int channelCount, + float volume, float pan) { + mStreamType = streamType; + mSampleRateInHz = sampleRate; + mAudioFormat = audioFormat; + mChannelCount = channelCount; + mVolume = volume; + mPan = pan; + + mBytesPerFrame = getBytesPerFrame(mAudioFormat) * mChannelCount; + mIsShortUtterance = false; + mAudioBufferSize = 0; + mBytesWritten = 0; + + mAudioTrack = null; + mStopped = false; + } + + public void init() { + AudioTrack track = createStreamingAudioTrack(); + + synchronized (mAudioTrackLock) { + mAudioTrack = track; + } + } + + public void stop() { + synchronized (mAudioTrackLock) { + if (mAudioTrack != null) { + mAudioTrack.stop(); + } + } + mStopped = true; + } + + public int write(byte[] data) { + if (mAudioTrack == null || mStopped) { + return -1; + } + final int bytesWritten = writeToAudioTrack(mAudioTrack, data); + mBytesWritten += bytesWritten; + return bytesWritten; + } + + public void waitAndRelease() { + // For "small" audio tracks, we have to stop() them to make them mixable, + // else the audio subsystem will wait indefinitely for us to fill the buffer + // before rendering the track mixable. + // + // If mStopped is true, the track would already have been stopped, so not + // much point not doing that again. + if (mBytesWritten < mAudioBufferSize && !mStopped) { + if (DBG) { + Log.d(TAG, "Stopping audio track to flush audio, state was : " + + mAudioTrack.getPlayState() + ",stopped= " + mStopped); + } + + mIsShortUtterance = true; + mAudioTrack.stop(); + } + + // Block until the audio track is done only if we haven't stopped yet. + if (!mStopped) { + if (DBG) Log.d(TAG, "Waiting for audio track to complete : " + mAudioTrack.hashCode()); + blockUntilDone(mAudioTrack); + } + + // The last call to AudioTrack.write( ) will return only after + // all data from the audioTrack has been sent to the mixer, so + // it's safe to release at this point. + if (DBG) Log.d(TAG, "Releasing audio track [" + mAudioTrack.hashCode() + "]"); + synchronized (mAudioTrackLock) { + mAudioTrack.release(); + mAudioTrack = null; + } + } + + + static int getChannelConfig(int channelCount) { + if (channelCount == 1) { + return AudioFormat.CHANNEL_OUT_MONO; + } else if (channelCount == 2){ + return AudioFormat.CHANNEL_OUT_STEREO; + } + + return 0; + } + + long getAudioLengthMs(int numBytes) { + final int unconsumedFrames = numBytes / mBytesPerFrame; + final long estimatedTimeMs = unconsumedFrames * 1000 / mSampleRateInHz; + + return estimatedTimeMs; + } + + private static int writeToAudioTrack(AudioTrack audioTrack, byte[] bytes) { + if (audioTrack.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) { + if (DBG) Log.d(TAG, "AudioTrack not playing, restarting : " + audioTrack.hashCode()); + audioTrack.play(); + } + + int count = 0; + while (count < bytes.length) { + // Note that we don't take bufferCopy.mOffset into account because + // it is guaranteed to be 0. + int written = audioTrack.write(bytes, count, bytes.length); + if (written <= 0) { + break; + } + count += written; + } + return count; + } + + private AudioTrack createStreamingAudioTrack() { + final int channelConfig = getChannelConfig(mChannelCount); + + int minBufferSizeInBytes + = AudioTrack.getMinBufferSize(mSampleRateInHz, channelConfig, mAudioFormat); + int bufferSizeInBytes = Math.max(MIN_AUDIO_BUFFER_SIZE, minBufferSizeInBytes); + + AudioTrack audioTrack = new AudioTrack(mStreamType, mSampleRateInHz, channelConfig, + mAudioFormat, bufferSizeInBytes, AudioTrack.MODE_STREAM); + if (audioTrack.getState() != AudioTrack.STATE_INITIALIZED) { + Log.w(TAG, "Unable to create audio track."); + audioTrack.release(); + return null; + } + + mAudioBufferSize = bufferSizeInBytes; + + setupVolume(audioTrack, mVolume, mPan); + return audioTrack; + } + + private static int getBytesPerFrame(int audioFormat) { + if (audioFormat == AudioFormat.ENCODING_PCM_8BIT) { + return 1; + } else if (audioFormat == AudioFormat.ENCODING_PCM_16BIT) { + return 2; + } + + return -1; + } + + + private void blockUntilDone(AudioTrack audioTrack) { + if (mBytesWritten <= 0) { + return; + } + + if (mIsShortUtterance) { + // In this case we would have called AudioTrack#stop() to flush + // buffers to the mixer. This makes the playback head position + // unobservable and notification markers do not work reliably. We + // have no option but to wait until we think the track would finish + // playing and release it after. + // + // This isn't as bad as it looks because (a) We won't end up waiting + // for much longer than we should because even at 4khz mono, a short + // utterance weighs in at about 2 seconds, and (b) such short utterances + // are expected to be relatively infrequent and in a stream of utterances + // this shows up as a slightly longer pause. + blockUntilEstimatedCompletion(); + } else { + blockUntilCompletion(audioTrack); + } + } + + private void blockUntilEstimatedCompletion() { + final int lengthInFrames = mBytesWritten / mBytesPerFrame; + final long estimatedTimeMs = (lengthInFrames * 1000 / mSampleRateInHz); + + if (DBG) Log.d(TAG, "About to sleep for: " + estimatedTimeMs + "ms for a short utterance"); + + try { + Thread.sleep(estimatedTimeMs); + } catch (InterruptedException ie) { + // Do nothing. + } + } + + private void blockUntilCompletion(AudioTrack audioTrack) { + final int lengthInFrames = mBytesWritten / mBytesPerFrame; + + int previousPosition = -1; + int currentPosition = 0; + long blockedTimeMs = 0; + + while ((currentPosition = audioTrack.getPlaybackHeadPosition()) < lengthInFrames && + audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING && !mStopped) { + + final long estimatedTimeMs = ((lengthInFrames - currentPosition) * 1000) / + audioTrack.getSampleRate(); + final long sleepTimeMs = clip(estimatedTimeMs, MIN_SLEEP_TIME_MS, MAX_SLEEP_TIME_MS); + + // Check if the audio track has made progress since the last loop + // iteration. We should then add in the amount of time that was + // spent sleeping in the last iteration. + if (currentPosition == previousPosition) { + // This works only because the sleep time that would have been calculated + // would be the same in the previous iteration too. + blockedTimeMs += sleepTimeMs; + // If we've taken too long to make progress, bail. + if (blockedTimeMs > MAX_PROGRESS_WAIT_MS) { + Log.w(TAG, "Waited unsuccessfully for " + MAX_PROGRESS_WAIT_MS + "ms " + + "for AudioTrack to make progress, Aborting"); + break; + } + } else { + blockedTimeMs = 0; + } + previousPosition = currentPosition; + + if (DBG) { + Log.d(TAG, "About to sleep for : " + sleepTimeMs + " ms," + + " Playback position : " + currentPosition + ", Length in frames : " + + lengthInFrames); + } + try { + Thread.sleep(sleepTimeMs); + } catch (InterruptedException ie) { + break; + } + } + } + + private static void setupVolume(AudioTrack audioTrack, float volume, float pan) { + final float vol = clip(volume, 0.0f, 1.0f); + final float panning = clip(pan, -1.0f, 1.0f); + + float volLeft = vol; + float volRight = vol; + if (panning > 0.0f) { + volLeft *= (1.0f - panning); + } else if (panning < 0.0f) { + volRight *= (1.0f + panning); + } + if (DBG) Log.d(TAG, "volLeft=" + volLeft + ",volRight=" + volRight); + if (audioTrack.setStereoVolume(volLeft, volRight) != AudioTrack.SUCCESS) { + Log.e(TAG, "Failed to set volume"); + } + } + + private static final long clip(long value, long min, long max) { + if (value < min) { + return min; + } + + if (value > max) { + return max; + } + + return value; + } + + private static float clip(float value, float min, float max) { + return value > max ? max : (value < min ? min : value); + } + +} diff --git a/core/java/android/speech/tts/EventLogTags.logtags b/core/java/android/speech/tts/EventLogTags.logtags index 1a9f5fe..f8654ad 100644 --- a/core/java/android/speech/tts/EventLogTags.logtags +++ b/core/java/android/speech/tts/EventLogTags.logtags @@ -2,5 +2,5 @@ option java_package android.speech.tts; -76001 tts_speak_success (engine|3),(caller|3),(length|1),(locale|3),(rate|1),(pitch|1),(engine_latency|2|3),(engine_total|2|3),(audio_latency|2|3) -76002 tts_speak_failure (engine|3),(caller|3),(length|1),(locale|3),(rate|1),(pitch|1) +76001 tts_speak_success (engine|3),(caller_uid|1),(caller_pid|1),(length|1),(locale|3),(rate|1),(pitch|1),(engine_latency|2|3),(engine_total|2|3),(audio_latency|2|3) +76002 tts_speak_failure (engine|3),(caller_uid|1),(caller_pid|1),(length|1),(locale|3),(rate|1),(pitch|1) diff --git a/core/java/android/speech/tts/EventLogger.java b/core/java/android/speech/tts/EventLogger.java index 63b954b..82ed4dd 100644 --- a/core/java/android/speech/tts/EventLogger.java +++ b/core/java/android/speech/tts/EventLogger.java @@ -17,6 +17,7 @@ package android.speech.tts; import android.os.SystemClock; import android.text.TextUtils; +import android.util.Log; /** * Writes data about a given speech synthesis request to the event logs. @@ -24,14 +25,15 @@ import android.text.TextUtils; * speech rate / pitch and the latency and overall time taken. * * Note that {@link EventLogger#onStopped()} and {@link EventLogger#onError()} - * might be called from any thread, but on {@link EventLogger#onPlaybackStart()} and + * might be called from any thread, but on {@link EventLogger#onAudioDataWritten()} and * {@link EventLogger#onComplete()} must be called from a single thread * (usually the audio playback thread} */ class EventLogger { private final SynthesisRequest mRequest; - private final String mCallingApp; private final String mServiceApp; + private final int mCallerUid; + private final int mCallerPid; private final long mReceivedTime; private long mPlaybackStartTime = -1; private volatile long mRequestProcessingStartTime = -1; @@ -42,10 +44,10 @@ class EventLogger { private volatile boolean mStopped = false; private boolean mLogWritten = false; - EventLogger(SynthesisRequest request, String callingApp, - String serviceApp) { + EventLogger(SynthesisRequest request, int callerUid, int callerPid, String serviceApp) { mRequest = request; - mCallingApp = callingApp; + mCallerUid = callerUid; + mCallerPid = callerPid; mServiceApp = serviceApp; mReceivedTime = SystemClock.elapsedRealtime(); } @@ -80,10 +82,10 @@ class EventLogger { /** * Notifies the logger that audio playback has started for some section * of the synthesis. This is normally some amount of time after the engine - * has synthesized data and varides depending on utterances and + * has synthesized data and varies depending on utterances and * other audio currently in the queue. */ - public void onPlaybackStart() { + public void onAudioDataWritten() { // For now, keep track of only the first chunk of audio // that was played. if (mPlaybackStartTime == -1) { @@ -119,10 +121,10 @@ class EventLogger { } long completionTime = SystemClock.elapsedRealtime(); - // onPlaybackStart() should normally always be called if an + // onAudioDataWritten() should normally always be called if an // error does not occur. if (mError || mPlaybackStartTime == -1 || mEngineCompleteTime == -1) { - EventLogTags.writeTtsSpeakFailure(mServiceApp, mCallingApp, + EventLogTags.writeTtsSpeakFailure(mServiceApp, mCallerUid, mCallerPid, getUtteranceLength(), getLocaleString(), mRequest.getSpeechRate(), mRequest.getPitch()); return; @@ -138,7 +140,8 @@ class EventLogger { final long audioLatency = mPlaybackStartTime - mReceivedTime; final long engineLatency = mEngineStartTime - mRequestProcessingStartTime; final long engineTotal = mEngineCompleteTime - mRequestProcessingStartTime; - EventLogTags.writeTtsSpeakSuccess(mServiceApp, mCallingApp, + + EventLogTags.writeTtsSpeakSuccess(mServiceApp, mCallerUid, mCallerPid, getUtteranceLength(), getLocaleString(), mRequest.getSpeechRate(), mRequest.getPitch(), engineLatency, engineTotal, audioLatency); diff --git a/core/java/android/speech/tts/ITextToSpeechService.aidl b/core/java/android/speech/tts/ITextToSpeechService.aidl index 1a8c1fb..ab63187 100644 --- a/core/java/android/speech/tts/ITextToSpeechService.aidl +++ b/core/java/android/speech/tts/ITextToSpeechService.aidl @@ -30,47 +30,47 @@ interface ITextToSpeechService { /** * Tells the engine to synthesize some speech and play it back. * - * @param callingApp The package name of the calling app. Used to connect requests - * callbacks and to clear requests when the calling app is stopping. + * @param callingInstance a binder representing the identity of the calling + * TextToSpeech object. * @param text The text to synthesize. * @param queueMode Determines what to do to requests already in the queue. * @param param Request parameters. */ - int speak(in String callingApp, in String text, in int queueMode, in Bundle params); + int speak(in IBinder callingInstance, in String text, in int queueMode, in Bundle params); /** * Tells the engine to synthesize some speech and write it to a file. * - * @param callingApp The package name of the calling app. Used to connect requests - * callbacks and to clear requests when the calling app is stopping. + * @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 param Request parameters. */ - int synthesizeToFile(in String callingApp, in String text, + int synthesizeToFile(in IBinder callingInstance, in String text, in String filename, in Bundle params); /** * Plays an existing audio resource. * - * @param callingApp The package name of the calling app. Used to connect requests - * callbacks and to clear requests when the calling app is stopping. + * @param callingInstance a binder representing the identity of the calling + * TextToSpeech object. * @param audioUri URI for the audio resource (a file or android.resource URI) * @param queueMode Determines what to do to requests already in the queue. * @param param Request parameters. */ - int playAudio(in String callingApp, in Uri audioUri, in int queueMode, in Bundle params); + int playAudio(in IBinder callingInstance, in Uri audioUri, in int queueMode, in Bundle params); /** * Plays silence. * - * @param callingApp The package name of the calling app. Used to connect requests - * callbacks and to clear requests when the calling app is stopping. + * @param callingInstance a binder representing the identity of the calling + * TextToSpeech object. * @param duration Number of milliseconds of silence to play. * @param queueMode Determines what to do to requests already in the queue. * @param param Request parameters. */ - int playSilence(in String callingApp, in long duration, in int queueMode, in Bundle params); + int playSilence(in IBinder callingInstance, in long duration, in int queueMode, in Bundle params); /** * Checks whether the service is currently playing some audio. @@ -81,10 +81,10 @@ interface ITextToSpeechService { * Interrupts the current utterance (if from the given app) and removes any utterances * in the queue that are from the given app. * - * @param callingApp Package name of the app whose utterances - * should be interrupted and cleared. + * @param callingInstance a binder representing the identity of the calling + * TextToSpeech object. */ - int stop(in String callingApp); + int stop(in IBinder callingInstance); /** * Returns the language, country and variant currently being used by the TTS engine. @@ -150,6 +150,6 @@ interface ITextToSpeechService { * @param callingApp Package name for the app whose utterance the callback will handle. * @param cb The callback. */ - void setCallback(in String callingApp, ITextToSpeechCallback cb); + void setCallback(in IBinder caller, ITextToSpeechCallback cb); } diff --git a/core/java/android/speech/tts/MessageParams.java b/core/java/android/speech/tts/MessageParams.java deleted file mode 100644 index de9cc07..0000000 --- a/core/java/android/speech/tts/MessageParams.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * 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.speech.tts; - -import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher; - -abstract class MessageParams { - static final int TYPE_SYNTHESIS = 1; - static final int TYPE_AUDIO = 2; - static final int TYPE_SILENCE = 3; - - private final UtteranceProgressDispatcher mDispatcher; - private final String mCallingApp; - - MessageParams(UtteranceProgressDispatcher dispatcher, String callingApp) { - mDispatcher = dispatcher; - mCallingApp = callingApp; - } - - UtteranceProgressDispatcher getDispatcher() { - return mDispatcher; - } - - String getCallingApp() { - return mCallingApp; - } - - @Override - public String toString() { - return "MessageParams[" + hashCode() + "]"; - } - - abstract int getType(); -} diff --git a/core/java/android/speech/tts/PlaybackQueueItem.java b/core/java/android/speech/tts/PlaybackQueueItem.java new file mode 100644 index 0000000..d0957ff --- /dev/null +++ b/core/java/android/speech/tts/PlaybackQueueItem.java @@ -0,0 +1,27 @@ +// Copyright 2011 Google Inc. All Rights Reserved. + +package android.speech.tts; + +import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher; + +abstract class PlaybackQueueItem implements Runnable { + private final UtteranceProgressDispatcher mDispatcher; + private final Object mCallerIdentity; + + PlaybackQueueItem(TextToSpeechService.UtteranceProgressDispatcher dispatcher, + Object callerIdentity) { + mDispatcher = dispatcher; + mCallerIdentity = callerIdentity; + } + + Object getCallerIdentity() { + return mCallerIdentity; + } + + protected UtteranceProgressDispatcher getDispatcher() { + return mDispatcher; + } + + public abstract void run(); + abstract void stop(boolean isError); +} diff --git a/core/java/android/speech/tts/PlaybackSynthesisCallback.java b/core/java/android/speech/tts/PlaybackSynthesisCallback.java index 91a3452..c99f201 100644 --- a/core/java/android/speech/tts/PlaybackSynthesisCallback.java +++ b/core/java/android/speech/tts/PlaybackSynthesisCallback.java @@ -47,34 +47,34 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { private final float mPan; /** - * Guards {@link #mAudioTrackHandler}, {@link #mToken} and {@link #mStopped}. + * Guards {@link #mAudioTrackHandler}, {@link #mItem} and {@link #mStopped}. */ private final Object mStateLock = new Object(); // Handler associated with a thread that plays back audio requests. private final AudioPlaybackHandler mAudioTrackHandler; // A request "token", which will be non null after start() has been called. - private SynthesisMessageParams mToken = null; + private SynthesisPlaybackQueueItem mItem = null; // Whether this request has been stopped. This is useful for keeping // track whether stop() has been called before start(). In all other cases, - // a non-null value of mToken will provide the same information. + // a non-null value of mItem will provide the same information. private boolean mStopped = false; private volatile boolean mDone = false; private final UtteranceProgressDispatcher mDispatcher; - private final String mCallingApp; + private final Object mCallerIdentity; private final EventLogger mLogger; PlaybackSynthesisCallback(int streamType, float volume, float pan, AudioPlaybackHandler audioTrackHandler, UtteranceProgressDispatcher dispatcher, - String callingApp, EventLogger logger) { + Object callerIdentity, EventLogger logger) { mStreamType = streamType; mVolume = volume; mPan = pan; mAudioTrackHandler = audioTrackHandler; mDispatcher = dispatcher; - mCallingApp = callingApp; + mCallerIdentity = callerIdentity; mLogger = logger; } @@ -89,28 +89,23 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { // Note that mLogger.mError might be true too at this point. mLogger.onStopped(); - SynthesisMessageParams token; + SynthesisPlaybackQueueItem item; synchronized (mStateLock) { if (mStopped) { Log.w(TAG, "stop() called twice"); return; } - token = mToken; + item = mItem; mStopped = true; } - if (token != null) { + if (item != null) { // This might result in the synthesis thread being woken up, at which - // point it will write an additional buffer to the token - but we + // point it will write an additional buffer to the item - but we // won't worry about that because the audio playback queue will be cleared // soon after (see SynthHandler#stop(String). - token.setIsError(wasError); - token.clearBuffers(); - if (wasError) { - // Also clean up the audio track if an error occurs. - mAudioTrackHandler.enqueueSynthesisDone(token); - } + item.stop(wasError); } else { // This happens when stop() or error() were called before start() was. @@ -145,7 +140,7 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { + "," + channelCount + ")"); } - int channelConfig = AudioPlaybackHandler.getChannelConfig(channelCount); + int channelConfig = BlockingAudioTrack.getChannelConfig(channelCount); if (channelConfig == 0) { Log.e(TAG, "Unsupported number of channels :" + channelCount); return TextToSpeech.ERROR; @@ -156,12 +151,11 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { if (DBG) Log.d(TAG, "stop() called before start(), returning."); return TextToSpeech.ERROR; } - SynthesisMessageParams params = new SynthesisMessageParams( + SynthesisPlaybackQueueItem item = new SynthesisPlaybackQueueItem( mStreamType, sampleRateInHz, audioFormat, channelCount, mVolume, mPan, - mDispatcher, mCallingApp, mLogger); - mAudioTrackHandler.enqueueSynthesisStart(params); - - mToken = params; + mDispatcher, mCallerIdentity, mLogger); + mAudioTrackHandler.enqueue(item); + mItem = item; } return TextToSpeech.SUCCESS; @@ -179,21 +173,25 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { + length + " bytes)"); } - SynthesisMessageParams token = null; + SynthesisPlaybackQueueItem item = null; synchronized (mStateLock) { - if (mToken == null || mStopped) { + if (mItem == null || mStopped) { return TextToSpeech.ERROR; } - token = mToken; + item = mItem; } // Sigh, another copy. final byte[] bufferCopy = new byte[length]; System.arraycopy(buffer, offset, bufferCopy, 0, length); - // Might block on mToken.this, if there are too many buffers waiting to + + // Might block on mItem.this, if there are too many buffers waiting to // be consumed. - token.addBuffer(bufferCopy); - mAudioTrackHandler.enqueueSynthesisDataAvailable(token); + try { + item.put(bufferCopy); + } catch (InterruptedException ie) { + return TextToSpeech.ERROR; + } mLogger.onEngineDataReceived(); @@ -204,7 +202,7 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { public int done() { if (DBG) Log.d(TAG, "done()"); - SynthesisMessageParams token = null; + SynthesisPlaybackQueueItem item = null; synchronized (mStateLock) { if (mDone) { Log.w(TAG, "Duplicate call to done()"); @@ -213,14 +211,14 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { mDone = true; - if (mToken == null) { + if (mItem == null) { return TextToSpeech.ERROR; } - token = mToken; + item = mItem; } - mAudioTrackHandler.enqueueSynthesisDone(token); + item.done(); mLogger.onEngineComplete(); return TextToSpeech.SUCCESS; diff --git a/core/java/android/speech/tts/SilenceMessageParams.java b/core/java/android/speech/tts/SilencePlaybackQueueItem.java index 9909126..a5e47ae 100644 --- a/core/java/android/speech/tts/SilenceMessageParams.java +++ b/core/java/android/speech/tts/SilencePlaybackQueueItem.java @@ -17,28 +17,29 @@ package android.speech.tts; import android.os.ConditionVariable; import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher; +import android.util.Log; -class SilenceMessageParams extends MessageParams { +class SilencePlaybackQueueItem extends PlaybackQueueItem { private final ConditionVariable mCondVar = new ConditionVariable(); private final long mSilenceDurationMs; - SilenceMessageParams(UtteranceProgressDispatcher dispatcher, - String callingApp, long silenceDurationMs) { - super(dispatcher, callingApp); + SilencePlaybackQueueItem(UtteranceProgressDispatcher dispatcher, + Object callerIdentity, long silenceDurationMs) { + super(dispatcher, callerIdentity); mSilenceDurationMs = silenceDurationMs; } - long getSilenceDurationMs() { - return mSilenceDurationMs; - } - @Override - int getType() { - return TYPE_SILENCE; + public void run() { + getDispatcher().dispatchOnStart(); + if (mSilenceDurationMs > 0) { + mCondVar.block(mSilenceDurationMs); + } + getDispatcher().dispatchOnDone(); } - ConditionVariable getConditionVariable() { - return mCondVar; + @Override + void stop(boolean isError) { + mCondVar.open(); } - } diff --git a/core/java/android/speech/tts/SynthesisMessageParams.java b/core/java/android/speech/tts/SynthesisMessageParams.java deleted file mode 100644 index ed66420..0000000 --- a/core/java/android/speech/tts/SynthesisMessageParams.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * 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.speech.tts; - -import android.media.AudioFormat; -import android.media.AudioTrack; -import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher; - -import java.util.LinkedList; - -/** - * Params required to play back a synthesis request. - */ -final class SynthesisMessageParams extends MessageParams { - private static final long MAX_UNCONSUMED_AUDIO_MS = 500; - - final int mStreamType; - final int mSampleRateInHz; - final int mAudioFormat; - final int mChannelCount; - final float mVolume; - final float mPan; - final EventLogger mLogger; - - final int mBytesPerFrame; - - volatile AudioTrack mAudioTrack; - // Written by the synthesis thread, but read on the audio playback - // thread. - volatile int mBytesWritten; - // A "short utterance" is one that uses less bytes than the audio - // track buffer size (mAudioBufferSize). In this case, we need to call - // AudioTrack#stop() to send pending buffers to the mixer, and slightly - // different logic is required to wait for the track to finish. - // - // Not volatile, accessed only from the audio playback thread. - boolean mIsShortUtterance; - int mAudioBufferSize; - // Always synchronized on "this". - int mUnconsumedBytes; - volatile boolean mIsError; - - private final LinkedList<ListEntry> mDataBufferList = new LinkedList<ListEntry>(); - - SynthesisMessageParams(int streamType, int sampleRate, - int audioFormat, int channelCount, - float volume, float pan, UtteranceProgressDispatcher dispatcher, - String callingApp, EventLogger logger) { - super(dispatcher, callingApp); - - mStreamType = streamType; - mSampleRateInHz = sampleRate; - mAudioFormat = audioFormat; - mChannelCount = channelCount; - mVolume = volume; - mPan = pan; - mLogger = logger; - - mBytesPerFrame = getBytesPerFrame(mAudioFormat) * mChannelCount; - - // initially null. - mAudioTrack = null; - mBytesWritten = 0; - mAudioBufferSize = 0; - mIsError = false; - } - - @Override - int getType() { - return TYPE_SYNTHESIS; - } - - synchronized void addBuffer(byte[] buffer) { - long unconsumedAudioMs = 0; - - while ((unconsumedAudioMs = getUnconsumedAudioLengthMs()) > MAX_UNCONSUMED_AUDIO_MS) { - try { - wait(); - } catch (InterruptedException ie) { - return; - } - } - - mDataBufferList.add(new ListEntry(buffer)); - mUnconsumedBytes += buffer.length; - } - - synchronized void clearBuffers() { - mDataBufferList.clear(); - mUnconsumedBytes = 0; - notifyAll(); - } - - synchronized ListEntry getNextBuffer() { - ListEntry entry = mDataBufferList.poll(); - if (entry != null) { - mUnconsumedBytes -= entry.mBytes.length; - notifyAll(); - } - - return entry; - } - - void setAudioTrack(AudioTrack audioTrack) { - mAudioTrack = audioTrack; - } - - AudioTrack getAudioTrack() { - return mAudioTrack; - } - - void setIsError(boolean isError) { - mIsError = isError; - } - - boolean isError() { - return mIsError; - } - - // Must be called synchronized on this. - private long getUnconsumedAudioLengthMs() { - final int unconsumedFrames = mUnconsumedBytes / mBytesPerFrame; - final long estimatedTimeMs = unconsumedFrames * 1000 / mSampleRateInHz; - - return estimatedTimeMs; - } - - private static int getBytesPerFrame(int audioFormat) { - if (audioFormat == AudioFormat.ENCODING_PCM_8BIT) { - return 1; - } else if (audioFormat == AudioFormat.ENCODING_PCM_16BIT) { - return 2; - } - - return -1; - } - - static final class ListEntry { - final byte[] mBytes; - - ListEntry(byte[] bytes) { - mBytes = bytes; - } - } -} - diff --git a/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java b/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java new file mode 100644 index 0000000..d299d70 --- /dev/null +++ b/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * 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.speech.tts; + +import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher; +import android.util.Log; + +import java.util.LinkedList; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Manages the playback of a list of byte arrays representing audio data + * that are queued by the engine to an audio track. + */ +final class SynthesisPlaybackQueueItem extends PlaybackQueueItem { + private static final String TAG = "TTS.SynthQueueItem"; + private static final boolean DBG = false; + + /** + * Maximum length of audio we leave unconsumed by the audio track. + * Calls to {@link #put(byte[])} will block until we have less than + * this amount of audio left to play back. + */ + private static final long MAX_UNCONSUMED_AUDIO_MS = 500; + + /** + * Guards accesses to mDataBufferList and mUnconsumedBytes. + */ + private final Lock mListLock = new ReentrantLock(); + private final Condition mReadReady = mListLock.newCondition(); + private final Condition mNotFull = mListLock.newCondition(); + + // Guarded by mListLock. + private final LinkedList<ListEntry> mDataBufferList = new LinkedList<ListEntry>(); + // Guarded by mListLock. + private int mUnconsumedBytes; + + /* + * While mStopped and mIsError can be written from any thread, mDone is written + * only from the synthesis thread. All three variables are read from the + * audio playback thread. + */ + private volatile boolean mStopped; + private volatile boolean mDone; + private volatile boolean mIsError; + + private final BlockingAudioTrack mAudioTrack; + private final EventLogger mLogger; + + + SynthesisPlaybackQueueItem(int streamType, int sampleRate, + int audioFormat, int channelCount, + float volume, float pan, UtteranceProgressDispatcher dispatcher, + Object callerIdentity, EventLogger logger) { + super(dispatcher, callerIdentity); + + mUnconsumedBytes = 0; + + mStopped = false; + mDone = false; + mIsError = false; + + mAudioTrack = new BlockingAudioTrack(streamType, sampleRate, audioFormat, + channelCount, volume, pan); + mLogger = logger; + } + + + @Override + public void run() { + final UtteranceProgressDispatcher dispatcher = getDispatcher(); + dispatcher.dispatchOnStart(); + + + mAudioTrack.init(); + + try { + byte[] buffer = null; + + // take() will block until: + // + // (a) there is a buffer available to tread. In which case + // a non null value is returned. + // OR (b) stop() is called in which case it will return null. + // OR (c) done() is called in which case it will return null. + while ((buffer = take()) != null) { + mAudioTrack.write(buffer); + mLogger.onAudioDataWritten(); + } + + } catch (InterruptedException ie) { + if (DBG) Log.d(TAG, "Interrupted waiting for buffers, cleaning up."); + } + + mAudioTrack.waitAndRelease(); + + if (mIsError) { + dispatcher.dispatchOnError(); + } else { + dispatcher.dispatchOnDone(); + } + + mLogger.onWriteData(); + } + + @Override + void stop(boolean isError) { + try { + mListLock.lock(); + + // Update our internal state. + mStopped = true; + mIsError = isError; + + // Wake up the audio playback thread if it was waiting on take(). + // take() will return null since mStopped was true, and will then + // break out of the data write loop. + mReadReady.signal(); + + // Wake up the synthesis thread if it was waiting on put(). Its + // buffers will no longer be copied since mStopped is true. The + // PlaybackSynthesisCallback that this synthesis corresponds to + // would also have been stopped, and so all calls to + // Callback.onDataAvailable( ) will return errors too. + mNotFull.signal(); + } finally { + mListLock.unlock(); + } + + // Stop the underlying audio track. This will stop sending + // data to the mixer and discard any pending buffers that the + // track holds. + mAudioTrack.stop(); + } + + void done() { + try { + mListLock.lock(); + + // Update state. + mDone = true; + + // Unblocks the audio playback thread if it was waiting on take() + // after having consumed all available buffers. It will then return + // null and leave the write loop. + mReadReady.signal(); + + // Just so that engines that try to queue buffers after + // calling done() don't block the synthesis thread forever. Ideally + // this should be called from the same thread as put() is, and hence + // this call should be pointless. + mNotFull.signal(); + } finally { + mListLock.unlock(); + } + } + + + void put(byte[] buffer) throws InterruptedException { + try { + mListLock.lock(); + long unconsumedAudioMs = 0; + + while ((unconsumedAudioMs = mAudioTrack.getAudioLengthMs(mUnconsumedBytes)) > + MAX_UNCONSUMED_AUDIO_MS && !mStopped) { + mNotFull.await(); + } + + // Don't bother queueing the buffer if we've stopped. The playback thread + // would have woken up when stop() is called (if it was blocked) and will + // proceed to leave the write loop since take() will return null when + // stopped. + if (mStopped) { + return; + } + + mDataBufferList.add(new ListEntry(buffer)); + mUnconsumedBytes += buffer.length; + mReadReady.signal(); + } finally { + mListLock.unlock(); + } + } + + private byte[] take() throws InterruptedException { + try { + mListLock.lock(); + + // Block if there are no available buffers, and stop() has not + // been called and done() has not been called. + while (mDataBufferList.size() == 0 && !mStopped && !mDone) { + mReadReady.await(); + } + + // If stopped, return null so that we can exit the playback loop + // as soon as possible. + if (mStopped) { + return null; + } + + // Remove the first entry from the queue. + ListEntry entry = mDataBufferList.poll(); + + // This is the normal playback loop exit case, when done() was + // called. (mDone will be true at this point). + if (entry == null) { + return null; + } + + mUnconsumedBytes -= entry.mBytes.length; + // Unblock the waiting writer. We use signal() and not signalAll() + // because there will only be one thread waiting on this (the + // Synthesis thread). + mNotFull.signal(); + + return entry.mBytes; + } finally { + mListLock.unlock(); + } + } + + static final class ListEntry { + final byte[] mBytes; + + ListEntry(byte[] bytes) { + mBytes = bytes; + } + } +} + diff --git a/core/java/android/speech/tts/TextToSpeech.java b/core/java/android/speech/tts/TextToSpeech.java index a220615..cd065ec 100755 --- a/core/java/android/speech/tts/TextToSpeech.java +++ b/core/java/android/speech/tts/TextToSpeech.java @@ -547,10 +547,6 @@ public class TextToSpeech { initTts(); } - private String getPackageName() { - return mPackageName; - } - private <R> R runActionNoReconnect(Action<R> action, R errorResult, String method) { return runAction(action, errorResult, method, false); } @@ -630,6 +626,10 @@ public class TextToSpeech { } } + private IBinder getCallerIdentity() { + return mServiceConnection.getCallerIdentity(); + } + /** * Releases the resources used by the TextToSpeech engine. * It is good practice for instance to call this method in the onDestroy() method of an Activity @@ -639,8 +639,8 @@ public class TextToSpeech { runActionNoReconnect(new Action<Void>() { @Override public Void run(ITextToSpeechService service) throws RemoteException { - service.setCallback(getPackageName(), null); - service.stop(getPackageName()); + service.setCallback(getCallerIdentity(), null); + service.stop(getCallerIdentity()); mServiceConnection.disconnect(); // Context#unbindService does not result in a call to // ServiceConnection#onServiceDisconnected. As a result, the @@ -800,10 +800,10 @@ public class TextToSpeech { public Integer run(ITextToSpeechService service) throws RemoteException { Uri utteranceUri = mUtterances.get(text); if (utteranceUri != null) { - return service.playAudio(getPackageName(), utteranceUri, queueMode, + return service.playAudio(getCallerIdentity(), utteranceUri, queueMode, getParams(params)); } else { - return service.speak(getPackageName(), text, queueMode, getParams(params)); + return service.speak(getCallerIdentity(), text, queueMode, getParams(params)); } } }, ERROR, "speak"); @@ -836,7 +836,7 @@ public class TextToSpeech { if (earconUri == null) { return ERROR; } - return service.playAudio(getPackageName(), earconUri, queueMode, + return service.playAudio(getCallerIdentity(), earconUri, queueMode, getParams(params)); } }, ERROR, "playEarcon"); @@ -863,7 +863,7 @@ public class TextToSpeech { return runAction(new Action<Integer>() { @Override public Integer run(ITextToSpeechService service) throws RemoteException { - return service.playSilence(getPackageName(), durationInMs, queueMode, + return service.playSilence(getCallerIdentity(), durationInMs, queueMode, getParams(params)); } }, ERROR, "playSilence"); @@ -926,7 +926,7 @@ public class TextToSpeech { return runAction(new Action<Integer>() { @Override public Integer run(ITextToSpeechService service) throws RemoteException { - return service.stop(getPackageName()); + return service.stop(getCallerIdentity()); } }, ERROR, "stop"); } @@ -1091,7 +1091,7 @@ public class TextToSpeech { return runAction(new Action<Integer>() { @Override public Integer run(ITextToSpeechService service) throws RemoteException { - return service.synthesizeToFile(getPackageName(), text, filename, + return service.synthesizeToFile(getCallerIdentity(), text, filename, getParams(params)); } }, ERROR, "synthesizeToFile"); @@ -1275,7 +1275,7 @@ public class TextToSpeech { mServiceConnection = this; mService = ITextToSpeechService.Stub.asInterface(service); try { - mService.setCallback(getPackageName(), mCallback); + mService.setCallback(getCallerIdentity(), mCallback); dispatchOnInit(SUCCESS); } catch (RemoteException re) { Log.e(TAG, "Error connecting to service, setCallback() failed"); @@ -1284,6 +1284,10 @@ public class TextToSpeech { } } + public IBinder getCallerIdentity() { + return mCallback; + } + public void onServiceDisconnected(ComponentName name) { synchronized(mStartLock) { mService = null; diff --git a/core/java/android/speech/tts/TextToSpeechService.java b/core/java/android/speech/tts/TextToSpeechService.java index aee678a..4c1a0af 100644 --- a/core/java/android/speech/tts/TextToSpeechService.java +++ b/core/java/android/speech/tts/TextToSpeechService.java @@ -18,6 +18,7 @@ package android.speech.tts; import android.app.Service; import android.content.Intent; import android.net.Uri; +import android.os.Binder; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; @@ -272,9 +273,9 @@ public abstract class TextToSpeechService extends Service { return old; } - private synchronized SpeechItem maybeRemoveCurrentSpeechItem(String callingApp) { + private synchronized SpeechItem maybeRemoveCurrentSpeechItem(Object callerIdentity) { if (mCurrentSpeechItem != null && - TextUtils.equals(mCurrentSpeechItem.getCallingApp(), callingApp)) { + mCurrentSpeechItem.getCallerIdentity() == callerIdentity) { SpeechItem current = mCurrentSpeechItem; mCurrentSpeechItem = null; return current; @@ -311,7 +312,7 @@ public abstract class TextToSpeechService extends Service { } if (queueMode == TextToSpeech.QUEUE_FLUSH) { - stopForApp(speechItem.getCallingApp()); + stopForApp(speechItem.getCallerIdentity()); } else if (queueMode == TextToSpeech.QUEUE_DESTROY) { stopAll(); } @@ -328,7 +329,7 @@ public abstract class TextToSpeechService extends Service { // stopForApp(String). // // Note that this string is interned, so the == comparison works. - msg.obj = speechItem.getCallingApp(); + msg.obj = speechItem.getCallerIdentity(); if (sendMessage(msg)) { return TextToSpeech.SUCCESS; } else { @@ -344,12 +345,12 @@ public abstract class TextToSpeechService extends Service { * * Called on a service binder thread. */ - public int stopForApp(String callingApp) { - if (TextUtils.isEmpty(callingApp)) { + public int stopForApp(Object callerIdentity) { + if (callerIdentity == null) { return TextToSpeech.ERROR; } - removeCallbacksAndMessages(callingApp); + removeCallbacksAndMessages(callerIdentity); // This stops writing data to the file / or publishing // items to the audio playback handler. // @@ -357,13 +358,13 @@ public abstract class TextToSpeechService extends Service { // belongs to the callingApp, else the item will be "orphaned" and // not stopped correctly if a stop request comes along for the item // from the app it belongs to. - SpeechItem current = maybeRemoveCurrentSpeechItem(callingApp); + SpeechItem current = maybeRemoveCurrentSpeechItem(callerIdentity); if (current != null) { current.stop(); } // Remove any enqueued audio too. - mAudioPlaybackHandler.removePlaybackItems(callingApp); + mAudioPlaybackHandler.stopForApp(callerIdentity); return TextToSpeech.SUCCESS; } @@ -377,7 +378,7 @@ public abstract class TextToSpeechService extends Service { // Remove all other items from the queue. removeCallbacksAndMessages(null); // Remove all pending playback as well. - mAudioPlaybackHandler.removeAllItems(); + mAudioPlaybackHandler.stop(); return TextToSpeech.SUCCESS; } @@ -393,18 +394,22 @@ public abstract class TextToSpeechService extends Service { * An item in the synth thread queue. */ private abstract class SpeechItem implements UtteranceProgressDispatcher { - private final String mCallingApp; + private final Object mCallerIdentity; protected final Bundle mParams; + private final int mCallerUid; + private final int mCallerPid; private boolean mStarted = false; private boolean mStopped = false; - public SpeechItem(String callingApp, Bundle params) { - mCallingApp = callingApp; + public SpeechItem(Object caller, int callerUid, int callerPid, Bundle params) { + mCallerIdentity = caller; mParams = params; + mCallerUid = callerUid; + mCallerPid = callerPid; } - public String getCallingApp() { - return mCallingApp; + public Object getCallerIdentity() { + return mCallerIdentity; } /** @@ -451,7 +456,7 @@ public abstract class TextToSpeechService extends Service { public void dispatchOnDone() { final String utteranceId = getUtteranceId(); if (utteranceId != null) { - mCallbacks.dispatchOnDone(getCallingApp(), utteranceId); + mCallbacks.dispatchOnDone(getCallerIdentity(), utteranceId); } } @@ -459,7 +464,7 @@ public abstract class TextToSpeechService extends Service { public void dispatchOnStart() { final String utteranceId = getUtteranceId(); if (utteranceId != null) { - mCallbacks.dispatchOnStart(getCallingApp(), utteranceId); + mCallbacks.dispatchOnStart(getCallerIdentity(), utteranceId); } } @@ -467,10 +472,18 @@ public abstract class TextToSpeechService extends Service { public void dispatchOnError() { final String utteranceId = getUtteranceId(); if (utteranceId != null) { - mCallbacks.dispatchOnError(getCallingApp(), utteranceId); + mCallbacks.dispatchOnError(getCallerIdentity(), utteranceId); } } + public int getCallerUid() { + return mCallerUid; + } + + public int getCallerPid() { + return mCallerPid; + } + protected synchronized boolean isStopped() { return mStopped; } @@ -518,13 +531,15 @@ public abstract class TextToSpeechService extends Service { private AbstractSynthesisCallback mSynthesisCallback; private final EventLogger mEventLogger; - public SynthesisSpeechItem(String callingApp, Bundle params, String text) { - super(callingApp, params); + public SynthesisSpeechItem(Object callerIdentity, int callerUid, int callerPid, + Bundle params, String text) { + super(callerIdentity, callerUid, callerPid, params); mText = text; mSynthesisRequest = new SynthesisRequest(mText, mParams); mDefaultLocale = getSettingsLocale(); setRequestParams(mSynthesisRequest); - mEventLogger = new EventLogger(mSynthesisRequest, getCallingApp(), mPackageName); + mEventLogger = new EventLogger(mSynthesisRequest, callerUid, callerPid, + mPackageName); } public String getText() { @@ -563,7 +578,7 @@ public abstract class TextToSpeechService extends Service { protected AbstractSynthesisCallback createSynthesisCallback() { return new PlaybackSynthesisCallback(getStreamType(), getVolume(), getPan(), - mAudioPlaybackHandler, this, getCallingApp(), mEventLogger); + mAudioPlaybackHandler, this, getCallerIdentity(), mEventLogger); } private void setRequestParams(SynthesisRequest request) { @@ -618,9 +633,10 @@ public abstract class TextToSpeechService extends Service { private class SynthesisToFileSpeechItem extends SynthesisSpeechItem { private final File mFile; - public SynthesisToFileSpeechItem(String callingApp, Bundle params, String text, + public SynthesisToFileSpeechItem(Object callerIdentity, int callerUid, int callerPid, + Bundle params, String text, File file) { - super(callingApp, params, text); + super(callerIdentity, callerUid, callerPid, params, text); mFile = file; } @@ -678,13 +694,12 @@ public abstract class TextToSpeechService extends Service { } private class AudioSpeechItem extends SpeechItem { - - private final BlockingMediaPlayer mPlayer; - private AudioMessageParams mToken; - - public AudioSpeechItem(String callingApp, Bundle params, Uri uri) { - super(callingApp, params); - mPlayer = new BlockingMediaPlayer(TextToSpeechService.this, uri, getStreamType()); + private final AudioPlaybackQueueItem mItem; + public AudioSpeechItem(Object callerIdentity, int callerUid, int callerPid, + Bundle params, Uri uri) { + super(callerIdentity, callerUid, callerPid, params); + mItem = new AudioPlaybackQueueItem(this, getCallerIdentity(), + TextToSpeechService.this, uri, getStreamType()); } @Override @@ -694,8 +709,7 @@ public abstract class TextToSpeechService extends Service { @Override protected int playImpl() { - mToken = new AudioMessageParams(this, getCallingApp(), mPlayer); - mAudioPlaybackHandler.enqueueAudio(mToken); + mAudioPlaybackHandler.enqueue(mItem); return TextToSpeech.SUCCESS; } @@ -707,10 +721,10 @@ public abstract class TextToSpeechService extends Service { private class SilenceSpeechItem extends SpeechItem { private final long mDuration; - private SilenceMessageParams mToken; - public SilenceSpeechItem(String callingApp, Bundle params, long duration) { - super(callingApp, params); + public SilenceSpeechItem(Object callerIdentity, int callerUid, int callerPid, + Bundle params, long duration) { + super(callerIdentity, callerUid, callerPid, params); mDuration = duration; } @@ -721,14 +735,14 @@ public abstract class TextToSpeechService extends Service { @Override protected int playImpl() { - mToken = new SilenceMessageParams(this, getCallingApp(), mDuration); - mAudioPlaybackHandler.enqueueSilence(mToken); + mAudioPlaybackHandler.enqueue(new SilencePlaybackQueueItem( + this, getCallerIdentity(), mDuration)); return TextToSpeech.SUCCESS; } @Override protected void stopImpl() { - // Do nothing. + // Do nothing, handled by AudioPlaybackHandler#stopForApp } } @@ -747,58 +761,67 @@ public abstract class TextToSpeechService extends Service { // NOTE: All calls that are passed in a calling app are interned so that // they can be used as message objects (which are tested for equality using ==). private final ITextToSpeechService.Stub mBinder = new ITextToSpeechService.Stub() { - - public int speak(String callingApp, String text, int queueMode, Bundle params) { - if (!checkNonNull(callingApp, text, params)) { + @Override + public int speak(IBinder caller, String text, int queueMode, Bundle params) { + if (!checkNonNull(caller, text, params)) { return TextToSpeech.ERROR; } - SpeechItem item = new SynthesisSpeechItem(intern(callingApp), params, text); + SpeechItem item = new SynthesisSpeechItem(caller, + Binder.getCallingUid(), Binder.getCallingPid(), params, text); return mSynthHandler.enqueueSpeechItem(queueMode, item); } - public int synthesizeToFile(String callingApp, String text, String filename, + @Override + public int synthesizeToFile(IBinder caller, String text, String filename, Bundle params) { - if (!checkNonNull(callingApp, text, filename, params)) { + if (!checkNonNull(caller, text, filename, params)) { return TextToSpeech.ERROR; } File file = new File(filename); - SpeechItem item = new SynthesisToFileSpeechItem(intern(callingApp), - params, text, file); + SpeechItem item = new SynthesisToFileSpeechItem(caller, Binder.getCallingUid(), + Binder.getCallingPid(), params, text, file); return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item); } - public int playAudio(String callingApp, Uri audioUri, int queueMode, Bundle params) { - if (!checkNonNull(callingApp, audioUri, params)) { + @Override + public int playAudio(IBinder caller, Uri audioUri, int queueMode, Bundle params) { + if (!checkNonNull(caller, audioUri, params)) { return TextToSpeech.ERROR; } - SpeechItem item = new AudioSpeechItem(intern(callingApp), params, audioUri); + SpeechItem item = new AudioSpeechItem(caller, + Binder.getCallingUid(), Binder.getCallingPid(), params, audioUri); return mSynthHandler.enqueueSpeechItem(queueMode, item); } - public int playSilence(String callingApp, long duration, int queueMode, Bundle params) { - if (!checkNonNull(callingApp, params)) { + @Override + public int playSilence(IBinder caller, long duration, int queueMode, Bundle params) { + if (!checkNonNull(caller, params)) { return TextToSpeech.ERROR; } - SpeechItem item = new SilenceSpeechItem(intern(callingApp), params, duration); + SpeechItem item = new SilenceSpeechItem(caller, + Binder.getCallingUid(), Binder.getCallingPid(), params, duration); return mSynthHandler.enqueueSpeechItem(queueMode, item); } + @Override public boolean isSpeaking() { return mSynthHandler.isSpeaking() || mAudioPlaybackHandler.isSpeaking(); } - public int stop(String callingApp) { - if (!checkNonNull(callingApp)) { + @Override + public int stop(IBinder caller) { + if (!checkNonNull(caller)) { return TextToSpeech.ERROR; } - return mSynthHandler.stopForApp(intern(callingApp)); + return mSynthHandler.stopForApp(caller); } + @Override public String[] getLanguage() { return onGetLanguage(); } @@ -807,6 +830,7 @@ public abstract class TextToSpeechService extends Service { * If defaults are enforced, then no language is "available" except * perhaps the default language selected by the user. */ + @Override public int isLanguageAvailable(String lang, String country, String variant) { if (!checkNonNull(lang)) { return TextToSpeech.ERROR; @@ -815,6 +839,7 @@ public abstract class TextToSpeechService extends Service { return onIsLanguageAvailable(lang, country, variant); } + @Override public String[] getFeaturesForLanguage(String lang, String country, String variant) { Set<String> features = onGetFeaturesForLanguage(lang, country, variant); String[] featuresArray = null; @@ -831,6 +856,7 @@ public abstract class TextToSpeechService extends Service { * There is no point loading a non default language if defaults * are enforced. */ + @Override public int loadLanguage(String lang, String country, String variant) { if (!checkNonNull(lang)) { return TextToSpeech.ERROR; @@ -839,13 +865,14 @@ public abstract class TextToSpeechService extends Service { return onLoadLanguage(lang, country, variant); } - public void setCallback(String packageName, ITextToSpeechCallback cb) { + @Override + public void setCallback(IBinder caller, ITextToSpeechCallback cb) { // Note that passing in a null callback is a valid use case. - if (!checkNonNull(packageName)) { + if (!checkNonNull(caller)) { return; } - mCallbacks.setCallback(packageName, cb); + mCallbacks.setCallback(caller, cb); } private String intern(String in) { @@ -862,18 +889,17 @@ public abstract class TextToSpeechService extends Service { }; private class CallbackMap extends RemoteCallbackList<ITextToSpeechCallback> { + private final HashMap<IBinder, ITextToSpeechCallback> mCallerToCallback + = new HashMap<IBinder, ITextToSpeechCallback>(); - private final HashMap<String, ITextToSpeechCallback> mAppToCallback - = new HashMap<String, ITextToSpeechCallback>(); - - public void setCallback(String packageName, ITextToSpeechCallback cb) { - synchronized (mAppToCallback) { + public void setCallback(IBinder caller, ITextToSpeechCallback cb) { + synchronized (mCallerToCallback) { ITextToSpeechCallback old; if (cb != null) { - register(cb, packageName); - old = mAppToCallback.put(packageName, cb); + register(cb, caller); + old = mCallerToCallback.put(caller, cb); } else { - old = mAppToCallback.remove(packageName); + old = mCallerToCallback.remove(caller); } if (old != null && old != cb) { unregister(old); @@ -881,8 +907,8 @@ public abstract class TextToSpeechService extends Service { } } - public void dispatchOnDone(String packageName, String utteranceId) { - ITextToSpeechCallback cb = getCallbackFor(packageName); + public void dispatchOnDone(Object callerIdentity, String utteranceId) { + ITextToSpeechCallback cb = getCallbackFor(callerIdentity); if (cb == null) return; try { cb.onDone(utteranceId); @@ -891,8 +917,8 @@ public abstract class TextToSpeechService extends Service { } } - public void dispatchOnStart(String packageName, String utteranceId) { - ITextToSpeechCallback cb = getCallbackFor(packageName); + public void dispatchOnStart(Object callerIdentity, String utteranceId) { + ITextToSpeechCallback cb = getCallbackFor(callerIdentity); if (cb == null) return; try { cb.onStart(utteranceId); @@ -902,8 +928,8 @@ public abstract class TextToSpeechService extends Service { } - public void dispatchOnError(String packageName, String utteranceId) { - ITextToSpeechCallback cb = getCallbackFor(packageName); + public void dispatchOnError(Object callerIdentity, String utteranceId) { + ITextToSpeechCallback cb = getCallbackFor(callerIdentity); if (cb == null) return; try { cb.onError(utteranceId); @@ -914,25 +940,26 @@ public abstract class TextToSpeechService extends Service { @Override public void onCallbackDied(ITextToSpeechCallback callback, Object cookie) { - String packageName = (String) cookie; - synchronized (mAppToCallback) { - mAppToCallback.remove(packageName); + IBinder caller = (IBinder) cookie; + synchronized (mCallerToCallback) { + mCallerToCallback.remove(caller); } - mSynthHandler.stopForApp(packageName); + mSynthHandler.stopForApp(caller); } @Override public void kill() { - synchronized (mAppToCallback) { - mAppToCallback.clear(); + synchronized (mCallerToCallback) { + mCallerToCallback.clear(); super.kill(); } } - private ITextToSpeechCallback getCallbackFor(String packageName) { + private ITextToSpeechCallback getCallbackFor(Object caller) { ITextToSpeechCallback cb; - synchronized (mAppToCallback) { - cb = mAppToCallback.get(packageName); + IBinder asBinder = (IBinder) caller; + synchronized (mCallerToCallback) { + cb = mCallerToCallback.get(asBinder); } return cb; diff --git a/core/java/android/text/format/DateUtils.java b/core/java/android/text/format/DateUtils.java index 7f8af7a..da10311 100644 --- a/core/java/android/text/format/DateUtils.java +++ b/core/java/android/text/format/DateUtils.java @@ -136,12 +136,12 @@ public class DateUtils private static java.text.DateFormat sStatusTimeFormat; private static String sElapsedFormatMMSS; private static String sElapsedFormatHMMSS; - + private static final String FAST_FORMAT_HMMSS = "%1$d:%2$02d:%3$02d"; private static final String FAST_FORMAT_MMSS = "%1$02d:%2$02d"; private static final char TIME_PADDING = '0'; private static final char TIME_SEPARATOR = ':'; - + public static final long SECOND_IN_MILLIS = 1000; public static final long MINUTE_IN_MILLIS = SECOND_IN_MILLIS * 60; @@ -201,7 +201,7 @@ public class DateUtils public static final String YEAR_FORMAT_TWO_DIGITS = "%g"; public static final String WEEKDAY_FORMAT = "%A"; public static final String ABBREV_WEEKDAY_FORMAT = "%a"; - + // This table is used to lookup the resource string id of a format string // used for formatting a start and end date that fall in the same year. // The index is constructed from a bit-wise OR of the boolean values: @@ -227,7 +227,7 @@ public class DateUtils com.android.internal.R.string.numeric_mdy1_time1_mdy2_time2, com.android.internal.R.string.numeric_wday1_mdy1_time1_wday2_mdy2_time2, }; - + // This table is used to lookup the resource string id of a format string // used for formatting a start and end date that fall in the same month. // The index is constructed from a bit-wise OR of the boolean values: @@ -256,7 +256,7 @@ public class DateUtils /** * Request the full spelled-out name. For use with the 'abbrev' parameter of * {@link #getDayOfWeekString} and {@link #getMonthString}. - * + * * @more <p> * e.g. "Sunday" or "January" */ @@ -265,7 +265,7 @@ public class DateUtils /** * Request an abbreviated version of the name. For use with the 'abbrev' * parameter of {@link #getDayOfWeekString} and {@link #getMonthString}. - * + * * @more <p> * e.g. "Sun" or "Jan" */ @@ -347,7 +347,7 @@ public class DateUtils * @return Localized month of the year. */ public static String getMonthString(int month, int abbrev) { - // Note that here we use sMonthsMedium for MEDIUM, SHORT and SHORTER. + // Note that here we use sMonthsMedium for MEDIUM, SHORT and SHORTER. // This is a shortcut to not spam the translators with too many variations // of the same string. If we find that in a language the distinction // is necessary, we can can add more without changing this API. @@ -380,7 +380,7 @@ public class DateUtils * @hide Pending API council approval */ public static String getStandaloneMonthString(int month, int abbrev) { - // Note that here we use sMonthsMedium for MEDIUM, SHORT and SHORTER. + // Note that here we use sMonthsMedium for MEDIUM, SHORT and SHORTER. // This is a shortcut to not spam the translators with too many variations // of the same string. If we find that in a language the distinction // is necessary, we can can add more without changing this API. @@ -434,7 +434,7 @@ public class DateUtils * <p> * Can use {@link #FORMAT_ABBREV_RELATIVE} flag to use abbreviated relative * times, like "42 mins ago". - * + * * @param time the time to describe, in milliseconds * @param now the current time in milliseconds * @param minResolution the minimum timespan to report. For example, a time @@ -450,7 +450,7 @@ public class DateUtils int flags) { Resources r = Resources.getSystem(); boolean abbrevRelative = (flags & (FORMAT_ABBREV_RELATIVE | FORMAT_ABBREV_ALL)) != 0; - + boolean past = (now >= time); long duration = Math.abs(now - time); @@ -525,7 +525,7 @@ public class DateUtils String format = r.getQuantityString(resId, (int) count); return String.format(format, count); } - + /** * Returns the number of days passed between two dates. * @@ -555,7 +555,7 @@ public class DateUtils * <li>Dec 12, 4:12 AM</li> * <li>11/14/2007, 8:20 AM</li> * </ul> - * + * * @param time some time in the past. * @param minResolution the minimum elapsed time (in milliseconds) to report * when showing relative times. For example, a time 3 seconds in @@ -584,7 +584,7 @@ public class DateUtils } CharSequence timeClause = formatDateRange(c, time, time, FORMAT_SHOW_TIME); - + String result; if (duration < transitionResolution) { CharSequence relativeClause = getRelativeTimeSpanString(time, now, minResolution, flags); @@ -601,7 +601,7 @@ public class DateUtils * Returns a string describing a day relative to the current day. For example if the day is * today this function returns "Today", if the day was a week ago it returns "7 days ago", and * if the day is in 2 weeks it returns "in 14 days". - * + * * @param r the resources to get the strings from * @param day the relative day to describe in UTC milliseconds * @param today the current time in UTC milliseconds @@ -618,7 +618,7 @@ public class DateUtils int days = Math.abs(currentDay - startDay); boolean past = (today > day); - + if (days == 1) { if (past) { return r.getString(com.android.internal.R.string.yesterday); @@ -635,7 +635,7 @@ public class DateUtils } else { resId = com.android.internal.R.plurals.in_num_days; } - + String format = r.getQuantityString(resId, days); return String.format(format, days); } @@ -677,11 +677,11 @@ public class DateUtils public static String formatElapsedTime(long elapsedSeconds) { return formatElapsedTime(null, elapsedSeconds); } - + /** * Formats an elapsed time in the form "MM:SS" or "H:MM:SS" * for display on the call-in-progress screen. - * + * * @param recycle {@link StringBuilder} to recycle, if possible * @param elapsedSeconds the elapsed time in seconds. */ @@ -724,7 +724,7 @@ public class DateUtils } sb.append(hours); sb.append(TIME_SEPARATOR); - if (minutes < 10) { + if (minutes < 10) { sb.append(TIME_PADDING); } else { sb.append(toDigitChar(minutes / 10)); @@ -755,7 +755,7 @@ public class DateUtils } else { sb.setLength(0); } - if (minutes < 10) { + if (minutes < 10) { sb.append(TIME_PADDING); } else { sb.append(toDigitChar(minutes / 10)); @@ -777,11 +777,11 @@ public class DateUtils private static char toDigitChar(long digit) { return (char) (digit + '0'); } - + /** * Format a date / time such that if the then is on the same day as now, it shows * just the time and if it's a different day, it shows just the date. - * + * * <p>The parameters dateFormat and timeFormat should each be one of * {@link java.text.DateFormat#DEFAULT}, * {@link java.text.DateFormat#FULL}, @@ -833,14 +833,14 @@ public class DateUtils public static boolean isToday(long when) { Time time = new Time(); time.set(when); - + int thenYear = time.year; int thenMonth = time.month; int thenMonthDay = time.monthDay; time.set(System.currentTimeMillis()); return (thenYear == time.year) - && (thenMonth == time.month) + && (thenMonth == time.month) && (thenMonthDay == time.monthDay); } @@ -914,7 +914,7 @@ public class DateUtils public static String writeDateTime(Calendar cal, StringBuilder sb) { int n; - + n = cal.get(Calendar.YEAR); sb.setCharAt(3, (char)('0'+n%10)); n /= 10; @@ -1015,7 +1015,7 @@ public class DateUtils /** * Formats a date or a time range according to the local conventions. - * + * * <p> * Example output strings (date formats in these examples are shown using * the US date format convention but that may change depending on the @@ -1036,10 +1036,10 @@ public class DateUtils * <li>Oct 9, 8:00am - Oct 10, 5:00pm</li> * <li>12/31/2007 - 01/01/2008</li> * </ul> - * + * * <p> * The flags argument is a bitmask of options from the following list: - * + * * <ul> * <li>FORMAT_SHOW_TIME</li> * <li>FORMAT_SHOW_WEEKDAY</li> @@ -1061,15 +1061,15 @@ public class DateUtils * <li>FORMAT_ABBREV_ALL</li> * <li>FORMAT_NUMERIC_DATE</li> * </ul> - * + * * <p> * If FORMAT_SHOW_TIME is set, the time is shown as part of the date range. * If the start and end time are the same, then just the start time is * shown. - * + * * <p> * If FORMAT_SHOW_WEEKDAY is set, then the weekday is shown. - * + * * <p> * If FORMAT_SHOW_YEAR is set, then the year is always shown. * If FORMAT_NO_YEAR is set, then the year is not shown. @@ -1082,80 +1082,91 @@ public class DateUtils * Normally the date is shown unless the start and end day are the same. * If FORMAT_SHOW_DATE is set, then the date is always shown, even for * same day ranges. - * + * * <p> * If FORMAT_NO_MONTH_DAY is set, then if the date is shown, just the * month name will be shown, not the day of the month. For example, * "January, 2008" instead of "January 6 - 12, 2008". - * + * * <p> * If FORMAT_CAP_AMPM is set and 12-hour time is used, then the "AM" * and "PM" are capitalized. You should not use this flag * because in some locales these terms cannot be capitalized, and in * many others it doesn't make sense to do so even though it is possible. - * + * * <p> * If FORMAT_NO_NOON is set and 12-hour time is used, then "12pm" is * shown instead of "noon". - * + * * <p> * If FORMAT_CAP_NOON is set and 12-hour time is used, then "Noon" is * shown instead of "noon". You should probably not use this flag * because in many locales it will not make sense to capitalize * the term. - * + * * <p> * If FORMAT_NO_MIDNIGHT is set and 12-hour time is used, then "12am" is * shown instead of "midnight". - * + * * <p> * If FORMAT_CAP_MIDNIGHT is set and 12-hour time is used, then "Midnight" * is shown instead of "midnight". You should probably not use this * flag because in many locales it will not make sense to capitalize * the term. - * + * * <p> * If FORMAT_12HOUR is set and the time is shown, then the time is * shown in the 12-hour time format. You should not normally set this. * Instead, let the time format be chosen automatically according to the * system settings. If both FORMAT_12HOUR and FORMAT_24HOUR are set, then * FORMAT_24HOUR takes precedence. - * + * * <p> * If FORMAT_24HOUR is set and the time is shown, then the time is * shown in the 24-hour time format. You should not normally set this. * Instead, let the time format be chosen automatically according to the * system settings. If both FORMAT_12HOUR and FORMAT_24HOUR are set, then * FORMAT_24HOUR takes precedence. - * + * * <p> * If FORMAT_UTC is set, then the UTC time zone is used for the start * and end milliseconds unless a time zone is specified. If a time zone * is specified it will be used regardless of the FORMAT_UTC flag. - * + * * <p> * If FORMAT_ABBREV_TIME is set and 12-hour time format is used, then the * start and end times (if shown) are abbreviated by not showing the minutes * if they are zero. For example, instead of "3:00pm" the time would be * abbreviated to "3pm". - * + * * <p> * If FORMAT_ABBREV_WEEKDAY is set, then the weekday (if shown) is * abbreviated to a 3-letter string. - * + * * <p> * If FORMAT_ABBREV_MONTH is set, then the month (if shown) is abbreviated * to a 3-letter string. - * + * * <p> * If FORMAT_ABBREV_ALL is set, then the weekday and the month (if shown) * are abbreviated to 3-letter strings. - * + * * <p> * If FORMAT_NUMERIC_DATE is set, then the date is shown in numeric format * instead of using the name of the month. For example, "12/31/2008" * instead of "December 31, 2008". - * + * + * <p> + * If the end date ends at 12:00am at the beginning of a day, it is + * formatted as the end of the previous day in two scenarios: + * <ul> + * <li>For single day events. This results in "8pm - midnight" instead of + * "Nov 10, 8pm - Nov 11, 12am".</li> + * <li>When the time is not displayed. This results in "Nov 10 - 11" for + * an event with a start date of Nov 10 and an end date of Nov 12 at + * 00:00.</li> + * </ul> + * * @param context the context is required only if the time is shown * @param formatter the Formatter used for formatting the date range. * Note: be sure to call setLength(0) on StringBuilder passed to @@ -1165,7 +1176,7 @@ public class DateUtils * @param flags a bit mask of options * @param timeZone the time zone to compute the string in. Use null for local * or if the FORMAT_UTC flag is being used. - * + * * @return the formatter with the formatted date/time range appended to the string buffer. */ public static Formatter formatDateRange(Context context, Formatter formatter, long startMillis, @@ -1215,20 +1226,6 @@ public class DateUtils dayDistance = endJulianDay - startJulianDay; } - // If the end date ends at 12am at the beginning of a day, - // then modify it to make it look like it ends at midnight on - // the previous day. This will allow us to display "8pm - midnight", - // for example, instead of "Nov 10, 8pm - Nov 11, 12am". But we only do - // this if it is midnight of the same day as the start date because - // for multiple-day events, an end time of "midnight on Nov 11" is - // ambiguous and confusing (is that midnight the start of Nov 11, or - // the end of Nov 11?). - // If we are not showing the time then also adjust the end date - // for multiple-day events. This is to allow us to display, for - // example, "Nov 10 -11" for an event with a start date of Nov 10 - // and an end date of Nov 12 at 00:00. - // If the start and end time are the same, then skip this and don't - // adjust the date. if (!isInstant && (endDate.hour | endDate.minute | endDate.second) == 0 && (!showTime || dayDistance <= 1)) { @@ -1592,7 +1589,7 @@ public class DateUtils * <li>Wed, October 31</li> * <li>10/31/2007</li> * </ul> - * + * * @param context the context is required only if the time is shown * @param millis a point in time in UTC milliseconds * @param flags a bit mask of formatting options @@ -1607,13 +1604,13 @@ public class DateUtils * are counted starting at midnight, which means that assuming that the current * time is March 31st, 0:30: * <ul> - * <li>"millis=0:10 today" will be displayed as "0:10"</li> + * <li>"millis=0:10 today" will be displayed as "0:10"</li> * <li>"millis=11:30pm the day before" will be displayed as "Mar 30"</li> * </ul> * If the given millis is in a different year, then the full date is * returned in numeric format (e.g., "10/12/2008"). - * - * @param withPreposition If true, the string returned will include the correct + * + * @param withPreposition If true, the string returned will include the correct * preposition ("at 9:20am", "on 10/12/2008" or "on May 29"). */ public static CharSequence getRelativeTimeSpanString(Context c, long millis, @@ -1661,9 +1658,9 @@ public class DateUtils } return result; } - + /** - * Convenience function to return relative time string without preposition. + * Convenience function to return relative time string without preposition. * @param c context for resources * @param millis time in milliseconds * @return {@link CharSequence} containing relative time. @@ -1672,7 +1669,7 @@ public class DateUtils public static CharSequence getRelativeTimeSpanString(Context c, long millis) { return getRelativeTimeSpanString(c, millis, false /* no preposition */); } - + private static Time sNowTime; private static Time sThenTime; } diff --git a/core/java/android/util/LruCache.java b/core/java/android/util/LruCache.java index 5540000..f1014a7 100644 --- a/core/java/android/util/LruCache.java +++ b/core/java/android/util/LruCache.java @@ -54,6 +54,10 @@ import java.util.Map; * <p>This class does not allow null to be used as a key or value. A return * value of null from {@link #get}, {@link #put} or {@link #remove} is * unambiguous: the key was not in the cache. + * + * <p>This class appeared in Android 3.1 (Honeycomb MR1); it's available as part + * of <a href="http://developer.android.com/sdk/compatibility-library.html">Android's + * Support Package</a> for earlier releases. */ public class LruCache<K, V> { private final LinkedHashMap<K, V> map; diff --git a/core/java/android/util/SparseLongArray.java b/core/java/android/util/SparseLongArray.java new file mode 100644 index 0000000..a08d5cb --- /dev/null +++ b/core/java/android/util/SparseLongArray.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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; + +/** + * 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 { + + private int[] mKeys; + private long[] mValues; + private int mSize; + + /** + * Creates a new SparseLongArray containing no mappings. + */ + public SparseLongArray() { + 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 SparseLongArray(int initialCapacity) { + initialCapacity = ArrayUtils.idealLongArraySize(initialCapacity); + + mKeys = new int[initialCapacity]; + mValues = new long[initialCapacity]; + mSize = 0; + } + + @Override + public SparseLongArray clone() { + SparseLongArray clone = null; + try { + clone = (SparseLongArray) 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(int 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(int key, long valueIfKeyNotFound) { + int i = 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(int key) { + int i = 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(int key, long value) { + int i = 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 int 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(int key) { + return 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(int 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); + + int[] nkeys = new int[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; + } + + private static int binarySearch(int[] a, int start, int len, long key) { + int high = start + len, low = start - 1, guess; + + while (high - low > 1) { + guess = (high + low) / 2; + + if (a[guess] < key) + low = guess; + else + high = guess; + } + + if (high == start + len) + return ~(start + len); + else if (a[high] == key) + return high; + else + return ~high; + } +} diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java new file mode 100644 index 0000000..63de128 --- /dev/null +++ b/core/java/android/view/Choreographer.java @@ -0,0 +1,382 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 com.android.internal.util.ArrayUtils; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.util.Log; + +/** + * Coodinates animations and drawing for UI on a particular thread. + * @hide + */ +public final class Choreographer extends Handler { + private static final String TAG = "Choreographer"; + private static final boolean DEBUG = false; + + // The default amount of time in ms between animation frames. + // When vsync is not enabled, we want to have some idea of how long we should + // wait before posting the next animation message. It is important that the + // default value be less than the true inter-frame delay on all devices to avoid + // situations where we might skip frames by waiting too long (we must compensate + // for jitter and hardware variations). Regardless of this value, the animation + // and display loop is ultimately rate-limited by how fast new graphics buffers can + // be dequeued. + private static final long DEFAULT_FRAME_DELAY = 10; + + // The number of milliseconds between animation frames. + private static long sFrameDelay = DEFAULT_FRAME_DELAY; + + // Thread local storage for the choreographer. + private static final ThreadLocal<Choreographer> sThreadInstance = + new ThreadLocal<Choreographer>() { + @Override + protected Choreographer initialValue() { + Looper looper = Looper.myLooper(); + if (looper == null) { + throw new IllegalStateException("The current thread must have a looper!"); + } + return new Choreographer(looper); + } + }; + + // System property to enable/disable vsync for animations and drawing. + // Enabled by default. + private static final boolean USE_VSYNC = SystemProperties.getBoolean( + "debug.choreographer.vsync", true); + + // System property to enable/disable the use of the vsync / animation timer + // for drawing rather than drawing immediately. + // Temporarily disabled by default because postponing performTraversals() violates + // assumptions about traversals happening in-order relative to other posted messages. + // Bug: 5721047 + private static final boolean USE_ANIMATION_TIMER_FOR_DRAW = SystemProperties.getBoolean( + "debug.choreographer.animdraw", false); + + private static final int MSG_DO_ANIMATION = 0; + private static final int MSG_DO_DRAW = 1; + + private final Looper mLooper; + + private OnAnimateListener[] mOnAnimateListeners; + private OnDrawListener[] mOnDrawListeners; + + private boolean mAnimationScheduled; + private boolean mDrawScheduled; + private FrameDisplayEventReceiver mFrameDisplayEventReceiver; + private long mLastAnimationTime; + private long mLastDrawTime; + + private Choreographer(Looper looper) { + super(looper); + mLooper = looper; + mLastAnimationTime = Long.MIN_VALUE; + mLastDrawTime = Long.MIN_VALUE; + } + + /** + * Gets the choreographer for this thread. + * Must be called on the UI thread. + * + * @return The choreographer for this thread. + * @throws IllegalStateException if the thread does not have a looper. + */ + public static Choreographer getInstance() { + return sThreadInstance.get(); + } + + /** + * The amount of time, in milliseconds, between each frame of the animation. This is a + * requested time that the animation will attempt to honor, but the actual delay between + * frames may be different, depending on system load and capabilities. This is a static + * function because the same delay will be applied to all animations, since they are all + * run off of a single timing loop. + * + * The frame delay may be ignored when the animation system uses an external timing + * source, such as the display refresh rate (vsync), to govern animations. + * + * @return the requested time between frames, in milliseconds + */ + public static long getFrameDelay() { + return sFrameDelay; + } + + /** + * The amount of time, in milliseconds, between each frame of the animation. This is a + * requested time that the animation will attempt to honor, but the actual delay between + * frames may be different, depending on system load and capabilities. This is a static + * function because the same delay will be applied to all animations, since they are all + * run off of a single timing loop. + * + * The frame delay may be ignored when the animation system uses an external timing + * source, such as the display refresh rate (vsync), to govern animations. + * + * @param frameDelay the requested time between frames, in milliseconds + */ + public static void setFrameDelay(long frameDelay) { + sFrameDelay = frameDelay; + } + + /** + * Schedules animation (and drawing) to occur on the next frame synchronization boundary. + * Must be called on the UI thread. + */ + public void scheduleAnimation() { + if (!mAnimationScheduled) { + mAnimationScheduled = true; + if (USE_VSYNC) { + if (DEBUG) { + Log.d(TAG, "Scheduling vsync for animation."); + } + if (mFrameDisplayEventReceiver == null) { + mFrameDisplayEventReceiver = new FrameDisplayEventReceiver(mLooper); + } + mFrameDisplayEventReceiver.scheduleVsync(); + } else { + final long now = SystemClock.uptimeMillis(); + final long nextAnimationTime = Math.max(mLastAnimationTime + sFrameDelay, now); + if (DEBUG) { + Log.d(TAG, "Scheduling animation in " + (nextAnimationTime - now) + " ms."); + } + sendEmptyMessageAtTime(MSG_DO_ANIMATION, nextAnimationTime); + } + } + } + + /** + * Schedules drawing to occur on the next frame synchronization boundary. + * Must be called on the UI thread. + */ + public void scheduleDraw() { + if (!mDrawScheduled) { + mDrawScheduled = true; + if (USE_ANIMATION_TIMER_FOR_DRAW) { + scheduleAnimation(); + } else { + if (DEBUG) { + Log.d(TAG, "Scheduling draw immediately."); + } + sendEmptyMessage(MSG_DO_DRAW); + } + } + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_DO_ANIMATION: + doAnimation(); + break; + case MSG_DO_DRAW: + doDraw(); + break; + } + } + + private void doAnimation() { + if (mAnimationScheduled) { + mAnimationScheduled = false; + + final long start = SystemClock.uptimeMillis(); + if (DEBUG) { + Log.d(TAG, "Performing animation: " + Math.max(0, start - mLastAnimationTime) + + " ms have elapsed since previous animation."); + } + mLastAnimationTime = start; + + final OnAnimateListener[] listeners = mOnAnimateListeners; + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].onAnimate(); + } + } + + if (DEBUG) { + Log.d(TAG, "Animation took " + (SystemClock.uptimeMillis() - start) + " ms."); + } + } + + if (USE_ANIMATION_TIMER_FOR_DRAW) { + doDraw(); + } + } + + private void doDraw() { + if (mDrawScheduled) { + mDrawScheduled = false; + + final long start = SystemClock.uptimeMillis(); + if (DEBUG) { + Log.d(TAG, "Performing draw: " + Math.max(0, start - mLastDrawTime) + + " ms have elapsed since previous draw."); + } + mLastDrawTime = start; + + final OnDrawListener[] listeners = mOnDrawListeners; + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].onDraw(); + } + } + + if (DEBUG) { + Log.d(TAG, "Draw took " + (SystemClock.uptimeMillis() - start) + " ms."); + } + } + } + + /** + * Adds an animation listener. + * Must be called on the UI thread. + * + * @param listener The listener to add. + */ + public void addOnAnimateListener(OnAnimateListener listener) { + if (listener == null) { + throw new IllegalArgumentException("listener must not be null"); + } + + if (DEBUG) { + Log.d(TAG, "Adding onAnimate listener: " + listener); + } + + mOnAnimateListeners = ArrayUtils.appendElement(OnAnimateListener.class, + mOnAnimateListeners, listener); + } + + /** + * Removes an animation listener. + * Must be called on the UI thread. + * + * @param listener The listener to remove. + */ + public void removeOnAnimateListener(OnAnimateListener listener) { + if (listener == null) { + throw new IllegalArgumentException("listener must not be null"); + } + + if (DEBUG) { + Log.d(TAG, "Removing onAnimate listener: " + listener); + } + + mOnAnimateListeners = ArrayUtils.removeElement(OnAnimateListener.class, + mOnAnimateListeners, listener); + stopTimingLoopIfNoListeners(); + } + + /** + * Adds a draw listener. + * Must be called on the UI thread. + * + * @param listener The listener to add. + */ + public void addOnDrawListener(OnDrawListener listener) { + if (listener == null) { + throw new IllegalArgumentException("listener must not be null"); + } + + if (DEBUG) { + Log.d(TAG, "Adding onDraw listener: " + listener); + } + + mOnDrawListeners = ArrayUtils.appendElement(OnDrawListener.class, + mOnDrawListeners, listener); + } + + /** + * Removes a draw listener. + * Must be called on the UI thread. + * + * @param listener The listener to remove. + */ + public void removeOnDrawListener(OnDrawListener listener) { + if (listener == null) { + throw new IllegalArgumentException("listener must not be null"); + } + + if (DEBUG) { + Log.d(TAG, "Removing onDraw listener: " + listener); + } + + mOnDrawListeners = ArrayUtils.removeElement(OnDrawListener.class, + mOnDrawListeners, listener); + stopTimingLoopIfNoListeners(); + } + + private void stopTimingLoopIfNoListeners() { + if (mOnDrawListeners == null && mOnAnimateListeners == null) { + if (DEBUG) { + Log.d(TAG, "Stopping timing loop."); + } + + if (mAnimationScheduled) { + mAnimationScheduled = false; + if (!USE_VSYNC) { + removeMessages(MSG_DO_ANIMATION); + } + } + + if (mDrawScheduled) { + mDrawScheduled = false; + if (!USE_ANIMATION_TIMER_FOR_DRAW) { + removeMessages(MSG_DO_DRAW); + } + } + + if (mFrameDisplayEventReceiver != null) { + mFrameDisplayEventReceiver.dispose(); + mFrameDisplayEventReceiver = null; + } + } + } + + /** + * Listens for animation frame timing events. + */ + public static interface OnAnimateListener { + /** + * Called to animate properties before drawing the frame. + */ + public void onAnimate(); + } + + /** + * Listens for draw frame timing events. + */ + public static interface OnDrawListener { + /** + * Called to draw the frame. + */ + public void onDraw(); + } + + private final class FrameDisplayEventReceiver extends DisplayEventReceiver { + public FrameDisplayEventReceiver(Looper looper) { + super(looper); + } + + @Override + public void onVsync(long timestampNanos, int frame) { + doAnimation(); + } + } +} diff --git a/core/java/android/view/DisplayEventReceiver.java b/core/java/android/view/DisplayEventReceiver.java new file mode 100644 index 0000000..d6711ee --- /dev/null +++ b/core/java/android/view/DisplayEventReceiver.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.os.Looper; +import android.os.MessageQueue; +import android.util.Log; + +/** + * Provides a low-level mechanism for an application to receive display events + * such as vertical sync. + * @hide + */ +public abstract class DisplayEventReceiver { + private static final String TAG = "DisplayEventReceiver"; + + private final CloseGuard mCloseGuard = CloseGuard.get(); + + private int mReceiverPtr; + + // We keep a reference message queue object here so that it is not + // GC'd while the native peer of the receiver is using them. + private MessageQueue mMessageQueue; + + private static native int nativeInit(DisplayEventReceiver receiver, + MessageQueue messageQueue); + private static native void nativeDispose(int receiverPtr); + private static native void nativeScheduleVsync(int receiverPtr); + + /** + * Creates a display event receiver. + * + * @param looper The looper to use when invoking callbacks. + */ + public DisplayEventReceiver(Looper looper) { + if (looper == null) { + throw new IllegalArgumentException("looper must not be null"); + } + + mMessageQueue = looper.getQueue(); + mReceiverPtr = nativeInit(this, mMessageQueue); + + mCloseGuard.open("dispose"); + } + + @Override + protected void finalize() throws Throwable { + try { + dispose(); + } finally { + super.finalize(); + } + } + + /** + * Disposes the receiver. + */ + public void dispose() { + if (mCloseGuard != null) { + mCloseGuard.close(); + } + if (mReceiverPtr != 0) { + nativeDispose(mReceiverPtr); + mReceiverPtr = 0; + } + mMessageQueue = null; + } + + /** + * Called when a vertical sync pulse is received. + * The recipient should render a frame and then call {@link #scheduleVsync} + * to schedule the next vertical sync pulse. + * + * @param timestampNanos The timestamp of the pulse, in the {@link System#nanoTime()} + * timebase. + * @param frame The frame number. Increases by one for each vertical sync interval. + */ + public void onVsync(long timestampNanos, int frame) { + } + + /** + * Schedules a single vertical sync pulse to be delivered when the next + * display frame begins. + */ + public void scheduleVsync() { + if (mReceiverPtr == 0) { + Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event " + + "receiver has already been disposed."); + } else { + nativeScheduleVsync(mReceiverPtr); + } + } + + // Called from native code. + @SuppressWarnings("unused") + private void dispatchVsync(long timestampNanos, int frame) { + onVsync(timestampNanos, frame); + } +} diff --git a/core/java/android/view/GLES20Canvas.java b/core/java/android/view/GLES20Canvas.java index 4ca299f..43a451d 100644 --- a/core/java/android/view/GLES20Canvas.java +++ b/core/java/android/view/GLES20Canvas.java @@ -154,6 +154,7 @@ class GLES20Canvas extends HardwareCanvas { static native void nSetTextureLayerTransform(int layerId, int matrix); static native void nDestroyLayer(int layerId); static native void nDestroyLayerDeferred(int layerId); + static native void nFlushLayer(int layerId); static native boolean nCopyLayer(int layerId, int bitmap); /////////////////////////////////////////////////////////////////////////// @@ -525,7 +526,7 @@ class GLES20Canvas extends HardwareCanvas { @Override public void setMatrix(Matrix matrix) { - nSetMatrix(mRenderer, matrix.native_instance); + nSetMatrix(mRenderer, matrix == null ? 0 : matrix.native_instance); } private static native void nSetMatrix(int renderer, int matrix); diff --git a/core/java/android/view/GLES20Layer.java b/core/java/android/view/GLES20Layer.java index fd3b9e5..4f25792 100644 --- a/core/java/android/view/GLES20Layer.java +++ b/core/java/android/view/GLES20Layer.java @@ -60,6 +60,13 @@ abstract class GLES20Layer extends HardwareLayer { } mLayer = 0; } + + @Override + void flush() { + if (mLayer != 0) { + GLES20Canvas.nFlushLayer(mLayer); + } + } static class Finalizer { private int mLayerId; diff --git a/core/java/android/view/GestureDetector.java b/core/java/android/view/GestureDetector.java index a496a9e..25d08ac 100644 --- a/core/java/android/view/GestureDetector.java +++ b/core/java/android/view/GestureDetector.java @@ -193,10 +193,8 @@ public class GestureDetector { } } - // TODO: ViewConfiguration - private int mBiggerTouchSlopSquare = 20 * 20; - private int mTouchSlopSquare; + private int mDoubleTapTouchSlopSquare; private int mDoubleTapSlopSquare; private int mMinimumFlingVelocity; private int mMaximumFlingVelocity; @@ -391,10 +389,11 @@ public class GestureDetector { mIgnoreMultitouch = ignoreMultitouch; // Fallback to support pre-donuts releases - int touchSlop, doubleTapSlop; + int touchSlop, doubleTapSlop, doubleTapTouchSlop; if (context == null) { //noinspection deprecation touchSlop = ViewConfiguration.getTouchSlop(); + doubleTapTouchSlop = touchSlop; // Hack rather than adding a hiden method for this doubleTapSlop = ViewConfiguration.getDoubleTapSlop(); //noinspection deprecation mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity(); @@ -402,11 +401,13 @@ public class GestureDetector { } else { final ViewConfiguration configuration = ViewConfiguration.get(context); touchSlop = configuration.getScaledTouchSlop(); + doubleTapTouchSlop = configuration.getScaledDoubleTapTouchSlop(); doubleTapSlop = configuration.getScaledDoubleTapSlop(); mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity(); mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity(); } mTouchSlopSquare = touchSlop * touchSlop; + mDoubleTapTouchSlopSquare = doubleTapTouchSlop * doubleTapTouchSlop; mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop; } @@ -545,7 +546,7 @@ public class GestureDetector { mHandler.removeMessages(SHOW_PRESS); mHandler.removeMessages(LONG_PRESS); } - if (distance > mBiggerTouchSlopSquare) { + if (distance > mDoubleTapTouchSlopSquare) { mAlwaysInBiggerTapRegion = false; } } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) { diff --git a/core/java/android/view/HardwareLayer.java b/core/java/android/view/HardwareLayer.java index 28389ab..d5666f3 100644 --- a/core/java/android/view/HardwareLayer.java +++ b/core/java/android/view/HardwareLayer.java @@ -116,6 +116,11 @@ abstract class HardwareLayer { abstract void destroy(); /** + * Flush the render queue associated with this layer. + */ + abstract void flush(); + + /** * This must be invoked before drawing onto this layer. * @param currentCanvas */ diff --git a/core/java/android/view/HardwareRenderer.java b/core/java/android/view/HardwareRenderer.java index ccb6489..3f793bb 100644 --- a/core/java/android/view/HardwareRenderer.java +++ b/core/java/android/view/HardwareRenderer.java @@ -441,6 +441,8 @@ public abstract class HardwareRenderer { } boolean mDirtyRegionsEnabled; + boolean mUpdateDirtyRegions; + final boolean mVsyncDisabled; final int mGlVersion; @@ -675,6 +677,12 @@ public abstract class HardwareRenderer { initCaches(); + enableDirtyRegions(); + + return mEglContext.getGL(); + } + + private void enableDirtyRegions() { // If mDirtyRegions is set, this means we have an EGL configuration // with EGL_SWAP_BEHAVIOR_PRESERVED_BIT set if (sDirtyRegions) { @@ -690,8 +698,6 @@ public abstract class HardwareRenderer { // configuration (see RENDER_DIRTY_REGIONS) mDirtyRegionsEnabled = GLES20Canvas.isBackBufferPreserved(); } - - return mEglContext.getGL(); } abstract void initCaches(); @@ -745,6 +751,9 @@ public abstract class HardwareRenderer { if (!createSurface(holder)) { return; } + + mUpdateDirtyRegions = true; + if (mCanvas != null) { setEnabled(true); } @@ -837,10 +846,37 @@ public abstract class HardwareRenderer { (view.mPrivateFlags & View.INVALIDATED) == View.INVALIDATED; view.mPrivateFlags &= ~View.INVALIDATED; + final long getDisplayListStartTime; + if (ViewDebug.DEBUG_LATENCY) { + getDisplayListStartTime = System.nanoTime(); + } + DisplayList displayList = view.getDisplayList(); + + if (ViewDebug.DEBUG_LATENCY) { + long now = System.nanoTime(); + Log.d(ViewDebug.DEBUG_LATENCY_TAG, "- getDisplayList() took " + + ((now - getDisplayListStartTime) * 0.000001f) + "ms"); + } + if (displayList != null) { - if (canvas.drawDisplayList(displayList, view.getWidth(), - view.getHeight(), mRedrawClip)) { + final long drawDisplayListStartTime; + if (ViewDebug.DEBUG_LATENCY) { + drawDisplayListStartTime = System.nanoTime(); + } + + boolean invalidateNeeded = canvas.drawDisplayList( + displayList, view.getWidth(), + view.getHeight(), mRedrawClip); + + if (ViewDebug.DEBUG_LATENCY) { + long now = System.nanoTime(); + Log.d(ViewDebug.DEBUG_LATENCY_TAG, "- drawDisplayList() took " + + ((now - drawDisplayListStartTime) * 0.000001f) + + "ms, invalidateNeeded=" + invalidateNeeded + "."); + } + + if (invalidateNeeded) { if (mRedrawClip.isEmpty() || view.getParent() == null) { view.invalidate(); } else { @@ -872,7 +908,19 @@ public abstract class HardwareRenderer { attachInfo.mIgnoreDirtyState = false; + final long eglSwapBuffersStartTime; + if (ViewDebug.DEBUG_LATENCY) { + eglSwapBuffersStartTime = System.nanoTime(); + } + sEgl.eglSwapBuffers(sEglDisplay, mEglSurface); + + if (ViewDebug.DEBUG_LATENCY) { + long now = System.nanoTime(); + Log.d(ViewDebug.DEBUG_LATENCY_TAG, "- eglSwapBuffers() took " + + ((now - eglSwapBuffersStartTime) * 0.000001f) + "ms"); + } + checkEglErrors(); return dirty == null; @@ -904,6 +952,10 @@ public abstract class HardwareRenderer { fallback(true); return SURFACE_STATE_ERROR; } else { + if (mUpdateDirtyRegions) { + enableDirtyRegions(); + mUpdateDirtyRegions = false; + } return SURFACE_STATE_UPDATED; } } diff --git a/core/java/android/view/InputEvent.java b/core/java/android/view/InputEvent.java index 01ddcc9..5602436 100755 --- a/core/java/android/view/InputEvent.java +++ b/core/java/android/view/InputEvent.java @@ -19,6 +19,8 @@ package android.view; import android.os.Parcel; import android.os.Parcelable; +import java.util.concurrent.atomic.AtomicInteger; + /** * Common base class for input events. */ @@ -27,8 +29,21 @@ public abstract class InputEvent implements Parcelable { protected static final int PARCEL_TOKEN_MOTION_EVENT = 1; /** @hide */ protected static final int PARCEL_TOKEN_KEY_EVENT = 2; - + + // Next sequence number. + private static final AtomicInteger mNextSeq = new AtomicInteger(); + + /** @hide */ + protected int mSeq; + + /** @hide */ + protected boolean mRecycled; + + private static final boolean TRACK_RECYCLED_LOCATION = false; + private RuntimeException mRecycledLocation; + /*package*/ InputEvent() { + mSeq = mNextSeq.getAndIncrement(); } /** @@ -82,7 +97,44 @@ public abstract class InputEvent implements Parcelable { * objects are fine. See {@link KeyEvent#recycle()} for details. * @hide */ - public abstract void recycle(); + public void recycle() { + if (TRACK_RECYCLED_LOCATION) { + if (mRecycledLocation != null) { + throw new RuntimeException(toString() + " recycled twice!", mRecycledLocation); + } + mRecycledLocation = new RuntimeException("Last recycled here"); + } else { + if (mRecycled) { + throw new RuntimeException(toString() + " recycled twice!"); + } + mRecycled = true; + } + } + + /** + * Conditionally recycled the event if it is appropriate to do so after + * dispatching the event to an application. + * + * If the event is a {@link MotionEvent} then it is recycled. + * + * If the event is a {@link KeyEvent} then it is NOT recycled, because applications + * expect key events to be immutable so once the event has been dispatched to + * the application we can no longer recycle it. + * @hide + */ + public void recycleIfNeededAfterDispatch() { + recycle(); + } + + /** + * Reinitializes the event on reuse (after recycling). + * @hide + */ + protected void prepareForReuse() { + mRecycled = false; + mRecycledLocation = null; + mSeq = mNextSeq.getAndIncrement(); + } /** * Gets a private flag that indicates when the system has detected that this input event @@ -113,6 +165,22 @@ public abstract class InputEvent implements Parcelable { */ public abstract long getEventTimeNano(); + /** + * Gets the unique sequence number of this event. + * Every input event that is created or received by a process has a + * unique sequence number. Moreover, a new sequence number is obtained + * each time an event object is recycled. + * + * Sequence numbers are only guaranteed to be locally unique within a process. + * Sequence numbers are not preserved when events are parceled. + * + * @return The unique sequence number of this event. + * @hide + */ + public int getSequenceNumber() { + return mSeq; + } + public int describeContents() { return 0; } diff --git a/core/java/android/view/InputEventConsistencyVerifier.java b/core/java/android/view/InputEventConsistencyVerifier.java index 9b081b2..fafe416 100644 --- a/core/java/android/view/InputEventConsistencyVerifier.java +++ b/core/java/android/view/InputEventConsistencyVerifier.java @@ -58,7 +58,7 @@ public final class InputEventConsistencyVerifier { // so that the verifier can detect when it has been asked to verify the same event twice. // It does not make sense to examine the contents of the last event since it may have // been recycled. - private InputEvent mLastEvent; + private int mLastEventSeq; private String mLastEventType; private int mLastNestingLevel; @@ -140,7 +140,7 @@ public final class InputEventConsistencyVerifier { * Resets the state of the input event consistency verifier. */ public void reset() { - mLastEvent = null; + mLastEventSeq = -1; mLastNestingLevel = 0; mTrackballDown = false; mTrackballUnhandled = false; @@ -573,17 +573,18 @@ public final class InputEventConsistencyVerifier { private boolean startEvent(InputEvent event, int nestingLevel, String eventType) { // Ignore the event if we already checked it at a higher nesting level. - if (event == mLastEvent && nestingLevel < mLastNestingLevel + final int seq = event.getSequenceNumber(); + if (seq == mLastEventSeq && nestingLevel < mLastNestingLevel && eventType == mLastEventType) { return false; } if (nestingLevel > 0) { - mLastEvent = event; + mLastEventSeq = seq; mLastEventType = eventType; mLastNestingLevel = nestingLevel; } else { - mLastEvent = null; + mLastEventSeq = -1; mLastEventType = null; mLastNestingLevel = 0; } diff --git a/core/java/android/view/InputEventReceiver.java b/core/java/android/view/InputEventReceiver.java new file mode 100644 index 0000000..764d8dc --- /dev/null +++ b/core/java/android/view/InputEventReceiver.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.os.Looper; +import android.os.MessageQueue; +import android.util.Log; + +/** + * Provides a low-level mechanism for an application to receive input events. + * @hide + */ +public abstract class InputEventReceiver { + private static final String TAG = "InputEventReceiver"; + + private final CloseGuard mCloseGuard = CloseGuard.get(); + + private int mReceiverPtr; + + // We keep references to the input channel and message queue objects here so that + // they are not GC'd while the native peer of the receiver is using them. + private InputChannel mInputChannel; + private MessageQueue mMessageQueue; + + // The sequence number of the event that is in progress. + private int mEventSequenceNumberInProgress = -1; + + private static native int nativeInit(InputEventReceiver receiver, + InputChannel inputChannel, MessageQueue messageQueue); + private static native void nativeDispose(int receiverPtr); + private static native void nativeFinishInputEvent(int receiverPtr, boolean handled); + + /** + * Creates an input event receiver bound to the specified input channel. + * + * @param inputChannel The input channel. + * @param looper The looper to use when invoking callbacks. + */ + public InputEventReceiver(InputChannel inputChannel, Looper looper) { + if (inputChannel == null) { + throw new IllegalArgumentException("inputChannel must not be null"); + } + if (looper == null) { + throw new IllegalArgumentException("looper must not be null"); + } + + mInputChannel = inputChannel; + mMessageQueue = looper.getQueue(); + mReceiverPtr = nativeInit(this, inputChannel, mMessageQueue); + + mCloseGuard.open("dispose"); + } + + @Override + protected void finalize() throws Throwable { + try { + dispose(); + } finally { + super.finalize(); + } + } + + /** + * Disposes the receiver. + */ + public void dispose() { + if (mCloseGuard != null) { + mCloseGuard.close(); + } + if (mReceiverPtr != 0) { + nativeDispose(mReceiverPtr); + mReceiverPtr = 0; + } + mInputChannel = null; + mMessageQueue = null; + } + + /** + * Called when an input event is received. + * The recipient should process the input event and then call {@link #finishInputEvent} + * to indicate whether the event was handled. No new input events will be received + * until {@link #finishInputEvent} is called. + * + * @param event The input event that was received. + */ + public void onInputEvent(InputEvent event) { + finishInputEvent(event, false); + } + + /** + * Finishes an input event and indicates whether it was handled. + * + * @param event The input event that was finished. + * @param handled True if the event was handled. + */ + public void finishInputEvent(InputEvent event, boolean handled) { + if (event == null) { + throw new IllegalArgumentException("event must not be null"); + } + if (mReceiverPtr == 0) { + Log.w(TAG, "Attempted to finish an input event but the input event " + + "receiver has already been disposed."); + } else { + if (event.getSequenceNumber() != mEventSequenceNumberInProgress) { + Log.w(TAG, "Attempted to finish an input event that is not in progress."); + } else { + mEventSequenceNumberInProgress = -1; + nativeFinishInputEvent(mReceiverPtr, handled); + } + } + event.recycleIfNeededAfterDispatch(); + } + + // Called from native code. + @SuppressWarnings("unused") + private void dispatchInputEvent(InputEvent event) { + mEventSequenceNumberInProgress = event.getSequenceNumber(); + onInputEvent(event); + } + + public static interface Factory { + public InputEventReceiver createInputEventReceiver( + InputChannel inputChannel, Looper looper); + } +} diff --git a/core/java/android/view/InputHandler.java b/core/java/android/view/InputHandler.java deleted file mode 100644 index 14ce14c..0000000 --- a/core/java/android/view/InputHandler.java +++ /dev/null @@ -1,43 +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.view; - -/** - * Handles input messages that arrive on an input channel. - * @hide - */ -public interface InputHandler { - /** - * Handle a key event. - * It is the responsibility of the callee to ensure that the finished callback is - * eventually invoked when the event processing is finished and the input system - * can send the next event. - * @param event The key event data. - * @param finishedCallback The callback to invoke when event processing is finished. - */ - public void handleKey(KeyEvent event, InputQueue.FinishedCallback finishedCallback); - - /** - * Handle a motion event. - * It is the responsibility of the callee to ensure that the finished callback is - * eventually invoked when the event processing is finished and the input system - * can send the next event. - * @param event The motion event data. - * @param finishedCallback The callback to invoke when event processing is finished. - */ - public void handleMotion(MotionEvent event, InputQueue.FinishedCallback finishedCallback); -} diff --git a/core/java/android/view/InputQueue.java b/core/java/android/view/InputQueue.java index 5735b63..909a3b2 100644 --- a/core/java/android/view/InputQueue.java +++ b/core/java/android/view/InputQueue.java @@ -16,18 +16,11 @@ package android.view; -import android.os.MessageQueue; -import android.util.Slog; - /** * An input queue provides a mechanism for an application to receive incoming * input events. Currently only usable from native code. */ public final class InputQueue { - private static final String TAG = "InputQueue"; - - private static final boolean DEBUG = false; - /** * Interface to receive notification of when an InputQueue is associated * and dissociated with a thread. @@ -48,13 +41,6 @@ public final class InputQueue { final InputChannel mChannel; - private static final Object sLock = new Object(); - - private static native void nativeRegisterInputChannel(InputChannel inputChannel, - InputHandler inputHandler, MessageQueue messageQueue); - private static native void nativeUnregisterInputChannel(InputChannel inputChannel); - private static native void nativeFinished(long finishedToken, boolean handled); - /** @hide */ public InputQueue(InputChannel channel) { mChannel = channel; @@ -64,121 +50,4 @@ public final class InputQueue { public InputChannel getInputChannel() { return mChannel; } - - /** - * Registers an input channel and handler. - * @param inputChannel The input channel to register. - * @param inputHandler The input handler to input events send to the target. - * @param messageQueue The message queue on whose thread the handler should be invoked. - * @hide - */ - public static void registerInputChannel(InputChannel inputChannel, InputHandler inputHandler, - MessageQueue messageQueue) { - if (inputChannel == null) { - throw new IllegalArgumentException("inputChannel must not be null"); - } - if (inputHandler == null) { - throw new IllegalArgumentException("inputHandler must not be null"); - } - if (messageQueue == null) { - throw new IllegalArgumentException("messageQueue must not be null"); - } - - synchronized (sLock) { - if (DEBUG) { - Slog.d(TAG, "Registering input channel '" + inputChannel + "'"); - } - - nativeRegisterInputChannel(inputChannel, inputHandler, messageQueue); - } - } - - /** - * Unregisters an input channel. - * Does nothing if the channel is not currently registered. - * @param inputChannel The input channel to unregister. - * @hide - */ - public static void unregisterInputChannel(InputChannel inputChannel) { - if (inputChannel == null) { - throw new IllegalArgumentException("inputChannel must not be null"); - } - - synchronized (sLock) { - if (DEBUG) { - Slog.d(TAG, "Unregistering input channel '" + inputChannel + "'"); - } - - nativeUnregisterInputChannel(inputChannel); - } - } - - @SuppressWarnings("unused") - private static void dispatchKeyEvent(InputHandler inputHandler, - KeyEvent event, long finishedToken) { - FinishedCallback finishedCallback = FinishedCallback.obtain(finishedToken); - inputHandler.handleKey(event, finishedCallback); - } - - @SuppressWarnings("unused") - private static void dispatchMotionEvent(InputHandler inputHandler, - MotionEvent event, long finishedToken) { - FinishedCallback finishedCallback = FinishedCallback.obtain(finishedToken); - inputHandler.handleMotion(event, finishedCallback); - } - - /** - * A callback that must be invoked to when finished processing an event. - * @hide - */ - public static final class FinishedCallback { - private static final boolean DEBUG_RECYCLING = false; - - private static final int RECYCLE_MAX_COUNT = 4; - - private static FinishedCallback sRecycleHead; - private static int sRecycleCount; - - private FinishedCallback mRecycleNext; - private long mFinishedToken; - - private FinishedCallback() { - } - - public static FinishedCallback obtain(long finishedToken) { - synchronized (sLock) { - FinishedCallback callback = sRecycleHead; - if (callback != null) { - sRecycleHead = callback.mRecycleNext; - sRecycleCount -= 1; - callback.mRecycleNext = null; - } else { - callback = new FinishedCallback(); - } - callback.mFinishedToken = finishedToken; - return callback; - } - } - - public void finished(boolean handled) { - synchronized (sLock) { - if (mFinishedToken == -1) { - throw new IllegalStateException("Event finished callback already invoked."); - } - - nativeFinished(mFinishedToken, handled); - mFinishedToken = -1; - - if (sRecycleCount < RECYCLE_MAX_COUNT) { - mRecycleNext = sRecycleHead; - sRecycleHead = this; - sRecycleCount += 1; - - if (DEBUG_RECYCLING) { - Slog.d(TAG, "Recycled finished callbacks: " + sRecycleCount); - } - } - } - } - } } diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java index f53e42c..104ed6a 100755 --- a/core/java/android/view/KeyEvent.java +++ b/core/java/android/view/KeyEvent.java @@ -1225,7 +1225,6 @@ public class KeyEvent extends InputEvent implements Parcelable { private static KeyEvent gRecyclerTop; private KeyEvent mNext; - private boolean mRecycled; private int mDeviceId; private int mSource; @@ -1535,8 +1534,8 @@ public class KeyEvent extends InputEvent implements Parcelable { gRecyclerTop = ev.mNext; gRecyclerUsed -= 1; } - ev.mRecycled = false; ev.mNext = null; + ev.prepareForReuse(); return ev; } @@ -1597,11 +1596,9 @@ public class KeyEvent extends InputEvent implements Parcelable { * * @hide */ + @Override public final void recycle() { - if (mRecycled) { - throw new RuntimeException(toString() + " recycled twice!"); - } - mRecycled = true; + super.recycle(); mCharacters = null; synchronized (gRecyclerLock) { @@ -1613,6 +1610,12 @@ public class KeyEvent extends InputEvent implements Parcelable { } } + /** @hide */ + @Override + public final void recycleIfNeededAfterDispatch() { + // Do nothing. + } + /** * Create a new key event that is the same as the given one, but whose * event time and repeat count are replaced with the given value. diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java index 8e0ab1a..92e8f4e 100644 --- a/core/java/android/view/MotionEvent.java +++ b/core/java/android/view/MotionEvent.java @@ -167,7 +167,6 @@ import android.util.SparseArray; */ public final class MotionEvent extends InputEvent implements Parcelable { private static final long NS_PER_MS = 1000000; - private static final boolean TRACK_RECYCLED_LOCATION = false; /** * An invalid pointer id. @@ -1315,8 +1314,6 @@ public final class MotionEvent extends InputEvent implements Parcelable { private int mNativePtr; private MotionEvent mNext; - private RuntimeException mRecycledLocation; - private boolean mRecycled; private static native int nativeInitialize(int nativePtr, int deviceId, int source, int action, int flags, int edgeFlags, @@ -1397,9 +1394,8 @@ public final class MotionEvent extends InputEvent implements Parcelable { gRecyclerTop = ev.mNext; gRecyclerUsed -= 1; } - ev.mRecycledLocation = null; - ev.mRecycled = false; ev.mNext = null; + ev.prepareForReuse(); return ev; } @@ -1646,20 +1642,9 @@ public final class MotionEvent extends InputEvent implements Parcelable { * Recycle the MotionEvent, to be re-used by a later caller. After calling * this function you must not ever touch the event again. */ + @Override public final void recycle() { - // Ensure recycle is only called once! - if (TRACK_RECYCLED_LOCATION) { - if (mRecycledLocation != null) { - throw new RuntimeException(toString() + " recycled twice!", mRecycledLocation); - } - mRecycledLocation = new RuntimeException("Last recycled here"); - //Log.w("MotionEvent", "Recycling event " + this, mRecycledLocation); - } else { - if (mRecycled) { - throw new RuntimeException(toString() + " recycled twice!"); - } - mRecycled = true; - } + super.recycle(); synchronized (gRecyclerLock) { if (gRecyclerUsed < MAX_RECYCLED) { diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 54bb056..5c93a42 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -62,6 +62,7 @@ import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityEventSource; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeProvider; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.view.inputmethod.EditorInfo; @@ -1970,6 +1971,21 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal public static final int FIND_VIEWS_WITH_CONTENT_DESCRIPTION = 0x00000002; /** + * Find views that contain {@link AccessibilityNodeProvider}. Such + * a View is a root of virtual view hierarchy and may contain the searched + * text. If this flag is set Views with providers are automatically + * added and it is a responsibility of the client to call the APIs of + * the provider to determine whether the virtual tree rooted at this View + * contains the text, i.e. getting the list of {@link AccessibilityNodeInfo}s + * represeting the virtual views with this text. + * + * @see #findViewsWithText(ArrayList, CharSequence, int) + * + * @hide + */ + public static final int FIND_VIEWS_WITH_ACCESSIBILITY_NODE_PROVIDERS = 0x00000004; + + /** * Controls the over-scroll mode for this view. * See {@link #overScrollBy(int, int, int, int, int, int, int, int, boolean)}, * {@link #OVER_SCROLL_ALWAYS}, {@link #OVER_SCROLL_IF_CONTENT_SCROLLS}, @@ -4108,14 +4124,20 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * Note: The client is responsible for recycling the obtained instance by calling * {@link AccessibilityNodeInfo#recycle()} to minimize object creation. * </p> + * * @return A populated {@link AccessibilityNodeInfo}. * * @see AccessibilityNodeInfo */ public AccessibilityNodeInfo createAccessibilityNodeInfo() { - AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(this); - onInitializeAccessibilityNodeInfo(info); - return info; + AccessibilityNodeProvider provider = getAccessibilityNodeProvider(); + if (provider != null) { + return provider.createAccessibilityNodeInfo(View.NO_ID); + } else { + AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(this); + onInitializeAccessibilityNodeInfo(info); + return info; + } } /** @@ -4220,6 +4242,36 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal } /** + * Gets the provider for managing a virtual view hierarchy rooted at this View + * and reported to {@link android.accessibilityservice.AccessibilityService}s + * that explore the window content. + * <p> + * If this method returns an instance, this instance is responsible for managing + * {@link AccessibilityNodeInfo}s describing the virtual sub-tree rooted at this + * View including the one representing the View itself. Similarly the returned + * instance is responsible for performing accessibility actions on any virtual + * view or the root view itself. + * </p> + * <p> + * If an {@link AccessibilityDelegate} has been specified via calling + * {@link #setAccessibilityDelegate(AccessibilityDelegate)} its + * {@link AccessibilityDelegate#getAccessibilityNodeProvider(View)} + * is responsible for handling this call. + * </p> + * + * @return The provider. + * + * @see AccessibilityNodeProvider + */ + public AccessibilityNodeProvider getAccessibilityNodeProvider() { + if (mAccessibilityDelegate != null) { + return mAccessibilityDelegate.getAccessibilityNodeProvider(this); + } else { + return null; + } + } + + /** * Gets the unique identifier of this view on the screen for accessibility purposes. * If this {@link View} is not attached to any window, {@value #NO_ID} is returned. * @@ -5238,14 +5290,18 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * * @param outViews The output list of matching Views. * @param searched The text to match against. - * + * * @see #FIND_VIEWS_WITH_TEXT * @see #FIND_VIEWS_WITH_CONTENT_DESCRIPTION * @see #setContentDescription(CharSequence) */ public void findViewsWithText(ArrayList<View> outViews, CharSequence searched, int flags) { - if ((flags & FIND_VIEWS_WITH_CONTENT_DESCRIPTION) != 0 && !TextUtils.isEmpty(searched) - && !TextUtils.isEmpty(mContentDescription)) { + if (getAccessibilityNodeProvider() != null) { + if ((flags & FIND_VIEWS_WITH_ACCESSIBILITY_NODE_PROVIDERS) != 0) { + outViews.add(this); + } + } else if ((flags & FIND_VIEWS_WITH_CONTENT_DESCRIPTION) != 0 + && !TextUtils.isEmpty(searched) && !TextUtils.isEmpty(mContentDescription)) { String searchedLowerCase = searched.toString().toLowerCase(); String contentDescriptionLowerCase = mContentDescription.toString().toLowerCase(); if (contentDescriptionLowerCase.contains(searchedLowerCase)) { @@ -10151,6 +10207,13 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal break; } } + + // Make sure the HardwareRenderer.validate() was invoked before calling this method + void flushLayer() { + if (mLayerType == LAYER_TYPE_HARDWARE && mHardwareLayer != null) { + mHardwareLayer.flush(); + } + } /** * <p>Returns a hardware layer that can be used to draw this view again @@ -10163,6 +10226,8 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal !mAttachInfo.mHardwareRenderer.isEnabled()) { return null; } + + if (!mAttachInfo.mHardwareRenderer.validate()) return null; final int width = mRight - mLeft; final int height = mBottom - mTop; @@ -10237,12 +10302,15 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal */ boolean destroyLayer() { if (mHardwareLayer != null) { - mHardwareLayer.destroy(); - mHardwareLayer = null; - - invalidate(true); - invalidateParentCaches(); + AttachInfo info = mAttachInfo; + if (info != null && info.mHardwareRenderer != null && + info.mHardwareRenderer.isEnabled() && info.mHardwareRenderer.validate()) { + mHardwareLayer.destroy(); + mHardwareLayer = null; + invalidate(true); + invalidateParentCaches(); + } return true; } return false; @@ -12152,13 +12220,16 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * @param location an array of two integers in which to hold the coordinates */ public void getLocationInWindow(int[] location) { - // When the view is not attached to a window, this method does not make sense - if (mAttachInfo == null) return; - if (location == null || location.length < 2) { throw new IllegalArgumentException("location must be an array of two integers"); } + if (mAttachInfo == null) { + // When the view is not attached to a window, this method does not make sense + location[0] = location[1] = 0; + return; + } + float[] position = mAttachInfo.mTmpTransformLocation; position[0] = position[1] = 0.0f; @@ -14995,5 +15066,23 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal AccessibilityEvent event) { return host.onRequestSendAccessibilityEventInternal(child, event); } + + /** + * Gets the provider for managing a virtual view hierarchy rooted at this View + * and reported to {@link android.accessibilityservice.AccessibilityService}s + * that explore the window content. + * <p> + * The default implementation behaves as + * {@link View#getAccessibilityNodeProvider() View#getAccessibilityNodeProvider()} for + * the case of no accessibility delegate been set. + * </p> + * + * @return The provider. + * + * @see AccessibilityNodeProvider + */ + public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) { + return null; + } } } diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java index 9bd42ef..b455ad5 100644 --- a/core/java/android/view/ViewConfiguration.java +++ b/core/java/android/view/ViewConfiguration.java @@ -49,7 +49,7 @@ public class ViewConfiguration { /** * Defines the width of the horizontal scrollbar and the height of the vertical scrollbar in - * pixels + * dips */ private static final int SCROLL_BAR_SIZE = 10; @@ -64,7 +64,7 @@ public class ViewConfiguration { private static final int SCROLL_BAR_DEFAULT_DELAY = 300; /** - * Defines the length of the fading edges in pixels + * Defines the length of the fading edges in dips */ private static final int FADING_EDGE_LENGTH = 12; @@ -134,7 +134,7 @@ public class ViewConfiguration { private static final int ZOOM_CONTROLS_TIMEOUT = 3000; /** - * Inset in pixels to look for touchable content when the user touches the edge of the screen + * Inset in dips to look for touchable content when the user touches the edge of the screen */ private static final int EDGE_SLOP = 12; @@ -152,6 +152,12 @@ public class ViewConfiguration { private static final int TOUCH_SLOP = 8; /** + * Distance the first touch can wander before we stop considering this event a double tap + * (in dips) + */ + private static final int DOUBLE_TAP_TOUCH_SLOP = TOUCH_SLOP; + + /** * Distance a touch can wander before we think the user is attempting a paged scroll * (in dips) * @@ -166,28 +172,28 @@ public class ViewConfiguration { private static final int PAGING_TOUCH_SLOP = TOUCH_SLOP * 2; /** - * Distance between the first touch and second touch to still be considered a double tap + * Distance in dips between the first touch and second touch to still be considered a double tap */ private static final int DOUBLE_TAP_SLOP = 100; /** - * Distance a touch needs to be outside of a window's bounds for it to + * Distance in dips a touch needs to be outside of a window's bounds for it to * count as outside for purposes of dismissing the window. */ private static final int WINDOW_TOUCH_SLOP = 16; /** - * Minimum velocity to initiate a fling, as measured in pixels per second + * Minimum velocity to initiate a fling, as measured in dips per second */ private static final int MINIMUM_FLING_VELOCITY = 50; /** - * Maximum velocity to initiate a fling, as measured in pixels per second + * Maximum velocity to initiate a fling, as measured in dips per second */ private static final int MAXIMUM_FLING_VELOCITY = 8000; /** - * Distance between a touch up event denoting the end of a touch exploration + * Distance in dips between a touch up event denoting the end of a touch exploration * gesture and the touch up event of a subsequent tap for the latter tap to be * considered as a tap i.e. to perform a click. */ @@ -214,12 +220,12 @@ public class ViewConfiguration { private static final float SCROLL_FRICTION = 0.015f; /** - * Max distance to overscroll for edge effects + * Max distance in dips to overscroll for edge effects */ private static final int OVERSCROLL_DISTANCE = 0; /** - * Max distance to overfling for edge effects + * Max distance in dips to overfling for edge effects */ private static final int OVERFLING_DISTANCE = 6; @@ -229,6 +235,7 @@ public class ViewConfiguration { private final int mMaximumFlingVelocity; private final int mScrollbarSize; private final int mTouchSlop; + private final int mDoubleTapTouchSlop; private final int mPagingTouchSlop; private final int mDoubleTapSlop; private final int mScaledTouchExplorationTapSlop; @@ -255,6 +262,7 @@ public class ViewConfiguration { mMaximumFlingVelocity = MAXIMUM_FLING_VELOCITY; mScrollbarSize = SCROLL_BAR_SIZE; mTouchSlop = TOUCH_SLOP; + mDoubleTapTouchSlop = DOUBLE_TAP_TOUCH_SLOP; mPagingTouchSlop = PAGING_TOUCH_SLOP; mDoubleTapSlop = DOUBLE_TAP_SLOP; mScaledTouchExplorationTapSlop = TOUCH_EXPLORATION_TAP_SLOP; @@ -318,6 +326,8 @@ public class ViewConfiguration { mTouchSlop = res.getDimensionPixelSize( com.android.internal.R.dimen.config_viewConfigurationTouchSlop); mPagingTouchSlop = mTouchSlop * 2; + + mDoubleTapTouchSlop = mTouchSlop; } /** @@ -342,7 +352,7 @@ public class ViewConfiguration { /** * @return The width of the horizontal scrollbar and the height of the vertical - * scrollbar in pixels + * scrollbar in dips * * @deprecated Use {@link #getScaledScrollBarSize()} instead. */ @@ -374,7 +384,7 @@ public class ViewConfiguration { } /** - * @return the length of the fading edges in pixels + * @return the length of the fading edges in dips * * @deprecated Use {@link #getScaledFadingEdgeLength()} instead. */ @@ -469,7 +479,7 @@ public class ViewConfiguration { } /** - * @return Inset in pixels to look for touchable content when the user touches the edge of the + * @return Inset in dips to look for touchable content when the user touches the edge of the * screen * * @deprecated Use {@link #getScaledEdgeSlop()} instead. @@ -488,7 +498,7 @@ public class ViewConfiguration { } /** - * @return Distance a touch can wander before we think the user is scrolling in pixels + * @return Distance in dips a touch can wander before we think the user is scrolling * * @deprecated Use {@link #getScaledTouchSlop()} instead. */ @@ -498,22 +508,31 @@ public class ViewConfiguration { } /** - * @return Distance a touch can wander before we think the user is scrolling in pixels + * @return Distance in pixels a touch can wander before we think the user is scrolling */ public int getScaledTouchSlop() { return mTouchSlop; } /** - * @return Distance a touch can wander before we think the user is scrolling a full page - * in dips + * @return Distance in pixels the first touch can wander before we do not consider this a + * potential double tap event + * @hide + */ + public int getScaledDoubleTapTouchSlop() { + return mDoubleTapTouchSlop; + } + + /** + * @return Distance in pixels a touch can wander before we think the user is scrolling a full + * page */ public int getScaledPagingTouchSlop() { return mPagingTouchSlop; } /** - * @return Distance between the first touch and second touch to still be + * @return Distance in dips between the first touch and second touch to still be * considered a double tap * @deprecated Use {@link #getScaledDoubleTapSlop()} instead. * @hide The only client of this should be GestureDetector, which needs this @@ -525,7 +544,7 @@ public class ViewConfiguration { } /** - * @return Distance between the first touch and second touch to still be + * @return Distance in pixels between the first touch and second touch to still be * considered a double tap */ public int getScaledDoubleTapSlop() { @@ -533,7 +552,7 @@ public class ViewConfiguration { } /** - * @return Distance between a touch up event denoting the end of a touch exploration + * @return Distance in pixels between a touch up event denoting the end of a touch exploration * gesture and the touch up event of a subsequent tap for the latter tap to be * considered as a tap i.e. to perform a click. * @@ -557,7 +576,7 @@ public class ViewConfiguration { } /** - * @return Distance a touch must be outside the bounds of a window for it + * @return Distance in dips a touch must be outside the bounds of a window for it * to be counted as outside the window for purposes of dismissing that * window. * @@ -569,16 +588,15 @@ public class ViewConfiguration { } /** - * @return Distance a touch must be outside the bounds of a window for it - * to be counted as outside the window for purposes of dismissing that - * window. + * @return Distance in pixels a touch must be outside the bounds of a window for it + * to be counted as outside the window for purposes of dismissing that window. */ public int getScaledWindowTouchSlop() { return mWindowTouchSlop; } /** - * @return Minimum velocity to initiate a fling, as measured in pixels per second. + * @return Minimum velocity to initiate a fling, as measured in dips per second. * * @deprecated Use {@link #getScaledMinimumFlingVelocity()} instead. */ @@ -595,7 +613,7 @@ public class ViewConfiguration { } /** - * @return Maximum velocity to initiate a fling, as measured in pixels per second. + * @return Maximum velocity to initiate a fling, as measured in dips per second. * * @deprecated Use {@link #getScaledMaximumFlingVelocity()} instead. */ @@ -634,14 +652,16 @@ public class ViewConfiguration { } /** - * @return The maximum distance a View should overscroll by when showing edge effects. + * @return The maximum distance a View should overscroll by when showing edge effects (in + * pixels). */ public int getScaledOverscrollDistance() { return mOverscrollDistance; } /** - * @return The maximum distance a View should overfling by when showing edge effects. + * @return The maximum distance a View should overfling by when showing edge effects (in + * pixels). */ public int getScaledOverflingDistance() { return mOverflingDistance; diff --git a/core/java/android/view/ViewDebug.java b/core/java/android/view/ViewDebug.java index 65e72c9..c1db572 100644 --- a/core/java/android/view/ViewDebug.java +++ b/core/java/android/view/ViewDebug.java @@ -127,16 +127,19 @@ public class ViewDebug { * Logs the relative difference between the time an event was created and the time it * was delivered. * - * Logs the time spent waiting for Surface.lockCanvas() or eglSwapBuffers(). - * This is time that the event loop spends blocked and unresponsive. Ideally, drawing - * and animations should be perfectly synchronized with VSYNC so that swap buffers - * is instantaneous. + * Logs the time spent waiting for Surface.lockCanvas(), Surface.unlockCanvasAndPost() + * or eglSwapBuffers(). This is time that the event loop spends blocked and unresponsive. + * Ideally, drawing and animations should be perfectly synchronized with VSYNC so that + * dequeuing and queueing buffers is instantaneous. * - * Logs the time spent in ViewRoot.performTraversals() or ViewRoot.draw(). + * Logs the time spent in ViewRoot.performTraversals() and ViewRoot.performDraw(). * @hide */ public static final boolean DEBUG_LATENCY = false; + /** @hide */ + public static final String DEBUG_LATENCY_TAG = "ViewLatency"; + /** * <p>Enables or disables views consistency check. Even when this property is enabled, * view consistency checks happen only if {@link false} is set diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 600bfe6..1102a47 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -2952,6 +2952,17 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager mDrawLayers = enabled; invalidate(true); + boolean flushLayers = !enabled; + AttachInfo info = mAttachInfo; + if (info != null && info.mHardwareRenderer != null && + info.mHardwareRenderer.isEnabled()) { + if (!info.mHardwareRenderer.validate()) { + flushLayers = false; + } + } else { + flushLayers = false; + } + // We need to invalidate any child with a layer. For instance, // if a child is backed by a hardware layer and we disable layers // the child is marked as not dirty (flags cleared the last time @@ -2962,6 +2973,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager for (int i = 0; i < mChildrenCount; i++) { View child = mChildren[i]; if (child.mLayerType != LAYER_TYPE_NONE) { + if (flushLayers) child.flushLayer(); child.invalidate(true); } } @@ -3943,8 +3955,11 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } while (parent != null); } else { // Check whether the child that requests the invalidate is fully opaque + // Views being animated or transformed are not considered opaque because we may + // be invalidating their old position and need the parent to paint behind them. + Matrix childMatrix = child.getMatrix(); final boolean isOpaque = child.isOpaque() && !drawAnimation && - child.getAnimation() == null; + child.getAnimation() == null && childMatrix.isIdentity(); // Mark the child as dirty, using the appropriate flag // Make sure we do not set both flags at the same time int opaqueFlag = isOpaque ? DIRTY_OPAQUE : DIRTY; @@ -3958,7 +3973,6 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final int[] location = attachInfo.mInvalidateChildLocation; location[CHILD_LEFT_INDEX] = child.mLeft; location[CHILD_TOP_INDEX] = child.mTop; - Matrix childMatrix = child.getMatrix(); if (!childMatrix.isIdentity()) { RectF boundingRect = attachInfo.mTmpTransformRect; boundingRect.set(dirty); diff --git a/core/java/android/view/ViewPropertyAnimator.java b/core/java/android/view/ViewPropertyAnimator.java index 84dc7d8..89a1ef2 100644 --- a/core/java/android/view/ViewPropertyAnimator.java +++ b/core/java/android/view/ViewPropertyAnimator.java @@ -837,6 +837,11 @@ public class ViewPropertyAnimator { */ @Override public void onAnimationUpdate(ValueAnimator animation) { + PropertyBundle propertyBundle = mAnimatorMap.get(animation); + if (propertyBundle == null) { + // Shouldn't happen, but just to play it safe + return; + } // alpha requires slightly different treatment than the other (transform) properties. // The logic in setAlpha() is not simply setting mAlpha, plus the invalidation // logic is dependent on how the view handles an internal call to onSetAlpha(). @@ -845,7 +850,6 @@ public class ViewPropertyAnimator { boolean alphaHandled = false; mView.invalidateParentCaches(); float fraction = animation.getAnimatedFraction(); - PropertyBundle propertyBundle = mAnimatorMap.get(animation); int propertyMask = propertyBundle.mPropertyMask; if ((propertyMask & TRANSFORM_MASK) != 0) { mView.invalidate(false); diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 6c982eb..72966ef 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -65,6 +65,7 @@ import android.view.accessibility.AccessibilityInteractionClient; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeProvider; import android.view.accessibility.IAccessibilityInteractionConnection; import android.view.accessibility.IAccessibilityInteractionConnectionCallback; import android.view.animation.AccelerateDecelerateInterpolator; @@ -95,7 +96,8 @@ import java.util.List; */ @SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"}) public final class ViewRootImpl extends Handler implements ViewParent, - View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks { + View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks, + Choreographer.OnDrawListener { private static final String TAG = "ViewRootImpl"; private static final boolean DBG = false; private static final boolean LOCAL_LOGV = false; @@ -109,7 +111,6 @@ public final class ViewRootImpl extends Handler implements ViewParent, private static final boolean DEBUG_IMF = false || LOCAL_LOGV; private static final boolean DEBUG_CONFIGURATION = false || LOCAL_LOGV; private static final boolean DEBUG_FPS = false; - private static final boolean WATCH_POINTER = false; /** * Set this system property to true to force the view hierarchy to render @@ -153,9 +154,6 @@ public final class ViewRootImpl extends Handler implements ViewParent, final TypedValue mTmpValue = new TypedValue(); final InputMethodCallback mInputMethodCallback; - final SparseArray<Object> mPendingEvents = new SparseArray<Object>(); - int mPendingEventSeq = 0; - final Thread mThread; final WindowLeaked mLocation; @@ -203,13 +201,14 @@ public final class ViewRootImpl extends Handler implements ViewParent, InputQueue.Callback mInputQueueCallback; InputQueue mInputQueue; FallbackEventHandler mFallbackEventHandler; + Choreographer mChoreographer; final Rect mTempRect; // used in the transaction to not thrash the heap. final Rect mVisRect; // used to retrieve visible rect of focused view. boolean mTraversalScheduled; long mLastTraversalFinishedTimeNanos; - long mLastDrawDurationNanos; + long mLastDrawFinishedTimeNanos; boolean mWillDrawSoon; boolean mLayoutRequested; boolean mFirst; @@ -218,7 +217,16 @@ public final class ViewRootImpl extends Handler implements ViewParent, boolean mNewSurfaceNeeded; boolean mHasHadWindowFocus; boolean mLastWasImTarget; - InputEventMessage mPendingInputEvents = null; + + // Pool of queued input events. + private static final int MAX_QUEUED_INPUT_EVENT_POOL_SIZE = 10; + private QueuedInputEvent mQueuedInputEventPool; + private int mQueuedInputEventPoolSize; + + // Input event queue. + QueuedInputEvent mFirstPendingInputEvent; + QueuedInputEvent mCurrentInputEvent; + boolean mProcessInputEventsScheduled; boolean mWindowAttributesChanged = false; int mWindowAttributesChangesFlag = 0; @@ -367,6 +375,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, mFallbackEventHandler = PolicyManager.makeNewFallbackEventHandler(context); mProfileRendering = Boolean.parseBoolean( SystemProperties.get(PROPERTY_PROFILE_RENDERING, "false")); + mChoreographer = Choreographer.getInstance(); } public static void addFirstDrawHandler(Runnable callback) { @@ -418,6 +427,8 @@ public final class ViewRootImpl extends Handler implements ViewParent, public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { synchronized (this) { if (mView == null) { + mChoreographer.addOnDrawListener(this); + mView = view; mFallbackEventHandler.setView(view); mWindowAttributes.copyFrom(attrs); @@ -551,8 +562,8 @@ public final class ViewRootImpl extends Handler implements ViewParent, mInputQueue = new InputQueue(mInputChannel); mInputQueueCallback.onInputQueueCreated(mInputQueue); } else { - InputQueue.registerInputChannel(mInputChannel, mInputHandler, - Looper.myQueue()); + mInputEventReceiver = new WindowInputEventReceiver(mInputChannel, + Looper.myLooper()); } } @@ -787,23 +798,19 @@ public final class ViewRootImpl extends Handler implements ViewParent, public void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; - - //noinspection ConstantConditions - if (ViewDebug.DEBUG_LATENCY && mLastTraversalFinishedTimeNanos != 0) { - final long now = System.nanoTime(); - Log.d(TAG, "Latency: Scheduled traversal, it has been " - + ((now - mLastTraversalFinishedTimeNanos) * 0.000001f) - + "ms since the last traversal finished."); - } - - sendEmptyMessage(DO_TRAVERSAL); + mChoreographer.scheduleDraw(); } } public void unscheduleTraversals() { + mTraversalScheduled = false; + } + + @Override + public void onDraw() { if (mTraversalScheduled) { mTraversalScheduled = false; - removeMessages(DO_TRAVERSAL); + doTraversal(); } } @@ -840,24 +847,45 @@ public final class ViewRootImpl extends Handler implements ViewParent, } } - private void processInputEvents(boolean outOfOrder) { - while (mPendingInputEvents != null) { - handleMessage(mPendingInputEvents.mMessage); - InputEventMessage tmpMessage = mPendingInputEvents; - mPendingInputEvents = mPendingInputEvents.mNext; - tmpMessage.recycle(); - if (outOfOrder) { - removeMessages(PROCESS_INPUT_EVENTS); + private void doTraversal() { + doProcessInputEvents(); + + if (mProfile) { + Debug.startMethodTracing("ViewAncestor"); + } + + final long traversalStartTime; + if (ViewDebug.DEBUG_LATENCY) { + traversalStartTime = System.nanoTime(); + if (mLastTraversalFinishedTimeNanos != 0) { + Log.d(ViewDebug.DEBUG_LATENCY_TAG, "Starting performTraversals(); it has been " + + ((traversalStartTime - mLastTraversalFinishedTimeNanos) * 0.000001f) + + "ms since the last traversals finished."); + } else { + Log.d(ViewDebug.DEBUG_LATENCY_TAG, "Starting performTraversals()."); } } + + performTraversals(); + + if (ViewDebug.DEBUG_LATENCY) { + long now = System.nanoTime(); + Log.d(ViewDebug.DEBUG_LATENCY_TAG, "performTraversals() took " + + ((now - traversalStartTime) * 0.000001f) + + "ms."); + mLastTraversalFinishedTimeNanos = now; + } + + if (mProfile) { + Debug.stopMethodTracing(); + mProfile = false; + } } private void performTraversals() { // cache mView since it is used so much below... final View host = mView; - processInputEvents(true); - if (DBG) { System.out.println("======================================"); System.out.println("performTraversals"); @@ -867,10 +895,8 @@ public final class ViewRootImpl extends Handler implements ViewParent, if (host == null || !mAdded) return; - mTraversalScheduled = false; mWillDrawSoon = true; boolean windowSizeMayChange = false; - boolean fullRedrawNeeded = mFullRedrawNeeded; boolean newSurface = false; boolean surfaceChanged = false; WindowManager.LayoutParams lp = mWindowAttributes; @@ -895,7 +921,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, CompatibilityInfo compatibilityInfo = mCompatibilityInfo.get(); if (compatibilityInfo.supportsScreen() == mLastInCompatMode) { params = lp; - fullRedrawNeeded = true; + mFullRedrawNeeded = true; mLayoutRequested = true; if (mLastInCompatMode) { params.flags &= ~WindowManager.LayoutParams.FLAG_COMPATIBLE_WINDOW; @@ -910,7 +936,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, Rect frame = mWinFrame; if (mFirst) { - fullRedrawNeeded = true; + mFullRedrawNeeded = true; mLayoutRequested = true; if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL) { @@ -954,7 +980,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) { if (DEBUG_ORIENTATION) Log.v(TAG, "View " + host + " resized to: " + frame); - fullRedrawNeeded = true; + mFullRedrawNeeded = true; mLayoutRequested = true; windowSizeMayChange = true; } @@ -1292,7 +1318,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, // before actually drawing them, so it can display then // all at once. newSurface = true; - fullRedrawNeeded = true; + mFullRedrawNeeded = true; mPreviousTransparentRegion.setEmpty(); if (mAttachInfo.mHardwareRenderer != null) { @@ -1328,7 +1354,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, } } else if (surfaceGenerationId != mSurface.getGenerationId() && mSurfaceHolder == null && mAttachInfo.mHardwareRenderer != null) { - fullRedrawNeeded = true; + mFullRedrawNeeded = true; try { mAttachInfo.mHardwareRenderer.updateSurface(mHolder); } catch (Surface.OutOfResourcesException e) { @@ -1614,6 +1640,11 @@ public final class ViewRootImpl extends Handler implements ViewParent, } } + // Remember if we must report the next draw. + if ((relayoutResult & WindowManagerImpl.RELAYOUT_RES_FIRST_TIME) != 0) { + mReportNextDraw = true; + } + boolean cancelDraw = attachInfo.mTreeObserver.dispatchOnPreDraw() || viewVisibility != View.VISIBLE; @@ -1624,42 +1655,8 @@ public final class ViewRootImpl extends Handler implements ViewParent, } mPendingTransitions.clear(); } - mFullRedrawNeeded = false; - - final long drawStartTime; - if (ViewDebug.DEBUG_LATENCY) { - drawStartTime = System.nanoTime(); - } - - draw(fullRedrawNeeded); - - if (ViewDebug.DEBUG_LATENCY) { - mLastDrawDurationNanos = System.nanoTime() - drawStartTime; - } - if ((relayoutResult&WindowManagerImpl.RELAYOUT_RES_FIRST_TIME) != 0 - || mReportNextDraw) { - if (LOCAL_LOGV) { - Log.v(TAG, "FINISHED DRAWING: " + mWindowAttributes.getTitle()); - } - mReportNextDraw = false; - if (mSurfaceHolder != null && mSurface.isValid()) { - mSurfaceHolderCallback.surfaceRedrawNeeded(mSurfaceHolder); - SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks(); - if (callbacks != null) { - for (SurfaceHolder.Callback c : callbacks) { - if (c instanceof SurfaceHolder.Callback2) { - ((SurfaceHolder.Callback2)c).surfaceRedrawNeeded( - mSurfaceHolder); - } - } - } - } - try { - sWindowSession.finishDrawing(mWindow); - } catch (RemoteException e) { - } - } + performDraw(); } else { // End any pending transitions on this non-visible window if (mPendingTransitions != null && mPendingTransitions.size() > 0) { @@ -1668,14 +1665,6 @@ public final class ViewRootImpl extends Handler implements ViewParent, } mPendingTransitions.clear(); } - // We were supposed to report when we are done drawing. Since we canceled the - // draw, remember it here. - if ((relayoutResult&WindowManagerImpl.RELAYOUT_RES_FIRST_TIME) != 0) { - mReportNextDraw = true; - } - if (fullRedrawNeeded) { - mFullRedrawNeeded = true; - } if (viewVisibility == View.VISIBLE) { // Try again @@ -1819,6 +1808,56 @@ public final class ViewRootImpl extends Handler implements ViewParent, } } + private void performDraw() { + final long drawStartTime; + if (ViewDebug.DEBUG_LATENCY) { + drawStartTime = System.nanoTime(); + if (mLastDrawFinishedTimeNanos != 0) { + Log.d(ViewDebug.DEBUG_LATENCY_TAG, "Starting draw(); it has been " + + ((drawStartTime - mLastDrawFinishedTimeNanos) * 0.000001f) + + "ms since the last draw finished."); + } else { + Log.d(ViewDebug.DEBUG_LATENCY_TAG, "Starting draw()."); + } + } + + final boolean fullRedrawNeeded = mFullRedrawNeeded; + mFullRedrawNeeded = false; + draw(fullRedrawNeeded); + + if (ViewDebug.DEBUG_LATENCY) { + long now = System.nanoTime(); + Log.d(ViewDebug.DEBUG_LATENCY_TAG, "performDraw() took " + + ((now - drawStartTime) * 0.000001f) + + "ms."); + mLastDrawFinishedTimeNanos = now; + } + + if (mReportNextDraw) { + mReportNextDraw = false; + + if (LOCAL_LOGV) { + Log.v(TAG, "FINISHED DRAWING: " + mWindowAttributes.getTitle()); + } + if (mSurfaceHolder != null && mSurface.isValid()) { + mSurfaceHolderCallback.surfaceRedrawNeeded(mSurfaceHolder); + SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks(); + if (callbacks != null) { + for (SurfaceHolder.Callback c : callbacks) { + if (c instanceof SurfaceHolder.Callback2) { + ((SurfaceHolder.Callback2)c).surfaceRedrawNeeded( + mSurfaceHolder); + } + } + } + } + try { + sWindowSession.finishDrawing(mWindow); + } catch (RemoteException e) { + } + } + } + private void draw(boolean fullRedrawNeeded) { Surface surface = mSurface; if (surface == null || !surface.isValid()) { @@ -1857,8 +1896,9 @@ public final class ViewRootImpl extends Handler implements ViewParent, mCurScrollY = yoff; fullRedrawNeeded = true; } - float appScale = mAttachInfo.mApplicationScale; - boolean scalingRequired = mAttachInfo.mScalingRequired; + + final float appScale = mAttachInfo.mApplicationScale; + final boolean scalingRequired = mAttachInfo.mScalingRequired; int resizeAlpha = 0; if (mResizeBuffer != null) { @@ -1873,7 +1913,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, } } - Rect dirty = mDirty; + final Rect dirty = mDirty; if (mSurfaceHolder != null) { // The app owns the surface, we won't draw. dirty.setEmpty(); @@ -1891,35 +1931,6 @@ public final class ViewRootImpl extends Handler implements ViewParent, dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f)); } - if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) { - if (!dirty.isEmpty() || mIsAnimating) { - mIsAnimating = false; - mHardwareYOffset = yoff; - mResizeAlpha = resizeAlpha; - - mCurrentDirty.set(dirty); - mCurrentDirty.union(mPreviousDirty); - mPreviousDirty.set(dirty); - dirty.setEmpty(); - - Rect currentDirty = mCurrentDirty; - if (animating) { - currentDirty = null; - } - - if (mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this, currentDirty)) { - mPreviousDirty.set(0, 0, mWidth, mHeight); - } - } - - if (animating) { - mFullRedrawNeeded = true; - scheduleTraversals(); - } - - return; - } - if (DEBUG_ORIENTATION || DEBUG_DRAW) { Log.v(TAG, "Draw " + mView + "/" + mWindowAttributes.getTitle() @@ -1930,64 +1941,79 @@ public final class ViewRootImpl extends Handler implements ViewParent, } if (!dirty.isEmpty() || mIsAnimating) { - Canvas canvas; - try { - int left = dirty.left; - int top = dirty.top; - int right = dirty.right; - int bottom = dirty.bottom; - - final long lockCanvasStartTime; - if (ViewDebug.DEBUG_LATENCY) { - lockCanvasStartTime = System.nanoTime(); - } + if (mAttachInfo.mHardwareRenderer != null + && mAttachInfo.mHardwareRenderer.isEnabled()) { + // Draw with hardware renderer. + mIsAnimating = false; + mHardwareYOffset = yoff; + mResizeAlpha = resizeAlpha; - canvas = surface.lockCanvas(dirty); + mCurrentDirty.set(dirty); + mCurrentDirty.union(mPreviousDirty); + mPreviousDirty.set(dirty); + dirty.setEmpty(); - if (ViewDebug.DEBUG_LATENCY) { - long now = System.nanoTime(); - Log.d(TAG, "Latency: Spent " - + ((now - lockCanvasStartTime) * 0.000001f) - + "ms waiting for surface.lockCanvas()"); + if (mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this, + animating ? null : mCurrentDirty)) { + mPreviousDirty.set(0, 0, mWidth, mHeight); } + } else { + // Draw with software renderer. + Canvas canvas; + try { + int left = dirty.left; + int top = dirty.top; + int right = dirty.right; + int bottom = dirty.bottom; + + final long lockCanvasStartTime; + if (ViewDebug.DEBUG_LATENCY) { + lockCanvasStartTime = System.nanoTime(); + } - if (left != dirty.left || top != dirty.top || right != dirty.right || - bottom != dirty.bottom) { - mAttachInfo.mIgnoreDirtyState = true; - } + canvas = mSurface.lockCanvas(dirty); - // TODO: Do this in native - canvas.setDensity(mDensity); - } catch (Surface.OutOfResourcesException e) { - Log.e(TAG, "OutOfResourcesException locking surface", e); - try { - if (!sWindowSession.outOfMemory(mWindow)) { - Slog.w(TAG, "No processes killed for memory; killing self"); - Process.killProcess(Process.myPid()); + if (ViewDebug.DEBUG_LATENCY) { + long now = System.nanoTime(); + Log.d(ViewDebug.DEBUG_LATENCY_TAG, "- lockCanvas() took " + + ((now - lockCanvasStartTime) * 0.000001f) + "ms"); } - } catch (RemoteException ex) { - } - mLayoutRequested = true; // ask wm for a new surface next time. - return; - } catch (IllegalArgumentException e) { - Log.e(TAG, "IllegalArgumentException locking surface", e); - // Don't assume this is due to out of memory, it could be - // something else, and if it is something else then we could - // kill stuff (or ourself) for no reason. - mLayoutRequested = true; // ask wm for a new surface next time. - return; - } - try { - if (!dirty.isEmpty() || mIsAnimating) { - long startTime = 0L; + if (left != dirty.left || top != dirty.top || right != dirty.right || + bottom != dirty.bottom) { + mAttachInfo.mIgnoreDirtyState = true; + } + + // TODO: Do this in native + canvas.setDensity(mDensity); + } catch (Surface.OutOfResourcesException e) { + Log.e(TAG, "OutOfResourcesException locking surface", e); + try { + if (!sWindowSession.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. + return; + } catch (IllegalArgumentException e) { + Log.e(TAG, "IllegalArgumentException locking surface", e); + // Don't assume this is due to out of memory, it could be + // something else, and if it is something else then we could + // kill stuff (or ourself) for no reason. + mLayoutRequested = true; // ask wm for a new surface next time. + return; + } + try { if (DEBUG_ORIENTATION || DEBUG_DRAW) { Log.v(TAG, "Surface " + surface + " drawing to bitmap w=" + canvas.getWidth() + ", h=" + canvas.getHeight()); //canvas.drawARGB(255, 255, 0, 0); } + long startTime = 0L; if (ViewDebug.DEBUG_PROFILE_DRAWING) { startTime = SystemClock.elapsedRealtime(); } @@ -2023,7 +2049,19 @@ public final class ViewRootImpl extends Handler implements ViewParent, canvas.setScreenDensity(scalingRequired ? DisplayMetrics.DENSITY_DEVICE : 0); mAttachInfo.mSetIgnoreDirtyState = false; + + final long drawStartTime; + if (ViewDebug.DEBUG_LATENCY) { + drawStartTime = System.nanoTime(); + } + mView.draw(canvas); + + if (ViewDebug.DEBUG_LATENCY) { + long now = System.nanoTime(); + Log.d(ViewDebug.DEBUG_LATENCY_TAG, "- draw() took " + + ((now - drawStartTime) * 0.000001f) + "ms"); + } } finally { if (!mAttachInfo.mSetIgnoreDirtyState) { // Only clear the flag if it was not set during the mView.draw() call @@ -2038,15 +2076,25 @@ public final class ViewRootImpl extends Handler implements ViewParent, if (ViewDebug.DEBUG_PROFILE_DRAWING) { EventLog.writeEvent(60000, SystemClock.elapsedRealtime() - startTime); } - } + } finally { + final long unlockCanvasAndPostStartTime; + if (ViewDebug.DEBUG_LATENCY) { + unlockCanvasAndPostStartTime = System.nanoTime(); + } - } finally { - surface.unlockCanvasAndPost(canvas); - } - } + surface.unlockCanvasAndPost(canvas); - if (LOCAL_LOGV) { - Log.v(TAG, "Surface " + surface + " unlockCanvasAndPost"); + if (ViewDebug.DEBUG_LATENCY) { + long now = System.nanoTime(); + Log.d(ViewDebug.DEBUG_LATENCY_TAG, "- unlockCanvasAndPost() took " + + ((now - unlockCanvasAndPostStartTime) * 0.000001f) + "ms"); + } + + if (LOCAL_LOGV) { + Log.v(TAG, "Surface " + surface + " unlockCanvasAndPost"); + } + } + } } if (animating) { @@ -2265,8 +2313,9 @@ public final class ViewRootImpl extends Handler implements ViewParent, mInputQueueCallback.onInputQueueDestroyed(mInputQueue); mInputQueueCallback = null; mInputQueue = null; - } else if (mInputChannel != null) { - InputQueue.unregisterInputChannel(mInputChannel); + } else if (mInputEventReceiver != null) { + mInputEventReceiver.dispose(); + mInputEventReceiver = null; } try { sWindowSession.remove(mWindow); @@ -2279,6 +2328,8 @@ public final class ViewRootImpl extends Handler implements ViewParent, mInputChannel.dispose(); mInputChannel = null; } + + mChoreographer.removeOnDrawListener(this); } void updateConfiguration(Configuration config, boolean force) { @@ -2333,17 +2384,14 @@ public final class ViewRootImpl extends Handler implements ViewParent, } } - public final static int DO_TRAVERSAL = 1000; public final static int DIE = 1001; public final static int RESIZED = 1002; public final static int RESIZED_REPORT = 1003; public final static int WINDOW_FOCUS_CHANGED = 1004; public final static int DISPATCH_KEY = 1005; - public final static int DISPATCH_POINTER = 1006; - public final static int DISPATCH_TRACKBALL = 1007; public final static int DISPATCH_APP_VISIBILITY = 1008; public final static int DISPATCH_GET_NEW_SURFACE = 1009; - public final static int FINISHED_EVENT = 1010; + public final static int IME_FINISHED_EVENT = 1010; public final static int DISPATCH_KEY_FROM_IME = 1011; public final static int FINISH_INPUT_CONNECTION = 1012; public final static int CHECK_FOCUS = 1013; @@ -2356,14 +2404,12 @@ public final class ViewRootImpl extends Handler implements ViewParent, public final static int DO_PERFORM_ACCESSIBILITY_ACTION = 1020; public final static int DO_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID = 1021; public final static int DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID = 1022; - public final static int DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_TEXT = 1023; - public final static int PROCESS_INPUT_EVENTS = 1024; + public final static int DO_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT = 1023; + public final static int DO_PROCESS_INPUT_EVENTS = 1024; @Override public String getMessageName(Message message) { switch (message.what) { - case DO_TRAVERSAL: - return "DO_TRAVERSAL"; case DIE: return "DIE"; case RESIZED: @@ -2374,16 +2420,12 @@ public final class ViewRootImpl extends Handler implements ViewParent, return "WINDOW_FOCUS_CHANGED"; case DISPATCH_KEY: return "DISPATCH_KEY"; - case DISPATCH_POINTER: - return "DISPATCH_POINTER"; - case DISPATCH_TRACKBALL: - return "DISPATCH_TRACKBALL"; case DISPATCH_APP_VISIBILITY: return "DISPATCH_APP_VISIBILITY"; case DISPATCH_GET_NEW_SURFACE: return "DISPATCH_GET_NEW_SURFACE"; - case FINISHED_EVENT: - return "FINISHED_EVENT"; + case IME_FINISHED_EVENT: + return "IME_FINISHED_EVENT"; case DISPATCH_KEY_FROM_IME: return "DISPATCH_KEY_FROM_IME"; case FINISH_INPUT_CONNECTION: @@ -2408,11 +2450,10 @@ public final class ViewRootImpl extends Handler implements ViewParent, return "DO_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID"; case DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID: return "DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID"; - case DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_TEXT: - return "DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_TEXT"; - case PROCESS_INPUT_EVENTS: - return "PROCESS_INPUT_EVENTS"; - + case DO_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT: + return "DO_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT"; + case DO_PROCESS_INPUT_EVENTS: + return "DO_PROCESS_INPUT_EVENTS"; } return super.getMessageName(message); } @@ -2428,51 +2469,12 @@ public final class ViewRootImpl extends Handler implements ViewParent, info.target.invalidate(info.left, info.top, info.right, info.bottom); info.release(); break; - case DO_TRAVERSAL: - if (mProfile) { - Debug.startMethodTracing("ViewAncestor"); - } - - final long traversalStartTime; - if (ViewDebug.DEBUG_LATENCY) { - traversalStartTime = System.nanoTime(); - mLastDrawDurationNanos = 0; - } - - performTraversals(); - - if (ViewDebug.DEBUG_LATENCY) { - long now = System.nanoTime(); - Log.d(TAG, "Latency: Spent " - + ((now - traversalStartTime) * 0.000001f) - + "ms in performTraversals(), with " - + (mLastDrawDurationNanos * 0.000001f) - + "ms of that time in draw()"); - mLastTraversalFinishedTimeNanos = now; - } - - if (mProfile) { - Debug.stopMethodTracing(); - mProfile = false; - } - break; - case FINISHED_EVENT: - handleFinishedEvent(msg.arg1, msg.arg2 != 0); - break; - case DISPATCH_KEY: - deliverKeyEvent((KeyEvent)msg.obj, msg.arg1 != 0); - break; - case DISPATCH_POINTER: - deliverPointerEvent((MotionEvent) msg.obj, msg.arg1 != 0); + case IME_FINISHED_EVENT: + handleImeFinishedEvent(msg.arg1, msg.arg2 != 0); break; - case DISPATCH_TRACKBALL: - deliverTrackballEvent((MotionEvent) msg.obj, msg.arg1 != 0); - break; - case DISPATCH_GENERIC_MOTION: - deliverGenericMotionEvent((MotionEvent) msg.obj, msg.arg1 != 0); - break; - case PROCESS_INPUT_EVENTS: - processInputEvents(false); + case DO_PROCESS_INPUT_EVENTS: + mProcessInputEventsScheduled = false; + doProcessInputEvents(); break; case DISPATCH_APP_VISIBILITY: handleAppVisibility(msg.arg1 != 0); @@ -2583,6 +2585,10 @@ public final class ViewRootImpl extends Handler implements ViewParent, case DIE: doDie(); break; + case DISPATCH_KEY: { + KeyEvent event = (KeyEvent)msg.obj; + enqueueInputEvent(event, null, 0); + } break; case DISPATCH_KEY_FROM_IME: { if (LOCAL_LOGV) Log.v( TAG, "Dispatching key " @@ -2594,7 +2600,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, //noinspection UnusedAssignment event = KeyEvent.changeFlags(event, event.getFlags() & ~KeyEvent.FLAG_FROM_SYSTEM); } - deliverKeyEventPostIme((KeyEvent)msg.obj, false); + enqueueInputEvent(event, null, QueuedInputEvent.FLAG_DELIVER_POST_IME); } break; case FINISH_INPUT_CONNECTION: { InputMethodManager imm = InputMethodManager.peekInstance(); @@ -2647,79 +2653,15 @@ public final class ViewRootImpl extends Handler implements ViewParent, .findAccessibilityNodeInfoByViewIdUiThread(msg); } } break; - case DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_TEXT: { + case DO_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT: { if (mView != null) { getAccessibilityInteractionController() - .findAccessibilityNodeInfosByViewTextUiThread(msg); + .findAccessibilityNodeInfosByTextUiThread(msg); } } break; } } - private void startInputEvent(InputQueue.FinishedCallback finishedCallback) { - if (mFinishedCallback != null) { - Slog.w(TAG, "Received a new input event from the input queue but there is " - + "already an unfinished input event in progress."); - } - - if (ViewDebug.DEBUG_LATENCY) { - mInputEventReceiveTimeNanos = System.nanoTime(); - mInputEventDeliverTimeNanos = 0; - mInputEventDeliverPostImeTimeNanos = 0; - } - - mFinishedCallback = finishedCallback; - } - - private void finishInputEvent(InputEvent event, boolean handled) { - if (LOCAL_LOGV) Log.v(TAG, "Telling window manager input event is finished"); - - if (mFinishedCallback == null) { - Slog.w(TAG, "Attempted to tell the input queue that the current input event " - + "is finished but there is no input event actually in progress."); - return; - } - - if (ViewDebug.DEBUG_LATENCY) { - final long now = System.nanoTime(); - final long eventTime = event.getEventTimeNano(); - final StringBuilder msg = new StringBuilder(); - msg.append("Latency: Spent "); - msg.append((now - mInputEventReceiveTimeNanos) * 0.000001f); - msg.append("ms processing "); - if (event instanceof KeyEvent) { - final KeyEvent keyEvent = (KeyEvent)event; - msg.append("key event, action="); - msg.append(KeyEvent.actionToString(keyEvent.getAction())); - } else { - final MotionEvent motionEvent = (MotionEvent)event; - msg.append("motion event, action="); - msg.append(MotionEvent.actionToString(motionEvent.getAction())); - msg.append(", historySize="); - msg.append(motionEvent.getHistorySize()); - } - msg.append(", handled="); - msg.append(handled); - msg.append(", received at +"); - msg.append((mInputEventReceiveTimeNanos - eventTime) * 0.000001f); - if (mInputEventDeliverTimeNanos != 0) { - msg.append("ms, delivered at +"); - msg.append((mInputEventDeliverTimeNanos - eventTime) * 0.000001f); - } - if (mInputEventDeliverPostImeTimeNanos != 0) { - msg.append("ms, delivered post IME at +"); - msg.append((mInputEventDeliverPostImeTimeNanos - eventTime) * 0.000001f); - } - msg.append("ms, finished at +"); - msg.append((now - eventTime) * 0.000001f); - msg.append("ms."); - Log.d(TAG, msg.toString()); - } - - mFinishedCallback.finished(handled); - mFinishedCallback = null; - } - /** * Something in the current window tells us we need to change the touch mode. For * example, we are not in touch mode, and the user touches the screen. @@ -2841,11 +2783,27 @@ public final class ViewRootImpl extends Handler implements ViewParent, return false; } - private void deliverPointerEvent(MotionEvent event, boolean sendDone) { + private void deliverInputEvent(QueuedInputEvent q) { if (ViewDebug.DEBUG_LATENCY) { - mInputEventDeliverTimeNanos = System.nanoTime(); + q.mDeliverTimeNanos = System.nanoTime(); } + if (q.mEvent instanceof KeyEvent) { + deliverKeyEvent(q); + } else { + final int source = q.mEvent.getSource(); + if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) { + deliverPointerEvent(q); + } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { + deliverTrackballEvent(q); + } else { + deliverGenericMotionEvent(q); + } + } + } + + private void deliverPointerEvent(QueuedInputEvent q) { + final MotionEvent event = (MotionEvent)q.mEvent; final boolean isTouchEvent = event.isTouchEvent(); if (mInputEventConsistencyVerifier != null) { if (isTouchEvent) { @@ -2857,7 +2815,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, // If there is no view, then the event will not be handled. if (mView == null || !mAdded) { - finishMotionEvent(event, sendDone, false); + finishInputEvent(q, false); return; } @@ -2892,41 +2850,23 @@ public final class ViewRootImpl extends Handler implements ViewParent, lt.sample("B Dispatched PointerEvents ", System.nanoTime() - event.getEventTimeNano()); } if (handled) { - finishMotionEvent(event, sendDone, true); + finishInputEvent(q, true); return; } // Pointer event was unhandled. - finishMotionEvent(event, sendDone, false); + finishInputEvent(q, false); } - private void finishMotionEvent(MotionEvent event, boolean sendDone, boolean handled) { - event.recycle(); - if (sendDone) { - finishInputEvent(event, handled); - } - //noinspection ConstantConditions - if (LOCAL_LOGV || WATCH_POINTER) { - if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { - Log.i(TAG, "Done dispatching!"); - } - } - } - - private void deliverTrackballEvent(MotionEvent event, boolean sendDone) { - if (ViewDebug.DEBUG_LATENCY) { - mInputEventDeliverTimeNanos = System.nanoTime(); - } - - if (DEBUG_TRACKBALL) Log.v(TAG, "Motion event:" + event); - + private void deliverTrackballEvent(QueuedInputEvent q) { + final MotionEvent event = (MotionEvent)q.mEvent; if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTrackballEvent(event, 0); } // If there is no view, then the event will not be handled. if (mView == null || !mAdded) { - finishMotionEvent(event, sendDone, false); + finishInputEvent(q, false); return; } @@ -2938,7 +2878,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, // touch mode here. ensureTouchMode(false); - finishMotionEvent(event, sendDone, true); + finishInputEvent(q, true); mLastTrackballTime = Integer.MIN_VALUE; return; } @@ -2962,18 +2902,18 @@ public final class ViewRootImpl extends Handler implements ViewParent, case MotionEvent.ACTION_DOWN: x.reset(2); y.reset(2); - deliverKeyEvent(new KeyEvent(curTime, curTime, + dispatchKey(new KeyEvent(curTime, curTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FALLBACK, - InputDevice.SOURCE_KEYBOARD), false); + InputDevice.SOURCE_KEYBOARD)); break; case MotionEvent.ACTION_UP: x.reset(2); y.reset(2); - deliverKeyEvent(new KeyEvent(curTime, curTime, + dispatchKey(new KeyEvent(curTime, curTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_CENTER, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FALLBACK, - InputDevice.SOURCE_KEYBOARD), false); + InputDevice.SOURCE_KEYBOARD)); break; } @@ -3024,38 +2964,35 @@ public final class ViewRootImpl extends Handler implements ViewParent, + keycode); movement--; int repeatCount = accelMovement - movement; - deliverKeyEvent(new KeyEvent(curTime, curTime, + dispatchKey(new KeyEvent(curTime, curTime, KeyEvent.ACTION_MULTIPLE, keycode, repeatCount, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FALLBACK, - InputDevice.SOURCE_KEYBOARD), false); + InputDevice.SOURCE_KEYBOARD)); } while (movement > 0) { if (DEBUG_TRACKBALL) Log.v("foo", "Delivering fake DPAD: " + keycode); movement--; curTime = SystemClock.uptimeMillis(); - deliverKeyEvent(new KeyEvent(curTime, curTime, + dispatchKey(new KeyEvent(curTime, curTime, KeyEvent.ACTION_DOWN, keycode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FALLBACK, - InputDevice.SOURCE_KEYBOARD), false); - deliverKeyEvent(new KeyEvent(curTime, curTime, + InputDevice.SOURCE_KEYBOARD)); + dispatchKey(new KeyEvent(curTime, curTime, KeyEvent.ACTION_UP, keycode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FALLBACK, - InputDevice.SOURCE_KEYBOARD), false); - } + InputDevice.SOURCE_KEYBOARD)); + } mLastTrackballTime = curTime; } // Unfortunately we can't tell whether the application consumed the keys, so // we always consider the trackball event handled. - finishMotionEvent(event, sendDone, true); + finishInputEvent(q, true); } - private void deliverGenericMotionEvent(MotionEvent event, boolean sendDone) { - if (ViewDebug.DEBUG_LATENCY) { - mInputEventDeliverTimeNanos = System.nanoTime(); - } - + private void deliverGenericMotionEvent(QueuedInputEvent q) { + final MotionEvent event = (MotionEvent)q.mEvent; if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onGenericMotionEvent(event, 0); } @@ -3068,7 +3005,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, if (isJoystick) { updateJoystickDirection(event, false); } - finishMotionEvent(event, sendDone, false); + finishInputEvent(q, false); return; } @@ -3077,16 +3014,16 @@ public final class ViewRootImpl extends Handler implements ViewParent, if (isJoystick) { updateJoystickDirection(event, false); } - finishMotionEvent(event, sendDone, true); + finishInputEvent(q, true); return; } if (isJoystick) { // Translate the joystick event into DPAD keys and try to deliver those. updateJoystickDirection(event, true); - finishMotionEvent(event, sendDone, true); + finishInputEvent(q, true); } else { - finishMotionEvent(event, sendDone, false); + finishInputEvent(q, false); } } @@ -3108,9 +3045,9 @@ public final class ViewRootImpl extends Handler implements ViewParent, if (xDirection != mLastJoystickXDirection) { if (mLastJoystickXKeyCode != 0) { - deliverKeyEvent(new KeyEvent(time, time, + dispatchKey(new KeyEvent(time, time, KeyEvent.ACTION_UP, mLastJoystickXKeyCode, 0, metaState, - deviceId, 0, KeyEvent.FLAG_FALLBACK, source), false); + deviceId, 0, KeyEvent.FLAG_FALLBACK, source)); mLastJoystickXKeyCode = 0; } @@ -3119,17 +3056,17 @@ public final class ViewRootImpl extends Handler implements ViewParent, if (xDirection != 0 && synthesizeNewKeys) { mLastJoystickXKeyCode = xDirection > 0 ? KeyEvent.KEYCODE_DPAD_RIGHT : KeyEvent.KEYCODE_DPAD_LEFT; - deliverKeyEvent(new KeyEvent(time, time, + dispatchKey(new KeyEvent(time, time, KeyEvent.ACTION_DOWN, mLastJoystickXKeyCode, 0, metaState, - deviceId, 0, KeyEvent.FLAG_FALLBACK, source), false); + deviceId, 0, KeyEvent.FLAG_FALLBACK, source)); } } if (yDirection != mLastJoystickYDirection) { if (mLastJoystickYKeyCode != 0) { - deliverKeyEvent(new KeyEvent(time, time, + dispatchKey(new KeyEvent(time, time, KeyEvent.ACTION_UP, mLastJoystickYKeyCode, 0, metaState, - deviceId, 0, KeyEvent.FLAG_FALLBACK, source), false); + deviceId, 0, KeyEvent.FLAG_FALLBACK, source)); mLastJoystickYKeyCode = 0; } @@ -3138,9 +3075,9 @@ public final class ViewRootImpl extends Handler implements ViewParent, if (yDirection != 0 && synthesizeNewKeys) { mLastJoystickYKeyCode = yDirection > 0 ? KeyEvent.KEYCODE_DPAD_DOWN : KeyEvent.KEYCODE_DPAD_UP; - deliverKeyEvent(new KeyEvent(time, time, + dispatchKey(new KeyEvent(time, time, KeyEvent.ACTION_DOWN, mLastJoystickYKeyCode, 0, metaState, - deviceId, 0, KeyEvent.FLAG_FALLBACK, source), false); + deviceId, 0, KeyEvent.FLAG_FALLBACK, source)); } } } @@ -3231,91 +3168,81 @@ public final class ViewRootImpl extends Handler implements ViewParent, return false; } - int enqueuePendingEvent(Object event, boolean sendDone) { - int seq = mPendingEventSeq+1; - if (seq < 0) seq = 0; - mPendingEventSeq = seq; - mPendingEvents.put(seq, event); - return sendDone ? seq : -seq; - } - - Object retrievePendingEvent(int seq) { - if (seq < 0) seq = -seq; - Object event = mPendingEvents.get(seq); - if (event != null) { - mPendingEvents.remove(seq); - } - return event; - } - - private void deliverKeyEvent(KeyEvent event, boolean sendDone) { - if (ViewDebug.DEBUG_LATENCY) { - mInputEventDeliverTimeNanos = System.nanoTime(); - } - + private void deliverKeyEvent(QueuedInputEvent q) { + final KeyEvent event = (KeyEvent)q.mEvent; if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onKeyEvent(event, 0); } - // If there is no view, then the event will not be handled. - if (mView == null || !mAdded) { - finishKeyEvent(event, sendDone, false); - return; - } - - if (LOCAL_LOGV) Log.v(TAG, "Dispatching key " + event + " to " + mView); + if ((q.mFlags & QueuedInputEvent.FLAG_DELIVER_POST_IME) == 0) { + // If there is no view, then the event will not be handled. + if (mView == null || !mAdded) { + finishInputEvent(q, false); + return; + } - // Perform predispatching before the IME. - if (mView.dispatchKeyEventPreIme(event)) { - finishKeyEvent(event, sendDone, true); - return; - } + if (LOCAL_LOGV) Log.v(TAG, "Dispatching key " + event + " to " + mView); - // Dispatch to the IME before propagating down the view hierarchy. - // The IME will eventually call back into handleFinishedEvent. - if (mLastWasImTarget) { - InputMethodManager imm = InputMethodManager.peekInstance(); - if (imm != null) { - int seq = enqueuePendingEvent(event, sendDone); - if (DEBUG_IMF) Log.v(TAG, "Sending key event to IME: seq=" - + seq + " event=" + event); - imm.dispatchKeyEvent(mView.getContext(), seq, event, mInputMethodCallback); + // Perform predispatching before the IME. + if (mView.dispatchKeyEventPreIme(event)) { + finishInputEvent(q, true); return; } + + // Dispatch to the IME before propagating down the view hierarchy. + // The IME will eventually call back into handleImeFinishedEvent. + if (mLastWasImTarget) { + InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm != null) { + final int seq = event.getSequenceNumber(); + if (DEBUG_IMF) Log.v(TAG, "Sending key event to IME: seq=" + + seq + " event=" + event); + imm.dispatchKeyEvent(mView.getContext(), seq, event, mInputMethodCallback); + return; + } + } } // Not dispatching to IME, continue with post IME actions. - deliverKeyEventPostIme(event, sendDone); + deliverKeyEventPostIme(q); } - private void handleFinishedEvent(int seq, boolean handled) { - final KeyEvent event = (KeyEvent)retrievePendingEvent(seq); - if (DEBUG_IMF) Log.v(TAG, "IME finished event: seq=" + seq - + " handled=" + handled + " event=" + event); - if (event != null) { - final boolean sendDone = seq >= 0; + void handleImeFinishedEvent(int seq, boolean handled) { + final QueuedInputEvent q = mCurrentInputEvent; + if (q != null && q.mEvent.getSequenceNumber() == seq) { + final KeyEvent event = (KeyEvent)q.mEvent; + if (DEBUG_IMF) { + Log.v(TAG, "IME finished event: seq=" + seq + + " handled=" + handled + " event=" + event); + } if (handled) { - finishKeyEvent(event, sendDone, true); + finishInputEvent(q, true); } else { - deliverKeyEventPostIme(event, sendDone); + deliverKeyEventPostIme(q); + } + } else { + if (DEBUG_IMF) { + Log.v(TAG, "IME finished event: seq=" + seq + + " handled=" + handled + ", event not found!"); } } } - private void deliverKeyEventPostIme(KeyEvent event, boolean sendDone) { + private void deliverKeyEventPostIme(QueuedInputEvent q) { + final KeyEvent event = (KeyEvent)q.mEvent; if (ViewDebug.DEBUG_LATENCY) { - mInputEventDeliverPostImeTimeNanos = System.nanoTime(); + q.mDeliverPostImeTimeNanos = System.nanoTime(); } // If the view went away, then the event will not be handled. if (mView == null || !mAdded) { - finishKeyEvent(event, sendDone, false); + finishInputEvent(q, false); return; } // If the key's purpose is to exit touch mode then we consume it and consider it handled. if (checkForLeavingTouchModeAndConsume(event)) { - finishKeyEvent(event, sendDone, true); + finishInputEvent(q, true); return; } @@ -3325,7 +3252,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, // Deliver the key to the view hierarchy. if (mView.dispatchKeyEvent(event)) { - finishKeyEvent(event, sendDone, true); + finishInputEvent(q, true); return; } @@ -3335,14 +3262,14 @@ public final class ViewRootImpl extends Handler implements ViewParent, && event.getRepeatCount() == 0 && !KeyEvent.isModifierKey(event.getKeyCode())) { if (mView.dispatchKeyShortcutEvent(event)) { - finishKeyEvent(event, sendDone, true); + finishInputEvent(q, true); return; } } // Apply the fallback event policy. if (mFallbackEventHandler.dispatchKeyEvent(event)) { - finishKeyEvent(event, sendDone, true); + finishInputEvent(q, true); return; } @@ -3397,14 +3324,14 @@ public final class ViewRootImpl extends Handler implements ViewParent, if (v.requestFocus(direction, mTempRect)) { playSoundEffect( SoundEffectConstants.getContantForFocusDirection(direction)); - finishKeyEvent(event, sendDone, true); + finishInputEvent(q, true); return; } } // Give the focused view a last chance to handle the dpad key. if (mView.dispatchUnhandledMove(focused, direction)) { - finishKeyEvent(event, sendDone, true); + finishInputEvent(q, true); return; } } @@ -3412,13 +3339,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, } // Key was unhandled. - finishKeyEvent(event, sendDone, false); - } - - private void finishKeyEvent(KeyEvent event, boolean sendDone, boolean handled) { - if (sendDone) { - finishInputEvent(event, handled); - } + finishInputEvent(q, false); } /* drag/drop */ @@ -3743,8 +3664,8 @@ public final class ViewRootImpl extends Handler implements ViewParent, } } - public void dispatchFinishedEvent(int seq, boolean handled) { - Message msg = obtainMessage(FINISHED_EVENT); + void dispatchImeFinishedEvent(int seq, boolean handled) { + Message msg = obtainMessage(IME_FINISHED_EVENT); msg.arg1 = seq; msg.arg2 = handled ? 1 : 0; sendMessage(msg); @@ -3773,152 +3694,182 @@ public final class ViewRootImpl extends Handler implements ViewParent, sendMessage(msg); } - private long mInputEventReceiveTimeNanos; - private long mInputEventDeliverTimeNanos; - private long mInputEventDeliverPostImeTimeNanos; - private InputQueue.FinishedCallback mFinishedCallback; - - private final InputHandler mInputHandler = new InputHandler() { - public void handleKey(KeyEvent event, InputQueue.FinishedCallback finishedCallback) { - startInputEvent(finishedCallback); - dispatchKey(event, true); - } - - public void handleMotion(MotionEvent event, InputQueue.FinishedCallback finishedCallback) { - startInputEvent(finishedCallback); - dispatchMotion(event, true); - } - }; - /** - * Utility class used to queue up input events which are then handled during - * performTraversals(). Doing it this way allows us to ensure that we are up to date with - * all input events just prior to drawing, instead of placing those events on the regular - * handler queue, potentially behind a drawing event. + * Represents a pending input event that is waiting in a queue. + * + * Input events are processed in serial order by the timestamp specified by + * {@link InputEvent#getEventTime()}. In general, the input dispatcher delivers + * one input event to the application at a time and waits for the application + * to finish handling it before delivering the next one. + * + * However, because the application or IME can synthesize and inject multiple + * key events at a time without going through the input dispatcher, we end up + * needing a queue on the application's side. */ - static class InputEventMessage { - Message mMessage; - InputEventMessage mNext; + private static final class QueuedInputEvent { + public static final int FLAG_DELIVER_POST_IME = 1 << 0; - private static final Object sPoolSync = new Object(); - private static InputEventMessage sPool; - private static int sPoolSize = 0; + public QueuedInputEvent mNext; - private static final int MAX_POOL_SIZE = 10; + public InputEvent mEvent; + public InputEventReceiver mReceiver; + public int mFlags; - private InputEventMessage(Message m) { - mMessage = m; - mNext = null; - } + // Used for latency calculations. + public long mReceiveTimeNanos; + public long mDeliverTimeNanos; + public long mDeliverPostImeTimeNanos; + } - /** - * Return a new Message instance from the global pool. Allows us to - * avoid allocating new objects in many cases. - */ - public static InputEventMessage obtain(Message msg) { - synchronized (sPoolSync) { - if (sPool != null) { - InputEventMessage m = sPool; - sPool = m.mNext; - m.mNext = null; - sPoolSize--; - m.mMessage = msg; - return m; - } - } - return new InputEventMessage(msg); + private QueuedInputEvent obtainQueuedInputEvent(InputEvent event, + InputEventReceiver receiver, int flags) { + QueuedInputEvent q = mQueuedInputEventPool; + if (q != null) { + mQueuedInputEventPoolSize -= 1; + mQueuedInputEventPool = q.mNext; + q.mNext = null; + } else { + q = new QueuedInputEvent(); } - /** - * Return the message to the pool. - */ - public void recycle() { - mMessage.recycle(); - synchronized (sPoolSync) { - if (sPoolSize < MAX_POOL_SIZE) { - mNext = sPool; - sPool = this; - sPoolSize++; - } - } + q.mEvent = event; + q.mReceiver = receiver; + q.mFlags = flags; + return q; + } + private void recycleQueuedInputEvent(QueuedInputEvent q) { + q.mEvent = null; + q.mReceiver = null; + + if (mQueuedInputEventPoolSize < MAX_QUEUED_INPUT_EVENT_POOL_SIZE) { + mQueuedInputEventPoolSize += 1; + q.mNext = mQueuedInputEventPool; + mQueuedInputEventPool = q; } } - /** - * Place the input event message at the end of the current pending list - */ - private void enqueueInputEvent(Message msg, long when) { - InputEventMessage inputMessage = InputEventMessage.obtain(msg); - if (mPendingInputEvents == null) { - mPendingInputEvents = inputMessage; + void enqueueInputEvent(InputEvent event, + InputEventReceiver receiver, int flags) { + QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags); + + if (ViewDebug.DEBUG_LATENCY) { + q.mReceiveTimeNanos = System.nanoTime(); + q.mDeliverTimeNanos = 0; + q.mDeliverPostImeTimeNanos = 0; + } + + // Always enqueue the input event in order, regardless of its time stamp. + // We do this because the application or the IME may inject key events + // in response to touch events and we want to ensure that the injected keys + // are processed in the order they were received and we cannot trust that + // the time stamp of injected events are monotonic. + QueuedInputEvent last = mFirstPendingInputEvent; + if (last == null) { + mFirstPendingInputEvent = q; } else { - InputEventMessage currMessage = mPendingInputEvents; - while (currMessage.mNext != null) { - currMessage = currMessage.mNext; + while (last.mNext != null) { + last = last.mNext; } - currMessage.mNext = inputMessage; + last.mNext = q; } - sendEmptyMessageAtTime(PROCESS_INPUT_EVENTS, when); + + scheduleProcessInputEvents(); } - public void dispatchKey(KeyEvent event) { - dispatchKey(event, false); + private void scheduleProcessInputEvents() { + if (!mProcessInputEventsScheduled) { + mProcessInputEventsScheduled = true; + sendEmptyMessage(DO_PROCESS_INPUT_EVENTS); + } } - private void dispatchKey(KeyEvent event, boolean sendDone) { - //noinspection ConstantConditions - if (false && event.getAction() == KeyEvent.ACTION_DOWN) { - if (event.getKeyCode() == KeyEvent.KEYCODE_CAMERA) { - if (DBG) Log.d("keydisp", "==================================================="); - if (DBG) Log.d("keydisp", "Focused view Hierarchy is:"); + private void doProcessInputEvents() { + while (mCurrentInputEvent == null && mFirstPendingInputEvent != null) { + QueuedInputEvent q = mFirstPendingInputEvent; + mFirstPendingInputEvent = q.mNext; + q.mNext = null; + mCurrentInputEvent = q; + deliverInputEvent(q); + } + + // We are done processing all input events that we can process right now + // so we can clear the pending flag immediately. + if (mProcessInputEventsScheduled) { + mProcessInputEventsScheduled = false; + removeMessages(DO_PROCESS_INPUT_EVENTS); + } + } - debug(); + private void finishInputEvent(QueuedInputEvent q, boolean handled) { + if (q != mCurrentInputEvent) { + throw new IllegalStateException("finished input event out of order"); + } - if (DBG) Log.d("keydisp", "==================================================="); + if (ViewDebug.DEBUG_LATENCY) { + final long now = System.nanoTime(); + final long eventTime = q.mEvent.getEventTimeNano(); + final StringBuilder msg = new StringBuilder(); + msg.append("Spent "); + msg.append((now - q.mReceiveTimeNanos) * 0.000001f); + msg.append("ms processing "); + if (q.mEvent instanceof KeyEvent) { + final KeyEvent keyEvent = (KeyEvent)q.mEvent; + msg.append("key event, action="); + msg.append(KeyEvent.actionToString(keyEvent.getAction())); + } else { + final MotionEvent motionEvent = (MotionEvent)q.mEvent; + msg.append("motion event, action="); + msg.append(MotionEvent.actionToString(motionEvent.getAction())); + msg.append(", historySize="); + msg.append(motionEvent.getHistorySize()); } + msg.append(", handled="); + msg.append(handled); + msg.append(", received at +"); + msg.append((q.mReceiveTimeNanos - eventTime) * 0.000001f); + if (q.mDeliverTimeNanos != 0) { + msg.append("ms, delivered at +"); + msg.append((q.mDeliverTimeNanos - eventTime) * 0.000001f); + } + if (q.mDeliverPostImeTimeNanos != 0) { + msg.append("ms, delivered post IME at +"); + msg.append((q.mDeliverPostImeTimeNanos - eventTime) * 0.000001f); + } + msg.append("ms, finished at +"); + msg.append((now - eventTime) * 0.000001f); + msg.append("ms."); + Log.d(ViewDebug.DEBUG_LATENCY_TAG, msg.toString()); } - Message msg = obtainMessage(DISPATCH_KEY); - msg.obj = event; - msg.arg1 = sendDone ? 1 : 0; + if (q.mReceiver != null) { + q.mReceiver.finishInputEvent(q.mEvent, handled); + } else { + q.mEvent.recycleIfNeededAfterDispatch(); + } - if (LOCAL_LOGV) Log.v( - TAG, "sending key " + event + " to " + mView); + recycleQueuedInputEvent(q); - enqueueInputEvent(msg, event.getEventTime()); - } - - private void dispatchMotion(MotionEvent event, boolean sendDone) { - int source = event.getSource(); - if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) { - dispatchPointer(event, sendDone); - } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { - dispatchTrackball(event, sendDone); - } else { - dispatchGenericMotion(event, sendDone); + mCurrentInputEvent = null; + if (mFirstPendingInputEvent != null) { + scheduleProcessInputEvents(); } } - private void dispatchPointer(MotionEvent event, boolean sendDone) { - Message msg = obtainMessage(DISPATCH_POINTER); - msg.obj = event; - msg.arg1 = sendDone ? 1 : 0; - enqueueInputEvent(msg, event.getEventTime()); - } + final class WindowInputEventReceiver extends InputEventReceiver { + public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) { + super(inputChannel, looper); + } - private void dispatchTrackball(MotionEvent event, boolean sendDone) { - Message msg = obtainMessage(DISPATCH_TRACKBALL); - msg.obj = event; - msg.arg1 = sendDone ? 1 : 0; - enqueueInputEvent(msg, event.getEventTime()); + @Override + public void onInputEvent(InputEvent event) { + enqueueInputEvent(event, this, 0); + } } + WindowInputEventReceiver mInputEventReceiver; - private void dispatchGenericMotion(MotionEvent event, boolean sendDone) { - Message msg = obtainMessage(DISPATCH_GENERIC_MOTION); - msg.obj = event; - msg.arg1 = sendDone ? 1 : 0; - enqueueInputEvent(msg, event.getEventTime()); + public void dispatchKey(KeyEvent event) { + enqueueInputEvent(event, null, 0); } public void dispatchAppVisibility(boolean visible) { @@ -4100,7 +4051,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, public void finishedEvent(int seq, boolean handled) { final ViewRootImpl viewAncestor = mViewAncestor.get(); if (viewAncestor != null) { - viewAncestor.dispatchFinishedEvent(seq, handled); + viewAncestor.dispatchImeFinishedEvent(seq, handled); } } @@ -4575,52 +4526,52 @@ public final class ViewRootImpl extends Handler implements ViewParent, */ static final class AccessibilityInteractionConnection extends IAccessibilityInteractionConnection.Stub { - private final WeakReference<ViewRootImpl> mRootImpl; + private final WeakReference<ViewRootImpl> mViewRootImpl; - AccessibilityInteractionConnection(ViewRootImpl viewAncestor) { - mRootImpl = new WeakReference<ViewRootImpl>(viewAncestor); + AccessibilityInteractionConnection(ViewRootImpl viewRootImpl) { + mViewRootImpl = new WeakReference<ViewRootImpl>(viewRootImpl); } - public void findAccessibilityNodeInfoByAccessibilityId(int accessibilityId, + public void findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId, int interactionId, IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, long interrogatingTid) { - ViewRootImpl viewRootImpl = mRootImpl.get(); - if (viewRootImpl != null) { + ViewRootImpl viewRootImpl = mViewRootImpl.get(); + if (viewRootImpl != null && viewRootImpl.mView != null) { viewRootImpl.getAccessibilityInteractionController() - .findAccessibilityNodeInfoByAccessibilityIdClientThread(accessibilityId, + .findAccessibilityNodeInfoByAccessibilityIdClientThread(accessibilityNodeId, interactionId, callback, interrogatingPid, interrogatingTid); } } - public void performAccessibilityAction(int accessibilityId, int action, + public void performAccessibilityAction(long accessibilityNodeId, int action, int interactionId, IAccessibilityInteractionConnectionCallback callback, int interogatingPid, long interrogatingTid) { - ViewRootImpl viewRootImpl = mRootImpl.get(); - if (viewRootImpl != null) { + ViewRootImpl viewRootImpl = mViewRootImpl.get(); + if (viewRootImpl != null && viewRootImpl.mView != null) { viewRootImpl.getAccessibilityInteractionController() - .performAccessibilityActionClientThread(accessibilityId, action, interactionId, - callback, interogatingPid, interrogatingTid); + .performAccessibilityActionClientThread(accessibilityNodeId, action, + interactionId, callback, interogatingPid, interrogatingTid); } } public void findAccessibilityNodeInfoByViewId(int viewId, int interactionId, IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, long interrogatingTid) { - ViewRootImpl viewRootImpl = mRootImpl.get(); - if (viewRootImpl != null) { + ViewRootImpl viewRootImpl = mViewRootImpl.get(); + if (viewRootImpl != null && viewRootImpl.mView != null) { viewRootImpl.getAccessibilityInteractionController() .findAccessibilityNodeInfoByViewIdClientThread(viewId, interactionId, callback, interrogatingPid, interrogatingTid); } } - public void findAccessibilityNodeInfosByViewText(String text, int accessibilityId, + public void findAccessibilityNodeInfosByText(String text, long accessibilityNodeId, int interactionId, IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, long interrogatingTid) { - ViewRootImpl viewRootImpl = mRootImpl.get(); - if (viewRootImpl != null) { + ViewRootImpl viewRootImpl = mViewRootImpl.get(); + if (viewRootImpl != null && viewRootImpl.mView != null) { viewRootImpl.getAccessibilityInteractionController() - .findAccessibilityNodeInfosByViewTextClientThread(text, accessibilityId, + .findAccessibilityNodeInfosByTextClientThread(text, accessibilityNodeId, interactionId, callback, interrogatingPid, interrogatingTid); } } @@ -4693,14 +4644,18 @@ public final class ViewRootImpl extends Handler implements ViewParent, } } - public void findAccessibilityNodeInfoByAccessibilityIdClientThread(int accessibilityId, - int interactionId, IAccessibilityInteractionConnectionCallback callback, - int interrogatingPid, long interrogatingTid) { + public void findAccessibilityNodeInfoByAccessibilityIdClientThread( + long accessibilityNodeId, int interactionId, + IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, + long interrogatingTid) { Message message = Message.obtain(); message.what = DO_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID; - message.arg1 = accessibilityId; - message.arg2 = interactionId; - message.obj = callback; + SomeArgs args = mPool.acquire(); + args.argi1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); + args.argi2 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId); + args.argi3 = interactionId; + args.arg1 = callback; + message.obj = args; // If the interrogation is performed by the same thread as the main UI // thread in this process, set the message as a static reference so // after this call completes the same thread but in the interrogating @@ -4708,23 +4663,31 @@ public final class ViewRootImpl extends Handler implements ViewParent, if (interrogatingPid == Process.myPid() && interrogatingTid == Looper.getMainLooper().getThread().getId()) { message.setTarget(ViewRootImpl.this); - AccessibilityInteractionClient.getInstance().setSameThreadMessage(message); + AccessibilityInteractionClient.getInstanceForThread( + interrogatingTid).setSameThreadMessage(message); } else { sendMessage(message); } } public void findAccessibilityNodeInfoByAccessibilityIdUiThread(Message message) { - final int accessibilityId = message.arg1; - final int interactionId = message.arg2; + SomeArgs args = (SomeArgs) message.obj; + final int accessibilityViewId = args.argi1; + final int virtualDescendantId = args.argi2; + final int interactionId = args.argi3; final IAccessibilityInteractionConnectionCallback callback = - (IAccessibilityInteractionConnectionCallback) message.obj; - + (IAccessibilityInteractionConnectionCallback) args.arg1; + mPool.release(args); AccessibilityNodeInfo info = null; try { - View target = findViewByAccessibilityId(accessibilityId); - if (target != null) { - info = target.createAccessibilityNodeInfo(); + View target = findViewByAccessibilityId(accessibilityViewId); + if (target != null && target.getVisibility() == View.VISIBLE) { + AccessibilityNodeProvider provider = target.getAccessibilityNodeProvider(); + if (provider != null) { + info = provider.createAccessibilityNodeInfo(virtualDescendantId); + } else if (virtualDescendantId == View.NO_ID) { + info = target.createAccessibilityNodeInfo(); + } } } finally { try { @@ -4750,7 +4713,8 @@ public final class ViewRootImpl extends Handler implements ViewParent, if (interrogatingPid == Process.myPid() && interrogatingTid == Looper.getMainLooper().getThread().getId()) { message.setTarget(ViewRootImpl.this); - AccessibilityInteractionClient.getInstance().setSameThreadMessage(message); + AccessibilityInteractionClient.getInstanceForThread( + interrogatingTid).setSameThreadMessage(message); } else { sendMessage(message); } @@ -4778,16 +4742,17 @@ public final class ViewRootImpl extends Handler implements ViewParent, } } - public void findAccessibilityNodeInfosByViewTextClientThread(String text, - int accessibilityViewId, int interactionId, + public void findAccessibilityNodeInfosByTextClientThread(String text, + long accessibilityNodeId, int interactionId, IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, long interrogatingTid) { Message message = Message.obtain(); - message.what = DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_TEXT; + message.what = DO_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT; SomeArgs args = mPool.acquire(); args.arg1 = text; - args.argi1 = accessibilityViewId; - args.argi2 = interactionId; + args.argi1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); + args.argi2 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId); + args.argi3 = interactionId; args.arg2 = callback; message.obj = args; // If the interrogation is performed by the same thread as the main UI @@ -4797,53 +4762,64 @@ public final class ViewRootImpl extends Handler implements ViewParent, if (interrogatingPid == Process.myPid() && interrogatingTid == Looper.getMainLooper().getThread().getId()) { message.setTarget(ViewRootImpl.this); - AccessibilityInteractionClient.getInstance().setSameThreadMessage(message); + AccessibilityInteractionClient.getInstanceForThread( + interrogatingTid).setSameThreadMessage(message); } else { sendMessage(message); } } - public void findAccessibilityNodeInfosByViewTextUiThread(Message message) { + public void findAccessibilityNodeInfosByTextUiThread(Message message) { SomeArgs args = (SomeArgs) message.obj; final String text = (String) args.arg1; final int accessibilityViewId = args.argi1; - final int interactionId = args.argi2; + final int virtualDescendantId = args.argi2; + final int interactionId = args.argi3; final IAccessibilityInteractionConnectionCallback callback = (IAccessibilityInteractionConnectionCallback) args.arg2; mPool.release(args); - List<AccessibilityNodeInfo> infos = null; try { - ArrayList<View> foundViews = mAttachInfo.mFocusablesTempList; - foundViews.clear(); - - View root = null; + View target = null; if (accessibilityViewId != View.NO_ID) { - root = findViewByAccessibilityId(accessibilityViewId); + target = findViewByAccessibilityId(accessibilityViewId); } else { - root = ViewRootImpl.this.mView; - } - - if (root == null || root.getVisibility() != View.VISIBLE) { - return; - } - - root.findViewsWithText(foundViews, text, View.FIND_VIEWS_WITH_TEXT - | View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION); - if (foundViews.isEmpty()) { - return; + target = ViewRootImpl.this.mView; } - - infos = mTempAccessibilityNodeInfoList; - infos.clear(); - - final int viewCount = foundViews.size(); - for (int i = 0; i < viewCount; i++) { - View foundView = foundViews.get(i); - if (foundView.getVisibility() == View.VISIBLE) { - infos.add(foundView.createAccessibilityNodeInfo()); + if (target != null && target.getVisibility() == View.VISIBLE) { + AccessibilityNodeProvider provider = target.getAccessibilityNodeProvider(); + if (provider != null) { + infos = provider.findAccessibilityNodeInfosByText(text, + virtualDescendantId); + } else if (virtualDescendantId == View.NO_ID) { + ArrayList<View> foundViews = mAttachInfo.mFocusablesTempList; + foundViews.clear(); + target.findViewsWithText(foundViews, text, View.FIND_VIEWS_WITH_TEXT + | View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION + | View.FIND_VIEWS_WITH_ACCESSIBILITY_NODE_PROVIDERS); + if (!foundViews.isEmpty()) { + infos = mTempAccessibilityNodeInfoList; + infos.clear(); + final int viewCount = foundViews.size(); + for (int i = 0; i < viewCount; i++) { + View foundView = foundViews.get(i); + if (foundView.getVisibility() == View.VISIBLE) { + provider = foundView.getAccessibilityNodeProvider(); + if (provider != null) { + List<AccessibilityNodeInfo> infosFromProvider = + provider.findAccessibilityNodeInfosByText(text, + virtualDescendantId); + if (infosFromProvider != null) { + infos.addAll(infosFromProvider); + } + } else { + infos.add(foundView.createAccessibilityNodeInfo()); + } + } + } + } } - } + } } finally { try { callback.setFindAccessibilityNodeInfosResult(infos, interactionId); @@ -4853,15 +4829,16 @@ public final class ViewRootImpl extends Handler implements ViewParent, } } - public void performAccessibilityActionClientThread(int accessibilityId, int action, + public void performAccessibilityActionClientThread(long accessibilityNodeId, int action, int interactionId, IAccessibilityInteractionConnectionCallback callback, int interogatingPid, long interrogatingTid) { Message message = Message.obtain(); message.what = DO_PERFORM_ACCESSIBILITY_ACTION; + message.arg1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); + message.arg2 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId); SomeArgs args = mPool.acquire(); - args.argi1 = accessibilityId; - args.argi2 = action; - args.argi3 = interactionId; + args.argi1 = action; + args.argi2 = interactionId; args.arg1 = callback; message.obj = args; // If the interrogation is performed by the same thread as the main UI @@ -4871,36 +4848,60 @@ public final class ViewRootImpl extends Handler implements ViewParent, if (interogatingPid == Process.myPid() && interrogatingTid == Looper.getMainLooper().getThread().getId()) { message.setTarget(ViewRootImpl.this); - AccessibilityInteractionClient.getInstance().setSameThreadMessage(message); + AccessibilityInteractionClient.getInstanceForThread( + interrogatingTid).setSameThreadMessage(message); } else { sendMessage(message); } } public void perfromAccessibilityActionUiThread(Message message) { + final int accessibilityViewId = message.arg1; + final int virtualDescendantId = message.arg2; SomeArgs args = (SomeArgs) message.obj; - final int accessibilityId = args.argi1; - final int action = args.argi2; - final int interactionId = args.argi3; + final int action = args.argi1; + final int interactionId = args.argi2; final IAccessibilityInteractionConnectionCallback callback = (IAccessibilityInteractionConnectionCallback) args.arg1; mPool.release(args); - boolean succeeded = false; try { - switch (action) { - case AccessibilityNodeInfo.ACTION_FOCUS: { - succeeded = performActionFocus(accessibilityId); - } break; - case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS: { - succeeded = performActionClearFocus(accessibilityId); - } break; - case AccessibilityNodeInfo.ACTION_SELECT: { - succeeded = performActionSelect(accessibilityId); - } break; - case AccessibilityNodeInfo.ACTION_CLEAR_SELECTION: { - succeeded = performActionClearSelection(accessibilityId); - } break; + View target = findViewByAccessibilityId(accessibilityViewId); + if (target != null && target.getVisibility() == View.VISIBLE) { + AccessibilityNodeProvider provider = target.getAccessibilityNodeProvider(); + if (provider != null) { + succeeded = provider.performAccessibilityAction(action, + virtualDescendantId); + } else if (virtualDescendantId == View.NO_ID) { + switch (action) { + case AccessibilityNodeInfo.ACTION_FOCUS: { + if (!target.hasFocus()) { + // Get out of touch mode since accessibility + // wants to move focus around. + ensureTouchMode(false); + succeeded = target.requestFocus(); + } + } break; + case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS: { + if (target.hasFocus()) { + target.clearFocus(); + succeeded = !target.isFocused(); + } + } break; + case AccessibilityNodeInfo.ACTION_SELECT: { + if (!target.isSelected()) { + target.setSelected(true); + succeeded = target.isSelected(); + } + } break; + case AccessibilityNodeInfo.ACTION_CLEAR_SELECTION: { + if (target.isSelected()) { + target.setSelected(false); + succeeded = !target.isSelected(); + } + } break; + } + } } } finally { try { @@ -4911,52 +4912,6 @@ public final class ViewRootImpl extends Handler implements ViewParent, } } - private boolean performActionFocus(int accessibilityId) { - View target = findViewByAccessibilityId(accessibilityId); - if (target == null || target.getVisibility() != View.VISIBLE) { - return false; - } - // Get out of touch mode since accessibility wants to move focus around. - ensureTouchMode(false); - return target.requestFocus(); - } - - private boolean performActionClearFocus(int accessibilityId) { - View target = findViewByAccessibilityId(accessibilityId); - if (target == null || target.getVisibility() != View.VISIBLE) { - return false; - } - if (!target.isFocused()) { - return false; - } - target.clearFocus(); - return !target.isFocused(); - } - - private boolean performActionSelect(int accessibilityId) { - View target = findViewByAccessibilityId(accessibilityId); - if (target == null || target.getVisibility() != View.VISIBLE) { - return false; - } - if (target.isSelected()) { - return false; - } - target.setSelected(true); - return target.isSelected(); - } - - private boolean performActionClearSelection(int accessibilityId) { - View target = findViewByAccessibilityId(accessibilityId); - if (target == null || target.getVisibility() != View.VISIBLE) { - return false; - } - if (!target.isSelected()) { - return false; - } - target.setSelected(false); - return !target.isSelected(); - } - private View findViewByAccessibilityId(int accessibilityId) { View root = ViewRootImpl.this.mView; if (root == null) { diff --git a/core/java/android/view/ViewTreeObserver.java b/core/java/android/view/ViewTreeObserver.java index db87175..c53fc6b 100644 --- a/core/java/android/view/ViewTreeObserver.java +++ b/core/java/android/view/ViewTreeObserver.java @@ -288,6 +288,14 @@ public final class ViewTreeObserver { } } + if (observer.mOnScrollChangedListeners != null) { + if (mOnScrollChangedListeners != null) { + mOnScrollChangedListeners.addAll(observer.mOnScrollChangedListeners); + } else { + mOnScrollChangedListeners = observer.mOnScrollChangedListeners; + } + } + observer.kill(); } diff --git a/core/java/android/view/VolumePanel.java b/core/java/android/view/VolumePanel.java index 48fe0df..24a3066 100644 --- a/core/java/android/view/VolumePanel.java +++ b/core/java/android/view/VolumePanel.java @@ -400,7 +400,7 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie if (LOGD) Log.d(TAG, "onVolumeChanged(streamType: " + streamType + ", flags: " + flags + ")"); if ((flags & AudioManager.FLAG_SHOW_UI) != 0) { - if (mActiveStreamType == -1) { + if (mActiveStreamType != streamType) { reorderSliders(streamType); } onShowVolumeChanged(streamType, flags); diff --git a/core/java/android/view/WindowManagerPolicy.java b/core/java/android/view/WindowManagerPolicy.java index 2e19bf6..7d729c6 100644 --- a/core/java/android/view/WindowManagerPolicy.java +++ b/core/java/android/view/WindowManagerPolicy.java @@ -340,7 +340,8 @@ public interface WindowManagerPolicy { * Add a fake window to the window manager. This window sits * at the top of the other windows and consumes events. */ - public FakeWindow addFakeWindow(Looper looper, InputHandler inputHandler, + public FakeWindow addFakeWindow(Looper looper, + InputEventReceiver.Factory inputEventReceiverFactory, String name, int windowType, int layoutParamsFlags, boolean canReceiveKeys, boolean hasFocus, boolean touchFullscreen); } diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java index 91dcac8..75b875a 100644 --- a/core/java/android/view/accessibility/AccessibilityEvent.java +++ b/core/java/android/view/accessibility/AccessibilityEvent.java @@ -844,7 +844,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par record.mParcelableData = parcel.readParcelable(null); parcel.readList(record.mText, null); record.mSourceWindowId = parcel.readInt(); - record.mSourceViewId = parcel.readInt(); + record.mSourceNodeId = parcel.readLong(); record.mSealed = (parcel.readInt() == 1); } @@ -893,7 +893,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par parcel.writeParcelable(record.mParcelableData, flags); parcel.writeList(record.mText); parcel.writeInt(record.mSourceWindowId); - parcel.writeInt(record.mSourceViewId); + parcel.writeLong(record.mSourceNodeId); parcel.writeInt(record.mSealed ? 1 : 0); } @@ -914,7 +914,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par if (DEBUG) { builder.append("\n"); builder.append("; sourceWindowId: ").append(mSourceWindowId); - builder.append("; sourceViewId: ").append(mSourceViewId); + builder.append("; mSourceNodeId: ").append(mSourceNodeId); for (int i = 0; i < mRecords.size(); i++) { AccessibilityRecord record = mRecords.get(i); builder.append(" Record "); diff --git a/core/java/android/view/accessibility/AccessibilityInteractionClient.java b/core/java/android/view/accessibility/AccessibilityInteractionClient.java index 96653e5..95c070c 100644 --- a/core/java/android/view/accessibility/AccessibilityInteractionClient.java +++ b/core/java/android/view/accessibility/AccessibilityInteractionClient.java @@ -22,6 +22,7 @@ import android.os.Message; import android.os.RemoteException; import android.os.SystemClock; import android.util.Log; +import android.util.LongSparseArray; import android.util.SparseArray; import java.util.Collections; @@ -73,7 +74,8 @@ public final class AccessibilityInteractionClient private static final Object sStaticLock = new Object(); - private static AccessibilityInteractionClient sInstance; + private static final LongSparseArray<AccessibilityInteractionClient> sClients = + new LongSparseArray<AccessibilityInteractionClient>(); private final AtomicInteger mInteractionIdCounter = new AtomicInteger(); @@ -96,17 +98,36 @@ public final class AccessibilityInteractionClient new SparseArray<IAccessibilityServiceConnection>(); /** - * @return The singleton of this class. + * @return The client for the current thread. */ public static AccessibilityInteractionClient getInstance() { + final long threadId = Thread.currentThread().getId(); + return getInstanceForThread(threadId); + } + + /** + * <strong>Note:</strong> We keep one instance per interrogating thread since + * the instance contains state which can lead to undesired thread interleavings. + * We do not have a thread local variable since other threads should be able to + * look up the correct client knowing a thread id. See ViewRootImpl for details. + * + * @return The client for a given <code>threadId</code>. + */ + public static AccessibilityInteractionClient getInstanceForThread(long threadId) { synchronized (sStaticLock) { - if (sInstance == null) { - sInstance = new AccessibilityInteractionClient(); + AccessibilityInteractionClient client = sClients.get(threadId); + if (client == null) { + client = new AccessibilityInteractionClient(); + sClients.put(threadId, client); } - return sInstance; + return client; } } + private AccessibilityInteractionClient() { + /* reducing constructor visibility */ + } + /** * Sets the message to be processed if the interacted view hierarchy * and the interacting client are running in the same thread. @@ -125,17 +146,18 @@ public final class AccessibilityInteractionClient * * @param connectionId The id of a connection for interacting with the system. * @param accessibilityWindowId A unique window id. - * @param accessibilityViewId A unique View accessibility id. + * @param accessibilityNodeId A unique node accessibility id + * (accessibility view and virtual descendant id). * @return An {@link AccessibilityNodeInfo} if found, null otherwise. */ public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId, - int accessibilityWindowId, int accessibilityViewId) { + int accessibilityWindowId, long accessibilityNodeId) { try { IAccessibilityServiceConnection connection = getConnection(connectionId); if (connection != null) { final int interactionId = mInteractionIdCounter.getAndIncrement(); final float windowScale = connection.findAccessibilityNodeInfoByAccessibilityId( - accessibilityWindowId, accessibilityViewId, interactionId, this, + accessibilityWindowId, accessibilityNodeId, interactionId, this, Thread.currentThread().getId()); // If the scale is zero the call has failed. if (windowScale > 0) { @@ -205,14 +227,14 @@ public final class AccessibilityInteractionClient * @param text The searched text. * @return A list of found {@link AccessibilityNodeInfo}s. */ - public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewTextInActiveWindow( + public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByTextInActiveWindow( int connectionId, String text) { try { IAccessibilityServiceConnection connection = getConnection(connectionId); if (connection != null) { final int interactionId = mInteractionIdCounter.getAndIncrement(); final float windowScale = - connection.findAccessibilityNodeInfosByViewTextInActiveWindow(text, + connection.findAccessibilityNodeInfosByTextInActiveWindow(text, interactionId, this, Thread.currentThread().getId()); // If the scale is zero the call has failed. if (windowScale > 0) { @@ -244,18 +266,18 @@ public final class AccessibilityInteractionClient * @param connectionId The id of a connection for interacting with the system. * @param text The searched text. * @param accessibilityWindowId A unique window id. - * @param accessibilityViewId A unique View accessibility id from where to start the search. - * Use {@link android.view.View#NO_ID} to start from the root. + * @param accessibilityNodeId A unique node id (accessibility and virtual descendant id) from + * where to start the search. Use {@link android.view.View#NO_ID} to start from the root. * @return A list of found {@link AccessibilityNodeInfo}s. */ - public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewText(int connectionId, - String text, int accessibilityWindowId, int accessibilityViewId) { + public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(int connectionId, + String text, int accessibilityWindowId, long accessibilityNodeId) { try { IAccessibilityServiceConnection connection = getConnection(connectionId); if (connection != null) { final int interactionId = mInteractionIdCounter.getAndIncrement(); - final float windowScale = connection.findAccessibilityNodeInfosByViewText(text, - accessibilityWindowId, accessibilityViewId, interactionId, this, + final float windowScale = connection.findAccessibilityNodeInfosByText(text, + accessibilityWindowId, accessibilityNodeId, interactionId, this, Thread.currentThread().getId()); // If the scale is zero the call has failed. if (windowScale > 0) { @@ -283,18 +305,18 @@ public final class AccessibilityInteractionClient * * @param connectionId The id of a connection for interacting with the system. * @param accessibilityWindowId The id of the window. - * @param accessibilityViewId A unique View accessibility id. + * @param accessibilityNodeId A unique node id (accessibility and virtual descendant id). * @param action The action to perform. * @return Whether the action was performed. */ public boolean performAccessibilityAction(int connectionId, int accessibilityWindowId, - int accessibilityViewId, int action) { + long accessibilityNodeId, int action) { try { IAccessibilityServiceConnection connection = getConnection(connectionId); if (connection != null) { final int interactionId = mInteractionIdCounter.getAndIncrement(); final boolean success = connection.performAccessibilityAction( - accessibilityWindowId, accessibilityViewId, action, interactionId, this, + accessibilityWindowId, accessibilityNodeId, action, interactionId, this, Thread.currentThread().getId()); if (success) { return getPerformAccessibilityActionResult(interactionId); diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java index 9b0f44a..6939c2c 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -20,7 +20,7 @@ import android.graphics.Rect; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; -import android.util.SparseIntArray; +import android.util.SparseLongArray; import android.view.View; import java.util.Collections; @@ -98,6 +98,59 @@ public class AccessibilityNodeInfo implements Parcelable { private static final int PROPERTY_SCROLLABLE = 0x00000200; + /** + * Bits that provide the id of a virtual descendant of a view. + */ + private static final long VIRTUAL_DESCENDANT_ID_MASK = 0xffffffff00000000L; + + /** + * Bit shift of {@link #VIRTUAL_DESCENDANT_ID_MASK} to get to the id for a + * virtual descendant of a view. Such a descendant does not exist in the view + * hierarchy and is only reported via the accessibility APIs. + */ + private static final int VIRTUAL_DESCENDANT_ID_SHIFT = 32; + + /** + * Gets the accessibility view id which identifies a View in the view three. + * + * @param accessibilityNodeId The id of an {@link AccessibilityNodeInfo}. + * @return The accessibility view id part of the node id. + * + * @hide + */ + public static int getAccessibilityViewId(long accessibilityNodeId) { + return (int) accessibilityNodeId; + } + + /** + * Gets the virtual descendant id which identifies an imaginary view in a + * containing View. + * + * @param accessibilityNodeId The id of an {@link AccessibilityNodeInfo}. + * @return The virtual view id part of the node id. + * + * @hide + */ + public static int getVirtualDescendantId(long accessibilityNodeId) { + return (int) ((accessibilityNodeId & VIRTUAL_DESCENDANT_ID_MASK) + >> VIRTUAL_DESCENDANT_ID_SHIFT); + } + + /** + * Makes a node id by shifting the <code>virtualDescendantId</code> + * by {@link #VIRTUAL_DESCENDANT_ID_SHIFT} and taking + * the bitwise or with the <code>accessibilityViewId</code>. + * + * @param accessibilityViewId A View accessibility id. + * @param virtualDescendantId A virtual descendant id. + * @return The node id. + * + * @hide + */ + public static long makeNodeId(int accessibilityViewId, int virtualDescendantId) { + return (((long) virtualDescendantId) << VIRTUAL_DESCENDANT_ID_SHIFT) | accessibilityViewId; + } + // Housekeeping. private static final int MAX_POOL_SIZE = 50; private static final Object sPoolLock = new Object(); @@ -108,9 +161,10 @@ public class AccessibilityNodeInfo implements Parcelable { private boolean mSealed; // Data. - private int mAccessibilityViewId = UNDEFINED; - private int mAccessibilityWindowId = UNDEFINED; - private int mParentAccessibilityViewId = UNDEFINED; + private int mWindowId = UNDEFINED; + private long mSourceNodeId = makeNodeId(UNDEFINED, UNDEFINED); + private long mParentNodeId = makeNodeId(UNDEFINED, UNDEFINED); + private int mBooleanProperties; private final Rect mBoundsInParent = new Rect(); private final Rect mBoundsInScreen = new Rect(); @@ -120,7 +174,7 @@ public class AccessibilityNodeInfo implements Parcelable { private CharSequence mText; private CharSequence mContentDescription; - private SparseIntArray mChildAccessibilityIds = new SparseIntArray(); + private SparseLongArray mChildIds = new SparseLongArray(); private int mActions; private int mConnectionId = UNDEFINED; @@ -134,13 +188,43 @@ public class AccessibilityNodeInfo implements Parcelable { /** * Sets the source. + * <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 source The info source. */ public void setSource(View source) { + setSource(source, UNDEFINED); + } + + /** + * Sets the source to be a virtual descendant of the given <code>root</code>. + * If <code>virtualDescendantId</code> is {@link View#NO_ID} the root + * is set as the source. + * <p> + * A virtual descendant is an imaginary View that is reported as a part of the view + * hierarchy for accessibility purposes. This enables custom views that draw complex + * content to report themselves as a tree of virtual views, thus conveying their + * logical structure. + * </p> + * <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 root The root of the virtual subtree. + * @param virtualDescendantId The id of the virtual descendant. + */ + public void setSource(View root, int virtualDescendantId) { enforceNotSealed(); - mAccessibilityViewId = source.getAccessibilityViewId(); - mAccessibilityWindowId = source.getAccessibilityWindowId(); + mWindowId = (root != null) ? root.getAccessibilityWindowId() : UNDEFINED; + final int rootAccessibilityViewId = + (root != null) ? root.getAccessibilityViewId() : UNDEFINED; + mSourceNodeId = makeNodeId(rootAccessibilityViewId, virtualDescendantId); } /** @@ -149,7 +233,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @return The window id. */ public int getWindowId() { - return mAccessibilityWindowId; + return mWindowId; } /** @@ -158,7 +242,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @return The child count. */ public int getChildCount() { - return mChildAccessibilityIds.size(); + return mChildIds.size(); } /** @@ -177,21 +261,20 @@ public class AccessibilityNodeInfo implements Parcelable { */ public AccessibilityNodeInfo getChild(int index) { enforceSealed(); - final int childAccessibilityViewId = mChildAccessibilityIds.get(index); - if (!canPerformRequestOverConnection(childAccessibilityViewId)) { + if (!canPerformRequestOverConnection(mSourceNodeId)) { return null; } + final long childId = mChildIds.get(index); AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); - return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId, - mAccessibilityWindowId, childAccessibilityViewId); + return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId, mWindowId, childId); } /** * Adds a child. * <p> - * <strong>Note:</strong> Cannot be called from an - * {@link android.accessibilityservice.AccessibilityService}. - * This class is made immutable before being delivered to an AccessibilityService. + * <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 child The child. @@ -199,10 +282,30 @@ public class AccessibilityNodeInfo implements Parcelable { * @throws IllegalStateException If called from an AccessibilityService. */ public void addChild(View child) { + addChild(child, UNDEFINED); + } + + /** + * Adds a virtual child which is a descendant of the given <code>root</code>. + * If <code>virtualDescendantId</code> is {@link View#NO_ID} the root + * is added as a child. + * <p> + * A virtual descendant is an imaginary View that is reported as a part of the view + * hierarchy for accessibility purposes. This enables custom views that draw complex + * content to report them selves as a tree of virtual views, thus conveying their + * logical structure. + * </p> + * + * @param root The root of the virtual subtree. + * @param virtualDescendantId The id of the virtual child. + */ + public void addChild(View root, int virtualDescendantId) { enforceNotSealed(); - final int childAccessibilityViewId = child.getAccessibilityViewId(); - final int index = mChildAccessibilityIds.size(); - mChildAccessibilityIds.put(index, childAccessibilityViewId); + final int index = mChildIds.size(); + final int rootAccessibilityViewId = + (root != null) ? root.getAccessibilityViewId() : UNDEFINED; + final long childNodeId = makeNodeId(rootAccessibilityViewId, virtualDescendantId); + mChildIds.put(index, childNodeId); } /** @@ -250,12 +353,11 @@ public class AccessibilityNodeInfo implements Parcelable { */ public boolean performAction(int action) { enforceSealed(); - if (!canPerformRequestOverConnection(mAccessibilityViewId)) { + if (!canPerformRequestOverConnection(mSourceNodeId)) { return false; } AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); - return client.performAccessibilityAction(mConnectionId, mAccessibilityWindowId, - mAccessibilityViewId, action); + return client.performAccessibilityAction(mConnectionId, mWindowId, mSourceNodeId, action); } /** @@ -274,12 +376,12 @@ public class AccessibilityNodeInfo implements Parcelable { */ public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String text) { enforceSealed(); - if (!canPerformRequestOverConnection(mAccessibilityViewId)) { + if (!canPerformRequestOverConnection(mSourceNodeId)) { return Collections.emptyList(); } AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); - return client.findAccessibilityNodeInfosByViewText(mConnectionId, text, - mAccessibilityWindowId, mAccessibilityViewId); + return client.findAccessibilityNodeInfosByText(mConnectionId, text, mWindowId, + mSourceNodeId); } /** @@ -294,12 +396,12 @@ public class AccessibilityNodeInfo implements Parcelable { */ public AccessibilityNodeInfo getParent() { enforceSealed(); - if (!canPerformRequestOverConnection(mParentAccessibilityViewId)) { + if (!canPerformRequestOverConnection(mParentNodeId)) { return null; } AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId, - mAccessibilityWindowId, mParentAccessibilityViewId); + mWindowId, mParentNodeId); } /** @@ -315,8 +417,33 @@ public class AccessibilityNodeInfo implements Parcelable { * @throws IllegalStateException If called from an AccessibilityService. */ public void setParent(View parent) { + setParent(parent, UNDEFINED); + } + + /** + * Sets the parent to be a virtual descendant of the given <code>root</code>. + * If <code>virtualDescendantId</code> equals to {@link View#NO_ID} the root + * is set as the parent. + * <p> + * A virtual descendant is an imaginary View that is reported as a part of the view + * hierarchy for accessibility purposes. This enables custom views that draw complex + * content to report them selves as a tree of virtual views, thus conveying their + * logical structure. + * </p> + * <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 root The root of the virtual subtree. + * @param virtualDescendantId The id of the virtual descendant. + */ + public void setParent(View root, int virtualDescendantId) { enforceNotSealed(); - mParentAccessibilityViewId = parent.getAccessibilityViewId(); + final int rootAccessibilityViewId = + (root != null) ? root.getAccessibilityViewId() : UNDEFINED; + mParentNodeId = makeNodeId(rootAccessibilityViewId, virtualDescendantId); } /** @@ -829,6 +956,7 @@ public class AccessibilityNodeInfo implements Parcelable { * Returns a cached instance if such is available otherwise a new one * and sets the source. * + * @param source The source view. * @return An instance. * * @see #setSource(View) @@ -840,6 +968,22 @@ public class AccessibilityNodeInfo implements Parcelable { } /** + * Returns a cached instance if such is available otherwise a new one + * and sets the source. + * + * @param root The root of the virtual subtree. + * @param virtualDescendantId The id of the virtual descendant. + * @return An instance. + * + * @see #setSource(View, int) + */ + public static AccessibilityNodeInfo obtain(View root, int virtualDescendantId) { + AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(); + info.setSource(root, virtualDescendantId); + return info; + } + + /** * Returns a cached instance if such is available otherwise a new one. * * @return An instance. @@ -903,16 +1047,16 @@ public class AccessibilityNodeInfo implements Parcelable { */ public void writeToParcel(Parcel parcel, int flags) { parcel.writeInt(isSealed() ? 1 : 0); - parcel.writeInt(mAccessibilityViewId); - parcel.writeInt(mAccessibilityWindowId); - parcel.writeInt(mParentAccessibilityViewId); + parcel.writeLong(mSourceNodeId); + parcel.writeInt(mWindowId); + parcel.writeLong(mParentNodeId); parcel.writeInt(mConnectionId); - SparseIntArray childIds = mChildAccessibilityIds; + SparseLongArray childIds = mChildIds; final int childIdsSize = childIds.size(); parcel.writeInt(childIdsSize); for (int i = 0; i < childIdsSize; i++) { - parcel.writeInt(childIds.valueAt(i)); + parcel.writeLong(childIds.valueAt(i)); } parcel.writeInt(mBoundsInParent.top); @@ -946,9 +1090,9 @@ public class AccessibilityNodeInfo implements Parcelable { */ private void init(AccessibilityNodeInfo other) { mSealed = other.mSealed; - mAccessibilityViewId = other.mAccessibilityViewId; - mParentAccessibilityViewId = other.mParentAccessibilityViewId; - mAccessibilityWindowId = other.mAccessibilityWindowId; + mSourceNodeId = other.mSourceNodeId; + mParentNodeId = other.mParentNodeId; + mWindowId = other.mWindowId; mConnectionId = other.mConnectionId; mBoundsInParent.set(other.mBoundsInParent); mBoundsInScreen.set(other.mBoundsInScreen); @@ -958,7 +1102,7 @@ public class AccessibilityNodeInfo implements Parcelable { mContentDescription = other.mContentDescription; mActions= other.mActions; mBooleanProperties = other.mBooleanProperties; - mChildAccessibilityIds = other.mChildAccessibilityIds.clone(); + mChildIds = other.mChildIds.clone(); } /** @@ -968,15 +1112,15 @@ public class AccessibilityNodeInfo implements Parcelable { */ private void initFromParcel(Parcel parcel) { mSealed = (parcel.readInt() == 1); - mAccessibilityViewId = parcel.readInt(); - mAccessibilityWindowId = parcel.readInt(); - mParentAccessibilityViewId = parcel.readInt(); + mSourceNodeId = parcel.readLong(); + mWindowId = parcel.readInt(); + mParentNodeId = parcel.readLong(); mConnectionId = parcel.readInt(); - SparseIntArray childIds = mChildAccessibilityIds; + SparseLongArray childIds = mChildIds; final int childrenSize = parcel.readInt(); for (int i = 0; i < childrenSize; i++) { - final int childId = parcel.readInt(); + final long childId = parcel.readLong(); childIds.put(i, childId); } @@ -1005,11 +1149,11 @@ public class AccessibilityNodeInfo implements Parcelable { */ private void clear() { mSealed = false; - mAccessibilityViewId = UNDEFINED; - mParentAccessibilityViewId = UNDEFINED; - mAccessibilityWindowId = UNDEFINED; + mSourceNodeId = makeNodeId(UNDEFINED, UNDEFINED); + mParentNodeId = makeNodeId(UNDEFINED, UNDEFINED); + mWindowId = UNDEFINED; mConnectionId = UNDEFINED; - mChildAccessibilityIds.clear(); + mChildIds.clear(); mBoundsInParent.set(0, 0, 0, 0); mBoundsInScreen.set(0, 0, 0, 0); mBooleanProperties = 0; @@ -1041,9 +1185,10 @@ public class AccessibilityNodeInfo implements Parcelable { } } - private boolean canPerformRequestOverConnection(int accessibilityViewId) { - return (mConnectionId != UNDEFINED && mAccessibilityWindowId != UNDEFINED - && accessibilityViewId != UNDEFINED); + private boolean canPerformRequestOverConnection(long accessibilityNodeId) { + return (mWindowId != UNDEFINED + && getAccessibilityViewId(accessibilityNodeId) != UNDEFINED + && mConnectionId != UNDEFINED); } @Override @@ -1058,10 +1203,10 @@ public class AccessibilityNodeInfo implements Parcelable { return false; } AccessibilityNodeInfo other = (AccessibilityNodeInfo) object; - if (mAccessibilityViewId != other.mAccessibilityViewId) { + if (mSourceNodeId != other.mSourceNodeId) { return false; } - if (mAccessibilityWindowId != other.mAccessibilityWindowId) { + if (mWindowId != other.mWindowId) { return false; } return true; @@ -1071,8 +1216,9 @@ public class AccessibilityNodeInfo implements Parcelable { public int hashCode() { final int prime = 31; int result = 1; - result = prime * result + mAccessibilityViewId; - result = prime * result + mAccessibilityWindowId; + result = prime * result + getAccessibilityViewId(mSourceNodeId); + result = prime * result + getVirtualDescendantId(mSourceNodeId); + result = prime * result + mWindowId; return result; } @@ -1082,9 +1228,10 @@ public class AccessibilityNodeInfo implements Parcelable { builder.append(super.toString()); if (DEBUG) { - builder.append("; accessibilityId: " + mAccessibilityViewId); - builder.append("; parentAccessibilityId: " + mParentAccessibilityViewId); - SparseIntArray childIds = mChildAccessibilityIds; + builder.append("; accessibilityViewId: " + getAccessibilityViewId(mSourceNodeId)); + builder.append("; virtualDescendantId: " + getVirtualDescendantId(mSourceNodeId)); + builder.append("; mParentNodeId: " + mParentNodeId); + SparseLongArray childIds = mChildIds; builder.append("; childAccessibilityIds: ["); for (int i = 0, count = childIds.size(); i < count; i++) { builder.append(childIds.valueAt(i)); diff --git a/core/java/android/view/accessibility/AccessibilityNodeProvider.java b/core/java/android/view/accessibility/AccessibilityNodeProvider.java new file mode 100644 index 0000000..5890417 --- /dev/null +++ b/core/java/android/view/accessibility/AccessibilityNodeProvider.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.accessibility; + +import android.accessibilityservice.AccessibilityService; +import android.view.View; + +import java.util.List; + +/** + * This class is the contract a client should implement to enable support of a + * virtual view hierarchy rooted at a given view for accessibility purposes. A virtual + * view hierarchy is a tree of imaginary Views that is reported as a part of the view + * hierarchy when an {@link AccessibilityService} explores the window content. + * Since the virtual View tree does not exist this class is responsible for + * managing the {@link AccessibilityNodeInfo}s describing that tree to accessibility + * services. + * </p> + * <p> + * The main use case of these APIs is to enable a custom view that draws complex content, + * for example a monthly calendar grid, to be presented as a tree of logical nodes, + * for example month days each containing events, thus conveying its logical structure. + * <p> + * <p> + * A typical use case is to override {@link View#getAccessibilityNodeProvider()} of the + * View that is a root of a virtual View hierarchy to return an instance of this class. + * In such a case this instance is responsible for managing {@link AccessibilityNodeInfo}s + * describing the virtual sub-tree rooted at the View including the one representing the + * View itself. Similarly the returned instance is responsible for performing accessibility + * actions on any virtual view or the root view itself. For example: + * </p> + * <pre> + * getAccessibilityNodeProvider( + * if (mAccessibilityNodeProvider == null) { + * mAccessibilityNodeProvider = new AccessibilityNodeProvider() { + * public boolean performAccessibilityAction(int action, int virtualDescendantId) { + * // Implementation. + * return false; + * } + * + * public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String text, int virtualDescendantId) { + * // Implementation. + * return null; + * } + * + * public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualDescendantId) { + * // Implementation. + * return null; + * } + * }); + * return mAccessibilityNodeProvider; + * </pre> + */ +public abstract class AccessibilityNodeProvider { + + /** + * Returns an {@link AccessibilityNodeInfo} representing a virtual view, + * i.e. a descendant of the host View, with the given <code>virtualViewId</code> + * or the host View itself if <code>virtualViewId</code> equals to {@link View#NO_ID}. + * <p> + * A virtual descendant is an imaginary View that is reported as a part of the view + * hierarchy for accessibility purposes. This enables custom views that draw complex + * content to report them selves as a tree of virtual views, thus conveying their + * logical structure. + * </p> + * <p> + * The implementer is responsible for obtaining an accessibility node info from the + * pool of reusable instances and setting the desired properties of the node info + * before returning it. + * </p> + * + * @param virtualViewId A client defined virtual view id. + * @return A populated {@link AccessibilityNodeInfo} for a virtual descendant or the + * host View. + * + * @see AccessibilityNodeInfo + */ + public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { + return null; + } + + /** + * Performs an accessibility action on a virtual view, i.e. a descendant of the + * host View, with the given <code>virtualViewId</code> or the host View itself + * if <code>virtualViewId</code> equals to {@link View#NO_ID}. + * + * @param action The action to perform. + * @param virtualViewId A client defined virtual view id. + * @return True if the action was performed. + * + * @see #createAccessibilityNodeInfo(int) + * @see AccessibilityNodeInfo + */ + public boolean performAccessibilityAction(int action, int virtualViewId) { + return false; + } + + /** + * Finds {@link AccessibilityNodeInfo}s by text. The match is case insensitive + * containment. The search is relative to the virtual view, i.e. a descendant of the + * host View, with the given <code>virtualViewId</code> or the host View itself + * <code>virtualViewId</code> equals to {@link View#NO_ID}. + * + * @param virtualViewId A client defined virtual view id which defined + * the root of the tree in which to perform the search. + * @param text The searched text. + * @return A list of node info. + * + * @see #createAccessibilityNodeInfo(int) + * @see AccessibilityNodeInfo + */ + public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String text, + int virtualViewId) { + return null; + } +} diff --git a/core/java/android/view/accessibility/AccessibilityRecord.java b/core/java/android/view/accessibility/AccessibilityRecord.java index 18d0f6f..07aeb9a 100644 --- a/core/java/android/view/accessibility/AccessibilityRecord.java +++ b/core/java/android/view/accessibility/AccessibilityRecord.java @@ -77,7 +77,7 @@ public class AccessibilityRecord { int mAddedCount= UNDEFINED; int mRemovedCount = UNDEFINED; - int mSourceViewId = UNDEFINED; + long mSourceNodeId = AccessibilityNodeInfo.makeNodeId(UNDEFINED, UNDEFINED); int mSourceWindowId = UNDEFINED; CharSequence mClassName; @@ -103,14 +103,28 @@ public class AccessibilityRecord { * @throws IllegalStateException If called from an AccessibilityService. */ public void setSource(View source) { + setSource(source, UNDEFINED); + } + + /** + * Sets the source to be a virtual descendant of the given <code>root</code>. + * If <code>virtualDescendantId</code> equals to {@link View#NO_ID} the root + * is set as the source. + * <p> + * A virtual descendant is an imaginary View that is reported as a part of the view + * hierarchy for accessibility purposes. This enables custom views that draw complex + * content to report them selves as a tree of virtual views, thus conveying their + * logical structure. + * </p> + * + * @param root The root of the virtual subtree. + * @param virtualDescendantId The id of the virtual descendant. + */ + public void setSource(View root, int virtualDescendantId) { enforceNotSealed(); - if (source != null) { - mSourceWindowId = source.getAccessibilityWindowId(); - mSourceViewId = source.getAccessibilityViewId(); - } else { - mSourceWindowId = UNDEFINED; - mSourceViewId = UNDEFINED; - } + mSourceWindowId = (root != null) ? root.getAccessibilityWindowId() : UNDEFINED; + final int rootViewId = (root != null) ? root.getAccessibilityViewId() : UNDEFINED; + mSourceNodeId = AccessibilityNodeInfo.makeNodeId(rootViewId, virtualDescendantId); } /** @@ -125,12 +139,12 @@ public class AccessibilityRecord { public AccessibilityNodeInfo getSource() { enforceSealed(); if (mConnectionId == UNDEFINED || mSourceWindowId == UNDEFINED - || mSourceViewId == UNDEFINED) { + || AccessibilityNodeInfo.getAccessibilityViewId(mSourceNodeId) == UNDEFINED) { return null; } AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId, mSourceWindowId, - mSourceViewId); + mSourceNodeId); } /** @@ -383,6 +397,7 @@ public class AccessibilityRecord { public int getMaxScrollX() { return mMaxScrollX; } + /** * Sets the max scroll offset of the source left edge in pixels. * @@ -708,7 +723,7 @@ public class AccessibilityRecord { mParcelableData = record.mParcelableData; mText.addAll(record.mText); mSourceWindowId = record.mSourceWindowId; - mSourceViewId = record.mSourceViewId; + mSourceNodeId = record.mSourceNodeId; mConnectionId = record.mConnectionId; } @@ -733,7 +748,7 @@ public class AccessibilityRecord { mBeforeText = null; mParcelableData = null; mText.clear(); - mSourceViewId = UNDEFINED; + mSourceNodeId = AccessibilityNodeInfo.makeNodeId(UNDEFINED, UNDEFINED); mSourceWindowId = UNDEFINED; mConnectionId = UNDEFINED; } diff --git a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl index 535d594..a90c427 100644 --- a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl +++ b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl @@ -27,7 +27,7 @@ import android.view.accessibility.IAccessibilityInteractionConnectionCallback; */ oneway interface IAccessibilityInteractionConnection { - void findAccessibilityNodeInfoByAccessibilityId(int accessibilityViewId, int interactionId, + void findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId, int interactionId, IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, long interrogatingTid); @@ -35,11 +35,11 @@ oneway interface IAccessibilityInteractionConnection { IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, long interrogatingTid); - void findAccessibilityNodeInfosByViewText(String text, int accessibilityViewId, + void findAccessibilityNodeInfosByText(String text, long accessibilityNodeId, int interactionId, IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, long interrogatingTid); - void performAccessibilityAction(int accessibilityId, int action, int interactionId, + void performAccessibilityAction(long accessibilityNodeId, int action, int interactionId, IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, long interrogatingTid); } diff --git a/core/java/android/view/textservice/SpellCheckerSession.java b/core/java/android/view/textservice/SpellCheckerSession.java index 489587e..1d66cbe 100644 --- a/core/java/android/view/textservice/SpellCheckerSession.java +++ b/core/java/android/view/textservice/SpellCheckerSession.java @@ -88,14 +88,17 @@ public class SpellCheckerSession { * This meta-data must reference an XML resource. **/ public static final String SERVICE_META_DATA = "android.view.textservice.scs"; + private static final String SUPPORT_SENTENCE_SPELL_CHECK = "SupportSentenceSpellCheck"; private static final int MSG_ON_GET_SUGGESTION_MULTIPLE = 1; + private static final int MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE = 2; private final InternalListener mInternalListener; private final ITextServicesManager mTextServicesManager; private final SpellCheckerInfo mSpellCheckerInfo; private final SpellCheckerSessionListenerImpl mSpellCheckerSessionListenerImpl; + private final SpellCheckerSubtype mSubtype; private boolean mIsUsed; private SpellCheckerSessionListener mSpellCheckerSessionListener; @@ -108,6 +111,9 @@ public class SpellCheckerSession { case MSG_ON_GET_SUGGESTION_MULTIPLE: handleOnGetSuggestionsMultiple((SuggestionsInfo[]) msg.obj); break; + case MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE: + handleOnGetSuggestionsMultipleForSentence((SuggestionsInfo[]) msg.obj); + break; } } }; @@ -117,7 +123,8 @@ public class SpellCheckerSession { * @hide */ public SpellCheckerSession( - SpellCheckerInfo info, ITextServicesManager tsm, SpellCheckerSessionListener listener) { + SpellCheckerInfo info, ITextServicesManager tsm, SpellCheckerSessionListener listener, + SpellCheckerSubtype subtype) { if (info == null || listener == null || tsm == null) { throw new NullPointerException(); } @@ -127,6 +134,7 @@ public class SpellCheckerSession { mTextServicesManager = tsm; mIsUsed = true; mSpellCheckerSessionListener = listener; + mSubtype = subtype; } /** @@ -167,6 +175,14 @@ public class SpellCheckerSession { } /** + * @hide + */ + public void getSuggestionsForSentence(TextInfo textInfo, int suggestionsLimit) { + mSpellCheckerSessionListenerImpl.getSuggestionsMultipleForSentence( + new TextInfo[] {textInfo}, suggestionsLimit); + } + + /** * Get candidate strings for a substring of the specified text. * @param textInfo text metadata for a spell checker * @param suggestionsLimit the number of limit of suggestions returned @@ -195,10 +211,15 @@ public class SpellCheckerSession { mSpellCheckerSessionListener.onGetSuggestions(suggestionInfos); } + private void handleOnGetSuggestionsMultipleForSentence(SuggestionsInfo[] suggestionInfos) { + mSpellCheckerSessionListener.onGetSuggestionsForSentence(suggestionInfos); + } + private static class SpellCheckerSessionListenerImpl extends ISpellCheckerSessionListener.Stub { private static final int TASK_CANCEL = 1; private static final int TASK_GET_SUGGESTIONS_MULTIPLE = 2; private static final int TASK_CLOSE = 3; + private static final int TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE = 4; private final Queue<SpellCheckerParams> mPendingTasks = new LinkedList<SpellCheckerParams>(); private Handler mHandler; @@ -236,6 +257,9 @@ public class SpellCheckerSession { case TASK_CLOSE: processClose(); break; + case TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE: + processGetSuggestionsMultipleForSentence(scp); + break; } } @@ -266,6 +290,15 @@ public class SpellCheckerSession { suggestionsLimit, sequentialWords)); } + public void getSuggestionsMultipleForSentence(TextInfo[] textInfos, int suggestionsLimit) { + if (DBG) { + Log.w(TAG, "getSuggestionsMultipleForSentence"); + } + processOrEnqueueTask( + new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE, + textInfos, suggestionsLimit, false)); + } + public void close() { if (DBG) { Log.w(TAG, "close"); @@ -355,10 +388,34 @@ public class SpellCheckerSession { } } + private void processGetSuggestionsMultipleForSentence(SpellCheckerParams scp) { + if (!checkOpenConnection()) { + return; + } + if (DBG) { + Log.w(TAG, "Get suggestions from the spell checker."); + } + if (scp.mTextInfos.length != 1) { + throw new IllegalArgumentException(); + } + try { + mISpellCheckerSession.onGetSuggestionsMultipleForSentence( + scp.mTextInfos, scp.mSuggestionsLimit); + } catch (RemoteException e) { + Log.e(TAG, "Failed to get suggestions " + e); + } + } + @Override public void onGetSuggestions(SuggestionsInfo[] results) { mHandler.sendMessage(Message.obtain(mHandler, MSG_ON_GET_SUGGESTION_MULTIPLE, results)); } + + @Override + public void onGetSuggestionsForSentence(SuggestionsInfo[] results) { + mHandler.sendMessage( + Message.obtain(mHandler, MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE, results)); + } } /** @@ -370,6 +427,10 @@ public class SpellCheckerSession { * @param results an array of results of getSuggestions */ public void onGetSuggestions(SuggestionsInfo[] results); + /** + * @hide + */ + public void onGetSuggestionsForSentence(SuggestionsInfo[] results); } private static class InternalListener extends ITextServicesSessionListener.Stub { @@ -411,4 +472,11 @@ public class SpellCheckerSession { public ISpellCheckerSessionListener getSpellCheckerSessionListener() { return mSpellCheckerSessionListenerImpl; } + + /** + * @hide + */ + public boolean isSentenceSpellCheckSupported() { + return mSubtype.containsExtraValueKey(SUPPORT_SENTENCE_SPELL_CHECK); + } } diff --git a/core/java/android/view/textservice/SpellCheckerSubtype.java b/core/java/android/view/textservice/SpellCheckerSubtype.java index aeb3ba6..1bbaf6c 100644 --- a/core/java/android/view/textservice/SpellCheckerSubtype.java +++ b/core/java/android/view/textservice/SpellCheckerSubtype.java @@ -21,9 +21,11 @@ import android.content.pm.ApplicationInfo; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; +import android.util.Slog; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; @@ -33,11 +35,15 @@ import java.util.Locale; * Subtype can describe locale (e.g. en_US, fr_FR...) used for settings. */ public final class SpellCheckerSubtype implements Parcelable { + private static final String TAG = SpellCheckerSubtype.class.getSimpleName(); + private static final String EXTRA_VALUE_PAIR_SEPARATOR = ","; + private static final String EXTRA_VALUE_KEY_VALUE_SEPARATOR = "="; private final int mSubtypeHashCode; private final int mSubtypeNameResId; private final String mSubtypeLocale; private final String mSubtypeExtraValue; + private HashMap<String, String> mExtraValueHashMapCache; /** * Constructor @@ -83,6 +89,48 @@ public final class SpellCheckerSubtype implements Parcelable { return mSubtypeExtraValue; } + private HashMap<String, String> getExtraValueHashMap() { + if (mExtraValueHashMapCache == null) { + mExtraValueHashMapCache = new HashMap<String, String>(); + final String[] pairs = mSubtypeExtraValue.split(EXTRA_VALUE_PAIR_SEPARATOR); + final int N = pairs.length; + for (int i = 0; i < N; ++i) { + final String[] pair = pairs[i].split(EXTRA_VALUE_KEY_VALUE_SEPARATOR); + if (pair.length == 1) { + mExtraValueHashMapCache.put(pair[0], null); + } else if (pair.length > 1) { + if (pair.length > 2) { + Slog.w(TAG, "ExtraValue has two or more '='s"); + } + mExtraValueHashMapCache.put(pair[0], pair[1]); + } + } + } + return mExtraValueHashMapCache; + } + + /** + * @hide + * The string of ExtraValue in subtype should be defined as follows: + * example: key0,key1=value1,key2,key3,key4=value4 + * @param key the key of extra value + * @return the subtype contains specified the extra value + */ + public boolean containsExtraValueKey(String key) { + return getExtraValueHashMap().containsKey(key); + } + + /** + * @hide + * The string of ExtraValue in subtype should be defined as follows: + * example: key0,key1=value1,key2,key3,key4=value4 + * @param key the key of extra value + * @return the value of the specified key + */ + public String getExtraValueOf(String key) { + return getExtraValueHashMap().get(key); + } + @Override public int hashCode() { return mSubtypeHashCode; diff --git a/core/java/android/view/textservice/SuggestionsInfo.java b/core/java/android/view/textservice/SuggestionsInfo.java index ddd0361..9b99770 100644 --- a/core/java/android/view/textservice/SuggestionsInfo.java +++ b/core/java/android/view/textservice/SuggestionsInfo.java @@ -21,11 +21,14 @@ import com.android.internal.util.ArrayUtils; import android.os.Parcel; import android.os.Parcelable; +import java.util.Arrays; + /** * This class contains a metadata of suggestions from the text service */ public final class SuggestionsInfo implements Parcelable { private static final String[] EMPTY = ArrayUtils.emptyArray(String.class); + private static final int NOT_A_LENGTH = -1; /** * Flag of the attributes of the suggestions that can be obtained by @@ -47,6 +50,8 @@ public final class SuggestionsInfo implements Parcelable { public static final int RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS = 0x0004; private final int mSuggestionsAttributes; private final String[] mSuggestions; + private final int[] mStartPosArray; + private final int[] mLengthArray; private final boolean mSuggestionsAvailable; private int mCookie; private int mSequence; @@ -57,16 +62,7 @@ public final class SuggestionsInfo implements Parcelable { * @param suggestions from the text service */ public SuggestionsInfo(int suggestionsAttributes, String[] suggestions) { - mSuggestionsAttributes = suggestionsAttributes; - if (suggestions == null) { - mSuggestions = EMPTY; - mSuggestionsAvailable = false; - } else { - mSuggestions = suggestions; - mSuggestionsAvailable = true; - } - mCookie = 0; - mSequence = 0; + this(suggestionsAttributes, suggestions, 0, 0); } /** @@ -78,12 +74,46 @@ public final class SuggestionsInfo implements Parcelable { */ public SuggestionsInfo( int suggestionsAttributes, String[] suggestions, int cookie, int sequence) { + this(suggestionsAttributes, suggestions, cookie, sequence, null, null); + } + + /** + * @hide + * Constructor. + * @param suggestionsAttributes from the text service + * @param suggestions from the text service + * @param cookie the cookie of the input TextInfo + * @param sequence the cookie of the input TextInfo + * @param startPosArray the array of start positions of suggestions + * @param lengthArray the array of length of suggestions + */ + public SuggestionsInfo( + int suggestionsAttributes, String[] suggestions, int cookie, int sequence, + int[] startPosArray, int[] lengthArray) { + final int suggestsLen; if (suggestions == null) { mSuggestions = EMPTY; mSuggestionsAvailable = false; + suggestsLen = 0; + mStartPosArray = new int[0]; + mLengthArray = new int[0]; } else { mSuggestions = suggestions; mSuggestionsAvailable = true; + suggestsLen = suggestions.length; + if (startPosArray == null || lengthArray == null) { + mStartPosArray = new int[suggestsLen]; + mLengthArray = new int[suggestsLen]; + for (int i = 0; i < suggestsLen; ++i) { + mStartPosArray[i] = 0; + mLengthArray[i] = NOT_A_LENGTH; + } + } else if (suggestsLen != startPosArray.length || suggestsLen != lengthArray.length) { + throw new IllegalArgumentException(); + } else { + mStartPosArray = Arrays.copyOf(startPosArray, suggestsLen); + mLengthArray = Arrays.copyOf(lengthArray, suggestsLen); + } } mSuggestionsAttributes = suggestionsAttributes; mCookie = cookie; @@ -96,6 +126,10 @@ public final class SuggestionsInfo implements Parcelable { mCookie = source.readInt(); mSequence = source.readInt(); mSuggestionsAvailable = source.readInt() == 1; + mStartPosArray = new int[mSuggestions.length]; + mLengthArray = new int[mSuggestions.length]; + source.readIntArray(mStartPosArray); + source.readIntArray(mLengthArray); } /** @@ -111,6 +145,8 @@ public final class SuggestionsInfo implements Parcelable { dest.writeInt(mCookie); dest.writeInt(mSequence); dest.writeInt(mSuggestionsAvailable ? 1 : 0); + dest.writeIntArray(mStartPosArray); + dest.writeIntArray(mLengthArray); } /** @@ -191,4 +227,24 @@ public final class SuggestionsInfo implements Parcelable { public int describeContents() { return 0; } + + /** + * @hide + */ + public int getSuggestionStartPosAt(int i) { + if (i >= 0 && i < mStartPosArray.length) { + return mStartPosArray[i]; + } + return -1; + } + + /** + * @hide + */ + public int getSuggestionLengthAt(int i) { + if (i >= 0 && i < mLengthArray.length) { + return mLengthArray[i]; + } + return -1; + } } diff --git a/core/java/android/view/textservice/TextServicesManager.java b/core/java/android/view/textservice/TextServicesManager.java index 69f88a5..fc59e6e 100644 --- a/core/java/android/view/textservice/TextServicesManager.java +++ b/core/java/android/view/textservice/TextServicesManager.java @@ -157,7 +157,8 @@ public final class TextServicesManager { if (subtypeInUse == null) { return null; } - final SpellCheckerSession session = new SpellCheckerSession(sci, sService, listener); + final SpellCheckerSession session = new SpellCheckerSession( + sci, sService, listener, subtypeInUse); try { sService.getSpellCheckerService(sci.getId(), subtypeInUse.getLocale(), session.getTextServicesSessionListener(), diff --git a/core/java/android/webkit/BrowserFrame.java b/core/java/android/webkit/BrowserFrame.java index c194559..d8f08b2 100644 --- a/core/java/android/webkit/BrowserFrame.java +++ b/core/java/android/webkit/BrowserFrame.java @@ -410,6 +410,7 @@ class BrowserFrame extends Handler { mCommitted = false; // remove pending draw to block update until mFirstLayoutDone is // set to true in didFirstLayout() + mWebViewCore.clearContent(); mWebViewCore.removeMessages(WebViewCore.EventHub.WEBKIT_DRAW); } } diff --git a/core/java/android/webkit/GeolocationPermissions.java b/core/java/android/webkit/GeolocationPermissions.java index 5d54180..d7b6adb 100755 --- a/core/java/android/webkit/GeolocationPermissions.java +++ b/core/java/android/webkit/GeolocationPermissions.java @@ -27,30 +27,47 @@ import java.util.Vector; /** - * This class is used to get Geolocation permissions from, and set them on the - * WebView. For example, it could be used to allow a user to manage Geolocation - * permissions from a browser's UI. + * This class is used to manage permissions for the WebView's Geolocation + * JavaScript API. * - * Permissions are managed on a per-origin basis, as required by the - * Geolocation spec - http://dev.w3.org/geo/api/spec-source.html. An origin - * specifies the scheme, host and port of particular frame. An origin is - * represented here as a string, using the output of - * WebCore::SecurityOrigin::toString. + * Geolocation permissions are applied to an origin, which consists of the + * host, scheme and port of a URI. In order for web content to use the + * Geolocation API, permission must be granted for that content's origin. * - * This class is the Java counterpart of the WebKit C++ GeolocationPermissions - * class. It simply marshalls calls from the UI thread to the WebKit thread. + * This class stores Geolocation permissions. An origin's permission state can + * be either allowed or denied. This class uses Strings to represent + * an origin. * - * Within WebKit, Geolocation permissions may be applied either temporarily - * (for the duration of the page) or permanently. This class deals only with - * permanent permissions. + * When an origin attempts to use the Geolocation API, but no permission state + * is currently set for that origin, + * {@link WebChromeClient#onGeolocationPermissionsShowPrompt(String,GeolocationPermissions.Callback) WebChromeClient.onGeolocationPermissionsShowPrompt()} + * is called. This allows the permission state to be set for that origin. + * + * The methods of this class can be used to modify and interrogate the stored + * Geolocation permissions at any time. */ +// This class is the Java counterpart of the WebKit C++ GeolocationPermissions +// class. It simply marshalls calls from the UI thread to the WebKit thread. +// +// Within WebKit, Geolocation permissions may be applied either temporarily +// (for the duration of the page) or permanently. This class deals only with +// permanent permissions. public final class GeolocationPermissions { /** - * Callback interface used by the browser to report a Geolocation permission - * state set by the user in response to a permissions prompt. + * A callback interface used by the host application to set the Geolocation + * permission state for an origin. */ public interface Callback { - public void invoke(String origin, boolean allow, boolean remember); + /** + * Set the Geolocation permission state for the supplied origin. + * @param origin The origin for which permissions are set. + * @param allow Whether or not the origin should be allowed to use the + * Geolocation API. + * @param retain Whether the permission should be retained beyond the + * lifetime of a page currently being displayed by a + * WebView. + */ + public void invoke(String origin, boolean allow, boolean retain); }; // Log tag @@ -82,7 +99,8 @@ public final class GeolocationPermissions { private static final String ALLOWED = "allowed"; /** - * Gets the singleton instance of the class. + * Get the singleton instance of this class. + * @return The singleton {@link GeolocationPermissions} instance. */ public static GeolocationPermissions getInstance() { if (sInstance == null) { @@ -196,15 +214,18 @@ public final class GeolocationPermissions { } /** - * Gets the set of origins for which Geolocation permissions are stored. - * Note that we represent the origins as strings. These are created using - * WebCore::SecurityOrigin::toString(). As long as all 'HTML 5 modules' - * (Database, Geolocation etc) do so, it's safe to match up origins based - * on this string. - * - * Callback is a ValueCallback object whose onReceiveValue method will be - * called asynchronously with the set of origins. + * Get the set of origins for which Geolocation permissions are stored. + * @param callback A {@link ValueCallback} to receive the result of this + * request. This object's + * {@link ValueCallback#onReceiveValue(T) onReceiveValue()} + * method will be invoked asynchronously with a set of + * Strings containing the origins for which Geolocation + * permissions are stored. */ + // Note that we represent the origins as strings. These are created using + // WebCore::SecurityOrigin::toString(). As long as all 'HTML 5 modules' + // (Database, Geolocation etc) do so, it's safe to match up origins based + // on this string. public void getOrigins(ValueCallback<Set<String> > callback) { if (callback != null) { if (WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName())) { @@ -217,10 +238,14 @@ public final class GeolocationPermissions { } /** - * Gets the permission state for the specified origin. - * - * Callback is a ValueCallback object whose onReceiveValue method will be - * called asynchronously with the permission state for the origin. + * Get the Geolocation permission state for the specified origin. + * @param origin The origin for which Geolocation permission is requested. + * @param callback A {@link ValueCallback} to receive the result of this + * request. This object's + * {@link ValueCallback#onReceiveValue(T) onReceiveValue()} + * method will be invoked asynchronously with a boolean + * indicating whether or not the origin can use the + * Geolocation API. */ public void getAllowed(String origin, ValueCallback<Boolean> callback) { if (callback == null) { @@ -242,27 +267,31 @@ public final class GeolocationPermissions { } /** - * Clears the permission state for the specified origin. This method may be - * called before the WebKit thread has intialized the message handler. - * Messages will be queued until this time. + * Clear the Geolocation permission state for the specified origin. + * @param origin The origin for which Geolocation permissions are cleared. */ + // This method may be called before the WebKit + // thread has intialized the message handler. Messages will be queued until + // this time. public void clear(String origin) { // Called on the UI thread. postMessage(Message.obtain(null, CLEAR, origin)); } /** - * Allows the specified origin. This method may be called before the WebKit - * thread has intialized the message handler. Messages will be queued until - * this time. + * Allow the specified origin to use the Geolocation API. + * @param origin The origin for which Geolocation API use is allowed. */ + // This method may be called before the WebKit + // thread has intialized the message handler. Messages will be queued until + // this time. public void allow(String origin) { // Called on the UI thread. postMessage(Message.obtain(null, ALLOW, origin)); } /** - * Clears the permission state for all origins. + * Clear the Geolocation permission state for all origins. */ public void clearAll() { // Called on the UI thread. diff --git a/core/java/android/webkit/JniUtil.java b/core/java/android/webkit/JniUtil.java index 4662040..7b44938 100644 --- a/core/java/android/webkit/JniUtil.java +++ b/core/java/android/webkit/JniUtil.java @@ -91,6 +91,16 @@ class JniUtil { return sCacheDirectory; } + /** + * Called by JNI. Gets the application's package name. + * @return String The application's package name + */ + private static synchronized String getPackageName() { + checkInitialized(); + + return sContext.getPackageName(); + } + private static final String ANDROID_CONTENT = "content:"; /** diff --git a/core/java/android/webkit/ValueCallback.java b/core/java/android/webkit/ValueCallback.java index 1a167e8..5c7d97f 100644 --- a/core/java/android/webkit/ValueCallback.java +++ b/core/java/android/webkit/ValueCallback.java @@ -17,11 +17,12 @@ package android.webkit; /** - * A callback interface used to returns values asynchronously + * A callback interface used to provide values asynchronously. */ public interface ValueCallback<T> { /** - * Invoked when we have the result + * Invoked when the value is available. + * @param value The value. */ public void onReceiveValue(T value); }; diff --git a/core/java/android/webkit/WebChromeClient.java b/core/java/android/webkit/WebChromeClient.java index 3d129f7..a6ef0ce 100644 --- a/core/java/android/webkit/WebChromeClient.java +++ b/core/java/android/webkit/WebChromeClient.java @@ -250,14 +250,24 @@ public class WebChromeClient { } /** - * Instructs the client to show a prompt to ask the user to set the - * Geolocation permission state for the specified origin. + * Notify the host application that web content from the specified origin + * is attempting to use the Geolocation API, but no permission state is + * currently set for that origin. The host application should invoke the + * specified callback with the desired permission state. See + * {@link GeolocationPermissions} for details. + * @param origin The origin of the web content attempting to use the + * Geolocation API. + * @param callback The callback to use to set the permission state for the + * origin. */ public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {} /** - * Instructs the client to hide the Geolocation permissions prompt. + * Notify the host application that a request for Geolocation permissions, + * made with a previous call to + * {@link #onGeolocationPermissionsShowPrompt(String,GeolocationPermissions.Callback) onGeolocationPermissionsShowPrompt()} + * has been canceled. Any related UI should therefore be hidden. */ public void onGeolocationPermissionsHidePrompt() {} diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index 24eebd7..a284a17 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -59,6 +59,7 @@ import android.os.Message; import android.os.StrictMode; import android.provider.Settings; import android.speech.tts.TextToSpeech; +import android.text.TextUtils; import android.util.AttributeSet; import android.util.EventLog; import android.util.Log; @@ -206,10 +207,10 @@ import static javax.microedition.khronos.egl.EGL10.*; * <li>Modifying the {@link android.webkit.WebSettings}, such as * enabling JavaScript with {@link android.webkit.WebSettings#setJavaScriptEnabled(boolean) * setJavaScriptEnabled()}. </li> - * <li>Adding JavaScript-to-Java interfaces with the {@link - * android.webkit.WebView#addJavascriptInterface} method. - * This lets you bind Java objects into the WebView so they can be - * controlled from the web pages JavaScript.</li> + * <li>Injecting Java objects into the WebView using the + * {@link android.webkit.WebView#addJavascriptInterface} method. This + * method allows you to inject Java objects into a page's JavaScript + * context, so that they can be accessed by JavaScript in the page.</li> * </ul> * * <p>Here's a more complicated example, showing error handling, @@ -849,13 +850,12 @@ public class WebView extends AbsoluteLayout // the alias via which accessibility JavaScript interface is exposed private static final String ALIAS_ACCESSIBILITY_JS_INTERFACE = "accessibility"; - // JavaScript to inject the script chooser which will - // pick the right script for the current URL - private static final String ACCESSIBILITY_SCRIPT_CHOOSER_JAVASCRIPT = + // Template for JavaScript that injects a screen-reader. + private static final String ACCESSIBILITY_SCREEN_READER_JAVASCRIPT_TEMPLATE = "javascript:(function() {" + " var chooser = document.createElement('script');" + " chooser.type = 'text/javascript';" + - " chooser.src = 'https://ssl.gstatic.com/accessibility/javascript/android/AndroidScriptChooser.user.js';" + + " chooser.src = '%1s';" + " document.getElementsByTagName('head')[0].appendChild(chooser);" + " })();"; @@ -2496,11 +2496,12 @@ public class WebView extends AbsoluteLayout } /** - * Return the reading level scale of the WebView + * Compute the reading level scale of the WebView + * @param scale The current scale. * @return The reading level scale. */ - /*package*/ float getReadingLevelScale() { - return mZoomManager.getReadingLevelScale(); + /*package*/ float computeReadingLevelScale(float scale) { + return mZoomManager.computeReadingLevelScale(scale); } /** @@ -3818,7 +3819,7 @@ public class WebView extends AbsoluteLayout if (onDeviceScriptInjectionEnabled) { ensureAccessibilityScriptInjectorInstance(false); // neither script injected nor script injection opted out => we inject - loadUrl(ACCESSIBILITY_SCRIPT_CHOOSER_JAVASCRIPT); + loadUrl(getScreenReaderInjectingJs()); // TODO: Set this flag after successfull script injection. Maybe upon injection // the chooser should update the meta tag and we check it to declare success mAccessibilityScriptInjected = true; @@ -3832,7 +3833,7 @@ public class WebView extends AbsoluteLayout } else if (axsParameterValue == ACCESSIBILITY_SCRIPT_INJECTION_PROVIDED) { ensureAccessibilityScriptInjectorInstance(false); // the URL provides accessibility but we still need to add our generic script - loadUrl(ACCESSIBILITY_SCRIPT_CHOOSER_JAVASCRIPT); + loadUrl(getScreenReaderInjectingJs()); } else { Log.e(LOGTAG, "Unknown URL value for the \"axs\" URL parameter: " + axsParameterValue); } @@ -3854,6 +3855,17 @@ public class WebView extends AbsoluteLayout } /** + * Gets JavaScript that injects a screen-reader. + * + * @return The JavaScript snippet. + */ + private String getScreenReaderInjectingJs() { + String screenReaderUrl = Settings.Secure.getString(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_SCREEN_READER_URL); + return String.format(ACCESSIBILITY_SCREEN_READER_JAVASCRIPT_TEMPLATE, screenReaderUrl); + } + + /** * Gets the "axs" URL parameter value. * * @param url A url to fetch the paramter from. @@ -4089,34 +4101,39 @@ public class WebView extends AbsoluteLayout } /** - * Use this function to bind an object to JavaScript so that the - * methods can be accessed from JavaScript. + * This method injects the supplied Java object into the WebView. The + * object is injected into the JavaScript context of the main frame, using + * the supplied name. This allows the Java object to be accessed from + * JavaScript. Note that that injected objects will not appear in + * JavaScript until the page is next (re)loaded. For example: + * <pre> webView.addJavascriptInterface(new Object(), "injectedObject"); + * webView.loadData("<!DOCTYPE html><title></title>", "text/html", null); + * webView.loadUrl("javascript:alert(injectedObject.toString())");</pre> * <p><strong>IMPORTANT:</strong> * <ul> - * <li> Using addJavascriptInterface() allows JavaScript to control your - * application. This can be a very useful feature or a dangerous security - * issue. When the HTML in the WebView is untrustworthy (for example, part - * or all of the HTML is provided by some person or process), then an - * attacker could inject HTML that will execute your code and possibly any - * code of the attacker's choosing.<br> - * Do not use addJavascriptInterface() unless all of the HTML in this - * WebView was written by you.</li> - * <li> The Java object that is bound runs in another thread and not in - * the thread that it was constructed in.</li> + * <li> addJavascriptInterface() can be used to allow JavaScript to control + * the host application. This is a powerful feature, but also presents a + * security risk. Use of this method in a WebView containing untrusted + * content could allow an attacker to manipulate the host application in + * unintended ways, executing Java code with the permissions of the host + * application. Use extreme care when using this method in a WebView which + * could contain untrusted content. + * <li> JavaScript interacts with Java object on a private, background + * thread of the WebView. Care is therefore required to maintain thread + * safety.</li> * </ul></p> - * @param obj The class instance to bind to JavaScript, null instances are - * ignored. - * @param interfaceName The name to used to expose the instance in - * JavaScript. + * @param object The Java object to inject into the WebView's JavaScript + * context. Null values are ignored. + * @param name The name used to expose the instance in JavaScript. */ - public void addJavascriptInterface(Object obj, String interfaceName) { + public void addJavascriptInterface(Object object, String name) { checkThread(); - if (obj == null) { + if (object == null) { return; } WebViewCore.JSInterfaceData arg = new WebViewCore.JSInterfaceData(); - arg.mObject = obj; - arg.mInterfaceName = interfaceName; + arg.mObject = object; + arg.mInterfaceName = name; mWebViewCore.sendMessage(EventHub.ADD_JS_INTERFACE, arg); } @@ -4245,6 +4262,9 @@ public class WebView extends AbsoluteLayout @Override protected void onDraw(Canvas canvas) { + if (inFullScreenMode()) { + return; // no need to draw anything if we aren't visible. + } // if mNativeClass is 0, the WebView is either destroyed or not // initialized. In either case, just draw the background color and return if (mNativeClass == 0) { @@ -5107,17 +5127,6 @@ public class WebView extends AbsoluteLayout canProvideGamma, gamma); } - /** - * Dump the V8 counters to standard output. - * Note that you need a build with V8 and WEBCORE_INSTRUMENTATION set to - * true. Otherwise, this will do nothing. - * - * @hide debug only - */ - public void dumpV8Counters() { - mWebViewCore.sendMessage(EventHub.DUMP_V8COUNTERS); - } - // This is used to determine long press with the center key. Does not // affect long press with the trackball/touch. private boolean mGotCenterDown = false; @@ -5307,9 +5316,6 @@ public class WebView extends AbsoluteLayout case KeyEvent.KEYCODE_8: dumpRenderTree(keyCode == KeyEvent.KEYCODE_7); break; - case KeyEvent.KEYCODE_9: - nativeInstrumentReport(); - return true; } } @@ -5842,7 +5848,8 @@ public class WebView extends AbsoluteLayout } calcOurContentVisibleRectF(mVisibleContentRect); nativeUpdateDrawGLFunction(mGLViewportEmpty ? null : mGLRectViewport, - mGLViewportEmpty ? null : mViewRectViewport, mVisibleContentRect); + mGLViewportEmpty ? null : mViewRectViewport, + mVisibleContentRect); } /** @@ -5989,6 +5996,7 @@ public class WebView extends AbsoluteLayout if (inFullScreenMode()) { mFullScreenHolder.hide(); mFullScreenHolder = null; + invalidate(); } } @@ -8664,6 +8672,7 @@ public class WebView extends AbsoluteLayout mFullScreenHolder = new PluginFullScreenHolder(WebView.this, orientation, npp); mFullScreenHolder.setContentView(view); mFullScreenHolder.show(); + invalidate(); break; } @@ -9602,7 +9611,6 @@ public class WebView extends AbsoluteLayout private native void nativeHideCursor(); private native boolean nativeHitSelection(int x, int y); private native String nativeImageURI(int x, int y); - private native void nativeInstrumentReport(); private native Rect nativeLayerBounds(int layer); /* package */ native boolean nativeMoveCursorToNextTextInput(); // return true if the page has been scrolled diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java index d136004..de4949c 100644 --- a/core/java/android/webkit/WebViewCore.java +++ b/core/java/android/webkit/WebViewCore.java @@ -497,6 +497,13 @@ public final class WebViewCore { message.sendToTarget(); } + /** + * Clear the picture set. To be called only on the WebCore thread. + */ + /* package */ void clearContent() { + nativeClearContent(); + } + //------------------------------------------------------------------------- // JNI methods //------------------------------------------------------------------------- @@ -599,8 +606,6 @@ public final class WebViewCore { private native void nativeDumpNavTree(); - private native void nativeDumpV8Counters(); - private native void nativeSetJsFlags(String flags); /** @@ -1007,7 +1012,6 @@ public final class WebViewCore { static final int DUMP_DOMTREE = 170; static final int DUMP_RENDERTREE = 171; static final int DUMP_NAVTREE = 172; - static final int DUMP_V8COUNTERS = 173; static final int SET_JS_FLAGS = 174; static final int CONTENT_INVALIDATE_ALL = 175; @@ -1528,10 +1532,6 @@ public final class WebViewCore { nativeDumpNavTree(); break; - case DUMP_V8COUNTERS: - nativeDumpV8Counters(); - break; - case SET_JS_FLAGS: nativeSetJsFlags((String)msg.obj); break; @@ -1567,7 +1567,7 @@ public final class WebViewCore { // Clear the view so that onDraw() will draw nothing // but white background // (See public method WebView.clearView) - nativeClearContent(); + clearContent(); break; case MESSAGE_RELAY: @@ -2521,7 +2521,7 @@ public final class WebViewCore { if (mSettings.isNarrowColumnLayout()) { // In case of automatic text reflow in fixed view port mode. mInitialViewState.mTextWrapScale = - mWebView.getReadingLevelScale(); + mWebView.computeReadingLevelScale(data.mScale); } } else { // Scale is given such as when page is restored, use it. diff --git a/core/java/android/webkit/ZoomManager.java b/core/java/android/webkit/ZoomManager.java index 14bdc42..8ffba64 100644 --- a/core/java/android/webkit/ZoomManager.java +++ b/core/java/android/webkit/ZoomManager.java @@ -316,7 +316,12 @@ class ZoomManager { * Returns the zoom scale used for reading text on a double-tap. */ public final float getReadingLevelScale() { - return mDisplayDensity * mDoubleTapZoomFactor; + return computeScaleWithLimits(computeReadingLevelScale(getZoomOverviewScale())); + } + + /* package */ final float computeReadingLevelScale(float scale) { + return Math.max(mDisplayDensity * mDoubleTapZoomFactor, + scale + MIN_DOUBLE_TAP_SCALE_INCREMENT); } public final float getInvDefaultScale() { @@ -678,7 +683,7 @@ class ZoomManager { } zoomToOverview(); } else { - zoomToReadingLevelOrMore(); + zoomToReadingLevel(); } } @@ -709,9 +714,8 @@ class ZoomManager { !mWebView.getSettings().getUseFixedViewport()); } - private void zoomToReadingLevelOrMore() { - final float zoomScale = Math.max(getReadingLevelScale(), - mActualScale + MIN_DOUBLE_TAP_SCALE_INCREMENT); + private void zoomToReadingLevel() { + final float readingScale = getReadingLevelScale(); int left = mWebView.nativeGetBlockLeftEdge(mAnchorX, mAnchorY, mActualScale); if (left != WebView.NO_LEFTEDGE) { @@ -721,13 +725,13 @@ class ZoomManager { // Re-calculate the zoom center so that the new scroll x will be // on the left edge. if (viewLeft > 0) { - mZoomCenterX = viewLeft * zoomScale / (zoomScale - mActualScale); + mZoomCenterX = viewLeft * readingScale / (readingScale - mActualScale); } else { mWebView.scrollBy(viewLeft, 0); mZoomCenterX = 0; } } - startZoomAnimation(zoomScale, + startZoomAnimation(readingScale, !mWebView.getSettings().getUseFixedViewport()); } diff --git a/core/java/android/widget/NumberPicker.java b/core/java/android/widget/NumberPicker.java index 13375bf..7d0f98e 100644 --- a/core/java/android/widget/NumberPicker.java +++ b/core/java/android/widget/NumberPicker.java @@ -583,10 +583,7 @@ public class NumberPicker extends LinearLayout { OnClickListener onClickListener = new OnClickListener() { public void onClick(View v) { - InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); - if (inputMethodManager != null && inputMethodManager.isActive(mInputText)) { - inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); - } + hideSoftInput(); mInputText.clearFocus(); if (v.getId() == R.id.increment) { changeCurrentByOne(true); @@ -598,6 +595,7 @@ public class NumberPicker extends LinearLayout { OnLongClickListener onLongClickListener = new OnLongClickListener() { public boolean onLongClick(View v) { + hideSoftInput(); mInputText.clearFocus(); if (v.getId() == R.id.increment) { postChangeCurrentByOneFromLongPress(true); @@ -786,6 +784,7 @@ public class NumberPicker extends LinearLayout { } mBeginEditOnUpEvent = scrollersFinished; mAdjustScrollerOnUpEvent = true; + hideSoftInput(); hideInputControls(); return true; } @@ -795,6 +794,7 @@ public class NumberPicker extends LinearLayout { } mAdjustScrollerOnUpEvent = false; setSelectorWheelState(SELECTOR_WHEEL_STATE_LARGE); + hideSoftInput(); hideInputControls(); return true; case MotionEvent.ACTION_MOVE: @@ -804,6 +804,7 @@ public class NumberPicker extends LinearLayout { mBeginEditOnUpEvent = false; onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); setSelectorWheelState(SELECTOR_WHEEL_STATE_LARGE); + hideSoftInput(); hideInputControls(); return true; } @@ -1062,6 +1063,16 @@ public class NumberPicker extends LinearLayout { } /** + * Hides the soft input of it is active for the input text. + */ + private void hideSoftInput() { + InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); + if (inputMethodManager != null && inputMethodManager.isActive(mInputText)) { + inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); + } + } + + /** * Computes the max width if no such specified as an attribute. */ private void tryComputeMaxWidth() { diff --git a/core/java/android/widget/SpellChecker.java b/core/java/android/widget/SpellChecker.java index 8f495c9..6700829 100644 --- a/core/java/android/widget/SpellChecker.java +++ b/core/java/android/widget/SpellChecker.java @@ -97,19 +97,19 @@ public class SpellChecker implements SpellCheckerSessionListener { mCookie = hashCode(); } - private void resetSession() { + private void setLocale(Locale locale) { closeSession(); - - mTextServicesManager = (TextServicesManager) mTextView.getContext(). - getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE); - if (!mTextServicesManager.isSpellCheckerEnabled()) { + final TextServicesManager textServicesManager = (TextServicesManager) + mTextView.getContext().getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE); + if (!textServicesManager.isSpellCheckerEnabled()) { mSpellCheckerSession = null; } else { - mSpellCheckerSession = mTextServicesManager.newSpellCheckerSession( + mSpellCheckerSession = textServicesManager.newSpellCheckerSession( null /* Bundle not currently used by the textServicesManager */, - mCurrentLocale, this, + locale, this, false /* means any available languages from current spell checker */); } + mCurrentLocale = locale; // Restore SpellCheckSpans in pool for (int i = 0; i < mLength; i++) { @@ -118,22 +118,9 @@ public class SpellChecker implements SpellCheckerSessionListener { } mLength = 0; - // Remove existing misspelled SuggestionSpans - mTextView.removeMisspelledSpans((Editable) mTextView.getText()); - - // This class is the listener for locale change: warn other locale-aware objects - mTextView.onLocaleChanged(); - } - - private void setLocale(Locale locale) { - mCurrentLocale = locale; - - resetSession(); - - // Change SpellParsers' wordIterator locale - mWordIterator = new WordIterator(locale); + mSpellParsers = new SpellParser[0]; - // This class is the listener for locale change: warn other locale-aware objects + // This class is the global listener for locale change: warn other locale-aware objects mTextView.onLocaleChanged(); } @@ -150,9 +137,13 @@ public class SpellChecker implements SpellCheckerSessionListener { mSpellCheckerSession.close(); } + stopAllSpellParsers(); + } + + private void stopAllSpellParsers() { final int length = mSpellParsers.length; for (int i = 0; i < length; i++) { - mSpellParsers[i].finish(); + mSpellParsers[i].stop(); } if (mSpellRunnable != null) { @@ -207,20 +198,15 @@ public class SpellChecker implements SpellCheckerSessionListener { // Re-check the entire text start = 0; end = mTextView.getText().length(); - } else { - final boolean spellCheckerActivated = mTextServicesManager.isSpellCheckerEnabled(); - if (isSessionActive() != spellCheckerActivated) { - // Spell checker has been turned of or off since last spellCheck - resetSession(); - } } if (!isSessionActive()) return; + // Find first available SpellParser from pool final int length = mSpellParsers.length; for (int i = 0; i < length; i++) { final SpellParser spellParser = mSpellParsers[i]; - if (spellParser.isFinished()) { + if (!spellParser.isParsing()) { spellParser.init(start, end); spellParser.parse(); return; @@ -278,6 +264,12 @@ public class SpellChecker implements SpellCheckerSessionListener { } @Override + public void onGetSuggestionsForSentence(SuggestionsInfo[] results) { + // TODO: Handle the position and length for each suggestion + onGetSuggestions(results); + } + + @Override public void onGetSuggestions(SuggestionsInfo[] results) { Editable editable = (Editable) mTextView.getText(); @@ -400,16 +392,23 @@ public class SpellChecker implements SpellCheckerSessionListener { private Object mRange = new Object(); public void init(int start, int end) { - ((Editable) mTextView.getText()).setSpan(mRange, start, end, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + setRangeSpan((Editable) mTextView.getText(), start, end); + } + + public void stop() { + removeRangeSpan((Editable) mTextView.getText()); + } + + public boolean isParsing() { + return ((Editable) mTextView.getText()).getSpanStart(mRange) >= 0; } - public void finish() { - ((Editable) mTextView.getText()).removeSpan(mRange); + private void setRangeSpan(Editable editable, int start, int end) { + editable.setSpan(mRange, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } - public boolean isFinished() { - return ((Editable) mTextView.getText()).getSpanStart(mRange) < 0; + private void removeRangeSpan(Editable editable) { + editable.removeSpan(mRange); } public void parse() { @@ -433,7 +432,7 @@ public class SpellChecker implements SpellCheckerSessionListener { wordEnd = mWordIterator.getEnd(wordStart); } if (wordEnd == BreakIterator.DONE) { - editable.removeSpan(mRange); + removeRangeSpan(editable); return; } @@ -511,9 +510,10 @@ public class SpellChecker implements SpellCheckerSessionListener { } if (scheduleOtherSpellCheck) { - editable.setSpan(mRange, wordStart, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + // Update range span: start new spell check from last wordStart + setRangeSpan(editable, wordStart, end); } else { - editable.removeSpan(mRange); + removeRangeSpan(editable); } spellCheck(); diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 6722d17..90fb106 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -6075,6 +6075,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mSavedMarqueeModeLayout = mLayout = mHintLayout = null; + mBoring = mHintBoring = null; + // Since it depends on the value of mLayout prepareCursorControllers(); } @@ -8932,6 +8934,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } void onLocaleChanged() { + removeMisspelledSpans((Editable) mText); // Will be re-created on demand in getWordIterator with the proper new locale mWordIterator = null; } @@ -11496,13 +11499,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private boolean mUserSetTextScaleX; private final Paint mHighlightPaint; private int mHighlightColor = 0x6633B5E5; - /** - * This is temporarily visible to fix bug 3085564 in webView. Do not rely on - * this field being protected. Will be restored as private when lineHeight - * feature request 3215097 is implemented - * @hide - */ - protected Layout mLayout; + private Layout mLayout; private long mShowCursor; private Blink mBlink; diff --git a/core/java/com/android/internal/os/AtomicFile.java b/core/java/com/android/internal/os/AtomicFile.java index b093977..445d10a 100644 --- a/core/java/com/android/internal/os/AtomicFile.java +++ b/core/java/com/android/internal/os/AtomicFile.java @@ -28,6 +28,17 @@ import java.io.IOException; /** * Helper class for performing atomic operations on a file, by creating a * backup file until a write has successfully completed. + * <p> + * Atomic file guarantees file integrity by ensuring that a file has + * been completely written and sync'd to disk before removing its backup. + * As long as the backup file exists, the original file is considered + * to be invalid (left over from a previous attempt to write the file). + * </p><p> + * Atomic file does not confer any file locking semantics. + * Do not use this class when the file may be accessed or modified concurrently + * by multiple threads or processes. The caller is responsible for ensuring + * appropriate mutual exclusion invariants whenever it accesses the file. + * </p> */ public class AtomicFile { private final File mBaseName; diff --git a/core/java/com/android/internal/textservice/ISpellCheckerSession.aidl b/core/java/com/android/internal/textservice/ISpellCheckerSession.aidl index 3c61968..ba0aa1a 100644 --- a/core/java/com/android/internal/textservice/ISpellCheckerSession.aidl +++ b/core/java/com/android/internal/textservice/ISpellCheckerSession.aidl @@ -24,6 +24,7 @@ import android.view.textservice.TextInfo; oneway interface ISpellCheckerSession { void onGetSuggestionsMultiple( in TextInfo[] textInfos, int suggestionsLimit, boolean multipleWords); + void onGetSuggestionsMultipleForSentence(in TextInfo[] textInfos, int suggestionsLimit); void onCancel(); void onClose(); } diff --git a/core/java/com/android/internal/textservice/ISpellCheckerSessionListener.aidl b/core/java/com/android/internal/textservice/ISpellCheckerSessionListener.aidl index 796b06e..b44dbc8 100644 --- a/core/java/com/android/internal/textservice/ISpellCheckerSessionListener.aidl +++ b/core/java/com/android/internal/textservice/ISpellCheckerSessionListener.aidl @@ -23,4 +23,5 @@ import android.view.textservice.SuggestionsInfo; */ oneway interface ISpellCheckerSessionListener { void onGetSuggestions(in SuggestionsInfo[] results); + void onGetSuggestionsForSentence(in SuggestionsInfo[] results); } diff --git a/core/java/com/android/internal/util/ArrayUtils.java b/core/java/com/android/internal/util/ArrayUtils.java index 3d22929..edeb2a8 100644 --- a/core/java/com/android/internal/util/ArrayUtils.java +++ b/core/java/com/android/internal/util/ArrayUtils.java @@ -17,7 +17,6 @@ package com.android.internal.util; import java.lang.reflect.Array; -import java.util.Collection; // XXX these should be changed to reflect the actual memory allocator we use. // it looks like right now objects want to be powers of 2 minus 8 @@ -142,4 +141,56 @@ public class ArrayUtils } return false; } + + /** + * Appends an element to a copy of the array and returns the copy. + * @param array The original array, or null to represent an empty array. + * @param element The element to add. + * @return A new array that contains all of the elements of the original array + * with the specified element added at the end. + */ + @SuppressWarnings("unchecked") + public static <T> T[] appendElement(Class<T> kind, T[] array, T element) { + final T[] result; + final int end; + if (array != null) { + end = array.length; + result = (T[])Array.newInstance(kind, end + 1); + System.arraycopy(array, 0, result, 0, end); + } else { + end = 0; + result = (T[])Array.newInstance(kind, 1); + } + result[end] = element; + return result; + } + + /** + * Removes an element from a copy of the array and returns the copy. + * If the element is not present, then the original array is returned unmodified. + * @param array The original array, or null to represent an empty array. + * @param element The element to remove. + * @return A new array that contains all of the elements of the original array + * except the first copy of the specified element removed. If the specified element + * was not present, then returns the original array. Returns null if the result + * would be an empty array. + */ + @SuppressWarnings("unchecked") + public static <T> T[] removeElement(Class<T> kind, T[] array, T element) { + if (array != null) { + final int length = array.length; + for (int i = 0; i < length; i++) { + if (array[i] == element) { + if (length == 1) { + return null; + } + T[] result = (T[])Array.newInstance(kind, length - 1); + System.arraycopy(array, 0, result, 0, i); + System.arraycopy(array, i + 1, result, i, length - i - 1); + return result; + } + } + } + return array; + } } diff --git a/core/java/com/android/internal/view/BaseInputHandler.java b/core/java/com/android/internal/view/BaseInputHandler.java deleted file mode 100644 index 74b4b06..0000000 --- a/core/java/com/android/internal/view/BaseInputHandler.java +++ /dev/null @@ -1,36 +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 com.android.internal.view; - -import android.view.InputHandler; -import android.view.InputQueue; -import android.view.KeyEvent; -import android.view.MotionEvent; - -/** - * Base do-nothing implementation of an input handler. - * @hide - */ -public abstract class BaseInputHandler implements InputHandler { - public void handleKey(KeyEvent event, InputQueue.FinishedCallback finishedCallback) { - finishedCallback.finished(false); - } - - public void handleMotion(MotionEvent event, InputQueue.FinishedCallback finishedCallback) { - finishedCallback.finished(false); - } -} |
