diff options
Diffstat (limited to 'core/java')
200 files changed, 7517 insertions, 3713 deletions
diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java index 13ceb4a..a9eaf29 100644 --- a/core/java/android/accessibilityservice/AccessibilityService.java +++ b/core/java/android/accessibilityservice/AccessibilityService.java @@ -25,12 +25,15 @@ import android.os.Message; import android.os.RemoteException; import android.util.Log; import android.view.KeyEvent; +import android.view.WindowManager; +import android.view.WindowManagerGlobal; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityInteractionClient; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityWindowInfo; import com.android.internal.os.HandlerCaller; +import com.android.internal.os.SomeArgs; import java.util.List; @@ -366,7 +369,7 @@ public abstract class AccessibilityService extends Service { public void onAccessibilityEvent(AccessibilityEvent event); public void onInterrupt(); public void onServiceConnected(); - public void onSetConnectionId(int connectionId); + public void init(int connectionId, IBinder windowToken); public boolean onGesture(int gestureId); public boolean onKeyEvent(KeyEvent event); } @@ -375,6 +378,10 @@ public abstract class AccessibilityService extends Service { private AccessibilityServiceInfo mInfo; + private IBinder mWindowToken; + + private WindowManager mWindowManager; + /** * Callback for {@link android.view.accessibility.AccessibilityEvent}s. * @@ -634,8 +641,12 @@ public abstract class AccessibilityService extends Service { } @Override - public void onSetConnectionId( int connectionId) { + public void init(int connectionId, IBinder windowToken) { mConnectionId = connectionId; + mWindowToken = windowToken; + + // Let the window manager know about our shiny new token. + WindowManagerGlobal.getInstance().setDefaultToken(mWindowToken); } @Override @@ -658,7 +669,7 @@ public abstract class AccessibilityService extends Service { */ public static class IAccessibilityServiceClientWrapper extends IAccessibilityServiceClient.Stub implements HandlerCaller.Callback { - private static final int DO_SET_SET_CONNECTION = 1; + private static final int DO_INIT = 1; private static final int DO_ON_INTERRUPT = 2; private static final int DO_ON_ACCESSIBILITY_EVENT = 3; private static final int DO_ON_GESTURE = 4; @@ -677,9 +688,10 @@ public abstract class AccessibilityService extends Service { mCaller = new HandlerCaller(context, looper, this, true /*asyncHandler*/); } - public void setConnection(IAccessibilityServiceConnection connection, int connectionId) { - Message message = mCaller.obtainMessageIO(DO_SET_SET_CONNECTION, connectionId, - connection); + public void init(IAccessibilityServiceConnection connection, int connectionId, + IBinder windowToken) { + Message message = mCaller.obtainMessageIOO(DO_INIT, connectionId, + connection, windowToken); mCaller.sendMessage(message); } @@ -730,20 +742,24 @@ public abstract class AccessibilityService extends Service { mCallback.onInterrupt(); } return; - case DO_SET_SET_CONNECTION: { + case DO_INIT: { mConnectionId = message.arg1; + SomeArgs args = (SomeArgs) message.obj; IAccessibilityServiceConnection connection = - (IAccessibilityServiceConnection) message.obj; + (IAccessibilityServiceConnection) args.arg1; + IBinder windowToken = (IBinder) args.arg2; + args.recycle(); if (connection != null) { AccessibilityInteractionClient.getInstance().addConnection(mConnectionId, connection); - mCallback.onSetConnectionId(mConnectionId); + mCallback.init(mConnectionId, windowToken); mCallback.onServiceConnected(); } else { AccessibilityInteractionClient.getInstance().removeConnection( mConnectionId); + mConnectionId = AccessibilityInteractionClient.NO_ID; AccessibilityInteractionClient.getInstance().clearCache(); - mCallback.onSetConnectionId(AccessibilityInteractionClient.NO_ID); + mCallback.init(AccessibilityInteractionClient.NO_ID, null); } } return; diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl index 6ce0219..8b503dd 100644 --- a/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl +++ b/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl @@ -28,7 +28,7 @@ import android.view.KeyEvent; */ oneway interface IAccessibilityServiceClient { - void setConnection(in IAccessibilityServiceConnection connection, int connectionId); + void init(in IAccessibilityServiceConnection connection, int connectionId, IBinder windowToken); void onAccessibilityEvent(in AccessibilityEvent event); diff --git a/core/java/android/animation/Animator.java b/core/java/android/animation/Animator.java index 3720c81..da48709 100644 --- a/core/java/android/animation/Animator.java +++ b/core/java/android/animation/Animator.java @@ -16,6 +16,8 @@ package android.animation; +import android.content.res.ConstantState; + import java.util.ArrayList; /** @@ -41,6 +43,18 @@ public abstract class Animator implements Cloneable { boolean mPaused = false; /** + * A set of flags which identify the type of configuration changes that can affect this + * Animator. Used by the Animator cache. + */ + int mChangingConfigurations = 0; + + /** + * If this animator is inflated from a constant state, keep a reference to it so that + * ConstantState will not be garbage collected until this animator is collected + */ + private AnimatorConstantState mConstantState; + + /** * Starts this animation. If the animation has a nonzero startDelay, the animation will start * running after that delay elapses. A non-delayed animation will have its initial * value(s) set immediately, followed by calls to @@ -295,25 +309,71 @@ public abstract class Animator implements Cloneable { } } + /** + * Return a mask of the configuration parameters for which this animator may change, requiring + * that it should be re-created from Resources. The default implementation returns whatever + * value was provided through setChangingConfigurations(int) or 0 by default. + * + * @return Returns a mask of the changing configuration parameters, as defined by + * {@link android.content.pm.ActivityInfo}. + * @see android.content.pm.ActivityInfo + * @hide + */ + public int getChangingConfigurations() { + return mChangingConfigurations; + } + + /** + * Set a mask of the configuration parameters for which this animator may change, requiring + * that it be re-created from resource. + * + * @param configs A mask of the changing configuration parameters, as + * defined by {@link android.content.pm.ActivityInfo}. + * + * @see android.content.pm.ActivityInfo + * @hide + */ + public void setChangingConfigurations(int configs) { + mChangingConfigurations = configs; + } + + /** + * Sets the changing configurations value to the union of the current changing configurations + * and the provided configs. + * This method is called while loading the animator. + * @hide + */ + public void appendChangingConfigurations(int configs) { + mChangingConfigurations |= configs; + } + + /** + * Return a {@link android.content.res.ConstantState} instance that holds the shared state of + * this Animator. + * <p> + * This constant state is used to create new instances of this animator when needed, instead + * of re-loading it from resources. Default implementation creates a new + * {@link AnimatorConstantState}. You can override this method to provide your custom logic or + * return null if you don't want this animator to be cached. + * + * @return The ConfigurationBoundResourceCache.BaseConstantState associated to this Animator. + * @see android.content.res.ConstantState + * @see #clone() + * @hide + */ + public ConstantState<Animator> createConstantState() { + return new AnimatorConstantState(this); + } + @Override public Animator clone() { try { final Animator anim = (Animator) super.clone(); if (mListeners != null) { - ArrayList<AnimatorListener> oldListeners = mListeners; - anim.mListeners = new ArrayList<AnimatorListener>(); - int numListeners = oldListeners.size(); - for (int i = 0; i < numListeners; ++i) { - anim.mListeners.add(oldListeners.get(i)); - } + anim.mListeners = new ArrayList<AnimatorListener>(mListeners); } if (mPauseListeners != null) { - ArrayList<AnimatorPauseListener> oldListeners = mPauseListeners; - anim.mPauseListeners = new ArrayList<AnimatorPauseListener>(); - int numListeners = oldListeners.size(); - for (int i = 0; i < numListeners; ++i) { - anim.mPauseListeners.add(oldListeners.get(i)); - } + anim.mPauseListeners = new ArrayList<AnimatorPauseListener>(mPauseListeners); } return anim; } catch (CloneNotSupportedException e) { @@ -469,4 +529,35 @@ public abstract class Animator implements Cloneable { public void setAllowRunningAsynchronously(boolean mayRunAsync) { // It is up to subclasses to support this, if they can. } + + /** + * Creates a {@link ConstantState} which holds changing configurations information associated + * with the given Animator. + * <p> + * When {@link #newInstance()} is called, default implementation clones the Animator. + */ + private static class AnimatorConstantState extends ConstantState<Animator> { + + final Animator mAnimator; + int mChangingConf; + + public AnimatorConstantState(Animator animator) { + mAnimator = animator; + // ensure a reference back to here so that constante state is not gc'ed. + mAnimator.mConstantState = this; + mChangingConf = mAnimator.getChangingConfigurations(); + } + + @Override + public int getChangingConfigurations() { + return mChangingConf; + } + + @Override + public Animator newInstance() { + final Animator clone = mAnimator.clone(); + clone.mConstantState = this; + return clone; + } + } } diff --git a/core/java/android/animation/AnimatorInflater.java b/core/java/android/animation/AnimatorInflater.java index 25417ed..688d7e4 100644 --- a/core/java/android/animation/AnimatorInflater.java +++ b/core/java/android/animation/AnimatorInflater.java @@ -16,6 +16,8 @@ package android.animation; import android.content.Context; +import android.content.res.ConfigurationBoundResourceCache; +import android.content.res.ConstantState; import android.content.res.Resources; import android.content.res.Resources.NotFoundException; import android.content.res.Resources.Theme; @@ -30,6 +32,8 @@ import android.util.TypedValue; import android.util.Xml; import android.view.InflateException; import android.view.animation.AnimationUtils; +import android.view.animation.BaseInterpolator; +import android.view.animation.Interpolator; import com.android.internal.R; @@ -67,6 +71,9 @@ public class AnimatorInflater { private static final boolean DBG_ANIMATOR_INFLATER = false; + // used to calculate changing configs for resource references + private static final TypedValue sTmpTypedValue = new TypedValue(); + /** * Loads an {@link Animator} object from a resource * @@ -98,11 +105,34 @@ public class AnimatorInflater { /** @hide */ public static Animator loadAnimator(Resources resources, Theme theme, int id, float pathErrorScale) throws NotFoundException { - + final ConfigurationBoundResourceCache<Animator> animatorCache = resources + .getAnimatorCache(); + Animator animator = animatorCache.get(id, theme); + if (animator != null) { + if (DBG_ANIMATOR_INFLATER) { + Log.d(TAG, "loaded animator from cache, " + resources.getResourceName(id)); + } + return animator; + } else if (DBG_ANIMATOR_INFLATER) { + Log.d(TAG, "cache miss for animator " + resources.getResourceName(id)); + } XmlResourceParser parser = null; try { parser = resources.getAnimation(id); - return createAnimatorFromXml(resources, theme, parser, pathErrorScale); + animator = createAnimatorFromXml(resources, theme, parser, pathErrorScale); + if (animator != null) { + animator.appendChangingConfigurations(getChangingConfigs(resources, id)); + final ConstantState<Animator> constantState = animator.createConstantState(); + if (constantState != null) { + if (DBG_ANIMATOR_INFLATER) { + Log.d(TAG, "caching animator for res " + resources.getResourceName(id)); + } + animatorCache.put(id, theme, constantState); + // create a new animator so that cached version is never used by the user + animator = constantState.newInstance(resources, theme); + } + } + return animator; } catch (XmlPullParserException ex) { Resources.NotFoundException rnf = new Resources.NotFoundException("Can't load animation resource ID #0x" + @@ -122,10 +152,29 @@ public class AnimatorInflater { public static StateListAnimator loadStateListAnimator(Context context, int id) throws NotFoundException { + final Resources resources = context.getResources(); + final ConfigurationBoundResourceCache<StateListAnimator> cache = resources + .getStateListAnimatorCache(); + final Theme theme = context.getTheme(); + StateListAnimator animator = cache.get(id, theme); + if (animator != null) { + return animator; + } XmlResourceParser parser = null; try { - parser = context.getResources().getAnimation(id); - return createStateListAnimatorFromXml(context, parser, Xml.asAttributeSet(parser)); + parser = resources.getAnimation(id); + animator = createStateListAnimatorFromXml(context, parser, Xml.asAttributeSet(parser)); + if (animator != null) { + animator.appendChangingConfigurations(getChangingConfigs(resources, id)); + final ConstantState<StateListAnimator> constantState = animator + .createConstantState(); + if (constantState != null) { + cache.put(id, theme, constantState); + // return a clone so that the animator in constant state is never used. + animator = constantState.newInstance(resources, theme); + } + } + return animator; } catch (XmlPullParserException ex) { Resources.NotFoundException rnf = new Resources.NotFoundException( @@ -172,14 +221,13 @@ public class AnimatorInflater { for (int i = 0; i < attributeCount; i++) { int attrName = attributeSet.getAttributeNameResource(i); if (attrName == R.attr.animation) { - animator = loadAnimator(context, - attributeSet.getAttributeResourceValue(i, 0)); + final int animId = attributeSet.getAttributeResourceValue(i, 0); + animator = loadAnimator(context, animId); } else { states[stateIndex++] = attributeSet.getAttributeBooleanValue(i, false) ? attrName : -attrName; } - } if (animator == null) { animator = createAnimatorFromXml(context.getResources(), @@ -192,7 +240,6 @@ public class AnimatorInflater { } stateListAnimator .addState(StateSet.trimStateSet(states, stateIndex), animator); - } break; } @@ -508,7 +555,6 @@ public class AnimatorInflater { private static Animator createAnimatorFromXml(Resources res, Theme theme, XmlPullParser parser, AttributeSet attrs, AnimatorSet parent, int sequenceOrdering, float pixelSize) throws XmlPullParserException, IOException { - Animator anim = null; ArrayList<Animator> childAnims = null; @@ -537,8 +583,8 @@ public class AnimatorInflater { } else { a = res.obtainAttributes(attrs, R.styleable.AnimatorSet); } - int ordering = a.getInt(R.styleable.AnimatorSet_ordering, - TOGETHER); + anim.appendChangingConfigurations(a.getChangingConfigurations()); + int ordering = a.getInt(R.styleable.AnimatorSet_ordering, TOGETHER); createAnimatorFromXml(res, theme, parser, attrs, (AnimatorSet) anim, ordering, pixelSize); a.recycle(); @@ -565,7 +611,6 @@ public class AnimatorInflater { parent.playSequentially(animsArray); } } - return anim; } @@ -591,7 +636,6 @@ public class AnimatorInflater { private static ValueAnimator loadAnimator(Resources res, Theme theme, AttributeSet attrs, ValueAnimator anim, float pathErrorScale) throws NotFoundException { - TypedArray arrayAnimator = null; TypedArray arrayObjectAnimator = null; @@ -609,25 +653,37 @@ public class AnimatorInflater { } else { arrayObjectAnimator = res.obtainAttributes(attrs, R.styleable.PropertyAnimator); } + anim.appendChangingConfigurations(arrayObjectAnimator.getChangingConfigurations()); } if (anim == null) { anim = new ValueAnimator(); } + anim.appendChangingConfigurations(arrayAnimator.getChangingConfigurations()); parseAnimatorFromTypeArray(anim, arrayAnimator, arrayObjectAnimator, pathErrorScale); - final int resID = - arrayAnimator.getResourceId(R.styleable.Animator_interpolator, 0); + final int resID = arrayAnimator.getResourceId(R.styleable.Animator_interpolator, 0); if (resID > 0) { - anim.setInterpolator(AnimationUtils.loadInterpolator(res, theme, resID)); + final Interpolator interpolator = AnimationUtils.loadInterpolator(res, theme, resID); + if (interpolator instanceof BaseInterpolator) { + anim.appendChangingConfigurations( + ((BaseInterpolator) interpolator).getChangingConfiguration()); + } + anim.setInterpolator(interpolator); } arrayAnimator.recycle(); if (arrayObjectAnimator != null) { arrayObjectAnimator.recycle(); } - return anim; } + + private static int getChangingConfigs(Resources resources, int id) { + synchronized (sTmpTypedValue) { + resources.getValue(id, sTmpTypedValue, true); + return sTmpTypedValue.changingConfigurations; + } + } } diff --git a/core/java/android/animation/AnimatorSet.java b/core/java/android/animation/AnimatorSet.java index 0aa8fdd..92762c3 100644 --- a/core/java/android/animation/AnimatorSet.java +++ b/core/java/android/animation/AnimatorSet.java @@ -241,6 +241,19 @@ public final class AnimatorSet extends Animator { } /** + * @hide + */ + @Override + public int getChangingConfigurations() { + int conf = super.getChangingConfigurations(); + final int nodeCount = mNodes.size(); + for (int i = 0; i < nodeCount; i ++) { + conf |= mNodes.get(i).animation.getChangingConfigurations(); + } + return conf; + } + + /** * Sets the TimeInterpolator for all current {@link #getChildAnimations() child animations} * of this AnimatorSet. The default value is null, which means that no interpolator * is set on this AnimatorSet. Setting the interpolator to any non-null value @@ -628,23 +641,25 @@ public final class AnimatorSet extends Animator { * manually, as we clone each Node (and its animation). The clone will then be sorted, * and will populate any appropriate lists, when it is started. */ + final int nodeCount = mNodes.size(); anim.mNeedsSort = true; anim.mTerminated = false; anim.mStarted = false; anim.mPlayingSet = new ArrayList<Animator>(); anim.mNodeMap = new HashMap<Animator, Node>(); - anim.mNodes = new ArrayList<Node>(); - anim.mSortedNodes = new ArrayList<Node>(); + anim.mNodes = new ArrayList<Node>(nodeCount); + anim.mSortedNodes = new ArrayList<Node>(nodeCount); anim.mReversible = mReversible; anim.mSetListener = null; // Walk through the old nodes list, cloning each node and adding it to the new nodemap. // One problem is that the old node dependencies point to nodes in the old AnimatorSet. // We need to track the old/new nodes in order to reconstruct the dependencies in the clone. - HashMap<Node, Node> nodeCloneMap = new HashMap<Node, Node>(); // <old, new> - for (Node node : mNodes) { + + for (int n = 0; n < nodeCount; n++) { + final Node node = mNodes.get(n); Node nodeClone = node.clone(); - nodeCloneMap.put(node, nodeClone); + node.mTmpClone = nodeClone; anim.mNodes.add(nodeClone); anim.mNodeMap.put(nodeClone.animation, nodeClone); // Clear out the dependencies in the clone; we'll set these up manually later @@ -652,40 +667,50 @@ public final class AnimatorSet extends Animator { nodeClone.tmpDependencies = null; nodeClone.nodeDependents = null; nodeClone.nodeDependencies = null; + // clear out any listeners that were set up by the AnimatorSet; these will // be set up when the clone's nodes are sorted - ArrayList<AnimatorListener> cloneListeners = nodeClone.animation.getListeners(); + final ArrayList<AnimatorListener> cloneListeners = nodeClone.animation.getListeners(); if (cloneListeners != null) { - ArrayList<AnimatorListener> listenersToRemove = null; - for (AnimatorListener listener : cloneListeners) { + for (int i = cloneListeners.size() - 1; i >= 0; i--) { + final AnimatorListener listener = cloneListeners.get(i); if (listener instanceof AnimatorSetListener) { - if (listenersToRemove == null) { - listenersToRemove = new ArrayList<AnimatorListener>(); - } - listenersToRemove.add(listener); - } - } - if (listenersToRemove != null) { - for (AnimatorListener listener : listenersToRemove) { - cloneListeners.remove(listener); + cloneListeners.remove(i); } } } } // Now that we've cloned all of the nodes, we're ready to walk through their // dependencies, mapping the old dependencies to the new nodes - for (Node node : mNodes) { - Node nodeClone = nodeCloneMap.get(node); + for (int n = 0; n < nodeCount; n++) { + final Node node = mNodes.get(n); + final Node clone = node.mTmpClone; if (node.dependencies != null) { - for (Dependency dependency : node.dependencies) { - Node clonedDependencyNode = nodeCloneMap.get(dependency.node); - Dependency cloneDependency = new Dependency(clonedDependencyNode, + clone.dependencies = new ArrayList<Dependency>(node.dependencies.size()); + final int depSize = node.dependencies.size(); + for (int i = 0; i < depSize; i ++) { + final Dependency dependency = node.dependencies.get(i); + Dependency cloneDependency = new Dependency(dependency.node.mTmpClone, dependency.rule); - nodeClone.addDependency(cloneDependency); + clone.dependencies.add(cloneDependency); + } + } + if (node.nodeDependents != null) { + clone.nodeDependents = new ArrayList<Node>(node.nodeDependents.size()); + for (Node dep : node.nodeDependents) { + clone.nodeDependents.add(dep.mTmpClone); + } + } + if (node.nodeDependencies != null) { + clone.nodeDependencies = new ArrayList<Node>(node.nodeDependencies.size()); + for (Node dep : node.nodeDependencies) { + clone.nodeDependencies.add(dep.mTmpClone); } } } - + for (int n = 0; n < nodeCount; n++) { + mNodes.get(n).mTmpClone = null; + } return anim; } @@ -1017,6 +1042,11 @@ public final class AnimatorSet extends Animator { public boolean done = false; /** + * Temporary field to hold the clone in AnimatorSet#clone. Cleaned after clone is complete + */ + private Node mTmpClone = null; + + /** * Constructs the Node with the animation that it encapsulates. A Node has no * dependencies by default; dependencies are added via the addDependency() * method. diff --git a/core/java/android/animation/FloatKeyframeSet.java b/core/java/android/animation/FloatKeyframeSet.java index 12e5862..abac246 100644 --- a/core/java/android/animation/FloatKeyframeSet.java +++ b/core/java/android/animation/FloatKeyframeSet.java @@ -19,6 +19,7 @@ package android.animation; import android.animation.Keyframe.FloatKeyframe; import java.util.ArrayList; +import java.util.List; /** * This class holds a collection of FloatKeyframe objects and is called by ValueAnimator to calculate @@ -47,8 +48,8 @@ class FloatKeyframeSet extends KeyframeSet implements Keyframes.FloatKeyframes { @Override public FloatKeyframeSet clone() { - ArrayList<Keyframe> keyframes = mKeyframes; - int numKeyframes = mKeyframes.size(); + final List<Keyframe> keyframes = mKeyframes; + final int numKeyframes = mKeyframes.size(); FloatKeyframe[] newKeyframes = new FloatKeyframe[numKeyframes]; for (int i = 0; i < numKeyframes; ++i) { newKeyframes[i] = (FloatKeyframe) keyframes.get(i).clone(); diff --git a/core/java/android/animation/IntKeyframeSet.java b/core/java/android/animation/IntKeyframeSet.java index 7a5b0ec..0ec5138 100644 --- a/core/java/android/animation/IntKeyframeSet.java +++ b/core/java/android/animation/IntKeyframeSet.java @@ -19,6 +19,7 @@ package android.animation; import android.animation.Keyframe.IntKeyframe; import java.util.ArrayList; +import java.util.List; /** * This class holds a collection of IntKeyframe objects and is called by ValueAnimator to calculate @@ -47,7 +48,7 @@ class IntKeyframeSet extends KeyframeSet implements Keyframes.IntKeyframes { @Override public IntKeyframeSet clone() { - ArrayList<Keyframe> keyframes = mKeyframes; + List<Keyframe> keyframes = mKeyframes; int numKeyframes = mKeyframes.size(); IntKeyframe[] newKeyframes = new IntKeyframe[numKeyframes]; for (int i = 0; i < numKeyframes; ++i) { diff --git a/core/java/android/animation/KeyframeSet.java b/core/java/android/animation/KeyframeSet.java index 8d15db2..0e99bff 100644 --- a/core/java/android/animation/KeyframeSet.java +++ b/core/java/android/animation/KeyframeSet.java @@ -18,6 +18,8 @@ package android.animation; import java.util.ArrayList; import java.util.Arrays; +import java.util.List; + import android.animation.Keyframe.IntKeyframe; import android.animation.Keyframe.FloatKeyframe; import android.animation.Keyframe.ObjectKeyframe; @@ -36,16 +38,16 @@ class KeyframeSet implements Keyframes { Keyframe mFirstKeyframe; Keyframe mLastKeyframe; TimeInterpolator mInterpolator; // only used in the 2-keyframe case - ArrayList<Keyframe> mKeyframes; // only used when there are not 2 keyframes + List<Keyframe> mKeyframes; // only used when there are not 2 keyframes TypeEvaluator mEvaluator; public KeyframeSet(Keyframe... keyframes) { mNumKeyframes = keyframes.length; - mKeyframes = new ArrayList<Keyframe>(); - mKeyframes.addAll(Arrays.asList(keyframes)); - mFirstKeyframe = mKeyframes.get(0); - mLastKeyframe = mKeyframes.get(mNumKeyframes - 1); + // immutable list + mKeyframes = Arrays.asList(keyframes); + mFirstKeyframe = keyframes[0]; + mLastKeyframe = keyframes[mNumKeyframes - 1]; mInterpolator = mLastKeyframe.getInterpolator(); } @@ -57,7 +59,7 @@ class KeyframeSet implements Keyframes { public void invalidateCache() { } - public ArrayList<Keyframe> getKeyframes() { + public List<Keyframe> getKeyframes() { return mKeyframes; } @@ -177,9 +179,9 @@ class KeyframeSet implements Keyframes { @Override public KeyframeSet clone() { - ArrayList<Keyframe> keyframes = mKeyframes; + List<Keyframe> keyframes = mKeyframes; int numKeyframes = mKeyframes.size(); - Keyframe[] newKeyframes = new Keyframe[numKeyframes]; + final Keyframe[] newKeyframes = new Keyframe[numKeyframes]; for (int i = 0; i < numKeyframes; ++i) { newKeyframes[i] = keyframes.get(i).clone(); } diff --git a/core/java/android/animation/Keyframes.java b/core/java/android/animation/Keyframes.java index 6611c6c..c921466 100644 --- a/core/java/android/animation/Keyframes.java +++ b/core/java/android/animation/Keyframes.java @@ -16,6 +16,7 @@ package android.animation; import java.util.ArrayList; +import java.util.List; /** * This interface abstracts a collection of Keyframe objects and is called by @@ -62,7 +63,7 @@ interface Keyframes extends Cloneable { * @return A list of all Keyframes contained by this. This may return null if this is * not made up of Keyframes. */ - ArrayList<Keyframe> getKeyframes(); + List<Keyframe> getKeyframes(); Keyframes clone(); diff --git a/core/java/android/animation/ObjectAnimator.java b/core/java/android/animation/ObjectAnimator.java index 500634c..59daaab 100644 --- a/core/java/android/animation/ObjectAnimator.java +++ b/core/java/android/animation/ObjectAnimator.java @@ -885,7 +885,8 @@ public final class ObjectAnimator extends ValueAnimator { } /** - * Sets the target object whose property will be animated by this animation + * Sets the target object whose property will be animated by this animation. If the + * animator has been started, it will be canceled. * * @param target The object being animated */ @@ -893,6 +894,9 @@ public final class ObjectAnimator extends ValueAnimator { public void setTarget(@Nullable Object target) { final Object oldTarget = getTarget(); if (oldTarget != target) { + if (isStarted()) { + cancel(); + } mTarget = target == null ? null : new WeakReference<Object>(target); // New target should cause re-initialization prior to starting mInitialized = false; diff --git a/core/java/android/animation/PropertyValuesHolder.java b/core/java/android/animation/PropertyValuesHolder.java index d372933..97426c3 100644 --- a/core/java/android/animation/PropertyValuesHolder.java +++ b/core/java/android/animation/PropertyValuesHolder.java @@ -27,6 +27,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.concurrent.locks.ReentrantReadWriteLock; /** @@ -791,7 +792,7 @@ public class PropertyValuesHolder implements Cloneable { // check to make sure that mProperty is on the class of target try { Object testValue = null; - ArrayList<Keyframe> keyframes = mKeyframes.getKeyframes(); + List<Keyframe> keyframes = mKeyframes.getKeyframes(); int keyframeCount = keyframes == null ? 0 : keyframes.size(); for (int i = 0; i < keyframeCount; i++) { Keyframe kf = keyframes.get(i); @@ -814,7 +815,7 @@ public class PropertyValuesHolder implements Cloneable { if (mSetter == null) { setupSetter(targetClass); } - ArrayList<Keyframe> keyframes = mKeyframes.getKeyframes(); + List<Keyframe> keyframes = mKeyframes.getKeyframes(); int keyframeCount = keyframes == null ? 0 : keyframes.size(); for (int i = 0; i < keyframeCount; i++) { Keyframe kf = keyframes.get(i); @@ -890,7 +891,7 @@ public class PropertyValuesHolder implements Cloneable { * @param target The object which holds the start values that should be set. */ void setupStartValue(Object target) { - ArrayList<Keyframe> keyframes = mKeyframes.getKeyframes(); + List<Keyframe> keyframes = mKeyframes.getKeyframes(); if (!keyframes.isEmpty()) { setupValue(target, keyframes.get(0)); } @@ -905,7 +906,7 @@ public class PropertyValuesHolder implements Cloneable { * @param target The object which holds the start values that should be set. */ void setupEndValue(Object target) { - ArrayList<Keyframe> keyframes = mKeyframes.getKeyframes(); + List<Keyframe> keyframes = mKeyframes.getKeyframes(); if (!keyframes.isEmpty()) { setupValue(target, keyframes.get(keyframes.size() - 1)); } diff --git a/core/java/android/animation/StateListAnimator.java b/core/java/android/animation/StateListAnimator.java index 7256a06..d49e914 100644 --- a/core/java/android/animation/StateListAnimator.java +++ b/core/java/android/animation/StateListAnimator.java @@ -16,6 +16,7 @@ package android.animation; +import android.content.res.ConstantState; import android.util.StateSet; import android.view.View; @@ -44,25 +45,31 @@ import java.util.ArrayList; * @attr ref android.R.styleable#DrawableStates_state_pressed * @attr ref android.R.styleable#StateListAnimatorItem_animation */ -public class StateListAnimator { - - private final ArrayList<Tuple> mTuples = new ArrayList<Tuple>(); +public class StateListAnimator implements Cloneable { + private ArrayList<Tuple> mTuples = new ArrayList<Tuple>(); private Tuple mLastMatch = null; - private Animator mRunningAnimator = null; - private WeakReference<View> mViewRef; + private StateListAnimatorConstantState mConstantState; + private AnimatorListenerAdapter mAnimatorListener; + private int mChangingConfigurations; - private AnimatorListenerAdapter mAnimatorListener = new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - animation.setTarget(null); - if (mRunningAnimator == animation) { - mRunningAnimator = null; + public StateListAnimator() { + initAnimatorListener(); + } + + private void initAnimatorListener() { + mAnimatorListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + animation.setTarget(null); + if (mRunningAnimator == animation) { + mRunningAnimator = null; + } } - } - }; + }; + } /** * Associates the given animator with the provided drawable state specs so that it will be run @@ -75,6 +82,7 @@ public class StateListAnimator { Tuple tuple = new Tuple(specs, animator); tuple.mAnimator.addListener(mAnimatorListener); mTuples.add(tuple); + mChangingConfigurations |= animator.getChangingConfigurations(); } /** @@ -118,12 +126,35 @@ public class StateListAnimator { for (int i = 0; i < size; i++) { mTuples.get(i).mAnimator.setTarget(null); } - mViewRef = null; mLastMatch = null; mRunningAnimator = null; } + @Override + public StateListAnimator clone() { + try { + StateListAnimator clone = (StateListAnimator) super.clone(); + clone.mTuples = new ArrayList<Tuple>(mTuples.size()); + clone.mLastMatch = null; + clone.mRunningAnimator = null; + clone.mViewRef = null; + clone.mAnimatorListener = null; + clone.initAnimatorListener(); + final int tupleSize = mTuples.size(); + for (int i = 0; i < tupleSize; i++) { + final Tuple tuple = mTuples.get(i); + final Animator animatorClone = tuple.mAnimator.clone(); + animatorClone.removeListener(mAnimatorListener); + clone.addState(tuple.mSpecs, animatorClone); + } + clone.setChangingConfigurations(getChangingConfigurations()); + return clone; + } catch (CloneNotSupportedException e) { + throw new AssertionError("cannot clone state list animator", e); + } + } + /** * Called by View * @hide @@ -182,6 +213,63 @@ public class StateListAnimator { } /** + * Return a mask of the configuration parameters for which this animator may change, requiring + * that it be re-created. The default implementation returns whatever was provided through + * {@link #setChangingConfigurations(int)} or 0 by default. + * + * @return Returns a mask of the changing configuration parameters, as defined by + * {@link android.content.pm.ActivityInfo}. + * + * @see android.content.pm.ActivityInfo + * @hide + */ + public int getChangingConfigurations() { + return mChangingConfigurations; + } + + /** + * Set a mask of the configuration parameters for which this animator may change, requiring + * that it should be recreated from resources instead of being cloned. + * + * @param configs A mask of the changing configuration parameters, as + * defined by {@link android.content.pm.ActivityInfo}. + * + * @see android.content.pm.ActivityInfo + * @hide + */ + public void setChangingConfigurations(int configs) { + mChangingConfigurations = configs; + } + + /** + * Sets the changing configurations value to the union of the current changing configurations + * and the provided configs. + * This method is called while loading the animator. + * @hide + */ + public void appendChangingConfigurations(int configs) { + mChangingConfigurations |= configs; + } + + /** + * Return a {@link android.content.res.ConstantState} instance that holds the shared state of + * this Animator. + * <p> + * This constant state is used to create new instances of this animator when needed. Default + * implementation creates a new {@link StateListAnimatorConstantState}. You can override this + * method to provide your custom logic or return null if you don't want this animator to be + * cached. + * + * @return The {@link android.content.res.ConstantState} associated to this Animator. + * @see android.content.res.ConstantState + * @see #clone() + * @hide + */ + public ConstantState<StateListAnimator> createConstantState() { + return new StateListAnimatorConstantState(this); + } + + /** * @hide */ public static class Tuple { @@ -209,4 +297,36 @@ public class StateListAnimator { return mAnimator; } } + + /** + * Creates a constant state which holds changing configurations information associated with the + * given Animator. + * <p> + * When new instance is called, default implementation clones the Animator. + */ + private static class StateListAnimatorConstantState + extends ConstantState<StateListAnimator> { + + final StateListAnimator mAnimator; + + int mChangingConf; + + public StateListAnimatorConstantState(StateListAnimator animator) { + mAnimator = animator; + mAnimator.mConstantState = this; + mChangingConf = mAnimator.getChangingConfigurations(); + } + + @Override + public int getChangingConfigurations() { + return mChangingConf; + } + + @Override + public StateListAnimator newInstance() { + final StateListAnimator clone = mAnimator.clone(); + clone.mConstantState = this; + return clone; + } + } } diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java index 0d17d67..07f79b8 100644 --- a/core/java/android/animation/ValueAnimator.java +++ b/core/java/android/animation/ValueAnimator.java @@ -16,6 +16,7 @@ package android.animation; +import android.content.res.ConfigurationBoundResourceCache; import android.os.Looper; import android.os.Trace; import android.util.AndroidRuntimeException; @@ -1289,12 +1290,7 @@ public class ValueAnimator extends Animator { public ValueAnimator clone() { final ValueAnimator anim = (ValueAnimator) super.clone(); if (mUpdateListeners != null) { - ArrayList<AnimatorUpdateListener> oldListeners = mUpdateListeners; - anim.mUpdateListeners = new ArrayList<AnimatorUpdateListener>(); - int numListeners = oldListeners.size(); - for (int i = 0; i < numListeners; ++i) { - anim.mUpdateListeners.add(oldListeners.get(i)); - } + anim.mUpdateListeners = new ArrayList<AnimatorUpdateListener>(mUpdateListeners); } anim.mSeekTime = -1; anim.mPlayingBackwards = false; diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 37e8aa4..7a636db 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -213,6 +213,13 @@ public class ActivityManager { public static final int BROADCAST_STICKY_CANT_HAVE_PERMISSION = -1; /** + * Result for IActivityManager.broadcastIntent: trying to send a broadcast + * to a stopped user. Fail. + * @hide + */ + public static final int BROADCAST_FAILED_USER_STOPPED = -2; + + /** * Type for IActivityManaqer.getIntentSender: this PendingIntent is * for a sendBroadcast operation. * @hide @@ -1243,26 +1250,16 @@ public class ActivityManager { } /** - * If set, the process of the root activity of the task will be killed - * as part of removing the task. - * @hide - */ - public static final int REMOVE_TASK_KILL_PROCESS = 0x0001; - - /** * Completely remove the given task. * * @param taskId Identifier of the task to be removed. - * @param flags Additional operational flags. May be 0 or - * {@link #REMOVE_TASK_KILL_PROCESS}. * @return Returns true if the given task was found and removed. * * @hide */ - public boolean removeTask(int taskId, int flags) - throws SecurityException { + public boolean removeTask(int taskId) throws SecurityException { try { - return ActivityManagerNative.getDefault().removeTask(taskId, flags); + return ActivityManagerNative.getDefault().removeTask(taskId); } catch (RemoteException e) { // System dead, we will be dead too soon! return false; diff --git a/core/java/android/app/ActivityManagerNative.java b/core/java/android/app/ActivityManagerNative.java index 4e2ff0b..c3028b7 100644 --- a/core/java/android/app/ActivityManagerNative.java +++ b/core/java/android/app/ActivityManagerNative.java @@ -1884,8 +1884,7 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM { data.enforceInterface(IActivityManager.descriptor); int taskId = data.readInt(); - int fl = data.readInt(); - boolean result = removeTask(taskId, fl); + boolean result = removeTask(taskId); reply.writeNoException(); reply.writeInt(result ? 1 : 0); return true; @@ -2280,6 +2279,20 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM return true; } + case START_IN_PLACE_ANIMATION_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + final Bundle bundle; + if (data.readInt() == 0) { + bundle = null; + } else { + bundle = data.readBundle(); + } + final ActivityOptions options = bundle == null ? null : new ActivityOptions(bundle); + startInPlaceAnimationOnFrontMostApplication(options); + reply.writeNoException(); + return true; + } + case REQUEST_VISIBLE_BEHIND_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); IBinder token = data.readStrongBinder(); @@ -4778,12 +4791,11 @@ class ActivityManagerProxy implements IActivityManager return result; } - public boolean removeTask(int taskId, int flags) throws RemoteException { + public boolean removeTask(int taskId) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); data.writeInt(taskId); - data.writeInt(flags); mRemote.transact(REMOVE_TASK_TRANSACTION, data, reply, 0); reply.readException(); boolean result = reply.readInt() != 0; @@ -5300,6 +5312,24 @@ class ActivityManagerProxy implements IActivityManager } @Override + public void startInPlaceAnimationOnFrontMostApplication(ActivityOptions options) + throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + if (options == null) { + data.writeInt(0); + } else { + data.writeInt(1); + data.writeBundle(options.toBundle()); + } + mRemote.transact(START_IN_PLACE_ANIMATION_TRANSACTION, data, reply, IBinder.FLAG_ONEWAY); + reply.readException(); + data.recycle(); + reply.recycle(); + } + + @Override public boolean requestVisibleBehind(IBinder token, boolean visible) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index cd6a4f5..3d390bf 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -63,6 +63,12 @@ public class ActivityOptions { public static final String KEY_ANIM_EXIT_RES_ID = "android:animExitRes"; /** + * Custom in-place animation resource ID. + * @hide + */ + public static final String KEY_ANIM_IN_PLACE_RES_ID = "android:animInPlaceRes"; + + /** * Bitmap for thumbnail animation. * @hide */ @@ -132,11 +138,14 @@ public class ActivityOptions { public static final int ANIM_THUMBNAIL_ASPECT_SCALE_UP = 8; /** @hide */ public static final int ANIM_THUMBNAIL_ASPECT_SCALE_DOWN = 9; + /** @hide */ + public static final int ANIM_CUSTOM_IN_PLACE = 10; private String mPackageName; private int mAnimationType = ANIM_NONE; private int mCustomEnterResId; private int mCustomExitResId; + private int mCustomInPlaceResId; private Bitmap mThumbnail; private int mStartX; private int mStartY; @@ -198,6 +207,30 @@ public class ActivityOptions { return opts; } + /** + * Creates an ActivityOptions specifying a custom animation to run in place on an existing + * activity. + * + * @param context Who is defining this. This is the application that the + * animation resources will be loaded from. + * @param animId A resource ID of the animation resource to use for + * the incoming activity. + * @return Returns a new ActivityOptions object that you can use to + * supply these options as the options Bundle when running an in-place animation. + * @hide + */ + public static ActivityOptions makeCustomInPlaceAnimation(Context context, int animId) { + if (animId == 0) { + throw new RuntimeException("You must specify a valid animation."); + } + + ActivityOptions opts = new ActivityOptions(); + opts.mPackageName = context.getPackageName(); + opts.mAnimationType = ANIM_CUSTOM_IN_PLACE; + opts.mCustomInPlaceResId = animId; + return opts; + } + private void setOnAnimationStartedListener(Handler handler, OnAnimationStartedListener listener) { if (listener != null) { @@ -540,6 +573,10 @@ public class ActivityOptions { opts.getBinder(KEY_ANIM_START_LISTENER)); break; + case ANIM_CUSTOM_IN_PLACE: + mCustomInPlaceResId = opts.getInt(KEY_ANIM_IN_PLACE_RES_ID, 0); + break; + case ANIM_SCALE_UP: mStartX = opts.getInt(KEY_ANIM_START_X, 0); mStartY = opts.getInt(KEY_ANIM_START_Y, 0); @@ -592,6 +629,11 @@ public class ActivityOptions { } /** @hide */ + public int getCustomInPlaceResId() { + return mCustomInPlaceResId; + } + + /** @hide */ public Bitmap getThumbnail() { return mThumbnail; } @@ -689,6 +731,9 @@ public class ActivityOptions { } mAnimationStartedListener = otherOptions.mAnimationStartedListener; break; + case ANIM_CUSTOM_IN_PLACE: + mCustomInPlaceResId = otherOptions.mCustomInPlaceResId; + break; case ANIM_SCALE_UP: mStartX = otherOptions.mStartX; mStartY = otherOptions.mStartY; @@ -756,6 +801,9 @@ public class ActivityOptions { b.putBinder(KEY_ANIM_START_LISTENER, mAnimationStartedListener != null ? mAnimationStartedListener.asBinder() : null); break; + case ANIM_CUSTOM_IN_PLACE: + b.putInt(KEY_ANIM_IN_PLACE_RES_ID, mCustomInPlaceResId); + break; case ANIM_SCALE_UP: b.putInt(KEY_ANIM_START_X, mStartX); b.putInt(KEY_ANIM_START_Y, mStartY); diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index dd49009..cf6c049 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -1742,6 +1742,12 @@ public final class ActivityThread { new LoadedApk(this, aInfo, compatInfo, baseLoader, securityViolation, includeCode && (aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage); + + if (mSystemThread && "android".equals(aInfo.packageName)) { + packageInfo.installSystemApplicationInfo(aInfo, + getSystemContext().mPackageInfo.getClassLoader()); + } + if (includeCode) { mPackages.put(aInfo.packageName, new WeakReference<LoadedApk>(packageInfo)); @@ -1802,10 +1808,6 @@ public final class ActivityThread { synchronized (this) { getSystemContext().installSystemApplicationInfo(info, classLoader); - // The code package for "android" in the system server needs - // to be the system context's package. - mPackages.put("android", new WeakReference<LoadedApk>(getSystemContext().mPackageInfo)); - // give ourselves a default profiler mProfiler = new Profiler(); } @@ -2513,7 +2515,12 @@ public final class ActivityThread { } public void handleInstallProvider(ProviderInfo info) { - installContentProviders(mInitialApplication, Lists.newArrayList(info)); + final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites(); + try { + installContentProviders(mInitialApplication, Lists.newArrayList(info)); + } finally { + StrictMode.setThreadPolicy(oldPolicy); + } } private void handleEnterAnimationComplete(IBinder token) { diff --git a/core/java/android/app/ActivityTransitionCoordinator.java b/core/java/android/app/ActivityTransitionCoordinator.java index 540376e..9062892 100644 --- a/core/java/android/app/ActivityTransitionCoordinator.java +++ b/core/java/android/app/ActivityTransitionCoordinator.java @@ -206,6 +206,8 @@ abstract class ActivityTransitionCoordinator extends ResultReceiver { private ArrayList<GhostViewListeners> mGhostViewListeners = new ArrayList<GhostViewListeners>(); private ArrayMap<View, Float> mOriginalAlphas = new ArrayMap<View, Float>(); + final private ArrayList<View> mRootSharedElements = new ArrayList<View>(); + private ArrayList<Matrix> mSharedElementParentMatrices; public ActivityTransitionCoordinator(Window window, ArrayList<String> allSharedElementNames, @@ -222,8 +224,7 @@ abstract class ActivityTransitionCoordinator extends ResultReceiver { if (mListener != null) { mListener.onMapSharedElements(mAllSharedElementNames, sharedElements); } - mSharedElementNames.addAll(sharedElements.keySet()); - mSharedElements.addAll(sharedElements.values()); + setSharedElements(sharedElements); if (getViewsTransition() != null && mTransitioningViews != null) { ViewGroup decorView = getDecor(); if (decorView != null) { @@ -234,6 +235,58 @@ abstract class ActivityTransitionCoordinator extends ResultReceiver { setEpicenter(); } + /** + * Iterates over the shared elements and adds them to the members in order. + * Shared elements that are nested in other shared elements are placed after the + * elements that they are nested in. This means that layout ordering can be done + * from first to last. + * + * @param sharedElements The map of transition names to shared elements to set into + * the member fields. + */ + private void setSharedElements(ArrayMap<String, View> sharedElements) { + boolean isFirstRun = true; + while (!sharedElements.isEmpty()) { + final int numSharedElements = sharedElements.size(); + for (int i = numSharedElements - 1; i >= 0; i--) { + final View view = sharedElements.valueAt(i); + final String name = sharedElements.keyAt(i); + if (isFirstRun && (view == null || !view.isAttachedToWindow() || name == null)) { + sharedElements.removeAt(i); + } else { + if (!isNested(view, sharedElements)) { + mSharedElementNames.add(name); + mSharedElements.add(view); + sharedElements.removeAt(i); + if (isFirstRun) { + // We need to keep track which shared elements are roots + // and which are nested. + mRootSharedElements.add(view); + } + } + } + } + isFirstRun = false; + } + } + + /** + * Returns true when view is nested in any of the values of sharedElements. + */ + private static boolean isNested(View view, ArrayMap<String, View> sharedElements) { + ViewParent parent = view.getParent(); + boolean isNested = false; + while (parent instanceof View) { + View parentView = (View) parent; + if (sharedElements.containsValue(parentView)) { + isNested = true; + break; + } + parent = parentView.getParent(); + } + return isNested; + } + protected void stripOffscreenViews() { if (mTransitioningViews == null) { return; @@ -449,11 +502,50 @@ abstract class ActivityTransitionCoordinator extends ResultReceiver { view.layout(x, y, x + width, y + height); } - protected void getSharedElementParentMatrix(View view, Matrix matrix) { - // Find the location in the view's parent - ViewGroup parent = (ViewGroup) view.getParent(); - matrix.reset(); - parent.transformMatrixToLocal(matrix); + private void setSharedElementMatrices() { + int numSharedElements = mSharedElements.size(); + if (numSharedElements > 0) { + mSharedElementParentMatrices = new ArrayList<Matrix>(numSharedElements); + } + for (int i = 0; i < numSharedElements; i++) { + View view = mSharedElements.get(i); + + // Find the location in the view's parent + ViewGroup parent = (ViewGroup) view.getParent(); + Matrix matrix = new Matrix(); + parent.transformMatrixToLocal(matrix); + + mSharedElementParentMatrices.add(matrix); + } + } + + private void getSharedElementParentMatrix(View view, Matrix matrix) { + final boolean isNestedInOtherSharedElement = !mRootSharedElements.contains(view); + final boolean useParentMatrix; + if (isNestedInOtherSharedElement) { + useParentMatrix = true; + } else { + final int index = mSharedElementParentMatrices == null ? -1 + : mSharedElements.indexOf(view); + if (index < 0) { + useParentMatrix = true; + } else { + // The indices of mSharedElementParentMatrices matches the + // mSharedElement matrices. + Matrix parentMatrix = mSharedElementParentMatrices.get(index); + matrix.set(parentMatrix); + useParentMatrix = false; + } + } + if (useParentMatrix) { + matrix.reset(); + ViewParent viewParent = view.getParent(); + if (viewParent instanceof ViewGroup) { + // Find the location in the view's parent + ViewGroup parent = (ViewGroup) viewParent; + parent.transformMatrixToLocal(matrix); + } + } } protected ArrayList<SharedElementOriginalState> setSharedElementState( @@ -536,16 +628,17 @@ abstract class ActivityTransitionCoordinator extends ResultReceiver { protected ArrayList<View> createSnapshots(Bundle state, Collection<String> names) { int numSharedElements = names.size(); + ArrayList<View> snapshots = new ArrayList<View>(numSharedElements); if (numSharedElements == 0) { - return null; + return snapshots; } - ArrayList<View> snapshots = new ArrayList<View>(numSharedElements); Context context = getWindow().getContext(); int[] decorLoc = new int[2]; ViewGroup decorView = getDecor(); if (decorView != null) { decorView.getLocationOnScreen(decorLoc); } + Matrix tempMatrix = new Matrix(); for (String name: names) { Bundle sharedElementBundle = state.getBundle(name); if (sharedElementBundle != null) { @@ -555,7 +648,7 @@ abstract class ActivityTransitionCoordinator extends ResultReceiver { snapshot = mListener.onCreateSnapshotView(context, parcelable); } if (snapshot != null) { - setSharedElementState(snapshot, name, state, null, null, decorLoc); + setSharedElementState(snapshot, name, state, tempMatrix, null, decorLoc); } snapshots.add(snapshot); } @@ -607,6 +700,8 @@ abstract class ActivityTransitionCoordinator extends ResultReceiver { mResultReceiver = null; mPendingTransition = null; mListener = null; + mRootSharedElements.clear(); + mSharedElementParentMatrices = null; } protected long getFadeDuration() { @@ -704,9 +799,10 @@ abstract class ActivityTransitionCoordinator extends ResultReceiver { } protected void moveSharedElementsToOverlay() { - if (!mWindow.getSharedElementsUseOverlay()) { + if (mWindow == null || !mWindow.getSharedElementsUseOverlay()) { return; } + setSharedElementMatrices(); int numSharedElements = mSharedElements.size(); ViewGroup decor = getDecor(); if (decor != null) { diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index 1b499cc..967e97e 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -1676,6 +1676,17 @@ final class ApplicationPackageManager extends PackageManager { * @hide */ public Drawable loadItemIcon(PackageItemInfo itemInfo, ApplicationInfo appInfo) { + Drawable dr = loadUnbadgedItemIcon(itemInfo, appInfo); + if (itemInfo.showUserIcon != UserHandle.USER_NULL) { + return dr; + } + return getUserBadgedIcon(dr, new UserHandle(mContext.getUserId())); + } + + /** + * @hide + */ + public Drawable loadUnbadgedItemIcon(PackageItemInfo itemInfo, ApplicationInfo appInfo) { if (itemInfo.showUserIcon != UserHandle.USER_NULL) { Bitmap bitmap = getUserManager().getUserIcon(itemInfo.showUserIcon); if (bitmap == null) { @@ -1690,7 +1701,7 @@ final class ApplicationPackageManager extends PackageManager { if (dr == null) { dr = itemInfo.loadDefaultIcon(this); } - return getUserBadgedIcon(dr, new UserHandle(mContext.getUserId())); + return dr; } private Drawable getBadgedDrawable(Drawable drawable, Drawable badgeDrawable, diff --git a/core/java/android/app/BackStackRecord.java b/core/java/android/app/BackStackRecord.java index 0092ee7..2784d44 100644 --- a/core/java/android/app/BackStackRecord.java +++ b/core/java/android/app/BackStackRecord.java @@ -38,6 +38,7 @@ import android.view.ViewTreeObserver; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.List; final class BackStackState implements Parcelable { final int[] mOps; @@ -1055,7 +1056,7 @@ final class BackStackRecord extends FragmentTransaction implements } private static ArrayList<View> captureExitingViews(Transition exitTransition, - Fragment outFragment, ArrayMap<String, View> namedViews) { + Fragment outFragment, ArrayMap<String, View> namedViews, View nonExistentView) { ArrayList<View> viewList = null; if (exitTransition != null) { viewList = new ArrayList<View>(); @@ -1064,7 +1065,10 @@ final class BackStackRecord extends FragmentTransaction implements if (namedViews != null) { viewList.removeAll(namedViews.values()); } - addTargets(exitTransition, viewList); + if (!viewList.isEmpty()) { + viewList.add(nonExistentView); + addTargets(exitTransition, viewList); + } } return viewList; } @@ -1132,11 +1136,8 @@ final class BackStackRecord extends FragmentTransaction implements namedViews = mapSharedElementsIn(state, isBack, inFragment); removeTargets(sharedElementTransition, sharedElementTargets); sharedElementTargets.clear(); - if (namedViews.isEmpty()) { - sharedElementTargets.add(state.nonExistentView); - } else { - sharedElementTargets.addAll(namedViews.values()); - } + sharedElementTargets.add(state.nonExistentView); + sharedElementTargets.addAll(namedViews.values()); addTargets(sharedElementTransition, sharedElementTargets); @@ -1153,6 +1154,9 @@ final class BackStackRecord extends FragmentTransaction implements if (namedViews != null) { enteringViews.removeAll(namedViews.values()); } + enteringViews.add(state.nonExistentView); + // We added this earlier to prevent any views being targeted. + enterTransition.removeTarget(state.nonExistentView); addTargets(enterTransition, enteringViews); } setSharedElementEpicenter(enterTransition, state); @@ -1293,11 +1297,8 @@ final class BackStackRecord extends FragmentTransaction implements ArrayList<View> sharedElementTargets = new ArrayList<View>(); if (sharedElementTransition != null) { namedViews = remapSharedElements(state, outFragment, isBack); - if (namedViews.isEmpty()) { - sharedElementTargets.add(state.nonExistentView); - } else { - sharedElementTargets.addAll(namedViews.values()); - } + sharedElementTargets.add(state.nonExistentView); + sharedElementTargets.addAll(namedViews.values()); addTargets(sharedElementTransition, sharedElementTargets); // Notify the start of the transition. @@ -1310,7 +1311,7 @@ final class BackStackRecord extends FragmentTransaction implements } ArrayList<View> exitingViews = captureExitingViews(exitTransition, outFragment, - namedViews); + namedViews, state.nonExistentView); if (exitingViews == null || exitingViews.isEmpty()) { exitTransition = null; } @@ -1388,20 +1389,69 @@ final class BackStackRecord extends FragmentTransaction implements } } - private static void removeTargets(Transition transition, ArrayList<View> views) { - int numViews = views.size(); - for (int i = 0; i < numViews; i++) { - transition.removeTarget(views.get(i)); + /** + * This method removes the views from transitions that target ONLY those views. + * The views list should match those added in addTargets and should contain + * one view that is not in the view hierarchy (state.nonExistentView). + */ + public static void removeTargets(Transition transition, ArrayList<View> views) { + if (transition instanceof TransitionSet) { + TransitionSet set = (TransitionSet) transition; + int numTransitions = set.getTransitionCount(); + for (int i = 0; i < numTransitions; i++) { + Transition child = set.getTransitionAt(i); + removeTargets(child, views); + } + } else if (!hasSimpleTarget(transition)) { + List<View> targets = transition.getTargets(); + if (targets != null && targets.size() == views.size() && + targets.containsAll(views)) { + // We have an exact match. We must have added these earlier in addTargets + for (int i = views.size() - 1; i >= 0; i--) { + transition.removeTarget(views.get(i)); + } + } } } - private static void addTargets(Transition transition, ArrayList<View> views) { - int numViews = views.size(); - for (int i = 0; i < numViews; i++) { - transition.addTarget(views.get(i)); + /** + * This method adds views as targets to the transition, but only if the transition + * doesn't already have a target. It is best for views to contain one View object + * that does not exist in the view hierarchy (state.nonExistentView) so that + * when they are removed later, a list match will suffice to remove the targets. + * Otherwise, if you happened to have targeted the exact views for the transition, + * the removeTargets call will remove them unexpectedly. + */ + public static void addTargets(Transition transition, ArrayList<View> views) { + if (transition instanceof TransitionSet) { + TransitionSet set = (TransitionSet) transition; + int numTransitions = set.getTransitionCount(); + for (int i = 0; i < numTransitions; i++) { + Transition child = set.getTransitionAt(i); + addTargets(child, views); + } + } else if (!hasSimpleTarget(transition)) { + List<View> targets = transition.getTargets(); + if (isNullOrEmpty(targets)) { + // We can just add the target views + int numViews = views.size(); + for (int i = 0; i < numViews; i++) { + transition.addTarget(views.get(i)); + } + } } } + private static boolean hasSimpleTarget(Transition transition) { + return !isNullOrEmpty(transition.getTargetIds()) || + !isNullOrEmpty(transition.getTargetNames()) || + !isNullOrEmpty(transition.getTargetTypes()); + } + + private static boolean isNullOrEmpty(List list) { + return list == null || list.isEmpty(); + } + /** * Remaps a name-to-View map, substituting different names for keys. * diff --git a/core/java/android/app/EnterTransitionCoordinator.java b/core/java/android/app/EnterTransitionCoordinator.java index add67f2..ecf19c7 100644 --- a/core/java/android/app/EnterTransitionCoordinator.java +++ b/core/java/android/app/EnterTransitionCoordinator.java @@ -57,7 +57,6 @@ class EnterTransitionCoordinator extends ActivityTransitionCoordinator { private boolean mIsViewsTransitionStarted; private boolean mIsViewsTransitionComplete; private boolean mIsSharedElementTransitionComplete; - private ArrayList<Matrix> mSharedElementParentMatrices; private Transition mEnterViewsTransition; public EnterTransitionCoordinator(Activity activity, ResultReceiver resultReceiver, @@ -122,7 +121,6 @@ class EnterTransitionCoordinator extends ActivityTransitionCoordinator { if (mIsReturning) { sendSharedElementDestination(); } else { - setSharedElementMatrices(); moveSharedElementsToOverlay(); } if (mSharedElementsBundle != null) { @@ -135,16 +133,17 @@ class EnterTransitionCoordinator extends ActivityTransitionCoordinator { return; } mAreViewsReady = true; + final ViewGroup decor = getDecor(); // Ensure the views have been laid out before capturing the views -- we need the epicenter. - if (sharedElements.isEmpty() || !sharedElements.valueAt(0).isLayoutRequested()) { + if (decor == null || (decor.isAttachedToWindow() && + (sharedElements.isEmpty() || !sharedElements.valueAt(0).isLayoutRequested()))) { viewsReady(sharedElements); } else { - final View sharedElement = sharedElements.valueAt(0); - sharedElement.getViewTreeObserver() + decor.getViewTreeObserver() .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { - sharedElement.getViewTreeObserver().removeOnPreDrawListener(this); + decor.getViewTreeObserver().removeOnPreDrawListener(this); viewsReady(sharedElements); return true; } @@ -194,7 +193,6 @@ class EnterTransitionCoordinator extends ActivityTransitionCoordinator { } if (allReady) { Bundle state = captureSharedElementState(); - setSharedElementMatrices(); moveSharedElementsToOverlay(); mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state); } else if (decorView != null) { @@ -205,7 +203,6 @@ class EnterTransitionCoordinator extends ActivityTransitionCoordinator { decorView.getViewTreeObserver().removeOnPreDrawListener(this); if (mResultReceiver != null) { Bundle state = captureSharedElementState(); - setSharedElementMatrices(); moveSharedElementsToOverlay(); mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state); } @@ -322,6 +319,7 @@ class EnterTransitionCoordinator extends ActivityTransitionCoordinator { if (mListener != null) { mListener.onRejectSharedElements(rejectedSnapshots); } + removeNullViews(rejectedSnapshots); startRejectedAnimations(rejectedSnapshots); // Now start shared element transition @@ -370,6 +368,16 @@ class EnterTransitionCoordinator extends ActivityTransitionCoordinator { } } + private static void removeNullViews(ArrayList<View> views) { + if (views != null) { + for (int i = views.size() - 1; i >= 0; i--) { + if (views.get(i) == null) { + views.remove(i); + } + } + } + } + private void onTakeSharedElements() { if (!mIsReadyForTransition || mSharedElementsBundle == null) { return; @@ -634,30 +642,4 @@ class EnterTransitionCoordinator extends ActivityTransitionCoordinator { }); } - private void setSharedElementMatrices() { - int numSharedElements = mSharedElements.size(); - if (numSharedElements > 0) { - mSharedElementParentMatrices = new ArrayList<Matrix>(numSharedElements); - } - for (int i = 0; i < numSharedElements; i++) { - View view = mSharedElements.get(i); - - // Find the location in the view's parent - ViewGroup parent = (ViewGroup) view.getParent(); - Matrix matrix = new Matrix(); - parent.transformMatrixToLocal(matrix); - - mSharedElementParentMatrices.add(matrix); - } - } - - @Override - protected void getSharedElementParentMatrix(View view, Matrix matrix) { - int index = mSharedElementParentMatrices == null ? -1 : mSharedElements.indexOf(view); - if (index < 0) { - super.getSharedElementParentMatrix(view, matrix); - } else { - matrix.set(mSharedElementParentMatrices.get(index)); - } - } } diff --git a/core/java/android/app/IActivityManager.java b/core/java/android/app/IActivityManager.java index be26f30..6433f3f 100644 --- a/core/java/android/app/IActivityManager.java +++ b/core/java/android/app/IActivityManager.java @@ -373,7 +373,7 @@ public interface IActivityManager extends IInterface { public boolean isUserRunning(int userid, boolean orStopping) throws RemoteException; public int[] getRunningUserIds() throws RemoteException; - public boolean removeTask(int taskId, int flags) throws RemoteException; + public boolean removeTask(int taskId) throws RemoteException; public void registerProcessObserver(IProcessObserver observer) throws RemoteException; public void unregisterProcessObserver(IProcessObserver observer) throws RemoteException; @@ -456,6 +456,9 @@ public interface IActivityManager extends IInterface { throws RemoteException; public Bitmap getTaskDescriptionIcon(String filename) throws RemoteException; + public void startInPlaceAnimationOnFrontMostApplication(ActivityOptions opts) + throws RemoteException; + public boolean requestVisibleBehind(IBinder token, boolean visible) throws RemoteException; public boolean isBackgroundVisibleBehind(IBinder token) throws RemoteException; public void backgroundResourcesReleased(IBinder token) throws RemoteException; @@ -781,4 +784,5 @@ public interface IActivityManager extends IInterface { int BOOT_ANIMATION_COMPLETE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+237; int GET_TASK_DESCRIPTION_ICON_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+238; int LAUNCH_ASSIST_INTENT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+239; + int START_IN_PLACE_ANIMATION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+240; } diff --git a/core/java/android/app/KeyguardManager.java b/core/java/android/app/KeyguardManager.java index cc9aed8..5038df9 100644 --- a/core/java/android/app/KeyguardManager.java +++ b/core/java/android/app/KeyguardManager.java @@ -16,10 +16,14 @@ package android.app; +import android.app.trust.ITrustManager; +import android.content.Context; import android.content.Intent; import android.os.Binder; import android.os.RemoteException; import android.os.IBinder; +import android.os.ServiceManager; +import android.os.UserHandle; import android.view.IWindowManager; import android.view.IOnKeyguardExitResult; import android.view.WindowManagerGlobal; @@ -33,6 +37,7 @@ import android.view.WindowManagerGlobal; */ public class KeyguardManager { private IWindowManager mWM; + private ITrustManager mTrustManager; /** * Intent used to prompt user for device credentials. @@ -151,6 +156,8 @@ public class KeyguardManager { KeyguardManager() { mWM = WindowManagerGlobal.getWindowManagerService(); + mTrustManager = ITrustManager.Stub.asInterface( + ServiceManager.getService(Context.TRUST_SERVICE)); } /** @@ -218,6 +225,34 @@ public class KeyguardManager { } /** + * Return whether unlocking the device is currently not requiring a password + * because of a trust agent. + * + * @return true if the keyguard can currently be unlocked without entering credentials + * because the device is in a trusted environment. + */ + public boolean isKeyguardInTrustedState() { + return isKeyguardInTrustedState(UserHandle.getCallingUserId()); + } + + /** + * Return whether unlocking the device is currently not requiring a password + * because of a trust agent. + * + * @param userId the user for which the trusted state should be reported. + * @return true if the keyguard can currently be unlocked without entering credentials + * because the device is in a trusted environment. + * @hide + */ + public boolean isKeyguardInTrustedState(int userId) { + try { + return mTrustManager.isTrusted(userId); + } catch (RemoteException e) { + return false; + } + } + + /** * @deprecated Use {@link android.view.WindowManager.LayoutParams#FLAG_DISMISS_KEYGUARD} * and/or {@link android.view.WindowManager.LayoutParams#FLAG_SHOW_WHEN_LOCKED} * instead; this allows you to seamlessly hide the keyguard as your application diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index fb10e17..dfe5cf5 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -23,6 +23,7 @@ import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.ColorStateList; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.PorterDuff; @@ -1112,7 +1113,11 @@ public class Notification implements Parcelable /** Notification action extra which contains wearable extensions */ private static final String EXTRA_WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS"; + // Keys within EXTRA_WEARABLE_EXTENSIONS for wearable options. private static final String KEY_FLAGS = "flags"; + private static final String KEY_IN_PROGRESS_LABEL = "inProgressLabel"; + private static final String KEY_CONFIRM_LABEL = "confirmLabel"; + private static final String KEY_CANCEL_LABEL = "cancelLabel"; // Flags bitwise-ored to mFlags private static final int FLAG_AVAILABLE_OFFLINE = 0x1; @@ -1122,6 +1127,10 @@ public class Notification implements Parcelable private int mFlags = DEFAULT_FLAGS; + private CharSequence mInProgressLabel; + private CharSequence mConfirmLabel; + private CharSequence mCancelLabel; + /** * Create a {@link android.app.Notification.Action.WearableExtender} with default * options. @@ -1138,6 +1147,9 @@ public class Notification implements Parcelable Bundle wearableBundle = action.getExtras().getBundle(EXTRA_WEARABLE_EXTENSIONS); if (wearableBundle != null) { mFlags = wearableBundle.getInt(KEY_FLAGS, DEFAULT_FLAGS); + mInProgressLabel = wearableBundle.getCharSequence(KEY_IN_PROGRESS_LABEL); + mConfirmLabel = wearableBundle.getCharSequence(KEY_CONFIRM_LABEL); + mCancelLabel = wearableBundle.getCharSequence(KEY_CANCEL_LABEL); } } @@ -1153,6 +1165,15 @@ public class Notification implements Parcelable if (mFlags != DEFAULT_FLAGS) { wearableBundle.putInt(KEY_FLAGS, mFlags); } + if (mInProgressLabel != null) { + wearableBundle.putCharSequence(KEY_IN_PROGRESS_LABEL, mInProgressLabel); + } + if (mConfirmLabel != null) { + wearableBundle.putCharSequence(KEY_CONFIRM_LABEL, mConfirmLabel); + } + if (mCancelLabel != null) { + wearableBundle.putCharSequence(KEY_CANCEL_LABEL, mCancelLabel); + } builder.getExtras().putBundle(EXTRA_WEARABLE_EXTENSIONS, wearableBundle); return builder; @@ -1162,6 +1183,9 @@ public class Notification implements Parcelable public WearableExtender clone() { WearableExtender that = new WearableExtender(); that.mFlags = this.mFlags; + that.mInProgressLabel = this.mInProgressLabel; + that.mConfirmLabel = this.mConfirmLabel; + that.mCancelLabel = this.mCancelLabel; return that; } @@ -1193,6 +1217,72 @@ public class Notification implements Parcelable mFlags &= ~mask; } } + + /** + * Set a label to display while the wearable is preparing to automatically execute the + * action. This is usually a 'ing' verb ending in ellipsis like "Sending..." + * + * @param label the label to display while the action is being prepared to execute + * @return this object for method chaining + */ + public WearableExtender setInProgressLabel(CharSequence label) { + mInProgressLabel = label; + return this; + } + + /** + * Get the label to display while the wearable is preparing to automatically execute + * the action. This is usually a 'ing' verb ending in ellipsis like "Sending..." + * + * @return the label to display while the action is being prepared to execute + */ + public CharSequence getInProgressLabel() { + return mInProgressLabel; + } + + /** + * Set a label to display to confirm that the action should be executed. + * This is usually an imperative verb like "Send". + * + * @param label the label to confirm the action should be executed + * @return this object for method chaining + */ + public WearableExtender setConfirmLabel(CharSequence label) { + mConfirmLabel = label; + return this; + } + + /** + * Get the label to display to confirm that the action should be executed. + * This is usually an imperative verb like "Send". + * + * @return the label to confirm the action should be executed + */ + public CharSequence getConfirmLabel() { + return mConfirmLabel; + } + + /** + * Set a label to display to cancel the action. + * This is usually an imperative verb, like "Cancel". + * + * @param label the label to display to cancel the action + * @return this object for method chaining + */ + public WearableExtender setCancelLabel(CharSequence label) { + mCancelLabel = label; + return this; + } + + /** + * Get the label to display to cancel the action. + * This is usually an imperative verb like "Cancel". + * + * @return the label to display to cancel the action + */ + public CharSequence getCancelLabel() { + return mCancelLabel; + } } } @@ -2773,6 +2863,14 @@ public class Notification implements Parcelable contentView.setViewVisibility(R.id.progress, View.VISIBLE); contentView.setProgressBar( R.id.progress, mProgressMax, mProgress, mProgressIndeterminate); + contentView.setProgressBackgroundTintList( + R.id.progress, ColorStateList.valueOf(mContext.getResources().getColor( + R.color.notification_progress_background_color))); + if (mColor != COLOR_DEFAULT) { + ColorStateList colorStateList = ColorStateList.valueOf(mColor); + contentView.setProgressTintList(R.id.progress, colorStateList); + contentView.setProgressIndeterminateTintList(R.id.progress, colorStateList); + } showLine2 = true; } else { contentView.setViewVisibility(R.id.progress, View.GONE); @@ -4331,10 +4429,23 @@ public class Notification implements Parcelable */ public static final int SIZE_FULL_SCREEN = 5; + /** + * Sentinel value for use with {@link #setHintScreenTimeout} to keep the screen on for a + * short amount of time when this notification is displayed on the screen. This + * is the default value. + */ + public static final int SCREEN_TIMEOUT_SHORT = 0; + + /** + * Sentinel value for use with {@link #setHintScreenTimeout} to keep the screen on + * for a longer amount of time when this notification is displayed on the screen. + */ + public static final int SCREEN_TIMEOUT_LONG = -1; + /** Notification extra which contains wearable extensions */ private static final String EXTRA_WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS"; - // Keys within EXTRA_WEARABLE_OPTIONS for wearable options. + // Keys within EXTRA_WEARABLE_EXTENSIONS for wearable options. private static final String KEY_ACTIONS = "actions"; private static final String KEY_FLAGS = "flags"; private static final String KEY_DISPLAY_INTENT = "displayIntent"; @@ -4346,12 +4457,14 @@ public class Notification implements Parcelable private static final String KEY_CUSTOM_SIZE_PRESET = "customSizePreset"; private static final String KEY_CUSTOM_CONTENT_HEIGHT = "customContentHeight"; private static final String KEY_GRAVITY = "gravity"; + private static final String KEY_HINT_SCREEN_TIMEOUT = "hintScreenTimeout"; // Flags bitwise-ored to mFlags private static final int FLAG_CONTENT_INTENT_AVAILABLE_OFFLINE = 0x1; private static final int FLAG_HINT_HIDE_ICON = 1 << 1; private static final int FLAG_HINT_SHOW_BACKGROUND_ONLY = 1 << 2; private static final int FLAG_START_SCROLL_BOTTOM = 1 << 3; + private static final int FLAG_HINT_AVOID_BACKGROUND_CLIPPING = 1 << 4; // Default value for flags integer private static final int DEFAULT_FLAGS = FLAG_CONTENT_INTENT_AVAILABLE_OFFLINE; @@ -4370,6 +4483,7 @@ public class Notification implements Parcelable private int mCustomSizePreset = SIZE_DEFAULT; private int mCustomContentHeight; private int mGravity = DEFAULT_GRAVITY; + private int mHintScreenTimeout; /** * Create a {@link android.app.Notification.WearableExtender} with default @@ -4405,6 +4519,7 @@ public class Notification implements Parcelable SIZE_DEFAULT); mCustomContentHeight = wearableBundle.getInt(KEY_CUSTOM_CONTENT_HEIGHT); mGravity = wearableBundle.getInt(KEY_GRAVITY, DEFAULT_GRAVITY); + mHintScreenTimeout = wearableBundle.getInt(KEY_HINT_SCREEN_TIMEOUT); } } @@ -4452,6 +4567,9 @@ public class Notification implements Parcelable if (mGravity != DEFAULT_GRAVITY) { wearableBundle.putInt(KEY_GRAVITY, mGravity); } + if (mHintScreenTimeout != 0) { + wearableBundle.putInt(KEY_HINT_SCREEN_TIMEOUT, mHintScreenTimeout); + } builder.getExtras().putBundle(EXTRA_WEARABLE_EXTENSIONS, wearableBundle); return builder; @@ -4471,6 +4589,7 @@ public class Notification implements Parcelable that.mCustomSizePreset = this.mCustomSizePreset; that.mCustomContentHeight = this.mCustomContentHeight; that.mGravity = this.mGravity; + that.mHintScreenTimeout = this.mHintScreenTimeout; return that; } @@ -4866,6 +4985,48 @@ public class Notification implements Parcelable return (mFlags & FLAG_HINT_SHOW_BACKGROUND_ONLY) != 0; } + /** + * Set a hint that this notification's background should not be clipped if possible. + * @param hintAvoidBackgroundClipping {@code true} to avoid clipping if possible. + * @return this object for method chaining + */ + public WearableExtender setHintAvoidBackgroundClipping( + boolean hintAvoidBackgroundClipping) { + setFlag(FLAG_HINT_AVOID_BACKGROUND_CLIPPING, hintAvoidBackgroundClipping); + return this; + } + + /** + * Get a hint that this notification's background should not be clipped if possible. + * @return {@code true} if it's ok if the background is clipped on the screen, false + * otherwise. The default value is {@code false} if this was never set. + */ + public boolean getHintAvoidBackgroundClipping() { + return (mFlags & FLAG_HINT_AVOID_BACKGROUND_CLIPPING) != 0; + } + + /** + * Set a hint that the screen should remain on for at least this duration when + * this notification is displayed on the screen. + * @param timeout The requested screen timeout in milliseconds. Can also be either + * {@link #SCREEN_TIMEOUT_SHORT} or {@link #SCREEN_TIMEOUT_LONG}. + * @return this object for method chaining + */ + public WearableExtender setHintScreenTimeout(int timeout) { + mHintScreenTimeout = timeout; + return this; + } + + /** + * Get the duration, in milliseconds, that the screen should remain on for + * when this notification is displayed. + * @return the duration in milliseconds if > 0, or either one of the sentinel values + * {@link #SCREEN_TIMEOUT_SHORT} or {@link #SCREEN_TIMEOUT_LONG}. + */ + public int getHintScreenTimeout() { + return mHintScreenTimeout; + } + private void setFlag(int mask, boolean value) { if (value) { mFlags |= mask; diff --git a/core/java/android/app/SharedElementCallback.java b/core/java/android/app/SharedElementCallback.java index 060bbe6..6ac2401 100644 --- a/core/java/android/app/SharedElementCallback.java +++ b/core/java/android/app/SharedElementCallback.java @@ -18,13 +18,16 @@ package android.app; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; -import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.RectF; import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Bundle; import android.os.Parcelable; import android.transition.TransitionUtils; import android.view.View; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; import java.util.List; import java.util.Map; @@ -40,6 +43,9 @@ import java.util.Map; */ public abstract class SharedElementCallback { private Matrix mTempMatrix; + private static final String BUNDLE_SNAPSHOT_BITMAP = "sharedElement:snapshot:bitmap"; + private static final String BUNDLE_SNAPSHOT_IMAGE_SCALETYPE = "sharedElement:snapshot:imageScaleType"; + private static final String BUNDLE_SNAPSHOT_IMAGE_MATRIX = "sharedElement:snapshot:imageMatrix"; static final SharedElementCallback NULL_CALLBACK = new SharedElementCallback() { }; @@ -142,6 +148,27 @@ public abstract class SharedElementCallback { */ public Parcelable onCaptureSharedElementSnapshot(View sharedElement, Matrix viewToGlobalMatrix, RectF screenBounds) { + if (sharedElement instanceof ImageView) { + ImageView imageView = ((ImageView) sharedElement); + Drawable d = imageView.getDrawable(); + Drawable bg = imageView.getBackground(); + if (d != null && (bg == null || bg.getAlpha() == 0)) { + Bitmap bitmap = TransitionUtils.createDrawableBitmap(d); + if (bitmap != null) { + Bundle bundle = new Bundle(); + bundle.putParcelable(BUNDLE_SNAPSHOT_BITMAP, bitmap); + bundle.putString(BUNDLE_SNAPSHOT_IMAGE_SCALETYPE, + imageView.getScaleType().toString()); + if (imageView.getScaleType() == ScaleType.MATRIX) { + Matrix matrix = imageView.getImageMatrix(); + float[] values = new float[9]; + matrix.getValues(values); + bundle.putFloatArray(BUNDLE_SNAPSHOT_IMAGE_MATRIX, values); + } + return bundle; + } + } + } if (mTempMatrix == null) { mTempMatrix = new Matrix(viewToGlobalMatrix); } else { @@ -169,7 +196,24 @@ public abstract class SharedElementCallback { */ public View onCreateSnapshotView(Context context, Parcelable snapshot) { View view = null; - if (snapshot instanceof Bitmap) { + if (snapshot instanceof Bundle) { + Bundle bundle = (Bundle) snapshot; + Bitmap bitmap = (Bitmap) bundle.getParcelable(BUNDLE_SNAPSHOT_BITMAP); + if (bitmap == null) { + return null; + } + ImageView imageView = new ImageView(context); + view = imageView; + imageView.setImageBitmap(bitmap); + imageView.setScaleType( + ScaleType.valueOf(bundle.getString(BUNDLE_SNAPSHOT_IMAGE_SCALETYPE))); + if (imageView.getScaleType() == ScaleType.MATRIX) { + float[] values = bundle.getFloatArray(BUNDLE_SNAPSHOT_IMAGE_MATRIX); + Matrix matrix = new Matrix(); + matrix.setValues(values); + imageView.setImageMatrix(matrix); + } + } else if (snapshot instanceof Bitmap) { Bitmap bitmap = (Bitmap) snapshot; view = new View(context); Resources resources = context.getResources(); diff --git a/core/java/android/app/UiAutomation.java b/core/java/android/app/UiAutomation.java index 4aec9e0..b0dd70f 100644 --- a/core/java/android/app/UiAutomation.java +++ b/core/java/android/app/UiAutomation.java @@ -25,6 +25,7 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Point; import android.hardware.display.DisplayManagerGlobal; +import android.os.IBinder; import android.os.Looper; import android.os.ParcelFileDescriptor; import android.os.RemoteException; @@ -919,7 +920,7 @@ public final class UiAutomation { public IAccessibilityServiceClientImpl(Looper looper) { super(null, looper, new Callbacks() { @Override - public void onSetConnectionId(int connectionId) { + public void init(int connectionId, IBinder windowToken) { synchronized (mLock) { mConnectionId = connectionId; mLock.notifyAll(); diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index a30ae57..9157b1b 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -31,6 +31,7 @@ import android.content.pm.ResolveInfo; import android.net.ProxyInfo; import android.os.Bundle; import android.os.Handler; +import android.os.PersistableBundle; import android.os.Process; import android.os.RemoteCallback; import android.os.RemoteException; @@ -40,6 +41,7 @@ import android.os.UserManager; import android.provider.Settings; import android.security.Credentials; import android.service.restrictions.RestrictionsReceiver; +import android.service.trust.TrustAgentService; import android.util.Log; import com.android.org.conscrypt.TrustedCertificateStore; @@ -60,13 +62,16 @@ import java.util.Collections; import java.util.List; /** - * Public interface for managing policies enforced on a device. Most clients - * of this class must have published a {@link DeviceAdminReceiver} that the user - * has currently enabled. + * Public interface for managing policies enforced on a device. Most clients of this class must be + * registered with the system as a + * <a href={@docRoot}guide/topics/admin/device-admin.html">device administrator</a>. Additionally, + * a device administrator may be registered as either a profile or device owner. A given method is + * accessible to all device administrators unless the documentation for that method specifies that + * it is restricted to either device or profile owners. * * <div class="special reference"> * <h3>Developer Guides</h3> - * <p>For more information about managing policies for device adminstration, read the + * <p>For more information about managing policies for device administration, read the * <a href="{@docRoot}guide/topics/admin/device-admin.html">Device Administration</a> * developer guide.</p> * </div> @@ -2584,6 +2589,10 @@ public class DevicePolicyManager { * <p>The application restrictions are only made visible to the target application and the * profile or device owner. * + * <p>If the restrictions are not available yet, but may be applied in the near future, + * the admin can notify the target application of that by adding + * {@link UserManager#KEY_RESTRICTIONS_PENDING} to the settings parameter. + * * <p>The calling device admin must be a profile or device owner; if it is not, a security * exception will be thrown. * @@ -2591,6 +2600,8 @@ public class DevicePolicyManager { * @param packageName The name of the package to update restricted settings for. * @param settings A {@link Bundle} to be parsed by the receiving application, conveying a new * set of active restrictions. + * + * @see UserManager#KEY_RESTRICTIONS_PENDING */ public void setApplicationRestrictions(ComponentName admin, String packageName, Bundle settings) { @@ -2604,25 +2615,29 @@ public class DevicePolicyManager { } /** - * Sets a list of features to enable for a TrustAgent component. This is meant to be - * used in conjunction with {@link #KEYGUARD_DISABLE_TRUST_AGENTS}, which will disable all - * trust agents but those with features enabled by this function call. + * Sets a list of configuration features to enable for a TrustAgent component. This is meant + * to be used in conjunction with {@link #KEYGUARD_DISABLE_TRUST_AGENTS}, which disables all + * trust agents but those enabled by this function call. If flag + * {@link #KEYGUARD_DISABLE_TRUST_AGENTS} is not set, then this call has no effect. * * <p>The calling device admin must have requested * {@link DeviceAdminInfo#USES_POLICY_DISABLE_KEYGUARD_FEATURES} to be able to call - * this method; if it has not, a security exception will be thrown. + * this method; if not, a security exception will be thrown. * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. - * @param agent Which component to enable features for. - * @param features List of features to enable. Consult specific TrustAgent documentation for - * the feature list. - * @hide + * @param target Component name of the agent to be enabled. + * @param options TrustAgent-specific feature bundle. If null for any admin, agent + * will be strictly disabled according to the state of the + * {@link #KEYGUARD_DISABLE_TRUST_AGENTS} flag. + * <p>If {@link #KEYGUARD_DISABLE_TRUST_AGENTS} is set and options is not null for all admins, + * then it's up to the TrustAgent itself to aggregate the values from all device admins. + * <p>Consult documentation for the specific TrustAgent to determine legal options parameters. */ - public void setTrustAgentFeaturesEnabled(ComponentName admin, ComponentName agent, - List<String> features) { + public void setTrustAgentConfiguration(ComponentName admin, ComponentName target, + PersistableBundle options) { if (mService != null) { try { - mService.setTrustAgentFeaturesEnabled(admin, agent, features, UserHandle.myUserId()); + mService.setTrustAgentConfiguration(admin, target, options, UserHandle.myUserId()); } catch (RemoteException e) { Log.w(TAG, "Failed talking with device policy service", e); } @@ -2630,24 +2645,30 @@ public class DevicePolicyManager { } /** - * Gets list of enabled features for the given TrustAgent component. If admin is - * null, this will return the intersection of all features enabled for the given agent by all - * admins. + * Gets configuration for the given trust agent based on aggregating all calls to + * {@link #setTrustAgentConfiguration(ComponentName, ComponentName, PersistableBundle)} for + * all device admins. * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @param agent Which component to get enabled features for. - * @return List of enabled features. - * @hide + * @return configuration for the given trust agent. */ - public List<String> getTrustAgentFeaturesEnabled(ComponentName admin, ComponentName agent) { + public List<PersistableBundle> getTrustAgentConfiguration(ComponentName admin, + ComponentName agent) { + return getTrustAgentConfiguration(admin, agent, UserHandle.myUserId()); + } + + /** @hide per-user version */ + public List<PersistableBundle> getTrustAgentConfiguration(ComponentName admin, + ComponentName agent, int userHandle) { if (mService != null) { try { - return mService.getTrustAgentFeaturesEnabled(admin, agent, UserHandle.myUserId()); + return mService.getTrustAgentConfiguration(admin, agent, userHandle); } catch (RemoteException e) { Log.w(TAG, "Failed talking with device policy service", e); } } - return new ArrayList<String>(); // empty list + return new ArrayList<PersistableBundle>(); // empty list } /** @@ -3133,9 +3154,10 @@ public class DevicePolicyManager { } /** - * Called by a profile owner to disable account management for a specific type of account. + * Called by a device owner or profile owner to disable account management for a specific type + * of account. * - * <p>The calling device admin must be a profile owner. If it is not, a + * <p>The calling device admin must be a device owner or profile owner. If it is not, a * security exception will be thrown. * * <p>When account management is disabled for an account type, adding or removing an account diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index c8e1780..07aa800 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -22,6 +22,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.net.ProxyInfo; import android.os.Bundle; +import android.os.PersistableBundle; import android.os.RemoteCallback; import android.os.UserHandle; import java.util.List; @@ -183,8 +184,10 @@ interface IDevicePolicyManager { boolean getCrossProfileCallerIdDisabled(in ComponentName who); boolean getCrossProfileCallerIdDisabledForUser(int userId); - void setTrustAgentFeaturesEnabled(in ComponentName admin, in ComponentName agent, in List<String> features, int userId); - List<String> getTrustAgentFeaturesEnabled(in ComponentName admin, in ComponentName agent, int userId); + void setTrustAgentConfiguration(in ComponentName admin, in ComponentName agent, + in PersistableBundle args, int userId); + List<PersistableBundle> getTrustAgentConfiguration(in ComponentName admin, + in ComponentName agent, int userId); boolean addCrossProfileWidgetProvider(in ComponentName admin, String packageName); boolean removeCrossProfileWidgetProvider(in ComponentName admin, String packageName); diff --git a/core/java/android/app/backup/IBackupManager.aidl b/core/java/android/app/backup/IBackupManager.aidl index 8a44c8e..0a2d4f5 100644 --- a/core/java/android/app/backup/IBackupManager.aidl +++ b/core/java/android/app/backup/IBackupManager.aidl @@ -291,4 +291,16 @@ interface IBackupManager { * {@hide} */ void opComplete(int token); + + /** + * Make the device's backup and restore machinery (in)active. When it is inactive, + * the device will not perform any backup operations, nor will it deliver data for + * restore, although clients can still safely call BackupManager methods. + * + * @param whichUser User handle of the defined user whose backup active state + * is to be adjusted. + * @param makeActive {@code true} when backup services are to be made active; + * {@code false} otherwise. + */ + void setBackupServiceActive(int whichUser, boolean makeActive); } diff --git a/core/java/android/app/trust/ITrustManager.aidl b/core/java/android/app/trust/ITrustManager.aidl index 6fbf87d..0193711 100644 --- a/core/java/android/app/trust/ITrustManager.aidl +++ b/core/java/android/app/trust/ITrustManager.aidl @@ -29,4 +29,5 @@ interface ITrustManager { void reportRequireCredentialEntry(int userId); void registerTrustListener(in ITrustListener trustListener); void unregisterTrustListener(in ITrustListener trustListener); + boolean isTrusted(int userId); } diff --git a/core/java/android/bluetooth/IBluetooth.aidl b/core/java/android/bluetooth/IBluetooth.aidl index 992f601..cd4535a 100644 --- a/core/java/android/bluetooth/IBluetooth.aidl +++ b/core/java/android/bluetooth/IBluetooth.aidl @@ -98,4 +98,7 @@ interface IBluetooth boolean isActivityAndEnergyReportingSupported(); void getActivityEnergyInfoFromController(); BluetoothActivityEnergyInfo reportActivityInfo(); + + // for dumpsys support + String dump(); } diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java index 2853c58..c8f9b7d 100644 --- a/core/java/android/content/ContentProvider.java +++ b/core/java/android/content/ContentProvider.java @@ -515,7 +515,10 @@ public abstract class ContentProvider implements ComponentCallbacks2 { } // last chance, check against any uri grants - if (context.checkUriPermission(uri, pid, uid, Intent.FLAG_GRANT_READ_URI_PERMISSION) + final int callingUserId = UserHandle.getUserId(uid); + final Uri userUri = (mSingleUser && !UserHandle.isSameUser(mMyUid, uid)) + ? maybeAddUserId(uri, callingUserId) : uri; + if (context.checkUriPermission(userUri, pid, uid, Intent.FLAG_GRANT_READ_URI_PERMISSION) == PERMISSION_GRANTED) { return; } diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index af6f181..e06f034 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -41,6 +41,7 @@ import android.os.Bundle; import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; +import android.os.Process; import android.os.StrictMode; import android.os.UserHandle; import android.provider.DocumentsContract; @@ -876,12 +877,44 @@ public class Intent implements Parcelable, Cloneable { * related methods. */ public static Intent createChooser(Intent target, CharSequence title) { + return createChooser(target, title, null); + } + + /** + * Convenience function for creating a {@link #ACTION_CHOOSER} Intent. + * + * <p>Builds a new {@link #ACTION_CHOOSER} Intent that wraps the given + * target intent, also optionally supplying a title. If the target + * intent has specified {@link #FLAG_GRANT_READ_URI_PERMISSION} or + * {@link #FLAG_GRANT_WRITE_URI_PERMISSION}, then these flags will also be + * set in the returned chooser intent, with its ClipData set appropriately: + * either a direct reflection of {@link #getClipData()} if that is non-null, + * or a new ClipData built from {@link #getData()}.</p> + * + * <p>The caller may optionally supply an {@link IntentSender} to receive a callback + * when the user makes a choice. This can be useful if the calling application wants + * to remember the last chosen target and surface it as a more prominent or one-touch + * affordance elsewhere in the UI for next time.</p> + * + * @param target The Intent that the user will be selecting an activity + * to perform. + * @param title Optional title that will be displayed in the chooser. + * @param sender Optional IntentSender to be called when a choice is made. + * @return Return a new Intent object that you can hand to + * {@link Context#startActivity(Intent) Context.startActivity()} and + * related methods. + */ + public static Intent createChooser(Intent target, CharSequence title, IntentSender sender) { Intent intent = new Intent(ACTION_CHOOSER); intent.putExtra(EXTRA_INTENT, target); if (title != null) { intent.putExtra(EXTRA_TITLE, title); } + if (sender != null) { + intent.putExtra(EXTRA_CHOSEN_COMPONENT_INTENT_SENDER, sender); + } + // Migrate any clip data and flags from target. int permFlags = target.getFlags() & (FLAG_GRANT_READ_URI_PERMISSION | FLAG_GRANT_WRITE_URI_PERMISSION | FLAG_GRANT_PERSISTABLE_URI_PERMISSION @@ -3140,6 +3173,26 @@ public class Intent implements Parcelable, Cloneable { "android.intent.extra.REPLACEMENT_EXTRAS"; /** + * An {@link IntentSender} that will be notified if a user successfully chooses a target + * component to handle an action in an {@link #ACTION_CHOOSER} activity. The IntentSender + * will have the extra {@link #EXTRA_CHOSEN_COMPONENT} appended to it containing the + * {@link ComponentName} of the chosen component. + * + * <p>In some situations this callback may never come, for example if the user abandons + * the chooser, switches to another task or any number of other reasons. Apps should not + * be written assuming that this callback will always occur.</p> + */ + public static final String EXTRA_CHOSEN_COMPONENT_INTENT_SENDER = + "android.intent.extra.CHOSEN_COMPONENT_INTENT_SENDER"; + + /** + * The {@link ComponentName} chosen by the user to complete an action. + * + * @see #EXTRA_CHOSEN_COMPONENT_INTENT_SENDER + */ + public static final String EXTRA_CHOSEN_COMPONENT = "android.intent.extra.CHOSEN_COMPONENT"; + + /** * A {@link android.view.KeyEvent} object containing the event that * triggered the creation of the Intent it is in. */ @@ -7446,8 +7499,10 @@ public class Intent implements Parcelable, Cloneable { */ public void prepareToEnterProcess() { if (mContentUserHint != UserHandle.USER_CURRENT) { - fixUris(mContentUserHint); - mContentUserHint = UserHandle.USER_CURRENT; + if (UserHandle.getAppId(Process.myUid()) != Process.SYSTEM_UID) { + fixUris(mContentUserHint); + mContentUserHint = UserHandle.USER_CURRENT; + } } } diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java index 5ee0b67..c164340 100644 --- a/core/java/android/content/pm/LauncherApps.java +++ b/core/java/android/content/pm/LauncherApps.java @@ -499,20 +499,4 @@ public class LauncherApps { obtainMessage(MSG_UNAVAILABLE, info).sendToTarget(); } } - - /** - * TODO Remove after 2014-09-22 - * @hide - */ - public void addCallback(Callback callback) { - registerCallback(callback); - } - - /** - * TODO Remove after 2014-09-22 - * @hide - */ - public void removeCallback(Callback callback) { - unregisterCallback(callback); - } } diff --git a/core/java/android/content/pm/PackageItemInfo.java b/core/java/android/content/pm/PackageItemInfo.java index cacdf8e..22a899c 100644 --- a/core/java/android/content/pm/PackageItemInfo.java +++ b/core/java/android/content/pm/PackageItemInfo.java @@ -138,7 +138,7 @@ public class PackageItemInfo { } return packageName; } - + /** * Retrieve the current graphical icon associated with this item. This * will call back on the given PackageManager to load the icon from @@ -156,6 +156,23 @@ public class PackageItemInfo { } /** + * Retrieve the current graphical icon associated with this item without + * the addition of a work badge if applicable. + * This will call back on the given PackageManager to load the icon from + * the application. + * + * @param pm A PackageManager from which the icon can be loaded; usually + * the PackageManager from which you originally retrieved this item. + * + * @return Returns a Drawable containing the item's icon. If the + * item does not have an icon, the item's default icon is returned + * such as the default activity icon. + */ + public Drawable loadUnbadgedIcon(PackageManager pm) { + return pm.loadUnbadgedItemIcon(this, getApplicationInfo()); + } + + /** * Retrieve the current graphical banner associated with this item. This * will call back on the given PackageManager to load the banner from * the application. diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index edd0d2a0..e9f7c50 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -3918,6 +3918,11 @@ public abstract class PackageManager { */ public abstract Drawable loadItemIcon(PackageItemInfo itemInfo, ApplicationInfo appInfo); + /** + * @hide + */ + public abstract Drawable loadUnbadgedItemIcon(PackageItemInfo itemInfo, ApplicationInfo appInfo); + /** {@hide} */ public abstract boolean isPackageAvailable(String packageName); diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index ca4ff6a..8515520 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -74,7 +74,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; -import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; @@ -380,7 +379,7 @@ public class PackageParser { */ public static PackageInfo generatePackageInfo(PackageParser.Package p, int gids[], int flags, long firstInstallTime, long lastUpdateTime, - HashSet<String> grantedPermissions, PackageUserState state) { + ArraySet<String> grantedPermissions, PackageUserState state) { return generatePackageInfo(p, gids, flags, firstInstallTime, lastUpdateTime, grantedPermissions, state, UserHandle.getCallingUserId()); @@ -401,7 +400,7 @@ public class PackageParser { public static PackageInfo generatePackageInfo(PackageParser.Package p, int gids[], int flags, long firstInstallTime, long lastUpdateTime, - HashSet<String> grantedPermissions, PackageUserState state, int userId) { + ArraySet<String> grantedPermissions, PackageUserState state, int userId) { if (!checkUseInstalledOrHidden(flags, state)) { return null; diff --git a/core/java/android/content/pm/PackageUserState.java b/core/java/android/content/pm/PackageUserState.java index 4dcad6f..a9c7be3 100644 --- a/core/java/android/content/pm/PackageUserState.java +++ b/core/java/android/content/pm/PackageUserState.java @@ -18,7 +18,7 @@ package android.content.pm; import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; -import java.util.HashSet; +import android.util.ArraySet; /** * Per-user state information about a package. @@ -34,8 +34,8 @@ public class PackageUserState { public String lastDisableAppCaller; - public HashSet<String> disabledComponents; - public HashSet<String> enabledComponents; + public ArraySet<String> disabledComponents; + public ArraySet<String> enabledComponents; public PackageUserState() { installed = true; @@ -51,9 +51,9 @@ public class PackageUserState { hidden = o.hidden; lastDisableAppCaller = o.lastDisableAppCaller; disabledComponents = o.disabledComponents != null - ? new HashSet<String>(o.disabledComponents) : null; + ? new ArraySet<String>(o.disabledComponents) : null; enabledComponents = o.enabledComponents != null - ? new HashSet<String>(o.enabledComponents) : null; + ? new ArraySet<String>(o.enabledComponents) : null; blockUninstall = o.blockUninstall; } -}
\ No newline at end of file +} diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java index 27bbb24..14af584 100644 --- a/core/java/android/content/res/Configuration.java +++ b/core/java/android/content/res/Configuration.java @@ -1365,13 +1365,13 @@ public final class Configuration implements Parcelable, Comparable<Configuration ArrayList<String> parts = new ArrayList<String>(); if (config.mcc != 0) { - parts.add(config.mcc + "mcc"); + parts.add("mcc" + config.mcc); if (config.mnc != 0) { - parts.add(config.mnc + "mnc"); + parts.add("mnc" + config.mnc); } } - if (!config.locale.getLanguage().isEmpty()) { + if (config.locale != null && !config.locale.getLanguage().isEmpty()) { parts.add(localeToResourceQualifier(config.locale)); } diff --git a/core/java/android/content/res/ConfigurationBoundResourceCache.java b/core/java/android/content/res/ConfigurationBoundResourceCache.java new file mode 100644 index 0000000..cde7e84 --- /dev/null +++ b/core/java/android/content/res/ConfigurationBoundResourceCache.java @@ -0,0 +1,138 @@ +/* +* Copyright (C) 2014 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package android.content.res; + +import android.util.ArrayMap; +import android.util.LongSparseArray; +import java.lang.ref.WeakReference; + +/** + * A Cache class which can be used to cache resource objects that are easy to clone but more + * expensive to inflate. + * @hide + */ +public class ConfigurationBoundResourceCache<T> { + + private final ArrayMap<String, LongSparseArray<WeakReference<ConstantState<T>>>> mCache = + new ArrayMap<String, LongSparseArray<WeakReference<ConstantState<T>>>>(); + + final Resources mResources; + + /** + * Creates a Resource cache for the given Resources instance. + * + * @param resources The Resource which can be used when creating new instances. + */ + public ConfigurationBoundResourceCache(Resources resources) { + mResources = resources; + } + + /** + * Adds a new item to the cache. + * + * @param key A custom key that uniquely identifies the resource. + * @param theme The Theme instance where this resource was loaded. + * @param constantState The constant state that can create new instances of the resource. + * + */ + public void put(long key, Resources.Theme theme, ConstantState<T> constantState) { + if (constantState == null) { + return; + } + final String themeKey = theme == null ? "" : theme.getKey(); + LongSparseArray<WeakReference<ConstantState<T>>> themedCache; + synchronized (this) { + themedCache = mCache.get(themeKey); + if (themedCache == null) { + themedCache = new LongSparseArray<WeakReference<ConstantState<T>>>(1); + mCache.put(themeKey, themedCache); + } + themedCache.put(key, new WeakReference<ConstantState<T>>(constantState)); + } + } + + /** + * If the resource is cached, creates a new instance of it and returns. + * + * @param key The long key which can be used to uniquely identify the resource. + * @param theme The The Theme instance where we want to load this resource. + * + * @return If this resources was loaded before, returns a new instance of it. Otherwise, returns + * null. + */ + public T get(long key, Resources.Theme theme) { + final String themeKey = theme != null ? theme.getKey() : ""; + final LongSparseArray<WeakReference<ConstantState<T>>> themedCache; + final WeakReference<ConstantState<T>> wr; + synchronized (this) { + themedCache = mCache.get(themeKey); + if (themedCache == null) { + return null; + } + wr = themedCache.get(key); + } + if (wr == null) { + return null; + } + final ConstantState entry = wr.get(); + if (entry != null) { + return (T) entry.newInstance(mResources, theme); + } else { // our entry has been purged + synchronized (this) { + // there is a potential race condition here where this entry may be put in + // another thread. But we prefer it to minimize lock duration + themedCache.delete(key); + } + } + return null; + } + + /** + * Users of ConfigurationBoundResourceCache must call this method whenever a configuration + * change happens. On this callback, the cache invalidates all resources that are not valid + * anymore. + * + * @param configChanges The configuration changes + */ + public void onConfigurationChange(final int configChanges) { + synchronized (this) { + final int size = mCache.size(); + for (int i = size - 1; i >= 0; i--) { + final LongSparseArray<WeakReference<ConstantState<T>>> + themeCache = mCache.valueAt(i); + onConfigurationChangeInt(themeCache, configChanges); + if (themeCache.size() == 0) { + mCache.removeAt(i); + } + } + } + } + + private void onConfigurationChangeInt( + final LongSparseArray<WeakReference<ConstantState<T>>> themeCache, + final int configChanges) { + final int size = themeCache.size(); + for (int i = size - 1; i >= 0; i--) { + final WeakReference<ConstantState<T>> wr = themeCache.valueAt(i); + final ConstantState<T> constantState = wr.get(); + if (constantState == null || Configuration.needNewResources( + configChanges, constantState.getChangingConfigurations())) { + themeCache.removeAt(i); + } + } + } + +} diff --git a/core/java/android/content/res/ConstantState.java b/core/java/android/content/res/ConstantState.java new file mode 100644 index 0000000..ee609df --- /dev/null +++ b/core/java/android/content/res/ConstantState.java @@ -0,0 +1,61 @@ +/* +* Copyright (C) 2014 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package android.content.res; + +/** + * A cache class that can provide new instances of a particular resource which may change + * depending on the current {@link Resources.Theme} or {@link Configuration}. + * <p> + * A constant state should be able to return a bitmask of changing configurations, which + * identifies the type of configuration changes that may invalidate this resource. These + * configuration changes can be obtained from {@link android.util.TypedValue}. Entities such as + * {@link android.animation.Animator} also provide a changing configuration method to include + * their dependencies (e.g. An AnimatorSet's changing configuration is the union of the + * changing configurations of each Animator in the set) + * @hide + */ +abstract public class ConstantState<T> { + + /** + * Return a bit mask of configuration changes that will impact + * this resource (and thus require completely reloading it). + */ + abstract public int getChangingConfigurations(); + + /** + * Create a new instance without supplying resources the caller + * is running in. + */ + public abstract T newInstance(); + + /** + * Create a new instance from its constant state. This + * must be implemented for resources that change based on the target + * density of their caller (that is depending on whether it is + * in compatibility mode). + */ + public T newInstance(Resources res) { + return newInstance(); + } + + /** + * Create a new instance from its constant state. This must be + * implemented for resources that can have a theme applied. + */ + public T newInstance(Resources res, Resources.Theme theme) { + return newInstance(res); + } +} diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index 7f276c2..0145e05 100644 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -16,6 +16,9 @@ package android.content.res; +import android.animation.Animator; +import android.animation.StateListAnimator; +import android.annotation.NonNull; import android.util.Pools.SynchronizedPool; import android.view.ViewDebug; import com.android.internal.util.XmlUtils; @@ -115,6 +118,10 @@ public class Resources { new ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>>(); private final LongSparseArray<WeakReference<ColorStateList>> mColorStateListCache = new LongSparseArray<WeakReference<ColorStateList>>(); + private final ConfigurationBoundResourceCache<Animator> mAnimatorCache = + new ConfigurationBoundResourceCache<Animator>(this); + private final ConfigurationBoundResourceCache<StateListAnimator> mStateListAnimatorCache = + new ConfigurationBoundResourceCache<StateListAnimator>(this); private TypedValue mTmpValue = new TypedValue(); private boolean mPreloading; @@ -183,6 +190,24 @@ public class Resources { } /** + * Used by AnimatorInflater. + * + * @hide + */ + public ConfigurationBoundResourceCache<Animator> getAnimatorCache() { + return mAnimatorCache; + } + + /** + * Used by AnimatorInflater. + * + * @hide + */ + public ConfigurationBoundResourceCache<StateListAnimator> getStateListAnimatorCache() { + return mStateListAnimatorCache; + } + + /** * This exception is thrown by the resource APIs when a requested resource * can not be found. */ @@ -1524,20 +1549,21 @@ public class Resources { * contents of the typed array are ultimately filled in by * {@link Resources#getValue}. * - * @param values The base set of attribute values, must be equal - * in length to {@code attrs} or {@code null}. All values - * must be of type {@link TypedValue#TYPE_ATTRIBUTE}. + * @param values The base set of attribute values, must be equal in + * length to {@code attrs}. All values must be of type + * {@link TypedValue#TYPE_ATTRIBUTE}. * @param attrs The desired attributes to be retrieved. * @return Returns a TypedArray holding an array of the attribute * values. Be sure to call {@link TypedArray#recycle()} * when done with it. * @hide */ - public TypedArray resolveAttributes(int[] values, int[] attrs) { + @NonNull + public TypedArray resolveAttributes(@NonNull int[] values, @NonNull int[] attrs) { final int len = attrs.length; - if (values != null && len != values.length) { + if (values == null || len != values.length) { throw new IllegalArgumentException( - "Base attribute values must be null or the same length as attrs"); + "Base attribute values must the same length as attrs"); } final TypedArray array = TypedArray.obtain(Resources.this, len); @@ -1761,23 +1787,7 @@ public class Resources { // the framework. mCompatibilityInfo.applyToDisplayMetrics(mMetrics); - int configChanges = 0xfffffff; - if (config != null) { - mTmpConfig.setTo(config); - int density = config.densityDpi; - if (density == Configuration.DENSITY_DPI_UNDEFINED) { - density = mMetrics.noncompatDensityDpi; - } - - mCompatibilityInfo.applyToConfiguration(density, mTmpConfig); - - if (mTmpConfig.locale == null) { - mTmpConfig.locale = Locale.getDefault(); - mTmpConfig.setLayoutDirection(mTmpConfig.locale); - } - configChanges = mConfiguration.updateFrom(mTmpConfig); - configChanges = ActivityInfo.activityInfoConfigToNative(configChanges); - } + int configChanges = calcConfigChanges(config); if (mConfiguration.locale == null) { mConfiguration.locale = Locale.getDefault(); mConfiguration.setLayoutDirection(mConfiguration.locale); @@ -1825,6 +1835,8 @@ public class Resources { clearDrawableCachesLocked(mDrawableCache, configChanges); clearDrawableCachesLocked(mColorDrawableCache, configChanges); + mAnimatorCache.onConfigurationChange(configChanges); + mStateListAnimatorCache.onConfigurationChange(configChanges); mColorStateListCache.clear(); @@ -1837,6 +1849,30 @@ public class Resources { } } + /** + * Called by ConfigurationBoundResourceCacheTest via reflection. + */ + private int calcConfigChanges(Configuration config) { + int configChanges = 0xfffffff; + if (config != null) { + mTmpConfig.setTo(config); + int density = config.densityDpi; + if (density == Configuration.DENSITY_DPI_UNDEFINED) { + density = mMetrics.noncompatDensityDpi; + } + + mCompatibilityInfo.applyToConfiguration(density, mTmpConfig); + + if (mTmpConfig.locale == null) { + mTmpConfig.locale = Locale.getDefault(); + mTmpConfig.setLayoutDirection(mTmpConfig.locale); + } + configChanges = mConfiguration.updateFrom(mTmpConfig); + configChanges = ActivityInfo.activityInfoConfigToNative(configChanges); + } + return configChanges; + } + private void clearDrawableCachesLocked( ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> caches, int configChanges) { @@ -2323,7 +2359,14 @@ public class Resources { final Drawable dr; if (cs != null) { - dr = cs.newDrawable(this, theme); + final Drawable clonedDr = cs.newDrawable(this); + if (theme != null) { + dr = clonedDr.mutate(); + dr.applyTheme(theme); + dr.clearMutated(); + } else { + dr = clonedDr; + } } else if (isColorDrawable) { dr = new ColorDrawable(value.data); } else { diff --git a/core/java/android/content/res/TypedArray.java b/core/java/android/content/res/TypedArray.java index 73b93c6..02602fb 100644 --- a/core/java/android/content/res/TypedArray.java +++ b/core/java/android/content/res/TypedArray.java @@ -807,6 +807,9 @@ public class TypedArray { /** * Determines whether there is an attribute at <var>index</var>. + * <p> + * <strong>Note:</strong> If the attribute was set to {@code @empty} or + * {@code @undefined}, this method returns {@code false}. * * @param index Index of attribute to retrieve. * @@ -824,6 +827,27 @@ public class TypedArray { } /** + * Determines whether there is an attribute at <var>index</var>, returning + * {@code true} if the attribute was explicitly set to {@code @empty} and + * {@code false} only if the attribute was undefined. + * + * @param index Index of attribute to retrieve. + * + * @return True if the attribute has a value or is empty, false otherwise. + */ + public boolean hasValueOrEmpty(int index) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + + index *= AssetManager.STYLE_NUM_ENTRIES; + final int[] data = mData; + final int type = data[index+AssetManager.STYLE_TYPE]; + return type != TypedValue.TYPE_NULL + || data[index+AssetManager.STYLE_DATA] == TypedValue.DATA_NULL_EMPTY; + } + + /** * Retrieve the raw TypedValue for the attribute at <var>index</var> * and return a temporary object holding its data. This object is only * valid until the next call on to {@link TypedArray}. diff --git a/core/java/android/hardware/Sensor.java b/core/java/android/hardware/Sensor.java index f514e42..cf6a779 100644 --- a/core/java/android/hardware/Sensor.java +++ b/core/java/android/hardware/Sensor.java @@ -329,7 +329,11 @@ public final class Sensor { * A sensor of this type triggers an event each time a step is taken by the user. The only * allowed value to return is 1.0 and an event is generated for each step. Like with any other * event, the timestamp indicates when the event (here the step) occurred, this corresponds to - * when the foot hit the ground, generating a high variation in acceleration. + * when the foot hit the ground, generating a high variation in acceleration. This sensor is + * only for detecting every individual step as soon as it is taken, for example to perform dead + * reckoning. If you only need aggregate number of steps taken over a period of time, register + * for {@link #TYPE_STEP_COUNTER} instead. It is defined as a + * {@link Sensor#REPORTING_MODE_SPECIAL_TRIGGER} sensor. * <p> * See {@link android.hardware.SensorEvent#values SensorEvent.values} for more details. */ @@ -349,7 +353,12 @@ public final class Sensor { * while activated. The value is returned as a float (with the fractional part set to zero) and * is reset to zero only on a system reboot. The timestamp of the event is set to the time when * the last step for that event was taken. This sensor is implemented in hardware and is - * expected to be low power. + * expected to be low power. If you want to continuously track the number of steps over a long + * period of time, do NOT unregister for this sensor, so that it keeps counting steps in the + * background even when the AP is in suspend mode and report the aggregate count when the AP + * is awake. Application needs to stay registered for this sensor because step counter does not + * count steps if it is not activated. This sensor is ideal for fitness tracking applications. + * It is defined as an {@link Sensor#REPORTING_MODE_ON_CHANGE} sensor. * <p> * See {@link android.hardware.SensorEvent#values SensorEvent.values} for more details. */ @@ -750,31 +759,41 @@ public final class Sensor { } /** - * Returns whether this sensor is a wake-up sensor. + * Returns true if the sensor is a wake-up sensor. * <p> - * Wake up sensors wake the application processor up when they have events to deliver. When a - * wake up sensor is registered to without batching enabled, each event will wake the - * application processor up. - * <p> - * When a wake up sensor is registered to with batching enabled, it - * wakes the application processor up when maxReportingLatency has elapsed or when the hardware - * FIFO storing the events from wake up sensors is getting full. - * <p> - * Non-wake up sensors never wake the application processor up. Their events are only reported - * when the application processor is awake, for example because the application holds a wake - * lock, or another source woke the application processor up. + * <b>Application Processor Power modes</b> <p> + * Application Processor(AP), is the processor on which applications run. When no wake lock is held + * and the user is not interacting with the device, this processor can enter a “Suspend” mode, + * reducing the power consumption by 10 times or more. + * </p> * <p> - * When a non-wake up sensor is registered to without batching enabled, the measurements made - * while the application processor is asleep might be lost and never returned. + * <b>Non-wake-up sensors</b> <p> + * Non-wake-up sensors are sensors that do not wake the AP out of suspend to report data. While + * the AP is in suspend mode, the sensors continue to function and generate events, which are + * put in a hardware FIFO. The events in the FIFO are delivered to the application when the AP + * wakes up. If the FIFO was too small to store all events generated while the AP was in + * suspend mode, the older events are lost: the oldest data is dropped to accommodate the newer + * data. In the extreme case where the FIFO is non-existent {@code maxFifoEventCount() == 0}, + * all events generated while the AP was in suspend mode are lost. Applications using + * non-wake-up sensors should usually: + * <ul> + * <li>Either unregister from the sensors when they do not need them, usually in the activity’s + * {@code onPause} method. This is the most common case. + * <li>Or realize that the sensors are consuming some power while the AP is in suspend mode and + * that even then, some events might be lost. + * </ul> + * </p> * <p> - * When a non-wake up sensor is registered to with batching enabled, the measurements made while - * the application processor is asleep are stored in the hardware FIFO for non-wake up sensors. - * When this FIFO gets full, new events start overwriting older events. When the application - * then wakes up, the latest events are returned, and some old events might be lost. The number - * of events actually returned depends on the hardware FIFO size, as well as on what other - * sensors are activated. If losing sensor events is not acceptable during batching, you must - * use the wake-up version of the sensor. - * @return true if this is a wake up sensor, false otherwise. + * <b>Wake-up sensors</b> <p> + * In opposition to non-wake-up sensors, wake-up sensors ensure that their data is delivered + * independently of the state of the AP. While the AP is awake, the wake-up sensors behave + * like non-wake-up-sensors. When the AP is asleep, wake-up sensors wake up the AP to deliver + * events. That is, the AP will wake up and the sensor will deliver the events before the + * maximum reporting latency is elapsed or the hardware FIFO gets full. See {@link + * SensorManager#registerListener(SensorEventListener, Sensor, int, int)} for more details. + * </p> + * + * @return <code>true</code> if this is a wake-up sensor, <code>false</code> otherwise. */ public boolean isWakeUpSensor() { return (mFlags & SENSOR_FLAG_WAKE_UP_SENSOR) != 0; diff --git a/core/java/android/hardware/SensorEventListener2.java b/core/java/android/hardware/SensorEventListener2.java index 70eff08..fd3e62b 100644 --- a/core/java/android/hardware/SensorEventListener2.java +++ b/core/java/android/hardware/SensorEventListener2.java @@ -21,15 +21,16 @@ package android.hardware; */ public interface SensorEventListener2 extends SensorEventListener { /** - * Called after flush() is completed. All the events in the batch at the point when - * the flush was called have been delivered to the applications registered for those - * sensor events. Flush Complete Events are sent ONLY to the application that has - * explicitly called flush(). If the hardware FIFO is flushed due to some other - * application calling flush(), flush complete event is not delivered to this application. + * Called after flush() is completed. All the events in the batch at the point when the flush + * was called have been delivered to the applications registered for those sensor events. In + * {@link android.os.Build.VERSION_CODES#KITKAT}, applications may receive flush complete events + * even if some other application has called flush() on the same sensor. Starting with + * {@link android.os.Build.VERSION_CODES#LOLLIPOP}, flush Complete events are sent ONLY to the + * application that has explicitly called flush(). If the hardware FIFO is flushed due to some + * other application calling flush(), flush complete event is not delivered to this application. * <p> * * @param sensor The {@link android.hardware.Sensor Sensor} on which flush was called. - * * @see android.hardware.SensorManager#flush(SensorEventListener) */ public void onFlushCompleted(Sensor sensor); diff --git a/core/java/android/hardware/SensorManager.java b/core/java/android/hardware/SensorManager.java index cccd624..e4e5a8c 100644 --- a/core/java/android/hardware/SensorManager.java +++ b/core/java/android/hardware/SensorManager.java @@ -626,73 +626,90 @@ public abstract class SensorManager { protected abstract void unregisterListenerImpl(SensorEventListener listener, Sensor sensor); /** - * Registers a {@link android.hardware.SensorEventListener - * SensorEventListener} for the given sensor. - * - * <p class="note"></p> - * Note: Don't use this method with a one shot trigger sensor such as - * {@link Sensor#TYPE_SIGNIFICANT_MOTION}. - * Use {@link #requestTriggerSensor(TriggerEventListener, Sensor)} instead. + * Registers a {@link android.hardware.SensorEventListener SensorEventListener} for the given + * sensor at the given sampling frequency. + * <p> + * The events will be delivered to the provided {@code SensorEventListener} as soon as they are + * available. To reduce the power consumption, applications can use + * {@link #registerListener(SensorEventListener, Sensor, int, int)} instead and specify a + * positive non-zero maximum reporting latency. + * </p> + * <p> + * In the case of non-wake-up sensors, the events are only delivered while the Application + * Processor (AP) is not in suspend mode. See {@link Sensor#isWakeUpSensor()} for more details. + * To ensure delivery of events from non-wake-up sensors even when the screen is OFF, the + * application registering to the sensor must hold a partial wake-lock to keep the AP awake, + * otherwise some events might be lost while the AP is asleep. Note that although events might + * be lost while the AP is asleep, the sensor will still consume power if it is not explicitly + * deactivated by the application. Applications must unregister their {@code + * SensorEventListener}s in their activity's {@code onPause()} method to avoid consuming power + * while the device is inactive. See {@link #registerListener(SensorEventListener, Sensor, int, + * int)} for more details on hardware FIFO (queueing) capabilities and when some sensor events + * might be lost. + * </p> + * <p> + * In the case of wake-up sensors, each event generated by the sensor will cause the AP to + * wake-up, ensuring that each event can be delivered. Because of this, registering to a wake-up + * sensor has very significant power implications. Call {@link Sensor#isWakeUpSensor()} to check + * whether a sensor is a wake-up sensor. See + * {@link #registerListener(SensorEventListener, Sensor, int, int)} for information on how to + * reduce the power impact of registering to wake-up sensors. + * </p> + * <p class="note"> + * Note: Don't use this method with one-shot trigger sensors such as + * {@link Sensor#TYPE_SIGNIFICANT_MOTION}. Use + * {@link #requestTriggerSensor(TriggerEventListener, Sensor)} instead. Use + * {@link Sensor#getReportingMode()} to obtain the reporting mode of a given sensor. * </p> * - * @param listener - * A {@link android.hardware.SensorEventListener SensorEventListener} - * object. - * - * @param sensor - * The {@link android.hardware.Sensor Sensor} to register to. - * - * @param rateUs - * The rate {@link android.hardware.SensorEvent sensor events} are - * delivered at. This is only a hint to the system. Events may be - * received faster or slower than the specified rate. Usually events - * are received faster. The value must be one of - * {@link #SENSOR_DELAY_NORMAL}, {@link #SENSOR_DELAY_UI}, - * {@link #SENSOR_DELAY_GAME}, or {@link #SENSOR_DELAY_FASTEST} - * or, the desired delay between events in microseconds. - * Specifying the delay in microseconds only works from Android - * 2.3 (API level 9) onwards. For earlier releases, you must use - * one of the {@code SENSOR_DELAY_*} constants. - * - * @return <code>true</code> if the sensor is supported and successfully - * enabled. - * + * @param listener A {@link android.hardware.SensorEventListener SensorEventListener} object. + * @param sensor The {@link android.hardware.Sensor Sensor} to register to. + * @param samplingPeriodUs The rate {@link android.hardware.SensorEvent sensor events} are + * delivered at. This is only a hint to the system. Events may be received faster or + * slower than the specified rate. Usually events are received faster. The value must + * be one of {@link #SENSOR_DELAY_NORMAL}, {@link #SENSOR_DELAY_UI}, + * {@link #SENSOR_DELAY_GAME}, or {@link #SENSOR_DELAY_FASTEST} or, the desired delay + * between events in microseconds. Specifying the delay in microseconds only works + * from Android 2.3 (API level 9) onwards. For earlier releases, you must use one of + * the {@code SENSOR_DELAY_*} constants. + * @return <code>true</code> if the sensor is supported and successfully enabled. * @see #registerListener(SensorEventListener, Sensor, int, Handler) * @see #unregisterListener(SensorEventListener) * @see #unregisterListener(SensorEventListener, Sensor) - * */ - public boolean registerListener(SensorEventListener listener, Sensor sensor, int rateUs) { - return registerListener(listener, sensor, rateUs, null); + public boolean registerListener(SensorEventListener listener, Sensor sensor, + int samplingPeriodUs) { + return registerListener(listener, sensor, samplingPeriodUs, null); } /** - * Enables batch mode for a sensor with the given rate and maxBatchReportLatency. If the - * underlying hardware does not support batch mode, this defaults to - * {@link #registerListener(SensorEventListener, Sensor, int)} and other parameters are - * ignored. In non-batch mode, all sensor events must be reported as soon as they are detected. - * While in batch mode, sensor events do not need to be reported as soon as they are detected. - * They can be temporarily stored in batches and reported in batches, as long as no event is - * delayed by more than "maxBatchReportLatency" microseconds. That is, all events since the - * previous batch are recorded and returned all at once. This allows to reduce the amount of - * interrupts sent to the SoC, and allows the SoC to switch to a lower power state (Idle) while - * the sensor is capturing and batching data. - * <p> - * Registering to a sensor in batch mode will not prevent the SoC from going to suspend mode. In - * this case, the sensor will continue to gather events and store it in a hardware FIFO. If the - * FIFO gets full before the AP wakes up again, some events will be lost, as the older events - * get overwritten by new events in the hardware FIFO. This can be avoided by holding a wake - * lock. If the application holds a wake lock, the SoC will not go to suspend mode, so no events - * will be lost, as the events will be reported before the FIFO gets full. - * </p> + * Registers a {@link android.hardware.SensorEventListener SensorEventListener} for the given + * sensor at the given sampling frequency and the given maximum reporting latency. * <p> - * Batching is always best effort. If a different application requests updates in continuous - * mode, this application will also get events in continuous mode. Batch mode updates can be - * unregistered by calling {@link #unregisterListener(SensorEventListener)}. + * This function is similar to {@link #registerListener(SensorEventListener, Sensor, int)} but + * it allows events to stay temporarily in the hardware FIFO (queue) before being delivered. The + * events can be stored in the hardware FIFO up to {@code maxReportLatencyUs} microseconds. Once + * one of the events in the FIFO needs to be reported, all of the events in the FIFO are + * reported sequentially. This means that some events will be reported before the maximum + * reporting latency has elapsed. + * </p><p> + * When {@code maxReportLatencyUs} is 0, the call is equivalent to a call to + * {@link #registerListener(SensorEventListener, Sensor, int)}, as it requires the events to be + * delivered as soon as possible. + * </p><p> + * When {@code sensor.maxFifoEventCount()} is 0, the sensor does not use a FIFO, so the call + * will also be equivalent to {@link #registerListener(SensorEventListener, Sensor, int)}. + * </p><p> + * Setting {@code maxReportLatencyUs} to a positive value allows to reduce the number of + * interrupts the AP (Application Processor) receives, hence reducing power consumption, as the + * AP can switch to a lower power state while the sensor is capturing the data. This is + * especially important when registering to wake-up sensors, for which each interrupt causes the + * AP to wake up if it was in suspend mode. See {@link Sensor#isWakeUpSensor()} for more + * information on wake-up sensors. * </p> * <p class="note"> * </p> - * Note: Don't use this method with a one shot trigger sensor such as + * Note: Don't use this method with one-shot trigger sensors such as * {@link Sensor#TYPE_SIGNIFICANT_MOTION}. Use * {@link #requestTriggerSensor(TriggerEventListener, Sensor)} instead. </p> * @@ -701,118 +718,104 @@ public abstract class SensorManager { * flush complete notifications, it should register with * {@link android.hardware.SensorEventListener SensorEventListener2} instead. * @param sensor The {@link android.hardware.Sensor Sensor} to register to. - * @param rateUs The desired delay between two consecutive events in microseconds. This is only - * a hint to the system. Events may be received faster or slower than the specified - * rate. Usually events are received faster. Can be one of + * @param samplingPeriodUs The desired delay between two consecutive events in microseconds. + * This is only a hint to the system. Events may be received faster or slower than + * the specified rate. Usually events are received faster. Can be one of * {@link #SENSOR_DELAY_NORMAL}, {@link #SENSOR_DELAY_UI}, * {@link #SENSOR_DELAY_GAME}, {@link #SENSOR_DELAY_FASTEST} or the delay in * microseconds. - * @param maxBatchReportLatencyUs An event in the batch can be delayed by at most - * maxBatchReportLatency microseconds. More events can be batched if this value is - * large. If this is set to zero, batch mode is disabled and events are delivered in - * continuous mode as soon as they are available which is equivalent to calling + * @param maxReportLatencyUs Maximum time in microseconds that events can be delayed before + * being reported to the application. A large value allows reducing the power + * consumption associated with the sensor. If maxReportLatencyUs is set to zero, + * events are delivered as soon as they are available, which is equivalent to calling * {@link #registerListener(SensorEventListener, Sensor, int)}. - * @return <code>true</code> if batch mode is successfully enabled for this sensor, - * <code>false</code> otherwise. + * @return <code>true</code> if the sensor is supported and successfully enabled. * @see #registerListener(SensorEventListener, Sensor, int) * @see #unregisterListener(SensorEventListener) * @see #flush(SensorEventListener) */ - public boolean registerListener(SensorEventListener listener, Sensor sensor, int rateUs, - int maxBatchReportLatencyUs) { - int delay = getDelay(rateUs); - return registerListenerImpl(listener, sensor, delay, null, maxBatchReportLatencyUs, 0); + public boolean registerListener(SensorEventListener listener, Sensor sensor, + int samplingPeriodUs, int maxReportLatencyUs) { + int delay = getDelay(samplingPeriodUs); + return registerListenerImpl(listener, sensor, delay, null, maxReportLatencyUs, 0); } /** * Registers a {@link android.hardware.SensorEventListener SensorEventListener} for the given * sensor. Events are delivered in continuous mode as soon as they are available. To reduce the - * battery usage, use {@link #registerListener(SensorEventListener, Sensor, int, int)} which - * enables batch mode for the sensor. - * - * <p class="note"></p> - * Note: Don't use this method with a one shot trigger sensor such as - * {@link Sensor#TYPE_SIGNIFICANT_MOTION}. - * Use {@link #requestTriggerSensor(TriggerEventListener, Sensor)} instead. + * power consumption, applications can use + * {@link #registerListener(SensorEventListener, Sensor, int, int)} instead and specify a + * positive non-zero maximum reporting latency. + * <p class="note"> * </p> + * Note: Don't use this method with a one shot trigger sensor such as + * {@link Sensor#TYPE_SIGNIFICANT_MOTION}. Use + * {@link #requestTriggerSensor(TriggerEventListener, Sensor)} instead. </p> * - * @param listener - * A {@link android.hardware.SensorEventListener SensorEventListener} - * object. - * - * @param sensor - * The {@link android.hardware.Sensor Sensor} to register to. - * - * @param rateUs - * The rate {@link android.hardware.SensorEvent sensor events} are - * delivered at. This is only a hint to the system. Events may be - * received faster or slower than the specified rate. Usually events - * are received faster. The value must be one of - * {@link #SENSOR_DELAY_NORMAL}, {@link #SENSOR_DELAY_UI}, - * {@link #SENSOR_DELAY_GAME}, or {@link #SENSOR_DELAY_FASTEST}. - * or, the desired delay between events in microseconds. - * Specifying the delay in microseconds only works from Android - * 2.3 (API level 9) onwards. For earlier releases, you must use - * one of the {@code SENSOR_DELAY_*} constants. - * - * @param handler - * The {@link android.os.Handler Handler} the - * {@link android.hardware.SensorEvent sensor events} will be - * delivered to. - * + * @param listener A {@link android.hardware.SensorEventListener SensorEventListener} object. + * @param sensor The {@link android.hardware.Sensor Sensor} to register to. + * @param samplingPeriodUs The rate {@link android.hardware.SensorEvent sensor events} are + * delivered at. This is only a hint to the system. Events may be received faster or + * slower than the specified rate. Usually events are received faster. The value must + * be one of {@link #SENSOR_DELAY_NORMAL}, {@link #SENSOR_DELAY_UI}, + * {@link #SENSOR_DELAY_GAME}, or {@link #SENSOR_DELAY_FASTEST} or, the desired + * delay between events in microseconds. Specifying the delay in microseconds only + * works from Android 2.3 (API level 9) onwards. For earlier releases, you must use + * one of the {@code SENSOR_DELAY_*} constants. + * @param handler The {@link android.os.Handler Handler} the {@link android.hardware.SensorEvent + * sensor events} will be delivered to. * @return <code>true</code> if the sensor is supported and successfully enabled. - * * @see #registerListener(SensorEventListener, Sensor, int) * @see #unregisterListener(SensorEventListener) * @see #unregisterListener(SensorEventListener, Sensor) */ - public boolean registerListener(SensorEventListener listener, Sensor sensor, int rateUs, - Handler handler) { - int delay = getDelay(rateUs); + public boolean registerListener(SensorEventListener listener, Sensor sensor, + int samplingPeriodUs, Handler handler) { + int delay = getDelay(samplingPeriodUs); return registerListenerImpl(listener, sensor, delay, handler, 0, 0); } /** - * Enables batch mode for a sensor with the given rate and maxBatchReportLatency. + * Registers a {@link android.hardware.SensorEventListener SensorEventListener} for the given + * sensor at the given sampling frequency and the given maximum reporting latency. + * * @param listener A {@link android.hardware.SensorEventListener SensorEventListener} object * that will receive the sensor events. If the application is interested in receiving * flush complete notifications, it should register with * {@link android.hardware.SensorEventListener SensorEventListener2} instead. * @param sensor The {@link android.hardware.Sensor Sensor} to register to. - * @param rateUs The desired delay between two consecutive events in microseconds. This is only - * a hint to the system. Events may be received faster or slower than the specified - * rate. Usually events are received faster. Can be one of + * @param samplingPeriodUs The desired delay between two consecutive events in microseconds. + * This is only a hint to the system. Events may be received faster or slower than + * the specified rate. Usually events are received faster. Can be one of * {@link #SENSOR_DELAY_NORMAL}, {@link #SENSOR_DELAY_UI}, * {@link #SENSOR_DELAY_GAME}, {@link #SENSOR_DELAY_FASTEST} or the delay in * microseconds. - * @param maxBatchReportLatencyUs An event in the batch can be delayed by at most - * maxBatchReportLatency microseconds. More events can be batched if this value is - * large. If this is set to zero, batch mode is disabled and events are delivered in - * continuous mode as soon as they are available which is equivalent to calling + * @param maxReportLatencyUs Maximum time in microseconds that events can be delayed before + * being reported to the application. A large value allows reducing the power + * consumption associated with the sensor. If maxReportLatencyUs is set to zero, + * events are delivered as soon as they are available, which is equivalent to calling * {@link #registerListener(SensorEventListener, Sensor, int)}. - * @param handler The {@link android.os.Handler Handler} the - * {@link android.hardware.SensorEvent sensor events} will be delivered to. - * - * @return <code>true</code> if batch mode is successfully enabled for this sensor, - * <code>false</code> otherwise. + * @param handler The {@link android.os.Handler Handler} the {@link android.hardware.SensorEvent + * sensor events} will be delivered to. + * @return <code>true</code> if the sensor is supported and successfully enabled. * @see #registerListener(SensorEventListener, Sensor, int, int) */ - public boolean registerListener(SensorEventListener listener, Sensor sensor, int rateUs, - int maxBatchReportLatencyUs, Handler handler) { - int delayUs = getDelay(rateUs); - return registerListenerImpl(listener, sensor, delayUs, handler, maxBatchReportLatencyUs, 0); + public boolean registerListener(SensorEventListener listener, Sensor sensor, int samplingPeriodUs, + int maxReportLatencyUs, Handler handler) { + int delayUs = getDelay(samplingPeriodUs); + return registerListenerImpl(listener, sensor, delayUs, handler, maxReportLatencyUs, 0); } /** @hide */ protected abstract boolean registerListenerImpl(SensorEventListener listener, Sensor sensor, - int delayUs, Handler handler, int maxBatchReportLatencyUs, int reservedFlags); + int delayUs, Handler handler, int maxReportLatencyUs, int reservedFlags); /** - * Flushes the batch FIFO of all the sensors registered for this listener. If there are events - * in the FIFO of the sensor, they are returned as if the batch timeout in the FIFO of the - * sensors had expired. Events are returned in the usual way through the SensorEventListener. - * This call doesn't affect the batch timeout for this sensor. This call is asynchronous and + * Flushes the FIFO of all the sensors registered for this listener. If there are events + * in the FIFO of the sensor, they are returned as if the maxReportLantecy of the FIFO has + * expired. Events are returned in the usual way through the SensorEventListener. + * This call doesn't affect the maxReportLantecy for this sensor. This call is asynchronous and * returns immediately. * {@link android.hardware.SensorEventListener2#onFlushCompleted onFlushCompleted} is called * after all the events in the batch at the time of calling this method have been delivered diff --git a/core/java/android/hardware/hdmi/HdmiClient.java b/core/java/android/hardware/hdmi/HdmiClient.java index aba90e4..45a79e1 100644 --- a/core/java/android/hardware/hdmi/HdmiClient.java +++ b/core/java/android/hardware/hdmi/HdmiClient.java @@ -3,13 +3,12 @@ package android.hardware.hdmi; import android.annotation.NonNull; import android.annotation.SystemApi; import android.hardware.hdmi.HdmiControlManager.VendorCommandListener; -import android.hardware.hdmi.IHdmiVendorCommandListener; import android.os.RemoteException; import android.util.Log; /** * Parent for classes of various HDMI-CEC device type used to access - * {@link HdmiControlService}. Contains methods and data used in common. + * the HDMI control system service. Contains methods and data used in common. * * @hide */ @@ -17,11 +16,13 @@ import android.util.Log; public abstract class HdmiClient { private static final String TAG = "HdmiClient"; - protected final IHdmiControlService mService; + /* package */ final IHdmiControlService mService; - protected abstract int getDeviceType(); + private IHdmiVendorCommandListener mIHdmiVendorCommandListener; - public HdmiClient(IHdmiControlService service) { + /* package */ abstract int getDeviceType(); + + /* package */ HdmiClient(IHdmiControlService service) { mService = service; } @@ -41,7 +42,7 @@ public abstract class HdmiClient { } /** - * Send a key event to other logical device. + * Sends a key event to other logical device. * * @param keyCode key code to send. Defined in {@link android.view.KeyEvent}. * @param isPressed true if this is key press event @@ -55,7 +56,7 @@ public abstract class HdmiClient { } /** - * Send vendor-specific command. + * Sends vendor-specific command. * * @param targetAddress address of the target device * @param params vendor-specific parameter. For <Vendor Command With ID> do not @@ -72,18 +73,23 @@ public abstract class HdmiClient { } /** - * Add a listener used to receive incoming vendor-specific command. + * Sets a listener used to receive incoming vendor-specific command. * * @param listener listener object */ - public void addVendorCommandListener(@NonNull VendorCommandListener listener) { + public void setVendorCommandListener(@NonNull VendorCommandListener listener) { if (listener == null) { throw new IllegalArgumentException("listener cannot be null"); } + if (mIHdmiVendorCommandListener != null) { + throw new IllegalStateException("listener was already set"); + } try { - mService.addVendorCommandListener(getListenerWrapper(listener), getDeviceType()); + IHdmiVendorCommandListener wrappedListener = getListenerWrapper(listener); + mService.addVendorCommandListener(wrappedListener, getDeviceType()); + mIHdmiVendorCommandListener = wrappedListener; } catch (RemoteException e) { - Log.e(TAG, "failed to add vendor command listener: ", e); + Log.e(TAG, "failed to set vendor command listener: ", e); } } @@ -91,8 +97,13 @@ public abstract class HdmiClient { final VendorCommandListener listener) { return new IHdmiVendorCommandListener.Stub() { @Override - public void onReceived(int srcAddress, byte[] params, boolean hasVendorId) { - listener.onReceived(srcAddress, params, hasVendorId); + public void onReceived(int srcAddress, int destAddress, byte[] params, + boolean hasVendorId) { + listener.onReceived(srcAddress, destAddress, params, hasVendorId); + } + @Override + public void onControlStateChanged(boolean enabled, int reason) { + listener.onControlStateChanged(enabled, reason); } }; } diff --git a/core/java/android/hardware/hdmi/HdmiControlManager.java b/core/java/android/hardware/hdmi/HdmiControlManager.java index 30f3576..308a219 100644 --- a/core/java/android/hardware/hdmi/HdmiControlManager.java +++ b/core/java/android/hardware/hdmi/HdmiControlManager.java @@ -21,6 +21,8 @@ import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.annotation.SystemApi; import android.os.RemoteException; +import android.util.ArrayMap; +import android.util.Log; /** * The {@link HdmiControlManager} class is used to send HDMI control messages @@ -36,6 +38,8 @@ import android.os.RemoteException; */ @SystemApi public final class HdmiControlManager { + private static final String TAG = "HdmiControlManager"; + @Nullable private final IHdmiControlService mService; /** @@ -56,7 +60,7 @@ public final class HdmiControlManager { /** * Message used by TV to receive volume status from Audio Receiver. It should check volume value - * that is retrieved from extra value with the key {@link #EXTRA_MESSAGE_EXTRAM_PARAM1}. If the + * that is retrieved from extra value with the key {@link #EXTRA_MESSAGE_EXTRA_PARAM1}. If the * value is in range of [0,100], it is current volume of Audio Receiver. And there is another * value, {@link #AVR_VOLUME_MUTED}, which is used to inform volume mute. */ @@ -71,7 +75,7 @@ public final class HdmiControlManager { * Used as an extra field in the intent {@link #ACTION_OSD_MESSAGE}. Contains the extra value * of the message. */ - public static final String EXTRA_MESSAGE_EXTRAM_PARAM1 = + public static final String EXTRA_MESSAGE_EXTRA_PARAM1 = "android.hardware.hdmi.extra.MESSAGE_EXTRA_PARAM1"; /** @@ -236,16 +240,24 @@ public final class HdmiControlManager { /** Clear timer error - CEC is disabled. */ public static final int CLEAR_TIMER_STATUS_CEC_DISABLE = 0xA2; + /** The HdmiControlService is started. */ + public static final int CONTROL_STATE_CHANGED_REASON_START = 0; + /** The state of HdmiControlService is changed by changing of settings. */ + public static final int CONTROL_STATE_CHANGED_REASON_SETTING = 1; + /** The HdmiControlService is enabled to wake up. */ + public static final int CONTROL_STATE_CHANGED_REASON_WAKEUP = 2; + /** The HdmiControlService will be disabled to standby. */ + public static final int CONTROL_STATE_CHANGED_REASON_STANDBY = 3; + // True if we have a logical device of type playback hosted in the system. private final boolean mHasPlaybackDevice; // True if we have a logical device of type TV hosted in the system. private final boolean mHasTvDevice; /** - * @hide - hide this constructor because it has a parameter of type - * IHdmiControlService, which is a system private class. The right way - * to create an instance of this class is using the factory - * Context.getSystemService. + * {@hide} - hide this constructor because it has a parameter of type IHdmiControlService, + * which is a system private class. The right way to create an instance of this class is + * using the factory Context.getSystemService. */ public HdmiControlManager(IHdmiControlService service) { mService = service; @@ -331,6 +343,9 @@ public final class HdmiControlManager { void onReceived(HdmiHotplugEvent event); } + private final ArrayMap<HotplugEventListener, IHdmiHotplugEventListener> + mHotplugEventListeners = new ArrayMap<>(); + /** * Listener used to get vendor-specific commands. */ @@ -339,11 +354,29 @@ public final class HdmiControlManager { * Called when a vendor command is received. * * @param srcAddress source logical address + * @param destAddress destination logical address * @param params vendor-specific parameters * @param hasVendorId {@code true} if the command is <Vendor Command * With ID>. The first 3 bytes of params is vendor id. */ - void onReceived(int srcAddress, byte[] params, boolean hasVendorId); + void onReceived(int srcAddress, int destAddress, byte[] params, boolean hasVendorId); + + /** + * The callback is called: + * <ul> + * <li> before HdmiControlService is disabled. + * <li> after HdmiControlService is enabled and the local address is assigned. + * </ul> + * The client shouldn't hold the thread too long since this is a blocking call. + * + * @param enabled {@code true} if HdmiControlService is enabled. + * @param reason the reason code why the state of HdmiControlService is changed. + * @see #CONTROL_STATE_CHANGED_REASON_START + * @see #CONTROL_STATE_CHANGED_REASON_SETTING + * @see #CONTROL_STATE_CHANGED_REASON_WAKEUP + * @see #CONTROL_STATE_CHANGED_REASON_STANDBY + */ + void onControlStateChanged(boolean enabled, int reason); } /** @@ -357,12 +390,19 @@ public final class HdmiControlManager { */ public void addHotplugEventListener(HotplugEventListener listener) { if (mService == null) { + Log.e(TAG, "HdmiControlService is not available"); + return; + } + if (mHotplugEventListeners.containsKey(listener)) { + Log.e(TAG, "listener is already registered"); return; } + IHdmiHotplugEventListener wrappedListener = getHotplugEventListenerWrapper(listener); + mHotplugEventListeners.put(listener, wrappedListener); try { - mService.addHotplugEventListener(getHotplugEventListenerWrapper(listener)); + mService.addHotplugEventListener(wrappedListener); } catch (RemoteException e) { - // Do nothing. + Log.e(TAG, "failed to add hotplug event listener: ", e); } } @@ -373,12 +413,18 @@ public final class HdmiControlManager { */ public void removeHotplugEventListener(HotplugEventListener listener) { if (mService == null) { + Log.e(TAG, "HdmiControlService is not available"); + return; + } + IHdmiHotplugEventListener wrappedListener = mHotplugEventListeners.remove(listener); + if (wrappedListener == null) { + Log.e(TAG, "tried to remove not-registered listener"); return; } try { - mService.removeHotplugEventListener(getHotplugEventListenerWrapper(listener)); + mService.removeHotplugEventListener(wrappedListener); } catch (RemoteException e) { - // Do nothing. + Log.e(TAG, "failed to remove hotplug event listener: ", e); } } diff --git a/core/java/android/hardware/hdmi/HdmiDeviceInfo.java b/core/java/android/hardware/hdmi/HdmiDeviceInfo.java index 7abea36..fe414e6 100644 --- a/core/java/android/hardware/hdmi/HdmiDeviceInfo.java +++ b/core/java/android/hardware/hdmi/HdmiDeviceInfo.java @@ -237,14 +237,14 @@ public class HdmiDeviceInfo implements Parcelable { } /** - * Return the id of the device. + * Returns the id of the device. */ public int getId() { return mId; } /** - * Return the id to be used for CEC device. + * Returns the id to be used for CEC device. * * @param address logical address of CEC device * @return id for CEC device @@ -255,7 +255,7 @@ public class HdmiDeviceInfo implements Parcelable { } /** - * Return the id to be used for MHL device. + * Returns the id to be used for MHL device. * * @param portId port which the MHL device is connected to * @return id for MHL device @@ -266,7 +266,7 @@ public class HdmiDeviceInfo implements Parcelable { } /** - * Return the id to be used for hardware port. + * Returns the id to be used for hardware port. * * @param portId port id * @return id for hardware port @@ -276,28 +276,28 @@ public class HdmiDeviceInfo implements Parcelable { } /** - * Return the CEC logical address of the device. + * Returns the CEC logical address of the device. */ public int getLogicalAddress() { return mLogicalAddress; } /** - * Return the physical address of the device. + * Returns the physical address of the device. */ public int getPhysicalAddress() { return mPhysicalAddress; } /** - * Return the port ID. + * Returns the port ID. */ public int getPortId() { return mPortId; } /** - * Return CEC type of the device. For more details, refer constants between {@link #DEVICE_TV} + * Returns CEC type of the device. For more details, refer constants between {@link #DEVICE_TV} * and {@link #DEVICE_INACTIVE}. */ public int getDeviceType() { @@ -305,7 +305,7 @@ public class HdmiDeviceInfo implements Parcelable { } /** - * Return device's power status. It should be one of the following values. + * Returns device's power status. It should be one of the following values. * <ul> * <li>{@link HdmiControlManager#POWER_STATUS_ON} * <li>{@link HdmiControlManager#POWER_STATUS_STANDBY} @@ -319,21 +319,21 @@ public class HdmiDeviceInfo implements Parcelable { } /** - * Return MHL device id. Return -1 for non-MHL device. + * Returns MHL device id. Return -1 for non-MHL device. */ public int getDeviceId() { return mDeviceId; } /** - * Return MHL adopter id. Return -1 for non-MHL device. + * Returns MHL adopter id. Return -1 for non-MHL device. */ public int getAdopterId() { return mAdopterId; } /** - * Return {@code true} if the device is of a type that can be an input source. + * Returns {@code true} if the device is of a type that can be an input source. */ public boolean isSourceType() { return mDeviceType == DEVICE_PLAYBACK @@ -342,7 +342,7 @@ public class HdmiDeviceInfo implements Parcelable { } /** - * Return {@code true} if the device represents an HDMI-CEC device. {@code false} if the device + * Returns {@code true} if the device represents an HDMI-CEC device. {@code false} if the device * is either MHL or other device. */ public boolean isCecDevice() { @@ -350,7 +350,7 @@ public class HdmiDeviceInfo implements Parcelable { } /** - * Return {@code true} if the device represents an MHL device. {@code false} if the device is + * Returns {@code true} if the device represents an MHL device. {@code false} if the device is * either CEC or other device. */ public boolean isMhlDevice() { @@ -358,14 +358,14 @@ public class HdmiDeviceInfo implements Parcelable { } /** - * Return display (OSD) name of the device. + * Returns display (OSD) name of the device. */ public String getDisplayName() { return mDisplayName; } /** - * Return vendor id of the device. Vendor id is used to distinguish devices built by other + * Returns vendor id of the device. Vendor id is used to distinguish devices built by other * manufactures. This is required for vendor-specific command on CEC standard. */ public int getVendorId() { @@ -373,7 +373,7 @@ public class HdmiDeviceInfo implements Parcelable { } /** - * Describe the kinds of special objects contained in this Parcelable's marshalled + * Describes the kinds of special objects contained in this Parcelable's marshalled * representation. */ @Override @@ -382,7 +382,7 @@ public class HdmiDeviceInfo implements Parcelable { } /** - * Serialize this object into a {@link Parcel}. + * Serializes this object into a {@link Parcel}. * * @param dest The Parcel in which the object should be written. * @param flags Additional flags about how the object should be written. May be 0 or diff --git a/core/java/android/hardware/hdmi/HdmiHotplugEvent.java b/core/java/android/hardware/hdmi/HdmiHotplugEvent.java index 7be4bc5..9476742 100644 --- a/core/java/android/hardware/hdmi/HdmiHotplugEvent.java +++ b/core/java/android/hardware/hdmi/HdmiHotplugEvent.java @@ -44,7 +44,7 @@ public final class HdmiHotplugEvent implements Parcelable { } /** - * Return the port number for which the event occurred. + * Returns the port number for which the event occurred. * * @return port number */ @@ -53,7 +53,7 @@ public final class HdmiHotplugEvent implements Parcelable { } /** - * Return the connection status associated with this event + * Returns the connection status associated with this event * * @return true if the device gets connected; otherwise false */ @@ -62,7 +62,7 @@ public final class HdmiHotplugEvent implements Parcelable { } /** - * Describe the kinds of special objects contained in this Parcelable's + * Describes the kinds of special objects contained in this Parcelable's * marshalled representation. */ @Override @@ -71,7 +71,7 @@ public final class HdmiHotplugEvent implements Parcelable { } /** - * Flatten this object in to a Parcel. + * Flattens this object in to a Parcel. * * @param dest The Parcel in which the object should be written. * @param flags Additional flags about how the object should be written. @@ -86,17 +86,19 @@ public final class HdmiHotplugEvent implements Parcelable { public static final Parcelable.Creator<HdmiHotplugEvent> CREATOR = new Parcelable.Creator<HdmiHotplugEvent>() { /** - * Rebuild a {@link HdmiHotplugEvent} previously stored with + * Rebuilds a {@link HdmiHotplugEvent} previously stored with * {@link Parcelable#writeToParcel(Parcel, int)}. * * @param p {@link HdmiHotplugEvent} object to read the Rating from * @return a new {@link HdmiHotplugEvent} created from the data in the parcel */ + @Override public HdmiHotplugEvent createFromParcel(Parcel p) { int port = p.readInt(); boolean connected = p.readByte() == 1; return new HdmiHotplugEvent(port, connected); } + @Override public HdmiHotplugEvent[] newArray(int size) { return new HdmiHotplugEvent[size]; } diff --git a/core/java/android/hardware/hdmi/HdmiPlaybackClient.java b/core/java/android/hardware/hdmi/HdmiPlaybackClient.java index 85ccb74..263d6b1 100644 --- a/core/java/android/hardware/hdmi/HdmiPlaybackClient.java +++ b/core/java/android/hardware/hdmi/HdmiPlaybackClient.java @@ -64,12 +64,12 @@ public final class HdmiPlaybackClient extends HdmiClient { public void onComplete(int status); } - HdmiPlaybackClient(IHdmiControlService service) { + /* package */ HdmiPlaybackClient(IHdmiControlService service) { super(service); } /** - * Perform the feature 'one touch play' from playback device to turn on display + * Performs the feature 'one touch play' from playback device to turn on display * and switch the input. * * @param callback {@link OneTouchPlayCallback} object to get informed @@ -90,7 +90,7 @@ public final class HdmiPlaybackClient extends HdmiClient { } /** - * Get the status of display device connected through HDMI bus. + * Gets the status of display device connected through HDMI bus. * * @param callback {@link DisplayStatusCallback} object to get informed * of the result diff --git a/core/java/android/hardware/hdmi/HdmiPortInfo.java b/core/java/android/hardware/hdmi/HdmiPortInfo.java index 2ec6126..e52baed 100644 --- a/core/java/android/hardware/hdmi/HdmiPortInfo.java +++ b/core/java/android/hardware/hdmi/HdmiPortInfo.java @@ -114,7 +114,7 @@ public final class HdmiPortInfo implements Parcelable { } /** - * Describe the kinds of special objects contained in this Parcelable's + * Describes the kinds of special objects contained in this Parcelable's * marshalled representation. */ @Override @@ -146,7 +146,7 @@ public final class HdmiPortInfo implements Parcelable { }; /** - * Serialize this object into a {@link Parcel}. + * Serializes this object into a {@link Parcel}. * * @param dest The Parcel in which the object should be written. * @param flags Additional flags about how the object should be written. diff --git a/core/java/android/hardware/hdmi/HdmiRecordListener.java b/core/java/android/hardware/hdmi/HdmiRecordListener.java index f6a348a..90b7768 100644 --- a/core/java/android/hardware/hdmi/HdmiRecordListener.java +++ b/core/java/android/hardware/hdmi/HdmiRecordListener.java @@ -25,7 +25,7 @@ import android.hardware.hdmi.HdmiRecordSources.RecordSource; */ @SystemApi public abstract class HdmiRecordListener { - protected HdmiRecordListener() {} + public HdmiRecordListener() {} /** * Called when TV received one touch record request from record device. The client of this @@ -34,11 +34,13 @@ public abstract class HdmiRecordListener { * @param recorderAddress * @return record source to be used for recording. Null if no device is available. */ - public abstract RecordSource getOneTouchRecordSource(int recorderAddress); + public abstract RecordSource onOneTouchRecordSourceRequested(int recorderAddress); /** * Called when one touch record is started or failed during initialization. * + * @param recorderAddress An address of recorder that reports result of one touch record + * request * @param result result code. For more details, please look at all constants starting with * "ONE_TOUCH_RECORD_". Only * {@link HdmiControlManager#ONE_TOUCH_RECORD_RECORDING_CURRENTLY_SELECTED_SOURCE}, @@ -47,15 +49,17 @@ public abstract class HdmiRecordListener { * {@link HdmiControlManager#ONE_TOUCH_RECORD_RECORDING_EXTERNAL_INPUT} mean normal * start of recording; otherwise, describes failure. */ - public void onOneTouchRecordResult(int result) { + public void onOneTouchRecordResult(int recorderAddress, int result) { } /** * Called when timer recording is started or failed during initialization. * + * @param recorderAddress An address of recorder that reports result of timer recording + * request * @param data timer status data. For more details, look at {@link TimerStatusData}. */ - public void onTimerRecordingResult(TimerStatusData data) { + public void onTimerRecordingResult(int recorderAddress, TimerStatusData data) { } /** @@ -230,6 +234,8 @@ public abstract class HdmiRecordListener { /** * Called when receiving result for clear timer recording request. * + * @param recorderAddress An address of recorder that reports result of clear timer recording + * request * @param result result of clear timer. It should be one of * {@link HdmiControlManager#CLEAR_TIMER_STATUS_TIMER_NOT_CLEARED_RECORDING} * {@link HdmiControlManager#CLEAR_TIMER_STATUS_TIMER_NOT_CLEARED_NO_MATCHING}, @@ -239,6 +245,6 @@ public abstract class HdmiRecordListener { * {@link HdmiControlManager#CLEAR_TIMER_STATUS_FAIL_TO_CLEAR_SELECTED_SOURCE}, * {@link HdmiControlManager#CLEAR_TIMER_STATUS_CEC_DISABLE}. */ - public void onClearTimerRecordingResult(int result) { + public void onClearTimerRecordingResult(int recorderAddress, int result) { } } diff --git a/core/java/android/hardware/hdmi/HdmiRecordSources.java b/core/java/android/hardware/hdmi/HdmiRecordSources.java index dcc41fa..922b8e7 100644 --- a/core/java/android/hardware/hdmi/HdmiRecordSources.java +++ b/core/java/android/hardware/hdmi/HdmiRecordSources.java @@ -55,23 +55,25 @@ public final class HdmiRecordSources { /** * Base class for each record source. + * @hide */ - static abstract class RecordSource { - protected final int mSourceType; - protected final int mExtraDataSize; + @SystemApi + public static abstract class RecordSource { + /* package */ final int mSourceType; + /* package */ final int mExtraDataSize; - protected RecordSource(int sourceType, int extraDataSize) { + /* package */ RecordSource(int sourceType, int extraDataSize) { mSourceType = sourceType; mExtraDataSize = extraDataSize; } - abstract int extraParamToByteArray(byte[] data, int index); + /* package */ abstract int extraParamToByteArray(byte[] data, int index); - final int getDataSize(boolean includeType) { + /* package */ final int getDataSize(boolean includeType) { return includeType ? mExtraDataSize + 1 : mExtraDataSize; } - final int toByteArray(boolean includeType, byte[] data, int index) { + /* package */ final int toByteArray(boolean includeType, byte[] data, int index) { if (includeType) { // 1 to 8 bytes (depends on source). // {[Record Source Type]} | @@ -92,7 +94,7 @@ public final class HdmiRecordSources { // ---- Own source ----------------------------------------------------------------------------- // --------------------------------------------------------------------------------------------- /** - * Create {@link OwnSource} of own source. + * Creates {@link OwnSource} of own source. */ public static OwnSource ofOwnSource() { return new OwnSource(); @@ -309,7 +311,7 @@ public final class HdmiRecordSources { */ public static final class DigitalChannelData implements DigitalServiceIdentification { /** Identifies the logical or virtual channel number of a service. */ - private ChannelIdentifier mChannelIdentifier; + private final ChannelIdentifier mChannelIdentifier; public static DigitalChannelData ofTwoNumbers(int majorNumber, int minorNumber) { return new DigitalChannelData( @@ -336,7 +338,7 @@ public final class HdmiRecordSources { } /** - * Create {@link DigitalServiceSource} with channel type. + * Creates {@link DigitalServiceSource} with channel type. * * @param broadcastSystem digital broadcast system. It should be one of * <ul> @@ -387,7 +389,7 @@ public final class HdmiRecordSources { } /** - * Create {@link DigitalServiceSource} of ARIB type. + * Creates {@link DigitalServiceSource} of ARIB type. * * @param aribType ARIB type. It should be one of * <ul> @@ -418,7 +420,7 @@ public final class HdmiRecordSources { } /** - * Create {@link DigitalServiceSource} of ATSC type. + * Creates {@link DigitalServiceSource} of ATSC type. * * @param atscType ATSC type. It should be one of * <ul> @@ -449,7 +451,7 @@ public final class HdmiRecordSources { } /** - * Create {@link DigitalServiceSource} of ATSC type. + * Creates {@link DigitalServiceSource} of ATSC type. * * @param dvbType DVB type. It should be one of * <ul> @@ -570,7 +572,7 @@ public final class HdmiRecordSources { public static final int BROADCAST_SYSTEM_PAL_OTHER_SYSTEM = 31; /** - * Create {@link AnalogueServiceSource} of analogue service. + * Creates {@link AnalogueServiceSource} of analogue service. * * @param broadcastType * @param frequency @@ -613,7 +615,7 @@ public final class HdmiRecordSources { */ @SystemApi public static final class AnalogueServiceSource extends RecordSource { - static final int EXTRA_DATA_SIZE = 4; + /* package */ static final int EXTRA_DATA_SIZE = 4; /** Indicates the Analogue broadcast type. */ private final int mBroadcastType; @@ -633,7 +635,7 @@ public final class HdmiRecordSources { } @Override - protected int extraParamToByteArray(byte[] data, int index) { + /* package */ int extraParamToByteArray(byte[] data, int index) { // [Analogue Broadcast Type] - 1 byte data[index] = (byte) mBroadcastType; // [Analogue Frequency] - 2 bytes @@ -649,7 +651,7 @@ public final class HdmiRecordSources { // ---- External plug data --------------------------------------------------------------------- // --------------------------------------------------------------------------------------------- /** - * Create {@link ExternalPlugData} of external plug type. + * Creates {@link ExternalPlugData} of external plug type. * * @param plugNumber plug number. It should be in range of [1, 255] * @hide @@ -693,7 +695,7 @@ public final class HdmiRecordSources { // ---- External physical address -------------------------------------------------------------- // --------------------------------------------------------------------------------------------- /** - * Create {@link ExternalPhysicalAddress} of external physical address. + * Creates {@link ExternalPhysicalAddress} of external physical address. * * @param physicalAddress * @hide @@ -752,7 +754,7 @@ public final class HdmiRecordSources { } /** - * Check the byte array of record source. + * Checks the byte array of record source. * @hide */ @SystemApi diff --git a/core/java/android/hardware/hdmi/HdmiTimerRecordSources.java b/core/java/android/hardware/hdmi/HdmiTimerRecordSources.java index 1780707..bf97375 100644 --- a/core/java/android/hardware/hdmi/HdmiTimerRecordSources.java +++ b/core/java/android/hardware/hdmi/HdmiTimerRecordSources.java @@ -67,7 +67,7 @@ public class HdmiTimerRecordSources { private HdmiTimerRecordSources() {} /** - * Create {@link TimerRecordSource} for digital source which is used for <Set Digital + * Creates {@link TimerRecordSource} for digital source which is used for <Set Digital * Timer>. * * @param timerInfo timer info used for timer recording @@ -82,7 +82,7 @@ public class HdmiTimerRecordSources { } /** - * Create {@link TimerRecordSource} for analogue source which is used for <Set Analogue + * Creates {@link TimerRecordSource} for analogue source which is used for <Set Analogue * Timer>. * * @param timerInfo timer info used for timer recording @@ -97,7 +97,7 @@ public class HdmiTimerRecordSources { } /** - * Create {@link TimerRecordSource} for external plug which is used for <Set External + * Creates {@link TimerRecordSource} for external plug which is used for <Set External * Timer>. * * @param timerInfo timer info used for timer recording @@ -112,7 +112,7 @@ public class HdmiTimerRecordSources { } /** - * Create {@link TimerRecordSource} for external physical address which is used for <Set + * Creates {@link TimerRecordSource} for external physical address which is used for <Set * External Timer>. * * @param timerInfo timer info used for timer recording @@ -140,7 +140,7 @@ public class HdmiTimerRecordSources { } /** - * Create {@link Duration} for time value. + * Creates {@link Duration} for time value. * * @param hour hour in range of [0, 23] * @param minute minute in range of [0, 60] @@ -162,7 +162,7 @@ public class HdmiTimerRecordSources { } /** - * Create {@link Duration} for duration value. + * Creates {@link Duration} for duration value. * * @param hour hour in range of [0, 99] * @param minute minute in range of [0, 59] @@ -184,21 +184,21 @@ public class HdmiTimerRecordSources { } private static class TimeUnit { - protected final int mHour; - protected final int mMinute; + /* package */ final int mHour; + /* package */ final int mMinute; - protected TimeUnit(int hour, int minute) { + /* package */ TimeUnit(int hour, int minute) { mHour = hour; mMinute = minute; } - protected int toByteArray(byte[] data, int index) { + /* package */ int toByteArray(byte[] data, int index) { data[index] = toBcdByte(mHour); data[index + 1] = toBcdByte(mMinute); return 2; } - protected static byte toBcdByte(int value) { + /* package */ static byte toBcdByte(int value) { int digitOfTen = (value / 10) % 10; int digitOfOne = value % 10; return (byte) ((digitOfTen << 4) | digitOfOne); @@ -247,7 +247,7 @@ public class HdmiTimerRecordSources { RECORDING_SEQUENCE_REPEAT_SATUREDAY); /** - * Create {@link TimerInfo} with the given information. + * Creates {@link TimerInfo} with the given information. * * @param dayOfMonth day of month * @param monthOfYear month of year @@ -426,7 +426,7 @@ public class HdmiTimerRecordSources { } /** - * Check the byte array of timer record source. + * Checks the byte array of timer record source. * @param sourcetype * @param recordSource * @hide diff --git a/core/java/android/hardware/hdmi/HdmiTvClient.java b/core/java/android/hardware/hdmi/HdmiTvClient.java index 9d92fd9..cef17dd 100644 --- a/core/java/android/hardware/hdmi/HdmiTvClient.java +++ b/core/java/android/hardware/hdmi/HdmiTvClient.java @@ -24,6 +24,9 @@ import android.util.Log; import libcore.util.EmptyArray; +import java.util.Collections; +import java.util.List; + /** * HdmiTvClient represents HDMI-CEC logical device of type TV in the Android system * which acts as TV/Display. It provides with methods that manage, interact with other @@ -40,13 +43,13 @@ public final class HdmiTvClient extends HdmiClient { */ public static final int VENDOR_DATA_SIZE = 16; - HdmiTvClient(IHdmiControlService service) { + /* package */ HdmiTvClient(IHdmiControlService service) { super(service); } // Factory method for HdmiTvClient. // Declared package-private. Accessed by HdmiControlManager only. - static HdmiTvClient create(IHdmiControlService service) { + /* package */ static HdmiTvClient create(IHdmiControlService service) { return new HdmiTvClient(service); } @@ -68,7 +71,7 @@ public final class HdmiTvClient extends HdmiClient { } /** - * Select a CEC logical device to be a new active source. + * Selects a CEC logical device to be a new active source. * * @param logicalAddress logical address of the device to select * @param callback callback to get the result with @@ -95,7 +98,7 @@ public final class HdmiTvClient extends HdmiClient { } /** - * Select a HDMI port to be a new route path. + * Selects a HDMI port to be a new route path. * * @param portId HDMI port to select * @param callback callback to get the result with @@ -125,7 +128,7 @@ public final class HdmiTvClient extends HdmiClient { } /** - * Set the listener used to get informed of the input change event. + * Sets the listener used to get informed of the input change event. * * @param listener listener object */ @@ -150,7 +153,22 @@ public final class HdmiTvClient extends HdmiClient { } /** - * Set system audio volume + * Returns all the CEC devices connected to TV. + * + * @return list of {@link HdmiDeviceInfo} for connected CEC devices. + * Empty list is returned if there is none. + */ + public List<HdmiDeviceInfo> getDeviceList() { + try { + return mService.getDeviceList(); + } catch (RemoteException e) { + Log.e("TAG", "Failed to call getDeviceList():", e); + return Collections.<HdmiDeviceInfo>emptyList(); + } + } + + /** + * Sets system audio volume * * @param oldIndex current volume index * @param newIndex volume index to be set @@ -165,7 +183,7 @@ public final class HdmiTvClient extends HdmiClient { } /** - * Set system audio mute status + * Sets system audio mute status * * @param mute {@code true} if muted; otherwise, {@code false} */ @@ -178,7 +196,7 @@ public final class HdmiTvClient extends HdmiClient { } /** - * Set record listener + * Sets record listener * * @param listener */ @@ -198,7 +216,7 @@ public final class HdmiTvClient extends HdmiClient { @Override public byte[] getOneTouchRecordSource(int recorderAddress) { HdmiRecordSources.RecordSource source = - callback.getOneTouchRecordSource(recorderAddress); + callback.onOneTouchRecordSourceRequested(recorderAddress); if (source == null) { return EmptyArray.BYTE; } @@ -208,31 +226,31 @@ public final class HdmiTvClient extends HdmiClient { } @Override - public void onOneTouchRecordResult(int result) { - callback.onOneTouchRecordResult(result); + public void onOneTouchRecordResult(int recorderAddress, int result) { + callback.onOneTouchRecordResult(recorderAddress, result); } @Override - public void onTimerRecordingResult(int result) { - callback.onTimerRecordingResult( + public void onTimerRecordingResult(int recorderAddress, int result) { + callback.onTimerRecordingResult(recorderAddress, HdmiRecordListener.TimerStatusData.parseFrom(result)); } @Override - public void onClearTimerRecordingResult(int result) { - callback.onClearTimerRecordingResult(result); + public void onClearTimerRecordingResult(int recorderAddress, int result) { + callback.onClearTimerRecordingResult(recorderAddress, result); } }; } /** - * Start one touch recording with the given recorder address and recorder source. + * Starts one touch recording with the given recorder address and recorder source. * <p> * Usage * <pre> * HdmiTvClient tvClient = ....; * // for own source. - * OwnSource ownSource = ownHdmiRecordSources.ownSource(); + * OwnSource ownSource = HdmiRecordSources.ofOwnSource(); * tvClient.startOneTouchRecord(recorderAddress, ownSource); * </pre> */ @@ -251,7 +269,7 @@ public final class HdmiTvClient extends HdmiClient { } /** - * Stop one touch record. + * Stops one touch record. * * @param recorderAddress recorder address where recoding will be stopped */ @@ -264,7 +282,7 @@ public final class HdmiTvClient extends HdmiClient { } /** - * Start timer recording with the given recoder address and recorder source. + * Starts timer recording with the given recoder address and recorder source. * <p> * Usage * <pre> @@ -313,7 +331,7 @@ public final class HdmiTvClient extends HdmiClient { } /** - * Clear timer recording with the given recorder address and recording source. + * Clears timer recording with the given recorder address and recording source. * For more details, please refer {@link #startTimerRecording(int, int, TimerRecordSource)}. */ public void clearTimerRecording(int recorderAddress, int sourceType, TimerRecordSource source) { @@ -339,7 +357,7 @@ public final class HdmiTvClient extends HdmiClient { } /** - * Set {@link HdmiMhlVendorCommandListener} to get incoming MHL vendor command. + * Sets {@link HdmiMhlVendorCommandListener} to get incoming MHL vendor command. * * @param listener to receive incoming MHL vendor command */ @@ -365,7 +383,7 @@ public final class HdmiTvClient extends HdmiClient { } /** - * Send MHL vendor command to the device connected to a port of the given portId. + * Sends MHL vendor command to the device connected to a port of the given portId. * * @param portId id of port to send MHL vendor command * @param offset offset in the in given data diff --git a/core/java/android/hardware/hdmi/IHdmiControlService.aidl b/core/java/android/hardware/hdmi/IHdmiControlService.aidl index 4866a9a..c1e924e 100644 --- a/core/java/android/hardware/hdmi/IHdmiControlService.aidl +++ b/core/java/android/hardware/hdmi/IHdmiControlService.aidl @@ -59,6 +59,7 @@ interface IHdmiControlService { void setSystemAudioMute(boolean mute); void setInputChangeListener(IHdmiInputChangeListener listener); List<HdmiDeviceInfo> getInputDevices(); + List<HdmiDeviceInfo> getDeviceList(); void sendVendorCommand(int deviceType, int targetAddress, in byte[] params, boolean hasVendorId); void addVendorCommandListener(IHdmiVendorCommandListener listener, int deviceType); diff --git a/core/java/android/hardware/hdmi/IHdmiRecordListener.aidl b/core/java/android/hardware/hdmi/IHdmiRecordListener.aidl index 44d9065..d2deb38 100644 --- a/core/java/android/hardware/hdmi/IHdmiRecordListener.aidl +++ b/core/java/android/hardware/hdmi/IHdmiRecordListener.aidl @@ -31,19 +31,25 @@ package android.hardware.hdmi; /** * Called when one touch record is started or failed during initialization. * + * @param recorderAddress An address of recorder that reports result of one touch record + * request * @param result result code for one touch record */ - void onOneTouchRecordResult(int result); + void onOneTouchRecordResult(int recorderAddress, int result); /** * Called when timer recording is started or failed during initialization. - + * + * @param recorderAddress An address of recorder that reports result of timer recording + * request * @param result result code for timer recording */ - void onTimerRecordingResult(int result); + void onTimerRecordingResult(int recorderAddress, int result); /** * Called when receiving result for clear timer recording request. * - * @param result result of clear timer. + * @param recorderAddress An address of recorder that reports result of clear timer recording + * request + * @param result result of clear timer */ - void onClearTimerRecordingResult(int result); + void onClearTimerRecordingResult(int recorderAddress, int result); }
\ No newline at end of file diff --git a/core/java/android/hardware/hdmi/IHdmiVendorCommandListener.aidl b/core/java/android/hardware/hdmi/IHdmiVendorCommandListener.aidl index 55cc925..a16e878 100644 --- a/core/java/android/hardware/hdmi/IHdmiVendorCommandListener.aidl +++ b/core/java/android/hardware/hdmi/IHdmiVendorCommandListener.aidl @@ -23,5 +23,6 @@ package android.hardware.hdmi; * @hide */ oneway interface IHdmiVendorCommandListener { - void onReceived(int logicalAddress, in byte[] operands, boolean hasVendorId); + void onReceived(int logicalAddress, int destAddress, in byte[] operands, boolean hasVendorId); + void onControlStateChanged(boolean enabled, int reason); } diff --git a/core/java/android/net/BaseNetworkStateTracker.java b/core/java/android/net/BaseNetworkStateTracker.java index 58d0048..e4e5b1e 100644 --- a/core/java/android/net/BaseNetworkStateTracker.java +++ b/core/java/android/net/BaseNetworkStateTracker.java @@ -170,16 +170,6 @@ public abstract class BaseNetworkStateTracker implements NetworkStateTracker { } @Override - public void addStackedLink(LinkProperties link) { - mLinkProperties.addStackedLink(link); - } - - @Override - public void removeStackedLink(LinkProperties link) { - mLinkProperties.removeStackedLink(link); - } - - @Override public void supplyMessenger(Messenger messenger) { // not supported on this network } diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index 9194ca8..1c9f4c6 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -1012,60 +1012,57 @@ public class ConnectivityManager { return null; } + /** + * Guess what the network request was trying to say so that the resulting + * network is accessible via the legacy (deprecated) API such as + * requestRouteToHost. + * This means we should try to be fairly preceise about transport and + * capability but ignore things such as networkSpecifier. + * If the request has more than one transport or capability it doesn't + * match the old legacy requests (they selected only single transport/capability) + * so this function cannot map the request to a single legacy type and + * the resulting network will not be available to the legacy APIs. + * + * TODO - This should be removed when the legacy APIs are removed. + */ private int inferLegacyTypeForNetworkCapabilities(NetworkCapabilities netCap) { if (netCap == null) { return TYPE_NONE; } + if (!netCap.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { return TYPE_NONE; } + + String type = null; + int result = TYPE_NONE; + if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)) { - if (netCap.equals(networkCapabilitiesForFeature(TYPE_MOBILE, "enableCBS"))) { - return TYPE_MOBILE_CBS; - } else { - return TYPE_NONE; - } - } - if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_IMS)) { - if (netCap.equals(networkCapabilitiesForFeature(TYPE_MOBILE, "enableIMS"))) { - return TYPE_MOBILE_IMS; - } else { - return TYPE_NONE; - } - } - if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_FOTA)) { - if (netCap.equals(networkCapabilitiesForFeature(TYPE_MOBILE, "enableFOTA"))) { - return TYPE_MOBILE_FOTA; - } else { - return TYPE_NONE; - } - } - if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_DUN)) { - if (netCap.equals(networkCapabilitiesForFeature(TYPE_MOBILE, "enableDUN"))) { - return TYPE_MOBILE_DUN; - } else { - return TYPE_NONE; - } - } - if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_SUPL)) { - if (netCap.equals(networkCapabilitiesForFeature(TYPE_MOBILE, "enableSUPL"))) { - return TYPE_MOBILE_SUPL; - } else { - return TYPE_NONE; - } - } - if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_MMS)) { - if (netCap.equals(networkCapabilitiesForFeature(TYPE_MOBILE, "enableMMS"))) { - return TYPE_MOBILE_MMS; - } else { - return TYPE_NONE; - } - } - if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) { - if (netCap.equals(networkCapabilitiesForFeature(TYPE_MOBILE, "enableHIPRI"))) { - return TYPE_MOBILE_HIPRI; - } else { - return TYPE_NONE; + type = "enableCBS"; + result = TYPE_MOBILE_CBS; + } else if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_IMS)) { + type = "enableIMS"; + result = TYPE_MOBILE_IMS; + } else if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_FOTA)) { + type = "enableFOTA"; + result = TYPE_MOBILE_FOTA; + } else if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_DUN)) { + type = "enableDUN"; + result = TYPE_MOBILE_DUN; + } else if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_SUPL)) { + type = "enableSUPL"; + result = TYPE_MOBILE_SUPL; + } else if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_MMS)) { + type = "enableMMS"; + result = TYPE_MOBILE_MMS; + } else if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) { + type = "enableHIPRI"; + result = TYPE_MOBILE_HIPRI; + } + if (type != null) { + NetworkCapabilities testCap = networkCapabilitiesForFeature(TYPE_MOBILE, type); + if (testCap.equalsNetCapabilities(netCap) && testCap.equalsTransportTypes(netCap)) { + return result; } } return TYPE_NONE; @@ -2381,17 +2378,15 @@ public class ConnectivityManager { /** * The lookup key for a {@link Network} object included with the intent after - * succesfully finding a network for the applications request. Retrieve it with + * successfully finding a network for the applications request. Retrieve it with * {@link android.content.Intent#getParcelableExtra(String)}. - * @hide */ public static final String EXTRA_NETWORK_REQUEST_NETWORK = "networkRequestNetwork"; /** * The lookup key for a {@link NetworkRequest} object included with the intent after - * succesfully finding a network for the applications request. Retrieve it with + * successfully finding a network for the applications request. Retrieve it with * {@link android.content.Intent#getParcelableExtra(String)}. - * @hide */ public static final String EXTRA_NETWORK_REQUEST_NETWORK_REQUEST = "networkRequestNetworkRequest"; @@ -2400,7 +2395,7 @@ public class ConnectivityManager { /** * Request a network to satisfy a set of {@link NetworkCapabilities}. * - * This function behavies identically to the version that takes a NetworkCallback, but instead + * This function behaves identically to the version that takes a NetworkCallback, but instead * of {@link NetworkCallback} a {@link PendingIntent} is used. This means * the request may outlive the calling application and get called back when a suitable * network is found. @@ -2421,21 +2416,46 @@ public class ConnectivityManager { * two Intents defined by {@link Intent#filterEquals}), then it will be removed and * replaced by this one, effectively releasing the previous {@link NetworkRequest}. * <p> - * The request may be released normally by calling {@link #unregisterNetworkCallback}. + * The request may be released normally by calling + * {@link #releaseNetworkRequest(android.app.PendingIntent)}. * * @param request {@link NetworkRequest} describing this request. * @param operation Action to perform when the network is available (corresponds * to the {@link NetworkCallback#onAvailable} call. Typically - * comes from {@link PendingIntent#getBroadcast}. - * @hide + * comes from {@link PendingIntent#getBroadcast}. Cannot be null. */ public void requestNetwork(NetworkRequest request, PendingIntent operation) { + checkPendingIntent(operation); try { mService.pendingRequestForNetwork(request.networkCapabilities, operation); } catch (RemoteException e) {} } /** + * Removes a request made via {@link #requestNetwork(NetworkRequest, android.app.PendingIntent)} + * <p> + * This method has the same behavior as {@link #unregisterNetworkCallback} with respect to + * releasing network resources and disconnecting. + * + * @param operation A PendingIntent equal (as defined by {@link Intent#filterEquals}) to the + * PendingIntent passed to + * {@link #requestNetwork(NetworkRequest, android.app.PendingIntent)} with the + * corresponding NetworkRequest you'd like to remove. Cannot be null. + */ + public void releaseNetworkRequest(PendingIntent operation) { + checkPendingIntent(operation); + try { + mService.releasePendingNetworkRequest(operation); + } catch (RemoteException e) {} + } + + private void checkPendingIntent(PendingIntent intent) { + if (intent == null) { + throw new IllegalArgumentException("PendingIntent cannot be null."); + } + } + + /** * Registers to receive notifications about all networks which satisfy the given * {@link NetworkRequest}. The callbacks will continue to be called until * either the application exits or {@link #unregisterNetworkCallback} is called @@ -2451,7 +2471,7 @@ public class ConnectivityManager { /** * Unregisters callbacks about and possibly releases networks originating from * {@link #requestNetwork} and {@link #registerNetworkCallback} calls. If the - * given {@code NetworkCallback} had previosuly been used with {@code #requestNetwork}, + * given {@code NetworkCallback} had previously been used with {@code #requestNetwork}, * any networks that had been connected to only to satisfy that request will be * disconnected. * diff --git a/core/java/android/net/DhcpResults.java b/core/java/android/net/DhcpResults.java index 71df60a..6159e1e 100644 --- a/core/java/android/net/DhcpResults.java +++ b/core/java/android/net/DhcpResults.java @@ -200,7 +200,7 @@ public class DhcpResults extends StaticIpConfiguration { vendorInfo = info; } - public void setDomains(String domains) { - domains = domains; + public void setDomains(String newDomains) { + domains = newDomains; } } diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl index a983d88..a7bbc53 100644 --- a/core/java/android/net/IConnectivityManager.aidl +++ b/core/java/android/net/IConnectivityManager.aidl @@ -156,6 +156,8 @@ interface IConnectivityManager NetworkRequest pendingRequestForNetwork(in NetworkCapabilities networkCapabilities, in PendingIntent operation); + void releasePendingNetworkRequest(in PendingIntent operation); + NetworkRequest listenForNetwork(in NetworkCapabilities networkCapabilities, in Messenger messenger, in IBinder binder); diff --git a/core/java/android/net/LinkAddress.java b/core/java/android/net/LinkAddress.java index c387055..384ab1c 100644 --- a/core/java/android/net/LinkAddress.java +++ b/core/java/android/net/LinkAddress.java @@ -21,12 +21,14 @@ import android.os.Parcelable; import android.util.Pair; import java.net.Inet4Address; +import java.net.Inet6Address; import java.net.InetAddress; import java.net.InterfaceAddress; import java.net.UnknownHostException; import static android.system.OsConstants.IFA_F_DADFAILED; import static android.system.OsConstants.IFA_F_DEPRECATED; +import static android.system.OsConstants.IFA_F_OPTIMISTIC; import static android.system.OsConstants.IFA_F_TENTATIVE; import static android.system.OsConstants.RT_SCOPE_HOST; import static android.system.OsConstants.RT_SCOPE_LINK; @@ -93,6 +95,20 @@ public class LinkAddress implements Parcelable { } /** + * Utility function to check if |address| is a Unique Local IPv6 Unicast Address + * (a.k.a. "ULA"; RFC 4193). + * + * Per RFC 4193 section 8, fc00::/7 identifies these addresses. + */ + private boolean isIPv6ULA() { + if (address != null && address instanceof Inet6Address) { + byte[] bytes = address.getAddress(); + return ((bytes[0] & (byte)0xfc) == (byte)0xfc); + } + return false; + } + + /** * Utility function for the constructors. */ private void init(InetAddress address, int prefixLength, int flags, int scope) { @@ -268,8 +284,16 @@ public class LinkAddress implements Parcelable { * @hide */ public boolean isGlobalPreferred() { + /** + * Note that addresses flagged as IFA_F_OPTIMISTIC are + * simultaneously flagged as IFA_F_TENTATIVE (when the tentative + * state has cleared either DAD has succeeded or failed, and both + * flags are cleared regardless). + */ return (scope == RT_SCOPE_UNIVERSE && - (flags & (IFA_F_DADFAILED | IFA_F_DEPRECATED | IFA_F_TENTATIVE)) == 0L); + !isIPv6ULA() && + (flags & (IFA_F_DADFAILED | IFA_F_DEPRECATED)) == 0L && + ((flags & IFA_F_TENTATIVE) == 0L || (flags & IFA_F_OPTIMISTIC) != 0L)); } /** diff --git a/core/java/android/net/LinkProperties.java b/core/java/android/net/LinkProperties.java index 662c576..8b0dfc9 100644 --- a/core/java/android/net/LinkProperties.java +++ b/core/java/android/net/LinkProperties.java @@ -493,16 +493,16 @@ public final class LinkProperties implements Parcelable { /** * Removes a stacked link. * - * If there a stacked link with the same interfacename as link, it is + * If there is a stacked link with the given interface name, it is * removed. Otherwise, nothing changes. * - * @param link The link to remove. + * @param iface The interface name of the link to remove. * @return true if the link was removed, false otherwise. * @hide */ - public boolean removeStackedLink(LinkProperties link) { - if (link != null && link.getInterfaceName() != null) { - LinkProperties removed = mStackedLinks.remove(link.getInterfaceName()); + public boolean removeStackedLink(String iface) { + if (iface != null) { + LinkProperties removed = mStackedLinks.remove(iface); return removed != null; } return false; @@ -675,17 +675,38 @@ public final class LinkProperties implements Parcelable { } /** - * Returns true if this link is provisioned for global connectivity. For IPv6, this requires an - * IP address, default route, and DNS server. For IPv4, this requires only an IPv4 address, - * because WifiStateMachine accepts static configurations that only specify an address but not - * DNS servers or a default route. + * Returns true if this link is provisioned for global IPv4 connectivity. + * This requires an IP address, default route, and DNS server. + * + * @return {@code true} if the link is provisioned, {@code false} otherwise. + */ + private boolean hasIPv4() { + return (hasIPv4Address() && + hasIPv4DefaultRoute() && + hasIPv4DnsServer()); + } + + /** + * Returns true if this link is provisioned for global IPv6 connectivity. + * This requires an IP address, default route, and DNS server. + * + * @return {@code true} if the link is provisioned, {@code false} otherwise. + */ + private boolean hasIPv6() { + return (hasGlobalIPv6Address() && + hasIPv6DefaultRoute() && + hasIPv6DnsServer()); + } + + /** + * Returns true if this link is provisioned for global connectivity, + * for at least one Internet Protocol family. * * @return {@code true} if the link is provisioned, {@code false} otherwise. * @hide */ public boolean isProvisioned() { - return (hasIPv4Address() || - (hasGlobalIPv6Address() && hasIPv6DefaultRoute() && hasIPv6DnsServer())); + return (hasIPv4() || hasIPv6()); } /** diff --git a/core/java/android/net/MobileDataStateTracker.java b/core/java/android/net/MobileDataStateTracker.java index d8012fe..40b7e06 100644 --- a/core/java/android/net/MobileDataStateTracker.java +++ b/core/java/android/net/MobileDataStateTracker.java @@ -661,16 +661,6 @@ public class MobileDataStateTracker extends BaseNetworkStateTracker { } @Override - public void addStackedLink(LinkProperties link) { - mLinkProperties.addStackedLink(link); - } - - @Override - public void removeStackedLink(LinkProperties link) { - mLinkProperties.removeStackedLink(link); - } - - @Override public String toString() { final CharArrayWriter writer = new CharArrayWriter(); final PrintWriter pw = new PrintWriter(writer); diff --git a/core/java/android/net/Network.java b/core/java/android/net/Network.java index 58f0fc0..4fa0593 100644 --- a/core/java/android/net/Network.java +++ b/core/java/android/net/Network.java @@ -21,7 +21,9 @@ import android.os.Parcelable; import android.os.Parcel; import android.system.ErrnoException; +import java.io.FileDescriptor; import java.io.IOException; +import java.net.DatagramSocket; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.MalformedURLException; @@ -264,18 +266,40 @@ public class Network implements Parcelable { } /** + * Binds the specified {@link DatagramSocket} to this {@code Network}. All data traffic on the + * socket will be sent on this {@code Network}, irrespective of any process-wide network binding + * set by {@link ConnectivityManager#setProcessDefaultNetwork}. The socket must not be + * connected. + */ + public void bindSocket(DatagramSocket socket) throws IOException { + // Apparently, the kernel doesn't update a connected UDP socket's routing upon mark changes. + if (socket.isConnected()) { + throw new SocketException("Socket is connected"); + } + // Query a property of the underlying socket to ensure that the socket's file descriptor + // exists, is available to bind to a network and is not closed. + socket.getReuseAddress(); + bindSocketFd(socket.getFileDescriptor$()); + } + + /** * Binds the specified {@link Socket} to this {@code Network}. All data traffic on the socket * will be sent on this {@code Network}, irrespective of any process-wide network binding set by * {@link ConnectivityManager#setProcessDefaultNetwork}. The socket must not be connected. */ public void bindSocket(Socket socket) throws IOException { + // Apparently, the kernel doesn't update a connected TCP socket's routing upon mark changes. if (socket.isConnected()) { throw new SocketException("Socket is connected"); } - // Query a property of the underlying socket to ensure the underlying - // socket exists so a file descriptor is available to bind to a network. + // Query a property of the underlying socket to ensure that the socket's file descriptor + // exists, is available to bind to a network and is not closed. socket.getReuseAddress(); - int err = NetworkUtils.bindSocketToNetwork(socket.getFileDescriptor$().getInt$(), netId); + bindSocketFd(socket.getFileDescriptor$()); + } + + private void bindSocketFd(FileDescriptor fd) throws IOException { + int err = NetworkUtils.bindSocketToNetwork(fd.getInt$(), netId); if (err != 0) { // bindSocketToNetwork returns negative errno. throw new ErrnoException("Binding socket to network " + netId, -err) diff --git a/core/java/android/net/NetworkCapabilities.java b/core/java/android/net/NetworkCapabilities.java index 1efe478..ce7ad65 100644 --- a/core/java/android/net/NetworkCapabilities.java +++ b/core/java/android/net/NetworkCapabilities.java @@ -235,7 +235,8 @@ public final class NetworkCapabilities implements Parcelable { return ((nc.mNetworkCapabilities & this.mNetworkCapabilities) == this.mNetworkCapabilities); } - private boolean equalsNetCapabilities(NetworkCapabilities nc) { + /** @hide */ + public boolean equalsNetCapabilities(NetworkCapabilities nc) { return (nc.mNetworkCapabilities == this.mNetworkCapabilities); } @@ -344,7 +345,8 @@ public final class NetworkCapabilities implements Parcelable { return ((this.mTransportTypes == 0) || ((this.mTransportTypes & nc.mTransportTypes) != 0)); } - private boolean equalsTransportTypes(NetworkCapabilities nc) { + /** @hide */ + public boolean equalsTransportTypes(NetworkCapabilities nc) { return (nc.mTransportTypes == this.mTransportTypes); } diff --git a/core/java/android/net/NetworkScoreManager.java b/core/java/android/net/NetworkScoreManager.java index 3f68a44..a939cce 100644 --- a/core/java/android/net/NetworkScoreManager.java +++ b/core/java/android/net/NetworkScoreManager.java @@ -41,10 +41,10 @@ import android.os.UserHandle; * <ul> * <li>Declares the {@link android.Manifest.permission#SCORE_NETWORKS} permission. * <li>Includes a receiver for {@link #ACTION_SCORE_NETWORKS} guarded by the - * {@link android.Manifest.permission#BROADCAST_SCORE_NETWORKS} permission which scores networks - * and (eventually) calls {@link #updateScores} with the results. If this receiver specifies an - * android:label attribute, this label will be used when referring to the application throughout - * system settings; otherwise, the application label will be used. + * {@link android.Manifest.permission#BROADCAST_NETWORK_PRIVILEGED} permission which scores + * networks and (eventually) calls {@link #updateScores} with the results. If this receiver + * specifies an android:label attribute, this label will be used when referring to the + * application throughout system settings; otherwise, the application label will be used. * </ul> * * <p>The system keeps track of an active scorer application; at any time, only this application @@ -192,12 +192,15 @@ public class NetworkScoreManager { /** * Set the active scorer to a new package and clear existing scores. * + * <p>Should never be called directly without obtaining user consent. This can be done by using + * the {@link #ACTION_CHANGE_ACTIVE} broadcast, or using a custom configuration activity. + * * @return true if the operation succeeded, or false if the new package is not a valid scorer. * @throws SecurityException if the caller does not hold the - * {@link android.Manifest.permission#BROADCAST_SCORE_NETWORKS} permission indicating - * that it can manage scorer applications. + * {@link android.Manifest.permission#SCORE_NETWORKS} permission. * @hide */ + @SystemApi public boolean setActiveScorer(String packageName) throws SecurityException { try { return mService.setActiveScorer(packageName); @@ -228,7 +231,7 @@ public class NetworkScoreManager { * * @return true if the broadcast was sent, or false if there is no active scorer. * @throws SecurityException if the caller does not hold the - * {@link android.Manifest.permission#BROADCAST_SCORE_NETWORKS} permission. + * {@link android.Manifest.permission#BROADCAST_NETWORK_PRIVILEGED} permission. * @hide */ public boolean requestScores(NetworkKey[] networks) throws SecurityException { @@ -252,7 +255,7 @@ public class NetworkScoreManager { * @param networkType the type of network this cache can handle. See {@link NetworkKey#type}. * @param scoreCache implementation of {@link INetworkScoreCache} to store the scores. * @throws SecurityException if the caller does not hold the - * {@link android.Manifest.permission#BROADCAST_SCORE_NETWORKS} permission. + * {@link android.Manifest.permission#BROADCAST_NETWORK_PRIVILEGED} permission. * @throws IllegalArgumentException if a score cache is already registered for this type. * @hide */ diff --git a/core/java/android/net/NetworkScorerAppManager.java b/core/java/android/net/NetworkScorerAppManager.java index c33f5ec..46f7194 100644 --- a/core/java/android/net/NetworkScorerAppManager.java +++ b/core/java/android/net/NetworkScorerAppManager.java @@ -79,7 +79,7 @@ public final class NetworkScorerAppManager { * <ul> * <li>Declares the {@link android.Manifest.permission#SCORE_NETWORKS} permission. * <li>Includes a receiver for {@link NetworkScoreManager#ACTION_SCORE_NETWORKS} guarded by the - * {@link android.Manifest.permission#BROADCAST_SCORE_NETWORKS} permission. + * {@link android.Manifest.permission#BROADCAST_NETWORK_PRIVILEGED} permission. * </ul> * * @return the list of scorers, or the empty list if there are no valid scorers. @@ -98,8 +98,8 @@ public final class NetworkScorerAppManager { // Should never happen with queryBroadcastReceivers, but invalid nonetheless. continue; } - if (!permission.BROADCAST_SCORE_NETWORKS.equals(receiverInfo.permission)) { - // Receiver doesn't require the BROADCAST_SCORE_NETWORKS permission, which means + if (!permission.BROADCAST_NETWORK_PRIVILEGED.equals(receiverInfo.permission)) { + // Receiver doesn't require the BROADCAST_NETWORK_PRIVILEGED permission, which means // anyone could trigger network scoring and flood the framework with score requests. continue; } diff --git a/core/java/android/net/NetworkStateTracker.java b/core/java/android/net/NetworkStateTracker.java index 35500cc..c80782c 100644 --- a/core/java/android/net/NetworkStateTracker.java +++ b/core/java/android/net/NetworkStateTracker.java @@ -216,16 +216,6 @@ public interface NetworkStateTracker { */ public void setDependencyMet(boolean met); - /** - * Informs the state tracker that another interface is stacked on top of it. - **/ - public void addStackedLink(LinkProperties link); - - /** - * Informs the state tracker that a stacked interface has been removed. - **/ - public void removeStackedLink(LinkProperties link); - /* * Called once to setup async channel between this and * the underlying network specific code. diff --git a/core/java/android/net/Proxy.java b/core/java/android/net/Proxy.java index 6a78c29..3477b02 100644 --- a/core/java/android/net/Proxy.java +++ b/core/java/android/net/Proxy.java @@ -19,17 +19,10 @@ package android.net; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.content.Context; -import android.net.ProxyInfo; import android.text.TextUtils; import android.util.Log; - import org.apache.http.HttpHost; -import org.apache.http.HttpRequest; -import org.apache.http.conn.routing.HttpRoute; -import org.apache.http.conn.routing.HttpRoutePlanner; -import org.apache.http.conn.scheme.SchemeRegistry; -import org.apache.http.protocol.HttpContext; import java.net.InetSocketAddress; import java.net.ProxySelector; @@ -212,6 +205,7 @@ public final class Proxy { * is no proxy. * {@hide} */ + // TODO: Get rid of this method. It's used only in tests. public static final HttpHost getPreferredHttpHost(Context context, String url) { java.net.Proxy prefProxy = getProxy(context, url); diff --git a/core/java/android/net/ProxyInfo.java b/core/java/android/net/ProxyInfo.java index 1534e2c..7694420 100644 --- a/core/java/android/net/ProxyInfo.java +++ b/core/java/android/net/ProxyInfo.java @@ -36,7 +36,13 @@ import java.util.Locale; * * Other HTTP stacks will need to obtain the proxy info from * {@link Proxy#PROXY_CHANGE_ACTION} broadcast as the extra {@link Proxy#EXTRA_PROXY_INFO}. + * + * @deprecated Please use {@link java.net.URL#openConnection}, {@link java.net.Proxy} and + * friends. The Apache HTTP client is no longer maintained and may be removed in a future + * release. Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> + * for further details. */ +@Deprecated public class ProxyInfo implements Parcelable { private String mHost; diff --git a/core/java/android/net/RssiCurve.java b/core/java/android/net/RssiCurve.java index f653f37..8ebe9e8 100644 --- a/core/java/android/net/RssiCurve.java +++ b/core/java/android/net/RssiCurve.java @@ -27,8 +27,8 @@ import java.util.Objects; * A curve defining the network score over a range of RSSI values. * * <p>For each RSSI bucket, the score may be any byte. Scores have no absolute meaning and are only - * considered relative to other scores assigned by the same scorer. Networks with no score are all - * considered equivalent and ranked below any network with a score. + * considered relative to other scores assigned by the same scorer. Networks with no score are + * treated equivalently to a network with score {@link Byte#MIN_VALUE}, and will not be used. * * <p>For example, consider a curve starting at -110 dBm with a bucket width of 10 and the * following buckets: {@code [-20, -10, 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120]}. @@ -52,6 +52,7 @@ import java.util.Objects; */ @SystemApi public class RssiCurve implements Parcelable { + private static final int DEFAULT_ACTIVE_NETWORK_RSSI_BOOST = 25; /** The starting dBm of the curve. */ public final int start; @@ -63,6 +64,15 @@ public class RssiCurve implements Parcelable { public final byte[] rssiBuckets; /** + * The RSSI boost to give this network when active, in dBm. + * + * <p>When the system is connected to this network, it will pretend that the network has this + * much higher of an RSSI. This is to avoid switching networks when another network has only a + * slightly higher score. + */ + public final int activeNetworkRssiBoost; + + /** * Construct a new {@link RssiCurve}. * * @param start the starting dBm of the curve. @@ -70,12 +80,25 @@ public class RssiCurve implements Parcelable { * @param rssiBuckets the score for each RSSI bucket. */ public RssiCurve(int start, int bucketWidth, byte[] rssiBuckets) { + this(start, bucketWidth, rssiBuckets, DEFAULT_ACTIVE_NETWORK_RSSI_BOOST); + } + + /** + * Construct a new {@link RssiCurve}. + * + * @param start the starting dBm of the curve. + * @param bucketWidth the width of each RSSI bucket, in dBm. + * @param rssiBuckets the score for each RSSI bucket. + * @param activeNetworkRssiBoost the RSSI boost to apply when this network is active, in dBm. + */ + public RssiCurve(int start, int bucketWidth, byte[] rssiBuckets, int activeNetworkRssiBoost) { this.start = start; this.bucketWidth = bucketWidth; if (rssiBuckets == null || rssiBuckets.length == 0) { throw new IllegalArgumentException("rssiBuckets must be at least one element large."); } this.rssiBuckets = rssiBuckets; + this.activeNetworkRssiBoost = activeNetworkRssiBoost; } private RssiCurve(Parcel in) { @@ -84,6 +107,7 @@ public class RssiCurve implements Parcelable { int bucketCount = in.readInt(); rssiBuckets = new byte[bucketCount]; in.readByteArray(rssiBuckets); + activeNetworkRssiBoost = in.readInt(); } @Override @@ -97,6 +121,7 @@ public class RssiCurve implements Parcelable { out.writeInt(bucketWidth); out.writeInt(rssiBuckets.length); out.writeByteArray(rssiBuckets); + out.writeInt(activeNetworkRssiBoost); } /** @@ -108,6 +133,23 @@ public class RssiCurve implements Parcelable { * @return the score for the given RSSI. */ public byte lookupScore(int rssi) { + return lookupScore(rssi, false /* isActiveNetwork */); + } + + /** + * Lookup the score for a given RSSI value. + * + * @param rssi The RSSI to lookup. If the RSSI falls below the start of the curve, the score at + * the start of the curve will be returned. If it falls after the end of the curve, the + * score at the end of the curve will be returned. + * @param isActiveNetwork Whether this network is currently active. + * @return the score for the given RSSI. + */ + public byte lookupScore(int rssi, boolean isActiveNetwork) { + if (isActiveNetwork) { + rssi += activeNetworkRssiBoost; + } + int index = (rssi - start) / bucketWidth; // Snap the index to the closest bucket if it falls outside the curve. @@ -136,12 +178,13 @@ public class RssiCurve implements Parcelable { return start == rssiCurve.start && bucketWidth == rssiCurve.bucketWidth && - Arrays.equals(rssiBuckets, rssiCurve.rssiBuckets); + Arrays.equals(rssiBuckets, rssiCurve.rssiBuckets) && + activeNetworkRssiBoost == rssiCurve.activeNetworkRssiBoost; } @Override public int hashCode() { - return Objects.hash(start, bucketWidth, rssiBuckets); + return Objects.hash(start, bucketWidth, rssiBuckets, activeNetworkRssiBoost); } @Override @@ -150,7 +193,9 @@ public class RssiCurve implements Parcelable { sb.append("RssiCurve[start=") .append(start) .append(",bucketWidth=") - .append(bucketWidth); + .append(bucketWidth) + .append(",activeNetworkRssiBoost=") + .append(activeNetworkRssiBoost); sb.append(",buckets="); for (int i = 0; i < rssiBuckets.length; i++) { diff --git a/core/java/android/net/SSLCertificateSocketFactory.java b/core/java/android/net/SSLCertificateSocketFactory.java index b0278d3..c15e6e5 100644 --- a/core/java/android/net/SSLCertificateSocketFactory.java +++ b/core/java/android/net/SSLCertificateSocketFactory.java @@ -154,7 +154,13 @@ public class SSLCertificateSocketFactory extends SSLSocketFactory { * for none. The socket timeout is reset to 0 after the handshake. * @param cache The {@link SSLSessionCache} to use, or null for no cache. * @return a new SocketFactory with the specified parameters + * + * @deprecated Use {@link #getDefault()} along with a {@link javax.net.ssl.HttpsURLConnection} + * instead. The Apache HTTP client is no longer maintained and may be removed in a future + * release. Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> + * for further details. */ + @Deprecated public static org.apache.http.conn.ssl.SSLSocketFactory getHttpSocketFactory( int handshakeTimeoutMillis, SSLSessionCache cache) { return new org.apache.http.conn.ssl.SSLSocketFactory( diff --git a/core/java/android/net/StaticIpConfiguration.java b/core/java/android/net/StaticIpConfiguration.java index 5a273cf..598a503 100644 --- a/core/java/android/net/StaticIpConfiguration.java +++ b/core/java/android/net/StaticIpConfiguration.java @@ -107,6 +107,7 @@ public class StaticIpConfiguration implements Parcelable { for (InetAddress dns : dnsServers) { lp.addDnsServer(dns); } + lp.setDomains(domains); return lp; } diff --git a/core/java/android/net/http/AndroidHttpClient.java b/core/java/android/net/http/AndroidHttpClient.java index 04f3974..a262076 100644 --- a/core/java/android/net/http/AndroidHttpClient.java +++ b/core/java/android/net/http/AndroidHttpClient.java @@ -74,7 +74,13 @@ import java.util.zip.GZIPOutputStream; * To retain cookies, simply add a cookie store to the HttpContext:</p> * * <pre>context.setAttribute(ClientContext.COOKIE_STORE, cookieStore);</pre> + * + * @deprecated Please use {@link java.net.URLConnection} and friends instead. + * The Apache HTTP client is no longer maintained and may be removed in a future + * release. Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> + * for further details. */ +@Deprecated public final class AndroidHttpClient implements HttpClient { // Gzip of data shorter than this probably won't be worthwhile @@ -108,7 +114,13 @@ public final class AndroidHttpClient implements HttpClient { * @param userAgent to report in your HTTP requests * @param context to use for caching SSL sessions (may be null for no caching) * @return AndroidHttpClient for you to use for all your requests. + * + * @deprecated Please use {@link java.net.URLConnection} and friends instead. See + * {@link android.net.SSLCertificateSocketFactory} for SSL cache support. If you'd + * like to set a custom useragent, please use {@link java.net.URLConnection#setRequestProperty(String, String)} + * with {@code field} set to {@code User-Agent}. */ + @Deprecated public static AndroidHttpClient newInstance(String userAgent, Context context) { HttpParams params = new BasicHttpParams(); @@ -148,7 +160,13 @@ public final class AndroidHttpClient implements HttpClient { * Create a new HttpClient with reasonable defaults (which you can update). * @param userAgent to report in your HTTP requests. * @return AndroidHttpClient for you to use for all your requests. + * + * @deprecated Please use {@link java.net.URLConnection} and friends instead. See + * {@link android.net.SSLCertificateSocketFactory} for SSL cache support. If you'd + * like to set a custom useragent, please use {@link java.net.URLConnection#setRequestProperty(String, String)} + * with {@code field} set to {@code User-Agent}. */ + @Deprecated public static AndroidHttpClient newInstance(String userAgent) { return newInstance(userAgent, null /* session cache */); } diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java index 24cdd77..f361695b 100644 --- a/core/java/android/os/Build.java +++ b/core/java/android/os/Build.java @@ -588,6 +588,11 @@ public class Build { * </ul> */ public static final int LOLLIPOP = 21; + + /** + * Lollipop with an extra sugar coating on the outside! + */ + public static final int LOLLIPOP_MR1 = 22; } /** The type of build, like "user" or "eng". */ diff --git a/core/java/android/os/ConditionVariable.java b/core/java/android/os/ConditionVariable.java index 07b221c..1e820f9 100644 --- a/core/java/android/os/ConditionVariable.java +++ b/core/java/android/os/ConditionVariable.java @@ -109,7 +109,7 @@ public class ConditionVariable * <p> * If the condition is already opened, return immediately. * - * @param timeout the minimum time to wait in milliseconds. + * @param timeout the maximum time to wait in milliseconds. * * @return true if the condition was opened, false if the call returns * because of the timeout. diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java index 18730b6..3f42d25 100644 --- a/core/java/android/os/Debug.java +++ b/core/java/android/os/Debug.java @@ -165,7 +165,7 @@ public final class Debug public int otherSwappedOut; /** @hide */ - public static final int NUM_OTHER_STATS = 16; + public static final int NUM_OTHER_STATS = 17; /** @hide */ public static final int NUM_DVK_STATS = 5; @@ -296,23 +296,24 @@ public final class Debug case 1: return "Stack"; case 2: return "Cursor"; case 3: return "Ashmem"; - case 4: return "Other dev"; - case 5: return ".so mmap"; - case 6: return ".jar mmap"; - case 7: return ".apk mmap"; - case 8: return ".ttf mmap"; - case 9: return ".dex mmap"; - case 10: return "code mmap"; - case 11: return "image mmap"; - case 12: return "Other mmap"; - case 13: return "Graphics"; - case 14: return "GL"; - case 15: return "Memtrack"; - case 16: return ".Heap"; - case 17: return ".LOS"; - case 18: return ".LinearAlloc"; - case 19: return ".GC"; - case 20: return ".JITCache"; + case 4: return "Gfx driver"; + case 5: return "Other dev"; + case 6: return ".so mmap"; + case 7: return ".jar mmap"; + case 8: return ".apk mmap"; + case 9: return ".ttf mmap"; + case 10: return ".dex mmap"; + case 11: return ".oat mmap"; + case 12: return ".art mmap"; + case 13: return "Other mmap"; + case 14: return "Graphics"; + case 15: return "GL"; + case 16: return "Memtrack"; + case 17: return ".Heap"; + case 18: return ".LOS"; + case 19: return ".LinearAlloc"; + case 20: return ".GC"; + case 21: return ".JITCache"; default: return "????"; } } @@ -1093,7 +1094,15 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo /** @hide */ public static final int MEMINFO_ZRAM_TOTAL = 8; /** @hide */ - public static final int MEMINFO_COUNT = 9; + public static final int MEMINFO_MAPPED = 9; + /** @hide */ + public static final int MEMINFO_VM_ALLOC_USED = 10; + /** @hide */ + public static final int MEMINFO_PAGE_TABLES = 11; + /** @hide */ + public static final int MEMINFO_KERNEL_STACK = 12; + /** @hide */ + public static final int MEMINFO_COUNT = 13; /** * Retrieves /proc/meminfo. outSizes is filled with fields diff --git a/core/java/android/os/INetworkManagementService.aidl b/core/java/android/os/INetworkManagementService.aidl index 16250c7..5d5d2b3 100644 --- a/core/java/android/os/INetworkManagementService.aidl +++ b/core/java/android/os/INetworkManagementService.aidl @@ -92,6 +92,11 @@ interface INetworkManagementService void enableIpv6(String iface); /** + * Enables or enables IPv6 ND offload. + */ + void setInterfaceIpv6NdOffload(String iface, boolean enable); + + /** * Retrieves the network routes currently configured on the specified * interface */ @@ -336,19 +341,19 @@ interface INetworkManagementService void removeVpnUidRanges(int netId, in UidRange[] ranges); /** - * Start the clatd (464xlat) service + * Start the clatd (464xlat) service on the given interface. */ void startClatd(String interfaceName); /** - * Stop the clatd (464xlat) service + * Stop the clatd (464xlat) service on the given interface. */ - void stopClatd(); + void stopClatd(String interfaceName); /** - * Determine whether the clatd (464xlat) service has been started + * Determine whether the clatd (464xlat) service has been started on the given interface. */ - boolean isClatdStarted(); + boolean isClatdStarted(String interfaceName); /** * Start listening for mobile activity state changes. diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 3234e77..bd6eeea 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -367,10 +367,27 @@ public class UserManager { * <p/>Type: Boolean * @see #setUserRestrictions(Bundle) * @see #getUserRestrictions() - * @hide */ public static final String DISALLOW_OUTGOING_BEAM = "no_outgoing_beam"; + /** + * Application restriction key that is used to indicate the pending arrival + * of real restrictions for the app. + * + * <p> + * Applications that support restrictions should check for the presence of this key. + * A <code>true</code> value indicates that restrictions may be applied in the near + * future but are not available yet. It is the responsibility of any + * management application that sets this flag to update it when the final + * restrictions are enforced. + * + * <p/>Key for application restrictions. + * <p/>Type: Boolean + * @see android.app.admin.DevicePolicyManager#addApplicationRestriction() + * @see android.app.admin.DevicePolicyManager#getApplicationRestriction() + */ + public static final String KEY_RESTRICTIONS_PENDING = "restrictions_pending"; + /** @hide */ public static final int PIN_VERIFICATION_FAILED_INCORRECT = -3; /** @hide */ diff --git a/core/java/android/preference/ListPreference.java b/core/java/android/preference/ListPreference.java index 8081a54..9482a72 100644 --- a/core/java/android/preference/ListPreference.java +++ b/core/java/android/preference/ListPreference.java @@ -162,10 +162,10 @@ public class ListPreference extends DialogPreference { @Override public CharSequence getSummary() { final CharSequence entry = getEntry(); - if (mSummary == null || entry == null) { + if (mSummary == null) { return super.getSummary(); } else { - return String.format(mSummary, entry); + return String.format(mSummary, entry == null ? "" : entry); } } diff --git a/core/java/android/preference/SeekBarVolumizer.java b/core/java/android/preference/SeekBarVolumizer.java index 671f722..3130b64 100644 --- a/core/java/android/preference/SeekBarVolumizer.java +++ b/core/java/android/preference/SeekBarVolumizer.java @@ -47,7 +47,6 @@ public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callba } private final Context mContext; - private final Handler mHandler; private final H mUiHandler = new H(); private final Callback mCallback; private final Uri mDefaultUri; @@ -55,8 +54,9 @@ public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callba private final int mStreamType; private final int mMaxStreamVolume; private final Receiver mReceiver = new Receiver(); - private final Observer mVolumeObserver; + private Handler mHandler; + private Observer mVolumeObserver; private int mOriginalStreamVolume; private Ringtone mRingtone; private int mLastProgress = -1; @@ -75,16 +75,8 @@ public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callba mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); mStreamType = streamType; mMaxStreamVolume = mAudioManager.getStreamMaxVolume(mStreamType); - HandlerThread thread = new HandlerThread(TAG + ".CallbackHandler"); - thread.start(); - mHandler = new Handler(thread.getLooper(), this); mCallback = callback; mOriginalStreamVolume = mAudioManager.getStreamVolume(mStreamType); - mVolumeObserver = new Observer(mHandler); - mContext.getContentResolver().registerContentObserver( - System.getUriFor(System.VOLUME_SETTINGS[mStreamType]), - false, mVolumeObserver); - mReceiver.setListening(true); if (defaultUri == null) { if (mStreamType == AudioManager.STREAM_RING) { defaultUri = Settings.System.DEFAULT_RINGTONE_URI; @@ -95,7 +87,6 @@ public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callba } } mDefaultUri = defaultUri; - mHandler.sendEmptyMessage(MSG_INIT_SAMPLE); } public void setSeekBar(SeekBar seekBar) { @@ -139,6 +130,7 @@ public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callba } private void postStartSample() { + if (mHandler == null) return; mHandler.removeMessages(MSG_START_SAMPLE); mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_START_SAMPLE), isSamplePlaying() ? CHECK_RINGTONE_PLAYBACK_DELAY_MS : 0); @@ -159,7 +151,8 @@ public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callba } } - void postStopSample() { + private void postStopSample() { + if (mHandler == null) return; // remove pending delayed start messages mHandler.removeMessages(MSG_START_SAMPLE); mHandler.removeMessages(MSG_STOP_SAMPLE); @@ -173,11 +166,27 @@ public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callba } public void stop() { + if (mHandler == null) return; // already stopped postStopSample(); mContext.getContentResolver().unregisterContentObserver(mVolumeObserver); - mSeekBar.setOnSeekBarChangeListener(null); mReceiver.setListening(false); + mSeekBar.setOnSeekBarChangeListener(null); mHandler.getLooper().quitSafely(); + mHandler = null; + mVolumeObserver = null; + } + + public void start() { + if (mHandler != null) return; // already started + HandlerThread thread = new HandlerThread(TAG + ".CallbackHandler"); + thread.start(); + mHandler = new Handler(thread.getLooper(), this); + mHandler.sendEmptyMessage(MSG_INIT_SAMPLE); + mVolumeObserver = new Observer(mHandler); + mContext.getContentResolver().registerContentObserver( + System.getUriFor(System.VOLUME_SETTINGS[mStreamType]), + false, mVolumeObserver); + mReceiver.setListening(true); } public void revertVolume() { @@ -193,7 +202,8 @@ public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callba postSetVolume(progress); } - void postSetVolume(int progress) { + private void postSetVolume(int progress) { + if (mHandler == null) return; // Do the volume changing separately to give responsive UI mLastProgress = progress; mHandler.removeMessages(MSG_SET_STREAM_VOLUME); diff --git a/core/java/android/preference/VolumePreference.java b/core/java/android/preference/VolumePreference.java index df9e10e..0d4c0b6 100644 --- a/core/java/android/preference/VolumePreference.java +++ b/core/java/android/preference/VolumePreference.java @@ -67,6 +67,7 @@ public class VolumePreference extends SeekBarDialogPreference implements final SeekBar seekBar = (SeekBar) view.findViewById(com.android.internal.R.id.seekbar); mSeekBarVolumizer = new SeekBarVolumizer(getContext(), mStreamType, null, this); + mSeekBarVolumizer.start(); mSeekBarVolumizer.setSeekBar(seekBar); getPreferenceManager().registerOnActivityStopListener(this); @@ -116,7 +117,7 @@ public class VolumePreference extends SeekBarDialogPreference implements public void onActivityStop() { if (mSeekBarVolumizer != null) { - mSeekBarVolumizer.postStopSample(); + mSeekBarVolumizer.stopSample(); } } diff --git a/core/java/android/print/IPrintDocumentAdapter.aidl b/core/java/android/print/IPrintDocumentAdapter.aidl index 9d384fb..8f33e0b 100644 --- a/core/java/android/print/IPrintDocumentAdapter.aidl +++ b/core/java/android/print/IPrintDocumentAdapter.aidl @@ -37,4 +37,5 @@ oneway interface IPrintDocumentAdapter { void write(in PageRange[] pages, in ParcelFileDescriptor fd, IWriteResultCallback callback, int sequence); void finish(); + void kill(String reason); } diff --git a/core/java/android/print/PrintManager.java b/core/java/android/print/PrintManager.java index bf8ac65..3fb812e 100644 --- a/core/java/android/print/PrintManager.java +++ b/core/java/android/print/PrintManager.java @@ -634,6 +634,17 @@ public final class PrintManager { } @Override + public void kill(String reason) { + synchronized (mLock) { + // If destroyed the handler is null. + if (!isDestroyedLocked()) { + mHandler.obtainMessage(MyHandler.MSG_ON_KILL, + reason).sendToTarget(); + } + } + } + + @Override public void onActivityPaused(Activity activity) { /* do nothing */ } @@ -719,6 +730,7 @@ public final class PrintManager { public static final int MSG_ON_LAYOUT = 2; public static final int MSG_ON_WRITE = 3; public static final int MSG_ON_FINISH = 4; + public static final int MSG_ON_KILL = 5; public MyHandler(Looper looper) { super(looper, null, true); @@ -794,6 +806,15 @@ public final class PrintManager { } } break; + case MSG_ON_KILL: { + if (DEBUG) { + Log.i(LOG_TAG, "onKill()"); + } + + String reason = (String) message.obj; + throw new RuntimeException(reason); + } + default: { throw new IllegalArgumentException("Unknown message: " + message.what); diff --git a/core/java/android/provider/CallLog.java b/core/java/android/provider/CallLog.java index 3e80ed0..3ec45e9 100644 --- a/core/java/android/provider/CallLog.java +++ b/core/java/android/provider/CallLog.java @@ -24,13 +24,17 @@ import android.content.Context; import android.content.Intent; import android.content.pm.UserInfo; import android.database.Cursor; +import android.location.Country; +import android.location.CountryDetector; import android.net.Uri; import android.os.UserHandle; import android.os.UserManager; import android.provider.ContactsContract.CommonDataKinds.Callable; import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.Data; import android.provider.ContactsContract.DataUsageFeedback; import android.telecom.PhoneAccountHandle; +import android.telephony.PhoneNumberUtils; import android.text.TextUtils; import com.android.internal.telephony.CallerInfo; @@ -404,7 +408,6 @@ public class CallLog { * @param accountHandle The accountHandle object identifying the provider of the call * @param start time stamp for the call in milliseconds * @param duration call duration in seconds - * @param subId the subscription id. * @param dataUsage data usage for the call in bytes, null if data usage was not tracked for * the call. * @param addForAllUsers If true, the call is added to the call log of all currently @@ -503,12 +506,13 @@ public class CallLog { if (cursor != null) { try { if (cursor.getCount() > 0 && cursor.moveToFirst()) { - final Uri feedbackUri = DataUsageFeedback.FEEDBACK_URI.buildUpon() - .appendPath(cursor.getString(0)) - .appendQueryParameter(DataUsageFeedback.USAGE_TYPE, - DataUsageFeedback.USAGE_TYPE_CALL) - .build(); - resolver.update(feedbackUri, new ContentValues(), null, null); + final String dataId = cursor.getString(0); + updateDataUsageStatForData(resolver, dataId); + if (duration >= MIN_DURATION_FOR_NORMALIZED_NUMBER_UPDATE_MS + && callType == Calls.OUTGOING_TYPE + && TextUtils.isEmpty(ci.normalizedNumber)) { + updateNormalizedNumber(context, resolver, dataId, number); + } } } finally { cursor.close(); @@ -581,5 +585,50 @@ public class CallLog { + " LIMIT -1 OFFSET 500)", null); return result; } + + private static void updateDataUsageStatForData(ContentResolver resolver, String dataId) { + final Uri feedbackUri = DataUsageFeedback.FEEDBACK_URI.buildUpon() + .appendPath(dataId) + .appendQueryParameter(DataUsageFeedback.USAGE_TYPE, + DataUsageFeedback.USAGE_TYPE_CALL) + .build(); + resolver.update(feedbackUri, new ContentValues(), null, null); + } + + /* + * Update the normalized phone number for the given dataId in the ContactsProvider, based + * on the user's current country. + */ + private static void updateNormalizedNumber(Context context, ContentResolver resolver, + String dataId, String number) { + if (TextUtils.isEmpty(number) || TextUtils.isEmpty(dataId)) { + return; + } + final String countryIso = getCurrentCountryIso(context); + if (TextUtils.isEmpty(countryIso)) { + return; + } + final String normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, + getCurrentCountryIso(context)); + if (TextUtils.isEmpty(normalizedNumber)) { + return; + } + final ContentValues values = new ContentValues(); + values.put(Phone.NORMALIZED_NUMBER, normalizedNumber); + resolver.update(Data.CONTENT_URI, values, Data._ID + "=?", new String[] {dataId}); + } + + private static String getCurrentCountryIso(Context context) { + String countryIso = null; + final CountryDetector detector = (CountryDetector) context.getSystemService( + Context.COUNTRY_DETECTOR); + if (detector != null) { + final Country country = detector.detectCountry(); + if (country != null) { + countryIso = country.getCountryIso(); + } + } + return countryIso; + } } } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index b1c338e..88c3897 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -787,11 +787,10 @@ public final class Settings { * <p> * Output: Nothing. * @see android.service.notification.NotificationListenerService - * @hide */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_NOTIFICATION_LISTENER_SETTINGS - = "android.settings.NOTIFICATION_LISTENER_SETTINGS"; + = "android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"; /** * @hide @@ -873,8 +872,9 @@ public final class Settings { /** * Activity Action: Show battery saver settings. - * - * @hide + * <p> + * In some cases, a matching Activity may not exist, so ensure you safeguard + * against this. */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_BATTERY_SAVER_SETTINGS @@ -2625,12 +2625,6 @@ public final class Settings { public static final String LOCK_TO_APP_ENABLED = "lock_to_app_enabled"; /** - * Whether lock-to-app will lock the keyguard when exiting. - * @hide - */ - public static final String LOCK_TO_APP_EXIT_LOCKED = "lock_to_app_exit_locked"; - - /** * I am the lolrus. * <p> * Nonzero values indicate that the user has a bukkit. @@ -2704,6 +2698,7 @@ public final class Settings { POINTER_SPEED, VIBRATE_WHEN_RINGING, RINGTONE, + LOCK_TO_APP_ENABLED, NOTIFICATION_SOUND }; @@ -3017,7 +3012,6 @@ public final class Settings { MOVED_TO_GLOBAL.add(Settings.Global.NETSTATS_ENABLED); MOVED_TO_GLOBAL.add(Settings.Global.NETSTATS_GLOBAL_ALERT_BYTES); MOVED_TO_GLOBAL.add(Settings.Global.NETSTATS_POLL_INTERVAL); - MOVED_TO_GLOBAL.add(Settings.Global.NETSTATS_REPORT_XT_OVER_DEV); MOVED_TO_GLOBAL.add(Settings.Global.NETSTATS_SAMPLE_ENABLED); MOVED_TO_GLOBAL.add(Settings.Global.NETSTATS_TIME_CACHE_MAX_AGE); MOVED_TO_GLOBAL.add(Settings.Global.NETSTATS_UID_BUCKET_DURATION); @@ -3667,6 +3661,12 @@ public final class Settings { "lock_biometric_weak_flags"; /** + * Whether lock-to-app will lock the keyguard when exiting. + * @hide + */ + public static final String LOCK_TO_APP_EXIT_LOCKED = "lock_to_app_exit_locked"; + + /** * Whether autolock is enabled (0 = false, 1 = true) */ public static final String LOCK_PATTERN_ENABLED = "lock_pattern_autolock"; @@ -4588,13 +4588,6 @@ public final class Settings { public static final String ANR_SHOW_BACKGROUND = "anr_show_background"; /** - * (Experimental). If nonzero, WebView uses data reduction proxy to save network - * bandwidth. Otherwise, WebView does not use data reduction proxy. - * @hide - */ - public static final String WEBVIEW_DATA_REDUCTION_PROXY = "webview_data_reduction_proxy"; - - /** * The {@link ComponentName} string of the service to be used as the voice recognition * service. * @@ -4739,8 +4732,8 @@ public final class Settings { public static final String SMS_DEFAULT_APPLICATION = "sms_default_application"; /** - * Name of a package that the current user has explicitly allowed to see all of that - * user's notifications. + * Names of the packages that the current user has explicitly allowed to + * see all of the user's notifications, separated by ':'. * * @hide */ @@ -5013,10 +5006,19 @@ public final class Settings { default: throw new IllegalArgumentException("Invalid location mode: " + mode); } - boolean gpsSuccess = Settings.Secure.setLocationProviderEnabledForUser( - cr, LocationManager.GPS_PROVIDER, gps, userId); + // Note it's important that we set the NLP mode first. The Google implementation + // of NLP clears its NLP consent setting any time it receives a + // LocationManager.PROVIDERS_CHANGED_ACTION broadcast and NLP is disabled. Also, + // it shows an NLP consent dialog any time it receives the broadcast, NLP is + // enabled, and the NLP consent is not set. If 1) we were to enable GPS first, + // 2) a setup wizard has its own NLP consent UI that sets the NLP consent setting, + // and 3) the receiver happened to complete before we enabled NLP, then the Google + // NLP would detect the attempt to enable NLP and show a redundant NLP consent + // dialog. Then the people who wrote the setup wizard would be sad. boolean nlpSuccess = Settings.Secure.setLocationProviderEnabledForUser( cr, LocationManager.NETWORK_PROVIDER, network, userId); + boolean gpsSuccess = Settings.Secure.setLocationProviderEnabledForUser( + cr, LocationManager.GPS_PROVIDER, gps, userId); return gpsSuccess && nlpSuccess; } } @@ -5453,8 +5455,6 @@ public final class Settings { public static final String NETSTATS_GLOBAL_ALERT_BYTES = "netstats_global_alert_bytes"; /** {@hide} */ public static final String NETSTATS_SAMPLE_ENABLED = "netstats_sample_enabled"; - /** {@hide} */ - public static final String NETSTATS_REPORT_XT_OVER_DEV = "netstats_report_xt_over_dev"; /** {@hide} */ public static final String NETSTATS_DEV_BUCKET_DURATION = "netstats_dev_bucket_duration"; @@ -6585,6 +6585,14 @@ public final class Settings { public static final String REQUIRE_PASSWORD_TO_DECRYPT = "require_password_to_decrypt"; /** + * Whether the Volte/VT is enabled + * <p> + * Type: int (0 for false, 1 for true) + * @hide + */ + public static final String ENHANCED_4G_MODE_ENABLED = "volte_vt_enabled"; + + /** * Settings to backup. This is here so that it's in the same place as the settings * keys and easy to update. * @@ -6615,7 +6623,8 @@ public final class Settings { WIFI_NUM_OPEN_NETWORKS_KEPT, EMERGENCY_TONE, CALL_AUTO_RETRY, - DOCK_AUDIO_MEDIA_ENABLED + DOCK_AUDIO_MEDIA_ENABLED, + LOW_POWER_MODE_TRIGGER_LEVEL }; // Populated lazily, guarded by class object: diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index 882a3c8..36401eb 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -17,11 +17,13 @@ package android.service.notification; import android.content.ComponentName; +import android.content.Context; import android.content.res.Resources; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; +import android.text.format.DateFormat; import android.util.Slog; import org.xmlpull.v1.XmlPullParser; @@ -32,8 +34,11 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; +import java.util.Locale; import java.util.Objects; +import com.android.internal.R; + /** * Persisted configuration for zen mode. * @@ -73,6 +78,7 @@ public class ZenModeConfig implements Parcelable { private static final String ALLOW_ATT_EVENTS = "events"; private static final String SLEEP_TAG = "sleep"; private static final String SLEEP_ATT_MODE = "mode"; + private static final String SLEEP_ATT_NONE = "none"; private static final String SLEEP_ATT_START_HR = "startHour"; private static final String SLEEP_ATT_START_MIN = "startMin"; @@ -102,6 +108,7 @@ public class ZenModeConfig implements Parcelable { public int sleepStartMinute; // 0-59 public int sleepEndHour; public int sleepEndMinute; + public boolean sleepNone; // false = priority, true = none public ComponentName[] conditionComponents; public Uri[] conditionIds; public Condition exitCondition; @@ -120,6 +127,7 @@ public class ZenModeConfig implements Parcelable { sleepStartMinute = source.readInt(); sleepEndHour = source.readInt(); sleepEndMinute = source.readInt(); + sleepNone = source.readInt() == 1; int len = source.readInt(); if (len > 0) { conditionComponents = new ComponentName[len]; @@ -150,6 +158,7 @@ public class ZenModeConfig implements Parcelable { dest.writeInt(sleepStartMinute); dest.writeInt(sleepEndHour); dest.writeInt(sleepEndMinute); + dest.writeInt(sleepNone ? 1 : 0); if (conditionComponents != null && conditionComponents.length > 0) { dest.writeInt(conditionComponents.length); dest.writeTypedArray(conditionComponents, 0); @@ -177,6 +186,7 @@ public class ZenModeConfig implements Parcelable { .append(",sleepMode=").append(sleepMode) .append(",sleepStart=").append(sleepStartHour).append('.').append(sleepStartMinute) .append(",sleepEnd=").append(sleepEndHour).append('.').append(sleepEndMinute) + .append(",sleepNone=").append(sleepNone) .append(",conditionComponents=") .append(conditionComponents == null ? null : TextUtils.join(",", conditionComponents)) .append(",conditionIds=") @@ -209,6 +219,7 @@ public class ZenModeConfig implements Parcelable { && other.allowFrom == allowFrom && other.allowEvents == allowEvents && Objects.equals(other.sleepMode, sleepMode) + && other.sleepNone == sleepNone && other.sleepStartHour == sleepStartHour && other.sleepStartMinute == sleepStartMinute && other.sleepEndHour == sleepEndHour @@ -221,7 +232,7 @@ public class ZenModeConfig implements Parcelable { @Override public int hashCode() { - return Objects.hash(allowCalls, allowMessages, allowFrom, allowEvents, sleepMode, + return Objects.hash(allowCalls, allowMessages, allowFrom, allowEvents, sleepMode, sleepNone, sleepStartHour, sleepStartMinute, sleepEndHour, sleepEndMinute, Arrays.hashCode(conditionComponents), Arrays.hashCode(conditionIds), exitCondition, exitConditionComponent); @@ -297,6 +308,7 @@ public class ZenModeConfig implements Parcelable { } else if (SLEEP_TAG.equals(tag)) { final String mode = parser.getAttributeValue(null, SLEEP_ATT_MODE); rt.sleepMode = isValidSleepMode(mode)? mode : null; + rt.sleepNone = safeBoolean(parser, SLEEP_ATT_NONE, false); final int startHour = safeInt(parser, SLEEP_ATT_START_HR, 0); final int startMinute = safeInt(parser, SLEEP_ATT_START_MIN, 0); final int endHour = safeInt(parser, SLEEP_ATT_END_HR, 0); @@ -340,6 +352,7 @@ public class ZenModeConfig implements Parcelable { if (sleepMode != null) { out.attribute(null, SLEEP_ATT_MODE, sleepMode); } + out.attribute(null, SLEEP_ATT_NONE, Boolean.toString(sleepNone)); out.attribute(null, SLEEP_ATT_START_HR, Integer.toString(sleepStartHour)); out.attribute(null, SLEEP_ATT_START_MIN, Integer.toString(sleepStartMinute)); out.attribute(null, SLEEP_ATT_END_HR, Integer.toString(sleepEndHour)); @@ -461,25 +474,39 @@ public class ZenModeConfig implements Parcelable { return downtime; } - public static Condition toTimeCondition(int minutesFromNow) { + public static Condition toTimeCondition(Context context, int minutesFromNow) { final long now = System.currentTimeMillis(); final long millis = minutesFromNow == 0 ? ZERO_VALUE_MS : minutesFromNow * MINUTES_MS; - return toTimeCondition(now + millis, minutesFromNow); + return toTimeCondition(context, now + millis, minutesFromNow, now); } - public static Condition toTimeCondition(long time, int minutes) { - final int num = minutes < 60 ? minutes : Math.round(minutes / 60f); - final int resId = minutes < 60 - ? com.android.internal.R.plurals.zen_mode_duration_minutes - : com.android.internal.R.plurals.zen_mode_duration_hours; - final String caption = Resources.getSystem().getQuantityString(resId, num, num); + public static Condition toTimeCondition(Context context, long time, int minutes, long now) { + final int num, summaryResId, line1ResId; + if (minutes < 60) { + // display as minutes + num = minutes; + summaryResId = R.plurals.zen_mode_duration_minutes_summary; + line1ResId = R.plurals.zen_mode_duration_minutes; + } else { + // display as hours + num = Math.round(minutes / 60f); + summaryResId = com.android.internal.R.plurals.zen_mode_duration_hours_summary; + line1ResId = com.android.internal.R.plurals.zen_mode_duration_hours; + } + final String skeleton = DateFormat.is24HourFormat(context) ? "Hm" : "hma"; + final String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton); + final CharSequence formattedTime = DateFormat.format(pattern, time); + final Resources res = context.getResources(); + final String summary = res.getQuantityString(summaryResId, num, num, formattedTime); + final String line1 = res.getQuantityString(line1ResId, num, num, formattedTime); + final String line2 = res.getString(R.string.zen_mode_until, formattedTime); final Uri id = toCountdownConditionId(time); - return new Condition(id, caption, "", "", 0, Condition.STATE_TRUE, + return new Condition(id, summary, line1, line2, 0, Condition.STATE_TRUE, Condition.FLAG_RELEVANT_NOW); } // For built-in conditions - private static final String SYSTEM_AUTHORITY = "android"; + public static final String SYSTEM_AUTHORITY = "android"; // Built-in countdown conditions, e.g. condition://android/countdown/1399917958951 private static final String COUNTDOWN_PATH = "countdown"; diff --git a/core/java/android/service/trust/ITrustAgentService.aidl b/core/java/android/service/trust/ITrustAgentService.aidl index bd80a3f..bb0c2b2 100644 --- a/core/java/android/service/trust/ITrustAgentService.aidl +++ b/core/java/android/service/trust/ITrustAgentService.aidl @@ -15,7 +15,7 @@ */ package android.service.trust; -import android.os.Bundle; +import android.os.PersistableBundle; import android.service.trust.ITrustAgentServiceCallback; /** @@ -25,6 +25,6 @@ import android.service.trust.ITrustAgentServiceCallback; interface ITrustAgentService { oneway void onUnlockAttempt(boolean successful); oneway void onTrustTimeout(); + oneway void onConfigure(in List<PersistableBundle> options, IBinder token); oneway void setCallback(ITrustAgentServiceCallback callback); - oneway void setTrustAgentFeaturesEnabled(in Bundle options, IBinder token); } diff --git a/core/java/android/service/trust/ITrustAgentServiceCallback.aidl b/core/java/android/service/trust/ITrustAgentServiceCallback.aidl index b107bcc..76b2be0 100644 --- a/core/java/android/service/trust/ITrustAgentServiceCallback.aidl +++ b/core/java/android/service/trust/ITrustAgentServiceCallback.aidl @@ -27,5 +27,5 @@ oneway interface ITrustAgentServiceCallback { void grantTrust(CharSequence message, long durationMs, boolean initiatedByUser); void revokeTrust(); void setManagingTrust(boolean managingTrust); - void onSetTrustAgentFeaturesEnabledCompleted(boolean result, IBinder token); + void onConfigureCompleted(boolean result, IBinder token); } diff --git a/core/java/android/service/trust/TrustAgentService.java b/core/java/android/service/trust/TrustAgentService.java index 3ef5b37..d6c997f 100644 --- a/core/java/android/service/trust/TrustAgentService.java +++ b/core/java/android/service/trust/TrustAgentService.java @@ -29,11 +29,14 @@ import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; +import android.os.PersistableBundle; import android.os.RemoteException; import android.os.SystemClock; import android.util.Log; import android.util.Slog; +import java.util.List; + /** * A service that notifies the system about whether it believes the environment of the device * to be trusted. @@ -86,17 +89,22 @@ public class TrustAgentService extends Service { */ public static final String TRUST_AGENT_META_DATA = "android.service.trust.trustagent"; - /** - * A white list of features that the given trust agent should support when otherwise disabled - * by device policy. - * @hide - */ - public static final String KEY_FEATURES = "trust_agent_features"; - private static final int MSG_UNLOCK_ATTEMPT = 1; - private static final int MSG_SET_TRUST_AGENT_FEATURES_ENABLED = 2; + private static final int MSG_CONFIGURE = 2; private static final int MSG_TRUST_TIMEOUT = 3; + /** + * Class containing raw data for a given configuration request. + */ + private static final class ConfigurationData { + final IBinder token; + final List<PersistableBundle> options; + ConfigurationData(List<PersistableBundle> opts, IBinder t) { + options = opts; + token = t; + } + } + private ITrustAgentServiceCallback mCallback; private Runnable mPendingGrantTrustTask; @@ -112,13 +120,12 @@ public class TrustAgentService extends Service { case MSG_UNLOCK_ATTEMPT: onUnlockAttempt(msg.arg1 != 0); break; - case MSG_SET_TRUST_AGENT_FEATURES_ENABLED: - Bundle features = msg.peekData(); - IBinder token = (IBinder) msg.obj; - boolean result = onSetTrustAgentFeaturesEnabled(features); + case MSG_CONFIGURE: + ConfigurationData data = (ConfigurationData) msg.obj; + boolean result = onConfigure(data.options); try { synchronized (mLock) { - mCallback.onSetTrustAgentFeaturesEnabledCompleted(result, token); + mCallback.onConfigureCompleted(result, data.token); } } catch (RemoteException e) { onError("calling onSetTrustAgentFeaturesEnabledCompleted()"); @@ -171,23 +178,16 @@ public class TrustAgentService extends Service { } /** - * Called when device policy wants to restrict features in the agent in response to - * {@link DevicePolicyManager#setTrustAgentFeaturesEnabled(ComponentName, ComponentName, java.util.List) }. - * Agents that support this feature should overload this method and return 'true'. + * Called when device policy admin wants to enable specific options for agent in response to + * {@link DevicePolicyManager#setKeyguardDisabledFeatures(ComponentName, int)} and + * {@link DevicePolicyManager#setTrustAgentConfiguration(ComponentName, ComponentName, + * PersistableBundle)}. + * <p>Agents that support configuration options should overload this method and return 'true'. * - * The list of options can be obtained by calling - * options.getStringArrayList({@link #KEY_FEATURES}). Presence of a feature string in the list - * means it should be enabled ("white-listed"). Absence of the feature means it should be - * disabled. An empty list means all features should be disabled. - * - * This function is only called if {@link DevicePolicyManager#KEYGUARD_DISABLE_TRUST_AGENTS} is - * set. - * - * @param options Option feature bundle. - * @return true if the {@link TrustAgentService} supports this feature. - * @hide + * @param options bundle containing all options or null if none. + * @return true if the {@link TrustAgentService} supports configuration options. */ - public boolean onSetTrustAgentFeaturesEnabled(Bundle options) { + public boolean onConfigure(List<PersistableBundle> options) { return false; } @@ -295,6 +295,12 @@ public class TrustAgentService extends Service { } @Override /* Binder API */ + public void onConfigure(List<PersistableBundle> args, IBinder token) { + mHandler.obtainMessage(MSG_CONFIGURE, new ConfigurationData(args, token)) + .sendToTarget(); + } + + @Override /* Binder API */ public void setCallback(ITrustAgentServiceCallback callback) { synchronized (mLock) { mCallback = callback; @@ -313,13 +319,6 @@ public class TrustAgentService extends Service { } } } - - @Override /* Binder API */ - public void setTrustAgentFeaturesEnabled(Bundle features, IBinder token) { - Message msg = mHandler.obtainMessage(MSG_SET_TRUST_AGENT_FEATURES_ENABLED, token); - msg.setData(features); - msg.sendToTarget(); - } } } diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 26e9a30..ceaf5f8 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -675,7 +675,8 @@ public abstract class WallpaperService extends Service { com.android.internal.R.style.Animation_Wallpaper; mInputChannel = new InputChannel(); if (mSession.addToDisplay(mWindow, mWindow.mSeq, mLayout, View.VISIBLE, - Display.DEFAULT_DISPLAY, mContentInsets, mInputChannel) < 0) { + Display.DEFAULT_DISPLAY, mContentInsets, mStableInsets, + mInputChannel) < 0) { Log.w(TAG, "Failed to add window while updating wallpaper surface."); return; } diff --git a/core/java/android/text/format/DateFormat.java b/core/java/android/text/format/DateFormat.java index 9fec9a1..933bcee 100755 --- a/core/java/android/text/format/DateFormat.java +++ b/core/java/android/text/format/DateFormat.java @@ -60,27 +60,45 @@ import libcore.icu.LocaleData; * {@code SimpleDateFormat}. */ public class DateFormat { - /** @deprecated Use a literal {@code '} instead. */ + /** + * @deprecated Use a literal {@code '} instead. + * @removed + */ @Deprecated public static final char QUOTE = '\''; - /** @deprecated Use a literal {@code 'a'} instead. */ + /** + * @deprecated Use a literal {@code 'a'} instead. + * @removed + */ @Deprecated public static final char AM_PM = 'a'; - /** @deprecated Use a literal {@code 'a'} instead; 'A' was always equivalent to 'a'. */ + /** + * @deprecated Use a literal {@code 'a'} instead; 'A' was always equivalent to 'a'. + * @removed + */ @Deprecated public static final char CAPITAL_AM_PM = 'A'; - /** @deprecated Use a literal {@code 'd'} instead. */ + /** + * @deprecated Use a literal {@code 'd'} instead. + * @removed + */ @Deprecated public static final char DATE = 'd'; - /** @deprecated Use a literal {@code 'E'} instead. */ + /** + * @deprecated Use a literal {@code 'E'} instead. + * @removed + */ @Deprecated public static final char DAY = 'E'; - /** @deprecated Use a literal {@code 'h'} instead. */ + /** + * @deprecated Use a literal {@code 'h'} instead. + * @removed + */ @Deprecated public static final char HOUR = 'h'; @@ -88,31 +106,51 @@ public class DateFormat { * @deprecated Use a literal {@code 'H'} (for compatibility with {@link SimpleDateFormat} * and Unicode) or {@code 'k'} (for compatibility with Android releases up to and including * Jelly Bean MR-1) instead. Note that the two are incompatible. + * + * @removed */ @Deprecated public static final char HOUR_OF_DAY = 'k'; - /** @deprecated Use a literal {@code 'm'} instead. */ + /** + * @deprecated Use a literal {@code 'm'} instead. + * @removed + */ @Deprecated public static final char MINUTE = 'm'; - /** @deprecated Use a literal {@code 'M'} instead. */ + /** + * @deprecated Use a literal {@code 'M'} instead. + * @removed + */ @Deprecated public static final char MONTH = 'M'; - /** @deprecated Use a literal {@code 'L'} instead. */ + /** + * @deprecated Use a literal {@code 'L'} instead. + * @removed + */ @Deprecated public static final char STANDALONE_MONTH = 'L'; - /** @deprecated Use a literal {@code 's'} instead. */ + /** + * @deprecated Use a literal {@code 's'} instead. + * @removed + */ @Deprecated public static final char SECONDS = 's'; - /** @deprecated Use a literal {@code 'z'} instead. */ + /** + * @deprecated Use a literal {@code 'z'} instead. + * @removed + */ @Deprecated public static final char TIME_ZONE = 'z'; - /** @deprecated Use a literal {@code 'y'} instead. */ + /** + * @deprecated Use a literal {@code 'y'} instead. + * @removed + */ @Deprecated public static final char YEAR = 'y'; @@ -306,9 +344,9 @@ public class DateFormat { } /** - * Gets the current date format stored as a char array. The array will contain - * 3 elements ({@link #DATE}, {@link #MONTH}, and {@link #YEAR}) in the order - * specified by the user's format preference. Note that this order is + * Gets the current date format stored as a char array. Returns a 3 element + * array containing the day ({@code 'd'}), month ({@code 'M'}), and year ({@code 'y'})) + * in the order specified by the user's format preference. Note that this order is * <i>only</i> appropriate for all-numeric dates; spelled-out (MEDIUM and LONG) * dates will generally contain other punctuation, spaces, or words, * not just the day, month, and year, and not necessarily in the same diff --git a/core/java/android/text/format/Formatter.java b/core/java/android/text/format/Formatter.java index b0cbcd2..b467f5a 100644 --- a/core/java/android/text/format/Formatter.java +++ b/core/java/android/text/format/Formatter.java @@ -110,6 +110,7 @@ public final class Formatter { private static final int SECONDS_PER_MINUTE = 60; private static final int SECONDS_PER_HOUR = 60 * 60; private static final int SECONDS_PER_DAY = 24 * 60 * 60; + private static final int MILLIS_PER_MINUTE = 1000 * 60; /** * Returns elapsed time for the given millis, in the following format: @@ -171,4 +172,24 @@ public final class Formatter { return context.getString(com.android.internal.R.string.durationSeconds, seconds); } } + + /** + * Returns elapsed time for the given millis, in the following format: + * 1 day 5 hrs; will include at most two units, can go down to minutes precision. + * @param context the application context + * @param millis the elapsed time in milli seconds + * @return the formatted elapsed time + * @hide + */ + public static String formatShortElapsedTimeRoundingUpToMinutes(Context context, long millis) { + long minutesRoundedUp = (millis + MILLIS_PER_MINUTE - 1) / MILLIS_PER_MINUTE; + + if (minutesRoundedUp == 0) { + return context.getString(com.android.internal.R.string.durationMinutes, 0); + } else if (minutesRoundedUp == 1) { + return context.getString(com.android.internal.R.string.durationMinute, 1); + } + + return formatShortElapsedTime(context, minutesRoundedUp * MILLIS_PER_MINUTE); + } } diff --git a/core/java/android/text/format/Time.java b/core/java/android/text/format/Time.java index aa6ad20..1e04eb4 100644 --- a/core/java/android/text/format/Time.java +++ b/core/java/android/text/format/Time.java @@ -48,7 +48,10 @@ import libcore.util.ZoneInfoDB; * <li>Much of the formatting / parsing assumes ASCII text and is therefore not suitable for * use with non-ASCII scripts.</li> * </ul> + * + * @deprecated Use {@link java.util.GregorianCalendar} instead. */ +@Deprecated public class Time { private static final String Y_M_D_T_H_M_S_000 = "%Y-%m-%dT%H:%M:%S.000"; private static final String Y_M_D_T_H_M_S_000_Z = "%Y-%m-%dT%H:%M:%S.000Z"; diff --git a/core/java/android/transition/ChangeBounds.java b/core/java/android/transition/ChangeBounds.java index 0da5fb6..c82587b 100644 --- a/core/java/android/transition/ChangeBounds.java +++ b/core/java/android/transition/ChangeBounds.java @@ -16,7 +16,9 @@ package android.transition; +import android.animation.AnimatorSet; import android.content.Context; +import android.content.res.TypedArray; import android.graphics.PointF; import android.animation.Animator; @@ -31,11 +33,12 @@ import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.util.AttributeSet; -import android.util.IntProperty; import android.util.Property; import android.view.View; import android.view.ViewGroup; +import com.android.internal.R; + import java.util.Map; /** @@ -43,17 +46,20 @@ import java.util.Map; * the scene change and animates those changes during the transition. * * <p>A ChangeBounds transition can be described in a resource file by using the - * tag <code>changeBounds</code>, along with the other standard + * tag <code>changeBounds</code>, using its attributes of + * {@link android.R.styleable#ChangeBounds} along with the other standard * attributes of {@link android.R.styleable#Transition}.</p> */ public class ChangeBounds extends Transition { private static final String PROPNAME_BOUNDS = "android:changeBounds:bounds"; + private static final String PROPNAME_CLIP = "android:changeBounds:clip"; private static final String PROPNAME_PARENT = "android:changeBounds:parent"; private static final String PROPNAME_WINDOW_X = "android:changeBounds:windowX"; private static final String PROPNAME_WINDOW_Y = "android:changeBounds:windowY"; private static final String[] sTransitionProperties = { PROPNAME_BOUNDS, + PROPNAME_CLIP, PROPNAME_PARENT, PROPNAME_WINDOW_X, PROPNAME_WINDOW_Y @@ -77,6 +83,83 @@ public class ChangeBounds extends Transition { } }; + private static final Property<ViewBounds, PointF> TOP_LEFT_PROPERTY = + new Property<ViewBounds, PointF>(PointF.class, "topLeft") { + @Override + public void set(ViewBounds viewBounds, PointF topLeft) { + viewBounds.setTopLeft(topLeft); + } + + @Override + public PointF get(ViewBounds viewBounds) { + return null; + } + }; + + private static final Property<ViewBounds, PointF> BOTTOM_RIGHT_PROPERTY = + new Property<ViewBounds, PointF>(PointF.class, "bottomRight") { + @Override + public void set(ViewBounds viewBounds, PointF bottomRight) { + viewBounds.setBottomRight(bottomRight); + } + + @Override + public PointF get(ViewBounds viewBounds) { + return null; + } + }; + + private static final Property<View, PointF> BOTTOM_RIGHT_ONLY_PROPERTY = + new Property<View, PointF>(PointF.class, "bottomRight") { + @Override + public void set(View view, PointF bottomRight) { + int left = view.getLeft(); + int top = view.getTop(); + int right = Math.round(bottomRight.x); + int bottom = Math.round(bottomRight.y); + view.setLeftTopRightBottom(left, top, right, bottom); + } + + @Override + public PointF get(View view) { + return null; + } + }; + + private static final Property<View, PointF> TOP_LEFT_ONLY_PROPERTY = + new Property<View, PointF>(PointF.class, "topLeft") { + @Override + public void set(View view, PointF topLeft) { + int left = Math.round(topLeft.x); + int top = Math.round(topLeft.y); + int right = view.getRight(); + int bottom = view.getBottom(); + view.setLeftTopRightBottom(left, top, right, bottom); + } + + @Override + public PointF get(View view) { + return null; + } + }; + + private static final Property<View, PointF> POSITION_PROPERTY = + new Property<View, PointF>(PointF.class, "position") { + @Override + public void set(View view, PointF topLeft) { + int left = Math.round(topLeft.x); + int top = Math.round(topLeft.y); + int right = left + view.getWidth(); + int bottom = top + view.getHeight(); + view.setLeftTopRightBottom(left, top, right, bottom); + } + + @Override + public PointF get(View view) { + return null; + } + }; + int[] tempLocation = new int[2]; boolean mResizeClip = false; boolean mReparent = false; @@ -88,6 +171,11 @@ public class ChangeBounds extends Transition { public ChangeBounds(Context context, AttributeSet attrs) { super(context, attrs); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ChangeBounds); + boolean resizeClip = a.getBoolean(R.styleable.ChangeBounds_resizeClip, false); + a.recycle(); + setResizeClip(resizeClip); } @Override @@ -95,11 +183,37 @@ public class ChangeBounds extends Transition { return sTransitionProperties; } + /** + * When <code>resizeClip</code> is true, ChangeBounds resizes the view using the clipBounds + * instead of changing the dimensions of the view during the animation. When + * <code>resizeClip</code> is false, ChangeBounds resizes the View by changing its dimensions. + * + * <p>When resizeClip is set to true, the clip bounds is modified by ChangeBounds. Therefore, + * {@link android.transition.ChangeClipBounds} is not compatible with ChangeBounds + * in this mode.</p> + * + * @param resizeClip Used to indicate whether the view bounds should be modified or the + * clip bounds should be modified by ChangeBounds. + * @see android.view.View#setClipBounds(android.graphics.Rect) + * @attr ref android.R.styleable#ChangeBounds_resizeClip + */ public void setResizeClip(boolean resizeClip) { mResizeClip = resizeClip; } /** + * Returns true when the ChangeBounds will resize by changing the clip bounds during the + * view animation or false when bounds are changed. The default value is false. + * + * @return true when the ChangeBounds will resize by changing the clip bounds during the + * view animation or false when bounds are changed. The default value is false. + * @attr ref android.R.styleable#ChangeBounds_resizeClip + */ + public boolean getResizeClip() { + return mResizeClip; + } + + /** * Setting this flag tells ChangeBounds to track the before/after parent * of every view using this transition. The flag is not enabled by * default because it requires the parent instances to be the same @@ -127,6 +241,9 @@ public class ChangeBounds extends Transition { values.values.put(PROPNAME_WINDOW_X, tempLocation[0]); values.values.put(PROPNAME_WINDOW_Y, tempLocation[1]); } + if (mResizeClip) { + values.values.put(PROPNAME_CLIP, view.getClipBounds()); + } } } @@ -170,158 +287,149 @@ public class ChangeBounds extends Transition { if (parentMatches(startParent, endParent)) { Rect startBounds = (Rect) startValues.values.get(PROPNAME_BOUNDS); Rect endBounds = (Rect) endValues.values.get(PROPNAME_BOUNDS); - int startLeft = startBounds.left; - int endLeft = endBounds.left; - int startTop = startBounds.top; - int endTop = endBounds.top; - int startRight = startBounds.right; - int endRight = endBounds.right; - int startBottom = startBounds.bottom; - int endBottom = endBounds.bottom; - int startWidth = startRight - startLeft; - int startHeight = startBottom - startTop; - int endWidth = endRight - endLeft; - int endHeight = endBottom - endTop; + final int startLeft = startBounds.left; + final int endLeft = endBounds.left; + final int startTop = startBounds.top; + final int endTop = endBounds.top; + final int startRight = startBounds.right; + final int endRight = endBounds.right; + final int startBottom = startBounds.bottom; + final int endBottom = endBounds.bottom; + final int startWidth = startRight - startLeft; + final int startHeight = startBottom - startTop; + final int endWidth = endRight - endLeft; + final int endHeight = endBottom - endTop; + Rect startClip = (Rect) startValues.values.get(PROPNAME_CLIP); + Rect endClip = (Rect) endValues.values.get(PROPNAME_CLIP); int numChanges = 0; if ((startWidth != 0 && startHeight != 0) || (endWidth != 0 && endHeight != 0)) { if (startLeft != endLeft || startTop != endTop) ++numChanges; if (startRight != endRight || startBottom != endBottom) ++numChanges; } + if ((startClip != null && !startClip.equals(endClip)) || + (startClip == null && endClip != null)) { + ++numChanges; + } if (numChanges > 0) { + Animator anim; if (!mResizeClip) { - Animator anim; - if (startWidth == endWidth && startHeight == endHeight) { - view.offsetLeftAndRight(startLeft - view.getLeft()); - view.offsetTopAndBottom(startTop - view.getTop()); - Path positionPath = getPathMotion().getPath(0, 0, endLeft - startLeft, - endTop - startTop); - anim = ObjectAnimator.ofInt(view, new HorizontalOffsetProperty(), - new VerticalOffsetProperty(), positionPath); - } else { - if (startLeft != endLeft) view.setLeft(startLeft); - if (startTop != endTop) view.setTop(startTop); - if (startRight != endRight) view.setRight(startRight); - if (startBottom != endBottom) view.setBottom(startBottom); - ObjectAnimator topLeftAnimator = null; - if (startLeft != endLeft || startTop != endTop) { + view.setLeftTopRightBottom(startLeft, startTop, startRight, startBottom); + if (numChanges == 2) { + if (startWidth == endWidth && startHeight == endHeight) { + Path topLeftPath = getPathMotion().getPath(startLeft, startTop, endLeft, + endTop); + anim = ObjectAnimator.ofObject(view, POSITION_PROPERTY, null, + topLeftPath); + } else { + final ViewBounds viewBounds = new ViewBounds(view); Path topLeftPath = getPathMotion().getPath(startLeft, startTop, endLeft, endTop); - topLeftAnimator = ObjectAnimator - .ofInt(view, "left", "top", topLeftPath); - } - ObjectAnimator bottomRightAnimator = null; - if (startRight != endRight || startBottom != endBottom) { + ObjectAnimator topLeftAnimator = ObjectAnimator + .ofObject(viewBounds, TOP_LEFT_PROPERTY, null, topLeftPath); + Path bottomRightPath = getPathMotion().getPath(startRight, startBottom, endRight, endBottom); - bottomRightAnimator = ObjectAnimator.ofInt(view, "right", "bottom", - bottomRightPath); + ObjectAnimator bottomRightAnimator = ObjectAnimator.ofObject(viewBounds, + BOTTOM_RIGHT_PROPERTY, null, bottomRightPath); + AnimatorSet set = new AnimatorSet(); + set.playTogether(topLeftAnimator, bottomRightAnimator); + anim = set; + set.addListener(new AnimatorListenerAdapter() { + // We need a strong reference to viewBounds until the + // animator ends. + private ViewBounds mViewBounds = viewBounds; + }); } - anim = TransitionUtils.mergeAnimators(topLeftAnimator, - bottomRightAnimator); - } - if (view.getParent() instanceof ViewGroup) { - final ViewGroup parent = (ViewGroup) view.getParent(); - parent.suppressLayout(true); - TransitionListener transitionListener = new TransitionListenerAdapter() { - boolean mCanceled = false; - - @Override - public void onTransitionCancel(Transition transition) { - parent.suppressLayout(false); - mCanceled = true; - } - - @Override - public void onTransitionEnd(Transition transition) { - if (!mCanceled) { - parent.suppressLayout(false); - } - } - - @Override - public void onTransitionPause(Transition transition) { - parent.suppressLayout(false); - } - - @Override - public void onTransitionResume(Transition transition) { - parent.suppressLayout(true); - } - }; - addListener(transitionListener); + } else if (startLeft != endLeft || startTop != endTop) { + Path topLeftPath = getPathMotion().getPath(startLeft, startTop, + endLeft, endTop); + anim = ObjectAnimator.ofObject(view, TOP_LEFT_ONLY_PROPERTY, null, + topLeftPath); + } else { + Path bottomRight = getPathMotion().getPath(startRight, startBottom, + endRight, endBottom); + anim = ObjectAnimator.ofObject(view, BOTTOM_RIGHT_ONLY_PROPERTY, null, + bottomRight); } - return anim; } else { - if (startWidth != endWidth) view.setRight(endLeft + - Math.max(startWidth, endWidth)); - if (startHeight != endHeight) view.setBottom(endTop + - Math.max(startHeight, endHeight)); - // TODO: don't clobber TX/TY - if (startLeft != endLeft) view.setTranslationX(startLeft - endLeft); - if (startTop != endTop) view.setTranslationY(startTop - endTop); - // Animate location with translationX/Y and size with clip bounds - float transXDelta = endLeft - startLeft; - float transYDelta = endTop - startTop; - int widthDelta = endWidth - startWidth; - int heightDelta = endHeight - startHeight; - numChanges = 0; - if (transXDelta != 0) numChanges++; - if (transYDelta != 0) numChanges++; - if (widthDelta != 0 || heightDelta != 0) numChanges++; - ObjectAnimator translationAnimator = null; - if (transXDelta != 0 || transYDelta != 0) { - Path topLeftPath = getPathMotion().getPath(0, 0, transXDelta, transYDelta); - translationAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, - View.TRANSLATION_Y, topLeftPath); + int maxWidth = Math.max(startWidth, endWidth); + int maxHeight = Math.max(startHeight, endHeight); + + view.setLeftTopRightBottom(startLeft, startTop, startLeft + maxWidth, + startTop + maxHeight); + + ObjectAnimator positionAnimator = null; + if (startLeft != endLeft || startTop != endTop) { + Path topLeftPath = getPathMotion().getPath(startLeft, startTop, endLeft, + endTop); + positionAnimator = ObjectAnimator.ofObject(view, POSITION_PROPERTY, null, + topLeftPath); + } + final Rect finalClip = endClip; + if (startClip == null) { + startClip = new Rect(0, 0, startWidth, startHeight); + } + if (endClip == null) { + endClip = new Rect(0, 0, endWidth, endHeight); } ObjectAnimator clipAnimator = null; - if (widthDelta != 0 || heightDelta != 0) { - Rect tempStartBounds = new Rect(0, 0, startWidth, startHeight); - Rect tempEndBounds = new Rect(0, 0, endWidth, endHeight); + if (!startClip.equals(endClip)) { + view.setClipBounds(startClip); clipAnimator = ObjectAnimator.ofObject(view, "clipBounds", sRectEvaluator, - tempStartBounds, tempEndBounds); - } - Animator anim = TransitionUtils.mergeAnimators(translationAnimator, - clipAnimator); - if (view.getParent() instanceof ViewGroup) { - final ViewGroup parent = (ViewGroup) view.getParent(); - parent.suppressLayout(true); - TransitionListener transitionListener = new TransitionListenerAdapter() { - boolean mCanceled = false; + startClip, endClip); + clipAnimator.addListener(new AnimatorListenerAdapter() { + private boolean mIsCanceled; @Override - public void onTransitionCancel(Transition transition) { - parent.suppressLayout(false); - mCanceled = true; + public void onAnimationCancel(Animator animation) { + mIsCanceled = true; } @Override - public void onTransitionEnd(Transition transition) { - if (!mCanceled) { - parent.suppressLayout(false); + public void onAnimationEnd(Animator animation) { + if (!mIsCanceled) { + view.setClipBounds(finalClip); + view.setLeftTopRightBottom(endLeft, endTop, endRight, + endBottom); } } + }); + } + anim = TransitionUtils.mergeAnimators(positionAnimator, + clipAnimator); + } + if (view.getParent() instanceof ViewGroup) { + final ViewGroup parent = (ViewGroup) view.getParent(); + parent.suppressLayout(true); + TransitionListener transitionListener = new TransitionListenerAdapter() { + boolean mCanceled = false; - @Override - public void onTransitionPause(Transition transition) { + @Override + public void onTransitionCancel(Transition transition) { + parent.suppressLayout(false); + mCanceled = true; + } + + @Override + public void onTransitionEnd(Transition transition) { + if (!mCanceled) { parent.suppressLayout(false); } + } + + @Override + public void onTransitionPause(Transition transition) { + parent.suppressLayout(false); + } - @Override - public void onTransitionResume(Transition transition) { - parent.suppressLayout(true); - } - }; - addListener(transitionListener); - } - anim.addListener(new AnimatorListenerAdapter() { @Override - public void onAnimationEnd(Animator animation) { - view.setClipBounds(null); + public void onTransitionResume(Transition transition) { + parent.suppressLayout(true); } - }); - return anim; + }; + addListener(transitionListener); } + return anim; } } else { int startX = (Integer) startValues.values.get(PROPNAME_WINDOW_X); @@ -357,47 +465,41 @@ public class ChangeBounds extends Transition { return null; } - private abstract static class OffsetProperty extends IntProperty<View> { - int mPreviousValue; - - public OffsetProperty(String name) { - super(name); - } - - @Override - public void setValue(View view, int value) { - int offset = value - mPreviousValue; - offsetBy(view, offset); - mPreviousValue = value; - } - - @Override - public Integer get(View object) { - return null; - } - - protected abstract void offsetBy(View view, int by); - } - - private static class HorizontalOffsetProperty extends OffsetProperty { - public HorizontalOffsetProperty() { - super("offsetLeftAndRight"); + private static class ViewBounds { + private int mLeft; + private int mTop; + private int mRight; + private int mBottom; + private boolean mIsTopLeftSet; + private boolean mIsBottomRightSet; + private View mView; + + public ViewBounds(View view) { + mView = view; } - @Override - protected void offsetBy(View view, int by) { - view.offsetLeftAndRight(by); + public void setTopLeft(PointF topLeft) { + mLeft = Math.round(topLeft.x); + mTop = Math.round(topLeft.y); + mIsTopLeftSet = true; + if (mIsBottomRightSet) { + setLeftTopRightBottom(); + } } - } - private static class VerticalOffsetProperty extends OffsetProperty { - public VerticalOffsetProperty() { - super("offsetTopAndBottom"); + public void setBottomRight(PointF bottomRight) { + mRight = Math.round(bottomRight.x); + mBottom = Math.round(bottomRight.y); + mIsBottomRightSet = true; + if (mIsTopLeftSet) { + setLeftTopRightBottom(); + } } - @Override - protected void offsetBy(View view, int by) { - view.offsetTopAndBottom(by); + private void setLeftTopRightBottom() { + mView.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom); + mIsTopLeftSet = false; + mIsBottomRightSet = false; } } } diff --git a/core/java/android/transition/ChangeTransform.java b/core/java/android/transition/ChangeTransform.java index 3fd28a6..a159b40 100644 --- a/core/java/android/transition/ChangeTransform.java +++ b/core/java/android/transition/ChangeTransform.java @@ -17,11 +17,14 @@ package android.transition; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; +import android.animation.FloatArrayEvaluator; import android.animation.ObjectAnimator; -import android.animation.ValueAnimator; +import android.animation.PropertyValuesHolder; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Matrix; +import android.graphics.Path; +import android.graphics.PointF; import android.util.AttributeSet; import android.util.Property; import android.view.GhostView; @@ -56,16 +59,35 @@ public class ChangeTransform extends Transition { PROPNAME_PARENT_MATRIX, }; - private static final Property<View, Matrix> ANIMATION_MATRIX_PROPERTY = - new Property<View, Matrix>(Matrix.class, "animationMatrix") { + /** + * This property sets the animation matrix properties that are not translations. + */ + private static final Property<PathAnimatorMatrix, float[]> NON_TRANSLATIONS_PROPERTY = + new Property<PathAnimatorMatrix, float[]>(float[].class, "nonTranslations") { @Override - public Matrix get(View object) { + public float[] get(PathAnimatorMatrix object) { return null; } @Override - public void set(View object, Matrix value) { - object.setAnimationMatrix(value); + public void set(PathAnimatorMatrix object, float[] value) { + object.setValues(value); + } + }; + + /** + * This property sets the translation animation matrix properties. + */ + private static final Property<PathAnimatorMatrix, PointF> TRANSLATIONS_PROPERTY = + new Property<PathAnimatorMatrix, PointF>(PointF.class, "translations") { + @Override + public PointF get(PathAnimatorMatrix object) { + return null; + } + + @Override + public void set(PathAnimatorMatrix object, PointF value) { + object.setTranslation(value); } }; @@ -261,8 +283,23 @@ public class ChangeTransform extends Transition { final View view = endValues.view; setIdentityTransforms(view); - ObjectAnimator animator = ObjectAnimator.ofObject(view, ANIMATION_MATRIX_PROPERTY, - new TransitionUtils.MatrixEvaluator(), startMatrix, endMatrix); + final float[] startMatrixValues = new float[9]; + startMatrix.getValues(startMatrixValues); + final float[] endMatrixValues = new float[9]; + endMatrix.getValues(endMatrixValues); + final PathAnimatorMatrix pathAnimatorMatrix = + new PathAnimatorMatrix(view, startMatrixValues); + + PropertyValuesHolder valuesProperty = PropertyValuesHolder.ofObject( + NON_TRANSLATIONS_PROPERTY, new FloatArrayEvaluator(new float[9]), + startMatrixValues, endMatrixValues); + Path path = getPathMotion().getPath(startMatrixValues[Matrix.MTRANS_X], + startMatrixValues[Matrix.MTRANS_Y], endMatrixValues[Matrix.MTRANS_X], + endMatrixValues[Matrix.MTRANS_Y]); + PropertyValuesHolder translationProperty = PropertyValuesHolder.ofObject( + TRANSLATIONS_PROPERTY, null, path); + ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(pathAnimatorMatrix, + valuesProperty, translationProperty); final Matrix finalEndMatrix = endMatrix; @@ -285,14 +322,13 @@ public class ChangeTransform extends Transition { view.setTagInternal(R.id.parentMatrix, null); } } - ANIMATION_MATRIX_PROPERTY.set(view, null); + view.setAnimationMatrix(null); transforms.restore(view); } @Override public void onAnimationPause(Animator animation) { - ValueAnimator animator = (ValueAnimator) animation; - Matrix currentMatrix = (Matrix) animator.getAnimatedValue(); + Matrix currentMatrix = pathAnimatorMatrix.getMatrix(); setCurrentMatrix(currentMatrix); } @@ -457,4 +493,47 @@ public class ChangeTransform extends Transition { mGhostView.setVisibility(View.VISIBLE); } } + + /** + * PathAnimatorMatrix allows the translations and the rest of the matrix to be set + * separately. This allows the PathMotion to affect the translations while scale + * and rotation are evaluated separately. + */ + private static class PathAnimatorMatrix { + private final Matrix mMatrix = new Matrix(); + private final View mView; + private final float[] mValues; + private float mTranslationX; + private float mTranslationY; + + public PathAnimatorMatrix(View view, float[] values) { + mView = view; + mValues = values.clone(); + mTranslationX = mValues[Matrix.MTRANS_X]; + mTranslationY = mValues[Matrix.MTRANS_Y]; + setAnimationMatrix(); + } + + public void setValues(float[] values) { + System.arraycopy(values, 0, mValues, 0, values.length); + setAnimationMatrix(); + } + + public void setTranslation(PointF translation) { + mTranslationX = translation.x; + mTranslationY = translation.y; + setAnimationMatrix(); + } + + private void setAnimationMatrix() { + mValues[Matrix.MTRANS_X] = mTranslationX; + mValues[Matrix.MTRANS_Y] = mTranslationY; + mMatrix.setValues(mValues); + mView.setAnimationMatrix(mMatrix); + } + + public Matrix getMatrix() { + return mMatrix; + } + } } diff --git a/core/java/android/transition/Transition.java b/core/java/android/transition/Transition.java index 6dede46..2705bcf 100644 --- a/core/java/android/transition/Transition.java +++ b/core/java/android/transition/Transition.java @@ -84,8 +84,8 @@ import com.android.internal.R; * * <p>Custom transition classes may be instantiated with a <code>transition</code> tag:</p> * <pre><transition class="my.app.transition.CustomTransition"/></pre> - * <p>Custom transition classes loaded from XML must have a public nullary (no argument) - * constructor.</p> + * <p>Custom transition classes loaded from XML should have a public constructor taking + * a {@link android.content.Context} and {@link android.util.AttributeSet}.</p> * * <p>Note that attributes for the transition are not required, just as they are * optional when declared in code; Transitions created from XML resources will use @@ -955,7 +955,7 @@ public abstract class Transition implements Cloneable { * Views with different IDs, or no IDs whatsoever, will be ignored. * * <p>Note that using ids to specify targets implies that ids should be unique - * within the view hierarchy underneat the scene root.</p> + * within the view hierarchy underneath the scene root.</p> * * @see View#getId() * @param targetId The id of a target view, must be a positive number. @@ -1790,6 +1790,10 @@ public abstract class Transition implements Cloneable { private static boolean isValueChanged(TransitionValues oldValues, TransitionValues newValues, String key) { + if (oldValues.values.containsKey(key) != newValues.values.containsKey(key)) { + // The transition didn't care about this particular value, so we don't care, either. + return false; + } Object oldValue = oldValues.values.get(key); Object newValue = newValues.values.get(key); boolean changed; diff --git a/core/java/android/transition/TransitionUtils.java b/core/java/android/transition/TransitionUtils.java index 03423ff..49ceb3b 100644 --- a/core/java/android/transition/TransitionUtils.java +++ b/core/java/android/transition/TransitionUtils.java @@ -22,8 +22,10 @@ import android.animation.TypeEvaluator; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Matrix; +import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; @@ -109,6 +111,35 @@ public class TransitionUtils { } /** + * Get a copy of bitmap of given drawable, return null if intrinsic size is zero + */ + public static Bitmap createDrawableBitmap(Drawable drawable) { + int width = drawable.getIntrinsicWidth(); + int height = drawable.getIntrinsicHeight(); + if (width <= 0 || height <= 0) { + return null; + } + float scale = Math.min(1f, ((float)MAX_IMAGE_SIZE) / (width * height)); + if (drawable instanceof BitmapDrawable && scale == 1f) { + // return same bitmap if scale down not needed + return ((BitmapDrawable) drawable).getBitmap(); + } + int bitmapWidth = (int) (width * scale); + int bitmapHeight = (int) (height * scale); + Bitmap bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + Rect existingBounds = drawable.getBounds(); + int left = existingBounds.left; + int top = existingBounds.top; + int right = existingBounds.right; + int bottom = existingBounds.bottom; + drawable.setBounds(0, 0, bitmapWidth, bitmapHeight); + drawable.draw(canvas); + drawable.setBounds(left, top, right, bottom); + return bitmap; + } + + /** * Creates a Bitmap of the given view, using the Matrix matrix to transform to the local * coordinates. <code>matrix</code> will be modified during the bitmap creation. * diff --git a/core/java/android/transition/Visibility.java b/core/java/android/transition/Visibility.java index f58291f..36bac31 100644 --- a/core/java/android/transition/Visibility.java +++ b/core/java/android/transition/Visibility.java @@ -484,12 +484,19 @@ public abstract class Visibility extends Transition { @Override boolean areValuesChanged(TransitionValues oldValues, TransitionValues newValues) { - VisibilityInfo changeInfo = getVisibilityChangeInfo(oldValues, newValues); if (oldValues == null && newValues == null) { return false; } + if (oldValues != null && newValues != null && + newValues.values.containsKey(PROPNAME_VISIBILITY) != + oldValues.values.containsKey(PROPNAME_VISIBILITY)) { + // The transition wasn't targeted in either the start or end, so it couldn't + // have changed. + return false; + } + VisibilityInfo changeInfo = getVisibilityChangeInfo(oldValues, newValues); return changeInfo.visibilityChange && (changeInfo.startVisibility == View.VISIBLE || - changeInfo.endVisibility == View.VISIBLE); + changeInfo.endVisibility == View.VISIBLE); } /** diff --git a/core/java/android/util/ArraySet.java b/core/java/android/util/ArraySet.java index 423e48b..68f725e 100644 --- a/core/java/android/util/ArraySet.java +++ b/core/java/android/util/ArraySet.java @@ -245,13 +245,20 @@ public final class ArraySet<E> implements Collection<E>, Set<E> { /** * Create a new ArraySet with the mappings from the given ArraySet. */ - public ArraySet(ArraySet set) { + public ArraySet(ArraySet<E> set) { this(); if (set != null) { addAll(set); } } + /** {@hide} */ + public ArraySet(Collection<E> set) { + this(); + if (set != null) { + addAll(set); + } + } /** * Make the array map empty. All storage is released. diff --git a/core/java/android/util/FloatMath.java b/core/java/android/util/FloatMath.java index 0ffd5bd..bdcf5ca 100644 --- a/core/java/android/util/FloatMath.java +++ b/core/java/android/util/FloatMath.java @@ -21,7 +21,10 @@ package android.util; * versions of Android with a JIT, these are significantly slower than * the equivalent {@code Math} functions, which should be used in preference * to these. + * + * @deprecated Use {@link java.lang.Math} instead. */ +@Deprecated public class FloatMath { /** Prevents instantiation. */ diff --git a/core/java/android/util/IntArray.java b/core/java/android/util/IntArray.java new file mode 100644 index 0000000..e8d3947 --- /dev/null +++ b/core/java/android/util/IntArray.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +import com.android.internal.util.ArrayUtils; + +import libcore.util.EmptyArray; + +/** + * Implements a growing array of int primitives. + * + * @hide + */ +public class IntArray implements Cloneable { + private static final int MIN_CAPACITY_INCREMENT = 12; + + private int[] mValues; + private int mSize; + + /** + * Creates an empty IntArray with the default initial capacity. + */ + public IntArray() { + this(10); + } + + /** + * Creates an empty IntArray with the specified initial capacity. + */ + public IntArray(int initialCapacity) { + if (initialCapacity == 0) { + mValues = EmptyArray.INT; + } else { + mValues = ArrayUtils.newUnpaddedIntArray(initialCapacity); + } + mSize = 0; + } + + /** + * Appends the specified value to the end of this array. + */ + public void add(int value) { + add(mSize, value); + } + + /** + * Inserts a value at the specified position in this array. + * + * @throws IndexOutOfBoundsException when index < 0 || index > size() + */ + public void add(int index, int value) { + if (index < 0 || index > mSize) { + throw new IndexOutOfBoundsException(); + } + + ensureCapacity(1); + + if (mSize - index != 0) { + System.arraycopy(mValues, index, mValues, index + 1, mSize - index); + } + + mValues[index] = value; + mSize++; + } + + /** + * Adds the values in the specified array to this array. + */ + public void addAll(IntArray values) { + final int count = values.mSize; + ensureCapacity(count); + + System.arraycopy(values.mValues, 0, mValues, mSize, count); + mSize += count; + } + + /** + * Ensures capacity to append at least <code>count</code> values. + */ + private void ensureCapacity(int count) { + final int currentSize = mSize; + final int minCapacity = currentSize + count; + if (minCapacity >= mValues.length) { + final int targetCap = currentSize + (currentSize < (MIN_CAPACITY_INCREMENT / 2) ? + MIN_CAPACITY_INCREMENT : currentSize >> 1); + final int newCapacity = targetCap > minCapacity ? targetCap : minCapacity; + final int[] newValues = ArrayUtils.newUnpaddedIntArray(newCapacity); + System.arraycopy(mValues, 0, newValues, 0, currentSize); + mValues = newValues; + } + } + + /** + * Removes all values from this array. + */ + public void clear() { + mSize = 0; + } + + @Override + public IntArray clone() throws CloneNotSupportedException { + final IntArray clone = (IntArray) super.clone(); + clone.mValues = mValues.clone(); + return clone; + } + + /** + * Returns the value at the specified position in this array. + */ + public int get(int index) { + if (index >= mSize) { + throw new ArrayIndexOutOfBoundsException(mSize, index); + } + return mValues[index]; + } + + /** + * Returns the index of the first occurrence of the specified value in this + * array, or -1 if this array does not contain the value. + */ + public int indexOf(int value) { + final int n = mSize; + for (int i = 0; i < n; i++) { + if (mValues[i] == value) { + return i; + } + } + return -1; + } + + /** + * Removes the value at the specified index from this array. + */ + public void remove(int index) { + if (index >= mSize) { + throw new ArrayIndexOutOfBoundsException(mSize, index); + } + System.arraycopy(mValues, index + 1, mValues, index, mSize - index - 1); + mSize--; + } + + /** + * Returns the number of values in this array. + */ + public int size() { + return mSize; + } +} diff --git a/core/java/android/util/PathParser.java b/core/java/android/util/PathParser.java index 6820f77..e5f3b2c 100644 --- a/core/java/android/util/PathParser.java +++ b/core/java/android/util/PathParser.java @@ -34,7 +34,11 @@ public class PathParser { Path path = new Path(); PathDataNode[] nodes = createNodesFromPathData(pathData); if (nodes != null) { - PathDataNode.nodesToPath(nodes, path); + try { + PathDataNode.nodesToPath(nodes, path); + } catch (RuntimeException e) { + throw new RuntimeException("Error in parsing " + pathData, e); + } return path; } return null; @@ -128,7 +132,12 @@ public class PathParser { while (end < s.length()) { c = s.charAt(end); - if (((c - 'A') * (c - 'Z') <= 0) || (((c - 'a') * (c - 'z') <= 0))) { + // Note that 'e' or 'E' are not valid path commands, but could be + // used for floating point numbers' scientific notation. + // Therefore, when searching for next command, we should ignore 'e' + // and 'E'. + if ((((c - 'A') * (c - 'Z') <= 0) || ((c - 'a') * (c - 'z') <= 0)) + && c != 'e' && c != 'E') { return end; } end++; @@ -142,9 +151,9 @@ public class PathParser { private static class ExtractFloatResult { // We need to return the position of the next separator and whether the - // next float starts with a '-'. + // next float starts with a '-' or a '.'. int mEndPosition; - boolean mEndWithNegSign; + boolean mEndWithNegOrDot; } /** @@ -179,8 +188,8 @@ public class PathParser { s.substring(startPosition, endPosition)); } - if (result.mEndWithNegSign) { - // Keep the '-' sign with next number. + if (result.mEndWithNegOrDot) { + // Keep the '-' or '.' sign with next number. startPosition = endPosition; } else { startPosition = endPosition + 1; @@ -188,8 +197,7 @@ public class PathParser { } return Arrays.copyOf(results, count); } catch (NumberFormatException e) { - Log.e(LOGTAG, "error in parsing \"" + s + "\""); - throw e; + throw new RuntimeException("error in parsing \"" + s + "\"", e); } } @@ -201,11 +209,15 @@ public class PathParser { * the starting position of next number, whether it is ending with a '-'. */ private static void extract(String s, int start, ExtractFloatResult result) { - // Now looking for ' ', ',' or '-' from the start. + // Now looking for ' ', ',', '.' or '-' from the start. int currentIndex = start; boolean foundSeparator = false; - result.mEndWithNegSign = false; + result.mEndWithNegOrDot = false; + boolean secondDot = false; + boolean isExponential = false; for (; currentIndex < s.length(); currentIndex++) { + boolean isPrevExponential = isExponential; + isExponential = false; char currentChar = s.charAt(currentIndex); switch (currentChar) { case ' ': @@ -213,11 +225,25 @@ public class PathParser { foundSeparator = true; break; case '-': - if (currentIndex != start) { + // The negative sign following a 'e' or 'E' is not a separator. + if (currentIndex != start && !isPrevExponential) { foundSeparator = true; - result.mEndWithNegSign = true; + result.mEndWithNegOrDot = true; } break; + case '.': + if (!secondDot) { + secondDot = true; + } else { + // This is the second dot, and it is considered as a separator. + foundSeparator = true; + result.mEndWithNegOrDot = true; + } + break; + case 'e': + case 'E': + isExponential = true; + break; } if (foundSeparator) { break; diff --git a/core/java/android/util/TypedValue.java b/core/java/android/util/TypedValue.java index 931fb81..74d4245 100644 --- a/core/java/android/util/TypedValue.java +++ b/core/java/android/util/TypedValue.java @@ -139,6 +139,17 @@ public class TypedValue { /* ------------------------------------------------------------ */ /** + * {@link #TYPE_NULL} data indicating the value was not specified. + */ + public static final int DATA_NULL_UNDEFINED = 0; + /** + * {@link #TYPE_NULL} data indicating the value was explicitly set to null. + */ + public static final int DATA_NULL_EMPTY = 1; + + /* ------------------------------------------------------------ */ + + /** * If {@link #density} is equal to this value, then the density should be * treated as the system's default density value: {@link DisplayMetrics#DENSITY_DEFAULT}. */ @@ -301,6 +312,18 @@ public class TypedValue { } /** + * Return the complex unit type for this value. For example, a dimen type + * with value 12sp will return {@link #COMPLEX_UNIT_SP}. Only use for values + * whose type is {@link #TYPE_DIMENSION}. + * + * @return The complex unit type. + */ + public int getComplexUnit() + { + return COMPLEX_UNIT_MASK & (data>>TypedValue.COMPLEX_UNIT_SHIFT); + } + + /** * Converts an unpacked complex data value holding a dimension to its final floating * point value. The two parameters <var>unit</var> and <var>value</var> * are as in {@link #TYPE_DIMENSION}. diff --git a/core/java/android/view/AccessibilityInteractionController.java b/core/java/android/view/AccessibilityInteractionController.java index 1cadf69..5e05683 100644 --- a/core/java/android/view/AccessibilityInteractionController.java +++ b/core/java/android/view/AccessibilityInteractionController.java @@ -1109,15 +1109,17 @@ final class AccessibilityInteractionController { || accessibilityViewId == providerHost.getAccessibilityViewId()) { final AccessibilityNodeInfo parent; if (virtualDescendantId != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) { - parent = provider.createAccessibilityNodeInfo( - virtualDescendantId); + parent = provider.createAccessibilityNodeInfo(virtualDescendantId); } else { - parent= provider.createAccessibilityNodeInfo( + parent = provider.createAccessibilityNodeInfo( AccessibilityNodeProvider.HOST_VIEW_ID); } - if (parent != null) { - outInfos.add(parent); + if (parent == null) { + // Couldn't obtain the parent, which means we have a + // disconnected sub-tree. Abort prefetch immediately. + return; } + outInfos.add(parent); parentNodeId = parent.getParentNodeId(); accessibilityViewId = AccessibilityNodeInfo.getAccessibilityViewId( parentNodeId); diff --git a/core/java/android/view/GLES20Canvas.java b/core/java/android/view/GLES20Canvas.java index 1b57c24..b86455a 100644 --- a/core/java/android/view/GLES20Canvas.java +++ b/core/java/android/view/GLES20Canvas.java @@ -242,7 +242,7 @@ class GLES20Canvas extends HardwareCanvas { void drawHardwareLayer(HardwareLayer layer, float x, float y, Paint paint) { layer.setLayerPaint(paint); - nDrawLayer(mRenderer, layer.getLayer(), x, y); + nDrawLayer(mRenderer, layer.getLayerHandle(), x, y); } private static native void nDrawLayer(long renderer, long layer, float x, float y); diff --git a/core/java/android/view/HardwareLayer.java b/core/java/android/view/HardwareLayer.java index 0c2e944..a130bda 100644 --- a/core/java/android/view/HardwareLayer.java +++ b/core/java/android/view/HardwareLayer.java @@ -126,8 +126,8 @@ final class HardwareLayer { mRenderer.detachSurfaceTexture(mFinalizer.get()); } - public long getLayer() { - return nGetLayer(mFinalizer.get()); + public long getLayerHandle() { + return mFinalizer.get(); } public void setSurfaceTexture(SurfaceTexture surface) { @@ -153,6 +153,5 @@ final class HardwareLayer { private static native void nUpdateRenderLayer(long layerUpdater, long displayList, int left, int top, int right, int bottom); - private static native long nGetLayer(long layerUpdater); private static native int nGetTexName(long layerUpdater); } diff --git a/core/java/android/view/IWindow.aidl b/core/java/android/view/IWindow.aidl index 3e7aae0..9fc80fc 100644 --- a/core/java/android/view/IWindow.aidl +++ b/core/java/android/view/IWindow.aidl @@ -85,4 +85,9 @@ oneway interface IWindow { * is done. */ void doneAnimating(); + + /** + * Called for non-application windows when the enter animation has completed. + */ + void dispatchWindowShown(); } diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index 6aa86c7..7b20e72 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -96,6 +96,7 @@ interface IWindowManager void overridePendingAppTransitionAspectScaledThumb(in Bitmap srcThumb, int startX, int startY, int targetWidth, int targetHeight, IRemoteCallback startedCallback, boolean scaleUp); + void overridePendingAppTransitionInPlace(String packageName, int anim); void executeAppTransition(); void setAppStartingWindow(IBinder token, String pkg, int theme, in CompatibilityInfo compatInfo, CharSequence nonLocalizedLabel, int labelRes, diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl index 037ed28..7b13e84 100644 --- a/core/java/android/view/IWindowSession.aidl +++ b/core/java/android/view/IWindowSession.aidl @@ -36,15 +36,16 @@ import android.view.Surface; */ interface IWindowSession { int add(IWindow window, int seq, in WindowManager.LayoutParams attrs, - in int viewVisibility, out Rect outContentInsets, + in int viewVisibility, out Rect outContentInsets, out Rect outStableInsets, out InputChannel outInputChannel); int addToDisplay(IWindow window, int seq, in WindowManager.LayoutParams attrs, in int viewVisibility, in int layerStackId, out Rect outContentInsets, - out InputChannel outInputChannel); + out Rect outStableInsets, out InputChannel outInputChannel); int addWithoutInputChannel(IWindow window, int seq, in WindowManager.LayoutParams attrs, - in int viewVisibility, out Rect outContentInsets); + in int viewVisibility, out Rect outContentInsets, out Rect outStableInsets); int addToDisplayWithoutInputChannel(IWindow window, int seq, in WindowManager.LayoutParams attrs, - in int viewVisibility, in int layerStackId, out Rect outContentInsets); + in int viewVisibility, in int layerStackId, out Rect outContentInsets, + out Rect outStableInsets); void remove(IWindow window); /** diff --git a/core/java/android/view/RenderNodeAnimator.java b/core/java/android/view/RenderNodeAnimator.java index debf45d..b95f9a4 100644 --- a/core/java/android/view/RenderNodeAnimator.java +++ b/core/java/android/view/RenderNodeAnimator.java @@ -219,7 +219,7 @@ public class RenderNodeAnimator extends Animator { @Override public void cancel() { - if (mState != STATE_FINISHED) { + if (mState != STATE_PREPARE && mState != STATE_FINISHED) { if (mState == STATE_DELAYED) { getHelper().removeDelayedAnimation(this); notifyStartListeners(); diff --git a/core/java/android/view/Surface.java b/core/java/android/view/Surface.java index da684e8..19142b8 100644 --- a/core/java/android/view/Surface.java +++ b/core/java/android/view/Surface.java @@ -311,7 +311,10 @@ public class Surface implements Parcelable { * Unlike {@link #lockCanvas(Rect)} this will return a hardware-accelerated * canvas. See the <a href="{@docRoot}guide/topics/graphics/hardware-accel.html#unsupported"> * unsupported drawing operations</a> for a list of what is and isn't - * supported in a hardware-accelerated canvas. + * supported in a hardware-accelerated canvas. It is also required to + * fully cover the surface every time {@link #lockHardwareCanvas()} is + * called as the buffer is not preserved between frames. Partial updates + * are not supported. * * @return A canvas for drawing into the surface. * diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java index afc804c..49be57d 100644 --- a/core/java/android/view/SurfaceView.java +++ b/core/java/android/view/SurfaceView.java @@ -496,7 +496,8 @@ public class SurfaceView extends View { mLayout.type = mWindowType; mLayout.gravity = Gravity.START|Gravity.TOP; mSession.addToDisplayWithoutInputChannel(mWindow, mWindow.mSeq, mLayout, - mVisible ? VISIBLE : GONE, display.getDisplayId(), mContentInsets); + mVisible ? VISIBLE : GONE, display.getDisplayId(), mContentInsets, + mStableInsets); } boolean realSizeChanged; diff --git a/core/java/android/view/ThreadedRenderer.java b/core/java/android/view/ThreadedRenderer.java index 1e46517..5579c13 100644 --- a/core/java/android/view/ThreadedRenderer.java +++ b/core/java/android/view/ThreadedRenderer.java @@ -66,6 +66,8 @@ public class ThreadedRenderer extends HardwareRenderer { private static final int SYNC_OK = 0; // Needs a ViewRoot invalidate private static final int SYNC_INVALIDATE_REQUIRED = 1 << 0; + // Spoiler: the reward is GPU-accelerated drawing, better find that Surface! + private static final int SYNC_LOST_SURFACE_REWARD_IF_FOUND = 1 << 1; private static final String[] VISUALIZERS = { PROFILE_PROPERTY_VISUALIZE_BARS, @@ -191,7 +193,8 @@ public class ThreadedRenderer extends HardwareRenderer { final float lightX = width / 2.0f; mWidth = width; mHeight = height; - if (surfaceInsets != null && !surfaceInsets.isEmpty()) { + if (surfaceInsets != null && (surfaceInsets.left != 0 || surfaceInsets.right != 0 + || surfaceInsets.top != 0 || surfaceInsets.bottom != 0)) { mHasInsets = true; mInsetLeft = surfaceInsets.left; mInsetTop = surfaceInsets.top; @@ -255,6 +258,9 @@ public class ThreadedRenderer extends HardwareRenderer { mProfilingEnabled = wantProfiling; changed = true; } + if (changed) { + invalidateRoot(); + } return changed; } @@ -332,6 +338,12 @@ public class ThreadedRenderer extends HardwareRenderer { int syncResult = nSyncAndDrawFrame(mNativeProxy, frameTimeNanos, recordDuration, view.getResources().getDisplayMetrics().density); + if ((syncResult & SYNC_LOST_SURFACE_REWARD_IF_FOUND) != 0) { + setEnabled(false); + // Invalidate since we failed to draw. This should fetch a Surface + // if it is still needed or do nothing if we are no longer drawing + attachInfo.mViewRootImpl.invalidate(); + } if ((syncResult & SYNC_INVALIDATE_REQUIRED) != 0) { attachInfo.mViewRootImpl.invalidate(); } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 1ecc8d9..1d09696 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -3109,6 +3109,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private MatchLabelForPredicate mMatchLabelForPredicate; /** + * Specifies a view before which this one is visited in accessibility traversal. + */ + private int mAccessibilityTraversalBeforeId = NO_ID; + + /** + * Specifies a view after which this one is visited in accessibility traversal. + */ + private int mAccessibilityTraversalAfterId = NO_ID; + + /** * Predicate for matching a view by its id. */ private MatchIdPredicate mMatchIdPredicate; @@ -3229,6 +3239,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ private ArrayList<OnLayoutChangeListener> mOnLayoutChangeListeners; + protected OnScrollChangeListener mOnScrollChangeListener; + /** * Listeners for attach events. */ @@ -3886,6 +3898,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, case com.android.internal.R.styleable.View_contentDescription: setContentDescription(a.getString(attr)); break; + case com.android.internal.R.styleable.View_accessibilityTraversalBefore: + setAccessibilityTraversalBefore(a.getResourceId(attr, NO_ID)); + break; + case com.android.internal.R.styleable.View_accessibilityTraversalAfter: + setAccessibilityTraversalAfter(a.getResourceId(attr, NO_ID)); + break; case com.android.internal.R.styleable.View_labelFor: setLabelFor(a.getResourceId(attr, NO_ID)); break; @@ -4606,6 +4624,17 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Register a callback to be invoked when the scroll position of this view + * changed. + * + * @param l The callback that will run. + * @hide Only used internally. + */ + public void setOnScrollChangeListener(OnScrollChangeListener l) { + getListenerInfo().mOnScrollChangeListener = l; + } + + /** * Register a callback to be invoked when focus of this view changed. * * @param l The callback that will run. @@ -5598,6 +5627,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (rootView == null) { rootView = this; } + View label = rootView.findLabelForView(this, mID); if (label != null) { info.setLabeledBy(label); @@ -5626,6 +5656,30 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } } + if (mAccessibilityTraversalBeforeId != View.NO_ID) { + View rootView = getRootView(); + if (rootView == null) { + rootView = this; + } + View next = rootView.findViewInsideOutShouldExist(this, + mAccessibilityTraversalBeforeId); + if (next != null) { + info.setTraversalBefore(next); + } + } + + if (mAccessibilityTraversalAfterId != View.NO_ID) { + View rootView = getRootView(); + if (rootView == null) { + rootView = this; + } + View next = rootView.findViewInsideOutShouldExist(this, + mAccessibilityTraversalAfterId); + if (next != null) { + info.setTraversalAfter(next); + } + } + info.setVisibleToUser(isVisibleToUser()); info.setPackageName(mContext.getPackageName()); @@ -5877,6 +5931,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return true; } + /** + * Adds the clickable rectangles withing the bounds of this view. They + * may overlap. This method is intended for use only by the accessibility + * layer. + * + * @param outRects List to which to add clickable areas. + */ + void addClickableRectsForAccessibility(List<RectF> outRects) { + if (isClickable() || isLongClickable()) { + RectF bounds = new RectF(); + bounds.set(0, 0, getWidth(), getHeight()); + outRects.add(bounds); + } + } + static void offsetRects(List<RectF> rects, float offsetX, float offsetY) { final int rectCount = rects.size(); for (int i = 0; i < rectCount; i++) { @@ -6015,6 +6084,94 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Sets the id of a view before which this one is visited in accessibility traversal. + * A screen-reader must visit the content of this view before the content of the one + * it precedes. For example, if view B is set to be before view A, then a screen-reader + * will traverse the entire content of B before traversing the entire content of A, + * regardles of what traversal strategy it is using. + * <p> + * Views that do not have specified before/after relationships are traversed in order + * determined by the screen-reader. + * </p> + * <p> + * Setting that this view is before a view that is not important for accessibility + * or if this view is not important for accessibility will have no effect as the + * screen-reader is not aware of unimportant views. + * </p> + * + * @param beforeId The id of a view this one precedes in accessibility traversal. + * + * @attr ref android.R.styleable#View_accessibilityTraversalBefore + * + * @see #setImportantForAccessibility(int) + */ + @RemotableViewMethod + public void setAccessibilityTraversalBefore(int beforeId) { + if (mAccessibilityTraversalBeforeId == beforeId) { + return; + } + mAccessibilityTraversalBeforeId = beforeId; + notifyViewAccessibilityStateChangedIfNeeded( + AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); + } + + /** + * Gets the id of a view before which this one is visited in accessibility traversal. + * + * @return The id of a view this one precedes in accessibility traversal if + * specified, otherwise {@link #NO_ID}. + * + * @see #setAccessibilityTraversalBefore(int) + */ + public int getAccessibilityTraversalBefore() { + return mAccessibilityTraversalBeforeId; + } + + /** + * Sets the id of a view after which this one is visited in accessibility traversal. + * A screen-reader must visit the content of the other view before the content of this + * one. For example, if view B is set to be after view A, then a screen-reader + * will traverse the entire content of A before traversing the entire content of B, + * regardles of what traversal strategy it is using. + * <p> + * Views that do not have specified before/after relationships are traversed in order + * determined by the screen-reader. + * </p> + * <p> + * Setting that this view is after a view that is not important for accessibility + * or if this view is not important for accessibility will have no effect as the + * screen-reader is not aware of unimportant views. + * </p> + * + * @param afterId The id of a view this one succedees in accessibility traversal. + * + * @attr ref android.R.styleable#View_accessibilityTraversalAfter + * + * @see #setImportantForAccessibility(int) + */ + @RemotableViewMethod + public void setAccessibilityTraversalAfter(int afterId) { + if (mAccessibilityTraversalAfterId == afterId) { + return; + } + mAccessibilityTraversalAfterId = afterId; + notifyViewAccessibilityStateChangedIfNeeded( + AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); + } + + /** + * Gets the id of a view after which this one is visited in accessibility traversal. + * + * @return The id of a view this one succeedes in accessibility traversal if + * specified, otherwise {@link #NO_ID}. + * + * @see #setAccessibilityTraversalAfter(int) + */ + public int getAccessibilityTraversalAfter() { + return mAccessibilityTraversalAfterId; + } + + /** * Gets the id of a view for which this view serves as a label for * accessibility purposes. * @@ -6033,11 +6190,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ @RemotableViewMethod public void setLabelFor(int id) { + if (mLabelForId == id) { + return; + } mLabelForId = id; if (mLabelForId != View.NO_ID && mID == View.NO_ID) { mID = generateViewId(); } + notifyViewAccessibilityStateChangedIfNeeded( + AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); } /** @@ -9779,6 +9941,29 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (ai != null) { ai.mViewScrollChanged = true; } + + if (mListenerInfo != null && mListenerInfo.mOnScrollChangeListener != null) { + mListenerInfo.mOnScrollChangeListener.onScrollChange(this, l, t, oldl, oldt); + } + } + + /** + * Interface definition for a callback to be invoked when the scroll + * position of a view changes. + * + * @hide Only used internally. + */ + public interface OnScrollChangeListener { + /** + * Called when the scroll position of a view changes. + * + * @param v The view whose scroll position has changed. + * @param scrollX Current horizontal scroll origin. + * @param scrollY Current vertical scroll origin. + * @param oldScrollX Previous horizontal scroll origin. + * @param oldScrollY Previous vertical scroll origin. + */ + void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY); } /** @@ -14047,7 +14232,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } else { draw(canvas); } - drawAccessibilityFocus(canvas); } } finally { renderNode.end(canvas); @@ -14342,7 +14526,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } else { draw(canvas); } - drawAccessibilityFocus(canvas); canvas.restoreToCount(restoreCount); canvas.setBitmap(null); @@ -14417,7 +14600,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } else { draw(canvas); } - drawAccessibilityFocus(canvas); mPrivateFlags = flags; @@ -15015,13 +15197,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) { mPrivateFlags &= ~PFLAG_DIRTY_MASK; dispatchDraw(canvas); - if (mOverlay != null && !mOverlay.isEmpty()) { - mOverlay.getOverlayView().draw(canvas); - } } else { draw(canvas); } - drawAccessibilityFocus(canvas); } else { mPrivateFlags &= ~PFLAG_DIRTY_MASK; ((HardwareCanvas) canvas).drawRenderNode(renderNode, null, flags); @@ -15273,50 +15451,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * Draws the accessibility focus rect onto the specified canvas. - * - * @param canvas Canvas on which to draw the focus rect - */ - private void drawAccessibilityFocus(Canvas canvas) { - if (mAttachInfo == null) { - return; - } - - final Rect bounds = mAttachInfo.mTmpInvalRect; - final ViewRootImpl viewRoot = getViewRootImpl(); - if (viewRoot == null || viewRoot.getAccessibilityFocusedHost() != this) { - return; - } - - final AccessibilityManager manager = AccessibilityManager.getInstance(mContext); - if (!manager.isEnabled() || !manager.isTouchExplorationEnabled()) { - return; - } - - final Drawable drawable = viewRoot.getAccessibilityFocusedDrawable(); - if (drawable == null) { - return; - } - - final AccessibilityNodeInfo virtualView = viewRoot.getAccessibilityFocusedVirtualView(); - if (virtualView != null) { - virtualView.getBoundsInScreen(bounds); - final int[] offset = mAttachInfo.mTmpLocation; - getLocationOnScreen(offset); - bounds.offset(-offset[0], -offset[1]); - } else { - bounds.set(0, 0, mRight - mLeft, mBottom - mTop); - } - - canvas.save(); - canvas.translate(mScrollX, mScrollY); - canvas.clipRect(bounds, Region.Op.REPLACE); - drawable.setBounds(bounds); - drawable.draw(canvas); - canvas.restore(); - } - - /** * Draws the background onto the specified canvas. * * @param canvas Canvas on which to draw the background @@ -15676,7 +15810,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, sizeChange(newWidth, newHeight, oldWidth, oldHeight); } - if ((mViewFlags & VISIBILITY_MASK) == VISIBLE) { + if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) { // If we are visible, force the DRAWN bit to on so that // this invalidate will go through (at least to our parent). // This is because someone may have invalidated this view @@ -15699,6 +15833,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return changed; } + /** + * Same as setFrame, but public and hidden. For use in {@link android.transition.ChangeBounds}. + * @hide + */ + public void setLeftTopRightBottom(int left, int top, int right, int bottom) { + setFrame(left, top, right, bottom); + } + private void sizeChange(int newWidth, int newHeight, int oldWidth, int oldHeight) { onSizeChanged(newWidth, newHeight, oldWidth, oldHeight); if (mOverlay != null) { @@ -16330,6 +16472,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (tintInfo.mHasTintMode) { mBackground.setTintMode(tintInfo.mTintMode); } + + // The drawable (or one of its children) may not have been + // stateful before applying the tint, so let's try again. + if (mBackground.isStateful()) { + mBackground.setState(getDrawableState()); + } } } } @@ -16617,8 +16765,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, invalidate(true); refreshDrawableState(); dispatchSetSelected(selected); - notifyViewAccessibilityStateChangedIfNeeded( - AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); + if (selected) { + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + } else { + notifyViewAccessibilityStateChangedIfNeeded( + AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); + } } } @@ -17414,17 +17566,22 @@ public class View implements Drawable.Callback, KeyEvent.Callback, long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL; if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2); - if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT || - widthMeasureSpec != mOldWidthMeasureSpec || - heightMeasureSpec != mOldHeightMeasureSpec) { + final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT; + final boolean isExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY && + MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY; + final boolean matchingSize = isExactly && + getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec) && + getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec); + if (forceLayout || !matchingSize && + (widthMeasureSpec != mOldWidthMeasureSpec || + heightMeasureSpec != mOldHeightMeasureSpec)) { // first clears the measured dimension flag mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET; resolveRtlPropertiesIfNeeded(); - int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 : - mMeasureCache.indexOfKey(key); + int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key); if (cacheIndex < 0 || sIgnoreMeasureCache) { // measure ourselves, this should set the measured dimension flag back onMeasure(widthMeasureSpec, heightMeasureSpec); diff --git a/core/java/android/view/ViewDebug.java b/core/java/android/view/ViewDebug.java index 12a49d5..50e64c6 100644 --- a/core/java/android/view/ViewDebug.java +++ b/core/java/android/view/ViewDebug.java @@ -1005,12 +1005,21 @@ public class ViewDebug { return fields; } - final ArrayList<Field> foundFields = new ArrayList<Field>(); - fields = klass.getDeclaredFields(); + final ArrayList<Field> declaredFields = new ArrayList(); + klass.getDeclaredFieldsUnchecked(false, declaredFields); - int count = fields.length; + final ArrayList<Field> foundFields = new ArrayList<Field>(); + final int count = declaredFields.size(); for (int i = 0; i < count; i++) { - final Field field = fields[i]; + final Field field = declaredFields.get(i); + + // Ensure the field type can be resolved. + try { + field.getType(); + } catch (NoClassDefFoundError e) { + continue; + } + if (field.isAnnotationPresent(ExportedProperty.class)) { field.setAccessible(true); foundFields.add(field); @@ -1039,12 +1048,22 @@ public class ViewDebug { return methods; } - final ArrayList<Method> foundMethods = new ArrayList<Method>(); - methods = klass.getDeclaredMethods(); + final ArrayList<Method> declaredMethods = new ArrayList(); + klass.getDeclaredMethodsUnchecked(false, declaredMethods); - int count = methods.length; + final ArrayList<Method> foundMethods = new ArrayList<Method>(); + final int count = declaredMethods.size(); for (int i = 0; i < count; i++) { - final Method method = methods[i]; + final Method method = declaredMethods.get(i); + + // Ensure the method return and parameter types can be resolved. + try { + method.getReturnType(); + method.getParameterTypes(); + } catch (NoClassDefFoundError e) { + continue; + } + if (method.getParameterTypes().length == 0 && method.isAnnotationPresent(ExportedProperty.class) && method.getReturnType() != Void.class) { diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 4116b6b..654a8ed 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -51,8 +51,10 @@ import com.android.internal.util.Predicate; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; @@ -468,6 +470,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager @ViewDebug.ExportedProperty(category = "layout") private int mChildCountWithTransientState = 0; + // Iterator over the children in decreasing Z order (top children first). + private OrderedChildIterator mOrderedChildIterator; + /** * Currently registered axes for nested scrolling. Flag set consisting of * {@link #SCROLL_AXIS_HORIZONTAL} {@link #SCROLL_AXIS_VERTICAL} or {@link #SCROLL_AXIS_NONE} @@ -817,19 +822,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager return false; } - // Check whether any clickable siblings cover the child - // view and if so keep track of the intersections. Also - // respect Z ordering when iterating over children. - ArrayList<View> orderedList = buildOrderedChildList(); - final boolean useCustomOrder = orderedList == null - && isChildrenDrawingOrderEnabled(); - - final int childCount = mChildrenCount; - for (int i = childCount - 1; i >= 0; i--) { - final int childIndex = useCustomOrder - ? getChildDrawingOrder(childCount, i) : i; - final View sibling = (orderedList == null) - ? mChildren[childIndex] : orderedList.get(childIndex); + Iterator<View> iterator = obtainOrderedChildIterator(); + while (iterator.hasNext()) { + View sibling = iterator.next(); // We care only about siblings over the child. if (sibling == child) { @@ -837,12 +832,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } // Ignore invisible views as they are not interactive. - if (sibling.getVisibility() != View.VISIBLE) { - continue; - } - - // If sibling is not interactive we do not care. - if (!sibling.isClickable() && !sibling.isLongClickable()) { + if (!isVisible(sibling)) { continue; } @@ -850,28 +840,37 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager RectF siblingBounds = mAttachInfo.mTmpTransformRect1; siblingBounds.set(0, 0, sibling.getWidth(), sibling.getHeight()); - // Take into account the sibling transformation matrix. - if (!sibling.hasIdentityMatrix()) { - sibling.getMatrix().mapRect(siblingBounds); - } - - // Offset the sibling to our coordinates. - final int siblingDx = sibling.mLeft - mScrollX; - final int siblingDy = sibling.mTop - mScrollY; - siblingBounds.offset(siblingDx, siblingDy); + // Translate the sibling bounds to our coordinates. + offsetChildRectToMyCoords(siblingBounds, sibling); // Compute the intersection between the child and the sibling. if (siblingBounds.intersect(bounds)) { - // If an interactive sibling completely covers the child, done. - if (siblingBounds.equals(bounds)) { - return false; + List<RectF> clickableRects = new ArrayList<>(); + sibling.addClickableRectsForAccessibility(clickableRects); + + final int clickableRectCount = clickableRects.size(); + for (int j = 0; j < clickableRectCount; j++) { + RectF clickableRect = clickableRects.get(j); + + // Translate the clickable rect to our coordinates. + offsetChildRectToMyCoords(clickableRect, sibling); + + // Compute the intersection between the child and the clickable rects. + if (clickableRect.intersect(bounds)) { + // If a clickable rect completely covers the child, done. + if (clickableRect.equals(bounds)) { + releaseOrderedChildIterator(); + return false; + } + // Keep track of the intersection rectangle. + intersections.add(clickableRect); + } } - // Keep track of the intersection rectangle. - RectF intersection = new RectF(siblingBounds); - intersections.add(intersection); } } + releaseOrderedChildIterator(); + if (mParent instanceof ViewGroup) { ViewGroup parentGroup = (ViewGroup) mParent; return parentGroup.translateBoundsAndIntersectionsInWindowCoordinates( @@ -881,6 +880,94 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager return true; } + @Override + void addClickableRectsForAccessibility(List<RectF> outRects) { + int sizeBefore = outRects.size(); + + super.addClickableRectsForAccessibility(outRects); + + // If we added ourselves, then no need to visit children. + if (outRects.size() > sizeBefore) { + return; + } + + Iterator<View> iterator = obtainOrderedChildIterator(); + while (iterator.hasNext()) { + View child = iterator.next(); + + // Cannot click on an invisible view. + if (!isVisible(child)) { + continue; + } + + sizeBefore = outRects.size(); + + // Add clickable rects in the child bounds. + child.addClickableRectsForAccessibility(outRects); + + // Offset the clickable rects for out children to our coordinates. + final int sizeAfter = outRects.size(); + for (int j = sizeBefore; j < sizeAfter; j++) { + RectF rect = outRects.get(j); + + // Translate the clickable rect to our coordinates. + offsetChildRectToMyCoords(rect, child); + + // If a clickable rect fills the parent, done. + if ((int) rect.left == 0 && (int) rect.top == 0 + && (int) rect.right == mRight && (int) rect.bottom == mBottom) { + releaseOrderedChildIterator(); + return; + } + } + } + + releaseOrderedChildIterator(); + } + + private void offsetChildRectToMyCoords(RectF rect, View child) { + if (!child.hasIdentityMatrix()) { + child.getMatrix().mapRect(rect); + } + final int childDx = child.mLeft - mScrollX; + final int childDy = child.mTop - mScrollY; + rect.offset(childDx, childDy); + } + + private static boolean isVisible(View view) { + return (view.getAlpha() > 0 && view.getTransitionAlpha() > 0 && + view.getVisibility() == VISIBLE); + } + + /** + * Obtains the iterator to traverse the children in a descending Z order. + * Only one party can use the iterator at any given time and you cannot + * modify the children while using this iterator. Acquisition if already + * obtained is an error. + * + * @return The child iterator. + */ + OrderedChildIterator obtainOrderedChildIterator() { + if (mOrderedChildIterator == null) { + mOrderedChildIterator = new OrderedChildIterator(); + } else if (mOrderedChildIterator.isInitialized()) { + throw new IllegalStateException("Already obtained"); + } + mOrderedChildIterator.initialize(); + return mOrderedChildIterator; + } + + /** + * Releases the iterator to traverse the children in a descending Z order. + * Release if not obtained is an error. + */ + void releaseOrderedChildIterator() { + if (mOrderedChildIterator == null || !mOrderedChildIterator.isInitialized()) { + throw new IllegalStateException("Not obtained"); + } + mOrderedChildIterator.release(); + } + /** * Called when a child view has changed whether or not it is tracking transient state. */ @@ -3293,7 +3380,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager /** * Populates (and returns) mPreSortedChildren with a pre-ordered list of the View's children, - * sorted first by Z, then by child drawing order (if applicable). + * sorted first by Z, then by child drawing order (if applicable). This list must be cleared + * after use to avoid leaking child Views. * * Uses a stable, insertion sort which is commonly O(n) for ViewGroups with very few elevated * children. @@ -3668,6 +3756,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * @see #generateDefaultLayoutParams() */ public void addView(View child, int index) { + if (child == null) { + throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup"); + } LayoutParams params = child.getLayoutParams(); if (params == null) { params = generateDefaultLayoutParams(); @@ -3725,6 +3816,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager System.out.println(this + " addView"); } + if (child == null) { + throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup"); + } + // addViewInner() will call child.requestLayout() when setting the new LayoutParams // therefore, we call requestLayout() on ourselves before, so that the child's request // will be blocked at our level @@ -3852,6 +3947,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager */ protected boolean addViewInLayout(View child, int index, LayoutParams params, boolean preventRequestLayout) { + if (child == null) { + throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup"); + } child.mParent = null; addViewInner(child, index, params, preventRequestLayout); child.mPrivateFlags = (child.mPrivateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN; @@ -4065,9 +4163,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p> */ public void removeView(View view) { - removeViewInternal(view); - requestLayout(); - invalidate(true); + if (removeViewInternal(view)) { + requestLayout(); + invalidate(true); + } } /** @@ -4130,11 +4229,13 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager invalidate(true); } - private void removeViewInternal(View view) { + private boolean removeViewInternal(View view) { final int index = indexOfChild(view); if (index >= 0) { removeViewInternal(index, view); + return true; } + return false; } private void removeViewInternal(int index, View view) { @@ -4149,9 +4250,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager clearChildFocus = true; } - if (view.isAccessibilityFocused()) { - view.clearAccessibilityFocus(); - } + view.clearAccessibilityFocus(); cancelTouchTarget(view); cancelHoverTarget(view); @@ -4244,9 +4343,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager clearChildFocus = true; } - if (view.isAccessibilityFocused()) { - view.clearAccessibilityFocus(); - } + view.clearAccessibilityFocus(); cancelTouchTarget(view); cancelHoverTarget(view); @@ -4331,9 +4428,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager clearChildFocus = true; } - if (view.isAccessibilityFocused()) { - view.clearAccessibilityFocus(); - } + view.clearAccessibilityFocus(); cancelTouchTarget(view); cancelHoverTarget(view); @@ -6488,7 +6583,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager */ public static class MarginLayoutParams extends ViewGroup.LayoutParams { /** - * The left margin in pixels of the child. + * The left margin in pixels of the child. Margin values should be positive. * Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value * to this field. */ @@ -6496,7 +6591,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager public int leftMargin; /** - * The top margin in pixels of the child. + * The top margin in pixels of the child. Margin values should be positive. * Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value * to this field. */ @@ -6504,7 +6599,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager public int topMargin; /** - * The right margin in pixels of the child. + * The right margin in pixels of the child. Margin values should be positive. * Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value * to this field. */ @@ -6512,7 +6607,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager public int rightMargin; /** - * The bottom margin in pixels of the child. + * The bottom margin in pixels of the child. Margin values should be positive. * Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value * to this field. */ @@ -6520,7 +6615,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager public int bottomMargin; /** - * The start margin in pixels of the child. + * The start margin in pixels of the child. Margin values should be positive. * Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value * to this field. */ @@ -6528,7 +6623,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager private int startMargin = DEFAULT_MARGIN_RELATIVE; /** - * The end margin in pixels of the child. + * The end margin in pixels of the child. Margin values should be positive. * Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value * to this field. */ @@ -6709,6 +6804,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * Sets the margins, in pixels. A call to {@link android.view.View#requestLayout()} needs * to be done so that the new margins are taken into account. Left and right margins may be * overriden by {@link android.view.View#requestLayout()} depending on layout direction. + * Margin values should be positive. * * @param left the left margin size * @param top the top margin size @@ -6738,7 +6834,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * Sets the relative margins, in pixels. A call to {@link android.view.View#requestLayout()} * needs to be done so that the new relative margins are taken into account. Left and right * margins may be overriden by {@link android.view.View#requestLayout()} depending on layout - * direction. + * direction. Margin values should be positive. * * @param start the start margin size * @param top the top margin size @@ -6761,7 +6857,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** - * Sets the relative start margin. + * Sets the relative start margin. Margin values should be positive. * * @param start the start margin size * @@ -6794,7 +6890,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** - * Sets the relative end margin. + * Sets the relative end margin. Margin values should be positive. * * @param end the end margin size * @@ -7281,4 +7377,57 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager canvas.drawLines(sDebugLines, paint); } + + private final class OrderedChildIterator implements Iterator<View> { + private List<View> mOrderedChildList; + private boolean mUseCustomOrder; + private int mCurrentIndex; + private boolean mInitialized; + + public void initialize() { + mOrderedChildList = buildOrderedChildList(); + mUseCustomOrder = (mOrderedChildList == null) + && isChildrenDrawingOrderEnabled(); + mCurrentIndex = mChildrenCount - 1; + mInitialized = true; + } + + public void release() { + if (mOrderedChildList != null) { + mOrderedChildList.clear(); + } + mUseCustomOrder = false; + mCurrentIndex = 0; + mInitialized = false; + } + + public boolean isInitialized() { + return mInitialized; + } + + @Override + public boolean hasNext() { + return (mCurrentIndex >= 0); + } + + @Override + public View next() { + if (!hasNext()) { + throw new NoSuchElementException("No such element"); + } + return getChild(mCurrentIndex--); + } + + private View getChild(int index) { + final int childIndex = mUseCustomOrder + ? getChildDrawingOrder(mChildrenCount, index) : index; + return (mOrderedChildList == null) + ? mChildren[childIndex] : mOrderedChildList.get(childIndex); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 81fc966..5d2a24b 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -526,7 +526,7 @@ public final class ViewRootImpl implements ViewParent, collectViewAttributes(); res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), - mAttachInfo.mContentInsets, mInputChannel); + mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mInputChannel); } catch (RemoteException e) { mAdded = false; mView = null; @@ -727,7 +727,10 @@ public final class ViewRootImpl implements ViewParent, mAttachInfo.mHardwareRenderer.destroy(); } - final boolean translucent = attrs.format != PixelFormat.OPAQUE; + final Rect insets = attrs.surfaceInsets; + final boolean hasSurfaceInsets = insets.left != 0 || insets.right != 0 + || insets.top != 0 || insets.bottom != 0; + final boolean translucent = attrs.format != PixelFormat.OPAQUE || hasSurfaceInsets; mAttachInfo.mHardwareRenderer = HardwareRenderer.create(mContext, translucent); if (mAttachInfo.mHardwareRenderer != null) { mAttachInfo.mHardwareRenderer.setName(attrs.getTitle().toString()); @@ -1646,6 +1649,9 @@ public final class ViewRootImpl implements ViewParent, mLastScrolledFocus.clear(); } mScrollY = mCurScrollY = 0; + if (mView instanceof RootViewSurfaceTaker) { + ((RootViewSurfaceTaker) mView).onRootViewScrollYChanged(mCurScrollY); + } if (mScroller != null) { mScroller.abortAnimation(); } @@ -1740,8 +1746,8 @@ public final class ViewRootImpl implements ViewParent, if (hwInitialized || mWidth != mAttachInfo.mHardwareRenderer.getWidth() || mHeight != mAttachInfo.mHardwareRenderer.getHeight()) { - final Rect surfaceInsets = params != null ? params.surfaceInsets : null; - mAttachInfo.mHardwareRenderer.setup(mWidth, mHeight, surfaceInsets); + mAttachInfo.mHardwareRenderer.setup( + mWidth, mHeight, mWindowAttributes.surfaceInsets); if (!hwInitialized) { mAttachInfo.mHardwareRenderer.invalidate(mSurface); mFullRedrawNeeded = true; @@ -2255,6 +2261,7 @@ public final class ViewRootImpl implements ViewParent, canvas.drawHardwareLayer(mResizeBuffer, mHardwareXOffset, mHardwareYOffset, mResizePaint); } + drawAccessibilityFocusedDrawableIfNeeded(canvas); } /** @@ -2415,6 +2422,9 @@ public final class ViewRootImpl implements ViewParent, if (mCurScrollY != curScrollY) { mCurScrollY = curScrollY; fullRedrawNeeded = true; + if (mView instanceof RootViewSurfaceTaker) { + ((RootViewSurfaceTaker) mView).onRootViewScrollYChanged(mCurScrollY); + } } final float appScale = mAttachInfo.mApplicationScale; @@ -2474,18 +2484,38 @@ public final class ViewRootImpl implements ViewParent, dirty.offset(surfaceInsets.left, surfaceInsets.right); } - if (!dirty.isEmpty() || mIsAnimating) { + boolean accessibilityFocusDirty = false; + final Drawable drawable = mAttachInfo.mAccessibilityFocusDrawable; + if (drawable != null) { + final Rect bounds = mAttachInfo.mTmpInvalRect; + final boolean hasFocus = getAccessibilityFocusedRect(bounds); + if (!hasFocus) { + bounds.setEmpty(); + } + if (!bounds.equals(drawable.getBounds())) { + accessibilityFocusDirty = true; + } + } + + if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) { if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) { + // If accessibility focus moved, always invalidate the root. + boolean invalidateRoot = accessibilityFocusDirty; + // Draw with hardware renderer. mIsAnimating = false; - boolean invalidateRoot = false; + if (mHardwareYOffset != yOffset || mHardwareXOffset != xOffset) { mHardwareYOffset = yOffset; mHardwareXOffset = xOffset; - mAttachInfo.mHardwareRenderer.invalidateRoot(); + invalidateRoot = true; } mResizeAlpha = resizeAlpha; + if (invalidateRoot) { + mAttachInfo.mHardwareRenderer.invalidateRoot(); + } + dirty.setEmpty(); mBlockResizeBuffer = false; @@ -2604,6 +2634,8 @@ public final class ViewRootImpl implements ViewParent, attachInfo.mSetIgnoreDirtyState = false; mView.draw(canvas); + + drawAccessibilityFocusedDrawableIfNeeded(canvas); } finally { if (!attachInfo.mSetIgnoreDirtyState) { // Only clear the flag if it was not set during the mView.draw() call @@ -2627,7 +2659,56 @@ public final class ViewRootImpl implements ViewParent, return true; } - Drawable getAccessibilityFocusedDrawable() { + /** + * We want to draw a highlight around the current accessibility focused. + * Since adding a style for all possible view is not a viable option we + * have this specialized drawing method. + * + * Note: We are doing this here to be able to draw the highlight for + * virtual views in addition to real ones. + * + * @param canvas The canvas on which to draw. + */ + private void drawAccessibilityFocusedDrawableIfNeeded(Canvas canvas) { + final Rect bounds = mAttachInfo.mTmpInvalRect; + if (getAccessibilityFocusedRect(bounds)) { + final Drawable drawable = getAccessibilityFocusedDrawable(); + if (drawable != null) { + drawable.setBounds(bounds); + drawable.draw(canvas); + } + } else if (mAttachInfo.mAccessibilityFocusDrawable != null) { + mAttachInfo.mAccessibilityFocusDrawable.setBounds(0, 0, 0, 0); + } + } + + private boolean getAccessibilityFocusedRect(Rect bounds) { + final AccessibilityManager manager = AccessibilityManager.getInstance(mView.mContext); + if (!manager.isEnabled() || !manager.isTouchExplorationEnabled()) { + return false; + } + + final View host = mAccessibilityFocusedHost; + if (host == null || host.mAttachInfo == null) { + return false; + } + + final AccessibilityNodeProvider provider = host.getAccessibilityNodeProvider(); + if (provider == null) { + host.getBoundsOnScreen(bounds); + } else if (mAccessibilityFocusedVirtualView != null) { + mAccessibilityFocusedVirtualView.getBoundsInScreen(bounds); + } else { + return false; + } + + final AttachInfo attachInfo = mAttachInfo; + bounds.offset(-attachInfo.mWindowLeft, -attachInfo.mWindowTop); + bounds.intersect(0, 0, attachInfo.mViewRootImpl.mWidth, attachInfo.mViewRootImpl.mHeight); + return !bounds.isEmpty(); + } + + private Drawable getAccessibilityFocusedDrawable() { // Lazily load the accessibility focus drawable. if (mAttachInfo.mAccessibilityFocusDrawable == null) { final TypedValue value = new TypedValue(); @@ -3014,6 +3095,7 @@ public final class ViewRootImpl implements ViewParent, private final static int MSG_INVALIDATE_WORLD = 23; private final static int MSG_WINDOW_MOVED = 24; private final static int MSG_SYNTHESIZE_INPUT_EVENT = 25; + private final static int MSG_DISPATCH_WINDOW_SHOWN = 26; final class ViewRootHandler extends Handler { @Override @@ -3063,6 +3145,8 @@ public final class ViewRootImpl implements ViewParent, return "MSG_WINDOW_MOVED"; case MSG_SYNTHESIZE_INPUT_EVENT: return "MSG_SYNTHESIZE_INPUT_EVENT"; + case MSG_DISPATCH_WINDOW_SHOWN: + return "MSG_DISPATCH_WINDOW_SHOWN"; } return super.getMessageName(message); } @@ -3291,6 +3375,9 @@ public final class ViewRootImpl implements ViewParent, invalidateWorld(mView); } } break; + case MSG_DISPATCH_WINDOW_SHOWN: { + handleDispatchWindowShown(); + } } } } @@ -5137,6 +5224,10 @@ public final class ViewRootImpl implements ViewParent, } } + public void handleDispatchWindowShown() { + mAttachInfo.mTreeObserver.dispatchOnWindowShown(); + } + public void getLastTouchPoint(Point outLocation) { outLocation.x = (int) mLastTouchPoint.x; outLocation.y = (int) mLastTouchPoint.y; @@ -5997,6 +6088,10 @@ public final class ViewRootImpl implements ViewParent, mHandler.sendMessage(msg); } + public void dispatchWindowShown() { + mHandler.sendEmptyMessage(MSG_DISPATCH_WINDOW_SHOWN); + } + public void dispatchCloseSystemDialogs(String reason) { Message msg = Message.obtain(); msg.what = MSG_CLOSE_SYSTEM_DIALOGS; @@ -6507,6 +6602,14 @@ public final class ViewRootImpl implements ViewParent, viewAncestor.dispatchDoneAnimating(); } } + + @Override + public void dispatchWindowShown() { + final ViewRootImpl viewAncestor = mViewAncestor.get(); + if (viewAncestor != null) { + viewAncestor.dispatchWindowShown(); + } + } } public static final class CalledFromWrongThreadException extends AndroidRuntimeException { diff --git a/core/java/android/view/ViewTreeObserver.java b/core/java/android/view/ViewTreeObserver.java index a9444b4..b85fec8 100644 --- a/core/java/android/view/ViewTreeObserver.java +++ b/core/java/android/view/ViewTreeObserver.java @@ -44,10 +44,15 @@ public final class ViewTreeObserver { private CopyOnWriteArray<OnComputeInternalInsetsListener> mOnComputeInternalInsetsListeners; private CopyOnWriteArray<OnScrollChangedListener> mOnScrollChangedListeners; private CopyOnWriteArray<OnPreDrawListener> mOnPreDrawListeners; + private CopyOnWriteArray<OnWindowShownListener> mOnWindowShownListeners; // These listeners cannot be mutated during dispatch private ArrayList<OnDrawListener> mOnDrawListeners; + /** Remains false until #dispatchOnWindowShown() is called. If a listener registers after + * that the listener will be immediately called. */ + private boolean mWindowShown; + private boolean mAlive = true; /** @@ -174,6 +179,19 @@ public final class ViewTreeObserver { } /** + * Interface definition for a callback noting when a system window has been displayed. + * This is only used for non-Activity windows. Activity windows can use + * Activity.onEnterAnimationComplete() to get the same signal. + * @hide + */ + public interface OnWindowShownListener { + /** + * Callback method to be invoked when a non-activity window is fully shown. + */ + void onWindowShown(); + } + + /** * Parameters used with OnComputeInternalInsetsListener. * * We are not yet ready to commit to this API and support it, so @@ -375,6 +393,14 @@ public final class ViewTreeObserver { } } + if (observer.mOnWindowShownListeners != null) { + if (mOnWindowShownListeners != null) { + mOnWindowShownListeners.addAll(observer.mOnWindowShownListeners); + } else { + mOnWindowShownListeners = observer.mOnWindowShownListeners; + } + } + observer.kill(); } @@ -568,6 +594,45 @@ public final class ViewTreeObserver { } /** + * Register a callback to be invoked when the view tree window has been shown + * + * @param listener The callback to add + * + * @throws IllegalStateException If {@link #isAlive()} returns false + * @hide + */ + public void addOnWindowShownListener(OnWindowShownListener listener) { + checkIsAlive(); + + if (mOnWindowShownListeners == null) { + mOnWindowShownListeners = new CopyOnWriteArray<OnWindowShownListener>(); + } + + mOnWindowShownListeners.add(listener); + if (mWindowShown) { + listener.onWindowShown(); + } + } + + /** + * Remove a previously installed window shown callback + * + * @param victim The callback to remove + * + * @throws IllegalStateException If {@link #isAlive()} returns false + * + * @see #addOnWindowShownListener(OnWindowShownListener) + * @hide + */ + public void removeOnWindowShownListener(OnWindowShownListener victim) { + checkIsAlive(); + if (mOnWindowShownListeners == null) { + return; + } + mOnWindowShownListeners.remove(victim); + } + + /** * <p>Register a callback to be invoked when the view tree is about to be drawn.</p> * <p><strong>Note:</strong> this method <strong>cannot</strong> be invoked from * {@link android.view.ViewTreeObserver.OnDrawListener#onDraw()}.</p> @@ -854,6 +919,27 @@ public final class ViewTreeObserver { } /** + * Notifies registered listeners that the window is now shown + * @hide + */ + @SuppressWarnings("unchecked") + public final void dispatchOnWindowShown() { + mWindowShown = true; + final CopyOnWriteArray<OnWindowShownListener> listeners = mOnWindowShownListeners; + if (listeners != null && listeners.size() > 0) { + CopyOnWriteArray.Access<OnWindowShownListener> access = listeners.start(); + try { + int count = access.size(); + for (int i = 0; i < count; i++) { + access.get(i).onWindowShown(); + } + } finally { + listeners.end(); + } + } + } + + /** * Notifies registered listeners that the drawing pass is about to start. */ public final void dispatchOnDraw() { diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java index 63ab7d2..0076abf 100644 --- a/core/java/android/view/Window.java +++ b/core/java/android/view/Window.java @@ -24,7 +24,6 @@ import android.content.res.TypedArray; import android.graphics.PixelFormat; import android.graphics.drawable.Drawable; import android.media.session.MediaController; -import android.media.session.MediaSession; import android.net.Uri; import android.os.Bundle; import android.os.IBinder; @@ -801,9 +800,6 @@ public abstract class Window { public void setFlags(int flags, int mask) { final WindowManager.LayoutParams attrs = getAttributes(); attrs.flags = (attrs.flags&~mask) | (flags&mask); - if ((mask&WindowManager.LayoutParams.FLAG_NEEDS_MENU_KEY) != 0) { - attrs.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SET_NEEDS_MENU_KEY; - } mForcedWindowFlags |= mask; dispatchWindowAttributesChanged(attrs); } @@ -817,6 +813,15 @@ public abstract class Window { /** * {@hide} */ + protected void setNeedsMenuKey(int value) { + final WindowManager.LayoutParams attrs = getAttributes(); + attrs.needsMenuKey = value; + dispatchWindowAttributesChanged(attrs); + } + + /** + * {@hide} + */ protected void dispatchWindowAttributesChanged(WindowManager.LayoutParams attrs) { if (mCallback != null) { mCallback.onWindowAttributesChanged(attrs); @@ -1069,17 +1074,36 @@ public abstract class Window { public abstract void onConfigurationChanged(Configuration newConfig); /** + * Sets the window elevation. + * + * @param elevation The window elevation. + * @see View#setElevation(float) + * @see android.R.styleable#Window_windowElevation + */ + public void setElevation(float elevation) {} + + /** + * Sets whether window content should be clipped to the outline of the + * window background. + * + * @param clipToOutline Whether window content should be clipped to the + * outline of the window background. + * @see View#setClipToOutline(boolean) + * @see android.R.styleable#Window_windowClipToOutline + */ + public void setClipToOutline(boolean clipToOutline) {} + + /** * Change the background of this window to a Drawable resource. Setting the * background to null will make the window be opaque. To make the window * transparent, you can use an empty drawable (for instance a ColorDrawable * with the color 0 or the system drawable android:drawable/empty.) * - * @param resid The resource identifier of a drawable resource which will be - * installed as the new background. + * @param resId The resource identifier of a drawable resource which will + * be installed as the new background. */ - public void setBackgroundDrawableResource(int resid) - { - setBackgroundDrawable(mContext.getDrawable(resid)); + public void setBackgroundDrawableResource(int resId) { + setBackgroundDrawable(mContext.getDrawable(resId)); } /** @@ -1125,31 +1149,31 @@ public abstract class Window { * Set an explicit Drawable value for feature of this window. You must * have called requestFeature(featureId) before calling this function. * - * @param featureId The desired drawable feature to change. - * Features are constants defined by Window. + * @param featureId The desired drawable feature to change. Features are + * constants defined by Window. * @param drawable A Drawable object to display. */ public abstract void setFeatureDrawable(int featureId, Drawable drawable); /** - * Set a custom alpha value for the given drawale feature, controlling how + * Set a custom alpha value for the given drawable feature, controlling how * much the background is visible through it. * - * @param featureId The desired drawable feature to change. - * Features are constants defined by Window. + * @param featureId The desired drawable feature to change. Features are + * constants defined by Window. * @param alpha The alpha amount, 0 is completely transparent and 255 is * completely opaque. */ public abstract void setFeatureDrawableAlpha(int featureId, int alpha); /** - * Set the integer value for a feature. The range of the value depends on - * the feature being set. For FEATURE_PROGRESSS, it should go from 0 to - * 10000. At 10000 the progress is complete and the indicator hidden. + * Set the integer value for a feature. The range of the value depends on + * the feature being set. For {@link #FEATURE_PROGRESS}, it should go from + * 0 to 10000. At 10000 the progress is complete and the indicator hidden. * - * @param featureId The desired feature to change. - * Features are constants defined by Window. - * @param value The value for the feature. The interpretation of this + * @param featureId The desired feature to change. Features are constants + * defined by Window. + * @param value The value for the feature. The interpretation of this * value is feature-specific. */ public abstract void setFeatureInt(int featureId, int value); diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 47ee52e..f4f047e 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -523,15 +523,6 @@ public interface WindowManager extends ViewManager { public static final int TYPE_MAGNIFICATION_OVERLAY = FIRST_SYSTEM_WINDOW+27; /** - * Window type: Recents. Same layer as {@link #TYPE_SYSTEM_DIALOG} but only appears on - * one user's screen. - * In multiuser systems shows on all users' windows. - * @hide - */ - public static final int TYPE_RECENTS_OVERLAY = FIRST_SYSTEM_WINDOW+28; - - - /** * Window type: keyguard scrim window. Shows if keyguard needs to be restarted. * In multiuser systems shows on all users' windows. * @hide @@ -551,6 +542,19 @@ public interface WindowManager extends ViewManager { public static final int TYPE_VOICE_INTERACTION = FIRST_SYSTEM_WINDOW+31; /** + * Window type: Windows that are overlaid <em>only</em> by an {@link + * android.accessibilityservice.AccessibilityService} for interception of + * user interactions without changing the windows an accessibility service + * can introspect. In particular, an accessibility service can introspect + * only windows that a sighted user can interact with which is they can touch + * these windows or can type into these windows. For example, if there + * is a full screen accessibility overlay that is touchable, the windows + * below it will be introspectable by an accessibility service regardless + * they are covered by a touchable window. + */ + public static final int TYPE_ACCESSIBILITY_OVERLAY = FIRST_SYSTEM_WINDOW+32; + + /** * End of types of system windows. */ public static final int LAST_SYSTEM_WINDOW = 2999; @@ -887,9 +891,6 @@ public interface WindowManager extends ViewManager { */ public static final int FLAG_TRANSLUCENT_NAVIGATION = 0x08000000; - // ----- HIDDEN FLAGS. - // These start at the high bit and go down. - /** * Flag for a window in local focus mode. * Window in local focus mode can control focus independent of window manager using @@ -912,17 +913,12 @@ public interface WindowManager extends ViewManager { public static final int FLAG_SLIPPERY = 0x20000000; /** - * Flag for a window belonging to an activity that responds to {@link KeyEvent#KEYCODE_MENU} - * and therefore needs a Menu key. For devices where Menu is a physical button this flag is - * ignored, but on devices where the Menu key is drawn in software it may be hidden unless - * this flag is set. - * - * (Note that Action Bars, when available, are the preferred way to offer additional - * functions otherwise accessed via an options menu.) - * - * {@hide} + * Window flag: When requesting layout with an attached window, the attached window may + * overlap with the screen decorations of the parent window such as the navigation bar. By + * including this flag, the window manager will layout the attached window within the decor + * frame of the parent window such that it doesn't overlap with screen decorations. */ - public static final int FLAG_NEEDS_MENU_KEY = 0x40000000; + public static final int FLAG_LAYOUT_ATTACHED_IN_DECOR = 0x40000000; /** * Flag indicating that this Window is responsible for drawing the background for the @@ -1065,16 +1061,6 @@ public interface WindowManager extends ViewManager { */ public static final int PRIVATE_FLAG_WANTS_OFFSET_NOTIFICATIONS = 0x00000004; - /** - * This is set for a window that has explicitly specified its - * FLAG_NEEDS_MENU_KEY, so we know the value on this window is the - * appropriate one to use. If this is not set, we should look at - * windows behind it to determine the appropriate value. - * - * @hide - */ - public static final int PRIVATE_FLAG_SET_NEEDS_MENU_KEY = 0x00000008; - /** In a multiuser system if this flag is set and the owner is a system process then this * window will appear on all user screens. This overrides the default behavior of window * types that normally only appear on the owning user's screen. Refer to each window type @@ -1122,6 +1108,45 @@ public interface WindowManager extends ViewManager { public int privateFlags; /** + * Value for {@link #needsMenuKey} for a window that has not explicitly specified if it + * needs {@link #NEEDS_MENU_SET_TRUE} or doesn't need {@link #NEEDS_MENU_SET_FALSE} a menu + * key. For this case, we should look at windows behind it to determine the appropriate + * value. + * + * @hide + */ + public static final int NEEDS_MENU_UNSET = 0; + + /** + * Value for {@link #needsMenuKey} for a window that has explicitly specified it needs a + * menu key. + * + * @hide + */ + public static final int NEEDS_MENU_SET_TRUE = 1; + + /** + * Value for {@link #needsMenuKey} for a window that has explicitly specified it doesn't + * needs a menu key. + * + * @hide + */ + public static final int NEEDS_MENU_SET_FALSE = 2; + + /** + * State variable for a window belonging to an activity that responds to + * {@link KeyEvent#KEYCODE_MENU} and therefore needs a Menu key. For devices where Menu is a + * physical button this variable is ignored, but on devices where the Menu key is drawn in + * software it may be hidden unless this variable is set to {@link #NEEDS_MENU_SET_TRUE}. + * + * (Note that Action Bars, when available, are the preferred way to offer additional + * functions otherwise accessed via an options menu.) + * + * {@hide} + */ + public int needsMenuKey = NEEDS_MENU_UNSET; + + /** * Given a particular set of window manager flags, determine whether * such a window may be a target for an input method when it has * focus. In particular, this checks the @@ -1129,9 +1154,9 @@ public interface WindowManager extends ViewManager { * flags and returns true if the combination of the two corresponds * to a window that needs to be behind the input method so that the * user can type into it. - * + * * @param flags The current window manager flags. - * + * * @return Returns true if such a window should be behind/interact * with an input method, false if not. */ @@ -1299,7 +1324,7 @@ public interface WindowManager extends ViewManager { * * @hide */ - public Rect surfaceInsets = new Rect(); + public final Rect surfaceInsets = new Rect(); /** * The desired bitmap format. May be one of the constants in @@ -1596,14 +1621,15 @@ public interface WindowManager extends ViewManager { out.writeInt(surfaceInsets.top); out.writeInt(surfaceInsets.right); out.writeInt(surfaceInsets.bottom); + out.writeInt(needsMenuKey); } - + public static final Parcelable.Creator<LayoutParams> CREATOR = new Parcelable.Creator<LayoutParams>() { public LayoutParams createFromParcel(Parcel in) { return new LayoutParams(in); } - + public LayoutParams[] newArray(int size) { return new LayoutParams[size]; } @@ -1643,8 +1669,9 @@ public interface WindowManager extends ViewManager { surfaceInsets.top = in.readInt(); surfaceInsets.right = in.readInt(); surfaceInsets.bottom = in.readInt(); + needsMenuKey = in.readInt(); } - + @SuppressWarnings({"PointlessBitwiseExpression"}) public static final int LAYOUT_CHANGED = 1<<0; public static final int TYPE_CHANGED = 1<<1; @@ -1678,14 +1705,16 @@ public interface WindowManager extends ViewManager { /** {@hide} */ public static final int PREFERRED_REFRESH_RATE_CHANGED = 1 << 21; /** {@hide} */ + public static final int NEEDS_MENU_KEY_CHANGED = 1 << 22; + /** {@hide} */ public static final int EVERYTHING_CHANGED = 0xffffffff; // internal buffer to backup/restore parameters under compatibility mode. private int[] mCompatibilityParamsBackup = null; - + public final int copyFrom(LayoutParams o) { int changes = 0; - + if (width != o.width) { width = o.width; changes |= LAYOUT_CHANGED; @@ -1822,9 +1851,14 @@ public interface WindowManager extends ViewManager { changes |= SURFACE_INSETS_CHANGED; } + if (needsMenuKey != o.needsMenuKey) { + needsMenuKey = o.needsMenuKey; + changes |= NEEDS_MENU_KEY_CHANGED; + } + return changes; } - + @Override public String debug(String output) { output += "Contents of " + this + ":"; @@ -1928,6 +1962,10 @@ public interface WindowManager extends ViewManager { if (!surfaceInsets.equals(Insets.NONE)) { sb.append(" surfaceInsets=").append(surfaceInsets); } + if (needsMenuKey != NEEDS_MENU_UNSET) { + sb.append(" needsMenuKey="); + sb.append(needsMenuKey); + } sb.append('}'); return sb.toString(); } diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java index 5926d5f..82b1073 100644 --- a/core/java/android/view/WindowManagerGlobal.java +++ b/core/java/android/view/WindowManagerGlobal.java @@ -118,6 +118,9 @@ public final class WindowManagerGlobal { private Runnable mSystemPropertyUpdater; + /** Default token to apply to added views. */ + private IBinder mDefaultToken; + private WindowManagerGlobal() { } @@ -169,6 +172,17 @@ public final class WindowManagerGlobal { } } + /** + * Sets the default token to use in {@link #addView} when no parent window + * token is available and no token has been explicitly set in the view's + * layout params. + * + * @param token Default window token to apply to added views. + */ + public void setDefaultToken(IBinder token) { + mDefaultToken = token; + } + public String[] getViewRootNames() { synchronized (mLock) { final int numRoots = mRoots.size(); @@ -216,6 +230,10 @@ public final class WindowManagerGlobal { } } + if (wparams.token == null && mDefaultToken != null) { + wparams.token = mDefaultToken; + } + ViewRootImpl root; View panelParentView = null; diff --git a/core/java/android/view/WindowManagerInternal.java b/core/java/android/view/WindowManagerInternal.java index 38e3723..f557b97 100644 --- a/core/java/android/view/WindowManagerInternal.java +++ b/core/java/android/view/WindowManagerInternal.java @@ -173,4 +173,20 @@ public abstract class WindowManagerInternal { * redrawn. */ public abstract void waitForAllWindowsDrawn(Runnable callback, long timeout); + + /** + * Adds a window token for a given window type. + * + * @param token The token to add. + * @param type The window type. + */ + public abstract void addWindowToken(android.os.IBinder token, int type); + + /** + * Removes a window token. + * + * @param token The toke to remove. + * @param removeWindows Whether to also remove the windows associated with the token. + */ + public abstract void removeWindowToken(android.os.IBinder token, boolean removeWindows); } diff --git a/core/java/android/view/WindowManagerPolicy.java b/core/java/android/view/WindowManagerPolicy.java index 673f075..b8e94ee 100644 --- a/core/java/android/view/WindowManagerPolicy.java +++ b/core/java/android/view/WindowManagerPolicy.java @@ -865,12 +865,15 @@ public interface WindowManagerPolicy { * Return the insets for the areas covered by system windows. These values * are computed on the most recent layout, so they are not guaranteed to * be correct. - * + * * @param attrs The LayoutParams of the window. - * @param contentInset The areas covered by system windows, expressed as positive insets - * + * @param outContentInsets The areas covered by system windows, expressed as positive insets. + * @param outStableInsets The areas covered by stable system windows irrespective of their + * current visibility. Expressed as positive insets. + * */ - public void getContentInsetHintLw(WindowManager.LayoutParams attrs, Rect contentInset); + public void getInsetHintLw(WindowManager.LayoutParams attrs, Rect outContentInsets, + Rect outStableInsets); /** * Called when layout of the windows is finished. After this function has diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java index 3987fbc..b5afdf7 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -547,6 +547,8 @@ public class AccessibilityNodeInfo implements Parcelable { private long mParentNodeId = ROOT_NODE_ID; private long mLabelForId = ROOT_NODE_ID; private long mLabeledById = ROOT_NODE_ID; + private long mTraversalBefore = ROOT_NODE_ID; + private long mTraversalAfter = ROOT_NODE_ID; private int mBooleanProperties; private final Rect mBoundsInParent = new Rect(); @@ -1046,6 +1048,126 @@ public class AccessibilityNodeInfo implements Parcelable { } /** + * Gets the node before which this one is visited during traversal. A screen-reader + * must visit the content of this node before the content of the one it precedes. + * + * @return The succeeding node if such or <code>null</code>. + * + * @see #setTraversalBefore(android.view.View) + * @see #setTraversalBefore(android.view.View, int) + */ + public AccessibilityNodeInfo getTraversalBefore() { + enforceSealed(); + return getNodeForAccessibilityId(mTraversalBefore); + } + + /** + * Sets the view before whose node this one should be visited during traversal. A + * screen-reader must visit the content of this node before the content of the one + * it precedes. + * <p> + * <strong>Note:</strong> Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * + * @param view The view providing the preceding node. + * + * @see #getTraversalBefore() + */ + public void setTraversalBefore(View view) { + setTraversalBefore(view, UNDEFINED_ITEM_ID); + } + + /** + * Sets the node before which this one is visited during traversal. A screen-reader + * must visit the content of this node before the content of the one it precedes. + * The successor is a virtual descendant of the given <code>root</code>. If + * <code>virtualDescendantId</code> equals to {@link View#NO_ID} the root is set + * as the successor. + * <p> + * A virtual descendant is an imaginary View that is reported as a part of the view + * hierarchy for accessibility purposes. This enables custom views that draw complex + * content to report them selves as a tree of virtual views, thus conveying their + * logical structure. + * </p> + * <p> + * <strong>Note:</strong> Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * + * @param root The root of the virtual subtree. + * @param virtualDescendantId The id of the virtual descendant. + */ + public void setTraversalBefore(View root, int virtualDescendantId) { + enforceNotSealed(); + final int rootAccessibilityViewId = (root != null) + ? root.getAccessibilityViewId() : UNDEFINED_ITEM_ID; + mTraversalBefore = makeNodeId(rootAccessibilityViewId, virtualDescendantId); + } + + /** + * Gets the node after which this one is visited in accessibility traversal. + * A screen-reader must visit the content of the other node before the content + * of this one. + * + * @return The succeeding node if such or <code>null</code>. + * + * @see #setTraversalAfter(android.view.View) + * @see #setTraversalAfter(android.view.View, int) + */ + public AccessibilityNodeInfo getTraversalAfter() { + enforceSealed(); + return getNodeForAccessibilityId(mTraversalAfter); + } + + /** + * Sets the view whose node is visited after this one in accessibility traversal. + * A screen-reader must visit the content of the other node before the content + * of this one. + * <p> + * <strong>Note:</strong> Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * + * @param view The previous view. + * + * @see #getTraversalAfter() + */ + public void setTraversalAfter(View view) { + setTraversalAfter(view, UNDEFINED_ITEM_ID); + } + + /** + * Sets the node after which this one is visited in accessibility traversal. + * A screen-reader must visit the content of the other node before the content + * of this one. If <code>virtualDescendantId</code> equals to {@link View#NO_ID} + * the root is set as the predecessor. + * <p> + * A virtual descendant is an imaginary View that is reported as a part of the view + * hierarchy for accessibility purposes. This enables custom views that draw complex + * content to report them selves as a tree of virtual views, thus conveying their + * logical structure. + * </p> + * <p> + * <strong>Note:</strong> Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * + * @param root The root of the virtual subtree. + * @param virtualDescendantId The id of the virtual descendant. + */ + public void setTraversalAfter(View root, int virtualDescendantId) { + enforceNotSealed(); + final int rootAccessibilityViewId = (root != null) + ? root.getAccessibilityViewId() : UNDEFINED_ITEM_ID; + mTraversalAfter = makeNodeId(rootAccessibilityViewId, virtualDescendantId); + } + + /** * Sets the maximum text length, or -1 for no limit. * <p> * Typically used to indicate that an editable text field has a limit on @@ -1229,13 +1351,7 @@ public class AccessibilityNodeInfo implements Parcelable { */ public AccessibilityNodeInfo getParent() { enforceSealed(); - if (!canPerformRequestOverConnection(mParentNodeId)) { - return null; - } - AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); - return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId, - mWindowId, mParentNodeId, false, FLAG_PREFETCH_PREDECESSORS - | FLAG_PREFETCH_DESCENDANTS | FLAG_PREFETCH_SIBLINGS); + return getNodeForAccessibilityId(mParentNodeId); } /** @@ -2055,13 +2171,7 @@ public class AccessibilityNodeInfo implements Parcelable { */ public AccessibilityNodeInfo getLabelFor() { enforceSealed(); - if (!canPerformRequestOverConnection(mLabelForId)) { - return null; - } - AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); - return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId, - mWindowId, mLabelForId, false, FLAG_PREFETCH_PREDECESSORS - | FLAG_PREFETCH_DESCENDANTS | FLAG_PREFETCH_SIBLINGS); + return getNodeForAccessibilityId(mLabelForId); } /** @@ -2113,13 +2223,7 @@ public class AccessibilityNodeInfo implements Parcelable { */ public AccessibilityNodeInfo getLabeledBy() { enforceSealed(); - if (!canPerformRequestOverConnection(mLabeledById)) { - return null; - } - AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); - return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId, - mWindowId, mLabeledById, false, FLAG_PREFETCH_PREDECESSORS - | FLAG_PREFETCH_DESCENDANTS | FLAG_PREFETCH_SIBLINGS); + return getNodeForAccessibilityId(mLabeledById); } /** @@ -2453,6 +2557,9 @@ public class AccessibilityNodeInfo implements Parcelable { parcel.writeLong(mParentNodeId); parcel.writeLong(mLabelForId); parcel.writeLong(mLabeledById); + parcel.writeLong(mTraversalBefore); + parcel.writeLong(mTraversalAfter); + parcel.writeInt(mConnectionId); final LongArray childIds = mChildNodeIds; @@ -2571,6 +2678,8 @@ public class AccessibilityNodeInfo implements Parcelable { mParentNodeId = other.mParentNodeId; mLabelForId = other.mLabelForId; mLabeledById = other.mLabeledById; + mTraversalBefore = other.mTraversalBefore; + mTraversalAfter = other.mTraversalAfter; mWindowId = other.mWindowId; mConnectionId = other.mConnectionId; mBoundsInParent.set(other.mBoundsInParent); @@ -2633,6 +2742,9 @@ public class AccessibilityNodeInfo implements Parcelable { mParentNodeId = parcel.readLong(); mLabelForId = parcel.readLong(); mLabeledById = parcel.readLong(); + mTraversalBefore = parcel.readLong(); + mTraversalAfter = parcel.readLong(); + mConnectionId = parcel.readInt(); final int childrenSize = parcel.readInt(); @@ -2725,6 +2837,8 @@ public class AccessibilityNodeInfo implements Parcelable { mParentNodeId = ROOT_NODE_ID; mLabelForId = ROOT_NODE_ID; mLabeledById = ROOT_NODE_ID; + mTraversalBefore = ROOT_NODE_ID; + mTraversalAfter = ROOT_NODE_ID; mWindowId = UNDEFINED_ITEM_ID; mConnectionId = UNDEFINED_CONNECTION_ID; mMaxTextLength = -1; @@ -2911,6 +3025,8 @@ public class AccessibilityNodeInfo implements Parcelable { builder.append("; accessibilityViewId: " + getAccessibilityViewId(mSourceNodeId)); builder.append("; virtualDescendantId: " + getVirtualDescendantId(mSourceNodeId)); builder.append("; mParentNodeId: " + mParentNodeId); + builder.append("; traversalBefore: ").append(mTraversalBefore); + builder.append("; traversalAfter: ").append(mTraversalAfter); int granularities = mMovementGranularities; builder.append("; MovementGranularities: ["); @@ -2963,6 +3079,16 @@ public class AccessibilityNodeInfo implements Parcelable { return builder.toString(); } + private AccessibilityNodeInfo getNodeForAccessibilityId(long accessibilityId) { + if (!canPerformRequestOverConnection(accessibilityId)) { + return null; + } + AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); + return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId, + mWindowId, accessibilityId, false, FLAG_PREFETCH_PREDECESSORS + | FLAG_PREFETCH_DESCENDANTS | FLAG_PREFETCH_SIBLINGS); + } + /** * A class defining an action that can be performed on an {@link AccessibilityNodeInfo}. * Each action has a unique id that is mandatory and optional data. diff --git a/core/java/android/view/accessibility/AccessibilityWindowInfo.java b/core/java/android/view/accessibility/AccessibilityWindowInfo.java index ad55f5f..e1942be 100644 --- a/core/java/android/view/accessibility/AccessibilityWindowInfo.java +++ b/core/java/android/view/accessibility/AccessibilityWindowInfo.java @@ -51,11 +51,24 @@ public final class AccessibilityWindowInfo implements Parcelable { */ public static final int TYPE_SYSTEM = 3; + /** + * Window type: Windows that are overlaid <em>only</em> by an {@link + * android.accessibilityservice.AccessibilityService} for interception of + * user interactions without changing the windows an accessibility service + * can introspect. In particular, an accessibility service can introspect + * only windows that a sighted user can interact with which they can touch + * these windows or can type into these windows. For example, if there + * is a full screen accessibility overlay that is touchable, the windows + * below it will be introspectable by an accessibility service regardless + * they are covered by a touchable window. + */ + public static final int TYPE_ACCESSIBILITY_OVERLAY = 4; + private static final int UNDEFINED = -1; private static final int BOOLEAN_PROPERTY_ACTIVE = 1 << 0; private static final int BOOLEAN_PROPERTY_FOCUSED = 1 << 1; - private static final int BOOLEAN_PROPERTY_ACCESSIBLITY_FOCUSED = 1 << 2; + private static final int BOOLEAN_PROPERTY_ACCESSIBILITY_FOCUSED = 1 << 2; // Housekeeping. private static final int MAX_POOL_SIZE = 10; @@ -85,6 +98,7 @@ public final class AccessibilityWindowInfo implements Parcelable { * @see #TYPE_APPLICATION * @see #TYPE_INPUT_METHOD * @see #TYPE_SYSTEM + * @see #TYPE_ACCESSIBILITY_OVERLAY */ public int getType() { return mType; @@ -93,7 +107,7 @@ public final class AccessibilityWindowInfo implements Parcelable { /** * Sets the type of the window. * - * @param The type + * @param type The type * * @hide */ @@ -115,7 +129,7 @@ public final class AccessibilityWindowInfo implements Parcelable { * Sets the layer which determines the Z-order of the window. Windows * with greater layer appear on top of windows with lesser layer. * - * @param The window layer. + * @param layer The window layer. * * @hide */ @@ -174,7 +188,7 @@ public final class AccessibilityWindowInfo implements Parcelable { /** * Sets the unique window id. * - * @param windowId The window id. + * @param id The window id. * * @hide */ @@ -230,7 +244,7 @@ public final class AccessibilityWindowInfo implements Parcelable { * the user is currently touching or the window has input focus * and the user is not touching any window. * - * @param Whether this is the active window. + * @param active Whether this is the active window. * * @hide */ @@ -250,7 +264,7 @@ public final class AccessibilityWindowInfo implements Parcelable { /** * Sets if this window has input focus. * - * @param Whether has input focus. + * @param focused Whether has input focus. * * @hide */ @@ -264,18 +278,18 @@ public final class AccessibilityWindowInfo implements Parcelable { * @return Whether has accessibility focus. */ public boolean isAccessibilityFocused() { - return getBooleanProperty(BOOLEAN_PROPERTY_ACCESSIBLITY_FOCUSED); + return getBooleanProperty(BOOLEAN_PROPERTY_ACCESSIBILITY_FOCUSED); } /** * Sets if this window has accessibility focus. * - * @param Whether has accessibility focus. + * @param focused Whether has accessibility focus. * * @hide */ public void setAccessibilityFocused(boolean focused) { - setBooleanProperty(BOOLEAN_PROPERTY_ACCESSIBLITY_FOCUSED, focused); + setBooleanProperty(BOOLEAN_PROPERTY_ACCESSIBILITY_FOCUSED, focused); } /** @@ -534,6 +548,9 @@ public final class AccessibilityWindowInfo implements Parcelable { case TYPE_SYSTEM: { return "TYPE_SYSTEM"; } + case TYPE_ACCESSIBILITY_OVERLAY: { + return "TYPE_ACCESSIBILITY_OVERLAY"; + } default: return "<UNKNOWN>"; } diff --git a/core/java/android/view/animation/AccelerateDecelerateInterpolator.java b/core/java/android/view/animation/AccelerateDecelerateInterpolator.java index ed6949a..21d5a5b 100644 --- a/core/java/android/view/animation/AccelerateDecelerateInterpolator.java +++ b/core/java/android/view/animation/AccelerateDecelerateInterpolator.java @@ -26,17 +26,17 @@ import com.android.internal.view.animation.NativeInterpolatorFactoryHelper; /** * An interpolator where the rate of change starts and ends slowly but * accelerates through the middle. - * */ @HasNativeInterpolator -public class AccelerateDecelerateInterpolator implements Interpolator, NativeInterpolatorFactory { +public class AccelerateDecelerateInterpolator extends BaseInterpolator + implements NativeInterpolatorFactory { public AccelerateDecelerateInterpolator() { } - + @SuppressWarnings({"UnusedDeclaration"}) public AccelerateDecelerateInterpolator(Context context, AttributeSet attrs) { } - + public float getInterpolation(float input) { return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f; } diff --git a/core/java/android/view/animation/AccelerateInterpolator.java b/core/java/android/view/animation/AccelerateInterpolator.java index 1c75f16..6c8d7b1 100644 --- a/core/java/android/view/animation/AccelerateInterpolator.java +++ b/core/java/android/view/animation/AccelerateInterpolator.java @@ -33,7 +33,7 @@ import com.android.internal.view.animation.NativeInterpolatorFactoryHelper; * */ @HasNativeInterpolator -public class AccelerateInterpolator implements Interpolator, NativeInterpolatorFactory { +public class AccelerateInterpolator extends BaseInterpolator implements NativeInterpolatorFactory { private final float mFactor; private final double mDoubleFactor; @@ -70,7 +70,7 @@ public class AccelerateInterpolator implements Interpolator, NativeInterpolatorF mFactor = a.getFloat(R.styleable.AccelerateInterpolator_factor, 1.0f); mDoubleFactor = 2 * mFactor; - + setChangingConfiguration(a.getChangingConfigurations()); a.recycle(); } diff --git a/core/java/android/view/animation/AnimationUtils.java b/core/java/android/view/animation/AnimationUtils.java index af4e04f..606c83e 100644 --- a/core/java/android/view/animation/AnimationUtils.java +++ b/core/java/android/view/animation/AnimationUtils.java @@ -321,7 +321,7 @@ public class AnimationUtils { private static Interpolator createInterpolatorFromXml(Resources res, Theme theme, XmlPullParser parser) throws XmlPullParserException, IOException { - Interpolator interpolator = null; + BaseInterpolator interpolator = null; // Make sure we are on a start tag. int type; @@ -361,10 +361,7 @@ public class AnimationUtils { } else { throw new RuntimeException("Unknown interpolator name: " + parser.getName()); } - } - return interpolator; - } } diff --git a/core/java/android/view/animation/AnticipateInterpolator.java b/core/java/android/view/animation/AnticipateInterpolator.java index fe756bd..fb66c31 100644 --- a/core/java/android/view/animation/AnticipateInterpolator.java +++ b/core/java/android/view/animation/AnticipateInterpolator.java @@ -31,7 +31,7 @@ import com.android.internal.view.animation.NativeInterpolatorFactoryHelper; * An interpolator where the change starts backward then flings forward. */ @HasNativeInterpolator -public class AnticipateInterpolator implements Interpolator, NativeInterpolatorFactory { +public class AnticipateInterpolator extends BaseInterpolator implements NativeInterpolatorFactory { private final float mTension; public AnticipateInterpolator() { @@ -60,9 +60,8 @@ public class AnticipateInterpolator implements Interpolator, NativeInterpolatorF a = res.obtainAttributes(attrs, R.styleable.AnticipateInterpolator); } - mTension = - a.getFloat(R.styleable.AnticipateInterpolator_tension, 2.0f); - + mTension = a.getFloat(R.styleable.AnticipateInterpolator_tension, 2.0f); + setChangingConfiguration(a.getChangingConfigurations()); a.recycle(); } diff --git a/core/java/android/view/animation/AnticipateOvershootInterpolator.java b/core/java/android/view/animation/AnticipateOvershootInterpolator.java index 78e5acf..1af72da 100644 --- a/core/java/android/view/animation/AnticipateOvershootInterpolator.java +++ b/core/java/android/view/animation/AnticipateOvershootInterpolator.java @@ -35,7 +35,8 @@ import static com.android.internal.R.styleable.AnticipateOvershootInterpolator; * the target value and finally goes back to the final value. */ @HasNativeInterpolator -public class AnticipateOvershootInterpolator implements Interpolator, NativeInterpolatorFactory { +public class AnticipateOvershootInterpolator extends BaseInterpolator + implements NativeInterpolatorFactory { private final float mTension; public AnticipateOvershootInterpolator() { @@ -78,7 +79,7 @@ public class AnticipateOvershootInterpolator implements Interpolator, NativeInte mTension = a.getFloat(AnticipateOvershootInterpolator_tension, 2.0f) * a.getFloat(AnticipateOvershootInterpolator_extraTension, 1.5f); - + setChangingConfiguration(a.getChangingConfigurations()); a.recycle(); } diff --git a/core/java/android/view/animation/BaseInterpolator.java b/core/java/android/view/animation/BaseInterpolator.java new file mode 100644 index 0000000..9c0014c --- /dev/null +++ b/core/java/android/view/animation/BaseInterpolator.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view.animation; + +/** + * An abstract class which is extended by default interpolators. + */ +abstract public class BaseInterpolator implements Interpolator { + private int mChangingConfiguration; + /** + * @hide + */ + public int getChangingConfiguration() { + return mChangingConfiguration; + } + + /** + * @hide + */ + void setChangingConfiguration(int changingConfiguration) { + mChangingConfiguration = changingConfiguration; + } +} diff --git a/core/java/android/view/animation/BounceInterpolator.java b/core/java/android/view/animation/BounceInterpolator.java index 9d8ca90..909eaa4 100644 --- a/core/java/android/view/animation/BounceInterpolator.java +++ b/core/java/android/view/animation/BounceInterpolator.java @@ -27,7 +27,7 @@ import com.android.internal.view.animation.NativeInterpolatorFactoryHelper; * An interpolator where the change bounces at the end. */ @HasNativeInterpolator -public class BounceInterpolator implements Interpolator, NativeInterpolatorFactory { +public class BounceInterpolator extends BaseInterpolator implements NativeInterpolatorFactory { public BounceInterpolator() { } diff --git a/core/java/android/view/animation/CycleInterpolator.java b/core/java/android/view/animation/CycleInterpolator.java index 3114aa3..663c109 100644 --- a/core/java/android/view/animation/CycleInterpolator.java +++ b/core/java/android/view/animation/CycleInterpolator.java @@ -33,7 +33,7 @@ import com.android.internal.view.animation.NativeInterpolatorFactoryHelper; * */ @HasNativeInterpolator -public class CycleInterpolator implements Interpolator, NativeInterpolatorFactory { +public class CycleInterpolator extends BaseInterpolator implements NativeInterpolatorFactory { public CycleInterpolator(float cycles) { mCycles = cycles; } @@ -52,7 +52,7 @@ public class CycleInterpolator implements Interpolator, NativeInterpolatorFactor } mCycles = a.getFloat(R.styleable.CycleInterpolator_cycles, 1.0f); - + setChangingConfiguration(a.getChangingConfigurations()); a.recycle(); } diff --git a/core/java/android/view/animation/DecelerateInterpolator.java b/core/java/android/view/animation/DecelerateInterpolator.java index 674207c..f426f60 100644 --- a/core/java/android/view/animation/DecelerateInterpolator.java +++ b/core/java/android/view/animation/DecelerateInterpolator.java @@ -33,7 +33,7 @@ import com.android.internal.view.animation.NativeInterpolatorFactoryHelper; * */ @HasNativeInterpolator -public class DecelerateInterpolator implements Interpolator, NativeInterpolatorFactory { +public class DecelerateInterpolator extends BaseInterpolator implements NativeInterpolatorFactory { public DecelerateInterpolator() { } @@ -62,7 +62,7 @@ public class DecelerateInterpolator implements Interpolator, NativeInterpolatorF } mFactor = a.getFloat(R.styleable.DecelerateInterpolator_factor, 1.0f); - + setChangingConfiguration(a.getChangingConfigurations()); a.recycle(); } diff --git a/core/java/android/view/animation/LinearInterpolator.java b/core/java/android/view/animation/LinearInterpolator.java index 552c611..2a047b4 100644 --- a/core/java/android/view/animation/LinearInterpolator.java +++ b/core/java/android/view/animation/LinearInterpolator.java @@ -25,17 +25,16 @@ import com.android.internal.view.animation.NativeInterpolatorFactoryHelper; /** * An interpolator where the rate of change is constant - * */ @HasNativeInterpolator -public class LinearInterpolator implements Interpolator, NativeInterpolatorFactory { +public class LinearInterpolator extends BaseInterpolator implements NativeInterpolatorFactory { public LinearInterpolator() { } - + public LinearInterpolator(Context context, AttributeSet attrs) { } - + public float getInterpolation(float input) { return input; } diff --git a/core/java/android/view/animation/OvershootInterpolator.java b/core/java/android/view/animation/OvershootInterpolator.java index d6c2808..306688a 100644 --- a/core/java/android/view/animation/OvershootInterpolator.java +++ b/core/java/android/view/animation/OvershootInterpolator.java @@ -32,7 +32,7 @@ import com.android.internal.view.animation.NativeInterpolatorFactoryHelper; * then comes back. */ @HasNativeInterpolator -public class OvershootInterpolator implements Interpolator, NativeInterpolatorFactory { +public class OvershootInterpolator extends BaseInterpolator implements NativeInterpolatorFactory { private final float mTension; public OvershootInterpolator() { @@ -61,9 +61,8 @@ public class OvershootInterpolator implements Interpolator, NativeInterpolatorFa a = res.obtainAttributes(attrs, R.styleable.OvershootInterpolator); } - mTension = - a.getFloat(R.styleable.OvershootInterpolator_tension, 2.0f); - + mTension = a.getFloat(R.styleable.OvershootInterpolator_tension, 2.0f); + setChangingConfiguration(a.getChangingConfigurations()); a.recycle(); } diff --git a/core/java/android/view/animation/PathInterpolator.java b/core/java/android/view/animation/PathInterpolator.java index 945ecf0..eec5555 100644 --- a/core/java/android/view/animation/PathInterpolator.java +++ b/core/java/android/view/animation/PathInterpolator.java @@ -42,7 +42,7 @@ import com.android.internal.R; * path.lineTo(1f, 1f); * </pre></blockquote></p> */ -public class PathInterpolator implements Interpolator { +public class PathInterpolator extends BaseInterpolator { // This governs how accurate the approximation of the Path is. private static final float PRECISION = 0.002f; @@ -98,7 +98,7 @@ public class PathInterpolator implements Interpolator { a = res.obtainAttributes(attrs, R.styleable.PathInterpolator); } parseInterpolatorFromTypeArray(a); - + setChangingConfiguration(a.getChangingConfigurations()); a.recycle(); } diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 0439168..6927660 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -2663,7 +2663,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * @return True if the selector should be shown */ boolean shouldShowSelector() { - return (!isInTouchMode()) || (touchModeDrawsInPressedState() && isPressed()); + return (isFocused() && !isInTouchMode()) || (touchModeDrawsInPressedState() && isPressed()); } private void drawSelector(Canvas canvas) { @@ -4653,7 +4653,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te if (mPositionScroller == null) { mPositionScroller = createPositionScroller(); } - mPositionScroller.startWithOffset(position, offset, offset); + mPositionScroller.startWithOffset(position, offset); } /** @@ -4910,9 +4910,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te if (position >= headerViewsCount && position < footerViewsStart) { // The view will be rebound to new data, clear any // system-managed transient state. - if (child.isAccessibilityFocused()) { - child.clearAccessibilityFocus(); - } + child.clearAccessibilityFocus(); mRecycler.addScrapView(child, position); } } @@ -4933,9 +4931,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te if (position >= headerViewsCount && position < footerViewsStart) { // The view will be rebound to new data, clear any // system-managed transient state. - if (child.isAccessibilityFocused()) { - child.clearAccessibilityFocus(); - } + child.clearAccessibilityFocus(); mRecycler.addScrapView(child, position); } } @@ -6776,9 +6772,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } private void clearAccessibilityFromScrap(View view) { - if (view.isAccessibilityFocused()) { - view.clearAccessibilityFocus(); - } + view.clearAccessibilityFocus(); view.setAccessibilityDelegate(null); } diff --git a/core/java/android/widget/AbsSeekBar.java b/core/java/android/widget/AbsSeekBar.java index b2cfdf7..d39960f 100644 --- a/core/java/android/widget/AbsSeekBar.java +++ b/core/java/android/widget/AbsSeekBar.java @@ -267,6 +267,12 @@ public abstract class AbsSeekBar extends ProgressBar { if (mHasThumbTintMode) { mThumb.setTintMode(mThumbTintMode); } + + // The drawable (or one of its children) may not have been + // stateful before applying the tint, so let's try again. + if (mThumb.isStateful()) { + mThumb.setState(getDrawableState()); + } } } diff --git a/core/java/android/widget/ActionMenuView.java b/core/java/android/widget/ActionMenuView.java index 7198e52..0a8a01f 100644 --- a/core/java/android/widget/ActionMenuView.java +++ b/core/java/android/widget/ActionMenuView.java @@ -429,7 +429,7 @@ public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvo } final int childCount = getChildCount(); - final int midVertical = (top + bottom) / 2; + final int midVertical = (bottom - top) / 2; final int dividerWidth = getDividerWidth(); int overflowWidth = 0; int nonOverflowWidth = 0; diff --git a/core/java/android/widget/AdapterView.java b/core/java/android/widget/AdapterView.java index b9f891c..5e2394c 100644 --- a/core/java/android/widget/AdapterView.java +++ b/core/java/android/widget/AdapterView.java @@ -215,7 +215,12 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { private boolean mDesiredFocusableState; private boolean mDesiredFocusableInTouchModeState; + /** Lazily-constructed runnable for dispatching selection events. */ private SelectionNotifier mSelectionNotifier; + + /** Selection notifier that's waiting for the next layout pass. */ + private SelectionNotifier mPendingSelectionNotifier; + /** * When set to true, calls to requestLayout() will not propagate up the parent hierarchy. * This is used to layout the children during a layout pass. @@ -854,39 +859,51 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { private class SelectionNotifier implements Runnable { public void run() { - if (mDataChanged) { - // Data has changed between when this SelectionNotifier - // was posted and now. We need to wait until the AdapterView - // has been synched to the new data. + mPendingSelectionNotifier = null; + + if (mDataChanged && getViewRootImpl() != null + && getViewRootImpl().isLayoutRequested()) { + // Data has changed between when this SelectionNotifier was + // posted and now. Postpone the notification until the next + // layout is complete and we run checkSelectionChanged(). if (getAdapter() != null) { - post(this); + mPendingSelectionNotifier = this; } } else { - fireOnSelected(); - performAccessibilityActionsOnSelected(); + dispatchOnItemSelected(); } } } void selectionChanged() { + // We're about to post or run the selection notifier, so we don't need + // a pending notifier. + mPendingSelectionNotifier = null; + if (mOnItemSelectedListener != null || AccessibilityManager.getInstance(mContext).isEnabled()) { if (mInLayout || mBlockLayoutRequests) { // If we are in a layout traversal, defer notification // by posting. This ensures that the view tree is - // in a consistent state and is able to accomodate + // in a consistent state and is able to accommodate // new layout or invalidate requests. if (mSelectionNotifier == null) { mSelectionNotifier = new SelectionNotifier(); + } else { + removeCallbacks(mSelectionNotifier); } post(mSelectionNotifier); } else { - fireOnSelected(); - performAccessibilityActionsOnSelected(); + dispatchOnItemSelected(); } } } + private void dispatchOnItemSelected() { + fireOnSelected(); + performAccessibilityActionsOnSelected(); + } + private void fireOnSelected() { if (mOnItemSelectedListener == null) { return; @@ -1042,12 +1059,22 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { notifySubtreeAccessibilityStateChangedIfNeeded(); } + /** + * Called after layout to determine whether the selection position needs to + * be updated. Also used to fire any pending selection events. + */ void checkSelectionChanged() { if ((mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId != mOldSelectedRowId)) { selectionChanged(); mOldSelectedPosition = mSelectedPosition; mOldSelectedRowId = mSelectedRowId; } + + // If we have a pending selection notification -- and we won't if we + // just fired one in selectionChanged() -- run it now. + if (mPendingSelectionNotifier != null) { + mPendingSelectionNotifier.run(); + } } /** diff --git a/core/java/android/widget/AppSecurityPermissions.java b/core/java/android/widget/AppSecurityPermissions.java index 10e56c7..5c05b5a 100644 --- a/core/java/android/widget/AppSecurityPermissions.java +++ b/core/java/android/widget/AppSecurityPermissions.java @@ -99,12 +99,12 @@ public class AppSecurityPermissions { public Drawable loadGroupIcon(PackageManager pm) { if (icon != 0) { - return loadIcon(pm); + return loadUnbadgedIcon(pm); } else { ApplicationInfo appInfo; try { appInfo = pm.getApplicationInfo(packageName, 0); - return appInfo.loadIcon(pm); + return appInfo.loadUnbadgedIcon(pm); } catch (NameNotFoundException e) { } } diff --git a/core/java/android/widget/CalendarView.java b/core/java/android/widget/CalendarView.java index ea60abb..f380d68 100644 --- a/core/java/android/widget/CalendarView.java +++ b/core/java/android/widget/CalendarView.java @@ -775,9 +775,14 @@ public class CalendarView extends FrameLayout { private ViewGroup mDayNamesHeader; /** - * Cached labels for the week names header. + * Cached abbreviations for day of week names. */ - private String[] mDayLabels; + private String[] mDayNamesShort; + + /** + * Cached full-length day of week names. + */ + private String[] mDayNamesLong; /** * The first day of the week. @@ -1306,11 +1311,14 @@ public class CalendarView extends FrameLayout { * Sets up the strings to be used by the header. */ private void setUpHeader() { - mDayLabels = new String[mDaysPerWeek]; + mDayNamesShort = new String[mDaysPerWeek]; + mDayNamesLong = new String[mDaysPerWeek]; for (int i = mFirstDayOfWeek, count = mFirstDayOfWeek + mDaysPerWeek; i < count; i++) { int calendarDay = (i > Calendar.SATURDAY) ? i - Calendar.SATURDAY : i; - mDayLabels[i - mFirstDayOfWeek] = DateUtils.getDayOfWeekString(calendarDay, + mDayNamesShort[i - mFirstDayOfWeek] = DateUtils.getDayOfWeekString(calendarDay, DateUtils.LENGTH_SHORTEST); + mDayNamesLong[i - mFirstDayOfWeek] = DateUtils.getDayOfWeekString(calendarDay, + DateUtils.LENGTH_LONG); } TextView label = (TextView) mDayNamesHeader.getChildAt(0); @@ -1325,7 +1333,8 @@ public class CalendarView extends FrameLayout { label.setTextAppearance(mContext, mWeekDayTextAppearanceResId); } if (i < mDaysPerWeek + 1) { - label.setText(mDayLabels[i - 1]); + label.setText(mDayNamesShort[i - 1]); + label.setContentDescription(mDayNamesLong[i - 1]); label.setVisibility(View.VISIBLE); } else { label.setVisibility(View.GONE); diff --git a/core/java/android/widget/CheckedTextView.java b/core/java/android/widget/CheckedTextView.java index eb8e8aa..69969a9 100644 --- a/core/java/android/widget/CheckedTextView.java +++ b/core/java/android/widget/CheckedTextView.java @@ -267,6 +267,12 @@ public class CheckedTextView extends TextView implements Checkable { if (mHasCheckMarkTintMode) { mCheckMarkDrawable.setTintMode(mCheckMarkTintMode); } + + // The drawable (or one of its children) may not have been + // stateful before applying the tint, so let's try again. + if (mCheckMarkDrawable.isStateful()) { + mCheckMarkDrawable.setState(getDrawableState()); + } } } diff --git a/core/java/android/widget/CompoundButton.java b/core/java/android/widget/CompoundButton.java index 7d9d305..447ccc2 100644 --- a/core/java/android/widget/CompoundButton.java +++ b/core/java/android/widget/CompoundButton.java @@ -29,6 +29,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.view.Gravity; +import android.view.SoundEffectConstants; import android.view.ViewDebug; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; @@ -114,15 +115,16 @@ public abstract class CompoundButton extends Button implements Checkable { @Override public boolean performClick() { - /* - * XXX: These are tiny, need some surrounding 'expanded touch area', - * which will need to be implemented in Button if we only override - * performClick() - */ - - /* When clicked, toggle the state */ toggle(); - return super.performClick(); + + final boolean handled = super.performClick(); + if (!handled) { + // View only makes a sound effect if the onClickListener was + // called, so we'll need to make one here instead. + playSoundEffect(SoundEffectConstants.CLICK); + } + + return handled; } @ViewDebug.ExportedProperty @@ -313,6 +315,12 @@ public abstract class CompoundButton extends Button implements Checkable { if (mHasButtonTintMode) { mButtonDrawable.setTintMode(mButtonTintMode); } + + // The drawable (or one of its children) may not have been + // stateful before applying the tint, so let's try again. + if (mButtonDrawable.isStateful()) { + mButtonDrawable.setState(getDrawableState()); + } } } diff --git a/core/java/android/widget/DatePickerCalendarDelegate.java b/core/java/android/widget/DatePickerCalendarDelegate.java index e71b383..cf3dbab 100644 --- a/core/java/android/widget/DatePickerCalendarDelegate.java +++ b/core/java/android/widget/DatePickerCalendarDelegate.java @@ -21,13 +21,11 @@ import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; -import android.graphics.drawable.Drawable; import android.os.Parcel; import android.os.Parcelable; import android.text.format.DateFormat; import android.text.format.DateUtils; import android.util.AttributeSet; -import android.util.SparseArray; import android.view.HapticFeedbackConstants; import android.view.LayoutInflater; import android.view.View; @@ -185,7 +183,12 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate i mHeaderYearTextView.getTextColors(), R.attr.state_selected, headerSelectedTextColor)); - mDayPickerView = new DayPickerView(mContext, this); + mDayPickerView = new DayPickerView(mContext); + mDayPickerView.setFirstDayOfWeek(mFirstDayOfWeek); + mDayPickerView.setRange(mMinDate, mMaxDate); + mDayPickerView.setDay(mCurrentDate); + mDayPickerView.setOnDaySelectedListener(mOnDaySelectedListener); + mYearPickerView = new YearPickerView(mContext); mYearPickerView.init(this); @@ -333,7 +336,7 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate i switch (viewIndex) { case MONTH_AND_DAY_VIEW: - mDayPickerView.onDateChanged(); + mDayPickerView.setDay(getSelectedDay()); if (mCurrentView != viewIndex) { mMonthAndDayLayout.setSelected(true); mHeaderYearTextView.setSelected(false); @@ -411,7 +414,8 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate i updateDisplay(false); } mMinDate.setTimeInMillis(minDate); - mDayPickerView.goTo(getSelectedDay(), false, true, true); + mDayPickerView.setRange(mMinDate, mMaxDate); + mYearPickerView.setRange(mMinDate, mMaxDate); } @Override @@ -432,7 +436,8 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate i updateDisplay(false); } mMaxDate.setTimeInMillis(maxDate); - mDayPickerView.goTo(getSelectedDay(), false, true, true); + mDayPickerView.setRange(mMinDate, mMaxDate); + mYearPickerView.setRange(mMinDate, mMaxDate); } @Override @@ -443,6 +448,8 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate i @Override public void setFirstDayOfWeek(int firstDayOfWeek) { mFirstDayOfWeek = firstDayOfWeek; + + mDayPickerView.setFirstDayOfWeek(firstDayOfWeek); } @Override @@ -454,36 +461,6 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate i } @Override - public int getMinYear() { - return mMinDate.get(Calendar.YEAR); - } - - @Override - public int getMaxYear() { - return mMaxDate.get(Calendar.YEAR); - } - - @Override - public int getMinMonth() { - return mMinDate.get(Calendar.MONTH); - } - - @Override - public int getMaxMonth() { - return mMaxDate.get(Calendar.MONTH); - } - - @Override - public int getMinDay() { - return mMinDate.get(Calendar.DAY_OF_MONTH); - } - - @Override - public int getMaxDay() { - return mMaxDate.get(Calendar.DAY_OF_MONTH); - } - - @Override public void setEnabled(boolean enabled) { mMonthAndDayLayout.setEnabled(enabled); mHeaderYearTextView.setEnabled(enabled); @@ -634,19 +611,12 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate i } } - @Override - public void onDayOfMonthSelected(int year, int month, int day) { - mCurrentDate.set(Calendar.YEAR, year); - mCurrentDate.set(Calendar.MONTH, month); - mCurrentDate.set(Calendar.DAY_OF_MONTH, day); - updatePickers(); - updateDisplay(true); - } - private void updatePickers() { for (OnDateChangedListener listener : mListeners) { listener.onDateChanged(); } + + mDayPickerView.setDay(getSelectedDay()); } @Override @@ -655,11 +625,6 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate i } @Override - public void unregisterOnDateChangedListener(OnDateChangedListener listener) { - mListeners.remove(listener); - } - - @Override public Calendar getSelectedDay() { return mCurrentDate; } @@ -680,6 +645,22 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate i } /** + * Listener called when the user selects a day in the day picker view. + */ + private final DayPickerView.OnDaySelectedListener + mOnDaySelectedListener = new DayPickerView.OnDaySelectedListener() { + @Override + public void onDaySelected(DayPickerView view, Calendar day) { + mCurrentDate.setTimeInMillis(day.getTimeInMillis()); + + updatePickers(); + updateDisplay(true); + + tryVibrate(); + } + }; + + /** * Class for managing state storing/restoring. */ private static class SavedState extends View.BaseSavedState { diff --git a/core/java/android/widget/DatePickerController.java b/core/java/android/widget/DatePickerController.java index 059709d..8f809ba 100644 --- a/core/java/android/widget/DatePickerController.java +++ b/core/java/android/widget/DatePickerController.java @@ -27,31 +27,9 @@ interface DatePickerController { void onYearSelected(int year); - void onDayOfMonthSelected(int year, int month, int day); - void registerOnDateChangedListener(OnDateChangedListener listener); - void unregisterOnDateChangedListener(OnDateChangedListener listener); - Calendar getSelectedDay(); - void setFirstDayOfWeek(int firstDayOfWeek); - int getFirstDayOfWeek(); - - int getMinYear(); - int getMaxYear(); - - int getMinMonth(); - int getMaxMonth(); - - int getMinDay(); - int getMaxDay(); - - void setMinDate(long minDate); - Calendar getMinDate(); - - void setMaxDate(long maxDate); - Calendar getMaxDate(); - void tryVibrate(); } diff --git a/core/java/android/widget/DateTimeView.java b/core/java/android/widget/DateTimeView.java index 45d1403..443884a 100644 --- a/core/java/android/widget/DateTimeView.java +++ b/core/java/android/widget/DateTimeView.java @@ -32,6 +32,7 @@ import android.widget.RemoteViews.RemoteView; import java.text.DateFormat; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Date; // @@ -62,8 +63,8 @@ public class DateTimeView extends TextView { int mLastDisplay = -1; DateFormat mLastFormat; - private boolean mAttachedToWindow; private long mUpdateTimeMillis; + private static final ThreadLocal<ReceiverInfo> sReceiverInfo = new ThreadLocal<ReceiverInfo>(); public DateTimeView(Context context) { super(context); @@ -76,15 +77,21 @@ public class DateTimeView extends TextView { @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); - registerReceivers(); - mAttachedToWindow = true; + ReceiverInfo ri = sReceiverInfo.get(); + if (ri == null) { + ri = new ReceiverInfo(); + sReceiverInfo.set(ri); + } + ri.addView(this); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); - unregisterReceivers(); - mAttachedToWindow = false; + final ReceiverInfo ri = sReceiverInfo.get(); + if (ri != null) { + ri.removeView(this); + } } @android.view.RemotableViewMethod @@ -204,49 +211,86 @@ public class DateTimeView extends TextView { } } - private void registerReceivers() { - Context context = getContext(); + void clearFormatAndUpdate() { + mLastFormat = null; + update(); + } - IntentFilter filter = new IntentFilter(); - filter.addAction(Intent.ACTION_TIME_TICK); - filter.addAction(Intent.ACTION_TIME_CHANGED); - filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); - filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); - context.registerReceiver(mBroadcastReceiver, filter); + private static class ReceiverInfo { + private final ArrayList<DateTimeView> mAttachedViews = new ArrayList<DateTimeView>(); + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (Intent.ACTION_TIME_TICK.equals(action)) { + if (System.currentTimeMillis() < getSoonestUpdateTime()) { + // The update() function takes a few milliseconds to run because of + // all of the time conversions it needs to do, so we can't do that + // every minute. + return; + } + } + // ACTION_TIME_CHANGED can also signal a change of 12/24 hr. format. + updateAll(); + } + }; - Uri uri = Settings.System.getUriFor(Settings.System.DATE_FORMAT); - context.getContentResolver().registerContentObserver(uri, true, mContentObserver); - } + private final ContentObserver mObserver = new ContentObserver(new Handler()) { + @Override + public void onChange(boolean selfChange) { + updateAll(); + } + }; - private void unregisterReceivers() { - Context context = getContext(); - context.unregisterReceiver(mBroadcastReceiver); - context.getContentResolver().unregisterContentObserver(mContentObserver); - } + public void addView(DateTimeView v) { + final boolean register = mAttachedViews.isEmpty(); + mAttachedViews.add(v); + if (register) { + register(v.getContext().getApplicationContext()); + } + } - private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (Intent.ACTION_TIME_TICK.equals(action)) { - if (System.currentTimeMillis() < mUpdateTimeMillis) { - // The update() function takes a few milliseconds to run because of - // all of the time conversions it needs to do, so we can't do that - // every minute. - return; + public void removeView(DateTimeView v) { + mAttachedViews.remove(v); + if (mAttachedViews.isEmpty()) { + unregister(v.getContext().getApplicationContext()); + } + } + + void updateAll() { + final int count = mAttachedViews.size(); + for (int i = 0; i < count; i++) { + mAttachedViews.get(i).clearFormatAndUpdate(); + } + } + + long getSoonestUpdateTime() { + long result = Long.MAX_VALUE; + final int count = mAttachedViews.size(); + for (int i = 0; i < count; i++) { + final long time = mAttachedViews.get(i).mUpdateTimeMillis; + if (time < result) { + result = time; } } - // ACTION_TIME_CHANGED can also signal a change of 12/24 hr. format. - mLastFormat = null; - update(); + return result; } - }; - private ContentObserver mContentObserver = new ContentObserver(new Handler()) { - @Override - public void onChange(boolean selfChange) { - mLastFormat = null; - update(); + void register(Context context) { + final IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_TIME_TICK); + filter.addAction(Intent.ACTION_TIME_CHANGED); + filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); + filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); + context.registerReceiver(mReceiver, filter); + + final Uri uri = Settings.System.getUriFor(Settings.System.DATE_FORMAT); + context.getContentResolver().registerContentObserver(uri, true, mObserver); } - }; + + void unregister(Context context) { + context.unregisterReceiver(mReceiver); + context.getContentResolver().unregisterContentObserver(mObserver); + } + } } diff --git a/core/java/android/widget/DayPickerView.java b/core/java/android/widget/DayPickerView.java index ca4095e..6cb1c9d 100644 --- a/core/java/android/widget/DayPickerView.java +++ b/core/java/android/widget/DayPickerView.java @@ -16,15 +16,12 @@ package android.widget; -import android.annotation.SuppressLint; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Configuration; -import android.os.Build; import android.os.Bundle; -import android.os.Handler; -import android.util.AttributeSet; import android.util.Log; +import android.util.MathUtils; import android.view.View; import android.view.ViewConfiguration; import android.view.accessibility.AccessibilityEvent; @@ -37,9 +34,7 @@ import java.util.Locale; /** * This displays a list of months in a calendar format with selectable days. */ -class DayPickerView extends ListView implements AbsListView.OnScrollListener, - OnDateChangedListener { - +class DayPickerView extends ListView implements AbsListView.OnScrollListener { private static final String TAG = "DayPickerView"; // How long the GoTo fling animation should last @@ -48,18 +43,22 @@ class DayPickerView extends ListView implements AbsListView.OnScrollListener, // How long to wait after receiving an onScrollStateChanged notification before acting on it private static final int SCROLL_CHANGE_DELAY = 40; - private static int LIST_TOP_OFFSET = -1; // so that the top line will be under the separator + // so that the top line will be under the separator + private static final int LIST_TOP_OFFSET = -1; - private SimpleDateFormat mYearFormat = new SimpleDateFormat("yyyy", Locale.getDefault()); + private final SimpleMonthAdapter mAdapter = new SimpleMonthAdapter(getContext()); - // These affect the scroll speed and feel - private float mFriction = 1.0f; + private final ScrollStateRunnable mScrollStateChangedRunnable = new ScrollStateRunnable(this); + + private SimpleDateFormat mYearFormat = new SimpleDateFormat("yyyy", Locale.getDefault()); // highlighted time private Calendar mSelectedDay = Calendar.getInstance(); - private SimpleMonthAdapter mAdapter; - private Calendar mTempDay = Calendar.getInstance(); + private Calendar mMinDate = Calendar.getInstance(); + private Calendar mMaxDate = Calendar.getInstance(); + + private OnDaySelectedListener mOnDaySelectedListener; // which month should be displayed/highlighted [0-11] private int mCurrentMonthDisplayed; @@ -68,60 +67,54 @@ class DayPickerView extends ListView implements AbsListView.OnScrollListener, // used for tracking what state listview is in private int mCurrentScrollState = OnScrollListener.SCROLL_STATE_IDLE; - private DatePickerController mController; private boolean mPerformingScroll; - private ScrollStateRunnable mScrollStateChangedRunnable = new ScrollStateRunnable(this); - - public DayPickerView(Context context, DatePickerController controller) { + public DayPickerView(Context context) { super(context); - init(); - setController(controller); - } - public void setController(DatePickerController controller) { - if (mController != null) { - mController.unregisterOnDateChangedListener(this); - } - mController = controller; - mController.registerOnDateChangedListener(this); - setUpAdapter(); setAdapter(mAdapter); - onDateChanged(); - } - - public void init() { setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); setDrawSelectorOnTop(false); - setUpListView(); + + goTo(mSelectedDay, false, true, true); + + mAdapter.setOnDaySelectedListener(mProxyOnDaySelectedListener); } - public void onChange() { - setUpAdapter(); - setAdapter(mAdapter); + public void setDay(Calendar day) { + goTo(day, false, true, true); + } + + public void setFirstDayOfWeek(int firstDayOfWeek) { + mAdapter.setFirstDayOfWeek(firstDayOfWeek); + } + + public void setRange(Calendar minDate, Calendar maxDate) { + mMinDate.setTimeInMillis(minDate.getTimeInMillis()); + mMaxDate.setTimeInMillis(maxDate.getTimeInMillis()); + + mAdapter.setRange(mMinDate, mMaxDate); + + // Changing the min/max date changes the selection position since we + // don't really have stable IDs. + goTo(mSelectedDay, false, true, true); } /** - * Creates a new adapter if necessary and sets up its parameters. Override - * this method to provide a custom adapter. + * Sets the listener to call when the user selects a day. + * + * @param listener The listener to call. */ - protected void setUpAdapter() { - if (mAdapter == null) { - mAdapter = new SimpleMonthAdapter(getContext(), mController); - } else { - mAdapter.setSelectedDay(mSelectedDay); - mAdapter.notifyDataSetChanged(); - } - // refresh the view with the new parameters - mAdapter.notifyDataSetChanged(); + public void setOnDaySelectedListener(OnDaySelectedListener listener) { + mOnDaySelectedListener = listener; } /* * Sets all the required fields for the list view. Override this method to * set a different list view behavior. */ - protected void setUpListView() { + private void setUpListView() { // Transparent background on scroll setCacheColorHint(0); // No dividers @@ -134,26 +127,19 @@ class DayPickerView extends ListView implements AbsListView.OnScrollListener, setOnScrollListener(this); setFadingEdgeLength(0); // Make the scrolling behavior nicer - setFriction(ViewConfiguration.getScrollFriction() * mFriction); + setFriction(ViewConfiguration.getScrollFriction()); } - private int getDiffMonths(Calendar start, Calendar end){ + private int getDiffMonths(Calendar start, Calendar end) { final int diffYears = end.get(Calendar.YEAR) - start.get(Calendar.YEAR); final int diffMonths = end.get(Calendar.MONTH) - start.get(Calendar.MONTH) + 12 * diffYears; return diffMonths; } private int getPositionFromDay(Calendar day) { - final int diffMonthMax = getDiffMonths(mController.getMinDate(), mController.getMaxDate()); - int diffMonth = getDiffMonths(mController.getMinDate(), day); - - if (diffMonth < 0 ) { - diffMonth = 0; - } else if (diffMonth > diffMonthMax) { - diffMonth = diffMonthMax; - } - - return diffMonth; + final int diffMonthMax = getDiffMonths(mMinDate, mMaxDate); + final int diffMonth = getDiffMonths(mMinDate, day); + return MathUtils.constrain(diffMonth, 0, diffMonthMax); } /** @@ -171,8 +157,7 @@ class DayPickerView extends ListView implements AbsListView.OnScrollListener, * visible * @return Whether or not the view animated to the new location */ - public boolean goTo(Calendar day, boolean animate, boolean setSelected, - boolean forceScroll) { + private boolean goTo(Calendar day, boolean animate, boolean setSelected, boolean forceScroll) { // Set the selected day if (setSelected) { @@ -361,11 +346,6 @@ class DayPickerView extends ListView implements AbsListView.OnScrollListener, return firstPosition + mostVisibleIndex; } - @Override - public void onDateChanged() { - goTo(mController.getSelectedDay(), false, true, true); - } - /** * Attempts to return the date that has accessibility focus. * @@ -464,10 +444,10 @@ class DayPickerView extends ListView implements AbsListView.OnScrollListener, } // Figure out what month is showing. - int firstVisiblePosition = getFirstVisiblePosition(); - int month = firstVisiblePosition % 12; - int year = firstVisiblePosition / 12 + mController.getMinYear(); - Calendar day = Calendar.getInstance(); + final int firstVisiblePosition = getFirstVisiblePosition(); + final int month = firstVisiblePosition % 12; + final int year = firstVisiblePosition / 12 + mMinDate.get(Calendar.YEAR); + final Calendar day = Calendar.getInstance(); day.set(year, month, 1); // Scroll either forward or backward one month. @@ -498,4 +478,18 @@ class DayPickerView extends ListView implements AbsListView.OnScrollListener, mPerformingScroll = true; return true; } + + public interface OnDaySelectedListener { + public void onDaySelected(DayPickerView view, Calendar day); + } + + private final SimpleMonthAdapter.OnDaySelectedListener + mProxyOnDaySelectedListener = new SimpleMonthAdapter.OnDaySelectedListener() { + @Override + public void onDaySelected(SimpleMonthAdapter adapter, Calendar day) { + if (mOnDaySelectedListener != null) { + mOnDaySelectedListener.onDaySelected(DayPickerView.this, day); + } + } + }; } diff --git a/core/java/android/widget/FrameLayout.java b/core/java/android/widget/FrameLayout.java index e317524..d974c29 100644 --- a/core/java/android/widget/FrameLayout.java +++ b/core/java/android/widget/FrameLayout.java @@ -384,6 +384,12 @@ public class FrameLayout extends ViewGroup { if (mHasForegroundTintMode) { mForeground.setTintMode(mForegroundTintMode); } + + // The drawable (or one of its children) may not have been + // stateful before applying the tint, so let's try again. + if (mForeground.isStateful()) { + mForeground.setState(getDrawableState()); + } } } diff --git a/core/java/android/widget/ImageView.java b/core/java/android/widget/ImageView.java index f90a9fe..c68bfca 100644 --- a/core/java/android/widget/ImageView.java +++ b/core/java/android/widget/ImageView.java @@ -386,21 +386,21 @@ public class ImageView extends View { */ @android.view.RemotableViewMethod public void setImageResource(int resId) { - if (mUri != null || mResource != resId) { - final int oldWidth = mDrawableWidth; - final int oldHeight = mDrawableHeight; + // The resource configuration may have changed, so we should always + // try to load the resource even if the resId hasn't changed. + final int oldWidth = mDrawableWidth; + final int oldHeight = mDrawableHeight; - updateDrawable(null); - mResource = resId; - mUri = null; + updateDrawable(null); + mResource = resId; + mUri = null; - resolveUri(); + resolveUri(); - if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) { - requestLayout(); - } - invalidate(); + if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) { + requestLayout(); } + invalidate(); } /** @@ -527,6 +527,12 @@ public class ImageView extends View { if (mHasDrawableTintMode) { mDrawable.setTintMode(mDrawableTintMode); } + + // The drawable (or one of its children) may not have been + // stateful before applying the tint, so let's try again. + if (mDrawable.isStateful()) { + mDrawable.setState(getDrawableState()); + } } } @@ -820,6 +826,7 @@ public class ImageView extends View { mDrawableHeight = d.getIntrinsicHeight(); applyImageTint(); applyColorMod(); + configureBounds(); } else { mDrawableWidth = mDrawableHeight = -1; @@ -1120,6 +1127,9 @@ public class ImageView extends View { /** @hide */ public void animateTransform(Matrix matrix) { + if (mDrawable == null) { + return; + } if (matrix == null) { mDrawable.setBounds(0, 0, getWidth(), getHeight()); } else { diff --git a/core/java/android/widget/ListPopupWindow.java b/core/java/android/widget/ListPopupWindow.java index 3c186e3..a31d37e 100644 --- a/core/java/android/widget/ListPopupWindow.java +++ b/core/java/android/widget/ListPopupWindow.java @@ -1252,14 +1252,7 @@ public class ListPopupWindow { final boolean wasForwarding = mForwarding; final boolean forwarding; if (wasForwarding) { - if (mWasLongPress) { - // If we started forwarding as a result of a long-press, - // just silently stop forwarding events so that the window - // stays open. - forwarding = onTouchForwarded(event); - } else { - forwarding = onTouchForwarded(event) || !onForwardingStopped(); - } + forwarding = onTouchForwarded(event) || !onForwardingStopped(); } else { forwarding = onTouchObserved(event) && onForwardingStarted(); @@ -1639,6 +1632,11 @@ public class ListPopupWindow { setPressed(false); updateSelectorState(); + final View motionView = getChildAt(mMotionPosition - mFirstPosition); + if (motionView != null) { + motionView.setPressed(false); + } + if (mClickAnimation != null) { mClickAnimation.cancel(); mClickAnimation = null; @@ -1653,6 +1651,15 @@ public class ListPopupWindow { setPressed(true); layoutChildren(); + // Manage the pressed view based on motion position. This allows us to + // play nicely with actual touch and scroll events. + final View motionView = getChildAt(mMotionPosition - mFirstPosition); + if (motionView != null) { + motionView.setPressed(false); + } + mMotionPosition = position; + child.setPressed(true); + // Ensure that keyboard focus starts from the last touched position. setSelectedPositionInt(position); positionSelectorLikeTouch(position, child, x, y); diff --git a/core/java/android/widget/OverScroller.java b/core/java/android/widget/OverScroller.java index 7b3dd31..a40d4f8 100644 --- a/core/java/android/widget/OverScroller.java +++ b/core/java/android/widget/OverScroller.java @@ -904,6 +904,10 @@ public class OverScroller { final long time = AnimationUtils.currentAnimationTimeMillis(); final long currentTime = time - mStartTime; + if (currentTime == 0) { + // Skip work but report that we're still going if we have a nonzero duration. + return mDuration > 0; + } if (currentTime > mDuration) { return false; } diff --git a/core/java/android/widget/PopupMenu.java b/core/java/android/widget/PopupMenu.java index 111dadc..2708398 100644 --- a/core/java/android/widget/PopupMenu.java +++ b/core/java/android/widget/PopupMenu.java @@ -16,6 +16,7 @@ package android.widget; +import com.android.internal.R; import com.android.internal.view.menu.MenuBuilder; import com.android.internal.view.menu.MenuPopupHelper; import com.android.internal.view.menu.MenuPresenter; @@ -37,10 +38,11 @@ import android.widget.ListPopupWindow.ForwardingListener; * of the popup will dismiss it. */ public class PopupMenu implements MenuBuilder.Callback, MenuPresenter.Callback { - private Context mContext; - private MenuBuilder mMenu; - private View mAnchor; - private MenuPopupHelper mPopup; + private final Context mContext; + private final MenuBuilder mMenu; + private final View mAnchor; + private final MenuPopupHelper mPopup; + private OnMenuItemClickListener mMenuItemClickListener; private OnDismissListener mDismissListener; private OnTouchListener mDragListener; @@ -58,31 +60,56 @@ public class PopupMenu implements MenuBuilder.Callback, MenuPresenter.Callback { } /** - * Construct a new PopupMenu. + * Constructor to create a new popup menu with an anchor view. * - * @param context Context for the PopupMenu. - * @param anchor Anchor view for this popup. The popup will appear below the anchor if there - * is room, or above it if there is not. + * @param context Context the popup menu is running in, through which it + * can access the current theme, resources, etc. + * @param anchor Anchor view for this popup. The popup will appear below + * the anchor if there is room, or above it if there is not. */ public PopupMenu(Context context, View anchor) { this(context, anchor, Gravity.NO_GRAVITY); } /** - * Construct a new PopupMenu. + * Constructor to create a new popup menu with an anchor view and alignment + * gravity. * - * @param context Context for the PopupMenu. - * @param anchor Anchor view for this popup. The popup will appear below the anchor if there - * is room, or above it if there is not. - * @param gravity The {@link Gravity} value for aligning the popup with its anchor + * @param context Context the popup menu is running in, through which it + * can access the current theme, resources, etc. + * @param anchor Anchor view for this popup. The popup will appear below + * the anchor if there is room, or above it if there is not. + * @param gravity The {@link Gravity} value for aligning the popup with its + * anchor. */ public PopupMenu(Context context, View anchor, int gravity) { - // TODO Theme? + this(context, anchor, gravity, R.attr.popupMenuStyle, 0); + } + + /** + * Constructor a create a new popup menu with a specific style. + * + * @param context Context the popup menu is running in, through which it + * can access the current theme, resources, etc. + * @param anchor Anchor view for this popup. The popup will appear below + * the anchor if there is room, or above it if there is not. + * @param gravity The {@link Gravity} value for aligning the popup with its + * anchor. + * @param popupStyleAttr An attribute in the current theme that contains a + * reference to a style resource that supplies default values for + * the popup window. Can be 0 to not look for defaults. + * @param popupStyleRes A resource identifier of a style resource that + * supplies default values for the popup window, used only if + * popupStyleAttr is 0 or can not be found in the theme. Can be 0 + * to not look for defaults. + */ + public PopupMenu(Context context, View anchor, int gravity, int popupStyleAttr, + int popupStyleRes) { mContext = context; mMenu = new MenuBuilder(context); mMenu.setCallback(this); mAnchor = anchor; - mPopup = new MenuPopupHelper(context, mMenu, anchor); + mPopup = new MenuPopupHelper(context, mMenu, anchor, false, popupStyleAttr, popupStyleRes); mPopup.setGravity(gravity); mPopup.setCallback(this); } diff --git a/core/java/android/widget/PopupWindow.java b/core/java/android/widget/PopupWindow.java index 41d3e320..396c0b9 100644 --- a/core/java/android/widget/PopupWindow.java +++ b/core/java/android/widget/PopupWindow.java @@ -97,9 +97,11 @@ public class PopupWindow { private boolean mAllowScrollingAnchorParent = true; private boolean mLayoutInsetDecor = false; private boolean mNotTouchModal; + private boolean mAttachedInDecor = true; + private boolean mAttachedInDecorSet = false; private OnTouchListener mTouchInterceptor; - + private int mWidthMode; private int mWidth; private int mLastWidth; @@ -196,54 +198,17 @@ public class PopupWindow { mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); final TypedArray a = context.obtainStyledAttributes( - attrs, com.android.internal.R.styleable.PopupWindow, defStyleAttr, defStyleRes); - - mBackground = a.getDrawable(R.styleable.PopupWindow_popupBackground); + attrs, R.styleable.PopupWindow, defStyleAttr, defStyleRes); + final Drawable bg = a.getDrawable(R.styleable.PopupWindow_popupBackground); mElevation = a.getDimension(R.styleable.PopupWindow_popupElevation, 0); mOverlapAnchor = a.getBoolean(R.styleable.PopupWindow_overlapAnchor, false); final int animStyle = a.getResourceId(R.styleable.PopupWindow_popupAnimationStyle, -1); - mAnimationStyle = animStyle == com.android.internal.R.style.Animation_PopupWindow ? -1 : - animStyle; + mAnimationStyle = animStyle == R.style.Animation_PopupWindow ? -1 : animStyle; - // If this is a StateListDrawable, try to find and store the drawable to be - // used when the drop-down is placed above its anchor view, and the one to be - // used when the drop-down is placed below its anchor view. We extract - // the drawables ourselves to work around a problem with using refreshDrawableState - // that it will take into account the padding of all drawables specified in a - // StateListDrawable, thus adding superfluous padding to drop-down views. - // - // We assume a StateListDrawable will have a drawable for ABOVE_ANCHOR_STATE_SET and - // at least one other drawable, intended for the 'below-anchor state'. - if (mBackground instanceof StateListDrawable) { - StateListDrawable background = (StateListDrawable) mBackground; - - // Find the above-anchor view - this one's easy, it should be labeled as such. - int aboveAnchorStateIndex = background.getStateDrawableIndex(ABOVE_ANCHOR_STATE_SET); - - // Now, for the below-anchor view, look for any other drawable specified in the - // StateListDrawable which is not for the above-anchor state and use that. - int count = background.getStateCount(); - int belowAnchorStateIndex = -1; - for (int i = 0; i < count; i++) { - if (i != aboveAnchorStateIndex) { - belowAnchorStateIndex = i; - break; - } - } - - // Store the drawables we found, if we found them. Otherwise, set them both - // to null so that we'll just use refreshDrawableState. - if (aboveAnchorStateIndex != -1 && belowAnchorStateIndex != -1) { - mAboveAnchorBackgroundDrawable = background.getStateDrawable(aboveAnchorStateIndex); - mBelowAnchorBackgroundDrawable = background.getStateDrawable(belowAnchorStateIndex); - } else { - mBelowAnchorBackgroundDrawable = null; - mAboveAnchorBackgroundDrawable = null; - } - } - a.recycle(); + + setBackgroundDrawable(bg); } /** @@ -316,6 +281,7 @@ public class PopupWindow { mContext = contentView.getContext(); mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); } + setContentView(contentView); setWidth(width); setHeight(height); @@ -343,6 +309,43 @@ public class PopupWindow { */ public void setBackgroundDrawable(Drawable background) { mBackground = background; + + // If this is a StateListDrawable, try to find and store the drawable to be + // used when the drop-down is placed above its anchor view, and the one to be + // used when the drop-down is placed below its anchor view. We extract + // the drawables ourselves to work around a problem with using refreshDrawableState + // that it will take into account the padding of all drawables specified in a + // StateListDrawable, thus adding superfluous padding to drop-down views. + // + // We assume a StateListDrawable will have a drawable for ABOVE_ANCHOR_STATE_SET and + // at least one other drawable, intended for the 'below-anchor state'. + if (mBackground instanceof StateListDrawable) { + StateListDrawable stateList = (StateListDrawable) mBackground; + + // Find the above-anchor view - this one's easy, it should be labeled as such. + int aboveAnchorStateIndex = stateList.getStateDrawableIndex(ABOVE_ANCHOR_STATE_SET); + + // Now, for the below-anchor view, look for any other drawable specified in the + // StateListDrawable which is not for the above-anchor state and use that. + int count = stateList.getStateCount(); + int belowAnchorStateIndex = -1; + for (int i = 0; i < count; i++) { + if (i != aboveAnchorStateIndex) { + belowAnchorStateIndex = i; + break; + } + } + + // Store the drawables we found, if we found them. Otherwise, set them both + // to null so that we'll just use refreshDrawableState. + if (aboveAnchorStateIndex != -1 && belowAnchorStateIndex != -1) { + mAboveAnchorBackgroundDrawable = stateList.getStateDrawable(aboveAnchorStateIndex); + mBelowAnchorBackgroundDrawable = stateList.getStateDrawable(belowAnchorStateIndex); + } else { + mBelowAnchorBackgroundDrawable = null; + mAboveAnchorBackgroundDrawable = null; + } + } } /** @@ -373,16 +376,16 @@ public class PopupWindow { public int getAnimationStyle() { return mAnimationStyle; } - + /** - * Set the flag on popup to ignore cheek press eventt; by default this flag + * Set the flag on popup to ignore cheek press event; by default this flag * is set to false * which means the pop wont ignore cheek press dispatch events. - * + * * <p>If the popup is showing, calling this method will take effect only * the next time the popup is shown or through a manual call to one of * the {@link #update()} methods.</p> - * + * * @see #update() */ public void setIgnoreCheekPress() { @@ -443,6 +446,19 @@ public class PopupWindow { if (mWindowManager == null && mContentView != null) { mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); } + + // Setting the default for attachedInDecor based on SDK version here + // instead of in the constructor since we might not have the context + // object in the constructor. We only want to set default here if the + // app hasn't already set the attachedInDecor. + if (mContext != null && !mAttachedInDecorSet) { + // Attach popup window in decor frame of parent window by default for + // {@link Build.VERSION_CODES.LOLLIPOP_MR1} or greater. Keep current + // behavior of not attaching to decor frame for older SDKs. + setAttachedInDecor(mContext.getApplicationInfo().targetSdkVersion + >= Build.VERSION_CODES.LOLLIPOP_MR1); + } + } /** @@ -452,7 +468,7 @@ public class PopupWindow { public void setTouchInterceptor(OnTouchListener l) { mTouchInterceptor = l; } - + /** * <p>Indicate whether the popup window can grab the focus.</p> * @@ -702,6 +718,36 @@ public class PopupWindow { } /** + * <p>Indicates whether the popup window will be attached in the decor frame of its parent + * window. + * + * @return true if the window will be attached to the decor frame of its parent window. + * + * @see #setAttachedInDecor(boolean) + * @see WindowManager.LayoutParams#FLAG_LAYOUT_ATTACHED_IN_DECOR + */ + public boolean isAttachedInDecor() { + return mAttachedInDecor; + } + + /** + * <p>This will attach the popup window to the decor frame of the parent window to avoid + * overlaping with screen decorations like the navigation bar. Overrides the default behavior of + * the flag {@link WindowManager.LayoutParams#FLAG_LAYOUT_ATTACHED_IN_DECOR}. + * + * <p>By default the flag is set on SDK version {@link Build.VERSION_CODES#LOLLIPOP_MR1} or + * greater and cleared on lesser SDK versions. + * + * @param enabled true if the popup should be attached to the decor frame of its parent window. + * + * @see WindowManager.LayoutParams#FLAG_LAYOUT_ATTACHED_IN_DECOR + */ + public void setAttachedInDecor(boolean enabled) { + mAttachedInDecor = enabled; + mAttachedInDecorSet = true; + } + + /** * Allows the popup window to force the flag * {@link WindowManager.LayoutParams#FLAG_LAYOUT_INSET_DECOR}, overriding default behavior. * This will cause the popup to inset its content to account for system windows overlaying @@ -1140,9 +1186,12 @@ public class PopupWindow { if (mNotTouchModal) { curFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; } + if (mAttachedInDecor) { + curFlags |= WindowManager.LayoutParams.FLAG_LAYOUT_ATTACHED_IN_DECOR; + } return curFlags; } - + private int computeAnimationResource() { if (mAnimationStyle == -1) { if (mIsDropdown) { diff --git a/core/java/android/widget/ProgressBar.java b/core/java/android/widget/ProgressBar.java index e9298c2..887a93b 100644 --- a/core/java/android/widget/ProgressBar.java +++ b/core/java/android/widget/ProgressBar.java @@ -604,6 +604,7 @@ public class ProgressBar extends View { * @see #getIndeterminateTintList() * @see Drawable#setTintList(ColorStateList) */ + @RemotableViewMethod public void setIndeterminateTintList(@Nullable ColorStateList tint) { if (mProgressTintInfo == null) { mProgressTintInfo = new ProgressTintInfo(); @@ -672,6 +673,12 @@ public class ProgressBar extends View { if (tintInfo.mHasIndeterminateTintMode) { mIndeterminateDrawable.setTintMode(tintInfo.mIndeterminateTintMode); } + + // The drawable (or one of its children) may not have been + // stateful before applying the tint, so let's try again. + if (mIndeterminateDrawable.isStateful()) { + mIndeterminateDrawable.setState(getDrawableState()); + } } } } @@ -780,6 +787,12 @@ public class ProgressBar extends View { if (mProgressTintInfo.mHasProgressTintMode) { target.setTintMode(mProgressTintInfo.mProgressTintMode); } + + // The drawable (or one of its children) may not have been + // stateful before applying the tint, so let's try again. + if (target.isStateful()) { + target.setState(getDrawableState()); + } } } } @@ -799,6 +812,12 @@ public class ProgressBar extends View { if (mProgressTintInfo.mHasProgressBackgroundTintMode) { target.setTintMode(mProgressTintInfo.mProgressBackgroundTintMode); } + + // The drawable (or one of its children) may not have been + // stateful before applying the tint, so let's try again. + if (target.isStateful()) { + target.setState(getDrawableState()); + } } } } @@ -818,6 +837,12 @@ public class ProgressBar extends View { if (mProgressTintInfo.mHasSecondaryProgressTintMode) { target.setTintMode(mProgressTintInfo.mSecondaryProgressTintMode); } + + // The drawable (or one of its children) may not have been + // stateful before applying the tint, so let's try again. + if (target.isStateful()) { + target.setState(getDrawableState()); + } } } } @@ -842,6 +867,7 @@ public class ProgressBar extends View { * @see #getProgressTintList() * @see Drawable#setTintList(ColorStateList) */ + @RemotableViewMethod public void setProgressTintList(@Nullable ColorStateList tint) { if (mProgressTintInfo == null) { mProgressTintInfo = new ProgressTintInfo(); @@ -923,6 +949,7 @@ public class ProgressBar extends View { * @see #getProgressBackgroundTintList() * @see Drawable#setTintList(ColorStateList) */ + @RemotableViewMethod public void setProgressBackgroundTintList(@Nullable ColorStateList tint) { if (mProgressTintInfo == null) { mProgressTintInfo = new ProgressTintInfo(); diff --git a/core/java/android/widget/RadialTimePickerView.java b/core/java/android/widget/RadialTimePickerView.java index 56f126c..24fc2bb 100644 --- a/core/java/android/widget/RadialTimePickerView.java +++ b/core/java/android/widget/RadialTimePickerView.java @@ -22,21 +22,19 @@ import android.animation.Keyframe; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; -import android.annotation.SuppressLint; import android.content.Context; -import android.content.res.ColorStateList; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; +import android.graphics.Rect; import android.graphics.Typeface; -import android.graphics.RectF; import android.os.Bundle; -import android.text.format.DateUtils; -import android.text.format.Time; import android.util.AttributeSet; +import android.util.IntArray; import android.util.Log; +import android.util.MathUtils; import android.util.TypedValue; import android.view.HapticFeedbackConstants; import android.view.MotionEvent; @@ -44,10 +42,11 @@ import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import com.android.internal.R; +import com.android.internal.widget.ExploreByTouchHelper; -import java.text.DateFormatSymbols; import java.util.ArrayList; import java.util.Calendar; import java.util.Locale; @@ -69,7 +68,6 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { private static final int HOURS = 0; private static final int MINUTES = 1; private static final int HOURS_INNER = 2; - private static final int AMPM = 3; private static final int SELECTOR_CIRCLE = 0; private static final int SELECTOR_DOT = 1; @@ -87,12 +85,6 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { // Alpha level of color for selector. private static final int ALPHA_SELECTOR = 60; // was 51 - // Alpha level of color for selected circle. - private static final int ALPHA_AMPM_SELECTED = ALPHA_SELECTOR; - - // Alpha level of color for pressed circle. - private static final int ALPHA_AMPM_PRESSED = 255; // was 175 - private static final float COSINE_30_DEGREES = ((float) Math.sqrt(3)) * 0.5f; private static final float SINE_30_DEGREES = 0.5f; @@ -105,17 +97,16 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { private static final int CENTER_RADIUS = 2; - private static final int[] STATE_SET_SELECTED = new int[] {R.attr.state_selected}; - private static int[] sSnapPrefer30sMap = new int[361]; + private final InvalidateUpdateListener mInvalidateUpdateListener = + new InvalidateUpdateListener(); + private final String[] mHours12Texts = new String[12]; private final String[] mOuterHours24Texts = new String[12]; private final String[] mInnerHours24Texts = new String[12]; private final String[] mMinutesTexts = new String[12]; - private final String[] mAmPmText = new String[2]; - private final Paint[] mPaint = new Paint[2]; private final int[] mColor = new int[2]; private final IntHolder[] mAlpha = new IntHolder[2]; @@ -126,14 +117,42 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { private final int[][] mColorSelector = new int[2][3]; private final IntHolder[][] mAlphaSelector = new IntHolder[2][3]; - private final Paint mPaintAmPmText = new Paint(); - private final Paint[] mPaintAmPmCircle = new Paint[2]; - private final Paint mPaintBackground = new Paint(); - private final Paint mPaintDisabled = new Paint(); private final Paint mPaintDebug = new Paint(); - private Typeface mTypeface; + private final Typeface mTypeface; + + private final float[] mCircleRadius = new float[3]; + + private final float[] mTextSize = new float[2]; + + private final float[][] mTextGridHeights = new float[2][7]; + private final float[][] mTextGridWidths = new float[2][7]; + + private final float[] mInnerTextGridHeights = new float[7]; + private final float[] mInnerTextGridWidths = new float[7]; + + private final float[] mCircleRadiusMultiplier = new float[2]; + private final float[] mNumbersRadiusMultiplier = new float[3]; + + private final float[] mTextSizeMultiplier = new float[3]; + + private final float[] mAnimationRadiusMultiplier = new float[3]; + + private final float mTransitionMidRadiusMultiplier; + private final float mTransitionEndRadiusMultiplier; + + private final int[] mLineLength = new int[3]; + private final int[] mSelectionRadius = new int[3]; + private final float mSelectionRadiusMultiplier; + private final int[] mSelectionDegrees = new int[3]; + + private final ArrayList<Animator> mHoursToMinutesAnims = new ArrayList<Animator>(); + private final ArrayList<Animator> mMinuteToHoursAnims = new ArrayList<Animator>(); + + private final RadialPickerTouchHelper mTouchHelper; + + private float mInnerTextSize; private boolean mIs24HourMode; private boolean mShowHours; @@ -147,66 +166,21 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { private int mXCenter; private int mYCenter; - private float[] mCircleRadius = new float[3]; - private int mMinHypotenuseForInnerNumber; private int mMaxHypotenuseForOuterNumber; private int mHalfwayHypotenusePoint; - private float[] mTextSize = new float[2]; - private float mInnerTextSize; - - private float[][] mTextGridHeights = new float[2][7]; - private float[][] mTextGridWidths = new float[2][7]; - - private float[] mInnerTextGridHeights = new float[7]; - private float[] mInnerTextGridWidths = new float[7]; - private String[] mOuterTextHours; private String[] mInnerTextHours; private String[] mOuterTextMinutes; - - private float[] mCircleRadiusMultiplier = new float[2]; - private float[] mNumbersRadiusMultiplier = new float[3]; - - private float[] mTextSizeMultiplier = new float[3]; - - private float[] mAnimationRadiusMultiplier = new float[3]; - - private float mTransitionMidRadiusMultiplier; - private float mTransitionEndRadiusMultiplier; - private AnimatorSet mTransition; - private InvalidateUpdateListener mInvalidateUpdateListener = new InvalidateUpdateListener(); - - private int[] mLineLength = new int[3]; - private int[] mSelectionRadius = new int[3]; - private float mSelectionRadiusMultiplier; - private int[] mSelectionDegrees = new int[3]; - - private int mAmPmCircleRadius; - private float mAmPmYCenter; - - private float mAmPmCircleRadiusMultiplier; - private int mAmPmTextColor; - - private float mLeftIndicatorXCenter; - private float mRightIndicatorXCenter; - - private int mAmPmUnselectedColor; - private int mAmPmSelectedColor; private int mAmOrPm; - private int mAmOrPmPressed; - private int mDisabledAlpha; - private RectF mRectF = new RectF(); - private boolean mInputEnabled = true; private OnValueSelectedListener mListener; - private final ArrayList<Animator> mHoursToMinutesAnims = new ArrayList<Animator>(); - private final ArrayList<Animator> mMinuteToHoursAnims = new ArrayList<Animator>(); + private boolean mInputEnabled = true; public interface OnValueSelectedListener { void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance); @@ -314,11 +288,21 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { return degrees; } + @SuppressWarnings("unused") + public RadialTimePickerView(Context context) { + this(context, null); + } + public RadialTimePickerView(Context context, AttributeSet attrs) { this(context, attrs, R.attr.timePickerStyle); } - public RadialTimePickerView(Context context, AttributeSet attrs, int defStyle) { + public RadialTimePickerView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public RadialTimePickerView( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs); // Pull disabled alpha from theme. @@ -329,28 +313,7 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { // process style attributes final Resources res = getResources(); final TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.TimePicker, - defStyle, 0); - - ColorStateList amPmBackgroundColor = a.getColorStateList( - R.styleable.TimePicker_amPmBackgroundColor); - if (amPmBackgroundColor == null) { - amPmBackgroundColor = res.getColorStateList( - R.color.timepicker_default_ampm_unselected_background_color_material); - } - - // Obtain the backup selected color. If the background color state - // list doesn't have a state for selected, we'll use this color. - final int amPmSelectedColor = a.getColor(R.styleable.TimePicker_amPmSelectedBackgroundColor, - res.getColor(R.color.timepicker_default_ampm_selected_background_color_material)); - amPmBackgroundColor = ColorStateList.addFirstIfMissing( - amPmBackgroundColor, R.attr.state_selected, amPmSelectedColor); - - mAmPmSelectedColor = amPmBackgroundColor.getColorForState( - STATE_SET_SELECTED, amPmSelectedColor); - mAmPmUnselectedColor = amPmBackgroundColor.getDefaultColor(); - - mAmPmTextColor = a.getColor(R.styleable.TimePicker_amPmTextColor, - res.getColor(R.color.timepicker_default_text_color_material)); + defStyleAttr, defStyleRes); mTypeface = Typeface.create("sans-serif", Typeface.NORMAL); @@ -419,16 +382,6 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { R.styleable.TimePicker_numbersSelectorColor, R.color.timepicker_default_selector_color_material); - mPaintAmPmText.setColor(mAmPmTextColor); - mPaintAmPmText.setTypeface(mTypeface); - mPaintAmPmText.setAntiAlias(true); - mPaintAmPmText.setTextAlign(Paint.Align.CENTER); - - mPaintAmPmCircle[AM] = new Paint(); - mPaintAmPmCircle[AM].setAntiAlias(true); - mPaintAmPmCircle[PM] = new Paint(); - mPaintAmPmCircle[PM].setAntiAlias(true); - mPaintBackground.setColor(a.getColor(R.styleable.TimePicker_numbersBackgroundColor, res.getColor(R.color.timepicker_default_numbers_background_color_material))); mPaintBackground.setAntiAlias(true); @@ -444,7 +397,14 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { mShowHours = true; mIs24HourMode = false; mAmOrPm = AM; - mAmOrPmPressed = -1; + + // Set up accessibility components. + mTouchHelper = new RadialPickerTouchHelper(); + setAccessibilityDelegate(mTouchHelper); + + if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } initHoursAndMinutesText(); initData(); @@ -470,8 +430,8 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { final int currentHour = calendar.get(Calendar.HOUR_OF_DAY); final int currentMinute = calendar.get(Calendar.MINUTE); - setCurrentHour(currentHour); - setCurrentMinute(currentMinute); + setCurrentHourInternal(currentHour, false, false); + setCurrentMinuteInternal(currentMinute, false); setHapticFeedbackEnabled(true); } @@ -493,8 +453,9 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { public void initialize(int hour, int minute, boolean is24HourMode) { mIs24HourMode = is24HourMode; - setCurrentHour(hour); - setCurrentMinute(minute); + + setCurrentHourInternal(hour, false, false); + setCurrentMinuteInternal(minute, false); } public void setCurrentItemShowing(int item, boolean animate) { @@ -524,23 +485,39 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { * @param hour the current hour between 0 and 23 (inclusive) */ public void setCurrentHour(int hour) { + setCurrentHourInternal(hour, true, false); + } + + /** + * Sets the current hour. + * + * @param hour The current hour + * @param callback Whether the value listener should be invoked + * @param autoAdvance Whether the listener should auto-advance to the next + * selection mode, e.g. hour to minutes + */ + private void setCurrentHourInternal(int hour, boolean callback, boolean autoAdvance) { final int degrees = (hour % 12) * DEGREES_FOR_ONE_HOUR; mSelectionDegrees[HOURS] = degrees; mSelectionDegrees[HOURS_INNER] = degrees; // 0 is 12 AM (midnight) and 12 is 12 PM (noon). - mAmOrPm = (hour == 0 || (hour % 24) < 12) ? AM : PM; - - if (mIs24HourMode) { - // Inner circle is 1 through 12. - mIsOnInnerCircle = hour >= 1 && hour <= 12; - } else { - mIsOnInnerCircle = false; + final int amOrPm = (hour == 0 || (hour % 24) < 12) ? AM : PM; + final boolean isOnInnerCircle = mIs24HourMode && hour >= 1 && hour <= 12; + if (mAmOrPm != amOrPm || mIsOnInnerCircle != isOnInnerCircle) { + mAmOrPm = amOrPm; + mIsOnInnerCircle = isOnInnerCircle; + + initData(); + updateLayoutData(); + mTouchHelper.invalidateRoot(); } - initData(); - updateLayoutData(); invalidate(); + + if (callback && mListener != null) { + mListener.onValueSelected(HOURS, hour, autoAdvance); + } } /** @@ -549,15 +526,19 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { * @return the current hour between 0 and 23 (inclusive) */ public int getCurrentHour() { - int hour = (mSelectionDegrees[mIsOnInnerCircle ? - HOURS_INNER : HOURS] / DEGREES_FOR_ONE_HOUR) % 12; + return getHourForDegrees( + mSelectionDegrees[mIsOnInnerCircle ? HOURS_INNER : HOURS], mIsOnInnerCircle); + } + + private int getHourForDegrees(int degrees, boolean innerCircle) { + int hour = (degrees / DEGREES_FOR_ONE_HOUR) % 12; if (mIs24HourMode) { // Convert the 12-hour value into 24-hour time based on where the // selector is positioned. - if (mIsOnInnerCircle && hour == 0) { + if (innerCircle && hour == 0) { // Inner circle is 1 through 12. hour = 12; - } else if (!mIsOnInnerCircle && hour != 0) { + } else if (!innerCircle && hour != 0) { // Outer circle is 13 through 23 and 0. hour += 12; } @@ -567,30 +548,55 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { return hour; } + private int getDegreesForHour(int hour) { + // Convert to be 0-11. + if (mIs24HourMode) { + if (hour >= 12) { + hour -= 12; + } + } else if (hour == 12) { + hour = 0; + } + return hour * DEGREES_FOR_ONE_HOUR; + } + public void setCurrentMinute(int minute) { + setCurrentMinuteInternal(minute, true); + } + + private void setCurrentMinuteInternal(int minute, boolean callback) { mSelectionDegrees[MINUTES] = (minute % 60) * DEGREES_FOR_ONE_MINUTE; + invalidate(); + + if (callback && mListener != null) { + mListener.onValueSelected(MINUTES, minute, false); + } } // Returns minutes in 0-59 range public int getCurrentMinute() { - return (mSelectionDegrees[MINUTES] / DEGREES_FOR_ONE_MINUTE); + return getMinuteForDegrees(mSelectionDegrees[MINUTES]); + } + + private int getMinuteForDegrees(int degrees) { + return degrees / DEGREES_FOR_ONE_MINUTE; + } + + private int getDegreesForMinute(int minute) { + return minute * DEGREES_FOR_ONE_MINUTE; } public void setAmOrPm(int val) { mAmOrPm = (val % 2); invalidate(); + mTouchHelper.invalidateRoot(); } public int getAmOrPm() { return mAmOrPm; } - public void swapAmPm() { - mAmOrPm = (mAmOrPm == AM) ? PM : AM; - invalidate(); - } - public void showHours(boolean animate) { if (mShowHours) return; mShowHours = true; @@ -621,10 +627,6 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { mInnerHours24Texts[i] = String.format("%d", HOURS_NUMBERS[i]); mMinutesTexts[i] = String.format("%02d", MINUTES_NUMBERS[i]); } - - String[] amPmStrings = TimePickerClockDelegate.getAmPmStrings(mContext); - mAmPmText[AM] = amPmStrings[0]; - mAmPmText[PM] = amPmStrings[1]; } private void initData() { @@ -674,9 +676,6 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { mAnimationRadiusMultiplier[HOURS_INNER] = 1; mAnimationRadiusMultiplier[MINUTES] = 1; - mAmPmCircleRadiusMultiplier = Float.parseFloat( - res.getString(R.string.timepicker_ampm_circle_radius_multiplier)); - mAlpha[HOURS].setValue(mShowHours ? ALPHA_OPAQUE : ALPHA_TRANSPARENT); mAlpha[MINUTES].setValue(mShowHours ? ALPHA_TRANSPARENT : ALPHA_OPAQUE); @@ -710,14 +709,6 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { mCircleRadius[HOURS_INNER] = min * mCircleRadiusMultiplier[HOURS]; mCircleRadius[MINUTES] = min * mCircleRadiusMultiplier[MINUTES]; - if (!mIs24HourMode) { - // We'll need to draw the AM/PM circles, so the main circle will need to have - // a slightly higher center. To keep the entire view centered vertically, we'll - // have to push it up by half the radius of the AM/PM circles. - int amPmCircleRadius = (int) (mCircleRadius[HOURS] * mAmPmCircleRadiusMultiplier); - mYCenter -= amPmCircleRadius / 2; - } - mMinHypotenuseForInnerNumber = (int) (mCircleRadius[HOURS] * mNumbersRadiusMultiplier[HOURS_INNER]) - mSelectionRadius[HOURS]; mMaxHypotenuseForOuterNumber = (int) (mCircleRadius[HOURS] @@ -739,16 +730,7 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { mSelectionRadius[HOURS_INNER] = mSelectionRadius[HOURS]; mSelectionRadius[MINUTES] = (int) (mCircleRadius[MINUTES] * mSelectionRadiusMultiplier); - mAmPmCircleRadius = (int) (mCircleRadius[HOURS] * mAmPmCircleRadiusMultiplier); - mPaintAmPmText.setTextSize(mAmPmCircleRadius * 3 / 4); - - // Line up the vertical center of the AM/PM circles with the bottom of the main circle. - mAmPmYCenter = mYCenter + mCircleRadius[HOURS]; - - // Line up the horizontal edges of the AM/PM circles with the horizontal edges - // of the main circle - mLeftIndicatorXCenter = mXCenter - mCircleRadius[HOURS] + mAmPmCircleRadius; - mRightIndicatorXCenter = mXCenter + mCircleRadius[HOURS] - mAmPmCircleRadius; + mTouchHelper.invalidateRoot(); } @Override @@ -780,9 +762,6 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { mColor[MINUTES], mAlpha[MINUTES].getValue()); drawCenter(canvas); - if (!mIs24HourMode) { - drawAmPm(canvas); - } if (DEBUG) { drawDebug(canvas); @@ -804,50 +783,6 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { drawSelector(canvas, MINUTES); } - private void drawAmPm(Canvas canvas) { - final boolean isLayoutRtl = isLayoutRtl(); - - int amColor = mAmPmUnselectedColor; - int amAlpha = ALPHA_OPAQUE; - int pmColor = mAmPmUnselectedColor; - int pmAlpha = ALPHA_OPAQUE; - if (mAmOrPm == AM) { - amColor = mAmPmSelectedColor; - amAlpha = ALPHA_AMPM_SELECTED; - } else if (mAmOrPm == PM) { - pmColor = mAmPmSelectedColor; - pmAlpha = ALPHA_AMPM_SELECTED; - } - if (mAmOrPmPressed == AM) { - amColor = mAmPmSelectedColor; - amAlpha = ALPHA_AMPM_PRESSED; - } else if (mAmOrPmPressed == PM) { - pmColor = mAmPmSelectedColor; - pmAlpha = ALPHA_AMPM_PRESSED; - } - - // Draw the two circles - mPaintAmPmCircle[AM].setColor(amColor); - mPaintAmPmCircle[AM].setAlpha(getMultipliedAlpha(amColor, amAlpha)); - canvas.drawCircle(isLayoutRtl ? mRightIndicatorXCenter : mLeftIndicatorXCenter, - mAmPmYCenter, mAmPmCircleRadius, mPaintAmPmCircle[AM]); - - mPaintAmPmCircle[PM].setColor(pmColor); - mPaintAmPmCircle[PM].setAlpha(getMultipliedAlpha(pmColor, pmAlpha)); - canvas.drawCircle(isLayoutRtl ? mLeftIndicatorXCenter : mRightIndicatorXCenter, - mAmPmYCenter, mAmPmCircleRadius, mPaintAmPmCircle[PM]); - - // Draw the AM/PM texts on top - mPaintAmPmText.setColor(mAmPmTextColor); - float textYCenter = mAmPmYCenter - - (int) (mPaintAmPmText.descent() + mPaintAmPmText.ascent()) / 2; - - canvas.drawText(isLayoutRtl ? mAmPmText[PM] : mAmPmText[AM], mLeftIndicatorXCenter, - textYCenter, mPaintAmPmText); - canvas.drawText(isLayoutRtl ? mAmPmText[AM] : mAmPmText[PM], mRightIndicatorXCenter, - textYCenter, mPaintAmPmText); - } - private int getMultipliedAlpha(int argb, int alpha) { return (int) (Color.alpha(argb) * (alpha / 255.0) + 0.5); } @@ -917,20 +852,17 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { float top = mYCenter - outerRadius; float right = mXCenter + outerRadius; float bottom = mYCenter + outerRadius; - mRectF = new RectF(left, top, right, bottom); - canvas.drawRect(mRectF, mPaintDebug); + canvas.drawRect(left, top, right, bottom, mPaintDebug); // Draw outer rectangle for background left = mXCenter - mCircleRadius[HOURS]; top = mYCenter - mCircleRadius[HOURS]; right = mXCenter + mCircleRadius[HOURS]; bottom = mYCenter + mCircleRadius[HOURS]; - mRectF.set(left, top, right, bottom); - canvas.drawRect(mRectF, mPaintDebug); + canvas.drawRect(left, top, right, bottom, mPaintDebug); // Draw outer view rectangle - mRectF.set(0, 0, getWidth(), getHeight()); - canvas.drawRect(mRectF, mPaintDebug); + canvas.drawRect(0, 0, getWidth(), getHeight(), mPaintDebug); // Draw selected time final String selected = String.format("%02d:%02d", getCurrentHour(), getCurrentMinute()); @@ -950,7 +882,7 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { float x = mXCenter - width / 2; float y = mYCenter + 1.5f * height; - canvas.drawText(selected.toString(), x, y, paint); + canvas.drawText(selected, x, y, paint); } private void calculateGridSizesHours() { @@ -1044,12 +976,14 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { } // Used for animating the hours by changing their radius + @SuppressWarnings("unused") private void setAnimationRadiusMultiplierHours(float animationRadiusMultiplier) { mAnimationRadiusMultiplier[HOURS] = animationRadiusMultiplier; mAnimationRadiusMultiplier[HOURS_INNER] = animationRadiusMultiplier; } // Used for animating the minutes by changing their radius + @SuppressWarnings("unused") private void setAnimationRadiusMultiplierMinutes(float animationRadiusMultiplier) { mAnimationRadiusMultiplier[MINUTES] = animationRadiusMultiplier; } @@ -1242,41 +1176,25 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { } final float opposite = Math.abs(y - mYCenter); - double degrees = Math.toDegrees(Math.asin(opposite / hypotenuse)); + int degrees = (int) (Math.toDegrees(Math.asin(opposite / hypotenuse)) + 0.5); // Now we have to translate to the correct quadrant. - boolean rightSide = (x > mXCenter); - boolean topSide = (y < mYCenter); - if (rightSide && topSide) { - degrees = 90 - degrees; - } else if (rightSide && !topSide) { - degrees = 90 + degrees; - } else if (!rightSide && !topSide) { - degrees = 270 - degrees; - } else if (!rightSide && topSide) { - degrees = 270 + degrees; - } - return (int) degrees; - } - - private int getIsTouchingAmOrPm(float x, float y) { - final boolean isLayoutRtl = isLayoutRtl(); - int squaredYDistance = (int) ((y - mAmPmYCenter) * (y - mAmPmYCenter)); - - int distanceToAmCenter = (int) Math.sqrt( - (x - mLeftIndicatorXCenter) * (x - mLeftIndicatorXCenter) + squaredYDistance); - if (distanceToAmCenter <= mAmPmCircleRadius) { - return (isLayoutRtl ? PM : AM); - } - - int distanceToPmCenter = (int) Math.sqrt( - (x - mRightIndicatorXCenter) * (x - mRightIndicatorXCenter) + squaredYDistance); - if (distanceToPmCenter <= mAmPmCircleRadius) { - return (isLayoutRtl ? AM : PM); + final boolean rightSide = (x > mXCenter); + final boolean topSide = (y < mYCenter); + if (rightSide) { + if (topSide) { + degrees = 90 - degrees; + } else { + degrees = 90 + degrees; + } + } else { + if (topSide) { + degrees = 270 + degrees; + } else { + degrees = 270 - degrees; + } } - - // Neither was close enough. - return -1; + return degrees; } @Override @@ -1295,181 +1213,326 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { switch(event.getAction()) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_MOVE: - mAmOrPmPressed = getIsTouchingAmOrPm(eventX, eventY); - if (mAmOrPmPressed != -1) { - result = true; - } else { - degrees = getDegreesFromXY(eventX, eventY); - if (degrees != -1) { - snapDegrees = (mShowHours ? - snapOnly30s(degrees, 0) : snapPrefer30s(degrees)) % 360; + degrees = getDegreesFromXY(eventX, eventY); + if (degrees != -1) { + snapDegrees = (mShowHours ? + snapOnly30s(degrees, 0) : snapPrefer30s(degrees)) % 360; + if (mShowHours) { + mSelectionDegrees[HOURS] = snapDegrees; + mSelectionDegrees[HOURS_INNER] = snapDegrees; + } else { + mSelectionDegrees[MINUTES] = snapDegrees; + } + performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK); + if (mListener != null) { if (mShowHours) { - mSelectionDegrees[HOURS] = snapDegrees; - mSelectionDegrees[HOURS_INNER] = snapDegrees; - } else { - mSelectionDegrees[MINUTES] = snapDegrees; - } - performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK); - if (mListener != null) { - if (mShowHours) { - mListener.onValueSelected(HOURS, getCurrentHour(), false); - } else { - mListener.onValueSelected(MINUTES, getCurrentMinute(), false); - } + mListener.onValueSelected(HOURS, getCurrentHour(), false); + } else { + mListener.onValueSelected(MINUTES, getCurrentMinute(), false); } - result = true; } + result = true; + invalidate(); } - invalidate(); - return result; + break; case MotionEvent.ACTION_UP: - mAmOrPmPressed = getIsTouchingAmOrPm(eventX, eventY); - if (mAmOrPmPressed != -1) { - if (mAmOrPm != mAmOrPmPressed) { - swapAmPm(); + degrees = getDegreesFromXY(eventX, eventY); + if (degrees != -1) { + snapDegrees = (mShowHours ? + snapOnly30s(degrees, 0) : snapPrefer30s(degrees)) % 360; + if (mShowHours) { + mSelectionDegrees[HOURS] = snapDegrees; + mSelectionDegrees[HOURS_INNER] = snapDegrees; + } else { + mSelectionDegrees[MINUTES] = snapDegrees; } - mAmOrPmPressed = -1; if (mListener != null) { - mListener.onValueSelected(AMPM, getCurrentHour(), true); - } - result = true; - } else { - degrees = getDegreesFromXY(eventX, eventY); - if (degrees != -1) { - snapDegrees = (mShowHours ? - snapOnly30s(degrees, 0) : snapPrefer30s(degrees)) % 360; if (mShowHours) { - mSelectionDegrees[HOURS] = snapDegrees; - mSelectionDegrees[HOURS_INNER] = snapDegrees; - } else { - mSelectionDegrees[MINUTES] = snapDegrees; + mListener.onValueSelected(HOURS, getCurrentHour(), true); + } else { + mListener.onValueSelected(MINUTES, getCurrentMinute(), true); } - if (mListener != null) { - if (mShowHours) { - mListener.onValueSelected(HOURS, getCurrentHour(), true); - } else { - mListener.onValueSelected(MINUTES, getCurrentMinute(), true); - } - } - result = true; } - } - if (result) { invalidate(); + result = true; } - return result; - - default: break; } - return false; + return result; } - /** - * Necessary for accessibility, to ensure we support "scrolling" forward and backward - * in the circle. - */ - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); - info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); - } - - /** - * Announce the currently-selected time when launched. - */ @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { - // Clear the event's current text so that only the current time will be spoken. - event.getText().clear(); - Time time = new Time(); - time.hour = getCurrentHour(); - time.minute = getCurrentMinute(); - long millis = time.normalize(true); - int flags = DateUtils.FORMAT_SHOW_TIME; - if (mIs24HourMode) { - flags |= DateUtils.FORMAT_24HOUR; - } - String timeString = DateUtils.formatDateTime(getContext(), millis, flags); - event.getText().add(timeString); + public boolean dispatchHoverEvent(MotionEvent event) { + // First right-of-refusal goes the touch exploration helper. + if (mTouchHelper.dispatchHoverEvent(event)) { return true; } - return super.dispatchPopulateAccessibilityEvent(event); + return super.dispatchHoverEvent(event); } - /** - * When scroll forward/backward events are received, jump the time to the higher/lower - * discrete, visible value on the circle. - */ - @SuppressLint("NewApi") - @Override - public boolean performAccessibilityAction(int action, Bundle arguments) { - if (super.performAccessibilityAction(action, arguments)) { - return true; + public void setInputEnabled(boolean inputEnabled) { + mInputEnabled = inputEnabled; + invalidate(); + } + + private class RadialPickerTouchHelper extends ExploreByTouchHelper { + private final Rect mTempRect = new Rect(); + + private final int TYPE_HOUR = 1; + private final int TYPE_MINUTE = 2; + + private final int SHIFT_TYPE = 0; + private final int MASK_TYPE = 0xF; + + private final int SHIFT_VALUE = 8; + private final int MASK_VALUE = 0xFF; + + /** Increment in which virtual views are exposed for minutes. */ + private final int MINUTE_INCREMENT = 5; + + public RadialPickerTouchHelper() { + super(RadialTimePickerView.this); } - int changeMultiplier = 0; - if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) { - changeMultiplier = 1; - } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) { - changeMultiplier = -1; + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD); + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD); } - if (changeMultiplier != 0) { - int value = 0; - int stepSize = 0; - if (mShowHours) { - stepSize = DEGREES_FOR_ONE_HOUR; - value = getCurrentHour() % 12; - } else { - stepSize = DEGREES_FOR_ONE_MINUTE; - value = getCurrentMinute(); + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle arguments) { + if (super.performAccessibilityAction(host, action, arguments)) { + return true; + } + + switch (action) { + case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: + adjustPicker(1); + return true; + case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: + adjustPicker(-1); + return true; } - int degrees = value * stepSize; - degrees = snapOnly30s(degrees, changeMultiplier); - value = degrees / stepSize; - int maxValue = 0; - int minValue = 0; + return false; + } + + private void adjustPicker(int step) { + final int stepSize; + final int initialValue; + final int maxValue; + final int minValue; if (mShowHours) { + stepSize = DEGREES_FOR_ONE_HOUR; + initialValue = getCurrentHour() % 12; + if (mIs24HourMode) { maxValue = 23; + minValue = 0; } else { maxValue = 12; minValue = 1; } } else { + stepSize = DEGREES_FOR_ONE_MINUTE; + initialValue = getCurrentMinute(); + maxValue = 55; + minValue = 0; } - if (value > maxValue) { - // If we scrolled forward past the highest number, wrap around to the lowest. - value = minValue; - } else if (value < minValue) { - // If we scrolled backward past the lowest number, wrap around to the highest. - value = maxValue; + + final int steppedValue = snapOnly30s(initialValue * stepSize, step) / stepSize; + final int clampedValue = MathUtils.constrain(steppedValue, minValue, maxValue); + if (mShowHours) { + setCurrentHour(clampedValue); + } else { + setCurrentMinute(clampedValue); } + } + + @Override + protected int getVirtualViewAt(float x, float y) { + final int id; + final int degrees = getDegreesFromXY(x, y); + if (degrees != -1) { + final int snapDegrees = snapOnly30s(degrees, 0) % 360; + if (mShowHours) { + final int hour = getHourForDegrees(snapDegrees, mIsOnInnerCircle); + id = makeId(TYPE_HOUR, hour); + } else { + final int current = getCurrentMinute(); + final int touched = getMinuteForDegrees(degrees); + final int snapped = getMinuteForDegrees(snapDegrees); + + // If the touched minute is closer to the current minute + // than it is to the snapped minute, return current. + final int minute; + if (Math.abs(current - touched) < Math.abs(snapped - touched)) { + minute = current; + } else { + minute = snapped; + } + id = makeId(TYPE_MINUTE, minute); + } + } else { + id = INVALID_ID; + } + + return id; + } + + @Override + protected void getVisibleVirtualViews(IntArray virtualViewIds) { if (mShowHours) { - setCurrentHour(value); - if (mListener != null) { - mListener.onValueSelected(HOURS, value, false); + final int min = mIs24HourMode ? 0 : 1; + final int max = mIs24HourMode ? 23 : 12; + for (int i = min; i <= max ; i++) { + virtualViewIds.add(makeId(TYPE_HOUR, i)); } } else { - setCurrentMinute(value); - if (mListener != null) { - mListener.onValueSelected(MINUTES, value, false); + final int current = getCurrentMinute(); + for (int i = 0; i < 60; i += MINUTE_INCREMENT) { + virtualViewIds.add(makeId(TYPE_MINUTE, i)); + + // If the current minute falls between two increments, + // insert an extra node for it. + if (current > i && current < i + MINUTE_INCREMENT) { + virtualViewIds.add(makeId(TYPE_MINUTE, current)); + } } } - return true; } - return false; - } + @Override + protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { + event.setClassName(getClass().getName()); - public void setInputEnabled(boolean inputEnabled) { - mInputEnabled = inputEnabled; - invalidate(); + final int type = getTypeFromId(virtualViewId); + final int value = getValueFromId(virtualViewId); + final CharSequence description = getVirtualViewDescription(type, value); + event.setContentDescription(description); + } + + @Override + protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) { + node.setClassName(getClass().getName()); + node.addAction(AccessibilityAction.ACTION_CLICK); + + final int type = getTypeFromId(virtualViewId); + final int value = getValueFromId(virtualViewId); + final CharSequence description = getVirtualViewDescription(type, value); + node.setContentDescription(description); + + getBoundsForVirtualView(virtualViewId, mTempRect); + node.setBoundsInParent(mTempRect); + + final boolean selected = isVirtualViewSelected(type, value); + node.setSelected(selected); + } + + @Override + protected boolean onPerformActionForVirtualView(int virtualViewId, int action, + Bundle arguments) { + if (action == AccessibilityNodeInfo.ACTION_CLICK) { + final int type = getTypeFromId(virtualViewId); + final int value = getValueFromId(virtualViewId); + if (type == TYPE_HOUR) { + final int hour = mIs24HourMode ? value : hour12To24(value, mAmOrPm); + setCurrentHour(hour); + return true; + } else if (type == TYPE_MINUTE) { + setCurrentMinute(value); + return true; + } + } + return false; + } + + private int hour12To24(int hour12, int amOrPm) { + int hour24 = hour12; + if (hour12 == 12) { + if (amOrPm == AM) { + hour24 = 0; + } + } else if (amOrPm == PM) { + hour24 += 12; + } + return hour24; + } + + private void getBoundsForVirtualView(int virtualViewId, Rect bounds) { + final float radius; + final int type = getTypeFromId(virtualViewId); + final int value = getValueFromId(virtualViewId); + final float centerRadius; + final float degrees; + if (type == TYPE_HOUR) { + final boolean innerCircle = mIs24HourMode && value > 0 && value <= 12; + if (innerCircle) { + centerRadius = mCircleRadius[HOURS_INNER] * mNumbersRadiusMultiplier[HOURS_INNER]; + radius = mSelectionRadius[HOURS_INNER]; + } else { + centerRadius = mCircleRadius[HOURS] * mNumbersRadiusMultiplier[HOURS]; + radius = mSelectionRadius[HOURS]; + } + + degrees = getDegreesForHour(value); + } else if (type == TYPE_MINUTE) { + centerRadius = mCircleRadius[MINUTES] * mNumbersRadiusMultiplier[MINUTES]; + degrees = getDegreesForMinute(value); + radius = mSelectionRadius[MINUTES]; + } else { + // This should never happen. + centerRadius = 0; + degrees = 0; + radius = 0; + } + + final double radians = Math.toRadians(degrees); + final float xCenter = mXCenter + centerRadius * (float) Math.sin(radians); + final float yCenter = mYCenter - centerRadius * (float) Math.cos(radians); + + bounds.set((int) (xCenter - radius), (int) (yCenter - radius), + (int) (xCenter + radius), (int) (yCenter + radius)); + } + + private CharSequence getVirtualViewDescription(int type, int value) { + final CharSequence description; + if (type == TYPE_HOUR || type == TYPE_MINUTE) { + description = Integer.toString(value); + } else { + description = null; + } + return description; + } + + private boolean isVirtualViewSelected(int type, int value) { + final boolean selected; + if (type == TYPE_HOUR) { + selected = getCurrentHour() == value; + } else if (type == TYPE_MINUTE) { + selected = getCurrentMinute() == value; + } else { + selected = false; + } + return selected; + } + + private int makeId(int type, int value) { + return type << SHIFT_TYPE | value << SHIFT_VALUE; + } + + private int getTypeFromId(int id) { + return id >>> SHIFT_TYPE & MASK_TYPE; + } + + private int getValueFromId(int id) { + return id >>> SHIFT_VALUE & MASK_VALUE; + } } private static class IntHolder { diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 7cb3c37..dd7fa18 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -27,6 +27,7 @@ import android.content.Intent; import android.content.IntentSender; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Bitmap; @@ -1069,6 +1070,7 @@ public class RemoteViews implements Parcelable, Filter { static final int BITMAP = 12; static final int BUNDLE = 13; static final int INTENT = 14; + static final int COLOR_STATE_LIST = 15; String methodName; int type; @@ -1142,6 +1144,11 @@ public class RemoteViews implements Parcelable, Filter { this.value = Intent.CREATOR.createFromParcel(in); } break; + case COLOR_STATE_LIST: + if (in.readInt() != 0) { + this.value = ColorStateList.CREATOR.createFromParcel(in); + } + break; default: break; } @@ -1212,6 +1219,11 @@ public class RemoteViews implements Parcelable, Filter { ((Intent)this.value).writeToParcel(out, flags); } break; + case COLOR_STATE_LIST: + out.writeInt(this.value != null ? 1 : 0); + if (this.value != null) { + ((ColorStateList)this.value).writeToParcel(out, flags); + } default: break; } @@ -1247,6 +1259,8 @@ public class RemoteViews implements Parcelable, Filter { return Bundle.class; case INTENT: return Intent.class; + case COLOR_STATE_LIST: + return ColorStateList.class; default: return null; } @@ -2207,6 +2221,42 @@ public class RemoteViews implements Parcelable, Filter { } /** + * @hide + * Equivalent to calling {@link android.widget.ProgressBar#setProgressTintList}. + * + * @param viewId The id of the view whose tint should change + * @param tint the tint to apply, may be {@code null} to clear tint + */ + public void setProgressTintList(int viewId, ColorStateList tint) { + addAction(new ReflectionAction(viewId, "setProgressTintList", + ReflectionAction.COLOR_STATE_LIST, tint)); + } + + /** + * @hide + * Equivalent to calling {@link android.widget.ProgressBar#setProgressBackgroundTintList}. + * + * @param viewId The id of the view whose tint should change + * @param tint the tint to apply, may be {@code null} to clear tint + */ + public void setProgressBackgroundTintList(int viewId, ColorStateList tint) { + addAction(new ReflectionAction(viewId, "setProgressBackgroundTintList", + ReflectionAction.COLOR_STATE_LIST, tint)); + } + + /** + * @hide + * Equivalent to calling {@link android.widget.ProgressBar#setIndeterminateTintList}. + * + * @param viewId The id of the view whose tint should change + * @param tint the tint to apply, may be {@code null} to clear tint + */ + public void setProgressIndeterminateTintList(int viewId, ColorStateList tint) { + addAction(new ReflectionAction(viewId, "setIndeterminateTintList", + ReflectionAction.COLOR_STATE_LIST, tint)); + } + + /** * Equivalent to calling {@link android.widget.TextView#setTextColor(int)}. * * @param viewId The id of the view whose text color should change @@ -2478,6 +2528,26 @@ public class RemoteViews implements Parcelable, Filter { } /** + * Equivalent to calling {@link android.view.View#setAccessibilityTraversalBefore(int)}. + * + * @param viewId The id of the view whose before view in accessibility traversal to set. + * @param nextId The id of the next in the accessibility traversal. + **/ + public void setAccessibilityTraversalBefore(int viewId, int nextId) { + setInt(viewId, "setAccessibilityTraversalBefore", nextId); + } + + /** + * Equivalent to calling {@link android.view.View#setAccessibilityTraversalAfter(int)}. + * + * @param viewId The id of the view whose after view in accessibility traversal to set. + * @param nextId The id of the next in the accessibility traversal. + **/ + public void setAccessibilityTraversalAfter(int viewId, int nextId) { + setInt(viewId, "setAccessibilityTraversalAfter", nextId); + } + + /** * Equivalent to calling View.setLabelFor(int). * * @param viewId The id of the view whose property to set. diff --git a/core/java/android/widget/SimpleMonthAdapter.java b/core/java/android/widget/SimpleMonthAdapter.java index 3bad235..ecd2912 100644 --- a/core/java/android/widget/SimpleMonthAdapter.java +++ b/core/java/android/widget/SimpleMonthAdapter.java @@ -20,27 +20,41 @@ import android.content.Context; import android.content.res.ColorStateList; import android.view.View; import android.view.ViewGroup; +import android.widget.SimpleMonthView.OnDayClickListener; import java.util.Calendar; -import java.util.HashMap; /** * An adapter for a list of {@link android.widget.SimpleMonthView} items. */ -class SimpleMonthAdapter extends BaseAdapter implements SimpleMonthView.OnDayClickListener { - private static final String TAG = "SimpleMonthAdapter"; +class SimpleMonthAdapter extends BaseAdapter { + private final Calendar mMinDate = Calendar.getInstance(); + private final Calendar mMaxDate = Calendar.getInstance(); private final Context mContext; - private final DatePickerController mController; - private Calendar mSelectedDay; + private Calendar mSelectedDay; private ColorStateList mCalendarTextColors; + private OnDaySelectedListener mOnDaySelectedListener; + + private int mFirstDayOfWeek; - public SimpleMonthAdapter(Context context, DatePickerController controller) { + public SimpleMonthAdapter(Context context) { mContext = context; - mController = controller; - init(); - setSelectedDay(mController.getSelectedDay()); + mSelectedDay = Calendar.getInstance(); + } + + public void setRange(Calendar min, Calendar max) { + mMinDate.setTimeInMillis(min.getTimeInMillis()); + mMaxDate.setTimeInMillis(max.getTimeInMillis()); + + notifyDataSetInvalidated(); + } + + public void setFirstDayOfWeek(int firstDayOfWeek) { + mFirstDayOfWeek = firstDayOfWeek; + + notifyDataSetInvalidated(); } /** @@ -49,29 +63,29 @@ class SimpleMonthAdapter extends BaseAdapter implements SimpleMonthView.OnDayCli * @param day The day to highlight */ public void setSelectedDay(Calendar day) { - if (mSelectedDay != day) { - mSelectedDay = day; - notifyDataSetChanged(); - } - } + mSelectedDay = day; - void setCalendarTextColor(ColorStateList colors) { - mCalendarTextColors = colors; + notifyDataSetChanged(); } /** - * Set up the gesture detector and selected time + * Sets the listener to call when the user selects a day. + * + * @param listener The listener to call. */ - protected void init() { - mSelectedDay = Calendar.getInstance(); + public void setOnDaySelectedListener(OnDaySelectedListener listener) { + mOnDaySelectedListener = listener; + } + + void setCalendarTextColor(ColorStateList colors) { + mCalendarTextColors = colors; } @Override public int getCount() { - final int diffYear = mController.getMaxYear() - mController.getMinYear(); - final int diffMonth = 1 + mController.getMaxMonth() - mController.getMinMonth() - + 12 * diffYear; - return diffMonth; + final int diffYear = mMaxDate.get(Calendar.YEAR) - mMinDate.get(Calendar.YEAR); + final int diffMonth = mMaxDate.get(Calendar.MONTH) - mMinDate.get(Calendar.MONTH); + return diffMonth + 12 * diffYear + 1; } @Override @@ -92,36 +106,34 @@ class SimpleMonthAdapter extends BaseAdapter implements SimpleMonthView.OnDayCli @SuppressWarnings("unchecked") @Override public View getView(int position, View convertView, ViewGroup parent) { - SimpleMonthView v; - HashMap<String, Integer> drawingParams = null; + final SimpleMonthView v; if (convertView != null) { v = (SimpleMonthView) convertView; - // We store the drawing parameters in the view so it can be recycled - drawingParams = (HashMap<String, Integer>) v.getTag(); } else { v = new SimpleMonthView(mContext); + // Set up the new view - AbsListView.LayoutParams params = new AbsListView.LayoutParams( + final AbsListView.LayoutParams params = new AbsListView.LayoutParams( AbsListView.LayoutParams.MATCH_PARENT, AbsListView.LayoutParams.MATCH_PARENT); v.setLayoutParams(params); v.setClickable(true); - v.setOnDayClickListener(this); + v.setOnDayClickListener(mOnDayClickListener); + if (mCalendarTextColors != null) { v.setTextColor(mCalendarTextColors); } } - if (drawingParams == null) { - drawingParams = new HashMap<String, Integer>(); - } else { - drawingParams.clear(); - } - final int currentMonth = position + mController.getMinMonth(); - final int month = currentMonth % 12; - final int year = currentMonth / 12 + mController.getMinYear(); - int selectedDay = -1; + final int minMonth = mMinDate.get(Calendar.MONTH); + final int minYear = mMinDate.get(Calendar.YEAR); + final int currentMonth = position + minMonth; + final int month = currentMonth % 12; + final int year = currentMonth / 12 + minYear; + final int selectedDay; if (isSelectedDayInMonth(year, month)) { selectedDay = mSelectedDay.get(Calendar.DAY_OF_MONTH); + } else { + selectedDay = -1; } // Invokes requestLayout() to ensure that the recycled view is set with the appropriate @@ -129,20 +141,20 @@ class SimpleMonthAdapter extends BaseAdapter implements SimpleMonthView.OnDayCli v.reuse(); final int enabledDayRangeStart; - if (mController.getMinMonth() == month && mController.getMinYear() == year) { - enabledDayRangeStart = mController.getMinDay(); + if (minMonth == month && minYear == year) { + enabledDayRangeStart = mMinDate.get(Calendar.DAY_OF_MONTH); } else { enabledDayRangeStart = 1; } final int enabledDayRangeEnd; - if (mController.getMaxMonth() == month && mController.getMaxYear() == year) { - enabledDayRangeEnd = mController.getMaxDay(); + if (mMaxDate.get(Calendar.MONTH) == month && mMaxDate.get(Calendar.YEAR) == year) { + enabledDayRangeEnd = mMaxDate.get(Calendar.DAY_OF_MONTH); } else { enabledDayRangeEnd = 31; } - v.setMonthParams(selectedDay, month, year, mController.getFirstDayOfWeek(), + v.setMonthParams(selectedDay, month, year, mFirstDayOfWeek, enabledDayRangeStart, enabledDayRangeEnd); v.invalidate(); @@ -153,22 +165,24 @@ class SimpleMonthAdapter extends BaseAdapter implements SimpleMonthView.OnDayCli return mSelectedDay.get(Calendar.YEAR) == year && mSelectedDay.get(Calendar.MONTH) == month; } - @Override - public void onDayClick(SimpleMonthView view, Calendar day) { - if (day != null) { - onDayTapped(day); - } + private boolean isCalendarInRange(Calendar value) { + return value.compareTo(mMinDate) >= 0 && value.compareTo(mMaxDate) <= 0; } - /** - * Maintains the same hour/min/sec but moves the day to the tapped day. - * - * @param day The day that was tapped - */ - protected void onDayTapped(Calendar day) { - mController.tryVibrate(); - mController.onDayOfMonthSelected(day.get(Calendar.YEAR), day.get(Calendar.MONTH), - day.get(Calendar.DAY_OF_MONTH)); - setSelectedDay(day); + private final OnDayClickListener mOnDayClickListener = new OnDayClickListener() { + @Override + public void onDayClick(SimpleMonthView view, Calendar day) { + if (day != null && isCalendarInRange(day)) { + setSelectedDay(day); + + if (mOnDaySelectedListener != null) { + mOnDaySelectedListener.onDaySelected(SimpleMonthAdapter.this, day); + } + } + } + }; + + public interface OnDaySelectedListener { + public void onDaySelected(SimpleMonthAdapter view, Calendar day); } } diff --git a/core/java/android/widget/SimpleMonthView.java b/core/java/android/widget/SimpleMonthView.java index a76241e..d2a37ac 100644 --- a/core/java/android/widget/SimpleMonthView.java +++ b/core/java/android/widget/SimpleMonthView.java @@ -31,6 +31,7 @@ import android.text.format.DateFormat; import android.text.format.DateUtils; import android.text.format.Time; import android.util.AttributeSet; +import android.util.IntArray; import android.util.MathUtils; import android.view.MotionEvent; import android.view.View; @@ -51,8 +52,6 @@ import java.util.Locale; * within the specified month. */ class SimpleMonthView extends View { - private static final String TAG = "SimpleMonthView"; - private static final int DEFAULT_HEIGHT = 32; private static final int MIN_HEIGHT = 10; @@ -66,15 +65,15 @@ class SimpleMonthView extends View { private static final int DAY_SEPARATOR_WIDTH = 1; + private final Formatter mFormatter; + private final StringBuilder mStringBuilder; + private final int mMiniDayNumberTextSize; private final int mMonthLabelTextSize; private final int mMonthDayLabelTextSize; private final int mMonthHeaderSize; private final int mDaySelectedCircleSize; - // used for scaling to the device density - private static float mScale = 0; - /** Single-letter (when available) formatter for the day of week label. */ private SimpleDateFormat mDayFormatter = new SimpleDateFormat("EEEEE", Locale.getDefault()); @@ -91,9 +90,6 @@ class SimpleMonthView extends View { private Paint mMonthTitlePaint; private Paint mMonthDayLabelPaint; - private final Formatter mFormatter; - private final StringBuilder mStringBuilder; - private int mMonth; private int mYear; @@ -154,11 +150,14 @@ class SimpleMonthView extends View { this(context, attrs, R.attr.datePickerStyle); } - public SimpleMonthView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs); + public SimpleMonthView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } - final Resources res = context.getResources(); + public SimpleMonthView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + final Resources res = context.getResources(); mDayOfWeekTypeface = res.getString(R.string.day_of_week_label_typeface); mMonthTitleTypeface = res.getString(R.string.sans_serif); @@ -610,7 +609,7 @@ class SimpleMonthView extends View { } @Override - protected void getVisibleVirtualViews(List<Integer> virtualViewIds) { + protected void getVisibleVirtualViews(IntArray virtualViewIds) { for (int day = 1; day <= mNumCells; day++) { virtualViewIds.add(day); } diff --git a/core/java/android/widget/Switch.java b/core/java/android/widget/Switch.java index a898084..4c8aa51 100644 --- a/core/java/android/widget/Switch.java +++ b/core/java/android/widget/Switch.java @@ -39,6 +39,7 @@ import android.util.FloatProperty; import android.util.MathUtils; import android.view.Gravity; import android.view.MotionEvent; +import android.view.SoundEffectConstants; import android.view.VelocityTracker; import android.view.ViewConfiguration; import android.view.accessibility.AccessibilityEvent; @@ -797,6 +798,7 @@ public class Switch extends CompoundButton { // Commit the change if the event is up and not canceled and the switch // has not been disabled during the drag. final boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled(); + final boolean oldState = isChecked(); final boolean newState; if (commitChange) { mVelocityTracker.computeCurrentVelocity(1000); @@ -807,10 +809,14 @@ public class Switch extends CompoundButton { newState = getTargetCheckedState(); } } else { - newState = isChecked(); + newState = oldState; + } + + if (newState != oldState) { + playSoundEffect(SoundEffectConstants.CLICK); + setChecked(newState); } - setChecked(newState); cancelSuperTouch(ev); } diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 5cdee53..0917b32 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -8518,6 +8518,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } return false; case AccessibilityNodeInfo.ACTION_SET_SELECTION: { if (isFocused() && canSelectText()) { + ensureIterableTextForAccessibilitySelectable(); CharSequence text = getIterableTextForAccessibility(); if (text == null) { return false; @@ -8543,6 +8544,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } } return false; + case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY: + case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: { + ensureIterableTextForAccessibilitySelectable(); + return super.performAccessibilityAction(action, arguments); + } default: { return super.performAccessibilityAction(action, arguments); } @@ -9032,10 +9038,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ @Override public CharSequence getIterableTextForAccessibility() { + return mText; + } + + private void ensureIterableTextForAccessibilitySelectable() { if (!(mText instanceof Spannable)) { setText(mText, BufferType.SPANNABLE); } - return mText; } /** diff --git a/core/java/android/widget/TimePicker.java b/core/java/android/widget/TimePicker.java index 85cf67b..26e02f8 100644 --- a/core/java/android/widget/TimePicker.java +++ b/core/java/android/widget/TimePicker.java @@ -86,12 +86,12 @@ public class TimePicker extends FrameLayout { switch (mode) { case MODE_CLOCK: - mDelegate = new TimePickerSpinnerDelegate( + mDelegate = new TimePickerClockDelegate( this, context, attrs, defStyleAttr, defStyleRes); break; case MODE_SPINNER: default: - mDelegate = new TimePickerClockDelegate( + mDelegate = new TimePickerSpinnerDelegate( this, context, attrs, defStyleAttr, defStyleRes); break; } diff --git a/core/java/android/widget/TimePickerClockDelegate.java b/core/java/android/widget/TimePickerClockDelegate.java index 8917f39..78ee247 100644 --- a/core/java/android/widget/TimePickerClockDelegate.java +++ b/core/java/android/widget/TimePickerClockDelegate.java @@ -17,365 +17,376 @@ package android.widget; import android.content.Context; +import android.content.res.ColorStateList; import android.content.res.Configuration; +import android.content.res.Resources; import android.content.res.TypedArray; import android.os.Parcel; import android.os.Parcelable; +import android.text.TextUtils; import android.text.format.DateFormat; import android.text.format.DateUtils; import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.view.HapticFeedbackConstants; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputMethodManager; + import com.android.internal.R; -import java.text.DateFormatSymbols; +import java.util.ArrayList; import java.util.Calendar; import java.util.Locale; -import libcore.icu.LocaleData; - -import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_AUTO; -import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES; - /** - * A delegate implementing the basic TimePicker + * A delegate implementing the radial clock-based TimePicker. */ -class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate { +class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate implements + RadialTimePickerView.OnValueSelectedListener { + + private static final String TAG = "TimePickerClockDelegate"; + + // Index used by RadialPickerLayout + private static final int HOUR_INDEX = 0; + private static final int MINUTE_INDEX = 1; + + // NOT a real index for the purpose of what's showing. + private static final int AMPM_INDEX = 2; + + // Also NOT a real index, just used for keyboard mode. + private static final int ENABLE_PICKER_INDEX = 3; + + static final int AM = 0; + static final int PM = 1; + private static final boolean DEFAULT_ENABLED_STATE = true; + private boolean mIsEnabled = DEFAULT_ENABLED_STATE; + private static final int HOURS_IN_HALF_DAY = 12; - // state - private boolean mIs24HourView; - private boolean mIsAm; + private final View mHeaderView; + private final TextView mHourView; + private final TextView mMinuteView; + private final View mAmPmLayout; + private final CheckedTextView mAmLabel; + private final CheckedTextView mPmLabel; + private final RadialTimePickerView mRadialTimePickerView; + private final TextView mSeparatorView; - // ui components - private final NumberPicker mHourSpinner; - private final NumberPicker mMinuteSpinner; - private final NumberPicker mAmPmSpinner; - private final EditText mHourSpinnerInput; - private final EditText mMinuteSpinnerInput; - private final EditText mAmPmSpinnerInput; - private final TextView mDivider; + private final String mAmText; + private final String mPmText; - // Note that the legacy implementation of the TimePicker is - // using a button for toggling between AM/PM while the new - // version uses a NumberPicker spinner. Therefore the code - // accommodates these two cases to be backwards compatible. - private final Button mAmPmButton; + private final float mDisabledAlpha; - private final String[] mAmPmStrings; + private boolean mAllowAutoAdvance; + private int mInitialHourOfDay; + private int mInitialMinute; + private boolean mIs24HourView; + + // For hardware IME input. + private char mPlaceholderText; + private String mDoublePlaceholderText; + private String mDeletedKeyFormat; + private boolean mInKbMode; + private ArrayList<Integer> mTypedTimes = new ArrayList<Integer>(); + private Node mLegalTimesTree; + private int mAmKeyCode; + private int mPmKeyCode; + + // Accessibility strings. + private String mHourPickerDescription; + private String mSelectHours; + private String mMinutePickerDescription; + private String mSelectMinutes; + + // Most recent time announcement values for accessibility. + private CharSequence mLastAnnouncedText; + private boolean mLastAnnouncedIsHour; - private boolean mIsEnabled = DEFAULT_ENABLED_STATE; private Calendar mTempCalendar; - private boolean mHourWithTwoDigit; - private char mHourFormat; public TimePickerClockDelegate(TimePicker delegator, Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(delegator, context); // process style attributes - final TypedArray a = mContext.obtainStyledAttributes( - attrs, R.styleable.TimePicker, defStyleAttr, defStyleRes); - final int layoutResourceId = a.getResourceId( - R.styleable.TimePicker_legacyLayout, R.layout.time_picker_legacy); - a.recycle(); + final TypedArray a = mContext.obtainStyledAttributes(attrs, + R.styleable.TimePicker, defStyleAttr, defStyleRes); + final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + final Resources res = mContext.getResources(); + + mHourPickerDescription = res.getString(R.string.hour_picker_description); + mSelectHours = res.getString(R.string.select_hours); + mMinutePickerDescription = res.getString(R.string.minute_picker_description); + mSelectMinutes = res.getString(R.string.select_minutes); + + String[] amPmStrings = TimePickerSpinnerDelegate.getAmPmStrings(context); + mAmText = amPmStrings[0]; + mPmText = amPmStrings[1]; + + final int layoutResourceId = a.getResourceId(R.styleable.TimePicker_internalLayout, + R.layout.time_picker_holo); + final View mainView = inflater.inflate(layoutResourceId, delegator); + + mHeaderView = mainView.findViewById(R.id.time_header); + mHeaderView.setBackground(a.getDrawable(R.styleable.TimePicker_headerBackground)); + + // Set up hour/minute labels. + mHourView = (TextView) mHeaderView.findViewById(R.id.hours); + mHourView.setOnClickListener(mClickListener); + mSeparatorView = (TextView) mHeaderView.findViewById(R.id.separator); + mMinuteView = (TextView) mHeaderView.findViewById(R.id.minutes); + mMinuteView.setOnClickListener(mClickListener); + + final int headerTimeTextAppearance = a.getResourceId( + R.styleable.TimePicker_headerTimeTextAppearance, 0); + if (headerTimeTextAppearance != 0) { + mHourView.setTextAppearance(context, headerTimeTextAppearance); + mSeparatorView.setTextAppearance(context, headerTimeTextAppearance); + mMinuteView.setTextAppearance(context, headerTimeTextAppearance); + } - final LayoutInflater inflater = LayoutInflater.from(mContext); - inflater.inflate(layoutResourceId, mDelegator, true); - - // hour - mHourSpinner = (NumberPicker) delegator.findViewById(R.id.hour); - mHourSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { - public void onValueChange(NumberPicker spinner, int oldVal, int newVal) { - updateInputState(); - if (!is24HourView()) { - if ((oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) || - (oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1)) { - mIsAm = !mIsAm; - updateAmPmControl(); - } - } - onTimeChanged(); - } - }); - mHourSpinnerInput = (EditText) mHourSpinner.findViewById(R.id.numberpicker_input); - mHourSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); - - // divider (only for the new widget style) - mDivider = (TextView) mDelegator.findViewById(R.id.divider); - if (mDivider != null) { - setDividerText(); - } - - // minute - mMinuteSpinner = (NumberPicker) mDelegator.findViewById(R.id.minute); - mMinuteSpinner.setMinValue(0); - mMinuteSpinner.setMaxValue(59); - mMinuteSpinner.setOnLongPressUpdateInterval(100); - mMinuteSpinner.setFormatter(NumberPicker.getTwoDigitFormatter()); - mMinuteSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { - public void onValueChange(NumberPicker spinner, int oldVal, int newVal) { - updateInputState(); - int minValue = mMinuteSpinner.getMinValue(); - int maxValue = mMinuteSpinner.getMaxValue(); - if (oldVal == maxValue && newVal == minValue) { - int newHour = mHourSpinner.getValue() + 1; - if (!is24HourView() && newHour == HOURS_IN_HALF_DAY) { - mIsAm = !mIsAm; - updateAmPmControl(); - } - mHourSpinner.setValue(newHour); - } else if (oldVal == minValue && newVal == maxValue) { - int newHour = mHourSpinner.getValue() - 1; - if (!is24HourView() && newHour == HOURS_IN_HALF_DAY - 1) { - mIsAm = !mIsAm; - updateAmPmControl(); - } - mHourSpinner.setValue(newHour); - } - onTimeChanged(); - } - }); - mMinuteSpinnerInput = (EditText) mMinuteSpinner.findViewById(R.id.numberpicker_input); - mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); - - // Get the localized am/pm strings and use them in the spinner. - mAmPmStrings = getAmPmStrings(context); - - // am/pm - final View amPmView = mDelegator.findViewById(R.id.amPm); - if (amPmView instanceof Button) { - mAmPmSpinner = null; - mAmPmSpinnerInput = null; - mAmPmButton = (Button) amPmView; - mAmPmButton.setOnClickListener(new View.OnClickListener() { - public void onClick(View button) { - button.requestFocus(); - mIsAm = !mIsAm; - updateAmPmControl(); - onTimeChanged(); - } - }); - } else { - mAmPmButton = null; - mAmPmSpinner = (NumberPicker) amPmView; - mAmPmSpinner.setMinValue(0); - mAmPmSpinner.setMaxValue(1); - mAmPmSpinner.setDisplayedValues(mAmPmStrings); - mAmPmSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { - public void onValueChange(NumberPicker picker, int oldVal, int newVal) { - updateInputState(); - picker.requestFocus(); - mIsAm = !mIsAm; - updateAmPmControl(); - onTimeChanged(); - } - }); - mAmPmSpinnerInput = (EditText) mAmPmSpinner.findViewById(R.id.numberpicker_input); - mAmPmSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE); - } - - if (isAmPmAtStart()) { - // Move the am/pm view to the beginning - ViewGroup amPmParent = (ViewGroup) delegator.findViewById(R.id.timePickerLayout); - amPmParent.removeView(amPmView); - amPmParent.addView(amPmView, 0); - // Swap layout margins if needed. They may be not symmetrical (Old Standard Theme - // for example and not for Holo Theme) - ViewGroup.MarginLayoutParams lp = - (ViewGroup.MarginLayoutParams) amPmView.getLayoutParams(); - final int startMargin = lp.getMarginStart(); - final int endMargin = lp.getMarginEnd(); - if (startMargin != endMargin) { - lp.setMarginStart(endMargin); - lp.setMarginEnd(startMargin); - } + // TODO: This can be removed once we support themed color state lists. + final int headerSelectedTextColor = a.getColor( + R.styleable.TimePicker_headerSelectedTextColor, + res.getColor(R.color.timepicker_default_selector_color_material)); + mHourView.setTextColor(ColorStateList.addFirstIfMissing(mHourView.getTextColors(), + R.attr.state_selected, headerSelectedTextColor)); + mMinuteView.setTextColor(ColorStateList.addFirstIfMissing(mMinuteView.getTextColors(), + R.attr.state_selected, headerSelectedTextColor)); + + // Set up AM/PM labels. + mAmPmLayout = mHeaderView.findViewById(R.id.ampm_layout); + mAmLabel = (CheckedTextView) mAmPmLayout.findViewById(R.id.am_label); + mAmLabel.setText(amPmStrings[0]); + mAmLabel.setOnClickListener(mClickListener); + mPmLabel = (CheckedTextView) mAmPmLayout.findViewById(R.id.pm_label); + mPmLabel.setText(amPmStrings[1]); + mPmLabel.setOnClickListener(mClickListener); + + final int headerAmPmTextAppearance = a.getResourceId( + R.styleable.TimePicker_headerAmPmTextAppearance, 0); + if (headerAmPmTextAppearance != 0) { + mAmLabel.setTextAppearance(context, headerAmPmTextAppearance); + mPmLabel.setTextAppearance(context, headerAmPmTextAppearance); } - getHourFormatData(); + a.recycle(); + + // Pull disabled alpha from theme. + final TypedValue outValue = new TypedValue(); + context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, outValue, true); + mDisabledAlpha = outValue.getFloat(); - // update controls to initial state - updateHourControl(); - updateMinuteControl(); - updateAmPmControl(); + mRadialTimePickerView = (RadialTimePickerView) mainView.findViewById( + R.id.radial_picker); - // set to current time - setCurrentHour(mTempCalendar.get(Calendar.HOUR_OF_DAY)); - setCurrentMinute(mTempCalendar.get(Calendar.MINUTE)); + setupListeners(); - if (!isEnabled()) { - setEnabled(false); - } + mAllowAutoAdvance = true; - // set the content descriptions - setContentDescriptions(); + // Set up for keyboard mode. + mDoublePlaceholderText = res.getString(R.string.time_placeholder); + mDeletedKeyFormat = res.getString(R.string.deleted_key); + mPlaceholderText = mDoublePlaceholderText.charAt(0); + mAmKeyCode = mPmKeyCode = -1; + generateLegalTimesTree(); - // If not explicitly specified this view is important for accessibility. - if (mDelegator.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { - mDelegator.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); - } + // Initialize with current time + final Calendar calendar = Calendar.getInstance(mCurrentLocale); + final int currentHour = calendar.get(Calendar.HOUR_OF_DAY); + final int currentMinute = calendar.get(Calendar.MINUTE); + initialize(currentHour, currentMinute, false /* 12h */, HOUR_INDEX); } - private void getHourFormatData() { - final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, - (mIs24HourView) ? "Hm" : "hm"); - final int lengthPattern = bestDateTimePattern.length(); - mHourWithTwoDigit = false; - char hourFormat = '\0'; - // Check if the returned pattern is single or double 'H', 'h', 'K', 'k'. We also save - // the hour format that we found. - for (int i = 0; i < lengthPattern; i++) { - final char c = bestDateTimePattern.charAt(i); - if (c == 'H' || c == 'h' || c == 'K' || c == 'k') { - mHourFormat = c; - if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) { - mHourWithTwoDigit = true; - } - break; - } - } + private void initialize(int hourOfDay, int minute, boolean is24HourView, int index) { + mInitialHourOfDay = hourOfDay; + mInitialMinute = minute; + mIs24HourView = is24HourView; + mInKbMode = false; + updateUI(index); } - private boolean isAmPmAtStart() { - final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, - "hm" /* skeleton */); + private void setupListeners() { + mHeaderView.setOnKeyListener(mKeyListener); + mHeaderView.setOnFocusChangeListener(mFocusListener); + mHeaderView.setFocusable(true); - return bestDateTimePattern.startsWith("a"); + mRadialTimePickerView.setOnValueSelectedListener(this); } - /** - * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":". - * - * See http://unicode.org/cldr/trac/browser/trunk/common/main - * - * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the - * separator as the character which is just after the hour marker in the returned pattern. - */ - private void setDividerText() { - final String skeleton = (mIs24HourView) ? "Hm" : "hm"; - final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, - skeleton); - final String separatorText; - int hourIndex = bestDateTimePattern.lastIndexOf('H'); - if (hourIndex == -1) { - hourIndex = bestDateTimePattern.lastIndexOf('h'); + private void updateUI(int index) { + // Update RadialPicker values + updateRadialPicker(index); + // Enable or disable the AM/PM view. + updateHeaderAmPm(); + // Update Hour and Minutes + updateHeaderHour(mInitialHourOfDay, false); + // Update time separator + updateHeaderSeparator(); + // Update Minutes + updateHeaderMinute(mInitialMinute, false); + // Invalidate everything + mDelegator.invalidate(); + } + + private void updateRadialPicker(int index) { + mRadialTimePickerView.initialize(mInitialHourOfDay, mInitialMinute, mIs24HourView); + setCurrentItemShowing(index, false, true); + } + + private int computeMaxWidthOfNumbers(int max) { + TextView tempView = new TextView(mContext); + tempView.setTextAppearance(mContext, R.style.TextAppearance_Material_TimePicker_TimeLabel); + ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + tempView.setLayoutParams(lp); + int maxWidth = 0; + for (int minutes = 0; minutes < max; minutes++) { + final String text = String.format("%02d", minutes); + tempView.setText(text); + tempView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); + maxWidth = Math.max(maxWidth, tempView.getMeasuredWidth()); } - if (hourIndex == -1) { - // Default case - separatorText = ":"; + return maxWidth; + } + + private void updateHeaderAmPm() { + if (mIs24HourView) { + mAmPmLayout.setVisibility(View.GONE); } else { - int minuteIndex = bestDateTimePattern.indexOf('m', hourIndex + 1); - if (minuteIndex == -1) { - separatorText = Character.toString(bestDateTimePattern.charAt(hourIndex + 1)); + final String bestDateTimePattern = DateFormat.getBestDateTimePattern( + mCurrentLocale, "hm"); + boolean amPmOnLeft = bestDateTimePattern.startsWith("a"); + if (TextUtils.getLayoutDirectionFromLocale(mCurrentLocale) == + View.LAYOUT_DIRECTION_RTL) { + amPmOnLeft = !amPmOnLeft; + } + + final ViewGroup.MarginLayoutParams params = + (ViewGroup.MarginLayoutParams) mAmPmLayout.getLayoutParams(); + + if (amPmOnLeft) { + params.leftMargin = 0; + params.rightMargin = computeMaxWidthOfNumbers(12 /* for hours */); } else { - separatorText = bestDateTimePattern.substring(hourIndex + 1, minuteIndex); + params.leftMargin = computeMaxWidthOfNumbers(60 /* for minutes */); + params.rightMargin = 0; } + + mAmPmLayout.setLayoutParams(params); + mAmPmLayout.setVisibility(View.VISIBLE); + + updateAmPmLabelStates(mInitialHourOfDay < 12 ? AM : PM); } - mDivider.setText(separatorText); } + /** + * Set the current hour. + */ @Override public void setCurrentHour(Integer currentHour) { - setCurrentHour(currentHour, true); - } - - private void setCurrentHour(Integer currentHour, boolean notifyTimeChanged) { - // why was Integer used in the first place? - if (currentHour == null || currentHour == getCurrentHour()) { + if (mInitialHourOfDay == currentHour) { return; } - if (!is24HourView()) { - // convert [0,23] ordinal to wall clock display - if (currentHour >= HOURS_IN_HALF_DAY) { - mIsAm = false; - if (currentHour > HOURS_IN_HALF_DAY) { - currentHour = currentHour - HOURS_IN_HALF_DAY; - } - } else { - mIsAm = true; - if (currentHour == 0) { - currentHour = HOURS_IN_HALF_DAY; - } - } - updateAmPmControl(); - } - mHourSpinner.setValue(currentHour); - if (notifyTimeChanged) { - onTimeChanged(); - } + mInitialHourOfDay = currentHour; + updateHeaderHour(currentHour, true); + updateHeaderAmPm(); + mRadialTimePickerView.setCurrentHour(currentHour); + mRadialTimePickerView.setAmOrPm(mInitialHourOfDay < 12 ? AM : PM); + mDelegator.invalidate(); + onTimeChanged(); } + /** + * @return The current hour in the range (0-23). + */ @Override public Integer getCurrentHour() { - int currentHour = mHourSpinner.getValue(); - if (is24HourView()) { + int currentHour = mRadialTimePickerView.getCurrentHour(); + if (mIs24HourView) { return currentHour; - } else if (mIsAm) { - return currentHour % HOURS_IN_HALF_DAY; } else { - return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY; + switch(mRadialTimePickerView.getAmOrPm()) { + case PM: + return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY; + case AM: + default: + return currentHour % HOURS_IN_HALF_DAY; + } } } + /** + * Set the current minute (0-59). + */ @Override public void setCurrentMinute(Integer currentMinute) { - if (currentMinute == getCurrentMinute()) { + if (mInitialMinute == currentMinute) { return; } - mMinuteSpinner.setValue(currentMinute); + mInitialMinute = currentMinute; + updateHeaderMinute(currentMinute, true); + mRadialTimePickerView.setCurrentMinute(currentMinute); + mDelegator.invalidate(); onTimeChanged(); } + /** + * @return The current minute. + */ @Override public Integer getCurrentMinute() { - return mMinuteSpinner.getValue(); + return mRadialTimePickerView.getCurrentMinute(); } + /** + * Set whether in 24 hour or AM/PM mode. + * + * @param is24HourView True = 24 hour mode. False = AM/PM. + */ @Override public void setIs24HourView(Boolean is24HourView) { - if (mIs24HourView == is24HourView) { + if (is24HourView == mIs24HourView) { return; } - // cache the current hour since spinner range changes and BEFORE changing mIs24HourView!! - int currentHour = getCurrentHour(); - // Order is important here. mIs24HourView = is24HourView; - getHourFormatData(); - updateHourControl(); - // set value after spinner range is updated - setCurrentHour(currentHour, false); - updateMinuteControl(); - updateAmPmControl(); + generateLegalTimesTree(); + int hour = mRadialTimePickerView.getCurrentHour(); + mInitialHourOfDay = hour; + updateHeaderHour(hour, false); + updateHeaderAmPm(); + updateRadialPicker(mRadialTimePickerView.getCurrentItemShowing()); + mDelegator.invalidate(); } + /** + * @return true if this is in 24 hour view else false. + */ @Override public boolean is24HourView() { return mIs24HourView; } @Override - public void setOnTimeChangedListener(TimePicker.OnTimeChangedListener onTimeChangedListener) { - mOnTimeChangedListener = onTimeChangedListener; + public void setOnTimeChangedListener(TimePicker.OnTimeChangedListener callback) { + mOnTimeChangedListener = callback; } @Override public void setEnabled(boolean enabled) { - mMinuteSpinner.setEnabled(enabled); - if (mDivider != null) { - mDivider.setEnabled(enabled); - } - mHourSpinner.setEnabled(enabled); - if (mAmPmSpinner != null) { - mAmPmSpinner.setEnabled(enabled); - } else { - mAmPmButton.setEnabled(enabled); - } + mHourView.setEnabled(enabled); + mMinuteView.setEnabled(enabled); + mAmLabel.setEnabled(enabled); + mPmLabel.setEnabled(enabled); + mRadialTimePickerView.setEnabled(enabled); mIsEnabled = enabled; } @@ -386,24 +397,38 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate { @Override public int getBaseline() { - return mHourSpinner.getBaseline(); + // does not support baseline alignment + return -1; } @Override public void onConfigurationChanged(Configuration newConfig) { - setCurrentLocale(newConfig.locale); + updateUI(mRadialTimePickerView.getCurrentItemShowing()); } @Override public Parcelable onSaveInstanceState(Parcelable superState) { - return new SavedState(superState, getCurrentHour(), getCurrentMinute()); + return new SavedState(superState, getCurrentHour(), getCurrentMinute(), + is24HourView(), inKbMode(), getTypedTimes(), getCurrentItemShowing()); } @Override public void onRestoreInstanceState(Parcelable state) { SavedState ss = (SavedState) state; - setCurrentHour(ss.getHour()); - setCurrentMinute(ss.getMinute()); + setInKbMode(ss.inKbMode()); + setTypedTimes(ss.getTypesTimes()); + initialize(ss.getHour(), ss.getMinute(), ss.is24HourMode(), ss.getCurrentItemShowing()); + mRadialTimePickerView.invalidate(); + if (mInKbMode) { + tryStartingKbMode(-1); + mHourView.invalidate(); + } + } + + @Override + public void setCurrentLocale(Locale locale) { + super.setCurrentLocale(locale); + mTempCalendar = Calendar.getInstance(locale); } @Override @@ -422,9 +447,9 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate { } mTempCalendar.set(Calendar.HOUR_OF_DAY, getCurrentHour()); mTempCalendar.set(Calendar.MINUTE, getCurrentMinute()); - String selectedDateUtterance = DateUtils.formatDateTime(mContext, + String selectedDate = DateUtils.formatDateTime(mContext, mTempCalendar.getTimeInMillis(), flags); - event.getText().add(selectedDateUtterance); + event.getText().add(selectedDate); } @Override @@ -437,121 +462,48 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate { info.setClassName(TimePicker.class.getName()); } - private void updateInputState() { - // Make sure that if the user changes the value and the IME is active - // for one of the inputs if this widget, the IME is closed. If the user - // changed the value via the IME and there is a next input the IME will - // be shown, otherwise the user chose another means of changing the - // value and having the IME up makes no sense. - InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); - if (inputMethodManager != null) { - if (inputMethodManager.isActive(mHourSpinnerInput)) { - mHourSpinnerInput.clearFocus(); - inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); - } else if (inputMethodManager.isActive(mMinuteSpinnerInput)) { - mMinuteSpinnerInput.clearFocus(); - inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); - } else if (inputMethodManager.isActive(mAmPmSpinnerInput)) { - mAmPmSpinnerInput.clearFocus(); - inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); - } - } - } - - private void updateAmPmControl() { - if (is24HourView()) { - if (mAmPmSpinner != null) { - mAmPmSpinner.setVisibility(View.GONE); - } else { - mAmPmButton.setVisibility(View.GONE); - } - } else { - int index = mIsAm ? Calendar.AM : Calendar.PM; - if (mAmPmSpinner != null) { - mAmPmSpinner.setValue(index); - mAmPmSpinner.setVisibility(View.VISIBLE); - } else { - mAmPmButton.setText(mAmPmStrings[index]); - mAmPmButton.setVisibility(View.VISIBLE); - } - } - mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); - } - /** - * Sets the current locale. + * Set whether in keyboard mode or not. * - * @param locale The current locale. + * @param inKbMode True means in keyboard mode. */ - @Override - public void setCurrentLocale(Locale locale) { - super.setCurrentLocale(locale); - mTempCalendar = Calendar.getInstance(locale); + private void setInKbMode(boolean inKbMode) { + mInKbMode = inKbMode; } - private void onTimeChanged() { - mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); - if (mOnTimeChangedListener != null) { - mOnTimeChangedListener.onTimeChanged(mDelegator, getCurrentHour(), - getCurrentMinute()); - } + /** + * @return true if in keyboard mode + */ + private boolean inKbMode() { + return mInKbMode; } - private void updateHourControl() { - if (is24HourView()) { - // 'k' means 1-24 hour - if (mHourFormat == 'k') { - mHourSpinner.setMinValue(1); - mHourSpinner.setMaxValue(24); - } else { - mHourSpinner.setMinValue(0); - mHourSpinner.setMaxValue(23); - } - } else { - // 'K' means 0-11 hour - if (mHourFormat == 'K') { - mHourSpinner.setMinValue(0); - mHourSpinner.setMaxValue(11); - } else { - mHourSpinner.setMinValue(1); - mHourSpinner.setMaxValue(12); - } - } - mHourSpinner.setFormatter(mHourWithTwoDigit ? NumberPicker.getTwoDigitFormatter() : null); + private void setTypedTimes(ArrayList<Integer> typeTimes) { + mTypedTimes = typeTimes; } - private void updateMinuteControl() { - if (is24HourView()) { - mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE); - } else { - mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); - } + /** + * @return an array of typed times + */ + private ArrayList<Integer> getTypedTimes() { + return mTypedTimes; } - private void setContentDescriptions() { - // Minute - trySetContentDescription(mMinuteSpinner, R.id.increment, - R.string.time_picker_increment_minute_button); - trySetContentDescription(mMinuteSpinner, R.id.decrement, - R.string.time_picker_decrement_minute_button); - // Hour - trySetContentDescription(mHourSpinner, R.id.increment, - R.string.time_picker_increment_hour_button); - trySetContentDescription(mHourSpinner, R.id.decrement, - R.string.time_picker_decrement_hour_button); - // AM/PM - if (mAmPmSpinner != null) { - trySetContentDescription(mAmPmSpinner, R.id.increment, - R.string.time_picker_increment_set_pm_button); - trySetContentDescription(mAmPmSpinner, R.id.decrement, - R.string.time_picker_decrement_set_am_button); - } + /** + * @return the index of the current item showing + */ + private int getCurrentItemShowing() { + return mRadialTimePickerView.getCurrentItemShowing(); } - private void trySetContentDescription(View root, int viewId, int contDescResId) { - View target = root.findViewById(viewId); - if (target != null) { - target.setContentDescription(mContext.getString(contDescResId)); + /** + * Propagate the time change + */ + private void onTimeChanged() { + mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + if (mOnTimeChangedListener != null) { + mOnTimeChangedListener.onTimeChanged(mDelegator, + getCurrentHour(), getCurrentMinute()); } } @@ -559,19 +511,34 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate { * Used to save / restore state of time picker */ private static class SavedState extends View.BaseSavedState { + private final int mHour; private final int mMinute; - - private SavedState(Parcelable superState, int hour, int minute) { + private final boolean mIs24HourMode; + private final boolean mInKbMode; + private final ArrayList<Integer> mTypedTimes; + private final int mCurrentItemShowing; + + private SavedState(Parcelable superState, int hour, int minute, boolean is24HourMode, + boolean isKbMode, ArrayList<Integer> typedTimes, + int currentItemShowing) { super(superState); mHour = hour; mMinute = minute; + mIs24HourMode = is24HourMode; + mInKbMode = isKbMode; + mTypedTimes = typedTimes; + mCurrentItemShowing = currentItemShowing; } private SavedState(Parcel in) { super(in); mHour = in.readInt(); mMinute = in.readInt(); + mIs24HourMode = (in.readInt() == 1); + mInKbMode = (in.readInt() == 1); + mTypedTimes = in.readArrayList(getClass().getClassLoader()); + mCurrentItemShowing = in.readInt(); } public int getHour() { @@ -582,11 +549,31 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate { return mMinute; } + public boolean is24HourMode() { + return mIs24HourMode; + } + + public boolean inKbMode() { + return mInKbMode; + } + + public ArrayList<Integer> getTypesTimes() { + return mTypedTimes; + } + + public int getCurrentItemShowing() { + return mCurrentItemShowing; + } + @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeInt(mHour); dest.writeInt(mMinute); + dest.writeInt(mIs24HourMode ? 1 : 0); + dest.writeInt(mInKbMode ? 1 : 0); + dest.writeList(mTypedTimes); + dest.writeInt(mCurrentItemShowing); } @SuppressWarnings({"unused", "hiding"}) @@ -601,11 +588,696 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate { }; } - public static String[] getAmPmStrings(Context context) { - String[] result = new String[2]; - LocaleData d = LocaleData.get(context.getResources().getConfiguration().locale); - result[0] = d.amPm[0].length() > 2 ? d.narrowAm : d.amPm[0]; - result[1] = d.amPm[1].length() > 2 ? d.narrowPm : d.amPm[1]; - return result; + private void tryVibrate() { + mDelegator.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK); + } + + private void updateAmPmLabelStates(int amOrPm) { + final boolean isAm = amOrPm == AM; + mAmLabel.setChecked(isAm); + mAmLabel.setAlpha(isAm ? 1 : mDisabledAlpha); + + final boolean isPm = amOrPm == PM; + mPmLabel.setChecked(isPm); + mPmLabel.setAlpha(isPm ? 1 : mDisabledAlpha); + } + + /** + * Called by the picker for updating the header display. + */ + @Override + public void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance) { + if (pickerIndex == HOUR_INDEX) { + if (mAllowAutoAdvance && autoAdvance) { + updateHeaderHour(newValue, false); + setCurrentItemShowing(MINUTE_INDEX, true, false); + mDelegator.announceForAccessibility(newValue + ". " + mSelectMinutes); + } else { + updateHeaderHour(newValue, true); + } + } else if (pickerIndex == MINUTE_INDEX){ + updateHeaderMinute(newValue, true); + } else if (pickerIndex == AMPM_INDEX) { + updateAmPmLabelStates(newValue); + } else if (pickerIndex == ENABLE_PICKER_INDEX) { + if (!isTypedTimeFullyLegal()) { + mTypedTimes.clear(); + } + finishKbMode(); + } + } + + private void updateHeaderHour(int value, boolean announce) { + final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, + (mIs24HourView) ? "Hm" : "hm"); + final int lengthPattern = bestDateTimePattern.length(); + boolean hourWithTwoDigit = false; + char hourFormat = '\0'; + // Check if the returned pattern is single or double 'H', 'h', 'K', 'k'. We also save + // the hour format that we found. + for (int i = 0; i < lengthPattern; i++) { + final char c = bestDateTimePattern.charAt(i); + if (c == 'H' || c == 'h' || c == 'K' || c == 'k') { + hourFormat = c; + if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) { + hourWithTwoDigit = true; + } + break; + } + } + final String format; + if (hourWithTwoDigit) { + format = "%02d"; + } else { + format = "%d"; + } + if (mIs24HourView) { + // 'k' means 1-24 hour + if (hourFormat == 'k' && value == 0) { + value = 24; + } + } else { + // 'K' means 0-11 hour + value = modulo12(value, hourFormat == 'K'); + } + CharSequence text = String.format(format, value); + mHourView.setText(text); + if (announce) { + tryAnnounceForAccessibility(text, true); + } + } + + private void tryAnnounceForAccessibility(CharSequence text, boolean isHour) { + if (mLastAnnouncedIsHour != isHour || !text.equals(mLastAnnouncedText)) { + // TODO: Find a better solution, potentially live regions? + mDelegator.announceForAccessibility(text); + mLastAnnouncedText = text; + mLastAnnouncedIsHour = isHour; + } + } + + private static int modulo12(int n, boolean startWithZero) { + int value = n % 12; + if (value == 0 && !startWithZero) { + value = 12; + } + return value; + } + + /** + * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":". + * + * See http://unicode.org/cldr/trac/browser/trunk/common/main + * + * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the + * separator as the character which is just after the hour marker in the returned pattern. + */ + private void updateHeaderSeparator() { + final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, + (mIs24HourView) ? "Hm" : "hm"); + final String separatorText; + // See http://www.unicode.org/reports/tr35/tr35-dates.html for hour formats + final char[] hourFormats = {'H', 'h', 'K', 'k'}; + int hIndex = lastIndexOfAny(bestDateTimePattern, hourFormats); + if (hIndex == -1) { + // Default case + separatorText = ":"; + } else { + separatorText = Character.toString(bestDateTimePattern.charAt(hIndex + 1)); + } + mSeparatorView.setText(separatorText); + } + + static private int lastIndexOfAny(String str, char[] any) { + final int lengthAny = any.length; + if (lengthAny > 0) { + for (int i = str.length() - 1; i >= 0; i--) { + char c = str.charAt(i); + for (int j = 0; j < lengthAny; j++) { + if (c == any[j]) { + return i; + } + } + } + } + return -1; + } + + private void updateHeaderMinute(int value, boolean announceForAccessibility) { + if (value == 60) { + value = 0; + } + final CharSequence text = String.format(mCurrentLocale, "%02d", value); + mMinuteView.setText(text); + if (announceForAccessibility) { + tryAnnounceForAccessibility(text, false); + } + } + + /** + * Show either Hours or Minutes. + */ + private void setCurrentItemShowing(int index, boolean animateCircle, boolean announce) { + mRadialTimePickerView.setCurrentItemShowing(index, animateCircle); + + if (index == HOUR_INDEX) { + if (announce) { + mDelegator.announceForAccessibility(mSelectHours); + } + } else { + if (announce) { + mDelegator.announceForAccessibility(mSelectMinutes); + } + } + + mHourView.setSelected(index == HOUR_INDEX); + mMinuteView.setSelected(index == MINUTE_INDEX); + } + + private void setAmOrPm(int amOrPm) { + updateAmPmLabelStates(amOrPm); + mRadialTimePickerView.setAmOrPm(amOrPm); + } + + /** + * For keyboard mode, processes key events. + * + * @param keyCode the pressed key. + * + * @return true if the key was successfully processed, false otherwise. + */ + private boolean processKeyUp(int keyCode) { + if (keyCode == KeyEvent.KEYCODE_DEL) { + if (mInKbMode) { + if (!mTypedTimes.isEmpty()) { + int deleted = deleteLastTypedKey(); + String deletedKeyStr; + if (deleted == getAmOrPmKeyCode(AM)) { + deletedKeyStr = mAmText; + } else if (deleted == getAmOrPmKeyCode(PM)) { + deletedKeyStr = mPmText; + } else { + deletedKeyStr = String.format("%d", getValFromKeyCode(deleted)); + } + mDelegator.announceForAccessibility( + String.format(mDeletedKeyFormat, deletedKeyStr)); + updateDisplay(true); + } + } + } else if (keyCode == KeyEvent.KEYCODE_0 || keyCode == KeyEvent.KEYCODE_1 + || keyCode == KeyEvent.KEYCODE_2 || keyCode == KeyEvent.KEYCODE_3 + || keyCode == KeyEvent.KEYCODE_4 || keyCode == KeyEvent.KEYCODE_5 + || keyCode == KeyEvent.KEYCODE_6 || keyCode == KeyEvent.KEYCODE_7 + || keyCode == KeyEvent.KEYCODE_8 || keyCode == KeyEvent.KEYCODE_9 + || (!mIs24HourView && + (keyCode == getAmOrPmKeyCode(AM) || keyCode == getAmOrPmKeyCode(PM)))) { + if (!mInKbMode) { + if (mRadialTimePickerView == null) { + // Something's wrong, because time picker should definitely not be null. + Log.e(TAG, "Unable to initiate keyboard mode, TimePicker was null."); + return true; + } + mTypedTimes.clear(); + tryStartingKbMode(keyCode); + return true; + } + // We're already in keyboard mode. + if (addKeyIfLegal(keyCode)) { + updateDisplay(false); + } + return true; + } + return false; + } + + /** + * Try to start keyboard mode with the specified key. + * + * @param keyCode The key to use as the first press. Keyboard mode will not be started if the + * key is not legal to start with. Or, pass in -1 to get into keyboard mode without a starting + * key. + */ + private void tryStartingKbMode(int keyCode) { + if (keyCode == -1 || addKeyIfLegal(keyCode)) { + mInKbMode = true; + onValidationChanged(false); + updateDisplay(false); + mRadialTimePickerView.setInputEnabled(false); + } } + + private boolean addKeyIfLegal(int keyCode) { + // If we're in 24hour mode, we'll need to check if the input is full. If in AM/PM mode, + // we'll need to see if AM/PM have been typed. + if ((mIs24HourView && mTypedTimes.size() == 4) || + (!mIs24HourView && isTypedTimeFullyLegal())) { + return false; + } + + mTypedTimes.add(keyCode); + if (!isTypedTimeLegalSoFar()) { + deleteLastTypedKey(); + return false; + } + + int val = getValFromKeyCode(keyCode); + mDelegator.announceForAccessibility(String.format("%d", val)); + // Automatically fill in 0's if AM or PM was legally entered. + if (isTypedTimeFullyLegal()) { + if (!mIs24HourView && mTypedTimes.size() <= 3) { + mTypedTimes.add(mTypedTimes.size() - 1, KeyEvent.KEYCODE_0); + mTypedTimes.add(mTypedTimes.size() - 1, KeyEvent.KEYCODE_0); + } + onValidationChanged(true); + } + + return true; + } + + /** + * Traverse the tree to see if the keys that have been typed so far are legal as is, + * or may become legal as more keys are typed (excluding backspace). + */ + private boolean isTypedTimeLegalSoFar() { + Node node = mLegalTimesTree; + for (int keyCode : mTypedTimes) { + node = node.canReach(keyCode); + if (node == null) { + return false; + } + } + return true; + } + + /** + * Check if the time that has been typed so far is completely legal, as is. + */ + private boolean isTypedTimeFullyLegal() { + if (mIs24HourView) { + // For 24-hour mode, the time is legal if the hours and minutes are each legal. Note: + // getEnteredTime() will ONLY call isTypedTimeFullyLegal() when NOT in 24hour mode. + int[] values = getEnteredTime(null); + return (values[0] >= 0 && values[1] >= 0 && values[1] < 60); + } else { + // For AM/PM mode, the time is legal if it contains an AM or PM, as those can only be + // legally added at specific times based on the tree's algorithm. + return (mTypedTimes.contains(getAmOrPmKeyCode(AM)) || + mTypedTimes.contains(getAmOrPmKeyCode(PM))); + } + } + + private int deleteLastTypedKey() { + int deleted = mTypedTimes.remove(mTypedTimes.size() - 1); + if (!isTypedTimeFullyLegal()) { + onValidationChanged(false); + } + return deleted; + } + + /** + * Get out of keyboard mode. If there is nothing in typedTimes, revert to TimePicker's time. + */ + private void finishKbMode() { + mInKbMode = false; + if (!mTypedTimes.isEmpty()) { + int values[] = getEnteredTime(null); + mRadialTimePickerView.setCurrentHour(values[0]); + mRadialTimePickerView.setCurrentMinute(values[1]); + if (!mIs24HourView) { + mRadialTimePickerView.setAmOrPm(values[2]); + } + mTypedTimes.clear(); + } + updateDisplay(false); + mRadialTimePickerView.setInputEnabled(true); + } + + /** + * Update the hours, minutes, and AM/PM displays with the typed times. If the typedTimes is + * empty, either show an empty display (filled with the placeholder text), or update from the + * timepicker's values. + * + * @param allowEmptyDisplay if true, then if the typedTimes is empty, use the placeholder text. + * Otherwise, revert to the timepicker's values. + */ + private void updateDisplay(boolean allowEmptyDisplay) { + if (!allowEmptyDisplay && mTypedTimes.isEmpty()) { + int hour = mRadialTimePickerView.getCurrentHour(); + int minute = mRadialTimePickerView.getCurrentMinute(); + updateHeaderHour(hour, false); + updateHeaderMinute(minute, false); + if (!mIs24HourView) { + updateAmPmLabelStates(hour < 12 ? AM : PM); + } + setCurrentItemShowing(mRadialTimePickerView.getCurrentItemShowing(), true, true); + onValidationChanged(true); + } else { + boolean[] enteredZeros = {false, false}; + int[] values = getEnteredTime(enteredZeros); + String hourFormat = enteredZeros[0] ? "%02d" : "%2d"; + String minuteFormat = (enteredZeros[1]) ? "%02d" : "%2d"; + String hourStr = (values[0] == -1) ? mDoublePlaceholderText : + String.format(hourFormat, values[0]).replace(' ', mPlaceholderText); + String minuteStr = (values[1] == -1) ? mDoublePlaceholderText : + String.format(minuteFormat, values[1]).replace(' ', mPlaceholderText); + mHourView.setText(hourStr); + mHourView.setSelected(false); + mMinuteView.setText(minuteStr); + mMinuteView.setSelected(false); + if (!mIs24HourView) { + updateAmPmLabelStates(values[2]); + } + } + } + + private int getValFromKeyCode(int keyCode) { + switch (keyCode) { + case KeyEvent.KEYCODE_0: + return 0; + case KeyEvent.KEYCODE_1: + return 1; + case KeyEvent.KEYCODE_2: + return 2; + case KeyEvent.KEYCODE_3: + return 3; + case KeyEvent.KEYCODE_4: + return 4; + case KeyEvent.KEYCODE_5: + return 5; + case KeyEvent.KEYCODE_6: + return 6; + case KeyEvent.KEYCODE_7: + return 7; + case KeyEvent.KEYCODE_8: + return 8; + case KeyEvent.KEYCODE_9: + return 9; + default: + return -1; + } + } + + /** + * Get the currently-entered time, as integer values of the hours and minutes typed. + * + * @param enteredZeros A size-2 boolean array, which the caller should initialize, and which + * may then be used for the caller to know whether zeros had been explicitly entered as either + * hours of minutes. This is helpful for deciding whether to show the dashes, or actual 0's. + * + * @return A size-3 int array. The first value will be the hours, the second value will be the + * minutes, and the third will be either AM or PM. + */ + private int[] getEnteredTime(boolean[] enteredZeros) { + int amOrPm = -1; + int startIndex = 1; + if (!mIs24HourView && isTypedTimeFullyLegal()) { + int keyCode = mTypedTimes.get(mTypedTimes.size() - 1); + if (keyCode == getAmOrPmKeyCode(AM)) { + amOrPm = AM; + } else if (keyCode == getAmOrPmKeyCode(PM)){ + amOrPm = PM; + } + startIndex = 2; + } + int minute = -1; + int hour = -1; + for (int i = startIndex; i <= mTypedTimes.size(); i++) { + int val = getValFromKeyCode(mTypedTimes.get(mTypedTimes.size() - i)); + if (i == startIndex) { + minute = val; + } else if (i == startIndex+1) { + minute += 10 * val; + if (enteredZeros != null && val == 0) { + enteredZeros[1] = true; + } + } else if (i == startIndex+2) { + hour = val; + } else if (i == startIndex+3) { + hour += 10 * val; + if (enteredZeros != null && val == 0) { + enteredZeros[0] = true; + } + } + } + + return new int[] { hour, minute, amOrPm }; + } + + /** + * Get the keycode value for AM and PM in the current language. + */ + private int getAmOrPmKeyCode(int amOrPm) { + // Cache the codes. + if (mAmKeyCode == -1 || mPmKeyCode == -1) { + // Find the first character in the AM/PM text that is unique. + KeyCharacterMap kcm = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); + char amChar; + char pmChar; + for (int i = 0; i < Math.max(mAmText.length(), mPmText.length()); i++) { + amChar = mAmText.toLowerCase(mCurrentLocale).charAt(i); + pmChar = mPmText.toLowerCase(mCurrentLocale).charAt(i); + if (amChar != pmChar) { + KeyEvent[] events = kcm.getEvents(new char[]{amChar, pmChar}); + // There should be 4 events: a down and up for both AM and PM. + if (events != null && events.length == 4) { + mAmKeyCode = events[0].getKeyCode(); + mPmKeyCode = events[2].getKeyCode(); + } else { + Log.e(TAG, "Unable to find keycodes for AM and PM."); + } + break; + } + } + } + if (amOrPm == AM) { + return mAmKeyCode; + } else if (amOrPm == PM) { + return mPmKeyCode; + } + + return -1; + } + + /** + * Create a tree for deciding what keys can legally be typed. + */ + private void generateLegalTimesTree() { + // Create a quick cache of numbers to their keycodes. + final int k0 = KeyEvent.KEYCODE_0; + final int k1 = KeyEvent.KEYCODE_1; + final int k2 = KeyEvent.KEYCODE_2; + final int k3 = KeyEvent.KEYCODE_3; + final int k4 = KeyEvent.KEYCODE_4; + final int k5 = KeyEvent.KEYCODE_5; + final int k6 = KeyEvent.KEYCODE_6; + final int k7 = KeyEvent.KEYCODE_7; + final int k8 = KeyEvent.KEYCODE_8; + final int k9 = KeyEvent.KEYCODE_9; + + // The root of the tree doesn't contain any numbers. + mLegalTimesTree = new Node(); + if (mIs24HourView) { + // We'll be re-using these nodes, so we'll save them. + Node minuteFirstDigit = new Node(k0, k1, k2, k3, k4, k5); + Node minuteSecondDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); + // The first digit must be followed by the second digit. + minuteFirstDigit.addChild(minuteSecondDigit); + + // The first digit may be 0-1. + Node firstDigit = new Node(k0, k1); + mLegalTimesTree.addChild(firstDigit); + + // When the first digit is 0-1, the second digit may be 0-5. + Node secondDigit = new Node(k0, k1, k2, k3, k4, k5); + firstDigit.addChild(secondDigit); + // We may now be followed by the first minute digit. E.g. 00:09, 15:58. + secondDigit.addChild(minuteFirstDigit); + + // When the first digit is 0-1, and the second digit is 0-5, the third digit may be 6-9. + Node thirdDigit = new Node(k6, k7, k8, k9); + // The time must now be finished. E.g. 0:55, 1:08. + secondDigit.addChild(thirdDigit); + + // When the first digit is 0-1, the second digit may be 6-9. + secondDigit = new Node(k6, k7, k8, k9); + firstDigit.addChild(secondDigit); + // We must now be followed by the first minute digit. E.g. 06:50, 18:20. + secondDigit.addChild(minuteFirstDigit); + + // The first digit may be 2. + firstDigit = new Node(k2); + mLegalTimesTree.addChild(firstDigit); + + // When the first digit is 2, the second digit may be 0-3. + secondDigit = new Node(k0, k1, k2, k3); + firstDigit.addChild(secondDigit); + // We must now be followed by the first minute digit. E.g. 20:50, 23:09. + secondDigit.addChild(minuteFirstDigit); + + // When the first digit is 2, the second digit may be 4-5. + secondDigit = new Node(k4, k5); + firstDigit.addChild(secondDigit); + // We must now be followd by the last minute digit. E.g. 2:40, 2:53. + secondDigit.addChild(minuteSecondDigit); + + // The first digit may be 3-9. + firstDigit = new Node(k3, k4, k5, k6, k7, k8, k9); + mLegalTimesTree.addChild(firstDigit); + // We must now be followed by the first minute digit. E.g. 3:57, 8:12. + firstDigit.addChild(minuteFirstDigit); + } else { + // We'll need to use the AM/PM node a lot. + // Set up AM and PM to respond to "a" and "p". + Node ampm = new Node(getAmOrPmKeyCode(AM), getAmOrPmKeyCode(PM)); + + // The first hour digit may be 1. + Node firstDigit = new Node(k1); + mLegalTimesTree.addChild(firstDigit); + // We'll allow quick input of on-the-hour times. E.g. 1pm. + firstDigit.addChild(ampm); + + // When the first digit is 1, the second digit may be 0-2. + Node secondDigit = new Node(k0, k1, k2); + firstDigit.addChild(secondDigit); + // Also for quick input of on-the-hour times. E.g. 10pm, 12am. + secondDigit.addChild(ampm); + + // When the first digit is 1, and the second digit is 0-2, the third digit may be 0-5. + Node thirdDigit = new Node(k0, k1, k2, k3, k4, k5); + secondDigit.addChild(thirdDigit); + // The time may be finished now. E.g. 1:02pm, 1:25am. + thirdDigit.addChild(ampm); + + // When the first digit is 1, the second digit is 0-2, and the third digit is 0-5, + // the fourth digit may be 0-9. + Node fourthDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); + thirdDigit.addChild(fourthDigit); + // The time must be finished now. E.g. 10:49am, 12:40pm. + fourthDigit.addChild(ampm); + + // When the first digit is 1, and the second digit is 0-2, the third digit may be 6-9. + thirdDigit = new Node(k6, k7, k8, k9); + secondDigit.addChild(thirdDigit); + // The time must be finished now. E.g. 1:08am, 1:26pm. + thirdDigit.addChild(ampm); + + // When the first digit is 1, the second digit may be 3-5. + secondDigit = new Node(k3, k4, k5); + firstDigit.addChild(secondDigit); + + // When the first digit is 1, and the second digit is 3-5, the third digit may be 0-9. + thirdDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); + secondDigit.addChild(thirdDigit); + // The time must be finished now. E.g. 1:39am, 1:50pm. + thirdDigit.addChild(ampm); + + // The hour digit may be 2-9. + firstDigit = new Node(k2, k3, k4, k5, k6, k7, k8, k9); + mLegalTimesTree.addChild(firstDigit); + // We'll allow quick input of on-the-hour-times. E.g. 2am, 5pm. + firstDigit.addChild(ampm); + + // When the first digit is 2-9, the second digit may be 0-5. + secondDigit = new Node(k0, k1, k2, k3, k4, k5); + firstDigit.addChild(secondDigit); + + // When the first digit is 2-9, and the second digit is 0-5, the third digit may be 0-9. + thirdDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); + secondDigit.addChild(thirdDigit); + // The time must be finished now. E.g. 2:57am, 9:30pm. + thirdDigit.addChild(ampm); + } + } + + /** + * Simple node class to be used for traversal to check for legal times. + * mLegalKeys represents the keys that can be typed to get to the node. + * mChildren are the children that can be reached from this node. + */ + private class Node { + private int[] mLegalKeys; + private ArrayList<Node> mChildren; + + public Node(int... legalKeys) { + mLegalKeys = legalKeys; + mChildren = new ArrayList<Node>(); + } + + public void addChild(Node child) { + mChildren.add(child); + } + + public boolean containsKey(int key) { + for (int i = 0; i < mLegalKeys.length; i++) { + if (mLegalKeys[i] == key) { + return true; + } + } + return false; + } + + public Node canReach(int key) { + if (mChildren == null) { + return null; + } + for (Node child : mChildren) { + if (child.containsKey(key)) { + return child; + } + } + return null; + } + } + + private final View.OnClickListener mClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + + final int amOrPm; + switch (v.getId()) { + case R.id.am_label: + setAmOrPm(AM); + break; + case R.id.pm_label: + setAmOrPm(PM); + break; + case R.id.hours: + setCurrentItemShowing(HOUR_INDEX, true, true); + break; + case R.id.minutes: + setCurrentItemShowing(MINUTE_INDEX, true, true); + break; + default: + // Failed to handle this click, don't vibrate. + return; + } + + tryVibrate(); + } + }; + + private final View.OnKeyListener mKeyListener = new View.OnKeyListener() { + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_UP) { + return processKeyUp(keyCode); + } + return false; + } + }; + + private final View.OnFocusChangeListener mFocusListener = new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (!hasFocus && mInKbMode && isTypedTimeFullyLegal()) { + finishKbMode(); + + if (mOnTimeChangedListener != null) { + mOnTimeChangedListener.onTimeChanged(mDelegator, + mRadialTimePickerView.getCurrentHour(), + mRadialTimePickerView.getCurrentMinute()); + } + } + } + }; } diff --git a/core/java/android/widget/TimePickerSpinnerDelegate.java b/core/java/android/widget/TimePickerSpinnerDelegate.java index 73e05e8..e162f4a 100644 --- a/core/java/android/widget/TimePickerSpinnerDelegate.java +++ b/core/java/android/widget/TimePickerSpinnerDelegate.java @@ -17,380 +17,365 @@ package android.widget; import android.content.Context; -import android.content.res.ColorStateList; import android.content.res.Configuration; -import android.content.res.Resources; import android.content.res.TypedArray; import android.os.Parcel; import android.os.Parcelable; -import android.text.TextUtils; import android.text.format.DateFormat; import android.text.format.DateUtils; import android.util.AttributeSet; -import android.util.Log; -import android.view.HapticFeedbackConstants; -import android.view.KeyCharacterMap; -import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; - +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; import com.android.internal.R; -import java.util.ArrayList; +import java.text.DateFormatSymbols; import java.util.Calendar; import java.util.Locale; -/** - * A view for selecting the time of day, in either 24 hour or AM/PM mode. - */ -class TimePickerSpinnerDelegate extends TimePicker.AbstractTimePickerDelegate implements - RadialTimePickerView.OnValueSelectedListener { - - private static final String TAG = "TimePickerDelegate"; - - // Index used by RadialPickerLayout - private static final int HOUR_INDEX = 0; - private static final int MINUTE_INDEX = 1; - - // NOT a real index for the purpose of what's showing. - private static final int AMPM_INDEX = 2; +import libcore.icu.LocaleData; - // Also NOT a real index, just used for keyboard mode. - private static final int ENABLE_PICKER_INDEX = 3; - - private static final int AM = 0; - private static final int PM = 1; +import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_AUTO; +import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES; +/** + * A delegate implementing the basic spinner-based TimePicker. + */ +class TimePickerSpinnerDelegate extends TimePicker.AbstractTimePickerDelegate { private static final boolean DEFAULT_ENABLED_STATE = true; - private boolean mIsEnabled = DEFAULT_ENABLED_STATE; - private static final int HOURS_IN_HALF_DAY = 12; - private View mHeaderView; - private TextView mHourView; - private TextView mMinuteView; - private TextView mAmPmTextView; - private RadialTimePickerView mRadialTimePickerView; - private TextView mSeparatorView; + // state + private boolean mIs24HourView; + private boolean mIsAm; - private String mAmText; - private String mPmText; + // ui components + private final NumberPicker mHourSpinner; + private final NumberPicker mMinuteSpinner; + private final NumberPicker mAmPmSpinner; + private final EditText mHourSpinnerInput; + private final EditText mMinuteSpinnerInput; + private final EditText mAmPmSpinnerInput; + private final TextView mDivider; - private boolean mAllowAutoAdvance; - private int mInitialHourOfDay; - private int mInitialMinute; - private boolean mIs24HourView; + // Note that the legacy implementation of the TimePicker is + // using a button for toggling between AM/PM while the new + // version uses a NumberPicker spinner. Therefore the code + // accommodates these two cases to be backwards compatible. + private final Button mAmPmButton; - // For hardware IME input. - private char mPlaceholderText; - private String mDoublePlaceholderText; - private String mDeletedKeyFormat; - private boolean mInKbMode; - private ArrayList<Integer> mTypedTimes = new ArrayList<Integer>(); - private Node mLegalTimesTree; - private int mAmKeyCode; - private int mPmKeyCode; - - // Accessibility strings. - private String mHourPickerDescription; - private String mSelectHours; - private String mMinutePickerDescription; - private String mSelectMinutes; + private final String[] mAmPmStrings; + private boolean mIsEnabled = DEFAULT_ENABLED_STATE; private Calendar mTempCalendar; + private boolean mHourWithTwoDigit; + private char mHourFormat; public TimePickerSpinnerDelegate(TimePicker delegator, Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(delegator, context); // process style attributes - final TypedArray a = mContext.obtainStyledAttributes(attrs, - R.styleable.TimePicker, defStyleAttr, defStyleRes); - final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( - Context.LAYOUT_INFLATER_SERVICE); - final Resources res = mContext.getResources(); - - mHourPickerDescription = res.getString(R.string.hour_picker_description); - mSelectHours = res.getString(R.string.select_hours); - mMinutePickerDescription = res.getString(R.string.minute_picker_description); - mSelectMinutes = res.getString(R.string.select_minutes); - - String[] amPmStrings = TimePickerClockDelegate.getAmPmStrings(context); - mAmText = amPmStrings[0]; - mPmText = amPmStrings[1]; - - final int layoutResourceId = a.getResourceId(R.styleable.TimePicker_internalLayout, - R.layout.time_picker_holo); - final View mainView = inflater.inflate(layoutResourceId, null); - mDelegator.addView(mainView); - - mHourView = (TextView) mainView.findViewById(R.id.hours); - mSeparatorView = (TextView) mainView.findViewById(R.id.separator); - mMinuteView = (TextView) mainView.findViewById(R.id.minutes); - mAmPmTextView = (TextView) mainView.findViewById(R.id.ampm_label); - - // Set up text appearances from style. - final int headerTimeTextAppearance = a.getResourceId( - R.styleable.TimePicker_headerTimeTextAppearance, 0); - if (headerTimeTextAppearance != 0) { - mHourView.setTextAppearance(context, headerTimeTextAppearance); - mSeparatorView.setTextAppearance(context, headerTimeTextAppearance); - mMinuteView.setTextAppearance(context, headerTimeTextAppearance); - } + final TypedArray a = mContext.obtainStyledAttributes( + attrs, R.styleable.TimePicker, defStyleAttr, defStyleRes); + final int layoutResourceId = a.getResourceId( + R.styleable.TimePicker_legacyLayout, R.layout.time_picker_legacy); + a.recycle(); - final int headerSelectedTextColor = a.getColor( - R.styleable.TimePicker_headerSelectedTextColor, - res.getColor(R.color.timepicker_default_selector_color_material)); - mHourView.setTextColor(ColorStateList.addFirstIfMissing(mHourView.getTextColors(), - R.attr.state_selected, headerSelectedTextColor)); - mMinuteView.setTextColor(ColorStateList.addFirstIfMissing(mMinuteView.getTextColors(), - R.attr.state_selected, headerSelectedTextColor)); - - final int headerAmPmTextAppearance = a.getResourceId( - R.styleable.TimePicker_headerAmPmTextAppearance, 0); - if (headerAmPmTextAppearance != 0) { - mAmPmTextView.setTextAppearance(context, headerAmPmTextAppearance); + final LayoutInflater inflater = LayoutInflater.from(mContext); + inflater.inflate(layoutResourceId, mDelegator, true); + + // hour + mHourSpinner = (NumberPicker) delegator.findViewById(R.id.hour); + mHourSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { + public void onValueChange(NumberPicker spinner, int oldVal, int newVal) { + updateInputState(); + if (!is24HourView()) { + if ((oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) || + (oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1)) { + mIsAm = !mIsAm; + updateAmPmControl(); + } + } + onTimeChanged(); + } + }); + mHourSpinnerInput = (EditText) mHourSpinner.findViewById(R.id.numberpicker_input); + mHourSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); + + // divider (only for the new widget style) + mDivider = (TextView) mDelegator.findViewById(R.id.divider); + if (mDivider != null) { + setDividerText(); + } + + // minute + mMinuteSpinner = (NumberPicker) mDelegator.findViewById(R.id.minute); + mMinuteSpinner.setMinValue(0); + mMinuteSpinner.setMaxValue(59); + mMinuteSpinner.setOnLongPressUpdateInterval(100); + mMinuteSpinner.setFormatter(NumberPicker.getTwoDigitFormatter()); + mMinuteSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { + public void onValueChange(NumberPicker spinner, int oldVal, int newVal) { + updateInputState(); + int minValue = mMinuteSpinner.getMinValue(); + int maxValue = mMinuteSpinner.getMaxValue(); + if (oldVal == maxValue && newVal == minValue) { + int newHour = mHourSpinner.getValue() + 1; + if (!is24HourView() && newHour == HOURS_IN_HALF_DAY) { + mIsAm = !mIsAm; + updateAmPmControl(); + } + mHourSpinner.setValue(newHour); + } else if (oldVal == minValue && newVal == maxValue) { + int newHour = mHourSpinner.getValue() - 1; + if (!is24HourView() && newHour == HOURS_IN_HALF_DAY - 1) { + mIsAm = !mIsAm; + updateAmPmControl(); + } + mHourSpinner.setValue(newHour); + } + onTimeChanged(); + } + }); + mMinuteSpinnerInput = (EditText) mMinuteSpinner.findViewById(R.id.numberpicker_input); + mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); + + // Get the localized am/pm strings and use them in the spinner. + mAmPmStrings = getAmPmStrings(context); + + // am/pm + final View amPmView = mDelegator.findViewById(R.id.amPm); + if (amPmView instanceof Button) { + mAmPmSpinner = null; + mAmPmSpinnerInput = null; + mAmPmButton = (Button) amPmView; + mAmPmButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View button) { + button.requestFocus(); + mIsAm = !mIsAm; + updateAmPmControl(); + onTimeChanged(); + } + }); + } else { + mAmPmButton = null; + mAmPmSpinner = (NumberPicker) amPmView; + mAmPmSpinner.setMinValue(0); + mAmPmSpinner.setMaxValue(1); + mAmPmSpinner.setDisplayedValues(mAmPmStrings); + mAmPmSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + updateInputState(); + picker.requestFocus(); + mIsAm = !mIsAm; + updateAmPmControl(); + onTimeChanged(); + } + }); + mAmPmSpinnerInput = (EditText) mAmPmSpinner.findViewById(R.id.numberpicker_input); + mAmPmSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE); + } + + if (isAmPmAtStart()) { + // Move the am/pm view to the beginning + ViewGroup amPmParent = (ViewGroup) delegator.findViewById(R.id.timePickerLayout); + amPmParent.removeView(amPmView); + amPmParent.addView(amPmView, 0); + // Swap layout margins if needed. They may be not symmetrical (Old Standard Theme + // for example and not for Holo Theme) + ViewGroup.MarginLayoutParams lp = + (ViewGroup.MarginLayoutParams) amPmView.getLayoutParams(); + final int startMargin = lp.getMarginStart(); + final int endMargin = lp.getMarginEnd(); + if (startMargin != endMargin) { + lp.setMarginStart(endMargin); + lp.setMarginEnd(startMargin); + } } - mHeaderView = mainView.findViewById(R.id.time_header); - mHeaderView.setBackground(a.getDrawable(R.styleable.TimePicker_headerBackground)); - - a.recycle(); + getHourFormatData(); - mRadialTimePickerView = (RadialTimePickerView) mainView.findViewById( - R.id.radial_picker); + // update controls to initial state + updateHourControl(); + updateMinuteControl(); + updateAmPmControl(); - setupListeners(); + // set to current time + setCurrentHour(mTempCalendar.get(Calendar.HOUR_OF_DAY)); + setCurrentMinute(mTempCalendar.get(Calendar.MINUTE)); - mAllowAutoAdvance = true; + if (!isEnabled()) { + setEnabled(false); + } - // Set up for keyboard mode. - mDoublePlaceholderText = res.getString(R.string.time_placeholder); - mDeletedKeyFormat = res.getString(R.string.deleted_key); - mPlaceholderText = mDoublePlaceholderText.charAt(0); - mAmKeyCode = mPmKeyCode = -1; - generateLegalTimesTree(); + // set the content descriptions + setContentDescriptions(); - // Initialize with current time - final Calendar calendar = Calendar.getInstance(mCurrentLocale); - final int currentHour = calendar.get(Calendar.HOUR_OF_DAY); - final int currentMinute = calendar.get(Calendar.MINUTE); - initialize(currentHour, currentMinute, false /* 12h */, HOUR_INDEX); - } - - private void initialize(int hourOfDay, int minute, boolean is24HourView, int index) { - mInitialHourOfDay = hourOfDay; - mInitialMinute = minute; - mIs24HourView = is24HourView; - mInKbMode = false; - updateUI(index); + // If not explicitly specified this view is important for accessibility. + if (mDelegator.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + mDelegator.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } } - private void setupListeners() { - mHeaderView.setOnKeyListener(mKeyListener); - mHeaderView.setOnFocusChangeListener(mFocusListener); - mHeaderView.setFocusable(true); - - mRadialTimePickerView.setOnValueSelectedListener(this); - - mHourView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - setCurrentItemShowing(HOUR_INDEX, true, true); - tryVibrate(); - } - }); - mMinuteView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - setCurrentItemShowing(MINUTE_INDEX, true, true); - tryVibrate(); + private void getHourFormatData() { + final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, + (mIs24HourView) ? "Hm" : "hm"); + final int lengthPattern = bestDateTimePattern.length(); + mHourWithTwoDigit = false; + char hourFormat = '\0'; + // Check if the returned pattern is single or double 'H', 'h', 'K', 'k'. We also save + // the hour format that we found. + for (int i = 0; i < lengthPattern; i++) { + final char c = bestDateTimePattern.charAt(i); + if (c == 'H' || c == 'h' || c == 'K' || c == 'k') { + mHourFormat = c; + if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) { + mHourWithTwoDigit = true; + } + break; } - }); + } } - private void updateUI(int index) { - // Update RadialPicker values - updateRadialPicker(index); - // Enable or disable the AM/PM view. - updateHeaderAmPm(); - // Update Hour and Minutes - updateHeaderHour(mInitialHourOfDay, true); - // Update time separator - updateHeaderSeparator(); - // Update Minutes - updateHeaderMinute(mInitialMinute); - // Invalidate everything - mDelegator.invalidate(); - } + private boolean isAmPmAtStart() { + final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, + "hm" /* skeleton */); - private void updateRadialPicker(int index) { - mRadialTimePickerView.initialize(mInitialHourOfDay, mInitialMinute, mIs24HourView); - setCurrentItemShowing(index, false, true); + return bestDateTimePattern.startsWith("a"); } - private int computeMaxWidthOfNumbers(int max) { - TextView tempView = new TextView(mContext); - tempView.setTextAppearance(mContext, R.style.TextAppearance_Material_TimePicker_TimeLabel); - ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT); - tempView.setLayoutParams(lp); - int maxWidth = 0; - for (int minutes = 0; minutes < max; minutes++) { - final String text = String.format("%02d", minutes); - tempView.setText(text); - tempView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); - maxWidth = Math.max(maxWidth, tempView.getMeasuredWidth()); + /** + * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":". + * + * See http://unicode.org/cldr/trac/browser/trunk/common/main + * + * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the + * separator as the character which is just after the hour marker in the returned pattern. + */ + private void setDividerText() { + final String skeleton = (mIs24HourView) ? "Hm" : "hm"; + final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, + skeleton); + final String separatorText; + int hourIndex = bestDateTimePattern.lastIndexOf('H'); + if (hourIndex == -1) { + hourIndex = bestDateTimePattern.lastIndexOf('h'); } - return maxWidth; - } - - private void updateHeaderAmPm() { - if (mIs24HourView) { - mAmPmTextView.setVisibility(View.GONE); + if (hourIndex == -1) { + // Default case + separatorText = ":"; } else { - mAmPmTextView.setVisibility(View.VISIBLE); - final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, - "hm"); - - boolean amPmOnLeft = bestDateTimePattern.startsWith("a"); - if (TextUtils.getLayoutDirectionFromLocale(mCurrentLocale) == - View.LAYOUT_DIRECTION_RTL) { - amPmOnLeft = !amPmOnLeft; - } - - RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) - mAmPmTextView.getLayoutParams(); - - if (amPmOnLeft) { - layoutParams.rightMargin = computeMaxWidthOfNumbers(12 /* for hours */); - layoutParams.removeRule(RelativeLayout.RIGHT_OF); - layoutParams.addRule(RelativeLayout.LEFT_OF, R.id.separator); + int minuteIndex = bestDateTimePattern.indexOf('m', hourIndex + 1); + if (minuteIndex == -1) { + separatorText = Character.toString(bestDateTimePattern.charAt(hourIndex + 1)); } else { - layoutParams.leftMargin = computeMaxWidthOfNumbers(60 /* for minutes */); - layoutParams.removeRule(RelativeLayout.LEFT_OF); - layoutParams.addRule(RelativeLayout.RIGHT_OF, R.id.separator); + separatorText = bestDateTimePattern.substring(hourIndex + 1, minuteIndex); } - - updateAmPmDisplay(mInitialHourOfDay < 12 ? AM : PM); - mAmPmTextView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - tryVibrate(); - int amOrPm = mRadialTimePickerView.getAmOrPm(); - if (amOrPm == AM) { - amOrPm = PM; - } else if (amOrPm == PM){ - amOrPm = AM; - } - updateAmPmDisplay(amOrPm); - mRadialTimePickerView.setAmOrPm(amOrPm); - } - }); } + mDivider.setText(separatorText); } - /** - * Set the current hour. - */ @Override public void setCurrentHour(Integer currentHour) { - if (mInitialHourOfDay == currentHour) { + setCurrentHour(currentHour, true); + } + + private void setCurrentHour(Integer currentHour, boolean notifyTimeChanged) { + // why was Integer used in the first place? + if (currentHour == null || currentHour == getCurrentHour()) { return; } - mInitialHourOfDay = currentHour; - updateHeaderHour(currentHour, true /* accessibility announce */); - updateHeaderAmPm(); - mRadialTimePickerView.setCurrentHour(currentHour); - mRadialTimePickerView.setAmOrPm(mInitialHourOfDay < 12 ? AM : PM); - mDelegator.invalidate(); - onTimeChanged(); + if (!is24HourView()) { + // convert [0,23] ordinal to wall clock display + if (currentHour >= HOURS_IN_HALF_DAY) { + mIsAm = false; + if (currentHour > HOURS_IN_HALF_DAY) { + currentHour = currentHour - HOURS_IN_HALF_DAY; + } + } else { + mIsAm = true; + if (currentHour == 0) { + currentHour = HOURS_IN_HALF_DAY; + } + } + updateAmPmControl(); + } + mHourSpinner.setValue(currentHour); + if (notifyTimeChanged) { + onTimeChanged(); + } } - /** - * @return The current hour in the range (0-23). - */ @Override public Integer getCurrentHour() { - int currentHour = mRadialTimePickerView.getCurrentHour(); - if (mIs24HourView) { + int currentHour = mHourSpinner.getValue(); + if (is24HourView()) { return currentHour; + } else if (mIsAm) { + return currentHour % HOURS_IN_HALF_DAY; } else { - switch(mRadialTimePickerView.getAmOrPm()) { - case PM: - return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY; - case AM: - default: - return currentHour % HOURS_IN_HALF_DAY; - } + return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY; } } - /** - * Set the current minute (0-59). - */ @Override public void setCurrentMinute(Integer currentMinute) { - if (mInitialMinute == currentMinute) { + if (currentMinute == getCurrentMinute()) { return; } - mInitialMinute = currentMinute; - updateHeaderMinute(currentMinute); - mRadialTimePickerView.setCurrentMinute(currentMinute); - mDelegator.invalidate(); + mMinuteSpinner.setValue(currentMinute); onTimeChanged(); } - /** - * @return The current minute. - */ @Override public Integer getCurrentMinute() { - return mRadialTimePickerView.getCurrentMinute(); + return mMinuteSpinner.getValue(); } - /** - * Set whether in 24 hour or AM/PM mode. - * - * @param is24HourView True = 24 hour mode. False = AM/PM. - */ @Override public void setIs24HourView(Boolean is24HourView) { - if (is24HourView == mIs24HourView) { + if (mIs24HourView == is24HourView) { return; } + // cache the current hour since spinner range changes and BEFORE changing mIs24HourView!! + int currentHour = getCurrentHour(); + // Order is important here. mIs24HourView = is24HourView; - generateLegalTimesTree(); - int hour = mRadialTimePickerView.getCurrentHour(); - mInitialHourOfDay = hour; - updateHeaderHour(hour, false /* no accessibility announce */); - updateHeaderAmPm(); - updateRadialPicker(mRadialTimePickerView.getCurrentItemShowing()); - mDelegator.invalidate(); + getHourFormatData(); + updateHourControl(); + // set value after spinner range is updated + setCurrentHour(currentHour, false); + updateMinuteControl(); + updateAmPmControl(); } - /** - * @return true if this is in 24 hour view else false. - */ @Override public boolean is24HourView() { return mIs24HourView; } @Override - public void setOnTimeChangedListener(TimePicker.OnTimeChangedListener callback) { - mOnTimeChangedListener = callback; + public void setOnTimeChangedListener(TimePicker.OnTimeChangedListener onTimeChangedListener) { + mOnTimeChangedListener = onTimeChangedListener; } @Override public void setEnabled(boolean enabled) { - mHourView.setEnabled(enabled); - mMinuteView.setEnabled(enabled); - mAmPmTextView.setEnabled(enabled); - mRadialTimePickerView.setEnabled(enabled); + mMinuteSpinner.setEnabled(enabled); + if (mDivider != null) { + mDivider.setEnabled(enabled); + } + mHourSpinner.setEnabled(enabled); + if (mAmPmSpinner != null) { + mAmPmSpinner.setEnabled(enabled); + } else { + mAmPmButton.setEnabled(enabled); + } mIsEnabled = enabled; } @@ -401,38 +386,24 @@ class TimePickerSpinnerDelegate extends TimePicker.AbstractTimePickerDelegate im @Override public int getBaseline() { - // does not support baseline alignment - return -1; + return mHourSpinner.getBaseline(); } @Override public void onConfigurationChanged(Configuration newConfig) { - updateUI(mRadialTimePickerView.getCurrentItemShowing()); + setCurrentLocale(newConfig.locale); } @Override public Parcelable onSaveInstanceState(Parcelable superState) { - return new SavedState(superState, getCurrentHour(), getCurrentMinute(), - is24HourView(), inKbMode(), getTypedTimes(), getCurrentItemShowing()); + return new SavedState(superState, getCurrentHour(), getCurrentMinute()); } @Override public void onRestoreInstanceState(Parcelable state) { SavedState ss = (SavedState) state; - setInKbMode(ss.inKbMode()); - setTypedTimes(ss.getTypesTimes()); - initialize(ss.getHour(), ss.getMinute(), ss.is24HourMode(), ss.getCurrentItemShowing()); - mRadialTimePickerView.invalidate(); - if (mInKbMode) { - tryStartingKbMode(-1); - mHourView.invalidate(); - } - } - - @Override - public void setCurrentLocale(Locale locale) { - super.setCurrentLocale(locale); - mTempCalendar = Calendar.getInstance(locale); + setCurrentHour(ss.getHour()); + setCurrentMinute(ss.getMinute()); } @Override @@ -451,9 +422,9 @@ class TimePickerSpinnerDelegate extends TimePicker.AbstractTimePickerDelegate im } mTempCalendar.set(Calendar.HOUR_OF_DAY, getCurrentHour()); mTempCalendar.set(Calendar.MINUTE, getCurrentMinute()); - String selectedDate = DateUtils.formatDateTime(mContext, + String selectedDateUtterance = DateUtils.formatDateTime(mContext, mTempCalendar.getTimeInMillis(), flags); - event.getText().add(selectedDate); + event.getText().add(selectedDateUtterance); } @Override @@ -466,48 +437,121 @@ class TimePickerSpinnerDelegate extends TimePicker.AbstractTimePickerDelegate im info.setClassName(TimePicker.class.getName()); } + private void updateInputState() { + // Make sure that if the user changes the value and the IME is active + // for one of the inputs if this widget, the IME is closed. If the user + // changed the value via the IME and there is a next input the IME will + // be shown, otherwise the user chose another means of changing the + // value and having the IME up makes no sense. + InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); + if (inputMethodManager != null) { + if (inputMethodManager.isActive(mHourSpinnerInput)) { + mHourSpinnerInput.clearFocus(); + inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); + } else if (inputMethodManager.isActive(mMinuteSpinnerInput)) { + mMinuteSpinnerInput.clearFocus(); + inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); + } else if (inputMethodManager.isActive(mAmPmSpinnerInput)) { + mAmPmSpinnerInput.clearFocus(); + inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); + } + } + } + + private void updateAmPmControl() { + if (is24HourView()) { + if (mAmPmSpinner != null) { + mAmPmSpinner.setVisibility(View.GONE); + } else { + mAmPmButton.setVisibility(View.GONE); + } + } else { + int index = mIsAm ? Calendar.AM : Calendar.PM; + if (mAmPmSpinner != null) { + mAmPmSpinner.setValue(index); + mAmPmSpinner.setVisibility(View.VISIBLE); + } else { + mAmPmButton.setText(mAmPmStrings[index]); + mAmPmButton.setVisibility(View.VISIBLE); + } + } + mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + } + /** - * Set whether in keyboard mode or not. + * Sets the current locale. * - * @param inKbMode True means in keyboard mode. + * @param locale The current locale. */ - private void setInKbMode(boolean inKbMode) { - mInKbMode = inKbMode; + @Override + public void setCurrentLocale(Locale locale) { + super.setCurrentLocale(locale); + mTempCalendar = Calendar.getInstance(locale); } - /** - * @return true if in keyboard mode - */ - private boolean inKbMode() { - return mInKbMode; + private void onTimeChanged() { + mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + if (mOnTimeChangedListener != null) { + mOnTimeChangedListener.onTimeChanged(mDelegator, getCurrentHour(), + getCurrentMinute()); + } } - private void setTypedTimes(ArrayList<Integer> typeTimes) { - mTypedTimes = typeTimes; + private void updateHourControl() { + if (is24HourView()) { + // 'k' means 1-24 hour + if (mHourFormat == 'k') { + mHourSpinner.setMinValue(1); + mHourSpinner.setMaxValue(24); + } else { + mHourSpinner.setMinValue(0); + mHourSpinner.setMaxValue(23); + } + } else { + // 'K' means 0-11 hour + if (mHourFormat == 'K') { + mHourSpinner.setMinValue(0); + mHourSpinner.setMaxValue(11); + } else { + mHourSpinner.setMinValue(1); + mHourSpinner.setMaxValue(12); + } + } + mHourSpinner.setFormatter(mHourWithTwoDigit ? NumberPicker.getTwoDigitFormatter() : null); } - /** - * @return an array of typed times - */ - private ArrayList<Integer> getTypedTimes() { - return mTypedTimes; + private void updateMinuteControl() { + if (is24HourView()) { + mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE); + } else { + mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); + } } - /** - * @return the index of the current item showing - */ - private int getCurrentItemShowing() { - return mRadialTimePickerView.getCurrentItemShowing(); + private void setContentDescriptions() { + // Minute + trySetContentDescription(mMinuteSpinner, R.id.increment, + R.string.time_picker_increment_minute_button); + trySetContentDescription(mMinuteSpinner, R.id.decrement, + R.string.time_picker_decrement_minute_button); + // Hour + trySetContentDescription(mHourSpinner, R.id.increment, + R.string.time_picker_increment_hour_button); + trySetContentDescription(mHourSpinner, R.id.decrement, + R.string.time_picker_decrement_hour_button); + // AM/PM + if (mAmPmSpinner != null) { + trySetContentDescription(mAmPmSpinner, R.id.increment, + R.string.time_picker_increment_set_pm_button); + trySetContentDescription(mAmPmSpinner, R.id.decrement, + R.string.time_picker_decrement_set_am_button); + } } - /** - * Propagate the time change - */ - private void onTimeChanged() { - mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); - if (mOnTimeChangedListener != null) { - mOnTimeChangedListener.onTimeChanged(mDelegator, - getCurrentHour(), getCurrentMinute()); + private void trySetContentDescription(View root, int viewId, int contDescResId) { + View target = root.findViewById(viewId); + if (target != null) { + target.setContentDescription(mContext.getString(contDescResId)); } } @@ -515,34 +559,19 @@ class TimePickerSpinnerDelegate extends TimePicker.AbstractTimePickerDelegate im * Used to save / restore state of time picker */ private static class SavedState extends View.BaseSavedState { - private final int mHour; private final int mMinute; - private final boolean mIs24HourMode; - private final boolean mInKbMode; - private final ArrayList<Integer> mTypedTimes; - private final int mCurrentItemShowing; - - private SavedState(Parcelable superState, int hour, int minute, boolean is24HourMode, - boolean isKbMode, ArrayList<Integer> typedTimes, - int currentItemShowing) { + + private SavedState(Parcelable superState, int hour, int minute) { super(superState); mHour = hour; mMinute = minute; - mIs24HourMode = is24HourMode; - mInKbMode = isKbMode; - mTypedTimes = typedTimes; - mCurrentItemShowing = currentItemShowing; } private SavedState(Parcel in) { super(in); mHour = in.readInt(); mMinute = in.readInt(); - mIs24HourMode = (in.readInt() == 1); - mInKbMode = (in.readInt() == 1); - mTypedTimes = in.readArrayList(getClass().getClassLoader()); - mCurrentItemShowing = in.readInt(); } public int getHour() { @@ -553,31 +582,11 @@ class TimePickerSpinnerDelegate extends TimePicker.AbstractTimePickerDelegate im return mMinute; } - public boolean is24HourMode() { - return mIs24HourMode; - } - - public boolean inKbMode() { - return mInKbMode; - } - - public ArrayList<Integer> getTypesTimes() { - return mTypedTimes; - } - - public int getCurrentItemShowing() { - return mCurrentItemShowing; - } - @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeInt(mHour); dest.writeInt(mMinute); - dest.writeInt(mIs24HourMode ? 1 : 0); - dest.writeInt(mInKbMode ? 1 : 0); - dest.writeList(mTypedTimes); - dest.writeInt(mCurrentItemShowing); } @SuppressWarnings({"unused", "hiding"}) @@ -592,667 +601,11 @@ class TimePickerSpinnerDelegate extends TimePicker.AbstractTimePickerDelegate im }; } - private void tryVibrate() { - mDelegator.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK); - } - - private void updateAmPmDisplay(int amOrPm) { - if (amOrPm == AM) { - mAmPmTextView.setText(mAmText); - mRadialTimePickerView.announceForAccessibility(mAmText); - } else if (amOrPm == PM){ - mAmPmTextView.setText(mPmText); - mRadialTimePickerView.announceForAccessibility(mPmText); - } else { - mAmPmTextView.setText(mDoublePlaceholderText); - } - } - - /** - * Called by the picker for updating the header display. - */ - @Override - public void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance) { - if (pickerIndex == HOUR_INDEX) { - updateHeaderHour(newValue, false); - String announcement = String.format("%d", newValue); - if (mAllowAutoAdvance && autoAdvance) { - setCurrentItemShowing(MINUTE_INDEX, true, false); - announcement += ". " + mSelectMinutes; - } else { - mRadialTimePickerView.setContentDescription( - mHourPickerDescription + ": " + newValue); - } - - mRadialTimePickerView.announceForAccessibility(announcement); - } else if (pickerIndex == MINUTE_INDEX){ - updateHeaderMinute(newValue); - mRadialTimePickerView.setContentDescription(mMinutePickerDescription + ": " + newValue); - } else if (pickerIndex == AMPM_INDEX) { - updateAmPmDisplay(newValue); - } else if (pickerIndex == ENABLE_PICKER_INDEX) { - if (!isTypedTimeFullyLegal()) { - mTypedTimes.clear(); - } - finishKbMode(); - } - } - - private void updateHeaderHour(int value, boolean announce) { - final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, - (mIs24HourView) ? "Hm" : "hm"); - final int lengthPattern = bestDateTimePattern.length(); - boolean hourWithTwoDigit = false; - char hourFormat = '\0'; - // Check if the returned pattern is single or double 'H', 'h', 'K', 'k'. We also save - // the hour format that we found. - for (int i = 0; i < lengthPattern; i++) { - final char c = bestDateTimePattern.charAt(i); - if (c == 'H' || c == 'h' || c == 'K' || c == 'k') { - hourFormat = c; - if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) { - hourWithTwoDigit = true; - } - break; - } - } - final String format; - if (hourWithTwoDigit) { - format = "%02d"; - } else { - format = "%d"; - } - if (mIs24HourView) { - // 'k' means 1-24 hour - if (hourFormat == 'k' && value == 0) { - value = 24; - } - } else { - // 'K' means 0-11 hour - value = modulo12(value, hourFormat == 'K'); - } - CharSequence text = String.format(format, value); - mHourView.setText(text); - if (announce) { - mRadialTimePickerView.announceForAccessibility(text); - } - } - - private static int modulo12(int n, boolean startWithZero) { - int value = n % 12; - if (value == 0 && !startWithZero) { - value = 12; - } - return value; - } - - /** - * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":". - * - * See http://unicode.org/cldr/trac/browser/trunk/common/main - * - * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the - * separator as the character which is just after the hour marker in the returned pattern. - */ - private void updateHeaderSeparator() { - final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, - (mIs24HourView) ? "Hm" : "hm"); - final String separatorText; - // See http://www.unicode.org/reports/tr35/tr35-dates.html for hour formats - final char[] hourFormats = {'H', 'h', 'K', 'k'}; - int hIndex = lastIndexOfAny(bestDateTimePattern, hourFormats); - if (hIndex == -1) { - // Default case - separatorText = ":"; - } else { - separatorText = Character.toString(bestDateTimePattern.charAt(hIndex + 1)); - } - mSeparatorView.setText(separatorText); - } - - static private int lastIndexOfAny(String str, char[] any) { - final int lengthAny = any.length; - if (lengthAny > 0) { - for (int i = str.length() - 1; i >= 0; i--) { - char c = str.charAt(i); - for (int j = 0; j < lengthAny; j++) { - if (c == any[j]) { - return i; - } - } - } - } - return -1; - } - - private void updateHeaderMinute(int value) { - if (value == 60) { - value = 0; - } - CharSequence text = String.format(mCurrentLocale, "%02d", value); - mRadialTimePickerView.announceForAccessibility(text); - mMinuteView.setText(text); - } - - /** - * Show either Hours or Minutes. - */ - private void setCurrentItemShowing(int index, boolean animateCircle, boolean announce) { - mRadialTimePickerView.setCurrentItemShowing(index, animateCircle); - - if (index == HOUR_INDEX) { - int hours = mRadialTimePickerView.getCurrentHour(); - if (!mIs24HourView) { - hours = hours % 12; - } - mRadialTimePickerView.setContentDescription(mHourPickerDescription + ": " + hours); - if (announce) { - mRadialTimePickerView.announceForAccessibility(mSelectHours); - } - } else { - int minutes = mRadialTimePickerView.getCurrentMinute(); - mRadialTimePickerView.setContentDescription(mMinutePickerDescription + ": " + minutes); - if (announce) { - mRadialTimePickerView.announceForAccessibility(mSelectMinutes); - } - } - - mHourView.setSelected(index == HOUR_INDEX); - mMinuteView.setSelected(index == MINUTE_INDEX); - } - - /** - * For keyboard mode, processes key events. - * - * @param keyCode the pressed key. - * - * @return true if the key was successfully processed, false otherwise. - */ - private boolean processKeyUp(int keyCode) { - if (keyCode == KeyEvent.KEYCODE_DEL) { - if (mInKbMode) { - if (!mTypedTimes.isEmpty()) { - int deleted = deleteLastTypedKey(); - String deletedKeyStr; - if (deleted == getAmOrPmKeyCode(AM)) { - deletedKeyStr = mAmText; - } else if (deleted == getAmOrPmKeyCode(PM)) { - deletedKeyStr = mPmText; - } else { - deletedKeyStr = String.format("%d", getValFromKeyCode(deleted)); - } - mRadialTimePickerView.announceForAccessibility( - String.format(mDeletedKeyFormat, deletedKeyStr)); - updateDisplay(true); - } - } - } else if (keyCode == KeyEvent.KEYCODE_0 || keyCode == KeyEvent.KEYCODE_1 - || keyCode == KeyEvent.KEYCODE_2 || keyCode == KeyEvent.KEYCODE_3 - || keyCode == KeyEvent.KEYCODE_4 || keyCode == KeyEvent.KEYCODE_5 - || keyCode == KeyEvent.KEYCODE_6 || keyCode == KeyEvent.KEYCODE_7 - || keyCode == KeyEvent.KEYCODE_8 || keyCode == KeyEvent.KEYCODE_9 - || (!mIs24HourView && - (keyCode == getAmOrPmKeyCode(AM) || keyCode == getAmOrPmKeyCode(PM)))) { - if (!mInKbMode) { - if (mRadialTimePickerView == null) { - // Something's wrong, because time picker should definitely not be null. - Log.e(TAG, "Unable to initiate keyboard mode, TimePicker was null."); - return true; - } - mTypedTimes.clear(); - tryStartingKbMode(keyCode); - return true; - } - // We're already in keyboard mode. - if (addKeyIfLegal(keyCode)) { - updateDisplay(false); - } - return true; - } - return false; - } - - /** - * Try to start keyboard mode with the specified key. - * - * @param keyCode The key to use as the first press. Keyboard mode will not be started if the - * key is not legal to start with. Or, pass in -1 to get into keyboard mode without a starting - * key. - */ - private void tryStartingKbMode(int keyCode) { - if (keyCode == -1 || addKeyIfLegal(keyCode)) { - mInKbMode = true; - onValidationChanged(false); - updateDisplay(false); - mRadialTimePickerView.setInputEnabled(false); - } - } - - private boolean addKeyIfLegal(int keyCode) { - // If we're in 24hour mode, we'll need to check if the input is full. If in AM/PM mode, - // we'll need to see if AM/PM have been typed. - if ((mIs24HourView && mTypedTimes.size() == 4) || - (!mIs24HourView && isTypedTimeFullyLegal())) { - return false; - } - - mTypedTimes.add(keyCode); - if (!isTypedTimeLegalSoFar()) { - deleteLastTypedKey(); - return false; - } - - int val = getValFromKeyCode(keyCode); - mRadialTimePickerView.announceForAccessibility(String.format("%d", val)); - // Automatically fill in 0's if AM or PM was legally entered. - if (isTypedTimeFullyLegal()) { - if (!mIs24HourView && mTypedTimes.size() <= 3) { - mTypedTimes.add(mTypedTimes.size() - 1, KeyEvent.KEYCODE_0); - mTypedTimes.add(mTypedTimes.size() - 1, KeyEvent.KEYCODE_0); - } - onValidationChanged(true); - } - - return true; + public static String[] getAmPmStrings(Context context) { + String[] result = new String[2]; + LocaleData d = LocaleData.get(context.getResources().getConfiguration().locale); + result[0] = d.amPm[0].length() > 2 ? d.narrowAm : d.amPm[0]; + result[1] = d.amPm[1].length() > 2 ? d.narrowPm : d.amPm[1]; + return result; } - - /** - * Traverse the tree to see if the keys that have been typed so far are legal as is, - * or may become legal as more keys are typed (excluding backspace). - */ - private boolean isTypedTimeLegalSoFar() { - Node node = mLegalTimesTree; - for (int keyCode : mTypedTimes) { - node = node.canReach(keyCode); - if (node == null) { - return false; - } - } - return true; - } - - /** - * Check if the time that has been typed so far is completely legal, as is. - */ - private boolean isTypedTimeFullyLegal() { - if (mIs24HourView) { - // For 24-hour mode, the time is legal if the hours and minutes are each legal. Note: - // getEnteredTime() will ONLY call isTypedTimeFullyLegal() when NOT in 24hour mode. - int[] values = getEnteredTime(null); - return (values[0] >= 0 && values[1] >= 0 && values[1] < 60); - } else { - // For AM/PM mode, the time is legal if it contains an AM or PM, as those can only be - // legally added at specific times based on the tree's algorithm. - return (mTypedTimes.contains(getAmOrPmKeyCode(AM)) || - mTypedTimes.contains(getAmOrPmKeyCode(PM))); - } - } - - private int deleteLastTypedKey() { - int deleted = mTypedTimes.remove(mTypedTimes.size() - 1); - if (!isTypedTimeFullyLegal()) { - onValidationChanged(false); - } - return deleted; - } - - /** - * Get out of keyboard mode. If there is nothing in typedTimes, revert to TimePicker's time. - */ - private void finishKbMode() { - mInKbMode = false; - if (!mTypedTimes.isEmpty()) { - int values[] = getEnteredTime(null); - mRadialTimePickerView.setCurrentHour(values[0]); - mRadialTimePickerView.setCurrentMinute(values[1]); - if (!mIs24HourView) { - mRadialTimePickerView.setAmOrPm(values[2]); - } - mTypedTimes.clear(); - } - updateDisplay(false); - mRadialTimePickerView.setInputEnabled(true); - } - - /** - * Update the hours, minutes, and AM/PM displays with the typed times. If the typedTimes is - * empty, either show an empty display (filled with the placeholder text), or update from the - * timepicker's values. - * - * @param allowEmptyDisplay if true, then if the typedTimes is empty, use the placeholder text. - * Otherwise, revert to the timepicker's values. - */ - private void updateDisplay(boolean allowEmptyDisplay) { - if (!allowEmptyDisplay && mTypedTimes.isEmpty()) { - int hour = mRadialTimePickerView.getCurrentHour(); - int minute = mRadialTimePickerView.getCurrentMinute(); - updateHeaderHour(hour, true); - updateHeaderMinute(minute); - if (!mIs24HourView) { - updateAmPmDisplay(hour < 12 ? AM : PM); - } - setCurrentItemShowing(mRadialTimePickerView.getCurrentItemShowing(), true, true); - onValidationChanged(true); - } else { - boolean[] enteredZeros = {false, false}; - int[] values = getEnteredTime(enteredZeros); - String hourFormat = enteredZeros[0] ? "%02d" : "%2d"; - String minuteFormat = (enteredZeros[1]) ? "%02d" : "%2d"; - String hourStr = (values[0] == -1) ? mDoublePlaceholderText : - String.format(hourFormat, values[0]).replace(' ', mPlaceholderText); - String minuteStr = (values[1] == -1) ? mDoublePlaceholderText : - String.format(minuteFormat, values[1]).replace(' ', mPlaceholderText); - mHourView.setText(hourStr); - mHourView.setSelected(false); - mMinuteView.setText(minuteStr); - mMinuteView.setSelected(false); - if (!mIs24HourView) { - updateAmPmDisplay(values[2]); - } - } - } - - private int getValFromKeyCode(int keyCode) { - switch (keyCode) { - case KeyEvent.KEYCODE_0: - return 0; - case KeyEvent.KEYCODE_1: - return 1; - case KeyEvent.KEYCODE_2: - return 2; - case KeyEvent.KEYCODE_3: - return 3; - case KeyEvent.KEYCODE_4: - return 4; - case KeyEvent.KEYCODE_5: - return 5; - case KeyEvent.KEYCODE_6: - return 6; - case KeyEvent.KEYCODE_7: - return 7; - case KeyEvent.KEYCODE_8: - return 8; - case KeyEvent.KEYCODE_9: - return 9; - default: - return -1; - } - } - - /** - * Get the currently-entered time, as integer values of the hours and minutes typed. - * - * @param enteredZeros A size-2 boolean array, which the caller should initialize, and which - * may then be used for the caller to know whether zeros had been explicitly entered as either - * hours of minutes. This is helpful for deciding whether to show the dashes, or actual 0's. - * - * @return A size-3 int array. The first value will be the hours, the second value will be the - * minutes, and the third will be either AM or PM. - */ - private int[] getEnteredTime(boolean[] enteredZeros) { - int amOrPm = -1; - int startIndex = 1; - if (!mIs24HourView && isTypedTimeFullyLegal()) { - int keyCode = mTypedTimes.get(mTypedTimes.size() - 1); - if (keyCode == getAmOrPmKeyCode(AM)) { - amOrPm = AM; - } else if (keyCode == getAmOrPmKeyCode(PM)){ - amOrPm = PM; - } - startIndex = 2; - } - int minute = -1; - int hour = -1; - for (int i = startIndex; i <= mTypedTimes.size(); i++) { - int val = getValFromKeyCode(mTypedTimes.get(mTypedTimes.size() - i)); - if (i == startIndex) { - minute = val; - } else if (i == startIndex+1) { - minute += 10 * val; - if (enteredZeros != null && val == 0) { - enteredZeros[1] = true; - } - } else if (i == startIndex+2) { - hour = val; - } else if (i == startIndex+3) { - hour += 10 * val; - if (enteredZeros != null && val == 0) { - enteredZeros[0] = true; - } - } - } - - return new int[] { hour, minute, amOrPm }; - } - - /** - * Get the keycode value for AM and PM in the current language. - */ - private int getAmOrPmKeyCode(int amOrPm) { - // Cache the codes. - if (mAmKeyCode == -1 || mPmKeyCode == -1) { - // Find the first character in the AM/PM text that is unique. - KeyCharacterMap kcm = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); - char amChar; - char pmChar; - for (int i = 0; i < Math.max(mAmText.length(), mPmText.length()); i++) { - amChar = mAmText.toLowerCase(mCurrentLocale).charAt(i); - pmChar = mPmText.toLowerCase(mCurrentLocale).charAt(i); - if (amChar != pmChar) { - KeyEvent[] events = kcm.getEvents(new char[]{amChar, pmChar}); - // There should be 4 events: a down and up for both AM and PM. - if (events != null && events.length == 4) { - mAmKeyCode = events[0].getKeyCode(); - mPmKeyCode = events[2].getKeyCode(); - } else { - Log.e(TAG, "Unable to find keycodes for AM and PM."); - } - break; - } - } - } - if (amOrPm == AM) { - return mAmKeyCode; - } else if (amOrPm == PM) { - return mPmKeyCode; - } - - return -1; - } - - /** - * Create a tree for deciding what keys can legally be typed. - */ - private void generateLegalTimesTree() { - // Create a quick cache of numbers to their keycodes. - final int k0 = KeyEvent.KEYCODE_0; - final int k1 = KeyEvent.KEYCODE_1; - final int k2 = KeyEvent.KEYCODE_2; - final int k3 = KeyEvent.KEYCODE_3; - final int k4 = KeyEvent.KEYCODE_4; - final int k5 = KeyEvent.KEYCODE_5; - final int k6 = KeyEvent.KEYCODE_6; - final int k7 = KeyEvent.KEYCODE_7; - final int k8 = KeyEvent.KEYCODE_8; - final int k9 = KeyEvent.KEYCODE_9; - - // The root of the tree doesn't contain any numbers. - mLegalTimesTree = new Node(); - if (mIs24HourView) { - // We'll be re-using these nodes, so we'll save them. - Node minuteFirstDigit = new Node(k0, k1, k2, k3, k4, k5); - Node minuteSecondDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); - // The first digit must be followed by the second digit. - minuteFirstDigit.addChild(minuteSecondDigit); - - // The first digit may be 0-1. - Node firstDigit = new Node(k0, k1); - mLegalTimesTree.addChild(firstDigit); - - // When the first digit is 0-1, the second digit may be 0-5. - Node secondDigit = new Node(k0, k1, k2, k3, k4, k5); - firstDigit.addChild(secondDigit); - // We may now be followed by the first minute digit. E.g. 00:09, 15:58. - secondDigit.addChild(minuteFirstDigit); - - // When the first digit is 0-1, and the second digit is 0-5, the third digit may be 6-9. - Node thirdDigit = new Node(k6, k7, k8, k9); - // The time must now be finished. E.g. 0:55, 1:08. - secondDigit.addChild(thirdDigit); - - // When the first digit is 0-1, the second digit may be 6-9. - secondDigit = new Node(k6, k7, k8, k9); - firstDigit.addChild(secondDigit); - // We must now be followed by the first minute digit. E.g. 06:50, 18:20. - secondDigit.addChild(minuteFirstDigit); - - // The first digit may be 2. - firstDigit = new Node(k2); - mLegalTimesTree.addChild(firstDigit); - - // When the first digit is 2, the second digit may be 0-3. - secondDigit = new Node(k0, k1, k2, k3); - firstDigit.addChild(secondDigit); - // We must now be followed by the first minute digit. E.g. 20:50, 23:09. - secondDigit.addChild(minuteFirstDigit); - - // When the first digit is 2, the second digit may be 4-5. - secondDigit = new Node(k4, k5); - firstDigit.addChild(secondDigit); - // We must now be followd by the last minute digit. E.g. 2:40, 2:53. - secondDigit.addChild(minuteSecondDigit); - - // The first digit may be 3-9. - firstDigit = new Node(k3, k4, k5, k6, k7, k8, k9); - mLegalTimesTree.addChild(firstDigit); - // We must now be followed by the first minute digit. E.g. 3:57, 8:12. - firstDigit.addChild(minuteFirstDigit); - } else { - // We'll need to use the AM/PM node a lot. - // Set up AM and PM to respond to "a" and "p". - Node ampm = new Node(getAmOrPmKeyCode(AM), getAmOrPmKeyCode(PM)); - - // The first hour digit may be 1. - Node firstDigit = new Node(k1); - mLegalTimesTree.addChild(firstDigit); - // We'll allow quick input of on-the-hour times. E.g. 1pm. - firstDigit.addChild(ampm); - - // When the first digit is 1, the second digit may be 0-2. - Node secondDigit = new Node(k0, k1, k2); - firstDigit.addChild(secondDigit); - // Also for quick input of on-the-hour times. E.g. 10pm, 12am. - secondDigit.addChild(ampm); - - // When the first digit is 1, and the second digit is 0-2, the third digit may be 0-5. - Node thirdDigit = new Node(k0, k1, k2, k3, k4, k5); - secondDigit.addChild(thirdDigit); - // The time may be finished now. E.g. 1:02pm, 1:25am. - thirdDigit.addChild(ampm); - - // When the first digit is 1, the second digit is 0-2, and the third digit is 0-5, - // the fourth digit may be 0-9. - Node fourthDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); - thirdDigit.addChild(fourthDigit); - // The time must be finished now. E.g. 10:49am, 12:40pm. - fourthDigit.addChild(ampm); - - // When the first digit is 1, and the second digit is 0-2, the third digit may be 6-9. - thirdDigit = new Node(k6, k7, k8, k9); - secondDigit.addChild(thirdDigit); - // The time must be finished now. E.g. 1:08am, 1:26pm. - thirdDigit.addChild(ampm); - - // When the first digit is 1, the second digit may be 3-5. - secondDigit = new Node(k3, k4, k5); - firstDigit.addChild(secondDigit); - - // When the first digit is 1, and the second digit is 3-5, the third digit may be 0-9. - thirdDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); - secondDigit.addChild(thirdDigit); - // The time must be finished now. E.g. 1:39am, 1:50pm. - thirdDigit.addChild(ampm); - - // The hour digit may be 2-9. - firstDigit = new Node(k2, k3, k4, k5, k6, k7, k8, k9); - mLegalTimesTree.addChild(firstDigit); - // We'll allow quick input of on-the-hour-times. E.g. 2am, 5pm. - firstDigit.addChild(ampm); - - // When the first digit is 2-9, the second digit may be 0-5. - secondDigit = new Node(k0, k1, k2, k3, k4, k5); - firstDigit.addChild(secondDigit); - - // When the first digit is 2-9, and the second digit is 0-5, the third digit may be 0-9. - thirdDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); - secondDigit.addChild(thirdDigit); - // The time must be finished now. E.g. 2:57am, 9:30pm. - thirdDigit.addChild(ampm); - } - } - - /** - * Simple node class to be used for traversal to check for legal times. - * mLegalKeys represents the keys that can be typed to get to the node. - * mChildren are the children that can be reached from this node. - */ - private class Node { - private int[] mLegalKeys; - private ArrayList<Node> mChildren; - - public Node(int... legalKeys) { - mLegalKeys = legalKeys; - mChildren = new ArrayList<Node>(); - } - - public void addChild(Node child) { - mChildren.add(child); - } - - public boolean containsKey(int key) { - for (int i = 0; i < mLegalKeys.length; i++) { - if (mLegalKeys[i] == key) { - return true; - } - } - return false; - } - - public Node canReach(int key) { - if (mChildren == null) { - return null; - } - for (Node child : mChildren) { - if (child.containsKey(key)) { - return child; - } - } - return null; - } - } - - private final View.OnKeyListener mKeyListener = new View.OnKeyListener() { - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - if (event.getAction() == KeyEvent.ACTION_UP) { - return processKeyUp(keyCode); - } - return false; - } - }; - - private final View.OnFocusChangeListener mFocusListener = new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View v, boolean hasFocus) { - if (!hasFocus && mInKbMode && isTypedTimeFullyLegal()) { - finishKbMode(); - - if (mOnTimeChangedListener != null) { - mOnTimeChangedListener.onTimeChanged(mDelegator, - mRadialTimePickerView.getCurrentHour(), - mRadialTimePickerView.getCurrentMinute()); - } - } - } - }; } diff --git a/core/java/android/widget/Toast.java b/core/java/android/widget/Toast.java index dd165ae..be4cdc1 100644 --- a/core/java/android/widget/Toast.java +++ b/core/java/android/widget/Toast.java @@ -235,6 +235,14 @@ public class Toast { public int getYOffset() { return mTN.mY; } + + /** + * Gets the LayoutParams for the Toast window. + * @hide + */ + public WindowManager.LayoutParams getWindowParams() { + return mTN.mParams; + } /** * Make a standard toast that just contains a text view. diff --git a/core/java/android/widget/Toolbar.java b/core/java/android/widget/Toolbar.java index 384e461..f90d64a 100644 --- a/core/java/android/widget/Toolbar.java +++ b/core/java/android/widget/Toolbar.java @@ -66,7 +66,9 @@ import java.util.List; * <li><em>A navigation button.</em> This may be an Up arrow, navigation menu toggle, close, * collapse, done or another glyph of the app's choosing. This button should always be used * to access other navigational destinations within the container of the Toolbar and - * its signified content or otherwise leave the current context signified by the Toolbar.</li> + * its signified content or otherwise leave the current context signified by the Toolbar. + * The navigation button is vertically aligned within the Toolbar's + * {@link android.R.styleable#View_minHeight minimum height}, if set.</li> * <li><em>A branded logo image.</em> This may extend to the height of the bar and can be * arbitrarily wide.</li> * <li><em>A title and subtitle.</em> The title should be a signpost for the Toolbar's current @@ -82,8 +84,9 @@ import java.util.List; * <li><em>An {@link ActionMenuView action menu}.</em> The menu of actions will pin to the * end of the Toolbar offering a few * <a href="http://developer.android.com/design/patterns/actionbar.html#ActionButtons"> - * frequent, important or typical</a> actions along with an optional overflow menu for - * additional actions.</li> + * frequent, important or typical</a> actions along with an optional overflow menu for + * additional actions. Action buttons are vertically aligned within the Toolbar's + * {@link android.R.styleable#View_minHeight minimum height}, if set.</li> * </ul> * </p> * @@ -101,6 +104,7 @@ public class Toolbar extends ViewGroup { private ImageView mLogoView; private Drawable mCollapseIcon; + private CharSequence mCollapseDescription; private ImageButton mCollapseButtonView; View mExpandedActionView; @@ -235,6 +239,7 @@ public class Toolbar extends ViewGroup { } mCollapseIcon = a.getDrawable(R.styleable.Toolbar_collapseIcon); + mCollapseDescription = a.getText(R.styleable.Toolbar_collapseContentDescription); final CharSequence title = a.getText(R.styleable.Toolbar_title); if (!TextUtils.isEmpty(title)) { @@ -995,6 +1000,7 @@ public class Toolbar extends ViewGroup { if (mCollapseButtonView == null) { mCollapseButtonView = new ImageButton(getContext(), null, 0, mNavButtonStyle); mCollapseButtonView.setImageDrawable(mCollapseIcon); + mCollapseButtonView.setContentDescription(mCollapseDescription); final LayoutParams lp = generateDefaultLayoutParams(); lp.gravity = Gravity.START | (mButtonGravity & Gravity.VERTICAL_GRAVITY_MASK); lp.mViewType = LayoutParams.EXPANDED; diff --git a/core/java/android/widget/YearPickerView.java b/core/java/android/widget/YearPickerView.java index 2bf07f9..24ed7ce 100644 --- a/core/java/android/widget/YearPickerView.java +++ b/core/java/android/widget/YearPickerView.java @@ -18,7 +18,6 @@ package android.widget; import android.content.Context; import android.content.res.Resources; -import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; @@ -33,10 +32,15 @@ import com.android.internal.R; */ class YearPickerView extends ListView implements AdapterView.OnItemClickListener, OnDateChangedListener { + private final Calendar mMinDate = Calendar.getInstance(); + private final Calendar mMaxDate = Calendar.getInstance(); + + private final YearAdapter mAdapter; + private final int mViewSize; + private final int mChildSize; + private DatePickerController mController; - private YearAdapter mAdapter; - private int mViewSize; - private int mChildSize; + private int mSelectedPosition = -1; private int mYearSelectedCircleColor; @@ -72,15 +76,23 @@ class YearPickerView extends ListView implements AdapterView.OnItemClickListener setOnItemClickListener(this); setDividerHeight(0); + + mAdapter = new YearAdapter(getContext(), R.layout.year_label_text_view); + setAdapter(mAdapter); + } + + public void setRange(Calendar min, Calendar max) { + mMinDate.setTimeInMillis(min.getTimeInMillis()); + mMaxDate.setTimeInMillis(max.getTimeInMillis()); + + updateAdapterData(); } public void init(DatePickerController controller) { mController = controller; mController.registerOnDateChangedListener(this); - mAdapter = new YearAdapter(getContext(), R.layout.year_label_text_view); updateAdapterData(); - setAdapter(mAdapter); onDateChanged(); } @@ -98,8 +110,9 @@ class YearPickerView extends ListView implements AdapterView.OnItemClickListener private void updateAdapterData() { mAdapter.clear(); - final int maxYear = mController.getMaxYear(); - for (int year = mController.getMinYear(); year <= maxYear; year++) { + + final int maxYear = mMaxDate.get(Calendar.YEAR); + for (int year = mMinDate.get(Calendar.YEAR); year <= maxYear; year++) { mAdapter.add(year); } } @@ -173,12 +186,13 @@ class YearPickerView extends ListView implements AdapterView.OnItemClickListener updateAdapterData(); mAdapter.notifyDataSetChanged(); postSetSelectionCentered( - mController.getSelectedDay().get(Calendar.YEAR) - mController.getMinYear()); + mController.getSelectedDay().get(Calendar.YEAR) - mMinDate.get(Calendar.YEAR)); } @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) { event.setFromIndex(0); event.setToIndex(0); diff --git a/core/java/com/android/internal/app/AlertController.java b/core/java/com/android/internal/app/AlertController.java index 0183e45..35e03c3 100644 --- a/core/java/com/android/internal/app/AlertController.java +++ b/core/java/com/android/internal/app/AlertController.java @@ -26,7 +26,6 @@ import android.content.DialogInterface; import android.content.res.TypedArray; import android.database.Cursor; import android.graphics.drawable.Drawable; -import android.os.Build; import android.os.Handler; import android.os.Message; import android.text.TextUtils; @@ -38,9 +37,12 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; +import android.view.ViewParent; +import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowInsets; import android.view.WindowManager; +import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.ArrayAdapter; @@ -449,11 +451,11 @@ public class AlertController { } private void setupView() { - final LinearLayout contentPanel = (LinearLayout) mWindow.findViewById(R.id.contentPanel); + final ViewGroup contentPanel = (ViewGroup) mWindow.findViewById(R.id.contentPanel); setupContent(contentPanel); final boolean hasButtons = setupButtons(); - final LinearLayout topPanel = (LinearLayout) mWindow.findViewById(R.id.topPanel); + final ViewGroup topPanel = (ViewGroup) mWindow.findViewById(R.id.topPanel); final TypedArray a = mContext.obtainStyledAttributes( null, R.styleable.AlertDialog, R.attr.alertDialogStyle, 0); final boolean hasTitle = setupTitle(topPanel); @@ -521,13 +523,13 @@ public class AlertController { a.recycle(); } - private boolean setupTitle(LinearLayout topPanel) { + private boolean setupTitle(ViewGroup topPanel) { boolean hasTitle = true; if (mCustomTitleView != null) { // Add the custom title view directly to the topPanel layout - LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); + LayoutParams lp = new LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); topPanel.addView(mCustomTitleView, 0, lp); @@ -571,7 +573,7 @@ public class AlertController { return hasTitle; } - private void setupContent(LinearLayout contentPanel) { + private void setupContent(ViewGroup contentPanel) { mScrollView = (ScrollView) mWindow.findViewById(R.id.scrollView); mScrollView.setFocusable(false); @@ -588,14 +590,77 @@ public class AlertController { mScrollView.removeView(mMessageView); if (mListView != null) { - contentPanel.removeView(mWindow.findViewById(R.id.scrollView)); - contentPanel.addView(mListView, - new LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); - contentPanel.setLayoutParams(new LinearLayout.LayoutParams(MATCH_PARENT, 0, 1.0f)); + final ViewGroup scrollParent = (ViewGroup) mScrollView.getParent(); + final int childIndex = scrollParent.indexOfChild(mScrollView); + scrollParent.removeViewAt(childIndex); + scrollParent.addView(mListView, childIndex, + new LayoutParams(MATCH_PARENT, MATCH_PARENT)); } else { contentPanel.setVisibility(View.GONE); } } + + // Set up scroll indicators (if present). + final View indicatorUp = mWindow.findViewById(R.id.scrollIndicatorUp); + final View indicatorDown = mWindow.findViewById(R.id.scrollIndicatorDown); + if (indicatorUp != null || indicatorDown != null) { + if (mMessage != null) { + // We're just showing the ScrollView, set up listener. + mScrollView.setOnScrollChangeListener(new View.OnScrollChangeListener() { + @Override + public void onScrollChange(View v, int scrollX, int scrollY, + int oldScrollX, int oldScrollY) { + manageScrollIndicators(v, indicatorUp, indicatorDown); + } + }); + // Set up the indicators following layout. + mScrollView.post(new Runnable() { + @Override + public void run() { + manageScrollIndicators(mScrollView, indicatorUp, indicatorDown); + } + }); + + } else if (mListView != null) { + // We're just showing the AbsListView, set up listener. + mListView.setOnScrollListener(new AbsListView.OnScrollListener() { + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + // That's cool, I guess? + } + + @Override + public void onScroll(AbsListView v, int firstVisibleItem, + int visibleItemCount, int totalItemCount) { + manageScrollIndicators(v, indicatorUp, indicatorDown); + } + }); + // Set up the indicators following layout. + mListView.post(new Runnable() { + @Override + public void run() { + manageScrollIndicators(mListView, indicatorUp, indicatorDown); + } + }); + } else { + // We don't have any content to scroll, remove the indicators. + if (indicatorUp != null) { + contentPanel.removeView(indicatorUp); + } + if (indicatorDown != null) { + contentPanel.removeView(indicatorDown); + } + } + } + } + + private static void manageScrollIndicators(View v, View upIndicator, View downIndicator) { + if (upIndicator != null) { + upIndicator.setVisibility(v.canScrollVertically(-1) ? View.VISIBLE : View.INVISIBLE); + } + if (downIndicator != null) { + downIndicator.setVisibility(v.canScrollVertically(1) ? View.VISIBLE : View.INVISIBLE); + } } private boolean setupButtons() { diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index 5267811..64bd6b6 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -16,13 +16,21 @@ package com.android.internal.app; +import android.app.Activity; +import android.content.ComponentName; import android.content.Intent; +import android.content.IntentSender; +import android.content.pm.ActivityInfo; import android.os.Bundle; import android.os.Parcelable; import android.util.Log; +import android.util.Slog; public class ChooserActivity extends ResolverActivity { + private static final String TAG = "ChooserActivity"; + private Bundle mReplacementExtras; + private IntentSender mChosenComponentSender; @Override protected void onCreate(Bundle savedInstanceState) { @@ -60,21 +68,45 @@ public class ChooserActivity extends ResolverActivity { initialIntents[i] = in; } } + mChosenComponentSender = intent.getParcelableExtra( + Intent.EXTRA_CHOSEN_COMPONENT_INTENT_SENDER); setSafeForwardingMode(true); super.onCreate(savedInstanceState, target, title, defaultTitleRes, initialIntents, null, false); } - public Intent getReplacementIntent(String packageName, Intent defIntent) { + @Override + public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) { + Intent result = defIntent; if (mReplacementExtras != null) { - final Bundle replExtras = mReplacementExtras.getBundle(packageName); + final Bundle replExtras = mReplacementExtras.getBundle(aInfo.packageName); if (replExtras != null) { - final Intent result = new Intent(defIntent); + result = new Intent(defIntent); result.putExtras(replExtras); - return result; } } - return defIntent; + if (aInfo.name.equals(IntentForwarderActivity.FORWARD_INTENT_TO_USER_OWNER) + || aInfo.name.equals(IntentForwarderActivity.FORWARD_INTENT_TO_MANAGED_PROFILE)) { + result = Intent.createChooser(result, + getIntent().getCharSequenceExtra(Intent.EXTRA_TITLE)); + } + return result; + } + + @Override + public void onActivityStarted(Intent intent) { + if (mChosenComponentSender != null) { + final ComponentName target = intent.getComponent(); + if (target != null) { + final Intent fillIn = new Intent().putExtra(Intent.EXTRA_CHOSEN_COMPONENT, target); + try { + mChosenComponentSender.sendIntent(this, Activity.RESULT_OK, fillIn, null, null); + } catch (IntentSender.SendIntentException e) { + Slog.e(TAG, "Unable to launch supplied IntentSender to report " + + "the chosen component: " + e); + } + } + } } private void modifyTargetIntent(Intent in) { diff --git a/core/java/com/android/internal/app/IntentForwarderActivity.java b/core/java/com/android/internal/app/IntentForwarderActivity.java index 6e2f84a..9656a21 100644 --- a/core/java/com/android/internal/app/IntentForwarderActivity.java +++ b/core/java/com/android/internal/app/IntentForwarderActivity.java @@ -58,21 +58,22 @@ public class IntentForwarderActivity extends Activity { Intent intentReceived = getIntent(); String className = intentReceived.getComponent().getClassName(); - final UserHandle userDest; + final int targetUserId; final int userMessageId; if (className.equals(FORWARD_INTENT_TO_USER_OWNER)) { userMessageId = com.android.internal.R.string.forward_intent_to_owner; - userDest = UserHandle.OWNER; + targetUserId = UserHandle.USER_OWNER; } else if (className.equals(FORWARD_INTENT_TO_MANAGED_PROFILE)) { userMessageId = com.android.internal.R.string.forward_intent_to_work; - userDest = getManagedProfile(); + targetUserId = getManagedProfile(); } else { Slog.wtf(TAG, IntentForwarderActivity.class.getName() + " cannot be called directly"); userMessageId = -1; - userDest = null; + targetUserId = UserHandle.USER_NULL; } - if (userDest == null) { // This covers the case where there is no managed profile. + if (targetUserId == UserHandle.USER_NULL) { + // This covers the case where there is no managed profile. finish(); return; } @@ -83,31 +84,24 @@ public class IntentForwarderActivity extends Activity { newIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT |Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP); int callingUserId = getUserId(); - IPackageManager ipm = AppGlobals.getPackageManager(); - String resolvedType = newIntent.resolveTypeIfNeeded(getContentResolver()); - boolean canForward = false; - Intent selector = newIntent.getSelector(); - if (selector == null) { - selector = newIntent; - } - try { - canForward = ipm.canForwardTo(selector, resolvedType, callingUserId, - userDest.getIdentifier()); - } catch (RemoteException e) { - Slog.e(TAG, "PackageManagerService is dead?"); - } - if (canForward) { - newIntent.setContentUserHint(callingUserId); + + if (canForward(newIntent, targetUserId)) { + if (newIntent.getAction().equals(Intent.ACTION_CHOOSER)) { + Intent innerIntent = (Intent) newIntent.getParcelableExtra(Intent.EXTRA_INTENT); + innerIntent.setContentUserHint(callingUserId); + } else { + newIntent.setContentUserHint(callingUserId); + } final android.content.pm.ResolveInfo ri = getPackageManager().resolveActivityAsUser( - newIntent, MATCH_DEFAULT_ONLY, userDest.getIdentifier()); + newIntent, MATCH_DEFAULT_ONLY, targetUserId); // Only show a disclosure if this is a normal (non-OS) app final boolean shouldShowDisclosure = !UserHandle.isSameApp(ri.activityInfo.applicationInfo.uid, Process.SYSTEM_UID); try { - startActivityAsCaller(newIntent, null, userDest.getIdentifier()); + startActivityAsCaller(newIntent, null, targetUserId); } catch (RuntimeException e) { int launchedFromUid = -1; String launchedFromPackage = "?"; @@ -129,26 +123,55 @@ public class IntentForwarderActivity extends Activity { } } else { Slog.wtf(TAG, "the intent: " + newIntent + "cannot be forwarded from user " - + callingUserId + " to user " + userDest.getIdentifier()); + + callingUserId + " to user " + targetUserId); } finish(); } + boolean canForward(Intent intent, int targetUserId) { + IPackageManager ipm = AppGlobals.getPackageManager(); + if (intent.getAction().equals(Intent.ACTION_CHOOSER)) { + // The EXTRA_INITIAL_INTENTS may not be allowed to be forwarded. + if (intent.hasExtra(Intent.EXTRA_INITIAL_INTENTS)) { + Slog.wtf(TAG, "An chooser intent with extra initial intents cannot be forwarded to" + + " a different user"); + return false; + } + if (intent.hasExtra(Intent.EXTRA_REPLACEMENT_EXTRAS)) { + Slog.wtf(TAG, "A chooser intent with replacement extras cannot be forwarded to a" + + " different user"); + return false; + } + intent = (Intent) intent.getParcelableExtra(Intent.EXTRA_INTENT); + } + String resolvedType = intent.resolveTypeIfNeeded(getContentResolver()); + if (intent.getSelector() != null) { + intent = intent.getSelector(); + } + try { + return ipm.canForwardTo(intent, resolvedType, getUserId(), + targetUserId); + } catch (RemoteException e) { + Slog.e(TAG, "PackageManagerService is dead?"); + return false; + } + } + /** - * Returns the managed profile for this device or null if there is no managed - * profile. + * Returns the userId of the managed profile for this device or UserHandle.USER_NULL if there is + * no managed profile. * * TODO: Remove the assumption that there is only one managed profile * on the device. */ - private UserHandle getManagedProfile() { + private int getManagedProfile() { UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE); List<UserInfo> relatedUsers = userManager.getProfiles(UserHandle.USER_OWNER); for (UserInfo userInfo : relatedUsers) { - if (userInfo.isManagedProfile()) return new UserHandle(userInfo.id); + if (userInfo.isManagedProfile()) return userInfo.id; } Slog.wtf(TAG, FORWARD_INTENT_TO_MANAGED_PROFILE + " has been called, but there is no managed profile"); - return null; + return UserHandle.USER_NULL; } } diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index 0062e2d..376db6e 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -74,6 +74,9 @@ import java.util.List; import java.util.Map; import java.util.Set; +import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR; +import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; + /** * This activity is displayed when the system attempts to start an Intent for * which there is more than one matching activity, allowing the user to decide @@ -97,6 +100,7 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic private int mMaxColumns; private int mLastSelected = ListView.INVALID_POSITION; private boolean mResolvingHome = false; + private int mProfileSwitchMessageId = -1; private UsageStatsManager mUsm; private Map<String, UsageStats> mStats; @@ -197,6 +201,11 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic List<ResolveInfo> rList, boolean alwaysUseOption) { setTheme(R.style.Theme_DeviceDefault_Resolver); super.onCreate(savedInstanceState); + + // Determine whether we should show that intent is forwarded + // from managed profile to owner or other way around. + setProfileSwitchMessageId(intent.getContentUserHint()); + try { mLaunchedFromUid = ActivityManagerNative.getDefault().getLaunchedFromUid( getActivityToken()); @@ -269,12 +278,15 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic mListView = (ListView) findViewById(R.id.resolver_list); mListView.setVisibility(View.GONE); } + // Prevent the Resolver window from becoming the top fullscreen window and thus from taking + // control of the system bars. + getWindow().clearFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR); final ResolverDrawerLayout rdl = (ResolverDrawerLayout) findViewById(R.id.contentPanel); if (rdl != null) { - rdl.setOnClickOutsideListener(new View.OnClickListener() { + rdl.setOnDismissedListener(new ResolverDrawerLayout.OnDismissedListener() { @Override - public void onClick(View v) { + public void onDismissed() { finish(); } }); @@ -314,6 +326,22 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic } } + private void setProfileSwitchMessageId(int contentUserHint) { + if (contentUserHint != UserHandle.USER_CURRENT && + contentUserHint != UserHandle.myUserId()) { + UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE); + UserInfo originUserInfo = userManager.getUserInfo(contentUserHint); + boolean originIsManaged = originUserInfo != null ? originUserInfo.isManagedProfile() + : false; + boolean targetIsManaged = userManager.isManagedProfile(); + if (originIsManaged && !targetIsManaged) { + mProfileSwitchMessageId = com.android.internal.R.string.forward_intent_to_owner; + } else if (!originIsManaged && targetIsManaged) { + mProfileSwitchMessageId = com.android.internal.R.string.forward_intent_to_work; + } + } + } + /** * Turn on launch mode that is safe to use when forwarding intents received from * applications and running in system processes. This mode uses Activity.startActivityAsCaller @@ -523,7 +551,7 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic /** * Replace me in subclasses! */ - public Intent getReplacementIntent(String packageName, Intent defIntent) { + public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) { return defIntent; } @@ -636,12 +664,19 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic } public void safelyStartActivity(Intent intent) { + // If needed, show that intent is forwarded + // from managed profile to owner or other way around. + if (mProfileSwitchMessageId != -1) { + Toast.makeText(this, getString(mProfileSwitchMessageId), Toast.LENGTH_LONG).show(); + } if (!mSafeForwardingMode) { startActivity(intent); + onActivityStarted(intent); return; } try { startActivityAsCaller(intent, null, UserHandle.USER_NULL); + onActivityStarted(intent); } catch (RuntimeException e) { String launchedFromPackage; try { @@ -656,6 +691,10 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic } } + public void onActivityStarted(Intent intent) { + // Do nothing + } + void showAppDetails(ResolveInfo ri) { Intent in = new Intent().setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) .setData(Uri.fromParts("package", ri.activityInfo.packageName, null)) @@ -819,6 +858,11 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic } ResolveInfo ri = new ResolveInfo(); ri.activityInfo = ai; + UserManager userManager = + (UserManager) getSystemService(Context.USER_SERVICE); + if (userManager.isManagedProfile()) { + ri.noResourceId = true; + } if (ii instanceof LabeledIntent) { LabeledIntent li = (LabeledIntent)ii; ri.resolvePackageName = li.getSourcePackage(); @@ -926,7 +970,7 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic DisplayResolveInfo dri = filtered ? getItem(position) : mList.get(position); Intent intent = new Intent(dri.origIntent != null ? dri.origIntent : - getReplacementIntent(dri.ri.activityInfo.packageName, mIntent)); + getReplacementIntent(dri.ri.activityInfo, mIntent)); intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT |Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP); ActivityInfo ai = dri.ri.activityInfo; diff --git a/core/java/com/android/internal/app/ToolbarActionBar.java b/core/java/com/android/internal/app/ToolbarActionBar.java index 4410f25..34b9dcb 100644 --- a/core/java/com/android/internal/app/ToolbarActionBar.java +++ b/core/java/com/android/internal/app/ToolbarActionBar.java @@ -40,7 +40,6 @@ import com.android.internal.widget.ToolbarWidgetWrapper; import java.util.ArrayList; public class ToolbarActionBar extends ActionBar { - private Toolbar mToolbar; private DecorToolbar mDecorToolbar; private boolean mToolbarMenuPrepared; private Window.Callback mWindowCallback; @@ -66,7 +65,6 @@ public class ToolbarActionBar extends ActionBar { }; public ToolbarActionBar(Toolbar toolbar, CharSequence title, Window.Callback windowCallback) { - mToolbar = toolbar; mDecorToolbar = new ToolbarWidgetWrapper(toolbar, false); mWindowCallback = new ToolbarCallbackWrapper(windowCallback); mDecorToolbar.setWindowCallback(mWindowCallback); @@ -91,8 +89,8 @@ public class ToolbarActionBar extends ActionBar { @Override public void setCustomView(int resId) { - final LayoutInflater inflater = LayoutInflater.from(mToolbar.getContext()); - setCustomView(inflater.inflate(resId, mToolbar, false)); + final LayoutInflater inflater = LayoutInflater.from(mDecorToolbar.getContext()); + setCustomView(inflater.inflate(resId, mDecorToolbar.getViewGroup(), false)); } @Override @@ -132,17 +130,17 @@ public class ToolbarActionBar extends ActionBar { @Override public void setElevation(float elevation) { - mToolbar.setElevation(elevation); + mDecorToolbar.getViewGroup().setElevation(elevation); } @Override public float getElevation() { - return mToolbar.getElevation(); + return mDecorToolbar.getViewGroup().getElevation(); } @Override public Context getThemedContext() { - return mToolbar.getContext(); + return mDecorToolbar.getContext(); } @Override @@ -152,12 +150,12 @@ public class ToolbarActionBar extends ActionBar { @Override public void setHomeAsUpIndicator(Drawable indicator) { - mToolbar.setNavigationIcon(indicator); + mDecorToolbar.setNavigationIcon(indicator); } @Override public void setHomeAsUpIndicator(int resId) { - mToolbar.setNavigationIcon(resId); + mDecorToolbar.setNavigationIcon(resId); } @Override @@ -280,7 +278,7 @@ public class ToolbarActionBar extends ActionBar { @Override public void setBackgroundDrawable(@Nullable Drawable d) { - mToolbar.setBackground(d); + mDecorToolbar.setBackgroundDrawable(d); } @Override @@ -290,12 +288,12 @@ public class ToolbarActionBar extends ActionBar { @Override public CharSequence getTitle() { - return mToolbar.getTitle(); + return mDecorToolbar.getTitle(); } @Override public CharSequence getSubtitle() { - return mToolbar.getSubtitle(); + return mDecorToolbar.getSubtitle(); } @Override @@ -389,44 +387,44 @@ public class ToolbarActionBar extends ActionBar { @Override public int getHeight() { - return mToolbar.getHeight(); + return mDecorToolbar.getHeight(); } @Override public void show() { // TODO: Consider a better transition for this. // Right now use no automatic transition so that the app can supply one if desired. - mToolbar.setVisibility(View.VISIBLE); + mDecorToolbar.setVisibility(View.VISIBLE); } @Override public void hide() { // TODO: Consider a better transition for this. // Right now use no automatic transition so that the app can supply one if desired. - mToolbar.setVisibility(View.GONE); + mDecorToolbar.setVisibility(View.GONE); } @Override public boolean isShowing() { - return mToolbar.getVisibility() == View.VISIBLE; + return mDecorToolbar.getVisibility() == View.VISIBLE; } @Override public boolean openOptionsMenu() { - return mToolbar.showOverflowMenu(); + return mDecorToolbar.showOverflowMenu(); } @Override public boolean invalidateOptionsMenu() { - mToolbar.removeCallbacks(mMenuInvalidator); - mToolbar.postOnAnimation(mMenuInvalidator); + mDecorToolbar.getViewGroup().removeCallbacks(mMenuInvalidator); + mDecorToolbar.getViewGroup().postOnAnimation(mMenuInvalidator); return true; } @Override public boolean collapseActionView() { - if (mToolbar.hasExpandedActionView()) { - mToolbar.collapseActionView(); + if (mDecorToolbar.hasExpandedActionView()) { + mDecorToolbar.collapseActionView(); return true; } return false; @@ -434,10 +432,10 @@ public class ToolbarActionBar extends ActionBar { void populateOptionsMenu() { if (!mMenuCallbackSet) { - mToolbar.setMenuCallbacks(new ActionMenuPresenterCallback(), new MenuBuilderCallback()); + mDecorToolbar.setMenuCallbacks(new ActionMenuPresenterCallback(), new MenuBuilderCallback()); mMenuCallbackSet = true; } - final Menu menu = mToolbar.getMenu(); + final Menu menu = mDecorToolbar.getMenu(); final MenuBuilder mb = menu instanceof MenuBuilder ? (MenuBuilder) menu : null; if (mb != null) { mb.stopDispatchingItemsChanged(); @@ -518,7 +516,7 @@ public class ToolbarActionBar extends ActionBar { } mClosingActionMenu = true; - mToolbar.dismissPopupMenus(); + mDecorToolbar.dismissPopupMenus(); if (mWindowCallback != null) { mWindowCallback.onPanelClosed(Window.FEATURE_ACTION_BAR, menu); } @@ -536,7 +534,7 @@ public class ToolbarActionBar extends ActionBar { @Override public void onMenuModeChange(MenuBuilder menu) { if (mWindowCallback != null) { - if (mToolbar.isOverflowMenuShowing()) { + if (mDecorToolbar.isOverflowMenuShowing()) { mWindowCallback.onPanelClosed(Window.FEATURE_ACTION_BAR, menu); } else if (mWindowCallback.onPreparePanel(Window.FEATURE_OPTIONS_PANEL, null, menu)) { diff --git a/core/java/com/android/internal/app/WindowDecorActionBar.java b/core/java/com/android/internal/app/WindowDecorActionBar.java index 2377c22..d95f0e5 100644 --- a/core/java/com/android/internal/app/WindowDecorActionBar.java +++ b/core/java/com/android/internal/app/WindowDecorActionBar.java @@ -496,7 +496,7 @@ public class WindowDecorActionBar extends ActionBar implements mOverlayLayout.setHideOnContentScrollEnabled(false); mContextView.killMode(); - ActionModeImpl mode = new ActionModeImpl(callback); + ActionModeImpl mode = new ActionModeImpl(mContextView.getContext(), callback); if (mode.dispatchOnCreate()) { mode.invalidate(); mContextView.initForMode(mode); @@ -876,7 +876,7 @@ public class WindowDecorActionBar extends ActionBar implements currentTheme.resolveAttribute(com.android.internal.R.attr.actionBarWidgetTheme, outValue, true); final int targetThemeRes = outValue.resourceId; - + if (targetThemeRes != 0 && mContext.getThemeResId() != targetThemeRes) { mThemedContext = new ContextThemeWrapper(mContext, targetThemeRes); } else { @@ -885,7 +885,7 @@ public class WindowDecorActionBar extends ActionBar implements } return mThemedContext; } - + @Override public boolean isTitleTruncated() { return mDecorToolbar != null && mDecorToolbar.isTitleTruncated(); @@ -933,23 +933,26 @@ public class WindowDecorActionBar extends ActionBar implements } /** - * @hide + * @hide */ public class ActionModeImpl extends ActionMode implements MenuBuilder.Callback { + private final Context mActionModeContext; + private final MenuBuilder mMenu; + private ActionMode.Callback mCallback; - private MenuBuilder mMenu; private WeakReference<View> mCustomView; - - public ActionModeImpl(ActionMode.Callback callback) { + + public ActionModeImpl(Context context, ActionMode.Callback callback) { + mActionModeContext = context; mCallback = callback; - mMenu = new MenuBuilder(getThemedContext()) + mMenu = new MenuBuilder(context) .setDefaultShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); mMenu.setCallback(this); } @Override public MenuInflater getMenuInflater() { - return new MenuInflater(getThemedContext()); + return new MenuInflater(mActionModeContext); } @Override @@ -1042,7 +1045,7 @@ public class WindowDecorActionBar extends ActionBar implements public CharSequence getSubtitle() { return mContextView.getSubtitle(); } - + @Override public void setTitleOptionalHint(boolean titleOptional) { super.setTitleOptionalHint(titleOptional); diff --git a/core/java/com/android/internal/http/multipart/FilePart.java b/core/java/com/android/internal/http/multipart/FilePart.java index bfcda00..45e4be6 100644 --- a/core/java/com/android/internal/http/multipart/FilePart.java +++ b/core/java/com/android/internal/http/multipart/FilePart.java @@ -51,9 +51,14 @@ import org.apache.commons.logging.LogFactory; * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a> * @author <a href="mailto:oleg@ural.ru">Oleg Kalnichevski</a> * - * @since 2.0 + * @since 2.0 * + * @deprecated Please use {@link java.net.URLConnection} and friends instead. + * The Apache HTTP client is no longer maintained and may be removed in a future + * release. Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> + * for further details. */ +@Deprecated public class FilePart extends PartBase { /** Default content encoding of file attachments. */ diff --git a/core/java/com/android/internal/http/multipart/MultipartEntity.java b/core/java/com/android/internal/http/multipart/MultipartEntity.java index 2c5e7f6..5319251 100644 --- a/core/java/com/android/internal/http/multipart/MultipartEntity.java +++ b/core/java/com/android/internal/http/multipart/MultipartEntity.java @@ -80,7 +80,13 @@ import org.apache.commons.logging.LogFactory; * </pre> * * @since 3.0 + * + * @deprecated Please use {@link java.net.URLConnection} and friends instead. + * The Apache HTTP client is no longer maintained and may be removed in a future + * release. Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> + * for further details. */ +@Deprecated public class MultipartEntity extends AbstractHttpEntity { private static final Log log = LogFactory.getLog(MultipartEntity.class); diff --git a/core/java/com/android/internal/http/multipart/Part.java b/core/java/com/android/internal/http/multipart/Part.java index cb1b546..1d66dc6 100644 --- a/core/java/com/android/internal/http/multipart/Part.java +++ b/core/java/com/android/internal/http/multipart/Part.java @@ -48,7 +48,13 @@ import org.apache.commons.logging.LogFactory; * @author <a href="mailto:oleg@ural.ru">Oleg Kalnichevski</a> * * @since 2.0 + * + * @deprecated Please use {@link java.net.URLConnection} and friends instead. + * The Apache HTTP client is no longer maintained and may be removed in a future + * release. Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> + * for further details. */ +@Deprecated public abstract class Part { /** Log object for this class. */ diff --git a/core/java/com/android/internal/http/multipart/StringPart.java b/core/java/com/android/internal/http/multipart/StringPart.java index c98257e..73d0f90 100644 --- a/core/java/com/android/internal/http/multipart/StringPart.java +++ b/core/java/com/android/internal/http/multipart/StringPart.java @@ -46,7 +46,13 @@ import org.apache.commons.logging.LogFactory; * @author <a href="mailto:oleg@ural.ru">Oleg Kalnichevski</a> * * @since 2.0 + * + * @deprecated Please use {@link java.net.URLConnection} and friends instead. + * The Apache HTTP client is no longer maintained and may be removed in a future + * release. Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> + * for further details. */ +@Deprecated public class StringPart extends PartBase { /** Log object for this class. */ diff --git a/core/java/com/android/internal/inputmethod/InputMethodUtils.java b/core/java/com/android/internal/inputmethod/InputMethodUtils.java index ac915d1..183527c 100644 --- a/core/java/com/android/internal/inputmethod/InputMethodUtils.java +++ b/core/java/com/android/internal/inputmethod/InputMethodUtils.java @@ -16,6 +16,8 @@ package com.android.internal.inputmethod; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.AppOpsManager; import android.content.ContentResolver; import android.content.Context; @@ -34,7 +36,9 @@ import android.view.textservice.SpellCheckerInfo; import android.view.textservice.TextServicesManager; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; @@ -115,8 +119,8 @@ public class InputMethodUtils { } /** - * @deprecated Use {@link Locale} returned from - * {@link #getFallbackLocaleForDefaultIme(ArrayList)} instead. + * @deprecated Use {@link #isSystemImeThatHasSubtypeOf(InputMethodInfo, Context, boolean, + * Locale, boolean, String)} instead. */ @Deprecated public static boolean isSystemImeThatHasEnglishKeyboardSubtype(InputMethodInfo imi) { @@ -126,25 +130,60 @@ public class InputMethodUtils { return containsSubtypeOf(imi, ENGLISH_LOCALE.getLanguage(), SUBTYPE_MODE_KEYBOARD); } + private static boolean isSystemImeThatHasSubtypeOf(final InputMethodInfo imi, + final Context context, final boolean checkDefaultAttribute, + @Nullable final Locale requiredLocale, final boolean checkCountry, + final String requiredSubtypeMode) { + if (!isSystemIme(imi)) { + return false; + } + if (checkDefaultAttribute && !imi.isDefault(context)) { + return false; + } + if (!containsSubtypeOf(imi, requiredLocale, checkCountry, requiredSubtypeMode)) { + return false; + } + return true; + } + + @Nullable public static Locale getFallbackLocaleForDefaultIme(final ArrayList<InputMethodInfo> imis, final Context context) { + // At first, find the fallback locale from the IMEs that are declared as "default" in the + // current locale. Note that IME developers can declare an IME as "default" only for + // some particular locales but "not default" for other locales. for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) { for (int i = 0; i < imis.size(); ++i) { - final InputMethodInfo imi = imis.get(i); - if (isSystemIme(imi) && imi.isDefault(context) && - containsSubtypeOf(imi, fallbackLocale, false /* ignoreCountry */, - SUBTYPE_MODE_KEYBOARD)) { + if (isSystemImeThatHasSubtypeOf(imis.get(i), context, + true /* checkDefaultAttribute */, fallbackLocale, + true /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) { + return fallbackLocale; + } + } + } + // If no fallback locale is found in the above condition, find fallback locales regardless + // of the "default" attribute as a last resort. + for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) { + for (int i = 0; i < imis.size(); ++i) { + if (isSystemImeThatHasSubtypeOf(imis.get(i), context, + false /* checkDefaultAttribute */, fallbackLocale, + true /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) { return fallbackLocale; } } } + Slog.w(TAG, "Found no fallback locale. imis=" + Arrays.toString(imis.toArray())); return null; } - private static boolean isSystemAuxilialyImeThatHasAutomaticSubtype(InputMethodInfo imi) { + private static boolean isSystemAuxilialyImeThatHasAutomaticSubtype(final InputMethodInfo imi, + final Context context, final boolean checkDefaultAttribute) { if (!isSystemIme(imi)) { return false; } + if (checkDefaultAttribute && !imi.isDefault(context)) { + return false; + } if (!imi.isAuxiliaryIme()) { return false; } @@ -166,98 +205,184 @@ public class InputMethodUtils { } } - public static ArrayList<InputMethodInfo> getDefaultEnabledImes( - Context context, boolean isSystemReady, ArrayList<InputMethodInfo> imis) { - // OK to store null in fallbackLocale because isImeThatHasSubtypeOf() is null-tolerant. - final Locale fallbackLocale = getFallbackLocaleForDefaultIme(imis, context); + private static final class InputMethodListBuilder { + // Note: We use LinkedHashSet instead of android.util.ArraySet because the enumeration + // order can have non-trivial effect in the call sites. + @NonNull + private final LinkedHashSet<InputMethodInfo> mInputMethodSet = new LinkedHashSet<>(); - if (!isSystemReady) { - final ArrayList<InputMethodInfo> retval = new ArrayList<>(); + public InputMethodListBuilder fillImes(final ArrayList<InputMethodInfo> imis, + final Context context, final boolean checkDefaultAttribute, + @Nullable final Locale locale, final boolean checkCountry, + final String requiredSubtypeMode) { for (int i = 0; i < imis.size(); ++i) { final InputMethodInfo imi = imis.get(i); - // TODO: We should check isAsciiCapable instead of relying on fallbackLocale. - if (isSystemIme(imi) && imi.isDefault(context) && - isImeThatHasSubtypeOf(imi, fallbackLocale, false /* ignoreCountry */, - SUBTYPE_MODE_KEYBOARD)) { - retval.add(imi); + if (isSystemImeThatHasSubtypeOf(imi, context, checkDefaultAttribute, locale, + checkCountry, requiredSubtypeMode)) { + mInputMethodSet.add(imi); } } - return retval; + return this; } - // OK to store null in fallbackLocale because isImeThatHasSubtypeOf() is null-tolerant. - final Locale systemLocale = getSystemLocaleFromContext(context); - // TODO: Use LinkedHashSet to simplify the code. - final ArrayList<InputMethodInfo> retval = new ArrayList<>(); - boolean systemLocaleKeyboardImeFound = false; - - // First, try to find IMEs with taking the system locale country into consideration. - for (int i = 0; i < imis.size(); ++i) { - final InputMethodInfo imi = imis.get(i); - if (!isSystemIme(imi) || !imi.isDefault(context)) { - continue; - } - final boolean isSystemLocaleKeyboardIme = isImeThatHasSubtypeOf(imi, systemLocale, - false /* ignoreCountry */, SUBTYPE_MODE_KEYBOARD); - // TODO: We should check isAsciiCapable instead of relying on fallbackLocale. - // TODO: Use LinkedHashSet to simplify the code. - if (isSystemLocaleKeyboardIme || - isImeThatHasSubtypeOf(imi, fallbackLocale, false /* ignoreCountry */, - SUBTYPE_MODE_ANY)) { - retval.add(imi); + // TODO: The behavior of InputMethodSubtype#overridesImplicitlyEnabledSubtype() should be + // documented more clearly. + public InputMethodListBuilder fillAuxiliaryImes(final ArrayList<InputMethodInfo> imis, + final Context context) { + // If one or more auxiliary input methods are available, OK to stop populating the list. + for (final InputMethodInfo imi : mInputMethodSet) { + if (imi.isAuxiliaryIme()) { + return this; + } } - systemLocaleKeyboardImeFound |= isSystemLocaleKeyboardIme; - } - - // System locale country doesn't match any IMEs, try to find IMEs in a country-agnostic - // way. - if (!systemLocaleKeyboardImeFound) { + boolean added = false; for (int i = 0; i < imis.size(); ++i) { final InputMethodInfo imi = imis.get(i); - if (!isSystemIme(imi) || !imi.isDefault(context)) { - continue; - } - if (isImeThatHasSubtypeOf(imi, fallbackLocale, false /* ignoreCountry */, - SUBTYPE_MODE_KEYBOARD)) { - // IMEs that have fallback locale are already added in the previous loop. We - // don't need to add them again here. - // TODO: Use LinkedHashSet to simplify the code. - continue; + if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context, + true /* checkDefaultAttribute */)) { + mInputMethodSet.add(imi); + added = true; } - if (isImeThatHasSubtypeOf(imi, systemLocale, true /* ignoreCountry */, - SUBTYPE_MODE_ANY)) { - retval.add(imi); + } + if (added) { + return this; + } + for (int i = 0; i < imis.size(); ++i) { + final InputMethodInfo imi = imis.get(i); + if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context, + false /* checkDefaultAttribute */)) { + mInputMethodSet.add(imi); } } + return this; } - // If one or more auxiliary input methods are available, OK to stop populating the list. - for (int i = 0; i < retval.size(); ++i) { - if (retval.get(i).isAuxiliaryIme()) { - return retval; - } + public boolean isEmpty() { + return mInputMethodSet.isEmpty(); } - for (int i = 0; i < imis.size(); ++i) { - final InputMethodInfo imi = imis.get(i); - if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi)) { - retval.add(imi); - } + + @NonNull + public ArrayList<InputMethodInfo> build() { + return new ArrayList<>(mInputMethodSet); } - return retval; } - public static boolean isImeThatHasSubtypeOf(final InputMethodInfo imi, - final Locale locale, final boolean ignoreCountry, final String mode) { - if (locale == null) { - return false; - } - return containsSubtypeOf(imi, locale, ignoreCountry, mode); + private static InputMethodListBuilder getMinimumKeyboardSetWithoutSystemLocale( + final ArrayList<InputMethodInfo> imis, final Context context, + @Nullable final Locale fallbackLocale) { + // Before the system becomes ready, we pick up at least one keyboard in the following order. + // The first user (device owner) falls into this category. + // 1. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: true + // 2. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: true + // 3. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: false + // 4. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: false + // TODO: We should check isAsciiCapable instead of relying on fallbackLocale. + + final InputMethodListBuilder builder = new InputMethodListBuilder(); + builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale, + true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); + if (!builder.isEmpty()) { + return builder; + } + builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale, + true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); + if (!builder.isEmpty()) { + return builder; + } + builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale, + false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); + if (!builder.isEmpty()) { + return builder; + } + builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale, + false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); + if (!builder.isEmpty()) { + return builder; + } + Slog.w(TAG, "No software keyboard is found. imis=" + Arrays.toString(imis.toArray()) + + " fallbackLocale=" + fallbackLocale); + return builder; + } + + private static InputMethodListBuilder getMinimumKeyboardSetWithSystemLocale( + final ArrayList<InputMethodInfo> imis, final Context context, + @Nullable final Locale systemLocale, @Nullable final Locale fallbackLocale) { + // Once the system becomes ready, we pick up at least one keyboard in the following order. + // Secondary users fall into this category in general. + // 1. checkDefaultAttribute: true, locale: systemLocale, checkCountry: true + // 2. checkDefaultAttribute: true, locale: systemLocale, checkCountry: false + // 3. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: true + // 4. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: false + // 5. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: true + // 6. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: false + // TODO: We should check isAsciiCapable instead of relying on fallbackLocale. + + final InputMethodListBuilder builder = new InputMethodListBuilder(); + builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale, + true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); + if (!builder.isEmpty()) { + return builder; + } + builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale, + false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); + if (!builder.isEmpty()) { + return builder; + } + builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale, + true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); + if (!builder.isEmpty()) { + return builder; + } + builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale, + false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); + if (!builder.isEmpty()) { + return builder; + } + builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale, + true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); + if (!builder.isEmpty()) { + return builder; + } + builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale, + false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); + if (!builder.isEmpty()) { + return builder; + } + Slog.w(TAG, "No software keyboard is found. imis=" + Arrays.toString(imis.toArray()) + + " systemLocale=" + systemLocale + " fallbackLocale=" + fallbackLocale); + return builder; + } + + public static ArrayList<InputMethodInfo> getDefaultEnabledImes(final Context context, + final boolean isSystemReady, final ArrayList<InputMethodInfo> imis) { + final Locale fallbackLocale = getFallbackLocaleForDefaultIme(imis, context); + if (!isSystemReady) { + // When the system is not ready, the system locale is not stable and reliable. Hence + // we will pick up IMEs that support software keyboard based on the fallback locale. + // Also pick up suitable IMEs regardless of the software keyboard support. + // (e.g. Voice IMEs) + return getMinimumKeyboardSetWithoutSystemLocale(imis, context, fallbackLocale) + .fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale, + true /* checkCountry */, SUBTYPE_MODE_ANY) + .build(); + } + + // When the system is ready, we will primarily rely on the system locale, but also keep + // relying on the fallback locale as a last resort. + // Also pick up suitable IMEs regardless of the software keyboard support (e.g. Voice IMEs), + // then pick up suitable auxiliary IMEs when necessary (e.g. Voice IMEs with "automatic" + // subtype) + final Locale systemLocale = getSystemLocaleFromContext(context); + return getMinimumKeyboardSetWithSystemLocale(imis, context, systemLocale, fallbackLocale) + .fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale, + true /* checkCountry */, SUBTYPE_MODE_ANY) + .fillAuxiliaryImes(imis, context) + .build(); } /** - * @deprecated Use {@link #isSystemIme(InputMethodInfo)} and - * {@link InputMethodInfo#isDefault(Context)} and - * {@link #isImeThatHasSubtypeOf(InputMethodInfo, Locale, boolean, String))} instead. + * @deprecated Use {@link #isSystemImeThatHasSubtypeOf(InputMethodInfo, Context, boolean, + * Locale, boolean, String)} instead. */ @Deprecated public static boolean isValidSystemDefaultIme( @@ -285,22 +410,25 @@ public class InputMethodUtils { } public static boolean containsSubtypeOf(final InputMethodInfo imi, - final Locale locale, final boolean ignoreCountry, final String mode) { + @Nullable final Locale locale, final boolean checkCountry, final String mode) { + if (locale == null) { + return false; + } final int N = imi.getSubtypeCount(); for (int i = 0; i < N; ++i) { final InputMethodSubtype subtype = imi.getSubtypeAt(i); - if (ignoreCountry) { - final Locale subtypeLocale = new Locale(getLanguageFromLocaleString( - subtype.getLocale())); - if (!subtypeLocale.getLanguage().equals(locale.getLanguage())) { - continue; - } - } else { + if (checkCountry) { // TODO: Use {@link Locale#toLanguageTag()} and // {@link Locale#forLanguageTag(languageTag)} instead. if (!TextUtils.equals(subtype.getLocale(), locale.toString())) { continue; } + } else { + final Locale subtypeLocale = new Locale(getLanguageFromLocaleString( + subtype.getLocale())); + if (!subtypeLocale.getLanguage().equals(locale.getLanguage())) { + continue; + } } if (mode == SUBTYPE_MODE_ANY || TextUtils.isEmpty(mode) || mode.equalsIgnoreCase(subtype.getMode())) { @@ -465,19 +593,9 @@ public class InputMethodUtils { return applicableSubtypes; } - private static List<InputMethodSubtype> getEnabledInputMethodSubtypeList( - Context context, InputMethodInfo imi, List<InputMethodSubtype> enabledSubtypes, - boolean allowsImplicitlySelectedSubtypes) { - if (allowsImplicitlySelectedSubtypes && enabledSubtypes.isEmpty()) { - enabledSubtypes = InputMethodUtils.getImplicitlyApplicableSubtypesLocked( - context.getResources(), imi); - } - return InputMethodSubtype.sort(context, 0, imi, enabledSubtypes); - } - /** * Returns the language component of a given locale string. - * TODO: Use {@link Locale#toLanguageTag()} and {@link Locale#forLanguageTag(languageTag)} + * TODO: Use {@link Locale#toLanguageTag()} and {@link Locale#forLanguageTag(String)} */ public static String getLanguageFromLocaleString(String locale) { final int idx = locale.indexOf('_'); diff --git a/core/java/com/android/internal/os/ProcessCpuTracker.java b/core/java/com/android/internal/os/ProcessCpuTracker.java index 86f580d..b5338df 100644 --- a/core/java/com/android/internal/os/ProcessCpuTracker.java +++ b/core/java/com/android/internal/os/ProcessCpuTracker.java @@ -23,8 +23,11 @@ import android.os.Process; import android.os.StrictMode; import android.os.SystemClock; import android.util.Slog; + import com.android.internal.util.FastPrintWriter; +import libcore.io.IoUtils; + import java.io.File; import java.io.FileInputStream; import java.io.PrintWriter; @@ -325,7 +328,12 @@ public class ProcessCpuTracker { mBaseIdleTime = idletime; } - mCurPids = collectStats("/proc", -1, mFirst, mCurPids, mProcStats); + final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads(); + try { + mCurPids = collectStats("/proc", -1, mFirst, mCurPids, mProcStats); + } finally { + StrictMode.setThreadPolicy(savedPolicy); + } final float[] loadAverages = mLoadAverageData; if (Process.readProcFile("/proc/loadavg", LOAD_AVERAGE_FORMAT, @@ -847,12 +855,7 @@ public class ProcessCpuTracker { } catch (java.io.FileNotFoundException e) { } catch (java.io.IOException e) { } finally { - if (is != null) { - try { - is.close(); - } catch (java.io.IOException e) { - } - } + IoUtils.closeQuietly(is); StrictMode.setThreadPolicy(savedPolicy); } return null; diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index 57472f8..a3c0db4 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -42,5 +42,6 @@ oneway interface IStatusBar void toggleRecentApps(); void preloadRecentApps(); void cancelPreloadRecentApps(); + void showScreenPinningRequest(); } diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl index 8794d31..5e610ed 100644 --- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl @@ -43,6 +43,7 @@ interface IStatusBarService void onPanelRevealed(); void onPanelHidden(); void onNotificationClick(String key); + void onNotificationActionClick(String key, int actionIndex); void onNotificationError(String pkg, String tag, int id, int uid, int initialPid, String message, int userId); void onClearAllNotifications(int userId); @@ -50,7 +51,7 @@ interface IStatusBarService void onNotificationVisibilityChanged( in String[] newlyVisibleKeys, in String[] noLongerVisibleKeys); void onNotificationExpansionChanged(in String key, in boolean userAction, in boolean expanded); - void setSystemUiVisibility(int vis, int mask); + void setSystemUiVisibility(int vis, int mask, String cause); void setWindowState(int window, int state); void showRecentApps(boolean triggeredFromAltTab); diff --git a/core/java/com/android/internal/util/MemInfoReader.java b/core/java/com/android/internal/util/MemInfoReader.java index 5f240f7..b71fa06 100644 --- a/core/java/com/android/internal/util/MemInfoReader.java +++ b/core/java/com/android/internal/util/MemInfoReader.java @@ -34,40 +34,65 @@ public final class MemInfoReader { } } + /** + * Total amount of RAM available to the kernel. + */ public long getTotalSize() { return mInfos[Debug.MEMINFO_TOTAL] * 1024; } + /** + * Amount of RAM that is not being used for anything. + */ public long getFreeSize() { return mInfos[Debug.MEMINFO_FREE] * 1024; } + /** + * Amount of RAM that the kernel is being used for caches, not counting caches + * that are mapped in to processes. + */ public long getCachedSize() { - return mInfos[Debug.MEMINFO_CACHED] * 1024; + return getCachedSizeKb() * 1024; } + /** + * Amount of RAM that is in use by the kernel for actual allocations. + */ + public long getKernelUsedSize() { + return getKernelUsedSizeKb() * 1024; + } + + /** + * Total amount of RAM available to the kernel. + */ public long getTotalSizeKb() { return mInfos[Debug.MEMINFO_TOTAL]; } + /** + * Amount of RAM that is not being used for anything. + */ public long getFreeSizeKb() { return mInfos[Debug.MEMINFO_FREE]; } + /** + * Amount of RAM that the kernel is being used for caches, not counting caches + * that are mapped in to processes. + */ public long getCachedSizeKb() { - return mInfos[Debug.MEMINFO_CACHED]; - } - - public long getBuffersSizeKb() { - return mInfos[Debug.MEMINFO_BUFFERS]; + return mInfos[Debug.MEMINFO_BUFFERS] + + mInfos[Debug.MEMINFO_CACHED] - mInfos[Debug.MEMINFO_MAPPED]; } - public long getShmemSizeKb() { - return mInfos[Debug.MEMINFO_SHMEM]; - } - - public long getSlabSizeKb() { - return mInfos[Debug.MEMINFO_SLAB]; + /** + * Amount of RAM that is in use by the kernel for actual allocations. + */ + public long getKernelUsedSizeKb() { + return mInfos[Debug.MEMINFO_SHMEM] + mInfos[Debug.MEMINFO_SLAB] + + mInfos[Debug.MEMINFO_VM_ALLOC_USED] + mInfos[Debug.MEMINFO_PAGE_TABLES] + + mInfos[Debug.MEMINFO_KERNEL_STACK]; } public long getSwapTotalSizeKb() { @@ -81,4 +106,8 @@ public final class MemInfoReader { public long getZramTotalSizeKb() { return mInfos[Debug.MEMINFO_ZRAM_TOTAL]; } + + public long[] getRawInfo() { + return mInfos; + } } diff --git a/core/java/com/android/internal/util/StateMachine.java b/core/java/com/android/internal/util/StateMachine.java index d26f79e..7ad3470 100644 --- a/core/java/com/android/internal/util/StateMachine.java +++ b/core/java/com/android/internal/util/StateMachine.java @@ -1940,13 +1940,19 @@ public class StateMachine { * @param args */ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - pw.println(getName() + ":"); - pw.println(" total records=" + getLogRecCount()); + pw.println(this.toString()); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(getName() + ":\n"); + sb.append(" total records=" + getLogRecCount() + "\n"); for (int i = 0; i < getLogRecSize(); i++) { - pw.printf(" rec[%d]: %s\n", i, getLogRec(i).toString()); - pw.flush(); + sb.append(" rec[" + i + "]: " + getLogRec(i).toString() + "\n"); } - pw.println("curState=" + getCurrentState().getName()); + sb.append("curState=" + getCurrentState().getName()); + return sb.toString(); } /** diff --git a/core/java/com/android/internal/view/BaseIWindow.java b/core/java/com/android/internal/view/BaseIWindow.java index 50a7a5e..993ab58 100644 --- a/core/java/com/android/internal/view/BaseIWindow.java +++ b/core/java/com/android/internal/view/BaseIWindow.java @@ -102,4 +102,8 @@ public class BaseIWindow extends IWindow.Stub { @Override public void doneAnimating() { } + + @Override + public void dispatchWindowShown() { + } } diff --git a/core/java/com/android/internal/view/RootViewSurfaceTaker.java b/core/java/com/android/internal/view/RootViewSurfaceTaker.java index 9c1b558..433ec73 100644 --- a/core/java/com/android/internal/view/RootViewSurfaceTaker.java +++ b/core/java/com/android/internal/view/RootViewSurfaceTaker.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.android.internal.view; import android.view.InputQueue; @@ -10,4 +25,5 @@ public interface RootViewSurfaceTaker { void setSurfaceFormat(int format); void setSurfaceKeepScreenOn(boolean keepOn); InputQueue.Callback willYouTakeTheInputQueue(); + void onRootViewScrollYChanged(int scrollY); } diff --git a/core/java/com/android/internal/view/menu/MenuPopupHelper.java b/core/java/com/android/internal/view/menu/MenuPopupHelper.java index 40f58e9..99bb1ac 100644 --- a/core/java/com/android/internal/view/menu/MenuPopupHelper.java +++ b/core/java/com/android/internal/view/menu/MenuPopupHelper.java @@ -54,6 +54,7 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On private final boolean mOverflowOnly; private final int mPopupMaxWidth; private final int mPopupStyleAttr; + private final int mPopupStyleRes; private View mAnchorView; private ListPopupWindow mPopup; @@ -73,21 +74,27 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On private int mDropDownGravity = Gravity.NO_GRAVITY; public MenuPopupHelper(Context context, MenuBuilder menu) { - this(context, menu, null, false, com.android.internal.R.attr.popupMenuStyle); + this(context, menu, null, false, com.android.internal.R.attr.popupMenuStyle, 0); } public MenuPopupHelper(Context context, MenuBuilder menu, View anchorView) { - this(context, menu, anchorView, false, com.android.internal.R.attr.popupMenuStyle); + this(context, menu, anchorView, false, com.android.internal.R.attr.popupMenuStyle, 0); } public MenuPopupHelper(Context context, MenuBuilder menu, View anchorView, boolean overflowOnly, int popupStyleAttr) { + this(context, menu, anchorView, overflowOnly, popupStyleAttr, 0); + } + + public MenuPopupHelper(Context context, MenuBuilder menu, View anchorView, + boolean overflowOnly, int popupStyleAttr, int popupStyleRes) { mContext = context; mInflater = LayoutInflater.from(context); mMenu = menu; mAdapter = new MenuAdapter(mMenu); mOverflowOnly = overflowOnly; mPopupStyleAttr = popupStyleAttr; + mPopupStyleRes = popupStyleRes; final Resources res = context.getResources(); mPopupMaxWidth = Math.max(res.getDisplayMetrics().widthPixels / 2, @@ -122,7 +129,7 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On } public boolean tryShow() { - mPopup = new ListPopupWindow(mContext, null, mPopupStyleAttr); + mPopup = new ListPopupWindow(mContext, null, mPopupStyleAttr, mPopupStyleRes); mPopup.setOnDismissListener(this); mPopup.setOnItemClickListener(this); mPopup.setAdapter(mAdapter); diff --git a/core/java/com/android/internal/widget/ActionBarContextView.java b/core/java/com/android/internal/widget/ActionBarContextView.java index 062a9b1..7c671e8 100644 --- a/core/java/com/android/internal/widget/ActionBarContextView.java +++ b/core/java/com/android/internal/widget/ActionBarContextView.java @@ -241,7 +241,7 @@ public class ActionBarContextView extends AbsActionBarView implements AnimatorLi if (!mSplitActionBar) { menu.addMenuPresenter(mActionMenuPresenter, mPopupContext); mMenuView = (ActionMenuView) mActionMenuPresenter.getMenuView(this); - mMenuView.setBackgroundDrawable(null); + mMenuView.setBackground(null); addView(mMenuView, layoutParams); } else { // Allow full screen width in split mode. diff --git a/core/java/com/android/internal/widget/ActionBarView.java b/core/java/com/android/internal/widget/ActionBarView.java index 91e5330..654d08b 100644 --- a/core/java/com/android/internal/widget/ActionBarView.java +++ b/core/java/com/android/internal/widget/ActionBarView.java @@ -19,8 +19,6 @@ package com.android.internal.widget; import android.animation.LayoutTransition; import android.app.ActionBar; import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; @@ -29,9 +27,7 @@ import android.os.Parcelable; import android.text.Layout; import android.text.TextUtils; import android.util.AttributeSet; -import android.util.TypedValue; import android.view.CollapsibleActionView; -import android.view.ContextThemeWrapper; import android.view.Gravity; import android.view.LayoutInflater; import android.view.Menu; @@ -111,10 +107,10 @@ public class ActionBarView extends AbsActionBarView implements DecorToolbar { private int mProgressBarPadding; private int mItemPadding; - private int mTitleStyleRes; - private int mSubtitleStyleRes; - private int mProgressStyle; - private int mIndeterminateProgressStyle; + private final int mTitleStyleRes; + private final int mSubtitleStyleRes; + private final int mProgressStyle; + private final int mIndeterminateProgressStyle; private boolean mUserTitle; private boolean mIncludeTabs; @@ -1345,6 +1341,22 @@ public class ActionBarView extends AbsActionBarView implements DecorToolbar { updateHomeAccessibility(mUpGoerFive.isEnabled()); } + @Override + public void setMenuCallbacks(MenuPresenter.Callback presenterCallback, + MenuBuilder.Callback menuBuilderCallback) { + if (mActionMenuPresenter != null) { + mActionMenuPresenter.setCallback(presenterCallback); + } + if (mOptionsMenu != null) { + mOptionsMenu.setCallback(menuBuilderCallback); + } + } + + @Override + public Menu getMenu() { + return mOptionsMenu; + } + static class SavedState extends BaseSavedState { int expandedMenuItemId; boolean isOverflowOpen; diff --git a/core/java/com/android/internal/widget/DecorToolbar.java b/core/java/com/android/internal/widget/DecorToolbar.java index f89f0b7..fb413b5 100644 --- a/core/java/com/android/internal/widget/DecorToolbar.java +++ b/core/java/com/android/internal/widget/DecorToolbar.java @@ -27,6 +27,8 @@ import android.view.ViewGroup; import android.view.Window; import android.widget.AdapterView; import android.widget.SpinnerAdapter; + +import com.android.internal.view.menu.MenuBuilder; import com.android.internal.view.menu.MenuPresenter; /** @@ -93,4 +95,11 @@ public interface DecorToolbar { void setDefaultNavigationIcon(Drawable icon); void saveHierarchyState(SparseArray<Parcelable> toolbarStates); void restoreHierarchyState(SparseArray<Parcelable> toolbarStates); + void setBackgroundDrawable(Drawable d); + int getHeight(); + void setVisibility(int visible); + int getVisibility(); + void setMenuCallbacks(MenuPresenter.Callback presenterCallback, + MenuBuilder.Callback menuBuilderCallback); + Menu getMenu(); } diff --git a/core/java/com/android/internal/widget/ExploreByTouchHelper.java b/core/java/com/android/internal/widget/ExploreByTouchHelper.java index 11c4ca1..0e046cb 100644 --- a/core/java/com/android/internal/widget/ExploreByTouchHelper.java +++ b/core/java/com/android/internal/widget/ExploreByTouchHelper.java @@ -19,6 +19,7 @@ package com.android.internal.widget; import android.content.Context; import android.graphics.Rect; import android.os.Bundle; +import android.util.IntArray; import android.view.accessibility.*; import android.view.MotionEvent; import android.view.View; @@ -26,11 +27,9 @@ import android.view.ViewParent; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.accessibility.AccessibilityNodeProvider; -import java.util.LinkedList; -import java.util.List; - /** * ExploreByTouchHelper is a utility class for implementing accessibility * support in custom {@link android.view.View}s that represent a collection of View-like @@ -54,14 +53,20 @@ public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate { /** Default class name used for virtual views. */ private static final String DEFAULT_CLASS_NAME = View.class.getName(); - // Temporary, reusable data structures. - private final Rect mTempScreenRect = new Rect(); - private final Rect mTempParentRect = new Rect(); - private final Rect mTempVisibleRect = new Rect(); - private final int[] mTempGlobalRect = new int[2]; + /** Default bounds used to determine if the client didn't set any. */ + private static final Rect INVALID_PARENT_BOUNDS = new Rect( + Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE); + + // Lazily-created temporary data structures used when creating nodes. + private Rect mTempScreenRect; + private Rect mTempParentRect; + private int[] mTempGlobalRect; + + /** Lazily-created temporary data structure used to compute visibility. */ + private Rect mTempVisibleRect; - /** View's context **/ - private Context mContext; + /** Lazily-created temporary data structure used to obtain child IDs. */ + private IntArray mTempArray; /** System accessibility manager, used to check state and send events. */ private final AccessibilityManager mManager; @@ -69,6 +74,9 @@ public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate { /** View whose internal structure is exposed through this helper. */ private final View mView; + /** Context of the host view. **/ + private final Context mContext; + /** Node provider that handles creating nodes and performing actions. */ private ExploreByTouchNodeProvider mNodeProvider; @@ -328,11 +336,17 @@ public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate { onInitializeAccessibilityNodeInfo(mView, node); // Add the virtual descendants. - final LinkedList<Integer> virtualViewIds = new LinkedList<Integer>(); + if (mTempArray == null) { + mTempArray = new IntArray(); + } else { + mTempArray.clear(); + } + final IntArray virtualViewIds = mTempArray; getVisibleVirtualViews(virtualViewIds); - for (Integer childVirtualViewId : virtualViewIds) { - node.addChild(mView, childVirtualViewId); + final int N = virtualViewIds.size(); + for (int i = 0; i < N; i++) { + node.addChild(mView, virtualViewIds.get(i)); } return node; @@ -367,11 +381,17 @@ public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate { * @return An {@link AccessibilityNodeInfo} for the specified item. */ private AccessibilityNodeInfo createNodeForChild(int virtualViewId) { + ensureTempRects(); + final Rect tempParentRect = mTempParentRect; + final int[] tempGlobalRect = mTempGlobalRect; + final Rect tempScreenRect = mTempScreenRect; + final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(); // Ensure the client has good defaults. node.setEnabled(true); node.setClassName(DEFAULT_CLASS_NAME); + node.setBoundsInParent(INVALID_PARENT_BOUNDS); // Allow the client to populate the node. onPopulateNodeForVirtualView(virtualViewId, node); @@ -382,8 +402,8 @@ public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate { + "populateNodeForVirtualViewId()"); } - node.getBoundsInParent(mTempParentRect); - if (mTempParentRect.isEmpty()) { + node.getBoundsInParent(tempParentRect); + if (tempParentRect.equals(INVALID_PARENT_BOUNDS)) { throw new RuntimeException("Callbacks must set parent bounds in " + "populateNodeForVirtualViewId()"); } @@ -406,29 +426,35 @@ public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate { // Manage internal accessibility focus state. if (mFocusedVirtualViewId == virtualViewId) { node.setAccessibilityFocused(true); - node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS); + node.addAction(AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS); } else { node.setAccessibilityFocused(false); - node.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS); + node.addAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS); } // Set the visibility based on the parent bound. - if (intersectVisibleToUser(mTempParentRect)) { + if (intersectVisibleToUser(tempParentRect)) { node.setVisibleToUser(true); - node.setBoundsInParent(mTempParentRect); + node.setBoundsInParent(tempParentRect); } // Calculate screen-relative bound. - mView.getLocationOnScreen(mTempGlobalRect); - final int offsetX = mTempGlobalRect[0]; - final int offsetY = mTempGlobalRect[1]; - mTempScreenRect.set(mTempParentRect); - mTempScreenRect.offset(offsetX, offsetY); - node.setBoundsInScreen(mTempScreenRect); + mView.getLocationOnScreen(tempGlobalRect); + final int offsetX = tempGlobalRect[0]; + final int offsetY = tempGlobalRect[1]; + tempScreenRect.set(tempParentRect); + tempScreenRect.offset(offsetX, offsetY); + node.setBoundsInScreen(tempScreenRect); return node; } + private void ensureTempRects() { + mTempGlobalRect = new int[2]; + mTempParentRect = new Rect(); + mTempScreenRect = new Rect(); + } + private boolean performAction(int virtualViewId, int action, Bundle arguments) { switch (virtualViewId) { case View.NO_ID: @@ -446,13 +472,13 @@ public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate { switch (action) { case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: - return manageFocusForChild(virtualViewId, action, arguments); + return manageFocusForChild(virtualViewId, action); default: return onPerformActionForVirtualView(virtualViewId, action, arguments); } } - private boolean manageFocusForChild(int virtualViewId, int action, Bundle arguments) { + private boolean manageFocusForChild(int virtualViewId, int action) { switch (action) { case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: return requestAccessibilityFocus(virtualViewId); @@ -498,12 +524,16 @@ public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate { } // If no portion of the parent is visible, this view is not visible. - if (!mView.getLocalVisibleRect(mTempVisibleRect)) { + if (mTempVisibleRect == null) { + mTempVisibleRect = new Rect(); + } + final Rect tempVisibleRect = mTempVisibleRect; + if (!mView.getLocalVisibleRect(tempVisibleRect)) { return false; } // Check if the view intersects the visible portion of the parent. - return localRect.intersect(mTempVisibleRect); + return localRect.intersect(tempVisibleRect); } /** @@ -583,7 +613,7 @@ public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate { * * @param virtualViewIds The list to populate with visible items */ - protected abstract void getVisibleVirtualViews(List<Integer> virtualViewIds); + protected abstract void getVisibleVirtualViews(IntArray virtualViewIds); /** * Populates an {@link AccessibilityEvent} with information about the diff --git a/core/java/com/android/internal/widget/ILockSettingsObserver.aidl b/core/java/com/android/internal/widget/ILockSettingsObserver.aidl index 6c354d8..edf8f0e 100644 --- a/core/java/com/android/internal/widget/ILockSettingsObserver.aidl +++ b/core/java/com/android/internal/widget/ILockSettingsObserver.aidl @@ -18,5 +18,14 @@ package com.android.internal.widget; /** {@hide} */ oneway interface ILockSettingsObserver { + /** + * Called when a lock setting has changed. + * + * Note: Impementations of this should do as little work as possible, because this may be + * called synchronously while writing a setting. + * + * @param key the key of the setting that has changed or {@code null} if any may have changed. + * @param userId the user whose setting has changed. + */ void onLockSettingChanged(in String key, in int userId); } diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index d6885da..a4b8380 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -65,6 +65,13 @@ public class LockPatternUtils { private static final boolean DEBUG = false; /** + * If true, LockPatternUtils will cache its values in-process. While this leads to faster reads, + * it can cause problems because writes to to the settings are no longer synchronous + * across all processes. + */ + private static final boolean ENABLE_CLIENT_CACHE = false; + + /** * The maximum number of incorrect attempts before the user is prevented * from trying again for {@link #FAILED_ATTEMPT_TIMEOUT_MS}. */ @@ -207,8 +214,13 @@ public class LockPatternUtils { private ILockSettings getLockSettings() { if (mLockSettingsService == null) { - mLockSettingsService = LockPatternUtilsCache.getInstance( - ILockSettings.Stub.asInterface(ServiceManager.getService("lock_settings"))); + ILockSettings service = ILockSettings.Stub.asInterface( + ServiceManager.getService("lock_settings")); + if (ENABLE_CLIENT_CACHE) { + mLockSettingsService = LockPatternUtilsCache.getInstance(service); + } else { + mLockSettingsService = service; + } } return mLockSettingsService; } diff --git a/core/java/com/android/internal/widget/LockPatternUtilsCache.java b/core/java/com/android/internal/widget/LockPatternUtilsCache.java index 624f67c..a9524ff 100644 --- a/core/java/com/android/internal/widget/LockPatternUtilsCache.java +++ b/core/java/com/android/internal/widget/LockPatternUtilsCache.java @@ -18,7 +18,9 @@ package com.android.internal.widget; import android.os.IBinder; import android.os.RemoteException; +import android.os.UserHandle; import android.util.ArrayMap; +import android.util.Log; /** * A decorator for {@link ILockSettings} that caches the key-value responses in memory. @@ -28,9 +30,11 @@ import android.util.ArrayMap; */ public class LockPatternUtilsCache implements ILockSettings { - private static final String HAS_LOCK_PATTERN_CACHE_KEY + private static final String TAG = "LockPatternUtilsCache"; + + public static final String HAS_LOCK_PATTERN_CACHE_KEY = "LockPatternUtils.Cache.HasLockPatternCacheKey"; - private static final String HAS_LOCK_PASSWORD_CACHE_KEY + public static final String HAS_LOCK_PASSWORD_CACHE_KEY = "LockPatternUtils.Cache.HasLockPasswordCacheKey"; private static LockPatternUtilsCache sInstance; @@ -53,7 +57,7 @@ public class LockPatternUtilsCache implements ILockSettings { // ILockSettings - private LockPatternUtilsCache(ILockSettings service) { + public LockPatternUtilsCache(ILockSettings service) { mService = service; try { service.registerObserver(mObserver); @@ -186,6 +190,7 @@ public class LockPatternUtilsCache implements ILockSettings { // Caching private Object peekCache(String key, int userId) { + if (!validateUserId(userId)) return null; synchronized (mCache) { // Safe to reuse mCacheKey, because it is not stored in the map. return mCache.get(mCacheKey.set(key, userId)); @@ -193,6 +198,7 @@ public class LockPatternUtilsCache implements ILockSettings { } private void putCache(String key, int userId, Object value) { + if (!validateUserId(userId)) return; synchronized (mCache) { // Create a new key, because this will be stored in the map. mCache.put(new CacheKey().set(key, userId), value); @@ -200,9 +206,14 @@ public class LockPatternUtilsCache implements ILockSettings { } private void invalidateCache(String key, int userId) { + if (!validateUserId(userId)) return; synchronized (mCache) { - // Safe to reuse mCacheKey, because it is not stored in the map. - mCache.remove(mCacheKey.set(key, userId)); + if (key != null) { + // Safe to reuse mCacheKey, because it is not stored in the map. + mCache.remove(mCacheKey.set(key, userId)); + } else { + mCache.clear(); + } } } @@ -213,6 +224,14 @@ public class LockPatternUtilsCache implements ILockSettings { } }; + private final boolean validateUserId(int userId) { + if (userId < UserHandle.USER_OWNER) { + Log.e(TAG, "User " + userId + " not supported: Must be a concrete user."); + return false; + } + return true; + } + private static final class CacheKey { String key; int userId; diff --git a/core/java/com/android/internal/widget/ResolverDrawerLayout.java b/core/java/com/android/internal/widget/ResolverDrawerLayout.java index 375822f..25b4945 100644 --- a/core/java/com/android/internal/widget/ResolverDrawerLayout.java +++ b/core/java/com/android/internal/widget/ResolverDrawerLayout.java @@ -63,18 +63,22 @@ public class ResolverDrawerLayout extends ViewGroup { private float mCollapseOffset; private int mCollapsibleHeight; + private int mUncollapsibleHeight; private int mTopOffset; private boolean mIsDragging; private boolean mOpenOnClick; private boolean mOpenOnLayout; + private boolean mDismissOnScrollerFinished; private final int mTouchSlop; private final float mMinFlingVelocity; private final OverScroller mScroller; private final VelocityTracker mVelocityTracker; - private OnClickListener mClickOutsideListener; + private OnDismissedListener mOnDismissedListener; + private RunOnDismissedListener mRunOnDismissedListener; + private float mInitialTouchX; private float mInitialTouchY; private float mLastTouchY; @@ -143,8 +147,8 @@ public class ResolverDrawerLayout extends ViewGroup { return isSmallCollapsed() ? mMaxCollapsedHeightSmall : mMaxCollapsedHeight; } - public void setOnClickOutsideListener(OnClickListener listener) { - mClickOutsideListener = listener; + public void setOnDismissedListener(OnDismissedListener listener) { + mOnDismissedListener = listener; } @Override @@ -194,7 +198,7 @@ public class ResolverDrawerLayout extends ViewGroup { } if (mIsDragging) { - mScroller.abortAnimation(); + abortAnimation(); } return mIsDragging || mOpenOnClick; } @@ -213,12 +217,9 @@ public class ResolverDrawerLayout extends ViewGroup { mInitialTouchX = x; mInitialTouchY = mLastTouchY = y; mActivePointerId = ev.getPointerId(0); - if (findChildUnder(mInitialTouchX, mInitialTouchY) == null && - mClickOutsideListener != null) { - mIsDragging = handled = true; - } - handled |= mCollapsibleHeight > 0; - mScroller.abortAnimation(); + mIsDragging = findChildUnder(mInitialTouchX, mInitialTouchY) != null; + handled = (!mIsDragging && mOnDismissedListener != null) || mCollapsibleHeight > 0; + abortAnimation(); } break; @@ -264,11 +265,12 @@ public class ResolverDrawerLayout extends ViewGroup { break; case MotionEvent.ACTION_UP: { + final boolean wasDragging = mIsDragging; mIsDragging = false; - if (!mIsDragging && findChildUnder(mInitialTouchX, mInitialTouchY) == null && + if (!wasDragging && findChildUnder(mInitialTouchX, mInitialTouchY) == null && findChildUnder(ev.getX(), ev.getY()) == null) { - if (mClickOutsideListener != null) { - mClickOutsideListener.onClick(this); + if (mOnDismissedListener != null) { + dispatchOnDismissed(); resetTouch(); return true; } @@ -281,7 +283,13 @@ public class ResolverDrawerLayout extends ViewGroup { mVelocityTracker.computeCurrentVelocity(1000); final float yvel = mVelocityTracker.getYVelocity(mActivePointerId); if (Math.abs(yvel) > mMinFlingVelocity) { - smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel); + if (mOnDismissedListener != null + && yvel > 0 && mCollapseOffset > mCollapsibleHeight) { + smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, yvel); + mDismissOnScrollerFinished = true; + } else { + smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel); + } } else { smoothScrollTo( mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0); @@ -327,17 +335,27 @@ public class ResolverDrawerLayout extends ViewGroup { @Override public void computeScroll() { super.computeScroll(); - if (!mScroller.isFinished()) { - final boolean keepGoing = mScroller.computeScrollOffset(); + if (mScroller.computeScrollOffset()) { + final boolean keepGoing = !mScroller.isFinished(); performDrag(mScroller.getCurrY() - mCollapseOffset); if (keepGoing) { postInvalidateOnAnimation(); + } else if (mDismissOnScrollerFinished && mOnDismissedListener != null) { + mRunOnDismissedListener = new RunOnDismissedListener(); + post(mRunOnDismissedListener); } } } + private void abortAnimation() { + mScroller.abortAnimation(); + mRunOnDismissedListener = null; + mDismissOnScrollerFinished = false; + } + private float performDrag(float dy) { - final float newPos = Math.max(0, Math.min(mCollapseOffset + dy, mCollapsibleHeight)); + final float newPos = Math.max(0, Math.min(mCollapseOffset + dy, + mCollapsibleHeight + mUncollapsibleHeight)); if (newPos != mCollapseOffset) { dy = newPos - mCollapseOffset; final int childCount = getChildCount(); @@ -356,11 +374,18 @@ public class ResolverDrawerLayout extends ViewGroup { return 0; } - private void smoothScrollTo(int yOffset, float velocity) { - if (getMaxCollapsedHeight() == 0) { - return; + void dispatchOnDismissed() { + if (mOnDismissedListener != null) { + mOnDismissedListener.onDismissed(); } - mScroller.abortAnimation(); + if (mRunOnDismissedListener != null) { + removeCallbacks(mRunOnDismissedListener); + mRunOnDismissedListener = null; + } + } + + private void smoothScrollTo(int yOffset, float velocity) { + abortAnimation(); final int sy = (int) mCollapseOffset; int dy = yOffset - sy; if (dy == 0) { @@ -490,6 +515,7 @@ public class ResolverDrawerLayout extends ViewGroup { protected void onDetachedFromWindow() { super.onDetachedFromWindow(); getViewTreeObserver().removeOnTouchModeChangeListener(mTouchModeChangeListener); + abortAnimation(); } @Override @@ -585,6 +611,7 @@ public class ResolverDrawerLayout extends ViewGroup { mCollapsibleHeight = Math.max(0, heightUsed - alwaysShowHeight - getMaxCollapsedHeight()); + mUncollapsibleHeight = heightUsed - mCollapsibleHeight; if (isLaidOut()) { mCollapseOffset = Math.min(mCollapseOffset, mCollapsibleHeight); @@ -734,4 +761,15 @@ public class ResolverDrawerLayout extends ViewGroup { } }; } + + public interface OnDismissedListener { + public void onDismissed(); + } + + private class RunOnDismissedListener implements Runnable { + @Override + public void run() { + dispatchOnDismissed(); + } + } } diff --git a/core/java/com/android/internal/widget/ToolbarWidgetWrapper.java b/core/java/com/android/internal/widget/ToolbarWidgetWrapper.java index 324a6c9..054ca30 100644 --- a/core/java/com/android/internal/widget/ToolbarWidgetWrapper.java +++ b/core/java/com/android/internal/widget/ToolbarWidgetWrapper.java @@ -657,4 +657,36 @@ public class ToolbarWidgetWrapper implements DecorToolbar { mToolbar.restoreHierarchyState(toolbarStates); } + @Override + public void setBackgroundDrawable(Drawable d) { + //noinspection deprecation + mToolbar.setBackgroundDrawable(d); + } + + @Override + public int getHeight() { + return mToolbar.getHeight(); + } + + @Override + public void setVisibility(int visible) { + mToolbar.setVisibility(visible); + } + + @Override + public int getVisibility() { + return mToolbar.getVisibility(); + } + + @Override + public void setMenuCallbacks(MenuPresenter.Callback presenterCallback, + MenuBuilder.Callback menuBuilderCallback) { + mToolbar.setMenuCallbacks(presenterCallback, menuBuilderCallback); + } + + @Override + public Menu getMenu() { + return mToolbar.getMenu(); + } + } |