diff options
Diffstat (limited to 'core/java')
46 files changed, 2751 insertions, 1147 deletions
diff --git a/core/java/android/animation/LayoutTransition.java b/core/java/android/animation/LayoutTransition.java index 634e4d8..c643137 100644 --- a/core/java/android/animation/LayoutTransition.java +++ b/core/java/android/animation/LayoutTransition.java @@ -119,6 +119,24 @@ public class LayoutTransition { public static final int DISAPPEARING = 3; /** + * A flag indicating the animation that runs on those items that are changing + * due to a layout change not caused by items being added to or removed + * from the container. This transition type is not enabled by default; it can be + * enabled via {@link #enableTransitionType(int)}. + */ + public static final int CHANGING = 4; + + /** + * Private bit fields used to set the collection of enabled transition types for + * mTransitionTypes. + */ + private static final int FLAG_APPEARING = 0x01; + private static final int FLAG_DISAPPEARING = 0x02; + private static final int FLAG_CHANGE_APPEARING = 0x04; + private static final int FLAG_CHANGE_DISAPPEARING = 0x08; + private static final int FLAG_CHANGING = 0x10; + + /** * These variables hold the animations that are currently used to run the transition effects. * These animations are set to defaults, but can be changed to custom animations by * calls to setAnimator(). @@ -127,11 +145,13 @@ public class LayoutTransition { private Animator mAppearingAnim = null; private Animator mChangingAppearingAnim = null; private Animator mChangingDisappearingAnim = null; + private Animator mChangingAnim = null; /** * These are the default animations, defined in the constructor, that will be used * unless the user specifies custom animations. */ + private static ObjectAnimator defaultChange; private static ObjectAnimator defaultChangeIn; private static ObjectAnimator defaultChangeOut; private static ObjectAnimator defaultFadeIn; @@ -143,15 +163,16 @@ public class LayoutTransition { private static long DEFAULT_DURATION = 300; /** - * The durations of the four different animations + * The durations of the different animations */ private long mChangingAppearingDuration = DEFAULT_DURATION; private long mChangingDisappearingDuration = DEFAULT_DURATION; + private long mChangingDuration = DEFAULT_DURATION; private long mAppearingDuration = DEFAULT_DURATION; private long mDisappearingDuration = DEFAULT_DURATION; /** - * The start delays of the four different animations. Note that the default behavior of + * The start delays of the different animations. Note that the default behavior of * the appearing item is the default duration, since it should wait for the items to move * before fading it. Same for the changing animation when disappearing; it waits for the item * to fade out before moving the other items. @@ -160,12 +181,14 @@ public class LayoutTransition { private long mDisappearingDelay = 0; private long mChangingAppearingDelay = 0; private long mChangingDisappearingDelay = DEFAULT_DURATION; + private long mChangingDelay = 0; /** - * The inter-animation delays used on the two changing animations + * The inter-animation delays used on the changing animations */ private long mChangingAppearingStagger = 0; private long mChangingDisappearingStagger = 0; + private long mChangingStagger = 0; /** * The default interpolators used for the animations @@ -174,6 +197,7 @@ public class LayoutTransition { private TimeInterpolator mDisappearingInterpolator = new AccelerateDecelerateInterpolator(); private TimeInterpolator mChangingAppearingInterpolator = new DecelerateInterpolator(); private TimeInterpolator mChangingDisappearingInterpolator = new DecelerateInterpolator(); + private TimeInterpolator mChangingInterpolator = new DecelerateInterpolator(); /** * These hashmaps are used to store the animations that are currently running as part of @@ -212,6 +236,13 @@ public class LayoutTransition { private long staggerDelay; /** + * These are the types of transition animations that the LayoutTransition is reacting + * to. By default, appearing/disappearing and the change animations related to them are + * enabled (not CHANGING). + */ + private int mTransitionTypes = FLAG_CHANGE_APPEARING | FLAG_CHANGE_DISAPPEARING | + FLAG_APPEARING | FLAG_DISAPPEARING; + /** * The set of listeners that should be notified when APPEARING/DISAPPEARING transitions * start and end. */ @@ -248,6 +279,9 @@ public class LayoutTransition { defaultChangeOut = defaultChangeIn.clone(); defaultChangeOut.setStartDelay(mChangingDisappearingDelay); defaultChangeOut.setInterpolator(mChangingDisappearingInterpolator); + defaultChange = defaultChangeIn.clone(); + defaultChange.setStartDelay(mChangingDelay); + defaultChange.setInterpolator(mChangingInterpolator); defaultFadeIn = ObjectAnimator.ofFloat(null, "alpha", 0f, 1f); defaultFadeIn.setDuration(DEFAULT_DURATION); @@ -260,6 +294,7 @@ public class LayoutTransition { } mChangingAppearingAnim = defaultChangeIn; mChangingDisappearingAnim = defaultChangeOut; + mChangingAnim = defaultChange; mAppearingAnim = defaultFadeIn; mDisappearingAnim = defaultFadeOut; } @@ -275,18 +310,101 @@ public class LayoutTransition { public void setDuration(long duration) { mChangingAppearingDuration = duration; mChangingDisappearingDuration = duration; + mChangingDuration = duration; mAppearingDuration = duration; mDisappearingDuration = duration; } /** + * Enables the specified transitionType for this LayoutTransition object. + * By default, a LayoutTransition listens for changes in children being + * added/remove/hidden/shown in the container, and runs the animations associated with + * those events. That is, all transition types besides {@link #CHANGING} are enabled by default. + * You can also enable {@link #CHANGING} animations by calling this method with the + * {@link #CHANGING} transitionType. + * + * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, + * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}. + */ + public void enableTransitionType(int transitionType) { + switch (transitionType) { + case APPEARING: + mTransitionTypes |= FLAG_APPEARING; + break; + case DISAPPEARING: + mTransitionTypes |= FLAG_DISAPPEARING; + break; + case CHANGE_APPEARING: + mTransitionTypes |= FLAG_CHANGE_APPEARING; + break; + case CHANGE_DISAPPEARING: + mTransitionTypes |= FLAG_CHANGE_DISAPPEARING; + break; + case CHANGING: + mTransitionTypes |= FLAG_CHANGING; + break; + } + } + + /** + * Disables the specified transitionType for this LayoutTransition object. + * By default, all transition types except {@link #CHANGING} are enabled. + * + * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, + * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}. + */ + public void disableTransitionType(int transitionType) { + switch (transitionType) { + case APPEARING: + mTransitionTypes &= ~FLAG_APPEARING; + break; + case DISAPPEARING: + mTransitionTypes &= ~FLAG_DISAPPEARING; + break; + case CHANGE_APPEARING: + mTransitionTypes &= ~FLAG_CHANGE_APPEARING; + break; + case CHANGE_DISAPPEARING: + mTransitionTypes &= ~FLAG_CHANGE_DISAPPEARING; + break; + case CHANGING: + mTransitionTypes &= ~FLAG_CHANGING; + break; + } + } + + /** + * Returns whether the specified transitionType is enabled for this LayoutTransition object. + * By default, all transition types except {@link #CHANGING} are enabled. + * + * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, + * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}. + * @return true if the specified transitionType is currently enabled, false otherwise. + */ + public boolean isTransitionTypeEnabled(int transitionType) { + switch (transitionType) { + case APPEARING: + return (mTransitionTypes & FLAG_APPEARING) == FLAG_APPEARING; + case DISAPPEARING: + return (mTransitionTypes & FLAG_DISAPPEARING) == FLAG_DISAPPEARING; + case CHANGE_APPEARING: + return (mTransitionTypes & FLAG_CHANGE_APPEARING) == FLAG_CHANGE_APPEARING; + case CHANGE_DISAPPEARING: + return (mTransitionTypes & FLAG_CHANGE_DISAPPEARING) == FLAG_CHANGE_DISAPPEARING; + case CHANGING: + return (mTransitionTypes & FLAG_CHANGING) == FLAG_CHANGING; + } + return false; + } + + /** * Sets the start delay on one of the animation objects used by this transition. The * <code>transitionType</code> parameter determines the animation whose start delay * is being set. * - * @param transitionType one of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, - * {@link #APPEARING}, or {@link #DISAPPEARING}, which determines the animation whose start - * delay is being set. + * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, + * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}, which determines + * the animation whose start delay is being set. * @param delay The length of time, in milliseconds, to delay before starting the animation. * @see Animator#setStartDelay(long) */ @@ -298,6 +416,9 @@ public class LayoutTransition { case CHANGE_DISAPPEARING: mChangingDisappearingDelay = delay; break; + case CHANGING: + mChangingDelay = delay; + break; case APPEARING: mAppearingDelay = delay; break; @@ -312,9 +433,9 @@ public class LayoutTransition { * <code>transitionType</code> parameter determines the animation whose start delay * is returned. * - * @param transitionType one of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, - * {@link #APPEARING}, or {@link #DISAPPEARING}, which determines the animation whose start - * delay is returned. + * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, + * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}, which determines + * the animation whose start delay is returned. * @return long The start delay of the specified animation. * @see Animator#getStartDelay() */ @@ -324,6 +445,8 @@ public class LayoutTransition { return mChangingAppearingDelay; case CHANGE_DISAPPEARING: return mChangingDisappearingDelay; + case CHANGING: + return mChangingDelay; case APPEARING: return mAppearingDelay; case DISAPPEARING: @@ -338,9 +461,9 @@ public class LayoutTransition { * <code>transitionType</code> parameter determines the animation whose duration * is being set. * - * @param transitionType one of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, - * {@link #APPEARING}, or {@link #DISAPPEARING}, which determines the animation whose - * duration is being set. + * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, + * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}, which determines + * the animation whose duration is being set. * @param duration The length of time, in milliseconds, that the specified animation should run. * @see Animator#setDuration(long) */ @@ -352,6 +475,9 @@ public class LayoutTransition { case CHANGE_DISAPPEARING: mChangingDisappearingDuration = duration; break; + case CHANGING: + mChangingDuration = duration; + break; case APPEARING: mAppearingDuration = duration; break; @@ -366,9 +492,9 @@ public class LayoutTransition { * <code>transitionType</code> parameter determines the animation whose duration * is returned. * - * @param transitionType one of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, - * {@link #APPEARING}, or {@link #DISAPPEARING}, which determines the animation whose - * duration is returned. + * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, + * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}, which determines + * the animation whose duration is returned. * @return long The duration of the specified animation. * @see Animator#getDuration() */ @@ -378,6 +504,8 @@ public class LayoutTransition { return mChangingAppearingDuration; case CHANGE_DISAPPEARING: return mChangingDisappearingDuration; + case CHANGING: + return mChangingDuration; case APPEARING: return mAppearingDuration; case DISAPPEARING: @@ -389,9 +517,10 @@ public class LayoutTransition { /** * Sets the length of time to delay between starting each animation during one of the - * CHANGE animations. + * change animations. * - * @param transitionType A value of {@link #CHANGE_APPEARING} or @link #CHANGE_DISAPPEARING}. + * @param transitionType A value of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, or + * {@link #CHANGING}. * @param duration The length of time, in milliseconds, to delay before launching the next * animation in the sequence. */ @@ -403,15 +532,19 @@ public class LayoutTransition { case CHANGE_DISAPPEARING: mChangingDisappearingStagger = duration; break; + case CHANGING: + mChangingStagger = duration; + break; // noop other cases } } /** - * Tets the length of time to delay between starting each animation during one of the - * CHANGE animations. + * Gets the length of time to delay between starting each animation during one of the + * change animations. * - * @param transitionType A value of {@link #CHANGE_APPEARING} or @link #CHANGE_DISAPPEARING}. + * @param transitionType A value of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, or + * {@link #CHANGING}. * @return long The length of time, in milliseconds, to delay before launching the next * animation in the sequence. */ @@ -421,6 +554,8 @@ public class LayoutTransition { return mChangingAppearingStagger; case CHANGE_DISAPPEARING: return mChangingDisappearingStagger; + case CHANGING: + return mChangingStagger; } // shouldn't reach here return 0; @@ -431,9 +566,9 @@ public class LayoutTransition { * <code>transitionType</code> parameter determines the animation whose interpolator * is being set. * - * @param transitionType one of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, - * {@link #APPEARING}, or {@link #DISAPPEARING}, which determines the animation whose - * duration is being set. + * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, + * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}, which determines + * the animation whose interpolator is being set. * @param interpolator The interpolator that the specified animation should use. * @see Animator#setInterpolator(TimeInterpolator) */ @@ -445,6 +580,9 @@ public class LayoutTransition { case CHANGE_DISAPPEARING: mChangingDisappearingInterpolator = interpolator; break; + case CHANGING: + mChangingInterpolator = interpolator; + break; case APPEARING: mAppearingInterpolator = interpolator; break; @@ -459,9 +597,9 @@ public class LayoutTransition { * <code>transitionType</code> parameter determines the animation whose interpolator * is returned. * - * @param transitionType one of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, - * {@link #APPEARING}, or {@link #DISAPPEARING}, which determines the animation whose - * duration is being set. + * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, + * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}, which determines + * the animation whose interpolator is being returned. * @return TimeInterpolator The interpolator that the specified animation uses. * @see Animator#setInterpolator(TimeInterpolator) */ @@ -471,6 +609,8 @@ public class LayoutTransition { return mChangingAppearingInterpolator; case CHANGE_DISAPPEARING: return mChangingDisappearingInterpolator; + case CHANGING: + return mChangingInterpolator; case APPEARING: return mAppearingInterpolator; case DISAPPEARING: @@ -504,9 +644,9 @@ public class LayoutTransition { * values queried when the transition begins may need to use a different mechanism * than a standard ObjectAnimator object.</p> * - * @param transitionType one of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, - * {@link #APPEARING}, or {@link #DISAPPEARING}, which determines the animation whose - * duration is being set. + * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, + * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}, which determines the + * animation whose animator is being set. * @param animator The animation being assigned. A value of <code>null</code> means that no * animation will be run for the specified transitionType. */ @@ -518,6 +658,9 @@ public class LayoutTransition { case CHANGE_DISAPPEARING: mChangingDisappearingAnim = animator; break; + case CHANGING: + mChangingAnim = animator; + break; case APPEARING: mAppearingAnim = animator; break; @@ -530,9 +673,9 @@ public class LayoutTransition { /** * Gets the animation used during one of the transition types that may run. * - * @param transitionType one of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, - * {@link #APPEARING}, or {@link #DISAPPEARING}, which determines the animation whose - * duration is being set. + * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, + * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}, which determines + * the animation whose animator is being returned. * @return Animator The animation being used for the given transition type. * @see #setAnimator(int, Animator) */ @@ -542,6 +685,8 @@ public class LayoutTransition { return mChangingAppearingAnim; case CHANGE_DISAPPEARING: return mChangingDisappearingAnim; + case CHANGING: + return mChangingAnim; case APPEARING: return mAppearingAnim; case DISAPPEARING: @@ -554,20 +699,44 @@ public class LayoutTransition { /** * This function sets up animations on all of the views that change during layout. * For every child in the parent, we create a change animation of the appropriate - * type (appearing or disappearing) and ask it to populate its start values from its + * type (appearing, disappearing, or changing) and ask it to populate its start values from its * target view. We add layout listeners to all child views and listen for changes. For * those views that change, we populate the end values for those animations and start them. * Animations are not run on unchanging views. * - * @param parent The container which is undergoing an appearing or disappearing change. - * @param newView The view being added to or removed from the parent. - * @param changeReason A value of APPEARING or DISAPPEARING, indicating whether the - * transition is occuring because an item is being added to or removed from the parent. + * @param parent The container which is undergoing a change. + * @param newView The view being added to or removed from the parent. May be null if the + * changeReason is CHANGING. + * @param changeReason A value of APPEARING, DISAPPEARING, or CHANGING, indicating whether the + * transition is occurring because an item is being added to or removed from the parent, or + * if it is running in response to a layout operation (that is, if the value is CHANGING). */ private void runChangeTransition(final ViewGroup parent, View newView, final int changeReason) { - Animator baseAnimator = (changeReason == APPEARING) ? - mChangingAppearingAnim : mChangingDisappearingAnim; + Animator baseAnimator = null; + Animator parentAnimator = null; + final long duration; + switch (changeReason) { + case APPEARING: + baseAnimator = mChangingAppearingAnim; + duration = mChangingAppearingDuration; + parentAnimator = defaultChangeIn; + break; + case DISAPPEARING: + baseAnimator = mChangingDisappearingAnim; + duration = mChangingDisappearingDuration; + parentAnimator = defaultChangeOut; + break; + case CHANGING: + baseAnimator = mChangingAnim; + duration = mChangingDuration; + parentAnimator = defaultChange; + break; + default: + // Shouldn't reach here + duration = 0; + break; + } // If the animation is null, there's nothing to do if (baseAnimator == null) { return; @@ -575,8 +744,6 @@ public class LayoutTransition { // reset the inter-animation delay, in case we use it later staggerDelay = 0; - final long duration = (changeReason == APPEARING) ? - mChangingAppearingDuration : mChangingDisappearingDuration; final ViewTreeObserver observer = parent.getViewTreeObserver(); // used for later cleanup if (!observer.isAlive()) { @@ -594,8 +761,6 @@ public class LayoutTransition { } } if (mAnimateParentHierarchy) { - Animator parentAnimator = (changeReason == APPEARING) ? - defaultChangeIn : defaultChangeOut; ViewGroup tempParent = parent; while (tempParent != null) { ViewParent parentParent = tempParent.getParent(); @@ -727,13 +892,20 @@ public class LayoutTransition { } } - long startDelay; - if (changeReason == APPEARING) { - startDelay = mChangingAppearingDelay + staggerDelay; - staggerDelay += mChangingAppearingStagger; - } else { - startDelay = mChangingDisappearingDelay + staggerDelay; - staggerDelay += mChangingDisappearingStagger; + long startDelay = 0; + switch (changeReason) { + case APPEARING: + startDelay = mChangingAppearingDelay + staggerDelay; + staggerDelay += mChangingAppearingStagger; + break; + case DISAPPEARING: + startDelay = mChangingDisappearingDelay + staggerDelay; + staggerDelay += mChangingDisappearingStagger; + break; + case CHANGING: + startDelay = mChangingDelay + staggerDelay; + staggerDelay += mChangingStagger; + break; } anim.setStartDelay(startDelay); anim.setDuration(duration); @@ -766,7 +938,8 @@ public class LayoutTransition { for (TransitionListener listener : mListeners) { listener.startTransition(LayoutTransition.this, parent, child, changeReason == APPEARING ? - CHANGE_APPEARING : CHANGE_DISAPPEARING); + CHANGE_APPEARING : changeReason == DISAPPEARING ? + CHANGE_DISAPPEARING : CHANGING); } } } @@ -784,7 +957,8 @@ public class LayoutTransition { for (TransitionListener listener : mListeners) { listener.endTransition(LayoutTransition.this, parent, child, changeReason == APPEARING ? - CHANGE_APPEARING : CHANGE_DISAPPEARING); + CHANGE_APPEARING : changeReason == DISAPPEARING ? + CHANGE_DISAPPEARING : CHANGING); } } } @@ -831,6 +1005,8 @@ public class LayoutTransition { anim.start(); anim.end(); } + // listeners should clean up the currentChangingAnimations list, but just in case... + currentChangingAnimations.clear(); } /** @@ -902,6 +1078,7 @@ public class LayoutTransition { switch (transitionType) { case CHANGE_APPEARING: case CHANGE_DISAPPEARING: + case CHANGING: if (currentChangingAnimations.size() > 0) { LinkedHashMap<View, Animator> currentAnimCopy = (LinkedHashMap<View, Animator>) currentChangingAnimations.clone(); @@ -1031,21 +1208,48 @@ public class LayoutTransition { * affect CHANGE_APPEARING or CHANGE_DISAPPEARING animations. */ private void addChild(ViewGroup parent, View child, boolean changesLayout) { - // Want disappearing animations to finish up before proceeding - cancel(DISAPPEARING); - if (changesLayout) { + if ((mTransitionTypes & FLAG_APPEARING) == FLAG_APPEARING) { + // Want disappearing animations to finish up before proceeding + cancel(DISAPPEARING); + } + if (changesLayout && (mTransitionTypes & FLAG_CHANGE_APPEARING) == FLAG_CHANGE_APPEARING) { // Also, cancel changing animations so that we start fresh ones from current locations cancel(CHANGE_APPEARING); + cancel(CHANGING); } - if (mListeners != null) { + if (mListeners != null && (mTransitionTypes & FLAG_APPEARING) == FLAG_APPEARING) { for (TransitionListener listener : mListeners) { listener.startTransition(this, parent, child, APPEARING); } } - if (changesLayout) { + if (changesLayout && (mTransitionTypes & FLAG_CHANGE_APPEARING) == FLAG_CHANGE_APPEARING) { runChangeTransition(parent, child, APPEARING); } - runAppearingTransition(parent, child); + if ((mTransitionTypes & FLAG_APPEARING) == FLAG_APPEARING) { + runAppearingTransition(parent, child); + } + } + + /** + * This method is called by ViewGroup when there is a call to layout() on the container + * with this LayoutTransition. If the CHANGING transition is enabled and if there is no other + * transition currently running on the container, then this call runs a CHANGING transition. + * The transition does not start immediately; it just sets up the mechanism to run if any + * of the children of the container change their layout parameters (similar to + * the CHANGE_APPEARING and CHANGE_DISAPPEARING transitions). + * + * @param parent The ViewGroup whose layout() method has been called. + * + * @hide + */ + public void layoutChange(ViewGroup parent) { + if ((mTransitionTypes & FLAG_CHANGING) == FLAG_CHANGING && !isRunning()) { + // This method is called for all calls to layout() in the container, including + // those caused by add/remove/hide/show events, which will already have set up + // transition animations. Avoid setting up CHANGING animations in this case; only + // do so when there is not a transition already running on the container. + runChangeTransition(parent, null, CHANGING); + } } /** @@ -1097,21 +1301,28 @@ public class LayoutTransition { * affect CHANGE_APPEARING or CHANGE_DISAPPEARING animations. */ private void removeChild(ViewGroup parent, View child, boolean changesLayout) { - // Want appearing animations to finish up before proceeding - cancel(APPEARING); - if (changesLayout) { + if ((mTransitionTypes & FLAG_DISAPPEARING) == FLAG_DISAPPEARING) { + // Want appearing animations to finish up before proceeding + cancel(APPEARING); + } + if (changesLayout && + (mTransitionTypes & FLAG_CHANGE_DISAPPEARING) == FLAG_CHANGE_DISAPPEARING) { // Also, cancel changing animations so that we start fresh ones from current locations cancel(CHANGE_DISAPPEARING); + cancel(CHANGING); } - if (mListeners != null) { + if (mListeners != null && (mTransitionTypes & FLAG_DISAPPEARING) == FLAG_DISAPPEARING) { for (TransitionListener listener : mListeners) { listener.startTransition(this, parent, child, DISAPPEARING); } } - if (changesLayout) { + if (changesLayout && + (mTransitionTypes & FLAG_CHANGE_DISAPPEARING) == FLAG_CHANGE_DISAPPEARING) { runChangeTransition(parent, child, DISAPPEARING); } - runDisappearingTransition(parent, child); + if ((mTransitionTypes & FLAG_DISAPPEARING) == FLAG_DISAPPEARING) { + runDisappearingTransition(parent, child); + } } /** diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java index fade20c..2154b14 100755 --- a/core/java/android/animation/ValueAnimator.java +++ b/core/java/android/animation/ValueAnimator.java @@ -150,6 +150,13 @@ public class ValueAnimator extends Animator { private boolean mStarted = false; /** + * Tracks whether we've notified listeners of the onAnimationSTart() event. This can be + * complex to keep track of since we notify listeners at different times depending on + * startDelay and whether start() was called before end(). + */ + private boolean mStartListenersCalled = false; + + /** * Flag that denotes whether the animation is set up and ready to go. Used to * set up animation that has not yet been started. */ @@ -885,6 +892,18 @@ public class ValueAnimator extends Animator { } } + private void notifyStartListeners() { + if (mListeners != null && !mStartListenersCalled) { + ArrayList<AnimatorListener> tmpListeners = + (ArrayList<AnimatorListener>) mListeners.clone(); + int numListeners = tmpListeners.size(); + for (int i = 0; i < numListeners; ++i) { + tmpListeners.get(i).onAnimationStart(this); + } + } + mStartListenersCalled = true; + } + /** * Start the animation playing. This version of start() takes a boolean flag that indicates * whether the animation should play in reverse. The flag is usually false, but may be set @@ -914,15 +933,7 @@ public class ValueAnimator extends Animator { setCurrentPlayTime(getCurrentPlayTime()); mPlayingState = STOPPED; mRunning = true; - - if (mListeners != null) { - ArrayList<AnimatorListener> tmpListeners = - (ArrayList<AnimatorListener>) mListeners.clone(); - int numListeners = tmpListeners.size(); - for (int i = 0; i < numListeners; ++i) { - tmpListeners.get(i).onAnimationStart(this); - } - } + notifyStartListeners(); } animationHandler.sendEmptyMessage(ANIMATION_START); } @@ -941,7 +952,11 @@ public class ValueAnimator extends Animator { || handler.mPendingAnimations.contains(this) || handler.mDelayedAnims.contains(this)) { // Only notify listeners if the animator has actually started - if (mRunning && mListeners != null) { + if ((mStarted || mRunning) && mListeners != null) { + if (!mRunning) { + // If it's not yet running, then start listeners weren't called. Call them now. + notifyStartListeners(); + } ArrayList<AnimatorListener> tmpListeners = (ArrayList<AnimatorListener>) mListeners.clone(); for (AnimatorListener listener : tmpListeners) { @@ -959,6 +974,7 @@ public class ValueAnimator extends Animator { // Special case if the animation has not yet started; get it ready for ending mStartedDelay = false; startAnimation(handler); + mStarted = true; } else if (!mInitialized) { initAnimation(); } @@ -1010,7 +1026,11 @@ public class ValueAnimator extends Animator { handler.mPendingAnimations.remove(this); handler.mDelayedAnims.remove(this); mPlayingState = STOPPED; - if (mRunning && mListeners != null) { + if ((mStarted || mRunning) && mListeners != null) { + if (!mRunning) { + // If it's not yet running, then start listeners weren't called. Call them now. + notifyStartListeners(); + } ArrayList<AnimatorListener> tmpListeners = (ArrayList<AnimatorListener>) mListeners.clone(); int numListeners = tmpListeners.size(); @@ -1020,6 +1040,7 @@ public class ValueAnimator extends Animator { } mRunning = false; mStarted = false; + mStartListenersCalled = false; } /** @@ -1032,12 +1053,7 @@ public class ValueAnimator extends Animator { if (mStartDelay > 0 && mListeners != null) { // Listeners were already notified in start() if startDelay is 0; this is // just for delayed animations - ArrayList<AnimatorListener> tmpListeners = - (ArrayList<AnimatorListener>) mListeners.clone(); - int numListeners = tmpListeners.size(); - for (int i = 0; i < numListeners; ++i) { - tmpListeners.get(i).onAnimationStart(this); - } + notifyStartListeners(); } } diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index c3cceaf..423b02a 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -73,6 +73,18 @@ public class ActivityOptions { public static final String KEY_ANIM_START_Y = "android:animStartY"; /** + * Initial width of the animation. + * @hide + */ + public static final String KEY_ANIM_START_WIDTH = "android:animStartWidth"; + + /** + * Initial height of the animation. + * @hide + */ + public static final String KEY_ANIM_START_HEIGHT = "android:animStartHeight"; + + /** * Callback for when animation is started. * @hide */ @@ -83,7 +95,9 @@ public class ActivityOptions { /** @hide */ public static final int ANIM_CUSTOM = 1; /** @hide */ - public static final int ANIM_THUMBNAIL = 2; + public static final int ANIM_SCALE_UP = 2; + /** @hide */ + public static final int ANIM_THUMBNAIL = 3; private String mPackageName; private int mAnimationType = ANIM_NONE; @@ -92,6 +106,8 @@ public class ActivityOptions { private Bitmap mThumbnail; private int mStartX; private int mStartY; + private int mStartWidth; + private int mStartHeight; private IRemoteCallback mAnimationStartedListener; /** @@ -127,6 +143,34 @@ public class ActivityOptions { } /** + * Create an ActivityOptions specifying an animation where the new + * activity is scaled from a small originating area of the screen to + * its final full representation. + * + * @param source The View that the new activity is animating from. This + * defines the coordinate space for <var>startX</var> and <var>startY</var>. + * @param startX The x starting location of the new activity, relative to <var>source</var>. + * @param startY The y starting location of the activity, relative to <var>source</var>. + * @param startWidth The initial width of the new activity. + * @param startWidth The initial height of the new activity. + * @return Returns a new ActivityOptions object that you can use to + * supply these options as the options Bundle when starting an activity. + */ + public static ActivityOptions makeScaleUpAnimation(View source, + int startX, int startY, int startWidth, int startHeight) { + ActivityOptions opts = new ActivityOptions(); + opts.mPackageName = source.getContext().getPackageName(); + opts.mAnimationType = ANIM_SCALE_UP; + int[] pts = new int[2]; + source.getLocationOnScreen(pts); + opts.mStartX = pts[0] + startX; + opts.mStartY = pts[1] + startY; + opts.mStartWidth = startWidth; + opts.mStartHeight = startHeight; + return opts; + } + + /** * Create an ActivityOptions specifying an animation where a thumbnail * is scaled from a given position to the new activity window that is * being started. @@ -135,8 +179,8 @@ public class ActivityOptions { * defines the coordinate space for <var>startX</var> and <var>startY</var>. * @param thumbnail The bitmap that will be shown as the initial thumbnail * of the animation. - * @param startX The x starting location of the bitmap, in screen coordiantes. - * @param startY The y starting location of the bitmap, in screen coordinates. + * @param startX The x starting location of the bitmap, relative to <var>source</var>. + * @param startY The y starting location of the bitmap, relative to <var>source</var>. * @return Returns a new ActivityOptions object that you can use to * supply these options as the options Bundle when starting an activity. */ @@ -154,8 +198,8 @@ public class ActivityOptions { * defines the coordinate space for <var>startX</var> and <var>startY</var>. * @param thumbnail The bitmap that will be shown as the initial thumbnail * of the animation. - * @param startX The x starting location of the bitmap, in screen coordiantes. - * @param startY The y starting location of the bitmap, in screen coordinates. + * @param startX The x starting location of the bitmap, relative to <var>source</var>. + * @param startY The y starting location of the bitmap, relative to <var>source</var>. * @param listener Optional OnAnimationStartedListener to find out when the * requested animation has started running. If for some reason the animation * is not executed, the callback will happen immediately. @@ -199,6 +243,11 @@ public class ActivityOptions { if (mAnimationType == ANIM_CUSTOM) { mCustomEnterResId = opts.getInt(KEY_ANIM_ENTER_RES_ID, 0); mCustomExitResId = opts.getInt(KEY_ANIM_EXIT_RES_ID, 0); + } else if (mAnimationType == ANIM_SCALE_UP) { + mStartX = opts.getInt(KEY_ANIM_START_X, 0); + mStartY = opts.getInt(KEY_ANIM_START_Y, 0); + mStartWidth = opts.getInt(KEY_ANIM_START_WIDTH, 0); + mStartHeight = opts.getInt(KEY_ANIM_START_HEIGHT, 0); } else if (mAnimationType == ANIM_THUMBNAIL) { mThumbnail = (Bitmap)opts.getParcelable(KEY_ANIM_THUMBNAIL); mStartX = opts.getInt(KEY_ANIM_START_X, 0); @@ -244,6 +293,16 @@ public class ActivityOptions { } /** @hide */ + public int getStartWidth() { + return mStartWidth; + } + + /** @hide */ + public int getStartHeight() { + return mStartHeight; + } + + /** @hide */ public IRemoteCallback getOnAnimationStartListener() { return mAnimationStartedListener; } @@ -281,6 +340,13 @@ public class ActivityOptions { mThumbnail = null; mAnimationStartedListener = null; break; + case ANIM_SCALE_UP: + mAnimationType = otherOptions.mAnimationType; + mStartX = otherOptions.mStartX; + mStartY = otherOptions.mStartY; + mStartWidth = otherOptions.mStartWidth; + mStartHeight = otherOptions.mStartHeight; + break; case ANIM_THUMBNAIL: mAnimationType = otherOptions.mAnimationType; mThumbnail = otherOptions.mThumbnail; @@ -316,6 +382,13 @@ public class ActivityOptions { b.putInt(KEY_ANIM_ENTER_RES_ID, mCustomEnterResId); b.putInt(KEY_ANIM_EXIT_RES_ID, mCustomExitResId); break; + case ANIM_SCALE_UP: + b.putInt(KEY_ANIM_TYPE, mAnimationType); + b.putInt(KEY_ANIM_START_X, mStartX); + b.putInt(KEY_ANIM_START_Y, mStartY); + b.putInt(KEY_ANIM_START_WIDTH, mStartWidth); + b.putInt(KEY_ANIM_START_HEIGHT, mStartHeight); + break; case ANIM_THUMBNAIL: b.putInt(KEY_ANIM_TYPE, mAnimationType); b.putParcelable(KEY_ANIM_THUMBNAIL, mThumbnail); @@ -323,6 +396,7 @@ public class ActivityOptions { b.putInt(KEY_ANIM_START_Y, mStartY); b.putIBinder(KEY_ANIM_START_LISTENER, mAnimationStartedListener != null ? mAnimationStartedListener.asBinder() : null); + break; } return b; } diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 0645aa9..8942135 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -45,6 +45,7 @@ import android.graphics.drawable.Drawable; import android.hardware.ISerialManager; import android.hardware.SensorManager; import android.hardware.SerialManager; +import android.hardware.SystemSensorManager; import android.hardware.input.IInputManager; import android.hardware.input.InputManager; import android.hardware.usb.IUsbManager; @@ -407,7 +408,7 @@ class ContextImpl extends Context { registerService(SENSOR_SERVICE, new ServiceFetcher() { public Object createService(ContextImpl ctx) { - return new SensorManager(ctx.mMainThread.getHandler().getLooper()); + return new SystemSensorManager(ctx.mMainThread.getHandler().getLooper()); }}); registerService(STATUS_BAR_SERVICE, new ServiceFetcher() { diff --git a/core/java/android/app/DownloadManager.java b/core/java/android/app/DownloadManager.java index dd58397..55f29e6 100644 --- a/core/java/android/app/DownloadManager.java +++ b/core/java/android/app/DownloadManager.java @@ -349,6 +349,7 @@ public class DownloadManager { private String mMimeType; private boolean mRoamingAllowed = true; private int mAllowedNetworkTypes = ~0; // default to all network types allowed + private boolean mAllowedOverMetered = true; private boolean mIsVisibleInDownloadsUi = true; private boolean mScannable = false; private boolean mUseSystemCache = false; @@ -609,8 +610,11 @@ public class DownloadManager { } /** - * Restrict the types of networks over which this download may proceed. By default, all - * network types are allowed. + * Restrict the types of networks over which this download may proceed. + * By default, all network types are allowed. Consider using + * {@link #setAllowedOverMetered(boolean)} instead, since it's more + * flexible. + * * @param flags any combination of the NETWORK_* bit flags. * @return this object */ @@ -620,6 +624,17 @@ public class DownloadManager { } /** + * Set whether this download may proceed over a metered network + * connection. By default, metered networks are allowed. + * + * @see ConnectivityManager#isActiveNetworkMetered() + */ + public Request setAllowedOverMetered(boolean allow) { + mAllowedOverMetered = allow; + return this; + } + + /** * Set whether this download may proceed over a roaming connection. By default, roaming is * allowed. * @param allowed whether to allow a roaming connection to be used @@ -672,6 +687,7 @@ public class DownloadManager { putIfNonNull(values, Downloads.Impl.COLUMN_DESCRIPTION, mDescription); putIfNonNull(values, Downloads.Impl.COLUMN_MIME_TYPE, mMimeType); + // TODO: add COLUMN_ALLOW_METERED and persist values.put(Downloads.Impl.COLUMN_VISIBILITY, mNotificationVisibility); values.put(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES, mAllowedNetworkTypes); values.put(Downloads.Impl.COLUMN_ALLOW_ROAMING, mRoamingAllowed); diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 5cce25f..22d84f0 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -1681,7 +1681,7 @@ public class Notification implements Parcelable } private RemoteViews makeBigContentView() { - RemoteViews contentView = mBuilder.applyStandardTemplateWithActions(R.layout.notification_template_base); + RemoteViews contentView = mBuilder.applyStandardTemplateWithActions(R.layout.notification_template_big_text); contentView.setTextViewText(R.id.big_text, mBigText); contentView.setViewVisibility(R.id.big_text, View.VISIBLE); @@ -1696,4 +1696,60 @@ public class Notification implements Parcelable return wip; } } + + /** + * Helper class for generating large-format notifications that include a list of (up to 5) strings. + * + * This class is a "rebuilder": It consumes a Builder object and modifies its behavior, like so: + * <pre class="prettyprint"> + * Notification noti = new Notification.DigestStyle( + * new Notification.Builder() + * .setContentTitle("New mail from " + sender.toString()) + * .setContentText(subject) + * .setSmallIcon(R.drawable.new_mail) + * .setLargeIcon(aBitmap)) + * .addLine(str1) + * .addLine(str2) + * .build(); + * </pre> + * + * @see Notification#bigContentView + */ + public static class InboxStyle { + private Builder mBuilder; + private ArrayList<CharSequence> mTexts = new ArrayList<CharSequence>(5); + + public InboxStyle(Builder builder) { + mBuilder = builder; + } + + public InboxStyle addLine(CharSequence cs) { + mTexts.add(cs); + return this; + } + + private RemoteViews makeBigContentView() { + RemoteViews contentView = mBuilder.applyStandardTemplateWithActions(R.layout.notification_template_inbox); + + int[] rowIds = {R.id.inbox_text0, R.id.inbox_text1, R.id.inbox_text2, R.id.inbox_text3, R.id.inbox_text4}; + + int i=0; + while (i < mTexts.size() && i < rowIds.length) { + CharSequence str = mTexts.get(i); + if (str != null && !str.equals("")) { + contentView.setViewVisibility(rowIds[i], View.VISIBLE); + contentView.setTextViewText(rowIds[i], str); + } + i++; + } + + return contentView; + } + + public Notification build() { + Notification wip = mBuilder.getNotification(); + wip.bigContentView = makeBigContentView(); + return wip; + } + } } diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 19e4372..6653336 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -6576,35 +6576,54 @@ public class Intent implements Parcelable, Cloneable { final String action = getAction(); if (ACTION_SEND.equals(action)) { - final Uri stream; + Uri stream = null; try { stream = getParcelableExtra(EXTRA_STREAM); } catch (ClassCastException e) { - return; } - if (stream != null) { + final CharSequence text = getCharSequenceExtra(EXTRA_TEXT); + final String htmlText = getStringExtra(EXTRA_HTML_TEXT); + if (stream != null || text != null || htmlText != null) { final ClipData clipData = new ClipData( - null, new String[] { getType() }, new ClipData.Item(stream)); - + null, new String[] { getType() }, + new ClipData.Item(text, htmlText, null, stream)); setClipData(clipData); addFlags(FLAG_GRANT_READ_URI_PERMISSION); } } else if (ACTION_SEND_MULTIPLE.equals(action)) { - final ArrayList<Uri> streams; + ArrayList<Uri> streams = null; try { streams = getParcelableArrayListExtra(EXTRA_STREAM); } catch (ClassCastException e) { - return; } - if (streams != null && streams.size() > 0) { - final Uri firstStream = streams.get(0); + final ArrayList<CharSequence> texts = getCharSequenceArrayListExtra(EXTRA_TEXT); + final ArrayList<String> htmlTexts = getStringArrayListExtra(EXTRA_HTML_TEXT); + int num = -1; + if (streams != null) { + num = streams.size(); + } + if (texts != null) { + if (num >= 0 && num != texts.size()) { + // Wha...! F- you. + return; + } + num = texts.size(); + } + if (htmlTexts != null) { + if (num >= 0 && num != htmlTexts.size()) { + // Wha...! F- you. + return; + } + num = htmlTexts.size(); + } + if (num > 0) { final ClipData clipData = new ClipData( - null, new String[] { getType() }, new ClipData.Item(firstStream)); + null, new String[] { getType() }, + makeClipItem(streams, texts, htmlTexts, 0)); - final int size = streams.size(); - for (int i = 1; i < size; i++) { - clipData.addItem(new ClipData.Item(streams.get(i))); + for (int i = 1; i < num; i++) { + clipData.addItem(makeClipItem(streams, texts, htmlTexts, i)); } setClipData(clipData); @@ -6612,4 +6631,12 @@ public class Intent implements Parcelable, Cloneable { } } } + + private static ClipData.Item makeClipItem(ArrayList<Uri> streams, ArrayList<CharSequence> texts, + ArrayList<String> htmlTexts, int which) { + Uri uri = streams != null ? streams.get(which) : null; + CharSequence text = texts != null ? texts.get(which) : null; + String htmlText = htmlTexts != null ? htmlTexts.get(which) : null; + return new ClipData.Item(text, htmlText, null, uri); + } } diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index b6ebbdf..185fcb9 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -3458,9 +3458,9 @@ public class PackageParser { ai.disableCompatibilityMode(); } if (stopped) { - p.applicationInfo.flags |= ApplicationInfo.FLAG_STOPPED; + ai.flags |= ApplicationInfo.FLAG_STOPPED; } else { - p.applicationInfo.flags &= ~ApplicationInfo.FLAG_STOPPED; + ai.flags &= ~ApplicationInfo.FLAG_STOPPED; } if (enabledState == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { ai.enabled = true; diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java index 254f652..04f6377 100644 --- a/core/java/android/database/sqlite/SQLiteConnection.java +++ b/core/java/android/database/sqlite/SQLiteConnection.java @@ -711,6 +711,7 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen throw new IllegalArgumentException("sql must not be null."); } + int changedRows = 0; final int cookie = mRecentOperations.beginOperation("executeForChangedRowCount", sql, bindArgs); try { @@ -721,8 +722,9 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen applyBlockGuardPolicy(statement); attachCancellationSignal(cancellationSignal); try { - return nativeExecuteForChangedRowCount( + changedRows = nativeExecuteForChangedRowCount( mConnectionPtr, statement.mStatementPtr); + return changedRows; } finally { detachCancellationSignal(cancellationSignal); } @@ -733,7 +735,9 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen mRecentOperations.failOperation(cookie, ex); throw ex; } finally { - mRecentOperations.endOperation(cookie); + if (mRecentOperations.endOperationDeferLog(cookie)) { + mRecentOperations.logOperation(cookie, "changedRows=" + changedRows); + } } } diff --git a/core/java/android/hardware/LegacySensorManager.java b/core/java/android/hardware/LegacySensorManager.java new file mode 100644 index 0000000..62c194f --- /dev/null +++ b/core/java/android/hardware/LegacySensorManager.java @@ -0,0 +1,434 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware; + +import android.os.RemoteException; +import android.os.ServiceManager; +import android.view.IRotationWatcher; +import android.view.IWindowManager; +import android.view.Surface; + +import java.util.HashMap; +import java.util.List; + +/** + * Helper class for implementing the legacy sensor manager API. + * @hide + */ +@SuppressWarnings("deprecation") +final class LegacySensorManager { + private static boolean sInitialized; + private static IWindowManager sWindowManager; + private static int sRotation = Surface.ROTATION_0; + + private final SensorManager mSensorManager; + + // List of legacy listeners. Guarded by mLegacyListenersMap. + private final HashMap<SensorListener, LegacyListener> mLegacyListenersMap = + new HashMap<SensorListener, LegacyListener>(); + + public LegacySensorManager(SensorManager sensorManager) { + mSensorManager = sensorManager; + + synchronized (SensorManager.class) { + if (!sInitialized) { + sWindowManager = IWindowManager.Stub.asInterface( + ServiceManager.getService("window")); + if (sWindowManager != null) { + // if it's null we're running in the system process + // which won't get the rotated values + try { + sRotation = sWindowManager.watchRotation( + new IRotationWatcher.Stub() { + public void onRotationChanged(int rotation) { + LegacySensorManager.onRotationChanged(rotation); + } + } + ); + } catch (RemoteException e) { + } + } + } + } + } + + public int getSensors() { + int result = 0; + final List<Sensor> fullList = mSensorManager.getFullSensorList(); + for (Sensor i : fullList) { + switch (i.getType()) { + case Sensor.TYPE_ACCELEROMETER: + result |= SensorManager.SENSOR_ACCELEROMETER; + break; + case Sensor.TYPE_MAGNETIC_FIELD: + result |= SensorManager.SENSOR_MAGNETIC_FIELD; + break; + case Sensor.TYPE_ORIENTATION: + result |= SensorManager.SENSOR_ORIENTATION + | SensorManager.SENSOR_ORIENTATION_RAW; + break; + } + } + return result; + } + + public boolean registerListener(SensorListener listener, int sensors, int rate) { + if (listener == null) { + return false; + } + boolean result = false; + result = registerLegacyListener(SensorManager.SENSOR_ACCELEROMETER, + Sensor.TYPE_ACCELEROMETER, listener, sensors, rate) || result; + result = registerLegacyListener(SensorManager.SENSOR_MAGNETIC_FIELD, + Sensor.TYPE_MAGNETIC_FIELD, listener, sensors, rate) || result; + result = registerLegacyListener(SensorManager.SENSOR_ORIENTATION_RAW, + Sensor.TYPE_ORIENTATION, listener, sensors, rate) || result; + result = registerLegacyListener(SensorManager.SENSOR_ORIENTATION, + Sensor.TYPE_ORIENTATION, listener, sensors, rate) || result; + result = registerLegacyListener(SensorManager.SENSOR_TEMPERATURE, + Sensor.TYPE_TEMPERATURE, listener, sensors, rate) || result; + return result; + } + + private boolean registerLegacyListener(int legacyType, int type, + SensorListener listener, int sensors, int rate) { + boolean result = false; + // Are we activating this legacy sensor? + if ((sensors & legacyType) != 0) { + // if so, find a suitable Sensor + Sensor sensor = mSensorManager.getDefaultSensor(type); + if (sensor != null) { + // We do all of this work holding the legacy listener lock to ensure + // that the invariants around listeners are maintained. This is safe + // because neither registerLegacyListener nor unregisterLegacyListener + // are called reentrantly while sensors are being registered or unregistered. + synchronized (mLegacyListenersMap) { + // If we don't already have one, create a LegacyListener + // to wrap this listener and process the events as + // they are expected by legacy apps. + LegacyListener legacyListener = mLegacyListenersMap.get(listener); + if (legacyListener == null) { + // we didn't find a LegacyListener for this client, + // create one, and put it in our list. + legacyListener = new LegacyListener(listener); + mLegacyListenersMap.put(listener, legacyListener); + } + + // register this legacy sensor with this legacy listener + if (legacyListener.registerSensor(legacyType)) { + // and finally, register the legacy listener with the new apis + result = mSensorManager.registerListener(legacyListener, sensor, rate); + } else { + result = true; // sensor already enabled + } + } + } + } + return result; + } + + public void unregisterListener(SensorListener listener, int sensors) { + if (listener == null) { + return; + } + unregisterLegacyListener(SensorManager.SENSOR_ACCELEROMETER, Sensor.TYPE_ACCELEROMETER, + listener, sensors); + unregisterLegacyListener(SensorManager.SENSOR_MAGNETIC_FIELD, Sensor.TYPE_MAGNETIC_FIELD, + listener, sensors); + unregisterLegacyListener(SensorManager.SENSOR_ORIENTATION_RAW, Sensor.TYPE_ORIENTATION, + listener, sensors); + unregisterLegacyListener(SensorManager.SENSOR_ORIENTATION, Sensor.TYPE_ORIENTATION, + listener, sensors); + unregisterLegacyListener(SensorManager.SENSOR_TEMPERATURE, Sensor.TYPE_TEMPERATURE, + listener, sensors); + } + + private void unregisterLegacyListener(int legacyType, int type, + SensorListener listener, int sensors) { + // Are we deactivating this legacy sensor? + if ((sensors & legacyType) != 0) { + // if so, find the corresponding Sensor + Sensor sensor = mSensorManager.getDefaultSensor(type); + if (sensor != null) { + // We do all of this work holding the legacy listener lock to ensure + // that the invariants around listeners are maintained. This is safe + // because neither registerLegacyListener nor unregisterLegacyListener + // are called re-entrantly while sensors are being registered or unregistered. + synchronized (mLegacyListenersMap) { + // do we know about this listener? + LegacyListener legacyListener = mLegacyListenersMap.get(listener); + if (legacyListener != null) { + // unregister this legacy sensor and if we don't + // need the corresponding Sensor, unregister it too + if (legacyListener.unregisterSensor(legacyType)) { + // corresponding sensor not needed, unregister + mSensorManager.unregisterListener(legacyListener, sensor); + + // finally check if we still need the legacyListener + // in our mapping, if not, get rid of it too. + if (!legacyListener.hasSensors()) { + mLegacyListenersMap.remove(listener); + } + } + } + } + } + } + } + + static void onRotationChanged(int rotation) { + synchronized (SensorManager.class) { + sRotation = rotation; + } + } + + static int getRotation() { + synchronized (SensorManager.class) { + return sRotation; + } + } + + private static final class LegacyListener implements SensorEventListener { + private float mValues[] = new float[6]; + private SensorListener mTarget; + private int mSensors; + private final LmsFilter mYawfilter = new LmsFilter(); + + LegacyListener(SensorListener target) { + mTarget = target; + mSensors = 0; + } + + boolean registerSensor(int legacyType) { + if ((mSensors & legacyType) != 0) { + return false; + } + boolean alreadyHasOrientationSensor = hasOrientationSensor(mSensors); + mSensors |= legacyType; + if (alreadyHasOrientationSensor && hasOrientationSensor(legacyType)) { + return false; // don't need to re-register the orientation sensor + } + return true; + } + + boolean unregisterSensor(int legacyType) { + if ((mSensors & legacyType) == 0) { + return false; + } + mSensors &= ~legacyType; + if (hasOrientationSensor(legacyType) && hasOrientationSensor(mSensors)) { + return false; // can't unregister the orientation sensor just yet + } + return true; + } + + boolean hasSensors() { + return mSensors != 0; + } + + private static boolean hasOrientationSensor(int sensors) { + return (sensors & (SensorManager.SENSOR_ORIENTATION + | SensorManager.SENSOR_ORIENTATION_RAW)) != 0; + } + + public void onAccuracyChanged(Sensor sensor, int accuracy) { + try { + mTarget.onAccuracyChanged(getLegacySensorType(sensor.getType()), accuracy); + } catch (AbstractMethodError e) { + // old app that doesn't implement this method + // just ignore it. + } + } + + public void onSensorChanged(SensorEvent event) { + final float v[] = mValues; + v[0] = event.values[0]; + v[1] = event.values[1]; + v[2] = event.values[2]; + int type = event.sensor.getType(); + int legacyType = getLegacySensorType(type); + mapSensorDataToWindow(legacyType, v, LegacySensorManager.getRotation()); + if (type == Sensor.TYPE_ORIENTATION) { + if ((mSensors & SensorManager.SENSOR_ORIENTATION_RAW)!=0) { + mTarget.onSensorChanged(SensorManager.SENSOR_ORIENTATION_RAW, v); + } + if ((mSensors & SensorManager.SENSOR_ORIENTATION)!=0) { + v[0] = mYawfilter.filter(event.timestamp, v[0]); + mTarget.onSensorChanged(SensorManager.SENSOR_ORIENTATION, v); + } + } else { + mTarget.onSensorChanged(legacyType, v); + } + } + + /* + * Helper function to convert the specified sensor's data to the windows's + * coordinate space from the device's coordinate space. + * + * output: 3,4,5: values in the old API format + * 0,1,2: transformed values in the old API format + * + */ + private void mapSensorDataToWindow(int sensor, + float[] values, int orientation) { + float x = values[0]; + float y = values[1]; + float z = values[2]; + + switch (sensor) { + case SensorManager.SENSOR_ORIENTATION: + case SensorManager.SENSOR_ORIENTATION_RAW: + z = -z; + break; + case SensorManager.SENSOR_ACCELEROMETER: + x = -x; + y = -y; + z = -z; + break; + case SensorManager.SENSOR_MAGNETIC_FIELD: + x = -x; + y = -y; + break; + } + values[0] = x; + values[1] = y; + values[2] = z; + values[3] = x; + values[4] = y; + values[5] = z; + + if ((orientation & Surface.ROTATION_90) != 0) { + // handles 90 and 270 rotation + switch (sensor) { + case SensorManager.SENSOR_ACCELEROMETER: + case SensorManager.SENSOR_MAGNETIC_FIELD: + values[0] =-y; + values[1] = x; + values[2] = z; + break; + case SensorManager.SENSOR_ORIENTATION: + case SensorManager.SENSOR_ORIENTATION_RAW: + values[0] = x + ((x < 270) ? 90 : -270); + values[1] = z; + values[2] = y; + break; + } + } + if ((orientation & Surface.ROTATION_180) != 0) { + x = values[0]; + y = values[1]; + z = values[2]; + // handles 180 (flip) and 270 (flip + 90) rotation + switch (sensor) { + case SensorManager.SENSOR_ACCELEROMETER: + case SensorManager.SENSOR_MAGNETIC_FIELD: + values[0] =-x; + values[1] =-y; + values[2] = z; + break; + case SensorManager.SENSOR_ORIENTATION: + case SensorManager.SENSOR_ORIENTATION_RAW: + values[0] = (x >= 180) ? (x - 180) : (x + 180); + values[1] =-y; + values[2] =-z; + break; + } + } + } + + private static int getLegacySensorType(int type) { + switch (type) { + case Sensor.TYPE_ACCELEROMETER: + return SensorManager.SENSOR_ACCELEROMETER; + case Sensor.TYPE_MAGNETIC_FIELD: + return SensorManager.SENSOR_MAGNETIC_FIELD; + case Sensor.TYPE_ORIENTATION: + return SensorManager.SENSOR_ORIENTATION_RAW; + case Sensor.TYPE_TEMPERATURE: + return SensorManager.SENSOR_TEMPERATURE; + } + return 0; + } + } + + private static final class LmsFilter { + private static final int SENSORS_RATE_MS = 20; + private static final int COUNT = 12; + private static final float PREDICTION_RATIO = 1.0f/3.0f; + private static final float PREDICTION_TIME = (SENSORS_RATE_MS*COUNT/1000.0f)*PREDICTION_RATIO; + private float mV[] = new float[COUNT*2]; + private float mT[] = new float[COUNT*2]; + private int mIndex; + + public LmsFilter() { + mIndex = COUNT; + } + + public float filter(long time, float in) { + float v = in; + final float ns = 1.0f / 1000000000.0f; + final float t = time*ns; + float v1 = mV[mIndex]; + if ((v-v1) > 180) { + v -= 360; + } else if ((v1-v) > 180) { + v += 360; + } + /* Manage the circular buffer, we write the data twice spaced + * by COUNT values, so that we don't have to copy the array + * when it's full + */ + mIndex++; + if (mIndex >= COUNT*2) + mIndex = COUNT; + mV[mIndex] = v; + mT[mIndex] = t; + mV[mIndex-COUNT] = v; + mT[mIndex-COUNT] = t; + + float A, B, C, D, E; + float a, b; + int i; + + A = B = C = D = E = 0; + for (i=0 ; i<COUNT-1 ; i++) { + final int j = mIndex - 1 - i; + final float Z = mV[j]; + final float T = 0.5f*(mT[j] + mT[j+1]) - t; + float dT = mT[j] - mT[j+1]; + dT *= dT; + A += Z*dT; + B += T*(T*dT); + C += (T*dT); + D += Z*(T*dT); + E += dT; + } + b = (A*B + C*D) / (E*B + C*C); + a = (E*b - A) / C; + float f = b + PREDICTION_TIME*a; + + // Normalize + f *= (1.0f / 360.0f); + if (((f>=0)?f:-f) >= 0.5f) + f = f - (float)Math.ceil(f + 0.5f) + 1.0f; + if (f < 0) + f += 1.0f; + f *= 360.0f; + return f; + } + } +} diff --git a/core/java/android/hardware/Sensor.java b/core/java/android/hardware/Sensor.java index 63fb32d..3c70dc6 100644 --- a/core/java/android/hardware/Sensor.java +++ b/core/java/android/hardware/Sensor.java @@ -131,7 +131,6 @@ public class Sensor { private float mResolution; private float mPower; private int mMinDelay; - private int mLegacyType; Sensor() { @@ -203,12 +202,4 @@ public class Sensor { mMaxRange = max; mResolution = res; } - - void setLegacyType(int legacyType) { - mLegacyType = legacyType; - } - - int getLegacyType() { - return mLegacyType; - } } diff --git a/core/java/android/hardware/SensorManager.java b/core/java/android/hardware/SensorManager.java index 3fdf246..aeb46cf 100644 --- a/core/java/android/hardware/SensorManager.java +++ b/core/java/android/hardware/SensorManager.java @@ -16,23 +16,12 @@ package android.hardware; -import android.os.Looper; -import android.os.Process; -import android.os.RemoteException; import android.os.Handler; -import android.os.Message; -import android.os.ServiceManager; import android.util.Log; import android.util.SparseArray; -import android.util.SparseBooleanArray; -import android.util.SparseIntArray; -import android.view.IRotationWatcher; -import android.view.IWindowManager; -import android.view.Surface; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.List; /** @@ -83,11 +72,19 @@ import java.util.List; * @see Sensor * */ -public class SensorManager -{ - private static final String TAG = "SensorManager"; +public abstract class SensorManager { + /** @hide */ + protected static final String TAG = "SensorManager"; + private static final float[] mTempMatrix = new float[16]; + // Cached lists of sensors by type. Guarded by mSensorListByType. + private final SparseArray<List<Sensor>> mSensorListByType = + new SparseArray<List<Sensor>>(); + + // Legacy sensor manager implementation. Guarded by mSensorListByType during initialization. + private LegacySensorManager mLegacySensorManager; + /* NOTE: sensor IDs must be a power of 2 */ /** @@ -353,340 +350,18 @@ public class SensorManager /** see {@link #remapCoordinateSystem} */ public static final int AXIS_MINUS_Z = AXIS_Z | 0x80; - /*-----------------------------------------------------------------------*/ - - Looper mMainLooper; - @SuppressWarnings("deprecation") - private HashMap<SensorListener, LegacyListener> mLegacyListenersMap = - new HashMap<SensorListener, LegacyListener>(); - - /*-----------------------------------------------------------------------*/ - - private static final int SENSOR_DISABLE = -1; - private static boolean sSensorModuleInitialized = false; - private static ArrayList<Sensor> sFullSensorsList = new ArrayList<Sensor>(); - private static SparseArray<List<Sensor>> sSensorListByType = new SparseArray<List<Sensor>>(); - private static IWindowManager sWindowManager; - private static int sRotation = Surface.ROTATION_0; - /* The thread and the sensor list are global to the process - * but the actual thread is spawned on demand */ - private static SensorThread sSensorThread; - private static int sQueue; - - // Used within this module from outside SensorManager, don't make private - static SparseArray<Sensor> sHandleToSensor = new SparseArray<Sensor>(); - static final ArrayList<ListenerDelegate> sListeners = - new ArrayList<ListenerDelegate>(); - - /*-----------------------------------------------------------------------*/ - - private class SensorEventPool { - private final int mPoolSize; - private final SensorEvent mPool[]; - private int mNumItemsInPool; - - private SensorEvent createSensorEvent() { - // maximal size for all legacy events is 3 - return new SensorEvent(3); - } - - SensorEventPool(int poolSize) { - mPoolSize = poolSize; - mNumItemsInPool = poolSize; - mPool = new SensorEvent[poolSize]; - } - - SensorEvent getFromPool() { - SensorEvent t = null; - synchronized (this) { - if (mNumItemsInPool > 0) { - // remove the "top" item from the pool - final int index = mPoolSize - mNumItemsInPool; - t = mPool[index]; - mPool[index] = null; - mNumItemsInPool--; - } - } - if (t == null) { - // the pool was empty or this item was removed from the pool for - // the first time. In any case, we need to create a new item. - t = createSensorEvent(); - } - return t; - } - - void returnToPool(SensorEvent t) { - synchronized (this) { - // is there space left in the pool? - if (mNumItemsInPool < mPoolSize) { - // if so, return the item to the pool - mNumItemsInPool++; - final int index = mPoolSize - mNumItemsInPool; - mPool[index] = t; - } - } - } - } - - private static SensorEventPool sPool; - - /*-----------------------------------------------------------------------*/ - - static private class SensorThread { - - Thread mThread; - boolean mSensorsReady; - - SensorThread() { - } - - @Override - protected void finalize() { - } - - // must be called with sListeners lock - boolean startLocked() { - try { - if (mThread == null) { - mSensorsReady = false; - SensorThreadRunnable runnable = new SensorThreadRunnable(); - Thread thread = new Thread(runnable, SensorThread.class.getName()); - thread.start(); - synchronized (runnable) { - while (mSensorsReady == false) { - runnable.wait(); - } - } - mThread = thread; - } - } catch (InterruptedException e) { - } - return mThread == null ? false : true; - } - - private class SensorThreadRunnable implements Runnable { - SensorThreadRunnable() { - } - - private boolean open() { - // NOTE: this cannot synchronize on sListeners, since - // it's held in the main thread at least until we - // return from here. - sQueue = sensors_create_queue(); - return true; - } - - public void run() { - //Log.d(TAG, "entering main sensor thread"); - final float[] values = new float[3]; - final int[] status = new int[1]; - final long timestamp[] = new long[1]; - Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_DISPLAY); - - if (!open()) { - return; - } - - synchronized (this) { - // we've open the driver, we're ready to open the sensors - mSensorsReady = true; - this.notify(); - } - - while (true) { - // wait for an event - final int sensor = sensors_data_poll(sQueue, values, status, timestamp); - - int accuracy = status[0]; - synchronized (sListeners) { - if (sensor == -1 || sListeners.isEmpty()) { - // we lost the connection to the event stream. this happens - // when the last listener is removed or if there is an error - if (sensor == -1 && !sListeners.isEmpty()) { - // log a warning in case of abnormal termination - Log.e(TAG, "_sensors_data_poll() failed, we bail out: sensors=" + sensor); - } - // we have no more listeners or polling failed, terminate the thread - sensors_destroy_queue(sQueue); - sQueue = 0; - mThread = null; - break; - } - final Sensor sensorObject = sHandleToSensor.get(sensor); - if (sensorObject != null) { - // report the sensor event to all listeners that - // care about it. - final int size = sListeners.size(); - for (int i=0 ; i<size ; i++) { - ListenerDelegate listener = sListeners.get(i); - if (listener.hasSensor(sensorObject)) { - // this is asynchronous (okay to call - // with sListeners lock held). - listener.onSensorChangedLocked(sensorObject, - values, timestamp, accuracy); - } - } - } - } - } - //Log.d(TAG, "exiting main sensor thread"); - } - } - } - - /*-----------------------------------------------------------------------*/ - - private class ListenerDelegate { - private final SensorEventListener mSensorEventListener; - private final ArrayList<Sensor> mSensorList = new ArrayList<Sensor>(); - private final Handler mHandler; - public SparseBooleanArray mSensors = new SparseBooleanArray(); - public SparseBooleanArray mFirstEvent = new SparseBooleanArray(); - public SparseIntArray mSensorAccuracies = new SparseIntArray(); - - ListenerDelegate(SensorEventListener listener, Sensor sensor, Handler handler) { - mSensorEventListener = listener; - Looper looper = (handler != null) ? handler.getLooper() : mMainLooper; - // currently we create one Handler instance per listener, but we could - // have one per looper (we'd need to pass the ListenerDelegate - // instance to handleMessage and keep track of them separately). - mHandler = new Handler(looper) { - @Override - public void handleMessage(Message msg) { - final SensorEvent t = (SensorEvent)msg.obj; - final int handle = t.sensor.getHandle(); - - switch (t.sensor.getType()) { - // Only report accuracy for sensors that support it. - case Sensor.TYPE_MAGNETIC_FIELD: - case Sensor.TYPE_ORIENTATION: - // call onAccuracyChanged() only if the value changes - final int accuracy = mSensorAccuracies.get(handle); - if ((t.accuracy >= 0) && (accuracy != t.accuracy)) { - mSensorAccuracies.put(handle, t.accuracy); - mSensorEventListener.onAccuracyChanged(t.sensor, t.accuracy); - } - break; - default: - // For other sensors, just report the accuracy once - if (mFirstEvent.get(handle) == false) { - mFirstEvent.put(handle, true); - mSensorEventListener.onAccuracyChanged( - t.sensor, SENSOR_STATUS_ACCURACY_HIGH); - } - break; - } - - mSensorEventListener.onSensorChanged(t); - sPool.returnToPool(t); - } - }; - addSensor(sensor); - } - - Object getListener() { - return mSensorEventListener; - } - - void addSensor(Sensor sensor) { - mSensors.put(sensor.getHandle(), true); - mSensorList.add(sensor); - } - int removeSensor(Sensor sensor) { - mSensors.delete(sensor.getHandle()); - mSensorList.remove(sensor); - return mSensors.size(); - } - boolean hasSensor(Sensor sensor) { - return mSensors.get(sensor.getHandle()); - } - List<Sensor> getSensors() { - return mSensorList; - } - - void onSensorChangedLocked(Sensor sensor, float[] values, long[] timestamp, int accuracy) { - SensorEvent t = sPool.getFromPool(); - final float[] v = t.values; - v[0] = values[0]; - v[1] = values[1]; - v[2] = values[2]; - t.timestamp = timestamp[0]; - t.accuracy = accuracy; - t.sensor = sensor; - Message msg = Message.obtain(); - msg.what = 0; - msg.obj = t; - msg.setAsynchronous(true); - mHandler.sendMessage(msg); - } - } /** * {@hide} */ - public SensorManager(Looper mainLooper) { - mMainLooper = mainLooper; - - - synchronized(sListeners) { - if (!sSensorModuleInitialized) { - sSensorModuleInitialized = true; - - nativeClassInit(); - - sWindowManager = IWindowManager.Stub.asInterface( - ServiceManager.getService("window")); - if (sWindowManager != null) { - // if it's null we're running in the system process - // which won't get the rotated values - try { - sRotation = sWindowManager.watchRotation( - new IRotationWatcher.Stub() { - public void onRotationChanged(int rotation) { - SensorManager.this.onRotationChanged(rotation); - } - } - ); - } catch (RemoteException e) { - } - } - - // initialize the sensor list - sensors_module_init(); - final ArrayList<Sensor> fullList = sFullSensorsList; - int i = 0; - do { - Sensor sensor = new Sensor(); - i = sensors_module_get_next_sensor(sensor, i); - - if (i>=0) { - //Log.d(TAG, "found sensor: " + sensor.getName() + - // ", handle=" + sensor.getHandle()); - sensor.setLegacyType(getLegacySensorType(sensor.getType())); - fullList.add(sensor); - sHandleToSensor.append(sensor.getHandle(), sensor); - } - } while (i>0); - - sPool = new SensorEventPool( sFullSensorsList.size()*2 ); - sSensorThread = new SensorThread(); - } - } + public SensorManager() { } - private int getLegacySensorType(int type) { - switch (type) { - case Sensor.TYPE_ACCELEROMETER: - return SENSOR_ACCELEROMETER; - case Sensor.TYPE_MAGNETIC_FIELD: - return SENSOR_MAGNETIC_FIELD; - case Sensor.TYPE_ORIENTATION: - return SENSOR_ORIENTATION_RAW; - case Sensor.TYPE_TEMPERATURE: - return SENSOR_TEMPERATURE; - } - return 0; - } + /** + * Gets the full list of sensors that are available. + * @hide + */ + protected abstract List<Sensor> getFullSensorList(); /** * @return available sensors. @@ -695,23 +370,7 @@ public class SensorManager */ @Deprecated public int getSensors() { - int result = 0; - final ArrayList<Sensor> fullList = sFullSensorsList; - for (Sensor i : fullList) { - switch (i.getType()) { - case Sensor.TYPE_ACCELEROMETER: - result |= SensorManager.SENSOR_ACCELEROMETER; - break; - case Sensor.TYPE_MAGNETIC_FIELD: - result |= SensorManager.SENSOR_MAGNETIC_FIELD; - break; - case Sensor.TYPE_ORIENTATION: - result |= SensorManager.SENSOR_ORIENTATION | - SensorManager.SENSOR_ORIENTATION_RAW; - break; - } - } - return result; + return getLegacySensorManager().getSensors(); } /** @@ -731,9 +390,9 @@ public class SensorManager public List<Sensor> getSensorList(int type) { // cache the returned lists the first time List<Sensor> list; - final ArrayList<Sensor> fullList = sFullSensorsList; - synchronized(fullList) { - list = sSensorListByType.get(type); + final List<Sensor> fullList = getFullSensorList(); + synchronized (mSensorListByType) { + list = mSensorListByType.get(type); if (list == null) { if (type == Sensor.TYPE_ALL) { list = fullList; @@ -745,7 +404,7 @@ public class SensorManager } } list = Collections.unmodifiableList(list); - sSensorListByType.append(type, list); + mSensorListByType.append(type, list); } } return list; @@ -817,143 +476,40 @@ public class SensorManager */ @Deprecated public boolean registerListener(SensorListener listener, int sensors, int rate) { - if (listener == null) { - return false; - } - boolean result = false; - result = registerLegacyListener(SENSOR_ACCELEROMETER, Sensor.TYPE_ACCELEROMETER, - listener, sensors, rate) || result; - result = registerLegacyListener(SENSOR_MAGNETIC_FIELD, Sensor.TYPE_MAGNETIC_FIELD, - listener, sensors, rate) || result; - result = registerLegacyListener(SENSOR_ORIENTATION_RAW, Sensor.TYPE_ORIENTATION, - listener, sensors, rate) || result; - result = registerLegacyListener(SENSOR_ORIENTATION, Sensor.TYPE_ORIENTATION, - listener, sensors, rate) || result; - result = registerLegacyListener(SENSOR_TEMPERATURE, Sensor.TYPE_TEMPERATURE, - listener, sensors, rate) || result; - return result; - } - - @SuppressWarnings("deprecation") - private boolean registerLegacyListener(int legacyType, int type, - SensorListener listener, int sensors, int rate) - { - if (listener == null) { - return false; - } - boolean result = false; - // Are we activating this legacy sensor? - if ((sensors & legacyType) != 0) { - // if so, find a suitable Sensor - Sensor sensor = getDefaultSensor(type); - if (sensor != null) { - // If we don't already have one, create a LegacyListener - // to wrap this listener and process the events as - // they are expected by legacy apps. - LegacyListener legacyListener = null; - synchronized (mLegacyListenersMap) { - legacyListener = mLegacyListenersMap.get(listener); - if (legacyListener == null) { - // we didn't find a LegacyListener for this client, - // create one, and put it in our list. - legacyListener = new LegacyListener(listener); - mLegacyListenersMap.put(listener, legacyListener); - } - } - // register this legacy sensor with this legacy listener - legacyListener.registerSensor(legacyType); - // and finally, register the legacy listener with the new apis - result = registerListener(legacyListener, sensor, rate); - } - } - return result; + return getLegacySensorManager().registerListener(listener, sensors, rate); } /** - * Unregisters a listener for the sensors with which it is registered. + * Unregisters a listener for all sensors. * * @deprecated This method is deprecated, use - * {@link SensorManager#unregisterListener(SensorEventListener, Sensor)} + * {@link SensorManager#unregisterListener(SensorEventListener)} * instead. * * @param listener * a SensorListener object - * - * @param sensors - * a bit masks of the sensors to unregister from */ @Deprecated - public void unregisterListener(SensorListener listener, int sensors) { - unregisterLegacyListener(SENSOR_ACCELEROMETER, Sensor.TYPE_ACCELEROMETER, - listener, sensors); - unregisterLegacyListener(SENSOR_MAGNETIC_FIELD, Sensor.TYPE_MAGNETIC_FIELD, - listener, sensors); - unregisterLegacyListener(SENSOR_ORIENTATION_RAW, Sensor.TYPE_ORIENTATION, - listener, sensors); - unregisterLegacyListener(SENSOR_ORIENTATION, Sensor.TYPE_ORIENTATION, - listener, sensors); - unregisterLegacyListener(SENSOR_TEMPERATURE, Sensor.TYPE_TEMPERATURE, - listener, sensors); - } - - @SuppressWarnings("deprecation") - private void unregisterLegacyListener(int legacyType, int type, - SensorListener listener, int sensors) - { - if (listener == null) { - return; - } - // do we know about this listener? - LegacyListener legacyListener = null; - synchronized (mLegacyListenersMap) { - legacyListener = mLegacyListenersMap.get(listener); - } - if (legacyListener != null) { - // Are we deactivating this legacy sensor? - if ((sensors & legacyType) != 0) { - // if so, find the corresponding Sensor - Sensor sensor = getDefaultSensor(type); - if (sensor != null) { - // unregister this legacy sensor and if we don't - // need the corresponding Sensor, unregister it too - if (legacyListener.unregisterSensor(legacyType)) { - // corresponding sensor not needed, unregister - unregisterListener(legacyListener, sensor); - // finally check if we still need the legacyListener - // in our mapping, if not, get rid of it too. - synchronized(sListeners) { - boolean found = false; - for (ListenerDelegate i : sListeners) { - if (i.getListener() == legacyListener) { - found = true; - break; - } - } - if (!found) { - synchronized (mLegacyListenersMap) { - mLegacyListenersMap.remove(listener); - } - } - } - } - } - } - } + public void unregisterListener(SensorListener listener) { + unregisterListener(listener, SENSOR_ALL | SENSOR_ORIENTATION_RAW); } /** - * Unregisters a listener for all sensors. + * Unregisters a listener for the sensors with which it is registered. * * @deprecated This method is deprecated, use - * {@link SensorManager#unregisterListener(SensorEventListener)} + * {@link SensorManager#unregisterListener(SensorEventListener, Sensor)} * instead. * * @param listener * a SensorListener object + * + * @param sensors + * a bit masks of the sensors to unregister from */ @Deprecated - public void unregisterListener(SensorListener listener) { - unregisterListener(listener, SENSOR_ALL | SENSOR_ORIENTATION_RAW); + public void unregisterListener(SensorListener listener, int sensors) { + getLegacySensorManager().unregisterListener(listener, sensors); } /** @@ -970,7 +526,11 @@ public class SensorManager * */ public void unregisterListener(SensorEventListener listener, Sensor sensor) { - unregisterListener((Object)listener, sensor); + if (listener == null || sensor == null) { + return; + } + + unregisterListenerImpl(listener, sensor); } /** @@ -984,9 +544,16 @@ public class SensorManager * */ public void unregisterListener(SensorEventListener listener) { - unregisterListener((Object)listener); + if (listener == null) { + return; + } + + unregisterListenerImpl(listener, null); } + /** @hide */ + protected abstract void unregisterListenerImpl(SensorEventListener listener, Sensor sensor); + /** * Registers a {@link android.hardware.SensorEventListener * SensorEventListener} for the given sensor. @@ -1019,31 +586,6 @@ public class SensorManager return registerListener(listener, sensor, rate, null); } - private boolean enableSensorLocked(Sensor sensor, int delay) { - boolean result = false; - for (ListenerDelegate i : sListeners) { - if (i.hasSensor(sensor)) { - String name = sensor.getName(); - int handle = sensor.getHandle(); - result = sensors_enable_sensor(sQueue, name, handle, delay); - break; - } - } - return result; - } - - private boolean disableSensorLocked(Sensor sensor) { - for (ListenerDelegate i : sListeners) { - if (i.hasSensor(sensor)) { - // not an error, it's just that this sensor is still in use - return true; - } - } - String name = sensor.getName(); - int handle = sensor.getHandle(); - return sensors_enable_sensor(sQueue, name, handle, SENSOR_DISABLE); - } - /** * Registers a {@link android.hardware.SensorEventListener * SensorEventListener} for the given sensor. @@ -1081,7 +623,7 @@ public class SensorManager if (listener == null || sensor == null) { return false; } - boolean result = true; + int delay = -1; switch (rate) { case SENSOR_DELAY_FASTEST: @@ -1101,92 +643,12 @@ public class SensorManager break; } - synchronized (sListeners) { - // look for this listener in our list - ListenerDelegate l = null; - for (ListenerDelegate i : sListeners) { - if (i.getListener() == listener) { - l = i; - break; - } - } - - // if we don't find it, add it to the list - if (l == null) { - l = new ListenerDelegate(listener, sensor, handler); - sListeners.add(l); - // if the list is not empty, start our main thread - if (!sListeners.isEmpty()) { - if (sSensorThread.startLocked()) { - if (!enableSensorLocked(sensor, delay)) { - // oops. there was an error - sListeners.remove(l); - result = false; - } - } else { - // there was an error, remove the listener - sListeners.remove(l); - result = false; - } - } else { - // weird, we couldn't add the listener - result = false; - } - } else { - l.addSensor(sensor); - if (!enableSensorLocked(sensor, delay)) { - // oops. there was an error - l.removeSensor(sensor); - result = false; - } - } - } - - return result; - } - - private void unregisterListener(Object listener, Sensor sensor) { - if (listener == null || sensor == null) { - return; - } - - synchronized (sListeners) { - final int size = sListeners.size(); - for (int i=0 ; i<size ; i++) { - ListenerDelegate l = sListeners.get(i); - if (l.getListener() == listener) { - if (l.removeSensor(sensor) == 0) { - // if we have no more sensors enabled on this listener, - // take it off the list. - sListeners.remove(i); - } - break; - } - } - disableSensorLocked(sensor); - } + return registerListenerImpl(listener, sensor, delay, handler); } - private void unregisterListener(Object listener) { - if (listener == null) { - return; - } - - synchronized (sListeners) { - final int size = sListeners.size(); - for (int i=0 ; i<size ; i++) { - ListenerDelegate l = sListeners.get(i); - if (l.getListener() == listener) { - sListeners.remove(i); - // disable all sensors for this listener - for (Sensor sensor : l.getSensors()) { - disableSensorLocked(sensor); - } - break; - } - } - } - } + /** @hide */ + protected abstract boolean registerListenerImpl(SensorEventListener listener, Sensor sensor, + int delay, Handler handler); /** * <p> @@ -1653,228 +1115,11 @@ public class SensorManager * @param p atmospheric pressure * @return Altitude in meters */ - public static float getAltitude(float p0, float p) { + public static float getAltitude(float p0, float p) { final float coef = 1.0f / 5.255f; return 44330.0f * (1.0f - (float)Math.pow(p/p0, coef)); } - - /** - * {@hide} - */ - public void onRotationChanged(int rotation) { - synchronized(sListeners) { - sRotation = rotation; - } - } - - static int getRotation() { - synchronized(sListeners) { - return sRotation; - } - } - - private class LegacyListener implements SensorEventListener { - private float mValues[] = new float[6]; - @SuppressWarnings("deprecation") - private SensorListener mTarget; - private int mSensors; - private final LmsFilter mYawfilter = new LmsFilter(); - - @SuppressWarnings("deprecation") - LegacyListener(SensorListener target) { - mTarget = target; - mSensors = 0; - } - - void registerSensor(int legacyType) { - mSensors |= legacyType; - } - - boolean unregisterSensor(int legacyType) { - mSensors &= ~legacyType; - int mask = SENSOR_ORIENTATION|SENSOR_ORIENTATION_RAW; - if (((legacyType&mask)!=0) && ((mSensors&mask)!=0)) { - return false; - } - return true; - } - - @SuppressWarnings("deprecation") - public void onAccuracyChanged(Sensor sensor, int accuracy) { - try { - mTarget.onAccuracyChanged(sensor.getLegacyType(), accuracy); - } catch (AbstractMethodError e) { - // old app that doesn't implement this method - // just ignore it. - } - } - - @SuppressWarnings("deprecation") - public void onSensorChanged(SensorEvent event) { - final float v[] = mValues; - v[0] = event.values[0]; - v[1] = event.values[1]; - v[2] = event.values[2]; - int legacyType = event.sensor.getLegacyType(); - mapSensorDataToWindow(legacyType, v, SensorManager.getRotation()); - if (event.sensor.getType() == Sensor.TYPE_ORIENTATION) { - if ((mSensors & SENSOR_ORIENTATION_RAW)!=0) { - mTarget.onSensorChanged(SENSOR_ORIENTATION_RAW, v); - } - if ((mSensors & SENSOR_ORIENTATION)!=0) { - v[0] = mYawfilter.filter(event.timestamp, v[0]); - mTarget.onSensorChanged(SENSOR_ORIENTATION, v); - } - } else { - mTarget.onSensorChanged(legacyType, v); - } - } - - /* - * Helper function to convert the specified sensor's data to the windows's - * coordinate space from the device's coordinate space. - * - * output: 3,4,5: values in the old API format - * 0,1,2: transformed values in the old API format - * - */ - private void mapSensorDataToWindow(int sensor, - float[] values, int orientation) { - float x = values[0]; - float y = values[1]; - float z = values[2]; - - switch (sensor) { - case SensorManager.SENSOR_ORIENTATION: - case SensorManager.SENSOR_ORIENTATION_RAW: - z = -z; - break; - case SensorManager.SENSOR_ACCELEROMETER: - x = -x; - y = -y; - z = -z; - break; - case SensorManager.SENSOR_MAGNETIC_FIELD: - x = -x; - y = -y; - break; - } - values[0] = x; - values[1] = y; - values[2] = z; - values[3] = x; - values[4] = y; - values[5] = z; - - if ((orientation & Surface.ROTATION_90) != 0) { - // handles 90 and 270 rotation - switch (sensor) { - case SENSOR_ACCELEROMETER: - case SENSOR_MAGNETIC_FIELD: - values[0] =-y; - values[1] = x; - values[2] = z; - break; - case SENSOR_ORIENTATION: - case SENSOR_ORIENTATION_RAW: - values[0] = x + ((x < 270) ? 90 : -270); - values[1] = z; - values[2] = y; - break; - } - } - if ((orientation & Surface.ROTATION_180) != 0) { - x = values[0]; - y = values[1]; - z = values[2]; - // handles 180 (flip) and 270 (flip + 90) rotation - switch (sensor) { - case SENSOR_ACCELEROMETER: - case SENSOR_MAGNETIC_FIELD: - values[0] =-x; - values[1] =-y; - values[2] = z; - break; - case SENSOR_ORIENTATION: - case SENSOR_ORIENTATION_RAW: - values[0] = (x >= 180) ? (x - 180) : (x + 180); - values[1] =-y; - values[2] =-z; - break; - } - } - } - } - - class LmsFilter { - private static final int SENSORS_RATE_MS = 20; - private static final int COUNT = 12; - private static final float PREDICTION_RATIO = 1.0f/3.0f; - private static final float PREDICTION_TIME = (SENSORS_RATE_MS*COUNT/1000.0f)*PREDICTION_RATIO; - private float mV[] = new float[COUNT*2]; - private float mT[] = new float[COUNT*2]; - private int mIndex; - - public LmsFilter() { - mIndex = COUNT; - } - - public float filter(long time, float in) { - float v = in; - final float ns = 1.0f / 1000000000.0f; - final float t = time*ns; - float v1 = mV[mIndex]; - if ((v-v1) > 180) { - v -= 360; - } else if ((v1-v) > 180) { - v += 360; - } - /* Manage the circular buffer, we write the data twice spaced - * by COUNT values, so that we don't have to copy the array - * when it's full - */ - mIndex++; - if (mIndex >= COUNT*2) - mIndex = COUNT; - mV[mIndex] = v; - mT[mIndex] = t; - mV[mIndex-COUNT] = v; - mT[mIndex-COUNT] = t; - - float A, B, C, D, E; - float a, b; - int i; - - A = B = C = D = E = 0; - for (i=0 ; i<COUNT-1 ; i++) { - final int j = mIndex - 1 - i; - final float Z = mV[j]; - final float T = 0.5f*(mT[j] + mT[j+1]) - t; - float dT = mT[j] - mT[j+1]; - dT *= dT; - A += Z*dT; - B += T*(T*dT); - C += (T*dT); - D += Z*(T*dT); - E += dT; - } - b = (A*B + C*D) / (E*B + C*C); - a = (E*b - A) / C; - float f = b + PREDICTION_TIME*a; - - // Normalize - f *= (1.0f / 360.0f); - if (((f>=0)?f:-f) >= 0.5f) - f = f - (float)Math.ceil(f + 0.5f) + 1.0f; - if (f < 0) - f += 1.0f; - f *= 360.0f; - return f; - } - } - - /** Helper function to compute the angle change between two rotation matrices. * Given a current rotation matrix (R) and a previous rotation matrix * (prevR) computes the rotation around the x,y, and z axes which @@ -2060,14 +1305,66 @@ public class SensorManager Q[3] = rv[2]; } - private static native void nativeClassInit(); + private LegacySensorManager getLegacySensorManager() { + synchronized (mSensorListByType) { + if (mLegacySensorManager == null) { + Log.i(TAG, "This application is using deprecated SensorManager API which will " + + "be removed someday. Please consider switching to the new API."); + mLegacySensorManager = new LegacySensorManager(this); + } + return mLegacySensorManager; + } + } + + /** + * Sensor event pool implementation. + * @hide + */ + protected static final class SensorEventPool { + private final int mPoolSize; + private final SensorEvent mPool[]; + private int mNumItemsInPool; + + private SensorEvent createSensorEvent() { + // maximal size for all legacy events is 3 + return new SensorEvent(3); + } + + SensorEventPool(int poolSize) { + mPoolSize = poolSize; + mNumItemsInPool = poolSize; + mPool = new SensorEvent[poolSize]; + } - private static native int sensors_module_init(); - private static native int sensors_module_get_next_sensor(Sensor sensor, int next); + SensorEvent getFromPool() { + SensorEvent t = null; + synchronized (this) { + if (mNumItemsInPool > 0) { + // remove the "top" item from the pool + final int index = mPoolSize - mNumItemsInPool; + t = mPool[index]; + mPool[index] = null; + mNumItemsInPool--; + } + } + if (t == null) { + // the pool was empty or this item was removed from the pool for + // the first time. In any case, we need to create a new item. + t = createSensorEvent(); + } + return t; + } - // Used within this module from outside SensorManager, don't make private - static native int sensors_create_queue(); - static native void sensors_destroy_queue(int queue); - static native boolean sensors_enable_sensor(int queue, String name, int sensor, int enable); - static native int sensors_data_poll(int queue, float[] values, int[] status, long[] timestamp); + void returnToPool(SensorEvent t) { + synchronized (this) { + // is there space left in the pool? + if (mNumItemsInPool < mPoolSize) { + // if so, return the item to the pool + mNumItemsInPool++; + final int index = mPoolSize - mNumItemsInPool; + mPool[index] = t; + } + } + } + } } diff --git a/core/java/android/hardware/SystemSensorManager.java b/core/java/android/hardware/SystemSensorManager.java new file mode 100644 index 0000000..0204e94 --- /dev/null +++ b/core/java/android/hardware/SystemSensorManager.java @@ -0,0 +1,398 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware; + +import android.os.Looper; +import android.os.Process; +import android.os.Handler; +import android.os.Message; +import android.util.Log; +import android.util.SparseArray; +import android.util.SparseBooleanArray; +import android.util.SparseIntArray; + +import java.util.ArrayList; +import java.util.List; + +/** + * Sensor manager implementation that communicates with the built-in + * system sensors. + * + * @hide + */ +public class SystemSensorManager extends SensorManager { + private static final int SENSOR_DISABLE = -1; + private static boolean sSensorModuleInitialized = false; + private static ArrayList<Sensor> sFullSensorsList = new ArrayList<Sensor>(); + /* The thread and the sensor list are global to the process + * but the actual thread is spawned on demand */ + private static SensorThread sSensorThread; + private static int sQueue; + + // Used within this module from outside SensorManager, don't make private + static SparseArray<Sensor> sHandleToSensor = new SparseArray<Sensor>(); + static final ArrayList<ListenerDelegate> sListeners = + new ArrayList<ListenerDelegate>(); + + // Common pool of sensor events. + static SensorEventPool sPool; + + // Looper associated with the context in which this instance was created. + final Looper mMainLooper; + + /*-----------------------------------------------------------------------*/ + + static private class SensorThread { + + Thread mThread; + boolean mSensorsReady; + + SensorThread() { + } + + @Override + protected void finalize() { + } + + // must be called with sListeners lock + boolean startLocked() { + try { + if (mThread == null) { + mSensorsReady = false; + SensorThreadRunnable runnable = new SensorThreadRunnable(); + Thread thread = new Thread(runnable, SensorThread.class.getName()); + thread.start(); + synchronized (runnable) { + while (mSensorsReady == false) { + runnable.wait(); + } + } + mThread = thread; + } + } catch (InterruptedException e) { + } + return mThread == null ? false : true; + } + + private class SensorThreadRunnable implements Runnable { + SensorThreadRunnable() { + } + + private boolean open() { + // NOTE: this cannot synchronize on sListeners, since + // it's held in the main thread at least until we + // return from here. + sQueue = sensors_create_queue(); + return true; + } + + public void run() { + //Log.d(TAG, "entering main sensor thread"); + final float[] values = new float[3]; + final int[] status = new int[1]; + final long timestamp[] = new long[1]; + Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_DISPLAY); + + if (!open()) { + return; + } + + synchronized (this) { + // we've open the driver, we're ready to open the sensors + mSensorsReady = true; + this.notify(); + } + + while (true) { + // wait for an event + final int sensor = sensors_data_poll(sQueue, values, status, timestamp); + + int accuracy = status[0]; + synchronized (sListeners) { + if (sensor == -1 || sListeners.isEmpty()) { + // we lost the connection to the event stream. this happens + // when the last listener is removed or if there is an error + if (sensor == -1 && !sListeners.isEmpty()) { + // log a warning in case of abnormal termination + Log.e(TAG, "_sensors_data_poll() failed, we bail out: sensors=" + sensor); + } + // we have no more listeners or polling failed, terminate the thread + sensors_destroy_queue(sQueue); + sQueue = 0; + mThread = null; + break; + } + final Sensor sensorObject = sHandleToSensor.get(sensor); + if (sensorObject != null) { + // report the sensor event to all listeners that + // care about it. + final int size = sListeners.size(); + for (int i=0 ; i<size ; i++) { + ListenerDelegate listener = sListeners.get(i); + if (listener.hasSensor(sensorObject)) { + // this is asynchronous (okay to call + // with sListeners lock held). + listener.onSensorChangedLocked(sensorObject, + values, timestamp, accuracy); + } + } + } + } + } + //Log.d(TAG, "exiting main sensor thread"); + } + } + } + + /*-----------------------------------------------------------------------*/ + + private class ListenerDelegate { + private final SensorEventListener mSensorEventListener; + private final ArrayList<Sensor> mSensorList = new ArrayList<Sensor>(); + private final Handler mHandler; + public SparseBooleanArray mSensors = new SparseBooleanArray(); + public SparseBooleanArray mFirstEvent = new SparseBooleanArray(); + public SparseIntArray mSensorAccuracies = new SparseIntArray(); + + ListenerDelegate(SensorEventListener listener, Sensor sensor, Handler handler) { + mSensorEventListener = listener; + Looper looper = (handler != null) ? handler.getLooper() : mMainLooper; + // currently we create one Handler instance per listener, but we could + // have one per looper (we'd need to pass the ListenerDelegate + // instance to handleMessage and keep track of them separately). + mHandler = new Handler(looper) { + @Override + public void handleMessage(Message msg) { + final SensorEvent t = (SensorEvent)msg.obj; + final int handle = t.sensor.getHandle(); + + switch (t.sensor.getType()) { + // Only report accuracy for sensors that support it. + case Sensor.TYPE_MAGNETIC_FIELD: + case Sensor.TYPE_ORIENTATION: + // call onAccuracyChanged() only if the value changes + final int accuracy = mSensorAccuracies.get(handle); + if ((t.accuracy >= 0) && (accuracy != t.accuracy)) { + mSensorAccuracies.put(handle, t.accuracy); + mSensorEventListener.onAccuracyChanged(t.sensor, t.accuracy); + } + break; + default: + // For other sensors, just report the accuracy once + if (mFirstEvent.get(handle) == false) { + mFirstEvent.put(handle, true); + mSensorEventListener.onAccuracyChanged( + t.sensor, SENSOR_STATUS_ACCURACY_HIGH); + } + break; + } + + mSensorEventListener.onSensorChanged(t); + sPool.returnToPool(t); + } + }; + addSensor(sensor); + } + + Object getListener() { + return mSensorEventListener; + } + + void addSensor(Sensor sensor) { + mSensors.put(sensor.getHandle(), true); + mSensorList.add(sensor); + } + int removeSensor(Sensor sensor) { + mSensors.delete(sensor.getHandle()); + mSensorList.remove(sensor); + return mSensors.size(); + } + boolean hasSensor(Sensor sensor) { + return mSensors.get(sensor.getHandle()); + } + List<Sensor> getSensors() { + return mSensorList; + } + + void onSensorChangedLocked(Sensor sensor, float[] values, long[] timestamp, int accuracy) { + SensorEvent t = sPool.getFromPool(); + final float[] v = t.values; + v[0] = values[0]; + v[1] = values[1]; + v[2] = values[2]; + t.timestamp = timestamp[0]; + t.accuracy = accuracy; + t.sensor = sensor; + Message msg = Message.obtain(); + msg.what = 0; + msg.obj = t; + msg.setAsynchronous(true); + mHandler.sendMessage(msg); + } + } + + /** + * {@hide} + */ + public SystemSensorManager(Looper mainLooper) { + mMainLooper = mainLooper; + + synchronized(sListeners) { + if (!sSensorModuleInitialized) { + sSensorModuleInitialized = true; + + nativeClassInit(); + + // initialize the sensor list + sensors_module_init(); + final ArrayList<Sensor> fullList = sFullSensorsList; + int i = 0; + do { + Sensor sensor = new Sensor(); + i = sensors_module_get_next_sensor(sensor, i); + + if (i>=0) { + //Log.d(TAG, "found sensor: " + sensor.getName() + + // ", handle=" + sensor.getHandle()); + fullList.add(sensor); + sHandleToSensor.append(sensor.getHandle(), sensor); + } + } while (i>0); + + sPool = new SensorEventPool( sFullSensorsList.size()*2 ); + sSensorThread = new SensorThread(); + } + } + } + + /** @hide */ + @Override + protected List<Sensor> getFullSensorList() { + return sFullSensorsList; + } + + private boolean enableSensorLocked(Sensor sensor, int delay) { + boolean result = false; + for (ListenerDelegate i : sListeners) { + if (i.hasSensor(sensor)) { + String name = sensor.getName(); + int handle = sensor.getHandle(); + result = sensors_enable_sensor(sQueue, name, handle, delay); + break; + } + } + return result; + } + + private boolean disableSensorLocked(Sensor sensor) { + for (ListenerDelegate i : sListeners) { + if (i.hasSensor(sensor)) { + // not an error, it's just that this sensor is still in use + return true; + } + } + String name = sensor.getName(); + int handle = sensor.getHandle(); + return sensors_enable_sensor(sQueue, name, handle, SENSOR_DISABLE); + } + + /** @hide */ + @Override + protected boolean registerListenerImpl(SensorEventListener listener, Sensor sensor, + int delay, Handler handler) { + boolean result = true; + synchronized (sListeners) { + // look for this listener in our list + ListenerDelegate l = null; + for (ListenerDelegate i : sListeners) { + if (i.getListener() == listener) { + l = i; + break; + } + } + + // if we don't find it, add it to the list + if (l == null) { + l = new ListenerDelegate(listener, sensor, handler); + sListeners.add(l); + // if the list is not empty, start our main thread + if (!sListeners.isEmpty()) { + if (sSensorThread.startLocked()) { + if (!enableSensorLocked(sensor, delay)) { + // oops. there was an error + sListeners.remove(l); + result = false; + } + } else { + // there was an error, remove the listener + sListeners.remove(l); + result = false; + } + } else { + // weird, we couldn't add the listener + result = false; + } + } else if (!l.hasSensor(sensor)) { + l.addSensor(sensor); + if (!enableSensorLocked(sensor, delay)) { + // oops. there was an error + l.removeSensor(sensor); + result = false; + } + } + } + + return result; + } + + /** @hide */ + @Override + protected void unregisterListenerImpl(SensorEventListener listener, Sensor sensor) { + synchronized (sListeners) { + final int size = sListeners.size(); + for (int i=0 ; i<size ; i++) { + ListenerDelegate l = sListeners.get(i); + if (l.getListener() == listener) { + if (sensor == null) { + sListeners.remove(i); + // disable all sensors for this listener + for (Sensor s : l.getSensors()) { + disableSensorLocked(s); + } + } else if (l.removeSensor(sensor) == 0) { + // if we have no more sensors enabled on this listener, + // take it off the list. + sListeners.remove(i); + disableSensorLocked(sensor); + } + break; + } + } + } + } + + private static native void nativeClassInit(); + + private static native int sensors_module_init(); + private static native int sensors_module_get_next_sensor(Sensor sensor, int next); + + // Used within this module from outside SensorManager, don't make private + static native int sensors_create_queue(); + static native void sensors_destroy_queue(int queue); + static native boolean sensors_enable_sensor(int queue, String name, int sensor, int enable); + static native int sensors_data_poll(int queue, float[] values, int[] status, long[] timestamp); +} diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index de16985..ef4209f 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -373,10 +373,11 @@ public class ConnectivityManager { } /** - * Gets you info about the current data network. - * Call {@link NetworkInfo#isConnected()} on the returned {@link NetworkInfo} - * to check if the device has a data connection. - */ + * Returns details about the currently active data network. When connected, + * this network is the default route for outgoing connections. You should + * always check {@link NetworkInfo#isConnected()} before initiating network + * traffic. This may return {@code null} when no networks are available. + */ public NetworkInfo getActiveNetworkInfo() { try { return mService.getActiveNetworkInfo(); @@ -856,4 +857,19 @@ public class ConnectivityManager { } catch (RemoteException e) {} return false; } + + /** + * Returns if the currently active data network is metered. A network is + * classified as metered when the user is sensitive to heavy data usage on + * that connection. You should check this before doing large data transfers, + * and warn the user or delay the operation until another network is + * available. + */ + public boolean isActiveNetworkMetered() { + try { + return mService.isActiveNetworkMetered(); + } catch (RemoteException e) { + return false; + } + } } diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl index 7046008..92aeff2 100644 --- a/core/java/android/net/IConnectivityManager.aidl +++ b/core/java/android/net/IConnectivityManager.aidl @@ -51,6 +51,7 @@ interface IConnectivityManager NetworkState[] getAllNetworkState(); NetworkQuotaInfo getActiveNetworkQuotaInfo(); + boolean isActiveNetworkMetered(); boolean setRadios(boolean onOff); diff --git a/core/java/android/net/INetworkPolicyManager.aidl b/core/java/android/net/INetworkPolicyManager.aidl index 35e8e47..3250ae7 100644 --- a/core/java/android/net/INetworkPolicyManager.aidl +++ b/core/java/android/net/INetworkPolicyManager.aidl @@ -51,5 +51,6 @@ interface INetworkPolicyManager { boolean getRestrictBackground(); NetworkQuotaInfo getNetworkQuotaInfo(in NetworkState state); + boolean isNetworkMetered(in NetworkState state); } diff --git a/core/java/android/net/INetworkStatsService.aidl b/core/java/android/net/INetworkStatsService.aidl index b4f6367..08d4c6c 100644 --- a/core/java/android/net/INetworkStatsService.aidl +++ b/core/java/android/net/INetworkStatsService.aidl @@ -32,6 +32,9 @@ interface INetworkStatsService { /** Return data layer snapshot of UID network usage. */ NetworkStats getDataLayerSnapshotForUid(int uid); + /** Return set of any ifaces associated with mobile networks since boot. */ + String[] getMobileIfaces(); + /** Increment data layer count of operations performed for UID and tag. */ void incrementOperationCount(int uid, int tag, int operationCount); diff --git a/core/java/android/net/NetworkTemplate.java b/core/java/android/net/NetworkTemplate.java index 50432a1..39a4d7b 100644 --- a/core/java/android/net/NetworkTemplate.java +++ b/core/java/android/net/NetworkTemplate.java @@ -48,6 +48,8 @@ public class NetworkTemplate implements Parcelable { public static final int MATCH_MOBILE_4G = 3; public static final int MATCH_WIFI = 4; public static final int MATCH_ETHERNET = 5; + public static final int MATCH_MOBILE_WILDCARD = 6; + public static final int MATCH_WIFI_WILDCARD = 7; /** * Set of {@link NetworkInfo#getType()} that reflect data usage. @@ -86,11 +88,19 @@ public class NetworkTemplate implements Parcelable { } /** + * Template to match {@link ConnectivityManager#TYPE_MOBILE} networks, + * regardless of IMSI. + */ + public static NetworkTemplate buildTemplateMobileWildcard() { + return new NetworkTemplate(MATCH_MOBILE_WILDCARD, null, null); + } + + /** * Template to match all {@link ConnectivityManager#TYPE_WIFI} networks, * regardless of SSID. */ public static NetworkTemplate buildTemplateWifiWildcard() { - return new NetworkTemplate(MATCH_WIFI, null, null); + return new NetworkTemplate(MATCH_WIFI_WILDCARD, null, null); } @Deprecated @@ -198,6 +208,10 @@ public class NetworkTemplate implements Parcelable { return matchesWifi(ident); case MATCH_ETHERNET: return matchesEthernet(ident); + case MATCH_MOBILE_WILDCARD: + return matchesMobileWildcard(ident); + case MATCH_WIFI_WILDCARD: + return matchesWifiWildcard(ident); default: throw new IllegalArgumentException("unknown network template"); } @@ -257,13 +271,7 @@ public class NetworkTemplate implements Parcelable { private boolean matchesWifi(NetworkIdentity ident) { switch (ident.mType) { case TYPE_WIFI: - if (mNetworkId == null) { - return true; - } else { - return Objects.equal(mNetworkId, ident.mNetworkId); - } - case TYPE_WIFI_P2P: - return mNetworkId == null; + return Objects.equal(mNetworkId, ident.mNetworkId); default: return false; } @@ -279,6 +287,24 @@ public class NetworkTemplate implements Parcelable { return false; } + private boolean matchesMobileWildcard(NetworkIdentity ident) { + if (ident.mType == TYPE_WIMAX) { + return true; + } else { + return contains(DATA_USAGE_NETWORK_TYPES, ident.mType); + } + } + + private boolean matchesWifiWildcard(NetworkIdentity ident) { + switch (ident.mType) { + case TYPE_WIFI: + case TYPE_WIFI_P2P: + return true; + default: + return false; + } + } + private static String getMatchRuleName(int matchRule) { switch (matchRule) { case MATCH_MOBILE_3G_LOWER: @@ -291,6 +317,10 @@ public class NetworkTemplate implements Parcelable { return "WIFI"; case MATCH_ETHERNET: return "ETHERNET"; + case MATCH_MOBILE_WILDCARD: + return "MOBILE_WILDCARD"; + case MATCH_WIFI_WILDCARD: + return "WIFI_WILDCARD"; default: return "UNKNOWN"; } diff --git a/core/java/android/net/TrafficStats.java b/core/java/android/net/TrafficStats.java index ee3e165..e437d2e 100644 --- a/core/java/android/net/TrafficStats.java +++ b/core/java/android/net/TrafficStats.java @@ -88,6 +88,16 @@ public class TrafficStats { */ public static final int TAG_SYSTEM_BACKUP = 0xFFFFFF03; + private static INetworkStatsService sStatsService; + + private synchronized static INetworkStatsService getStatsService() { + if (sStatsService == null) { + sStatsService = INetworkStatsService.Stub.asInterface( + ServiceManager.getService(Context.NETWORK_STATS_SERVICE)); + } + return sStatsService; + } + /** * Snapshot of {@link NetworkStats} when the currently active profiling * session started, or {@code null} if no session active. @@ -228,11 +238,9 @@ public class TrafficStats { * @param operationCount Number of operations to increment count by. */ public static void incrementOperationCount(int tag, int operationCount) { - final INetworkStatsService statsService = INetworkStatsService.Stub.asInterface( - ServiceManager.getService(Context.NETWORK_STATS_SERVICE)); final int uid = android.os.Process.myUid(); try { - statsService.incrementOperationCount(uid, tag, operationCount); + getStatsService().incrementOperationCount(uid, tag, operationCount); } catch (RemoteException e) { throw new RuntimeException(e); } @@ -257,7 +265,13 @@ public class TrafficStats { * @return number of packets. If the statistics are not supported by this device, * {@link #UNSUPPORTED} will be returned. */ - public static native long getMobileTxPackets(); + public static long getMobileTxPackets() { + long total = 0; + for (String iface : getMobileIfaces()) { + total += getTxPackets(iface); + } + return total; + } /** * Get the total number of packets received through the mobile interface. @@ -265,7 +279,13 @@ public class TrafficStats { * @return number of packets. If the statistics are not supported by this device, * {@link #UNSUPPORTED} will be returned. */ - public static native long getMobileRxPackets(); + public static long getMobileRxPackets() { + long total = 0; + for (String iface : getMobileIfaces()) { + total += getRxPackets(iface); + } + return total; + } /** * Get the total number of bytes transmitted through the mobile interface. @@ -273,7 +293,13 @@ public class TrafficStats { * @return number of bytes. If the statistics are not supported by this device, * {@link #UNSUPPORTED} will be returned. */ - public static native long getMobileTxBytes(); + public static long getMobileTxBytes() { + long total = 0; + for (String iface : getMobileIfaces()) { + total += getTxBytes(iface); + } + return total; + } /** * Get the total number of bytes received through the mobile interface. @@ -281,7 +307,13 @@ public class TrafficStats { * @return number of bytes. If the statistics are not supported by this device, * {@link #UNSUPPORTED} will be returned. */ - public static native long getMobileRxBytes(); + public static long getMobileRxBytes() { + long total = 0; + for (String iface : getMobileIfaces()) { + total += getRxBytes(iface); + } + return total; + } /** * Get the total number of packets transmitted through the specified interface. @@ -290,7 +322,9 @@ public class TrafficStats { * {@link #UNSUPPORTED} will be returned. * @hide */ - public static native long getTxPackets(String iface); + public static long getTxPackets(String iface) { + return nativeGetIfaceStat(iface, TYPE_TX_PACKETS); + } /** * Get the total number of packets received through the specified interface. @@ -299,7 +333,9 @@ public class TrafficStats { * {@link #UNSUPPORTED} will be returned. * @hide */ - public static native long getRxPackets(String iface); + public static long getRxPackets(String iface) { + return nativeGetIfaceStat(iface, TYPE_RX_PACKETS); + } /** * Get the total number of bytes transmitted through the specified interface. @@ -308,7 +344,9 @@ public class TrafficStats { * {@link #UNSUPPORTED} will be returned. * @hide */ - public static native long getTxBytes(String iface); + public static long getTxBytes(String iface) { + return nativeGetIfaceStat(iface, TYPE_TX_BYTES); + } /** * Get the total number of bytes received through the specified interface. @@ -317,8 +355,9 @@ public class TrafficStats { * {@link #UNSUPPORTED} will be returned. * @hide */ - public static native long getRxBytes(String iface); - + public static long getRxBytes(String iface) { + return nativeGetIfaceStat(iface, TYPE_RX_BYTES); + } /** * Get the total number of packets sent through all network interfaces. @@ -326,7 +365,9 @@ public class TrafficStats { * @return the number of packets. If the statistics are not supported by this device, * {@link #UNSUPPORTED} will be returned. */ - public static native long getTotalTxPackets(); + public static long getTotalTxPackets() { + return nativeGetTotalStat(TYPE_TX_PACKETS); + } /** * Get the total number of packets received through all network interfaces. @@ -334,7 +375,9 @@ public class TrafficStats { * @return number of packets. If the statistics are not supported by this device, * {@link #UNSUPPORTED} will be returned. */ - public static native long getTotalRxPackets(); + public static long getTotalRxPackets() { + return nativeGetTotalStat(TYPE_RX_PACKETS); + } /** * Get the total number of bytes sent through all network interfaces. @@ -342,7 +385,9 @@ public class TrafficStats { * @return number of bytes. If the statistics are not supported by this device, * {@link #UNSUPPORTED} will be returned. */ - public static native long getTotalTxBytes(); + public static long getTotalTxBytes() { + return nativeGetTotalStat(TYPE_TX_BYTES); + } /** * Get the total number of bytes received through all network interfaces. @@ -350,7 +395,9 @@ public class TrafficStats { * @return number of bytes. If the statistics are not supported by this device, * {@link #UNSUPPORTED} will be returned. */ - public static native long getTotalRxBytes(); + public static long getTotalRxBytes() { + return nativeGetTotalStat(TYPE_RX_BYTES); + } /** * Get the number of bytes sent through the network for this UID. @@ -483,7 +530,6 @@ public class TrafficStats { */ public static native long getUidTcpRxSegments(int uid); - /** * Get the number of UDP packets sent for this UID. * Includes DNS requests. @@ -515,13 +561,33 @@ public class TrafficStats { * special permission. */ private static NetworkStats getDataLayerSnapshotForUid(Context context) { - final INetworkStatsService statsService = INetworkStatsService.Stub.asInterface( - ServiceManager.getService(Context.NETWORK_STATS_SERVICE)); final int uid = android.os.Process.myUid(); try { - return statsService.getDataLayerSnapshotForUid(uid); + return getStatsService().getDataLayerSnapshotForUid(uid); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** + * Return set of any ifaces associated with mobile networks since boot. + * Interfaces are never removed from this list, so counters should always be + * monotonic. + */ + private static String[] getMobileIfaces() { + try { + return getStatsService().getMobileIfaces(); } catch (RemoteException e) { throw new RuntimeException(e); } } + + // NOTE: keep these in sync with android_net_TrafficStats.cpp + private static final int TYPE_RX_BYTES = 0; + private static final int TYPE_RX_PACKETS = 1; + private static final int TYPE_TX_BYTES = 2; + private static final int TYPE_TX_PACKETS = 3; + + private static native long nativeGetTotalStat(int type); + private static native long nativeGetIfaceStat(String iface, int type); } diff --git a/core/java/android/net/nsd/DnsSdServiceInfo.java b/core/java/android/net/nsd/DnsSdServiceInfo.java index 33c3eb9..66abd3a 100644 --- a/core/java/android/net/nsd/DnsSdServiceInfo.java +++ b/core/java/android/net/nsd/DnsSdServiceInfo.java @@ -22,8 +22,8 @@ import android.os.Parcel; import java.net.InetAddress; /** - * Defines a service based on DNS service discovery - * {@hide} + * A class representing service information for network service discovery + * {@see NsdManager} */ public class DnsSdServiceInfo implements NetworkServiceInfo, Parcelable { @@ -40,56 +40,63 @@ public class DnsSdServiceInfo implements NetworkServiceInfo, Parcelable { public DnsSdServiceInfo() { } + /** @hide */ public DnsSdServiceInfo(String sn, String rt, DnsSdTxtRecord tr) { mServiceName = sn; mServiceType = rt; mTxtRecord = tr; } + /** Get the service name */ @Override - /** @hide */ public String getServiceName() { return mServiceName; } + /** Set the service name */ @Override - /** @hide */ public void setServiceName(String s) { mServiceName = s; } + /** Get the service type */ @Override - /** @hide */ public String getServiceType() { return mServiceType; } + /** Set the service type */ @Override - /** @hide */ public void setServiceType(String s) { mServiceType = s; } + /** @hide */ public DnsSdTxtRecord getTxtRecord() { return mTxtRecord; } + /** @hide */ public void setTxtRecord(DnsSdTxtRecord t) { mTxtRecord = new DnsSdTxtRecord(t); } + /** Get the host address. The host address is valid for a resolved service. */ public InetAddress getHost() { return mHost; } + /** Set the host address */ public void setHost(InetAddress s) { mHost = s; } + /** Get port number. The port number is valid for a resolved service. */ public int getPort() { return mPort; } + /** Set port number */ public void setPort(int p) { mPort = p; } @@ -147,5 +154,4 @@ public class DnsSdServiceInfo implements NetworkServiceInfo, Parcelable { return new DnsSdServiceInfo[size]; } }; - } diff --git a/core/java/android/net/nsd/DnsSdTxtRecord.java b/core/java/android/net/nsd/DnsSdTxtRecord.java index 952e02f..ccb9a91 100644 --- a/core/java/android/net/nsd/DnsSdTxtRecord.java +++ b/core/java/android/net/nsd/DnsSdTxtRecord.java @@ -37,7 +37,6 @@ import java.util.Arrays; * The DnsSdTxtRecord object stores the entire TXT data as a single byte array, traversing it * as need be to implement its various methods. * - * @hide */ public class DnsSdTxtRecord implements Parcelable { private static final byte mSeperator = '='; diff --git a/core/java/android/net/nsd/NsdManager.java b/core/java/android/net/nsd/NsdManager.java index 505f11b..dac8d20 100644 --- a/core/java/android/net/nsd/NsdManager.java +++ b/core/java/android/net/nsd/NsdManager.java @@ -24,29 +24,110 @@ import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.os.Messenger; +import android.text.TextUtils; import android.util.Log; import com.android.internal.util.AsyncChannel; import com.android.internal.util.Protocol; /** - * The Network Service Discovery Manager class provides the API for service - * discovery. Service discovery enables applications to discover and connect with services - * on a network. Example applications include a game application discovering another instance - * of the game application or a printer application discovering other printers on a network. + * The Network Service Discovery Manager class provides the API to discover services + * on a network. As an example, if device A and device B are connected over a Wi-Fi + * network, a game registered on device A can be discovered by a game on device + * B. Another example use case is an application discovering printers on the network. + * + * <p> The API currently supports DNS based service discovery and discovery is currently + * limited to a local network over Multicast DNS. In future, it will be extended to + * support wide area discovery and other service discovery mechanisms. + * DNS service discovery is described at http://files.dns-sd.org/draft-cheshire-dnsext-dns-sd.txt * * <p> The API is asynchronous and responses to requests from an application are on listener - * callbacks provided by the application. The application needs to do an initialization with - * {@link #initialize} before doing any operation. + * callbacks provided by the application. The application must invoke {@link #initialize} before + * doing any other operation. + * + * <p> There are three main operations the API supports - registration, discovery and resolution. + * <pre> + * Application start + * | + * | <---------------------------------------------- + * initialize() | + * | | + * | Wait until channel connects | + * | before doing any operation | + * | | + * onChannelConnected() __________ | + * | | | + * | | | + * | onServiceRegistered() | | + * Register any local services / | | + * to be advertised with \ | | If application needs to + * registerService() onFailure() | | do any further operations + * | | | again, it needs to + * | | | initialize() connection + * discoverServices() | | to framework again + * | | | + * Maintain a list to track | | + * discovered services | | + * | | | + * |---------> |-> onChannelDisconnected() + * | | | + * | onServiceFound() | + * | | | + * | add service to list | + * | | | + * |<---------- | + * | | + * |---------> | + * | | | + * | onServiceLost() | + * | | | + * | remove service from list | + * | | | + * |<---------- | + * | | + * | | + * | Connect to a service | + * | from list ? | + * | | + * resolveService() | + * | | + * onServiceResolved() | + * | | + * Establish connection to service | + * with the host and port information | + * | | + * | ___________| + * deinitialize() + * when done with all operations + * or before quit + * + * </pre> + * An application that needs to advertise itself over a network for other applications to + * discover it can do so with a call to {@link #registerService}. If Example is a http based + * application that can provide HTML data to peer services, it can register a name "Example" + * with service type "_http._tcp". A successful registration is notified with a callback to + * {@link DnsSdRegisterListener#onServiceRegistered} and a failure to register is notified + * over {@link DnsSdRegisterListener#onFailure} + * + * <p> A peer application looking for http services can initiate a discovery for "_http._tcp" + * with a call to {@link #discoverServices}. A service found is notified with a callback + * to {@link DnsSdDiscoveryListener#onServiceFound} and a service lost is notified on + * {@link DnsSdDiscoveryListener#onServiceLost}. + * + * <p> Once the peer application discovers the "Example" http srevice, and needs to receive data + * from the "Example" application, it can initiate a resolve with {@link #resolveService} to + * resolve the host and port details for the purpose of establishing a connection. A successful + * resolve is notified on {@link DnsSdResolveListener#onServiceResolved} and a failure is notified + * on {@link DnsSdResolveListener#onFailure}. * - * <p> Android currently supports DNS based service discovery and it is limited to a local - * network with the use of multicast DNS. In future, this class will be - * extended to support other service discovery mechanisms. + * Applications can reserve for a service type at + * http://www.iana.org/form/ports-service. Existing services can be found at + * http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xml * * Get an instance of this class by calling {@link android.content.Context#getSystemService(String) * Context.getSystemService(Context.NSD_SERVICE)}. - * @hide * + * {@see DnsSdServiceInfo} */ public class NsdManager { private static final String TAG = "NsdManager"; @@ -80,27 +161,32 @@ public class NsdManager { public static final int REGISTER_SERVICE_SUCCEEDED = BASE + 11; /** @hide */ - public static final int UPDATE_SERVICE = BASE + 12; + public static final int UNREGISTER_SERVICE = BASE + 12; /** @hide */ - public static final int UPDATE_SERVICE_FAILED = BASE + 13; + public static final int UNREGISTER_SERVICE_FAILED = BASE + 13; /** @hide */ - public static final int UPDATE_SERVICE_SUCCEEDED = BASE + 14; + public static final int UNREGISTER_SERVICE_SUCCEEDED = BASE + 14; /** @hide */ - public static final int RESOLVE_SERVICE = BASE + 15; + public static final int UPDATE_SERVICE = BASE + 15; /** @hide */ - public static final int RESOLVE_SERVICE_FAILED = BASE + 16; + public static final int UPDATE_SERVICE_FAILED = BASE + 16; /** @hide */ - public static final int RESOLVE_SERVICE_SUCCEEDED = BASE + 17; + public static final int UPDATE_SERVICE_SUCCEEDED = BASE + 17; /** @hide */ - public static final int STOP_RESOLVE = BASE + 18; + public static final int RESOLVE_SERVICE = BASE + 18; /** @hide */ - public static final int STOP_RESOLVE_FAILED = BASE + 19; + public static final int RESOLVE_SERVICE_FAILED = BASE + 19; /** @hide */ - public static final int STOP_RESOLVE_SUCCEEDED = BASE + 20; - + public static final int RESOLVE_SERVICE_SUCCEEDED = BASE + 20; + /** @hide */ + public static final int STOP_RESOLVE = BASE + 21; + /** @hide */ + public static final int STOP_RESOLVE_FAILED = BASE + 22; + /** @hide */ + public static final int STOP_RESOLVE_SUCCEEDED = BASE + 23; /** * Create a new Nsd instance. Applications use @@ -115,36 +201,44 @@ public class NsdManager { } /** + * Passed with onFailure() calls. * Indicates that the operation failed due to an internal error. */ public static final int ERROR = 0; /** - * Indicates that the operation failed because service discovery is unsupported on the device. + * Passed with onFailure() calls. + * Indicates that the operation failed because service discovery + * is unsupported on the device. */ public static final int UNSUPPORTED = 1; /** - * Indicates that the operation failed because the framework is busy and - * unable to service the request. + * Passed with onFailure() calls. + * Indicates that the operation failed because the framework is + * busy and unable to service the request. */ public static final int BUSY = 2; /** + * Passed with onFailure() calls. * Indicates that the operation failed because it is already active. */ public static final int ALREADY_ACTIVE = 3; /** + * Passed with onFailure() calls. * Indicates that the operation failed because maximum limit on * service registrations has reached. */ public static final int MAX_REGS_REACHED = 4; - - /** Interface for callback invocation when framework channel is connected or lost */ public interface ChannelListener { + /** + * The channel to the framework is connected. + * Application can initiate calls into the framework using the channel instance passed. + */ public void onChannelConnected(Channel c); /** * The channel to the framework has been disconnected. @@ -153,6 +247,7 @@ public class NsdManager { public void onChannelDisconnected(); } + /** Generic interface for callback invocation for a success or failure */ public interface ActionListener { public void onFailure(int errorCode); @@ -160,11 +255,12 @@ public class NsdManager { public void onSuccess(); } + /** Interface for callback invocation for service discovery */ public interface DnsSdDiscoveryListener { public void onFailure(int errorCode); - public void onStarted(String registrationType); + public void onStarted(String serviceType); public void onServiceFound(DnsSdServiceInfo serviceInfo); @@ -172,6 +268,7 @@ public class NsdManager { } + /** Interface for callback invocation for service registration */ public interface DnsSdRegisterListener { public void onFailure(int errorCode); @@ -179,6 +276,7 @@ public class NsdManager { public void onServiceRegistered(int registeredId, DnsSdServiceInfo serviceInfo); } + /** @hide */ public interface DnsSdUpdateRegistrationListener { public void onFailure(int errorCode); @@ -186,6 +284,7 @@ public class NsdManager { public void onServiceUpdated(int registeredId, DnsSdTxtRecord txtRecord); } + /** Interface for callback invocation for service resolution */ public interface DnsSdResolveListener { public void onFailure(int errorCode); @@ -208,6 +307,7 @@ public class NsdManager { private DnsSdDiscoveryListener mDnsSdDiscoveryListener; private ActionListener mDnsSdStopDiscoveryListener; private DnsSdRegisterListener mDnsSdRegisterListener; + private ActionListener mDnsSdUnregisterListener; private DnsSdUpdateRegistrationListener mDnsSdUpdateListener; private DnsSdResolveListener mDnsSdResolveListener; private ActionListener mDnsSdStopResolveListener; @@ -279,7 +379,17 @@ public class NsdManager { (DnsSdServiceInfo) message.obj); } break; - case UPDATE_SERVICE_FAILED: + case UNREGISTER_SERVICE_FAILED: + if (mDnsSdUnregisterListener != null) { + mDnsSdUnregisterListener.onFailure(message.arg1); + } + break; + case UNREGISTER_SERVICE_SUCCEEDED: + if (mDnsSdUnregisterListener != null) { + mDnsSdUnregisterListener.onSuccess(); + } + break; + case UPDATE_SERVICE_FAILED: if (mDnsSdUpdateListener != null) { mDnsSdUpdateListener.onFailure(message.arg1); } @@ -319,6 +429,10 @@ public class NsdManager { } } + private static void checkChannel(Channel c) { + if (c == null) throw new IllegalArgumentException("Channel needs to be initialized"); + } + /** * Registers the application with the service discovery framework. This function * must be the first to be called before any other operations are performed. No service @@ -327,7 +441,7 @@ public class NsdManager { * * @param srcContext is the context of the source * @param srcLooper is the Looper on which the callbacks are receivied - * @param listener for callback at loss of framework communication. + * @param listener for callback at loss of framework communication. Cannot be null. */ public void initialize(Context srcContext, Looper srcLooper, ChannelListener listener) { Messenger messenger = getMessenger(); @@ -339,88 +453,142 @@ public class NsdManager { } /** - * Set the listener for service discovery. Can be null. - */ - public void setDiscoveryListener(Channel c, DnsSdDiscoveryListener b) { - if (c == null) throw new IllegalArgumentException("Channel needs to be initialized"); - c.mDnsSdDiscoveryListener = b; - } - - /** - * Set the listener for stop service discovery. Can be null. - */ - public void setStopDiscoveryListener(Channel c, ActionListener a) { - if (c == null) throw new IllegalArgumentException("Channel needs to be initialized"); - c.mDnsSdStopDiscoveryListener = a; - } - - /** - * Set the listener for service registration. Can be null. - */ - public void setRegisterListener(Channel c, DnsSdRegisterListener b) { - if (c == null) throw new IllegalArgumentException("Channel needs to be initialized"); - c.mDnsSdRegisterListener = b; - } - - /** - * Set the listener for service registration. Can be null. + * Disconnects application from service discovery framework. No further operations + * will succeed until a {@link #initialize} is called again. + * + * @param c channel initialized with {@link #initialize} */ - public void setUpdateRegistrationListener(Channel c, DnsSdUpdateRegistrationListener b) { - if (c == null) throw new IllegalArgumentException("Channel needs to be initialized"); - c.mDnsSdUpdateListener = b; + public void deinitialize(Channel c) { + checkChannel(c); + c.mAsyncChannel.disconnect(); } /** - * Set the listener for service resolution. Can be null. + * Register a service to be discovered by other services. + * + * <p> The function call immediately returns after sending a request to register service + * to the framework. The application is notified of a success to initiate + * discovery through the callback {@link DnsSdRegisterListener#onServiceRegistered} or a failure + * through {@link DnsSdRegisterListener#onFailure}. + * + * @param c is the channel created at {@link #initialize} + * @param serviceType The service type being advertised. + * @param port on which the service is listenering for incoming connections + * @param listener for success or failure callback. Can be null. */ - public void setResolveListener(Channel c, DnsSdResolveListener b) { - if (c == null) throw new IllegalArgumentException("Channel needs to be initialized"); - c.mDnsSdResolveListener = b; + public void registerService(Channel c, String serviceName, String serviceType, int port, + DnsSdRegisterListener listener) { + checkChannel(c); + if (TextUtils.isEmpty(serviceName) || TextUtils.isEmpty(serviceType)) { + throw new IllegalArgumentException("Service name or type cannot be empty"); + } + if (port <= 0) { + throw new IllegalArgumentException("Invalid port number"); + } + DnsSdServiceInfo serviceInfo = new DnsSdServiceInfo(serviceName, serviceType, null); + serviceInfo.setPort(port); + c.mDnsSdRegisterListener = listener; + c.mAsyncChannel.sendMessage(REGISTER_SERVICE, serviceInfo); } /** - * Set the listener for stopping service resolution. Can be null. + * Unregister a service registered through {@link #registerService} + * @param c is the channel created at {@link #initialize} + * @param registeredId is obtained at {@link DnsSdRegisterListener#onServiceRegistered} + * @param listener provides callbacks for success or failure. Can be null. */ - public void setStopResolveListener(Channel c, ActionListener b) { - if (c == null) throw new IllegalArgumentException("Channel needs to be initialized"); - c.mDnsSdStopResolveListener = b; - } - - public void registerService(Channel c, DnsSdServiceInfo serviceInfo) { - if (c == null) throw new IllegalArgumentException("Channel needs to be initialized"); - if (serviceInfo == null) throw new IllegalArgumentException("Null serviceInfo"); - c.mAsyncChannel.sendMessage(REGISTER_SERVICE, serviceInfo); + public void unregisterService(Channel c, int registeredId, ActionListener listener) { + checkChannel(c); + c.mDnsSdUnregisterListener = listener; + c.mAsyncChannel.sendMessage(UNREGISTER_SERVICE, registeredId); } + /** @hide */ public void updateService(Channel c, int registeredId, DnsSdTxtRecord txtRecord) { - if (c == null) throw new IllegalArgumentException("Channel needs to be initialized"); + checkChannel(c); c.mAsyncChannel.sendMessage(UPDATE_SERVICE, registeredId, 0, txtRecord); } - public void discoverServices(Channel c, String serviceType) { - if (c == null) throw new IllegalArgumentException("Channel needs to be initialized"); - if (c.mDnsSdDiscoveryListener == null) throw new - IllegalStateException("Discovery listener needs to be set first"); + /** + * Initiate service discovery to browse for instances of a service type. Service discovery + * consumes network bandwidth and will continue until the application calls + * {@link #stopServiceDiscovery}. + * + * <p> The function call immediately returns after sending a request to start service + * discovery to the framework. The application is notified of a success to initiate + * discovery through the callback {@link DnsSdDiscoveryListener#onStarted} or a failure + * through {@link DnsSdDiscoveryListener#onFailure}. + * + * <p> Upon successful start, application is notified when a service is found with + * {@link DnsSdDiscoveryListener#onServiceFound} or when a service is lost with + * {@link DnsSdDiscoveryListener#onServiceLost}. + * + * <p> Upon failure to start, service discovery is not active and application does + * not need to invoke {@link #stopServiceDiscovery} + * + * @param c is the channel created at {@link #initialize} + * @param serviceType The service type being discovered. Examples include "_http._tcp" for + * http services or "_ipp._tcp" for printers + * @param listener provides callbacks when service is found or lost. Cannot be null. + */ + public void discoverServices(Channel c, String serviceType, DnsSdDiscoveryListener listener) { + checkChannel(c); + if (listener == null) { + throw new IllegalStateException("Discovery listener needs to be set first"); + } + if (TextUtils.isEmpty(serviceType)) { + throw new IllegalStateException("Service type cannot be empty"); + } DnsSdServiceInfo s = new DnsSdServiceInfo(); s.setServiceType(serviceType); + c.mDnsSdDiscoveryListener = listener; c.mAsyncChannel.sendMessage(DISCOVER_SERVICES, s); } - public void stopServiceDiscovery(Channel c) { - if (c == null) throw new IllegalArgumentException("Channel needs to be initialized"); + /** + * Stop service discovery initiated with {@link #discoverServices}. An active service + * discovery is notified to the application with {@link DnsSdDiscoveryListener#onStarted} + * and it stays active until the application invokes a stop service discovery. + * + * <p> Upon failure to start service discovery notified through + * {@link DnsSdDiscoveryListener#onFailure} service discovery is not active and + * application does not need to stop it. + * + * @param c is the channel created at {@link #initialize} + * @param listener notifies success or failure. Can be null. + */ + public void stopServiceDiscovery(Channel c, ActionListener listener) { + checkChannel(c); + c.mDnsSdStopDiscoveryListener = listener; c.mAsyncChannel.sendMessage(STOP_DISCOVERY); } - public void resolveService(Channel c, DnsSdServiceInfo serviceInfo) { - if (c == null) throw new IllegalArgumentException("Channel needs to be initialized"); - if (serviceInfo == null) throw new IllegalArgumentException("Null serviceInfo"); - if (c.mDnsSdResolveListener == null) throw new - IllegalStateException("Resolve listener needs to be set first"); + /** + * Resolve a discovered service. An application can resolve a service right before + * establishing a connection to fetch the IP and port details on which to setup + * the connection. + * + * @param c is the channel created at {@link #initialize} + * @param serviceName of the the service + * @param serviceType of the service + * @param listener to receive callback upon success or failure. Cannot be null. + */ + public void resolveService(Channel c, String serviceName, String serviceType, + DnsSdResolveListener listener) { + checkChannel(c); + if (TextUtils.isEmpty(serviceName) || TextUtils.isEmpty(serviceType)) { + throw new IllegalArgumentException("Service name or type cannot be empty"); + } + if (listener == null) throw new + IllegalStateException("Resolve listener cannot be null"); + c.mDnsSdResolveListener = listener; + DnsSdServiceInfo serviceInfo = new DnsSdServiceInfo(serviceName, serviceType, null); c.mAsyncChannel.sendMessage(RESOLVE_SERVICE, serviceInfo); } + /** @hide */ public void stopServiceResolve(Channel c) { - if (c == null) throw new IllegalArgumentException("Channel needs to be initialized"); + checkChannel(c); if (c.mDnsSdResolveListener == null) throw new IllegalStateException("Resolve listener needs to be set first"); c.mAsyncChannel.sendMessage(STOP_RESOLVE); diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java index 20a731e..98fe06a 100644 --- a/core/java/android/os/Debug.java +++ b/core/java/android/os/Debug.java @@ -1324,4 +1324,42 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo return false; } } + + /** + * Return a String describing the calling method and location at a particular stack depth. + * @param callStack the Thread stack + * @param depth the depth of stack to return information for. + * @return the String describing the caller at that depth. + */ + private static String getCaller(StackTraceElement callStack[], int depth) { + // callStack[4] is the caller of the method that called getCallers() + if (4 + depth >= callStack.length) { + return "<bottom of call stack>"; + } + StackTraceElement caller = callStack[4 + depth]; + return caller.getClassName() + "." + caller.getMethodName() + ":" + caller.getLineNumber(); + } + + /** + * Return a string consisting of methods and locations at multiple call stack levels. + * @param depth the number of levels to return, starting with the immediate caller. + * @return a string describing the call stack. + * {@hide} + */ + public static String getCallers(final int depth) { + final StackTraceElement[] callStack = Thread.currentThread().getStackTrace(); + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < depth; i++) { + sb.append(getCaller(callStack, i)).append(" "); + } + return sb.toString(); + } + + /** + * @return a String describing the immediate caller of the calling function. + * {@hide} + */ + public static String getCaller() { + return getCaller(Thread.currentThread().getStackTrace(), 0); + } } diff --git a/core/java/android/os/Trace.java b/core/java/android/os/Trace.java index af94a37..4645fab 100644 --- a/core/java/android/os/Trace.java +++ b/core/java/android/os/Trace.java @@ -33,6 +33,7 @@ public final class Trace { public static final long TRACE_TAG_GRAPHICS = 1L << 1; public static final long TRACE_TAG_INPUT = 1L << 2; public static final long TRACE_TAG_VIEW = 1L << 3; + public static final long TRACE_TAG_WEBVIEW = 1L << 4; private static final long sEnabledTags = nativeGetEnabledTags(); diff --git a/core/java/android/provider/CalendarContract.java b/core/java/android/provider/CalendarContract.java index 83799c4..c4aa691 100644 --- a/core/java/android/provider/CalendarContract.java +++ b/core/java/android/provider/CalendarContract.java @@ -756,6 +756,22 @@ public final class CalendarContract { public static final int ATTENDEE_STATUS_DECLINED = 2; public static final int ATTENDEE_STATUS_INVITED = 3; public static final int ATTENDEE_STATUS_TENTATIVE = 4; + + /** + * The identity of the attendee as referenced in + * {@link ContactsContract.CommonDataKinds.Identity#IDENTITY}. + * This is required only if {@link #ATTENDEE_ID_NAMESPACE} is present. Column name. + * <P>Type: STRING</P> + */ + public static final String ATTENDEE_IDENTITY = "attendeeIdentity"; + + /** + * The identity name space of the attendee as referenced in + * {@link ContactsContract.CommonDataKinds.Identity#NAMESPACE}. + * This is required only if {@link #ATTENDEE_IDENTITY} is present. Column name. + * <P>Type: STRING</P> + */ + public static final String ATTENDEE_ID_NAMESPACE = "attendeeIdNamespace"; } /** @@ -773,6 +789,8 @@ public final class CalendarContract { * <li>{@link #ATTENDEE_RELATIONSHIP}</li> * <li>{@link #ATTENDEE_TYPE}</li> * <li>{@link #ATTENDEE_STATUS}</li> + * <li>{@link #ATTENDEE_IDENTITY}</li> + * <li>{@link #ATTENDEE_ID_NAMESPACE}</li> * </ul> */ public static final class Attendees implements BaseColumns, AttendeesColumns, EventsColumns { @@ -1221,12 +1239,17 @@ public final class CalendarContract { Attendees.ATTENDEE_RELATIONSHIP, Attendees.ATTENDEE_TYPE, Attendees.ATTENDEE_STATUS, + Attendees.ATTENDEE_IDENTITY, + Attendees.ATTENDEE_ID_NAMESPACE }; private static final int COLUMN_ATTENDEE_NAME = 0; private static final int COLUMN_ATTENDEE_EMAIL = 1; private static final int COLUMN_ATTENDEE_RELATIONSHIP = 2; private static final int COLUMN_ATTENDEE_TYPE = 3; private static final int COLUMN_ATTENDEE_STATUS = 4; + private static final int COLUMN_ATTENDEE_IDENTITY = 5; + private static final int COLUMN_ATTENDEE_ID_NAMESPACE = 6; + private static final String[] EXTENDED_PROJECTION = new String[] { ExtendedProperties._ID, ExtendedProperties.NAME, @@ -1362,6 +1385,10 @@ public final class CalendarContract { subCursor.getInt(COLUMN_ATTENDEE_TYPE)); attendeeValues.put(Attendees.ATTENDEE_STATUS, subCursor.getInt(COLUMN_ATTENDEE_STATUS)); + attendeeValues.put(Attendees.ATTENDEE_IDENTITY, + subCursor.getInt(COLUMN_ATTENDEE_IDENTITY)); + attendeeValues.put(Attendees.ATTENDEE_ID_NAMESPACE, + subCursor.getInt(COLUMN_ATTENDEE_ID_NAMESPACE)); entity.addSubValue(Attendees.CONTENT_URI, attendeeValues); } } finally { diff --git a/core/java/android/server/BluetoothService.java b/core/java/android/server/BluetoothService.java index 850349b..36c0189 100755 --- a/core/java/android/server/BluetoothService.java +++ b/core/java/android/server/BluetoothService.java @@ -611,6 +611,11 @@ public class BluetoothService extends IBluetooth.Stub { /*package*/ void initBluetoothAfterTurningOn() { String discoverable = getProperty("Discoverable", false); String timeout = getProperty("DiscoverableTimeout", false); + if (timeout == null) { + Log.w(TAG, "Null DiscoverableTimeout property"); + // assign a number, anything not 0 + timeout = "1"; + } if (discoverable.equals("true") && Integer.valueOf(timeout) != 0) { setAdapterPropertyBooleanNative("Discoverable", 0); } diff --git a/core/java/android/service/textservice/SpellCheckerService.java b/core/java/android/service/textservice/SpellCheckerService.java index 53ce32d..c579e6e 100644 --- a/core/java/android/service/textservice/SpellCheckerService.java +++ b/core/java/android/service/textservice/SpellCheckerService.java @@ -26,12 +26,18 @@ import android.os.Bundle; import android.os.IBinder; import android.os.Process; import android.os.RemoteException; +import android.text.TextUtils; +import android.text.method.WordIterator; import android.util.Log; import android.view.textservice.SentenceSuggestionsInfo; import android.view.textservice.SuggestionsInfo; import android.view.textservice.TextInfo; +import android.widget.SpellChecker; import java.lang.ref.WeakReference; +import java.text.BreakIterator; +import java.util.ArrayList; +import java.util.Locale; /** * SpellCheckerService provides an abstract base class for a spell checker. @@ -92,6 +98,7 @@ public abstract class SpellCheckerService extends Service { */ public static abstract class Session { private InternalISpellCheckerSession mInternalSession; + private volatile SentenceLevelAdapter mSentenceLevelAdapter; /** * @hide @@ -142,8 +149,8 @@ public abstract class SpellCheckerService extends Service { /** * Get sentence suggestions for specified texts in an array of TextInfo. - * The default implementation returns an array of SentenceSuggestionsInfo by simply - * calling onGetSuggestions. + * The default implementation splits the input text to words and returns + * {@link SentenceSuggestionsInfo} which contains suggestions for each word. * This function will run on the incoming IPC thread. * So, this is not called on the main thread, * but will be called in series on another thread. @@ -156,14 +163,41 @@ public abstract class SpellCheckerService extends Service { */ public SentenceSuggestionsInfo[] onGetSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit) { - final int length = textInfos.length; - final SentenceSuggestionsInfo[] retval = new SentenceSuggestionsInfo[length]; - for (int i = 0; i < length; ++i) { - final SuggestionsInfo si = onGetSuggestions(textInfos[i], suggestionsLimit); - si.setCookieAndSequence(textInfos[i].getCookie(), textInfos[i].getSequence()); - final int N = textInfos[i].getText().length(); - retval[i] = new SentenceSuggestionsInfo( - new SuggestionsInfo[] {si}, new int[]{0}, new int[]{N}); + if (textInfos == null || textInfos.length == 0) { + return SentenceLevelAdapter.EMPTY_SENTENCE_SUGGESTIONS_INFOS; + } + if (DBG) { + Log.d(TAG, "onGetSentenceSuggestionsMultiple: + " + textInfos.length + ", " + + suggestionsLimit); + } + if (mSentenceLevelAdapter == null) { + synchronized(this) { + if (mSentenceLevelAdapter == null) { + final String localeStr = getLocale(); + if (!TextUtils.isEmpty(localeStr)) { + mSentenceLevelAdapter = new SentenceLevelAdapter(new Locale(localeStr)); + } + } + } + } + if (mSentenceLevelAdapter == null) { + return SentenceLevelAdapter.EMPTY_SENTENCE_SUGGESTIONS_INFOS; + } + final int infosSize = textInfos.length; + final SentenceSuggestionsInfo[] retval = new SentenceSuggestionsInfo[infosSize]; + for (int i = 0; i < infosSize; ++i) { + final SentenceLevelAdapter.SentenceTextInfoParams textInfoParams = + mSentenceLevelAdapter.getSplitWords(textInfos[i]); + final ArrayList<SentenceLevelAdapter.SentenceWordItem> mItems = + textInfoParams.mItems; + final int itemsSize = mItems.size(); + final TextInfo[] splitTextInfos = new TextInfo[itemsSize]; + for (int j = 0; j < itemsSize; ++j) { + splitTextInfos[j] = mItems.get(j).mTextInfo; + } + retval[i] = SentenceLevelAdapter.reconstructSuggestions( + textInfoParams, onGetSuggestionsMultiple( + splitTextInfos, suggestionsLimit, true)); } return retval; } @@ -290,4 +324,135 @@ public abstract class SpellCheckerService extends Service { return internalSession; } } + + /** + * Adapter class to accommodate word level spell checking APIs to sentence level spell checking + * APIs used in + * {@link SpellCheckerService.Session#onGetSuggestionsMultiple(TextInfo[], int, boolean)} + */ + private static class SentenceLevelAdapter { + public static final SentenceSuggestionsInfo[] EMPTY_SENTENCE_SUGGESTIONS_INFOS = + new SentenceSuggestionsInfo[] {}; + private static final SuggestionsInfo EMPTY_SUGGESTIONS_INFO = new SuggestionsInfo(0, null); + /** + * Container for split TextInfo parameters + */ + public static class SentenceWordItem { + public final TextInfo mTextInfo; + public final int mStart; + public final int mLength; + public SentenceWordItem(TextInfo ti, int start, int end) { + mTextInfo = ti; + mStart = start; + mLength = end - start; + } + } + + /** + * Container for originally queried TextInfo and parameters + */ + public static class SentenceTextInfoParams { + final TextInfo mOriginalTextInfo; + final ArrayList<SentenceWordItem> mItems; + final int mSize; + public SentenceTextInfoParams(TextInfo ti, ArrayList<SentenceWordItem> items) { + mOriginalTextInfo = ti; + mItems = items; + mSize = items.size(); + } + } + + private final WordIterator mWordIterator; + public SentenceLevelAdapter(Locale locale) { + mWordIterator = new WordIterator(locale); + } + + private SentenceTextInfoParams getSplitWords(TextInfo originalTextInfo) { + final WordIterator wordIterator = mWordIterator; + final CharSequence originalText = originalTextInfo.getText(); + final int cookie = originalTextInfo.getCookie(); + final int start = 0; + final int end = originalText.length(); + final ArrayList<SentenceWordItem> wordItems = new ArrayList<SentenceWordItem>(); + wordIterator.setCharSequence(originalText, 0, originalText.length()); + int wordEnd = wordIterator.following(start); + int wordStart = wordIterator.getBeginning(wordEnd); + if (DBG) { + Log.d(TAG, "iterator: break: ---- 1st word start = " + wordStart + ", end = " + + wordEnd + "\n" + originalText); + } + while (wordStart <= end && wordEnd != BreakIterator.DONE + && wordStart != BreakIterator.DONE) { + if (wordEnd >= start && wordEnd > wordStart) { + final String query = originalText.subSequence(wordStart, wordEnd).toString(); + final TextInfo ti = new TextInfo(query, cookie, query.hashCode()); + wordItems.add(new SentenceWordItem(ti, wordStart, wordEnd)); + if (DBG) { + Log.d(TAG, "Adapter: word (" + (wordItems.size() - 1) + ") " + query); + } + } + wordEnd = wordIterator.following(wordEnd); + if (wordEnd == BreakIterator.DONE) { + break; + } + wordStart = wordIterator.getBeginning(wordEnd); + } + if (originalText.length() >= SpellChecker.WORD_ITERATOR_INTERVAL + && wordItems.size() >= 2) { + if (DBG) { + Log.w(TAG, "Remove possibly divided word: " + + wordItems.get(0).mTextInfo.getText()); + } + wordItems.remove(0); + } + return new SentenceTextInfoParams(originalTextInfo, wordItems); + } + + public static SentenceSuggestionsInfo reconstructSuggestions( + SentenceTextInfoParams originalTextInfoParams, SuggestionsInfo[] results) { + if (results == null || results.length == 0) { + return null; + } + if (DBG) { + Log.w(TAG, "Adapter: onGetSuggestions: got " + results.length); + } + if (originalTextInfoParams == null) { + if (DBG) { + Log.w(TAG, "Adapter: originalTextInfoParams is null."); + } + return null; + } + final int originalCookie = originalTextInfoParams.mOriginalTextInfo.getCookie(); + final int originalSequence = + originalTextInfoParams.mOriginalTextInfo.getSequence(); + + final int querySize = originalTextInfoParams.mSize; + final int[] offsets = new int[querySize]; + final int[] lengths = new int[querySize]; + final SuggestionsInfo[] reconstructedSuggestions = new SuggestionsInfo[querySize]; + for (int i = 0; i < querySize; ++i) { + final SentenceWordItem item = originalTextInfoParams.mItems.get(i); + SuggestionsInfo result = null; + for (int j = 0; j < results.length; ++j) { + final SuggestionsInfo cur = results[j]; + if (cur != null && cur.getSequence() == item.mTextInfo.getSequence()) { + result = cur; + result.setCookieAndSequence(originalCookie, originalSequence); + break; + } + } + offsets[i] = item.mStart; + lengths[i] = item.mLength; + reconstructedSuggestions[i] = result != null ? result : EMPTY_SUGGESTIONS_INFO; + if (DBG) { + final int size = reconstructedSuggestions[i].getSuggestionsCount(); + Log.w(TAG, "reconstructedSuggestions(" + i + ")" + size + ", first = " + + (size > 0 ? reconstructedSuggestions[i].getSuggestionAt(0) + : "<none>") + ", offset = " + offsets[i] + ", length = " + + lengths[i]); + } + } + return new SentenceSuggestionsInfo(reconstructedSuggestions, offsets, lengths); + } + } } diff --git a/core/java/android/view/AccessibilityInteractionController.java b/core/java/android/view/AccessibilityInteractionController.java index ab21b32..6c1a6bf 100644 --- a/core/java/android/view/AccessibilityInteractionController.java +++ b/core/java/android/view/AccessibilityInteractionController.java @@ -602,6 +602,7 @@ final class AccessibilityInteractionController { // tree traversal. return (view.mAttachInfo != null && view.mAttachInfo.mWindowVisibility == View.VISIBLE + && view.getAlpha() > 0 && view.isShown() && view.getGlobalVisibleRect(mViewRootImpl.mTempRect)); } diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index 8fe8e40..b70d7b5 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -80,6 +80,8 @@ interface IWindowManager void prepareAppTransition(int transit, boolean alwaysKeepCurrent); int getPendingAppTransition(); void overridePendingAppTransition(String packageName, int enterAnim, int exitAnim); + void overridePendingAppTransitionScaleUp(int startX, int startY, int startWidth, + int startHeight); void overridePendingAppTransitionThumb(in Bitmap srcThumb, int startX, int startY, IRemoteCallback startedCallback); void executeAppTransition(); diff --git a/core/java/android/view/TextureView.java b/core/java/android/view/TextureView.java index 32029ba..214dc5c 100644 --- a/core/java/android/view/TextureView.java +++ b/core/java/android/view/TextureView.java @@ -676,7 +676,6 @@ public class TextureView extends View { * * @param surfaceTexture The {@link SurfaceTexture} that the view should use. * @see SurfaceTexture#detachFromGLContext() - * @hide */ public void setSurfaceTexture(SurfaceTexture surfaceTexture) { if (surfaceTexture == null) { diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 962e13a..0ded5f9 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -24,6 +24,7 @@ import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Camera; import android.graphics.Canvas; +import android.graphics.Insets; import android.graphics.Interpolator; import android.graphics.LinearGradient; import android.graphics.Matrix; @@ -2698,6 +2699,12 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal protected int mPaddingBottom; /** + * The layout insets in pixels, that is the distance in pixels between the + * visible edges of this view its bounds. + */ + private Insets mLayoutInsets; + + /** * Briefly describes the view and is primarily used for accessibility support. */ private CharSequence mContentDescription; @@ -5807,18 +5814,11 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * * @see #FOCUSABLES_ALL * @see #FOCUSABLES_TOUCH_MODE - * @see #FOCUSABLES_ACCESSIBILITY */ public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { if (views == null) { return; } - if ((focusableMode & FOCUSABLE_IN_TOUCH_MODE) == FOCUSABLE_IN_TOUCH_MODE) { - if (isFocusable() && (!isInTouchMode() || isFocusableInTouchMode())) { - views.add(this); - return; - } - } if ((focusableMode & FOCUSABLES_ACCESSIBILITY) == FOCUSABLES_ACCESSIBILITY) { if (AccessibilityManager.getInstance(mContext).isEnabled() && includeForAccessibility()) { @@ -5826,14 +5826,14 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal return; } } - if ((focusableMode & FOCUSABLES_ALL) == FOCUSABLES_ALL) { - if (isFocusable()) { - views.add(this); - return; - } - } else { - throw new IllegalArgumentException("Unknow focusable mode: " + focusableMode); + if (!isFocusable()) { + return; + } + if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE + && isInTouchMode() && !isFocusableInTouchMode()) { + return; } + views.add(this); } /** @@ -13842,6 +13842,29 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal } /** + * @hide + */ + public Insets getLayoutInsets() { + if (mLayoutInsets == null) { + if (mBackground == null) { + mLayoutInsets = Insets.NONE; + } else { + Rect insetRect = new Rect(); + boolean hasInsets = mBackground.getLayoutInsets(insetRect); + mLayoutInsets = hasInsets ? Insets.of(insetRect) : Insets.NONE; + } + } + return mLayoutInsets; + } + + /** + * @hide + */ + public void setLayoutInsets(Insets layoutInsets) { + mLayoutInsets = layoutInsets; + } + + /** * Changes the selection state of this view. A view can be selected or not. * Note that selection is not the same as focus. Views are typically * selected in the context of an AdapterView like ListView or GridView; diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 7e90e2b..6371963 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -170,6 +170,12 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager */ protected int mGroupFlags; + /* + * THe layout mode: either {@link #UNDEFINED_LAYOUT_MODE}, {@link #COMPONENT_BOUNDS} or + * {@link #LAYOUT_BOUNDS} + */ + private int mLayoutMode = UNDEFINED_LAYOUT_MODE; + /** * NOTE: If you change the flags below make sure to reflect the changes * the DisplayList class @@ -335,6 +341,22 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager */ public static final int PERSISTENT_ALL_CACHES = 0x3; + // Layout Modes + + private static final int UNDEFINED_LAYOUT_MODE = -1; + + /** + * This constant is a {@link #setLayoutMode(int) layoutMode}. + * Component bounds are the raw values of {@link #getLeft() left}, {@link #getTop() top}, + * {@link #getRight() right} and {@link #getBottom() bottom}. + */ + public static final int COMPONENT_BOUNDS = 0; + + /** + * This constant is a {@link #setLayoutMode(int) layoutMode}. + */ + public static final int LAYOUT_BOUNDS = 1; + /** * We clip to padding when FLAG_CLIP_TO_PADDING and FLAG_PADDING_NOT_NULL * are set at the same time. @@ -4185,6 +4207,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager @Override public final void layout(int l, int t, int r, int b) { if (mTransition == null || !mTransition.isChangingLayout()) { + if (mTransition != null) { + mTransition.layoutChange(this); + } super.layout(l, t, r, b); } else { // record the fact that we noop'd it; request layout when transition finishes @@ -4424,6 +4449,50 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** + * Returns the basis of alignment during the layout of this view group: + * either {@link #COMPONENT_BOUNDS} or {@link #LAYOUT_BOUNDS}. + * + * @return the layout mode to use during layout operations + * + * @see #setLayoutMode(int) + */ + public int getLayoutMode() { + if (mLayoutMode == UNDEFINED_LAYOUT_MODE) { + ViewParent parent = getParent(); + if (parent instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) parent; + return viewGroup.getLayoutMode(); + } else { + int targetSdkVersion = mContext.getApplicationInfo().targetSdkVersion; + boolean preJellyBean = targetSdkVersion < Build.VERSION_CODES.JELLY_BEAN; + return preJellyBean ? COMPONENT_BOUNDS : LAYOUT_BOUNDS; + } + + } + return mLayoutMode; + } + + /** + * Sets the basis of alignment during alignment of this view group. + * Valid values are either {@link #COMPONENT_BOUNDS} or {@link #LAYOUT_BOUNDS}. + * <p> + * The default is to query the property of the parent if this view group has a parent. + * If this ViewGroup is the root of the view hierarchy the default + * value is {@link #LAYOUT_BOUNDS} for target SDK's greater than JellyBean, + * {@link #LAYOUT_BOUNDS} otherwise. + * + * @param layoutMode the layout mode to use during layout operations + * + * @see #getLayoutMode() + */ + public void setLayoutMode(int layoutMode) { + if (mLayoutMode != layoutMode) { + mLayoutMode = layoutMode; + requestLayout(); + } + } + + /** * Returns a new set of layout parameters based on the supplied attributes set. * * @param attrs the attributes to build the layout parameters from diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index b4554d5..1472993 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -2977,7 +2977,10 @@ public final class ViewRootImpl implements ViewParent, // be when the window is first being added, and mFocused isn't // set yet. final View focused = mView.findFocus(); - if (focused != null && !focused.isFocusableInTouchMode()) { + if (focused != null) { + if (focused.isFocusableInTouchMode()) { + return true; + } final ViewGroup ancestorToTakeFocus = findAncestorToTakeFocusInTouchMode(focused); if (ancestorToTakeFocus != null) { diff --git a/core/java/android/view/textservice/SpellCheckerInfo.java b/core/java/android/view/textservice/SpellCheckerInfo.java index d05c1af..2167862 100644 --- a/core/java/android/view/textservice/SpellCheckerInfo.java +++ b/core/java/android/view/textservice/SpellCheckerInfo.java @@ -45,7 +45,6 @@ public final class SpellCheckerInfo implements Parcelable { private final ResolveInfo mService; private final String mId; private final int mLabel; - private final boolean mSupportsSentenceSpellCheck; /** * The spell checker setting activity's name, used by the system settings to @@ -98,9 +97,6 @@ public final class SpellCheckerInfo implements Parcelable { label = sa.getResourceId(com.android.internal.R.styleable.SpellChecker_label, 0); settingsActivityComponent = sa.getString( com.android.internal.R.styleable.SpellChecker_settingsActivity); - mSupportsSentenceSpellCheck = sa.getBoolean( - com.android.internal.R.styleable.SpellChecker_supportsSentenceSpellCheck, - false); sa.recycle(); final int depth = parser.getDepth(); @@ -142,7 +138,6 @@ public final class SpellCheckerInfo implements Parcelable { */ public SpellCheckerInfo(Parcel source) { mLabel = source.readInt(); - mSupportsSentenceSpellCheck = source.readInt() != 0; mId = source.readString(); mSettingsActivityName = source.readString(); mService = ResolveInfo.CREATOR.createFromParcel(source); @@ -158,13 +153,6 @@ public final class SpellCheckerInfo implements Parcelable { } /** - * @hide - */ - public boolean isSentenceSpellCheckSupported() { - return mSupportsSentenceSpellCheck; - } - - /** * Return the component of the service that implements. */ public ComponentName getComponent() { @@ -188,7 +176,6 @@ public final class SpellCheckerInfo implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mLabel); - dest.writeInt(mSupportsSentenceSpellCheck ? 1 : 0); dest.writeString(mId); dest.writeString(mSettingsActivityName); mService.writeToParcel(dest, flags); diff --git a/core/java/android/view/textservice/SpellCheckerSession.java b/core/java/android/view/textservice/SpellCheckerSession.java index 9dc05e4..628da3c 100644 --- a/core/java/android/view/textservice/SpellCheckerSession.java +++ b/core/java/android/view/textservice/SpellCheckerSession.java @@ -91,8 +91,6 @@ public class SpellCheckerSession { * This meta-data must reference an XML resource. **/ public static final String SERVICE_META_DATA = "android.view.textservice.scs"; - private static final String SUPPORT_SENTENCE_SPELL_CHECK = "SupportSentenceSpellCheck"; - private static final int MSG_ON_GET_SUGGESTION_MULTIPLE = 1; private static final int MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE = 2; @@ -191,7 +189,9 @@ public class SpellCheckerSession { * Get candidate strings for a substring of the specified text. * @param textInfo text metadata for a spell checker * @param suggestionsLimit the maximum number of suggestions that will be returned + * @deprecated use {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)} instead */ + @Deprecated public void getSuggestions(TextInfo textInfo, int suggestionsLimit) { getSuggestions(new TextInfo[] {textInfo}, suggestionsLimit, false); } @@ -201,13 +201,14 @@ public class SpellCheckerSession { * @param textInfos an array of text metadata for a spell checker * @param suggestionsLimit the maximum number of suggestions that will be returned * @param sequentialWords true if textInfos can be treated as sequential words. + * @deprecated use {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)} instead */ + @Deprecated public void getSuggestions( TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) { if (DBG) { Log.w(TAG, "getSuggestions from " + mSpellCheckerInfo.getId()); } - // TODO: Handle multiple words suggestions by using WordBreakIterator mSpellCheckerSessionListenerImpl.getSuggestionsMultiple( textInfos, suggestionsLimit, sequentialWords); } @@ -281,7 +282,7 @@ public class SpellCheckerSession { break; case TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE: if (DBG) { - Log.w(TAG, "Get suggestions from the spell checker."); + Log.w(TAG, "Get sentence suggestions from the spell checker."); } try { session.onGetSentenceSuggestionsMultiple( @@ -492,11 +493,4 @@ public class SpellCheckerSession { public ISpellCheckerSessionListener getSpellCheckerSessionListener() { return mSpellCheckerSessionListenerImpl; } - - /** - * @return true if the spell checker supports sentence level spell checking APIs - */ - public boolean isSentenceSpellCheckSupported() { - return mSubtype.containsExtraValueKey(SUPPORT_SENTENCE_SPELL_CHECK); - } } diff --git a/core/java/android/webkit/CallbackProxy.java b/core/java/android/webkit/CallbackProxy.java index 64fbdd5..ab798e8 100644 --- a/core/java/android/webkit/CallbackProxy.java +++ b/core/java/android/webkit/CallbackProxy.java @@ -1711,7 +1711,7 @@ class CallbackProxy extends Handler { void onIsSupportedCallback(boolean isSupported) { Message msg = obtainMessage(SEARCHBOX_IS_SUPPORTED_CALLBACK); - msg.obj = new Boolean(isSupported); + msg.obj = Boolean.valueOf(isSupported); sendMessage(msg); } diff --git a/core/java/android/webkit/GeolocationPermissions.java b/core/java/android/webkit/GeolocationPermissions.java index 1441541..a916884 100755 --- a/core/java/android/webkit/GeolocationPermissions.java +++ b/core/java/android/webkit/GeolocationPermissions.java @@ -252,7 +252,7 @@ public class GeolocationPermissions { } if (WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName())) { boolean allowed = nativeGetAllowed(origin); - callback.onReceiveValue(new Boolean(allowed)); + callback.onReceiveValue(Boolean.valueOf(allowed)); } else { Map values = new HashMap<String, Object>(); values.put(ORIGIN, origin); diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java index 105285c..ba48da1 100644 --- a/core/java/android/webkit/WebSettings.java +++ b/core/java/android/webkit/WebSettings.java @@ -190,15 +190,19 @@ public class WebSettings { } /** - * Sets whether the WebView should use its built-in zoom mechanisms, as - * opposed to separate zoom controls. The built-in zoom mechanisms comprise - * on-screen zoom controls, which are displayed over the WebView's content, - * and the use of a pinch gesture to control zooming. Whether or not these - * on-screen controls are displayed can be set with {@link #setDisplayZoomControls}. - * The separate zoom controls are no longer supported, so it is recommended - * that this setting is always enabled. - * @param enabled Whether the WebView should use the built-in zoom mechanism. - */ + * Sets whether the WebView should use its built-in zoom mechanisms. The + * built-in zoom mechanisms comprise on-screen zoom controls, which are + * displayed over the WebView's content, and the use of a pinch gesture to + * control zooming. Whether or not these on-screen controls are displayed + * can be set with {@link #setDisplayZoomControls}. + * <p> + * The built-in mechanisms are the only currently supported zoom + * mechanisms, so it is recommended that this setting is always enabled. + * @param enabled Whether the WebView should use its built-in zoom mechanisms. + */ + // This method was intended to select between the built-in zoom mechanisms + // and the separate zoom controls. The latter were obtained using + // {@link WebView#getZoomControls}, which is now hidden. public void setBuiltInZoomControls(boolean enabled) { throw new MustOverrideException(); } diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index 5498622..bd10cca 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -1530,17 +1530,15 @@ public class WebView extends AbsoluteLayout } /** - * Returns a view containing zoom controls i.e. +/- buttons. The caller is - * in charge of installing this view to the view hierarchy. This view will - * become visible when the user starts scrolling via touch and fade away if - * the user does not interact with it. + * Gets the zoom controls for the WebView, as a separate View. The caller is + * responsible for inserting this View into the layout hierarchy. * <p/> - * API version 3 introduces a built-in zoom mechanism that is shown - * automatically by the MapView. This is the preferred approach for - * showing the zoom UI. + * API Level 3 introduced built-in zoom mechanisms for the WebView, as + * opposed to these separate zoom controls. The built-in mechanisms are + * preferred and can be enabled using + * {@link WebSettings#setBuiltInZoomControls}. * - * @deprecated The built-in zoom mechanism is preferred, see - * {@link WebSettings#setBuiltInZoomControls(boolean)}. + * @deprecated The built-in zoom mechanisms are preferred. * @hide since API version 16. */ @Deprecated diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java index e2880d6..aea23c0 100644 --- a/core/java/android/webkit/WebViewCore.java +++ b/core/java/android/webkit/WebViewCore.java @@ -1772,6 +1772,9 @@ public final class WebViewCore { @Override public boolean dispatchWebKitEvent(MotionEvent event, int eventType, int flags) { + if (mNativeClass == 0) { + return false; + } switch (eventType) { case WebViewInputDispatcher.EVENT_TYPE_CLICK: return nativeMouseClick(mNativeClass); diff --git a/core/java/android/widget/AdapterView.java b/core/java/android/widget/AdapterView.java index 1a2231e..abfc577 100644 --- a/core/java/android/widget/AdapterView.java +++ b/core/java/android/widget/AdapterView.java @@ -650,7 +650,8 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { mEmptyView = emptyView; // If not explicitly specified this view is important for accessibility. - if (emptyView.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + if (emptyView != null + && emptyView.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { emptyView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); } diff --git a/core/java/android/widget/GridLayout.java b/core/java/android/widget/GridLayout.java index 60dd55c..1cb676f 100644 --- a/core/java/android/widget/GridLayout.java +++ b/core/java/android/widget/GridLayout.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.Insets; import android.graphics.Paint; import android.util.AttributeSet; import android.util.Log; @@ -559,9 +560,9 @@ public class GridLayout extends ViewGroup { int flags = (gravity & mask) >> shift; switch (flags) { case (AXIS_SPECIFIED | AXIS_PULL_BEFORE): - return LEADING; + return horizontal ? LEFT : TOP; case (AXIS_SPECIFIED | AXIS_PULL_AFTER): - return TRAILING; + return horizontal ? RIGHT : BOTTOM; case (AXIS_SPECIFIED | AXIS_PULL_BEFORE | AXIS_PULL_AFTER): return FILL; case AXIS_SPECIFIED: @@ -1042,12 +1043,15 @@ public class GridLayout extends ViewGroup { int rightMargin = getMargin(c, true, false); int bottomMargin = getMargin(c, false, false); + int sumMarginsX = leftMargin + rightMargin; + int sumMarginsY = topMargin + bottomMargin; + // Alignment offsets: the location of the view relative to its alignment group. - int alignmentOffsetX = boundsX.getOffset(c, hAlign, leftMargin + pWidth + rightMargin); - int alignmentOffsetY = boundsY.getOffset(c, vAlign, topMargin + pHeight + bottomMargin); + int alignmentOffsetX = boundsX.getOffset(this, c, hAlign, pWidth + sumMarginsX, true); + int alignmentOffsetY = boundsY.getOffset(this, c, vAlign, pHeight + sumMarginsY, false); - int width = hAlign.getSizeInCell(c, pWidth, cellWidth - leftMargin - rightMargin); - int height = vAlign.getSizeInCell(c, pHeight, cellHeight - topMargin - bottomMargin); + int width = hAlign.getSizeInCell(c, pWidth, cellWidth - sumMarginsX); + int height = vAlign.getSizeInCell(c, pHeight, cellHeight - sumMarginsY); int dx = x1 + gravityOffsetX + alignmentOffsetX; @@ -1181,7 +1185,7 @@ public class GridLayout extends ViewGroup { View c = getChildAt(i); LayoutParams lp = getLayoutParams(c); Spec spec = horizontal ? lp.columnSpec : lp.rowSpec; - groupBounds.getValue(i).include(c, spec, GridLayout.this, this); + groupBounds.getValue(i).include(GridLayout.this, c, spec, this); } } @@ -2138,16 +2142,30 @@ public class GridLayout extends ViewGroup { return before + after; } - protected int getOffset(View c, Alignment alignment, int size) { - return before - alignment.getAlignmentValue(c, size); + private int getAlignmentValue(GridLayout gl, View c, int size, Alignment a, boolean horiz) { + boolean useLayoutBounds = gl.getLayoutMode() == LAYOUT_BOUNDS; + if (!useLayoutBounds) { + return a.getAlignmentValue(c, size); + } else { + Insets insets = c.getLayoutInsets(); + int leadingInset = horiz ? insets.left : insets.top; // RTL? + int trailingInset = horiz ? insets.right : insets.bottom; // RTL? + int totalInset = leadingInset + trailingInset; + return leadingInset + a.getAlignmentValue(c, size - totalInset); + } + } + + protected int getOffset(GridLayout gl, View c, Alignment a, int size, boolean horizontal) { + return before - getAlignmentValue(gl, c, size, a, horizontal); } - protected final void include(View c, Spec spec, GridLayout gridLayout, Axis axis) { + protected final void include(GridLayout gl, View c, Spec spec, Axis axis) { this.flexibility &= spec.getFlexibility(); - int size = gridLayout.getMeasurementIncludingMargin(c, axis.horizontal); - Alignment alignment = gridLayout.getAlignment(spec.alignment, axis.horizontal); + boolean horizontal = axis.horizontal; + int size = gl.getMeasurementIncludingMargin(c, horizontal); + Alignment alignment = gl.getAlignment(spec.alignment, horizontal); // todo test this works correctly when the returned value is UNDEFINED - int before = alignment.getAlignmentValue(c, size); + int before = getAlignmentValue(gl, c, size, alignment, horizontal); include(before, size - before); } @@ -2614,8 +2632,8 @@ public class GridLayout extends ViewGroup { } @Override - protected int getOffset(View c, Alignment alignment, int size) { - return max(0, super.getOffset(c, alignment, size)); + protected int getOffset(GridLayout gl, View c, Alignment a, int size, boolean hrz) { + return max(0, super.getOffset(gl, c, a, size, hrz)); } }; } diff --git a/core/java/android/widget/SpellChecker.java b/core/java/android/widget/SpellChecker.java index c725b64..a13ee5a 100644 --- a/core/java/android/widget/SpellChecker.java +++ b/core/java/android/widget/SpellChecker.java @@ -24,6 +24,7 @@ import android.text.Spanned; import android.text.method.WordIterator; import android.text.style.SpellCheckSpan; import android.text.style.SuggestionSpan; +import android.util.Log; import android.view.textservice.SentenceSuggestionsInfo; import android.view.textservice.SpellCheckerSession; import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener; @@ -43,6 +44,8 @@ import java.util.Locale; * @hide */ public class SpellChecker implements SpellCheckerSessionListener { + private static final String TAG = SpellChecker.class.getSimpleName(); + private static final boolean DBG = false; // No more than this number of words will be parsed on each iteration to ensure a minimum // lock of the UI thread @@ -116,7 +119,7 @@ public class SpellChecker implements SpellCheckerSessionListener { null /* Bundle not currently used by the textServicesManager */, mCurrentLocale, this, false /* means any available languages from current spell checker */); - mIsSentenceSpellCheckSupported = mSpellCheckerSession.isSentenceSpellCheckSupported(); + mIsSentenceSpellCheckSupported = true; } // Restore SpellCheckSpans in pool @@ -266,6 +269,12 @@ public class SpellChecker implements SpellCheckerSessionListener { editable.subSequence(start, end).toString(); spellCheckSpan.setSpellCheckInProgress(true); textInfos[textInfosCount++] = new TextInfo(word, mCookie, mIds[i]); + if (DBG) { + Log.d(TAG, "create TextInfo: (" + i + "/" + mLength + ")" + word + + ", cookie = " + mCookie + ", seq = " + + mIds[i] + ", sel start = " + selectionStart + ", sel end = " + + selectionEnd + ", start = " + start + ", end = " + end); + } } } @@ -507,7 +516,20 @@ public class SpellChecker implements SpellCheckerSessionListener { if (regionEnd <= spellCheckStart) { return; } - addSpellCheckSpan(editable, spellCheckStart, regionEnd); + final int selectionStart = Selection.getSelectionStart(editable); + final int selectionEnd = Selection.getSelectionEnd(editable); + if (DBG) { + Log.d(TAG, "addSpellCheckSpan: " + + editable.subSequence(spellCheckStart, regionEnd) + + ", regionEnd = " + regionEnd + ", spellCheckStart = " + + spellCheckStart + ", sel start = " + selectionStart + ", sel end =" + + selectionEnd); + } + // Do not check this word if the user is currently editing it + if (spellCheckStart >= 0 && regionEnd > spellCheckStart + && (selectionEnd < spellCheckStart || selectionStart > regionEnd)) { + addSpellCheckSpan(editable, spellCheckStart, regionEnd); + } } else { while (wordStart <= end) { if (wordEnd >= start && wordEnd > wordStart) { diff --git a/core/java/com/android/internal/app/ActionBarImpl.java b/core/java/com/android/internal/app/ActionBarImpl.java index f1dffa1..1ba6d43 100644 --- a/core/java/com/android/internal/app/ActionBarImpl.java +++ b/core/java/com/android/internal/app/ActionBarImpl.java @@ -51,6 +51,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.accessibility.AccessibilityEvent; +import android.view.animation.AnimationUtils; import android.widget.SpinnerAdapter; import java.lang.ref.WeakReference; @@ -596,19 +597,23 @@ public class ActionBarImpl extends ActionBar { if (mCurWindowVisibility == View.VISIBLE && (mShowHideAnimationEnabled || alwaysAnimate)) { mTopVisibilityView.setAlpha(0); + mTopVisibilityView.setTranslationY(-mTopVisibilityView.getHeight()); AnimatorSet anim = new AnimatorSet(); AnimatorSet.Builder b = anim.play(ObjectAnimator.ofFloat(mTopVisibilityView, "alpha", 1)); + b.with(ObjectAnimator.ofFloat(mTopVisibilityView, "translationY", 0)); if (mContentView != null) { b.with(ObjectAnimator.ofFloat(mContentView, "translationY", -mTopVisibilityView.getHeight(), 0)); - mTopVisibilityView.setTranslationY(-mTopVisibilityView.getHeight()); - b.with(ObjectAnimator.ofFloat(mTopVisibilityView, "translationY", 0)); } if (mSplitView != null && mContextDisplayMode == CONTEXT_DISPLAY_SPLIT) { mSplitView.setAlpha(0); + mSplitView.setTranslationY(mSplitView.getHeight()); mSplitView.setVisibility(View.VISIBLE); b.with(ObjectAnimator.ofFloat(mSplitView, "alpha", 1)); + b.with(ObjectAnimator.ofFloat(mSplitView, "translationY", 0)); } + anim.setInterpolator(AnimationUtils.loadInterpolator(mContext, + com.android.internal.R.interpolator.decelerate_quad)); anim.addListener(mShowListener); mCurrentShowAnim = anim; anim.start(); @@ -638,16 +643,20 @@ public class ActionBarImpl extends ActionBar { mContainerView.setTransitioning(true); AnimatorSet anim = new AnimatorSet(); AnimatorSet.Builder b = anim.play(ObjectAnimator.ofFloat(mTopVisibilityView, "alpha", 0)); + b.with(ObjectAnimator.ofFloat(mTopVisibilityView, "translationY", + -mTopVisibilityView.getHeight())); if (mContentView != null) { b.with(ObjectAnimator.ofFloat(mContentView, "translationY", 0, -mTopVisibilityView.getHeight())); - b.with(ObjectAnimator.ofFloat(mTopVisibilityView, "translationY", - -mTopVisibilityView.getHeight())); } if (mSplitView != null && mSplitView.getVisibility() == View.VISIBLE) { mSplitView.setAlpha(1); b.with(ObjectAnimator.ofFloat(mSplitView, "alpha", 0)); + b.with(ObjectAnimator.ofFloat(mSplitView, "translationY", + mSplitView.getHeight())); } + anim.setInterpolator(AnimationUtils.loadInterpolator(mContext, + com.android.internal.R.interpolator.accelerate_quad)); anim.addListener(mHideListener); mCurrentShowAnim = anim; anim.start(); diff --git a/core/java/com/android/internal/util/XmlUtils.java b/core/java/com/android/internal/util/XmlUtils.java index e00a853..6a09fe0 100644 --- a/core/java/com/android/internal/util/XmlUtils.java +++ b/core/java/com/android/internal/util/XmlUtils.java @@ -888,4 +888,19 @@ public class XmlUtils ; } } + + public static boolean nextElementWithin(XmlPullParser parser, int outerDepth) + throws IOException, XmlPullParserException { + for (;;) { + int type = parser.next(); + if (type == XmlPullParser.END_DOCUMENT + || (type == XmlPullParser.END_TAG && parser.getDepth() == outerDepth)) { + return false; + } + if (type == XmlPullParser.START_TAG + && parser.getDepth() == outerDepth + 1) { + return true; + } + } + } } diff --git a/core/java/com/android/internal/widget/SizeAdaptiveLayout.java b/core/java/com/android/internal/widget/SizeAdaptiveLayout.java new file mode 100644 index 0000000..adfd3dc --- /dev/null +++ b/core/java/com/android/internal/widget/SizeAdaptiveLayout.java @@ -0,0 +1,405 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget; + +import com.android.internal.R; + +import android.animation.Animator; +import android.animation.Animator.AnimatorListener; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.ViewDebug; +import android.view.ViewGroup; +import android.widget.RemoteViews.RemoteView; + +/** + * A layout that switches between its children based on the requested layout height. + * Each child specifies its minimum and maximum valid height. Results are undefined + * if children specify overlapping ranges. A child may specify the maximum height + * as 'unbounded' to indicate that it is willing to be displayed arbitrarily tall. + * + * <p> + * See {@link SizeAdaptiveLayout.LayoutParams} for a full description of the + * layout parameters used by SizeAdaptiveLayout. + */ +@RemoteView +public class SizeAdaptiveLayout extends ViewGroup { + + private static final String TAG = "SizeAdaptiveLayout"; + private static final boolean DEBUG = false; + private static final long CROSSFADE_TIME = 250; + + // TypedArray indices + private static final int MIN_VALID_HEIGHT = + R.styleable.SizeAdaptiveLayout_Layout_layout_minHeight; + private static final int MAX_VALID_HEIGHT = + R.styleable.SizeAdaptiveLayout_Layout_layout_maxHeight; + + // view state + private View mActiveChild; + private View mLastActive; + + // animation state + private AnimatorSet mTransitionAnimation; + private AnimatorListener mAnimatorListener; + private ObjectAnimator mFadePanel; + private ObjectAnimator mFadeView; + private int mCanceledAnimationCount; + private View mEnteringView; + private View mLeavingView; + // View used to hide larger views under smaller ones to create a uniform crossfade + private View mModestyPanel; + private int mModestyPanelTop; + + public SizeAdaptiveLayout(Context context) { + super(context); + initialize(); + } + + public SizeAdaptiveLayout(Context context, AttributeSet attrs) { + super(context, attrs); + initialize(); + } + + public SizeAdaptiveLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + initialize(); + } + + private void initialize() { + mModestyPanel = new View(getContext()); + // If the SizeAdaptiveLayout has a solid background, use it as a transition hint. + if (getBackground() instanceof ColorDrawable) { + mModestyPanel.setBackgroundDrawable(getBackground()); + } else { + mModestyPanel.setBackgroundColor(Color.BLACK); + } + SizeAdaptiveLayout.LayoutParams layout = + new SizeAdaptiveLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); + mModestyPanel.setLayoutParams(layout); + addView(mModestyPanel); + mFadePanel = ObjectAnimator.ofFloat(mModestyPanel, "alpha", 0f); + mFadeView = ObjectAnimator.ofFloat(null, "alpha", 0f); + mAnimatorListener = new BringToFrontOnEnd(); + mTransitionAnimation = new AnimatorSet(); + mTransitionAnimation.play(mFadeView).with(mFadePanel); + mTransitionAnimation.setDuration(CROSSFADE_TIME); + mTransitionAnimation.addListener(mAnimatorListener); + } + + /** + * Visible for testing + * @hide + */ + public Animator getTransitionAnimation() { + return mTransitionAnimation; + } + + /** + * Visible for testing + * @hide + */ + public View getModestyPanel() { + return mModestyPanel; + } + + @Override + public void onAttachedToWindow() { + mLastActive = null; + // make sure all views start off invisible. + for (int i = 0; i < getChildCount(); i++) { + getChildAt(i).setVisibility(View.GONE); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (DEBUG) Log.d(TAG, this + " measure spec: " + + MeasureSpec.toString(heightMeasureSpec)); + View model = selectActiveChild(heightMeasureSpec); + SizeAdaptiveLayout.LayoutParams lp = + (SizeAdaptiveLayout.LayoutParams) model.getLayoutParams(); + if (DEBUG) Log.d(TAG, "active min: " + lp.minHeight + " max: " + lp.maxHeight); + measureChild(model, widthMeasureSpec, heightMeasureSpec); + int childHeight = model.getMeasuredHeight(); + int childWidth = model.getMeasuredHeight(); + int childState = combineMeasuredStates(0, model.getMeasuredState()); + if (DEBUG) Log.d(TAG, "measured child at: " + childHeight); + int resolvedWidth = resolveSizeAndState(childWidth, widthMeasureSpec, childState); + int resolvedheight = resolveSizeAndState(childHeight, heightMeasureSpec, childState); + setMeasuredDimension(resolvedWidth, resolvedheight); + if (DEBUG) Log.d(TAG, "resolved to: " + resolvedheight); + } + + //TODO extend to width and height + private View selectActiveChild(int heightMeasureSpec) { + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + final int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + View unboundedView = null; + View tallestView = null; + int tallestViewSize = 0; + View smallestView = null; + int smallestViewSize = Integer.MAX_VALUE; + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child != mModestyPanel) { + SizeAdaptiveLayout.LayoutParams lp = + (SizeAdaptiveLayout.LayoutParams) child.getLayoutParams(); + if (DEBUG) Log.d(TAG, "looking at " + i + + " with min: " + lp.minHeight + + " max: " + lp.maxHeight); + if (lp.maxHeight == SizeAdaptiveLayout.LayoutParams.UNBOUNDED && + unboundedView == null) { + unboundedView = child; + } + if (lp.maxHeight > tallestViewSize) { + tallestViewSize = lp.maxHeight; + tallestView = child; + } + if (lp.minHeight < smallestViewSize) { + smallestViewSize = lp.minHeight; + smallestView = child; + } + if (heightMode != MeasureSpec.UNSPECIFIED && + heightSize >= lp.minHeight && heightSize <= lp.maxHeight) { + if (DEBUG) Log.d(TAG, " found exact match, finishing early"); + return child; + } + } + } + if (unboundedView != null) { + tallestView = unboundedView; + } + if (heightMode == MeasureSpec.UNSPECIFIED) { + return tallestView; + } + if (heightSize > tallestViewSize) { + return tallestView; + } + return smallestView; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (DEBUG) Log.d(TAG, this + " onlayout height: " + (bottom - top)); + mLastActive = mActiveChild; + int measureSpec = View.MeasureSpec.makeMeasureSpec(bottom - top, + View.MeasureSpec.EXACTLY); + mActiveChild = selectActiveChild(measureSpec); + mActiveChild.setVisibility(View.VISIBLE); + + if (mLastActive != mActiveChild && mLastActive != null) { + if (DEBUG) Log.d(TAG, this + " changed children from: " + mLastActive + + " to: " + mActiveChild); + + mEnteringView = mActiveChild; + mLeavingView = mLastActive; + + mEnteringView.setAlpha(1f); + + mModestyPanel.setAlpha(1f); + mModestyPanel.bringToFront(); + mModestyPanelTop = mLeavingView.getHeight(); + mModestyPanel.setVisibility(View.VISIBLE); + // TODO: mModestyPanel background should be compatible with mLeavingView + + mLeavingView.bringToFront(); + + if (mTransitionAnimation.isRunning()) { + mTransitionAnimation.cancel(); + } + mFadeView.setTarget(mLeavingView); + mFadeView.setFloatValues(0f); + mFadePanel.setFloatValues(0f); + mTransitionAnimation.setupStartValues(); + mTransitionAnimation.start(); + } + final int childWidth = mActiveChild.getMeasuredWidth(); + final int childHeight = mActiveChild.getMeasuredHeight(); + // TODO investigate setting LAYER_TYPE_HARDWARE on mLastActive + mActiveChild.layout(0, 0, 0 + childWidth, 0 + childHeight); + + if (DEBUG) Log.d(TAG, "got modesty offset of " + mModestyPanelTop); + mModestyPanel.layout(0, mModestyPanelTop, 0 + childWidth, mModestyPanelTop + childHeight); + } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + if (DEBUG) Log.d(TAG, "generate layout from attrs"); + return new SizeAdaptiveLayout.LayoutParams(getContext(), attrs); + } + + @Override + protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + if (DEBUG) Log.d(TAG, "generate default layout from viewgroup"); + return new SizeAdaptiveLayout.LayoutParams(p); + } + + @Override + protected LayoutParams generateDefaultLayoutParams() { + if (DEBUG) Log.d(TAG, "generate default layout from null"); + return new SizeAdaptiveLayout.LayoutParams(); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof SizeAdaptiveLayout.LayoutParams; + } + + /** + * Per-child layout information associated with ViewSizeAdaptiveLayout. + * + * TODO extend to width and height + * + * @attr ref android.R.styleable#SizeAdaptiveLayout_Layout_layout_minHeight + * @attr ref android.R.styleable#SizeAdaptiveLayout_Layout_layout_maxHeight + */ + public static class LayoutParams extends ViewGroup.LayoutParams { + + /** + * Indicates the minimum valid height for the child. + */ + @ViewDebug.ExportedProperty(category = "layout") + public int minHeight; + + /** + * Indicates the maximum valid height for the child. + */ + @ViewDebug.ExportedProperty(category = "layout") + public int maxHeight; + + /** + * Constant value for maxHeight that indicates there is not maximum height. + */ + public static final int UNBOUNDED = -1; + + /** + * {@inheritDoc} + */ + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + if (DEBUG) { + Log.d(TAG, "construct layout from attrs"); + for (int i = 0; i < attrs.getAttributeCount(); i++) { + Log.d(TAG, " " + attrs.getAttributeName(i) + " = " + + attrs.getAttributeValue(i)); + } + } + TypedArray a = + c.obtainStyledAttributes(attrs, + R.styleable.SizeAdaptiveLayout_Layout); + + minHeight = a.getDimensionPixelSize(MIN_VALID_HEIGHT, 0); + if (DEBUG) Log.d(TAG, "got minHeight of: " + minHeight); + + try { + maxHeight = a.getLayoutDimension(MAX_VALID_HEIGHT, UNBOUNDED); + if (DEBUG) Log.d(TAG, "got maxHeight of: " + maxHeight); + } catch (Exception e) { + if (DEBUG) Log.d(TAG, "caught exception looking for maxValidHeight " + e); + } + + a.recycle(); + } + + /** + * Creates a new set of layout parameters with the specified width, height + * and valid height bounds. + * + * @param width the width, either {@link #MATCH_PARENT}, + * {@link #WRAP_CONTENT} or a fixed size in pixels + * @param height the height, either {@link #MATCH_PARENT}, + * {@link #WRAP_CONTENT} or a fixed size in pixels + * @param minHeight the minimum height of this child + * @param maxHeight the maximum height of this child + * or {@link #UNBOUNDED} if the child can grow forever + */ + public LayoutParams(int width, int height, int minHeight, int maxHeight) { + super(width, height); + this.minHeight = minHeight; + this.maxHeight = maxHeight; + } + + /** + * {@inheritDoc} + */ + public LayoutParams(int width, int height) { + this(width, height, UNBOUNDED, UNBOUNDED); + } + + /** + * Constructs a new LayoutParams with default values as defined in {@link LayoutParams}. + */ + public LayoutParams() { + this(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + } + + /** + * {@inheritDoc} + */ + public LayoutParams(ViewGroup.LayoutParams p) { + super(p); + minHeight = UNBOUNDED; + maxHeight = UNBOUNDED; + } + + public String debug(String output) { + return output + "SizeAdaptiveLayout.LayoutParams={" + + ", max=" + maxHeight + + ", max=" + minHeight + "}"; + } + } + + class BringToFrontOnEnd implements AnimatorListener { + @Override + public void onAnimationEnd(Animator animation) { + if (mCanceledAnimationCount == 0) { + mLeavingView.setVisibility(View.GONE); + mModestyPanel.setVisibility(View.GONE); + mEnteringView.bringToFront(); + mEnteringView = null; + mLeavingView = null; + } else { + mCanceledAnimationCount--; + } + } + + @Override + public void onAnimationCancel(Animator animation) { + mCanceledAnimationCount++; + } + + @Override + public void onAnimationRepeat(Animator animation) { + if (DEBUG) Log.d(TAG, "fade animation repeated: should never happen."); + assert(false); + } + + @Override + public void onAnimationStart(Animator animation) { + } + } +} |
