summaryrefslogtreecommitdiffstats
path: root/core/java
diff options
context:
space:
mode:
Diffstat (limited to 'core/java')
-rw-r--r--core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl18
-rw-r--r--core/java/android/accounts/ChooseTypeAndAccountActivity.java16
-rwxr-xr-xcore/java/android/animation/ValueAnimator.java359
-rw-r--r--core/java/android/app/Instrumentation.java22
-rw-r--r--core/java/android/app/StatusBarManager.java5
-rw-r--r--core/java/android/bluetooth/BluetoothAdapter.java21
-rw-r--r--core/java/android/content/pm/ResolveInfo.java4
-rw-r--r--core/java/android/database/BulkCursorNative.java4
-rw-r--r--core/java/android/database/CrossProcessCursorWrapper.java8
-rw-r--r--core/java/android/database/CursorToBulkCursorAdaptor.java11
-rw-r--r--core/java/android/database/DatabaseUtils.java26
-rw-r--r--core/java/android/database/IBulkCursor.java12
-rw-r--r--core/java/android/database/sqlite/SQLiteCursor.java26
-rw-r--r--core/java/android/database/sqlite/SQLiteQuery.java29
-rw-r--r--core/java/android/hardware/Camera.java61
-rw-r--r--core/java/android/net/ConnectivityManager.java5
-rw-r--r--core/java/android/net/DhcpStateMachine.java34
-rw-r--r--core/java/android/net/InterfaceConfiguration.java139
-rw-r--r--core/java/android/net/NetworkInfo.java17
-rw-r--r--core/java/android/net/Uri.java116
-rw-r--r--core/java/android/nfc/NfcAdapter.java98
-rw-r--r--core/java/android/nfc/NfcManager.java4
-rw-r--r--core/java/android/os/AsyncTask.java10
-rw-r--r--core/java/android/os/RecoverySystem.java10
-rw-r--r--core/java/android/provider/ContactsContract.java33
-rw-r--r--core/java/android/provider/Settings.java64
-rw-r--r--core/java/android/server/BluetoothAdapterStateMachine.java11
-rw-r--r--core/java/android/server/BluetoothPanProfileHandler.java10
-rw-r--r--core/java/android/service/textservice/SpellCheckerService.java29
-rw-r--r--core/java/android/service/wallpaper/WallpaperService.java43
-rw-r--r--core/java/android/speech/tts/AudioMessageParams.java38
-rw-r--r--core/java/android/speech/tts/AudioPlaybackHandler.java591
-rw-r--r--core/java/android/speech/tts/AudioPlaybackQueueItem.java (renamed from core/java/android/speech/tts/BlockingMediaPlayer.java)101
-rw-r--r--core/java/android/speech/tts/BlockingAudioTrack.java338
-rw-r--r--core/java/android/speech/tts/EventLogTags.logtags4
-rw-r--r--core/java/android/speech/tts/EventLogger.java23
-rw-r--r--core/java/android/speech/tts/ITextToSpeechService.aidl32
-rw-r--r--core/java/android/speech/tts/MessageParams.java47
-rw-r--r--core/java/android/speech/tts/PlaybackQueueItem.java27
-rw-r--r--core/java/android/speech/tts/PlaybackSynthesisCallback.java62
-rw-r--r--core/java/android/speech/tts/SilencePlaybackQueueItem.java (renamed from core/java/android/speech/tts/SilenceMessageParams.java)27
-rw-r--r--core/java/android/speech/tts/SynthesisMessageParams.java159
-rw-r--r--core/java/android/speech/tts/SynthesisPlaybackQueueItem.java245
-rwxr-xr-xcore/java/android/speech/tts/TextToSpeech.java30
-rw-r--r--core/java/android/speech/tts/TextToSpeechService.java191
-rw-r--r--core/java/android/text/format/DateUtils.java133
-rw-r--r--core/java/android/util/LruCache.java4
-rw-r--r--core/java/android/util/SparseLongArray.java247
-rw-r--r--core/java/android/view/Choreographer.java382
-rw-r--r--core/java/android/view/DisplayEventReceiver.java115
-rw-r--r--core/java/android/view/GLES20Canvas.java3
-rw-r--r--core/java/android/view/GLES20Layer.java7
-rw-r--r--core/java/android/view/GestureDetector.java11
-rw-r--r--core/java/android/view/HardwareLayer.java5
-rw-r--r--core/java/android/view/HardwareRenderer.java60
-rwxr-xr-xcore/java/android/view/InputEvent.java72
-rw-r--r--core/java/android/view/InputEventConsistencyVerifier.java11
-rw-r--r--core/java/android/view/InputEventReceiver.java141
-rw-r--r--core/java/android/view/InputHandler.java43
-rw-r--r--core/java/android/view/InputQueue.java131
-rwxr-xr-xcore/java/android/view/KeyEvent.java15
-rw-r--r--core/java/android/view/MotionEvent.java21
-rw-r--r--core/java/android/view/View.java117
-rw-r--r--core/java/android/view/ViewConfiguration.java76
-rw-r--r--core/java/android/view/ViewDebug.java13
-rw-r--r--core/java/android/view/ViewGroup.java18
-rw-r--r--core/java/android/view/ViewPropertyAnimator.java6
-rw-r--r--core/java/android/view/ViewRootImpl.java1299
-rw-r--r--core/java/android/view/ViewTreeObserver.java8
-rw-r--r--core/java/android/view/VolumePanel.java2
-rw-r--r--core/java/android/view/WindowManagerPolicy.java3
-rw-r--r--core/java/android/view/accessibility/AccessibilityEvent.java6
-rw-r--r--core/java/android/view/accessibility/AccessibilityInteractionClient.java60
-rw-r--r--core/java/android/view/accessibility/AccessibilityNodeInfo.java259
-rw-r--r--core/java/android/view/accessibility/AccessibilityNodeProvider.java130
-rw-r--r--core/java/android/view/accessibility/AccessibilityRecord.java39
-rw-r--r--core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl6
-rw-r--r--core/java/android/view/textservice/SpellCheckerSession.java70
-rw-r--r--core/java/android/view/textservice/SpellCheckerSubtype.java48
-rw-r--r--core/java/android/view/textservice/SuggestionsInfo.java76
-rw-r--r--core/java/android/view/textservice/TextServicesManager.java3
-rw-r--r--core/java/android/webkit/BrowserFrame.java1
-rwxr-xr-xcore/java/android/webkit/GeolocationPermissions.java101
-rw-r--r--core/java/android/webkit/JniUtil.java10
-rw-r--r--core/java/android/webkit/ValueCallback.java5
-rw-r--r--core/java/android/webkit/WebChromeClient.java16
-rw-r--r--core/java/android/webkit/WebView.java106
-rw-r--r--core/java/android/webkit/WebViewCore.java18
-rw-r--r--core/java/android/webkit/ZoomManager.java18
-rw-r--r--core/java/android/widget/NumberPicker.java19
-rw-r--r--core/java/android/widget/SpellChecker.java78
-rw-r--r--core/java/android/widget/TextView.java11
-rw-r--r--core/java/com/android/internal/os/AtomicFile.java11
-rw-r--r--core/java/com/android/internal/textservice/ISpellCheckerSession.aidl1
-rw-r--r--core/java/com/android/internal/textservice/ISpellCheckerSessionListener.aidl1
-rw-r--r--core/java/com/android/internal/util/ArrayUtils.java53
-rw-r--r--core/java/com/android/internal/view/BaseInputHandler.java36
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);
- }
-}