diff options
Diffstat (limited to 'core/java/android')
21 files changed, 1242 insertions, 472 deletions
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/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/content/Intent.java b/core/java/android/content/Intent.java index 9948985..4e5598b 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -2315,6 +2315,11 @@ public class Intent implements Parcelable, Cloneable { /** * Used with {@link #ACTION_MAIN} to launch the browser application. * The activity should be able to browse the Internet. + * <p>NOTE: This should not be used as the primary key of an Intent, + * since it will not result in the app launching with the correct + * action and category. Instead, use this with + * {@link #makeMainSelectorActivity(String, String) to generate a main + * Intent with this category in the selector.</p> */ @SdkConstant(SdkConstantType.INTENT_CATEGORY) public static final String CATEGORY_APP_BROWSER = "android.intent.category.APP_BROWSER"; @@ -2322,6 +2327,11 @@ public class Intent implements Parcelable, Cloneable { /** * Used with {@link #ACTION_MAIN} to launch the calculator application. * The activity should be able to perform standard arithmetic operations. + * <p>NOTE: This should not be used as the primary key of an Intent, + * since it will not result in the app launching with the correct + * action and category. Instead, use this with + * {@link #makeMainSelectorActivity(String, String) to generate a main + * Intent with this category in the selector.</p> */ @SdkConstant(SdkConstantType.INTENT_CATEGORY) public static final String CATEGORY_APP_CALCULATOR = "android.intent.category.APP_CALCULATOR"; @@ -2329,6 +2339,11 @@ public class Intent implements Parcelable, Cloneable { /** * Used with {@link #ACTION_MAIN} to launch the calendar application. * The activity should be able to view and manipulate calendar entries. + * <p>NOTE: This should not be used as the primary key of an Intent, + * since it will not result in the app launching with the correct + * action and category. Instead, use this with + * {@link #makeMainSelectorActivity(String, String) to generate a main + * Intent with this category in the selector.</p> */ @SdkConstant(SdkConstantType.INTENT_CATEGORY) public static final String CATEGORY_APP_CALENDAR = "android.intent.category.APP_CALENDAR"; @@ -2336,6 +2351,11 @@ public class Intent implements Parcelable, Cloneable { /** * Used with {@link #ACTION_MAIN} to launch the contacts application. * The activity should be able to view and manipulate address book entries. + * <p>NOTE: This should not be used as the primary key of an Intent, + * since it will not result in the app launching with the correct + * action and category. Instead, use this with + * {@link #makeMainSelectorActivity(String, String) to generate a main + * Intent with this category in the selector.</p> */ @SdkConstant(SdkConstantType.INTENT_CATEGORY) public static final String CATEGORY_APP_CONTACTS = "android.intent.category.APP_CONTACTS"; @@ -2343,6 +2363,11 @@ public class Intent implements Parcelable, Cloneable { /** * Used with {@link #ACTION_MAIN} to launch the email application. * The activity should be able to send and receive email. + * <p>NOTE: This should not be used as the primary key of an Intent, + * since it will not result in the app launching with the correct + * action and category. Instead, use this with + * {@link #makeMainSelectorActivity(String, String) to generate a main + * Intent with this category in the selector.</p> */ @SdkConstant(SdkConstantType.INTENT_CATEGORY) public static final String CATEGORY_APP_EMAIL = "android.intent.category.APP_EMAIL"; @@ -2351,6 +2376,11 @@ public class Intent implements Parcelable, Cloneable { * Used with {@link #ACTION_MAIN} to launch the gallery application. * The activity should be able to view and manipulate image and video files * stored on the device. + * <p>NOTE: This should not be used as the primary key of an Intent, + * since it will not result in the app launching with the correct + * action and category. Instead, use this with + * {@link #makeMainSelectorActivity(String, String) to generate a main + * Intent with this category in the selector.</p> */ @SdkConstant(SdkConstantType.INTENT_CATEGORY) public static final String CATEGORY_APP_GALLERY = "android.intent.category.APP_GALLERY"; @@ -2358,6 +2388,11 @@ public class Intent implements Parcelable, Cloneable { /** * Used with {@link #ACTION_MAIN} to launch the maps application. * The activity should be able to show the user's current location and surroundings. + * <p>NOTE: This should not be used as the primary key of an Intent, + * since it will not result in the app launching with the correct + * action and category. Instead, use this with + * {@link #makeMainSelectorActivity(String, String) to generate a main + * Intent with this category in the selector.</p> */ @SdkConstant(SdkConstantType.INTENT_CATEGORY) public static final String CATEGORY_APP_MAPS = "android.intent.category.APP_MAPS"; @@ -2365,13 +2400,24 @@ public class Intent implements Parcelable, Cloneable { /** * Used with {@link #ACTION_MAIN} to launch the messaging application. * The activity should be able to send and receive text messages. + * <p>NOTE: This should not be used as the primary key of an Intent, + * since it will not result in the app launching with the correct + * action and category. Instead, use this with + * {@link #makeMainSelectorActivity(String, String) to generate a main + * Intent with this category in the selector.</p> */ @SdkConstant(SdkConstantType.INTENT_CATEGORY) public static final String CATEGORY_APP_MESSAGING = "android.intent.category.APP_MESSAGING"; /** * Used with {@link #ACTION_MAIN} to launch the music application. - * The activity should be able to play, browse, or manipulate music files stored on the device. + * The activity should be able to play, browse, or manipulate music files + * stored on the device. + * <p>NOTE: This should not be used as the primary key of an Intent, + * since it will not result in the app launching with the correct + * action and category. Instead, use this with + * {@link #makeMainSelectorActivity(String, String) to generate a main + * Intent with this category in the selector.</p> */ @SdkConstant(SdkConstantType.INTENT_CATEGORY) public static final String CATEGORY_APP_MUSIC = "android.intent.category.APP_MUSIC"; @@ -2963,6 +3009,7 @@ public class Intent implements Parcelable, Cloneable { private HashSet<String> mCategories; private Bundle mExtras; private Rect mSourceBounds; + private Intent mSelector; // --------------------------------------------------------------------- @@ -2991,6 +3038,9 @@ public class Intent implements Parcelable, Cloneable { if (o.mSourceBounds != null) { this.mSourceBounds = new Rect(o.mSourceBounds); } + if (o.mSelector != null) { + this.mSelector = new Intent(o.mSelector); + } } @Override @@ -3131,6 +3181,39 @@ public class Intent implements Parcelable, Cloneable { } /** + * Make an Intent for the main activity of an application, without + * specifying a specific activity to run but giving a selector to find + * the activity. This results in a final Intent that is structured + * the same as when the application is launched from + * Home. For anything else that wants to launch an application in the + * same way, it is important that they use an Intent structured the same + * way, and can use this function to ensure this is the case. + * + * <p>The returned Intent has {@link #ACTION_MAIN} as its action, and includes the + * category {@link #CATEGORY_LAUNCHER}. This does <em>not</em> have + * {@link #FLAG_ACTIVITY_NEW_TASK} set, though typically you will want + * to do that through {@link #addFlags(int)} on the returned Intent. + * + * @param selectorAction The action name of the Intent's selector. + * @param selectorCategory The name of a category to add to the Intent's + * selector. + * @return Returns a newly created Intent that can be used to launch the + * activity as a main application entry. + * + * @see #setSelector(Intent) + */ + public static Intent makeMainSelectorActivity(String selectorAction, + String selectorCategory) { + Intent intent = new Intent(ACTION_MAIN); + intent.addCategory(CATEGORY_LAUNCHER); + Intent selector = new Intent(); + selector.setAction(selectorAction); + selector.addCategory(selectorCategory); + intent.setSelector(selector); + return intent; + } + + /** * Make an Intent that can be used to re-launch an application's task * in its base state. This is like {@link #makeMainActivity(ComponentName)}, * but also sets the flags {@link #FLAG_ACTIVITY_NEW_TASK} and @@ -3205,6 +3288,7 @@ public class Intent implements Parcelable, Cloneable { // new format Intent intent = new Intent(ACTION_VIEW); + Intent baseIntent = intent; // fetch data part, if present String data = i >= 0 ? uri.substring(0, i) : null; @@ -3214,8 +3298,9 @@ public class Intent implements Parcelable, Cloneable { // loop over contents of Intent, all name=value; while (!uri.startsWith("end", i)) { int eq = uri.indexOf('=', i); - int semi = uri.indexOf(';', eq); - String value = Uri.decode(uri.substring(eq + 1, semi)); + if (eq < 0) eq = i-1; + int semi = uri.indexOf(';', i); + String value = eq < semi ? Uri.decode(uri.substring(eq + 1, semi)) : ""; // action if (uri.startsWith("action=", i)) { @@ -3257,6 +3342,11 @@ public class Intent implements Parcelable, Cloneable { intent.mSourceBounds = Rect.unflattenFromString(value); } + // selector + else if (semi == (i+3) && uri.startsWith("SEL", i)) { + intent = new Intent(); + } + // extra else { String key = Uri.decode(uri.substring(i + 2, eq)); @@ -3280,6 +3370,12 @@ public class Intent implements Parcelable, Cloneable { i = semi + 1; } + if (intent != baseIntent) { + // The Intent had a selector; fix it up. + baseIntent.setSelector(intent); + intent = baseIntent; + } + if (data != null) { if (data.startsWith("intent:")) { data = data.substring(7); @@ -3605,7 +3701,7 @@ public class Intent implements Parcelable, Cloneable { * Return the set of all categories in the intent. If there are no categories, * returns NULL. * - * @return Set The set of categories you can examine. Do not modify! + * @return The set of categories you can examine. Do not modify! * * @see #hasCategory * @see #addCategory @@ -3615,6 +3711,16 @@ public class Intent implements Parcelable, Cloneable { } /** + * Return the specific selector associated with this Intent. If there is + * none, returns null. See {@link #setSelector} for more information. + * + * @see #setSelector + */ + public Intent getSelector() { + return mSelector; + } + + /** * Sets the ClassLoader that will be used when unmarshalling * any Parcelable values from the extras of this Intent. * @@ -4433,6 +4539,49 @@ public class Intent implements Parcelable, Cloneable { } /** + * Set a selector for this Intent. This is a modification to the kinds of + * things the Intent will match. If the selector is set, it will be used + * when trying to find entities that can handle the Intent, instead of the + * main contents of the Intent. This allows you build an Intent containing + * a generic protocol while targeting it more specifically. + * + * <p>An example of where this may be used is with things like + * {@link #CATEGORY_APP_BROWSER}. This category allows you to build an + * Intent that will launch the Browser application. However, the correct + * main entry point of an application is actually {@link #ACTION_MAIN} + * {@link #CATEGORY_LAUNCHER} with {@link #setComponent(ComponentName)} + * used to specify the actual Activity to launch. If you launch the browser + * with something different, undesired behavior may happen if the user has + * previously or later launches it the normal way, since they do not match. + * Instead, you can build an Intent with the MAIN action (but no ComponentName + * yet specified) and set a selector with {@link #ACTION_MAIN} and + * {@link #CATEGORY_APP_BROWSER} to point it specifically to the browser activity. + * + * <p>Setting a selector does not impact the behavior of + * {@link #filterEquals(Intent)} and {@link #filterHashCode()}. This is part of the + * desired behavior of a selector -- it does not impact the base meaning + * of the Intent, just what kinds of things will be matched against it + * when determining who can handle it.</p> + * + * <p>You can not use both a selector and {@link #setPackage(String)} on + * the same base Intent.</p> + * + * @param selector The desired selector Intent; set to null to not use + * a special selector. + */ + public void setSelector(Intent selector) { + if (selector == this) { + throw new IllegalArgumentException( + "Intent being set as a selector of itself"); + } + if (selector != null && mPackage != null) { + throw new IllegalArgumentException( + "Can't set selector when package name is already set"); + } + mSelector = selector; + } + + /** * Add extended data to the intent. The name must include a package * prefix, for example the app com.android.contacts would use names * like "com.android.contacts.ShowAll". @@ -5259,6 +5408,10 @@ public class Intent implements Parcelable, Cloneable { * @see #resolveActivity */ public Intent setPackage(String packageName) { + if (packageName != null && mSelector != null) { + throw new IllegalArgumentException( + "Can't set package name when selector is already set"); + } mPackage = packageName; return this; } @@ -5394,12 +5547,18 @@ public class Intent implements Parcelable, Cloneable { public static final int FILL_IN_PACKAGE = 1<<4; /** - * Use with {@link #fillIn} to allow the current package value to be + * Use with {@link #fillIn} to allow the current bounds rectangle to be * overwritten, even if it is already set. */ public static final int FILL_IN_SOURCE_BOUNDS = 1<<5; /** + * Use with {@link #fillIn} to allow the current selector to be + * overwritten, even if it is already set. + */ + public static final int FILL_IN_SELECTOR = 1<<6; + + /** * Copy the contents of <var>other</var> in to this object, but only * where fields are not defined by this object. For purposes of a field * being defined, the following pieces of data in the Intent are @@ -5419,11 +5578,13 @@ public class Intent implements Parcelable, Cloneable { * * <p>In addition, you can use the {@link #FILL_IN_ACTION}, * {@link #FILL_IN_DATA}, {@link #FILL_IN_CATEGORIES}, {@link #FILL_IN_PACKAGE}, - * and {@link #FILL_IN_COMPONENT} to override the restriction where the + * {@link #FILL_IN_COMPONENT}, {@link #FILL_IN_SOURCE_BOUNDS}, and + * {@link #FILL_IN_SELECTOR} to override the restriction where the * corresponding field will not be replaced if it is already set. * * <p>Note: The component field will only be copied if {@link #FILL_IN_COMPONENT} is explicitly - * specified. + * specified. The selector will only be copied if {@link #FILL_IN_SELECTOR} is + * explicitly specified. * * <p>For example, consider Intent A with {data="foo", categories="bar"} * and Intent B with {action="gotit", data-type="some/thing", @@ -5439,7 +5600,8 @@ public class Intent implements Parcelable, Cloneable { * * @return Returns a bit mask of {@link #FILL_IN_ACTION}, * {@link #FILL_IN_DATA}, {@link #FILL_IN_CATEGORIES}, {@link #FILL_IN_PACKAGE}, - * and {@link #FILL_IN_COMPONENT} indicating which fields were changed. + * {@link #FILL_IN_COMPONENT}, {@link #FILL_IN_SOURCE_BOUNDS}, and + * {@link #FILL_IN_SELECTOR} indicating which fields were changed. */ public int fillIn(Intent other, int flags) { int changes = 0; @@ -5464,8 +5626,20 @@ public class Intent implements Parcelable, Cloneable { } if (other.mPackage != null && (mPackage == null || (flags&FILL_IN_PACKAGE) != 0)) { - mPackage = other.mPackage; - changes |= FILL_IN_PACKAGE; + // Only do this if mSelector is not set. + if (mSelector == null) { + mPackage = other.mPackage; + changes |= FILL_IN_PACKAGE; + } + } + // Selector is special: it can only be set if explicitly allowed, + // for the same reason as the component name. + if (other.mSelector != null && (flags&FILL_IN_SELECTOR) != 0) { + if (mPackage == null) { + mSelector = new Intent(other.mSelector); + mPackage = null; + changes |= FILL_IN_SELECTOR; + } } // Component is special: it can -only- be set if explicitly allowed, // since otherwise the sender could force the intent somewhere the @@ -5763,6 +5937,11 @@ public class Intent implements Parcelable, Cloneable { first = false; b.append("(has extras)"); } + if (mSelector != null) { + b.append(" sel={"); + mSelector.toShortString(b, secure, comp, extras); + b.append("}"); + } } /** @@ -5823,6 +6002,21 @@ public class Intent implements Parcelable, Cloneable { uri.append("#Intent;"); + toUriInner(uri, scheme, flags); + if (mSelector != null) { + uri.append("SEL;"); + // Note that for now we are not going to try to handle the + // data part; not clear how to represent this as a URI, and + // not much utility in it. + mSelector.toUriInner(uri, null, flags); + } + + uri.append("end"); + + return uri.toString(); + } + + private void toUriInner(StringBuilder uri, String scheme, int flags) { if (scheme != null) { uri.append("scheme=").append(scheme).append(';'); } @@ -5877,10 +6071,6 @@ public class Intent implements Parcelable, Cloneable { } } } - - uri.append("end"); - - return uri.toString(); } public int describeContents() { @@ -5911,6 +6101,13 @@ public class Intent implements Parcelable, Cloneable { out.writeInt(0); } + if (mSelector != null) { + out.writeInt(1); + mSelector.writeToParcel(out, flags); + } else { + out.writeInt(0); + } + out.writeBundle(mExtras); } @@ -5952,6 +6149,10 @@ public class Intent implements Parcelable, Cloneable { mCategories = null; } + if (in.readInt() != 0) { + mSelector = new Intent(in); + } + mExtras = in.readBundle(); } diff --git a/core/java/android/content/SyncManager.java b/core/java/android/content/SyncManager.java index b2909b3..3c4e545 100644 --- a/core/java/android/content/SyncManager.java +++ b/core/java/android/content/SyncManager.java @@ -24,6 +24,7 @@ import com.google.android.collect.Maps; import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.OnAccountsUpdateListener; +import android.app.ActivityManager; import android.app.AlarmManager; import android.app.Notification; import android.app.NotificationManager; @@ -86,8 +87,13 @@ public class SyncManager implements OnAccountsUpdateListener { private static final long MAX_TIME_PER_SYNC; static { - MAX_SIMULTANEOUS_INITIALIZATION_SYNCS = SystemProperties.getInt("sync.max_init_syncs", 5); - MAX_SIMULTANEOUS_REGULAR_SYNCS = SystemProperties.getInt("sync.max_regular_syncs", 2); + final boolean isLargeRAM = ActivityManager.isLargeRAM(); + int defaultMaxInitSyncs = isLargeRAM ? 5 : 2; + int defaultMaxRegularSyncs = isLargeRAM ? 2 : 1; + MAX_SIMULTANEOUS_INITIALIZATION_SYNCS = + SystemProperties.getInt("sync.max_init_syncs", defaultMaxInitSyncs); + MAX_SIMULTANEOUS_REGULAR_SYNCS = + SystemProperties.getInt("sync.max_regular_syncs", defaultMaxRegularSyncs); LOCAL_SYNC_DELAY = SystemProperties.getLong("sync.local_sync_delay", 30 * 1000 /* 30 seconds */); MAX_TIME_PER_SYNC = diff --git a/core/java/android/inputmethodservice/KeyboardView.java b/core/java/android/inputmethodservice/KeyboardView.java index 5143f7f..7257521 100644 --- a/core/java/android/inputmethodservice/KeyboardView.java +++ b/core/java/android/inputmethodservice/KeyboardView.java @@ -31,6 +31,7 @@ import android.inputmethodservice.Keyboard.Key; import android.media.AudioManager; import android.os.Handler; import android.os.Message; +import android.provider.Settings; import android.util.AttributeSet; import android.util.TypedValue; import android.view.GestureDetector; @@ -967,8 +968,13 @@ public class KeyboardView extends View implements View.OnClickListener { AccessibilityEvent event = AccessibilityEvent.obtain(eventType); onInitializeAccessibilityEvent(event); String text = null; - // Add text only if headset is used to avoid leaking passwords. - if (mAudioManager.isBluetoothA2dpOn() || mAudioManager.isWiredHeadsetOn()) { + // This is very efficient since the properties are cached. + final boolean speakPassword = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD, 0) != 0; + // Add text only if password announcement is enabled or if headset is + // used to avoid leaking passwords. + if (speakPassword || mAudioManager.isBluetoothA2dpOn() + || mAudioManager.isWiredHeadsetOn()) { switch (code) { case Keyboard.KEYCODE_ALT: text = mContext.getString(R.string.keyboardview_keycode_alt); 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/provider/Settings.java b/core/java/android/provider/Settings.java index 1b5d73e..7b0e0ab 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -2774,6 +2774,11 @@ public final class Settings { "enabled_accessibility_services"; /** + * Whether to speak passwords while in accessibility mode. + */ + public static final String ACCESSIBILITY_SPEAK_PASSWORD = "speak_password"; + + /** * If injection of accessibility enhancing JavaScript scripts * is enabled. * <p> @@ -4121,6 +4126,7 @@ public final class Settings { ENABLED_ACCESSIBILITY_SERVICES, TOUCH_EXPLORATION_ENABLED, ACCESSIBILITY_ENABLED, + ACCESSIBILITY_SPEAK_PASSWORD, TTS_USE_DEFAULTS, TTS_DEFAULT_RATE, TTS_DEFAULT_PITCH, diff --git a/core/java/android/server/BluetoothAdapterStateMachine.java b/core/java/android/server/BluetoothAdapterStateMachine.java index c59a05a..f4a390e 100644 --- a/core/java/android/server/BluetoothAdapterStateMachine.java +++ b/core/java/android/server/BluetoothAdapterStateMachine.java @@ -186,8 +186,8 @@ final class BluetoothAdapterStateMachine extends StateMachine { switch(message.what) { case USER_TURN_ON: // starts turning on BT module, broadcast this out - transitionTo(mWarmUp); broadcastState(BluetoothAdapter.STATE_TURNING_ON); + transitionTo(mWarmUp); if (prepareBluetooth()) { // this is user request, save the setting if ((Boolean) message.obj) { @@ -209,8 +209,8 @@ final class BluetoothAdapterStateMachine extends StateMachine { case AIRPLANE_MODE_OFF: if (getBluetoothPersistedSetting()) { // starts turning on BT module, broadcast this out - transitionTo(mWarmUp); broadcastState(BluetoothAdapter.STATE_TURNING_ON); + transitionTo(mWarmUp); if (prepareBluetooth()) { // We will continue turn the BT on all the way to the BluetoothOn state deferMessage(obtainMessage(TURN_ON_CONTINUE)); @@ -366,9 +366,9 @@ final class BluetoothAdapterStateMachine extends StateMachine { // let it fall to TURN_ON_CONTINUE: //$FALL-THROUGH$ case TURN_ON_CONTINUE: + broadcastState(BluetoothAdapter.STATE_TURNING_ON); mBluetoothService.switchConnectable(true); transitionTo(mSwitching); - broadcastState(BluetoothAdapter.STATE_TURNING_ON); break; case AIRPLANE_MODE_ON: case TURN_COLD: @@ -378,9 +378,9 @@ final class BluetoothAdapterStateMachine extends StateMachine { break; case AIRPLANE_MODE_OFF: if (getBluetoothPersistedSetting()) { + broadcastState(BluetoothAdapter.STATE_TURNING_ON); transitionTo(mSwitching); mBluetoothService.switchConnectable(true); - broadcastState(BluetoothAdapter.STATE_TURNING_ON); } break; case PER_PROCESS_TURN_ON: @@ -526,8 +526,8 @@ final class BluetoothAdapterStateMachine extends StateMachine { } //$FALL-THROUGH$ to AIRPLANE_MODE_ON case AIRPLANE_MODE_ON: - transitionTo(mSwitching); broadcastState(BluetoothAdapter.STATE_TURNING_OFF); + transitionTo(mSwitching); if (mBluetoothService.getAdapterConnectionState() != BluetoothAdapter.STATE_DISCONNECTED) { mBluetoothService.disconnectDevices(); 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 aa0bfd2..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); /////////////////////////////////////////////////////////////////////////// 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/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/View.java b/core/java/android/view/View.java index 304a9a1..af8f8cb 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -10207,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 @@ -10219,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; @@ -10293,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; diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java index d824e36..05c5daa 100644 --- a/core/java/android/view/ViewConfiguration.java +++ b/core/java/android/view/ViewConfiguration.java @@ -139,9 +139,17 @@ public class ViewConfiguration { private static final int EDGE_SLOP = 12; /** - * Distance a touch can wander before we think the user is scrolling in dips + * Distance a touch can wander before we think the user is scrolling in dips. + * Note that this value defined here is only used as a fallback by legacy/misbehaving + * applications that do not provide a Context for determining density/configuration-dependent + * values. + * + * To alter this value, see the configuration resource config_viewConfigurationTouchSlop + * in frameworks/base/core/res/res/values/config.xml or the appropriate device resource overlay. + * It may be appropriate to tweak this on a device-specific basis in an overlay based on + * the characteristics of the touch panel and firmware. */ - private static final int TOUCH_SLOP = 16; + private static final int TOUCH_SLOP = 4; /** * Distance the first touch can wander before we stop considering this event a double tap @@ -152,6 +160,14 @@ public class ViewConfiguration { /** * Distance a touch can wander before we think the user is attempting a paged scroll * (in dips) + * + * Note that this value defined here is only used as a fallback by legacy/misbehaving + * applications that do not provide a Context for determining density/configuration-dependent + * values. + * + * See the note above on {@link #TOUCH_SLOP} regarding the dimen resource + * config_viewConfigurationTouchSlop. ViewConfiguration will report a paging touch slop of + * config_viewConfigurationTouchSlop * 2 when provided with a Context. */ private static final int PAGING_TOUCH_SLOP = TOUCH_SLOP * 2; @@ -285,9 +301,6 @@ public class ViewConfiguration { mMinimumFlingVelocity = (int) (density * MINIMUM_FLING_VELOCITY + 0.5f); mMaximumFlingVelocity = (int) (density * MAXIMUM_FLING_VELOCITY + 0.5f); mScrollbarSize = (int) (density * SCROLL_BAR_SIZE + 0.5f); - mTouchSlop = (int) (sizeAndDensity * TOUCH_SLOP + 0.5f); - mDoubleTapTouchSlop = (int) (sizeAndDensity * DOUBLE_TAP_TOUCH_SLOP + 0.5f); - mPagingTouchSlop = (int) (sizeAndDensity * PAGING_TOUCH_SLOP + 0.5f); mDoubleTapSlop = (int) (sizeAndDensity * DOUBLE_TAP_SLOP + 0.5f); mScaledTouchExplorationTapSlop = (int) (density * TOUCH_EXPLORATION_TAP_SLOP + 0.5f); mWindowTouchSlop = (int) (sizeAndDensity * WINDOW_TOUCH_SLOP + 0.5f); @@ -310,6 +323,11 @@ public class ViewConfiguration { mFadingMarqueeEnabled = res.getBoolean( com.android.internal.R.bool.config_ui_enableFadingMarquee); + mTouchSlop = res.getDimensionPixelSize( + com.android.internal.R.dimen.config_viewConfigurationTouchSlop); + mPagingTouchSlop = mTouchSlop * 2; + + mDoubleTapTouchSlop = mTouchSlop; } /** diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 2a041f7..5035cae 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -2958,6 +2958,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 @@ -2968,6 +2979,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); } } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 95c473c..7a9d82c 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -96,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; @@ -110,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 @@ -201,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; @@ -225,7 +226,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, // Input event queue. QueuedInputEvent mFirstPendingInputEvent; QueuedInputEvent mCurrentInputEvent; - boolean mProcessInputEventsPending; + boolean mProcessInputEventsScheduled; boolean mWindowAttributesChanged = false; int mWindowAttributesChangesFlag = 0; @@ -374,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) { @@ -425,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); @@ -794,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(); } } @@ -847,12 +847,45 @@ public final class ViewRootImpl extends Handler implements ViewParent, } } + 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(); - if (DBG) { System.out.println("======================================"); System.out.println("performTraversals"); @@ -862,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; @@ -890,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; @@ -905,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) { @@ -949,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; } @@ -1287,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) { @@ -1323,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) { @@ -1609,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; @@ -1619,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) { @@ -1663,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 @@ -1814,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()) { @@ -1852,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) { @@ -1868,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(); @@ -1886,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() @@ -1925,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(); } @@ -2045,23 +2076,23 @@ 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 { + final long unlockCanvasAndPostStartTime; + if (ViewDebug.DEBUG_LATENCY) { + unlockCanvasAndPostStartTime = System.nanoTime(); + } - surface.unlockCanvasAndPost(canvas); + surface.unlockCanvasAndPost(canvas); - if (ViewDebug.DEBUG_LATENCY) { - long now = System.nanoTime(); - Log.d(ViewDebug.DEBUG_LATENCY_TAG, "- unlockCanvasAndPost() took " - + ((now - unlockCanvasAndPostStartTime) * 0.000001f) + "ms"); - } + 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 (LOCAL_LOGV) { + Log.v(TAG, "Surface " + surface + " unlockCanvasAndPost"); + } } } } @@ -2297,6 +2328,8 @@ public final class ViewRootImpl extends Handler implements ViewParent, mInputChannel.dispose(); mInputChannel = null; } + + mChoreographer.removeOnDrawListener(this); } void updateConfiguration(Configuration config, boolean force) { @@ -2351,14 +2384,11 @@ 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 IME_FINISHED_EVENT = 1010; @@ -2380,8 +2410,6 @@ public final class ViewRootImpl extends Handler implements ViewParent, @Override public String getMessageName(Message message) { switch (message.what) { - case DO_TRAVERSAL: - return "DO_TRAVERSAL"; case DIE: return "DIE"; case RESIZED: @@ -2392,10 +2420,6 @@ 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: @@ -2445,45 +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; - 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; - } - break; case IME_FINISHED_EVENT: handleImeFinishedEvent(msg.arg1, msg.arg2 != 0); break; case DO_PROCESS_INPUT_EVENTS: - mProcessInputEventsPending = false; - processInputEvents(); + mProcessInputEventsScheduled = false; + doProcessInputEvents(); break; case DISPATCH_APP_VISIBILITY: handleAppVisibility(msg.arg1 != 0); @@ -2594,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 " @@ -3782,13 +3777,13 @@ public final class ViewRootImpl extends Handler implements ViewParent, } private void scheduleProcessInputEvents() { - if (!mProcessInputEventsPending) { - mProcessInputEventsPending = true; + if (!mProcessInputEventsScheduled) { + mProcessInputEventsScheduled = true; sendEmptyMessage(DO_PROCESS_INPUT_EVENTS); } } - void processInputEvents() { + private void doProcessInputEvents() { while (mCurrentInputEvent == null && mFirstPendingInputEvent != null) { QueuedInputEvent q = mFirstPendingInputEvent; mFirstPendingInputEvent = q.mNext; @@ -3799,8 +3794,8 @@ public final class ViewRootImpl extends Handler implements ViewParent, // We are done processing all input events that we can process right now // so we can clear the pending flag immediately. - if (mProcessInputEventsPending) { - mProcessInputEventsPending = false; + if (mProcessInputEventsScheduled) { + mProcessInputEventsScheduled = false; removeMessages(DO_PROCESS_INPUT_EVENTS); } } diff --git a/core/java/android/webkit/WebTextView.java b/core/java/android/webkit/WebTextView.java index f18a396..3574a0d 100644 --- a/core/java/android/webkit/WebTextView.java +++ b/core/java/android/webkit/WebTextView.java @@ -237,7 +237,6 @@ import java.util.ArrayList; private void growOrShrink(boolean grow) { AbsoluteLayout.LayoutParams lp = (AbsoluteLayout.LayoutParams) getLayoutParams(); if (grow) { - Log.i("webtextview", "grow"); lp.x -= mRingInset; lp.y -= mRingInset; lp.width += 2 * mRingInset; @@ -245,7 +244,6 @@ import java.util.ArrayList; setPadding(getPaddingLeft() + mRingInset, getPaddingTop() + mRingInset, getPaddingRight() + mRingInset, getPaddingBottom() + mRingInset); } else { - Log.i("webtextview", "shrink"); lp.x += mRingInset; lp.y += mRingInset; lp.width -= 2 * mRingInset; diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index 4958d3c..b68dec9 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -4250,6 +4250,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) { @@ -5833,7 +5836,8 @@ public class WebView extends AbsoluteLayout } calcOurContentVisibleRectF(mVisibleContentRect); nativeUpdateDrawGLFunction(mGLViewportEmpty ? null : mGLRectViewport, - mGLViewportEmpty ? null : mViewRectViewport, mVisibleContentRect); + mGLViewportEmpty ? null : mViewRectViewport, + mVisibleContentRect); } /** @@ -5980,6 +5984,7 @@ public class WebView extends AbsoluteLayout if (inFullScreenMode()) { mFullScreenHolder.hide(); mFullScreenHolder = null; + invalidate(); } } @@ -8655,6 +8660,7 @@ public class WebView extends AbsoluteLayout mFullScreenHolder = new PluginFullScreenHolder(WebView.this, orientation, npp); mFullScreenHolder.setContentView(view); mFullScreenHolder.show(); + invalidate(); break; } diff --git a/core/java/android/widget/SpellChecker.java b/core/java/android/widget/SpellChecker.java index d03db10..4bd7165 100644 --- a/core/java/android/widget/SpellChecker.java +++ b/core/java/android/widget/SpellChecker.java @@ -272,9 +272,11 @@ public class SpellChecker implements SpellCheckerSessionListener { ((attributes & SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO) > 0); SpellCheckSpan spellCheckSpan = mSpellCheckSpans[j]; + if (!isInDictionary && looksLikeTypo) { createMisspelledSuggestionSpan(editable, suggestionsInfo, spellCheckSpan); } + editable.removeSpan(spellCheckSpan); break; } @@ -295,20 +297,21 @@ public class SpellChecker implements SpellCheckerSessionListener { }, SPELL_PAUSE_DURATION); } - private void createMisspelledSuggestionSpan(Editable editable, - SuggestionsInfo suggestionsInfo, SpellCheckSpan spellCheckSpan) { + private void createMisspelledSuggestionSpan(Editable editable, SuggestionsInfo suggestionsInfo, + SpellCheckSpan spellCheckSpan) { final int start = editable.getSpanStart(spellCheckSpan); final int end = editable.getSpanEnd(spellCheckSpan); - if (start < 0 || end < 0) return; // span was removed in the meantime + if (start < 0 || end <= start) return; // span was removed in the meantime // Other suggestion spans may exist on that region, with identical suggestions, filter - // them out to avoid duplicates. First, filter suggestion spans on that exact region. + // them out to avoid duplicates. SuggestionSpan[] suggestionSpans = editable.getSpans(start, end, SuggestionSpan.class); final int length = suggestionSpans.length; for (int i = 0; i < length; i++) { final int spanStart = editable.getSpanStart(suggestionSpans[i]); final int spanEnd = editable.getSpanEnd(suggestionSpans[i]); if (spanStart != start || spanEnd != end) { + // Nulled (to avoid new array allocation) if not on that exact same region suggestionSpans[i] = null; } } @@ -355,6 +358,8 @@ public class SpellChecker implements SpellCheckerSessionListener { SuggestionSpan suggestionSpan = new SuggestionSpan(mTextView.getContext(), suggestions, SuggestionSpan.FLAG_EASY_CORRECT | SuggestionSpan.FLAG_MISSPELLED); editable.setSpan(suggestionSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + mTextView.invalidateRegion(start, end); } private class SpellParser { diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 1fab1ca..82bcd3e 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -4326,15 +4326,24 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } private void invalidateCursor(int a, int b, int c) { + if (a >= 0 || b >= 0 || c >= 0) { + int start = Math.min(Math.min(a, b), c); + int end = Math.max(Math.max(a, b), c); + invalidateRegion(start, end); + } + } + + /** + * Invalidates the region of text enclosed between the start and end text offsets. + * + * @hide + */ + void invalidateRegion(int start, int end) { if (mLayout == null) { invalidate(); } else { - if (a >= 0 || b >= 0 || c >= 0) { - int first = Math.min(Math.min(a, b), c); - int last = Math.max(Math.max(a, b), c); - - int line = mLayout.getLineForOffset(first); - int top = mLayout.getLineTop(line); + int lineStart = mLayout.getLineForOffset(start); + int top = mLayout.getLineTop(lineStart); // This is ridiculous, but the descent from the line above // can hang down into the line we really want to redraw, @@ -4342,36 +4351,36 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // sure everything that needs to be redrawn really is. // (But not the whole line above, because that would cause // the same problem with the descenders on the line above it!) - if (line > 0) { - top -= mLayout.getLineDescent(line - 1); + if (lineStart > 0) { + top -= mLayout.getLineDescent(lineStart - 1); } - int line2; + int lineEnd; - if (first == last) - line2 = line; + if (start == end) + lineEnd = lineStart; else - line2 = mLayout.getLineForOffset(last); + lineEnd = mLayout.getLineForOffset(end); - int bottom = mLayout.getLineTop(line2 + 1); + int bottom = mLayout.getLineBottom(lineEnd); - final int horizontalPadding = getCompoundPaddingLeft(); + final int compoundPaddingLeft = getCompoundPaddingLeft(); final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true); - - // If used, the cursor drawables can have an arbitrary dimension that can go beyond - // the invalidated lines specified above. - for (int i = 0; i < mCursorCount; i++) { - Rect bounds = mCursorDrawable[i].getBounds(); - top = Math.min(top, bounds.top); - bottom = Math.max(bottom, bounds.bottom); - // Horizontal bounds are already full width, no need to update + + int left, right; + if (lineStart == lineEnd) { + left = (int) mLayout.getPrimaryHorizontal(start); + right = (int) (mLayout.getPrimaryHorizontal(end) + 1.0); + left += compoundPaddingLeft; + right += compoundPaddingLeft; + } else { + // Rectangle bounding box when the region spans several lines + left = compoundPaddingLeft; + right = getWidth() - getCompoundPaddingRight(); } - invalidate(horizontalPadding + mScrollX, top + verticalPadding, - horizontalPadding + mScrollX + getWidth() - - getCompoundPaddingLeft() - getCompoundPaddingRight(), - bottom + verticalPadding); - } + invalidate(mScrollX + left, verticalPadding + top, + mScrollX + right, verticalPadding + bottom); } } @@ -5904,10 +5913,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (cursorOffsetVertical != 0) { canvas.translate(0, -cursorOffsetVertical); } - invalidate(true); + invalidate(true); // TODO invalidate cursor region only } else { stopAnimation(); - invalidate(false); + invalidate(false); // TODO invalidate cursor region only } } @@ -7729,10 +7738,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener onSelectionChanged(newSelStart, newSelEnd); } } - - if (what instanceof UpdateAppearance || what instanceof ParagraphStyle - || (what instanceof SuggestionSpan && (((SuggestionSpan)what).getFlags() - & SuggestionSpan.FLAG_AUTO_CORRECTION) != 0)) { + + if (what instanceof UpdateAppearance || what instanceof ParagraphStyle) { if (ims == null || ims.mBatchEditNesting == 0) { invalidate(); mHighlightPathBogus = true; |
