diff options
Diffstat (limited to 'core/java')
549 files changed, 36112 insertions, 20658 deletions
diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java index 3f1845a..9d6aa13 100644 --- a/core/java/android/accessibilityservice/AccessibilityService.java +++ b/core/java/android/accessibilityservice/AccessibilityService.java @@ -27,7 +27,6 @@ import android.os.RemoteException; import android.util.Log; import android.view.KeyEvent; import android.view.WindowManager; -import android.view.WindowManagerGlobal; import android.view.WindowManagerImpl; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityInteractionClient; diff --git a/core/java/android/alsa/AlsaCardsParser.java b/core/java/android/alsa/AlsaCardsParser.java deleted file mode 100644 index 8b44881..0000000 --- a/core/java/android/alsa/AlsaCardsParser.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * 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.alsa; - -import android.util.Slog; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.IOException; -import java.util.Vector; - -/** - * @hide Retrieves information from an ALSA "cards" file. - */ -public class AlsaCardsParser { - private static final String TAG = "AlsaCardsParser"; - - private static LineTokenizer tokenizer_ = new LineTokenizer(" :[]"); - - public class AlsaCardRecord { - public int mCardNum = -1; - public String mField1 = ""; - public String mCardName = ""; - public String mCardDescription = ""; - - public AlsaCardRecord() {} - - public boolean parse(String line, int lineIndex) { - int tokenIndex = 0; - int delimIndex = 0; - if (lineIndex == 0) { - // line # (skip) - tokenIndex = tokenizer_.nextToken(line, tokenIndex); - delimIndex = tokenizer_.nextDelimiter(line, tokenIndex); - - // mField1 - tokenIndex = tokenizer_.nextToken(line, delimIndex); - delimIndex = tokenizer_.nextDelimiter(line, tokenIndex); - mField1 = line.substring(tokenIndex, delimIndex); - - // mCardName - tokenIndex = tokenizer_.nextToken(line, delimIndex); - // delimIndex = tokenizer_.nextDelimiter(line, tokenIndex); - mCardName = line.substring(tokenIndex); - // done - } else if (lineIndex == 1) { - tokenIndex = tokenizer_.nextToken(line, 0); - if (tokenIndex != -1) { - mCardDescription = line.substring(tokenIndex); - } - } - - return true; - } - - public String textFormat() { - return mCardName + " : " + mCardDescription; - } - } - - private Vector<AlsaCardRecord> cardRecords_ = new Vector<AlsaCardRecord>(); - - public void scan() { - cardRecords_.clear(); - final String cardsFilePath = "/proc/asound/cards"; - File cardsFile = new File(cardsFilePath); - try { - FileReader reader = new FileReader(cardsFile); - BufferedReader bufferedReader = new BufferedReader(reader); - String line = ""; - while ((line = bufferedReader.readLine()) != null) { - AlsaCardRecord cardRecord = new AlsaCardRecord(); - cardRecord.parse(line, 0); - cardRecord.parse(line = bufferedReader.readLine(), 1); - cardRecords_.add(cardRecord); - } - reader.close(); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - public AlsaCardRecord getCardRecordAt(int index) { - return cardRecords_.get(index); - } - - public int getNumCardRecords() { - return cardRecords_.size(); - } - - public void Log() { - int numCardRecs = getNumCardRecords(); - for (int index = 0; index < numCardRecs; ++index) { - Slog.w(TAG, "usb:" + getCardRecordAt(index).textFormat()); - } - } - - public AlsaCardsParser() {} -} diff --git a/core/java/android/animation/AnimatorInflater.java b/core/java/android/animation/AnimatorInflater.java index 688d7e4..021194c 100644 --- a/core/java/android/animation/AnimatorInflater.java +++ b/core/java/android/animation/AnimatorInflater.java @@ -15,6 +15,7 @@ */ package android.animation; +import android.annotation.AnimatorRes; import android.content.Context; import android.content.res.ConfigurationBoundResourceCache; import android.content.res.ConstantState; @@ -66,8 +67,7 @@ public class AnimatorInflater { private static final int VALUE_TYPE_FLOAT = 0; private static final int VALUE_TYPE_INT = 1; private static final int VALUE_TYPE_PATH = 2; - private static final int VALUE_TYPE_COLOR = 4; - private static final int VALUE_TYPE_CUSTOM = 5; + private static final int VALUE_TYPE_COLOR = 3; private static final boolean DBG_ANIMATOR_INFLATER = false; @@ -82,7 +82,7 @@ public class AnimatorInflater { * @return The animator object reference by the specified id * @throws android.content.res.Resources.NotFoundException when the animation cannot be loaded */ - public static Animator loadAnimator(Context context, int id) + public static Animator loadAnimator(Context context, @AnimatorRes int id) throws NotFoundException { return loadAnimator(context.getResources(), context.getTheme(), id); } @@ -296,39 +296,50 @@ public class AnimatorInflater { } } - /** - * @param anim The animator, must not be null - * @param arrayAnimator Incoming typed array for Animator's attributes. - * @param arrayObjectAnimator Incoming typed array for Object Animator's - * attributes. - * @param pixelSize The relative pixel size, used to calculate the - * maximum error for path animations. - */ - private static void parseAnimatorFromTypeArray(ValueAnimator anim, - TypedArray arrayAnimator, TypedArray arrayObjectAnimator, float pixelSize) { - long duration = arrayAnimator.getInt(R.styleable.Animator_duration, 300); - - long startDelay = arrayAnimator.getInt(R.styleable.Animator_startOffset, 0); - - int valueType = arrayAnimator.getInt(R.styleable.Animator_valueType, - VALUE_TYPE_FLOAT); - - TypeEvaluator evaluator = null; + private static PropertyValuesHolder getPVH(TypedArray styledAttributes, int valueType, + int valueFromId, int valueToId, String propertyName) { boolean getFloats = (valueType == VALUE_TYPE_FLOAT); - TypedValue tvFrom = arrayAnimator.peekValue(R.styleable.Animator_valueFrom); + TypedValue tvFrom = styledAttributes.peekValue(valueFromId); boolean hasFrom = (tvFrom != null); int fromType = hasFrom ? tvFrom.type : 0; - TypedValue tvTo = arrayAnimator.peekValue(R.styleable.Animator_valueTo); + TypedValue tvTo = styledAttributes.peekValue(valueToId); boolean hasTo = (tvTo != null); int toType = hasTo ? tvTo.type : 0; - // TODO: Further clean up this part of code into 4 types : path, color, - // integer and float. + PropertyValuesHolder returnValue = null; + if (valueType == VALUE_TYPE_PATH) { - evaluator = setupAnimatorForPath(anim, arrayAnimator); + String fromString = styledAttributes.getString(valueFromId); + String toString = styledAttributes.getString(valueToId); + PathParser.PathDataNode[] nodesFrom = PathParser.createNodesFromPathData(fromString); + PathParser.PathDataNode[] nodesTo = PathParser.createNodesFromPathData(toString); + + if (nodesFrom != null || nodesTo != null) { + if (nodesFrom != null) { + TypeEvaluator evaluator = + new PathDataEvaluator(PathParser.deepCopyNodes(nodesFrom)); + if (nodesTo != null) { + if (!PathParser.canMorph(nodesFrom, nodesTo)) { + throw new InflateException(" Can't morph from " + fromString + " to " + + toString); + } + returnValue = PropertyValuesHolder.ofObject(propertyName, evaluator, + nodesFrom, nodesTo); + } else { + returnValue = PropertyValuesHolder.ofObject(propertyName, evaluator, + (Object) nodesFrom); + } + } else if (nodesTo != null) { + TypeEvaluator evaluator = + new PathDataEvaluator(PathParser.deepCopyNodes(nodesTo)); + returnValue = PropertyValuesHolder.ofObject(propertyName, evaluator, + (Object) nodesTo); + } + } } else { + TypeEvaluator evaluator = null; // Integer and float value types are handled here. if ((hasFrom && (fromType >= TypedValue.TYPE_FIRST_COLOR_INT) && (fromType <= TypedValue.TYPE_LAST_COLOR_INT)) || @@ -338,7 +349,101 @@ public class AnimatorInflater { getFloats = false; evaluator = ArgbEvaluator.getInstance(); } - setupValues(anim, arrayAnimator, getFloats, hasFrom, fromType, hasTo, toType); + if (getFloats) { + float valueFrom; + float valueTo; + if (hasFrom) { + if (fromType == TypedValue.TYPE_DIMENSION) { + valueFrom = styledAttributes.getDimension(valueFromId, 0f); + } else { + valueFrom = styledAttributes.getFloat(valueFromId, 0f); + } + if (hasTo) { + if (toType == TypedValue.TYPE_DIMENSION) { + valueTo = styledAttributes.getDimension(valueToId, 0f); + } else { + valueTo = styledAttributes.getFloat(valueToId, 0f); + } + returnValue = PropertyValuesHolder.ofFloat(propertyName, + valueFrom, valueTo); + } else { + returnValue = PropertyValuesHolder.ofFloat(propertyName, valueFrom); + } + } else { + if (toType == TypedValue.TYPE_DIMENSION) { + valueTo = styledAttributes.getDimension(valueToId, 0f); + } else { + valueTo = styledAttributes.getFloat(valueToId, 0f); + } + returnValue = PropertyValuesHolder.ofFloat(propertyName, valueTo); + } + } else { + int valueFrom; + int valueTo; + if (hasFrom) { + if (fromType == TypedValue.TYPE_DIMENSION) { + valueFrom = (int) styledAttributes.getDimension(valueFromId, 0f); + } else if ((fromType >= TypedValue.TYPE_FIRST_COLOR_INT) && + (fromType <= TypedValue.TYPE_LAST_COLOR_INT)) { + valueFrom = styledAttributes.getColor(valueFromId, 0); + } else { + valueFrom = styledAttributes.getInt(valueFromId, 0); + } + if (hasTo) { + if (toType == TypedValue.TYPE_DIMENSION) { + valueTo = (int) styledAttributes.getDimension(valueToId, 0f); + } else if ((toType >= TypedValue.TYPE_FIRST_COLOR_INT) && + (toType <= TypedValue.TYPE_LAST_COLOR_INT)) { + valueTo = styledAttributes.getColor(valueToId, 0); + } else { + valueTo = styledAttributes.getInt(valueToId, 0); + } + returnValue = PropertyValuesHolder.ofInt(propertyName, valueFrom, valueTo); + } else { + returnValue = PropertyValuesHolder.ofInt(propertyName, valueFrom); + } + } else { + if (hasTo) { + if (toType == TypedValue.TYPE_DIMENSION) { + valueTo = (int) styledAttributes.getDimension(valueToId, 0f); + } else if ((toType >= TypedValue.TYPE_FIRST_COLOR_INT) && + (toType <= TypedValue.TYPE_LAST_COLOR_INT)) { + valueTo = styledAttributes.getColor(valueToId, 0); + } else { + valueTo = styledAttributes.getInt(valueToId, 0); + } + returnValue = PropertyValuesHolder.ofInt(propertyName, valueTo); + } + } + } + if (returnValue != null && evaluator != null) { + returnValue.setEvaluator(evaluator); + } + } + + return returnValue; + } + + /** + * @param anim The animator, must not be null + * @param arrayAnimator Incoming typed array for Animator's attributes. + * @param arrayObjectAnimator Incoming typed array for Object Animator's + * attributes. + * @param pixelSize The relative pixel size, used to calculate the + * maximum error for path animations. + */ + private static void parseAnimatorFromTypeArray(ValueAnimator anim, + TypedArray arrayAnimator, TypedArray arrayObjectAnimator, float pixelSize) { + long duration = arrayAnimator.getInt(R.styleable.Animator_duration, 300); + + long startDelay = arrayAnimator.getInt(R.styleable.Animator_startOffset, 0); + + int valueType = arrayAnimator.getInt(R.styleable.Animator_valueType, VALUE_TYPE_FLOAT); + + PropertyValuesHolder pvh = getPVH(arrayAnimator, valueType, + R.styleable.Animator_valueFrom, R.styleable.Animator_valueTo, ""); + if (pvh != null) { + anim.setValues(pvh); } anim.setDuration(duration); @@ -353,12 +458,10 @@ public class AnimatorInflater { arrayAnimator.getInt(R.styleable.Animator_repeatMode, ValueAnimator.RESTART)); } - if (evaluator != null) { - anim.setEvaluator(evaluator); - } if (arrayObjectAnimator != null) { - setupObjectAnimator(anim, arrayObjectAnimator, getFloats, pixelSize); + setupObjectAnimator(anim, arrayObjectAnimator, valueType == VALUE_TYPE_FLOAT, + pixelSize); } } @@ -570,6 +673,7 @@ public class AnimatorInflater { } String name = parser.getName(); + boolean gotValues = false; if (name.equals("objectAnimator")) { anim = loadObjectAnimator(res, theme, attrs, pixelSize); @@ -588,11 +692,18 @@ public class AnimatorInflater { createAnimatorFromXml(res, theme, parser, attrs, (AnimatorSet) anim, ordering, pixelSize); a.recycle(); + } else if (name.equals("propertyValuesHolder")) { + PropertyValuesHolder[] values = loadValues(res, theme, parser, + Xml.asAttributeSet(parser)); + if (values != null && anim != null && (anim instanceof ValueAnimator)) { + ((ValueAnimator) anim).setValues(values); + } + gotValues = true; } else { throw new RuntimeException("Unknown animator name: " + parser.getName()); } - if (parent != null) { + if (parent != null && !gotValues) { if (childAnims == null) { childAnims = new ArrayList<Animator>(); } @@ -612,7 +723,233 @@ public class AnimatorInflater { } } return anim; + } + + private static PropertyValuesHolder[] loadValues(Resources res, Theme theme, + XmlPullParser parser, AttributeSet attrs) throws XmlPullParserException, IOException { + ArrayList<PropertyValuesHolder> values = null; + + int type; + while ((type = parser.getEventType()) != XmlPullParser.END_TAG && + type != XmlPullParser.END_DOCUMENT) { + + if (type != XmlPullParser.START_TAG) { + parser.next(); + continue; + } + + String name = parser.getName(); + + if (name.equals("propertyValuesHolder")) { + TypedArray a; + if (theme != null) { + a = theme.obtainStyledAttributes(attrs, R.styleable.PropertyValuesHolder, 0, 0); + } else { + a = res.obtainAttributes(attrs, R.styleable.PropertyValuesHolder); + } + String propertyName = a.getString(R.styleable.PropertyValuesHolder_propertyName); + int valueType = a.getInt(R.styleable.PropertyValuesHolder_valueType, + VALUE_TYPE_FLOAT); + PropertyValuesHolder pvh = loadPvh(res, theme, parser, propertyName, valueType); + if (pvh == null) { + pvh = getPVH(a, valueType, + R.styleable.PropertyValuesHolder_valueFrom, + R.styleable.PropertyValuesHolder_valueTo, propertyName); + } + if (pvh != null) { + if (values == null) { + values = new ArrayList<PropertyValuesHolder>(); + } + values.add(pvh); + } + a.recycle(); + } + + parser.next(); + } + + PropertyValuesHolder[] valuesArray = null; + if (values != null) { + int count = values.size(); + valuesArray = new PropertyValuesHolder[count]; + for (int i = 0; i < count; ++i) { + valuesArray[i] = values.get(i); + } + } + return valuesArray; + } + + private static void dumpKeyframes(Object[] keyframes, String header) { + if (keyframes == null || keyframes.length == 0) { + return; + } + Log.d(TAG, header); + int count = keyframes.length; + for (int i = 0; i < count; ++i) { + Keyframe keyframe = (Keyframe) keyframes[i]; + Log.d(TAG, "Keyframe " + i + ": fraction " + + (keyframe.getFraction() < 0 ? "null" : keyframe.getFraction()) + ", " + + ", value : " + ((keyframe.hasValue()) ? keyframe.getValue() : "null")); + } + } + + private static PropertyValuesHolder loadPvh(Resources res, Theme theme, XmlPullParser parser, + String propertyName, int valueType) + throws XmlPullParserException, IOException { + + PropertyValuesHolder value = null; + ArrayList<Keyframe> keyframes = null; + + int type; + while ((type = parser.next()) != XmlPullParser.END_TAG && + type != XmlPullParser.END_DOCUMENT) { + String name = parser.getName(); + if (name.equals("keyframe")) { + Keyframe keyframe = loadKeyframe(res, theme, Xml.asAttributeSet(parser), valueType); + if (keyframe != null) { + if (keyframes == null) { + keyframes = new ArrayList<Keyframe>(); + } + keyframes.add(keyframe); + } + parser.next(); + } + } + + int count; + if (keyframes != null && (count = keyframes.size()) > 0) { + // make sure we have keyframes at 0 and 1 + // If we have keyframes with set fractions, add keyframes at start/end + // appropriately. If start/end have no set fractions: + // if there's only one keyframe, set its fraction to 1 and add one at 0 + // if >1 keyframe, set the last fraction to 1, the first fraction to 0 + Keyframe firstKeyframe = keyframes.get(0); + Keyframe lastKeyframe = keyframes.get(count - 1); + float endFraction = lastKeyframe.getFraction(); + if (endFraction < 1) { + if (endFraction < 0) { + lastKeyframe.setFraction(1); + } else { + keyframes.add(keyframes.size(), createNewKeyframe(lastKeyframe, 1)); + ++count; + } + } + float startFraction = firstKeyframe.getFraction(); + if (startFraction != 0) { + if (startFraction < 0) { + firstKeyframe.setFraction(0); + } else { + keyframes.add(0, createNewKeyframe(firstKeyframe, 0)); + ++count; + } + } + Keyframe[] keyframeArray = new Keyframe[count]; + keyframes.toArray(keyframeArray); + for (int i = 0; i < count; ++i) { + Keyframe keyframe = keyframeArray[i]; + if (keyframe.getFraction() < 0) { + if (i == 0) { + keyframe.setFraction(0); + } else if (i == count - 1) { + keyframe.setFraction(1); + } else { + // figure out the start/end parameters of the current gap + // in fractions and distribute the gap among those keyframes + int startIndex = i; + int endIndex = i; + for (int j = startIndex + 1; j < count - 1; ++j) { + if (keyframeArray[j].getFraction() >= 0) { + break; + } + endIndex = j; + } + float gap = keyframeArray[endIndex + 1].getFraction() - + keyframeArray[startIndex - 1].getFraction(); + distributeKeyframes(keyframeArray, gap, startIndex, endIndex); + } + } + } + value = PropertyValuesHolder.ofKeyframe(propertyName, keyframeArray); + if (valueType == VALUE_TYPE_COLOR) { + value.setEvaluator(ArgbEvaluator.getInstance()); + } + } + + return value; + } + + private static Keyframe createNewKeyframe(Keyframe sampleKeyframe, float fraction) { + return sampleKeyframe.getType() == float.class ? + Keyframe.ofFloat(fraction) : + (sampleKeyframe.getType() == int.class) ? + Keyframe.ofInt(fraction) : + Keyframe.ofObject(fraction); + } + + /** + * Utility function to set fractions on keyframes to cover a gap in which the + * fractions are not currently set. Keyframe fractions will be distributed evenly + * in this gap. For example, a gap of 1 keyframe in the range 0-1 will be at .5, a gap + * of .6 spread between two keyframes will be at .2 and .4 beyond the fraction at the + * keyframe before startIndex. + * Assumptions: + * - First and last keyframe fractions (bounding this spread) are already set. So, + * for example, if no fractions are set, we will already set first and last keyframe + * fraction values to 0 and 1. + * - startIndex must be >0 (which follows from first assumption). + * - endIndex must be >= startIndex. + * + * @param keyframes the array of keyframes + * @param gap The total gap we need to distribute + * @param startIndex The index of the first keyframe whose fraction must be set + * @param endIndex The index of the last keyframe whose fraction must be set + */ + private static void distributeKeyframes(Keyframe[] keyframes, float gap, + int startIndex, int endIndex) { + int count = endIndex - startIndex + 2; + float increment = gap / count; + for (int i = startIndex; i <= endIndex; ++i) { + keyframes[i].setFraction(keyframes[i-1].getFraction() + increment); + } + } + + private static Keyframe loadKeyframe(Resources res, Theme theme, AttributeSet attrs, + int valueType) + throws XmlPullParserException, IOException { + + TypedArray a; + if (theme != null) { + a = theme.obtainStyledAttributes(attrs, R.styleable.Keyframe, 0, 0); + } else { + a = res.obtainAttributes(attrs, R.styleable.Keyframe); + } + + Keyframe keyframe = null; + + float fraction = a.getFloat(R.styleable.Keyframe_fraction, -1); + + boolean hasValue = a.peekValue(R.styleable.Keyframe_value) != null; + + if (hasValue) { + switch (valueType) { + case VALUE_TYPE_FLOAT: + float value = a.getFloat(R.styleable.Keyframe_value, 0); + keyframe = Keyframe.ofFloat(fraction, value); + break; + case VALUE_TYPE_COLOR: + case VALUE_TYPE_INT: + int intValue = a.getInt(R.styleable.Keyframe_value, 0); + keyframe = Keyframe.ofInt(fraction, intValue); + break; + } + } else { + keyframe = (valueType == VALUE_TYPE_FLOAT) ? Keyframe.ofFloat(fraction) : + Keyframe.ofInt(fraction); + } + + a.recycle(); + return keyframe; } private static ObjectAnimator loadObjectAnimator(Resources res, Theme theme, AttributeSet attrs, diff --git a/core/java/android/animation/AnimatorSet.java b/core/java/android/animation/AnimatorSet.java index 92762c3..53d5237 100644 --- a/core/java/android/animation/AnimatorSet.java +++ b/core/java/android/animation/AnimatorSet.java @@ -972,6 +972,18 @@ public final class AnimatorSet extends Animator { } } + @Override + public String toString() { + String returnVal = "AnimatorSet@" + Integer.toHexString(hashCode()) + "{"; + boolean prevNeedsSort = mNeedsSort; + sortNodes(); + mNeedsSort = prevNeedsSort; + for (Node node : mSortedNodes) { + returnVal += "\n " + node.animation.toString(); + } + return returnVal + "\n}"; + } + /** * Dependency holds information about the node that some other node is * dependent upon and the nature of that dependency. diff --git a/core/java/android/animation/FloatKeyframeSet.java b/core/java/android/animation/FloatKeyframeSet.java index abac246..56da940 100644 --- a/core/java/android/animation/FloatKeyframeSet.java +++ b/core/java/android/animation/FloatKeyframeSet.java @@ -18,7 +18,6 @@ package android.animation; import android.animation.Keyframe.FloatKeyframe; -import java.util.ArrayList; import java.util.List; /** diff --git a/core/java/android/animation/IntKeyframeSet.java b/core/java/android/animation/IntKeyframeSet.java index 0ec5138..12a4bf9 100644 --- a/core/java/android/animation/IntKeyframeSet.java +++ b/core/java/android/animation/IntKeyframeSet.java @@ -18,7 +18,6 @@ package android.animation; import android.animation.Keyframe.IntKeyframe; -import java.util.ArrayList; import java.util.List; /** diff --git a/core/java/android/animation/KeyframeSet.java b/core/java/android/animation/KeyframeSet.java index 0e99bff..c80e162 100644 --- a/core/java/android/animation/KeyframeSet.java +++ b/core/java/android/animation/KeyframeSet.java @@ -16,7 +16,6 @@ package android.animation; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; diff --git a/core/java/android/animation/Keyframes.java b/core/java/android/animation/Keyframes.java index c921466..c149bed 100644 --- a/core/java/android/animation/Keyframes.java +++ b/core/java/android/animation/Keyframes.java @@ -15,7 +15,6 @@ */ package android.animation; -import java.util.ArrayList; import java.util.List; /** diff --git a/core/java/android/animation/ObjectAnimator.java b/core/java/android/animation/ObjectAnimator.java index 59daaab..3f71d51 100644 --- a/core/java/android/animation/ObjectAnimator.java +++ b/core/java/android/animation/ObjectAnimator.java @@ -16,6 +16,7 @@ package android.animation; +import android.annotation.CallSuper; import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Path; @@ -24,7 +25,6 @@ import android.util.Log; import android.util.Property; import java.lang.ref.WeakReference; -import java.util.ArrayList; /** * This subclass of {@link ValueAnimator} provides support for animating properties on target objects. @@ -33,6 +33,27 @@ import java.util.ArrayList; * are then determined internally and the animation will call these functions as necessary to * animate the property. * + * <p>Animators can be created from either code or resource files, as shown here:</p> + * + * {@sample development/samples/ApiDemos/res/anim/object_animator.xml ObjectAnimatorResources} + * + * <p>When using resource files, it is possible to use {@link PropertyValuesHolder} and + * {@link Keyframe} to create more complex animations. Using PropertyValuesHolders + * allows animators to animate several properties in parallel, as shown in this sample:</p> + * + * {@sample development/samples/ApiDemos/res/anim/object_animator_pvh.xml + * PropertyValuesHolderResources} + * + * <p>Using Keyframes allows animations to follow more complex paths from the start + * to the end values. Note that you can specify explicit fractional values (from 0 to 1) for + * each keyframe to determine when, in the overall duration, the animation should arrive at that + * value. Alternatively, you can leave the fractions off and the keyframes will be equally + * distributed within the total duration. Also, a keyframe with no value will derive its value + * from the target object when the animator starts, just like animators with only one + * value specified.</p> + * + * {@sample development/samples/ApiDemos/res/anim/object_animator_pvh_kf.xml KeyframeResources} + * * <div class="special reference"> * <h3>Developer Guides</h3> * <p>For more information about animating with {@code ObjectAnimator}, read the @@ -841,6 +862,7 @@ public final class ObjectAnimator extends ValueAnimator { * <p>Overriders of this method should call the superclass method to cause * internal mechanisms to be set up correctly.</p> */ + @CallSuper @Override void initAnimation() { if (!mInitialized) { @@ -941,6 +963,7 @@ public final class ObjectAnimator extends ValueAnimator { * * @param fraction The elapsed fraction of the animation. */ + @CallSuper @Override void animateValue(float fraction) { final Object target = getTarget(); diff --git a/core/java/android/animation/PropertyValuesHolder.java b/core/java/android/animation/PropertyValuesHolder.java index bd7bca0..8928e99 100644 --- a/core/java/android/animation/PropertyValuesHolder.java +++ b/core/java/android/animation/PropertyValuesHolder.java @@ -25,10 +25,8 @@ import android.util.Property; 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; /** * This class holds information about a property and the values that that property diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java index 9709555..85dc832 100644 --- a/core/java/android/animation/ValueAnimator.java +++ b/core/java/android/animation/ValueAnimator.java @@ -16,7 +16,7 @@ package android.animation; -import android.content.res.ConfigurationBoundResourceCache; +import android.annotation.CallSuper; import android.os.Looper; import android.os.Trace; import android.util.AndroidRuntimeException; @@ -40,6 +40,21 @@ import java.util.HashMap; * out of an animation. This behavior can be changed by calling * {@link ValueAnimator#setInterpolator(TimeInterpolator)}.</p> * + * <p>Animators can be created from either code or resource files. Here is an example + * of a ValueAnimator resource file:</p> + * + * {@sample development/samples/ApiDemos/res/anim/animator.xml ValueAnimatorResources} + * + * <p>It is also possible to use a combination of {@link PropertyValuesHolder} and + * {@link Keyframe} resource tags to create a multi-step animation. + * Note that you can specify explicit fractional values (from 0 to 1) for + * each keyframe to determine when, in the overall duration, the animation should arrive at that + * value. Alternatively, you can leave the fractions off and the keyframes will be equally + * distributed within the total duration:</p> + * + * {@sample development/samples/ApiDemos/res/anim/value_animator_pvh_kf.xml + * ValueAnimatorKeyframeResources} + * * <div class="special reference"> * <h3>Developer Guides</h3> * <p>For more information about animating with {@code ValueAnimator}, read the @@ -492,6 +507,7 @@ public class ValueAnimator extends Animator { * <p>Overrides of this method should call the superclass method to ensure * that internal mechanisms for the animation are set up correctly.</p> */ + @CallSuper void initAnimation() { if (!mInitialized) { int numValues = mValues.length; @@ -1361,6 +1377,7 @@ public class ValueAnimator extends Animator { * * @param fraction The elapsed fraction of the animation. */ + @CallSuper void animateValue(float fraction) { fraction = mInterpolator.getInterpolation(fraction); mCurrentFraction = fraction; diff --git a/core/java/android/annotation/CallSuper.java b/core/java/android/annotation/CallSuper.java new file mode 100644 index 0000000..82e2723 --- /dev/null +++ b/core/java/android/annotation/CallSuper.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2015 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.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that any overriding methods should invoke this method as well. + * <p> + * Example: + * <pre>{@code + * @CallSuper + * public abstract void onFocusLost(); + * }</pre> + * + * @hide + */ +@Retention(SOURCE) +@Target({METHOD}) +public @interface CallSuper { +}
\ No newline at end of file diff --git a/core/java/android/annotation/CheckResult.java b/core/java/android/annotation/CheckResult.java new file mode 100644 index 0000000..787514e --- /dev/null +++ b/core/java/android/annotation/CheckResult.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2015 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.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that the annotated method returns a result that it typically is + * an error to ignore. This is usually used for methods that have no side effect, + * so calling it without actually looking at the result usually means the developer + * has misunderstood what the method does. + * <p> + * Example: + * <pre>{@code + * public @CheckResult String trim(String s) { return s.trim(); } + * ... + * s.trim(); // this is probably an error + * s = s.trim(); // ok + * }</pre> + * + * @hide + */ +@Retention(SOURCE) +@Target({METHOD}) +public @interface CheckResult { + /** Defines the name of the suggested method to use instead, if applicable (using + * the same signature format as javadoc.) If there is more than one possibility, + * list them all separated by commas. + * <p> + * For example, ProcessBuilder has a method named {@code redirectErrorStream()} + * which sounds like it might redirect the error stream. It does not. It's just + * a getter which returns whether the process builder will redirect the error stream, + * and to actually set it, you must call {@code redirectErrorStream(boolean)}. + * In that case, the method should be defined like this: + * <pre> + * @CheckResult(suggest="#redirectErrorStream(boolean)") + * public boolean redirectErrorStream() { ... } + * </pre> + */ + String suggest() default ""; +}
\ No newline at end of file diff --git a/core/java/android/annotation/ColorInt.java b/core/java/android/annotation/ColorInt.java new file mode 100644 index 0000000..69d196c --- /dev/null +++ b/core/java/android/annotation/ColorInt.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2015 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.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.LOCAL_VARIABLE; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that the annotated element represents a packed color + * int, {@code AARRGGBB}. If applied to an int array, every element + * in the array represents a color integer. + * <p> + * Example: + * <pre>{@code + * public abstract void setTextColor(@ColorInt int color); + * }</pre> + * + * @hide + */ +@Retention(SOURCE) +@Target({PARAMETER,METHOD,LOCAL_VARIABLE,FIELD}) +public @interface ColorInt { +}
\ No newline at end of file diff --git a/core/java/android/annotation/FloatRange.java b/core/java/android/annotation/FloatRange.java new file mode 100644 index 0000000..3a7c150 --- /dev/null +++ b/core/java/android/annotation/FloatRange.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2015 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.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.LOCAL_VARIABLE; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that the annotated element should be a float or double in the given range + * <p> + * Example: + * <pre>{@code + * @FloatRange(from=0.0,to=1.0) + * public float getAlpha() { + * ... + * } + * }</pre> + * + * @hide + */ +@Retention(SOURCE) +@Target({METHOD,PARAMETER,FIELD,LOCAL_VARIABLE}) +public @interface FloatRange { + /** Smallest value. Whether it is inclusive or not is determined + * by {@link #fromInclusive} */ + double from() default Double.NEGATIVE_INFINITY; + /** Largest value. Whether it is inclusive or not is determined + * by {@link #toInclusive} */ + double to() default Double.POSITIVE_INFINITY; + + /** Whether the from value is included in the range */ + boolean fromInclusive() default true; + + /** Whether the to value is included in the range */ + boolean toInclusive() default true; +}
\ No newline at end of file diff --git a/core/java/android/annotation/IntRange.java b/core/java/android/annotation/IntRange.java new file mode 100644 index 0000000..1e3c072 --- /dev/null +++ b/core/java/android/annotation/IntRange.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2015 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.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.LOCAL_VARIABLE; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that the annotated element should be an int or long in the given range + * <p> + * Example: + * <pre>{@code + * @IntRange(from=0,to=255) + * public int getAlpha() { + * ... + * } + * }</pre> + * + * @hide + */ +@Retention(SOURCE) +@Target({METHOD,PARAMETER,FIELD,LOCAL_VARIABLE}) +public @interface IntRange { + /** Smallest value, inclusive */ + long from() default Long.MIN_VALUE; + /** Largest value, inclusive */ + long to() default Long.MAX_VALUE; +}
\ No newline at end of file diff --git a/core/java/android/annotation/Size.java b/core/java/android/annotation/Size.java new file mode 100644 index 0000000..389b819 --- /dev/null +++ b/core/java/android/annotation/Size.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2015 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.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.LOCAL_VARIABLE; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that the annotated element should have a given size or length. + * Note that "-1" means "unset". Typically used with a parameter or + * return value of type array or collection. + * <p> + * Example: + * <pre>{@code + * public void getLocationInWindow(@Size(2) int[] location) { + * ... + * } + * }</pre> + * + * @hide + */ +@Retention(SOURCE) +@Target({PARAMETER,LOCAL_VARIABLE,METHOD,FIELD}) +public @interface Size { + /** An exact size (or -1 if not specified) */ + long value() default -1; + /** A minimum size, inclusive */ + long min() default Long.MIN_VALUE; + /** A maximum size, inclusive */ + long max() default Long.MAX_VALUE; + /** The size must be a multiple of this factor */ + long multiple() default 1; +}
\ No newline at end of file diff --git a/core/java/android/annotation/TransitionRes.java b/core/java/android/annotation/TransitionRes.java new file mode 100644 index 0000000..06bac74 --- /dev/null +++ b/core/java/android/annotation/TransitionRes.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2015 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that an integer parameter, field or method return value is expected + * to be a transition resource reference. + * + * {@hide} + */ +@Documented +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +public @interface TransitionRes { +} diff --git a/core/java/android/app/ActionBar.java b/core/java/android/app/ActionBar.java index 014a7af..94e3b66 100644 --- a/core/java/android/app/ActionBar.java +++ b/core/java/android/app/ActionBar.java @@ -16,9 +16,12 @@ package android.app; +import android.annotation.DrawableRes; import android.annotation.IntDef; +import android.annotation.LayoutRes; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.StringRes; import android.content.Context; import android.content.res.Configuration; import android.content.res.TypedArray; @@ -30,14 +33,10 @@ import android.view.KeyEvent; import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; -import android.view.ViewGroup.MarginLayoutParams; import android.view.Window; import android.widget.SpinnerAdapter; -import android.widget.Toolbar; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.Map; /** * A primary toolbar within the activity that may display the activity title, application-level @@ -256,7 +255,7 @@ public abstract class ActionBar { * * @see #setDisplayOptions(int, int) */ - public abstract void setCustomView(int resId); + public abstract void setCustomView(@LayoutRes int resId); /** * Set the icon to display in the 'home' section of the action bar. @@ -271,7 +270,7 @@ public abstract class ActionBar { * @see #setDisplayUseLogoEnabled(boolean) * @see #setDisplayShowHomeEnabled(boolean) */ - public abstract void setIcon(int resId); + public abstract void setIcon(@DrawableRes int resId); /** * Set the icon to display in the 'home' section of the action bar. @@ -301,7 +300,7 @@ public abstract class ActionBar { * @see #setDisplayUseLogoEnabled(boolean) * @see #setDisplayShowHomeEnabled(boolean) */ - public abstract void setLogo(int resId); + public abstract void setLogo(@DrawableRes int resId); /** * Set the logo to display in the 'home' section of the action bar. @@ -397,7 +396,7 @@ public abstract class ActionBar { * @see #setTitle(CharSequence) * @see #setDisplayOptions(int, int) */ - public abstract void setTitle(int resId); + public abstract void setTitle(@StringRes int resId); /** * Set the action bar's subtitle. This will only be displayed if @@ -420,7 +419,7 @@ public abstract class ActionBar { * @see #setSubtitle(CharSequence) * @see #setDisplayOptions(int, int) */ - public abstract void setSubtitle(int resId); + public abstract void setSubtitle(@StringRes int resId); /** * Set display options. This changes all display option bits at once. To change @@ -892,7 +891,7 @@ public abstract class ActionBar { * @see #setDisplayHomeAsUpEnabled(boolean) * @see #setHomeActionContentDescription(int) */ - public void setHomeAsUpIndicator(int resId) { } + public void setHomeAsUpIndicator(@DrawableRes int resId) { } /** * Set an alternate description for the Home/Up action, when enabled. @@ -931,7 +930,7 @@ public abstract class ActionBar { * @see #setHomeAsUpIndicator(int) * @see #setHomeAsUpIndicator(android.graphics.drawable.Drawable) */ - public void setHomeActionContentDescription(int resId) { } + public void setHomeActionContentDescription(@StringRes int resId) { } /** * Enable hiding the action bar on content scroll. @@ -1154,7 +1153,7 @@ public abstract class ActionBar { * @param resId Resource ID referring to the drawable to use as an icon * @return The current instance for call chaining */ - public abstract Tab setIcon(int resId); + public abstract Tab setIcon(@DrawableRes int resId); /** * Set the text displayed on this tab. Text may be truncated if there is not @@ -1172,7 +1171,7 @@ public abstract class ActionBar { * @param resId A resource ID referring to the text that should be displayed * @return The current instance for call chaining */ - public abstract Tab setText(int resId); + public abstract Tab setText(@StringRes int resId); /** * Set a custom view to be used for this tab. This overrides values set by @@ -1190,7 +1189,7 @@ public abstract class ActionBar { * @param layoutResId A layout resource to inflate and use as a custom tab view * @return The current instance for call chaining */ - public abstract Tab setCustomView(int layoutResId); + public abstract Tab setCustomView(@LayoutRes int layoutResId); /** * Retrieve a previously set custom view for this tab. @@ -1235,7 +1234,7 @@ public abstract class ActionBar { * @see #setContentDescription(CharSequence) * @see #getContentDescription() */ - public abstract Tab setContentDescription(int resId); + public abstract Tab setContentDescription(@StringRes int resId); /** * Set a description of this tab's content for use in accessibility support. diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 9568897..7fcbe35 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -16,7 +16,14 @@ package android.app; +import android.annotation.CallSuper; +import android.annotation.DrawableRes; +import android.annotation.IdRes; +import android.annotation.IntDef; +import android.annotation.LayoutRes; import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.StyleRes; import android.os.PersistableBundle; import android.transition.Scene; import android.transition.TransitionManager; @@ -27,10 +34,7 @@ import android.widget.Toolbar; import com.android.internal.app.IVoiceInteractor; import com.android.internal.app.WindowDecorActionBar; import com.android.internal.app.ToolbarActionBar; -import com.android.internal.policy.PolicyManager; -import android.annotation.IntDef; -import android.annotation.Nullable; import android.annotation.SystemApi; import android.app.admin.DevicePolicyManager; import android.content.ComponentCallbacks2; @@ -84,6 +88,7 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.MotionEvent; +import android.view.PhoneWindow; import android.view.View; import android.view.View.OnCreateContextMenuListener; import android.view.ViewGroup; @@ -362,7 +367,7 @@ import java.util.HashMap; * * <p>Note the "Killable" column in the above table -- for those methods that * are marked as being killable, after that method returns the process hosting the - * activity may killed by the system <em>at any time</em> without another line + * activity may be killed by the system <em>at any time</em> without another line * of its code being executed. Because of this, you should use the * {@link #onPause} method to write any persistent data (such as user edits) * to storage. In addition, the method @@ -743,6 +748,7 @@ public class Activity extends ContextThemeWrapper final FragmentManagerImpl mFragments = new FragmentManagerImpl(); final FragmentContainer mContainer = new FragmentContainer() { @Override + @Nullable public View findViewById(int id) { return Activity.this.findViewById(id); } @@ -781,6 +787,7 @@ public class Activity extends ContextThemeWrapper private boolean mChangeCanvasToTranslucent; private boolean mTitleReady = false; + private int mActionModeTypeStarting = ActionMode.TYPE_PRIMARY; private int mDefaultKeyMode = DEFAULT_KEYS_DISABLE; private SpannableStringBuilder mDefaultKeySsb = null; @@ -916,6 +923,7 @@ public class Activity extends ContextThemeWrapper * @see #onRestoreInstanceState * @see #onPostCreate */ + @CallSuper protected void onCreate(@Nullable Bundle savedInstanceState) { if (DEBUG_LIFECYCLE) Slog.v(TAG, "onCreate " + this + ": " + savedInstanceState); if (mLastNonConfigurationInstances != null) { @@ -1117,6 +1125,7 @@ public class Activity extends ContextThemeWrapper * recently supplied in {@link #onSaveInstanceState}. <b><i>Note: Otherwise it is null.</i></b> * @see #onCreate */ + @CallSuper protected void onPostCreate(@Nullable Bundle savedInstanceState) { if (!isChild()) { mTitleReady = true; @@ -1154,6 +1163,7 @@ public class Activity extends ContextThemeWrapper * @see #onStop * @see #onResume */ + @CallSuper protected void onStart() { if (DEBUG_LIFECYCLE) Slog.v(TAG, "onStart " + this); mCalled = true; @@ -1191,6 +1201,7 @@ public class Activity extends ContextThemeWrapper * @see #onStart * @see #onResume */ + @CallSuper protected void onRestart() { mCalled = true; } @@ -1215,6 +1226,7 @@ public class Activity extends ContextThemeWrapper * @see #onPostResume * @see #onPause */ + @CallSuper protected void onResume() { if (DEBUG_LIFECYCLE) Slog.v(TAG, "onResume " + this); getApplication().dispatchActivityResumed(this); @@ -1234,6 +1246,7 @@ public class Activity extends ContextThemeWrapper * * @see #onResume */ + @CallSuper protected void onPostResume() { final Window win = getWindow(); if (win != null) win.makeActive(); @@ -1242,22 +1255,18 @@ public class Activity extends ContextThemeWrapper } /** - * @hide * Check whether this activity is running as part of a voice interaction with the user. * If true, it should perform its interaction with the user through the * {@link VoiceInteractor} returned by {@link #getVoiceInteractor}. */ - @SystemApi public boolean isVoiceInteraction() { return mVoiceInteractor != null; } /** - * @hide * Retrieve the active {@link VoiceInteractor} that the user is going through to * interact with this activity. */ - @SystemApi public VoiceInteractor getVoiceInteractor() { return mVoiceInteractor; } @@ -1463,6 +1472,7 @@ public class Activity extends ContextThemeWrapper * @see #onSaveInstanceState * @see #onStop */ + @CallSuper protected void onPause() { if (DEBUG_LIFECYCLE) Slog.v(TAG, "onPause " + this); getApplication().dispatchActivityPaused(this); @@ -1538,7 +1548,7 @@ public class Activity extends ContextThemeWrapper * {@link Intent#ACTION_ASSIST} Intent with all of the context of the current * application. You can override this method to place into the bundle anything * you would like to appear in the {@link Intent#EXTRA_ASSIST_CONTEXT} part - * of the assist Intent. The default implementation does nothing. + * of the assist Intent. * * <p>This function will be called after any global assist callbacks that had * been registered with {@link Application#registerOnProvideAssistDataListener @@ -1548,6 +1558,28 @@ public class Activity extends ContextThemeWrapper } /** + * This is called when the user is requesting an assist, to provide references + * to content related to the current activity. Before being called, the + * {@code outContent} Intent is filled with the base Intent of the activity (the Intent + * returned by {@link #getIntent()}). The Intent's extras are stripped of any types + * that are not valid for {@link PersistableBundle} or non-framework Parcelables, and + * the flags {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION} and + * {@link Intent#FLAG_GRANT_PERSISTABLE_URI_PERMISSION} are cleared from the Intent. + * + * <p>Custom implementation may adjust the content intent to better reflect the top-level + * context of the activity, and fill in its ClipData with additional content of + * interest that the user is currently viewing. For example, an image gallery application + * that has launched in to an activity allowing the user to swipe through pictures should + * modify the intent to reference the current image they are looking it; such an + * application when showing a list of pictures should add a ClipData that has + * references to all of the pictures currently visible on screen.</p> + * + * @param outContent The assist content to return. + */ + public void onProvideAssistContent(AssistContent outContent) { + } + + /** * Called when you are no longer visible to the user. You will next * receive either {@link #onRestart}, {@link #onDestroy}, or nothing, * depending on later user activity. @@ -1565,6 +1597,7 @@ public class Activity extends ContextThemeWrapper * @see #onSaveInstanceState * @see #onDestroy */ + @CallSuper protected void onStop() { if (DEBUG_LIFECYCLE) Slog.v(TAG, "onStop " + this); if (mActionBar != null) mActionBar.setShowHideAnimationEnabled(false); @@ -1602,6 +1635,7 @@ public class Activity extends ContextThemeWrapper * @see #finish * @see #isFinishing */ + @CallSuper protected void onDestroy() { if (DEBUG_LIFECYCLE) Slog.v(TAG, "onDestroy " + this); mCalled = true; @@ -2068,7 +2102,8 @@ public class Activity extends ContextThemeWrapper * * @return The view if found or null otherwise. */ - public View findViewById(int id) { + @Nullable + public View findViewById(@IdRes int id) { return getWindow().findViewById(id); } @@ -2141,7 +2176,7 @@ public class Activity extends ContextThemeWrapper * @see #setContentView(android.view.View) * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams) */ - public void setContentView(int layoutResID) { + public void setContentView(@LayoutRes int layoutResID) { getWindow().setContentView(layoutResID); initWindowDecorActionBar(); } @@ -3609,7 +3644,7 @@ public class Activity extends ContextThemeWrapper * Convenience for calling * {@link android.view.Window#setFeatureDrawableResource}. */ - public final void setFeatureDrawableResource(int featureId, int resId) { + public final void setFeatureDrawableResource(int featureId, @DrawableRes int resId) { getWindow().setFeatureDrawableResource(featureId, resId); } @@ -3664,7 +3699,7 @@ public class Activity extends ContextThemeWrapper } @Override - protected void onApplyThemeResource(Resources.Theme theme, int resid, + protected void onApplyThemeResource(Resources.Theme theme, @StyleRes int resid, boolean first) { if (mParent == null) { super.onApplyThemeResource(theme, resid, first); @@ -4619,7 +4654,7 @@ public class Activity extends ContextThemeWrapper if (Looper.myLooper() != mMainThread.getLooper()) { throw new IllegalStateException("Must be called from main thread"); } - mMainThread.requestRelaunchActivity(mToken, null, null, 0, false, null, false); + mMainThread.requestRelaunchActivity(mToken, null, null, 0, false, null, null, false); } /** @@ -5581,6 +5616,7 @@ public class Activity extends ContextThemeWrapper * @see #requestVisibleBehind(boolean) * @see #onBackgroundVisibleBehindChanged(boolean) */ + @CallSuper public void onVisibleBehindCanceled() { mCalled = true; } @@ -5664,10 +5700,10 @@ public class Activity extends ContextThemeWrapper } /** - * Start an action mode. + * Start an action mode of the default type {@link ActionMode#TYPE_PRIMARY}. * - * @param callback Callback that will manage lifecycle events for this context mode - * @return The ContextMode that was started, or null if it was canceled + * @param callback Callback that will manage lifecycle events for this action mode + * @return The ActionMode that was started, or null if it was canceled * * @see ActionMode */ @@ -5677,6 +5713,20 @@ public class Activity extends ContextThemeWrapper } /** + * Start an action mode of the given type. + * + * @param callback Callback that will manage lifecycle events for this action mode + * @param type One of {@link ActionMode#TYPE_PRIMARY} or {@link ActionMode#TYPE_FLOATING}. + * @return The ActionMode that was started, or null if it was canceled + * + * @see ActionMode + */ + @Nullable + public ActionMode startActionMode(ActionMode.Callback callback, int type) { + return mWindow.getDecorView().startActionMode(callback, type); + } + + /** * Give the Activity a chance to control the UI for an action mode requested * by the system. * @@ -5690,19 +5740,37 @@ public class Activity extends ContextThemeWrapper @Nullable @Override public ActionMode onWindowStartingActionMode(ActionMode.Callback callback) { - initWindowDecorActionBar(); - if (mActionBar != null) { - return mActionBar.startActionMode(callback); + // Only Primary ActionModes are represented in the ActionBar. + if (mActionModeTypeStarting == ActionMode.TYPE_PRIMARY) { + initWindowDecorActionBar(); + if (mActionBar != null) { + return mActionBar.startActionMode(callback); + } } return null; } /** + * {@inheritDoc} + */ + @Nullable + @Override + public ActionMode onWindowStartingActionMode(ActionMode.Callback callback, int type) { + try { + mActionModeTypeStarting = type; + return onWindowStartingActionMode(callback); + } finally { + mActionModeTypeStarting = ActionMode.TYPE_PRIMARY; + } + } + + /** * Notifies the Activity that an action mode has been started. * Activity subclasses overriding this method should call the superclass implementation. * * @param mode The new action mode. */ + @CallSuper @Override public void onActionModeStarted(ActionMode mode) { } @@ -5713,6 +5781,7 @@ public class Activity extends ContextThemeWrapper * * @param mode The action mode that just finished. */ + @CallSuper @Override public void onActionModeFinished(ActionMode mode) { } @@ -5929,7 +5998,7 @@ public class Activity extends ContextThemeWrapper mFragments.attachActivity(this, mContainer, null); - mWindow = PolicyManager.makeNewWindow(this); + mWindow = new PhoneWindow(this); mWindow.setCallback(this); mWindow.setOnWindowDismissedCallback(this); mWindow.getLayoutInflater().setPrivateFactory(this); @@ -6080,6 +6149,17 @@ public class Activity extends ContextThemeWrapper " did not call through to super.onResume()"); } + // invisible activities must be finished before onResume() completes + if (!mVisibleFromClient && !mFinished) { + Log.w(TAG, "An activity without a UI must call finish() before onResume() completes"); + if (getApplicationInfo().targetSdkVersion + > android.os.Build.VERSION_CODES.LOLLIPOP_MR1) { + throw new IllegalStateException( + "Activity " + mComponent.toShortString() + + " did not call finish() prior to onResume() completing"); + } + } + // Now really resume, and install the current status bar and menu. mCalled = false; diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 7a636db..29b024ac 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -256,6 +256,9 @@ public class ActivityManager { /** @hide User operation call: given user id is the current user, can't be stopped. */ public static final int USER_OP_IS_CURRENT = -2; + /** @hide Process does not exist. */ + public static final int PROCESS_STATE_NONEXISTENT = -1; + /** @hide Process is a persistent system process. */ public static final int PROCESS_STATE_PERSISTENT = 0; @@ -306,6 +309,27 @@ public class ActivityManager { /** @hide Process is being cached for later use and is empty. */ public static final int PROCESS_STATE_CACHED_EMPTY = 13; + /** @hide requestType for assist context: only basic information. */ + public static final int ASSIST_CONTEXT_BASIC = 0; + + /** @hide requestType for assist context: generate full AssistStructure. */ + public static final int ASSIST_CONTEXT_FULL = 1; + + /** + * Lock task mode is not active. + */ + public static final int LOCK_TASK_MODE_NONE = 0; + + /** + * Full lock task mode is active. + */ + public static final int LOCK_TASK_MODE_LOCKED = 1; + + /** + * App pinning mode is active. + */ + public static final int LOCK_TASK_MODE_PINNED = 2; + Point mAppTaskThumbnailSize; /*package*/ ActivityManager(Context context, Handler handler) { @@ -2681,12 +2705,25 @@ public class ActivityManager { * no new tasks can be created or switched to. * * @see Activity#startLockTask() + * + * @deprecated Use {@link #getLockTaskModeState} instead. */ public boolean isInLockTaskMode() { + return getLockTaskModeState() != LOCK_TASK_MODE_NONE; + } + + /** + * Return the current state of task locking. The three possible outcomes + * are {@link #LOCK_TASK_MODE_NONE}, {@link #LOCK_TASK_MODE_LOCKED} + * and {@link #LOCK_TASK_MODE_PINNED}. + * + * @see Activity#startLockTask() + */ + public int getLockTaskModeState() { try { - return ActivityManagerNative.getDefault().isInLockTaskMode(); + return ActivityManagerNative.getDefault().getLockTaskModeState(); } catch (RemoteException e) { - return false; + return ActivityManager.LOCK_TASK_MODE_NONE; } } diff --git a/core/java/android/app/ActivityManagerNative.java b/core/java/android/app/ActivityManagerNative.java index e94cdae..1484af8 100644 --- a/core/java/android/app/ActivityManagerNative.java +++ b/core/java/android/app/ActivityManagerNative.java @@ -50,6 +50,7 @@ import android.text.TextUtils; import android.util.Log; import android.util.Singleton; import com.android.internal.app.IVoiceInteractor; +import com.android.internal.os.IResultReceiver; import java.util.ArrayList; import java.util.List; @@ -689,14 +690,6 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM return true; } - case MOVE_TASK_TO_BACK_TRANSACTION: { - data.enforceInterface(IActivityManager.descriptor); - int task = data.readInt(); - moveTaskToBack(task); - reply.writeNoException(); - return true; - } - case MOVE_ACTIVITY_TASK_TO_BACK_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); IBinder token = data.readStrongBinder(); @@ -728,7 +721,6 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM case RESIZE_STACK_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); int stackId = data.readInt(); - float weight = data.readFloat(); Rect r = Rect.CREATOR.createFromParcel(data); resizeStack(stackId, r); reply.writeNoException(); @@ -774,6 +766,14 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM return true; } + case GET_FOCUSED_STACK_ID_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + int focusedStackId = getFocusedStackId(); + reply.writeNoException(); + reply.writeInt(focusedStackId); + return true; + } + case REGISTER_TASK_STACK_LISTENER_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); IBinder token = data.readStrongBinder(); @@ -2114,6 +2114,15 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM return true; } + case REQUEST_ASSIST_CONTEXT_EXTRAS_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + int requestType = data.readInt(); + IResultReceiver receiver = IResultReceiver.Stub.asInterface(data.readStrongBinder()); + requestAssistContextExtras(requestType, receiver); + reply.writeNoException(); + return true; + } + case REPORT_ASSIST_CONTEXT_EXTRAS_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); IBinder token = data.readStrongBinder(); @@ -2183,13 +2192,13 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM return true; } - case CREATE_ACTIVITY_CONTAINER_TRANSACTION: { + case CREATE_VIRTUAL_ACTIVITY_CONTAINER_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); IBinder parentActivityToken = data.readStrongBinder(); IActivityContainerCallback callback = IActivityContainerCallback.Stub.asInterface(data.readStrongBinder()); IActivityContainer activityContainer = - createActivityContainer(parentActivityToken, callback); + createVirtualActivityContainer(parentActivityToken, callback); reply.writeNoException(); if (activityContainer != null) { reply.writeInt(1); @@ -2209,6 +2218,20 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM return true; } + case CREATE_STACK_ON_DISPLAY: { + data.enforceInterface(IActivityManager.descriptor); + int displayId = data.readInt(); + IActivityContainer activityContainer = createStackOnDisplay(displayId); + reply.writeNoException(); + if (activityContainer != null) { + reply.writeInt(1); + reply.writeStrongBinder(activityContainer.asBinder()); + } else { + reply.writeInt(0); + } + return true; + } + case GET_ACTIVITY_DISPLAY_ID_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); IBinder activityToken = data.readStrongBinder(); @@ -2271,6 +2294,14 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM return true; } + case GET_LOCK_TASK_MODE_STATE_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + final int lockTaskModeState = getLockTaskModeState(); + reply.writeNoException(); + reply.writeInt(lockTaskModeState); + return true; + } + case SET_TASK_DESCRIPTION_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); IBinder token = data.readStrongBinder(); @@ -2281,6 +2312,24 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM return true; } + case SET_TASK_RESIZEABLE_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + int taskId = data.readInt(); + boolean resizeable = (data.readInt() == 1) ? true : false; + setTaskResizeable(taskId, resizeable); + reply.writeNoException(); + return true; + } + + case RESIZE_TASK_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + int taskId = data.readInt(); + Rect r = Rect.CREATOR.createFromParcel(data); + resizeTask(taskId, r); + reply.writeNoException(); + return true; + } + case GET_TASK_DESCRIPTION_ICON_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); String filename = data.readString(); @@ -2374,6 +2423,33 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM reply.writeNoException(); return true; } + + case SET_DUMP_HEAP_DEBUG_LIMIT_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + String procName = data.readString(); + long maxMemSize = data.readLong(); + setDumpHeapDebugLimit(procName, maxMemSize); + reply.writeNoException(); + return true; + } + + case DUMP_HEAP_FINISHED_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + String path = data.readString(); + dumpHeapFinished(path); + reply.writeNoException(); + return true; + } + + case SET_VOICE_KEEP_AWAKE_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + IVoiceInteractionSession session = IVoiceInteractionSession.Stub.asInterface( + data.readStrongBinder()); + boolean keepAwake = data.readInt() != 0; + setVoiceKeepAwake(session, keepAwake); + reply.writeNoException(); + return true; + } } return super.onTransact(code, data, reply, flags); @@ -2994,7 +3070,7 @@ class ActivityManagerProxy implements IActivityManager ArrayList<IAppTask> list = null; int N = reply.readInt(); if (N >= 0) { - list = new ArrayList<IAppTask>(); + list = new ArrayList<>(); while (N > 0) { IAppTask task = IAppTask.Stub.asInterface(reply.readStrongBinder()); list.add(task); @@ -3032,7 +3108,8 @@ class ActivityManagerProxy implements IActivityManager reply.recycle(); return size; } - public List getTasks(int maxNum, int flags) throws RemoteException { + public List<ActivityManager.RunningTaskInfo> getTasks(int maxNum, int flags) + throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); @@ -3040,10 +3117,10 @@ class ActivityManagerProxy implements IActivityManager data.writeInt(flags); mRemote.transact(GET_TASKS_TRANSACTION, data, reply, 0); reply.readException(); - ArrayList list = null; + ArrayList<ActivityManager.RunningTaskInfo> list = null; int N = reply.readInt(); if (N >= 0) { - list = new ArrayList(); + list = new ArrayList<>(); while (N > 0) { ActivityManager.RunningTaskInfo info = ActivityManager.RunningTaskInfo.CREATOR @@ -3087,7 +3164,8 @@ class ActivityManagerProxy implements IActivityManager reply.recycle(); return taskThumbnail; } - public List getServices(int maxNum, int flags) throws RemoteException { + public List<ActivityManager.RunningServiceInfo> getServices(int maxNum, int flags) + throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); @@ -3095,10 +3173,10 @@ class ActivityManagerProxy implements IActivityManager data.writeInt(flags); mRemote.transact(GET_SERVICES_TRANSACTION, data, reply, 0); reply.readException(); - ArrayList list = null; + ArrayList<ActivityManager.RunningServiceInfo> list = null; int N = reply.readInt(); if (N >= 0) { - list = new ArrayList(); + list = new ArrayList<>(); while (N > 0) { ActivityManager.RunningServiceInfo info = ActivityManager.RunningServiceInfo.CREATOR @@ -3168,17 +3246,6 @@ class ActivityManagerProxy implements IActivityManager data.recycle(); reply.recycle(); } - public void moveTaskToBack(int task) throws RemoteException - { - Parcel data = Parcel.obtain(); - Parcel reply = Parcel.obtain(); - data.writeInterfaceToken(IActivityManager.descriptor); - data.writeInt(task); - mRemote.transact(MOVE_TASK_TO_BACK_TRANSACTION, data, reply, 0); - reply.readException(); - data.recycle(); - reply.recycle(); - } public boolean moveActivityTaskToBack(IBinder token, boolean nonRoot) throws RemoteException { Parcel data = Parcel.obtain(); @@ -3288,6 +3355,18 @@ class ActivityManagerProxy implements IActivityManager reply.recycle(); } @Override + public int getFocusedStackId() throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + mRemote.transact(GET_FOCUSED_STACK_ID_TRANSACTION, data, reply, 0); + reply.readException(); + int focusedStackId = reply.readInt(); + data.recycle(); + reply.recycle(); + return focusedStackId; + } + @Override public void registerTaskStackListener(ITaskStackListener listener) throws RemoteException { Parcel data = Parcel.obtain(); @@ -5115,6 +5194,19 @@ class ActivityManagerProxy implements IActivityManager return res; } + public void requestAssistContextExtras(int requestType, IResultReceiver receiver) + throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeInt(requestType); + data.writeStrongBinder(receiver.asBinder()); + mRemote.transact(REQUEST_ASSIST_CONTEXT_EXTRAS_TRANSACTION, data, reply, 0); + reply.readException(); + data.recycle(); + reply.recycle(); + } + public void reportAssistContextExtras(IBinder token, Bundle extras) throws RemoteException { Parcel data = Parcel.obtain(); @@ -5211,14 +5303,14 @@ class ActivityManagerProxy implements IActivityManager reply.recycle(); } - public IActivityContainer createActivityContainer(IBinder parentActivityToken, + public IActivityContainer createVirtualActivityContainer(IBinder parentActivityToken, IActivityContainerCallback callback) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); data.writeStrongBinder(parentActivityToken); data.writeStrongBinder(callback == null ? null : callback.asBinder()); - mRemote.transact(CREATE_ACTIVITY_CONTAINER_TRANSACTION, data, reply, 0); + mRemote.transact(CREATE_VIRTUAL_ACTIVITY_CONTAINER_TRANSACTION, data, reply, 0); reply.readException(); final int result = reply.readInt(); final IActivityContainer res; @@ -5245,7 +5337,28 @@ class ActivityManagerProxy implements IActivityManager } @Override - public int getActivityDisplayId(IBinder activityToken) throws RemoteException { + public IActivityContainer createStackOnDisplay(int displayId) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeInt(displayId); + mRemote.transact(CREATE_STACK_ON_DISPLAY, data, reply, 0); + reply.readException(); + final int result = reply.readInt(); + final IActivityContainer res; + if (result == 1) { + res = IActivityContainer.Stub.asInterface(reply.readStrongBinder()); + } else { + res = null; + } + data.recycle(); + reply.recycle(); + return res; + } + + @Override + public int getActivityDisplayId(IBinder activityToken) + throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); @@ -5342,6 +5455,19 @@ class ActivityManagerProxy implements IActivityManager } @Override + public int getLockTaskModeState() throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + mRemote.transact(GET_LOCK_TASK_MODE_STATE_TRANSACTION, data, reply, 0); + reply.readException(); + int lockTaskModeState = reply.readInt(); + data.recycle(); + reply.recycle(); + return lockTaskModeState; + } + + @Override public void setTaskDescription(IBinder token, ActivityManager.TaskDescription values) throws RemoteException { Parcel data = Parcel.obtain(); @@ -5356,6 +5482,33 @@ class ActivityManagerProxy implements IActivityManager } @Override + public void setTaskResizeable(int taskId, boolean resizeable) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeInt(taskId); + data.writeInt(resizeable ? 1 : 0); + mRemote.transact(SET_TASK_RESIZEABLE_TRANSACTION, data, reply, IBinder.FLAG_ONEWAY); + reply.readException(); + data.recycle(); + reply.recycle(); + } + + @Override + public void resizeTask(int taskId, Rect r) throws RemoteException + { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeInt(taskId); + r.writeToParcel(data, 0); + mRemote.transact(RESIZE_TASK_TRANSACTION, data, reply, IBinder.FLAG_ONEWAY); + reply.readException(); + data.recycle(); + reply.recycle(); + } + + @Override public Bitmap getTaskDescriptionIcon(String filename) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); @@ -5490,5 +5643,44 @@ class ActivityManagerProxy implements IActivityManager reply.recycle(); } + @Override + public void setDumpHeapDebugLimit(String processName, long maxMemSize) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeString(processName); + data.writeLong(maxMemSize); + mRemote.transact(SET_DUMP_HEAP_DEBUG_LIMIT_TRANSACTION, data, reply, 0); + reply.readException(); + data.recycle(); + reply.recycle(); + } + + @Override + public void dumpHeapFinished(String path) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeString(path); + mRemote.transact(DUMP_HEAP_FINISHED_TRANSACTION, data, reply, 0); + reply.readException(); + data.recycle(); + reply.recycle(); + } + + @Override + public void setVoiceKeepAwake(IVoiceInteractionSession session, boolean keepAwake) + throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeStrongBinder(session.asBinder()); + data.writeInt(keepAwake ? 1 : 0); + mRemote.transact(SET_VOICE_KEEP_AWAKE_TRANSACTION, data, reply, 0); + reply.readException(); + data.recycle(); + reply.recycle(); + } + private IBinder mRemote; } diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index 39ae65c..8909b28 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -140,6 +140,8 @@ public class ActivityOptions { public static final int ANIM_THUMBNAIL_ASPECT_SCALE_DOWN = 9; /** @hide */ public static final int ANIM_CUSTOM_IN_PLACE = 10; + /** @hide */ + public static final int ANIM_CLIP_REVEAL = 11; private String mPackageName; private int mAnimationType = ANIM_NONE; @@ -291,6 +293,33 @@ public class ActivityOptions { } /** + * Create an ActivityOptions specifying an animation where the new + * activity is revealed from a small originating area of the screen to + * its final full representation. + * + * @param source The View that the new activity is animating from. This + * defines the coordinate space for <var>startX</var> and <var>startY</var>. + * @param startX The x starting location of the new activity, relative to <var>source</var>. + * @param startY The y starting location of the activity, relative to <var>source</var>. + * @param width The initial width of the new activity. + * @param height The initial height of the new activity. + * @return Returns a new ActivityOptions object that you can use to + * supply these options as the options Bundle when starting an activity. + */ + public static ActivityOptions makeClipRevealAnimation(View source, + int startX, int startY, int width, int height) { + ActivityOptions opts = new ActivityOptions(); + opts.mAnimationType = ANIM_CLIP_REVEAL; + int[] pts = new int[2]; + source.getLocationOnScreen(pts); + opts.mStartX = pts[0] + startX; + opts.mStartY = pts[1] + startY; + opts.mWidth = width; + opts.mHeight = height; + return opts; + } + + /** * Create an ActivityOptions specifying an animation where a thumbnail * is scaled from a given position to the new activity window that is * being started. @@ -582,6 +611,7 @@ public class ActivityOptions { break; case ANIM_SCALE_UP: + case ANIM_CLIP_REVEAL: mStartX = opts.getInt(KEY_ANIM_START_X, 0); mStartY = opts.getInt(KEY_ANIM_START_Y, 0); mWidth = opts.getInt(KEY_ANIM_WIDTH, 0); @@ -809,6 +839,7 @@ public class ActivityOptions { b.putInt(KEY_ANIM_IN_PLACE_RES_ID, mCustomInPlaceResId); break; case ANIM_SCALE_UP: + case ANIM_CLIP_REVEAL: b.putInt(KEY_ANIM_START_X, mStartX); b.putInt(KEY_ANIM_START_Y, mStartY); b.putInt(KEY_ANIM_WIDTH, mWidth); diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index dda9952..7b8ec74 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -46,7 +46,6 @@ import android.graphics.Canvas; import android.hardware.display.DisplayManagerGlobal; import android.net.ConnectivityManager; import android.net.IConnectivityManager; -import android.net.LinkProperties; import android.net.Network; import android.net.Proxy; import android.net.ProxyInfo; @@ -87,8 +86,6 @@ import android.util.Slog; import android.util.SuperNotCalledException; import android.view.Display; import android.view.HardwareRenderer; -import android.view.IWindowManager; -import android.view.IWindowSessionCallback; import android.view.View; import android.view.ViewDebug; import android.view.ViewManager; @@ -163,8 +160,8 @@ public final class ActivityThread { private static final boolean DEBUG_PROVIDER = false; private static final long MIN_TIME_BETWEEN_GCS = 5*1000; private static final int SQLITE_MEM_RELEASED_EVENT_LOG_TAG = 75003; - private static final int LOG_ON_PAUSE_CALLED = 30021; - private static final int LOG_ON_RESUME_CALLED = 30022; + private static final int LOG_AM_ON_PAUSE_CALLED = 30021; + private static final int LOG_AM_ON_RESUME_CALLED = 30022; /** Type for IActivityManager.serviceDoneExecuting: anonymous operation */ public static final int SERVICE_DONE_EXECUTING_ANON = 0; @@ -294,6 +291,9 @@ public final class ActivityThread { boolean hideForNow; Configuration newConfig; Configuration createdConfig; + Configuration overrideConfig; + // Used for consolidating configs before sending on to Activity. + private Configuration tmpConfig = new Configuration(); ActivityClientRecord nextIdle; ProfilerInfo profilerInfo; @@ -557,6 +557,15 @@ public final class ActivityThread { int requestType; } + static final class ActivityConfigChangeData { + final IBinder activityToken; + final Configuration overrideConfig; + public ActivityConfigChangeData(IBinder token, Configuration config) { + activityToken = token; + overrideConfig = config; + } + } + private native void dumpGraphicsInfo(FileDescriptor fd); private class ApplicationThread extends ApplicationThreadNative { @@ -616,12 +625,13 @@ public final class ActivityThread { // we use token to identify this activity without having to send the // activity itself back to the activity manager. (matters more with ipc) + @Override public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident, - ActivityInfo info, Configuration curConfig, CompatibilityInfo compatInfo, - String referrer, IVoiceInteractor voiceInteractor, int procState, Bundle state, - PersistableBundle persistentState, List<ResultInfo> pendingResults, - List<ReferrerIntent> pendingNewIntents, boolean notResumed, boolean isForward, - ProfilerInfo profilerInfo) { + ActivityInfo info, Configuration curConfig, Configuration overrideConfig, + CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor, + int procState, Bundle state, PersistableBundle persistentState, + List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents, + boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) { updateProcessState(procState, false); @@ -645,16 +655,19 @@ public final class ActivityThread { r.profilerInfo = profilerInfo; + r.overrideConfig = overrideConfig; updatePendingConfiguration(curConfig); sendMessage(H.LAUNCH_ACTIVITY, r); } + @Override public final void scheduleRelaunchActivity(IBinder token, List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents, - int configChanges, boolean notResumed, Configuration config) { + int configChanges, boolean notResumed, Configuration config, + Configuration overrideConfig) { requestRelaunchActivity(token, pendingResults, pendingNewIntents, - configChanges, notResumed, config, true); + configChanges, notResumed, config, overrideConfig, true); } public final void scheduleNewIntent(List<ReferrerIntent> intents, IBinder token) { @@ -884,14 +897,19 @@ public final class ActivityThread { sticky, sendingUser); } + @Override public void scheduleLowMemory() { sendMessage(H.LOW_MEMORY, null); } - public void scheduleActivityConfigurationChanged(IBinder token) { - sendMessage(H.ACTIVITY_CONFIGURATION_CHANGED, token); + @Override + public void scheduleActivityConfigurationChanged( + IBinder token, Configuration overrideConfig) { + sendMessage(H.ACTIVITY_CONFIGURATION_CHANGED, + new ActivityConfigChangeData(token, overrideConfig)); } + @Override public void profilerControl(boolean start, ProfilerInfo profilerInfo, int profileType) { sendMessage(H.PROFILER_CONTROL, profilerInfo, start ? 1 : 0, profileType); } @@ -1081,7 +1099,7 @@ public final class ActivityThread { @Override public void dumpGfxInfo(FileDescriptor fd, String[] args) { dumpGraphicsInfo(fd); - WindowManagerGlobal.getInstance().dumpGfxInfo(fd); + WindowManagerGlobal.getInstance().dumpGfxInfo(fd, args); } private void dumpDatabaseInfo(FileDescriptor fd, String[] args) { @@ -1451,7 +1469,7 @@ public final class ActivityThread { break; case ACTIVITY_CONFIGURATION_CHANGED: Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityConfigChanged"); - handleActivityConfigurationChanged((IBinder)msg.obj); + handleActivityConfigurationChanged((ActivityConfigChangeData)msg.obj); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); break; case PROFILER_CONTROL: @@ -1669,7 +1687,7 @@ public final class ActivityThread { String[] libDirs, int displayId, Configuration overrideConfiguration, LoadedApk pkgInfo) { return mResourcesManager.getTopLevelResources(resDir, splitResDirs, overlayDirs, libDirs, - displayId, overrideConfiguration, pkgInfo.getCompatibilityInfo(), null); + displayId, overrideConfiguration, pkgInfo.getCompatibilityInfo()); } final Handler getHandler() { @@ -2352,31 +2370,28 @@ public final class ActivityThread { return activity; } - private Context createBaseContextForActivity(ActivityClientRecord r, - final Activity activity) { - ContextImpl appContext = ContextImpl.createActivityContext(this, r.packageInfo, r.token); - appContext.setOuterContext(activity); - Context baseContext = appContext; - - final DisplayManagerGlobal dm = DisplayManagerGlobal.getInstance(); + private Context createBaseContextForActivity(ActivityClientRecord r, final Activity activity) { + int displayId = Display.DEFAULT_DISPLAY; try { - final int displayId = ActivityManagerNative.getDefault().getActivityDisplayId(r.token); - if (displayId > Display.DEFAULT_DISPLAY) { - Display display = dm.getRealDisplay(displayId, r.token); - baseContext = appContext.createDisplayContext(display); - } + displayId = ActivityManagerNative.getDefault().getActivityDisplayId(r.token); } catch (RemoteException e) { } + ContextImpl appContext = ContextImpl.createActivityContext( + this, r.packageInfo, displayId, r.overrideConfig); + appContext.setOuterContext(activity); + Context baseContext = appContext; + + final DisplayManagerGlobal dm = DisplayManagerGlobal.getInstance(); // For debugging purposes, if the activity's package name contains the value of // the "debug.use-second-display" system property as a substring, then show // its content on a secondary display if there is one. String pkgName = SystemProperties.get("debug.second-display.pkg"); if (pkgName != null && !pkgName.isEmpty() && r.packageInfo.mPackageName.contains(pkgName)) { - for (int displayId : dm.getDisplayIds()) { - if (displayId != Display.DEFAULT_DISPLAY) { - Display display = dm.getRealDisplay(displayId, r.token); + for (int id : dm.getDisplayIds()) { + if (id != Display.DEFAULT_DISPLAY) { + Display display = dm.getRealDisplay(id, r.overrideConfig); baseContext = appContext.createDisplayContext(display); break; } @@ -2504,6 +2519,17 @@ public final class ActivityThread { if (r != null) { r.activity.getApplication().dispatchOnProvideAssistData(r.activity, data); r.activity.onProvideAssistData(data); + if (cmd.requestType == ActivityManager.ASSIST_CONTEXT_FULL) { + data.putParcelable(AssistStructure.ASSIST_KEY, new AssistStructure(r.activity)); + AssistContent content = new AssistContent(); + Intent intent = new Intent(r.activity.getIntent()); + intent.setFlags(intent.getFlags() & ~(Intent.FLAG_GRANT_WRITE_URI_PERMISSION + | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)); + intent.removeUnsafeExtras(); + content.setIntent(intent); + r.activity.onProvideAssistContent(content); + data.putParcelable(AssistContent.ASSIST_KEY, content); + } } if (data.isEmpty()) { data = null; @@ -2995,7 +3021,7 @@ public final class ActivityThread { } r.activity.performResume(); - EventLog.writeEvent(LOG_ON_RESUME_CALLED, + EventLog.writeEvent(LOG_AM_ON_RESUME_CALLED, UserHandle.myUserId(), r.activity.getComponentName().getClassName()); r.paused = false; @@ -3090,10 +3116,14 @@ public final class ActivityThread { if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) { if (r.newConfig != null) { + r.tmpConfig.setTo(r.newConfig); + if (r.overrideConfig != null) { + r.tmpConfig.updateFrom(r.overrideConfig); + } if (DEBUG_CONFIGURATION) Slog.v(TAG, "Resuming activity " - + r.activityInfo.name + " with newConfig " + r.newConfig); - performConfigurationChanged(r.activity, r.newConfig); - freeTextLayoutCachesIfNeeded(r.activity.mCurrentConfig.diff(r.newConfig)); + + r.activityInfo.name + " with newConfig " + r.tmpConfig); + performConfigurationChanged(r.activity, r.tmpConfig); + freeTextLayoutCachesIfNeeded(r.activity.mCurrentConfig.diff(r.tmpConfig)); r.newConfig = null; } if (localLOGV) Slog.v(TAG, "Resuming " + r + " with isForward=" @@ -3265,7 +3295,7 @@ public final class ActivityThread { // Now we are idle. r.activity.mCalled = false; mInstrumentation.callActivityOnPause(r.activity); - EventLog.writeEvent(LOG_ON_PAUSE_CALLED, UserHandle.myUserId(), + EventLog.writeEvent(LOG_AM_ON_PAUSE_CALLED, UserHandle.myUserId(), r.activity.getComponentName().getClassName()); if (!r.activity.mCalled) { throw new SuperNotCalledException( @@ -3422,10 +3452,14 @@ public final class ActivityThread { } } if (r.newConfig != null) { + r.tmpConfig.setTo(r.newConfig); + if (r.overrideConfig != null) { + r.tmpConfig.updateFrom(r.overrideConfig); + } if (DEBUG_CONFIGURATION) Slog.v(TAG, "Updating activity vis " - + r.activityInfo.name + " with new config " + r.newConfig); - performConfigurationChanged(r.activity, r.newConfig); - freeTextLayoutCachesIfNeeded(r.activity.mCurrentConfig.diff(r.newConfig)); + + r.activityInfo.name + " with new config " + r.tmpConfig); + performConfigurationChanged(r.activity, r.tmpConfig); + freeTextLayoutCachesIfNeeded(r.activity.mCurrentConfig.diff(r.tmpConfig)); r.newConfig = null; } } else { @@ -3559,7 +3593,7 @@ public final class ActivityThread { // request all activities to relaunch for the changes to take place for (Map.Entry<IBinder, ActivityClientRecord> entry : mActivities.entrySet()) { - requestRelaunchActivity(entry.getKey(), null, null, 0, false, null, false); + requestRelaunchActivity(entry.getKey(), null, null, 0, false, null, null, false); } } } @@ -3662,7 +3696,7 @@ public final class ActivityThread { try { r.activity.mCalled = false; mInstrumentation.callActivityOnPause(r.activity); - EventLog.writeEvent(LOG_ON_PAUSE_CALLED, UserHandle.myUserId(), + EventLog.writeEvent(LOG_AM_ON_PAUSE_CALLED, UserHandle.myUserId(), r.activity.getComponentName().getClassName()); if (!r.activity.mCalled) { throw new SuperNotCalledException( @@ -3803,7 +3837,7 @@ public final class ActivityThread { public final void requestRelaunchActivity(IBinder token, List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents, int configChanges, boolean notResumed, Configuration config, - boolean fromServer) { + Configuration overrideConfig, boolean fromServer) { ActivityClientRecord target = null; synchronized (mResourcesManager) { @@ -3838,6 +3872,7 @@ public final class ActivityThread { ActivityClientRecord existing = mActivities.get(token); if (existing != null) { target.startsNotResumed = existing.paused; + target.overrideConfig = existing.overrideConfig; } target.onlyLocalRequest = true; } @@ -3852,6 +3887,9 @@ public final class ActivityThread { if (config != null) { target.createdConfig = config; } + if (overrideConfig != null) { + target.overrideConfig = overrideConfig; + } target.pendingConfigChanges |= configChanges; } } @@ -3964,6 +4002,7 @@ public final class ActivityThread { } } r.startsNotResumed = tmp.startsNotResumed; + r.overrideConfig = tmp.overrideConfig; handleLaunchActivity(r, currentIntent); } @@ -4151,16 +4190,21 @@ public final class ActivityThread { } } - final void handleActivityConfigurationChanged(IBinder token) { - ActivityClientRecord r = mActivities.get(token); + final void handleActivityConfigurationChanged(ActivityConfigChangeData data) { + ActivityClientRecord r = mActivities.get(data.activityToken); if (r == null || r.activity == null) { return; } if (DEBUG_CONFIGURATION) Slog.v(TAG, "Handle activity config changed: " + r.activityInfo.name); - - performConfigurationChanged(r.activity, mCompatConfiguration); + + r.tmpConfig.setTo(mCompatConfiguration); + if (data.overrideConfig != null) { + r.overrideConfig = data.overrideConfig; + r.tmpConfig.updateFrom(data.overrideConfig); + } + performConfigurationChanged(r.activity, r.tmpConfig); freeTextLayoutCachesIfNeeded(r.activity.mCurrentConfig.diff(mCompatConfiguration)); @@ -4212,6 +4256,10 @@ public final class ActivityThread { } else { Debug.dumpNativeHeap(dhd.fd.getFileDescriptor()); } + try { + ActivityManagerNative.getDefault().dumpHeapFinished(dhd.path); + } catch (RemoteException e) { + } } final void handleDispatchPackageBroadcast(int cmd, String[] packages) { diff --git a/core/java/android/app/ActivityTransitionCoordinator.java b/core/java/android/app/ActivityTransitionCoordinator.java index e3b27b5..2939322 100644 --- a/core/java/android/app/ActivityTransitionCoordinator.java +++ b/core/java/android/app/ActivityTransitionCoordinator.java @@ -206,7 +206,6 @@ 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, @@ -253,17 +252,10 @@ abstract class ActivityTransitionCoordinator extends ResultReceiver { 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); - } - } + } else if (!isNested(view, sharedElements)) { + mSharedElementNames.add(name); + mSharedElements.add(view); + sharedElements.removeAt(i); } } isFirstRun = false; @@ -520,24 +512,9 @@ abstract class ActivityTransitionCoordinator extends ResultReceiver { } 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) { + final int index = mSharedElementParentMatrices == null ? -1 + : mSharedElements.indexOf(view); + if (index < 0) { matrix.reset(); ViewParent viewParent = view.getParent(); if (viewParent instanceof ViewGroup) { @@ -545,6 +522,11 @@ abstract class ActivityTransitionCoordinator extends ResultReceiver { ViewGroup parent = (ViewGroup) viewParent; parent.transformMatrixToLocal(matrix); } + } else { + // The indices of mSharedElementParentMatrices matches the + // mSharedElement matrices. + Matrix parentMatrix = mSharedElementParentMatrices.get(index); + matrix.set(parentMatrix); } } @@ -701,7 +683,6 @@ abstract class ActivityTransitionCoordinator extends ResultReceiver { mResultReceiver = null; mPendingTransition = null; mListener = null; - mRootSharedElements.clear(); mSharedElementParentMatrices = null; } @@ -817,9 +798,12 @@ abstract class ActivityTransitionCoordinator extends ResultReceiver { ViewGroup decor = getDecor(); if (decor != null) { boolean moveWithParent = moveSharedElementWithParent(); + Matrix tempMatrix = new Matrix(); for (int i = 0; i < numSharedElements; i++) { View view = mSharedElements.get(i); - GhostView.addGhost(view, decor); + tempMatrix.reset(); + mSharedElementParentMatrices.get(i).invert(tempMatrix); + GhostView.addGhost(view, decor, tempMatrix); ViewGroup parent = (ViewGroup) view.getParent(); if (moveWithParent && !isInTransitionGroup(parent, decor)) { GhostViewListeners listener = new GhostViewListeners(view, parent, decor); diff --git a/core/java/android/app/ActivityTransitionState.java b/core/java/android/app/ActivityTransitionState.java index a2bfa4e..5c6fe46 100644 --- a/core/java/android/app/ActivityTransitionState.java +++ b/core/java/android/app/ActivityTransitionState.java @@ -18,7 +18,6 @@ package android.app; import android.os.Bundle; import android.os.ResultReceiver; import android.transition.Transition; -import android.util.ArrayMap; import android.util.SparseArray; import android.view.View; import android.view.ViewGroup; diff --git a/core/java/android/app/ActivityView.java b/core/java/android/app/ActivityView.java index fecaf6f..2cb27b0 100644 --- a/core/java/android/app/ActivityView.java +++ b/core/java/android/app/ActivityView.java @@ -82,7 +82,7 @@ public class ActivityView extends ViewGroup { try { mActivityContainer = new ActivityContainerWrapper( - ActivityManagerNative.getDefault().createActivityContainer( + ActivityManagerNative.getDefault().createVirtualActivityContainer( mActivity.getActivityToken(), new ActivityContainerCallback(this))); } catch (RemoteException e) { throw new RuntimeException("ActivityView: Unable to create ActivityContainer. " diff --git a/core/java/android/app/AlarmManager.java b/core/java/android/app/AlarmManager.java index 2c596e5..5dd02ae 100644 --- a/core/java/android/app/AlarmManager.java +++ b/core/java/android/app/AlarmManager.java @@ -26,7 +26,6 @@ import android.os.Parcelable; import android.os.RemoteException; import android.os.UserHandle; import android.os.WorkSource; -import android.os.Parcelable.Creator; /** * This class provides access to the system alarm services. These allow you diff --git a/core/java/android/app/AlertDialog.java b/core/java/android/app/AlertDialog.java index 3c6458f..3e545f9 100644 --- a/core/java/android/app/AlertDialog.java +++ b/core/java/android/app/AlertDialog.java @@ -18,6 +18,10 @@ package android.app; import com.android.internal.app.AlertController; +import android.annotation.ArrayRes; +import android.annotation.AttrRes; +import android.annotation.DrawableRes; +import android.annotation.StringRes; import android.content.Context; import android.content.DialogInterface; import android.database.Cursor; @@ -34,6 +38,8 @@ import android.widget.Button; import android.widget.ListAdapter; import android.widget.ListView; +import com.android.internal.R; + /** * A subclass of Dialog that can display one, two or three buttons. If you only want to * display a String in this dialog box, use the setMessage() method. If you @@ -44,7 +50,7 @@ import android.widget.ListView; * FrameLayout fl = (FrameLayout) findViewById(android.R.id.custom); * fl.addView(myView, new LayoutParams(MATCH_PARENT, WRAP_CONTENT)); * </pre> - * + * * <p>The AlertDialog class takes care of automatically setting * {@link WindowManager.LayoutParams#FLAG_ALT_FOCUSABLE_IM * WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM} for you based on whether @@ -66,31 +72,46 @@ public class AlertDialog extends Dialog implements DialogInterface { /** * Special theme constant for {@link #AlertDialog(Context, int)}: use * the traditional (pre-Holo) alert dialog theme. + * + * @deprecated Use {@link android.R.style#Theme_Material_Dialog_Alert}. */ + @Deprecated public static final int THEME_TRADITIONAL = 1; - + /** * Special theme constant for {@link #AlertDialog(Context, int)}: use * the holographic alert theme with a dark background. + * + * @deprecated Use {@link android.R.style#Theme_Material_Dialog_Alert}. */ + @Deprecated public static final int THEME_HOLO_DARK = 2; - + /** * Special theme constant for {@link #AlertDialog(Context, int)}: use * the holographic alert theme with a light background. + * + * @deprecated Use {@link android.R.style#Theme_Material_Light_Dialog_Alert}. */ + @Deprecated public static final int THEME_HOLO_LIGHT = 3; /** * Special theme constant for {@link #AlertDialog(Context, int)}: use * the device's default alert theme with a dark background. + * + * @deprecated Use {@link android.R.style#Theme_DeviceDefault_Dialog_Alert}. */ + @Deprecated public static final int THEME_DEVICE_DEFAULT_DARK = 4; /** * Special theme constant for {@link #AlertDialog(Context, int)}: use * the device's default alert theme with a light background. + * + * @deprecated Use {@link android.R.style#Theme_DeviceDefault_Light_Dialog_Alert}. */ + @Deprecated public static final int THEME_DEVICE_DEFAULT_LIGHT = 5; /** @@ -104,55 +125,92 @@ public class AlertDialog extends Dialog implements DialogInterface { * @hide */ public static final int LAYOUT_HINT_SIDE = 1; - + + /** + * Creates an alert dialog that uses the default alert dialog theme. + * <p> + * The default alert dialog theme is defined by + * {@link android.R.attr#alertDialogTheme} within the parent + * {@code context}'s theme. + * + * @param context the parent context + */ protected AlertDialog(Context context) { - this(context, resolveDialogTheme(context, 0), true); + this(context, 0); } /** - * Construct an AlertDialog that uses an explicit theme. The actual style - * that an AlertDialog uses is a private implementation, however you can - * here supply either the name of an attribute in the theme from which - * to get the dialog's style (such as {@link android.R.attr#alertDialogTheme} - * or one of the constants {@link #THEME_TRADITIONAL}, - * {@link #THEME_HOLO_DARK}, or {@link #THEME_HOLO_LIGHT}. + * Creates an alert dialog that uses the default alert dialog theme and a + * custom cancel listener. + * <p> + * This is functionally identical to: + * <pre> + * AlertDialog dialog = new AlertDialog(context); + * alertDialog.setCancelable(cancelable); + * alertDialog.setOnCancelListener(cancelListener); + * </pre> + * <p> + * The default alert dialog theme is defined by + * {@link android.R.attr#alertDialogTheme} within the parent + * {@code context}'s theme. + * + * @param context the parent context */ - protected AlertDialog(Context context, int theme) { - this(context, theme, true); + protected AlertDialog(Context context, boolean cancelable, OnCancelListener cancelListener) { + this(context, 0); + + setCancelable(cancelable); + setOnCancelListener(cancelListener); } - AlertDialog(Context context, int theme, boolean createThemeContextWrapper) { - super(context, resolveDialogTheme(context, theme), createThemeContextWrapper); + /** + * Creates an alert dialog that uses an explicit theme resource. + * <p> + * The specified theme resource ({@code themeResId}) is applied on top of + * the parent {@code context}'s theme. It may be specified as a style + * resource containing a fully-populated theme, such as + * {@link android.R.style#Theme_Material_Dialog}, to replace all attributes + * in the parent {@code context}'s theme including primary and accent + * colors. + * <p> + * To preserve attributes such as primary and accent colors, the + * {@code themeResId} may instead be specified as an overlay theme such as + * {@link android.R.style#ThemeOverlay_Material_Dialog}. This will override + * only the window attributes necessary to style the alert window as a + * dialog. + * <p> + * Alternatively, the {@code themeResId} may be specified as {@code 0} to + * use the parent {@code context}'s resolved value for + * {@link android.R.attr#alertDialogTheme}. + * + * @param context the parent context + * @param themeResId the resource ID of the theme against which to inflate + * this dialog, or {@code 0} to use the parent + * {@code context}'s default alert dialog theme + */ + protected AlertDialog(Context context, @AttrRes int themeResId) { + super(context, resolveDialogTheme(context, themeResId)); mWindow.alwaysReadCloseOnTouchAttr(); mAlert = new AlertController(getContext(), this, getWindow()); } - protected AlertDialog(Context context, boolean cancelable, OnCancelListener cancelListener) { - super(context, resolveDialogTheme(context, 0)); - mWindow.alwaysReadCloseOnTouchAttr(); - setCancelable(cancelable); - setOnCancelListener(cancelListener); - mAlert = new AlertController(context, this, getWindow()); - } - - static int resolveDialogTheme(Context context, int resid) { - if (resid == THEME_TRADITIONAL) { - return com.android.internal.R.style.Theme_Dialog_Alert; - } else if (resid == THEME_HOLO_DARK) { - return com.android.internal.R.style.Theme_Holo_Dialog_Alert; - } else if (resid == THEME_HOLO_LIGHT) { - return com.android.internal.R.style.Theme_Holo_Light_Dialog_Alert; - } else if (resid == THEME_DEVICE_DEFAULT_DARK) { - return com.android.internal.R.style.Theme_DeviceDefault_Dialog_Alert; - } else if (resid == THEME_DEVICE_DEFAULT_LIGHT) { - return com.android.internal.R.style.Theme_DeviceDefault_Light_Dialog_Alert; - } else if (resid >= 0x01000000) { // start of real resource IDs. - return resid; + static int resolveDialogTheme(Context context, int themeResId) { + if (themeResId == THEME_TRADITIONAL) { + return R.style.Theme_Dialog_Alert; + } else if (themeResId == THEME_HOLO_DARK) { + return R.style.Theme_Holo_Dialog_Alert; + } else if (themeResId == THEME_HOLO_LIGHT) { + return R.style.Theme_Holo_Light_Dialog_Alert; + } else if (themeResId == THEME_DEVICE_DEFAULT_DARK) { + return R.style.Theme_DeviceDefault_Dialog_Alert; + } else if (themeResId == THEME_DEVICE_DEFAULT_LIGHT) { + return R.style.Theme_DeviceDefault_Light_Dialog_Alert; + } else if (themeResId >= 0x01000000) { // start of real resource IDs. + return themeResId; } else { - TypedValue outValue = new TypedValue(); - context.getTheme().resolveAttribute(com.android.internal.R.attr.alertDialogTheme, - outValue, true); + final TypedValue outValue = new TypedValue(); + context.getTheme().resolveAttribute(R.attr.alertDialogTheme, outValue, true); return outValue.resourceId; } } @@ -173,13 +231,13 @@ public class AlertDialog extends Dialog implements DialogInterface { /** * Gets the list view used in the dialog. - * + * * @return The {@link ListView} from the dialog. */ public ListView getListView() { return mAlert.getListView(); } - + @Override public void setTitle(CharSequence title) { super.setTitle(title); @@ -192,7 +250,7 @@ public class AlertDialog extends Dialog implements DialogInterface { public void setCustomTitle(View customTitleView) { mAlert.setCustomTitle(customTitleView); } - + public void setMessage(CharSequence message) { mAlert.setMessage(message); } @@ -203,9 +261,9 @@ public class AlertDialog extends Dialog implements DialogInterface { public void setView(View view) { mAlert.setView(view); } - + /** - * Set the view to display in that dialog, specifying the spacing to appear around that + * Set the view to display in that dialog, specifying the spacing to appear around that * view. * * @param view The view to show in the content area of the dialog @@ -229,7 +287,7 @@ public class AlertDialog extends Dialog implements DialogInterface { /** * Set a message to be sent when a button is pressed. - * + * * @param whichButton Which button to set the message for, can be one of * {@link DialogInterface#BUTTON_POSITIVE}, * {@link DialogInterface#BUTTON_NEGATIVE}, or @@ -240,10 +298,10 @@ public class AlertDialog extends Dialog implements DialogInterface { public void setButton(int whichButton, CharSequence text, Message msg) { mAlert.setButton(whichButton, text, null, msg); } - + /** * Set a listener to be invoked when the positive button of the dialog is pressed. - * + * * @param whichButton Which button to set the listener on, can be one of * {@link DialogInterface#BUTTON_POSITIVE}, * {@link DialogInterface#BUTTON_NEGATIVE}, or @@ -263,7 +321,7 @@ public class AlertDialog extends Dialog implements DialogInterface { public void setButton(CharSequence text, Message msg) { setButton(BUTTON_POSITIVE, text, msg); } - + /** * @deprecated Use {@link #setButton(int, CharSequence, Message)} with * {@link DialogInterface#BUTTON_NEGATIVE}. @@ -284,7 +342,7 @@ public class AlertDialog extends Dialog implements DialogInterface { /** * Set a listener to be invoked when button 1 of the dialog is pressed. - * + * * @param text The text to display in button 1. * @param listener The {@link DialogInterface.OnClickListener} to use. * @deprecated Use @@ -327,10 +385,10 @@ public class AlertDialog extends Dialog implements DialogInterface { * @param resId the resourceId of the drawable to use as the icon or 0 * if you don't want an icon. */ - public void setIcon(int resId) { + public void setIcon(@DrawableRes int resId) { mAlert.setIcon(resId); } - + public void setIcon(Drawable icon) { mAlert.setIcon(icon); } @@ -340,7 +398,7 @@ public class AlertDialog extends Dialog implements DialogInterface { * * @param attrId ID of a theme attribute that points to a drawable resource. */ - public void setIconAttribute(int attrId) { + public void setIconAttribute(@AttrRes int attrId) { TypedValue out = new TypedValue(); mContext.getTheme().resolveAttribute(attrId, out, true); mAlert.setIcon(out.resourceId); @@ -349,7 +407,7 @@ public class AlertDialog extends Dialog implements DialogInterface { public void setInverseBackgroundForced(boolean forceInverseBackground) { mAlert.setInverseBackgroundForced(forceInverseBackground); } - + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -367,35 +425,57 @@ public class AlertDialog extends Dialog implements DialogInterface { if (mAlert.onKeyUp(keyCode, event)) return true; return super.onKeyUp(keyCode, event); } - + public static class Builder { private final AlertController.AlertParams P; - private int mTheme; - + private int mThemeResId; + /** - * Constructor using a context for this builder and the {@link AlertDialog} it creates. + * Creates a builder for an alert dialog that uses the default alert + * dialog theme. + * <p> + * The default alert dialog theme is defined by + * {@link android.R.attr#alertDialogTheme} within the parent + * {@code context}'s theme. + * + * @param context the parent context */ public Builder(Context context) { this(context, resolveDialogTheme(context, 0)); } /** - * Constructor using a context and theme for this builder and - * the {@link AlertDialog} it creates. The actual theme - * that an AlertDialog uses is a private implementation, however you can - * here supply either the name of an attribute in the theme from which - * to get the dialog's style (such as {@link android.R.attr#alertDialogTheme} - * or one of the constants - * {@link AlertDialog#THEME_TRADITIONAL AlertDialog.THEME_TRADITIONAL}, - * {@link AlertDialog#THEME_HOLO_DARK AlertDialog.THEME_HOLO_DARK}, or - * {@link AlertDialog#THEME_HOLO_LIGHT AlertDialog.THEME_HOLO_LIGHT}. + * Creates a builder for an alert dialog that uses an explicit theme + * resource. + * <p> + * The specified theme resource ({@code themeResId}) is applied on top + * of the parent {@code context}'s theme. It may be specified as a + * style resource containing a fully-populated theme, such as + * {@link android.R.style#Theme_Material_Dialog}, to replace all + * attributes in the parent {@code context}'s theme including primary + * and accent colors. + * <p> + * To preserve attributes such as primary and accent colors, the + * {@code themeResId} may instead be specified as an overlay theme such + * as {@link android.R.style#ThemeOverlay_Material_Dialog}. This will + * override only the window attributes necessary to style the alert + * window as a dialog. + * <p> + * Alternatively, the {@code themeResId} may be specified as {@code 0} + * to use the parent {@code context}'s resolved value for + * {@link android.R.attr#alertDialogTheme}. + * + * @param context the parent context + * @param themeResId the resource ID of the theme against which to inflate + * this dialog, or {@code 0} to use the parent + * {@code context}'s default alert dialog theme */ - public Builder(Context context, int theme) { + public Builder(Context context, int themeResId) { P = new AlertController.AlertParams(new ContextThemeWrapper( - context, resolveDialogTheme(context, theme))); - mTheme = theme; + context, resolveDialogTheme(context, themeResId))); + mThemeResId = themeResId; } - + /** * Returns a {@link Context} with the appropriate theme for dialogs created by this Builder. * Applications should use this Context for obtaining LayoutInflaters for inflating views @@ -413,11 +493,11 @@ public class AlertDialog extends Dialog implements DialogInterface { * * @return This Builder object to allow for chaining of calls to set methods */ - public Builder setTitle(int titleId) { + public Builder setTitle(@StringRes int titleId) { P.mTitle = P.mContext.getText(titleId); return this; } - + /** * Set the title displayed in the {@link Dialog}. * @@ -427,33 +507,38 @@ public class AlertDialog extends Dialog implements DialogInterface { P.mTitle = title; return this; } - + /** - * Set the title using the custom view {@code customTitleView}. The - * methods {@link #setTitle(int)} and {@link #setIcon(int)} should be - * sufficient for most titles, but this is provided if the title needs - * more customization. Using this will replace the title and icon set - * via the other methods. - * - * @param customTitleView The custom view to use as the title. + * Set the title using the custom view {@code customTitleView}. + * <p> + * The methods {@link #setTitle(int)} and {@link #setIcon(int)} should + * be sufficient for most titles, but this is provided if the title + * needs more customization. Using this will replace the title and icon + * set via the other methods. + * <p> + * <strong>Note:</strong> To ensure consistent styling, the custom view + * should be inflated or constructed using the alert dialog's themed + * context obtained via {@link #getContext()}. * - * @return This Builder object to allow for chaining of calls to set methods + * @param customTitleView the custom view to use as the title + * @return this Builder object to allow for chaining of calls to set + * methods */ public Builder setCustomTitle(View customTitleView) { P.mCustomTitleView = customTitleView; return this; } - + /** * Set the message to display using the given resource id. * * @return This Builder object to allow for chaining of calls to set methods */ - public Builder setMessage(int messageId) { + public Builder setMessage(@StringRes int messageId) { P.mMessage = P.mContext.getText(messageId); return this; } - + /** * Set the message to display. * @@ -463,7 +548,7 @@ public class AlertDialog extends Dialog implements DialogInterface { P.mMessage = message; return this; } - + /** * Set the resource id of the {@link Drawable} to be used in the title. * <p> @@ -471,15 +556,20 @@ public class AlertDialog extends Dialog implements DialogInterface { * * @return This Builder object to allow for chaining of calls to set methods */ - public Builder setIcon(int iconId) { + public Builder setIcon(@DrawableRes int iconId) { P.mIconId = iconId; return this; } - + /** * Set the {@link Drawable} to be used in the title. - * - * @return This Builder object to allow for chaining of calls to set methods + * <p> + * <strong>Note:</strong> To ensure consistent styling, the drawable + * should be inflated or constructed using the alert dialog's themed + * context obtained via {@link #getContext()}. + * + * @return this Builder object to allow for chaining of calls to set + * methods */ public Builder setIcon(Drawable icon) { P.mIcon = icon; @@ -495,7 +585,7 @@ public class AlertDialog extends Dialog implements DialogInterface { * * @param attrId ID of a theme attribute that points to a drawable resource. */ - public Builder setIconAttribute(int attrId) { + public Builder setIconAttribute(@AttrRes int attrId) { TypedValue out = new TypedValue(); P.mContext.getTheme().resolveAttribute(attrId, out, true); P.mIconId = out.resourceId; @@ -509,12 +599,12 @@ public class AlertDialog extends Dialog implements DialogInterface { * * @return This Builder object to allow for chaining of calls to set methods */ - public Builder setPositiveButton(int textId, final OnClickListener listener) { + public Builder setPositiveButton(@StringRes int textId, final OnClickListener listener) { P.mPositiveButtonText = P.mContext.getText(textId); P.mPositiveButtonListener = listener; return this; } - + /** * Set a listener to be invoked when the positive button of the dialog is pressed. * @param text The text to display in the positive button @@ -527,7 +617,7 @@ public class AlertDialog extends Dialog implements DialogInterface { P.mPositiveButtonListener = listener; return this; } - + /** * Set a listener to be invoked when the negative button of the dialog is pressed. * @param textId The resource id of the text to display in the negative button @@ -535,12 +625,12 @@ public class AlertDialog extends Dialog implements DialogInterface { * * @return This Builder object to allow for chaining of calls to set methods */ - public Builder setNegativeButton(int textId, final OnClickListener listener) { + public Builder setNegativeButton(@StringRes int textId, final OnClickListener listener) { P.mNegativeButtonText = P.mContext.getText(textId); P.mNegativeButtonListener = listener; return this; } - + /** * Set a listener to be invoked when the negative button of the dialog is pressed. * @param text The text to display in the negative button @@ -553,7 +643,7 @@ public class AlertDialog extends Dialog implements DialogInterface { P.mNegativeButtonListener = listener; return this; } - + /** * Set a listener to be invoked when the neutral button of the dialog is pressed. * @param textId The resource id of the text to display in the neutral button @@ -561,12 +651,12 @@ public class AlertDialog extends Dialog implements DialogInterface { * * @return This Builder object to allow for chaining of calls to set methods */ - public Builder setNeutralButton(int textId, final OnClickListener listener) { + public Builder setNeutralButton(@StringRes int textId, final OnClickListener listener) { P.mNeutralButtonText = P.mContext.getText(textId); P.mNeutralButtonListener = listener; return this; } - + /** * Set a listener to be invoked when the neutral button of the dialog is pressed. * @param text The text to display in the neutral button @@ -579,7 +669,7 @@ public class AlertDialog extends Dialog implements DialogInterface { P.mNeutralButtonListener = listener; return this; } - + /** * Sets whether the dialog is cancelable or not. Default is true. * @@ -589,7 +679,7 @@ public class AlertDialog extends Dialog implements DialogInterface { P.mCancelable = cancelable; return this; } - + /** * Sets the callback that will be called if the dialog is canceled. * @@ -607,7 +697,7 @@ public class AlertDialog extends Dialog implements DialogInterface { P.mOnCancelListener = onCancelListener; return this; } - + /** * Sets the callback that will be called when the dialog is dismissed for any reason. * @@ -627,19 +717,19 @@ public class AlertDialog extends Dialog implements DialogInterface { P.mOnKeyListener = onKeyListener; return this; } - + /** * Set a list of items to be displayed in the dialog as the content, you will be notified of the * selected item via the supplied listener. This should be an array type i.e. R.array.foo * * @return This Builder object to allow for chaining of calls to set methods */ - public Builder setItems(int itemsId, final OnClickListener listener) { + public Builder setItems(@ArrayRes int itemsId, final OnClickListener listener) { P.mItems = P.mContext.getResources().getTextArray(itemsId); P.mOnClickListener = listener; return this; } - + /** * Set a list of items to be displayed in the dialog as the content, you will be notified of the * selected item via the supplied listener. @@ -651,12 +741,12 @@ public class AlertDialog extends Dialog implements DialogInterface { P.mOnClickListener = listener; return this; } - + /** * Set a list of items, which are supplied by the given {@link ListAdapter}, to be * displayed in the dialog as the content, you will be notified of the * selected item via the supplied listener. - * + * * @param adapter The {@link ListAdapter} to supply the list of items * @param listener The listener that will be called when an item is clicked. * @@ -667,12 +757,12 @@ public class AlertDialog extends Dialog implements DialogInterface { P.mOnClickListener = listener; return this; } - + /** * Set a list of items, which are supplied by the given {@link Cursor}, to be * displayed in the dialog as the content, you will be notified of the * selected item via the supplied listener. - * + * * @param cursor The {@link Cursor} to supply the list of items * @param listener The listener that will be called when an item is clicked. * @param labelColumn The column name on the cursor containing the string to display @@ -687,7 +777,7 @@ public class AlertDialog extends Dialog implements DialogInterface { P.mOnClickListener = listener; return this; } - + /** * Set a list of items to be displayed in the dialog as the content, * you will be notified of the selected item via the supplied listener. @@ -695,7 +785,7 @@ public class AlertDialog extends Dialog implements DialogInterface { * a check mark displayed to the right of the text for each checked * item. Clicking on an item in the list will not dismiss the dialog. * Clicking on a button will dismiss the dialog. - * + * * @param itemsId the resource id of an array i.e. R.array.foo * @param checkedItems specifies which items are checked. It should be null in which case no * items are checked. If non null it must be exactly the same length as the array of @@ -706,7 +796,7 @@ public class AlertDialog extends Dialog implements DialogInterface { * * @return This Builder object to allow for chaining of calls to set methods */ - public Builder setMultiChoiceItems(int itemsId, boolean[] checkedItems, + public Builder setMultiChoiceItems(@ArrayRes int itemsId, boolean[] checkedItems, final OnMultiChoiceClickListener listener) { P.mItems = P.mContext.getResources().getTextArray(itemsId); P.mOnCheckboxClickListener = listener; @@ -714,14 +804,14 @@ public class AlertDialog extends Dialog implements DialogInterface { P.mIsMultiChoice = true; return this; } - + /** * Set a list of items to be displayed in the dialog as the content, * you will be notified of the selected item via the supplied listener. * The list will have a check mark displayed to the right of the text * for each checked item. Clicking on an item in the list will not * dismiss the dialog. Clicking on a button will dismiss the dialog. - * + * * @param items the text of the items to be displayed in the list. * @param checkedItems specifies which items are checked. It should be null in which case no * items are checked. If non null it must be exactly the same length as the array of @@ -732,7 +822,7 @@ public class AlertDialog extends Dialog implements DialogInterface { * * @return This Builder object to allow for chaining of calls to set methods */ - public Builder setMultiChoiceItems(CharSequence[] items, boolean[] checkedItems, + public Builder setMultiChoiceItems(CharSequence[] items, boolean[] checkedItems, final OnMultiChoiceClickListener listener) { P.mItems = items; P.mOnCheckboxClickListener = listener; @@ -740,14 +830,14 @@ public class AlertDialog extends Dialog implements DialogInterface { P.mIsMultiChoice = true; return this; } - + /** * Set a list of items to be displayed in the dialog as the content, * you will be notified of the selected item via the supplied listener. * The list will have a check mark displayed to the right of the text * for each checked item. Clicking on an item in the list will not * dismiss the dialog. Clicking on a button will dismiss the dialog. - * + * * @param cursor the cursor used to provide the items. * @param isCheckedColumn specifies the column name on the cursor to use to determine * whether a checkbox is checked or not. It must return an integer value where 1 @@ -760,7 +850,7 @@ public class AlertDialog extends Dialog implements DialogInterface { * * @return This Builder object to allow for chaining of calls to set methods */ - public Builder setMultiChoiceItems(Cursor cursor, String isCheckedColumn, String labelColumn, + public Builder setMultiChoiceItems(Cursor cursor, String isCheckedColumn, String labelColumn, final OnMultiChoiceClickListener listener) { P.mCursor = cursor; P.mOnCheckboxClickListener = listener; @@ -769,14 +859,14 @@ public class AlertDialog extends Dialog implements DialogInterface { P.mIsMultiChoice = true; return this; } - + /** * Set a list of items to be displayed in the dialog as the content, you will be notified of * the selected item via the supplied listener. This should be an array type i.e. * R.array.foo The list will have a check mark displayed to the right of the text for the * checked item. Clicking on an item in the list will not dismiss the dialog. Clicking on a * button will dismiss the dialog. - * + * * @param itemsId the resource id of an array i.e. R.array.foo * @param checkedItem specifies which item is checked. If -1 no items are checked. * @param listener notified when an item on the list is clicked. The dialog will not be @@ -785,7 +875,7 @@ public class AlertDialog extends Dialog implements DialogInterface { * * @return This Builder object to allow for chaining of calls to set methods */ - public Builder setSingleChoiceItems(int itemsId, int checkedItem, + public Builder setSingleChoiceItems(@ArrayRes int itemsId, int checkedItem, final OnClickListener listener) { P.mItems = P.mContext.getResources().getTextArray(itemsId); P.mOnClickListener = listener; @@ -793,13 +883,13 @@ public class AlertDialog extends Dialog implements DialogInterface { P.mIsSingleChoice = true; return this; } - + /** * Set a list of items to be displayed in the dialog as the content, you will be notified of * the selected item via the supplied listener. The list will have a check mark displayed to * the right of the text for the checked item. Clicking on an item in the list will not * dismiss the dialog. Clicking on a button will dismiss the dialog. - * + * * @param cursor the cursor to retrieve the items from. * @param checkedItem specifies which item is checked. If -1 no items are checked. * @param labelColumn The column name on the cursor containing the string to display in the @@ -810,7 +900,7 @@ public class AlertDialog extends Dialog implements DialogInterface { * * @return This Builder object to allow for chaining of calls to set methods */ - public Builder setSingleChoiceItems(Cursor cursor, int checkedItem, String labelColumn, + public Builder setSingleChoiceItems(Cursor cursor, int checkedItem, String labelColumn, final OnClickListener listener) { P.mCursor = cursor; P.mOnClickListener = listener; @@ -819,13 +909,13 @@ public class AlertDialog extends Dialog implements DialogInterface { P.mIsSingleChoice = true; return this; } - + /** * Set a list of items to be displayed in the dialog as the content, you will be notified of * the selected item via the supplied listener. The list will have a check mark displayed to * the right of the text for the checked item. Clicking on an item in the list will not * dismiss the dialog. Clicking on a button will dismiss the dialog. - * + * * @param items the items to be displayed. * @param checkedItem specifies which item is checked. If -1 no items are checked. * @param listener notified when an item on the list is clicked. The dialog will not be @@ -840,14 +930,14 @@ public class AlertDialog extends Dialog implements DialogInterface { P.mCheckedItem = checkedItem; P.mIsSingleChoice = true; return this; - } - + } + /** * Set a list of items to be displayed in the dialog as the content, you will be notified of * the selected item via the supplied listener. The list will have a check mark displayed to * the right of the text for the checked item. Clicking on an item in the list will not * dismiss the dialog. Clicking on a button will dismiss the dialog. - * + * * @param adapter The {@link ListAdapter} to supply the list of items * @param checkedItem specifies which item is checked. If -1 no items are checked. * @param listener notified when an item on the list is clicked. The dialog will not be @@ -863,26 +953,25 @@ public class AlertDialog extends Dialog implements DialogInterface { P.mIsSingleChoice = true; return this; } - + /** * Sets a listener to be invoked when an item in the list is selected. - * - * @param listener The listener to be invoked. - * @see AdapterView#setOnItemSelectedListener(android.widget.AdapterView.OnItemSelectedListener) * - * @return This Builder object to allow for chaining of calls to set methods + * @param listener the listener to be invoked + * @return this Builder object to allow for chaining of calls to set methods + * @see AdapterView#setOnItemSelectedListener(android.widget.AdapterView.OnItemSelectedListener) */ public Builder setOnItemSelectedListener(final AdapterView.OnItemSelectedListener listener) { P.mOnItemSelectedListener = listener; return this; } - + /** * Set a custom view resource to be the contents of the Dialog. The * resource will be inflated, adding all top-level views to the screen. * * @param layoutResId Resource ID to be inflated. - * @return This Builder object to allow for chaining of calls to set + * @return this Builder object to allow for chaining of calls to set * methods */ public Builder setView(int layoutResId) { @@ -893,12 +982,18 @@ public class AlertDialog extends Dialog implements DialogInterface { } /** - * Set a custom view to be the contents of the Dialog. If the supplied view is an instance - * of a {@link ListView} the light background will be used. + * Sets a custom view to be the contents of the alert dialog. + * <p> + * When using a pre-Holo theme, if the supplied view is an instance of + * a {@link ListView} then the light background will be used. + * <p> + * <strong>Note:</strong> To ensure consistent styling, the custom view + * should be inflated or constructed using the alert dialog's themed + * context obtained via {@link #getContext()}. * - * @param view The view to use as the contents of the Dialog. - * - * @return This Builder object to allow for chaining of calls to set methods + * @param view the view to use as the contents of the alert dialog + * @return this Builder object to allow for chaining of calls to set + * methods */ public Builder setView(View view) { P.mView = view; @@ -906,29 +1001,34 @@ public class AlertDialog extends Dialog implements DialogInterface { P.mViewSpacingSpecified = false; return this; } - - /** - * Set a custom view to be the contents of the Dialog, specifying the - * spacing to appear around that view. If the supplied view is an - * instance of a {@link ListView} the light background will be used. - * - * @param view The view to use as the contents of the Dialog. - * @param viewSpacingLeft Spacing between the left edge of the view and - * the dialog frame - * @param viewSpacingTop Spacing between the top edge of the view and - * the dialog frame - * @param viewSpacingRight Spacing between the right edge of the view - * and the dialog frame - * @param viewSpacingBottom Spacing between the bottom edge of the view - * and the dialog frame - * @return This Builder object to allow for chaining of calls to set + + /** + * Sets a custom view to be the contents of the alert dialog and + * specifies additional padding around that view. + * <p> + * When using a pre-Holo theme, if the supplied view is an instance of + * a {@link ListView} then the light background will be used. + * <p> + * <strong>Note:</strong> To ensure consistent styling, the custom view + * should be inflated or constructed using the alert dialog's themed + * context obtained via {@link #getContext()}. + * + * @param view the view to use as the contents of the alert dialog + * @param viewSpacingLeft spacing between the left edge of the view and + * the dialog frame + * @param viewSpacingTop spacing between the top edge of the view and + * the dialog frame + * @param viewSpacingRight spacing between the right edge of the view + * and the dialog frame + * @param viewSpacingBottom spacing between the bottom edge of the view + * and the dialog frame + * @return this Builder object to allow for chaining of calls to set * methods - * - * - * This is currently hidden because it seems like people should just - * be able to put padding around the view. - * @hide + * + * @hide Remove once the framework usages have been replaced. + * @deprecated Set the padding on the view itself. */ + @Deprecated public Builder setView(View view, int viewSpacingLeft, int viewSpacingTop, int viewSpacingRight, int viewSpacingBottom) { P.mView = view; @@ -940,15 +1040,18 @@ public class AlertDialog extends Dialog implements DialogInterface { P.mViewSpacingBottom = viewSpacingBottom; return this; } - + /** - * Sets the Dialog to use the inverse background, regardless of what the - * contents is. - * - * @param useInverseBackground Whether to use the inverse background - * - * @return This Builder object to allow for chaining of calls to set methods + * Sets the alert dialog to use the inverse background, regardless of + * what the contents is. + * + * @param useInverseBackground whether to use the inverse background + * @return this Builder object to allow for chaining of calls to set methods + * @deprecated This flag is only used for pre-Material themes. Instead, + * specify the window background using on the alert dialog + * theme. */ + @Deprecated public Builder setInverseBackgroundForced(boolean useInverseBackground) { P.mForceInverseBackground = useInverseBackground; return this; @@ -964,13 +1067,15 @@ public class AlertDialog extends Dialog implements DialogInterface { /** - * Creates a {@link AlertDialog} with the arguments supplied to this builder. It does not - * {@link Dialog#show()} the dialog. This allows the user to do any extra processing - * before displaying the dialog. Use {@link #show()} if you don't have any other processing - * to do and want this to be created and displayed. + * Creates an {@link AlertDialog} with the arguments supplied to this + * builder. + * <p> + * Calling this method does not display the dialog. If no additional + * processing is needed, {@link #show()} may be called instead to both + * create and display the dialog. */ public AlertDialog create() { - final AlertDialog dialog = new AlertDialog(P.mContext, mTheme, false); + final AlertDialog dialog = new AlertDialog(P.mContext, mThemeResId); P.apply(dialog.mAlert); dialog.setCancelable(P.mCancelable); if (P.mCancelable) { @@ -985,14 +1090,20 @@ public class AlertDialog extends Dialog implements DialogInterface { } /** - * Creates a {@link AlertDialog} with the arguments supplied to this builder and - * {@link Dialog#show()}'s the dialog. + * Creates an {@link AlertDialog} with the arguments supplied to this + * builder and immediately displays the dialog. + * <p> + * Calling this method is functionally identical to: + * <pre> + * AlertDialog dialog = builder.create(); + * dialog.show(); + * </pre> */ public AlertDialog show() { - AlertDialog dialog = create(); + final AlertDialog dialog = create(); dialog.show(); return dialog; } } - + } diff --git a/core/java/android/app/AppImportanceMonitor.java b/core/java/android/app/AppImportanceMonitor.java index c760e1e..e0d0d8d 100644 --- a/core/java/android/app/AppImportanceMonitor.java +++ b/core/java/android/app/AppImportanceMonitor.java @@ -22,8 +22,6 @@ import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.util.SparseArray; -import android.util.SparseIntArray; - import java.util.List; /** diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 95870cf..4bd2332 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -206,8 +206,10 @@ public class AppOpsManager { public static final int OP_PROJECT_MEDIA = 46; /** @hide Activate a VPN connection without user intervention. */ public static final int OP_ACTIVATE_VPN = 47; + /** @hide Access the WallpaperManagerAPI to write wallpapers. */ + public static final int OP_WRITE_WALLPAPER = 48; /** @hide */ - public static final int _NUM_OP = 48; + public static final int _NUM_OP = 49; /** Access to coarse location information. */ public static final String OPSTR_COARSE_LOCATION = @@ -285,6 +287,7 @@ public class AppOpsManager { OP_TOAST_WINDOW, OP_PROJECT_MEDIA, OP_ACTIVATE_VPN, + OP_WRITE_WALLPAPER, }; /** @@ -340,6 +343,7 @@ public class AppOpsManager { null, null, OPSTR_ACTIVATE_VPN, + null, }; /** @@ -395,6 +399,7 @@ public class AppOpsManager { "TOAST_WINDOW", "PROJECT_MEDIA", "ACTIVATE_VPN", + "WRITE_WALLPAPER", }; /** @@ -450,6 +455,7 @@ public class AppOpsManager { null, // no permission for displaying toasts null, // no permission for projecting media null, // no permission for activating vpn + null, // no permission for supporting wallpaper }; /** @@ -506,6 +512,7 @@ public class AppOpsManager { UserManager.DISALLOW_CREATE_WINDOWS, // TOAST_WINDOW null, //PROJECT_MEDIA UserManager.DISALLOW_CONFIG_VPN, // ACTIVATE_VPN + UserManager.DISALLOW_WALLPAPER, // WRITE_WALLPAPER }; /** @@ -561,6 +568,7 @@ public class AppOpsManager { true, //TOAST_WINDOW false, //PROJECT_MEDIA false, //ACTIVATE_VPN + false, //WALLPAPER }; /** @@ -615,6 +623,7 @@ public class AppOpsManager { AppOpsManager.MODE_ALLOWED, AppOpsManager.MODE_IGNORED, // OP_PROJECT_MEDIA AppOpsManager.MODE_IGNORED, // OP_ACTIVATE_VPN + AppOpsManager.MODE_ALLOWED, }; /** @@ -673,6 +682,7 @@ public class AppOpsManager { false, false, false, + false, }; private static HashMap<String, Integer> sOpStrToOp = new HashMap<String, Integer>(); diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index d808c8b..9f81670 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -16,6 +16,9 @@ package android.app; +import android.annotation.DrawableRes; +import android.annotation.StringRes; +import android.annotation.XmlRes; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Intent; @@ -730,7 +733,7 @@ final class ApplicationPackageManager extends PackageManager { } } - @Override public Drawable getDrawable(String packageName, int resid, + @Override public Drawable getDrawable(String packageName, @DrawableRes int resid, ApplicationInfo appInfo) { ResourceName name = new ResourceName(packageName, resid); Drawable dr = getCachedIcon(name); @@ -1137,7 +1140,7 @@ final class ApplicationPackageManager extends PackageManager { } @Override - public CharSequence getText(String packageName, int resid, + public CharSequence getText(String packageName, @StringRes int resid, ApplicationInfo appInfo) { ResourceName name = new ResourceName(packageName, resid); CharSequence text = getCachedString(name); @@ -1170,7 +1173,7 @@ final class ApplicationPackageManager extends PackageManager { } @Override - public XmlResourceParser getXml(String packageName, int resid, + public XmlResourceParser getXml(String packageName, @XmlRes int resid, ApplicationInfo appInfo) { if (appInfo == null) { try { @@ -1659,7 +1662,7 @@ final class ApplicationPackageManager extends PackageManager { int flags) { try { mPM.addCrossProfileIntentFilter(filter, mContext.getOpPackageName(), - mContext.getUserId(), sourceUserId, targetUserId, flags); + sourceUserId, targetUserId, flags); } catch (RemoteException e) { // Should never happen! } @@ -1671,8 +1674,7 @@ final class ApplicationPackageManager extends PackageManager { @Override public void clearCrossProfileIntentFilters(int sourceUserId) { try { - mPM.clearCrossProfileIntentFilters(sourceUserId, mContext.getOpPackageName(), - mContext.getUserId()); + mPM.clearCrossProfileIntentFilters(sourceUserId, mContext.getOpPackageName()); } catch (RemoteException e) { // Should never happen! } diff --git a/core/java/android/app/ApplicationThreadNative.java b/core/java/android/app/ApplicationThreadNative.java index eb3ddb2..b6989ab 100644 --- a/core/java/android/app/ApplicationThreadNative.java +++ b/core/java/android/app/ApplicationThreadNative.java @@ -140,6 +140,10 @@ public abstract class ApplicationThreadNative extends Binder int ident = data.readInt(); ActivityInfo info = ActivityInfo.CREATOR.createFromParcel(data); Configuration curConfig = Configuration.CREATOR.createFromParcel(data); + Configuration overrideConfig = null; + if (data.readInt() != 0) { + overrideConfig = Configuration.CREATOR.createFromParcel(data); + } CompatibilityInfo compatInfo = CompatibilityInfo.CREATOR.createFromParcel(data); String referrer = data.readString(); IVoiceInteractor voiceInteractor = IVoiceInteractor.Stub.asInterface( @@ -153,8 +157,8 @@ public abstract class ApplicationThreadNative extends Binder boolean isForward = data.readInt() != 0; ProfilerInfo profilerInfo = data.readInt() != 0 ? ProfilerInfo.CREATOR.createFromParcel(data) : null; - scheduleLaunchActivity(intent, b, ident, info, curConfig, compatInfo, referrer, - voiceInteractor, procState, state, persistentState, ri, pi, + scheduleLaunchActivity(intent, b, ident, info, curConfig, overrideConfig, compatInfo, + referrer, voiceInteractor, procState, state, persistentState, ri, pi, notResumed, isForward, profilerInfo); return true; } @@ -167,14 +171,15 @@ public abstract class ApplicationThreadNative extends Binder List<ReferrerIntent> pi = data.createTypedArrayList(ReferrerIntent.CREATOR); int configChanges = data.readInt(); boolean notResumed = data.readInt() != 0; - Configuration config = null; + Configuration config = Configuration.CREATOR.createFromParcel(data); + Configuration overrideConfig = null; if (data.readInt() != 0) { - config = Configuration.CREATOR.createFromParcel(data); + overrideConfig = Configuration.CREATOR.createFromParcel(data); } - scheduleRelaunchActivity(b, ri, pi, configChanges, notResumed, config); + scheduleRelaunchActivity(b, ri, pi, configChanges, notResumed, config, overrideConfig); return true; } - + case SCHEDULE_NEW_INTENT_TRANSACTION: { data.enforceInterface(IApplicationThread.descriptor); @@ -404,7 +409,11 @@ public abstract class ApplicationThreadNative extends Binder { data.enforceInterface(IApplicationThread.descriptor); IBinder b = data.readStrongBinder(); - scheduleActivityConfigurationChanged(b); + Configuration overrideConfig = null; + if (data.readInt() != 0) { + overrideConfig = Configuration.CREATOR.createFromParcel(data); + } + scheduleActivityConfigurationChanged(b, overrideConfig); return true; } @@ -775,11 +784,11 @@ class ApplicationThreadProxy implements IApplicationThread { } public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident, - ActivityInfo info, Configuration curConfig, CompatibilityInfo compatInfo, - String referrer, IVoiceInteractor voiceInteractor, int procState, Bundle state, - PersistableBundle persistentState, List<ResultInfo> pendingResults, - List<ReferrerIntent> pendingNewIntents, boolean notResumed, boolean isForward, - ProfilerInfo profilerInfo) throws RemoteException { + ActivityInfo info, Configuration curConfig, Configuration overrideConfig, + CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor, + int procState, Bundle state, PersistableBundle persistentState, + List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents, + boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) throws RemoteException { Parcel data = Parcel.obtain(); data.writeInterfaceToken(IApplicationThread.descriptor); intent.writeToParcel(data, 0); @@ -787,6 +796,12 @@ class ApplicationThreadProxy implements IApplicationThread { data.writeInt(ident); info.writeToParcel(data, 0); curConfig.writeToParcel(data, 0); + if (overrideConfig != null) { + data.writeInt(1); + overrideConfig.writeToParcel(data, 0); + } else { + data.writeInt(0); + } compatInfo.writeToParcel(data, 0); data.writeString(referrer); data.writeStrongBinder(voiceInteractor != null ? voiceInteractor.asBinder() : null); @@ -810,8 +825,8 @@ class ApplicationThreadProxy implements IApplicationThread { public final void scheduleRelaunchActivity(IBinder token, List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents, - int configChanges, boolean notResumed, Configuration config) - throws RemoteException { + int configChanges, boolean notResumed, Configuration config, + Configuration overrideConfig) throws RemoteException { Parcel data = Parcel.obtain(); data.writeInterfaceToken(IApplicationThread.descriptor); data.writeStrongBinder(token); @@ -819,9 +834,10 @@ class ApplicationThreadProxy implements IApplicationThread { data.writeTypedList(pendingNewIntents); data.writeInt(configChanges); data.writeInt(notResumed ? 1 : 0); - if (config != null) { + config.writeToParcel(data, 0); + if (overrideConfig != null) { data.writeInt(1); - config.writeToParcel(data, 0); + overrideConfig.writeToParcel(data, 0); } else { data.writeInt(0); } @@ -1104,6 +1120,7 @@ class ApplicationThreadProxy implements IApplicationThread { data.recycle(); } + @Override public final void scheduleLowMemory() throws RemoteException { Parcel data = Parcel.obtain(); data.writeInterfaceToken(IApplicationThread.descriptor); @@ -1112,16 +1129,24 @@ class ApplicationThreadProxy implements IApplicationThread { data.recycle(); } + @Override public final void scheduleActivityConfigurationChanged( - IBinder token) throws RemoteException { + IBinder token, Configuration overrideConfig) throws RemoteException { Parcel data = Parcel.obtain(); data.writeInterfaceToken(IApplicationThread.descriptor); data.writeStrongBinder(token); + if (overrideConfig != null) { + data.writeInt(1); + overrideConfig.writeToParcel(data, 0); + } else { + data.writeInt(0); + } mRemote.transact(SCHEDULE_ACTIVITY_CONFIGURATION_CHANGED_TRANSACTION, data, null, IBinder.FLAG_ONEWAY); data.recycle(); } + @Override public void profilerControl(boolean start, ProfilerInfo profilerInfo, int profileType) throws RemoteException { Parcel data = Parcel.obtain(); diff --git a/core/java/android/app/AssistContent.aidl b/core/java/android/app/AssistContent.aidl new file mode 100644 index 0000000..a6321bf --- /dev/null +++ b/core/java/android/app/AssistContent.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015, 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.app; + +parcelable AssistContent; diff --git a/core/java/android/app/AssistContent.java b/core/java/android/app/AssistContent.java new file mode 100644 index 0000000..ace4af7 --- /dev/null +++ b/core/java/android/app/AssistContent.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2015 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.app; + +import android.content.ClipData; +import android.content.Intent; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Holds information about the content an application is viewing, to hand to an + * assistant at the user's request. This is filled in by + * {@link Activity#onProvideAssistContent Activity.onProvideAssistContent}. + */ +public class AssistContent implements Parcelable { + private Intent mIntent; + private ClipData mClipData; + + /** + * Key name this data structure is stored in the Bundle generated by + * {@link Activity#onProvideAssistData}. + */ + public static final String ASSIST_KEY = "android:assist_content"; + + /** + * Retrieve the framework-generated AssistContent that is stored within + * the Bundle filled in by {@link Activity#onProvideAssistContent}. + */ + public static AssistContent getAssistContent(Bundle assistBundle) { + return assistBundle.getParcelable(ASSIST_KEY); + } + + public AssistContent() { + } + + /** + * Sets the Intent associated with the content, describing the current top-level context of + * the activity. If this contains a reference to a piece of data related to the activity, + * be sure to set {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} so the accessibilty + * service can access it. + */ + public void setIntent(Intent intent) { + mIntent = intent; + } + + /** + * Return the current {@link #setIntent}, which you can modify in-place. + */ + public Intent getIntent() { + return mIntent; + } + + /** + * Optional additional content items that are involved with + * the current UI. Access to this content will be granted to the assistant as if you + * are sending it through an Intent with {@link Intent#FLAG_GRANT_READ_URI_PERMISSION}. + */ + public void setClipData(ClipData clip) { + mClipData = clip; + } + + /** + * Return the current {@link #setClipData}, which you can modify in-place. + */ + public ClipData getClipData() { + return mClipData; + } + + AssistContent(Parcel in) { + if (in.readInt() != 0) { + mIntent = Intent.CREATOR.createFromParcel(in); + } + if (in.readInt() != 0) { + mClipData = ClipData.CREATOR.createFromParcel(in); + } + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + if (mIntent != null) { + dest.writeInt(1); + mIntent.writeToParcel(dest, flags); + } else { + dest.writeInt(0); + } + if (mClipData != null) { + dest.writeInt(1); + mClipData.writeToParcel(dest, flags); + } else { + dest.writeInt(0); + } + } + + public static final Parcelable.Creator<AssistContent> CREATOR + = new Parcelable.Creator<AssistContent>() { + public AssistContent createFromParcel(Parcel in) { + return new AssistContent(in); + } + + public AssistContent[] newArray(int size) { + return new AssistContent[size]; + } + }; +} diff --git a/core/java/android/app/AssistStructure.aidl b/core/java/android/app/AssistStructure.aidl new file mode 100644 index 0000000..07fb2453 --- /dev/null +++ b/core/java/android/app/AssistStructure.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015, 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.app; + +parcelable AssistStructure; diff --git a/core/java/android/app/AssistStructure.java b/core/java/android/app/AssistStructure.java new file mode 100644 index 0000000..25153fc --- /dev/null +++ b/core/java/android/app/AssistStructure.java @@ -0,0 +1,618 @@ +/* + * Copyright (C) 2015 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.app; + +import android.content.ComponentName; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.PooledStringReader; +import android.os.PooledStringWriter; +import android.text.TextPaint; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.view.ViewAssistStructure; +import android.view.ViewGroup; +import android.view.ViewRootImpl; +import android.view.WindowManagerGlobal; +import android.widget.Checkable; + +import java.util.ArrayList; + +/** + * Assist data automatically created by the platform's implementation + * of {@link Activity#onProvideAssistData}. Retrieve it from the assist + * data with {@link #getAssistStructure(android.os.Bundle)}. + */ +final public class AssistStructure implements Parcelable { + static final String TAG = "AssistStructure"; + + /** + * Key name this data structure is stored in the Bundle generated by + * {@link Activity#onProvideAssistData}. + */ + public static final String ASSIST_KEY = "android:assist_structure"; + + final ComponentName mActivityComponent; + + final ArrayList<ViewNodeImpl> mRootViews = new ArrayList<>(); + + ViewAssistStructureImpl mTmpViewAssistStructureImpl = new ViewAssistStructureImpl(); + Bundle mTmpExtras = new Bundle(); + + final static class ViewAssistStructureImpl extends ViewAssistStructure { + CharSequence mText; + int mTextSelectionStart = -1; + int mTextSelectionEnd = -1; + int mTextColor = ViewNode.TEXT_COLOR_UNDEFINED; + int mTextBackgroundColor = ViewNode.TEXT_COLOR_UNDEFINED; + float mTextSize = 0; + int mTextStyle = 0; + CharSequence mHint; + + @Override + public void setText(CharSequence text) { + mText = text; + mTextSelectionStart = mTextSelectionEnd = -1; + } + + @Override + public void setText(CharSequence text, int selectionStart, int selectionEnd) { + mText = text; + mTextSelectionStart = selectionStart; + mTextSelectionEnd = selectionEnd; + } + + @Override + public void setTextPaint(TextPaint paint) { + mTextColor = paint.getColor(); + mTextBackgroundColor = paint.bgColor; + mTextSize = paint.getTextSize(); + mTextStyle = 0; + Typeface tf = paint.getTypeface(); + if (tf != null) { + if (tf.isBold()) { + mTextStyle |= ViewNode.TEXT_STYLE_BOLD; + } + if (tf.isItalic()) { + mTextStyle |= ViewNode.TEXT_STYLE_ITALIC; + } + } + int pflags = paint.getFlags(); + if ((pflags& Paint.FAKE_BOLD_TEXT_FLAG) != 0) { + mTextStyle |= ViewNode.TEXT_STYLE_BOLD; + } + if ((pflags& Paint.UNDERLINE_TEXT_FLAG) != 0) { + mTextStyle |= ViewNode.TEXT_STYLE_UNDERLINE; + } + if ((pflags& Paint.STRIKE_THRU_TEXT_FLAG) != 0) { + mTextStyle |= ViewNode.TEXT_STYLE_STRIKE_THRU; + } + } + + @Override + public void setHint(CharSequence hint) { + mHint = hint; + } + + @Override + public CharSequence getText() { + return mText; + } + + @Override + public int getTextSelectionStart() { + return mTextSelectionStart; + } + + @Override + public int getTextSelectionEnd() { + return mTextSelectionEnd; + } + + @Override + public CharSequence getHint() { + return mHint; + } + } + + final static class ViewNodeTextImpl { + final CharSequence mText; + final int mTextSelectionStart; + final int mTextSelectionEnd; + int mTextColor; + int mTextBackgroundColor; + float mTextSize; + int mTextStyle; + final String mHint; + + ViewNodeTextImpl(ViewAssistStructureImpl data) { + mText = data.mText; + mTextSelectionStart = data.mTextSelectionStart; + mTextSelectionEnd = data.mTextSelectionEnd; + mTextColor = data.mTextColor; + mTextBackgroundColor = data.mTextBackgroundColor; + mTextSize = data.mTextSize; + mTextStyle = data.mTextStyle; + mHint = data.mHint != null ? data.mHint.toString() : null; + } + + ViewNodeTextImpl(Parcel in) { + mText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + mTextSelectionStart = in.readInt(); + mTextSelectionEnd = in.readInt(); + mTextColor = in.readInt(); + mTextBackgroundColor = in.readInt(); + mTextSize = in.readFloat(); + mTextStyle = in.readInt(); + mHint = in.readString(); + } + + void writeToParcel(Parcel out) { + TextUtils.writeToParcel(mText, out, 0); + out.writeInt(mTextSelectionStart); + out.writeInt(mTextSelectionEnd); + out.writeInt(mTextColor); + out.writeInt(mTextBackgroundColor); + out.writeFloat(mTextSize); + out.writeInt(mTextStyle); + out.writeString(mHint); + } + } + + final static class ViewNodeImpl { + final int mX; + final int mY; + final int mScrollX; + final int mScrollY; + final int mWidth; + final int mHeight; + + static final int FLAGS_DISABLED = 0x00000001; + static final int FLAGS_VISIBILITY_MASK = View.VISIBLE|View.INVISIBLE|View.GONE; + static final int FLAGS_FOCUSABLE = 0x00000010; + static final int FLAGS_FOCUSED = 0x00000020; + static final int FLAGS_ACCESSIBILITY_FOCUSED = 0x04000000; + static final int FLAGS_SELECTED = 0x00000040; + static final int FLAGS_ACTIVATED = 0x40000000; + static final int FLAGS_CHECKABLE = 0x00000100; + static final int FLAGS_CHECKED = 0x00000200; + static final int FLAGS_CLICKABLE = 0x00004000; + static final int FLAGS_LONG_CLICKABLE = 0x00200000; + + final int mFlags; + + final String mClassName; + final String mContentDescription; + + final ViewNodeTextImpl mText; + final Bundle mExtras; + + final ViewNodeImpl[] mChildren; + + ViewNodeImpl(AssistStructure assistStructure, View view, int left, int top, + CharSequence contentDescription) { + mX = left; + mY = top; + mScrollX = view.getScrollX(); + mScrollY = view.getScrollY(); + mWidth = view.getWidth(); + mHeight = view.getHeight(); + int flags = view.getVisibility(); + if (!view.isEnabled()) { + flags |= FLAGS_DISABLED; + } + if (!view.isClickable()) { + flags |= FLAGS_CLICKABLE; + } + if (!view.isFocusable()) { + flags |= FLAGS_FOCUSABLE; + } + if (!view.isFocused()) { + flags |= FLAGS_FOCUSED; + } + if (!view.isAccessibilityFocused()) { + flags |= FLAGS_ACCESSIBILITY_FOCUSED; + } + if (!view.isSelected()) { + flags |= FLAGS_SELECTED; + } + if (!view.isActivated()) { + flags |= FLAGS_ACTIVATED; + } + if (!view.isLongClickable()) { + flags |= FLAGS_LONG_CLICKABLE; + } + if (view instanceof Checkable) { + flags |= FLAGS_CHECKABLE; + if (((Checkable)view).isChecked()) { + flags |= FLAGS_CHECKED; + } + } + mFlags = flags; + mClassName = view.getAccessibilityClassName().toString(); + mContentDescription = contentDescription != null ? contentDescription.toString() : null; + final ViewAssistStructureImpl viewData = assistStructure.mTmpViewAssistStructureImpl; + final Bundle extras = assistStructure.mTmpExtras; + view.onProvideAssistStructure(viewData, extras); + if (viewData.mText != null || viewData.mHint != null) { + mText = new ViewNodeTextImpl(viewData); + assistStructure.mTmpViewAssistStructureImpl = new ViewAssistStructureImpl(); + } else { + mText = null; + } + if (!extras.isEmpty()) { + mExtras = extras; + assistStructure.mTmpExtras = new Bundle(); + } else { + mExtras = null; + } + if (view instanceof ViewGroup) { + ViewGroup vg = (ViewGroup)view; + final int NCHILDREN = vg.getChildCount(); + if (NCHILDREN > 0) { + mChildren = new ViewNodeImpl[NCHILDREN]; + for (int i=0; i<NCHILDREN; i++) { + mChildren[i] = new ViewNodeImpl(assistStructure, vg.getChildAt(i)); + } + } else { + mChildren = null; + } + } else { + mChildren = null; + } + } + + ViewNodeImpl(AssistStructure assistStructure, View view) { + this(assistStructure, view, view.getLeft(), view.getTop(), view.getContentDescription()); + } + + ViewNodeImpl(Parcel in, PooledStringReader preader) { + mX = in.readInt(); + mY = in.readInt(); + mScrollX = in.readInt(); + mScrollY = in.readInt(); + mWidth = in.readInt(); + mHeight = in.readInt(); + mFlags = in.readInt(); + mClassName = preader.readString(); + mContentDescription = in.readString(); + if (in.readInt() != 0) { + mText = new ViewNodeTextImpl(in); + } else { + mText = null; + } + mExtras = in.readBundle(); + final int NCHILDREN = in.readInt(); + if (NCHILDREN > 0) { + mChildren = new ViewNodeImpl[NCHILDREN]; + for (int i=0; i<NCHILDREN; i++) { + mChildren[i] = new ViewNodeImpl(in, preader); + } + } else { + mChildren = null; + } + } + + void writeToParcel(Parcel out, PooledStringWriter pwriter) { + out.writeInt(mX); + out.writeInt(mY); + out.writeInt(mScrollX); + out.writeInt(mScrollY); + out.writeInt(mWidth); + out.writeInt(mHeight); + out.writeInt(mFlags); + pwriter.writeString(mClassName); + out.writeString(mContentDescription); + if (mText != null) { + out.writeInt(1); + mText.writeToParcel(out); + } else { + out.writeInt(0); + } + out.writeBundle(mExtras); + if (mChildren != null) { + final int NCHILDREN = mChildren.length; + out.writeInt(NCHILDREN); + for (int i=0; i<NCHILDREN; i++) { + mChildren[i].writeToParcel(out, pwriter); + } + } else { + out.writeInt(0); + } + } + } + + /** + * Provides access to information about a single view in the assist data. + */ + static public class ViewNode { + /** + * Magic value for text color that has not been defined, which is very unlikely + * to be confused with a real text color. + */ + public static final int TEXT_COLOR_UNDEFINED = 1; + + public static final int TEXT_STYLE_BOLD = 1<<0; + public static final int TEXT_STYLE_ITALIC = 1<<1; + public static final int TEXT_STYLE_UNDERLINE = 1<<2; + public static final int TEXT_STYLE_STRIKE_THRU = 1<<3; + + ViewNodeImpl mImpl; + + public ViewNode() { + } + + public int getLeft() { + return mImpl.mX; + } + + public int getTop() { + return mImpl.mY; + } + + public int getScrollX() { + return mImpl.mScrollX; + } + + public int getScrollY() { + return mImpl.mScrollY; + } + + public int getWidth() { + return mImpl.mWidth; + } + + public int getHeight() { + return mImpl.mHeight; + } + + public int getVisibility() { + return mImpl.mFlags&ViewNodeImpl.FLAGS_VISIBILITY_MASK; + } + + public boolean isEnabled() { + return (mImpl.mFlags&ViewNodeImpl.FLAGS_DISABLED) == 0; + } + + public boolean isClickable() { + return (mImpl.mFlags&ViewNodeImpl.FLAGS_CLICKABLE) != 0; + } + + public boolean isFocusable() { + return (mImpl.mFlags&ViewNodeImpl.FLAGS_FOCUSABLE) != 0; + } + + public boolean isFocused() { + return (mImpl.mFlags&ViewNodeImpl.FLAGS_FOCUSED) != 0; + } + + public boolean isAccessibilityFocused() { + return (mImpl.mFlags&ViewNodeImpl.FLAGS_ACCESSIBILITY_FOCUSED) != 0; + } + + public boolean isCheckable() { + return (mImpl.mFlags&ViewNodeImpl.FLAGS_CHECKABLE) != 0; + } + + public boolean isChecked() { + return (mImpl.mFlags&ViewNodeImpl.FLAGS_CHECKED) != 0; + } + + public boolean isSelected() { + return (mImpl.mFlags&ViewNodeImpl.FLAGS_SELECTED) != 0; + } + + public boolean isActivated() { + return (mImpl.mFlags&ViewNodeImpl.FLAGS_ACTIVATED) != 0; + } + + public boolean isLongClickable() { + return (mImpl.mFlags&ViewNodeImpl.FLAGS_LONG_CLICKABLE) != 0; + } + + public String getClassName() { + return mImpl.mClassName; + } + + public String getContentDescription() { + return mImpl.mContentDescription; + } + + public CharSequence getText() { + return mImpl.mText != null ? mImpl.mText.mText : null; + } + + public int getTextSelectionStart() { + return mImpl.mText != null ? mImpl.mText.mTextSelectionStart : -1; + } + + public int getTextSelectionEnd() { + return mImpl.mText != null ? mImpl.mText.mTextSelectionEnd : -1; + } + + public int getTextColor() { + return mImpl.mText != null ? mImpl.mText.mTextColor : TEXT_COLOR_UNDEFINED; + } + + public int getTextBackgroundColor() { + return mImpl.mText != null ? mImpl.mText.mTextBackgroundColor : TEXT_COLOR_UNDEFINED; + } + + public float getTextSize() { + return mImpl.mText != null ? mImpl.mText.mTextSize : 0; + } + + public int getTextStyle() { + return mImpl.mText != null ? mImpl.mText.mTextStyle : 0; + } + + public String getHint() { + return mImpl.mText != null ? mImpl.mText.mHint : null; + } + + public Bundle getExtras() { + return mImpl.mExtras; + } + + public int getChildCount() { + return mImpl.mChildren != null ? mImpl.mChildren.length : 0; + } + + public void getChildAt(int index, ViewNode outNode) { + outNode.mImpl = mImpl.mChildren[index]; + } + } + + AssistStructure(Activity activity) { + mActivityComponent = activity.getComponentName(); + ArrayList<ViewRootImpl> views = WindowManagerGlobal.getInstance().getRootViews( + activity.getActivityToken()); + for (int i=0; i<views.size(); i++) { + ViewRootImpl root = views.get(i); + View view = root.getView(); + Rect rect = new Rect(); + view.getBoundsOnScreen(rect); + CharSequence title = root.getTitle(); + mRootViews.add(new ViewNodeImpl(this, view, rect.left, rect.top, + title != null ? title : view.getContentDescription())); + } + } + + AssistStructure(Parcel in) { + PooledStringReader preader = new PooledStringReader(in); + mActivityComponent = ComponentName.readFromParcel(in); + final int N = in.readInt(); + for (int i=0; i<N; i++) { + mRootViews.add(new ViewNodeImpl(in, preader)); + } + //dump(); + } + + /** @hide */ + public void dump() { + Log.i(TAG, "Activity: " + mActivityComponent.flattenToShortString()); + ViewNode node = new ViewNode(); + final int N = getWindowCount(); + for (int i=0; i<N; i++) { + Log.i(TAG, "Window #" + i + ":"); + getWindowAt(i, node); + dump(" ", node); + } + } + + void dump(String prefix, ViewNode node) { + Log.i(TAG, prefix + "View [" + node.getLeft() + "," + node.getTop() + + " " + node.getWidth() + "x" + node.getHeight() + "]" + " " + node.getClassName()); + int scrollX = node.getScrollX(); + int scrollY = node.getScrollY(); + if (scrollX != 0 || scrollY != 0) { + Log.i(TAG, prefix + " Scroll: " + scrollX + "," + scrollY); + } + String contentDescription = node.getContentDescription(); + if (contentDescription != null) { + Log.i(TAG, prefix + " Content description: " + contentDescription); + } + CharSequence text = node.getText(); + if (text != null) { + Log.i(TAG, prefix + " Text (sel " + node.getTextSelectionStart() + "-" + + node.getTextSelectionEnd() + "): " + text); + Log.i(TAG, prefix + " Text size: " + node.getTextSize() + " , style: #" + + node.getTextStyle()); + Log.i(TAG, prefix + " Text color fg: #" + Integer.toHexString(node.getTextColor()) + + ", bg: #" + Integer.toHexString(node.getTextBackgroundColor())); + } + String hint = node.getHint(); + if (hint != null) { + Log.i(TAG, prefix + " Hint: " + hint); + } + Bundle extras = node.getExtras(); + if (extras != null) { + Log.i(TAG, prefix + " Extras: " + extras); + } + final int NCHILDREN = node.getChildCount(); + if (NCHILDREN > 0) { + Log.i(TAG, prefix + " Children:"); + String cprefix = prefix + " "; + ViewNode cnode = new ViewNode(); + for (int i=0; i<NCHILDREN; i++) { + node.getChildAt(i, cnode); + dump(cprefix, cnode); + } + } + } + + /** + * Retrieve the framework-generated AssistStructure that is stored within + * the Bundle filled in by {@link Activity#onProvideAssistData}. + */ + public static AssistStructure getAssistStructure(Bundle assistBundle) { + return assistBundle.getParcelable(ASSIST_KEY); + } + + public ComponentName getActivityComponent() { + return mActivityComponent; + } + + /** + * Return the number of window contents that have been collected in this assist data. + */ + public int getWindowCount() { + return mRootViews.size(); + } + + /** + * Return the root view for one of the windows in the assist data. + * @param index Which window to retrieve, may be 0 to {@link #getWindowCount()}-1. + * @param outNode Node in which to place the window's root view. + */ + public void getWindowAt(int index, ViewNode outNode) { + outNode.mImpl = mRootViews.get(index); + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel out, int flags) { + int start = out.dataPosition(); + PooledStringWriter pwriter = new PooledStringWriter(out); + ComponentName.writeToParcel(mActivityComponent, out); + final int N = mRootViews.size(); + out.writeInt(N); + for (int i=0; i<N; i++) { + mRootViews.get(i).writeToParcel(out, pwriter); + } + pwriter.finish(); + Log.i(TAG, "Flattened assist data: " + (out.dataPosition() - start) + " bytes"); + } + + public static final Parcelable.Creator<AssistStructure> CREATOR + = new Parcelable.Creator<AssistStructure>() { + public AssistStructure createFromParcel(Parcel in) { + return new AssistStructure(in); + } + + public AssistStructure[] newArray(int size) { + return new AssistStructure[size]; + } + }; +} diff --git a/core/java/android/app/BackStackRecord.java b/core/java/android/app/BackStackRecord.java index 2784d44..83451aa 100644 --- a/core/java/android/app/BackStackRecord.java +++ b/core/java/android/app/BackStackRecord.java @@ -25,7 +25,6 @@ import android.text.TextUtils; import android.transition.Transition; import android.transition.TransitionManager; import android.transition.TransitionSet; -import android.transition.TransitionUtils; import android.util.ArrayMap; import android.util.Log; import android.util.LogWriter; diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 2ef046d..eb27830 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -16,18 +16,9 @@ package android.app; -import android.app.usage.IUsageStatsManager; -import android.app.usage.UsageStatsManager; -import android.appwidget.AppWidgetManager; -import android.os.Build; -import android.service.persistentdata.IPersistentDataBlockService; -import android.service.persistentdata.PersistentDataBlockManager; - -import com.android.internal.appwidget.IAppWidgetService; -import com.android.internal.policy.PolicyManager; +import com.android.internal.annotations.GuardedBy; import com.android.internal.util.Preconditions; -import android.bluetooth.BluetoothManager; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentProvider; @@ -35,19 +26,15 @@ import android.content.ContentResolver; import android.content.Context; import android.content.ContextWrapper; import android.content.IContentProvider; +import android.content.IIntentReceiver; import android.content.Intent; import android.content.IntentFilter; -import android.content.IIntentReceiver; import android.content.IntentSender; -import android.content.IRestrictionsManager; import android.content.ReceiverCallNotAllowedException; -import android.content.RestrictionsManager; import android.content.ServiceConnection; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; -import android.content.pm.ILauncherApps; import android.content.pm.IPackageManager; -import android.content.pm.LauncherApps; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.AssetManager; @@ -59,96 +46,28 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; -import android.hardware.ConsumerIrManager; -import android.hardware.ISerialManager; -import android.hardware.SerialManager; -import android.hardware.SystemSensorManager; -import android.hardware.hdmi.HdmiControlManager; -import android.hardware.hdmi.IHdmiControlService; -import android.hardware.camera2.CameraManager; import android.hardware.display.DisplayManager; -import android.hardware.input.InputManager; -import android.hardware.usb.IUsbManager; -import android.hardware.usb.UsbManager; -import android.location.CountryDetector; -import android.location.ICountryDetector; -import android.location.ILocationManager; -import android.location.LocationManager; -import android.media.AudioManager; -import android.media.MediaRouter; -import android.media.projection.MediaProjectionManager; -import android.media.session.MediaSessionManager; -import android.media.tv.ITvInputManager; -import android.media.tv.TvInputManager; -import android.net.ConnectivityManager; -import android.net.IConnectivityManager; -import android.net.EthernetManager; -import android.net.IEthernetManager; -import android.net.INetworkPolicyManager; -import android.net.NetworkPolicyManager; -import android.net.NetworkScoreManager; import android.net.Uri; -import android.net.nsd.INsdManager; -import android.net.nsd.NsdManager; -import android.net.wifi.IWifiManager; -import android.net.wifi.WifiManager; -import android.net.wifi.p2p.IWifiP2pManager; -import android.net.wifi.p2p.WifiP2pManager; -import android.net.wifi.IWifiScanner; -import android.net.wifi.WifiScanner; -import android.net.wifi.IRttManager; -import android.net.wifi.RttManager; -import android.nfc.NfcManager; -import android.os.BatteryManager; import android.os.Binder; +import android.os.Build; import android.os.Bundle; import android.os.Debug; -import android.os.DropBoxManager; import android.os.Environment; import android.os.FileUtils; import android.os.Handler; import android.os.IBinder; -import android.os.IPowerManager; -import android.os.IUserManager; import android.os.Looper; -import android.os.PowerManager; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; -import android.os.SystemVibrator; -import android.os.UserManager; import android.os.storage.IMountService; -import android.os.storage.StorageManager; -import android.print.IPrintManager; -import android.print.PrintManager; -import android.service.fingerprint.IFingerprintService; -import android.service.fingerprint.FingerprintManager; -import android.telecom.TelecomManager; -import android.telephony.SubscriptionManager; -import android.telephony.TelephonyManager; -import android.content.ClipboardManager; import android.util.AndroidRuntimeException; import android.util.ArrayMap; import android.util.Log; import android.util.Slog; -import android.view.DisplayAdjustments; -import android.view.ContextThemeWrapper; import android.view.Display; -import android.view.WindowManagerImpl; -import android.view.accessibility.AccessibilityManager; -import android.view.accessibility.CaptioningManager; -import android.view.inputmethod.InputMethodManager; -import android.view.textservice.TextServicesManager; -import android.accounts.AccountManager; -import android.accounts.IAccountManager; -import android.app.admin.DevicePolicyManager; -import android.app.job.IJobScheduler; -import android.app.trust.TrustManager; - -import com.android.internal.annotations.GuardedBy; -import com.android.internal.app.IAppOpsService; -import com.android.internal.os.IDropBoxManagerService; +import android.view.DisplayAdjustments; import java.io.File; import java.io.FileInputStream; @@ -156,8 +75,6 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashMap; class ReceiverRestrictedContext extends ContextWrapper { ReceiverRestrictedContext(Context base) { @@ -231,7 +148,6 @@ class ContextImpl extends Context { private final Resources mResources; private final Display mDisplay; // may be null if default display private final DisplayAdjustments mDisplayAdjustments = new DisplayAdjustments(); - private final Configuration mOverrideConfiguration; private final boolean mRestricted; @@ -267,508 +183,8 @@ class ContextImpl extends Context { private static final String[] EMPTY_FILE_LIST = {}; - /** - * Override this class when the system service constructor needs a - * ContextImpl. Else, use StaticServiceFetcher below. - */ - /*package*/ static class ServiceFetcher { - int mContextCacheIndex = -1; - - /** - * Main entrypoint; only override if you don't need caching. - */ - public Object getService(ContextImpl ctx) { - ArrayList<Object> cache = ctx.mServiceCache; - Object service; - synchronized (cache) { - if (cache.size() == 0) { - // Initialize the cache vector on first access. - // At this point sNextPerContextServiceCacheIndex - // is the number of potential services that are - // cached per-Context. - for (int i = 0; i < sNextPerContextServiceCacheIndex; i++) { - cache.add(null); - } - } else { - service = cache.get(mContextCacheIndex); - if (service != null) { - return service; - } - } - service = createService(ctx); - cache.set(mContextCacheIndex, service); - return service; - } - } - - /** - * Override this to create a new per-Context instance of the - * service. getService() will handle locking and caching. - */ - public Object createService(ContextImpl ctx) { - throw new RuntimeException("Not implemented"); - } - } - - /** - * Override this class for services to be cached process-wide. - */ - abstract static class StaticServiceFetcher extends ServiceFetcher { - private Object mCachedInstance; - - @Override - public final Object getService(ContextImpl unused) { - synchronized (StaticServiceFetcher.this) { - Object service = mCachedInstance; - if (service != null) { - return service; - } - return mCachedInstance = createStaticService(); - } - } - - public abstract Object createStaticService(); - } - - private static final HashMap<String, ServiceFetcher> SYSTEM_SERVICE_MAP = - new HashMap<String, ServiceFetcher>(); - - private static int sNextPerContextServiceCacheIndex = 0; - private static void registerService(String serviceName, ServiceFetcher fetcher) { - if (!(fetcher instanceof StaticServiceFetcher)) { - fetcher.mContextCacheIndex = sNextPerContextServiceCacheIndex++; - } - SYSTEM_SERVICE_MAP.put(serviceName, fetcher); - } - - // This one's defined separately and given a variable name so it - // can be re-used by getWallpaperManager(), avoiding a HashMap - // lookup. - private static ServiceFetcher WALLPAPER_FETCHER = new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - return new WallpaperManager(ctx.getOuterContext(), - ctx.mMainThread.getHandler()); - }}; - - static { - registerService(ACCESSIBILITY_SERVICE, new ServiceFetcher() { - public Object getService(ContextImpl ctx) { - return AccessibilityManager.getInstance(ctx); - }}); - - registerService(CAPTIONING_SERVICE, new ServiceFetcher() { - public Object getService(ContextImpl ctx) { - return new CaptioningManager(ctx); - }}); - - registerService(ACCOUNT_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - IBinder b = ServiceManager.getService(ACCOUNT_SERVICE); - IAccountManager service = IAccountManager.Stub.asInterface(b); - return new AccountManager(ctx, service); - }}); - - registerService(ACTIVITY_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - return new ActivityManager(ctx.getOuterContext(), ctx.mMainThread.getHandler()); - }}); - - registerService(ALARM_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - IBinder b = ServiceManager.getService(ALARM_SERVICE); - IAlarmManager service = IAlarmManager.Stub.asInterface(b); - return new AlarmManager(service, ctx); - }}); - - registerService(AUDIO_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - return new AudioManager(ctx); - }}); - - registerService(MEDIA_ROUTER_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - return new MediaRouter(ctx); - }}); - - registerService(BLUETOOTH_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - return new BluetoothManager(ctx); - }}); - - registerService(HDMI_CONTROL_SERVICE, new StaticServiceFetcher() { - public Object createStaticService() { - IBinder b = ServiceManager.getService(HDMI_CONTROL_SERVICE); - return new HdmiControlManager(IHdmiControlService.Stub.asInterface(b)); - }}); - - registerService(CLIPBOARD_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - return new ClipboardManager(ctx.getOuterContext(), - ctx.mMainThread.getHandler()); - }}); - - registerService(CONNECTIVITY_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - IBinder b = ServiceManager.getService(CONNECTIVITY_SERVICE); - return new ConnectivityManager(IConnectivityManager.Stub.asInterface(b)); - }}); - - registerService(COUNTRY_DETECTOR, new StaticServiceFetcher() { - public Object createStaticService() { - IBinder b = ServiceManager.getService(COUNTRY_DETECTOR); - return new CountryDetector(ICountryDetector.Stub.asInterface(b)); - }}); - - registerService(DEVICE_POLICY_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - return DevicePolicyManager.create(ctx, ctx.mMainThread.getHandler()); - }}); - - registerService(DOWNLOAD_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - return new DownloadManager(ctx.getContentResolver(), ctx.getPackageName()); - }}); - - registerService(BATTERY_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - return new BatteryManager(); - }}); - - registerService(NFC_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - return new NfcManager(ctx); - }}); - - registerService(DROPBOX_SERVICE, new StaticServiceFetcher() { - public Object createStaticService() { - return createDropBoxManager(); - }}); - - registerService(INPUT_SERVICE, new StaticServiceFetcher() { - public Object createStaticService() { - return InputManager.getInstance(); - }}); - - registerService(DISPLAY_SERVICE, new ServiceFetcher() { - @Override - public Object createService(ContextImpl ctx) { - return new DisplayManager(ctx.getOuterContext()); - }}); - - registerService(INPUT_METHOD_SERVICE, new StaticServiceFetcher() { - public Object createStaticService() { - return InputMethodManager.getInstance(); - }}); - - registerService(TEXT_SERVICES_MANAGER_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - return TextServicesManager.getInstance(); - }}); - - registerService(KEYGUARD_SERVICE, new ServiceFetcher() { - public Object getService(ContextImpl ctx) { - // TODO: why isn't this caching it? It wasn't - // before, so I'm preserving the old behavior and - // using getService(), instead of createService() - // which would do the caching. - return new KeyguardManager(); - }}); - - registerService(LAYOUT_INFLATER_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - return PolicyManager.makeNewLayoutInflater(ctx.getOuterContext()); - }}); - - registerService(LOCATION_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - IBinder b = ServiceManager.getService(LOCATION_SERVICE); - return new LocationManager(ctx, ILocationManager.Stub.asInterface(b)); - }}); - - registerService(NETWORK_POLICY_SERVICE, new ServiceFetcher() { - @Override - public Object createService(ContextImpl ctx) { - return new NetworkPolicyManager(INetworkPolicyManager.Stub.asInterface( - ServiceManager.getService(NETWORK_POLICY_SERVICE))); - } - }); - - registerService(NOTIFICATION_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - final Context outerContext = ctx.getOuterContext(); - return new NotificationManager( - new ContextThemeWrapper(outerContext, - Resources.selectSystemTheme(0, - outerContext.getApplicationInfo().targetSdkVersion, - com.android.internal.R.style.Theme_Dialog, - com.android.internal.R.style.Theme_Holo_Dialog, - com.android.internal.R.style.Theme_DeviceDefault_Dialog, - com.android.internal.R.style.Theme_DeviceDefault_Light_Dialog)), - ctx.mMainThread.getHandler()); - }}); - - registerService(NSD_SERVICE, new ServiceFetcher() { - @Override - public Object createService(ContextImpl ctx) { - IBinder b = ServiceManager.getService(NSD_SERVICE); - INsdManager service = INsdManager.Stub.asInterface(b); - return new NsdManager(ctx.getOuterContext(), service); - }}); - - // Note: this was previously cached in a static variable, but - // constructed using mMainThread.getHandler(), so converting - // it to be a regular Context-cached service... - registerService(POWER_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - IBinder b = ServiceManager.getService(POWER_SERVICE); - IPowerManager service = IPowerManager.Stub.asInterface(b); - if (service == null) { - Log.wtf(TAG, "Failed to get power manager service."); - } - return new PowerManager(ctx.getOuterContext(), - service, ctx.mMainThread.getHandler()); - }}); - - registerService(SEARCH_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - return new SearchManager(ctx.getOuterContext(), - ctx.mMainThread.getHandler()); - }}); - - registerService(SENSOR_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - return new SystemSensorManager(ctx.getOuterContext(), - ctx.mMainThread.getHandler().getLooper()); - }}); - - registerService(STATUS_BAR_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - return new StatusBarManager(ctx.getOuterContext()); - }}); - - registerService(STORAGE_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - try { - return new StorageManager( - ctx.getContentResolver(), ctx.mMainThread.getHandler().getLooper()); - } catch (RemoteException rex) { - Log.e(TAG, "Failed to create StorageManager", rex); - return null; - } - }}); - - registerService(TELEPHONY_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - return new TelephonyManager(ctx.getOuterContext()); - }}); - - registerService(TELEPHONY_SUBSCRIPTION_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - return new SubscriptionManager(ctx.getOuterContext()); - }}); - - registerService(TELECOM_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - return new TelecomManager(ctx.getOuterContext()); - }}); - - registerService(UI_MODE_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - return new UiModeManager(); - }}); - - registerService(USB_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - IBinder b = ServiceManager.getService(USB_SERVICE); - return new UsbManager(ctx, IUsbManager.Stub.asInterface(b)); - }}); - - registerService(SERIAL_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - IBinder b = ServiceManager.getService(SERIAL_SERVICE); - return new SerialManager(ctx, ISerialManager.Stub.asInterface(b)); - }}); - - registerService(VIBRATOR_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - return new SystemVibrator(ctx); - }}); - - registerService(WALLPAPER_SERVICE, WALLPAPER_FETCHER); - - registerService(WIFI_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - IBinder b = ServiceManager.getService(WIFI_SERVICE); - IWifiManager service = IWifiManager.Stub.asInterface(b); - return new WifiManager(ctx.getOuterContext(), service); - }}); - - registerService(WIFI_P2P_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - IBinder b = ServiceManager.getService(WIFI_P2P_SERVICE); - IWifiP2pManager service = IWifiP2pManager.Stub.asInterface(b); - return new WifiP2pManager(service); - }}); - - registerService(WIFI_SCANNING_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - IBinder b = ServiceManager.getService(WIFI_SCANNING_SERVICE); - IWifiScanner service = IWifiScanner.Stub.asInterface(b); - return new WifiScanner(ctx.getOuterContext(), service); - }}); - - registerService(WIFI_RTT_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - IBinder b = ServiceManager.getService(WIFI_RTT_SERVICE); - IRttManager service = IRttManager.Stub.asInterface(b); - return new RttManager(ctx.getOuterContext(), service); - }}); - - registerService(ETHERNET_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - IBinder b = ServiceManager.getService(ETHERNET_SERVICE); - IEthernetManager service = IEthernetManager.Stub.asInterface(b); - return new EthernetManager(ctx.getOuterContext(), service); - }}); - - registerService(WINDOW_SERVICE, new ServiceFetcher() { - Display mDefaultDisplay; - public Object getService(ContextImpl ctx) { - Display display = ctx.mDisplay; - if (display == null) { - if (mDefaultDisplay == null) { - DisplayManager dm = (DisplayManager)ctx.getOuterContext(). - getSystemService(Context.DISPLAY_SERVICE); - mDefaultDisplay = dm.getDisplay(Display.DEFAULT_DISPLAY); - } - display = mDefaultDisplay; - } - return new WindowManagerImpl(display); - }}); - - registerService(USER_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - IBinder b = ServiceManager.getService(USER_SERVICE); - IUserManager service = IUserManager.Stub.asInterface(b); - return new UserManager(ctx, service); - }}); - - registerService(APP_OPS_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - IBinder b = ServiceManager.getService(APP_OPS_SERVICE); - IAppOpsService service = IAppOpsService.Stub.asInterface(b); - return new AppOpsManager(ctx, service); - }}); - - registerService(CAMERA_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - return new CameraManager(ctx); - } - }); - - registerService(LAUNCHER_APPS_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - IBinder b = ServiceManager.getService(LAUNCHER_APPS_SERVICE); - ILauncherApps service = ILauncherApps.Stub.asInterface(b); - return new LauncherApps(ctx, service); - } - }); - - registerService(RESTRICTIONS_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - IBinder b = ServiceManager.getService(RESTRICTIONS_SERVICE); - IRestrictionsManager service = IRestrictionsManager.Stub.asInterface(b); - return new RestrictionsManager(ctx, service); - } - }); - registerService(PRINT_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - IBinder iBinder = ServiceManager.getService(Context.PRINT_SERVICE); - IPrintManager service = IPrintManager.Stub.asInterface(iBinder); - return new PrintManager(ctx.getOuterContext(), service, UserHandle.myUserId(), - UserHandle.getAppId(Process.myUid())); - }}); - - registerService(CONSUMER_IR_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - return new ConsumerIrManager(ctx); - }}); - - registerService(MEDIA_SESSION_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - return new MediaSessionManager(ctx); - } - }); - - registerService(TRUST_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - IBinder b = ServiceManager.getService(TRUST_SERVICE); - return new TrustManager(b); - } - }); - - registerService(FINGERPRINT_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - IBinder binder = ServiceManager.getService(FINGERPRINT_SERVICE); - IFingerprintService service = IFingerprintService.Stub.asInterface(binder); - return new FingerprintManager(ctx.getOuterContext(), service); - } - }); - - registerService(TV_INPUT_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - IBinder iBinder = ServiceManager.getService(TV_INPUT_SERVICE); - ITvInputManager service = ITvInputManager.Stub.asInterface(iBinder); - return new TvInputManager(service, UserHandle.myUserId()); - } - }); - - registerService(NETWORK_SCORE_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - return new NetworkScoreManager(ctx); - } - }); - - registerService(USAGE_STATS_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - IBinder iBinder = ServiceManager.getService(USAGE_STATS_SERVICE); - IUsageStatsManager service = IUsageStatsManager.Stub.asInterface(iBinder); - return new UsageStatsManager(ctx.getOuterContext(), service); - } - }); - - registerService(JOB_SCHEDULER_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - IBinder b = ServiceManager.getService(JOB_SCHEDULER_SERVICE); - return new JobSchedulerImpl(IJobScheduler.Stub.asInterface(b)); - }}); - - registerService(PERSISTENT_DATA_BLOCK_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - IBinder b = ServiceManager.getService(PERSISTENT_DATA_BLOCK_SERVICE); - IPersistentDataBlockService persistentDataBlockService = - IPersistentDataBlockService.Stub.asInterface(b); - if (persistentDataBlockService != null) { - return new PersistentDataBlockManager(persistentDataBlockService); - } else { - // not supported - return null; - } - } - }); - - registerService(MEDIA_PROJECTION_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - return new MediaProjectionManager(ctx); - } - }); - - registerService(APPWIDGET_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - IBinder b = ServiceManager.getService(APPWIDGET_SERVICE); - return new AppWidgetManager(ctx, IAppWidgetService.Stub.asInterface(b)); - }}); - } + // The system service cache for the system services that are cached per-ContextImpl. + final Object[] mServiceCache = SystemServiceRegistry.createServiceCache(); static ContextImpl getImpl(Context context) { Context nextContext; @@ -779,11 +195,6 @@ class ContextImpl extends Context { return (ContextImpl)context; } - // The system service cache for the system services that are - // cached per-ContextImpl. Package-scoped to avoid accessor - // methods. - final ArrayList<Object> mServiceCache = new ArrayList<Object>(); - @Override public AssetManager getAssets() { return getResources().getAssets(); @@ -898,6 +309,7 @@ class ContextImpl extends Context { throw new RuntimeException("Not supported in system context"); } + @Override public File getSharedPrefsFile(String name) { return makeFilename(getPreferencesDir(), name + ".xml"); } @@ -1185,40 +597,51 @@ class ContextImpl extends Context { } @Override + @Deprecated public Drawable getWallpaper() { return getWallpaperManager().getDrawable(); } @Override + @Deprecated public Drawable peekWallpaper() { return getWallpaperManager().peekDrawable(); } @Override + @Deprecated public int getWallpaperDesiredMinimumWidth() { return getWallpaperManager().getDesiredMinimumWidth(); } @Override + @Deprecated public int getWallpaperDesiredMinimumHeight() { return getWallpaperManager().getDesiredMinimumHeight(); } @Override - public void setWallpaper(Bitmap bitmap) throws IOException { + @Deprecated + public void setWallpaper(Bitmap bitmap) throws IOException { getWallpaperManager().setBitmap(bitmap); } @Override + @Deprecated public void setWallpaper(InputStream data) throws IOException { getWallpaperManager().setStream(data); } @Override + @Deprecated public void clearWallpaper() throws IOException { getWallpaperManager().clear(); } + private WallpaperManager getWallpaperManager() { + return getSystemService(WallpaperManager.class); + } + @Override public void startActivity(Intent intent) { warnIfCallingFromSystemProcess(); @@ -1490,6 +913,7 @@ class ContextImpl extends Context { } @Override + @Deprecated public void sendStickyBroadcast(Intent intent) { warnIfCallingFromSystemProcess(); String resolvedType = intent.resolveTypeIfNeeded(getContentResolver()); @@ -1504,6 +928,7 @@ class ContextImpl extends Context { } @Override + @Deprecated public void sendStickyOrderedBroadcast(Intent intent, BroadcastReceiver resultReceiver, Handler scheduler, int initialCode, String initialData, @@ -1538,6 +963,7 @@ class ContextImpl extends Context { } @Override + @Deprecated public void removeStickyBroadcast(Intent intent) { String resolvedType = intent.resolveTypeIfNeeded(getContentResolver()); if (resolvedType != null) { @@ -1553,6 +979,7 @@ class ContextImpl extends Context { } @Override + @Deprecated public void sendStickyBroadcastAsUser(Intent intent, UserHandle user) { String resolvedType = intent.resolveTypeIfNeeded(getContentResolver()); try { @@ -1565,6 +992,7 @@ class ContextImpl extends Context { } @Override + @Deprecated public void sendStickyOrderedBroadcastAsUser(Intent intent, UserHandle user, BroadcastReceiver resultReceiver, Handler scheduler, int initialCode, String initialData, @@ -1598,6 +1026,7 @@ class ContextImpl extends Context { } @Override + @Deprecated public void removeStickyBroadcastAsUser(Intent intent, UserHandle user) { String resolvedType = intent.resolveTypeIfNeeded(getContentResolver()); if (resolvedType != null) { @@ -1834,25 +1263,12 @@ class ContextImpl extends Context { @Override public Object getSystemService(String name) { - ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name); - return fetcher == null ? null : fetcher.getService(this); + return SystemServiceRegistry.getSystemService(this, name); } - private WallpaperManager getWallpaperManager() { - return (WallpaperManager) WALLPAPER_FETCHER.getService(this); - } - - /* package */ static DropBoxManager createDropBoxManager() { - IBinder b = ServiceManager.getService(DROPBOX_SERVICE); - IDropBoxManagerService service = IDropBoxManagerService.Stub.asInterface(b); - if (service == null) { - // Don't return a DropBoxManager that will NPE upon use. - // This also avoids caching a broken DropBoxManager in - // getDropBoxManager during early boot, before the - // DROPBOX_SERVICE is registered. - return null; - } - return new DropBoxManager(service); + @Override + public String getSystemServiceName(Class<?> serviceClass) { + return SystemServiceRegistry.getSystemServiceName(serviceClass); } @Override @@ -1921,6 +1337,7 @@ class ContextImpl extends Context { } } + @Override public void enforcePermission( String permission, int pid, int uid, String message) { enforce(permission, @@ -1930,6 +1347,7 @@ class ContextImpl extends Context { message); } + @Override public void enforceCallingPermission(String permission, String message) { enforce(permission, checkCallingPermission(permission), @@ -1938,6 +1356,7 @@ class ContextImpl extends Context { message); } + @Override public void enforceCallingOrSelfPermission( String permission, String message) { enforce(permission, @@ -2075,6 +1494,7 @@ class ContextImpl extends Context { } } + @Override public void enforceUriPermission( Uri uri, int pid, int uid, int modeFlags, String message) { enforceForUri( @@ -2082,6 +1502,7 @@ class ContextImpl extends Context { false, uid, uri, message); } + @Override public void enforceCallingUriPermission( Uri uri, int modeFlags, String message) { enforceForUri( @@ -2090,6 +1511,7 @@ class ContextImpl extends Context { Binder.getCallingUid(), uri, message); } + @Override public void enforceCallingOrSelfUriPermission( Uri uri, int modeFlags, String message) { enforceForUri( @@ -2098,6 +1520,7 @@ class ContextImpl extends Context { Binder.getCallingUid(), uri, message); } + @Override public void enforceUriPermission( Uri uri, String readPermission, String writePermission, int pid, int uid, int modeFlags, String message) { @@ -2132,7 +1555,7 @@ class ContextImpl extends Context { final boolean restricted = (flags & CONTEXT_RESTRICTED) == CONTEXT_RESTRICTED; ContextImpl c = new ContextImpl(this, mMainThread, pi, mActivityToken, new UserHandle(UserHandle.getUserId(application.uid)), restricted, - mDisplay, mOverrideConfiguration); + mDisplay, null); if (c.mResources != null) { return c; } @@ -2155,14 +1578,14 @@ class ContextImpl extends Context { final boolean restricted = (flags & CONTEXT_RESTRICTED) == CONTEXT_RESTRICTED; if (packageName.equals("system") || packageName.equals("android")) { return new ContextImpl(this, mMainThread, mPackageInfo, mActivityToken, - user, restricted, mDisplay, mOverrideConfiguration); + user, restricted, mDisplay, null); } LoadedApk pi = mMainThread.getPackageInfo(packageName, mResources.getCompatibilityInfo(), flags | CONTEXT_REGISTER_PACKAGE, user.getIdentifier()); if (pi != null) { ContextImpl c = new ContextImpl(this, mMainThread, pi, mActivityToken, - user, restricted, mDisplay, mOverrideConfiguration); + user, restricted, mDisplay, null); if (c.mResources != null) { return c; } @@ -2190,7 +1613,15 @@ class ContextImpl extends Context { } return new ContextImpl(this, mMainThread, mPackageInfo, mActivityToken, - mUser, mRestricted, display, mOverrideConfiguration); + mUser, mRestricted, display, null); + } + + Display getDisplay() { + if (mDisplay != null) { + return mDisplay; + } + DisplayManager dm = getSystemService(DisplayManager.class); + return dm.getDisplay(Display.DEFAULT_DISPLAY); } private int getDisplayId() { @@ -2227,6 +1658,7 @@ class ContextImpl extends Context { } /** {@hide} */ + @Override public int getUserId() { return mUser.getIdentifier(); } @@ -2236,7 +1668,7 @@ class ContextImpl extends Context { ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, false, null, null); context.mResources.updateConfiguration(context.mResourcesManager.getConfiguration(), - context.mResourcesManager.getDisplayMetricsLocked(Display.DEFAULT_DISPLAY)); + context.mResourcesManager.getDisplayMetricsLocked()); return context; } @@ -2247,11 +1679,12 @@ class ContextImpl extends Context { } static ContextImpl createActivityContext(ActivityThread mainThread, - LoadedApk packageInfo, IBinder activityToken) { + LoadedApk packageInfo, int displayId, Configuration overrideConfiguration) { if (packageInfo == null) throw new IllegalArgumentException("packageInfo"); - if (activityToken == null) throw new IllegalArgumentException("activityInfo"); - return new ContextImpl(null, mainThread, - packageInfo, activityToken, null, false, null, null); + final Display display = ResourcesManager.getInstance().getAdjustedDisplay( + displayId, overrideConfiguration); + return new ContextImpl(null, mainThread, packageInfo, null, null, false, display, + overrideConfiguration); } private ContextImpl(ContextImpl container, ActivityThread mainThread, @@ -2271,30 +1704,30 @@ class ContextImpl extends Context { mPackageInfo = packageInfo; mResourcesManager = ResourcesManager.getInstance(); mDisplay = display; - mOverrideConfiguration = overrideConfiguration; final int displayId = getDisplayId(); CompatibilityInfo compatInfo = null; if (container != null) { compatInfo = container.getDisplayAdjustments(displayId).getCompatibilityInfo(); } - if (compatInfo == null && displayId == Display.DEFAULT_DISPLAY) { - compatInfo = packageInfo.getCompatibilityInfo(); + if (compatInfo == null) { + compatInfo = (displayId == Display.DEFAULT_DISPLAY) + ? packageInfo.getCompatibilityInfo() + : CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO; } mDisplayAdjustments.setCompatibilityInfo(compatInfo); - mDisplayAdjustments.setActivityToken(activityToken); + mDisplayAdjustments.setConfiguration(overrideConfiguration); Resources resources = packageInfo.getResources(mainThread); if (resources != null) { - if (activityToken != null - || displayId != Display.DEFAULT_DISPLAY + if (displayId != Display.DEFAULT_DISPLAY || overrideConfiguration != null || (compatInfo != null && compatInfo.applicationScale != resources.getCompatibilityInfo().applicationScale)) { resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(), packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(), packageInfo.getApplicationInfo().sharedLibraryFiles, displayId, - overrideConfiguration, compatInfo, activityToken); + overrideConfiguration, compatInfo); } } mResources = resources; @@ -2351,6 +1784,7 @@ class ContextImpl extends Context { return mActivityToken; } + @SuppressWarnings("deprecation") static void setFilePermissionsFromMode(String name, int mode, int extraPermissions) { int perms = FileUtils.S_IRUSR|FileUtils.S_IWUSR diff --git a/core/java/android/app/DatePickerDialog.java b/core/java/android/app/DatePickerDialog.java index f79d32b..3fbbdff 100644 --- a/core/java/android/app/DatePickerDialog.java +++ b/core/java/android/app/DatePickerDialog.java @@ -131,6 +131,9 @@ public class DatePickerDialog extends AlertDialog implements OnClickListener, switch (which) { case BUTTON_POSITIVE: if (mDateSetListener != null) { + // Clearing focus forces the dialog to commit any pending + // changes, e.g. typed text in a NumberPicker. + mDatePicker.clearFocus(); mDateSetListener.onDateSet(mDatePicker, mDatePicker.getYear(), mDatePicker.getMonth(), mDatePicker.getDayOfMonth()); } diff --git a/core/java/android/app/Dialog.java b/core/java/android/app/Dialog.java index 067073a..9defcbe 100644 --- a/core/java/android/app/Dialog.java +++ b/core/java/android/app/Dialog.java @@ -16,14 +16,19 @@ package android.app; -import android.content.pm.ApplicationInfo; +import android.annotation.CallSuper; +import android.annotation.DrawableRes; +import android.annotation.IdRes; +import android.annotation.LayoutRes; +import android.annotation.StringRes; import com.android.internal.app.WindowDecorActionBar; -import com.android.internal.policy.PolicyManager; +import android.annotation.Nullable; import android.content.ComponentName; import android.content.Context; import android.content.ContextWrapper; import android.content.DialogInterface; +import android.content.pm.ApplicationInfo; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; @@ -42,6 +47,7 @@ import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; +import android.view.PhoneWindow; import android.view.View; import android.view.View.OnCreateContextMenuListener; import android.view.ViewGroup; @@ -115,6 +121,8 @@ public class Dialog implements DialogInterface, Window.Callback, private ActionMode mActionMode; + private int mActionModeTypeStarting = ActionMode.TYPE_PRIMARY; + private final Runnable mDismissAction = new Runnable() { public void run() { dismissDialog(); @@ -134,14 +142,14 @@ public class Dialog implements DialogInterface, Window.Callback, /** * Create a Dialog window that uses a custom dialog style. - * + * * @param context The Context in which the Dialog should run. In particular, it * uses the window manager and theme from this context to * present its UI. - * @param theme A style resource describing the theme to use for the - * window. See <a href="{@docRoot}guide/topics/resources/available-resources.html#stylesandthemes">Style - * and Theme Resources</a> for more information about defining and using - * styles. This theme is applied on top of the current theme in + * @param theme A style resource describing the theme to use for the + * window. See <a href="{@docRoot}guide/topics/resources/available-resources.html#stylesandthemes">Style + * and Theme Resources</a> for more information about defining and using + * styles. This theme is applied on top of the current theme in * <var>context</var>. If 0, the default dialog theme will be used. */ public Dialog(Context context, int theme) { @@ -162,7 +170,7 @@ public class Dialog implements DialogInterface, Window.Callback, } mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); - Window w = PolicyManager.makeNewWindow(mContext); + Window w = new PhoneWindow(mContext); mWindow = w; w.setCallback(this); w.setOnWindowDismissedCallback(this); @@ -476,7 +484,8 @@ public class Dialog implements DialogInterface, Window.Callback, * @param id the identifier of the view to find * @return The view with the given id or null. */ - public View findViewById(int id) { + @Nullable + public View findViewById(@IdRes int id) { return mWindow.findViewById(id); } @@ -486,7 +495,7 @@ public class Dialog implements DialogInterface, Window.Callback, * * @param layoutResID Resource ID to be inflated. */ - public void setContentView(int layoutResID) { + public void setContentView(@LayoutRes int layoutResID) { mWindow.setContentView(layoutResID); } @@ -540,7 +549,7 @@ public class Dialog implements DialogInterface, Window.Callback, * * @param titleId the title's text resource identifier */ - public void setTitle(int titleId) { + public void setTitle(@StringRes int titleId) { setTitle(mContext.getText(titleId)); } @@ -966,13 +975,13 @@ public class Dialog implements DialogInterface, Window.Callback, public boolean onContextItemSelected(MenuItem item) { return false; } - + /** * @see Activity#onContextMenuClosed(Menu) */ public void onContextMenuClosed(Menu menu) { } - + /** * This hook is called when the user signals the desire to start a search. */ @@ -991,8 +1000,12 @@ public class Dialog implements DialogInterface, Window.Callback, } } + /** + * {@inheritDoc} + */ + @Override public ActionMode onWindowStartingActionMode(ActionMode.Callback callback) { - if (mActionBar != null) { + if (mActionBar != null && mActionModeTypeStarting == ActionMode.TYPE_PRIMARY) { return mActionBar.startActionMode(callback); } return null; @@ -1000,10 +1013,24 @@ public class Dialog implements DialogInterface, Window.Callback, /** * {@inheritDoc} + */ + @Override + public ActionMode onWindowStartingActionMode(ActionMode.Callback callback, int type) { + try { + mActionModeTypeStarting = type; + return onWindowStartingActionMode(callback); + } finally { + mActionModeTypeStarting = ActionMode.TYPE_PRIMARY; + } + } + + /** + * {@inheritDoc} * * Note that if you override this method you should always call through * to the superclass implementation by calling super.onActionModeStarted(mode). */ + @CallSuper public void onActionModeStarted(ActionMode mode) { mActionMode = mode; } @@ -1014,6 +1041,7 @@ public class Dialog implements DialogInterface, Window.Callback, * Note that if you override this method you should always call through * to the superclass implementation by calling super.onActionModeFinished(mode). */ + @CallSuper public void onActionModeFinished(ActionMode mode) { if (mode == mActionMode) { mActionMode = null; @@ -1070,7 +1098,7 @@ public class Dialog implements DialogInterface, Window.Callback, * Convenience for calling * {@link android.view.Window#setFeatureDrawableResource}. */ - public final void setFeatureDrawableResource(int featureId, int resId) { + public final void setFeatureDrawableResource(int featureId, @DrawableRes int resId) { getWindow().setFeatureDrawableResource(featureId, resId); } diff --git a/core/java/android/app/Fragment.java b/core/java/android/app/Fragment.java index ab28d95..bdcc312 100644 --- a/core/java/android/app/Fragment.java +++ b/core/java/android/app/Fragment.java @@ -18,6 +18,7 @@ package android.app; import android.animation.Animator; import android.annotation.Nullable; +import android.annotation.StringRes; import android.content.ComponentCallbacks2; import android.content.Context; import android.content.Intent; @@ -795,7 +796,7 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene * * @param resId Resource id for the CharSequence text */ - public final CharSequence getText(int resId) { + public final CharSequence getText(@StringRes int resId) { return getResources().getText(resId); } @@ -805,7 +806,7 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene * * @param resId Resource id for the string */ - public final String getString(int resId) { + public final String getString(@StringRes int resId) { return getResources().getString(resId); } @@ -818,7 +819,7 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene * @param formatArgs The format arguments that will be used for substitution. */ - public final String getString(int resId, Object... formatArgs) { + public final String getString(@StringRes int resId, Object... formatArgs) { return getResources().getString(resId, formatArgs); } @@ -1243,7 +1244,7 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene * @param savedInstanceState If the fragment is being re-created from * a previous saved state, this is the state. */ - public void onCreate(Bundle savedInstanceState) { + public void onCreate(@Nullable Bundle savedInstanceState) { mCalled = true; } @@ -1309,7 +1310,7 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene * @param savedInstanceState If the fragment is being re-created from * a previous saved state, this is the state. */ - public void onActivityCreated(Bundle savedInstanceState) { + public void onActivityCreated(@Nullable Bundle savedInstanceState) { mCalled = true; } @@ -2008,6 +2009,7 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene mChildFragmentManager = new FragmentManagerImpl(); mChildFragmentManager.attachActivity(mActivity, new FragmentContainer() { @Override + @Nullable public View findViewById(int id) { if (mView == null) { throw new IllegalStateException("Fragment does not have a view"); diff --git a/core/java/android/app/FragmentManager.java b/core/java/android/app/FragmentManager.java index ccceef4..975b20d 100644 --- a/core/java/android/app/FragmentManager.java +++ b/core/java/android/app/FragmentManager.java @@ -19,7 +19,9 @@ package android.app; import android.animation.Animator; import android.animation.AnimatorInflater; import android.animation.AnimatorListenerAdapter; +import android.annotation.Nullable; import android.content.Context; +import android.annotation.IdRes; import android.content.res.Configuration; import android.content.res.TypedArray; import android.os.Bundle; @@ -394,7 +396,8 @@ final class FragmentManagerState implements Parcelable { * Callbacks from FragmentManagerImpl to its container. */ interface FragmentContainer { - public View findViewById(int id); + @Nullable + public View findViewById(@IdRes int id); public boolean hasView(); } diff --git a/core/java/android/app/IActivityContainer.aidl b/core/java/android/app/IActivityContainer.aidl index cc3b10c..170aff3 100644 --- a/core/java/android/app/IActivityContainer.aidl +++ b/core/java/android/app/IActivityContainer.aidl @@ -30,6 +30,7 @@ interface IActivityContainer { int startActivity(in Intent intent); int startActivityIntentSender(in IIntentSender intentSender); int getDisplayId(); + int getStackId(); boolean injectEvent(in InputEvent event); void release(); } diff --git a/core/java/android/app/IActivityManager.java b/core/java/android/app/IActivityManager.java index a138dbb..d794aa3 100644 --- a/core/java/android/app/IActivityManager.java +++ b/core/java/android/app/IActivityManager.java @@ -51,6 +51,7 @@ import android.os.RemoteException; import android.os.StrictMode; import android.service.voice.IVoiceInteractionSession; import com.android.internal.app.IVoiceInteractor; +import com.android.internal.os.IResultReceiver; import java.util.List; @@ -131,7 +132,6 @@ public interface IActivityManager extends IInterface { public List<ActivityManager.ProcessErrorStateInfo> getProcessesInErrorState() throws RemoteException; public void moveTaskToFront(int task, int flags, Bundle options) throws RemoteException; - public void moveTaskToBack(int task) throws RemoteException; public boolean moveActivityTaskToBack(IBinder token, boolean nonRoot) throws RemoteException; public void moveTaskBackwards(int task) throws RemoteException; public void moveTaskToStack(int taskId, int stackId, boolean toTop) throws RemoteException; @@ -140,6 +140,7 @@ public interface IActivityManager extends IInterface { public StackInfo getStackInfo(int stackId) throws RemoteException; public boolean isInHomeStack(int taskId) throws RemoteException; public void setFocusedStack(int stackId) throws RemoteException; + public int getFocusedStackId() throws RemoteException; public void registerTaskStackListener(ITaskStackListener listener) throws RemoteException; public int getTaskForActivity(IBinder token, boolean onlyRoot) throws RemoteException; public ContentProviderHolder getContentProvider(IApplicationThread caller, @@ -419,6 +420,9 @@ public interface IActivityManager extends IInterface { public Bundle getAssistContextExtras(int requestType) throws RemoteException; + public void requestAssistContextExtras(int requestType, IResultReceiver receiver) + throws RemoteException; + public void reportAssistContextExtras(IBinder token, Bundle extras) throws RemoteException; public boolean launchAssistIntent(Intent intent, int requestType, String hint, int userHandle) @@ -434,9 +438,11 @@ public interface IActivityManager extends IInterface { public void performIdleMaintenance() throws RemoteException; - public IActivityContainer createActivityContainer(IBinder parentActivityToken, + public IActivityContainer createVirtualActivityContainer(IBinder parentActivityToken, IActivityContainerCallback callback) throws RemoteException; + public IActivityContainer createStackOnDisplay(int displayId) throws RemoteException; + public void deleteActivityContainer(IActivityContainer container) throws RemoteException; public int getActivityDisplayId(IBinder activityToken) throws RemoteException; @@ -455,8 +461,12 @@ public interface IActivityManager extends IInterface { public boolean isInLockTaskMode() throws RemoteException; + public int getLockTaskModeState() throws RemoteException; + public void setTaskDescription(IBinder token, ActivityManager.TaskDescription values) throws RemoteException; + public void setTaskResizeable(int taskId, boolean resizeable) throws RemoteException; + public void resizeTask(int taskId, Rect bounds) throws RemoteException; public Bitmap getTaskDescriptionIcon(String filename) throws RemoteException; public void startInPlaceAnimationOnFrontMostApplication(ActivityOptions opts) @@ -472,6 +482,12 @@ public interface IActivityManager extends IInterface { public void systemBackupRestored() throws RemoteException; public void notifyCleartextNetwork(int uid, byte[] firstPacket) throws RemoteException; + public void setDumpHeapDebugLimit(String processName, long maxMemSize) throws RemoteException; + public void dumpHeapFinished(String path) throws RemoteException; + + public void setVoiceKeepAwake(IVoiceInteractionSession session, boolean keepAwake) + throws RemoteException; + /* * Private non-Binder interfaces */ @@ -502,7 +518,7 @@ public interface IActivityManager extends IInterface { dest.writeStrongBinder(null); } dest.writeStrongBinder(connection); - dest.writeInt(noReleaseNeeded ? 1:0); + dest.writeInt(noReleaseNeeded ? 1 : 0); } public static final Parcelable.Creator<ContentProviderHolder> CREATOR @@ -521,7 +537,7 @@ public interface IActivityManager extends IInterface { private ContentProviderHolder(Parcel source) { info = ProviderInfo.CREATOR.createFromParcel(source); provider = ContentProviderNative.asInterface( - source.readStrongBinder()); + source.readStrongBinder()); connection = source.readStrongBinder(); noReleaseNeeded = source.readInt() != 0; } @@ -598,7 +614,7 @@ public interface IActivityManager extends IInterface { int GET_CALLING_ACTIVITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+21; int GET_TASKS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+22; int MOVE_TASK_TO_FRONT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+23; - int MOVE_TASK_TO_BACK_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+24; + int MOVE_TASK_BACKWARDS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+25; int GET_TASK_FOR_ACTIVITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+26; @@ -738,7 +754,7 @@ public interface IActivityManager extends IInterface { int KILL_UID_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+164; int SET_USER_IS_MONKEY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+165; int HANG_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+166; - int CREATE_ACTIVITY_CONTAINER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+167; + int CREATE_VIRTUAL_ACTIVITY_CONTAINER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+167; int MOVE_TASK_TO_STACK_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+168; int RESIZE_STACK_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+169; int GET_ALL_STACK_INFOS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+170; @@ -797,4 +813,13 @@ public interface IActivityManager extends IInterface { // Start of M transactions int NOTIFY_CLEARTEXT_NETWORK_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+280; + int CREATE_STACK_ON_DISPLAY = IBinder.FIRST_CALL_TRANSACTION+281; + int GET_FOCUSED_STACK_ID_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+282; + int SET_TASK_RESIZEABLE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+283; + int REQUEST_ASSIST_CONTEXT_EXTRAS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+284; + int RESIZE_TASK_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+285; + int GET_LOCK_TASK_MODE_STATE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+286; + int SET_DUMP_HEAP_DEBUG_LIMIT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+287; + int DUMP_HEAP_FINISHED_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+288; + int SET_VOICE_KEEP_AWAKE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+289; } diff --git a/core/java/android/app/IApplicationThread.java b/core/java/android/app/IApplicationThread.java index 8bf8cd7..3fb82f6 100644 --- a/core/java/android/app/IApplicationThread.java +++ b/core/java/android/app/IApplicationThread.java @@ -33,7 +33,6 @@ import android.os.PersistableBundle; import android.os.RemoteException; import android.os.IBinder; import android.os.IInterface; -import android.service.voice.IVoiceInteractionSession; import com.android.internal.app.IVoiceInteractor; import com.android.internal.content.ReferrerIntent; @@ -59,14 +58,14 @@ public interface IApplicationThread extends IInterface { throws RemoteException; void scheduleSendResult(IBinder token, List<ResultInfo> results) throws RemoteException; void scheduleLaunchActivity(Intent intent, IBinder token, int ident, - ActivityInfo info, Configuration curConfig, CompatibilityInfo compatInfo, - String referrer, IVoiceInteractor voiceInteractor, int procState, Bundle state, - PersistableBundle persistentState, List<ResultInfo> pendingResults, - List<ReferrerIntent> pendingNewIntents, boolean notResumed, boolean isForward, - ProfilerInfo profilerInfo) throws RemoteException; + ActivityInfo info, Configuration curConfig, Configuration overrideConfig, + CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor, + int procState, Bundle state, PersistableBundle persistentState, + List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents, + boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) throws RemoteException; void scheduleRelaunchActivity(IBinder token, List<ResultInfo> pendingResults, - List<ReferrerIntent> pendingNewIntents, int configChanges, - boolean notResumed, Configuration config) throws RemoteException; + List<ReferrerIntent> pendingNewIntents, int configChanges, boolean notResumed, + Configuration config, Configuration overrideConfig) throws RemoteException; void scheduleNewIntent(List<ReferrerIntent> intent, IBinder token) throws RemoteException; void scheduleDestroyActivity(IBinder token, boolean finished, int configChanges) throws RemoteException; @@ -115,7 +114,8 @@ public interface IApplicationThread extends IInterface { int resultCode, String data, Bundle extras, boolean ordered, boolean sticky, int sendingUser, int processState) throws RemoteException; void scheduleLowMemory() throws RemoteException; - void scheduleActivityConfigurationChanged(IBinder token) throws RemoteException; + void scheduleActivityConfigurationChanged(IBinder token, Configuration overrideConfig) + throws RemoteException; void profilerControl(boolean start, ProfilerInfo profilerInfo, int profileType) throws RemoteException; void dumpHeap(boolean managed, String path, ParcelFileDescriptor fd) diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index 88b9080..5d864df 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -75,6 +75,7 @@ interface INotificationManager ZenModeConfig getZenModeConfig(); boolean setZenModeConfig(in ZenModeConfig config); + oneway void setZenMode(int mode); oneway void notifyConditions(String pkg, in IConditionProvider provider, in Condition[] conditions); oneway void requestZenModeConditions(in IConditionListener callback, int relevance); oneway void setZenModeCondition(in Condition condition); diff --git a/core/java/android/app/ISearchManager.aidl b/core/java/android/app/ISearchManager.aidl index 03e7ff4..6d27910 100644 --- a/core/java/android/app/ISearchManager.aidl +++ b/core/java/android/app/ISearchManager.aidl @@ -31,5 +31,5 @@ interface ISearchManager { ComponentName getGlobalSearchActivity(); ComponentName getWebSearchActivity(); ComponentName getAssistIntent(int userHandle); - boolean launchAssistAction(int requestType, String hint, int userHandle); + boolean launchAssistAction(String hint, int userHandle); } diff --git a/core/java/android/app/IWallpaperManager.aidl b/core/java/android/app/IWallpaperManager.aidl index 3b5900b..ccba250 100644 --- a/core/java/android/app/IWallpaperManager.aidl +++ b/core/java/android/app/IWallpaperManager.aidl @@ -29,13 +29,18 @@ interface IWallpaperManager { /** * Set the wallpaper. */ - ParcelFileDescriptor setWallpaper(String name); + ParcelFileDescriptor setWallpaper(String name, in String callingPackage); /** * Set the live wallpaper. */ + void setWallpaperComponentChecked(in ComponentName name, in String callingPackage); + + /** + * Set the live wallpaper. + */ void setWallpaperComponent(in ComponentName name); - + /** * Get the wallpaper. */ @@ -50,7 +55,7 @@ interface IWallpaperManager { /** * Clear the wallpaper. */ - void clearWallpaper(); + void clearWallpaper(in String callingPackage); /** * Return whether there is a wallpaper set with the given name. @@ -61,7 +66,7 @@ interface IWallpaperManager { * Sets the dimension hint for the wallpaper. These hints indicate the desired * minimum width and height for the wallpaper. */ - void setDimensionHints(in int width, in int height); + void setDimensionHints(in int width, in int height, in String callingPackage); /** * Returns the desired minimum width for the wallpaper. @@ -76,7 +81,7 @@ interface IWallpaperManager { /** * Sets extra padding that we would like the wallpaper to have outside of the display. */ - void setDisplayPadding(in Rect padding); + void setDisplayPadding(in Rect padding, in String callingPackage); /** * Returns the name of the wallpaper. Private API. @@ -87,4 +92,9 @@ interface IWallpaperManager { * Informs the service that wallpaper settings have been restored. Private API. */ void settingsRestored(); + + /** + * Check whether wallpapers are supported for the calling user. + */ + boolean isWallpaperSupported(in String callingPackage); } diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index ad2b61f..5572d30 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -1322,7 +1322,10 @@ public class Instrumentation { /* * Starts allocation counting. This triggers a gc and resets the counts. + * + * @deprecated Accurate counting is a burden on the runtime and may be removed. */ + @Deprecated public void startAllocCounting() { // Before we start trigger a GC and reset the debug counts. Run the // finalizers and another GC before starting and stopping the alloc @@ -1340,7 +1343,10 @@ public class Instrumentation { /* * Stops allocation counting. + * + * @deprecated Accurate counting is a burden on the runtime and may be removed. */ + @Deprecated public void stopAllocCounting() { Runtime.getRuntime().gc(); Runtime.getRuntime().runFinalization(); diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 860b9cc..b824e97 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -16,6 +16,8 @@ package android.app; +import android.annotation.ColorInt; +import android.annotation.DrawableRes; import android.annotation.IntDef; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; @@ -165,6 +167,7 @@ public class Notification implements Parcelable * The resource id of a drawable to use as the icon in the status bar. * This is required; notifications with an invalid icon resource will not be shown. */ + @DrawableRes public int icon; /** @@ -336,6 +339,7 @@ public class Notification implements Parcelable * @see #FLAG_SHOW_LIGHTS * @see #flags */ + @ColorInt public int ledARGB; /** @@ -413,7 +417,6 @@ public class Notification implements Parcelable * Bit to be bitwise-ored into the {@link #flags} field that should be * set if the notification should be canceled when it is clicked by the * user. - */ public static final int FLAG_AUTO_CANCEL = 0x00000010; @@ -518,12 +521,14 @@ public class Notification implements Parcelable * {@link #icon} image (stenciled in white) atop a field of this color. Alpha components are * ignored. */ + @ColorInt public int color = COLOR_DEFAULT; /** * Special value of {@link #color} telling the system not to decorate this notification with * any special color but instead use default colors when presenting this notification. */ + @ColorInt public static final int COLOR_DEFAULT = 0; // AKA Color.TRANSPARENT /** @@ -2128,7 +2133,7 @@ public class Notification implements Parcelable * A resource ID in the application's package of the drawable to use. * @see Notification#icon */ - public Builder setSmallIcon(int icon) { + public Builder setSmallIcon(@DrawableRes int icon) { mSmallIcon = icon; return this; } @@ -2144,7 +2149,7 @@ public class Notification implements Parcelable * @see Notification#icon * @see Notification#iconLevel */ - public Builder setSmallIcon(int icon, int level) { + public Builder setSmallIcon(@DrawableRes int icon, int level) { mSmallIcon = icon; mSmallIconLevel = level; return this; @@ -2386,7 +2391,7 @@ public class Notification implements Parcelable * @see Notification#ledOnMS * @see Notification#ledOffMS */ - public Builder setLights(int argb, int onMs, int offMs) { + public Builder setLights(@ColorInt int argb, int onMs, int offMs) { mLedArgb = argb; mLedOnMs = onMs; mLedOffMs = offMs; @@ -2710,7 +2715,7 @@ public class Notification implements Parcelable * * @return The same Builder. */ - public Builder setColor(int argb) { + public Builder setColor(@ColorInt int argb) { mColor = argb; return this; } @@ -2865,7 +2870,7 @@ public class Notification implements Parcelable contentView.setProgressBar( R.id.progress, mProgressMax, mProgress, mProgressIndeterminate); contentView.setProgressBackgroundTintList( - R.id.progress, ColorStateList.valueOf(mContext.getResources().getColor( + R.id.progress, ColorStateList.valueOf(mContext.getColor( R.color.notification_progress_background_color))); if (mColor != COLOR_DEFAULT) { ColorStateList colorStateList = ColorStateList.valueOf(mColor); @@ -3039,7 +3044,7 @@ public class Notification implements Parcelable private void processLegacyAction(Action action, RemoteViews button) { if (!isLegacy() || mColorUtil.isGrayscaleIcon(mContext, action.icon)) { button.setTextViewCompoundDrawablesRelativeColorFilter(R.id.action0, 0, - mContext.getResources().getColor(R.color.notification_action_color_filter), + mContext.getColor(R.color.notification_action_color_filter), PorterDuff.Mode.MULTIPLY); } } @@ -3137,7 +3142,7 @@ public class Notification implements Parcelable private int resolveColor() { if (mColor == COLOR_DEFAULT) { - return mContext.getResources().getColor(R.color.notification_icon_bg_color); + return mContext.getColor(R.color.notification_icon_bg_color); } return mColor; } @@ -4316,9 +4321,9 @@ public class Notification implements Parcelable * Applies the special text colors for media notifications to all text views. */ private void styleText(RemoteViews contentView) { - int primaryColor = mBuilder.mContext.getResources().getColor( + int primaryColor = mBuilder.mContext.getColor( R.color.notification_media_primary_color); - int secondaryColor = mBuilder.mContext.getResources().getColor( + int secondaryColor = mBuilder.mContext.getColor( R.color.notification_media_secondary_color); contentView.setTextColor(R.id.title, primaryColor); if (mBuilder.showsTimeOrChronometer()) { @@ -5065,6 +5070,868 @@ public class Notification implements Parcelable } /** + * <p>Helper class to add Android Auto extensions to notifications. To create a notification + * with car extensions: + * + * <ol> + * <li>Create an {@link Notification.Builder}, setting any desired + * properties. + * <li>Create a {@link CarExtender}. + * <li>Set car-specific properties using the {@code add} and {@code set} methods of + * {@link CarExtender}. + * <li>Call {@link Notification.Builder#extend(Notification.Extender)} + * to apply the extensions to a notification. + * </ol> + * + * <pre class="prettyprint"> + * Notification notification = new Notification.Builder(context) + * ... + * .extend(new CarExtender() + * .set*(...)) + * .build(); + * </pre> + * + * <p>Car extensions can be accessed on an existing notification by using the + * {@code CarExtender(Notification)} constructor, and then using the {@code get} methods + * to access values. + */ + public static final class CarExtender implements Extender { + private static final String TAG = "CarExtender"; + + private static final String EXTRA_CAR_EXTENDER = "android.car.EXTENSIONS"; + private static final String EXTRA_LARGE_ICON = "large_icon"; + private static final String EXTRA_CONVERSATION = "car_conversation"; + private static final String EXTRA_COLOR = "app_color"; + + private Bitmap mLargeIcon; + private UnreadConversation mUnreadConversation; + private int mColor = Notification.COLOR_DEFAULT; + + /** + * Create a {@link CarExtender} with default options. + */ + public CarExtender() { + } + + /** + * Create a {@link CarExtender} from the CarExtender options of an existing Notification. + * + * @param notif The notification from which to copy options. + */ + public CarExtender(Notification notif) { + Bundle carBundle = notif.extras == null ? + null : notif.extras.getBundle(EXTRA_CAR_EXTENDER); + if (carBundle != null) { + mLargeIcon = carBundle.getParcelable(EXTRA_LARGE_ICON); + mColor = carBundle.getInt(EXTRA_COLOR, Notification.COLOR_DEFAULT); + + Bundle b = carBundle.getBundle(EXTRA_CONVERSATION); + mUnreadConversation = UnreadConversation.getUnreadConversationFromBundle(b); + } + } + + /** + * Apply car extensions to a notification that is being built. This is typically called by + * the {@link Notification.Builder#extend(Notification.Extender)} + * method of {@link Notification.Builder}. + */ + @Override + public Notification.Builder extend(Notification.Builder builder) { + Bundle carExtensions = new Bundle(); + + if (mLargeIcon != null) { + carExtensions.putParcelable(EXTRA_LARGE_ICON, mLargeIcon); + } + if (mColor != Notification.COLOR_DEFAULT) { + carExtensions.putInt(EXTRA_COLOR, mColor); + } + + if (mUnreadConversation != null) { + Bundle b = mUnreadConversation.getBundleForUnreadConversation(); + carExtensions.putBundle(EXTRA_CONVERSATION, b); + } + + builder.getExtras().putBundle(EXTRA_CAR_EXTENDER, carExtensions); + return builder; + } + + /** + * Sets the accent color to use when Android Auto presents the notification. + * + * Android Auto uses the color set with {@link Notification.Builder#setColor(int)} + * to accent the displayed notification. However, not all colors are acceptable in an + * automotive setting. This method can be used to override the color provided in the + * notification in such a situation. + */ + public CarExtender setColor(@ColorInt int color) { + mColor = color; + return this; + } + + /** + * Gets the accent color. + * + * @see setColor + */ + @ColorInt + public int getColor() { + return mColor; + } + + /** + * Sets the large icon of the car notification. + * + * If no large icon is set in the extender, Android Auto will display the icon + * specified by {@link Notification.Builder#setLargeIcon(android.graphics.Bitmap)} + * + * @param largeIcon The large icon to use in the car notification. + * @return This object for method chaining. + */ + public CarExtender setLargeIcon(Bitmap largeIcon) { + mLargeIcon = largeIcon; + return this; + } + + /** + * Gets the large icon used in this car notification, or null if no icon has been set. + * + * @return The large icon for the car notification. + * @see CarExtender#setLargeIcon + */ + public Bitmap getLargeIcon() { + return mLargeIcon; + } + + /** + * Sets the unread conversation in a message notification. + * + * @param unreadConversation The unread part of the conversation this notification conveys. + * @return This object for method chaining. + */ + public CarExtender setUnreadConversation(UnreadConversation unreadConversation) { + mUnreadConversation = unreadConversation; + return this; + } + + /** + * Returns the unread conversation conveyed by this notification. + * @see #setUnreadConversation(UnreadConversation) + */ + public UnreadConversation getUnreadConversation() { + return mUnreadConversation; + } + + /** + * A class which holds the unread messages from a conversation. + */ + public static class UnreadConversation { + private static final String KEY_AUTHOR = "author"; + private static final String KEY_TEXT = "text"; + private static final String KEY_MESSAGES = "messages"; + private static final String KEY_REMOTE_INPUT = "remote_input"; + private static final String KEY_ON_REPLY = "on_reply"; + private static final String KEY_ON_READ = "on_read"; + private static final String KEY_PARTICIPANTS = "participants"; + private static final String KEY_TIMESTAMP = "timestamp"; + + private final String[] mMessages; + private final RemoteInput mRemoteInput; + private final PendingIntent mReplyPendingIntent; + private final PendingIntent mReadPendingIntent; + private final String[] mParticipants; + private final long mLatestTimestamp; + + UnreadConversation(String[] messages, RemoteInput remoteInput, + PendingIntent replyPendingIntent, PendingIntent readPendingIntent, + String[] participants, long latestTimestamp) { + mMessages = messages; + mRemoteInput = remoteInput; + mReadPendingIntent = readPendingIntent; + mReplyPendingIntent = replyPendingIntent; + mParticipants = participants; + mLatestTimestamp = latestTimestamp; + } + + /** + * Gets the list of messages conveyed by this notification. + */ + public String[] getMessages() { + return mMessages; + } + + /** + * Gets the remote input that will be used to convey the response to a message list, or + * null if no such remote input exists. + */ + public RemoteInput getRemoteInput() { + return mRemoteInput; + } + + /** + * Gets the pending intent that will be triggered when the user replies to this + * notification. + */ + public PendingIntent getReplyPendingIntent() { + return mReplyPendingIntent; + } + + /** + * Gets the pending intent that Android Auto will send after it reads aloud all messages + * in this object's message list. + */ + public PendingIntent getReadPendingIntent() { + return mReadPendingIntent; + } + + /** + * Gets the participants in the conversation. + */ + public String[] getParticipants() { + return mParticipants; + } + + /** + * Gets the firs participant in the conversation. + */ + public String getParticipant() { + return mParticipants.length > 0 ? mParticipants[0] : null; + } + + /** + * Gets the timestamp of the conversation. + */ + public long getLatestTimestamp() { + return mLatestTimestamp; + } + + Bundle getBundleForUnreadConversation() { + Bundle b = new Bundle(); + String author = null; + if (mParticipants != null && mParticipants.length > 1) { + author = mParticipants[0]; + } + Parcelable[] messages = new Parcelable[mMessages.length]; + for (int i = 0; i < messages.length; i++) { + Bundle m = new Bundle(); + m.putString(KEY_TEXT, mMessages[i]); + m.putString(KEY_AUTHOR, author); + messages[i] = m; + } + b.putParcelableArray(KEY_MESSAGES, messages); + if (mRemoteInput != null) { + b.putParcelable(KEY_REMOTE_INPUT, mRemoteInput); + } + b.putParcelable(KEY_ON_REPLY, mReplyPendingIntent); + b.putParcelable(KEY_ON_READ, mReadPendingIntent); + b.putStringArray(KEY_PARTICIPANTS, mParticipants); + b.putLong(KEY_TIMESTAMP, mLatestTimestamp); + return b; + } + + static UnreadConversation getUnreadConversationFromBundle(Bundle b) { + if (b == null) { + return null; + } + Parcelable[] parcelableMessages = b.getParcelableArray(KEY_MESSAGES); + String[] messages = null; + if (parcelableMessages != null) { + String[] tmp = new String[parcelableMessages.length]; + boolean success = true; + for (int i = 0; i < tmp.length; i++) { + if (!(parcelableMessages[i] instanceof Bundle)) { + success = false; + break; + } + tmp[i] = ((Bundle) parcelableMessages[i]).getString(KEY_TEXT); + if (tmp[i] == null) { + success = false; + break; + } + } + if (success) { + messages = tmp; + } else { + return null; + } + } + + PendingIntent onRead = b.getParcelable(KEY_ON_READ); + PendingIntent onReply = b.getParcelable(KEY_ON_REPLY); + + RemoteInput remoteInput = b.getParcelable(KEY_REMOTE_INPUT); + + String[] participants = b.getStringArray(KEY_PARTICIPANTS); + if (participants == null || participants.length != 1) { + return null; + } + + return new UnreadConversation(messages, + remoteInput, + onReply, + onRead, + participants, b.getLong(KEY_TIMESTAMP)); + } + }; + + /** + * Builder class for {@link CarExtender.UnreadConversation} objects. + */ + public static class Builder { + private final List<String> mMessages = new ArrayList<String>(); + private final String mParticipant; + private RemoteInput mRemoteInput; + private PendingIntent mReadPendingIntent; + private PendingIntent mReplyPendingIntent; + private long mLatestTimestamp; + + /** + * Constructs a new builder for {@link CarExtender.UnreadConversation}. + * + * @param name The name of the other participant in the conversation. + */ + public Builder(String name) { + mParticipant = name; + } + + /** + * Appends a new unread message to the list of messages for this conversation. + * + * The messages should be added from oldest to newest. + * + * @param message The text of the new unread message. + * @return This object for method chaining. + */ + public Builder addMessage(String message) { + mMessages.add(message); + return this; + } + + /** + * Sets the pending intent and remote input which will convey the reply to this + * notification. + * + * @param pendingIntent The pending intent which will be triggered on a reply. + * @param remoteInput The remote input parcelable which will carry the reply. + * @return This object for method chaining. + * + * @see CarExtender.UnreadConversation#getRemoteInput + * @see CarExtender.UnreadConversation#getReplyPendingIntent + */ + public Builder setReplyAction( + PendingIntent pendingIntent, RemoteInput remoteInput) { + mRemoteInput = remoteInput; + mReplyPendingIntent = pendingIntent; + + return this; + } + + /** + * Sets the pending intent that will be sent once the messages in this notification + * are read. + * + * @param pendingIntent The pending intent to use. + * @return This object for method chaining. + */ + public Builder setReadPendingIntent(PendingIntent pendingIntent) { + mReadPendingIntent = pendingIntent; + return this; + } + + /** + * Sets the timestamp of the most recent message in an unread conversation. + * + * If a messaging notification has been posted by your application and has not + * yet been cancelled, posting a later notification with the same id and tag + * but without a newer timestamp may result in Android Auto not displaying a + * heads up notification for the later notification. + * + * @param timestamp The timestamp of the most recent message in the conversation. + * @return This object for method chaining. + */ + public Builder setLatestTimestamp(long timestamp) { + mLatestTimestamp = timestamp; + return this; + } + + /** + * Builds a new unread conversation object. + * + * @return The new unread conversation object. + */ + public UnreadConversation build() { + String[] messages = mMessages.toArray(new String[mMessages.size()]); + String[] participants = { mParticipant }; + return new UnreadConversation(messages, mRemoteInput, mReplyPendingIntent, + mReadPendingIntent, participants, mLatestTimestamp); + } + } + } + + /** + * <p> + * Helper class to add content info extensions to notifications. To create a notification with + * content info extensions: + * <ol> + * <li>Create an {@link Notification.Builder}, setting any desired properties. + * <li>Create a {@link ContentInfoExtender}. + * <li>Set content info specific properties using the {@code add} and {@code set} methods of + * {@link ContentInfoExtender}. + * <li>Call {@link Notification.Builder#extend(Notification.Extender)} to apply the extensions + * to a notification. + * </ol> + * + * <pre class="prettyprint">Notification notification = new Notification.Builder(context) * ... * .extend(new ContentInfoExtender() * .set*(...)) * .build(); * </pre> + * <p> + * Content info extensions can be accessed on an existing notification by using the + * {@code ContentInfoExtender(Notification)} constructor, and then using the {@code get} methods + * to access values. + */ + public static final class ContentInfoExtender implements Extender { + private static final String TAG = "ContentInfoExtender"; + + // Key for the Content info extensions bundle in the main Notification extras bundle + private static final String EXTRA_CONTENT_INFO_EXTENDER = "android.CONTENT_INFO_EXTENSIONS"; + + // Keys within EXTRA_CONTENT_INFO_EXTENDER for individual content info options. + + private static final String KEY_CONTENT_TYPE = "android.contentType"; + + private static final String KEY_CONTENT_GENRES = "android.contentGenre"; + + private static final String KEY_CONTENT_PRICING_TYPE = "android.contentPricing.type"; + + private static final String KEY_CONTENT_PRICING_VALUE = "android.contentPricing.value"; + + private static final String KEY_CONTENT_STATUS = "android.contentStatus"; + + private static final String KEY_CONTENT_MATURITY_RATING = "android.contentMaturity"; + + private static final String KEY_CONTENT_RUN_LENGTH = "android.contentLength"; + + + /** + * Value to be used with {@link #setContentTypes} to indicate that the content referred by + * the notification item is a video clip. + */ + public static final String CONTENT_TYPE_VIDEO = "android.contentType.video"; + + /** + * Value to be used with {@link #setContentTypes} to indicate that the content referred by + * the notification item is a movie. + */ + public static final String CONTENT_TYPE_MOVIE = "android.contentType.movie"; + + /** + * Value to be used with {@link #setContentTypes} to indicate that the content referred by + * the notification item is a trailer. + */ + public static final String CONTENT_TYPE_TRAILER = "android.contentType.trailer"; + + /** + * Value to be used with {@link #setContentTypes} to indicate that the content referred by + * the notification item is serial. It can refer to an entire show, a single season or + * series, or a single episode. + */ + public static final String CONTENT_TYPE_SERIAL = "android.contentType.serial"; + + /** + * Value to be used with {@link #setContentTypes} to indicate that the content referred by + * the notification item is a song or album. + */ + public static final String CONTENT_TYPE_MUSIC = "android.contentType.music"; + + /** + * Value to be used with {@link #setContentTypes} to indicate that the content referred by + * the notification item is a radio station. + */ + public static final String CONTENT_TYPE_RADIO = "android.contentType.radio"; + + /** + * Value to be used with {@link #setContentTypes} to indicate that the content referred by + * the notification item is a podcast. + */ + public static final String CONTENT_TYPE_PODCAST = "android.contentType.podcast"; + + /** + * Value to be used with {@link #setContentTypes} to indicate that the content referred by + * the notification item is a news item. + */ + public static final String CONTENT_TYPE_NEWS = "android.contentType.news"; + + /** + * Value to be used with {@link #setContentTypes} to indicate that the content referred by + * the notification item is sports. + */ + public static final String CONTENT_TYPE_SPORTS = "android.contentType.sports"; + + /** + * Value to be used with {@link #setContentTypes} to indicate that the content referred by + * the notification item is an application. + */ + public static final String CONTENT_TYPE_APP = "android.contentType.app"; + + /** + * Value to be used with {@link #setContentTypes} to indicate that the content referred by + * the notification item is a game. + */ + public static final String CONTENT_TYPE_GAME = "android.contentType.game"; + + /** + * Value to be used with {@link #setContentTypes} to indicate that the content referred by + * the notification item is a book. + */ + public static final String CONTENT_TYPE_BOOK = "android.contentType.book"; + + /** + * Value to be used with {@link #setContentTypes} to indicate that the content referred by + * the notification item is a comic book. + */ + public static final String CONTENT_TYPE_COMIC = "android.contentType.comic"; + + /** + * Value to be used with {@link #setContentTypes} to indicate that the content referred by + * the notification item is a magazine. + */ + public static final String CONTENT_TYPE_MAGAZINE = "android.contentType.magazine"; + + /** + * Value to be used with {@link #setContentTypes} to indicate that the content referred by + * the notification item is a website. + */ + public static final String CONTENT_TYPE_WEBSITE = "android.contentType.website"; + + + /** + * Value to be used with {@link #setPricingInformation} to indicate that the content + * referred by the notification item is free to consume. + */ + public static final String CONTENT_PRICING_FREE = "android.contentPrice.free"; + + /** + * Value to be used with {@link #setPricingInformation} to indicate that the content + * referred by the notification item is available as a rental, and the price value provided + * is the rental price for the item. + */ + public static final String CONTENT_PRICING_RENTAL = "android.contentPrice.rental"; + + /** + * Value to be used with {@link #setPricingInformation} to indicate that the content + * referred by the notification item is available for purchase, and the price value provided + * is the purchase price for the item. + */ + public static final String CONTENT_PRICING_PURCHASE = "android.contentPrice.purchase"; + + /** + * Value to be used with {@link #setPricingInformation} to indicate that the content + * referred by the notification item is available as part of a subscription based service, + * and the price value provided is the subscription price for the service. + */ + public static final String CONTENT_PRICING_SUBSCRIPTION = + "android.contentPrice.subscription"; + + /** + * Value to be used with {@link #setStatus} to indicate that the content referred by the + * notification is available and ready to be consumed immediately. + */ + public static final int CONTENT_STATUS_READY = 0; + + /** + * Value to be used with {@link #setStatus} to indicate that the content referred by the + * notification is pending, waiting on either a download or purchase operation to complete + * before it can be consumed. + */ + public static final int CONTENT_STATUS_PENDING = 1; + + /** + * Value to be used with {@link #setStatus} to indicate that the content referred by the + * notification is available, but needs to be first purchased, rented, subscribed or + * downloaded before it can be consumed. + */ + public static final int CONTENT_STATUS_AVAILABLE = 2; + + /** + * Value to be used with {@link #setStatus} to indicate that the content referred by the + * notification is not available. This could be content not available in a certain region or + * incompatible with the device in use. + */ + public static final int CONTENT_STATUS_UNAVAILABLE = 3; + + /** + * Value to be used with {@link #setMaturityRating} to indicate that the content referred by + * the notification is suitable for all audiences. + */ + public static final String CONTENT_MATURITY_ALL = "android.contentMaturity.all"; + + /** + * Value to be used with {@link #setMaturityRating} to indicate that the content + * referred by the notification is suitable for audiences of low maturity and above. + */ + public static final String CONTENT_MATURITY_LOW = "android.contentMaturity.low"; + + /** + * Value to be used with {@link #setMaturityRating} to indicate that the content + * referred by the notification is suitable for audiences of medium maturity and above. + */ + public static final String CONTENT_MATURITY_MEDIUM = "android.contentMaturity.medium"; + + /** + * Value to be used with {@link #setMaturityRating} to indicate that the content + * referred by the notification is suitable for audiences of high maturity and above. + */ + public static final String CONTENT_MATURITY_HIGH = "android.contentMaturity.high"; + + private String[] mTypes; + private String[] mGenres; + private String mPricingType; + private String mPricingValue; + private int mContentStatus = -1; + private String mMaturityRating; + private long mRunLength = -1; + + /** + * Create a {@link ContentInfoExtender} with default options. + */ + public ContentInfoExtender() { + } + + /** + * Create a {@link ContentInfoExtender} from the ContentInfoExtender options of an existing + * Notification. + * + * @param notif The notification from which to copy options. + */ + public ContentInfoExtender(Notification notif) { + Bundle contentBundle = notif.extras == null ? + null : notif.extras.getBundle(EXTRA_CONTENT_INFO_EXTENDER); + if (contentBundle != null) { + mTypes = contentBundle.getStringArray(KEY_CONTENT_TYPE); + mGenres = contentBundle.getStringArray(KEY_CONTENT_GENRES); + mPricingType = contentBundle.getString(KEY_CONTENT_PRICING_TYPE); + mPricingValue = contentBundle.getString(KEY_CONTENT_PRICING_VALUE); + mContentStatus = contentBundle.getInt(KEY_CONTENT_STATUS, -1); + mMaturityRating = contentBundle.getString(KEY_CONTENT_MATURITY_RATING); + mRunLength = contentBundle.getLong(KEY_CONTENT_RUN_LENGTH, -1); + } + } + + /** + * Apply content extensions to a notification that is being built. This is typically called + * by the {@link Notification.Builder#extend(Notification.Extender)} method of + * {@link Notification.Builder}. + */ + @Override + public Notification.Builder extend(Notification.Builder builder) { + Bundle contentBundle = new Bundle(); + + if (mTypes != null) { + contentBundle.putStringArray(KEY_CONTENT_TYPE, mTypes); + } + if (mGenres != null) { + contentBundle.putStringArray(KEY_CONTENT_GENRES, mGenres); + } + if (mPricingType != null) { + contentBundle.putString(KEY_CONTENT_PRICING_TYPE, mPricingType); + } + if (mPricingValue != null) { + contentBundle.putString(KEY_CONTENT_PRICING_VALUE, mPricingValue); + } + if (mContentStatus != -1) { + contentBundle.putInt(KEY_CONTENT_STATUS, mContentStatus); + } + if (mMaturityRating != null) { + contentBundle.putString(KEY_CONTENT_MATURITY_RATING, mMaturityRating); + } + if (mRunLength > 0) { + contentBundle.putLong(KEY_CONTENT_RUN_LENGTH, mRunLength); + } + + builder.getExtras().putBundle(EXTRA_CONTENT_INFO_EXTENDER, contentBundle); + return builder; + } + + /** + * Sets the content types associated with the notification content. The first tag entry will + * be considered the primary type for the content and will be used for ranking purposes. + * Other secondary type tags may be provided, if applicable, and may be used for filtering + * purposes. + * + * @param types Array of predefined type tags (see the <code>CONTENT_TYPE_*</code> + * constants) that describe the content referred to by a notification. + */ + public ContentInfoExtender setContentTypes(String[] types) { + mTypes = types; + return this; + } + + /** + * Returns an array containing the content types that describe the content associated with + * the notification. The first tag entry is considered the primary type for the content, and + * is used for content ranking purposes. + * + * @return An array of predefined type tags (see the <code>CONTENT_TYPE_*</code> constants) + * that describe the content associated with the notification. + * @see ContentInfoExtender#setContentTypes + */ + public String[] getContentTypes() { + return mTypes; + } + + /** + * Returns the primary content type tag for the content associated with the notification. + * + * @return A predefined type tag (see the <code>CONTENT_TYPE_*</code> constants) indicating + * the primary type for the content associated with the notification. + * @see ContentInfoExtender#setContentTypes + */ + public String getPrimaryContentType() { + if (mTypes == null || mTypes.length == 0) { + return null; + } + return mTypes[0]; + } + + /** + * Sets the content genres associated with the notification content. These genres may be + * used for content ranking. Genres are open ended String tags. + * <p> + * Some examples: "comedy", "action", "dance", "electronica", "racing", etc. + * + * @param genres Array of genre string tags that describe the content referred to by a + * notification. + */ + public ContentInfoExtender setGenres(String[] genres) { + mGenres = genres; + return this; + } + + /** + * Returns an array containing the content genres that describe the content associated with + * the notification. + * + * @return An array of genre tags that describe the content associated with the + * notification. + * @see ContentInfoExtender#setGenres + */ + public String[] getGenres() { + return mGenres; + } + + /** + * Sets the pricing and availability information for the content associated with the + * notification. The provided information will indicate the access model for the content + * (free, rental, purchase or subscription) and the price value (if not free). + * + * @param priceType Pricing type for this content. Must be one of the predefined pricing + * type tags (see the <code>CONTENT_PRICING_*</code> constants). + * @param priceValue A string containing a representation of the content price in the + * current locale and currency. + * @return This object for method chaining. + */ + public ContentInfoExtender setPricingInformation(String priceType, String priceValue) { + mPricingType = priceType; + mPricingValue = priceValue; + return this; + } + + /** + * Gets the pricing type for the content associated with the notification. + * + * @return A predefined tag indicating the pricing type for the content (see the <code> + * CONTENT_PRICING_*</code> constants). + * @see ContentInfoExtender#setPricingInformation + */ + public String getPricingType() { + return mPricingType; + } + + /** + * Gets the price value (when applicable) for the content associated with a notification. + * The value will be provided as a String containing the price in the appropriate currency + * for the current locale. + * + * @return A string containing a representation of the content price in the current locale + * and currency. + * @see ContentInfoExtender#setPricingInformation + */ + public String getPricingValue() { + if (mPricingType == null || CONTENT_PRICING_FREE.equals(mPricingType)) { + return null; + } + return mPricingValue; + } + + /** + * Sets the availability status for the content associated with the notification. This + * status indicates whether the referred content is ready to be consumed on the device, or + * if the user must first purchase, rent, subscribe to, or download the content. + * + * @param contentStatus The status value for this content. Must be one of the predefined + * content status values (see the <code>CONTENT_STATUS_*</code> constants). + */ + public ContentInfoExtender setStatus(int contentStatus) { + mContentStatus = contentStatus; + return this; + } + + /** + * Returns status value for the content associated with the notification. This status + * indicates whether the referred content is ready to be consumed on the device, or if the + * user must first purchase, rent, subscribe to, or download the content. + * + * @return The status value for this content, or -1 is a valid status has not been specified + * (see the <code>CONTENT_STATUS_*</code> for the defined valid status values). + * @see ContentInfoExtender#setStatus + */ + public int getStatus() { + return mContentStatus; + } + + /** + * Sets the maturity level rating for the content associated with the notification. + * + * @param maturityRating A tag indicating the maturity level rating for the content. This + * tag must be one of the predefined maturity rating tags (see the <code> + * CONTENT_MATURITY_*</code> constants). + */ + public ContentInfoExtender setMaturityRating(String maturityRating) { + mMaturityRating = maturityRating; + return this; + } + + /** + * Returns the maturity level rating for the content associated with the notification. + * + * @return returns a predefined tag indicating the maturity level rating for the content + * (see the <code> CONTENT_MATURITY_*</code> constants). + * @see ContentInfoExtender#setMaturityRating + */ + public String getMaturityRating() { + return mMaturityRating; + } + + /** + * Sets the running time (when applicable) for the content associated with the notification. + * + * @param length The runing time, in seconds, of the content associated with the + * notification. + */ + public ContentInfoExtender setRunningTime(long length) { + mRunLength = length; + return this; + } + + /** + * Returns the running time for the content associated with the notification. + * + * @return The running time, in seconds, of the content associated with the notification. + * @see ContentInfoExtender#setRunningTime + */ + public long getRunningTime() { + return mRunLength; + } + } + + /** * Get an array of Notification objects from a parcelable array bundle field. * Update the bundle to have a typed array so fetches in the future don't need * to do an array copy. diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index cf54107..479327d 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -27,6 +27,9 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.StrictMode; import android.os.UserHandle; +import android.service.notification.Condition; +import android.service.notification.IConditionListener; +import android.service.notification.ZenModeConfig; import android.util.Log; /** @@ -276,5 +279,53 @@ public class NotificationManager } } + /** + * @hide + */ + public void setZenMode(int mode) { + INotificationManager service = getService(); + try { + service.setZenMode(mode); + } catch (RemoteException e) { + } + } + + /** + * @hide + */ + public void requestZenModeConditions(IConditionListener listener, int relevance) { + INotificationManager service = getService(); + try { + service.requestZenModeConditions(listener, relevance); + } catch (RemoteException e) { + } + } + + /** + * @hide + */ + public void setZenModeCondition(Condition exitCondition) { + INotificationManager service = getService(); + try { + service.setZenModeCondition(exitCondition); + } catch (RemoteException e) { + } + } + + /** + * @hide + */ + public Condition getZenModeCondition() { + INotificationManager service = getService(); + try { + final ZenModeConfig config = service.getZenModeConfig(); + if (config != null) { + return config.exitCondition; + } + } catch (RemoteException e) { + } + return null; + } + private Context mContext; } diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java index 1691d8e..79797c9 100644 --- a/core/java/android/app/ResourcesManager.java +++ b/core/java/android/app/ResourcesManager.java @@ -25,33 +25,29 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.ResourcesKey; import android.hardware.display.DisplayManagerGlobal; -import android.os.IBinder; import android.util.ArrayMap; import android.util.DisplayMetrics; +import android.util.Log; +import android.util.Pair; import android.util.Slog; import android.view.Display; -import android.view.DisplayAdjustments; - import java.lang.ref.WeakReference; import java.util.Locale; /** @hide */ public class ResourcesManager { static final String TAG = "ResourcesManager"; - static final boolean DEBUG_CACHE = false; - static final boolean DEBUG_STATS = true; + private static final boolean DEBUG = false; private static ResourcesManager sResourcesManager; - final ArrayMap<ResourcesKey, WeakReference<Resources> > mActiveResources - = new ArrayMap<ResourcesKey, WeakReference<Resources> >(); - - final ArrayMap<DisplayAdjustments, DisplayMetrics> mDefaultDisplayMetrics - = new ArrayMap<DisplayAdjustments, DisplayMetrics>(); + private final ArrayMap<ResourcesKey, WeakReference<Resources> > mActiveResources = + new ArrayMap<>(); + private final ArrayMap<Pair<Integer, Configuration>, WeakReference<Display>> mDisplays = + new ArrayMap<>(); CompatibilityInfo mResCompatibilityInfo; Configuration mResConfiguration; - final Configuration mTmpConfig = new Configuration(); public static ResourcesManager getInstance() { synchronized (ResourcesManager.class) { @@ -66,46 +62,18 @@ public class ResourcesManager { return mResConfiguration; } - public void flushDisplayMetricsLocked() { - mDefaultDisplayMetrics.clear(); - } - - public DisplayMetrics getDisplayMetricsLocked(int displayId) { - return getDisplayMetricsLocked(displayId, DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS); + DisplayMetrics getDisplayMetricsLocked() { + return getDisplayMetricsLocked(Display.DEFAULT_DISPLAY); } - public DisplayMetrics getDisplayMetricsLocked(int displayId, DisplayAdjustments daj) { - boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY); - DisplayMetrics dm = isDefaultDisplay ? mDefaultDisplayMetrics.get(daj) : null; - if (dm != null) { - return dm; - } - dm = new DisplayMetrics(); - - DisplayManagerGlobal displayManager = DisplayManagerGlobal.getInstance(); - if (displayManager == null) { - // may be null early in system startup - dm.setToDefaults(); - return dm; - } - - if (isDefaultDisplay) { - mDefaultDisplayMetrics.put(daj, dm); - } - - Display d = displayManager.getCompatibleDisplay(displayId, daj); - if (d != null) { - d.getMetrics(dm); + DisplayMetrics getDisplayMetricsLocked(int displayId) { + DisplayMetrics dm = new DisplayMetrics(); + final Display display = getAdjustedDisplay(displayId, Configuration.EMPTY); + if (display != null) { + display.getMetrics(dm); } else { - // Display no longer exists - // FIXME: This would not be a problem if we kept the Display object around - // instead of using the raw display id everywhere. The Display object caches - // its information even after the display has been removed. dm.setToDefaults(); } - //Slog.i("foo", "New metrics: w=" + metrics.widthPixels + " h=" - // + metrics.heightPixels + " den=" + metrics.density - // + " xdpi=" + metrics.xdpi + " ydpi=" + metrics.ydpi); return dm; } @@ -141,39 +109,73 @@ public class ResourcesManager { } /** + * Returns an adjusted {@link Display} object based on the inputs or null if display isn't + * available. + * + * @param displayId display Id. + * @param overrideConfiguration override configurations. + */ + public Display getAdjustedDisplay(final int displayId, Configuration overrideConfiguration) { + final Configuration configCopy = (overrideConfiguration != null) + ? new Configuration(overrideConfiguration) : new Configuration(); + final Pair<Integer, Configuration> key = Pair.create(displayId, configCopy); + synchronized (this) { + WeakReference<Display> wd = mDisplays.get(key); + if (wd != null) { + final Display display = wd.get(); + if (display != null) { + return display; + } + } + final DisplayManagerGlobal dm = DisplayManagerGlobal.getInstance(); + if (dm == null) { + // may be null early in system startup + return null; + } + final Display display = dm.getRealDisplay(displayId, key.second); + if (display != null) { + mDisplays.put(key, new WeakReference<>(display)); + } + return display; + } + } + + /** * Creates the top level Resources for applications with the given compatibility info. * * @param resDir the resource directory. + * @param splitResDirs split resource directories. * @param overlayDirs the resource overlay directories. * @param libDirs the shared library resource dirs this app references. - * @param compatInfo the compability info. Must not be null. - * @param token the application token for determining stack bounds. + * @param displayId display Id. + * @param overrideConfiguration override configurations. + * @param compatInfo the compatibility info. Must not be null. */ - public Resources getTopLevelResources(String resDir, String[] splitResDirs, + Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs, String[] libDirs, int displayId, - Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) { + Configuration overrideConfiguration, CompatibilityInfo compatInfo) { final float scale = compatInfo.applicationScale; - ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, scale, token); + Configuration overrideConfigCopy = (overrideConfiguration != null) + ? new Configuration(overrideConfiguration) : null; + ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfigCopy, scale); Resources r; synchronized (this) { // Resources is app scale dependent. - if (false) { - Slog.w(TAG, "getTopLevelResources: " + resDir + " / " + scale); - } + if (DEBUG) Slog.w(TAG, "getTopLevelResources: " + resDir + " / " + scale); + WeakReference<Resources> wr = mActiveResources.get(key); r = wr != null ? wr.get() : null; - //if (r != null) Slog.i(TAG, "isUpToDate " + resDir + ": " + r.getAssets().isUpToDate()); + //if (r != null) Log.i(TAG, "isUpToDate " + resDir + ": " + r.getAssets().isUpToDate()); if (r != null && r.getAssets().isUpToDate()) { - if (false) { - Slog.w(TAG, "Returning cached resources " + r + " " + resDir - + ": appScale=" + r.getCompatibilityInfo().applicationScale); - } + if (DEBUG) Slog.w(TAG, "Returning cached resources " + r + " " + resDir + + ": appScale=" + r.getCompatibilityInfo().applicationScale + + " key=" + key + " overrideConfig=" + overrideConfiguration); return r; } } //if (r != null) { - // Slog.w(TAG, "Throwing away out-of-date resources!!!! " + // Log.w(TAG, "Throwing away out-of-date resources!!!! " // + r + " " + resDir); //} @@ -203,17 +205,21 @@ public class ResourcesManager { if (libDirs != null) { for (String libDir : libDirs) { - if (assets.addAssetPath(libDir) == 0) { - Slog.w(TAG, "Asset path '" + libDir + - "' does not exist or contains no resources."); + if (libDir.endsWith(".apk")) { + // Avoid opening files we know do not have resources, + // like code-only .jar files. + if (assets.addAssetPath(libDir) == 0) { + Log.w(TAG, "Asset path '" + libDir + + "' does not exist or contains no resources."); + } } } } - //Slog.i(TAG, "Resource: key=" + key + ", display metrics=" + metrics); + //Log.i(TAG, "Resource: key=" + key + ", display metrics=" + metrics); DisplayMetrics dm = getDisplayMetricsLocked(displayId); Configuration config; - boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY); + final boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY); final boolean hasOverrideConfig = key.hasOverrideConfiguration(); if (!isDefaultDisplay || hasOverrideConfig) { config = new Configuration(getConfiguration()); @@ -222,16 +228,14 @@ public class ResourcesManager { } if (hasOverrideConfig) { config.updateFrom(key.mOverrideConfiguration); + if (DEBUG) Slog.v(TAG, "Applied overrideConfig=" + key.mOverrideConfiguration); } } else { config = getConfiguration(); } - r = new Resources(assets, dm, config, compatInfo, token); - if (false) { - Slog.i(TAG, "Created app resources " + resDir + " " + r + ": " - + r.getConfiguration() + " appScale=" - + r.getCompatibilityInfo().applicationScale); - } + r = new Resources(assets, dm, config, compatInfo); + if (DEBUG) Slog.i(TAG, "Created app resources " + resDir + " " + r + ": " + + r.getConfiguration() + " appScale=" + r.getCompatibilityInfo().applicationScale); synchronized (this) { WeakReference<Resources> wr = mActiveResources.get(key); @@ -244,24 +248,26 @@ public class ResourcesManager { } // XXX need to remove entries when weak references go away - mActiveResources.put(key, new WeakReference<Resources>(r)); + mActiveResources.put(key, new WeakReference<>(r)); + if (DEBUG) Slog.v(TAG, "mActiveResources.size()=" + mActiveResources.size()); return r; } } - public final boolean applyConfigurationToResourcesLocked(Configuration config, + final boolean applyConfigurationToResourcesLocked(Configuration config, CompatibilityInfo compat) { if (mResConfiguration == null) { mResConfiguration = new Configuration(); } if (!mResConfiguration.isOtherSeqNewer(config) && compat == null) { - if (DEBUG_CONFIGURATION) Slog.v(TAG, "Skipping new config: curSeq=" + if (DEBUG || DEBUG_CONFIGURATION) Slog.v(TAG, "Skipping new config: curSeq=" + mResConfiguration.seq + ", newSeq=" + config.seq); return false; } int changes = mResConfiguration.updateFrom(config); - flushDisplayMetricsLocked(); - DisplayMetrics defaultDisplayMetrics = getDisplayMetricsLocked(Display.DEFAULT_DISPLAY); + // Things might have changed in display manager, so clear the cached displays. + mDisplays.clear(); + DisplayMetrics defaultDisplayMetrics = getDisplayMetricsLocked(); if (compat != null && (mResCompatibilityInfo == null || !mResCompatibilityInfo.equals(compat))) { @@ -283,11 +289,11 @@ public class ResourcesManager { Configuration tmpConfig = null; - for (int i=mActiveResources.size()-1; i>=0; i--) { + for (int i = mActiveResources.size() - 1; i >= 0; i--) { ResourcesKey key = mActiveResources.keyAt(i); Resources r = mActiveResources.valueAt(i).get(); if (r != null) { - if (DEBUG_CONFIGURATION) Slog.v(TAG, "Changing resources " + if (DEBUG || DEBUG_CONFIGURATION) Slog.v(TAG, "Changing resources " + r + " config to: " + config); int displayId = key.mDisplayId; boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY); diff --git a/core/java/android/app/SearchDialog.java b/core/java/android/app/SearchDialog.java index af1810b..c719a0e 100644 --- a/core/java/android/app/SearchDialog.java +++ b/core/java/android/app/SearchDialog.java @@ -639,6 +639,12 @@ public class SearchDialog extends Dialog { public ActionMode startActionModeForChild(View child, ActionMode.Callback callback) { return null; } + + @Override + public ActionMode startActionModeForChild( + View child, ActionMode.Callback callback, int type) { + return null; + } } private boolean isEmpty(AutoCompleteTextView actv) { diff --git a/core/java/android/app/SearchManager.java b/core/java/android/app/SearchManager.java index d7c4467..fa27631 100644 --- a/core/java/android/app/SearchManager.java +++ b/core/java/android/app/SearchManager.java @@ -969,7 +969,7 @@ public class SearchManager intent.setComponent(comp); if (inclContext) { IActivityManager am = ActivityManagerNative.getDefault(); - Bundle extras = am.getAssistContextExtras(0); + Bundle extras = am.getAssistContextExtras(ActivityManager.ASSIST_CONTEXT_BASIC); if (extras != null) { intent.replaceExtras(extras); } @@ -985,12 +985,12 @@ public class SearchManager * Launch an assist action for the current top activity. * @hide */ - public boolean launchAssistAction(int requestType, String hint, int userHandle) { + public boolean launchAssistAction(String hint, int userHandle) { try { if (mService == null) { return false; } - return mService.launchAssistAction(requestType, hint, userHandle); + return mService.launchAssistAction(hint, userHandle); } catch (RemoteException re) { Log.e(TAG, "launchAssistAction() failed: " + re); return false; diff --git a/core/java/android/app/SearchableInfo.java b/core/java/android/app/SearchableInfo.java index 922ebdd..c7d2140 100644 --- a/core/java/android/app/SearchableInfo.java +++ b/core/java/android/app/SearchableInfo.java @@ -19,6 +19,7 @@ package android.app; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import android.annotation.StringRes; import android.content.ComponentName; import android.content.Context; import android.content.pm.ActivityInfo; @@ -682,6 +683,7 @@ public final class SearchableInfo implements Parcelable { * @return A resource id, or {@code 0} if no language model was specified. * @see android.R.styleable#Searchable_voiceLanguageModel */ + @StringRes public int getVoiceLanguageModeId() { return mVoiceLanguageModeId; } @@ -692,6 +694,7 @@ public final class SearchableInfo implements Parcelable { * @return A resource id, or {@code 0} if no voice prompt text was specified. * @see android.R.styleable#Searchable_voicePromptText */ + @StringRes public int getVoicePromptTextId() { return mVoicePromptTextId; } @@ -702,6 +705,7 @@ public final class SearchableInfo implements Parcelable { * @return A resource id, or {@code 0} if no language was specified. * @see android.R.styleable#Searchable_voiceLanguage */ + @StringRes public int getVoiceLanguageId() { return mVoiceLanguageId; } diff --git a/core/java/android/app/Service.java b/core/java/android/app/Service.java index c8e0031..21a3543 100644 --- a/core/java/android/app/Service.java +++ b/core/java/android/app/Service.java @@ -16,6 +16,7 @@ package android.app; +import android.annotation.Nullable; import android.content.ComponentCallbacks2; import android.content.ComponentName; import android.content.Intent; @@ -498,6 +499,7 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac * @return Return an IBinder through which clients can call on to the * service. */ + @Nullable public abstract IBinder onBind(Intent intent); /** diff --git a/core/java/android/app/SharedPreferencesImpl.java b/core/java/android/app/SharedPreferencesImpl.java index 4427ce1..e617553 100644 --- a/core/java/android/app/SharedPreferencesImpl.java +++ b/core/java/android/app/SharedPreferencesImpl.java @@ -16,6 +16,7 @@ package android.app; +import android.annotation.Nullable; import android.content.SharedPreferences; import android.os.FileUtils; import android.os.Looper; @@ -217,7 +218,8 @@ final class SharedPreferencesImpl implements SharedPreferences { } } - public String getString(String key, String defValue) { + @Nullable + public String getString(String key, @Nullable String defValue) { synchronized (this) { awaitLoadedLocked(); String v = (String)mMap.get(key); @@ -225,7 +227,8 @@ final class SharedPreferencesImpl implements SharedPreferences { } } - public Set<String> getStringSet(String key, Set<String> defValues) { + @Nullable + public Set<String> getStringSet(String key, @Nullable Set<String> defValues) { synchronized (this) { awaitLoadedLocked(); Set<String> v = (Set<String>) mMap.get(key); @@ -303,13 +306,13 @@ final class SharedPreferencesImpl implements SharedPreferences { private final Map<String, Object> mModified = Maps.newHashMap(); private boolean mClear = false; - public Editor putString(String key, String value) { + public Editor putString(String key, @Nullable String value) { synchronized (this) { mModified.put(key, value); return this; } } - public Editor putStringSet(String key, Set<String> values) { + public Editor putStringSet(String key, @Nullable Set<String> values) { synchronized (this) { mModified.put(key, (values == null) ? null : new HashSet<String>(values)); diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java new file mode 100644 index 0000000..fd7bae7 --- /dev/null +++ b/core/java/android/app/SystemServiceRegistry.java @@ -0,0 +1,784 @@ +/* + * Copyright (C) 2015 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.app; + +import com.android.internal.app.IAppOpsService; +import com.android.internal.appwidget.IAppWidgetService; +import com.android.internal.os.IDropBoxManagerService; + +import android.accounts.AccountManager; +import android.accounts.IAccountManager; +import android.app.admin.DevicePolicyManager; +import android.app.job.IJobScheduler; +import android.app.job.JobScheduler; +import android.app.trust.TrustManager; +import android.app.usage.IUsageStatsManager; +import android.app.usage.UsageStatsManager; +import android.appwidget.AppWidgetManager; +import android.bluetooth.BluetoothManager; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.IRestrictionsManager; +import android.content.RestrictionsManager; +import android.content.pm.ILauncherApps; +import android.content.pm.LauncherApps; +import android.content.res.Resources; +import android.hardware.ConsumerIrManager; +import android.hardware.ISerialManager; +import android.hardware.SensorManager; +import android.hardware.SerialManager; +import android.hardware.SystemSensorManager; +import android.hardware.camera2.CameraManager; +import android.hardware.display.DisplayManager; +import android.hardware.hdmi.HdmiControlManager; +import android.hardware.hdmi.IHdmiControlService; +import android.hardware.input.InputManager; +import android.hardware.usb.IUsbManager; +import android.hardware.usb.UsbManager; +import android.hardware.radio.RadioManager; +import android.location.CountryDetector; +import android.location.ICountryDetector; +import android.location.ILocationManager; +import android.location.LocationManager; +import android.media.AudioManager; +import android.media.MediaRouter; +import android.media.midi.IMidiManager; +import android.media.midi.MidiManager; +import android.media.projection.MediaProjectionManager; +import android.media.session.MediaSessionManager; +import android.media.tv.ITvInputManager; +import android.media.tv.TvInputManager; +import android.net.ConnectivityManager; +import android.net.EthernetManager; +import android.net.IConnectivityManager; +import android.net.IEthernetManager; +import android.net.INetworkPolicyManager; +import android.net.NetworkPolicyManager; +import android.net.NetworkScoreManager; +import android.net.nsd.INsdManager; +import android.net.nsd.NsdManager; +import android.net.wifi.IRttManager; +import android.net.wifi.IWifiManager; +import android.net.wifi.IWifiScanner; +import android.net.wifi.RttManager; +import android.net.wifi.WifiManager; +import android.net.wifi.WifiScanner; +import android.net.wifi.p2p.IWifiP2pManager; +import android.net.wifi.p2p.WifiP2pManager; +import android.net.wifi.passpoint.IWifiPasspointManager; +import android.net.wifi.passpoint.WifiPasspointManager; +import android.nfc.NfcManager; +import android.os.BatteryManager; +import android.os.DropBoxManager; +import android.os.IBinder; +import android.os.IPowerManager; +import android.os.IUserManager; +import android.os.PowerManager; +import android.os.Process; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemVibrator; +import android.os.UserHandle; +import android.os.UserManager; +import android.os.Vibrator; +import android.os.storage.StorageManager; +import android.print.IPrintManager; +import android.print.PrintManager; +import android.service.fingerprint.FingerprintManager; +import android.service.fingerprint.IFingerprintService; +import android.service.persistentdata.IPersistentDataBlockService; +import android.service.persistentdata.PersistentDataBlockManager; +import android.telecom.TelecomManager; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.util.Log; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.PhoneLayoutInflater; +import android.view.WindowManager; +import android.view.WindowManagerImpl; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.CaptioningManager; +import android.view.inputmethod.InputMethodManager; +import android.view.textservice.TextServicesManager; + +import java.util.HashMap; + +/** + * Manages all of the system services that can be returned by {@link Context#getSystemService}. + * Used by {@link ContextImpl}. + */ +final class SystemServiceRegistry { + private final static String TAG = "SystemServiceRegistry"; + + // Service registry information. + // This information is never changed once static initialization has completed. + private static final HashMap<Class<?>, String> SYSTEM_SERVICE_NAMES = + new HashMap<Class<?>, String>(); + private static final HashMap<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS = + new HashMap<String, ServiceFetcher<?>>(); + private static int sServiceCacheSize; + + // Not instantiable. + private SystemServiceRegistry() { } + + static { + registerService(Context.ACCESSIBILITY_SERVICE, AccessibilityManager.class, + new CachedServiceFetcher<AccessibilityManager>() { + @Override + public AccessibilityManager createService(ContextImpl ctx) { + return AccessibilityManager.getInstance(ctx); + }}); + + registerService(Context.CAPTIONING_SERVICE, CaptioningManager.class, + new CachedServiceFetcher<CaptioningManager>() { + @Override + public CaptioningManager createService(ContextImpl ctx) { + return new CaptioningManager(ctx); + }}); + + registerService(Context.ACCOUNT_SERVICE, AccountManager.class, + new CachedServiceFetcher<AccountManager>() { + @Override + public AccountManager createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(Context.ACCOUNT_SERVICE); + IAccountManager service = IAccountManager.Stub.asInterface(b); + return new AccountManager(ctx, service); + }}); + + registerService(Context.ACTIVITY_SERVICE, ActivityManager.class, + new CachedServiceFetcher<ActivityManager>() { + @Override + public ActivityManager createService(ContextImpl ctx) { + return new ActivityManager(ctx.getOuterContext(), ctx.mMainThread.getHandler()); + }}); + + registerService(Context.ALARM_SERVICE, AlarmManager.class, + new CachedServiceFetcher<AlarmManager>() { + @Override + public AlarmManager createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(Context.ALARM_SERVICE); + IAlarmManager service = IAlarmManager.Stub.asInterface(b); + return new AlarmManager(service, ctx); + }}); + + registerService(Context.AUDIO_SERVICE, AudioManager.class, + new CachedServiceFetcher<AudioManager>() { + @Override + public AudioManager createService(ContextImpl ctx) { + return new AudioManager(ctx); + }}); + + registerService(Context.MEDIA_ROUTER_SERVICE, MediaRouter.class, + new CachedServiceFetcher<MediaRouter>() { + @Override + public MediaRouter createService(ContextImpl ctx) { + return new MediaRouter(ctx); + }}); + + registerService(Context.BLUETOOTH_SERVICE, BluetoothManager.class, + new CachedServiceFetcher<BluetoothManager>() { + @Override + public BluetoothManager createService(ContextImpl ctx) { + return new BluetoothManager(ctx); + }}); + + registerService(Context.HDMI_CONTROL_SERVICE, HdmiControlManager.class, + new StaticServiceFetcher<HdmiControlManager>() { + @Override + public HdmiControlManager createService() { + IBinder b = ServiceManager.getService(Context.HDMI_CONTROL_SERVICE); + return new HdmiControlManager(IHdmiControlService.Stub.asInterface(b)); + }}); + + registerService(Context.CLIPBOARD_SERVICE, ClipboardManager.class, + new CachedServiceFetcher<ClipboardManager>() { + @Override + public ClipboardManager createService(ContextImpl ctx) { + return new ClipboardManager(ctx.getOuterContext(), + ctx.mMainThread.getHandler()); + }}); + + // The clipboard service moved to a new package. If someone asks for the old + // interface by class then we want to redirect over to the new interface instead + // (which extends it). + SYSTEM_SERVICE_NAMES.put(android.text.ClipboardManager.class, Context.CLIPBOARD_SERVICE); + + registerService(Context.CONNECTIVITY_SERVICE, ConnectivityManager.class, + new StaticServiceFetcher<ConnectivityManager>() { + @Override + public ConnectivityManager createService() { + IBinder b = ServiceManager.getService(Context.CONNECTIVITY_SERVICE); + return new ConnectivityManager(IConnectivityManager.Stub.asInterface(b)); + }}); + + registerService(Context.COUNTRY_DETECTOR, CountryDetector.class, + new StaticServiceFetcher<CountryDetector>() { + @Override + public CountryDetector createService() { + IBinder b = ServiceManager.getService(Context.COUNTRY_DETECTOR); + return new CountryDetector(ICountryDetector.Stub.asInterface(b)); + }}); + + registerService(Context.DEVICE_POLICY_SERVICE, DevicePolicyManager.class, + new CachedServiceFetcher<DevicePolicyManager>() { + @Override + public DevicePolicyManager createService(ContextImpl ctx) { + return DevicePolicyManager.create(ctx, ctx.mMainThread.getHandler()); + }}); + + registerService(Context.DOWNLOAD_SERVICE, DownloadManager.class, + new CachedServiceFetcher<DownloadManager>() { + @Override + public DownloadManager createService(ContextImpl ctx) { + return new DownloadManager(ctx.getContentResolver(), ctx.getPackageName()); + }}); + + registerService(Context.BATTERY_SERVICE, BatteryManager.class, + new StaticServiceFetcher<BatteryManager>() { + @Override + public BatteryManager createService() { + return new BatteryManager(); + }}); + + registerService(Context.NFC_SERVICE, NfcManager.class, + new CachedServiceFetcher<NfcManager>() { + @Override + public NfcManager createService(ContextImpl ctx) { + return new NfcManager(ctx); + }}); + + registerService(Context.DROPBOX_SERVICE, DropBoxManager.class, + new StaticServiceFetcher<DropBoxManager>() { + @Override + public DropBoxManager createService() { + IBinder b = ServiceManager.getService(Context.DROPBOX_SERVICE); + IDropBoxManagerService service = IDropBoxManagerService.Stub.asInterface(b); + if (service == null) { + // Don't return a DropBoxManager that will NPE upon use. + // This also avoids caching a broken DropBoxManager in + // getDropBoxManager during early boot, before the + // DROPBOX_SERVICE is registered. + return null; + } + return new DropBoxManager(service); + }}); + + registerService(Context.INPUT_SERVICE, InputManager.class, + new StaticServiceFetcher<InputManager>() { + @Override + public InputManager createService() { + return InputManager.getInstance(); + }}); + + registerService(Context.DISPLAY_SERVICE, DisplayManager.class, + new CachedServiceFetcher<DisplayManager>() { + @Override + public DisplayManager createService(ContextImpl ctx) { + return new DisplayManager(ctx.getOuterContext()); + }}); + + registerService(Context.INPUT_METHOD_SERVICE, InputMethodManager.class, + new StaticServiceFetcher<InputMethodManager>() { + @Override + public InputMethodManager createService() { + return InputMethodManager.getInstance(); + }}); + + registerService(Context.TEXT_SERVICES_MANAGER_SERVICE, TextServicesManager.class, + new StaticServiceFetcher<TextServicesManager>() { + @Override + public TextServicesManager createService() { + return TextServicesManager.getInstance(); + }}); + + registerService(Context.KEYGUARD_SERVICE, KeyguardManager.class, + new StaticServiceFetcher<KeyguardManager>() { + @Override + public KeyguardManager createService() { + return new KeyguardManager(); + }}); + + registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class, + new CachedServiceFetcher<LayoutInflater>() { + @Override + public LayoutInflater createService(ContextImpl ctx) { + return new PhoneLayoutInflater(ctx.getOuterContext()); + }}); + + registerService(Context.LOCATION_SERVICE, LocationManager.class, + new CachedServiceFetcher<LocationManager>() { + @Override + public LocationManager createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(Context.LOCATION_SERVICE); + return new LocationManager(ctx, ILocationManager.Stub.asInterface(b)); + }}); + + registerService(Context.NETWORK_POLICY_SERVICE, NetworkPolicyManager.class, + new StaticServiceFetcher<NetworkPolicyManager>() { + @Override + public NetworkPolicyManager createService() { + return new NetworkPolicyManager(INetworkPolicyManager.Stub.asInterface( + ServiceManager.getService(Context.NETWORK_POLICY_SERVICE))); + }}); + + registerService(Context.NOTIFICATION_SERVICE, NotificationManager.class, + new CachedServiceFetcher<NotificationManager>() { + @Override + public NotificationManager createService(ContextImpl ctx) { + final Context outerContext = ctx.getOuterContext(); + return new NotificationManager( + new ContextThemeWrapper(outerContext, + Resources.selectSystemTheme(0, + outerContext.getApplicationInfo().targetSdkVersion, + com.android.internal.R.style.Theme_Dialog, + com.android.internal.R.style.Theme_Holo_Dialog, + com.android.internal.R.style.Theme_DeviceDefault_Dialog, + com.android.internal.R.style.Theme_DeviceDefault_Light_Dialog)), + ctx.mMainThread.getHandler()); + }}); + + registerService(Context.NSD_SERVICE, NsdManager.class, + new CachedServiceFetcher<NsdManager>() { + @Override + public NsdManager createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(Context.NSD_SERVICE); + INsdManager service = INsdManager.Stub.asInterface(b); + return new NsdManager(ctx.getOuterContext(), service); + }}); + + registerService(Context.POWER_SERVICE, PowerManager.class, + new CachedServiceFetcher<PowerManager>() { + @Override + public PowerManager createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(Context.POWER_SERVICE); + IPowerManager service = IPowerManager.Stub.asInterface(b); + if (service == null) { + Log.wtf(TAG, "Failed to get power manager service."); + } + return new PowerManager(ctx.getOuterContext(), + service, ctx.mMainThread.getHandler()); + }}); + + registerService(Context.SEARCH_SERVICE, SearchManager.class, + new CachedServiceFetcher<SearchManager>() { + @Override + public SearchManager createService(ContextImpl ctx) { + return new SearchManager(ctx.getOuterContext(), + ctx.mMainThread.getHandler()); + }}); + + registerService(Context.SENSOR_SERVICE, SensorManager.class, + new CachedServiceFetcher<SensorManager>() { + @Override + public SensorManager createService(ContextImpl ctx) { + return new SystemSensorManager(ctx.getOuterContext(), + ctx.mMainThread.getHandler().getLooper()); + }}); + + registerService(Context.STATUS_BAR_SERVICE, StatusBarManager.class, + new CachedServiceFetcher<StatusBarManager>() { + @Override + public StatusBarManager createService(ContextImpl ctx) { + return new StatusBarManager(ctx.getOuterContext()); + }}); + + registerService(Context.STORAGE_SERVICE, StorageManager.class, + new CachedServiceFetcher<StorageManager>() { + @Override + public StorageManager createService(ContextImpl ctx) { + try { + return new StorageManager( + ctx.getContentResolver(), ctx.mMainThread.getHandler().getLooper()); + } catch (RemoteException rex) { + Log.e(TAG, "Failed to create StorageManager", rex); + return null; + } + }}); + + registerService(Context.TELEPHONY_SERVICE, TelephonyManager.class, + new CachedServiceFetcher<TelephonyManager>() { + @Override + public TelephonyManager createService(ContextImpl ctx) { + return new TelephonyManager(ctx.getOuterContext()); + }}); + + registerService(Context.TELEPHONY_SUBSCRIPTION_SERVICE, SubscriptionManager.class, + new CachedServiceFetcher<SubscriptionManager>() { + @Override + public SubscriptionManager createService(ContextImpl ctx) { + return new SubscriptionManager(ctx.getOuterContext()); + }}); + + registerService(Context.TELECOM_SERVICE, TelecomManager.class, + new CachedServiceFetcher<TelecomManager>() { + @Override + public TelecomManager createService(ContextImpl ctx) { + return new TelecomManager(ctx.getOuterContext()); + }}); + + registerService(Context.UI_MODE_SERVICE, UiModeManager.class, + new CachedServiceFetcher<UiModeManager>() { + @Override + public UiModeManager createService(ContextImpl ctx) { + return new UiModeManager(); + }}); + + registerService(Context.USB_SERVICE, UsbManager.class, + new CachedServiceFetcher<UsbManager>() { + @Override + public UsbManager createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(Context.USB_SERVICE); + return new UsbManager(ctx, IUsbManager.Stub.asInterface(b)); + }}); + + registerService(Context.SERIAL_SERVICE, SerialManager.class, + new CachedServiceFetcher<SerialManager>() { + @Override + public SerialManager createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(Context.SERIAL_SERVICE); + return new SerialManager(ctx, ISerialManager.Stub.asInterface(b)); + }}); + + registerService(Context.VIBRATOR_SERVICE, Vibrator.class, + new CachedServiceFetcher<Vibrator>() { + @Override + public Vibrator createService(ContextImpl ctx) { + return new SystemVibrator(ctx); + }}); + + registerService(Context.WALLPAPER_SERVICE, WallpaperManager.class, + new CachedServiceFetcher<WallpaperManager>() { + @Override + public WallpaperManager createService(ContextImpl ctx) { + return new WallpaperManager(ctx.getOuterContext(), + ctx.mMainThread.getHandler()); + }}); + + registerService(Context.WIFI_SERVICE, WifiManager.class, + new CachedServiceFetcher<WifiManager>() { + @Override + public WifiManager createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(Context.WIFI_SERVICE); + IWifiManager service = IWifiManager.Stub.asInterface(b); + return new WifiManager(ctx.getOuterContext(), service); + }}); + + registerService(Context.WIFI_PASSPOINT_SERVICE, WifiPasspointManager.class, + new CachedServiceFetcher<WifiPasspointManager>() { + @Override + public WifiPasspointManager createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(Context.WIFI_PASSPOINT_SERVICE); + IWifiPasspointManager service = IWifiPasspointManager.Stub.asInterface(b); + return new WifiPasspointManager(ctx.getOuterContext(), service); + }}); + + registerService(Context.WIFI_P2P_SERVICE, WifiP2pManager.class, + new StaticServiceFetcher<WifiP2pManager>() { + @Override + public WifiP2pManager createService() { + IBinder b = ServiceManager.getService(Context.WIFI_P2P_SERVICE); + IWifiP2pManager service = IWifiP2pManager.Stub.asInterface(b); + return new WifiP2pManager(service); + }}); + + registerService(Context.WIFI_SCANNING_SERVICE, WifiScanner.class, + new CachedServiceFetcher<WifiScanner>() { + @Override + public WifiScanner createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(Context.WIFI_SCANNING_SERVICE); + IWifiScanner service = IWifiScanner.Stub.asInterface(b); + return new WifiScanner(ctx.getOuterContext(), service); + }}); + + registerService(Context.WIFI_RTT_SERVICE, RttManager.class, + new CachedServiceFetcher<RttManager>() { + @Override + public RttManager createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(Context.WIFI_RTT_SERVICE); + IRttManager service = IRttManager.Stub.asInterface(b); + return new RttManager(ctx.getOuterContext(), service); + }}); + + registerService(Context.ETHERNET_SERVICE, EthernetManager.class, + new CachedServiceFetcher<EthernetManager>() { + @Override + public EthernetManager createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(Context.ETHERNET_SERVICE); + IEthernetManager service = IEthernetManager.Stub.asInterface(b); + return new EthernetManager(ctx.getOuterContext(), service); + }}); + + registerService(Context.WINDOW_SERVICE, WindowManager.class, + new CachedServiceFetcher<WindowManager>() { + @Override + public WindowManager createService(ContextImpl ctx) { + return new WindowManagerImpl(ctx.getDisplay()); + }}); + + registerService(Context.USER_SERVICE, UserManager.class, + new CachedServiceFetcher<UserManager>() { + @Override + public UserManager createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(Context.USER_SERVICE); + IUserManager service = IUserManager.Stub.asInterface(b); + return new UserManager(ctx, service); + }}); + + registerService(Context.APP_OPS_SERVICE, AppOpsManager.class, + new CachedServiceFetcher<AppOpsManager>() { + @Override + public AppOpsManager createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(Context.APP_OPS_SERVICE); + IAppOpsService service = IAppOpsService.Stub.asInterface(b); + return new AppOpsManager(ctx, service); + }}); + + registerService(Context.CAMERA_SERVICE, CameraManager.class, + new CachedServiceFetcher<CameraManager>() { + @Override + public CameraManager createService(ContextImpl ctx) { + return new CameraManager(ctx); + }}); + + registerService(Context.LAUNCHER_APPS_SERVICE, LauncherApps.class, + new CachedServiceFetcher<LauncherApps>() { + @Override + public LauncherApps createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(Context.LAUNCHER_APPS_SERVICE); + ILauncherApps service = ILauncherApps.Stub.asInterface(b); + return new LauncherApps(ctx, service); + }}); + + registerService(Context.RESTRICTIONS_SERVICE, RestrictionsManager.class, + new CachedServiceFetcher<RestrictionsManager>() { + @Override + public RestrictionsManager createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(Context.RESTRICTIONS_SERVICE); + IRestrictionsManager service = IRestrictionsManager.Stub.asInterface(b); + return new RestrictionsManager(ctx, service); + }}); + + registerService(Context.PRINT_SERVICE, PrintManager.class, + new CachedServiceFetcher<PrintManager>() { + @Override + public PrintManager createService(ContextImpl ctx) { + IBinder iBinder = ServiceManager.getService(Context.PRINT_SERVICE); + IPrintManager service = IPrintManager.Stub.asInterface(iBinder); + return new PrintManager(ctx.getOuterContext(), service, UserHandle.myUserId(), + UserHandle.getAppId(Process.myUid())); + }}); + + registerService(Context.CONSUMER_IR_SERVICE, ConsumerIrManager.class, + new CachedServiceFetcher<ConsumerIrManager>() { + @Override + public ConsumerIrManager createService(ContextImpl ctx) { + return new ConsumerIrManager(ctx); + }}); + + registerService(Context.MEDIA_SESSION_SERVICE, MediaSessionManager.class, + new CachedServiceFetcher<MediaSessionManager>() { + @Override + public MediaSessionManager createService(ContextImpl ctx) { + return new MediaSessionManager(ctx); + }}); + + registerService(Context.TRUST_SERVICE, TrustManager.class, + new StaticServiceFetcher<TrustManager>() { + @Override + public TrustManager createService() { + IBinder b = ServiceManager.getService(Context.TRUST_SERVICE); + return new TrustManager(b); + }}); + + registerService(Context.FINGERPRINT_SERVICE, FingerprintManager.class, + new CachedServiceFetcher<FingerprintManager>() { + @Override + public FingerprintManager createService(ContextImpl ctx) { + IBinder binder = ServiceManager.getService(Context.FINGERPRINT_SERVICE); + IFingerprintService service = IFingerprintService.Stub.asInterface(binder); + return new FingerprintManager(ctx.getOuterContext(), service); + }}); + + registerService(Context.TV_INPUT_SERVICE, TvInputManager.class, + new StaticServiceFetcher<TvInputManager>() { + @Override + public TvInputManager createService() { + IBinder iBinder = ServiceManager.getService(Context.TV_INPUT_SERVICE); + ITvInputManager service = ITvInputManager.Stub.asInterface(iBinder); + return new TvInputManager(service, UserHandle.myUserId()); + }}); + + registerService(Context.NETWORK_SCORE_SERVICE, NetworkScoreManager.class, + new CachedServiceFetcher<NetworkScoreManager>() { + @Override + public NetworkScoreManager createService(ContextImpl ctx) { + return new NetworkScoreManager(ctx); + }}); + + registerService(Context.USAGE_STATS_SERVICE, UsageStatsManager.class, + new CachedServiceFetcher<UsageStatsManager>() { + @Override + public UsageStatsManager createService(ContextImpl ctx) { + IBinder iBinder = ServiceManager.getService(Context.USAGE_STATS_SERVICE); + IUsageStatsManager service = IUsageStatsManager.Stub.asInterface(iBinder); + return new UsageStatsManager(ctx.getOuterContext(), service); + }}); + + registerService(Context.JOB_SCHEDULER_SERVICE, JobScheduler.class, + new StaticServiceFetcher<JobScheduler>() { + @Override + public JobScheduler createService() { + IBinder b = ServiceManager.getService(Context.JOB_SCHEDULER_SERVICE); + return new JobSchedulerImpl(IJobScheduler.Stub.asInterface(b)); + }}); + + registerService(Context.PERSISTENT_DATA_BLOCK_SERVICE, PersistentDataBlockManager.class, + new StaticServiceFetcher<PersistentDataBlockManager>() { + @Override + public PersistentDataBlockManager createService() { + IBinder b = ServiceManager.getService(Context.PERSISTENT_DATA_BLOCK_SERVICE); + IPersistentDataBlockService persistentDataBlockService = + IPersistentDataBlockService.Stub.asInterface(b); + if (persistentDataBlockService != null) { + return new PersistentDataBlockManager(persistentDataBlockService); + } else { + // not supported + return null; + } + }}); + + registerService(Context.MEDIA_PROJECTION_SERVICE, MediaProjectionManager.class, + new CachedServiceFetcher<MediaProjectionManager>() { + @Override + public MediaProjectionManager createService(ContextImpl ctx) { + return new MediaProjectionManager(ctx); + }}); + + registerService(Context.APPWIDGET_SERVICE, AppWidgetManager.class, + new CachedServiceFetcher<AppWidgetManager>() { + @Override + public AppWidgetManager createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(Context.APPWIDGET_SERVICE); + return new AppWidgetManager(ctx, IAppWidgetService.Stub.asInterface(b)); + }}); + + registerService(Context.MIDI_SERVICE, MidiManager.class, + new CachedServiceFetcher<MidiManager>() { + @Override + public MidiManager createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(Context.MIDI_SERVICE); + return new MidiManager(ctx, IMidiManager.Stub.asInterface(b)); + }}); + + registerService(Context.RADIO_SERVICE, RadioManager.class, + new CachedServiceFetcher<RadioManager>() { + @Override + public RadioManager createService(ContextImpl ctx) { + return new RadioManager(ctx); + }}); + } + + /** + * Creates an array which is used to cache per-Context service instances. + */ + public static Object[] createServiceCache() { + return new Object[sServiceCacheSize]; + } + + /** + * Gets a system service from a given context. + */ + public static Object getSystemService(ContextImpl ctx, String name) { + ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name); + return fetcher != null ? fetcher.getService(ctx) : null; + } + + /** + * Gets the name of the system-level service that is represented by the specified class. + */ + public static String getSystemServiceName(Class<?> serviceClass) { + return SYSTEM_SERVICE_NAMES.get(serviceClass); + } + + /** + * Statically registers a system service with the context. + * This method must be called during static initialization only. + */ + private static <T> void registerService(String serviceName, Class<T> serviceClass, + ServiceFetcher<T> serviceFetcher) { + SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName); + SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher); + } + + /** + * Base interface for classes that fetch services. + * These objects must only be created during static initialization. + */ + static abstract interface ServiceFetcher<T> { + T getService(ContextImpl ctx); + } + + /** + * Override this class when the system service constructor needs a + * ContextImpl and should be cached and retained by that context. + */ + static abstract class CachedServiceFetcher<T> implements ServiceFetcher<T> { + private final int mCacheIndex; + + public CachedServiceFetcher() { + mCacheIndex = sServiceCacheSize++; + } + + @Override + @SuppressWarnings("unchecked") + public final T getService(ContextImpl ctx) { + final Object[] cache = ctx.mServiceCache; + synchronized (cache) { + // Fetch or create the service. + Object service = cache[mCacheIndex]; + if (service == null) { + service = createService(ctx); + cache[mCacheIndex] = service; + } + return (T)service; + } + } + + public abstract T createService(ContextImpl ctx); + } + + /** + * Override this class when the system service does not need a ContextImpl + * and should be cached and retained process-wide. + */ + static abstract class StaticServiceFetcher<T> implements ServiceFetcher<T> { + private T mCachedInstance; + + @Override + public final T getService(ContextImpl unused) { + synchronized (StaticServiceFetcher.this) { + if (mCachedInstance == null) { + mCachedInstance = createService(); + } + return mCachedInstance; + } + } + + public abstract T createService(); + } +} diff --git a/core/java/android/app/TimePickerDialog.java b/core/java/android/app/TimePickerDialog.java index 3a2c21b..a3b3022 100644 --- a/core/java/android/app/TimePickerDialog.java +++ b/core/java/android/app/TimePickerDialog.java @@ -31,20 +31,21 @@ import android.widget.TimePicker.ValidationCallback; import com.android.internal.R; /** - * A dialog that prompts the user for the time of day using a {@link TimePicker}. + * A dialog that prompts the user for the time of day using a + * {@link TimePicker}. * - * <p>See the <a href="{@docRoot}guide/topics/ui/controls/pickers.html">Pickers</a> - * guide.</p> + * <p> + * See the <a href="{@docRoot}guide/topics/ui/controls/pickers.html">Pickers</a> + * guide. */ public class TimePickerDialog extends AlertDialog implements OnClickListener, OnTimeChangedListener { - private static final String HOUR = "hour"; private static final String MINUTE = "minute"; private static final String IS_24_HOUR = "is24hour"; private final TimePicker mTimePicker; - private final OnTimeSetListener mTimeSetCallback; + private final OnTimeSetListener mTimeSetListener; private final int mInitialHourOfDay; private final int mInitialMinute; @@ -52,59 +53,70 @@ public class TimePickerDialog extends AlertDialog implements OnClickListener, /** * The callback interface used to indicate the user is done filling in - * the time (they clicked on the 'Done' button). + * the time (e.g. they clicked on the 'OK' button). */ public interface OnTimeSetListener { - /** - * @param view The view associated with this listener. - * @param hourOfDay The hour that was set. - * @param minute The minute that was set. + * Called when the user is done setting a new time and the dialog has + * closed. + * + * @param view the view associated with this listener + * @param hourOfDay the hour that was set + * @param minute the minute that was set */ - void onTimeSet(TimePicker view, int hourOfDay, int minute); + public void onTimeSet(TimePicker view, int hourOfDay, int minute); } /** - * @param context Parent. - * @param callBack How parent is notified. - * @param hourOfDay The initial hour. - * @param minute The initial minute. - * @param is24HourView Whether this is a 24 hour view, or AM/PM. + * Creates a new time picker dialog. + * + * @param context the parent context + * @param listener the listener to call when the time is set + * @param hourOfDay the initial hour + * @param minute the initial minute + * @param is24HourView whether this is a 24 hour view or AM/PM */ - public TimePickerDialog(Context context, - OnTimeSetListener callBack, - int hourOfDay, int minute, boolean is24HourView) { - this(context, 0, callBack, hourOfDay, minute, is24HourView); + public TimePickerDialog(Context context, OnTimeSetListener listener, int hourOfDay, int minute, + boolean is24HourView) { + this(context, 0, listener, hourOfDay, minute, is24HourView); } - static int resolveDialogTheme(Context context, int resid) { - if (resid == 0) { + static int resolveDialogTheme(Context context, int resId) { + if (resId == 0) { final TypedValue outValue = new TypedValue(); context.getTheme().resolveAttribute(R.attr.timePickerDialogTheme, outValue, true); return outValue.resourceId; } else { - return resid; + return resId; } } /** - * @param context Parent. - * @param theme the theme to apply to this dialog - * @param callBack How parent is notified. - * @param hourOfDay The initial hour. - * @param minute The initial minute. + * Creates a new time picker dialog with the specified theme. + * + * @param context the parent context + * @param themeResId the resource ID of the theme to apply to this dialog + * @param listener the listener to call when the time is set + * @param hourOfDay the initial hour + * @param minute the initial minute * @param is24HourView Whether this is a 24 hour view, or AM/PM. */ - public TimePickerDialog(Context context, int theme, OnTimeSetListener callBack, int hourOfDay, - int minute, boolean is24HourView) { - super(context, resolveDialogTheme(context, theme)); + public TimePickerDialog(Context context, int themeResId, OnTimeSetListener listener, + int hourOfDay, int minute, boolean is24HourView) { + super(context, resolveDialogTheme(context, themeResId)); - mTimeSetCallback = callBack; + mTimeSetListener = listener; mInitialHourOfDay = hourOfDay; mInitialMinute = minute; mIs24HourView = is24HourView; final Context themeContext = getContext(); + + + final TypedValue outValue = new TypedValue(); + context.getTheme().resolveAttribute(R.attr.timePickerDialogTheme, outValue, true); + final int layoutResId = outValue.resourceId; + final LayoutInflater inflater = LayoutInflater.from(themeContext); final View view = inflater.inflate(R.layout.time_picker_dialog, null); setView(view); @@ -129,8 +141,8 @@ public class TimePickerDialog extends AlertDialog implements OnClickListener, public void onClick(DialogInterface dialog, int which) { switch (which) { case BUTTON_POSITIVE: - if (mTimeSetCallback != null) { - mTimeSetCallback.onTimeSet(mTimePicker, mTimePicker.getCurrentHour(), + if (mTimeSetListener != null) { + mTimeSetListener.onTimeSet(mTimePicker, mTimePicker.getCurrentHour(), mTimePicker.getCurrentMinute()); } break; diff --git a/core/java/android/app/UiModeManager.java b/core/java/android/app/UiModeManager.java index 0a255f7..0f6ce12 100644 --- a/core/java/android/app/UiModeManager.java +++ b/core/java/android/app/UiModeManager.java @@ -219,10 +219,9 @@ public class UiModeManager { } /** - * Returns the currently configured night mode. - * - * @return {@link #MODE_NIGHT_NO}, {@link #MODE_NIGHT_YES}, or - * {@link #MODE_NIGHT_AUTO}. When an error occurred -1 is returned. + * @return the currently configured night mode. May be one of + * {@link #MODE_NIGHT_NO}, {@link #MODE_NIGHT_YES}, + * {@link #MODE_NIGHT_AUTO}, or -1 on error. */ public int getNightMode() { if (mService != null) { diff --git a/core/java/android/app/VoiceInteractor.aidl b/core/java/android/app/VoiceInteractor.aidl new file mode 100644 index 0000000..40a4a0e --- /dev/null +++ b/core/java/android/app/VoiceInteractor.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015, 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.app; + +parcelable VoiceInteractor.PickOptionRequest.Option; diff --git a/core/java/android/app/VoiceInteractor.java b/core/java/android/app/VoiceInteractor.java index 723cb9b..da7bb05 100644 --- a/core/java/android/app/VoiceInteractor.java +++ b/core/java/android/app/VoiceInteractor.java @@ -16,12 +16,13 @@ package android.app; -import android.annotation.SystemApi; import android.content.Context; import android.os.Bundle; import android.os.IBinder; import android.os.Looper; import android.os.Message; +import android.os.Parcel; +import android.os.Parcelable; import android.os.RemoteException; import android.util.ArrayMap; import android.util.Log; @@ -34,7 +35,6 @@ import com.android.internal.os.SomeArgs; import java.util.ArrayList; /** - * @hide * Interface for an {@link Activity} to interact with the user through voice. Use * {@link android.app.Activity#getVoiceInteractor() Activity.getVoiceInteractor} * to retrieve the interface, if the activity is currently involved in a voice interaction. @@ -56,7 +56,6 @@ import java.util.ArrayList; * request, rather than holding on to the activity instance yourself, either explicitly * or implicitly through a non-static inner class. */ -@SystemApi public class VoiceInteractor { static final String TAG = "VoiceInteractor"; static final boolean DEBUG = true; @@ -72,6 +71,7 @@ public class VoiceInteractor { public void executeMessage(Message msg) { SomeArgs args = (SomeArgs)msg.obj; Request request; + boolean complete; switch (msg.what) { case MSG_CONFIRMATION_RESULT: request = pullRequest((IVoiceInteractorRequest)args.arg1, true); @@ -84,13 +84,28 @@ public class VoiceInteractor { request.clear(); } break; + case MSG_PICK_OPTION_RESULT: + complete = msg.arg1 != 0; + request = pullRequest((IVoiceInteractorRequest)args.arg1, complete); + if (DEBUG) Log.d(TAG, "onPickOptionResult: req=" + + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request + + " finished=" + complete + " selection=" + args.arg2 + + " result=" + args.arg3); + if (request != null) { + ((PickOptionRequest)request).onPickOptionResult(complete, + (PickOptionRequest.Option[]) args.arg2, (Bundle) args.arg3); + if (complete) { + request.clear(); + } + } + break; case MSG_COMPLETE_VOICE_RESULT: request = pullRequest((IVoiceInteractorRequest)args.arg1, true); if (DEBUG) Log.d(TAG, "onCompleteVoice: req=" + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request + " result=" + args.arg1); if (request != null) { - ((CompleteVoiceRequest)request).onCompleteResult((Bundle) args.arg2); + ((CompleteVoiceRequest)request).onCompleteResult((Bundle) args.arg1); request.clear(); } break; @@ -98,20 +113,22 @@ public class VoiceInteractor { request = pullRequest((IVoiceInteractorRequest)args.arg1, true); if (DEBUG) Log.d(TAG, "onAbortVoice: req=" + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request - + " result=" + args.arg1); + + " result=" + args.arg2); if (request != null) { ((AbortVoiceRequest)request).onAbortResult((Bundle) args.arg2); request.clear(); } break; case MSG_COMMAND_RESULT: - request = pullRequest((IVoiceInteractorRequest)args.arg1, msg.arg1 != 0); + complete = msg.arg1 != 0; + request = pullRequest((IVoiceInteractorRequest)args.arg1, complete); if (DEBUG) Log.d(TAG, "onCommandResult: req=" + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request - + " result=" + args.arg2); + + " completed=" + msg.arg1 + " result=" + args.arg2); if (request != null) { - ((CommandRequest)request).onCommandResult((Bundle) args.arg2); - if (msg.arg1 != 0) { + ((CommandRequest)request).onCommandResult(msg.arg1 != 0, + (Bundle) args.arg2); + if (complete) { request.clear(); } } @@ -131,10 +148,17 @@ public class VoiceInteractor { final IVoiceInteractorCallback.Stub mCallback = new IVoiceInteractorCallback.Stub() { @Override - public void deliverConfirmationResult(IVoiceInteractorRequest request, boolean confirmed, + public void deliverConfirmationResult(IVoiceInteractorRequest request, boolean finished, Bundle result) { mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIOO( - MSG_CONFIRMATION_RESULT, confirmed ? 1 : 0, request, result)); + MSG_CONFIRMATION_RESULT, finished ? 1 : 0, request, result)); + } + + @Override + public void deliverPickOptionResult(IVoiceInteractorRequest request, + boolean finished, PickOptionRequest.Option[] options, Bundle result) { + mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIOOO( + MSG_PICK_OPTION_RESULT, finished ? 1 : 0, request, options, result)); } @Override @@ -158,25 +182,30 @@ public class VoiceInteractor { @Override public void deliverCancel(IVoiceInteractorRequest request) throws RemoteException { - mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageO( - MSG_CANCEL_RESULT, request)); + mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOO( + MSG_CANCEL_RESULT, request, null)); } }; final ArrayMap<IBinder, Request> mActiveRequests = new ArrayMap<IBinder, Request>(); static final int MSG_CONFIRMATION_RESULT = 1; - static final int MSG_COMPLETE_VOICE_RESULT = 2; - static final int MSG_ABORT_VOICE_RESULT = 3; - static final int MSG_COMMAND_RESULT = 4; - static final int MSG_CANCEL_RESULT = 5; + static final int MSG_PICK_OPTION_RESULT = 2; + static final int MSG_COMPLETE_VOICE_RESULT = 3; + static final int MSG_ABORT_VOICE_RESULT = 4; + static final int MSG_COMMAND_RESULT = 5; + static final int MSG_CANCEL_RESULT = 6; + /** + * Base class for voice interaction requests that can be submitted to the interactor. + * Do not instantiate this directly -- instead, use the appropriate subclass. + */ public static abstract class Request { IVoiceInteractorRequest mRequestInterface; Context mContext; Activity mActivity; - public Request() { + Request() { } public void cancel() { @@ -214,22 +243,25 @@ public class VoiceInteractor { String packageName, IVoiceInteractorCallback callback) throws RemoteException; } + /** + * Confirms an operation with the user via the trusted system + * VoiceInteractionService. This allows an Activity to complete an unsafe operation that + * would require the user to touch the screen when voice interaction mode is not enabled. + * The result of the confirmation will be returned through an asynchronous call to + * either {@link #onConfirmationResult(boolean, android.os.Bundle)} or + * {@link #onCancel()}. + * + * <p>In some cases this may be a simple yes / no confirmation or the confirmation could + * include context information about how the action will be completed + * (e.g. booking a cab might include details about how long until the cab arrives) + * so the user can give a confirmation. + */ public static class ConfirmationRequest extends Request { final CharSequence mPrompt; final Bundle mExtras; /** - * Confirms an operation with the user via the trusted system - * VoiceInteractionService. This allows an Activity to complete an unsafe operation that - * would require the user to touch the screen when voice interaction mode is not enabled. - * The result of the confirmation will be returned through an asynchronous call to - * either {@link #onConfirmationResult(boolean, android.os.Bundle)} or - * {@link #onCancel()}. - * - * <p>In some cases this may be a simple yes / no confirmation or the confirmation could - * include context information about how the action will be completed - * (e.g. booking a cab might include details about how long until the cab arrives) - * so the user can give a confirmation. + * Create a new confirmation request. * @param prompt Optional confirmation text to read to the user as the action being * confirmed. * @param extras Additional optional information. @@ -248,19 +280,155 @@ public class VoiceInteractor { } } + /** + * Select a single option from multiple potential options with the user via the trusted system + * VoiceInteractionService. Typically, the application would present this visually as + * a list view to allow selecting the option by touch. + * The result of the confirmation will be returned through an asynchronous call to + * either {@link #onPickOptionResult} or {@link #onCancel()}. + */ + public static class PickOptionRequest extends Request { + final CharSequence mPrompt; + final Option[] mOptions; + final Bundle mExtras; + + /** + * Represents a single option that the user may select using their voice. + */ + public static final class Option implements Parcelable { + final CharSequence mLabel; + ArrayList<CharSequence> mSynonyms; + Bundle mExtras; + + /** + * Creates an option that a user can select with their voice by matching the label + * or one of several synonyms. + * @param label The label that will both be matched against what the user speaks + * and displayed visually. + */ + public Option(CharSequence label) { + mLabel = label; + } + + /** + * Add a synonym term to the option to indicate an alternative way the content + * may be matched. + * @param synonym The synonym that will be matched against what the user speaks, + * but not displayed. + */ + public Option addSynonym(CharSequence synonym) { + if (mSynonyms == null) { + mSynonyms = new ArrayList<>(); + } + mSynonyms.add(synonym); + return this; + } + + public CharSequence getLabel() { + return mLabel; + } + + public int countSynonyms() { + return mSynonyms != null ? mSynonyms.size() : 0; + } + + public CharSequence getSynonymAt(int index) { + return mSynonyms != null ? mSynonyms.get(index) : null; + } + + /** + * Set optional extra information associated with this option. Note that this + * method takes ownership of the supplied extras Bundle. + */ + public void setExtras(Bundle extras) { + mExtras = extras; + } + + /** + * Return any optional extras information associated with this option, or null + * if there is none. Note that this method returns a reference to the actual + * extras Bundle in the option, so modifications to it will directly modify the + * extras in the option. + */ + public Bundle getExtras() { + return mExtras; + } + + Option(Parcel in) { + mLabel = in.readCharSequence(); + mSynonyms = in.readCharSequenceList(); + mExtras = in.readBundle(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeCharSequence(mLabel); + dest.writeCharSequenceList(mSynonyms); + dest.writeBundle(mExtras); + } + + public static final Parcelable.Creator<Option> CREATOR + = new Parcelable.Creator<Option>() { + public Option createFromParcel(Parcel in) { + return new Option(in); + } + + public Option[] newArray(int size) { + return new Option[size]; + } + }; + }; + + /** + * Create a new pick option request. + * @param prompt Optional question to be spoken to the user via text to speech. + * @param options The set of {@link Option}s the user is selecting from. + * @param extras Additional optional information. + */ + public PickOptionRequest(CharSequence prompt, Option[] options, Bundle extras) { + mPrompt = prompt; + mOptions = options; + mExtras = extras; + } + + /** + * Called when a single option is confirmed or narrowed to one of several options. + * @param finished True if the voice interaction has finished making a selection, in + * which case {@code selections} contains the final result. If false, this request is + * still active and you will continue to get calls on it. + * @param selections Either a single {@link Option} or one of several {@link Option}s the + * user has narrowed the choices down to. + * @param result Additional optional information. + */ + public void onPickOptionResult(boolean finished, Option[] selections, Bundle result) { + } + + IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName, + IVoiceInteractorCallback callback) throws RemoteException { + return interactor.startPickOption(packageName, callback, mPrompt, mOptions, mExtras); + } + } + + /** + * Reports that the current interaction was successfully completed with voice, so the + * application can report the final status to the user. When the response comes back, the + * voice system has handled the request and is ready to switch; at that point the + * application can start a new non-voice activity or finish. Be sure when starting the new + * activity to use {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK + * Intent.FLAG_ACTIVITY_NEW_TASK} to keep the new activity out of the current voice + * interaction task. + */ public static class CompleteVoiceRequest extends Request { final CharSequence mMessage; final Bundle mExtras; /** - * Reports that the current interaction was successfully completed with voice, so the - * application can report the final status to the user. When the response comes back, the - * voice system has handled the request and is ready to switch; at that point the - * application can start a new non-voice activity or finish. Be sure when starting the new - * activity to use {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK - * Intent.FLAG_ACTIVITY_NEW_TASK} to keep the new activity out of the current voice - * interaction task. - * + * Create a new completed voice interaction request. * @param message Optional message to tell user about the completion status of the task. * @param extras Additional optional information. */ @@ -278,21 +446,23 @@ public class VoiceInteractor { } } + /** + * Reports that the current interaction can not be complete with voice, so the + * application will need to switch to a traditional input UI. Applications should + * only use this when they need to completely bail out of the voice interaction + * and switch to a traditional UI. When the response comes back, the voice + * system has handled the request and is ready to switch; at that point the application + * can start a new non-voice activity. Be sure when starting the new activity + * to use {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK + * Intent.FLAG_ACTIVITY_NEW_TASK} to keep the new activity out of the current voice + * interaction task. + */ public static class AbortVoiceRequest extends Request { final CharSequence mMessage; final Bundle mExtras; /** - * Reports that the current interaction can not be complete with voice, so the - * application will need to switch to a traditional input UI. Applications should - * only use this when they need to completely bail out of the voice interaction - * and switch to a traditional UI. When the response comes back, the voice - * system has handled the request and is ready to switch; at that point the application - * can start a new non-voice activity. Be sure when starting the new activity - * to use {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK - * Intent.FLAG_ACTIVITY_NEW_TASK} to keep the new activity out of the current voice - * interaction task. - * + * Create a new voice abort request. * @param message Optional message to tell user about not being able to complete * the interaction with voice. * @param extras Additional optional information. @@ -311,25 +481,27 @@ public class VoiceInteractor { } } + /** + * Execute an extended command using the trusted system VoiceInteractionService. + * This allows an Activity to request additional information from the user needed to + * complete an action (e.g. booking a table might have several possible times that the + * user could select from or an app might need the user to agree to a terms of service). + * The result of the confirmation will be returned through an asynchronous call to + * either {@link #onCommandResult(boolean, android.os.Bundle)} or + * {@link #onCancel()}. + * + * <p>The command is a string that describes the generic operation to be performed. + * The command will determine how the properties in extras are interpreted and the set of + * available commands is expected to grow over time. An example might be + * "com.google.voice.commands.REQUEST_NUMBER_BAGS" to request the number of bags as part of + * airline check-in. (This is not an actual working example.) + */ public static class CommandRequest extends Request { final String mCommand; final Bundle mArgs; /** - * Execute a command using the trusted system VoiceInteractionService. - * This allows an Activity to request additional information from the user needed to - * complete an action (e.g. booking a table might have several possible times that the - * user could select from or an app might need the user to agree to a terms of service). - * The result of the confirmation will be returned through an asynchronous call to - * either {@link #onCommandResult(android.os.Bundle)} or - * {@link #onCancel()}. - * - * <p>The command is a string that describes the generic operation to be performed. - * The command will determine how the properties in extras are interpreted and the set of - * available commands is expected to grow over time. An example might be - * "com.google.voice.commands.REQUEST_NUMBER_BAGS" to request the number of bags as part of - * airline check-in. (This is not an actual working example.) - * + * Create a new generic command request. * @param command The desired command to perform. * @param args Additional arguments to control execution of the command. */ @@ -338,7 +510,12 @@ public class VoiceInteractor { mArgs = args; } - public void onCommandResult(Bundle result) { + /** + * Results for CommandRequest can be returned in partial chunks. + * The isCompleted is set to true iff all results have been returned, indicating the + * CommandRequest has completed. + */ + public void onCommandResult(boolean isCompleted, Bundle result) { } IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName, diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index 90d84ee..22e79b6 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -16,6 +16,7 @@ package android.app; +import android.annotation.RawRes; import android.annotation.SystemApi; import android.content.ComponentName; import android.content.ContentResolver; @@ -63,7 +64,10 @@ import java.util.List; * Provides access to the system wallpaper. With WallpaperManager, you can * get the current wallpaper, get the desired dimensions for the wallpaper, set * the wallpaper, and more. Get an instance of WallpaperManager with - * {@link #getInstance(android.content.Context) getInstance()}. + * {@link #getInstance(android.content.Context) getInstance()}. + * + * <p> An app can check whether wallpapers are supported for the current user, by calling + * {@link #isWallpaperSupported()}. */ public class WallpaperManager { private static String TAG = "WallpaperManager"; @@ -187,7 +191,7 @@ public class WallpaperManager { } @Override - public void setColorFilter(ColorFilter cf) { + public void setColorFilter(ColorFilter colorFilter) { throw new UnsupportedOperationException("Not supported with this drawable"); } @@ -248,6 +252,15 @@ public class WallpaperManager { public Bitmap peekWallpaperBitmap(Context context, boolean returnDefault) { synchronized (this) { + if (mService != null) { + try { + if (!mService.isWallpaperSupported(context.getOpPackageName())) { + return null; + } + } catch (RemoteException e) { + // Ignore + } + } if (mWallpaper != null) { return mWallpaper; } @@ -617,7 +630,9 @@ public class WallpaperManager { * wallpaper will require reloading it again from disk. */ public void forgetLoadedWallpaper() { - sGlobals.forgetLoadedWallpaper(); + if (isWallpaperSupported()) { + sGlobals.forgetLoadedWallpaper(); + } } /** @@ -707,7 +722,7 @@ public class WallpaperManager { * @throws IOException If an error occurs reverting to the built-in * wallpaper. */ - public void setResource(int resid) throws IOException { + public void setResource(@RawRes int resid) throws IOException { if (sGlobals.mService == null) { Log.w(TAG, "WallpaperService not running"); return; @@ -716,7 +731,7 @@ public class WallpaperManager { Resources resources = mContext.getResources(); /* Set the wallpaper to the default values */ ParcelFileDescriptor fd = sGlobals.mService.setWallpaper( - "res:" + resources.getResourceName(resid)); + "res:" + resources.getResourceName(resid), mContext.getOpPackageName()); if (fd != null) { FileOutputStream fos = null; try { @@ -752,7 +767,8 @@ public class WallpaperManager { return; } try { - ParcelFileDescriptor fd = sGlobals.mService.setWallpaper(null); + ParcelFileDescriptor fd = sGlobals.mService.setWallpaper(null, + mContext.getOpPackageName()); if (fd == null) { return; } @@ -791,7 +807,8 @@ public class WallpaperManager { return; } try { - ParcelFileDescriptor fd = sGlobals.mService.setWallpaper(null); + ParcelFileDescriptor fd = sGlobals.mService.setWallpaper(null, + mContext.getOpPackageName()); if (fd == null) { return; } @@ -823,7 +840,7 @@ public class WallpaperManager { * with the given resource ID. That is, their wallpaper has been * set through {@link #setResource(int)} with the same resource id. */ - public boolean hasResourceWallpaper(int resid) { + public boolean hasResourceWallpaper(@RawRes int resid) { if (sGlobals.mService == null) { Log.w(TAG, "WallpaperService not running"); return false; @@ -944,7 +961,8 @@ public class WallpaperManager { if (sGlobals.mService == null) { Log.w(TAG, "WallpaperService not running"); } else { - sGlobals.mService.setDimensionHints(minimumWidth, minimumHeight); + sGlobals.mService.setDimensionHints(minimumWidth, minimumHeight, + mContext.getOpPackageName()); } } catch (RemoteException e) { // Ignore @@ -965,7 +983,7 @@ public class WallpaperManager { if (sGlobals.mService == null) { Log.w(TAG, "WallpaperService not running"); } else { - sGlobals.mService.setDisplayPadding(padding); + sGlobals.mService.setDisplayPadding(padding, mContext.getOpPackageName()); } } catch (RemoteException e) { // Ignore @@ -1005,7 +1023,7 @@ public class WallpaperManager { return; } try { - sGlobals.mService.clearWallpaper(); + sGlobals.mService.clearWallpaper(mContext.getOpPackageName()); } catch (RemoteException e) { // Ignore } @@ -1026,7 +1044,7 @@ public class WallpaperManager { return false; } try { - sGlobals.mService.setWallpaperComponent(name); + sGlobals.mService.setWallpaperComponentChecked(name, mContext.getOpPackageName()); return true; } catch (RemoteException e) { // Ignore @@ -1095,7 +1113,24 @@ public class WallpaperManager { // Ignore. } } - + + /** + * Returns whether wallpapers are supported for the calling user. If this function returns + * false, any attempts to changing the wallpaper will have no effect. + */ + public boolean isWallpaperSupported() { + if (sGlobals.mService == null) { + Log.w(TAG, "WallpaperService not running"); + } else { + try { + return sGlobals.mService.isWallpaperSupported(mContext.getOpPackageName()); + } catch (RemoteException e) { + // Ignore + } + } + return false; + } + /** * Clear the offsets previously associated with this window through * {@link #setWallpaperOffsets(IBinder, float, float)}. This reverts diff --git a/core/java/android/app/admin/DeviceAdminInfo.java b/core/java/android/app/admin/DeviceAdminInfo.java index 3074b49..d1e40ae 100644 --- a/core/java/android/app/admin/DeviceAdminInfo.java +++ b/core/java/android/app/admin/DeviceAdminInfo.java @@ -165,15 +165,24 @@ public final class DeviceAdminInfo implements Parcelable { /** @hide */ public static class PolicyInfo { public final int ident; - final public String tag; - final public int label; - final public int description; - - public PolicyInfo(int identIn, String tagIn, int labelIn, int descriptionIn) { - ident = identIn; - tag = tagIn; - label = labelIn; - description = descriptionIn; + public final String tag; + public final int label; + public final int description; + public final int labelForSecondaryUsers; + public final int descriptionForSecondaryUsers; + + public PolicyInfo(int ident, String tag, int label, int description) { + this(ident, tag, label, description, label, description); + } + + public PolicyInfo(int ident, String tag, int label, int description, + int labelForSecondaryUsers, int descriptionForSecondaryUsers) { + this.ident = ident; + this.tag = tag; + this.label = label; + this.description = description; + this.labelForSecondaryUsers = labelForSecondaryUsers; + this.descriptionForSecondaryUsers = descriptionForSecondaryUsers; } } @@ -184,7 +193,10 @@ public final class DeviceAdminInfo implements Parcelable { static { sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_WIPE_DATA, "wipe-data", com.android.internal.R.string.policylab_wipeData, - com.android.internal.R.string.policydesc_wipeData)); + com.android.internal.R.string.policydesc_wipeData, + com.android.internal.R.string.policylab_wipeData_secondaryUser, + com.android.internal.R.string.policydesc_wipeData_secondaryUser + )); sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_RESET_PASSWORD, "reset-password", com.android.internal.R.string.policylab_resetPassword, com.android.internal.R.string.policydesc_resetPassword)); @@ -193,7 +205,10 @@ public final class DeviceAdminInfo implements Parcelable { com.android.internal.R.string.policydesc_limitPassword)); sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_WATCH_LOGIN, "watch-login", com.android.internal.R.string.policylab_watchLogin, - com.android.internal.R.string.policydesc_watchLogin)); + com.android.internal.R.string.policydesc_watchLogin, + com.android.internal.R.string.policylab_watchLogin, + com.android.internal.R.string.policydesc_watchLogin_secondaryUser + )); sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_FORCE_LOCK, "force-lock", com.android.internal.R.string.policylab_forceLock, com.android.internal.R.string.policydesc_forceLock)); diff --git a/core/java/android/app/admin/DeviceAdminReceiver.java b/core/java/android/app/admin/DeviceAdminReceiver.java index c53e749..fe284ce 100644 --- a/core/java/android/app/admin/DeviceAdminReceiver.java +++ b/core/java/android/app/admin/DeviceAdminReceiver.java @@ -25,6 +25,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.security.KeyChain; /** * Base class for implementing a device administration component. This @@ -167,8 +168,8 @@ public class DeviceAdminReceiver extends BroadcastReceiver { /** * Action sent to a device administrator to notify that the device is entering - * lock task mode from an authorized package. The extra {@link #EXTRA_LOCK_TASK_PACKAGE} - * will describe the authorized package using lock task mode. + * lock task mode. The extra {@link #EXTRA_LOCK_TASK_PACKAGE} + * will describe the package using lock task mode. * * <p>The calling device admin must be the device owner or profile * owner to receive this broadcast. @@ -181,7 +182,7 @@ public class DeviceAdminReceiver extends BroadcastReceiver { /** * Action sent to a device administrator to notify that the device is exiting - * lock task mode from an authorized package. + * lock task mode. * * <p>The calling device admin must be the device owner or profile * owner to receive this broadcast. @@ -214,7 +215,8 @@ public class DeviceAdminReceiver extends BroadcastReceiver { * <p>A device admin application which listens to this intent can find out if the device was * provisioned for the device owner or profile owner case by calling respectively * {@link android.app.admin.DevicePolicyManager#isDeviceOwnerApp} and - * {@link android.app.admin.DevicePolicyManager#isProfileOwnerApp}. + * {@link android.app.admin.DevicePolicyManager#isProfileOwnerApp}. You will generally handle + * this in {@link DeviceAdminReceiver#onProfileProvisioningComplete}. * * <p>Input: Nothing.</p> * <p>Output: Nothing</p> @@ -224,6 +226,44 @@ public class DeviceAdminReceiver extends BroadcastReceiver { "android.app.action.PROFILE_PROVISIONING_COMPLETE"; /** + * Broadcast Action: This broadcast is sent to indicate that the system is ready for the device + * initializer to perform user setup tasks. This is only applicable to devices managed by a + * device owner app. + * + * <p>The broadcast will be limited to the {@link DeviceAdminReceiver} component specified in + * the (@link DevicePolicyManager#EXTRA_PROVISIONING_DEVICE_INITIALIZER_COMPONENT_NAME) field + * of the original intent or NFC bump that started the provisioning process. You will generally + * handle this in {@link DeviceAdminReceiver#onReadyForUserInitialization}. + * + * <p>Input: Nothing.</p> + * <p>Output: Nothing</p> + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_READY_FOR_USER_INITIALIZATION = + "android.app.action.READY_FOR_USER_INITIALIZATION"; + + /** @hide */ + public static final String ACTION_CHOOSE_PRIVATE_KEY_ALIAS = "android.app.action.CHOOSE_PRIVATE_KEY_ALIAS"; + + /** @hide */ + public static final String EXTRA_CHOOSE_PRIVATE_KEY_SENDER_UID = "android.app.extra.CHOOSE_PRIVATE_KEY_SENDER_UID"; + + /** @hide */ + public static final String EXTRA_CHOOSE_PRIVATE_KEY_HOST = "android.app.extra.CHOOSE_PRIVATE_KEY_HOST"; + + /** @hide */ + public static final String EXTRA_CHOOSE_PRIVATE_KEY_PORT = "android.app.extra.CHOOSE_PRIVATE_KEY_PORT"; + + /** @hide */ + public static final String EXTRA_CHOOSE_PRIVATE_KEY_URL = "android.app.extra.CHOOSE_PRIVATE_KEY_URL"; + + /** @hide */ + public static final String EXTRA_CHOOSE_PRIVATE_KEY_ALIAS = "android.app.extra.CHOOSE_PRIVATE_KEY_ALIAS"; + + /** @hide */ + public static final String EXTRA_CHOOSE_PRIVATE_KEY_RESPONSE = "android.app.extra.CHOOSE_PRIVATE_KEY_RESPONSE"; + + /** * Name under which a DevicePolicy component publishes information * about itself. This meta-data must reference an XML resource containing * a device-admin tag. @@ -360,20 +400,20 @@ public class DeviceAdminReceiver extends BroadcastReceiver { /** * Called when provisioning of a managed profile or managed device has completed successfully. * - * <p> As a prerequisit for the execution of this callback the (@link DeviceAdminReceiver} has + * <p> As a prerequisite for the execution of this callback the {@link DeviceAdminReceiver} has * to declare an intent filter for {@link #ACTION_PROFILE_PROVISIONING_COMPLETE}. * Its component must also be specified in the {@link DevicePolicyManager#EXTRA_DEVICE_ADMIN} * of the {@link DevicePolicyManager#ACTION_PROVISION_MANAGED_PROFILE} intent that started the * managed provisioning. * - * <p>When provisioning is complete, the managed profile is hidden until the profile owner - * calls {DevicePolicyManager#setProfileEnabled(ComponentName admin)}. Typically a profile - * owner will enable the profile when it has finished any additional setup such as adding an - * account by using the {@link AccountManager} and calling apis to bring the profile into the - * desired state. + * <p>When provisioning of a managed profile is complete, the managed profile is hidden until + * the profile owner calls {DevicePolicyManager#setProfileEnabled(ComponentName admin)}. + * Typically a profile owner will enable the profile when it has finished any additional setup + * such as adding an account by using the {@link AccountManager} and calling apis to bring the + * profile into the desired state. * * <p> Note that provisioning completes without waiting for any server interactions, so the - * profile owner needs to wait for data to be available if required (e.g android device ids or + * profile owner needs to wait for data to be available if required (e.g. android device ids or * other data that is set as a result of server interactions). * * @param context The running context as per {@link #onReceive}. @@ -383,8 +423,31 @@ public class DeviceAdminReceiver extends BroadcastReceiver { } /** - * Called when a device is entering lock task mode by a package authorized - * by {@link DevicePolicyManager#isLockTaskPermitted(String)} + * Called during provisioning of a managed device to allow the device initializer to perform + * user setup steps. Only device initializers should override this method. + * + * <p> Called when the DeviceAdminReceiver receives a + * {@link #ACTION_READY_FOR_USER_INITIALIZATION} broadcast. As a prerequisite for the execution + * of this callback the {@link DeviceAdminReceiver} has + * to declare an intent filter for {@link #ACTION_READY_FOR_USER_INITIALIZATION}. Only the + * component specified in the + * {@link DevicePolicyManager#EXTRA_PROVISIONING_DEVICE_INITIALIZER_COMPONENT_NAME} field of the + * original intent or NFC bump that started the provisioning process will receive this callback. + * + * <p>It is not assumed that the device initializer is finished when it returns from + * this call, as it may do additional setup asynchronously. The device initializer must call + * {DevicePolicyManager#setUserEnabled(ComponentName admin)} when it has finished any additional + * setup (such as adding an account by using the {@link AccountManager}) in order for the user + * to be functional. + * + * @param context The running context as per {@link #onReceive}. + * @param intent The received intent as per {@link #onReceive}. + */ + public void onReadyForUserInitialization(Context context, Intent intent) { + } + + /** + * Called when a device is entering lock task mode. * * @param context The running context as per {@link #onReceive}. * @param intent The received intent as per {@link #onReceive}. @@ -394,8 +457,7 @@ public class DeviceAdminReceiver extends BroadcastReceiver { } /** - * Called when a device is exiting lock task mode by a package authorized - * by {@link DevicePolicyManager#isLockTaskPermitted(String)} + * Called when a device is exiting lock task mode. * * @param context The running context as per {@link #onReceive}. * @param intent The received intent as per {@link #onReceive}. @@ -404,6 +466,26 @@ public class DeviceAdminReceiver extends BroadcastReceiver { } /** + * Allows this receiver to select the alias for a private key and certificate pair for + * authentication. If this method returns null, the default {@link android.app.Activity} will be + * shown that lets the user pick a private key and certificate pair. + * + * @param context The running context as per {@link #onReceive}. + * @param intent The received intent as per {@link #onReceive}. + * @param uid The uid asking for the private key and certificate pair. + * @param host The authentication host, may be null. + * @param port The authentication port, or -1. + * @param url The URL to authenticate, may be null. + * @param alias The alias preselected by the client, or null. + * @return The private key alias to return and grant access to. + * @see KeyChain#choosePrivateKeyAlias + */ + public String onChoosePrivateKeyAlias(Context context, Intent intent, int uid, String host, + int port, String url, String alias) { + return null; + } + + /** * Intercept standard device administrator broadcasts. Implementations * should not override this method; it is better to implement the * convenience callbacks for each action. @@ -432,11 +514,22 @@ public class DeviceAdminReceiver extends BroadcastReceiver { onPasswordExpiring(context, intent); } else if (ACTION_PROFILE_PROVISIONING_COMPLETE.equals(action)) { onProfileProvisioningComplete(context, intent); + } else if (ACTION_CHOOSE_PRIVATE_KEY_ALIAS.equals(action)) { + int uid = intent.getIntExtra(EXTRA_CHOOSE_PRIVATE_KEY_SENDER_UID, -1); + String host = intent.getStringExtra(EXTRA_CHOOSE_PRIVATE_KEY_HOST); + int port = intent.getIntExtra(EXTRA_CHOOSE_PRIVATE_KEY_PORT, -1); + String url = intent.getStringExtra(EXTRA_CHOOSE_PRIVATE_KEY_URL); + String alias = intent.getStringExtra(EXTRA_CHOOSE_PRIVATE_KEY_ALIAS); + String chosenAlias = onChoosePrivateKeyAlias(context, intent, uid, host, port, url, + alias); + setResultData(chosenAlias); } else if (ACTION_LOCK_TASK_ENTERING.equals(action)) { String pkg = intent.getStringExtra(EXTRA_LOCK_TASK_PACKAGE); onLockTaskModeEntering(context, intent, pkg); } else if (ACTION_LOCK_TASK_EXITING.equals(action)) { onLockTaskModeExiting(context, intent); + } else if (ACTION_READY_FOR_USER_INITIALIZATION.equals(action)) { + onReadyForUserInitialization(context, intent); } } } diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index a01a96a..a659acb 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -28,6 +28,7 @@ import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.graphics.Bitmap; import android.net.ProxyInfo; import android.os.Bundle; import android.os.Handler; @@ -41,7 +42,6 @@ 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; @@ -52,11 +52,15 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Proxy; +import java.security.KeyFactory; import java.security.PrivateKey; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -64,7 +68,7 @@ import java.util.List; /** * 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 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. @@ -105,11 +109,17 @@ public class DevicePolicyManager { * Provisioning adds a managed profile and sets the MDM as the profile owner who has full * control over the profile. * - * <p>This intent must contain the extra {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME}. + * In version {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this intent must contain the + * extra {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME}. + * As of {@link android.os.Build.VERSION_CODES#MNC}, it should contain the extra + * {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME} instead, although specifying only + * {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME} is still supported. * - * <p> When managed provisioning has completed, an intent of the type - * {@link DeviceAdminReceiver#ACTION_PROFILE_PROVISIONING_COMPLETE} is broadcast to the - * managed profile. + * <p> When managed provisioning has completed, broadcasts are sent to the application specified + * in the provisioning intent. The + * {@link DeviceAdminReceiver#ACTION_PROFILE_PROVISIONING_COMPLETE} broadcast is sent in the + * managed profile and the {@link #ACTION_MANAGED_PROFILE_PROVISIONED} broadcast is sent in + * the primary profile. * * <p> If provisioning fails, the managedProfile is removed so the device returns to its * previous state. @@ -144,11 +154,36 @@ public class DevicePolicyManager { * * <p>This package is set as device owner when device owner provisioning is started by an NFC * message containing an NFC record with MIME type {@link #MIME_TYPE_PROVISIONING_NFC}. + * + * <p> When this extra is set, the application must have exactly one device admin receiver. + * This receiver will be set as the profile or device owner and active admin.</p> + + * @see DeviceAdminReceiver + * @deprecated Use {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME}. This extra is still + * supported. */ + @Deprecated public static final String EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME = "android.app.extra.PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME"; /** + * A ComponentName extra indicating the device admin receiver of the mobile device management + * application that will be set as the profile owner or device owner and active admin. + * + * <p>If an application starts provisioning directly via an intent with action + * {@link #ACTION_PROVISION_MANAGED_PROFILE} the package name of this component has to match the + * package name of the application that started provisioning. + * + * <p>This component is set as device owner and active admin when device owner provisioning is + * started by an NFC message containing an NFC record with MIME type + * {@link #MIME_TYPE_PROVISIONING_NFC}. + * + * @see DeviceAdminReceiver + */ + public static final String EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME + = "android.app.extra.PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME"; + + /** * An {@link android.accounts.Account} extra holding the account to migrate during managed * profile provisioning. If the account supplied is present in the primary user, it will be * copied, along with its credentials to the managed profile and removed from the primary user. @@ -328,6 +363,76 @@ public class DevicePolicyManager { = "android.app.extra.PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM"; /** + * Broadcast Action: This broadcast is sent to indicate that provisioning of a managed profile + * has completed successfully. + * + * <p>The broadcast is limited to the primary profile, to the app specified in the provisioning + * intent (@see #ACTION_PROVISION_MANAGED_PROFILE). + * + * <p>This intent will contain the extra {@link #EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE} which + * corresponds to the account requested to be migrated at provisioning time, if any. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_MANAGED_PROFILE_PROVISIONED + = "android.app.action.MANAGED_PROFILE_PROVISIONED"; + + /** + * A boolean extra indicating whether device encryption is required as part of Device Owner + * provisioning. + * + * <p>Use in an NFC record with {@link #MIME_TYPE_PROVISIONING_NFC} that starts device owner + * provisioning via an NFC bump. + */ + public static final String EXTRA_PROVISIONING_SKIP_ENCRYPTION = + "android.app.extra.PROVISIONING_SKIP_ENCRYPTION"; + + /** + * On devices managed by a device owner app, a String representation of a Component name extra + * indicating the component of the application that is temporarily granted device owner + * privileges during device initialization and profile owner privileges during secondary user + * initialization. + * + * <p>Use in an NFC record with {@link #MIME_TYPE_PROVISIONING_NFC} that starts device owner + * provisioning via an NFC bump. + * @see ComponentName#unflattenFromString() + */ + public static final String EXTRA_PROVISIONING_DEVICE_INITIALIZER_COMPONENT_NAME + = "android.app.extra.PROVISIONING_DEVICE_INITIALIZER_COMPONENT_NAME"; + + /** + * A String extra holding an http url that specifies the download location of the device + * initializer package. When not provided it is assumed that the device initializer package is + * already installed. + * + * <p>Use in an NFC record with {@link #MIME_TYPE_PROVISIONING_NFC} that starts device owner + * provisioning via an NFC bump. + */ + public static final String EXTRA_PROVISIONING_DEVICE_INITIALIZER_PACKAGE_DOWNLOAD_LOCATION + = "android.app.extra.PROVISIONING_DEVICE_INITIALIZER_PACKAGE_DOWNLOAD_LOCATION"; + + /** + * A String extra holding a http cookie header which should be used in the http request to the + * url specified in {@link #EXTRA_PROVISIONING_DEVICE_INITIALIZER_PACKAGE_DOWNLOAD_LOCATION}. + * + * <p>Use in an NFC record with {@link #MIME_TYPE_PROVISIONING_NFC} that starts device owner + * provisioning via an NFC bump. + */ + public static final String EXTRA_PROVISIONING_DEVICE_INITIALIZER_PACKAGE_DOWNLOAD_COOKIE_HEADER + = "android.app.extra.PROVISIONING_DEVICE_INITIALIZER_PACKAGE_DOWNLOAD_COOKIE_HEADER"; + + /** + * A String extra holding the SHA-1 checksum of the file at download location specified in + * {@link #EXTRA_PROVISIONING_DEVICE_INITIALIZER_PACKAGE_DOWNLOAD_LOCATION}. If this doesn't + * match the file at the download location an error will be shown to the user and the user will + * be asked to factory reset the device. + * + * <p>Use in an NFC record with {@link #MIME_TYPE_PROVISIONING_NFC} that starts device owner + * provisioning via an NFC bump. + */ + public static final String EXTRA_PROVISIONING_DEVICE_INITIALIZER_PACKAGE_CHECKSUM + = "android.app.extra.PROVISIONING_DEVICE_INITIALIZER_PACKAGE_CHECKSUM"; + + /** * This MIME type is used for starting the Device Owner provisioning. * * <p>During device owner provisioning a device admin app is set as the owner of the device. @@ -343,7 +448,6 @@ public class DevicePolicyManager { * <p>The NFC record must contain a serialized {@link java.util.Properties} object which * contains the following properties: * <ul> - * <li>{@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME}</li> * <li>{@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION}</li> * <li>{@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_COOKIE_HEADER}, optional</li> * <li>{@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM}</li> @@ -357,7 +461,17 @@ public class DevicePolicyManager { * <li>{@link #EXTRA_PROVISIONING_WIFI_PROXY_HOST}, optional</li> * <li>{@link #EXTRA_PROVISIONING_WIFI_PROXY_PORT} (convert to String), optional</li> * <li>{@link #EXTRA_PROVISIONING_WIFI_PROXY_BYPASS}, optional</li> - * <li>{@link #EXTRA_PROVISIONING_WIFI_PAC_URL}, optional</li></ul> + * <li>{@link #EXTRA_PROVISIONING_WIFI_PAC_URL}, optional</li> + * <li>{@link #EXTRA_PROVISIONING_SKIP_ENCRYPTION}, optional</li></ul> + * + * <p> + * In version {@link android.os.Build.VERSION_CODES#LOLLIPOP}, it should also contain + * {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME}. + * As of {@link android.os.Build.VERSION_CODES#MNC}, it should contain + * {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME} instead, (although + * specifying only {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME} is still supported). + * This componentName must have been converted to a String via + * {@link android.content.ComponentName#flattenToString()} * * <p> When device owner provisioning has completed, an intent of the type * {@link DeviceAdminReceiver#ACTION_PROFILE_PROVISIONING_COMPLETE} is broadcasted to the @@ -690,7 +804,7 @@ public class DevicePolicyManager { public void setPasswordQuality(ComponentName admin, int quality) { if (mService != null) { try { - mService.setPasswordQuality(admin, quality, UserHandle.myUserId()); + mService.setPasswordQuality(admin, quality); } catch (RemoteException e) { Log.w(TAG, "Failed talking with device policy service", e); } @@ -743,7 +857,7 @@ public class DevicePolicyManager { public void setPasswordMinimumLength(ComponentName admin, int length) { if (mService != null) { try { - mService.setPasswordMinimumLength(admin, length, UserHandle.myUserId()); + mService.setPasswordMinimumLength(admin, length); } catch (RemoteException e) { Log.w(TAG, "Failed talking with device policy service", e); } @@ -797,7 +911,7 @@ public class DevicePolicyManager { public void setPasswordMinimumUpperCase(ComponentName admin, int length) { if (mService != null) { try { - mService.setPasswordMinimumUpperCase(admin, length, UserHandle.myUserId()); + mService.setPasswordMinimumUpperCase(admin, length); } catch (RemoteException e) { Log.w(TAG, "Failed talking with device policy service", e); } @@ -858,7 +972,7 @@ public class DevicePolicyManager { public void setPasswordMinimumLowerCase(ComponentName admin, int length) { if (mService != null) { try { - mService.setPasswordMinimumLowerCase(admin, length, UserHandle.myUserId()); + mService.setPasswordMinimumLowerCase(admin, length); } catch (RemoteException e) { Log.w(TAG, "Failed talking with device policy service", e); } @@ -918,7 +1032,7 @@ public class DevicePolicyManager { public void setPasswordMinimumLetters(ComponentName admin, int length) { if (mService != null) { try { - mService.setPasswordMinimumLetters(admin, length, UserHandle.myUserId()); + mService.setPasswordMinimumLetters(admin, length); } catch (RemoteException e) { Log.w(TAG, "Failed talking with device policy service", e); } @@ -976,7 +1090,7 @@ public class DevicePolicyManager { public void setPasswordMinimumNumeric(ComponentName admin, int length) { if (mService != null) { try { - mService.setPasswordMinimumNumeric(admin, length, UserHandle.myUserId()); + mService.setPasswordMinimumNumeric(admin, length); } catch (RemoteException e) { Log.w(TAG, "Failed talking with device policy service", e); } @@ -1035,7 +1149,7 @@ public class DevicePolicyManager { public void setPasswordMinimumSymbols(ComponentName admin, int length) { if (mService != null) { try { - mService.setPasswordMinimumSymbols(admin, length, UserHandle.myUserId()); + mService.setPasswordMinimumSymbols(admin, length); } catch (RemoteException e) { Log.w(TAG, "Failed talking with device policy service", e); } @@ -1093,7 +1207,7 @@ public class DevicePolicyManager { public void setPasswordMinimumNonLetter(ComponentName admin, int length) { if (mService != null) { try { - mService.setPasswordMinimumNonLetter(admin, length, UserHandle.myUserId()); + mService.setPasswordMinimumNonLetter(admin, length); } catch (RemoteException e) { Log.w(TAG, "Failed talking with device policy service", e); } @@ -1153,7 +1267,7 @@ public class DevicePolicyManager { public void setPasswordHistoryLength(ComponentName admin, int length) { if (mService != null) { try { - mService.setPasswordHistoryLength(admin, length, UserHandle.myUserId()); + mService.setPasswordHistoryLength(admin, length); } catch (RemoteException e) { Log.w(TAG, "Failed talking with device policy service", e); } @@ -1185,7 +1299,7 @@ public class DevicePolicyManager { public void setPasswordExpirationTimeout(ComponentName admin, long timeout) { if (mService != null) { try { - mService.setPasswordExpirationTimeout(admin, timeout, UserHandle.myUserId()); + mService.setPasswordExpirationTimeout(admin, timeout); } catch (RemoteException e) { Log.w(TAG, "Failed talking with device policy service", e); } @@ -1330,7 +1444,7 @@ public class DevicePolicyManager { public void setMaximumFailedPasswordsForWipe(ComponentName admin, int num) { if (mService != null) { try { - mService.setMaximumFailedPasswordsForWipe(admin, num, UserHandle.myUserId()); + mService.setMaximumFailedPasswordsForWipe(admin, num); } catch (RemoteException e) { Log.w(TAG, "Failed talking with device policy service", e); } @@ -1414,7 +1528,7 @@ public class DevicePolicyManager { public boolean resetPassword(String password, int flags) { if (mService != null) { try { - return mService.resetPassword(password, flags, UserHandle.myUserId()); + return mService.resetPassword(password, flags); } catch (RemoteException e) { Log.w(TAG, "Failed talking with device policy service", e); } @@ -1438,7 +1552,7 @@ public class DevicePolicyManager { public void setMaximumTimeToLock(ComponentName admin, long timeMs) { if (mService != null) { try { - mService.setMaximumTimeToLock(admin, timeMs, UserHandle.myUserId()); + mService.setMaximumTimeToLock(admin, timeMs); } catch (RemoteException e) { Log.w(TAG, "Failed talking with device policy service", e); } @@ -1503,8 +1617,8 @@ public class DevicePolicyManager { public static final int WIPE_RESET_PROTECTION_DATA = 0x0002; /** - * Ask the user data be wiped. This will cause the device to reboot, - * erasing all user data while next booting up. + * Ask the user data be wiped. Wiping the primary user will cause the + * device to reboot, erasing all user data while next booting up. * * <p>The calling device admin must have requested * {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} to be able to call @@ -1588,7 +1702,7 @@ public class DevicePolicyManager { != android.net.Proxy.PROXY_VALID) throw new IllegalArgumentException(); } - return mService.setGlobalProxy(admin, hostSpec, exclSpec, UserHandle.myUserId()); + return mService.setGlobalProxy(admin, hostSpec, exclSpec); } catch (RemoteException e) { Log.w(TAG, "Failed talking with device policy service", e); } @@ -1652,7 +1766,7 @@ public class DevicePolicyManager { public static final int ENCRYPTION_STATUS_INACTIVE = 1; /** - * Result code for {@link #setStorageEncryption} and {@link #getStorageEncryptionStatus}: + * Result code for {@link #getStorageEncryptionStatus}: * indicating that encryption is not currently active, but is currently * being activated. This is only reported by devices that support * encryption of data and only when the storage is currently @@ -1668,6 +1782,13 @@ public class DevicePolicyManager { public static final int ENCRYPTION_STATUS_ACTIVE = 3; /** + * Result code for {@link #getStorageEncryptionStatus}: + * indicating that encryption is active, but an encryption key has not + * been set by the user. + */ + public static final int ENCRYPTION_STATUS_ACTIVE_DEFAULT_KEY = 4; + + /** * Activity action: begin the process of encrypting data on the device. This activity should * be launched after using {@link #setStorageEncryption} to request encryption be activated. * After resuming from this activity, use {@link #getStorageEncryption} @@ -1754,7 +1875,7 @@ public class DevicePolicyManager { public int setStorageEncryption(ComponentName admin, boolean encrypt) { if (mService != null) { try { - return mService.setStorageEncryption(admin, encrypt, UserHandle.myUserId()); + return mService.setStorageEncryption(admin, encrypt); } catch (RemoteException e) { Log.w(TAG, "Failed talking with device policy service", e); } @@ -1791,12 +1912,15 @@ public class DevicePolicyManager { * storage system does not support encryption. If the * result is {@link #ENCRYPTION_STATUS_INACTIVE}, use {@link * #ACTION_START_ENCRYPTION} to begin the process of encrypting or decrypting the - * storage. If the result is {@link #ENCRYPTION_STATUS_ACTIVATING} or + * storage. If the result is {@link #ENCRYPTION_STATUS_ACTIVE_DEFAULT_KEY}, the + * storage system has enabled encryption but no password is set so further action + * may be required. If the result is {@link #ENCRYPTION_STATUS_ACTIVATING} or * {@link #ENCRYPTION_STATUS_ACTIVE}, no further action is required. * * @return current status of encryption. The value will be one of * {@link #ENCRYPTION_STATUS_UNSUPPORTED}, {@link #ENCRYPTION_STATUS_INACTIVE}, - * {@link #ENCRYPTION_STATUS_ACTIVATING}, or{@link #ENCRYPTION_STATUS_ACTIVE}. + * {@link #ENCRYPTION_STATUS_ACTIVATING}, {@link #ENCRYPTION_STATUS_ACTIVE_DEFAULT_KEY}, + * or {@link #ENCRYPTION_STATUS_ACTIVE}. */ public int getStorageEncryptionStatus() { return getStorageEncryptionStatus(UserHandle.myUserId()); @@ -1934,13 +2058,15 @@ public class DevicePolicyManager { String alias) { try { final byte[] pemCert = Credentials.convertToPem(cert); - return mService.installKeyPair(who, privKey.getEncoded(), pemCert, alias); - } catch (CertificateException e) { - Log.w(TAG, "Error encoding certificate", e); - } catch (IOException e) { - Log.w(TAG, "Error writing certificate", e); + final byte[] pkcs8Key = KeyFactory.getInstance(privKey.getAlgorithm()) + .getKeySpec(privKey, PKCS8EncodedKeySpec.class).getEncoded(); + return mService.installKeyPair(who, pkcs8Key, pemCert, alias); } catch (RemoteException e) { Log.w(TAG, "Failed talking with device policy service", e); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + Log.w(TAG, "Failed to obtain private key material", e); + } catch (CertificateException | IOException e) { + Log.w(TAG, "Could not pem-encode certificate", e); } return false; } @@ -1971,7 +2097,7 @@ public class DevicePolicyManager { public void setCameraDisabled(ComponentName admin, boolean disabled) { if (mService != null) { try { - mService.setCameraDisabled(admin, disabled, UserHandle.myUserId()); + mService.setCameraDisabled(admin, disabled); } catch (RemoteException e) { Log.w(TAG, "Failed talking with device policy service", e); } @@ -2015,7 +2141,7 @@ public class DevicePolicyManager { public void setScreenCaptureDisabled(ComponentName admin, boolean disabled) { if (mService != null) { try { - mService.setScreenCaptureDisabled(admin, UserHandle.myUserId(), disabled); + mService.setScreenCaptureDisabled(admin, disabled); } catch (RemoteException e) { Log.w(TAG, "Failed talking with device policy service", e); } @@ -2059,7 +2185,7 @@ public class DevicePolicyManager { public void setAutoTimeRequired(ComponentName admin, boolean required) { if (mService != null) { try { - mService.setAutoTimeRequired(admin, UserHandle.myUserId(), required); + mService.setAutoTimeRequired(admin, required); } catch (RemoteException e) { Log.w(TAG, "Failed talking with device policy service", e); } @@ -2100,7 +2226,7 @@ public class DevicePolicyManager { public void setKeyguardDisabledFeatures(ComponentName admin, int which) { if (mService != null) { try { - mService.setKeyguardDisabledFeatures(admin, which, UserHandle.myUserId()); + mService.setKeyguardDisabledFeatures(admin, which); } catch (RemoteException e) { Log.w(TAG, "Failed talking with device policy service", e); } @@ -2350,6 +2476,113 @@ public class DevicePolicyManager { } /** + * Sets the given component as the device initializer. The package must already be installed and + * set as an active device administrator, and there must not be an existing device initializer, + * for this call to succeed. This method can only be called by an app holding the + * MANAGE_DEVICE_ADMINS permission before the device is provisioned or by a device owner app. A + * device initializer app is granted device owner privileges during device initialization and + * profile owner privileges during secondary user initialization. + * @param who Which {@link DeviceAdminReceiver} this request is associated with, or null if not + * called by the device owner. + * @param initializer Which {@link DeviceAdminReceiver} to make device initializer. + * @param initializerName The user-visible name of the device initializer. + * @return whether the package was successfully registered as the device initializer. + * @throws IllegalArgumentException if the package name is null or invalid + * @throws IllegalStateException if the caller is not device owner or the device has + * already been provisioned or a device initializer already exists. + */ + public boolean setDeviceInitializer(ComponentName who, ComponentName initializer, + String initializerName) throws IllegalArgumentException, IllegalStateException { + if (mService != null) { + try { + return mService.setDeviceInitializer(who, initializer, initializerName); + } catch (RemoteException re) { + Log.w(TAG, "Failed to set device initializer"); + } + } + return false; + } + + /** + * Used to determine if a particular package has been registered as the device initializer. + * + * @param packageName the package name of the app, to compare with the registered device + * initializer app, if any. + * @return whether or not the caller is registered as the device initializer app. + */ + public boolean isDeviceInitializerApp(String packageName) { + if (mService != null) { + try { + return mService.isDeviceInitializer(packageName); + } catch (RemoteException re) { + Log.w(TAG, "Failed to check device initializer"); + } + } + return false; + } + + /** + * Removes the device initializer, so that it will not be invoked on user initialization for any + * subsequently created users. This method can be called by either the device owner or device + * initializer itself. The caller must be an active administrator. + * + * @param who Which {@link DeviceAdminReceiver} this request is associated with. + */ + public void clearDeviceInitializerApp(ComponentName who) { + if (mService != null) { + try { + mService.clearDeviceInitializer(who); + } catch (RemoteException re) { + Log.w(TAG, "Failed to clear device initializer"); + } + } + } + + /** + * @hide + * Gets the device initializer of the system. + * + * @return the package name of the device initializer. + */ + @SystemApi + public String getDeviceInitializerApp() { + if (mService != null) { + try { + return mService.getDeviceInitializer(); + } catch (RemoteException re) { + Log.w(TAG, "Failed to get device initializer"); + } + } + return null; + } + + /** + * Sets the enabled state of the user. A user should be enabled only once it is ready to + * be used. + * + * <p>Device initializer must call this method to mark the user as functional. + * Only the device initializer agent can call this. + * + * <p>When the user is enabled, if the device initializer is not also the device owner, the + * device initializer will no longer have elevated permissions to call methods in this class. + * Additionally, it will be removed as an active administrator and its + * {@link DeviceAdminReceiver} will be disabled. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @return whether the user is now enabled. + */ + public boolean setUserEnabled(ComponentName admin) { + if (mService != null) { + try { + return mService.setUserEnabled(admin); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + return false; + } + + /** * @hide * @deprecated Use #ACTION_SET_PROFILE_OWNER * Sets the given component as an active admin and registers the package as the profile @@ -2417,27 +2650,6 @@ public class DevicePolicyManager { } /** - * @deprecated Use setProfileOwner(ComponentName ...) - * @hide - * Sets the given package as the profile owner of the given user profile. The package must - * already be installed and there shouldn't be an existing profile owner registered for this - * user. Also, this method must be called before the user has been used for the first time. - * @param packageName the package name of the application to be registered as profile owner. - * @param ownerName the human readable name of the organisation associated with this DPM. - * @param userHandle the userId to set the profile owner for. - * @return whether the package was successfully registered as the profile owner. - * @throws IllegalArgumentException if packageName is null, the package isn't installed, or - * the user has already been set up. - */ - public boolean setProfileOwner(String packageName, String ownerName, int userHandle) - throws IllegalArgumentException { - if (packageName == null) { - throw new NullPointerException("packageName cannot be null"); - } - return setProfileOwner(new ComponentName(packageName, ""), ownerName, userHandle); - } - - /** * @hide * Sets the given component as the profile owner of the given user profile. The package must * already be installed and there shouldn't be an existing profile owner registered for this @@ -2698,14 +2910,12 @@ public class DevicePolicyManager { * <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. - * @hide */ public void setTrustAgentConfiguration(ComponentName admin, ComponentName target, PersistableBundle configuration) { if (mService != null) { try { - mService.setTrustAgentConfiguration(admin, target, configuration, - UserHandle.myUserId()); + mService.setTrustAgentConfiguration(admin, target, configuration); } catch (RemoteException e) { Log.w(TAG, "Failed talking with device policy service", e); } @@ -2725,7 +2935,6 @@ public class DevicePolicyManager { * for this {@param agent} or calls it with a null configuration, null is returned. * @param agent Which component to get enabled features for. * @return configuration for the given trust agent. - * @hide */ public List<PersistableBundle> getTrustAgentConfiguration(ComponentName admin, ComponentName agent) { @@ -3109,8 +3318,7 @@ public class DevicePolicyManager { } /** - * Called by a profile or device owner to set a user restriction specified - * by the key. + * Called by a profile or device owner to set a user restriction specified by the key. * <p> * The calling device admin must be a profile or device owner; if it is not, * a security exception will be thrown. @@ -3131,8 +3339,7 @@ public class DevicePolicyManager { } /** - * Called by a profile or device owner to clear a user restriction specified - * by the key. + * Called by a profile or device owner to clear a user restriction specified by the key. * <p> * The calling device admin must be a profile or device owner; if it is not, * a security exception will be thrown. @@ -3153,7 +3360,7 @@ public class DevicePolicyManager { } /** - * Called by device or profile owner to hide or unhide packages. When a package is hidden it + * Called by profile or device owners to hide or unhide packages. When a package is hidden it * is unavailable for use, but the data and actual package file remain. * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. @@ -3175,7 +3382,7 @@ public class DevicePolicyManager { } /** - * Called by device or profile owner to determine if a package is hidden. + * Called by profile or device owners to determine if a package is hidden. * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @param packageName The name of the package to retrieve the hidden status of. @@ -3193,7 +3400,7 @@ public class DevicePolicyManager { } /** - * Called by profile or device owner to re-enable a system app that was disabled by default + * Called by profile or device owners to re-enable a system app that was disabled by default * when the user was initialized. * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. @@ -3210,7 +3417,7 @@ public class DevicePolicyManager { } /** - * Called by profile or device owner to re-enable system apps by intent that were disabled + * Called by profile or device owners to re-enable system apps by intent that were disabled * by default when the user was initialized. * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. @@ -3296,7 +3503,8 @@ public class DevicePolicyManager { * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * * @see Activity#startLockTask() - * @see DeviceAdminReceiver#onLockTaskModeChanged(Context, Intent, boolean, String) + * @see DeviceAdminReceiver#onLockTaskModeEntering(Context, Intent, String) + * @see DeviceAdminReceiver#onLockTaskModeExiting(Context, Intent) * @see UserManager#DISALLOW_CREATE_WINDOWS */ public void setLockTaskPackages(ComponentName admin, String[] packages) @@ -3351,14 +3559,22 @@ public class DevicePolicyManager { * <li>{@link Settings.Global#ADB_ENABLED}</li> * <li>{@link Settings.Global#AUTO_TIME}</li> * <li>{@link Settings.Global#AUTO_TIME_ZONE}</li> - * <li>{@link Settings.Global#BLUETOOTH_ON}</li> + * <li>{@link Settings.Global#BLUETOOTH_ON} + * Changing this setting has not effect as of {@link android.os.Build.VERSION_CODES#MNC}. Use + * {@link android.bluetooth.BluetoothAdapter#enable()} and + * {@link android.bluetooth.BluetoothAdapter#disable()} instead.</li> * <li>{@link Settings.Global#DATA_ROAMING}</li> * <li>{@link Settings.Global#DEVELOPMENT_SETTINGS_ENABLED}</li> * <li>{@link Settings.Global#MODE_RINGER}</li> * <li>{@link Settings.Global#NETWORK_PREFERENCE}</li> * <li>{@link Settings.Global#USB_MASS_STORAGE_ENABLED}</li> - * <li>{@link Settings.Global#WIFI_ON}</li> + * <li>{@link Settings.Global#WIFI_ON} + * Changing this setting has not effect as of {@link android.os.Build.VERSION_CODES#MNC}. Use + * {@link android.net.wifi.WifiManager#setWifiEnabled(boolean)} instead.</li> * <li>{@link Settings.Global#WIFI_SLEEP_POLICY}</li> + * <li>{@link Settings.Global#STAY_ON_WHILE_PLUGGED_IN} + * This setting is only available from {@link android.os.Build.VERSION_CODES#MNC} onwards + * and can only be set if {@link #setMaximumTimeToLock} is not used to set a timeout.</li> * </ul> * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. @@ -3478,9 +3694,14 @@ public class DevicePolicyManager { /** * Check whether the current user has been blocked by device policy from uninstalling a package. * Requires the caller to be the profile owner if checking a specific admin's policy. + * <p> + * <strong>Note:</strong> Starting from {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1}, the + * behavior of this API is changed such that passing <code>null</code> as the <code>admin</code> + * parameter will return if any admin has blocked the uninstallation. Before L MR1, passing + * <code>null</code> will cause a NullPointerException to be raised. * * @param admin The name of the admin component whose blocking policy will be checked, or null - * to check if any admin has blocked the uninstallation. + * to check if any admin has blocked the uninstallation. * @param packageName package to check. * @return true if uninstallation is blocked. */ @@ -3575,4 +3796,18 @@ public class DevicePolicyManager { } return Collections.emptyList(); } + + /** + * Called by profile or device owners to set the current user's photo. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @param icon the bitmap to set as the photo. + */ + public void setUserIcon(ComponentName admin, Bitmap icon) { + try { + mService.setUserIcon(admin, icon); + } catch (RemoteException re) { + Log.w(TAG, "Could not set the user icon ", re); + } + } } diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index 0ca60c0..f69cf36 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -20,6 +20,7 @@ package android.app.admin; import android.content.ComponentName; import android.content.Intent; import android.content.IntentFilter; +import android.graphics.Bitmap; import android.net.ProxyInfo; import android.os.Bundle; import android.os.PersistableBundle; @@ -32,34 +33,34 @@ import java.util.List; * {@hide} */ interface IDevicePolicyManager { - void setPasswordQuality(in ComponentName who, int quality, int userHandle); + void setPasswordQuality(in ComponentName who, int quality); int getPasswordQuality(in ComponentName who, int userHandle); - void setPasswordMinimumLength(in ComponentName who, int length, int userHandle); + void setPasswordMinimumLength(in ComponentName who, int length); int getPasswordMinimumLength(in ComponentName who, int userHandle); - void setPasswordMinimumUpperCase(in ComponentName who, int length, int userHandle); + void setPasswordMinimumUpperCase(in ComponentName who, int length); int getPasswordMinimumUpperCase(in ComponentName who, int userHandle); - void setPasswordMinimumLowerCase(in ComponentName who, int length, int userHandle); + void setPasswordMinimumLowerCase(in ComponentName who, int length); int getPasswordMinimumLowerCase(in ComponentName who, int userHandle); - void setPasswordMinimumLetters(in ComponentName who, int length, int userHandle); + void setPasswordMinimumLetters(in ComponentName who, int length); int getPasswordMinimumLetters(in ComponentName who, int userHandle); - void setPasswordMinimumNumeric(in ComponentName who, int length, int userHandle); + void setPasswordMinimumNumeric(in ComponentName who, int length); int getPasswordMinimumNumeric(in ComponentName who, int userHandle); - void setPasswordMinimumSymbols(in ComponentName who, int length, int userHandle); + void setPasswordMinimumSymbols(in ComponentName who, int length); int getPasswordMinimumSymbols(in ComponentName who, int userHandle); - void setPasswordMinimumNonLetter(in ComponentName who, int length, int userHandle); + void setPasswordMinimumNonLetter(in ComponentName who, int length); int getPasswordMinimumNonLetter(in ComponentName who, int userHandle); - void setPasswordHistoryLength(in ComponentName who, int length, int userHandle); + void setPasswordHistoryLength(in ComponentName who, int length); int getPasswordHistoryLength(in ComponentName who, int userHandle); - void setPasswordExpirationTimeout(in ComponentName who, long expiration, int userHandle); + void setPasswordExpirationTimeout(in ComponentName who, long expiration); long getPasswordExpirationTimeout(in ComponentName who, int userHandle); long getPasswordExpiration(in ComponentName who, int userHandle); @@ -68,33 +69,33 @@ interface IDevicePolicyManager { int getCurrentFailedPasswordAttempts(int userHandle); int getProfileWithMinimumFailedPasswordsForWipe(int userHandle); - void setMaximumFailedPasswordsForWipe(in ComponentName admin, int num, int userHandle); + void setMaximumFailedPasswordsForWipe(in ComponentName admin, int num); int getMaximumFailedPasswordsForWipe(in ComponentName admin, int userHandle); - boolean resetPassword(String password, int flags, int userHandle); + boolean resetPassword(String password, int flags); - void setMaximumTimeToLock(in ComponentName who, long timeMs, int userHandle); + void setMaximumTimeToLock(in ComponentName who, long timeMs); long getMaximumTimeToLock(in ComponentName who, int userHandle); void lockNow(); void wipeData(int flags, int userHandle); - ComponentName setGlobalProxy(in ComponentName admin, String proxySpec, String exclusionList, int userHandle); + ComponentName setGlobalProxy(in ComponentName admin, String proxySpec, String exclusionList); ComponentName getGlobalProxyAdmin(int userHandle); void setRecommendedGlobalProxy(in ComponentName admin, in ProxyInfo proxyInfo); - int setStorageEncryption(in ComponentName who, boolean encrypt, int userHandle); + int setStorageEncryption(in ComponentName who, boolean encrypt); boolean getStorageEncryption(in ComponentName who, int userHandle); int getStorageEncryptionStatus(int userHandle); - void setCameraDisabled(in ComponentName who, boolean disabled, int userHandle); + void setCameraDisabled(in ComponentName who, boolean disabled); boolean getCameraDisabled(in ComponentName who, int userHandle); - void setScreenCaptureDisabled(in ComponentName who, int userHandle, boolean disabled); + void setScreenCaptureDisabled(in ComponentName who, boolean disabled); boolean getScreenCaptureDisabled(in ComponentName who, int userHandle); - void setKeyguardDisabledFeatures(in ComponentName who, int which, int userHandle); + void setKeyguardDisabledFeatures(in ComponentName who, int which); int getKeyguardDisabledFeatures(in ComponentName who, int userHandle); void setActiveAdmin(in ComponentName policyReceiver, boolean refreshing, int userHandle); @@ -129,6 +130,7 @@ interface IDevicePolicyManager { void enforceCanManageCaCerts(in ComponentName admin); boolean installKeyPair(in ComponentName who, in byte[] privKeyBuffer, in byte[] certBuffer, String alias); + void choosePrivateKeyAlias(int uid, in String host, int port, in String url, in String alias, IBinder aliasCallback); void addPersistentPreferredActivity(in ComponentName admin, in IntentFilter filter, in ComponentName activity); void clearPackagePersistentPreferredActivities(in ComponentName admin, String packageName); @@ -186,7 +188,7 @@ interface IDevicePolicyManager { boolean getCrossProfileCallerIdDisabledForUser(int userId); void setTrustAgentConfiguration(in ComponentName admin, in ComponentName agent, - in PersistableBundle args, int userId); + in PersistableBundle args); List<PersistableBundle> getTrustAgentConfiguration(in ComponentName admin, in ComponentName agent, int userId); @@ -194,8 +196,16 @@ interface IDevicePolicyManager { boolean removeCrossProfileWidgetProvider(in ComponentName admin, String packageName); List<String> getCrossProfileWidgetProviders(in ComponentName admin); - void setAutoTimeRequired(in ComponentName who, int userHandle, boolean required); + void setAutoTimeRequired(in ComponentName who, boolean required); boolean getAutoTimeRequired(); boolean isRemovingAdmin(in ComponentName adminReceiver, int userHandle); + + boolean setUserEnabled(in ComponentName who); + boolean isDeviceInitializer(String packageName); + void clearDeviceInitializer(in ComponentName who); + boolean setDeviceInitializer(in ComponentName who, in ComponentName initializer, String initializerName); + String getDeviceInitializer(); + + void setUserIcon(in ComponentName admin, in Bitmap icon); } diff --git a/core/java/android/app/backup/BackupAgent.java b/core/java/android/app/backup/BackupAgent.java index 1b1e600..7f89100 100644 --- a/core/java/android/app/backup/BackupAgent.java +++ b/core/java/android/app/backup/BackupAgent.java @@ -258,6 +258,7 @@ public abstract class BackupAgent extends ContextWrapper { * * <ul> * <li>The contents of the {@link #getCacheDir()} directory</li> + * <li>The contents of the {@link #getCodeCacheDir()} directory</li> * <li>The contents of the {@link #getNoBackupFilesDir()} directory</li> * <li>The contents of the app's shared library directory</li> * </ul> @@ -285,6 +286,7 @@ public abstract class BackupAgent extends ContextWrapper { String databaseDir = getDatabasePath("foo").getParentFile().getCanonicalPath(); String sharedPrefsDir = getSharedPrefsFile("foo").getParentFile().getCanonicalPath(); String cacheDir = getCacheDir().getCanonicalPath(); + String codeCacheDir = getCodeCacheDir().getCanonicalPath(); String libDir = (appInfo.nativeLibraryDir != null) ? new File(appInfo.nativeLibraryDir).getCanonicalPath() : null; @@ -298,6 +300,7 @@ public abstract class BackupAgent extends ContextWrapper { filterSet.add(libDir); } filterSet.add(cacheDir); + filterSet.add(codeCacheDir); filterSet.add(databaseDir); filterSet.add(sharedPrefsDir); filterSet.add(filesDir); @@ -354,6 +357,7 @@ public abstract class BackupAgent extends ContextWrapper { String dbDir; String spDir; String cacheDir; + String codeCacheDir; String libDir; String efDir = null; String filePath; @@ -367,6 +371,7 @@ public abstract class BackupAgent extends ContextWrapper { dbDir = getDatabasePath("foo").getParentFile().getCanonicalPath(); spDir = getSharedPrefsFile("foo").getParentFile().getCanonicalPath(); cacheDir = getCacheDir().getCanonicalPath(); + codeCacheDir = getCodeCacheDir().getCanonicalPath(); libDir = (appInfo.nativeLibraryDir == null) ? null : new File(appInfo.nativeLibraryDir).getCanonicalPath(); @@ -380,7 +385,8 @@ public abstract class BackupAgent extends ContextWrapper { } // Now figure out which well-defined tree the file is placed in, working from - // most to least specific. We also specifically exclude the lib and cache dirs. + // most to least specific. We also specifically exclude the lib, cache, + // and code_cache dirs. filePath = file.getCanonicalPath(); } catch (IOException e) { Log.w(TAG, "Unable to obtain canonical paths"); @@ -388,9 +394,10 @@ public abstract class BackupAgent extends ContextWrapper { } if (filePath.startsWith(cacheDir) + || filePath.startsWith(codeCacheDir) || filePath.startsWith(libDir) || filePath.startsWith(nbFilesDir)) { - Log.w(TAG, "lib, cache, and no_backup files are not backed up"); + Log.w(TAG, "lib, cache, code_cache, and no_backup files are not backed up"); return; } diff --git a/core/java/android/app/backup/BackupDataOutput.java b/core/java/android/app/backup/BackupDataOutput.java index 048a4bb..1fe63e7 100644 --- a/core/java/android/app/backup/BackupDataOutput.java +++ b/core/java/android/app/backup/BackupDataOutput.java @@ -18,8 +18,6 @@ package android.app.backup; import android.annotation.SystemApi; import android.os.ParcelFileDescriptor; -import android.os.Process; - import java.io.FileDescriptor; import java.io.IOException; diff --git a/core/java/android/app/job/JobParameters.java b/core/java/android/app/job/JobParameters.java index 62734f2..7ee39f5 100644 --- a/core/java/android/app/job/JobParameters.java +++ b/core/java/android/app/job/JobParameters.java @@ -17,7 +17,6 @@ package android.app.job; import android.app.job.IJobCallback; -import android.app.job.IJobCallback.Stub; import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; diff --git a/core/java/android/app/job/JobScheduler.java b/core/java/android/app/job/JobScheduler.java index 89efeb2..a1c11f3 100644 --- a/core/java/android/app/job/JobScheduler.java +++ b/core/java/android/app/job/JobScheduler.java @@ -18,8 +18,6 @@ package android.app.job; import java.util.List; -import android.content.Context; - /** * This is an API for scheduling various types of jobs against the framework that will be executed * in your application's own process. diff --git a/core/java/android/app/usage/UsageEvents.java b/core/java/android/app/usage/UsageEvents.java index 3cf3c95..58279d7 100644 --- a/core/java/android/app/usage/UsageEvents.java +++ b/core/java/android/app/usage/UsageEvents.java @@ -68,6 +68,11 @@ public final class UsageEvents implements Parcelable { public static final int CONFIGURATION_CHANGE = 5; /** + * An event type denoting that a package was interacted with in some way. + */ + public static final int INTERACTION = 6; + + /** * {@hide} */ public String mPackage; diff --git a/core/java/android/app/usage/UsageStatsManagerInternal.java b/core/java/android/app/usage/UsageStatsManagerInternal.java index 083a48a..0122069 100644 --- a/core/java/android/app/usage/UsageStatsManagerInternal.java +++ b/core/java/android/app/usage/UsageStatsManagerInternal.java @@ -37,6 +37,16 @@ public abstract class UsageStatsManagerInternal { public abstract void reportEvent(ComponentName component, int userId, int eventType); /** + * Reports an event to the UsageStatsManager. + * + * @param packageName The package for which this event occurred. + * @param userId The user id to which the component belongs to. + * @param eventType The event that occurred. Valid values can be found at + * {@link UsageEvents} + */ + public abstract void reportEvent(String packageName, int userId, int eventType); + + /** * Reports a configuration change to the UsageStatsManager. * * @param config The new device configuration. diff --git a/core/java/android/appwidget/AppWidgetHost.java b/core/java/android/appwidget/AppWidgetHost.java index 8e86824..1af4953 100644 --- a/core/java/android/appwidget/AppWidgetHost.java +++ b/core/java/android/appwidget/AppWidgetHost.java @@ -16,6 +16,7 @@ package android.appwidget; +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashMap; @@ -58,18 +59,28 @@ public class AppWidgetHost { private DisplayMetrics mDisplayMetrics; private String mContextOpPackageName; - Handler mHandler; - int mHostId; - Callbacks mCallbacks = new Callbacks(); - final HashMap<Integer,AppWidgetHostView> mViews = new HashMap<Integer, AppWidgetHostView>(); + private final Handler mHandler; + private final int mHostId; + private final Callbacks mCallbacks; + private final HashMap<Integer,AppWidgetHostView> mViews = new HashMap<>(); private OnClickHandler mOnClickHandler; - class Callbacks extends IAppWidgetHost.Stub { + static class Callbacks extends IAppWidgetHost.Stub { + private final WeakReference<Handler> mWeakHandler; + + public Callbacks(Handler handler) { + mWeakHandler = new WeakReference<>(handler); + } + public void updateAppWidget(int appWidgetId, RemoteViews views) { if (isLocalBinder() && views != null) { views = views.clone(); } - Message msg = mHandler.obtainMessage(HANDLE_UPDATE, appWidgetId, 0, views); + Handler handler = mWeakHandler.get(); + if (handler == null) { + return; + } + Message msg = handler.obtainMessage(HANDLE_UPDATE, appWidgetId, 0, views); msg.sendToTarget(); } @@ -77,20 +88,36 @@ public class AppWidgetHost { if (isLocalBinder() && info != null) { info = info.clone(); } - Message msg = mHandler.obtainMessage(HANDLE_PROVIDER_CHANGED, + Handler handler = mWeakHandler.get(); + if (handler == null) { + return; + } + Message msg = handler.obtainMessage(HANDLE_PROVIDER_CHANGED, appWidgetId, 0, info); msg.sendToTarget(); } public void providersChanged() { - mHandler.obtainMessage(HANDLE_PROVIDERS_CHANGED).sendToTarget(); + Handler handler = mWeakHandler.get(); + if (handler == null) { + return; + } + handler.obtainMessage(HANDLE_PROVIDERS_CHANGED).sendToTarget(); } public void viewDataChanged(int appWidgetId, int viewId) { - Message msg = mHandler.obtainMessage(HANDLE_VIEW_DATA_CHANGED, + Handler handler = mWeakHandler.get(); + if (handler == null) { + return; + } + Message msg = handler.obtainMessage(HANDLE_VIEW_DATA_CHANGED, appWidgetId, viewId); msg.sendToTarget(); } + + private static boolean isLocalBinder() { + return Process.myPid() == Binder.getCallingPid(); + } } class UpdateHandler extends Handler { @@ -132,6 +159,7 @@ public class AppWidgetHost { mHostId = hostId; mOnClickHandler = handler; mHandler = new UpdateHandler(looper); + mCallbacks = new Callbacks(mHandler); mDisplayMetrics = context.getResources().getDisplayMetrics(); bindService(); } @@ -251,10 +279,6 @@ public class AppWidgetHost { } } - private boolean isLocalBinder() { - return Process.myPid() == Binder.getCallingPid(); - } - /** * Stop listening to changes for this AppWidget. */ diff --git a/core/java/android/appwidget/AppWidgetHostView.java b/core/java/android/appwidget/AppWidgetHostView.java index 1ff476e..3219fe7 100644 --- a/core/java/android/appwidget/AppWidgetHostView.java +++ b/core/java/android/appwidget/AppWidgetHostView.java @@ -31,9 +31,7 @@ import android.os.Build; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; -import android.os.Process; import android.os.SystemClock; -import android.os.UserHandle; import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; @@ -588,9 +586,10 @@ public class AppWidgetHostView extends FrameLayout { return tv; } + /** @hide */ @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); + public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfoInternal(info); info.setClassName(AppWidgetHostView.class.getName()); } diff --git a/core/java/android/bluetooth/BluetoothA2dp.java b/core/java/android/bluetooth/BluetoothA2dp.java index 0450150..767f59e 100644 --- a/core/java/android/bluetooth/BluetoothA2dp.java +++ b/core/java/android/bluetooth/BluetoothA2dp.java @@ -22,6 +22,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; +import android.media.AudioManager; import android.os.IBinder; import android.os.ParcelUuid; import android.os.RemoteException; @@ -415,9 +416,16 @@ public final class BluetoothA2dp implements BluetoothProfile { } /** - * Tells remote device to adjust volume. Only if absolute volume is supported. + * Tells remote device to adjust volume. Only if absolute volume is + * supported. Uses the following values: + * <ul> + * <li>{@link AudioManager#ADJUST_LOWER}</li> + * <li>{@link AudioManager#ADJUST_RAISE}</li> + * <li>{@link AudioManager#ADJUST_MUTE}</li> + * <li>{@link AudioManager#ADJUST_UNMUTE}</li> + * </ul> * - * @param direction 1 to increase volume, or -1 to decrease volume + * @param direction One of the supported adjust values. * @hide */ public void adjustAvrcpAbsoluteVolume(int direction) { diff --git a/core/java/android/bluetooth/BluetoothActivityEnergyInfo.java b/core/java/android/bluetooth/BluetoothActivityEnergyInfo.java index ce87329..161c339 100644 --- a/core/java/android/bluetooth/BluetoothActivityEnergyInfo.java +++ b/core/java/android/bluetooth/BluetoothActivityEnergyInfo.java @@ -26,32 +26,32 @@ import android.os.Parcelable; * @hide */ public final class BluetoothActivityEnergyInfo implements Parcelable { + private final long mTimestamp; private final int mBluetoothStackState; private final int mControllerTxTimeMs; private final int mControllerRxTimeMs; private final int mControllerIdleTimeMs; private final int mControllerEnergyUsed; - private final long timestamp; public static final int BT_STACK_STATE_INVALID = 0; public static final int BT_STACK_STATE_STATE_ACTIVE = 1; public static final int BT_STACK_STATE_STATE_SCANNING = 2; public static final int BT_STACK_STATE_STATE_IDLE = 3; - public BluetoothActivityEnergyInfo(int stackState, int txTime, int rxTime, - int idleTime, int energyUsed) { + public BluetoothActivityEnergyInfo(long timestamp, int stackState, + int txTime, int rxTime, int idleTime, int energyUsed) { + mTimestamp = timestamp; mBluetoothStackState = stackState; mControllerTxTimeMs = txTime; mControllerRxTimeMs = rxTime; mControllerIdleTimeMs = idleTime; mControllerEnergyUsed = energyUsed; - timestamp = System.currentTimeMillis(); } @Override public String toString() { return "BluetoothActivityEnergyInfo{" - + " timestamp=" + timestamp + + " mTimestamp=" + mTimestamp + " mBluetoothStackState=" + mBluetoothStackState + " mControllerTxTimeMs=" + mControllerTxTimeMs + " mControllerRxTimeMs=" + mControllerRxTimeMs @@ -63,13 +63,14 @@ public final class BluetoothActivityEnergyInfo implements Parcelable { public static final Parcelable.Creator<BluetoothActivityEnergyInfo> CREATOR = new Parcelable.Creator<BluetoothActivityEnergyInfo>() { public BluetoothActivityEnergyInfo createFromParcel(Parcel in) { + long timestamp = in.readLong(); int stackState = in.readInt(); int txTime = in.readInt(); int rxTime = in.readInt(); int idleTime = in.readInt(); int energyUsed = in.readInt(); - return new BluetoothActivityEnergyInfo(stackState, txTime, rxTime, - idleTime, energyUsed); + return new BluetoothActivityEnergyInfo(timestamp, stackState, + txTime, rxTime, idleTime, energyUsed); } public BluetoothActivityEnergyInfo[] newArray(int size) { return new BluetoothActivityEnergyInfo[size]; @@ -77,6 +78,7 @@ public final class BluetoothActivityEnergyInfo implements Parcelable { }; public void writeToParcel(Parcel out, int flags) { + out.writeLong(mTimestamp); out.writeInt(mBluetoothStackState); out.writeInt(mControllerTxTimeMs); out.writeInt(mControllerRxTimeMs); @@ -123,11 +125,12 @@ public final class BluetoothActivityEnergyInfo implements Parcelable { public int getControllerEnergyUsed() { return mControllerEnergyUsed; } + /** - * @return timestamp(wall clock) of record creation + * @return timestamp(real time elapsed in milliseconds since boot) of record creation. */ public long getTimeStamp() { - return timestamp; + return mTimestamp; } /** diff --git a/core/java/android/bluetooth/BluetoothAdapter.java b/core/java/android/bluetooth/BluetoothAdapter.java index b8f4bf8..be26eac 100644 --- a/core/java/android/bluetooth/BluetoothAdapter.java +++ b/core/java/android/bluetooth/BluetoothAdapter.java @@ -26,9 +26,7 @@ import android.bluetooth.le.ScanRecord; import android.bluetooth.le.ScanResult; import android.bluetooth.le.ScanSettings; import android.content.Context; -import android.os.Handler; import android.os.IBinder; -import android.os.Looper; import android.os.ParcelUuid; import android.os.RemoteException; import android.os.ServiceManager; @@ -36,7 +34,6 @@ import android.util.Log; import android.util.Pair; import java.io.IOException; -import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; diff --git a/core/java/android/bluetooth/BluetoothHeadsetClientCall.java b/core/java/android/bluetooth/BluetoothHeadsetClientCall.java index a15bd97..7b5a045 100644 --- a/core/java/android/bluetooth/BluetoothHeadsetClientCall.java +++ b/core/java/android/bluetooth/BluetoothHeadsetClientCall.java @@ -61,6 +61,7 @@ public final class BluetoothHeadsetClientCall implements Parcelable { */ public static final int CALL_STATE_TERMINATED = 7; + private final BluetoothDevice mDevice; private final int mId; private int mState; private String mNumber; @@ -70,8 +71,9 @@ public final class BluetoothHeadsetClientCall implements Parcelable { /** * Creates BluetoothHeadsetClientCall instance. */ - public BluetoothHeadsetClientCall(int id, int state, String number, boolean multiParty, - boolean outgoing) { + public BluetoothHeadsetClientCall(BluetoothDevice device, int id, int state, String number, + boolean multiParty, boolean outgoing) { + mDevice = device; mId = id; mState = state; mNumber = number != null ? number : ""; @@ -114,6 +116,15 @@ public final class BluetoothHeadsetClientCall implements Parcelable { } /** + * Gets call's device. + * + * @return call device. + */ + public BluetoothDevice getDevice() { + return mDevice; + } + + /** * Gets call's Id. * * @return call id. @@ -161,7 +172,9 @@ public final class BluetoothHeadsetClientCall implements Parcelable { } public String toString() { - StringBuilder builder = new StringBuilder("BluetoothHeadsetClientCall{mId: "); + StringBuilder builder = new StringBuilder("BluetoothHeadsetClientCall{mDevice: "); + builder.append(mDevice); + builder.append(", mId: "); builder.append(mId); builder.append(", mState: "); switch (mState) { @@ -192,8 +205,9 @@ public final class BluetoothHeadsetClientCall implements Parcelable { new Parcelable.Creator<BluetoothHeadsetClientCall>() { @Override public BluetoothHeadsetClientCall createFromParcel(Parcel in) { - return new BluetoothHeadsetClientCall(in.readInt(), in.readInt(), - in.readString(), in.readInt() == 1, in.readInt() == 1); + return new BluetoothHeadsetClientCall((BluetoothDevice)in.readParcelable(null), + in.readInt(), in.readInt(), in.readString(), + in.readInt() == 1, in.readInt() == 1); } @Override @@ -204,6 +218,7 @@ public final class BluetoothHeadsetClientCall implements Parcelable { @Override public void writeToParcel(Parcel out, int flags) { + out.writeParcelable(mDevice, 0); out.writeInt(mId); out.writeInt(mState); out.writeString(mNumber); diff --git a/core/java/android/bluetooth/le/TruncatedFilter.java b/core/java/android/bluetooth/le/TruncatedFilter.java index 6a6b3e3..685b174 100644 --- a/core/java/android/bluetooth/le/TruncatedFilter.java +++ b/core/java/android/bluetooth/le/TruncatedFilter.java @@ -17,9 +17,6 @@ package android.bluetooth.le; import android.annotation.SystemApi; -import android.os.Parcel; -import android.os.Parcelable; - import java.util.List; /** diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java index 0cff4c0..393cf8e 100644 --- a/core/java/android/content/ContentProvider.java +++ b/core/java/android/content/ContentProvider.java @@ -19,6 +19,7 @@ package android.content; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.Manifest.permission.INTERACT_ACROSS_USERS; +import android.annotation.Nullable; import android.app.AppOpsManager; import android.content.pm.PathPermission; import android.content.pm.ProviderInfo; @@ -364,7 +365,8 @@ public abstract class ContentProvider implements ComponentCallbacks2 { } @Override - public Bundle call(String callingPkg, String method, String arg, Bundle extras) { + public Bundle call( + String callingPkg, String method, @Nullable String arg, @Nullable Bundle extras) { final String original = setCallingPackage(callingPkg); try { return ContentProvider.this.call(method, arg, extras); @@ -1742,7 +1744,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 { * @return provider-defined return value. May be {@code null}, which is also * the default for providers which don't implement any call methods. */ - public Bundle call(String method, String arg, Bundle extras) { + public Bundle call(String method, @Nullable String arg, @Nullable Bundle extras) { return null; } diff --git a/core/java/android/content/ContentProviderOperation.java b/core/java/android/content/ContentProviderOperation.java index 136e54d..49ac062 100644 --- a/core/java/android/content/ContentProviderOperation.java +++ b/core/java/android/content/ContentProviderOperation.java @@ -208,6 +208,22 @@ public class ContentProviderOperation implements Parcelable { return mType; } + public boolean isInsert() { + return mType == TYPE_INSERT; + } + + public boolean isDelete() { + return mType == TYPE_DELETE; + } + + public boolean isUpdate() { + return mType == TYPE_UPDATE; + } + + public boolean isAssertQuery() { + return mType == TYPE_ASSERT; + } + public boolean isWriteOperation() { return mType == TYPE_DELETE || mType == TYPE_INSERT || mType == TYPE_UPDATE; } diff --git a/core/java/android/content/ContentProviderResult.java b/core/java/android/content/ContentProviderResult.java index ec3d002..4196f27 100644 --- a/core/java/android/content/ContentProviderResult.java +++ b/core/java/android/content/ContentProviderResult.java @@ -18,7 +18,6 @@ package android.content; import android.content.ContentProvider; import android.net.Uri; -import android.os.UserHandle; import android.os.Parcelable; import android.os.Parcel; diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index a09fee9..17a8eb7 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -17,6 +17,7 @@ package android.content; import android.accounts.Account; +import android.annotation.Nullable; import android.app.ActivityManagerNative; import android.app.ActivityThread; import android.app.AppGlobals; @@ -1357,7 +1358,8 @@ public abstract class ContentResolver { * @throws NullPointerException if uri or method is null * @throws IllegalArgumentException if uri is not known */ - public final Bundle call(Uri uri, String method, String arg, Bundle extras) { + public final Bundle call( + Uri uri, String method, @Nullable String arg, @Nullable Bundle extras) { if (uri == null) { throw new NullPointerException("uri == null"); } diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 26735a6..80b5e0b 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -16,14 +16,19 @@ package android.content; +import android.annotation.CheckResult; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.StringDef; +import android.annotation.StringRes; +import android.annotation.StyleRes; +import android.annotation.StyleableRes; import android.annotation.SystemApi; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.AssetManager; +import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; @@ -32,7 +37,6 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; -import android.media.MediaScannerConnection.OnScanCompletedListener; import android.net.Uri; import android.os.Bundle; import android.os.Environment; @@ -146,12 +150,13 @@ public abstract class Context { @IntDef(flag = true, value = { BIND_AUTO_CREATE, - BIND_AUTO_CREATE, BIND_DEBUG_UNBIND, BIND_NOT_FOREGROUND, BIND_ABOVE_CLIENT, BIND_ALLOW_OOM_MANAGEMENT, - BIND_WAIVE_PRIORITY + BIND_WAIVE_PRIORITY, + BIND_IMPORTANT, + BIND_ADJUST_WITH_ACTIVITY }) @Retention(RetentionPolicy.SOURCE) public @interface BindServiceFlags {} @@ -363,7 +368,7 @@ public abstract class Context { * * @param resId Resource id for the CharSequence text */ - public final CharSequence getText(int resId) { + public final CharSequence getText(@StringRes int resId) { return getResources().getText(resId); } @@ -373,7 +378,7 @@ public abstract class Context { * * @param resId Resource id for the string */ - public final String getString(int resId) { + public final String getString(@StringRes int resId) { return getResources().getString(resId); } @@ -386,23 +391,60 @@ public abstract class Context { * @param formatArgs The format arguments that will be used for substitution. */ - public final String getString(int resId, Object... formatArgs) { + public final String getString(@StringRes int resId, Object... formatArgs) { return getResources().getString(resId, formatArgs); } /** - * Return a drawable object associated with a particular resource ID and + * Returns a color associated with a particular resource ID and styled for + * the current theme. + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * @return A single color value in the form 0xAARRGGBB. + * @throws android.content.res.Resources.NotFoundException if the given ID + * does not exist. + */ + @Nullable + public final int getColor(int id) { + return getResources().getColor(id, getTheme()); + } + + /** + * Returns a drawable object associated with a particular resource ID and * styled for the current theme. * * @param id The desired resource identifier, as generated by the aapt * tool. This integer encodes the package, type, and resource * entry. The value 0 is an invalid identifier. - * @return Drawable An object that can be used to draw this resource. + * @return An object that can be used to draw this resource, or + * {@code null} if the resource could not be resolved. + * @throws android.content.res.Resources.NotFoundException if the given ID + * does not exist. */ + @Nullable public final Drawable getDrawable(int id) { return getResources().getDrawable(id, getTheme()); } + /** + * Returns a color state list associated with a particular resource ID and + * styled for the current theme. + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * @return A color state list, or {@code null} if the resource could not be + * resolved. + * @throws android.content.res.Resources.NotFoundException if the given ID + * does not exist. + */ + @Nullable + public final ColorStateList getColorStateList(int id) { + return getResources().getColorStateList(id, getTheme()); + } + /** * Set the base theme for this context. Note that this should be called * before any views are instantiated in the Context (for example before @@ -411,7 +453,7 @@ public abstract class Context { * * @param resid The style resource describing the theme. */ - public abstract void setTheme(int resid); + public abstract void setTheme(@StyleRes int resid); /** @hide Needed for some internal implementation... not public because * you can't assume this actually means anything. */ @@ -427,10 +469,10 @@ public abstract class Context { /** * Retrieve styled attribute information in this Context's theme. See - * {@link Resources.Theme#obtainStyledAttributes(int[])} + * {@link android.content.res.Resources.Theme#obtainStyledAttributes(int[])} * for more information. * - * @see Resources.Theme#obtainStyledAttributes(int[]) + * @see android.content.res.Resources.Theme#obtainStyledAttributes(int[]) */ public final TypedArray obtainStyledAttributes( int[] attrs) { @@ -439,22 +481,22 @@ public abstract class Context { /** * Retrieve styled attribute information in this Context's theme. See - * {@link Resources.Theme#obtainStyledAttributes(int, int[])} + * {@link android.content.res.Resources.Theme#obtainStyledAttributes(int, int[])} * for more information. * - * @see Resources.Theme#obtainStyledAttributes(int, int[]) + * @see android.content.res.Resources.Theme#obtainStyledAttributes(int, int[]) */ public final TypedArray obtainStyledAttributes( - int resid, int[] attrs) throws Resources.NotFoundException { + @StyleableRes int resid, int[] attrs) throws Resources.NotFoundException { return getTheme().obtainStyledAttributes(resid, attrs); } /** * Retrieve styled attribute information in this Context's theme. See - * {@link Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int)} + * {@link android.content.res.Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int)} * for more information. * - * @see Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int) + * @see android.content.res.Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int) */ public final TypedArray obtainStyledAttributes( AttributeSet set, int[] attrs) { @@ -463,10 +505,10 @@ public abstract class Context { /** * Retrieve styled attribute information in this Context's theme. See - * {@link Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int)} + * {@link android.content.res.Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int)} * for more information. * - * @see Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int) + * @see android.content.res.Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int) */ public final TypedArray obtainStyledAttributes( AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes) { @@ -711,7 +753,8 @@ public abstract class Context { * are not automatically scanned by the media scanner, you can explicitly * add them to the media database with * {@link android.media.MediaScannerConnection#scanFile(Context, String[], String[], - * OnScanCompletedListener) MediaScannerConnection.scanFile}. + * android.media.MediaScannerConnection.OnScanCompletedListener) + * MediaScannerConnection.scanFile}. * Note that this is not the same as * {@link android.os.Environment#getExternalStoragePublicDirectory * Environment.getExternalStoragePublicDirectory()}, which provides @@ -1876,7 +1919,7 @@ public abstract class Context { * @return The first sticky intent found that matches <var>filter</var>, * or null if there are none. * - * @see #registerReceiver(BroadcastReceiver, IntentFilter, String, Handler + * @see #registerReceiver(BroadcastReceiver, IntentFilter, String, Handler) * @see #sendBroadcast * @see #unregisterReceiver */ @@ -2039,7 +2082,9 @@ public abstract class Context { * @hide */ @SystemApi - public boolean bindServiceAsUser(Intent service, ServiceConnection conn, int flags, UserHandle user) { + @SuppressWarnings("unused") + public boolean bindServiceAsUser(Intent service, ServiceConnection conn, + int flags, UserHandle user) { throw new RuntimeException("Not implemented. Must override in a subclass."); } @@ -2110,17 +2155,21 @@ public abstract class Context { WIFI_PASSPOINT_SERVICE, WIFI_P2P_SERVICE, WIFI_SCANNING_SERVICE, + //@hide: WIFI_RTT_SERVICE, //@hide: ETHERNET_SERVICE, WIFI_RTT_SERVICE, NSD_SERVICE, AUDIO_SERVICE, + //@hide: FINGERPRINT_SERVICE, MEDIA_ROUTER_SERVICE, TELEPHONY_SERVICE, + TELEPHONY_SUBSCRIPTION_SERVICE, TELECOM_SERVICE, CLIPBOARD_SERVICE, INPUT_METHOD_SERVICE, TEXT_SERVICES_MANAGER_SERVICE, APPWIDGET_SERVICE, + //@hide: VOICE_INTERACTION_MANAGER_SERVICE, //@hide: BACKUP_SERVICE, DROPBOX_SERVICE, DEVICE_POLICY_SERVICE, @@ -2132,17 +2181,26 @@ public abstract class Context { USB_SERVICE, LAUNCHER_APPS_SERVICE, //@hide: SERIAL_SERVICE, + //@hide: HDMI_CONTROL_SERVICE, INPUT_SERVICE, DISPLAY_SERVICE, - //@hide: SCHEDULING_POLICY_SERVICE, USER_SERVICE, - //@hide: APP_OPS_SERVICE + RESTRICTIONS_SERVICE, + APP_OPS_SERVICE, CAMERA_SERVICE, PRINT_SERVICE, + CONSUMER_IR_SERVICE, + //@hide: TRUST_SERVICE, + TV_INPUT_SERVICE, + //@hide: NETWORK_SCORE_SERVICE, + USAGE_STATS_SERVICE, MEDIA_SESSION_SERVICE, BATTERY_SERVICE, JOB_SCHEDULER_SERVICE, + //@hide: PERSISTENT_DATA_BLOCK_SERVICE, MEDIA_PROJECTION_SERVICE, + MIDI_SERVICE, + RADIO_SERVICE, }) @Retention(RetentionPolicy.SOURCE) public @interface ServiceName {} @@ -2262,6 +2320,51 @@ public abstract class Context { public abstract Object getSystemService(@ServiceName @NonNull String name); /** + * Return the handle to a system-level service by class. + * <p> + * Currently available classes are: + * {@link android.view.WindowManager}, {@link android.view.LayoutInflater}, + * {@link android.app.ActivityManager}, {@link android.os.PowerManager}, + * {@link android.app.AlarmManager}, {@link android.app.NotificationManager}, + * {@link android.app.KeyguardManager}, {@link android.location.LocationManager}, + * {@link android.app.SearchManager}, {@link android.os.Vibrator}, + * {@link android.net.ConnectivityManager}, + * {@link android.net.wifi.WifiManager}, + * {@link android.media.AudioManager}, {@link android.media.MediaRouter}, + * {@link android.telephony.TelephonyManager}, {@link android.telephony.SubscriptionManager}, + * {@link android.view.inputmethod.InputMethodManager}, + * {@link android.app.UiModeManager}, {@link android.app.DownloadManager}, + * {@link android.os.BatteryManager}, {@link android.app.job.JobScheduler}. + * </p><p> + * Note: System services obtained via this API may be closely associated with + * the Context in which they are obtained from. In general, do not share the + * service objects between various different contexts (Activities, Applications, + * Services, Providers, etc.) + * </p> + * + * @param serviceClass The class of the desired service. + * @return The service or null if the class is not a supported system service. + */ + @SuppressWarnings("unchecked") + public final <T> T getSystemService(Class<T> serviceClass) { + // Because subclasses may override getSystemService(String) we cannot + // perform a lookup by class alone. We must first map the class to its + // service name then invoke the string-based method. + String serviceName = getSystemServiceName(serviceClass); + return serviceName != null ? (T)getSystemService(serviceName) : null; + } + + /** + * Gets the name of the system-level service that is represented by the specified class. + * + * @param serviceClass The class of the desired service. + * @return The service name or null if the class is not a supported system service. + * + * @hide + */ + public abstract String getSystemServiceName(Class<?> serviceClass); + + /** * Use with {@link #getSystemService} to retrieve a * {@link android.os.PowerManager} for controlling power management, * including "wake locks," which let you keep the device on while @@ -2556,7 +2659,7 @@ public abstract class Context { * of fingerprints. * * @see #getSystemService - * @see android.app.FingerprintManager + * @see android.service.fingerprint.FingerprintManager * @hide */ public static final String FINGERPRINT_SERVICE = "fingerprint"; @@ -2612,11 +2715,11 @@ public abstract class Context { /** * Use with {@link #getSystemService} to retrieve a - * {@link android.text.ClipboardManager} for accessing and modifying + * {@link android.content.ClipboardManager} for accessing and modifying * the contents of the global clipboard. * * @see #getSystemService - * @see android.text.ClipboardManager + * @see android.content.ClipboardManager */ public static final String CLIPBOARD_SERVICE = "clipboard"; @@ -2911,11 +3014,31 @@ public abstract class Context { * android.media.projection.MediaProjectionManager} instance for managing * media projection sessions. * @see #getSystemService - * @see android.media.projection.ProjectionManager + * @see android.media.projection.MediaProjectionManager */ public static final String MEDIA_PROJECTION_SERVICE = "media_projection"; /** + * Use with {@link #getSystemService} to retrieve a + * {@link android.media.midi.MidiManager} for accessing the MIDI service. + * + * @see #getSystemService + * @hide + */ + public static final String MIDI_SERVICE = "midi"; + + + /** + * Use with {@link #getSystemService} to retrieve a + * {@link android.hardware.radio.RadioManager} for accessing the broadcast radio service. + * + * @see #getSystemService + * @hide + */ + public static final String RADIO_SERVICE = "radio"; + + + /** * Determine whether the given permission is allowed for a particular * process and user ID running in the system. * @@ -2931,6 +3054,7 @@ public abstract class Context { * @see PackageManager#checkPermission(String, String) * @see #checkCallingPermission */ + @CheckResult(suggest="#enforcePermission(String,int,int,String)") @PackageManager.PermissionResult public abstract int checkPermission(@NonNull String permission, int pid, int uid); @@ -2960,6 +3084,7 @@ public abstract class Context { * @see #checkPermission * @see #checkCallingOrSelfPermission */ + @CheckResult(suggest="#enforceCallingPermission(String,String)") @PackageManager.PermissionResult public abstract int checkCallingPermission(@NonNull String permission); @@ -2979,6 +3104,7 @@ public abstract class Context { * @see #checkPermission * @see #checkCallingPermission */ + @CheckResult(suggest="#enforceCallingOrSelfPermission(String,String)") @PackageManager.PermissionResult public abstract int checkCallingOrSelfPermission(@NonNull String permission); @@ -3123,6 +3249,7 @@ public abstract class Context { * * @see #checkCallingUriPermission */ + @CheckResult(suggest="#enforceUriPermission(Uri,int,int,String)") public abstract int checkUriPermission(Uri uri, int pid, int uid, @Intent.AccessUriMode int modeFlags); @@ -3151,6 +3278,7 @@ public abstract class Context { * * @see #checkUriPermission(Uri, int, int, int) */ + @CheckResult(suggest="#enforceCallingUriPermission(Uri,int,String)") public abstract int checkCallingUriPermission(Uri uri, @Intent.AccessUriMode int modeFlags); /** @@ -3170,6 +3298,7 @@ public abstract class Context { * * @see #checkCallingUriPermission */ + @CheckResult(suggest="#enforceCallingOrSelfUriPermission(Uri,int,String)") public abstract int checkCallingOrSelfUriPermission(Uri uri, @Intent.AccessUriMode int modeFlags); @@ -3195,6 +3324,7 @@ public abstract class Context { * is allowed to access that uri or holds one of the given permissions, or * {@link PackageManager#PERMISSION_DENIED} if it is not. */ + @CheckResult(suggest="#enforceUriPermission(Uri,String,String,int,int,int,String)") public abstract int checkUriPermission(@Nullable Uri uri, @Nullable String readPermission, @Nullable String writePermission, int pid, int uid, @Intent.AccessUriMode int modeFlags); @@ -3338,7 +3468,7 @@ public abstract class Context { * are not shared, however they share common state (Resources, ClassLoader, * etc) so the Context instance itself is fairly lightweight. * - * <p>Throws {@link PackageManager.NameNotFoundException} if there is no + * <p>Throws {@link android.content.pm.PackageManager.NameNotFoundException} if there is no * application with the given package name. * * <p>Throws {@link java.lang.SecurityException} if the Context requested diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java index cfae1cf..6e8b7c1 100644 --- a/core/java/android/content/ContextWrapper.java +++ b/core/java/android/content/ContextWrapper.java @@ -284,36 +284,43 @@ public class ContextWrapper extends Context { } @Override + @Deprecated public Drawable getWallpaper() { return mBase.getWallpaper(); } @Override + @Deprecated public Drawable peekWallpaper() { return mBase.peekWallpaper(); } @Override + @Deprecated public int getWallpaperDesiredMinimumWidth() { return mBase.getWallpaperDesiredMinimumWidth(); } @Override + @Deprecated public int getWallpaperDesiredMinimumHeight() { return mBase.getWallpaperDesiredMinimumHeight(); } @Override + @Deprecated public void setWallpaper(Bitmap bitmap) throws IOException { mBase.setWallpaper(bitmap); } @Override + @Deprecated public void setWallpaper(InputStream data) throws IOException { mBase.setWallpaper(data); } @Override + @Deprecated public void clearWallpaper() throws IOException { mBase.clearWallpaper(); } @@ -445,11 +452,13 @@ public class ContextWrapper extends Context { } @Override + @Deprecated public void sendStickyBroadcast(Intent intent) { mBase.sendStickyBroadcast(intent); } @Override + @Deprecated public void sendStickyOrderedBroadcast( Intent intent, BroadcastReceiver resultReceiver, Handler scheduler, int initialCode, String initialData, @@ -460,16 +469,19 @@ public class ContextWrapper extends Context { } @Override + @Deprecated public void removeStickyBroadcast(Intent intent) { mBase.removeStickyBroadcast(intent); } @Override + @Deprecated public void sendStickyBroadcastAsUser(Intent intent, UserHandle user) { mBase.sendStickyBroadcastAsUser(intent, user); } @Override + @Deprecated public void sendStickyOrderedBroadcastAsUser(Intent intent, UserHandle user, BroadcastReceiver resultReceiver, Handler scheduler, int initialCode, String initialData, @@ -479,6 +491,7 @@ public class ContextWrapper extends Context { } @Override + @Deprecated public void removeStickyBroadcastAsUser(Intent intent, UserHandle user) { mBase.removeStickyBroadcastAsUser(intent, user); } @@ -563,6 +576,11 @@ public class ContextWrapper extends Context { } @Override + public String getSystemServiceName(Class<?> serviceClass) { + return mBase.getSystemServiceName(serviceClass); + } + + @Override public int checkPermission(String permission, int pid, int uid) { return mBase.checkPermission(permission, pid, uid); } @@ -679,6 +697,7 @@ public class ContextWrapper extends Context { } /** @hide */ + @Override public Context createApplicationContext(ApplicationInfo application, int flags) throws PackageManager.NameNotFoundException { return mBase.createApplicationContext(application, flags); diff --git a/core/java/android/content/IContentProvider.java b/core/java/android/content/IContentProvider.java index f858406..4afe38b 100644 --- a/core/java/android/content/IContentProvider.java +++ b/core/java/android/content/IContentProvider.java @@ -16,6 +16,7 @@ package android.content; +import android.annotation.Nullable; import android.content.res.AssetFileDescriptor; import android.database.Cursor; import android.net.Uri; @@ -56,7 +57,8 @@ public interface IContentProvider extends IInterface { public ContentProviderResult[] applyBatch(String callingPkg, ArrayList<ContentProviderOperation> operations) throws RemoteException, OperationApplicationException; - public Bundle call(String callingPkg, String method, String arg, Bundle extras) + public Bundle call( + String callingPkg, String method, @Nullable String arg, @Nullable Bundle extras) throws RemoteException; public ICancellationSignal createCancellationSignal() throws RemoteException; diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 2fe727c..f685475 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -23,6 +23,7 @@ import android.util.ArraySet; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import android.annotation.AnyRes; import android.annotation.IntDef; import android.annotation.SdkConstant; import android.annotation.SystemApi; @@ -762,11 +763,11 @@ public class Intent implements Parcelable, Cloneable { * identifier. * * @param context The context of the application. - * @param resourceId The resource idenfitier for the icon. + * @param resourceId The resource identifier for the icon. * @return A new ShortcutIconResource with the specified's context package name - * and icon resource idenfitier. + * and icon resource identifier.`` */ - public static ShortcutIconResource fromContext(Context context, int resourceId) { + public static ShortcutIconResource fromContext(Context context, @AnyRes int resourceId) { ShortcutIconResource icon = new ShortcutIconResource(); icon.packageName = context.getPackageName(); icon.resourceName = context.getResources().getResourceName(resourceId); @@ -1238,6 +1239,13 @@ public class Intent implements Parcelable, Cloneable { = "android.intent.extra.ASSIST_PACKAGE"; /** + * An optional field on {@link #ACTION_ASSIST} containing the uid of the current foreground + * application package at the time the assist was invoked. + */ + public static final String EXTRA_ASSIST_UID + = "android.intent.extra.ASSIST_UID"; + + /** * An optional field on {@link #ACTION_ASSIST} and containing additional contextual * information supplied by the current foreground app at the time of the assist request. * This is a {@link Bundle} of additional data. @@ -1760,6 +1768,7 @@ public class Intent implements Parcelable, Cloneable { * <p class="note">This is a protected intent that can only be sent * by the system. */ + @SystemApi @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_QUERY_PACKAGE_RESTART = "android.intent.action.QUERY_PACKAGE_RESTART"; /** @@ -2781,6 +2790,31 @@ public class Intent implements Parcelable, Cloneable { /** {@hide} */ public static final String ACTION_MASTER_CLEAR = "android.intent.action.MASTER_CLEAR"; + /** + * Broadcast action: report that a settings element is being restored from backup. The intent + * contains three extras: EXTRA_SETTING_NAME is a string naming the restored setting, + * EXTRA_SETTING_NEW_VALUE is the value being restored, and EXTRA_SETTING_PREVIOUS_VALUE + * is the value of that settings entry prior to the restore operation. All of these values are + * represented as strings. + * + * <p>This broadcast is sent only for settings provider entries known to require special handling + * around restore time. These entries are found in the BROADCAST_ON_RESTORE table within + * the provider's backup agent implementation. + * + * @see #EXTRA_SETTING_NAME + * @see #EXTRA_SETTING_PREVIOUS_VALUE + * @see #EXTRA_SETTING_NEW_VALUE + * {@hide} + */ + public static final String ACTION_SETTING_RESTORED = "android.os.action.SETTING_RESTORED"; + + /** {@hide} */ + public static final String EXTRA_SETTING_NAME = "setting_name"; + /** {@hide} */ + public static final String EXTRA_SETTING_PREVIOUS_VALUE = "previous_value"; + /** {@hide} */ + public static final String EXTRA_SETTING_NEW_VALUE = "new_value"; + // --------------------------------------------------------------------- // --------------------------------------------------------------------- // Standard intent categories (see addCategory()). @@ -2808,14 +2842,12 @@ public class Intent implements Parcelable, Cloneable { @SdkConstant(SdkConstantType.INTENT_CATEGORY) public static final String CATEGORY_BROWSABLE = "android.intent.category.BROWSABLE"; /** - * @hide * Categories for activities that can participate in voice interaction. * An activity that supports this category must be prepared to run with * no UI shown at all (though in some case it may have a UI shown), and * rely on {@link android.app.VoiceInteractor} to interact with the user. */ @SdkConstant(SdkConstantType.INTENT_CATEGORY) - @SystemApi public static final String CATEGORY_VOICE = "android.intent.category.VOICE"; /** * Set if the activity should be considered as an alternative action to @@ -3254,6 +3286,7 @@ public class Intent implements Parcelable, Cloneable { /** * @hide String array of package names. */ + @SystemApi public static final String EXTRA_PACKAGES = "android.intent.extra.PACKAGES"; /** @@ -5385,6 +5418,16 @@ public class Intent implements Parcelable, Cloneable { } /** + * Filter extras to only basic types. + * @hide + */ + public void removeUnsafeExtras() { + if (mExtras != null) { + mExtras.filterValues(); + } + } + + /** * Retrieve any special flags associated with this intent. You will * normally just set them with {@link #setFlags} and let the system * take the appropriate action with them. diff --git a/core/java/android/content/RestrictionEntry.java b/core/java/android/content/RestrictionEntry.java index 5341ea8..6d79626 100644 --- a/core/java/android/content/RestrictionEntry.java +++ b/core/java/android/content/RestrictionEntry.java @@ -16,6 +16,7 @@ package android.content; +import android.annotation.ArrayRes; import android.os.Parcel; import android.os.Parcelable; @@ -277,7 +278,7 @@ public class RestrictionEntry implements Parcelable { * @param stringArrayResId the resource id for a string array containing the possible values. * @see #setChoiceValues(String[]) */ - public void setChoiceValues(Context context, int stringArrayResId) { + public void setChoiceValues(Context context, @ArrayRes int stringArrayResId) { mChoiceValues = context.getResources().getStringArray(stringArrayResId); } @@ -307,7 +308,7 @@ public class RestrictionEntry implements Parcelable { * @param context the application context, used for retrieving the resources. * @param stringArrayResId the resource id of a string array containing the possible entries. */ - public void setChoiceEntries(Context context, int stringArrayResId) { + public void setChoiceEntries(Context context, @ArrayRes int stringArrayResId) { mChoiceEntries = context.getResources().getStringArray(stringArrayResId); } diff --git a/core/java/android/content/SharedPreferences.java b/core/java/android/content/SharedPreferences.java index 462f473..1d16516 100644 --- a/core/java/android/content/SharedPreferences.java +++ b/core/java/android/content/SharedPreferences.java @@ -16,6 +16,8 @@ package android.content; +import android.annotation.Nullable; + import java.util.Map; import java.util.Set; @@ -76,7 +78,7 @@ public interface SharedPreferences { * @return Returns a reference to the same Editor object, so you can * chain put calls together. */ - Editor putString(String key, String value); + Editor putString(String key, @Nullable String value); /** * Set a set of String values in the preferences editor, to be written @@ -89,7 +91,7 @@ public interface SharedPreferences { * @return Returns a reference to the same Editor object, so you can * chain put calls together. */ - Editor putStringSet(String key, Set<String> values); + Editor putStringSet(String key, @Nullable Set<String> values); /** * Set an int value in the preferences editor, to be written back once @@ -252,7 +254,8 @@ public interface SharedPreferences { * * @throws ClassCastException */ - String getString(String key, String defValue); + @Nullable + String getString(String key, @Nullable String defValue); /** * Retrieve a set of String values from the preferences. @@ -270,7 +273,8 @@ public interface SharedPreferences { * * @throws ClassCastException */ - Set<String> getStringSet(String key, Set<String> defValues); + @Nullable + Set<String> getStringSet(String key, @Nullable Set<String> defValues); /** * Retrieve an int value from the preferences. diff --git a/core/java/android/content/UndoManager.java b/core/java/android/content/UndoManager.java index e9ec5a4..46c2c6e 100644 --- a/core/java/android/content/UndoManager.java +++ b/core/java/android/content/UndoManager.java @@ -20,9 +20,9 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.ParcelableParcel; import android.text.TextUtils; +import android.util.ArrayMap; import java.util.ArrayList; -import java.util.HashMap; /** * Top-level class for managing and interacting with the global undo state for @@ -54,7 +54,9 @@ import java.util.HashMap; * @hide */ public class UndoManager { - private final HashMap<String, UndoOwner> mOwners = new HashMap<String, UndoOwner>(); + // The common case is a single undo owner (e.g. for a TextView), so default to that capacity. + private final ArrayMap<String, UndoOwner> mOwners = + new ArrayMap<String, UndoOwner>(1 /* capacity */); private final ArrayList<UndoState> mUndos = new ArrayList<UndoState>(); private final ArrayList<UndoState> mRedos = new ArrayList<UndoState>(); private int mUpdateCount; @@ -103,8 +105,7 @@ public class UndoManager { return owner; } - owner = new UndoOwner(tag); - owner.mManager = this; + owner = new UndoOwner(tag, this); owner.mData = data; mOwners.put(tag, owner); return owner; @@ -114,20 +115,17 @@ public class UndoManager { // XXX need to figure out how to prune. if (false) { mOwners.remove(owner.mTag); - owner.mManager = null; } } /** - * Flatten the current undo state into a Parcelable object, which can later be restored - * with {@link #restoreInstanceState(android.os.Parcelable)}. + * Flatten the current undo state into a Parcel object, which can later be restored + * with {@link #restoreInstanceState(android.os.Parcel, java.lang.ClassLoader)}. */ - public Parcelable saveInstanceState() { + public void saveInstanceState(Parcel p) { if (mUpdateCount > 0) { throw new IllegalStateException("Can't save state while updating"); } - ParcelableParcel pp = new ParcelableParcel(getClass().getClassLoader()); - Parcel p = pp.getParcel(); mStateSeq++; if (mStateSeq <= 0) { mStateSeq = 0; @@ -151,7 +149,6 @@ public class UndoManager { mRedos.get(i).writeToParcel(p); } p.writeInt(0); - return pp; } void saveOwner(UndoOwner owner, Parcel out) { @@ -162,31 +159,30 @@ public class UndoManager { owner.mSavedIdx = mNextSavedIdx; out.writeInt(owner.mSavedIdx); out.writeString(owner.mTag); + out.writeInt(owner.mOpCount); mNextSavedIdx++; } } /** - * Restore an undo state previously created with {@link #saveInstanceState()}. This will - * restore the UndoManager's state to almost exactly what it was at the point it had + * Restore an undo state previously created with {@link #saveInstanceState(Parcel)}. This + * will restore the UndoManager's state to almost exactly what it was at the point it had * been previously saved; the only information not restored is the data object * associated with each {@link UndoOwner}, which requires separate calls to * {@link #getOwner(String, Object)} to re-associate the owner with its data. */ - public void restoreInstanceState(Parcelable state) { + public void restoreInstanceState(Parcel p, ClassLoader loader) { if (mUpdateCount > 0) { throw new IllegalStateException("Can't save state while updating"); } forgetUndos(null, -1); forgetRedos(null, -1); - ParcelableParcel pp = (ParcelableParcel)state; - Parcel p = pp.getParcel(); mHistorySize = p.readInt(); mStateOwners = new UndoOwner[p.readInt()]; int stype; while ((stype=p.readInt()) != 0) { - UndoState ustate = new UndoState(this, p, pp.getClassLoader()); + UndoState ustate = new UndoState(this, p, loader); if (stype == 1) { mUndos.add(0, ustate); } else { @@ -200,7 +196,9 @@ public class UndoManager { UndoOwner owner = mStateOwners[idx]; if (owner == null) { String tag = in.readString(); - owner = new UndoOwner(tag); + int opCount = in.readInt(); + owner = new UndoOwner(tag, this); + owner.mOpCount = opCount; mStateOwners[idx] = owner; mOwners.put(tag, owner); } @@ -308,12 +306,15 @@ public class UndoManager { } int removed = 0; - for (int i=0; i<mUndos.size() && removed < count; i++) { + int i = 0; + while (i < mUndos.size() && removed < count) { UndoState state = mUndos.get(i); if (count > 0 && matchOwners(state, owners)) { state.destroy(); mUndos.remove(i); removed++; + } else { + i++; } } @@ -326,12 +327,15 @@ public class UndoManager { } int removed = 0; - for (int i=0; i<mRedos.size() && removed < count; i++) { + int i = 0; + while (i < mRedos.size() && removed < count) { UndoState state = mRedos.get(i); if (count > 0 && matchOwners(state, owners)) { state.destroy(); mRedos.remove(i); removed++; + } else { + i++; } } diff --git a/core/java/android/content/UndoOwner.java b/core/java/android/content/UndoOwner.java index d0cdc95..fd257ab 100644 --- a/core/java/android/content/UndoOwner.java +++ b/core/java/android/content/UndoOwner.java @@ -23,8 +23,8 @@ package android.content; */ public class UndoOwner { final String mTag; + final UndoManager mManager; - UndoManager mManager; Object mData; int mOpCount; @@ -32,8 +32,15 @@ public class UndoOwner { int mStateSeq; int mSavedIdx; - UndoOwner(String tag) { + UndoOwner(String tag, UndoManager manager) { + if (tag == null) { + throw new NullPointerException("tag can't be null"); + } + if (manager == null) { + throw new NullPointerException("manager can't be null"); + } mTag = tag; + mManager = manager; } /** @@ -54,4 +61,15 @@ public class UndoOwner { public Object getData() { return mData; } + + @Override + public String toString() { + return "UndoOwner:[mTag=" + mTag + + " mManager=" + mManager + + " mData=" + mData + + " mData=" + mData + + " mOpCount=" + mOpCount + + " mStateSeq=" + mStateSeq + + " mSavedIdx=" + mSavedIdx + "]"; + } } diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index 641f843..4723c0d 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -641,6 +641,13 @@ public class ActivityInfo extends ComponentInfo */ public String parentActivityName; + /** + * Value indicating if the activity is resizeable to any dimension. + * See {@link android.R.attr#resizeableActivity}. + * @hide + */ + public boolean resizeable; + public ActivityInfo() { } @@ -702,6 +709,7 @@ public class ActivityInfo extends ComponentInfo if (uiOptions != 0) { pw.println(prefix + " uiOptions=0x" + Integer.toHexString(uiOptions)); } + pw.println(prefix + "resizeable=" + resizeable); super.dumpBack(pw, prefix); } @@ -730,6 +738,7 @@ public class ActivityInfo extends ComponentInfo dest.writeString(parentActivityName); dest.writeInt(persistableMode); dest.writeInt(maxRecents); + dest.writeInt(resizeable ? 1 : 0); } public static final Parcelable.Creator<ActivityInfo> CREATOR @@ -757,5 +766,6 @@ public class ActivityInfo extends ComponentInfo parentActivityName = source.readString(); persistableMode = source.readInt(); maxRecents = source.readInt(); + resizeable = (source.readInt() == 1); } } diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index 05c19db..29befc8 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -610,6 +610,11 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { */ public int installLocation = PackageInfo.INSTALL_LOCATION_UNSPECIFIED; + /** + * True when the application's rendering should be hardware accelerated. + */ + public boolean hardwareAccelerated; + public void dump(Printer pw, String prefix) { super.dumpFront(pw, prefix); if (className != null) { @@ -649,6 +654,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { } pw.println(prefix + "enabled=" + enabled + " targetSdkVersion=" + targetSdkVersion + " versionCode=" + versionCode); + pw.println(prefix + "hardwareAccelerated=" + hardwareAccelerated); if (manageSpaceActivityName != null) { pw.println(prefix + "manageSpaceActivityName="+manageSpaceActivityName); } @@ -734,6 +740,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { descriptionRes = orig.descriptionRes; uiOptions = orig.uiOptions; backupAgentName = orig.backupAgentName; + hardwareAccelerated = orig.hardwareAccelerated; } @@ -785,6 +792,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { dest.writeString(backupAgentName); dest.writeInt(descriptionRes); dest.writeInt(uiOptions); + dest.writeInt(hardwareAccelerated ? 1 : 0); } public static final Parcelable.Creator<ApplicationInfo> CREATOR @@ -835,6 +843,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { backupAgentName = source.readString(); descriptionRes = source.readInt(); uiOptions = source.readInt(); + hardwareAccelerated = source.readInt() != 0; } /** diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index b518498..3e5d362 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -261,9 +261,9 @@ interface IPackageManager { void clearPackagePersistentPreferredActivities(String packageName, int userId); void addCrossProfileIntentFilter(in IntentFilter intentFilter, String ownerPackage, - int ownerUserId, int sourceUserId, int targetUserId, int flags); + int sourceUserId, int targetUserId, int flags); - void clearCrossProfileIntentFilters(int sourceUserId, String ownerPackage, int ownerUserId); + void clearCrossProfileIntentFilters(int sourceUserId, String ownerPackage); /** * Report the set of 'Home' activity candidates, plus (if any) which of them diff --git a/core/java/android/content/pm/LauncherActivityInfo.java b/core/java/android/content/pm/LauncherActivityInfo.java index ee23fcd..87b97aa 100644 --- a/core/java/android/content/pm/LauncherActivityInfo.java +++ b/core/java/android/content/pm/LauncherActivityInfo.java @@ -39,6 +39,7 @@ public class LauncherActivityInfo { private ActivityInfo mActivityInfo; private ComponentName mComponentName; + private ResolveInfo mResolveInfo; private UserHandle mUser; private long mFirstInstallTime; @@ -52,6 +53,7 @@ public class LauncherActivityInfo { LauncherActivityInfo(Context context, ResolveInfo info, UserHandle user, long firstInstallTime) { this(context); + mResolveInfo = info; mActivityInfo = info.activityInfo; mComponentName = LauncherApps.getComponentName(info); mUser = user; @@ -92,7 +94,7 @@ public class LauncherActivityInfo { * @return The label for the activity. */ public CharSequence getLabel() { - return mActivityInfo.loadLabel(mPm); + return mResolveInfo.loadLabel(mPm); } /** @@ -104,8 +106,22 @@ public class LauncherActivityInfo { * @return The drawable associated with the activity */ public Drawable getIcon(int density) { - // TODO: Use density - return mActivityInfo.loadIcon(mPm); + int iconRes = mResolveInfo.getIconResource(); + Resources resources = null; + Drawable icon = null; + // Get the preferred density icon from the app's resources + if (density != 0 && iconRes != 0) { + try { + resources = mPm.getResourcesForApplication(mActivityInfo.applicationInfo); + icon = resources.getDrawableForDensity(iconRes, density); + } catch (NameNotFoundException | Resources.NotFoundException exc) { + } + } + // Get the default density icon + if (icon == null) { + icon = mResolveInfo.loadIcon(mPm); + } + return icon; } /** @@ -151,23 +167,7 @@ public class LauncherActivityInfo { * @return A badged icon for the activity. */ public Drawable getBadgedIcon(int density) { - int iconRes = mActivityInfo.getIconResource(); - Resources resources = null; - Drawable originalIcon = null; - try { - resources = mPm.getResourcesForApplication(mActivityInfo.applicationInfo); - try { - if (density != 0) { - originalIcon = resources.getDrawableForDensity(iconRes, density); - } - } catch (Resources.NotFoundException e) { - } - } catch (NameNotFoundException nnfe) { - } - - if (originalIcon == null) { - originalIcon = mActivityInfo.loadIcon(mPm); - } + Drawable originalIcon = getIcon(density); if (originalIcon instanceof BitmapDrawable) { return mPm.getUserBadgedIcon(originalIcon, mUser); diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java index c164340..c81517a 100644 --- a/core/java/android/content/pm/LauncherApps.java +++ b/core/java/android/content/pm/LauncherApps.java @@ -16,7 +16,6 @@ package android.content.pm; -import android.app.AppGlobals; import android.content.ComponentName; import android.content.Context; import android.content.Intent; diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index e9f7c50..3da57cb 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -16,11 +16,15 @@ package android.content.pm; +import android.annotation.CheckResult; +import android.annotation.DrawableRes; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; +import android.annotation.StringRes; import android.annotation.SystemApi; +import android.annotation.XmlRes; import android.app.PackageDeleteObserver; import android.app.PackageInstallObserver; import android.app.admin.DevicePolicyManager; @@ -2118,6 +2122,7 @@ public abstract class PackageManager { * @see #PERMISSION_GRANTED * @see #PERMISSION_DENIED */ + @CheckResult public abstract int checkPermission(String permName, String pkgName); /** @@ -2245,6 +2250,7 @@ public abstract class PackageManager { * @see #SIGNATURE_NO_MATCH * @see #SIGNATURE_UNKNOWN_PACKAGE */ + @CheckResult public abstract int checkSignatures(String pkg1, String pkg2); /** @@ -2267,6 +2273,7 @@ public abstract class PackageManager { * @see #SIGNATURE_NO_MATCH * @see #SIGNATURE_UNKNOWN_PACKAGE */ + @CheckResult public abstract int checkSignatures(int uid1, int uid2); /** @@ -2710,7 +2717,7 @@ public abstract class PackageManager { * @return Returns a Drawable holding the requested image. Returns null if * an image could not be found for any reason. */ - public abstract Drawable getDrawable(String packageName, int resid, + public abstract Drawable getDrawable(String packageName, @DrawableRes int resid, ApplicationInfo appInfo); /** @@ -3012,7 +3019,7 @@ public abstract class PackageManager { * @return Returns a CharSequence holding the requested text. Returns null * if the text could not be found for any reason. */ - public abstract CharSequence getText(String packageName, int resid, + public abstract CharSequence getText(String packageName, @StringRes int resid, ApplicationInfo appInfo); /** @@ -3031,7 +3038,7 @@ public abstract class PackageManager { * data. Returns null if the xml resource could not be found for any * reason. */ - public abstract XmlResourceParser getXml(String packageName, int resid, + public abstract XmlResourceParser getXml(String packageName, @XmlRes int resid, ApplicationInfo appInfo); /** @@ -3685,11 +3692,11 @@ public abstract class PackageManager { * {@link #addPreferredActivity}, that are * currently registered with the system. * - * @param outFilters A list in which to place the filters of all of the - * preferred activities, or null for none. - * @param outActivities A list in which to place the component names of - * all of the preferred activities, or null for none. - * @param packageName An option package in which you would like to limit + * @param outFilters A required list in which to place the filters of all of the + * preferred activities. + * @param outActivities A required list in which to place the component names of + * all of the preferred activities. + * @param packageName An optional package in which you would like to limit * the list. If null, all activities will be returned; if non-null, only * those activities in the given package are returned. * @@ -3697,8 +3704,8 @@ public abstract class PackageManager { * (the number of distinct IntentFilter records, not the number of unique * activity components) that were found. */ - public abstract int getPreferredActivities(List<IntentFilter> outFilters, - List<ComponentName> outActivities, String packageName); + public abstract int getPreferredActivities(@NonNull List<IntentFilter> outFilters, + @NonNull List<ComponentName> outActivities, String packageName); /** * Ask for the set of available 'home' activities and the current explicit diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index 4952ba1..c443ff3 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -49,6 +49,7 @@ import android.util.Pair; import android.util.Slog; import android.util.TypedValue; +import com.android.internal.R; import com.android.internal.util.ArrayUtils; import com.android.internal.util.XmlUtils; @@ -2518,6 +2519,7 @@ public class PackageParser { owner.baseHardwareAccelerated = sa.getBoolean( com.android.internal.R.styleable.AndroidManifestApplication_hardwareAccelerated, owner.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.ICE_CREAM_SANDWICH); + ai.hardwareAccelerated = owner.baseHardwareAccelerated; if (sa.getBoolean( com.android.internal.R.styleable.AndroidManifestApplication_hasCode, @@ -2766,9 +2768,17 @@ public class PackageParser { } } + addSharedLibrariesForBackwardCompatibility(owner); + return true; } + private static void addSharedLibrariesForBackwardCompatibility(Package owner) { + if (owner.applicationInfo.targetSdkVersion <= Build.VERSION_CODES.LOLLIPOP_MR1) { + owner.usesLibraries = ArrayUtils.add(owner.usesLibraries, "org.apache.http.legacy"); + } + } + /** * Parse the {@code application} XML tree at the current parse location in a * <em>split APK</em> manifest. @@ -2953,20 +2963,19 @@ public class PackageParser { XmlPullParser parser, AttributeSet attrs, int flags, String[] outError, boolean receiver, boolean hardwareAccelerated) throws XmlPullParserException, IOException { - TypedArray sa = res.obtainAttributes(attrs, - com.android.internal.R.styleable.AndroidManifestActivity); + TypedArray sa = res.obtainAttributes(attrs, R.styleable.AndroidManifestActivity); if (mParseActivityArgs == null) { mParseActivityArgs = new ParseComponentArgs(owner, outError, - com.android.internal.R.styleable.AndroidManifestActivity_name, - com.android.internal.R.styleable.AndroidManifestActivity_label, - com.android.internal.R.styleable.AndroidManifestActivity_icon, - com.android.internal.R.styleable.AndroidManifestActivity_logo, - com.android.internal.R.styleable.AndroidManifestActivity_banner, + R.styleable.AndroidManifestActivity_name, + R.styleable.AndroidManifestActivity_label, + R.styleable.AndroidManifestActivity_icon, + R.styleable.AndroidManifestActivity_logo, + R.styleable.AndroidManifestActivity_banner, mSeparateProcesses, - com.android.internal.R.styleable.AndroidManifestActivity_process, - com.android.internal.R.styleable.AndroidManifestActivity_description, - com.android.internal.R.styleable.AndroidManifestActivity_enabled); + R.styleable.AndroidManifestActivity_process, + R.styleable.AndroidManifestActivity_description, + R.styleable.AndroidManifestActivity_enabled); } mParseActivityArgs.tag = receiver ? "<receiver>" : "<activity>"; @@ -2979,22 +2988,18 @@ public class PackageParser { return null; } - boolean setExported = sa.hasValue( - com.android.internal.R.styleable.AndroidManifestActivity_exported); + boolean setExported = sa.hasValue(R.styleable.AndroidManifestActivity_exported); if (setExported) { - a.info.exported = sa.getBoolean( - com.android.internal.R.styleable.AndroidManifestActivity_exported, false); + a.info.exported = sa.getBoolean(R.styleable.AndroidManifestActivity_exported, false); } - a.info.theme = sa.getResourceId( - com.android.internal.R.styleable.AndroidManifestActivity_theme, 0); + a.info.theme = sa.getResourceId(R.styleable.AndroidManifestActivity_theme, 0); - a.info.uiOptions = sa.getInt( - com.android.internal.R.styleable.AndroidManifestActivity_uiOptions, + a.info.uiOptions = sa.getInt(R.styleable.AndroidManifestActivity_uiOptions, a.info.applicationInfo.uiOptions); String parentName = sa.getNonConfigurationString( - com.android.internal.R.styleable.AndroidManifestActivity_parentActivityName, + R.styleable.AndroidManifestActivity_parentActivityName, Configuration.NATIVE_CONFIG_VERSION); if (parentName != null) { String parentClassName = buildClassName(a.info.packageName, parentName, outError); @@ -3008,8 +3013,7 @@ public class PackageParser { } String str; - str = sa.getNonConfigurationString( - com.android.internal.R.styleable.AndroidManifestActivity_permission, 0); + str = sa.getNonConfigurationString(R.styleable.AndroidManifestActivity_permission, 0); if (str == null) { a.info.permission = owner.applicationInfo.permission; } else { @@ -3017,140 +3021,116 @@ public class PackageParser { } str = sa.getNonConfigurationString( - com.android.internal.R.styleable.AndroidManifestActivity_taskAffinity, + R.styleable.AndroidManifestActivity_taskAffinity, Configuration.NATIVE_CONFIG_VERSION); a.info.taskAffinity = buildTaskAffinityName(owner.applicationInfo.packageName, owner.applicationInfo.taskAffinity, str, outError); a.info.flags = 0; if (sa.getBoolean( - com.android.internal.R.styleable.AndroidManifestActivity_multiprocess, - false)) { + R.styleable.AndroidManifestActivity_multiprocess, false)) { a.info.flags |= ActivityInfo.FLAG_MULTIPROCESS; } - if (sa.getBoolean( - com.android.internal.R.styleable.AndroidManifestActivity_finishOnTaskLaunch, - false)) { + if (sa.getBoolean(R.styleable.AndroidManifestActivity_finishOnTaskLaunch, false)) { a.info.flags |= ActivityInfo.FLAG_FINISH_ON_TASK_LAUNCH; } - if (sa.getBoolean( - com.android.internal.R.styleable.AndroidManifestActivity_clearTaskOnLaunch, - false)) { + if (sa.getBoolean(R.styleable.AndroidManifestActivity_clearTaskOnLaunch, false)) { a.info.flags |= ActivityInfo.FLAG_CLEAR_TASK_ON_LAUNCH; } - if (sa.getBoolean( - com.android.internal.R.styleable.AndroidManifestActivity_noHistory, - false)) { + if (sa.getBoolean(R.styleable.AndroidManifestActivity_noHistory, false)) { a.info.flags |= ActivityInfo.FLAG_NO_HISTORY; } - if (sa.getBoolean( - com.android.internal.R.styleable.AndroidManifestActivity_alwaysRetainTaskState, - false)) { + if (sa.getBoolean(R.styleable.AndroidManifestActivity_alwaysRetainTaskState, false)) { a.info.flags |= ActivityInfo.FLAG_ALWAYS_RETAIN_TASK_STATE; } - if (sa.getBoolean( - com.android.internal.R.styleable.AndroidManifestActivity_stateNotNeeded, - false)) { + if (sa.getBoolean(R.styleable.AndroidManifestActivity_stateNotNeeded, false)) { a.info.flags |= ActivityInfo.FLAG_STATE_NOT_NEEDED; } - if (sa.getBoolean( - com.android.internal.R.styleable.AndroidManifestActivity_excludeFromRecents, - false)) { + if (sa.getBoolean(R.styleable.AndroidManifestActivity_excludeFromRecents, false)) { a.info.flags |= ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS; } - if (sa.getBoolean( - com.android.internal.R.styleable.AndroidManifestActivity_allowTaskReparenting, + if (sa.getBoolean(R.styleable.AndroidManifestActivity_allowTaskReparenting, (owner.applicationInfo.flags&ApplicationInfo.FLAG_ALLOW_TASK_REPARENTING) != 0)) { a.info.flags |= ActivityInfo.FLAG_ALLOW_TASK_REPARENTING; } - if (sa.getBoolean( - com.android.internal.R.styleable.AndroidManifestActivity_finishOnCloseSystemDialogs, - false)) { + if (sa.getBoolean(R.styleable.AndroidManifestActivity_finishOnCloseSystemDialogs, false)) { a.info.flags |= ActivityInfo.FLAG_FINISH_ON_CLOSE_SYSTEM_DIALOGS; } - if (sa.getBoolean( - com.android.internal.R.styleable.AndroidManifestActivity_showOnLockScreen, - false)) { + if (sa.getBoolean(R.styleable.AndroidManifestActivity_showOnLockScreen, false)) { a.info.flags |= ActivityInfo.FLAG_SHOW_ON_LOCK_SCREEN; } - if (sa.getBoolean( - com.android.internal.R.styleable.AndroidManifestActivity_immersive, - false)) { + if (sa.getBoolean(R.styleable.AndroidManifestActivity_immersive, false)) { a.info.flags |= ActivityInfo.FLAG_IMMERSIVE; } + if (sa.getBoolean(R.styleable.AndroidManifestActivity_primaryUserOnly, false)) { + a.info.flags |= ActivityInfo.FLAG_PRIMARY_USER_ONLY; + } + if (!receiver) { - if (sa.getBoolean( - com.android.internal.R.styleable.AndroidManifestActivity_hardwareAccelerated, + if (sa.getBoolean(R.styleable.AndroidManifestActivity_hardwareAccelerated, hardwareAccelerated)) { a.info.flags |= ActivityInfo.FLAG_HARDWARE_ACCELERATED; } a.info.launchMode = sa.getInt( - com.android.internal.R.styleable.AndroidManifestActivity_launchMode, - ActivityInfo.LAUNCH_MULTIPLE); + R.styleable.AndroidManifestActivity_launchMode, ActivityInfo.LAUNCH_MULTIPLE); a.info.documentLaunchMode = sa.getInt( - com.android.internal.R.styleable.AndroidManifestActivity_documentLaunchMode, + R.styleable.AndroidManifestActivity_documentLaunchMode, ActivityInfo.DOCUMENT_LAUNCH_NONE); a.info.maxRecents = sa.getInt( - com.android.internal.R.styleable.AndroidManifestActivity_maxRecents, + R.styleable.AndroidManifestActivity_maxRecents, ActivityManager.getDefaultAppRecentsLimitStatic()); - a.info.screenOrientation = sa.getInt( - com.android.internal.R.styleable.AndroidManifestActivity_screenOrientation, - ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); - a.info.configChanges = sa.getInt( - com.android.internal.R.styleable.AndroidManifestActivity_configChanges, - 0); + a.info.configChanges = sa.getInt(R.styleable.AndroidManifestActivity_configChanges, 0); a.info.softInputMode = sa.getInt( - com.android.internal.R.styleable.AndroidManifestActivity_windowSoftInputMode, - 0); + R.styleable.AndroidManifestActivity_windowSoftInputMode, 0); a.info.persistableMode = sa.getInteger( - com.android.internal.R.styleable.AndroidManifestActivity_persistableMode, + R.styleable.AndroidManifestActivity_persistableMode, ActivityInfo.PERSIST_ROOT_ONLY); - if (sa.getBoolean( - com.android.internal.R.styleable.AndroidManifestActivity_allowEmbedded, - false)) { + if (sa.getBoolean(R.styleable.AndroidManifestActivity_allowEmbedded, false)) { a.info.flags |= ActivityInfo.FLAG_ALLOW_EMBEDDED; } - if (sa.getBoolean( - com.android.internal.R.styleable.AndroidManifestActivity_autoRemoveFromRecents, - false)) { + if (sa.getBoolean(R.styleable.AndroidManifestActivity_autoRemoveFromRecents, false)) { a.info.flags |= ActivityInfo.FLAG_AUTO_REMOVE_FROM_RECENTS; } - if (sa.getBoolean( - com.android.internal.R.styleable.AndroidManifestActivity_relinquishTaskIdentity, - false)) { + if (sa.getBoolean(R.styleable.AndroidManifestActivity_relinquishTaskIdentity, false)) { a.info.flags |= ActivityInfo.FLAG_RELINQUISH_TASK_IDENTITY; } - if (sa.getBoolean( - com.android.internal.R.styleable.AndroidManifestActivity_resumeWhilePausing, - false)) { + if (sa.getBoolean(R.styleable.AndroidManifestActivity_resumeWhilePausing, false)) { a.info.flags |= ActivityInfo.FLAG_RESUME_WHILE_PAUSING; } + + a.info.resizeable = sa.getBoolean( + R.styleable.AndroidManifestActivity_resizeableActivity, + owner.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.MNC); + if (a.info.resizeable) { + // Fixed screen orientation isn't supported with resizeable activities. + a.info.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; + } else { + a.info.screenOrientation = sa.getInt( + R.styleable.AndroidManifestActivity_screenOrientation, + ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + } } else { a.info.launchMode = ActivityInfo.LAUNCH_MULTIPLE; a.info.configChanges = 0; - } - if (receiver) { - if (sa.getBoolean( - com.android.internal.R.styleable.AndroidManifestActivity_singleUser, - false)) { + if (sa.getBoolean(R.styleable.AndroidManifestActivity_singleUser, false)) { a.info.flags |= ActivityInfo.FLAG_SINGLE_USER; if (a.info.exported && (flags & PARSE_IS_PRIVILEGED) == 0) { Slog.w(TAG, "Activity exported request ignored due to singleUser: " @@ -3160,11 +3140,6 @@ public class PackageParser { setExported = true; } } - if (sa.getBoolean( - com.android.internal.R.styleable.AndroidManifestActivity_primaryUserOnly, - false)) { - a.info.flags |= ActivityInfo.FLAG_PRIMARY_USER_ONLY; - } } sa.recycle(); diff --git a/core/java/android/content/pm/RegisteredServicesCache.java b/core/java/android/content/pm/RegisteredServicesCache.java index 391ef22..c2d2f65 100644 --- a/core/java/android/content/pm/RegisteredServicesCache.java +++ b/core/java/android/content/pm/RegisteredServicesCache.java @@ -27,6 +27,7 @@ import android.content.res.XmlResourceParser; import android.os.Environment; import android.os.Handler; import android.os.UserHandle; +import android.os.UserManager; import android.util.AtomicFile; import android.util.AttributeSet; import android.util.Log; @@ -35,6 +36,7 @@ import android.util.SparseArray; import android.util.Xml; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.FastXmlSerializer; import com.google.android.collect.Lists; @@ -46,9 +48,9 @@ import org.xmlpull.v1.XmlSerializer; import java.io.File; import java.io.FileDescriptor; -import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collection; @@ -56,6 +58,8 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import libcore.io.IoUtils; + /** * Cache of registered services. This cache is lazily built by interrogating * {@link PackageManager} on a per-user basis. It's updated as packages are @@ -71,6 +75,7 @@ import java.util.Map; public abstract class RegisteredServicesCache<V> { private static final String TAG = "PackageManager"; private static final boolean DEBUG = false; + protected static final String REGISTERED_SERVICES_DIR = "registered_services"; public final Context mContext; private final String mInterfaceName; @@ -81,32 +86,54 @@ public abstract class RegisteredServicesCache<V> { private final Object mServicesLock = new Object(); @GuardedBy("mServicesLock") - private boolean mPersistentServicesFileDidNotExist; - @GuardedBy("mServicesLock") private final SparseArray<UserServices<V>> mUserServices = new SparseArray<UserServices<V>>(2); private static class UserServices<V> { @GuardedBy("mServicesLock") - public final Map<V, Integer> persistentServices = Maps.newHashMap(); + final Map<V, Integer> persistentServices = Maps.newHashMap(); + @GuardedBy("mServicesLock") + Map<V, ServiceInfo<V>> services = null; @GuardedBy("mServicesLock") - public Map<V, ServiceInfo<V>> services = null; + boolean mPersistentServicesFileDidNotExist = true; } + @GuardedBy("mServicesLock") private UserServices<V> findOrCreateUserLocked(int userId) { + return findOrCreateUserLocked(userId, true); + } + + @GuardedBy("mServicesLock") + private UserServices<V> findOrCreateUserLocked(int userId, boolean loadFromFileIfNew) { UserServices<V> services = mUserServices.get(userId); if (services == null) { services = new UserServices<V>(); mUserServices.put(userId, services); + if (loadFromFileIfNew && mSerializerAndParser != null) { + // Check if user exists and try loading data from file + // clear existing data if there was an error during migration + UserInfo user = getUser(userId); + if (user != null) { + AtomicFile file = createFileForUser(user.id); + if (file.getBaseFile().exists()) { + if (DEBUG) { + Slog.i(TAG, String.format("Loading u%s data from %s", user.id, file)); + } + InputStream is = null; + try { + is = file.openRead(); + readPersistentServicesLocked(is); + } catch (Exception e) { + Log.w(TAG, "Error reading persistent services for user " + user.id, e); + } finally { + IoUtils.closeQuietly(is); + } + } + } + } } return services; } - /** - * This file contains the list of known services. We would like to maintain this forever - * so we store it as an XML file. - */ - private final AtomicFile mPersistentServicesFile; - // the listener and handler are synchronized on "this" and must be updated together private RegisteredServicesCacheListener<V> mListener; private Handler mHandler; @@ -119,13 +146,7 @@ public abstract class RegisteredServicesCache<V> { mAttributesName = attributeName; mSerializerAndParser = serializerAndParser; - File dataDir = Environment.getDataDirectory(); - File systemDir = new File(dataDir, "system"); - File syncDir = new File(systemDir, "registered_services"); - mPersistentServicesFile = new AtomicFile(new File(syncDir, interfaceName + ".xml")); - - // Load persisted services from disk - readPersistentServicesLocked(); + migrateIfNecessaryLocked(); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); @@ -139,6 +160,11 @@ public abstract class RegisteredServicesCache<V> { sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); mContext.registerReceiver(mExternalReceiver, sdFilter); + + // Register for user-related events + IntentFilter userFilter = new IntentFilter(); + sdFilter.addAction(Intent.ACTION_USER_REMOVED); + mContext.registerReceiver(mUserRemovedReceiver, userFilter); } private final void handlePackageEvent(Intent intent, int userId) { @@ -190,6 +216,17 @@ public abstract class RegisteredServicesCache<V> { } }; + private final BroadcastReceiver mUserRemovedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); + if (DEBUG) { + Slog.d(TAG, "u" + userId + " removed - cleaning up"); + } + onUserRemoved(userId); + } + }; + public void invalidateCache(int userId) { synchronized (mServicesLock) { final UserServices<V> user = findOrCreateUserLocked(userId); @@ -303,7 +340,8 @@ public abstract class RegisteredServicesCache<V> { } } - private boolean inSystemImage(int callerUid) { + @VisibleForTesting + protected boolean inSystemImage(int callerUid) { String[] packages = mContext.getPackageManager().getPackagesForUid(callerUid); for (String name : packages) { try { @@ -319,6 +357,13 @@ public abstract class RegisteredServicesCache<V> { return false; } + @VisibleForTesting + protected List<ResolveInfo> queryIntentServices(int userId) { + final PackageManager pm = mContext.getPackageManager(); + return pm.queryIntentServicesAsUser( + new Intent(mInterfaceName), PackageManager.GET_META_DATA, userId); + } + /** * Populate {@link UserServices#services} by scanning installed packages for * given {@link UserHandle}. @@ -331,10 +376,8 @@ public abstract class RegisteredServicesCache<V> { Slog.d(TAG, "generateServicesMap() for " + userId + ", changed UIDs = " + changedUids); } - final PackageManager pm = mContext.getPackageManager(); final ArrayList<ServiceInfo<V>> serviceInfos = new ArrayList<ServiceInfo<V>>(); - final List<ResolveInfo> resolveInfos = pm.queryIntentServicesAsUser( - new Intent(mInterfaceName), PackageManager.GET_META_DATA, userId); + final List<ResolveInfo> resolveInfos = queryIntentServices(userId); for (ResolveInfo resolveInfo : resolveInfos) { try { ServiceInfo<V> info = parseServiceInfo(resolveInfo); @@ -343,9 +386,7 @@ public abstract class RegisteredServicesCache<V> { continue; } serviceInfos.add(info); - } catch (XmlPullParserException e) { - Log.w(TAG, "Unable to load service info " + resolveInfo.toString(), e); - } catch (IOException e) { + } catch (XmlPullParserException|IOException e) { Log.w(TAG, "Unable to load service info " + resolveInfo.toString(), e); } } @@ -377,7 +418,7 @@ public abstract class RegisteredServicesCache<V> { changed = true; user.services.put(info.type, info); user.persistentServices.put(info.type, info.uid); - if (!(mPersistentServicesFileDidNotExist && firstScan)) { + if (!(user.mPersistentServicesFileDidNotExist && firstScan)) { notifyListener(info.type, userId, false /* removed */); } } else if (previousUid == info.uid) { @@ -447,7 +488,7 @@ public abstract class RegisteredServicesCache<V> { } } if (changed) { - writePersistentServicesLocked(); + writePersistentServicesLocked(user, userId); } } } @@ -481,7 +522,8 @@ public abstract class RegisteredServicesCache<V> { return false; } - private ServiceInfo<V> parseServiceInfo(ResolveInfo service) + @VisibleForTesting + protected ServiceInfo<V> parseServiceInfo(ResolveInfo service) throws XmlPullParserException, IOException { android.content.pm.ServiceInfo si = service.serviceInfo; ComponentName componentName = new ComponentName(si.packageName, si.name); @@ -528,93 +570,156 @@ public abstract class RegisteredServicesCache<V> { /** * Read all sync status back in to the initial engine state. */ - private void readPersistentServicesLocked() { - mUserServices.clear(); + private void readPersistentServicesLocked(InputStream is) + throws XmlPullParserException, IOException { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(is, null); + int eventType = parser.getEventType(); + while (eventType != XmlPullParser.START_TAG + && eventType != XmlPullParser.END_DOCUMENT) { + eventType = parser.next(); + } + String tagName = parser.getName(); + if ("services".equals(tagName)) { + eventType = parser.next(); + do { + if (eventType == XmlPullParser.START_TAG && parser.getDepth() == 2) { + tagName = parser.getName(); + if ("service".equals(tagName)) { + V service = mSerializerAndParser.createFromXml(parser); + if (service == null) { + break; + } + String uidString = parser.getAttributeValue(null, "uid"); + final int uid = Integer.parseInt(uidString); + final int userId = UserHandle.getUserId(uid); + final UserServices<V> user = findOrCreateUserLocked(userId, + false /*loadFromFileIfNew*/) ; + user.persistentServices.put(service, uid); + } + } + eventType = parser.next(); + } while (eventType != XmlPullParser.END_DOCUMENT); + } + } + + private void migrateIfNecessaryLocked() { if (mSerializerAndParser == null) { return; } - FileInputStream fis = null; - try { - mPersistentServicesFileDidNotExist = !mPersistentServicesFile.getBaseFile().exists(); - if (mPersistentServicesFileDidNotExist) { - return; - } - fis = mPersistentServicesFile.openRead(); - XmlPullParser parser = Xml.newPullParser(); - parser.setInput(fis, null); - int eventType = parser.getEventType(); - while (eventType != XmlPullParser.START_TAG - && eventType != XmlPullParser.END_DOCUMENT) { - eventType = parser.next(); - } - String tagName = parser.getName(); - if ("services".equals(tagName)) { - eventType = parser.next(); - do { - if (eventType == XmlPullParser.START_TAG && parser.getDepth() == 2) { - tagName = parser.getName(); - if ("service".equals(tagName)) { - V service = mSerializerAndParser.createFromXml(parser); - if (service == null) { - break; + File systemDir = new File(getDataDirectory(), "system"); + File syncDir = new File(systemDir, REGISTERED_SERVICES_DIR); + AtomicFile oldFile = new AtomicFile(new File(syncDir, mInterfaceName + ".xml")); + boolean oldFileExists = oldFile.getBaseFile().exists(); + + if (oldFileExists) { + File marker = new File(syncDir, mInterfaceName + ".xml.migrated"); + // if not migrated, perform the migration and add a marker + if (!marker.exists()) { + if (DEBUG) { + Slog.i(TAG, "Marker file " + marker + " does not exist - running migration"); + } + InputStream is = null; + try { + is = oldFile.openRead(); + mUserServices.clear(); + readPersistentServicesLocked(is); + } catch (Exception e) { + Log.w(TAG, "Error reading persistent services, starting from scratch", e); + } finally { + IoUtils.closeQuietly(is); + } + try { + for (UserInfo user : getUsers()) { + UserServices<V> userServices = mUserServices.get(user.id); + if (userServices != null) { + if (DEBUG) { + Slog.i(TAG, "Migrating u" + user.id + " services " + + userServices.persistentServices); } - String uidString = parser.getAttributeValue(null, "uid"); - final int uid = Integer.parseInt(uidString); - final int userId = UserHandle.getUserId(uid); - final UserServices<V> user = findOrCreateUserLocked(userId); - user.persistentServices.put(service, uid); + writePersistentServicesLocked(userServices, user.id); } } - eventType = parser.next(); - } while (eventType != XmlPullParser.END_DOCUMENT); - } - } catch (Exception e) { - Log.w(TAG, "Error reading persistent services, starting from scratch", e); - } finally { - if (fis != null) { - try { - fis.close(); - } catch (java.io.IOException e1) { + marker.createNewFile(); + } catch (Exception e) { + Log.w(TAG, "Migration failed", e); } + // Migration is complete and we don't need to keep data for all users anymore, + // It will be loaded from a new location when requested + mUserServices.clear(); } } } /** - * Write all sync status to the sync status file. + * Writes services of a specified user to the file. */ - private void writePersistentServicesLocked() { + private void writePersistentServicesLocked(UserServices<V> user, int userId) { if (mSerializerAndParser == null) { return; } + AtomicFile atomicFile = createFileForUser(userId); FileOutputStream fos = null; try { - fos = mPersistentServicesFile.startWrite(); + fos = atomicFile.startWrite(); XmlSerializer out = new FastXmlSerializer(); out.setOutput(fos, "utf-8"); out.startDocument(null, true); out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); out.startTag(null, "services"); - for (int i = 0; i < mUserServices.size(); i++) { - final UserServices<V> user = mUserServices.valueAt(i); - for (Map.Entry<V, Integer> service : user.persistentServices.entrySet()) { - out.startTag(null, "service"); - out.attribute(null, "uid", Integer.toString(service.getValue())); - mSerializerAndParser.writeAsXml(service.getKey(), out); - out.endTag(null, "service"); - } + for (Map.Entry<V, Integer> service : user.persistentServices.entrySet()) { + out.startTag(null, "service"); + out.attribute(null, "uid", Integer.toString(service.getValue())); + mSerializerAndParser.writeAsXml(service.getKey(), out); + out.endTag(null, "service"); } out.endTag(null, "services"); out.endDocument(); - mPersistentServicesFile.finishWrite(fos); - } catch (java.io.IOException e1) { + atomicFile.finishWrite(fos); + } catch (IOException e1) { Log.w(TAG, "Error writing accounts", e1); if (fos != null) { - mPersistentServicesFile.failWrite(fos); + atomicFile.failWrite(fos); } } } + @VisibleForTesting + protected void onUserRemoved(int userId) { + mUserServices.remove(userId); + } + + @VisibleForTesting + protected List<UserInfo> getUsers() { + return UserManager.get(mContext).getUsers(true); + } + + @VisibleForTesting + protected UserInfo getUser(int userId) { + return UserManager.get(mContext).getUserInfo(userId); + } + + private AtomicFile createFileForUser(int userId) { + File userDir = getUserSystemDirectory(userId); + File userFile = new File(userDir, REGISTERED_SERVICES_DIR + "/" + mInterfaceName + ".xml"); + return new AtomicFile(userFile); + } + + @VisibleForTesting + protected File getUserSystemDirectory(int userId) { + return Environment.getUserSystemDirectory(userId); + } + + @VisibleForTesting + protected File getDataDirectory() { + return Environment.getDataDirectory(); + } + + @VisibleForTesting + protected Map<V, Integer> getPersistentServices(int userId) { + return findOrCreateUserLocked(userId).persistentServices; + } + public abstract V parseServiceAttributes(Resources res, String packageName, AttributeSet attrs); } diff --git a/core/java/android/content/res/ColorStateList.java b/core/java/android/content/res/ColorStateList.java index 68a39d3..841b09d 100644 --- a/core/java/android/content/res/ColorStateList.java +++ b/core/java/android/content/res/ColorStateList.java @@ -16,8 +16,13 @@ package android.content.res; +import android.annotation.ColorInt; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.res.Resources.Theme; import android.graphics.Color; +import com.android.internal.R; import com.android.internal.util.ArrayUtils; import com.android.internal.util.GrowingArrayUtils; @@ -25,6 +30,7 @@ import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import android.util.AttributeSet; +import android.util.Log; import android.util.MathUtils; import android.util.SparseArray; import android.util.StateSet; @@ -64,71 +70,128 @@ import java.util.Arrays; * List Resource</a>.</p> */ public class ColorStateList implements Parcelable { - private int[][] mStateSpecs; // must be parallel to mColors - private int[] mColors; // must be parallel to mStateSpecs - private int mDefaultColor = 0xffff0000; - + private static final String TAG = "ColorStateList"; + private static final int DEFAULT_COLOR = Color.RED; private static final int[][] EMPTY = new int[][] { new int[0] }; private static final SparseArray<WeakReference<ColorStateList>> sCache = new SparseArray<WeakReference<ColorStateList>>(); - private ColorStateList() { } + private int[][] mThemeAttrs; + private int mChangingConfigurations; + + private int[][] mStateSpecs; + private int[] mColors; + private int mDefaultColor; + private boolean mIsOpaque; + + private ColorStateList() { + // Not publicly instantiable. + } /** * Creates a ColorStateList that returns the specified mapping from * states to colors. */ - public ColorStateList(int[][] states, int[] colors) { + public ColorStateList(int[][] states, @ColorInt int[] colors) { mStateSpecs = states; mColors = colors; - if (states.length > 0) { - mDefaultColor = colors[0]; - - for (int i = 0; i < states.length; i++) { - if (states[i].length == 0) { - mDefaultColor = colors[i]; - } - } - } + onColorsChanged(); } /** - * Creates or retrieves a ColorStateList that always returns a single color. + * @return A ColorStateList containing a single color. */ - public static ColorStateList valueOf(int color) { - // TODO: should we collect these eventually? + @NonNull + public static ColorStateList valueOf(@ColorInt int color) { synchronized (sCache) { - final WeakReference<ColorStateList> ref = sCache.get(color); + final int index = sCache.indexOfKey(color); + if (index >= 0) { + final ColorStateList cached = sCache.valueAt(index).get(); + if (cached != null) { + return cached; + } + + // Prune missing entry. + sCache.removeAt(index); + } - ColorStateList csl = ref != null ? ref.get() : null; - if (csl != null) { - return csl; + // Prune the cache before adding new items. + final int N = sCache.size(); + for (int i = N - 1; i >= 0; i--) { + if (sCache.valueAt(i).get() == null) { + sCache.removeAt(i); + } } - csl = new ColorStateList(EMPTY, new int[] { color }); + final ColorStateList csl = new ColorStateList(EMPTY, new int[] { color }); sCache.put(color, new WeakReference<ColorStateList>(csl)); return csl; } } /** - * Create a ColorStateList from an XML document, given a set of {@link Resources}. + * Creates a ColorStateList with the same properties as another + * ColorStateList. + * <p> + * The properties of the new ColorStateList can be modified without + * affecting the source ColorStateList. + * + * @param orig the source color state list */ + private ColorStateList(ColorStateList orig) { + if (orig != null) { + mStateSpecs = orig.mStateSpecs; + mDefaultColor = orig.mDefaultColor; + mIsOpaque = orig.mIsOpaque; + + // Deep copy, this may change due to theming. + mColors = orig.mColors.clone(); + } + } + + /** + * Creates a ColorStateList from an XML document. + * + * @param r Resources against which the ColorStateList should be inflated. + * @param parser Parser for the XML document defining the ColorStateList. + * @return A new color state list. + * + * @deprecated Use #createFromXml(Resources, XmlPullParser parser, Theme) + */ + @NonNull + @Deprecated public static ColorStateList createFromXml(Resources r, XmlPullParser parser) throws XmlPullParserException, IOException { + return createFromXml(r, parser, null); + } + + /** + * Creates a ColorStateList from an XML document using given a set of + * {@link Resources} and a {@link Theme}. + * + * @param r Resources against which the ColorStateList should be inflated. + * @param parser Parser for the XML document defining the ColorStateList. + * @param theme Optional theme to apply to the color state list, may be + * {@code null}. + * @return A new color state list. + */ + @NonNull + public static ColorStateList createFromXml(@NonNull Resources r, @NonNull XmlPullParser parser, + @Nullable Theme theme) throws XmlPullParserException, IOException { final AttributeSet attrs = Xml.asAttributeSet(parser); int type; - while ((type=parser.next()) != XmlPullParser.START_TAG + while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { + // Seek parser to start tag. } if (type != XmlPullParser.START_TAG) { throw new XmlPullParserException("No start tag found"); } - return createFromXmlInner(r, parser, attrs); + return createFromXmlInner(r, parser, attrs, theme); } /** @@ -136,28 +199,31 @@ public class ColorStateList implements Parcelable { * tag in an XML document, tries to create a ColorStateList from that tag. * * @throws XmlPullParserException if the current tag is not <selector> - * @return A color state list for the current tag. + * @return A new color state list for the current tag. */ - private static ColorStateList createFromXmlInner(Resources r, XmlPullParser parser, - AttributeSet attrs) throws XmlPullParserException, IOException { - final ColorStateList colorStateList; + @NonNull + private static ColorStateList createFromXmlInner(@NonNull Resources r, + @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme) + throws XmlPullParserException, IOException { final String name = parser.getName(); - if (name.equals("selector")) { - colorStateList = new ColorStateList(); - } else { + if (!name.equals("selector")) { throw new XmlPullParserException( - parser.getPositionDescription() + ": invalid drawable tag " + name); + parser.getPositionDescription() + ": invalid color state list tag " + name); } - colorStateList.inflate(r, parser, attrs); + final ColorStateList colorStateList = new ColorStateList(); + colorStateList.inflate(r, parser, attrs, theme); return colorStateList; } /** - * Creates a new ColorStateList that has the same states and - * colors as this one but where each color has the specified alpha value - * (0-255). + * Creates a new ColorStateList that has the same states and colors as this + * one but where each color has the specified alpha value (0-255). + * + * @param alpha The new alpha channel value (0-255). + * @return A new color state list. */ + @NonNull public ColorStateList withAlpha(int alpha) { final int[] colors = new int[mColors.length]; final int len = colors.length; @@ -171,88 +237,154 @@ public class ColorStateList implements Parcelable { /** * Fill in this object based on the contents of an XML "selector" element. */ - private void inflate(Resources r, XmlPullParser parser, AttributeSet attrs) + private void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, + @NonNull AttributeSet attrs, @Nullable Theme theme) throws XmlPullParserException, IOException { - int type; - final int innerDepth = parser.getDepth()+1; int depth; + int type; + + int changingConfigurations = 0; + int defaultColor = DEFAULT_COLOR; + + boolean hasUnresolvedAttrs = false; int[][] stateSpecList = ArrayUtils.newUnpaddedArray(int[].class, 20); + int[][] themeAttrsList = new int[stateSpecList.length][]; int[] colorList = new int[stateSpecList.length]; int listSize = 0; - while ((type=parser.next()) != XmlPullParser.END_DOCUMENT - && ((depth=parser.getDepth()) >= innerDepth - || type != XmlPullParser.END_TAG)) { - if (type != XmlPullParser.START_TAG) { + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) { + if (type != XmlPullParser.START_TAG || depth > innerDepth + || !parser.getName().equals("item")) { continue; } - if (depth > innerDepth || !parser.getName().equals("item")) { - continue; - } + final TypedArray a = Resources.obtainAttributes(r, theme, attrs, + R.styleable.ColorStateListItem); + final int[] themeAttrs = a.extractThemeAttrs(); + final int baseColor = a.getColor(R.styleable.ColorStateListItem_color, 0); + final float alphaMod = a.getFloat(R.styleable.ColorStateListItem_alpha, 1.0f); - int alphaRes = 0; - float alpha = 1.0f; - int colorRes = 0; - int color = 0xffff0000; - boolean haveColor = false; + changingConfigurations |= a.getChangingConfigurations(); - int i; + a.recycle(); + + // Parse all unrecognized attributes as state specifiers. int j = 0; final int numAttrs = attrs.getAttributeCount(); int[] stateSpec = new int[numAttrs]; - for (i = 0; i < numAttrs; i++) { + for (int i = 0; i < numAttrs; i++) { final int stateResId = attrs.getAttributeNameResource(i); - if (stateResId == 0) break; - if (stateResId == com.android.internal.R.attr.alpha) { - alphaRes = attrs.getAttributeResourceValue(i, 0); - if (alphaRes == 0) { - alpha = attrs.getAttributeFloatValue(i, 1.0f); - } - } else if (stateResId == com.android.internal.R.attr.color) { - colorRes = attrs.getAttributeResourceValue(i, 0); - if (colorRes == 0) { - color = attrs.getAttributeIntValue(i, color); - haveColor = true; - } - } else { - stateSpec[j++] = attrs.getAttributeBooleanValue(i, false) - ? stateResId : -stateResId; + switch (stateResId) { + case R.attr.color: + case R.attr.alpha: + // Recognized attribute, ignore. + break; + default: + stateSpec[j++] = attrs.getAttributeBooleanValue(i, false) + ? stateResId : -stateResId; } } stateSpec = StateSet.trimStateSet(stateSpec, j); - if (colorRes != 0) { - color = r.getColor(colorRes); - } else if (!haveColor) { - throw new XmlPullParserException( - parser.getPositionDescription() - + ": <item> tag requires a 'android:color' attribute."); - } - - if (alphaRes != 0) { - alpha = r.getFloat(alphaRes); - } - // Apply alpha modulation. - final int alphaMod = MathUtils.constrain((int) (Color.alpha(color) * alpha), 0, 255); - color = (color & 0xFFFFFF) | (alphaMod << 24); - + final int color = modulateColorAlpha(baseColor, alphaMod); if (listSize == 0 || stateSpec.length == 0) { - mDefaultColor = color; + defaultColor = color; + } + + if (themeAttrs != null) { + hasUnresolvedAttrs = true; } colorList = GrowingArrayUtils.append(colorList, listSize, color); + themeAttrsList = GrowingArrayUtils.append(themeAttrsList, listSize, themeAttrs); stateSpecList = GrowingArrayUtils.append(stateSpecList, listSize, stateSpec); listSize++; } + mChangingConfigurations = changingConfigurations; + mDefaultColor = defaultColor; + + if (hasUnresolvedAttrs) { + mThemeAttrs = new int[listSize][]; + System.arraycopy(themeAttrsList, 0, mThemeAttrs, 0, listSize); + } else { + mThemeAttrs = null; + } + mColors = new int[listSize]; mStateSpecs = new int[listSize][]; System.arraycopy(colorList, 0, mColors, 0, listSize); System.arraycopy(stateSpecList, 0, mStateSpecs, 0, listSize); + + onColorsChanged(); + } + + /** + * Returns whether a theme can be applied to this color state list, which + * usually indicates that the color state list has unresolved theme + * attributes. + * + * @return whether a theme can be applied to this color state list + */ + public boolean canApplyTheme() { + return mThemeAttrs != null; + } + + /** + * Applies a theme to this color state list. + * + * @param t the theme to apply + */ + public void applyTheme(Theme t) { + if (mThemeAttrs == null) { + return; + } + + boolean hasUnresolvedAttrs = false; + + final int[][] themeAttrsList = mThemeAttrs; + final int N = themeAttrsList.length; + for (int i = 0; i < N; i++) { + if (themeAttrsList[i] != null) { + final TypedArray a = t.resolveAttributes(themeAttrsList[i], + R.styleable.ColorStateListItem); + final int baseColor = a.getColor( + R.styleable.ColorStateListItem_color, mColors[i]); + final float alphaMod = a.getFloat( + R.styleable.ColorStateListItem_alpha, 1.0f); + + mColors[i] = modulateColorAlpha(baseColor, alphaMod); + mChangingConfigurations |= a.getChangingConfigurations(); + + themeAttrsList[i] = a.extractThemeAttrs(themeAttrsList[i]); + if (themeAttrsList[i] != null) { + hasUnresolvedAttrs = true; + } + + a.recycle(); + } + } + + if (!hasUnresolvedAttrs) { + mThemeAttrs = null; + } + + onColorsChanged(); + } + + private int modulateColorAlpha(int baseColor, float alphaMod) { + if (alphaMod == 1.0f) { + return baseColor; + } + + final int baseAlpha = Color.alpha(baseColor); + final int alpha = MathUtils.constrain((int) (baseAlpha * alphaMod + 0.5f), 0, 255); + final int color = (baseColor & 0xFFFFFF) | (alpha << 24); + return color; } /** @@ -275,28 +407,24 @@ public class ColorStateList implements Parcelable { * @return True if this color state list is opaque. */ public boolean isOpaque() { - final int n = mColors.length; - for (int i = 0; i < n; i++) { - if (Color.alpha(mColors[i]) != 0xFF) { - return false; - } - } - return true; + return mIsOpaque; } /** - * Return the color associated with the given set of {@link android.view.View} states. + * Return the color associated with the given set of + * {@link android.view.View} states. * * @param stateSet an array of {@link android.view.View} states - * @param defaultColor the color to return if there's not state spec in this - * {@link ColorStateList} that matches the stateSet. + * @param defaultColor the color to return if there's no matching state + * spec in this {@link ColorStateList} that matches the + * stateSet. * * @return the color associated with that set of states in this {@link ColorStateList}. */ - public int getColorForState(int[] stateSet, int defaultColor) { + public int getColorForState(@Nullable int[] stateSet, int defaultColor) { final int setLength = mStateSpecs.length; for (int i = 0; i < setLength; i++) { - int[] stateSpec = mStateSpecs[i]; + final int[] stateSpec = mStateSpecs[i]; if (StateSet.stateSetMatches(stateSpec, stateSet)) { return mColors[i]; } @@ -309,12 +437,15 @@ public class ColorStateList implements Parcelable { * * @return the default color in this {@link ColorStateList}. */ + @ColorInt public int getDefaultColor() { return mDefaultColor; } /** - * Return the states in this {@link ColorStateList}. + * Return the states in this {@link ColorStateList}. The returned array + * should not be modified. + * * @return the states in this {@link ColorStateList} * @hide */ @@ -323,7 +454,9 @@ public class ColorStateList implements Parcelable { } /** - * Return the colors in this {@link ColorStateList}. + * Return the colors in this {@link ColorStateList}. The returned array + * should not be modified. + * * @return the colors in this {@link ColorStateList} * @hide */ @@ -331,52 +464,82 @@ public class ColorStateList implements Parcelable { return mColors; } + @Override + public String toString() { + return "ColorStateList{" + + "mThemeAttrs=" + Arrays.deepToString(mThemeAttrs) + + "mChangingConfigurations=" + mChangingConfigurations + + "mStateSpecs=" + Arrays.deepToString(mStateSpecs) + + "mColors=" + Arrays.toString(mColors) + + "mDefaultColor=" + mDefaultColor + '}'; + } + /** - * If the color state list does not already have an entry matching the - * specified state, prepends a state set and color pair to a color state - * list. - * <p> - * This is a workaround used in TimePicker and DatePicker until we can - * add support for theme attributes in ColorStateList. - * - * @param colorStateList the source color state list - * @param state the state to prepend - * @param color the color to use for the given state - * @return a new color state list, or the source color state list if there - * was already a matching state set - * - * @hide Remove when we can support theme attributes. + * Updates the default color and opacity. */ - public static ColorStateList addFirstIfMissing( - ColorStateList colorStateList, int state, int color) { - final int[][] inputStates = colorStateList.getStates(); - for (int i = 0; i < inputStates.length; i++) { - final int[] inputState = inputStates[i]; - for (int j = 0; j < inputState.length; j++) { - if (inputState[j] == state) { - return colorStateList; + private void onColorsChanged() { + int defaultColor = DEFAULT_COLOR; + boolean isOpaque = true; + + final int[][] states = mStateSpecs; + final int[] colors = mColors; + final int N = states.length; + if (N > 0) { + defaultColor = colors[0]; + + for (int i = N - 1; i > 0; i--) { + if (states[i].length == 0) { + defaultColor = colors[i]; + break; } } - } - final int[][] outputStates = new int[inputStates.length + 1][]; - System.arraycopy(inputStates, 0, outputStates, 1, inputStates.length); - outputStates[0] = new int[] { state }; + for (int i = 0; i < N; i++) { + if (Color.alpha(colors[i]) != 0xFF) { + isOpaque = false; + break; + } + } + } - final int[] inputColors = colorStateList.getColors(); - final int[] outputColors = new int[inputColors.length + 1]; - System.arraycopy(inputColors, 0, outputColors, 1, inputColors.length); - outputColors[0] = color; + mDefaultColor = defaultColor; + mIsOpaque = isOpaque; + } - return new ColorStateList(outputStates, outputColors); + /** + * @return A factory that can create new instances of this ColorStateList. + */ + ColorStateListFactory getFactory() { + return new ColorStateListFactory(this); } - @Override - public String toString() { - return "ColorStateList{" + - "mStateSpecs=" + Arrays.deepToString(mStateSpecs) + - "mColors=" + Arrays.toString(mColors) + - "mDefaultColor=" + mDefaultColor + '}'; + static class ColorStateListFactory extends ConstantState<ColorStateList> { + final ColorStateList mSrc; + + public ColorStateListFactory(ColorStateList src) { + mSrc = src; + } + + @Override + public int getChangingConfigurations() { + return mSrc.mChangingConfigurations; + } + + @Override + public ColorStateList newInstance() { + return mSrc; + } + + @Override + public ColorStateList newInstance(Resources res, Theme theme) { + if (theme == null || !mSrc.canApplyTheme()) { + return mSrc; + } + + final ColorStateList clone = new ColorStateList(mSrc); + clone.applyTheme(theme); + return clone; + } } @Override @@ -386,6 +549,9 @@ public class ColorStateList implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { + if (canApplyTheme()) { + Log.w(TAG, "Wrote partially-resolved ColorStateList to parcel!"); + } final int N = mStateSpecs.length; dest.writeInt(N); for (int i = 0; i < N; i++) { diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index 73913b6..95ad57e 100644 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -16,33 +16,48 @@ 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 android.annotation.ColorInt; import com.android.internal.util.XmlUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import android.animation.Animator; +import android.animation.StateListAnimator; +import android.annotation.AnimRes; +import android.annotation.AnyRes; +import android.annotation.ArrayRes; +import android.annotation.BoolRes; +import android.annotation.ColorRes; +import android.annotation.DimenRes; +import android.annotation.DrawableRes; +import android.annotation.FractionRes; +import android.annotation.IntegerRes; +import android.annotation.LayoutRes; +import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.PluralsRes; +import android.annotation.RawRes; +import android.annotation.StringRes; +import android.annotation.XmlRes; import android.content.pm.ActivityInfo; +import android.content.res.ColorStateList.ColorStateListFactory; import android.graphics.Movie; -import android.graphics.drawable.Drawable; import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable.ConstantState; import android.os.Build; import android.os.Bundle; -import android.os.IBinder; import android.os.Trace; import android.util.ArrayMap; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; +import android.util.LongSparseArray; +import android.util.Pools.SynchronizedPool; import android.util.Slog; import android.util.TypedValue; -import android.util.LongSparseArray; +import android.view.ViewDebug; import java.io.IOException; import java.io.InputStream; @@ -97,8 +112,8 @@ public class Resources { private static final LongSparseArray<ConstantState>[] sPreloadedDrawables; private static final LongSparseArray<ConstantState> sPreloadedColorDrawables = new LongSparseArray<ConstantState>(); - private static final LongSparseArray<ColorStateList> sPreloadedColorStateLists - = new LongSparseArray<ColorStateList>(); + private static final LongSparseArray<ColorStateListFactory> sPreloadedColorStateLists + = new LongSparseArray<ColorStateListFactory>(); // Pool of TypedArrays targeted to this Resources object. final SynchronizedPool<TypedArray> mTypedArrayPool = new SynchronizedPool<TypedArray>(5); @@ -116,8 +131,8 @@ public class Resources { new ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>>(); private final ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> mColorDrawableCache = new ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>>(); - private final LongSparseArray<WeakReference<ColorStateList>> mColorStateListCache = - new LongSparseArray<WeakReference<ColorStateList>>(); + private final ConfigurationBoundResourceCache<ColorStateList> mColorStateListCache = + new ConfigurationBoundResourceCache<ColorStateList>(this); private final ConfigurationBoundResourceCache<Animator> mAnimatorCache = new ConfigurationBoundResourceCache<Animator>(this); private final ConfigurationBoundResourceCache<StateListAnimator> mStateListAnimatorCache = @@ -140,9 +155,6 @@ public class Resources { private CompatibilityInfo mCompatibilityInfo = CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO; - @SuppressWarnings("unused") - private WeakReference<IBinder> mToken; - static { sPreloadedDrawables = new LongSparseArray[2]; sPreloadedDrawables[0] = new LongSparseArray<ConstantState>(); @@ -223,46 +235,44 @@ public class Resources { /** * Create a new Resources object on top of an existing set of assets in an * AssetManager. - * - * @param assets Previously created AssetManager. - * @param metrics Current display metrics to consider when + * + * @param assets Previously created AssetManager. + * @param metrics Current display metrics to consider when * selecting/computing resource values. - * @param config Desired device configuration to consider when + * @param config Desired device configuration to consider when * selecting/computing resource values (optional). */ public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) { - this(assets, metrics, config, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null); + this(assets, metrics, config, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO); } /** * Creates a new Resources object with CompatibilityInfo. - * - * @param assets Previously created AssetManager. - * @param metrics Current display metrics to consider when + * + * @param assets Previously created AssetManager. + * @param metrics Current display metrics to consider when * selecting/computing resource values. - * @param config Desired device configuration to consider when + * @param config Desired device configuration to consider when * selecting/computing resource values (optional). * @param compatInfo this resource's compatibility info. Must not be null. - * @param token The Activity token for determining stack affiliation. Usually null. * @hide */ public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config, - CompatibilityInfo compatInfo, IBinder token) { + CompatibilityInfo compatInfo) { mAssets = assets; mMetrics.setToDefaults(); if (compatInfo != null) { mCompatibilityInfo = compatInfo; } - mToken = new WeakReference<IBinder>(token); updateConfiguration(config, metrics); assets.ensureStringBlocks(); } /** * Return a global shared Resources object that provides access to only - * system resources (no application resources), and is not configured for - * the current screen (can not use dimension units, does not change based - * on orientation, etc). + * system resources (no application resources), and is not configured for + * the current screen (can not use dimension units, does not change based + * on orientation, etc). */ public static Resources getSystem() { synchronized (sSync) { @@ -291,7 +301,7 @@ public class Resources { * @return CharSequence The string data associated with the resource, plus * possibly styled text information. */ - public CharSequence getText(int id) throws NotFoundException { + public CharSequence getText(@StringRes int id) throws NotFoundException { CharSequence res = mAssets.getResourceText(id); if (res != null) { return res; @@ -320,7 +330,8 @@ public class Resources { * @return CharSequence The string data associated with the resource, plus * possibly styled text information. */ - public CharSequence getQuantityText(int id, int quantity) throws NotFoundException { + public CharSequence getQuantityText(@PluralsRes int id, int quantity) + throws NotFoundException { NativePluralRules rule = getPluralRule(); CharSequence res = mAssets.getResourceBagText(id, attrForQuantityCode(rule.quantityForInt(quantity))); @@ -381,7 +392,7 @@ public class Resources { * @return String The string data associated with the resource, * stripped of styled text information. */ - public String getString(int id) throws NotFoundException { + public String getString(@StringRes int id) throws NotFoundException { CharSequence res = getText(id); if (res != null) { return res.toString(); @@ -409,7 +420,8 @@ public class Resources { * @return String The string data associated with the resource, * stripped of styled text information. */ - public String getString(int id, Object... formatArgs) throws NotFoundException { + public String getString(@StringRes int id, Object... formatArgs) + throws NotFoundException { String raw = getString(id); return String.format(mConfiguration.locale, raw, formatArgs); } @@ -439,7 +451,7 @@ public class Resources { * @return String The string data associated with the resource, * stripped of styled text information. */ - public String getQuantityString(int id, int quantity, Object... formatArgs) + public String getQuantityString(@PluralsRes int id, int quantity, Object... formatArgs) throws NotFoundException { String raw = getQuantityText(id, quantity).toString(); return String.format(mConfiguration.locale, raw, formatArgs); @@ -465,7 +477,8 @@ public class Resources { * @return String The string data associated with the resource, * stripped of styled text information. */ - public String getQuantityString(int id, int quantity) throws NotFoundException { + public String getQuantityString(@PluralsRes int id, int quantity) + throws NotFoundException { return getQuantityText(id, quantity).toString(); } @@ -483,7 +496,7 @@ public class Resources { * @return CharSequence The string data associated with the resource, plus * possibly styled text information, or def if id is 0 or not found. */ - public CharSequence getText(int id, CharSequence def) { + public CharSequence getText(@StringRes int id, CharSequence def) { CharSequence res = id != 0 ? mAssets.getResourceText(id) : null; return res != null ? res : def; } @@ -499,7 +512,7 @@ public class Resources { * * @return The styled text array associated with the resource. */ - public CharSequence[] getTextArray(int id) throws NotFoundException { + public CharSequence[] getTextArray(@ArrayRes int id) throws NotFoundException { CharSequence[] res = mAssets.getResourceTextArray(id); if (res != null) { return res; @@ -519,7 +532,8 @@ public class Resources { * * @return The string array associated with the resource. */ - public String[] getStringArray(int id) throws NotFoundException { + public String[] getStringArray(@ArrayRes int id) + throws NotFoundException { String[] res = mAssets.getResourceStringArray(id); if (res != null) { return res; @@ -539,7 +553,7 @@ public class Resources { * * @return The int array associated with the resource. */ - public int[] getIntArray(int id) throws NotFoundException { + public int[] getIntArray(@ArrayRes int id) throws NotFoundException { int[] res = mAssets.getArrayIntResource(id); if (res != null) { return res; @@ -561,7 +575,8 @@ public class Resources { * Be sure to call {@link TypedArray#recycle() TypedArray.recycle()} * when done with it. */ - public TypedArray obtainTypedArray(int id) throws NotFoundException { + public TypedArray obtainTypedArray(@ArrayRes int id) + throws NotFoundException { int len = mAssets.getArraySize(id); if (len < 0) { throw new NotFoundException("Array resource ID #0x" @@ -592,7 +607,7 @@ public class Resources { * @see #getDimensionPixelOffset * @see #getDimensionPixelSize */ - public float getDimension(int id) throws NotFoundException { + public float getDimension(@DimenRes int id) throws NotFoundException { synchronized (mAccessLock) { TypedValue value = mTmpValue; if (value == null) { @@ -627,7 +642,7 @@ public class Resources { * @see #getDimension * @see #getDimensionPixelSize */ - public int getDimensionPixelOffset(int id) throws NotFoundException { + public int getDimensionPixelOffset(@DimenRes int id) throws NotFoundException { synchronized (mAccessLock) { TypedValue value = mTmpValue; if (value == null) { @@ -664,7 +679,7 @@ public class Resources { * @see #getDimension * @see #getDimensionPixelOffset */ - public int getDimensionPixelSize(int id) throws NotFoundException { + public int getDimensionPixelSize(@DimenRes int id) throws NotFoundException { synchronized (mAccessLock) { TypedValue value = mTmpValue; if (value == null) { @@ -698,7 +713,7 @@ public class Resources { * * @throws NotFoundException Throws NotFoundException if the given ID does not exist. */ - public float getFraction(int id, int base, int pbase) { + public float getFraction(@FractionRes int id, int base, int pbase) { synchronized (mAccessLock) { TypedValue value = mTmpValue; if (value == null) { @@ -748,7 +763,7 @@ public class Resources { */ @Deprecated @Nullable - public Drawable getDrawable(int id) throws NotFoundException { + public Drawable getDrawable(@DrawableRes int id) throws NotFoundException { final Drawable d = getDrawable(id, null); if (d != null && d.canApplyTheme()) { Log.w(TAG, "Drawable " + getResourceName(id) + " has unresolved theme " @@ -773,7 +788,7 @@ public class Resources { * not exist. */ @Nullable - public Drawable getDrawable(int id, @Nullable Theme theme) throws NotFoundException { + public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme) throws NotFoundException { TypedValue value; synchronized (mAccessLock) { value = mTmpValue; @@ -821,7 +836,7 @@ public class Resources { */ @Deprecated @Nullable - public Drawable getDrawableForDensity(int id, int density) throws NotFoundException { + public Drawable getDrawableForDensity(@DrawableRes int id, int density) throws NotFoundException { return getDrawableForDensity(id, density, null); } @@ -840,7 +855,7 @@ public class Resources { * not exist. */ @Nullable - public Drawable getDrawableForDensity(int id, int density, @Nullable Theme theme) { + public Drawable getDrawableForDensity(@DrawableRes int id, int density, @Nullable Theme theme) { TypedValue value; synchronized (mAccessLock) { value = mTmpValue; @@ -884,7 +899,7 @@ public class Resources { * @throws NotFoundException Throws NotFoundException if the given ID does not exist. * */ - public Movie getMovie(int id) throws NotFoundException { + public Movie getMovie(@RawRes int id) throws NotFoundException { InputStream is = openRawResource(id); Movie movie = Movie.decodeStream(is); try { @@ -897,20 +912,44 @@ public class Resources { } /** - * Return a color integer associated with a particular resource ID. - * If the resource holds a complex - * {@link android.content.res.ColorStateList}, then the default color from - * the set is returned. + * Returns a color integer associated with a particular resource ID. If the + * resource holds a complex {@link ColorStateList}, then the default color + * from the set is returned. * * @param id The desired resource identifier, as generated by the aapt * tool. This integer encodes the package, type, and resource * entry. The value 0 is an invalid identifier. * - * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * @throws NotFoundException Throws NotFoundException if the given ID does + * not exist. + * + * @return A single color value in the form 0xAARRGGBB. + * @deprecated Use {@link #getColor(int, Theme)} instead. + */ + @ColorInt + @Deprecated + public int getColor(@ColorRes int id) throws NotFoundException { + return getColor(id, null); + } + + /** + * Returns a themed color integer associated with a particular resource ID. + * If the resource holds a complex {@link ColorStateList}, then the default + * color from the set is returned. + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * @param theme The theme used to style the color attributes, may be + * {@code null}. + * + * @throws NotFoundException Throws NotFoundException if the given ID does + * not exist. * - * @return Returns a single color value in the form 0xAARRGGBB. + * @return A single color value in the form 0xAARRGGBB. */ - public int getColor(int id) throws NotFoundException { + @ColorInt + public int getColor(@ColorRes int id, @Nullable Theme theme) throws NotFoundException { TypedValue value; synchronized (mAccessLock) { value = mTmpValue; @@ -919,40 +958,78 @@ public class Resources { } getValue(id, value, true); if (value.type >= TypedValue.TYPE_FIRST_INT - && value.type <= TypedValue.TYPE_LAST_INT) { + && value.type <= TypedValue.TYPE_LAST_INT) { mTmpValue = value; return value.data; } else if (value.type != TypedValue.TYPE_STRING) { throw new NotFoundException( - "Resource ID #0x" + Integer.toHexString(id) + " type #0x" - + Integer.toHexString(value.type) + " is not valid"); + "Resource ID #0x" + Integer.toHexString(id) + " type #0x" + + Integer.toHexString(value.type) + " is not valid"); } mTmpValue = null; } - ColorStateList csl = loadColorStateList(value, id); + + final ColorStateList csl = loadColorStateList(value, id, theme); synchronized (mAccessLock) { if (mTmpValue == null) { mTmpValue = value; } } + return csl.getDefaultColor(); } /** - * Return a color state list associated with a particular resource ID. The - * resource may contain either a single raw color value, or a complex - * {@link android.content.res.ColorStateList} holding multiple possible colors. + * Returns a color state list associated with a particular resource ID. The + * resource may contain either a single raw color value or a complex + * {@link ColorStateList} holding multiple possible colors. * * @param id The desired resource identifier of a {@link ColorStateList}, - * as generated by the aapt tool. This integer encodes the package, type, and resource - * entry. The value 0 is an invalid identifier. + * as generated by the aapt tool. This integer encodes the + * package, type, and resource entry. The value 0 is an invalid + * identifier. * - * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * @throws NotFoundException Throws NotFoundException if the given ID does + * not exist. + * + * @return A ColorStateList object containing either a single solid color + * or multiple colors that can be selected based on a state. + * @deprecated Use {@link #getColorStateList(int, Theme)} instead. + */ + @Nullable + @Deprecated + public ColorStateList getColorStateList(@ColorRes int id) throws NotFoundException { + final ColorStateList csl = getColorStateList(id, null); + if (csl != null && csl.canApplyTheme()) { + Log.w(TAG, "ColorStateList " + getResourceName(id) + " has " + + "unresolved theme attributes! Consider using " + + "Resources.getColorStateList(int, Theme) or " + + "Context.getColorStateList(int).", new RuntimeException()); + } + return csl; + } + + /** + * Returns a themed color state list associated with a particular resource + * ID. The resource may contain either a single raw color value or a + * complex {@link ColorStateList} holding multiple possible colors. * - * @return Returns a ColorStateList object containing either a single - * solid color or multiple colors that can be selected based on a state. + * @param id The desired resource identifier of a {@link ColorStateList}, + * as generated by the aapt tool. This integer encodes the + * package, type, and resource entry. The value 0 is an invalid + * identifier. + * @param theme The theme used to style the color attributes, may be + * {@code null}. + * + * @throws NotFoundException Throws NotFoundException if the given ID does + * not exist. + * + * @return A themed ColorStateList object containing either a single solid + * color or multiple colors that can be selected based on a state. */ - public ColorStateList getColorStateList(int id) throws NotFoundException { + @Nullable + public ColorStateList getColorStateList(@ColorRes int id, @Nullable Theme theme) + throws NotFoundException { TypedValue value; synchronized (mAccessLock) { value = mTmpValue; @@ -963,12 +1040,14 @@ public class Resources { } getValue(id, value, true); } - ColorStateList res = loadColorStateList(value, id); + + final ColorStateList res = loadColorStateList(value, id, theme); synchronized (mAccessLock) { if (mTmpValue == null) { mTmpValue = value; } } + return res; } @@ -985,7 +1064,7 @@ public class Resources { * * @return Returns the boolean value contained in the resource. */ - public boolean getBoolean(int id) throws NotFoundException { + public boolean getBoolean(@BoolRes int id) throws NotFoundException { synchronized (mAccessLock) { TypedValue value = mTmpValue; if (value == null) { @@ -1013,7 +1092,7 @@ public class Resources { * * @return Returns the integer value contained in the resource. */ - public int getInteger(int id) throws NotFoundException { + public int getInteger(@IntegerRes int id) throws NotFoundException { synchronized (mAccessLock) { TypedValue value = mTmpValue; if (value == null) { @@ -1078,7 +1157,7 @@ public class Resources { * * @see #getXml */ - public XmlResourceParser getLayout(int id) throws NotFoundException { + public XmlResourceParser getLayout(@LayoutRes int id) throws NotFoundException { return loadXmlResourceParser(id, "layout"); } @@ -1102,7 +1181,7 @@ public class Resources { * * @see #getXml */ - public XmlResourceParser getAnimation(int id) throws NotFoundException { + public XmlResourceParser getAnimation(@AnimRes int id) throws NotFoundException { return loadXmlResourceParser(id, "anim"); } @@ -1127,7 +1206,7 @@ public class Resources { * * @see android.util.AttributeSet */ - public XmlResourceParser getXml(int id) throws NotFoundException { + public XmlResourceParser getXml(@XmlRes int id) throws NotFoundException { return loadXmlResourceParser(id, "xml"); } @@ -1145,7 +1224,7 @@ public class Resources { * @throws NotFoundException Throws NotFoundException if the given ID does not exist. * */ - public InputStream openRawResource(int id) throws NotFoundException { + public InputStream openRawResource(@RawRes int id) throws NotFoundException { TypedValue value; synchronized (mAccessLock) { value = mTmpValue; @@ -1177,7 +1256,8 @@ public class Resources { * * @throws NotFoundException Throws NotFoundException if the given ID does not exist. */ - public InputStream openRawResource(int id, TypedValue value) throws NotFoundException { + public InputStream openRawResource(@RawRes int id, TypedValue value) + throws NotFoundException { getValue(id, value, true); try { @@ -1212,7 +1292,8 @@ public class Resources { * @throws NotFoundException Throws NotFoundException if the given ID does not exist. * */ - public AssetFileDescriptor openRawResourceFd(int id) throws NotFoundException { + public AssetFileDescriptor openRawResourceFd(@RawRes int id) + throws NotFoundException { TypedValue value; synchronized (mAccessLock) { value = mTmpValue; @@ -1257,7 +1338,7 @@ public class Resources { * @throws NotFoundException Throws NotFoundException if the given ID does not exist. * */ - public void getValue(int id, TypedValue outValue, boolean resolveRefs) + public void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs) throws NotFoundException { boolean found = mAssets.getResourceValue(id, 0, outValue, resolveRefs); if (found) { @@ -1280,8 +1361,8 @@ public class Resources { * not exist. * @see #getValue(String, TypedValue, boolean) */ - public void getValueForDensity(int id, int density, TypedValue outValue, boolean resolveRefs) - throws NotFoundException { + public void getValueForDensity(@AnyRes int id, int density, TypedValue outValue, + boolean resolveRefs) throws NotFoundException { boolean found = mAssets.getResourceValue(id, density, outValue, resolveRefs); if (found) { return; @@ -1640,7 +1721,7 @@ public class Resources { * @throws NotFoundException Throws NotFoundException if the given ID * does not exist. */ - public Drawable getDrawable(int id) throws NotFoundException { + public Drawable getDrawable(@DrawableRes int id) throws NotFoundException { return Resources.this.getDrawable(id, this); } @@ -1843,11 +1924,10 @@ public class Resources { clearDrawableCachesLocked(mDrawableCache, configChanges); clearDrawableCachesLocked(mColorDrawableCache, configChanges); + mColorStateListCache.onConfigurationChange(configChanges); mAnimatorCache.onConfigurationChange(configChanges); mStateListAnimatorCache.onConfigurationChange(configChanges); - mColorStateListCache.clear(); - flushLayoutCache(); } synchronized (sSync) { @@ -2046,7 +2126,7 @@ public class Resources { * * @hide */ - public static boolean resourceHasPackage(int resid) { + public static boolean resourceHasPackage(@AnyRes int resid) { return (resid >>> 24) != 0; } @@ -2064,7 +2144,7 @@ public class Resources { * @see #getResourceTypeName * @see #getResourceEntryName */ - public String getResourceName(int resid) throws NotFoundException { + public String getResourceName(@AnyRes int resid) throws NotFoundException { String str = mAssets.getResourceName(resid); if (str != null) return str; throw new NotFoundException("Unable to find resource ID #0x" @@ -2083,7 +2163,7 @@ public class Resources { * * @see #getResourceName */ - public String getResourcePackageName(int resid) throws NotFoundException { + public String getResourcePackageName(@AnyRes int resid) throws NotFoundException { String str = mAssets.getResourcePackageName(resid); if (str != null) return str; throw new NotFoundException("Unable to find resource ID #0x" @@ -2102,7 +2182,7 @@ public class Resources { * * @see #getResourceName */ - public String getResourceTypeName(int resid) throws NotFoundException { + public String getResourceTypeName(@AnyRes int resid) throws NotFoundException { String str = mAssets.getResourceTypeName(resid); if (str != null) return str; throw new NotFoundException("Unable to find resource ID #0x" @@ -2121,7 +2201,7 @@ public class Resources { * * @see #getResourceName */ - public String getResourceEntryName(int resid) throws NotFoundException { + public String getResourceEntryName(@AnyRes int resid) throws NotFoundException { String str = mAssets.getResourceEntryName(resid); if (str != null) return str; throw new NotFoundException("Unable to find resource ID #0x" @@ -2425,6 +2505,9 @@ public class Resources { final String themeKey = theme == null ? "" : theme.mKey; LongSparseArray<WeakReference<ConstantState>> themedCache = caches.get(themeKey); if (themedCache == null) { + // Clean out the caches before we add more. This shouldn't + // happen very often. + pruneCaches(caches); themedCache = new LongSparseArray<WeakReference<ConstantState>>(1); caches.put(themeKey, themedCache); } @@ -2434,12 +2517,46 @@ public class Resources { } /** + * Prunes empty caches from the cache map. + * + * @param caches The map of caches to prune. + */ + private void pruneCaches(ArrayMap<String, + LongSparseArray<WeakReference<ConstantState>>> caches) { + final int N = caches.size(); + for (int i = N - 1; i >= 0; i--) { + final LongSparseArray<WeakReference<ConstantState>> cache = caches.valueAt(i); + if (pruneCache(cache)) { + caches.removeAt(i); + } + } + } + + /** + * Prunes obsolete weak references from a cache, returning {@code true} if + * the cache is empty and should be removed. + * + * @param cache The cache of weak references to prune. + * @return {@code true} if the cache is empty and should be removed. + */ + private boolean pruneCache(LongSparseArray<WeakReference<ConstantState>> cache) { + final int N = cache.size(); + for (int i = N - 1; i >= 0; i--) { + final WeakReference entry = cache.valueAt(i); + if (entry == null || entry.get() == null) { + cache.removeAt(i); + } + } + return cache.size() == 0; + } + + /** * Loads a drawable from XML or resources stream. */ private Drawable loadDrawableForCookie(TypedValue value, int id, Theme theme) { if (value.string == null) { throw new NotFoundException("Resource \"" + getResourceName(id) + "\" (" - + Integer.toHexString(id) + ") is not a Drawable (color or path): " + value); + + Integer.toHexString(id) + ") is not a Drawable (color or path): " + value); } final String file = value.string.toString(); @@ -2530,7 +2647,8 @@ public class Resources { return null; } - /*package*/ ColorStateList loadColorStateList(TypedValue value, int id) + @Nullable + ColorStateList loadColorStateList(TypedValue value, int id, Theme theme) throws NotFoundException { if (TRACE_FOR_PRELOAD) { // Log only framework resources @@ -2544,101 +2662,107 @@ public class Resources { ColorStateList csl; - if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && - value.type <= TypedValue.TYPE_LAST_COLOR_INT) { - - csl = sPreloadedColorStateLists.get(key); - if (csl != null) { - return csl; + // Handle inline color definitions. + if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT + && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { + final ColorStateListFactory factory = sPreloadedColorStateLists.get(key); + if (factory != null) { + return factory.newInstance(); } csl = ColorStateList.valueOf(value.data); + if (mPreloading) { if (verifyPreloadConfig(value.changingConfigurations, 0, value.resourceId, "color")) { - sPreloadedColorStateLists.put(key, csl); + sPreloadedColorStateLists.put(key, csl.getFactory()); } } return csl; } - csl = getCachedColorStateList(key); + final ConfigurationBoundResourceCache<ColorStateList> cache = mColorStateListCache; + + csl = cache.get(key, theme); if (csl != null) { return csl; } - csl = sPreloadedColorStateLists.get(key); + final ColorStateListFactory factory = sPreloadedColorStateLists.get(key); + if (factory != null) { + csl = factory.newInstance(this, theme); + } + + if (csl == null) { + csl = loadColorStateListForCookie(value, id, theme); + } + if (csl != null) { - return csl; + if (mPreloading) { + if (verifyPreloadConfig(value.changingConfigurations, 0, value.resourceId, + "color")) { + sPreloadedColorStateLists.put(key, csl.getFactory()); + } + } else { + cache.put(key, theme, csl.getFactory()); + } } + return csl; + } + + private ColorStateList loadColorStateListForCookie(TypedValue value, int id, Theme theme) { if (value.string == null) { - throw new NotFoundException( - "Resource is not a ColorStateList (color or path): " + value); + throw new UnsupportedOperationException( + "Can't convert to color state list: type=0x" + value.type); } - + final String file = value.string.toString(); + if (TRACE_FOR_MISS_PRELOAD) { + // Log only framework resources + if ((id >>> 24) == 0x1) { + final String name = getResourceName(id); + if (name != null) { + Log.d(TAG, "Loading framework color state list #" + Integer.toHexString(id) + + ": " + name + " at " + file); + } + } + } + + if (DEBUG_LOAD) { + Log.v(TAG, "Loading color state list for cookie " + value.assetCookie + ": " + file); + } + + final ColorStateList csl; + + Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file); if (file.endsWith(".xml")) { - Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file); try { final XmlResourceParser rp = loadXmlResourceParser( - file, id, value.assetCookie, "colorstatelist"); - csl = ColorStateList.createFromXml(this, rp); + file, id, value.assetCookie, "colorstatelist"); + csl = ColorStateList.createFromXml(this, rp, theme); rp.close(); } catch (Exception e) { Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); - NotFoundException rnf = new NotFoundException( - "File " + file + " from color state list resource ID #0x" - + Integer.toHexString(id)); + final NotFoundException rnf = new NotFoundException( + "File " + file + " from color state list resource ID #0x" + + Integer.toHexString(id)); rnf.initCause(e); throw rnf; } - Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); } else { + Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); throw new NotFoundException( "File " + file + " from drawable resource ID #0x" - + Integer.toHexString(id) + ": .xml extension required"); - } - - if (csl != null) { - if (mPreloading) { - if (verifyPreloadConfig(value.changingConfigurations, 0, value.resourceId, - "color")) { - sPreloadedColorStateLists.put(key, csl); - } - } else { - synchronized (mAccessLock) { - //Log.i(TAG, "Saving cached color state list @ #" + - // Integer.toHexString(key.intValue()) - // + " in " + this + ": " + csl); - mColorStateListCache.put(key, new WeakReference<ColorStateList>(csl)); - } - } + + Integer.toHexString(id) + ": .xml extension required"); } + Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); return csl; } - private ColorStateList getCachedColorStateList(long key) { - synchronized (mAccessLock) { - WeakReference<ColorStateList> wr = mColorStateListCache.get(key); - if (wr != null) { // we have the key - ColorStateList entry = wr.get(); - if (entry != null) { - //Log.i(TAG, "Returning cached color state list @ #" + - // Integer.toHexString(((Integer)key).intValue()) - // + " in " + this + ": " + entry); - return entry; - } else { // our entry has been purged - mColorStateListCache.delete(key); - } - } - } - return null; - } - /*package*/ XmlResourceParser loadXmlResourceParser(int id, String type) throws NotFoundException { synchronized (mAccessLock) { @@ -2715,6 +2839,20 @@ public class Resources { } } + /** + * Obtains styled attributes from the theme, if available, or unstyled + * resources if the theme is null. + * + * @hide + */ + public static TypedArray obtainAttributes( + Resources res, Theme theme, AttributeSet set, int[] attrs) { + if (theme == null) { + return res.obtainAttributes(set, attrs); + } + return theme.obtainStyledAttributes(set, attrs, 0, 0); + } + private Resources() { mAssets = AssetManager.getSystem(); // NOTE: Intentionally leaving this uninitialized (all values set diff --git a/core/java/android/content/res/ResourcesKey.java b/core/java/android/content/res/ResourcesKey.java index 4ae3825..9548d49 100644 --- a/core/java/android/content/res/ResourcesKey.java +++ b/core/java/android/content/res/ResourcesKey.java @@ -16,27 +16,23 @@ package android.content.res; -import android.os.IBinder; +import java.util.Objects; /** @hide */ public final class ResourcesKey { - final String mResDir; - final float mScale; + private final String mResDir; + private final float mScale; private final int mHash; - private final IBinder mToken; public final int mDisplayId; - public final Configuration mOverrideConfiguration = new Configuration(); + public final Configuration mOverrideConfiguration; public ResourcesKey(String resDir, int displayId, Configuration overrideConfiguration, - float scale, IBinder token) { + float scale) { mResDir = resDir; mDisplayId = displayId; - if (overrideConfiguration != null) { - mOverrideConfiguration.setTo(overrideConfiguration); - } + mOverrideConfiguration = overrideConfiguration; mScale = scale; - mToken = token; int hash = 17; hash = 31 * hash + (mResDir == null ? 0 : mResDir.hashCode()); @@ -48,7 +44,8 @@ public final class ResourcesKey { } public boolean hasOverrideConfiguration() { - return !Configuration.EMPTY.equals(mOverrideConfiguration); + return mOverrideConfiguration != null + && !Configuration.EMPTY.equals(mOverrideConfiguration); } @Override @@ -63,17 +60,9 @@ public final class ResourcesKey { } ResourcesKey peer = (ResourcesKey) obj; - if ((mResDir == null) && (peer.mResDir != null)) { - return false; - } - if ((mResDir != null) && (peer.mResDir == null)) { + if (!Objects.equals(mResDir, peer.mResDir)) { return false; } - if ((mResDir != null) && (peer.mResDir != null)) { - if (!mResDir.equals(peer.mResDir)) { - return false; - } - } if (mDisplayId != peer.mDisplayId) { return false; } diff --git a/core/java/android/content/res/StringBlock.java b/core/java/android/content/res/StringBlock.java index 9652db7..5cfc41f 100644 --- a/core/java/android/content/res/StringBlock.java +++ b/core/java/android/content/res/StringBlock.java @@ -328,7 +328,7 @@ final class StringBlock { String name = color.substring(1); int colorRes = res.getIdentifier(name, "color", "android"); if (colorRes != 0) { - ColorStateList colors = res.getColorStateList(colorRes); + ColorStateList colors = res.getColorStateList(colorRes, null); if (foreground) { return new TextAppearanceSpan(null, 0, 0, colors, null); } else { diff --git a/core/java/android/content/res/TypedArray.java b/core/java/android/content/res/TypedArray.java index 02602fb..1fca920 100644 --- a/core/java/android/content/res/TypedArray.java +++ b/core/java/android/content/res/TypedArray.java @@ -16,11 +16,13 @@ package android.content.res; +import android.annotation.AnyRes; +import android.annotation.ColorInt; import android.annotation.Nullable; import android.graphics.drawable.Drawable; +import android.os.StrictMode; import android.util.AttributeSet; import android.util.DisplayMetrics; -import android.util.Log; import android.util.TypedValue; import com.android.internal.util.XmlUtils; @@ -73,7 +75,9 @@ public class TypedArray { /*package*/ TypedValue mValue = new TypedValue(); /** - * Return the number of values in this array. + * Returns the number of values in this array. + * + * @throws RuntimeException if the TypedArray has already been recycled. */ public int length() { if (mRecycled) { @@ -85,6 +89,8 @@ public class TypedArray { /** * Return the number of indices in the array that actually have data. + * + * @throws RuntimeException if the TypedArray has already been recycled. */ public int getIndexCount() { if (mRecycled) { @@ -95,13 +101,14 @@ public class TypedArray { } /** - * Return an index in the array that has data. + * Returns an index in the array that has data. * * @param at The index you would like to returned, ranging from 0 to - * {@link #getIndexCount()}. + * {@link #getIndexCount()}. * * @return The index at the given offset, which can be used with - * {@link #getValue} and related APIs. + * {@link #getValue} and related APIs. + * @throws RuntimeException if the TypedArray has already been recycled. */ public int getIndex(int at) { if (mRecycled) { @@ -112,7 +119,9 @@ public class TypedArray { } /** - * Return the Resources object this array was loaded from. + * Returns the Resources object this array was loaded from. + * + * @throws RuntimeException if the TypedArray has already been recycled. */ public Resources getResources() { if (mRecycled) { @@ -123,12 +132,17 @@ public class TypedArray { } /** - * Retrieve the styled string value for the attribute at <var>index</var>. + * Retrieves the styled string value for the attribute at <var>index</var>. + * <p> + * If the attribute is not a string, this method will attempt to coerce + * it to a string. * * @param index Index of attribute to retrieve. * - * @return CharSequence holding string data. May be styled. Returns - * null if the attribute is not defined. + * @return CharSequence holding string data. May be styled. Returns + * {@code null} if the attribute is not defined or could not be + * coerced to a string. + * @throws RuntimeException if the TypedArray has already been recycled. */ public CharSequence getText(int index) { if (mRecycled) { @@ -144,23 +158,28 @@ public class TypedArray { return loadStringValueAt(index); } - TypedValue v = mValue; + final TypedValue v = mValue; if (getValueAt(index, v)) { - Log.w(Resources.TAG, "Converting to string: " + v); + StrictMode.noteResourceMismatch(v); return v.coerceToString(); } - Log.w(Resources.TAG, "getString of bad type: 0x" - + Integer.toHexString(type)); - return null; + + // We already checked for TYPE_NULL. This should never happen. + throw new RuntimeException("getText of bad type: 0x" + Integer.toHexString(type)); } /** - * Retrieve the string value for the attribute at <var>index</var>. + * Retrieves the string value for the attribute at <var>index</var>. + * <p> + * If the attribute is not a string, this method will attempt to coerce + * it to a string. * * @param index Index of attribute to retrieve. * - * @return String holding string data. Any styling information is - * removed. Returns null if the attribute is not defined. + * @return String holding string data. Any styling information is removed. + * Returns {@code null} if the attribute is not defined or could + * not be coerced to a string. + * @throws RuntimeException if the TypedArray has already been recycled. */ public String getString(int index) { if (mRecycled) { @@ -176,19 +195,19 @@ public class TypedArray { return loadStringValueAt(index).toString(); } - TypedValue v = mValue; + final TypedValue v = mValue; if (getValueAt(index, v)) { - Log.w(Resources.TAG, "Converting to string: " + v); - CharSequence cs = v.coerceToString(); + StrictMode.noteResourceMismatch(v); + final CharSequence cs = v.coerceToString(); return cs != null ? cs.toString() : null; } - Log.w(Resources.TAG, "getString of bad type: 0x" - + Integer.toHexString(type)); - return null; + + // We already checked for TYPE_NULL. This should never happen. + throw new RuntimeException("getString of bad type: 0x" + Integer.toHexString(type)); } /** - * Retrieve the string value for the attribute at <var>index</var>, but + * Retrieves the string value for the attribute at <var>index</var>, but * only if that string comes from an immediate value in an XML file. That * is, this does not allow references to string resources, string * attributes, or conversions from other types. As such, this method @@ -197,9 +216,10 @@ public class TypedArray { * * @param index Index of attribute to retrieve. * - * @return String holding string data. Any styling information is - * removed. Returns null if the attribute is not defined or is not - * an immediate string value. + * @return String holding string data. Any styling information is removed. + * Returns {@code null} if the attribute is not defined or is not + * an immediate string value. + * @throws RuntimeException if the TypedArray has already been recycled. */ public String getNonResourceString(int index) { if (mRecycled) { @@ -220,16 +240,17 @@ public class TypedArray { } /** - * @hide - * Retrieve the string value for the attribute at <var>index</var> that is + * Retrieves the string value for the attribute at <var>index</var> that is * not allowed to change with the given configurations. * * @param index Index of attribute to retrieve. * @param allowedChangingConfigs Bit mask of configurations from - * {@link Configuration}.NATIVE_CONFIG_* that are allowed to change. + * {@link Configuration}.NATIVE_CONFIG_* that are allowed to change. * - * @return String holding string data. Any styling information is - * removed. Returns null if the attribute is not defined. + * @return String holding string data. Any styling information is removed. + * Returns {@code null} if the attribute is not defined. + * @throws RuntimeException if the TypedArray has already been recycled. + * @hide */ public String getNonConfigurationString(int index, int allowedChangingConfigs) { if (mRecycled) { @@ -248,24 +269,33 @@ public class TypedArray { return loadStringValueAt(index).toString(); } - TypedValue v = mValue; + final TypedValue v = mValue; if (getValueAt(index, v)) { - Log.w(Resources.TAG, "Converting to string: " + v); - CharSequence cs = v.coerceToString(); + StrictMode.noteResourceMismatch(v); + final CharSequence cs = v.coerceToString(); return cs != null ? cs.toString() : null; } - Log.w(Resources.TAG, "getString of bad type: 0x" - + Integer.toHexString(type)); - return null; + + // We already checked for TYPE_NULL. This should never happen. + throw new RuntimeException("getNonConfigurationString of bad type: 0x" + + Integer.toHexString(type)); } /** * Retrieve the boolean value for the attribute at <var>index</var>. + * <p> + * If the attribute is an integer value, this method will return whether + * it is equal to zero. If the attribute is not a boolean or integer value, + * this method will attempt to coerce it to an integer using + * {@link Integer#decode(String)} and return whether it is equal to zero. * * @param index Index of attribute to retrieve. - * @param defValue Value to return if the attribute is not defined. + * @param defValue Value to return if the attribute is not defined or + * cannot be coerced to an integer. * - * @return Attribute boolean value, or defValue if not defined. + * @return Boolean value of the attribute, or defValue if the attribute was + * not defined or could not be coerced to an integer. + * @throws RuntimeException if the TypedArray has already been recycled. */ public boolean getBoolean(int index, boolean defValue) { if (mRecycled) { @@ -278,28 +308,33 @@ public class TypedArray { if (type == TypedValue.TYPE_NULL) { return defValue; } else if (type >= TypedValue.TYPE_FIRST_INT - && type <= TypedValue.TYPE_LAST_INT) { + && type <= TypedValue.TYPE_LAST_INT) { return data[index+AssetManager.STYLE_DATA] != 0; } - TypedValue v = mValue; + final TypedValue v = mValue; if (getValueAt(index, v)) { - Log.w(Resources.TAG, "Converting to boolean: " + v); - return XmlUtils.convertValueToBoolean( - v.coerceToString(), defValue); + StrictMode.noteResourceMismatch(v); + return XmlUtils.convertValueToBoolean(v.coerceToString(), defValue); } - Log.w(Resources.TAG, "getBoolean of bad type: 0x" - + Integer.toHexString(type)); - return defValue; + + // We already checked for TYPE_NULL. This should never happen. + throw new RuntimeException("getBoolean of bad type: 0x" + Integer.toHexString(type)); } /** * Retrieve the integer value for the attribute at <var>index</var>. + * <p> + * If the attribute is not an integer, this method will attempt to coerce + * it to an integer using {@link Integer#decode(String)}. * * @param index Index of attribute to retrieve. - * @param defValue Value to return if the attribute is not defined. + * @param defValue Value to return if the attribute is not defined or + * cannot be coerced to an integer. * - * @return Attribute int value, or defValue if not defined. + * @return Integer value of the attribute, or defValue if the attribute was + * not defined or could not be coerced to an integer. + * @throws RuntimeException if the TypedArray has already been recycled. */ public int getInt(int index, int defValue) { if (mRecycled) { @@ -312,27 +347,31 @@ public class TypedArray { if (type == TypedValue.TYPE_NULL) { return defValue; } else if (type >= TypedValue.TYPE_FIRST_INT - && type <= TypedValue.TYPE_LAST_INT) { + && type <= TypedValue.TYPE_LAST_INT) { return data[index+AssetManager.STYLE_DATA]; } - TypedValue v = mValue; + final TypedValue v = mValue; if (getValueAt(index, v)) { - Log.w(Resources.TAG, "Converting to int: " + v); - return XmlUtils.convertValueToInt( - v.coerceToString(), defValue); + StrictMode.noteResourceMismatch(v); + return XmlUtils.convertValueToInt(v.coerceToString(), defValue); } - Log.w(Resources.TAG, "getInt of bad type: 0x" - + Integer.toHexString(type)); - return defValue; + + // We already checked for TYPE_NULL. This should never happen. + throw new RuntimeException("getInt of bad type: 0x" + Integer.toHexString(type)); } /** * Retrieve the float value for the attribute at <var>index</var>. + * <p> + * If the attribute is not a float or an integer, this method will attempt + * to coerce it to a float using {@link Float#parseFloat(String)}. * * @param index Index of attribute to retrieve. * - * @return Attribute float value, or defValue if not defined.. + * @return Attribute float value, or defValue if the attribute was + * not defined or could not be coerced to a float. + * @throws RuntimeException if the TypedArray has already been recycled. */ public float getFloat(int index, float defValue) { if (mRecycled) { @@ -347,21 +386,21 @@ public class TypedArray { } else if (type == TypedValue.TYPE_FLOAT) { return Float.intBitsToFloat(data[index+AssetManager.STYLE_DATA]); } else if (type >= TypedValue.TYPE_FIRST_INT - && type <= TypedValue.TYPE_LAST_INT) { + && type <= TypedValue.TYPE_LAST_INT) { return data[index+AssetManager.STYLE_DATA]; } - TypedValue v = mValue; + final TypedValue v = mValue; if (getValueAt(index, v)) { - Log.w(Resources.TAG, "Converting to float: " + v); - CharSequence str = v.coerceToString(); + final CharSequence str = v.coerceToString(); if (str != null) { + StrictMode.noteResourceMismatch(v); return Float.parseFloat(str.toString()); } } - Log.w(Resources.TAG, "getFloat of bad type: 0x" - + Integer.toHexString(type)); - return defValue; + + // We already checked for TYPE_NULL. This should never happen. + throw new RuntimeException("getFloat of bad type: 0x" + Integer.toHexString(type)); } /** @@ -369,14 +408,21 @@ public class TypedArray { * the attribute references a color resource holding a complex * {@link android.content.res.ColorStateList}, then the default color from * the set is returned. + * <p> + * This method will throw an exception if the attribute is defined but is + * not an integer color or color state list. * * @param index Index of attribute to retrieve. * @param defValue Value to return if the attribute is not defined or * not a resource. * * @return Attribute color value, or defValue if not defined. + * @throws RuntimeException if the TypedArray has already been recycled. + * @throws UnsupportedOperationException if the attribute is defined but is + * not an integer color or color state list. */ - public int getColor(int index, int defValue) { + @ColorInt + public int getColor(int index, @ColorInt int defValue) { if (mRecycled) { throw new RuntimeException("Cannot make calls to a recycled instance!"); } @@ -387,18 +433,21 @@ public class TypedArray { if (type == TypedValue.TYPE_NULL) { return defValue; } else if (type >= TypedValue.TYPE_FIRST_INT - && type <= TypedValue.TYPE_LAST_INT) { + && type <= TypedValue.TYPE_LAST_INT) { return data[index+AssetManager.STYLE_DATA]; } else if (type == TypedValue.TYPE_STRING) { final TypedValue value = mValue; if (getValueAt(index, value)) { - ColorStateList csl = mResources.loadColorStateList( - value, value.resourceId); + final ColorStateList csl = mResources.loadColorStateList( + value, value.resourceId, mTheme); return csl.getDefaultColor(); } return defValue; } else if (type == TypedValue.TYPE_ATTRIBUTE) { - throw new RuntimeException("Failed to resolve attribute at index " + index); + final TypedValue value = mValue; + getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value); + throw new UnsupportedOperationException( + "Failed to resolve attribute at index " + index + ": " + value); } throw new UnsupportedOperationException("Can't convert to color: type=0x" @@ -408,12 +457,22 @@ public class TypedArray { /** * Retrieve the ColorStateList for the attribute at <var>index</var>. * The value may be either a single solid color or a reference to - * a color or complex {@link android.content.res.ColorStateList} description. + * a color or complex {@link android.content.res.ColorStateList} + * description. + * <p> + * This method will return {@code null} if the attribute is not defined or + * is not an integer color or color state list. * * @param index Index of attribute to retrieve. * - * @return ColorStateList for the attribute, or null if not defined. + * @return ColorStateList for the attribute, or {@code null} if not + * defined. + * @throws RuntimeException if the attribute if the TypedArray has already + * been recycled. + * @throws UnsupportedOperationException if the attribute is defined but is + * not an integer color or color state list. */ + @Nullable public ColorStateList getColorStateList(int index) { if (mRecycled) { throw new RuntimeException("Cannot make calls to a recycled instance!"); @@ -422,21 +481,28 @@ public class TypedArray { final TypedValue value = mValue; if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) { if (value.type == TypedValue.TYPE_ATTRIBUTE) { - throw new RuntimeException("Failed to resolve attribute at index " + index); + throw new UnsupportedOperationException( + "Failed to resolve attribute at index " + index + ": " + value); } - return mResources.loadColorStateList(value, value.resourceId); + return mResources.loadColorStateList(value, value.resourceId, mTheme); } return null; } /** * Retrieve the integer value for the attribute at <var>index</var>. + * <p> + * Unlike {@link #getInt(int, int)}, this method will throw an exception if + * the attribute is defined but is not an integer. * * @param index Index of attribute to retrieve. * @param defValue Value to return if the attribute is not defined or * not a resource. * * @return Attribute integer value, or defValue if not defined. + * @throws RuntimeException if the TypedArray has already been recycled. + * @throws UnsupportedOperationException if the attribute is defined but is + * not an integer. */ public int getInteger(int index, int defValue) { if (mRecycled) { @@ -449,10 +515,13 @@ public class TypedArray { if (type == TypedValue.TYPE_NULL) { return defValue; } else if (type >= TypedValue.TYPE_FIRST_INT - && type <= TypedValue.TYPE_LAST_INT) { + && type <= TypedValue.TYPE_LAST_INT) { return data[index+AssetManager.STYLE_DATA]; } else if (type == TypedValue.TYPE_ATTRIBUTE) { - throw new RuntimeException("Failed to resolve attribute at index " + index); + final TypedValue value = mValue; + getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value); + throw new UnsupportedOperationException( + "Failed to resolve attribute at index " + index + ": " + value); } throw new UnsupportedOperationException("Can't convert to integer: type=0x" @@ -460,17 +529,23 @@ public class TypedArray { } /** - * Retrieve a dimensional unit attribute at <var>index</var>. Unit + * Retrieve a dimensional unit attribute at <var>index</var>. Unit * conversions are based on the current {@link DisplayMetrics} * associated with the resources this {@link TypedArray} object * came from. + * <p> + * This method will throw an exception if the attribute is defined but is + * not a dimension. * * @param index Index of attribute to retrieve. * @param defValue Value to return if the attribute is not defined or * not a resource. * * @return Attribute dimension value multiplied by the appropriate - * metric, or defValue if not defined. + * metric, or defValue if not defined. + * @throws RuntimeException if the TypedArray has already been recycled. + * @throws UnsupportedOperationException if the attribute is defined but is + * not an integer. * * @see #getDimensionPixelOffset * @see #getDimensionPixelSize @@ -487,9 +562,12 @@ public class TypedArray { return defValue; } else if (type == TypedValue.TYPE_DIMENSION) { return TypedValue.complexToDimension( - data[index+AssetManager.STYLE_DATA], mMetrics); + data[index + AssetManager.STYLE_DATA], mMetrics); } else if (type == TypedValue.TYPE_ATTRIBUTE) { - throw new RuntimeException("Failed to resolve attribute at index " + index); + final TypedValue value = mValue; + getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value); + throw new UnsupportedOperationException( + "Failed to resolve attribute at index " + index + ": " + value); } throw new UnsupportedOperationException("Can't convert to dimension: type=0x" @@ -502,13 +580,19 @@ public class TypedArray { * {@link #getDimension}, except the returned value is converted to * integer pixels for you. An offset conversion involves simply * truncating the base value to an integer. + * <p> + * This method will throw an exception if the attribute is defined but is + * not a dimension. * * @param index Index of attribute to retrieve. * @param defValue Value to return if the attribute is not defined or * not a resource. * * @return Attribute dimension value multiplied by the appropriate - * metric and truncated to integer pixels, or defValue if not defined. + * metric and truncated to integer pixels, or defValue if not defined. + * @throws RuntimeException if the TypedArray has already been recycled. + * @throws UnsupportedOperationException if the attribute is defined but is + * not an integer. * * @see #getDimension * @see #getDimensionPixelSize @@ -525,9 +609,12 @@ public class TypedArray { return defValue; } else if (type == TypedValue.TYPE_DIMENSION) { return TypedValue.complexToDimensionPixelOffset( - data[index+AssetManager.STYLE_DATA], mMetrics); + data[index + AssetManager.STYLE_DATA], mMetrics); } else if (type == TypedValue.TYPE_ATTRIBUTE) { - throw new RuntimeException("Failed to resolve attribute at index " + index); + final TypedValue value = mValue; + getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value); + throw new UnsupportedOperationException( + "Failed to resolve attribute at index " + index + ": " + value); } throw new UnsupportedOperationException("Can't convert to dimension: type=0x" @@ -541,13 +628,19 @@ public class TypedArray { * integer pixels for use as a size. A size conversion involves * rounding the base value, and ensuring that a non-zero base value * is at least one pixel in size. + * <p> + * This method will throw an exception if the attribute is defined but is + * not a dimension. * * @param index Index of attribute to retrieve. * @param defValue Value to return if the attribute is not defined or * not a resource. * * @return Attribute dimension value multiplied by the appropriate - * metric and truncated to integer pixels, or defValue if not defined. + * metric and truncated to integer pixels, or defValue if not defined. + * @throws RuntimeException if the TypedArray has already been recycled. + * @throws UnsupportedOperationException if the attribute is defined but is + * not a dimension. * * @see #getDimension * @see #getDimensionPixelOffset @@ -566,7 +659,10 @@ public class TypedArray { return TypedValue.complexToDimensionPixelSize( data[index+AssetManager.STYLE_DATA], mMetrics); } else if (type == TypedValue.TYPE_ATTRIBUTE) { - throw new RuntimeException("Failed to resolve attribute at index " + index); + final TypedValue value = mValue; + getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value); + throw new UnsupportedOperationException( + "Failed to resolve attribute at index " + index + ": " + value); } throw new UnsupportedOperationException("Can't convert to dimension: type=0x" @@ -578,12 +674,18 @@ public class TypedArray { * {@link android.view.ViewGroup}'s layout_width and layout_height * attributes. This is only here for performance reasons; applications * should use {@link #getDimensionPixelSize}. + * <p> + * This method will throw an exception if the attribute is defined but is + * not a dimension or integer (enum). * * @param index Index of the attribute to retrieve. * @param name Textual name of attribute for error reporting. * * @return Attribute dimension value multiplied by the appropriate - * metric and truncated to integer pixels. + * metric and truncated to integer pixels. + * @throws RuntimeException if the TypedArray has already been recycled. + * @throws UnsupportedOperationException if the attribute is defined but is + * not a dimension or integer (enum). */ public int getLayoutDimension(int index, String name) { if (mRecycled) { @@ -600,10 +702,13 @@ public class TypedArray { return TypedValue.complexToDimensionPixelSize( data[index+AssetManager.STYLE_DATA], mMetrics); } else if (type == TypedValue.TYPE_ATTRIBUTE) { - throw new RuntimeException("Failed to resolve attribute at index " + index); + final TypedValue value = mValue; + getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value); + throw new UnsupportedOperationException( + "Failed to resolve attribute at index " + index + ": " + value); } - throw new RuntimeException(getPositionDescription() + throw new UnsupportedOperationException(getPositionDescription() + ": You must supply a " + name + " attribute."); } @@ -615,10 +720,11 @@ public class TypedArray { * * @param index Index of the attribute to retrieve. * @param defValue The default value to return if this attribute is not - * default or contains the wrong type of data. + * default or contains the wrong type of data. * * @return Attribute dimension value multiplied by the appropriate - * metric and truncated to integer pixels. + * metric and truncated to integer pixels. + * @throws RuntimeException if the TypedArray has already been recycled. */ public int getLayoutDimension(int index, int defValue) { if (mRecycled) { @@ -633,14 +739,14 @@ public class TypedArray { return data[index+AssetManager.STYLE_DATA]; } else if (type == TypedValue.TYPE_DIMENSION) { return TypedValue.complexToDimensionPixelSize( - data[index+AssetManager.STYLE_DATA], mMetrics); + data[index + AssetManager.STYLE_DATA], mMetrics); } return defValue; } /** - * Retrieve a fractional unit attribute at <var>index</var>. + * Retrieves a fractional unit attribute at <var>index</var>. * * @param index Index of attribute to retrieve. * @param base The base value of this fraction. In other words, a @@ -652,7 +758,10 @@ public class TypedArray { * not a resource. * * @return Attribute fractional value multiplied by the appropriate - * base value, or defValue if not defined. + * base value, or defValue if not defined. + * @throws RuntimeException if the TypedArray has already been recycled. + * @throws UnsupportedOperationException if the attribute is defined but is + * not a fraction. */ public float getFraction(int index, int base, int pbase, float defValue) { if (mRecycled) { @@ -668,7 +777,10 @@ public class TypedArray { return TypedValue.complexToFraction( data[index+AssetManager.STYLE_DATA], base, pbase); } else if (type == TypedValue.TYPE_ATTRIBUTE) { - throw new RuntimeException("Failed to resolve attribute at index " + index); + final TypedValue value = mValue; + getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value); + throw new UnsupportedOperationException( + "Failed to resolve attribute at index " + index + ": " + value); } throw new UnsupportedOperationException("Can't convert to fraction: type=0x" @@ -676,7 +788,7 @@ public class TypedArray { } /** - * Retrieve the resource identifier for the attribute at + * Retrieves the resource identifier for the attribute at * <var>index</var>. Note that attribute resource as resolved when * the overall {@link TypedArray} object is retrieved. As a * result, this function will return the resource identifier of the @@ -688,7 +800,9 @@ public class TypedArray { * not a resource. * * @return Attribute resource identifier, or defValue if not defined. + * @throws RuntimeException if the TypedArray has already been recycled. */ + @AnyRes public int getResourceId(int index, int defValue) { if (mRecycled) { throw new RuntimeException("Cannot make calls to a recycled instance!"); @@ -706,13 +820,15 @@ public class TypedArray { } /** - * Retrieve the theme attribute resource identifier for the attribute at + * Retrieves the theme attribute resource identifier for the attribute at * <var>index</var>. * * @param index Index of attribute to retrieve. * @param defValue Value to return if the attribute is not defined or not a - * resource. + * resource. + * * @return Theme attribute resource identifier, or defValue if not defined. + * @throws RuntimeException if the TypedArray has already been recycled. * @hide */ public int getThemeAttributeId(int index, int defValue) { @@ -730,10 +846,16 @@ public class TypedArray { /** * Retrieve the Drawable for the attribute at <var>index</var>. + * <p> + * This method will throw an exception if the attribute is defined but is + * not a color or drawable resource. * * @param index Index of attribute to retrieve. * - * @return Drawable for the attribute, or null if not defined. + * @return Drawable for the attribute, or {@code null} if not defined. + * @throws RuntimeException if the TypedArray has already been recycled. + * @throws UnsupportedOperationException if the attribute is defined but is + * not a color or drawable resource. */ @Nullable public Drawable getDrawable(int index) { @@ -744,7 +866,8 @@ public class TypedArray { final TypedValue value = mValue; if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) { if (value.type == TypedValue.TYPE_ATTRIBUTE) { - throw new RuntimeException("Failed to resolve attribute at index " + index); + throw new UnsupportedOperationException( + "Failed to resolve attribute at index " + index + ": " + value); } return mResources.loadDrawable(value, value.resourceId, mTheme); } @@ -756,10 +879,15 @@ public class TypedArray { * This gets the resource ID of the selected attribute, and uses * {@link Resources#getTextArray Resources.getTextArray} of the owning * Resources object to retrieve its String[]. + * <p> + * This method will throw an exception if the attribute is defined but is + * not a text array resource. * * @param index Index of attribute to retrieve. * - * @return CharSequence[] for the attribute, or null if not defined. + * @return CharSequence[] for the attribute, or {@code null} if not + * defined. + * @throws RuntimeException if the TypedArray has already been recycled. */ public CharSequence[] getTextArray(int index) { if (mRecycled) { @@ -780,7 +908,8 @@ public class TypedArray { * @param outValue TypedValue object in which to place the attribute's * data. * - * @return Returns true if the value was retrieved, else false. + * @return {@code true} if the value was retrieved, false otherwise. + * @throws RuntimeException if the TypedArray has already been recycled. */ public boolean getValue(int index, TypedValue outValue) { if (mRecycled) { @@ -794,7 +923,9 @@ public class TypedArray { * Returns the type of attribute at the specified index. * * @param index Index of attribute whose type to retrieve. + * * @return Attribute type. + * @throws RuntimeException if the TypedArray has already been recycled. */ public int getType(int index) { if (mRecycled) { @@ -814,6 +945,7 @@ public class TypedArray { * @param index Index of attribute to retrieve. * * @return True if the attribute has a value, false otherwise. + * @throws RuntimeException if the TypedArray has already been recycled. */ public boolean hasValue(int index) { if (mRecycled) { @@ -834,6 +966,7 @@ public class TypedArray { * @param index Index of attribute to retrieve. * * @return True if the attribute has a value or is empty, false otherwise. + * @throws RuntimeException if the TypedArray has already been recycled. */ public boolean hasValueOrEmpty(int index) { if (mRecycled) { @@ -857,6 +990,7 @@ public class TypedArray { * @return Returns a TypedValue object if the attribute is defined, * containing its data; otherwise returns null. (You will not * receive a TypedValue whose type is TYPE_NULL.) + * @throws RuntimeException if the TypedArray has already been recycled. */ public TypedValue peekValue(int index) { if (mRecycled) { @@ -872,6 +1006,9 @@ public class TypedArray { /** * Returns a message about the parser state suitable for printing error messages. + * + * @return Human-readable description of current parser state. + * @throws RuntimeException if the TypedArray has already been recycled. */ public String getPositionDescription() { if (mRecycled) { @@ -882,8 +1019,10 @@ public class TypedArray { } /** - * Recycle the TypedArray, to be re-used by a later caller. After calling + * Recycles the TypedArray, to be re-used by a later caller. After calling * this function you must not ever touch the typed array again. + * + * @throws RuntimeException if the TypedArray has already been recycled. */ public void recycle() { if (mRecycled) { @@ -908,9 +1047,19 @@ public class TypedArray { * @return an array of length {@link #getIndexCount()} populated with theme * attributes, or null if there are no theme attributes in the typed * array + * @throws RuntimeException if the TypedArray has already been recycled. * @hide */ + @Nullable public int[] extractThemeAttrs() { + return extractThemeAttrs(null); + } + + /** + * @hide + */ + @Nullable + public int[] extractThemeAttrs(@Nullable int[] scrap) { if (mRecycled) { throw new RuntimeException("Cannot make calls to a recycled instance!"); } @@ -922,6 +1071,7 @@ public class TypedArray { for (int i = 0; i < N; i++) { final int index = i * AssetManager.STYLE_NUM_ENTRIES; if (data[index + AssetManager.STYLE_TYPE] != TypedValue.TYPE_ATTRIBUTE) { + // Not an attribute, ignore. continue; } @@ -930,13 +1080,20 @@ public class TypedArray { final int attr = data[index + AssetManager.STYLE_DATA]; if (attr == 0) { - // This attribute is useless! + // Useless data, ignore. continue; } + // Ensure we have a usable attribute array. if (attrs == null) { - attrs = new int[N]; + if (scrap != null && scrap.length == N) { + attrs = scrap; + Arrays.fill(attrs, 0); + } else { + attrs = new int[N]; + } } + attrs[i] = attr; } @@ -949,9 +1106,14 @@ public class TypedArray { * * @return Returns a mask of the changing configuration parameters, as * defined by {@link android.content.pm.ActivityInfo}. + * @throws RuntimeException if the TypedArray has already been recycled. * @see android.content.pm.ActivityInfo */ public int getChangingConfigurations() { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + int changingConfig = 0; final int[] data = mData; diff --git a/core/java/android/database/DatabaseUtils.java b/core/java/android/database/DatabaseUtils.java index c125544..e61664c 100644 --- a/core/java/android/database/DatabaseUtils.java +++ b/core/java/android/database/DatabaseUtils.java @@ -16,8 +16,6 @@ package android.database; -import org.apache.commons.codec.binary.Hex; - import android.content.ContentValues; import android.content.Context; import android.content.OperationApplicationException; @@ -416,11 +414,33 @@ public class DatabaseUtils { * @return the collation key in hex format */ public static String getHexCollationKey(String name) { - byte [] arr = getCollationKeyInBytes(name); - char[] keys = Hex.encodeHex(arr); + byte[] arr = getCollationKeyInBytes(name); + char[] keys = encodeHex(arr); return new String(keys, 0, getKeyLen(arr) * 2); } + + /** + * Used building output as Hex + */ + private static final char[] DIGITS = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' + }; + + private static char[] encodeHex(byte[] input) { + int l = input.length; + char[] out = new char[l << 1]; + + // two characters form the hex value. + for (int i = 0, j = 0; i < l; i++) { + out[j++] = DIGITS[(0xF0 & input[i]) >>> 4 ]; + out[j++] = DIGITS[ 0x0F & input[i] ]; + } + + return out; + } + private static int getKeyLen(byte[] arr) { if (arr[arr.length - 1] != 0) { return arr.length; diff --git a/core/java/android/gesture/GestureLibraries.java b/core/java/android/gesture/GestureLibraries.java index 6d6c156..611d9ab 100644 --- a/core/java/android/gesture/GestureLibraries.java +++ b/core/java/android/gesture/GestureLibraries.java @@ -16,6 +16,7 @@ package android.gesture; +import android.annotation.RawRes; import android.util.Log; import static android.gesture.GestureConstants.*; import android.content.Context; @@ -44,7 +45,7 @@ public final class GestureLibraries { return fromFile(context.getFileStreamPath(name)); } - public static GestureLibrary fromRawResource(Context context, int resourceId) { + public static GestureLibrary fromRawResource(Context context, @RawRes int resourceId) { return new ResourceGestureLibrary(context, resourceId); } diff --git a/core/java/android/gesture/GestureOverlayView.java b/core/java/android/gesture/GestureOverlayView.java index e1a2a25..e0d454c 100644 --- a/core/java/android/gesture/GestureOverlayView.java +++ b/core/java/android/gesture/GestureOverlayView.java @@ -16,6 +16,7 @@ package android.gesture; +import android.annotation.ColorInt; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; @@ -204,18 +205,20 @@ public class GestureOverlayView extends FrameLayout { mOrientation = orientation; } - public void setGestureColor(int color) { + public void setGestureColor(@ColorInt int color) { mCertainGestureColor = color; } - public void setUncertainGestureColor(int color) { + public void setUncertainGestureColor(@ColorInt int color) { mUncertainGestureColor = color; } + @ColorInt public int getUncertainGestureColor() { return mUncertainGestureColor; } + @ColorInt public int getGestureColor() { return mCertainGestureColor; } diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java index 310ab76..49f6513 100644 --- a/core/java/android/hardware/Camera.java +++ b/core/java/android/hardware/Camera.java @@ -44,7 +44,6 @@ import android.view.SurfaceHolder; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.ArrayList; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; diff --git a/core/java/android/hardware/ICameraService.aidl b/core/java/android/hardware/ICameraService.aidl index 2bc3dd4..9aede01 100644 --- a/core/java/android/hardware/ICameraService.aidl +++ b/core/java/android/hardware/ICameraService.aidl @@ -81,4 +81,6 @@ interface ICameraService int clientUid, // Container for an ICamera object out BinderHolder device); + + int setTorchMode(String CameraId, boolean enabled, IBinder clientBinder); } diff --git a/core/java/android/hardware/ICameraServiceListener.aidl b/core/java/android/hardware/ICameraServiceListener.aidl index c548496..49278b6 100644 --- a/core/java/android/hardware/ICameraServiceListener.aidl +++ b/core/java/android/hardware/ICameraServiceListener.aidl @@ -23,4 +23,6 @@ interface ICameraServiceListener * Keep up-to-date with frameworks/av/include/camera/ICameraServiceListener.h */ void onStatusChanged(int status, int cameraId); + + void onTorchStatusChanged(int status, String cameraId); } diff --git a/core/java/android/hardware/Sensor.java b/core/java/android/hardware/Sensor.java index fa5e9d2..39f4cca 100644 --- a/core/java/android/hardware/Sensor.java +++ b/core/java/android/hardware/Sensor.java @@ -833,4 +833,96 @@ public final class Sensor { + ", type=" + mType + ", maxRange=" + mMaxRange + ", resolution=" + mResolution + ", power=" + mPower + ", minDelay=" + mMinDelay + "}"; } + + /** + * Sets the Type associated with the sensor. + * NOTE: to be used only by native bindings in SensorManager. + * + * This allows interned static strings to be used across all representations of the Sensor. If + * a sensor type is not referenced here, it will still be interned by the native SensorManager. + * + * @return {@code true} if the StringType was successfully set, {@code false} otherwise. + */ + private boolean setType(int value) { + mType = value; + switch (mType) { + case TYPE_ACCELEROMETER: + mStringType = STRING_TYPE_ACCELEROMETER; + return true; + case TYPE_AMBIENT_TEMPERATURE: + mStringType = STRING_TYPE_AMBIENT_TEMPERATURE; + return true; + case TYPE_GAME_ROTATION_VECTOR: + mStringType = STRING_TYPE_GAME_ROTATION_VECTOR; + return true; + case TYPE_GEOMAGNETIC_ROTATION_VECTOR: + mStringType = STRING_TYPE_GEOMAGNETIC_ROTATION_VECTOR; + return true; + case TYPE_GLANCE_GESTURE: + mStringType = STRING_TYPE_GLANCE_GESTURE; + return true; + case TYPE_GRAVITY: + mStringType = STRING_TYPE_GRAVITY; + return true; + case TYPE_GYROSCOPE: + mStringType = STRING_TYPE_GYROSCOPE; + return true; + case TYPE_GYROSCOPE_UNCALIBRATED: + mStringType = STRING_TYPE_GYROSCOPE_UNCALIBRATED; + return true; + case TYPE_HEART_RATE: + mStringType = STRING_TYPE_HEART_RATE; + return true; + case TYPE_LIGHT: + mStringType = STRING_TYPE_LIGHT; + return true; + case TYPE_LINEAR_ACCELERATION: + mStringType = STRING_TYPE_LINEAR_ACCELERATION; + return true; + case TYPE_MAGNETIC_FIELD: + mStringType = STRING_TYPE_MAGNETIC_FIELD; + return true; + case TYPE_MAGNETIC_FIELD_UNCALIBRATED: + mStringType = STRING_TYPE_MAGNETIC_FIELD_UNCALIBRATED; + return true; + case TYPE_PICK_UP_GESTURE: + mStringType = STRING_TYPE_PICK_UP_GESTURE; + return true; + case TYPE_PRESSURE: + mStringType = STRING_TYPE_PRESSURE; + return true; + case TYPE_PROXIMITY: + mStringType = STRING_TYPE_PROXIMITY; + return true; + case TYPE_RELATIVE_HUMIDITY: + mStringType = STRING_TYPE_RELATIVE_HUMIDITY; + return true; + case TYPE_ROTATION_VECTOR: + mStringType = STRING_TYPE_ROTATION_VECTOR; + return true; + case TYPE_SIGNIFICANT_MOTION: + mStringType = STRING_TYPE_SIGNIFICANT_MOTION; + return true; + case TYPE_STEP_COUNTER: + mStringType = STRING_TYPE_STEP_COUNTER; + return true; + case TYPE_STEP_DETECTOR: + mStringType = STRING_TYPE_STEP_DETECTOR; + return true; + case TYPE_TILT_DETECTOR: + mStringType = SENSOR_STRING_TYPE_TILT_DETECTOR; + return true; + case TYPE_WAKE_GESTURE: + mStringType = STRING_TYPE_WAKE_GESTURE; + return true; + case TYPE_ORIENTATION: + mStringType = STRING_TYPE_ORIENTATION; + return true; + case TYPE_TEMPERATURE: + mStringType = STRING_TYPE_TEMPERATURE; + return true; + default: + return false; + } + } } diff --git a/core/java/android/hardware/camera2/CameraAccessException.java b/core/java/android/hardware/camera2/CameraAccessException.java index 91ef6ca..84e9912 100644 --- a/core/java/android/hardware/camera2/CameraAccessException.java +++ b/core/java/android/hardware/camera2/CameraAccessException.java @@ -28,16 +28,14 @@ import android.util.AndroidException; */ public class CameraAccessException extends AndroidException { /** - * The camera device is in use already - * @hide + * The camera device is in use already. */ public static final int CAMERA_IN_USE = 4; /** - * The system-wide limit for number of open cameras has been reached, - * and more camera devices cannot be opened until previous instances are - * closed. - * @hide + * The system-wide limit for number of open cameras or camera resources has + * been reached, and more camera devices cannot be opened or torch mode + * cannot be turned on until previous instances are closed. */ public static final int MAX_CAMERAS_IN_USE = 5; diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java index 7cf8fb0..a0217c2 100644 --- a/core/java/android/hardware/camera2/CameraCharacteristics.java +++ b/core/java/android/hardware/camera2/CameraCharacteristics.java @@ -638,6 +638,44 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri new Key<android.hardware.camera2.params.HighSpeedVideoConfiguration[]>("android.control.availableHighSpeedVideoConfigurations", android.hardware.camera2.params.HighSpeedVideoConfiguration[].class); /** + * <p>Whether the camera device supports {@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock}</p> + * <p>LIMITED or FULL devices will always list <code>true</code></p> + * <p>This key is available on all devices.</p> + * + * @see CaptureRequest#CONTROL_AE_LOCK + */ + @PublicKey + public static final Key<Boolean> CONTROL_AE_LOCK_AVAILABLE = + new Key<Boolean>("android.control.aeLockAvailable", boolean.class); + + /** + * <p>Whether the camera device supports {@link CaptureRequest#CONTROL_AWB_LOCK android.control.awbLock}</p> + * <p>LIMITED or FULL devices will always list <code>true</code></p> + * <p>This key is available on all devices.</p> + * + * @see CaptureRequest#CONTROL_AWB_LOCK + */ + @PublicKey + public static final Key<Boolean> CONTROL_AWB_LOCK_AVAILABLE = + new Key<Boolean>("android.control.awbLockAvailable", boolean.class); + + /** + * <p>List of control modes for {@link CaptureRequest#CONTROL_MODE android.control.mode} that are supported by this camera + * device.</p> + * <p>This list contains control modes that can be set for the camera device. + * LEGACY mode devices will always support AUTO mode. LIMITED and FULL + * devices will always support OFF, AUTO modes.</p> + * <p><b>Range of valid values:</b><br> + * Any value listed in {@link CaptureRequest#CONTROL_MODE android.control.mode}</p> + * <p>This key is available on all devices.</p> + * + * @see CaptureRequest#CONTROL_MODE + */ + @PublicKey + public static final Key<int[]> CONTROL_AVAILABLE_MODES = + new Key<int[]>("android.control.availableModes", int[].class); + + /** * <p>List of edge enhancement modes for {@link CaptureRequest#EDGE_MODE android.edge.mode} that are supported by this camera * device.</p> * <p>Full-capability camera devices must always support OFF; all devices will list FAST.</p> @@ -889,10 +927,12 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri * <ul> * <li>{@link #LENS_FACING_FRONT FRONT}</li> * <li>{@link #LENS_FACING_BACK BACK}</li> + * <li>{@link #LENS_FACING_EXTERNAL EXTERNAL}</li> * </ul></p> * <p>This key is available on all devices.</p> * @see #LENS_FACING_FRONT * @see #LENS_FACING_BACK + * @see #LENS_FACING_EXTERNAL */ @PublicKey public static final Key<Integer> LENS_FACING = @@ -1069,8 +1109,11 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri * android.scaler.availableInputOutputFormatsMap. When using an * input stream, there must be at least one output stream * configured to to receive the reprocessed images.</p> + * <p>When an input stream and some output streams are used in a reprocessing request, + * only the input buffer will be used to produce these output stream buffers, and a + * new sensor image will not be captured.</p> * <p>For example, for Zero Shutter Lag (ZSL) still capture use case, the input - * stream image format will be RAW_OPAQUE, the associated output stream image format + * stream image format will be OPAQUE, the associated output stream image format * should be JPEG.</p> * <p><b>Range of valid values:</b><br></p> * <p>0 or 1.</p> @@ -1080,8 +1123,8 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p> * * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL - * @hide */ + @PublicKey public static final Key<Integer> REQUEST_MAX_NUM_INPUT_STREAMS = new Key<Integer>("android.request.maxNumInputStreams", int.class); @@ -1157,8 +1200,10 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri * <li>{@link #REQUEST_AVAILABLE_CAPABILITIES_MANUAL_SENSOR MANUAL_SENSOR}</li> * <li>{@link #REQUEST_AVAILABLE_CAPABILITIES_MANUAL_POST_PROCESSING MANUAL_POST_PROCESSING}</li> * <li>{@link #REQUEST_AVAILABLE_CAPABILITIES_RAW RAW}</li> + * <li>{@link #REQUEST_AVAILABLE_CAPABILITIES_OPAQUE_REPROCESSING OPAQUE_REPROCESSING}</li> * <li>{@link #REQUEST_AVAILABLE_CAPABILITIES_READ_SENSOR_SETTINGS READ_SENSOR_SETTINGS}</li> * <li>{@link #REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE BURST_CAPTURE}</li> + * <li>{@link #REQUEST_AVAILABLE_CAPABILITIES_YUV_REPROCESSING YUV_REPROCESSING}</li> * </ul></p> * <p>This key is available on all devices.</p> * @@ -1167,8 +1212,10 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri * @see #REQUEST_AVAILABLE_CAPABILITIES_MANUAL_SENSOR * @see #REQUEST_AVAILABLE_CAPABILITIES_MANUAL_POST_PROCESSING * @see #REQUEST_AVAILABLE_CAPABILITIES_RAW + * @see #REQUEST_AVAILABLE_CAPABILITIES_OPAQUE_REPROCESSING * @see #REQUEST_AVAILABLE_CAPABILITIES_READ_SENSOR_SETTINGS * @see #REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE + * @see #REQUEST_AVAILABLE_CAPABILITIES_YUV_REPROCESSING */ @PublicKey public static final Key<int[]> REQUEST_AVAILABLE_CAPABILITIES = @@ -1345,10 +1392,10 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri * <p>The mapping of image formats that are supported by this * camera device for input streams, to their corresponding output formats.</p> * <p>All camera devices with at least 1 - * android.request.maxNumInputStreams will have at least one + * {@link CameraCharacteristics#REQUEST_MAX_NUM_INPUT_STREAMS android.request.maxNumInputStreams} will have at least one * available input format.</p> * <p>The camera device will support the following map of formats, - * if its dependent capability is supported:</p> + * if its dependent capability ({@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities}) is supported:</p> * <table> * <thead> * <tr> @@ -1359,45 +1406,42 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri * </thead> * <tbody> * <tr> - * <td align="left">RAW_OPAQUE</td> + * <td align="left">OPAQUE</td> * <td align="left">JPEG</td> - * <td align="left">ZSL</td> + * <td align="left">OPAQUE_REPROCESSING</td> * </tr> * <tr> - * <td align="left">RAW_OPAQUE</td> + * <td align="left">OPAQUE</td> * <td align="left">YUV_420_888</td> - * <td align="left">ZSL</td> - * </tr> - * <tr> - * <td align="left">RAW_OPAQUE</td> - * <td align="left">RAW16</td> - * <td align="left">RAW</td> + * <td align="left">OPAQUE_REPROCESSING</td> * </tr> * <tr> - * <td align="left">RAW16</td> * <td align="left">YUV_420_888</td> - * <td align="left">RAW</td> + * <td align="left">JPEG</td> + * <td align="left">YUV_REPROCESSING</td> * </tr> * <tr> - * <td align="left">RAW16</td> - * <td align="left">JPEG</td> - * <td align="left">RAW</td> + * <td align="left">YUV_420_888</td> + * <td align="left">YUV_420_888</td> + * <td align="left">YUV_REPROCESSING</td> * </tr> * </tbody> * </table> - * <p>For ZSL-capable camera devices, using the RAW_OPAQUE format + * <p>OPAQUE refers to a device-internal format that is not directly application-visible. + * An OPAQUE input or output surface can be acquired by + * OpaqueImageRingBufferQueue#getInputSurface() or + * OpaqueImageRingBufferQueue#getOutputSurface(). + * For a OPAQUE_REPROCESSING-capable camera device, using the OPAQUE format * as either input or output will never hurt maximum frame rate (i.e. - * StreamConfigurationMap#getOutputStallDuration(int,Size) - * for a <code>format =</code> RAW_OPAQUE is always 0).</p> + * StreamConfigurationMap#getOutputStallDuration(klass,Size) is always 0), + * where klass is android.media.OpaqueImageRingBufferQueue.class.</p> * <p>Attempting to configure an input stream with output streams not * listed as available in this map is not valid.</p> * <p>TODO: typedef to ReprocessFormatMap</p> * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> - * <p><b>Full capability</b> - - * Present on all camera devices that report being {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_FULL HARDWARE_LEVEL_FULL} devices in the - * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p> * - * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL + * @see CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES + * @see CameraCharacteristics#REQUEST_MAX_NUM_INPUT_STREAMS * @hide */ public static final Key<int[]> SCALER_AVAILABLE_INPUT_OUTPUT_FORMATS_MAP = @@ -1899,6 +1943,23 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri new Key<Integer>("android.sensor.info.timestampSource", int.class); /** + * <p>Whether the RAW images output from this camera device are subject to + * lens shading correction.</p> + * <p>If TRUE, all images produced by the camera device in the RAW image formats will + * have lens shading correction already applied to it. If FALSE, the images will + * not be adjusted for lens shading correction. + * See {@link CameraCharacteristics#REQUEST_MAX_NUM_OUTPUT_RAW android.request.maxNumOutputRaw} for a list of RAW image formats.</p> + * <p>This key will be <code>null</code> for all devices do not report this information. + * Devices with RAW capability will always report this information in this key.</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * + * @see CameraCharacteristics#REQUEST_MAX_NUM_OUTPUT_RAW + */ + @PublicKey + public static final Key<Boolean> SENSOR_INFO_LENS_SHADING_APPLIED = + new Key<Boolean>("android.sensor.info.lensShadingApplied", boolean.class); + + /** * <p>The standard reference illuminant used as the scene light source when * calculating the {@link CameraCharacteristics#SENSOR_COLOR_TRANSFORM1 android.sensor.colorTransform1}, * {@link CameraCharacteristics#SENSOR_CALIBRATION_TRANSFORM1 android.sensor.calibrationTransform1}, and @@ -2189,6 +2250,22 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri new Key<int[]>("android.sensor.availableTestPatternModes", int[].class); /** + * <p>List of lens shading modes for {@link CaptureRequest#SHADING_MODE android.shading.mode} that are supported by this camera device.</p> + * <p>This list contains lens shading modes that can be set for the camera device. + * Camera devices that support the MANUAL_POST_PROCESSING capability will always + * list OFF and FAST mode. This includes all FULL level devices. + * LEGACY devices will always only support FAST mode.</p> + * <p><b>Range of valid values:</b><br> + * Any value listed in {@link CaptureRequest#SHADING_MODE android.shading.mode}</p> + * <p>This key is available on all devices.</p> + * + * @see CaptureRequest#SHADING_MODE + */ + @PublicKey + public static final Key<int[]> SHADING_AVAILABLE_MODES = + new Key<int[]>("android.shading.availableModes", int[].class); + + /** * <p>List of face detection modes for {@link CaptureRequest#STATISTICS_FACE_DETECT_MODE android.statistics.faceDetectMode} that are * supported by this camera device.</p> * <p>OFF is always supported.</p> @@ -2232,6 +2309,23 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri new Key<boolean[]>("android.statistics.info.availableHotPixelMapModes", boolean[].class); /** + * <p>List of lens shading map output modes for {@link CaptureRequest#STATISTICS_LENS_SHADING_MAP_MODE android.statistics.lensShadingMapMode} that + * are supported by this camera device.</p> + * <p>If no lens shading map output is available for this camera device, this key will + * contain only OFF.</p> + * <p>ON is always supported on devices with the RAW capability. + * LEGACY mode devices will always only support OFF.</p> + * <p><b>Range of valid values:</b><br> + * Any value listed in {@link CaptureRequest#STATISTICS_LENS_SHADING_MAP_MODE android.statistics.lensShadingMapMode}</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * + * @see CaptureRequest#STATISTICS_LENS_SHADING_MAP_MODE + */ + @PublicKey + public static final Key<byte[]> STATISTICS_INFO_AVAILABLE_LENS_SHADING_MAP_MODES = + new Key<byte[]>("android.statistics.info.availableLensShadingMapModes", byte[].class); + + /** * <p>Maximum number of supported points in the * tonemap curve that can be used for {@link CaptureRequest#TONEMAP_CURVE android.tonemap.curve}.</p> * <p>If the actual number of points provided by the application (in {@link CaptureRequest#TONEMAP_CURVE android.tonemap.curve}*) is @@ -2255,8 +2349,13 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri /** * <p>List of tonemapping modes for {@link CaptureRequest#TONEMAP_MODE android.tonemap.mode} that are supported by this camera * device.</p> - * <p>Camera devices that support the MANUAL_POST_PROCESSING capability will always list - * CONTRAST_CURVE and FAST. This includes all FULL level devices.</p> + * <p>Camera devices that support the MANUAL_POST_PROCESSING capability will always contain + * at least one of below mode combinations:</p> + * <ul> + * <li>CONTRAST_CURVE and FAST</li> + * <li>GAMMA_VALUE, PRESET_CURVE, and FAST</li> + * </ul> + * <p>This includes all FULL level devices.</p> * <p><b>Range of valid values:</b><br> * Any value listed in {@link CaptureRequest#TONEMAP_MODE android.tonemap.mode}</p> * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> @@ -2394,6 +2493,83 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri public static final Key<Integer> SYNC_MAX_LATENCY = new Key<Integer>("android.sync.maxLatency", int.class); + /** + * <p>The available depth dataspace stream + * configurations that this camera device supports + * (i.e. format, width, height, output/input stream).</p> + * <p>These are output stream configurations for use with + * dataSpace HAL_DATASPACE_DEPTH. The configurations are + * listed as <code>(format, width, height, input?)</code> tuples.</p> + * <p>Only devices that support depth output for at least + * the HAL_PIXEL_FORMAT_Y16 dense depth map may include + * this entry.</p> + * <p>A device that also supports the HAL_PIXEL_FORMAT_BLOB + * sparse depth point cloud must report a single entry for + * the format in this list as <code>(HAL_PIXEL_FORMAT_BLOB, + * android.depth.maxDepthSamples, 1, OUTPUT)</code> in addition to + * the entries for HAL_PIXEL_FORMAT_Y16.</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * <p><b>Limited capability</b> - + * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the + * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p> + * + * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL + * @hide + */ + public static final Key<android.hardware.camera2.params.StreamConfiguration[]> DEPTH_AVAILABLE_DEPTH_STREAM_CONFIGURATIONS = + new Key<android.hardware.camera2.params.StreamConfiguration[]>("android.depth.availableDepthStreamConfigurations", android.hardware.camera2.params.StreamConfiguration[].class); + + /** + * <p>This lists the minimum frame duration for each + * format/size combination for depth output formats.</p> + * <p>This should correspond to the frame duration when only that + * stream is active, with all processing (typically in android.*.mode) + * set to either OFF or FAST.</p> + * <p>When multiple streams are used in a request, the minimum frame + * duration will be max(individual stream min durations).</p> + * <p>The minimum frame duration of a stream (of a particular format, size) + * is the same regardless of whether the stream is input or output.</p> + * <p>See {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration} and + * android.scaler.availableStallDurations for more details about + * calculating the max frame rate.</p> + * <p>(Keep in sync with + * StreamConfigurationMap#getOutputMinFrameDuration)</p> + * <p><b>Units</b>: (format, width, height, ns) x n</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * <p><b>Limited capability</b> - + * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the + * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p> + * + * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL + * @see CaptureRequest#SENSOR_FRAME_DURATION + * @hide + */ + public static final Key<android.hardware.camera2.params.StreamConfigurationDuration[]> DEPTH_AVAILABLE_DEPTH_MIN_FRAME_DURATIONS = + new Key<android.hardware.camera2.params.StreamConfigurationDuration[]>("android.depth.availableDepthMinFrameDurations", android.hardware.camera2.params.StreamConfigurationDuration[].class); + + /** + * <p>This lists the maximum stall duration for each + * format/size combination for depth streams.</p> + * <p>A stall duration is how much extra time would get added + * to the normal minimum frame duration for a repeating request + * that has streams with non-zero stall.</p> + * <p>This functions similarly to + * android.scaler.availableStallDurations for depth + * streams.</p> + * <p>All depth output stream formats may have a nonzero stall + * duration.</p> + * <p><b>Units</b>: (format, width, height, ns) x n</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * <p><b>Limited capability</b> - + * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the + * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p> + * + * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL + * @hide + */ + public static final Key<android.hardware.camera2.params.StreamConfigurationDuration[]> DEPTH_AVAILABLE_DEPTH_STALL_DURATIONS = + new Key<android.hardware.camera2.params.StreamConfigurationDuration[]>("android.depth.availableDepthStallDurations", android.hardware.camera2.params.StreamConfigurationDuration[].class); + /*~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~ * End generated code *~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~O@*/ diff --git a/core/java/android/hardware/camera2/CameraDevice.java b/core/java/android/hardware/camera2/CameraDevice.java index bec9489..fd4cf3c 100644 --- a/core/java/android/hardware/camera2/CameraDevice.java +++ b/core/java/android/hardware/camera2/CameraDevice.java @@ -17,7 +17,7 @@ package android.hardware.camera2; import android.hardware.camera2.params.StreamConfigurationMap; -import android.graphics.ImageFormat; +import android.hardware.camera2.params.OutputConfiguration; import android.os.Handler; import android.view.Surface; @@ -381,6 +381,20 @@ public abstract class CameraDevice implements AutoCloseable { throws CameraAccessException; /** + * <p>Create a new camera capture session by providing the target output set of Surfaces and + * its corresponding surface configuration to the camera device.</p> + * + * @see #createCaptureSession + * @see OutputConfiguration + * + * @hide + */ + public abstract void createCaptureSessionByOutputConfiguration( + List<OutputConfiguration> outputConfigurations, + CameraCaptureSession.StateCallback callback, Handler handler) + throws CameraAccessException; + + /** * <p>Create a {@link CaptureRequest.Builder} for new capture requests, * initialized with template for a target use case. The settings are chosen * to be the best options for the specific camera device, so it is not diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index a25b94a..b513379 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -27,6 +27,7 @@ import android.hardware.camera2.utils.CameraServiceBinderDecorator; import android.hardware.camera2.utils.CameraRuntimeException; import android.hardware.camera2.utils.BinderHolder; import android.os.IBinder; +import android.os.Binder; import android.os.Handler; import android.os.Looper; import android.os.RemoteException; @@ -109,8 +110,11 @@ public final class CameraManager { * of the state of individual CameraManager instances.</p> * * @param callback the new callback to send camera availability notices to - * @param handler The handler on which the callback should be invoked, or - * {@code null} to use the current thread's {@link android.os.Looper looper}. + * @param handler The handler on which the callback should be invoked, or {@code null} to use + * the current thread's {@link android.os.Looper looper}. + * + * @throws IllegalArgumentException if the handler is {@code null} but the current thread has + * no looper. */ public void registerAvailabilityCallback(AvailabilityCallback callback, Handler handler) { if (handler == null) { @@ -138,6 +142,52 @@ public final class CameraManager { } /** + * Register a callback to be notified about torch mode status. + * + * <p>Registering the same callback again will replace the handler with the + * new one provided.</p> + * + * <p>The first time a callback is registered, it is immediately called + * with the torch mode status of all currently known camera devices.</p> + * + * <p>Since this callback will be registered with the camera service, remember to unregister it + * once it is no longer needed; otherwise the callback will continue to receive events + * indefinitely and it may prevent other resources from being released. Specifically, the + * callbacks will be invoked independently of the general activity lifecycle and independently + * of the state of individual CameraManager instances.</p> + * + * @param callback The new callback to send torch mode status to + * @param handler The handler on which the callback should be invoked, or {@code null} to use + * the current thread's {@link android.os.Looper looper}. + * + * @throws IllegalArgumentException if the handler is {@code null} but the current thread has + * no looper. + */ + public void registerTorchCallback(TorchCallback callback, Handler handler) { + if (handler == null) { + Looper looper = Looper.myLooper(); + if (looper == null) { + throw new IllegalArgumentException( + "No handler given, and current thread has no looper!"); + } + handler = new Handler(looper); + } + CameraManagerGlobal.get().registerTorchCallback(callback, handler); + } + + /** + * Remove a previously-added callback; the callback will no longer receive torch mode status + * callbacks. + * + * <p>Removing a callback that isn't registered has no effect.</p> + * + * @param callback The callback to remove from the notification list + */ + public void unregisterTorchCallback(TorchCallback callback) { + CameraManagerGlobal.get().unregisterTorchCallback(callback); + } + + /** * <p>Query the capabilities of a camera device. These capabilities are * immutable for a given camera.</p> * @@ -384,6 +434,49 @@ public final class CameraManager { } /** + * Set the flash unit's torch mode of the camera of the given ID without opening the camera + * device. + * + * <p>Use {@link #getCameraIdList} to get the list of available camera devices and use + * {@link #getCameraCharacteristics} to check whether the camera device has a flash unit. + * Note that even if a camera device has a flash unit, turning on the torch mode may fail + * if the camera device or other camera resources needed to turn on the torch mode are in use. + * </p> + * + * <p> If {@link #setTorchMode} is called to turn on or off the torch mode successfully, + * {@link CameraManager.TorchCallback#onTorchModeChanged} will be invoked. + * However, even if turning on the torch mode is successful, the application does not have the + * exclusive ownership of the flash unit or the camera device. The torch mode will be turned + * off and becomes unavailable when the camera device that the flash unit belongs to becomes + * unavailable or when other camera resources to keep the torch on become unavailable ( + * {@link CameraManager.TorchCallback#onTorchModeUnavailable} will be invoked). Also, + * other applications are free to call {@link #setTorchMode} to turn off the torch mode ( + * {@link CameraManager.TorchCallback#onTorchModeChanged} will be invoked). If the latest + * application that turned on the torch mode exits, the torch mode will be turned off. + * + * @param cameraId + * The unique identifier of the camera device that the flash unit belongs to. + * @param enabled + * The desired state of the torch mode for the target camera device. Set to + * {@code true} to turn on the torch mode. Set to {@code false} to turn off the + * torch mode. + * + * @throws CameraAccessException if it failed to access the flash unit. + * {@link CameraAccessException#CAMERA_IN_USE} will be thrown if the camera device + * is in use. {@link CameraAccessException#MAX_CAMERAS_IN_USE} will be thrown if + * other camera resources needed to turn on the torch mode are in use. + * {@link CameraAccessException#CAMERA_DISCONNECTED} will be thrown if camera + * service is not available. + * + * @throws IllegalArgumentException if cameraId was null, cameraId doesn't match any currently + * or previously available camera device, or the camera device doesn't have a + * flash unit. + */ + public void setTorchMode(String cameraId, boolean enabled) throws CameraAccessException { + CameraManagerGlobal.get().setTorchMode(cameraId, enabled); + } + + /** * A callback for camera devices becoming available or * unavailable to open. * @@ -428,6 +521,63 @@ public final class CameraManager { } /** + * A callback for camera flash torch modes becoming unavailable, disabled, or enabled. + * + * <p>The torch mode becomes unavailable when the camera device it belongs to becomes + * unavailable or other camera resouces it needs become busy due to other higher priority + * camera activities. The torch mode becomes disabled when it was turned off or when the camera + * device it belongs to is no longer in use and other camera resources it needs are no longer + * busy. A camera's torch mode is turned off when an application calls {@link #setTorchMode} to + * turn off the camera's torch mode, or when an application turns on another camera's torch mode + * if keeping multiple torch modes on simultaneously is not supported. The torch mode becomes + * enabled when it is turned on via {@link #setTorchMode}.</p> + * + * <p>The torch mode is available to set via {@link #setTorchMode} only when it's in a disabled + * or enabled state.</p> + * + * <p>Extend this callback and pass an instance of the subclass to + * {@link CameraManager#registerTorchCallback} to be notified of such status changes. + * </p> + * + * @see registerTorchCallback + */ + public static abstract class TorchCallback { + /** + * A camera's torch mode has become unavailable to set via {@link #setTorchMode}. + * + * <p>If torch mode was previously turned on by calling {@link #setTorchMode}, it will be + * turned off before {@link CameraManager.TorchCallback#onTorchModeUnavailable} is + * invoked. {@link #setTorchMode} will fail until the torch mode has entered a disabled or + * enabled state again.</p> + * + * <p>The default implementation of this method does nothing.</p> + * + * @param cameraId The unique identifier of the camera whose torch mode has become + * unavailable. + */ + public void onTorchModeUnavailable(String cameraId) { + // default empty implementation + } + + /** + * A camera's torch mode has become enabled or disabled and can be changed via + * {@link #setTorchMode}. + * + * <p>The default implementation of this method does nothing.</p> + * + * @param cameraId The unique identifier of the camera whose torch mode has been changed. + * + * @param enabled The state that the torch mode of the camera has been changed to. + * {@code true} when the torch mode has become on and available to be turned + * off. {@code false} when the torch mode has becomes off and available to + * be turned on. + */ + public void onTorchModeChanged(String cameraId, boolean enabled) { + // default empty implementation + } + } + + /** * Return or create the list of currently connected camera devices. * * <p>In case of errors connecting to the camera service, will return an empty list.</p> @@ -583,6 +733,27 @@ public final class CameraManager { private final ArrayMap<AvailabilityCallback, Handler> mCallbackMap = new ArrayMap<AvailabilityCallback, Handler>(); + // Keep up-to-date with ICameraServiceListener.h + + // torch mode has become not available to set via setTorchMode(). + public static final int TORCH_STATUS_NOT_AVAILABLE = 0; + // torch mode is off and available to be turned on via setTorchMode(). + public static final int TORCH_STATUS_AVAILABLE_OFF = 1; + // torch mode is on and available to be turned off via setTorchMode(). + public static final int TORCH_STATUS_AVAILABLE_ON = 2; + + // End enums shared with ICameraServiceListener.h + + // torch client binder to set the torch mode with. + private Binder mTorchClientBinder = new Binder(); + + // Camera ID -> Torch status map + private final ArrayMap<String, Integer> mTorchStatus = new ArrayMap<String, Integer>(); + + // Registered torch callbacks and their handlers + private final ArrayMap<TorchCallback, Handler> mTorchCallbackMap = + new ArrayMap<TorchCallback, Handler>(); + private final Object mLock = new Object(); // Access only through getCameraService to deal with binder death @@ -668,15 +839,46 @@ public final class CameraManager { } } + public void setTorchMode(String cameraId, boolean enabled) throws CameraAccessException { + synchronized(mLock) { + + if (cameraId == null) { + throw new IllegalArgumentException("cameraId was null"); + } + + ICameraService cameraService = getCameraService(); + if (cameraService == null) { + throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED, + "Camera service is currently unavailable"); + } + + try { + int status = cameraService.setTorchMode(cameraId, enabled, mTorchClientBinder); + } catch(CameraRuntimeException e) { + int problem = e.getReason(); + switch (problem) { + case CameraAccessException.CAMERA_ERROR: + throw new IllegalArgumentException( + "the camera device doesn't have a flash unit."); + default: + throw e.asChecked(); + } + } catch (RemoteException e) { + throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED, + "Camera service is currently unavailable"); + } + } + } + private void handleRecoverableSetupErrors(CameraRuntimeException e, String msg) { int problem = e.getReason(); switch (problem) { - case CameraAccessException.CAMERA_DISCONNECTED: - String errorMsg = CameraAccessException.getDefaultMessage(problem); - Log.w(TAG, msg + ": " + errorMsg); - break; - default: - throw new IllegalStateException(msg, e.asChecked()); + case CameraAccessException.CAMERA_DISCONNECTED: + String errorMsg = CameraAccessException.getDefaultMessage(problem); + Log.w(TAG, msg + ": " + errorMsg); + break; + default: + throw new IllegalStateException(msg, e.asChecked()); } } @@ -701,6 +903,17 @@ public final class CameraManager { } } + private boolean validTorchStatus(int status) { + switch (status) { + case TORCH_STATUS_NOT_AVAILABLE: + case TORCH_STATUS_AVAILABLE_ON: + case TORCH_STATUS_AVAILABLE_OFF: + return true; + default: + return false; + } + } + private void postSingleUpdate(final AvailabilityCallback callback, final Handler handler, final String id, final int status) { if (isAvailable(status)) { @@ -722,6 +935,32 @@ public final class CameraManager { } } + private void postSingleTorchUpdate(final TorchCallback callback, final Handler handler, + final String id, final int status) { + switch(status) { + case TORCH_STATUS_AVAILABLE_ON: + case TORCH_STATUS_AVAILABLE_OFF: + handler.post( + new Runnable() { + @Override + public void run() { + callback.onTorchModeChanged(id, status == + TORCH_STATUS_AVAILABLE_ON); + } + }); + break; + default: + handler.post( + new Runnable() { + @Override + public void run() { + callback.onTorchModeUnavailable(id); + } + }); + break; + } + } + /** * Send the state of all known cameras to the provided listener, to initialize * the listener's knowledge of camera state. @@ -791,6 +1030,44 @@ public final class CameraManager { } } // onStatusChangedLocked + private void updateTorchCallbackLocked(TorchCallback callback, Handler handler) { + for (int i = 0; i < mTorchStatus.size(); i++) { + String id = mTorchStatus.keyAt(i); + Integer status = mTorchStatus.valueAt(i); + postSingleTorchUpdate(callback, handler, id, status); + } + } + + private void onTorchStatusChangedLocked(int status, String id) { + if (DEBUG) { + Log.v(TAG, + String.format("Camera id %s has torch status changed to 0x%x", id, status)); + } + + if (!validTorchStatus(status)) { + Log.e(TAG, String.format("Ignoring invalid device %s torch status 0x%x", id, + status)); + return; + } + + Integer oldStatus = mTorchStatus.put(id, status); + if (oldStatus != null && oldStatus == status) { + if (DEBUG) { + Log.v(TAG, String.format( + "Torch status changed to 0x%x, which is what it already was", + status)); + } + return; + } + + final int callbackCount = mTorchCallbackMap.size(); + for (int i = 0; i < callbackCount; i++) { + final Handler handler = mTorchCallbackMap.valueAt(i); + final TorchCallback callback = mTorchCallbackMap.keyAt(i); + postSingleTorchUpdate(callback, handler, id, status); + } + } // onTorchStatusChangedLocked + /** * Register a callback to be notified about camera device availability with the * global listener singleton. @@ -820,6 +1097,22 @@ public final class CameraManager { } } + public void registerTorchCallback(TorchCallback callback, Handler handler) { + synchronized(mLock) { + Handler oldHandler = mTorchCallbackMap.put(callback, handler); + // For new callbacks, provide initial torch information + if (oldHandler == null) { + updateTorchCallbackLocked(callback, handler); + } + } + } + + public void unregisterTorchCallback(TorchCallback callback) { + synchronized(mLock) { + mTorchCallbackMap.remove(callback); + } + } + /** * Callback from camera service notifying the process about camera availability changes */ @@ -830,6 +1123,13 @@ public final class CameraManager { } } + @Override + public void onTorchStatusChanged(int status, String cameraId) throws RemoteException { + synchronized (mLock) { + onTorchStatusChangedLocked(status, cameraId); + } + } + /** * Listener for camera service death. * @@ -844,9 +1144,9 @@ public final class CameraManager { mCameraService = null; - // Tell listeners that the cameras are _available_, because any existing clients - // will have gotten disconnected. This is optimistic under the assumption that - // the service will be back shortly. + // Tell listeners that the cameras and torch modes are _available_, because any + // existing clients will have gotten disconnected. This is optimistic under the + // assumption that the service will be back shortly. // // Without this, a camera service crash while a camera is open will never signal // to listeners that previously in-use cameras are now available. @@ -854,6 +1154,11 @@ public final class CameraManager { String cameraId = mDeviceStatus.keyAt(i); onStatusChangedLocked(STATUS_PRESENT, cameraId); } + for (int i = 0; i < mTorchStatus.size(); i++) { + String cameraId = mTorchStatus.keyAt(i); + onTorchStatusChangedLocked(TORCH_STATUS_AVAILABLE_OFF, cameraId); + } + } } diff --git a/core/java/android/hardware/camera2/CameraMetadata.java b/core/java/android/hardware/camera2/CameraMetadata.java index 1b10858..7f901c8 100644 --- a/core/java/android/hardware/camera2/CameraMetadata.java +++ b/core/java/android/hardware/camera2/CameraMetadata.java @@ -288,6 +288,13 @@ public abstract class CameraMetadata<TKey> { */ public static final int LENS_FACING_BACK = 1; + /** + * <p>The camera device is an external camera, and has no fixed facing relative to the + * device's screen.</p> + * @see CameraCharacteristics#LENS_FACING + */ + public static final int LENS_FACING_EXTERNAL = 2; + // // Enumeration values for CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES // @@ -367,13 +374,19 @@ public abstract class CameraMetadata<TKey> { * The camera device supports basic manual control of the image post-processing * stages. This means the following controls are guaranteed to be supported:</p> * <ul> - * <li>Manual tonemap control<ul> + * <li> + * <p>Manual tonemap control</p> + * <ul> * <li>{@link CaptureRequest#TONEMAP_CURVE android.tonemap.curve}</li> * <li>{@link CaptureRequest#TONEMAP_MODE android.tonemap.mode}</li> * <li>{@link CameraCharacteristics#TONEMAP_MAX_CURVE_POINTS android.tonemap.maxCurvePoints}</li> + * <li>android.tonemap.gamma</li> + * <li>android.tonemap.presetCurve</li> * </ul> * </li> - * <li>Manual white balance control<ul> + * <li> + * <p>Manual white balance control</p> + * <ul> * <li>{@link CaptureRequest#COLOR_CORRECTION_TRANSFORM android.colorCorrection.transform}</li> * <li>{@link CaptureRequest#COLOR_CORRECTION_GAINS android.colorCorrection.gains}</li> * </ul> @@ -432,23 +445,40 @@ public abstract class CameraMetadata<TKey> { public static final int REQUEST_AVAILABLE_CAPABILITIES_RAW = 3; /** - * <p>The camera device supports the Zero Shutter Lag use case.</p> + * <p>The camera device supports the Zero Shutter Lag reprocessing use case.</p> * <ul> - * <li>At least one input stream can be used.</li> - * <li>RAW_OPAQUE is supported as an output/input format</li> - * <li>Using RAW_OPAQUE does not cause a frame rate drop + * <li>One input stream is supported, that is, <code>{@link CameraCharacteristics#REQUEST_MAX_NUM_INPUT_STREAMS android.request.maxNumInputStreams} == 1</code>.</li> + * <li>OPAQUE is supported as an output/input format, that is, + * StreamConfigurationMap#getOutputSizes(klass) and + * StreamConfigurationMap#getInputSizes(klass) return non empty Size[] and have common + * sizes, where klass is android.media.OpaqueImageRingBufferQueue.class. See + * android.scaler.availableInputOutputFormatsMap for detailed information about + * OPAQUE format.</li> + * <li>android.scaler.availableInputOutputFormatsMap has the required map entries.</li> + * <li>Using OPAQUE does not cause a frame rate drop * relative to the sensor's maximum capture rate (at that - * resolution).</li> - * <li>RAW_OPAQUE will be reprocessable into both YUV_420_888 + * resolution), see android.scaler.availableInputOutputFormatsMap for more details.</li> + * <li>OPAQUE will be reprocessable into both YUV_420_888 * and JPEG formats.</li> - * <li>The maximum available resolution for RAW_OPAQUE streams + * <li>The maximum available resolution for OPAQUE streams * (both input/output) will match the maximum available * resolution of JPEG streams.</li> + * <li>Only below controls are effective for reprocessing requests and + * will be present in capture results, other controls in reprocess + * requests will be ignored by the camera device.<ul> + * <li>android.jpeg.*</li> + * <li>{@link CaptureRequest#NOISE_REDUCTION_MODE android.noiseReduction.mode}</li> + * <li>{@link CaptureRequest#EDGE_MODE android.edge.mode}</li> + * </ul> + * </li> * </ul> + * + * @see CaptureRequest#EDGE_MODE + * @see CaptureRequest#NOISE_REDUCTION_MODE + * @see CameraCharacteristics#REQUEST_MAX_NUM_INPUT_STREAMS * @see CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES - * @hide */ - public static final int REQUEST_AVAILABLE_CAPABILITIES_ZSL = 4; + public static final int REQUEST_AVAILABLE_CAPABILITIES_OPAQUE_REPROCESSING = 4; /** * <p>The camera device supports accurately reporting the sensor settings for many of @@ -508,6 +538,45 @@ public abstract class CameraMetadata<TKey> { */ public static final int REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE = 6; + /** + * <p>The camera device supports the YUV420_888 reprocessing use case, similar as + * OPAQUE_REPROCESSING, This capability requires the camera device to support the + * following:</p> + * <ul> + * <li>One input stream is supported, that is, <code>{@link CameraCharacteristics#REQUEST_MAX_NUM_INPUT_STREAMS android.request.maxNumInputStreams} == 1</code>.</li> + * <li>YUV420_888 is supported as a common format for both input and output, that is, + * StreamConfigurationMap#getOutputSizes(YUV420_888) and + * StreamConfigurationMap#getInputSizes(YUV420_888) return non empty Size[] and have + * common sizes.</li> + * <li>android.scaler.availableInputOutputFormatsMap has the required map entries.</li> + * <li>Using YUV420_888 does not cause a frame rate drop + * relative to the sensor's maximum capture rate (at that + * resolution), see android.scaler.availableInputOutputFormatsMap for more details.</li> + * <li>YUV420_888 will be reprocessable into both YUV_420_888 + * and JPEG formats.</li> + * <li>The maximum available resolution for YUV420_888 streams + * (both input/output) will match the maximum available + * resolution of JPEG streams.</li> + * <li>Only the below controls are effective for reprocessing requests and will be + * present in capture results. The reprocess requests are from the original capture + * results that are assocaited with the intermidate YUV420_888 output buffers. + * All other controls in the reprocess requests will be ignored by the camera device.<ul> + * <li>android.jpeg.*</li> + * <li>{@link CaptureRequest#NOISE_REDUCTION_MODE android.noiseReduction.mode}</li> + * <li>{@link CaptureRequest#EDGE_MODE android.edge.mode}</li> + * <li>{@link CaptureRequest#REPROCESS_EFFECTIVE_EXPOSURE_FACTOR android.reprocess.effectiveExposureFactor}</li> + * </ul> + * </li> + * </ul> + * + * @see CaptureRequest#EDGE_MODE + * @see CaptureRequest#NOISE_REDUCTION_MODE + * @see CaptureRequest#REPROCESS_EFFECTIVE_EXPOSURE_FACTOR + * @see CameraCharacteristics#REQUEST_MAX_NUM_INPUT_STREAMS + * @see CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES + */ + public static final int REQUEST_AVAILABLE_CAPABILITIES_YUV_REPROCESSING = 7; + // // Enumeration values for CameraCharacteristics#SCALER_CROPPING_TYPE // @@ -966,6 +1035,14 @@ public abstract class CameraMetadata<TKey> { */ public static final int CONTROL_AE_PRECAPTURE_TRIGGER_START = 1; + /** + * <p>The camera device will cancel any currently active or completed + * precapture metering sequence, the auto-exposure routine will return to its + * initial state.</p> + * @see CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER + */ + public static final int CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL = 2; + // // Enumeration values for CaptureRequest#CONTROL_AF_MODE // @@ -1823,6 +1900,13 @@ public abstract class CameraMetadata<TKey> { */ public static final int NOISE_REDUCTION_MODE_HIGH_QUALITY = 2; + /** + * <p>MINIMAL noise reduction is applied without reducing frame rate relative to + * sensor output. </p> + * @see CaptureRequest#NOISE_REDUCTION_MODE + */ + public static final int NOISE_REDUCTION_MODE_MINIMAL = 3; + // // Enumeration values for CaptureRequest#SENSOR_TEST_PATTERN_MODE // @@ -2026,6 +2110,43 @@ public abstract class CameraMetadata<TKey> { */ public static final int TONEMAP_MODE_HIGH_QUALITY = 2; + /** + * <p>Use the gamma value specified in android.tonemap.gamma to peform + * tonemapping.</p> + * <p>All color enhancement and tonemapping must be disabled, except + * for applying the tonemapping curve specified by android.tonemap.gamma.</p> + * <p>Must not slow down frame rate relative to raw sensor output.</p> + * @see CaptureRequest#TONEMAP_MODE + */ + public static final int TONEMAP_MODE_GAMMA_VALUE = 3; + + /** + * <p>Use the preset tonemapping curve specified in + * android.tonemap.presetCurve to peform tonemapping.</p> + * <p>All color enhancement and tonemapping must be disabled, except + * for applying the tonemapping curve specified by + * android.tonemap.presetCurve.</p> + * <p>Must not slow down frame rate relative to raw sensor output.</p> + * @see CaptureRequest#TONEMAP_MODE + */ + public static final int TONEMAP_MODE_PRESET_CURVE = 4; + + // + // Enumeration values for CaptureRequest#TONEMAP_PRESET_CURVE + // + + /** + * <p>Tonemapping curve is defined by sRGB</p> + * @see CaptureRequest#TONEMAP_PRESET_CURVE + */ + public static final int TONEMAP_PRESET_CURVE_SRGB = 0; + + /** + * <p>Tonemapping curve is defined by ITU-R BT.709</p> + * @see CaptureRequest#TONEMAP_PRESET_CURVE + */ + public static final int TONEMAP_PRESET_CURVE_REC709 = 1; + // // Enumeration values for CaptureResult#CONTROL_AE_STATE // @@ -2073,7 +2194,10 @@ public abstract class CameraMetadata<TKey> { * <p>AE has been asked to do a precapture sequence * and is currently executing it.</p> * <p>Precapture can be triggered through setting - * {@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger} to START.</p> + * {@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger} to START. Currently + * active and completed (if it causes camera device internal AE lock) precapture + * metering sequence can be canceled through setting + * {@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger} to CANCEL.</p> * <p>Once PRECAPTURE completes, AE will transition to CONVERGED * or FLASH_REQUIRED as appropriate. This is a transient * state, the camera device may skip reporting this state in diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java index b417496..7569ea5 100644 --- a/core/java/android/hardware/camera2/CaptureRequest.java +++ b/core/java/android/hardware/camera2/CaptureRequest.java @@ -552,6 +552,8 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * in this matrix result metadata. The transform should keep the magnitude * of the output color values within <code>[0, 1.0]</code> (assuming input color * values is within the normalized range <code>[0, 1.0]</code>), or clipping may occur.</p> + * <p>The valid range of each matrix element varies on different devices, but + * values within [-1.5, 3.0] are guaranteed not to be clipped.</p> * <p><b>Units</b>: Unitless scale factors</p> * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> * <p><b>Full capability</b> - @@ -575,6 +577,10 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * TRANSFORM_MATRIX.</p> * <p>The gains in the result metadata are the gains actually * applied by the camera device to the current frame.</p> + * <p>The valid range of gains varies on different devices, but gains + * between [1.0, 3.0] are guaranteed not to be clipped. Even if a given + * device allows gains below 1.0, this is usually not recommended because + * this can create color artifacts.</p> * <p><b>Units</b>: Unitless gain factors</p> * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> * <p><b>Full capability</b> - @@ -724,7 +730,14 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * ({@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime}) and sensitivity ({@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity}) * parameters. The flash may be fired if the {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} * is ON_AUTO_FLASH/ON_AUTO_FLASH_REDEYE and the scene is too dark. If the - * {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} is ON_ALWAYS_FLASH, the scene may become overexposed.</p> + * {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} is ON_ALWAYS_FLASH, the scene may become overexposed. + * Similarly, AE precapture trigger CANCEL has no effect when AE is already locked.</p> + * <p>When an AE precapture sequence is triggered, AE unlock will not be able to unlock + * the AE if AE is locked by the camera device internally during precapture metering + * sequence In other words, submitting requests with AE unlock has no effect for an + * ongoing precapture metering sequence. Otherwise, the precapture metering sequence + * will never succeed in a sequence of preview requests where AE lock is always set + * to <code>false</code>.</p> * <p>Since the camera device has a pipeline of in-flight requests, the settings that * get locked do not necessarily correspond to the settings that were present in the * latest capture result received from the camera device, since additional captures @@ -869,6 +882,11 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * included at all in the request settings. When included and * set to START, the camera device will trigger the auto-exposure (AE) * precapture metering sequence.</p> + * <p>When set to CANCEL, the camera device will cancel any active + * precapture metering trigger, and return to its initial AE state. + * If a precapture metering sequence is already completed, and the camera + * device has implicitly locked the AE for subsequent still capture, the + * CANCEL trigger will unlock the AE and return to its initial AE state.</p> * <p>The precapture sequence should be triggered before starting a * high-quality still capture for final metering decisions to * be made, and for firing pre-capture flash pulses to estimate @@ -884,7 +902,11 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * submitted. To ensure that the AE routine restarts normal scan, the application should * submit a request with <code>{@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} == true</code>, followed by a request * with <code>{@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} == false</code>, if the application decides not to submit a - * still capture request after the precapture sequence completes.</p> + * still capture request after the precapture sequence completes. Alternatively, for + * API level 23 or newer devices, the CANCEL can be used to unlock the camera device + * internally locked AE if the application doesn't submit a still capture request after + * the AE precapture trigger. Note that, the CANCEL was added in API level 23, and must not + * be used in devices that have earlier API levels.</p> * <p>The exact effect of auto-exposure (AE) precapture trigger * depends on the current AE mode and state; see * {@link CaptureResult#CONTROL_AE_STATE android.control.aeState} for AE precapture state transition @@ -897,6 +919,7 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * <ul> * <li>{@link #CONTROL_AE_PRECAPTURE_TRIGGER_IDLE IDLE}</li> * <li>{@link #CONTROL_AE_PRECAPTURE_TRIGGER_START START}</li> + * <li>{@link #CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL CANCEL}</li> * </ul></p> * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> * <p><b>Limited capability</b> - @@ -909,6 +932,7 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL * @see #CONTROL_AE_PRECAPTURE_TRIGGER_IDLE * @see #CONTROL_AE_PRECAPTURE_TRIGGER_START + * @see #CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL */ @PublicKey public static final Key<Integer> CONTROL_AE_PRECAPTURE_TRIGGER = @@ -1164,7 +1188,7 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * <p>This control (except for MANUAL) is only effective if * <code>{@link CaptureRequest#CONTROL_MODE android.control.mode} != OFF</code> and any 3A routine is active.</p> * <p>ZERO_SHUTTER_LAG will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} - * contains ZSL. MANUAL will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} + * contains OPAQUE_REPROCESSING. MANUAL will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} * contains MANUAL_SENSOR. Other intent values are always supported.</p> * <p><b>Possible values:</b> * <ul> @@ -1249,10 +1273,6 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * update, as if this frame is never captured. This mode can be used in the scenario * where the application doesn't want a 3A manual control capture to affect * the subsequent auto 3A capture results.</p> - * <p>LEGACY mode devices will only support AUTO and USE_SCENE_MODE modes. - * LIMITED mode devices will only support OFF and OFF_KEEP_STATE if they - * support the MANUAL_SENSOR and MANUAL_POST_PROCSESING capabilities. - * FULL mode devices will always support OFF and OFF_KEEP_STATE.</p> * <p><b>Possible values:</b> * <ul> * <li>{@link #CONTROL_MODE_OFF OFF}</li> @@ -1260,9 +1280,12 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * <li>{@link #CONTROL_MODE_USE_SCENE_MODE USE_SCENE_MODE}</li> * <li>{@link #CONTROL_MODE_OFF_KEEP_STATE OFF_KEEP_STATE}</li> * </ul></p> + * <p><b>Available values for this device:</b><br> + * {@link CameraCharacteristics#CONTROL_AVAILABLE_MODES android.control.availableModes}</p> * <p>This key is available on all devices.</p> * * @see CaptureRequest#CONTROL_AF_MODE + * @see CameraCharacteristics#CONTROL_AVAILABLE_MODES * @see #CONTROL_MODE_OFF * @see #CONTROL_MODE_AUTO * @see #CONTROL_MODE_USE_SCENE_MODE @@ -1382,6 +1405,10 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * camera device will use the highest-quality enhancement algorithms, * even if it slows down capture rate. FAST means the camera device will * not slow down capture rate when applying edge enhancement.</p> + * <p>For YUV_REPROCESSING, these FAST/HIGH_QUALITY modes both mean that the camera + * device will apply FAST/HIGH_QUALITY YUV-domain edge enhancement, respectively. + * The camera device may adjust its internal noise reduction parameters for best + * image quality based on the {@link CaptureRequest#REPROCESS_EFFECTIVE_EXPOSURE_FACTOR android.reprocess.effectiveExposureFactor}, if it is set.</p> * <p><b>Possible values:</b> * <ul> * <li>{@link #EDGE_MODE_OFF OFF}</li> @@ -1397,6 +1424,7 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * * @see CameraCharacteristics#EDGE_AVAILABLE_EDGE_MODES * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL + * @see CaptureRequest#REPROCESS_EFFECTIVE_EXPOSURE_FACTOR * @see #EDGE_MODE_OFF * @see #EDGE_MODE_FAST * @see #EDGE_MODE_HIGH_QUALITY @@ -1761,18 +1789,28 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> /** * <p>Mode of operation for the noise reduction algorithm.</p> * <p>The noise reduction algorithm attempts to improve image quality by removing - * excessive noise added by the capture process, especially in dark conditions. - * OFF means no noise reduction will be applied by the camera device.</p> + * excessive noise added by the capture process, especially in dark conditions.</p> + * <p>OFF means no noise reduction will be applied by the camera device, for both raw and + * YUV domain.</p> + * <p>MINIMAL means that only sensor raw domain basic noise reduction is enabled ,to remove + * demosaicing or other processing artifacts. For YUV_REPROCESSING, MINIMAL is same as OFF. + * This mode is optional, may not be support by all devices. The application should check + * {@link CameraCharacteristics#NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES android.noiseReduction.availableNoiseReductionModes} before using it.</p> * <p>FAST/HIGH_QUALITY both mean camera device determined noise filtering * will be applied. HIGH_QUALITY mode indicates that the camera device * will use the highest-quality noise filtering algorithms, * even if it slows down capture rate. FAST means the camera device will not * slow down capture rate when applying noise filtering.</p> + * <p>For YUV_REPROCESSING, these FAST/HIGH_QUALITY modes both mean that the camera device + * will apply FAST/HIGH_QUALITY YUV domain noise reduction, respectively. The camera device + * may adjust the noise reduction parameters for best image quality based on the + * {@link CaptureRequest#REPROCESS_EFFECTIVE_EXPOSURE_FACTOR android.reprocess.effectiveExposureFactor} if it is set.</p> * <p><b>Possible values:</b> * <ul> * <li>{@link #NOISE_REDUCTION_MODE_OFF OFF}</li> * <li>{@link #NOISE_REDUCTION_MODE_FAST FAST}</li> * <li>{@link #NOISE_REDUCTION_MODE_HIGH_QUALITY HIGH_QUALITY}</li> + * <li>{@link #NOISE_REDUCTION_MODE_MINIMAL MINIMAL}</li> * </ul></p> * <p><b>Available values for this device:</b><br> * {@link CameraCharacteristics#NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES android.noiseReduction.availableNoiseReductionModes}</p> @@ -1783,9 +1821,11 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL * @see CameraCharacteristics#NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES + * @see CaptureRequest#REPROCESS_EFFECTIVE_EXPOSURE_FACTOR * @see #NOISE_REDUCTION_MODE_OFF * @see #NOISE_REDUCTION_MODE_FAST * @see #NOISE_REDUCTION_MODE_HIGH_QUALITY + * @see #NOISE_REDUCTION_MODE_MINIMAL */ @PublicKey public static final Key<Integer> NOISE_REDUCTION_MODE = @@ -2078,6 +2118,8 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * <li>{@link #SHADING_MODE_FAST FAST}</li> * <li>{@link #SHADING_MODE_HIGH_QUALITY HIGH_QUALITY}</li> * </ul></p> + * <p><b>Available values for this device:</b><br> + * {@link CameraCharacteristics#SHADING_AVAILABLE_MODES android.shading.availableModes}</p> * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> * <p><b>Full capability</b> - * Present on all camera devices that report being {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_FULL HARDWARE_LEVEL_FULL} devices in the @@ -2086,6 +2128,7 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * @see CaptureRequest#CONTROL_AE_MODE * @see CaptureRequest#CONTROL_AWB_MODE * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL + * @see CameraCharacteristics#SHADING_AVAILABLE_MODES * @see CaptureResult#STATISTICS_LENS_SHADING_CORRECTION_MAP * @see CaptureRequest#STATISTICS_LENS_SHADING_MAP_MODE * @see #SHADING_MODE_OFF @@ -2148,12 +2191,15 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * <li>{@link #STATISTICS_LENS_SHADING_MAP_MODE_OFF OFF}</li> * <li>{@link #STATISTICS_LENS_SHADING_MAP_MODE_ON ON}</li> * </ul></p> + * <p><b>Available values for this device:</b><br> + * {@link CameraCharacteristics#STATISTICS_INFO_AVAILABLE_LENS_SHADING_MAP_MODES android.statistics.info.availableLensShadingMapModes}</p> * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> * <p><b>Full capability</b> - * Present on all camera devices that report being {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_FULL HARDWARE_LEVEL_FULL} devices in the * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p> * * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL + * @see CameraCharacteristics#STATISTICS_INFO_AVAILABLE_LENS_SHADING_MAP_MODES * @see #STATISTICS_LENS_SHADING_MAP_MODE_OFF * @see #STATISTICS_LENS_SHADING_MAP_MODE_ON */ @@ -2340,6 +2386,8 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * <li>{@link #TONEMAP_MODE_CONTRAST_CURVE CONTRAST_CURVE}</li> * <li>{@link #TONEMAP_MODE_FAST FAST}</li> * <li>{@link #TONEMAP_MODE_HIGH_QUALITY HIGH_QUALITY}</li> + * <li>{@link #TONEMAP_MODE_GAMMA_VALUE GAMMA_VALUE}</li> + * <li>{@link #TONEMAP_MODE_PRESET_CURVE PRESET_CURVE}</li> * </ul></p> * <p><b>Available values for this device:</b><br> * {@link CameraCharacteristics#TONEMAP_AVAILABLE_TONE_MAP_MODES android.tonemap.availableToneMapModes}</p> @@ -2355,12 +2403,60 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * @see #TONEMAP_MODE_CONTRAST_CURVE * @see #TONEMAP_MODE_FAST * @see #TONEMAP_MODE_HIGH_QUALITY + * @see #TONEMAP_MODE_GAMMA_VALUE + * @see #TONEMAP_MODE_PRESET_CURVE */ @PublicKey public static final Key<Integer> TONEMAP_MODE = new Key<Integer>("android.tonemap.mode", int.class); /** + * <p>Tonemapping curve to use when {@link CaptureRequest#TONEMAP_MODE android.tonemap.mode} is + * GAMMA_VALUE</p> + * <p>The tonemap curve will be defined the following formula: + * * OUT = pow(IN, 1.0 / gamma) + * where IN and OUT is the input pixel value scaled to range [0.0, 1.0], + * pow is the power function and gamma is the gamma value specified by this + * key.</p> + * <p>The same curve will be applied to all color channels. The camera device + * may clip the input gamma value to its supported range. The actual applied + * value will be returned in capture result.</p> + * <p>The valid range of gamma value varies on different devices, but values + * within [1.0, 5.0] are guaranteed not to be clipped.</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * + * @see CaptureRequest#TONEMAP_MODE + */ + @PublicKey + public static final Key<Float> TONEMAP_GAMMA = + new Key<Float>("android.tonemap.gamma", float.class); + + /** + * <p>Tonemapping curve to use when {@link CaptureRequest#TONEMAP_MODE android.tonemap.mode} is + * PRESET_CURVE</p> + * <p>The tonemap curve will be defined by specified standard.</p> + * <p>sRGB (approximated by 16 control points):</p> + * <p><img alt="sRGB tonemapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/srgb_tonemap.png" /></p> + * <p>Rec. 709 (approximated by 16 control points):</p> + * <p><img alt="Rec. 709 tonemapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/rec709_tonemap.png" /></p> + * <p>Note that above figures show a 16 control points approximation of preset + * curves. Camera devices may apply a different approximation to the curve.</p> + * <p><b>Possible values:</b> + * <ul> + * <li>{@link #TONEMAP_PRESET_CURVE_SRGB SRGB}</li> + * <li>{@link #TONEMAP_PRESET_CURVE_REC709 REC709}</li> + * </ul></p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * + * @see CaptureRequest#TONEMAP_MODE + * @see #TONEMAP_PRESET_CURVE_SRGB + * @see #TONEMAP_PRESET_CURVE_REC709 + */ + @PublicKey + public static final Key<Integer> TONEMAP_PRESET_CURVE = + new Key<Integer>("android.tonemap.presetCurve", int.class); + + /** * <p>This LED is nominally used to indicate to the user * that the camera is powered on and may be streaming images back to the * Application Processor. In certain rare circumstances, the OS may @@ -2426,6 +2522,52 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> public static final Key<Boolean> BLACK_LEVEL_LOCK = new Key<Boolean>("android.blackLevel.lock", boolean.class); + /** + * <p>The amount of exposure time increase factor applied to the original output + * frame by the application processing before sending for reprocessing.</p> + * <p>This is optional, and will be supported if the camera device supports YUV_REPROCESSING + * capability ({@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains YUV_REPROCESSING).</p> + * <p>For some YUV reprocessing use cases, the application may choose to filter the original + * output frames to effectively reduce the noise to the same level as a frame that was + * captured with longer exposure time. To be more specific, assuming the original captured + * images were captured with a sensitivity of S and an exposure time of T, the model in + * the camera device is that the amount of noise in the image would be approximately what + * would be expected if the original capture parameters had been a sensitivity of + * S/effectiveExposureFactor and an exposure time of T*effectiveExposureFactor, rather + * than S and T respectively. If the captured images were processed by the application + * before being sent for reprocessing, then the application may have used image processing + * algorithms and/or multi-frame image fusion to reduce the noise in the + * application-processed images (input images). By using the effectiveExposureFactor + * control, the application can communicate to the camera device the actual noise level + * improvement in the application-processed image. With this information, the camera + * device can select appropriate noise reduction and edge enhancement parameters to avoid + * excessive noise reduction ({@link CaptureRequest#NOISE_REDUCTION_MODE android.noiseReduction.mode}) and insufficient edge + * enhancement ({@link CaptureRequest#EDGE_MODE android.edge.mode}) being applied to the reprocessed frames.</p> + * <p>For example, for multi-frame image fusion use case, the application may fuse + * multiple output frames together to a final frame for reprocessing. When N image are + * fused into 1 image for reprocessing, the exposure time increase factor could be up to + * square root of N (based on a simple photon shot noise model). The camera device will + * adjust the reprocessing noise reduction and edge enhancement parameters accordingly to + * produce the best quality images.</p> + * <p>This is relative factor, 1.0 indicates the application hasn't processed the input + * buffer in a way that affects its effective exposure time.</p> + * <p>This control is only effective for YUV reprocessing capture request. For noise + * reduction reprocessing, it is only effective when <code>{@link CaptureRequest#NOISE_REDUCTION_MODE android.noiseReduction.mode} != OFF</code>. + * Similarly, for edge enhancement reprocessing, it is only effective when + * <code>{@link CaptureRequest#EDGE_MODE android.edge.mode} != OFF</code>.</p> + * <p><b>Units</b>: Relative exposure time increase factor.</p> + * <p><b>Range of valid values:</b><br> + * >= 1.0</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * + * @see CaptureRequest#EDGE_MODE + * @see CaptureRequest#NOISE_REDUCTION_MODE + * @see CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES + */ + @PublicKey + public static final Key<Float> REPROCESS_EFFECTIVE_EXPOSURE_FACTOR = + new Key<Float>("android.reprocess.effectiveExposureFactor", float.class); + /*~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~ * End generated code *~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~O@*/ diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java index f17100d..b84dc2e 100644 --- a/core/java/android/hardware/camera2/CaptureResult.java +++ b/core/java/android/hardware/camera2/CaptureResult.java @@ -403,6 +403,8 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * in this matrix result metadata. The transform should keep the magnitude * of the output color values within <code>[0, 1.0]</code> (assuming input color * values is within the normalized range <code>[0, 1.0]</code>), or clipping may occur.</p> + * <p>The valid range of each matrix element varies on different devices, but + * values within [-1.5, 3.0] are guaranteed not to be clipped.</p> * <p><b>Units</b>: Unitless scale factors</p> * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> * <p><b>Full capability</b> - @@ -426,6 +428,10 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * TRANSFORM_MATRIX.</p> * <p>The gains in the result metadata are the gains actually * applied by the camera device to the current frame.</p> + * <p>The valid range of gains varies on different devices, but gains + * between [1.0, 3.0] are guaranteed not to be clipped. Even if a given + * device allows gains below 1.0, this is usually not recommended because + * this can create color artifacts.</p> * <p><b>Units</b>: Unitless gain factors</p> * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> * <p><b>Full capability</b> - @@ -575,7 +581,14 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * ({@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime}) and sensitivity ({@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity}) * parameters. The flash may be fired if the {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} * is ON_AUTO_FLASH/ON_AUTO_FLASH_REDEYE and the scene is too dark. If the - * {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} is ON_ALWAYS_FLASH, the scene may become overexposed.</p> + * {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} is ON_ALWAYS_FLASH, the scene may become overexposed. + * Similarly, AE precapture trigger CANCEL has no effect when AE is already locked.</p> + * <p>When an AE precapture sequence is triggered, AE unlock will not be able to unlock + * the AE if AE is locked by the camera device internally during precapture metering + * sequence In other words, submitting requests with AE unlock has no effect for an + * ongoing precapture metering sequence. Otherwise, the precapture metering sequence + * will never succeed in a sequence of preview requests where AE lock is always set + * to <code>false</code>.</p> * <p>Since the camera device has a pipeline of in-flight requests, the settings that * get locked do not necessarily correspond to the settings that were present in the * latest capture result received from the camera device, since additional captures @@ -720,6 +733,11 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * included at all in the request settings. When included and * set to START, the camera device will trigger the auto-exposure (AE) * precapture metering sequence.</p> + * <p>When set to CANCEL, the camera device will cancel any active + * precapture metering trigger, and return to its initial AE state. + * If a precapture metering sequence is already completed, and the camera + * device has implicitly locked the AE for subsequent still capture, the + * CANCEL trigger will unlock the AE and return to its initial AE state.</p> * <p>The precapture sequence should be triggered before starting a * high-quality still capture for final metering decisions to * be made, and for firing pre-capture flash pulses to estimate @@ -735,7 +753,11 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * submitted. To ensure that the AE routine restarts normal scan, the application should * submit a request with <code>{@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} == true</code>, followed by a request * with <code>{@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} == false</code>, if the application decides not to submit a - * still capture request after the precapture sequence completes.</p> + * still capture request after the precapture sequence completes. Alternatively, for + * API level 23 or newer devices, the CANCEL can be used to unlock the camera device + * internally locked AE if the application doesn't submit a still capture request after + * the AE precapture trigger. Note that, the CANCEL was added in API level 23, and must not + * be used in devices that have earlier API levels.</p> * <p>The exact effect of auto-exposure (AE) precapture trigger * depends on the current AE mode and state; see * {@link CaptureResult#CONTROL_AE_STATE android.control.aeState} for AE precapture state transition @@ -748,6 +770,7 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * <ul> * <li>{@link #CONTROL_AE_PRECAPTURE_TRIGGER_IDLE IDLE}</li> * <li>{@link #CONTROL_AE_PRECAPTURE_TRIGGER_START START}</li> + * <li>{@link #CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL CANCEL}</li> * </ul></p> * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> * <p><b>Limited capability</b> - @@ -760,6 +783,7 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL * @see #CONTROL_AE_PRECAPTURE_TRIGGER_IDLE * @see #CONTROL_AE_PRECAPTURE_TRIGGER_START + * @see #CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL */ @PublicKey public static final Key<Integer> CONTROL_AE_PRECAPTURE_TRIGGER = @@ -892,11 +916,29 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * <td align="center">Ready for high-quality capture</td> * </tr> * <tr> - * <td align="center">Any state</td> + * <td align="center">LOCKED</td> + * <td align="center">aeLock is ON and aePrecaptureTrigger is START</td> + * <td align="center">LOCKED</td> + * <td align="center">Precapture trigger is ignored when AE is already locked</td> + * </tr> + * <tr> + * <td align="center">LOCKED</td> + * <td align="center">aeLock is ON and aePrecaptureTrigger is CANCEL</td> + * <td align="center">LOCKED</td> + * <td align="center">Precapture trigger is ignored when AE is already locked</td> + * </tr> + * <tr> + * <td align="center">Any state (excluding LOCKED)</td> * <td align="center">{@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger} is START</td> * <td align="center">PRECAPTURE</td> * <td align="center">Start AE precapture metering sequence</td> * </tr> + * <tr> + * <td align="center">Any state (excluding LOCKED)</td> + * <td align="center">{@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger} is CANCEL</td> + * <td align="center">INACTIVE</td> + * <td align="center">Currently active precapture metering sequence is canceled</td> + * </tr> * </tbody> * </table> * <p>For the above table, the camera device may skip reporting any state changes that happen @@ -922,18 +964,30 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * <td align="center">Values are already good, transient states are skipped by camera device.</td> * </tr> * <tr> - * <td align="center">Any state</td> + * <td align="center">Any state (excluding LOCKED)</td> * <td align="center">{@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger} is START, sequence done</td> * <td align="center">FLASH_REQUIRED</td> * <td align="center">Converged but too dark w/o flash after a precapture sequence, transient states are skipped by camera device.</td> * </tr> * <tr> - * <td align="center">Any state</td> + * <td align="center">Any state (excluding LOCKED)</td> * <td align="center">{@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger} is START, sequence done</td> * <td align="center">CONVERGED</td> * <td align="center">Converged after a precapture sequence, transient states are skipped by camera device.</td> * </tr> * <tr> + * <td align="center">Any state (excluding LOCKED)</td> + * <td align="center">{@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger} is CANCEL, converged</td> + * <td align="center">FLASH_REQUIRED</td> + * <td align="center">Converged but too dark w/o flash after a precapture sequence is canceled, transient states are skipped by camera device.</td> + * </tr> + * <tr> + * <td align="center">Any state (excluding LOCKED)</td> + * <td align="center">{@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger} is CANCEL, converged</td> + * <td align="center">CONVERGED</td> + * <td align="center">Converged after a precapture sequenceis canceled, transient states are skipped by camera device.</td> + * </tr> + * <tr> * <td align="center">CONVERGED</td> * <td align="center">Camera device finished AE scan</td> * <td align="center">FLASH_REQUIRED</td> @@ -1637,7 +1691,7 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * <p>This control (except for MANUAL) is only effective if * <code>{@link CaptureRequest#CONTROL_MODE android.control.mode} != OFF</code> and any 3A routine is active.</p> * <p>ZERO_SHUTTER_LAG will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} - * contains ZSL. MANUAL will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} + * contains OPAQUE_REPROCESSING. MANUAL will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} * contains MANUAL_SENSOR. Other intent values are always supported.</p> * <p><b>Possible values:</b> * <ul> @@ -1865,10 +1919,6 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * update, as if this frame is never captured. This mode can be used in the scenario * where the application doesn't want a 3A manual control capture to affect * the subsequent auto 3A capture results.</p> - * <p>LEGACY mode devices will only support AUTO and USE_SCENE_MODE modes. - * LIMITED mode devices will only support OFF and OFF_KEEP_STATE if they - * support the MANUAL_SENSOR and MANUAL_POST_PROCSESING capabilities. - * FULL mode devices will always support OFF and OFF_KEEP_STATE.</p> * <p><b>Possible values:</b> * <ul> * <li>{@link #CONTROL_MODE_OFF OFF}</li> @@ -1876,9 +1926,12 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * <li>{@link #CONTROL_MODE_USE_SCENE_MODE USE_SCENE_MODE}</li> * <li>{@link #CONTROL_MODE_OFF_KEEP_STATE OFF_KEEP_STATE}</li> * </ul></p> + * <p><b>Available values for this device:</b><br> + * {@link CameraCharacteristics#CONTROL_AVAILABLE_MODES android.control.availableModes}</p> * <p>This key is available on all devices.</p> * * @see CaptureRequest#CONTROL_AF_MODE + * @see CameraCharacteristics#CONTROL_AVAILABLE_MODES * @see #CONTROL_MODE_OFF * @see #CONTROL_MODE_AUTO * @see #CONTROL_MODE_USE_SCENE_MODE @@ -1998,6 +2051,10 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * camera device will use the highest-quality enhancement algorithms, * even if it slows down capture rate. FAST means the camera device will * not slow down capture rate when applying edge enhancement.</p> + * <p>For YUV_REPROCESSING, these FAST/HIGH_QUALITY modes both mean that the camera + * device will apply FAST/HIGH_QUALITY YUV-domain edge enhancement, respectively. + * The camera device may adjust its internal noise reduction parameters for best + * image quality based on the {@link CaptureRequest#REPROCESS_EFFECTIVE_EXPOSURE_FACTOR android.reprocess.effectiveExposureFactor}, if it is set.</p> * <p><b>Possible values:</b> * <ul> * <li>{@link #EDGE_MODE_OFF OFF}</li> @@ -2013,6 +2070,7 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * * @see CameraCharacteristics#EDGE_AVAILABLE_EDGE_MODES * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL + * @see CaptureRequest#REPROCESS_EFFECTIVE_EXPOSURE_FACTOR * @see #EDGE_MODE_OFF * @see #EDGE_MODE_FAST * @see #EDGE_MODE_HIGH_QUALITY @@ -2475,18 +2533,28 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { /** * <p>Mode of operation for the noise reduction algorithm.</p> * <p>The noise reduction algorithm attempts to improve image quality by removing - * excessive noise added by the capture process, especially in dark conditions. - * OFF means no noise reduction will be applied by the camera device.</p> + * excessive noise added by the capture process, especially in dark conditions.</p> + * <p>OFF means no noise reduction will be applied by the camera device, for both raw and + * YUV domain.</p> + * <p>MINIMAL means that only sensor raw domain basic noise reduction is enabled ,to remove + * demosaicing or other processing artifacts. For YUV_REPROCESSING, MINIMAL is same as OFF. + * This mode is optional, may not be support by all devices. The application should check + * {@link CameraCharacteristics#NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES android.noiseReduction.availableNoiseReductionModes} before using it.</p> * <p>FAST/HIGH_QUALITY both mean camera device determined noise filtering * will be applied. HIGH_QUALITY mode indicates that the camera device * will use the highest-quality noise filtering algorithms, * even if it slows down capture rate. FAST means the camera device will not * slow down capture rate when applying noise filtering.</p> + * <p>For YUV_REPROCESSING, these FAST/HIGH_QUALITY modes both mean that the camera device + * will apply FAST/HIGH_QUALITY YUV domain noise reduction, respectively. The camera device + * may adjust the noise reduction parameters for best image quality based on the + * {@link CaptureRequest#REPROCESS_EFFECTIVE_EXPOSURE_FACTOR android.reprocess.effectiveExposureFactor} if it is set.</p> * <p><b>Possible values:</b> * <ul> * <li>{@link #NOISE_REDUCTION_MODE_OFF OFF}</li> * <li>{@link #NOISE_REDUCTION_MODE_FAST FAST}</li> * <li>{@link #NOISE_REDUCTION_MODE_HIGH_QUALITY HIGH_QUALITY}</li> + * <li>{@link #NOISE_REDUCTION_MODE_MINIMAL MINIMAL}</li> * </ul></p> * <p><b>Available values for this device:</b><br> * {@link CameraCharacteristics#NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES android.noiseReduction.availableNoiseReductionModes}</p> @@ -2497,9 +2565,11 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL * @see CameraCharacteristics#NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES + * @see CaptureRequest#REPROCESS_EFFECTIVE_EXPOSURE_FACTOR * @see #NOISE_REDUCTION_MODE_OFF * @see #NOISE_REDUCTION_MODE_FAST * @see #NOISE_REDUCTION_MODE_HIGH_QUALITY + * @see #NOISE_REDUCTION_MODE_MINIMAL */ @PublicKey public static final Key<Integer> NOISE_REDUCTION_MODE = @@ -2984,6 +3054,8 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * <li>{@link #SHADING_MODE_FAST FAST}</li> * <li>{@link #SHADING_MODE_HIGH_QUALITY HIGH_QUALITY}</li> * </ul></p> + * <p><b>Available values for this device:</b><br> + * {@link CameraCharacteristics#SHADING_AVAILABLE_MODES android.shading.availableModes}</p> * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> * <p><b>Full capability</b> - * Present on all camera devices that report being {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_FULL HARDWARE_LEVEL_FULL} devices in the @@ -2992,6 +3064,7 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * @see CaptureRequest#CONTROL_AE_MODE * @see CaptureRequest#CONTROL_AWB_MODE * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL + * @see CameraCharacteristics#SHADING_AVAILABLE_MODES * @see CaptureResult#STATISTICS_LENS_SHADING_CORRECTION_MAP * @see CaptureRequest#STATISTICS_LENS_SHADING_MAP_MODE * @see #SHADING_MODE_OFF @@ -3155,7 +3228,7 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { /** * <p>The shading map is a low-resolution floating-point map * that lists the coefficients used to correct for vignetting, for each - * Bayer color channel.</p> + * Bayer color channel of RAW image data.</p> * <p>The least shaded section of the image should have a gain factor * of 1; all other sections should have gains above 1.</p> * <p>When {@link CaptureRequest#COLOR_CORRECTION_MODE android.colorCorrection.mode} = TRANSFORM_MATRIX, the map @@ -3191,8 +3264,20 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * <img alt="Green (odd rows) lens shading map" src="../../../../images/camera2/metadata/android.statistics.lensShadingMap/green_o_shading.png" /> * <img alt="Blue lens shading map" src="../../../../images/camera2/metadata/android.statistics.lensShadingMap/blue_shading.png" /></p> * <p>As a visualization only, inverting the full-color map to recover an - * image of a gray wall (using bicubic interpolation for visual quality) as captured by the sensor gives:</p> + * image of a gray wall (using bicubic interpolation for visual quality) + * as captured by the sensor gives:</p> * <p><img alt="Image of a uniform white wall (inverse shading map)" src="../../../../images/camera2/metadata/android.statistics.lensShadingMap/inv_shading.png" /></p> + * <p>Note that the RAW image data might be subject to lens shading + * correction not reported on this map. Query + * {@link CameraCharacteristics#SENSOR_INFO_LENS_SHADING_APPLIED android.sensor.info.lensShadingApplied} to see if RAW image data has subject + * to lens shading correction. If {@link CameraCharacteristics#SENSOR_INFO_LENS_SHADING_APPLIED android.sensor.info.lensShadingApplied} + * is TRUE, the RAW image data is subject to partial or full lens shading + * correction. In the case full lens shading correction is applied to RAW + * images, the gain factor map reported in this key will contain all 1.0 gains. + * In other words, the map reported in this key is the remaining lens shading + * that needs to be applied on the RAW image to get images without lens shading + * artifacts. See {@link CameraCharacteristics#REQUEST_MAX_NUM_OUTPUT_RAW android.request.maxNumOutputRaw} for a list of RAW image + * formats.</p> * <p><b>Range of valid values:</b><br> * Each gain factor is >= 1</p> * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> @@ -3202,6 +3287,8 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * * @see CaptureRequest#COLOR_CORRECTION_MODE * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL + * @see CameraCharacteristics#REQUEST_MAX_NUM_OUTPUT_RAW + * @see CameraCharacteristics#SENSOR_INFO_LENS_SHADING_APPLIED * @hide */ public static final Key<float[]> STATISTICS_LENS_SHADING_MAP = @@ -3339,12 +3426,15 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * <li>{@link #STATISTICS_LENS_SHADING_MAP_MODE_OFF OFF}</li> * <li>{@link #STATISTICS_LENS_SHADING_MAP_MODE_ON ON}</li> * </ul></p> + * <p><b>Available values for this device:</b><br> + * {@link CameraCharacteristics#STATISTICS_INFO_AVAILABLE_LENS_SHADING_MAP_MODES android.statistics.info.availableLensShadingMapModes}</p> * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> * <p><b>Full capability</b> - * Present on all camera devices that report being {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_FULL HARDWARE_LEVEL_FULL} devices in the * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p> * * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL + * @see CameraCharacteristics#STATISTICS_INFO_AVAILABLE_LENS_SHADING_MAP_MODES * @see #STATISTICS_LENS_SHADING_MAP_MODE_OFF * @see #STATISTICS_LENS_SHADING_MAP_MODE_ON */ @@ -3531,6 +3621,8 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * <li>{@link #TONEMAP_MODE_CONTRAST_CURVE CONTRAST_CURVE}</li> * <li>{@link #TONEMAP_MODE_FAST FAST}</li> * <li>{@link #TONEMAP_MODE_HIGH_QUALITY HIGH_QUALITY}</li> + * <li>{@link #TONEMAP_MODE_GAMMA_VALUE GAMMA_VALUE}</li> + * <li>{@link #TONEMAP_MODE_PRESET_CURVE PRESET_CURVE}</li> * </ul></p> * <p><b>Available values for this device:</b><br> * {@link CameraCharacteristics#TONEMAP_AVAILABLE_TONE_MAP_MODES android.tonemap.availableToneMapModes}</p> @@ -3546,12 +3638,60 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * @see #TONEMAP_MODE_CONTRAST_CURVE * @see #TONEMAP_MODE_FAST * @see #TONEMAP_MODE_HIGH_QUALITY + * @see #TONEMAP_MODE_GAMMA_VALUE + * @see #TONEMAP_MODE_PRESET_CURVE */ @PublicKey public static final Key<Integer> TONEMAP_MODE = new Key<Integer>("android.tonemap.mode", int.class); /** + * <p>Tonemapping curve to use when {@link CaptureRequest#TONEMAP_MODE android.tonemap.mode} is + * GAMMA_VALUE</p> + * <p>The tonemap curve will be defined the following formula: + * * OUT = pow(IN, 1.0 / gamma) + * where IN and OUT is the input pixel value scaled to range [0.0, 1.0], + * pow is the power function and gamma is the gamma value specified by this + * key.</p> + * <p>The same curve will be applied to all color channels. The camera device + * may clip the input gamma value to its supported range. The actual applied + * value will be returned in capture result.</p> + * <p>The valid range of gamma value varies on different devices, but values + * within [1.0, 5.0] are guaranteed not to be clipped.</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * + * @see CaptureRequest#TONEMAP_MODE + */ + @PublicKey + public static final Key<Float> TONEMAP_GAMMA = + new Key<Float>("android.tonemap.gamma", float.class); + + /** + * <p>Tonemapping curve to use when {@link CaptureRequest#TONEMAP_MODE android.tonemap.mode} is + * PRESET_CURVE</p> + * <p>The tonemap curve will be defined by specified standard.</p> + * <p>sRGB (approximated by 16 control points):</p> + * <p><img alt="sRGB tonemapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/srgb_tonemap.png" /></p> + * <p>Rec. 709 (approximated by 16 control points):</p> + * <p><img alt="Rec. 709 tonemapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/rec709_tonemap.png" /></p> + * <p>Note that above figures show a 16 control points approximation of preset + * curves. Camera devices may apply a different approximation to the curve.</p> + * <p><b>Possible values:</b> + * <ul> + * <li>{@link #TONEMAP_PRESET_CURVE_SRGB SRGB}</li> + * <li>{@link #TONEMAP_PRESET_CURVE_REC709 REC709}</li> + * </ul></p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * + * @see CaptureRequest#TONEMAP_MODE + * @see #TONEMAP_PRESET_CURVE_SRGB + * @see #TONEMAP_PRESET_CURVE_REC709 + */ + @PublicKey + public static final Key<Integer> TONEMAP_PRESET_CURVE = + new Key<Integer>("android.tonemap.presetCurve", int.class); + + /** * <p>This LED is nominally used to indicate to the user * that the camera is powered on and may be streaming images back to the * Application Processor. In certain rare circumstances, the OS may @@ -3657,6 +3797,52 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { public static final Key<Long> SYNC_FRAME_NUMBER = new Key<Long>("android.sync.frameNumber", long.class); + /** + * <p>The amount of exposure time increase factor applied to the original output + * frame by the application processing before sending for reprocessing.</p> + * <p>This is optional, and will be supported if the camera device supports YUV_REPROCESSING + * capability ({@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains YUV_REPROCESSING).</p> + * <p>For some YUV reprocessing use cases, the application may choose to filter the original + * output frames to effectively reduce the noise to the same level as a frame that was + * captured with longer exposure time. To be more specific, assuming the original captured + * images were captured with a sensitivity of S and an exposure time of T, the model in + * the camera device is that the amount of noise in the image would be approximately what + * would be expected if the original capture parameters had been a sensitivity of + * S/effectiveExposureFactor and an exposure time of T*effectiveExposureFactor, rather + * than S and T respectively. If the captured images were processed by the application + * before being sent for reprocessing, then the application may have used image processing + * algorithms and/or multi-frame image fusion to reduce the noise in the + * application-processed images (input images). By using the effectiveExposureFactor + * control, the application can communicate to the camera device the actual noise level + * improvement in the application-processed image. With this information, the camera + * device can select appropriate noise reduction and edge enhancement parameters to avoid + * excessive noise reduction ({@link CaptureRequest#NOISE_REDUCTION_MODE android.noiseReduction.mode}) and insufficient edge + * enhancement ({@link CaptureRequest#EDGE_MODE android.edge.mode}) being applied to the reprocessed frames.</p> + * <p>For example, for multi-frame image fusion use case, the application may fuse + * multiple output frames together to a final frame for reprocessing. When N image are + * fused into 1 image for reprocessing, the exposure time increase factor could be up to + * square root of N (based on a simple photon shot noise model). The camera device will + * adjust the reprocessing noise reduction and edge enhancement parameters accordingly to + * produce the best quality images.</p> + * <p>This is relative factor, 1.0 indicates the application hasn't processed the input + * buffer in a way that affects its effective exposure time.</p> + * <p>This control is only effective for YUV reprocessing capture request. For noise + * reduction reprocessing, it is only effective when <code>{@link CaptureRequest#NOISE_REDUCTION_MODE android.noiseReduction.mode} != OFF</code>. + * Similarly, for edge enhancement reprocessing, it is only effective when + * <code>{@link CaptureRequest#EDGE_MODE android.edge.mode} != OFF</code>.</p> + * <p><b>Units</b>: Relative exposure time increase factor.</p> + * <p><b>Range of valid values:</b><br> + * >= 1.0</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * + * @see CaptureRequest#EDGE_MODE + * @see CaptureRequest#NOISE_REDUCTION_MODE + * @see CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES + */ + @PublicKey + public static final Key<Float> REPROCESS_EFFECTIVE_EXPOSURE_FACTOR = + new Key<Float>("android.reprocess.effectiveExposureFactor", float.class); + /*~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~ * End generated code *~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~O@*/ diff --git a/core/java/android/hardware/camera2/ICameraDeviceUser.aidl b/core/java/android/hardware/camera2/ICameraDeviceUser.aidl index 50a58ed..01f2396 100644 --- a/core/java/android/hardware/camera2/ICameraDeviceUser.aidl +++ b/core/java/android/hardware/camera2/ICameraDeviceUser.aidl @@ -16,7 +16,7 @@ package android.hardware.camera2; -import android.view.Surface; +import android.hardware.camera2.params.OutputConfiguration; import android.hardware.camera2.impl.CameraMetadataNative; import android.hardware.camera2.CaptureRequest; @@ -66,7 +66,7 @@ interface ICameraDeviceUser int deleteStream(int streamId); // non-negative value is the stream ID. negative value is status_t - int createStream(int width, int height, int format, in Surface surface); + int createStream(in OutputConfiguration outputConfiguration); int createDefaultRequest(int templateId, out CameraMetadataNative request); diff --git a/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java index 5bc7f71..e87a2f8 100644 --- a/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java @@ -21,11 +21,9 @@ import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.dispatch.ArgumentReplacingDispatcher; import android.hardware.camera2.dispatch.BroadcastDispatcher; -import android.hardware.camera2.dispatch.Dispatchable; import android.hardware.camera2.dispatch.DuckTypingDispatcher; import android.hardware.camera2.dispatch.HandlerDispatcher; import android.hardware.camera2.dispatch.InvokeDispatcher; -import android.hardware.camera2.dispatch.NullDispatcher; import android.hardware.camera2.utils.TaskDrainer; import android.hardware.camera2.utils.TaskSingleDrainer; import android.os.Handler; diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java index ec450bd..38f8e39 100644 --- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java @@ -28,6 +28,7 @@ import android.hardware.camera2.CaptureFailure; import android.hardware.camera2.ICameraDeviceCallbacks; import android.hardware.camera2.ICameraDeviceUser; import android.hardware.camera2.TotalCaptureResult; +import android.hardware.camera2.params.OutputConfiguration; import android.hardware.camera2.utils.CameraBinderDecorator; import android.hardware.camera2.utils.CameraRuntimeException; import android.hardware.camera2.utils.LongParcelable; @@ -78,7 +79,8 @@ public class CameraDeviceImpl extends CameraDevice { private int mRepeatingRequestId = REQUEST_ID_NONE; private final ArrayList<Integer> mRepeatingRequestIdDeletedList = new ArrayList<Integer>(); // Map stream IDs to Surfaces - private final SparseArray<Surface> mConfiguredOutputs = new SparseArray<Surface>(); + private final SparseArray<OutputConfiguration> mConfiguredOutputs = + new SparseArray<OutputConfiguration>(); private final String mCameraId; private final CameraCharacteristics mCharacteristics; @@ -314,7 +316,11 @@ public class CameraDeviceImpl extends CameraDevice { public void configureOutputs(List<Surface> outputs) throws CameraAccessException { // Leave this here for backwards compatibility with older code using this directly - configureOutputsChecked(outputs); + ArrayList<OutputConfiguration> outputConfigs = new ArrayList<>(outputs.size()); + for (Surface s : outputs) { + outputConfigs.add(new OutputConfiguration(s)); + } + configureOutputsChecked(outputConfigs); } /** @@ -333,28 +339,30 @@ public class CameraDeviceImpl extends CameraDevice { * * @throws CameraAccessException if there were any unexpected problems during configuration */ - public boolean configureOutputsChecked(List<Surface> outputs) throws CameraAccessException { + public boolean configureOutputsChecked(List<OutputConfiguration> outputs) + throws CameraAccessException { // Treat a null input the same an empty list if (outputs == null) { - outputs = new ArrayList<Surface>(); + outputs = new ArrayList<OutputConfiguration>(); } boolean success = false; synchronized(mInterfaceLock) { checkIfCameraClosedOrInError(); - - HashSet<Surface> addSet = new HashSet<Surface>(outputs); // Streams to create - List<Integer> deleteList = new ArrayList<Integer>(); // Streams to delete + // Streams to create + HashSet<OutputConfiguration> addSet = new HashSet<OutputConfiguration>(outputs); + // Streams to delete + List<Integer> deleteList = new ArrayList<Integer>(); // Determine which streams need to be created, which to be deleted for (int i = 0; i < mConfiguredOutputs.size(); ++i) { int streamId = mConfiguredOutputs.keyAt(i); - Surface s = mConfiguredOutputs.valueAt(i); + OutputConfiguration outConfig = mConfiguredOutputs.valueAt(i); - if (!outputs.contains(s)) { + if (!outputs.contains(outConfig)) { deleteList.add(streamId); } else { - addSet.remove(s); // Don't create a stream previously created + addSet.remove(outConfig); // Don't create a stream previously created } } @@ -372,11 +380,11 @@ public class CameraDeviceImpl extends CameraDevice { } // Add all new streams - for (Surface s : addSet) { - // TODO: remove width,height,format since we are ignoring - // it. - int streamId = mRemoteDevice.createStream(0, 0, 0, s); - mConfiguredOutputs.put(streamId, s); + for (OutputConfiguration outConfig : outputs) { + if (addSet.contains(outConfig)) { + int streamId = mRemoteDevice.createStream(outConfig); + mConfiguredOutputs.put(streamId, outConfig); + } } try { @@ -417,6 +425,18 @@ public class CameraDeviceImpl extends CameraDevice { public void createCaptureSession(List<Surface> outputs, CameraCaptureSession.StateCallback callback, Handler handler) throws CameraAccessException { + List<OutputConfiguration> outConfigurations = new ArrayList<>(outputs.size()); + for (Surface surface : outputs) { + outConfigurations.add(new OutputConfiguration(surface)); + } + createCaptureSessionByOutputConfiguration(outConfigurations, callback, handler); + } + + @Override + public void createCaptureSessionByOutputConfiguration( + List<OutputConfiguration> outputConfigurations, + CameraCaptureSession.StateCallback callback, Handler handler) + throws CameraAccessException { synchronized(mInterfaceLock) { if (DEBUG) { Log.d(TAG, "createCaptureSession"); @@ -434,7 +454,8 @@ public class CameraDeviceImpl extends CameraDevice { boolean configureSuccess = true; CameraAccessException pendingException = null; try { - configureSuccess = configureOutputsChecked(outputs); // and then block until IDLE + // configure outputs and then block until IDLE + configureSuccess = configureOutputsChecked(outputConfigurations); } catch (CameraAccessException e) { configureSuccess = false; pendingException = e; @@ -443,10 +464,14 @@ public class CameraDeviceImpl extends CameraDevice { } } + List<Surface> outSurfaces = new ArrayList<>(outputConfigurations.size()); + for (OutputConfiguration config : outputConfigurations) { + outSurfaces.add(config.getSurface()); + } // Fire onConfigured if configureOutputs succeeded, fire onConfigureFailed otherwise. CameraCaptureSessionImpl newSession = new CameraCaptureSessionImpl(mNextSessionId++, - outputs, callback, handler, this, mDeviceHandler, + outSurfaces, callback, handler, this, mDeviceHandler, configureSuccess); // TODO: wait until current session closes, then create the new session diff --git a/core/java/android/hardware/camera2/legacy/BurstHolder.java b/core/java/android/hardware/camera2/legacy/BurstHolder.java index b9c89f8..e7b3682 100644 --- a/core/java/android/hardware/camera2/legacy/BurstHolder.java +++ b/core/java/android/hardware/camera2/legacy/BurstHolder.java @@ -17,10 +17,6 @@ package android.hardware.camera2.legacy; import android.hardware.camera2.CaptureRequest; -import android.hardware.camera2.impl.CameraMetadataNative; -import android.util.Log; -import android.view.Surface; - import java.util.ArrayList; import java.util.Collection; import java.util.List; diff --git a/core/java/android/hardware/camera2/legacy/CameraDeviceUserShim.java b/core/java/android/hardware/camera2/legacy/CameraDeviceUserShim.java index fcf172c..70f3463 100644 --- a/core/java/android/hardware/camera2/legacy/CameraDeviceUserShim.java +++ b/core/java/android/hardware/camera2/legacy/CameraDeviceUserShim.java @@ -26,6 +26,7 @@ import android.hardware.camera2.ICameraDeviceUser; import android.hardware.camera2.utils.LongParcelable; import android.hardware.camera2.impl.CameraMetadataNative; import android.hardware.camera2.impl.CaptureResultExtras; +import android.hardware.camera2.params.OutputConfiguration; import android.hardware.camera2.utils.CameraBinderDecorator; import android.hardware.camera2.utils.CameraRuntimeException; import android.os.ConditionVariable; @@ -504,7 +505,7 @@ public class CameraDeviceUserShim implements ICameraDeviceUser { } @Override - public int createStream(int width, int height, int format, Surface surface) { + public int createStream(OutputConfiguration outputConfiguration) { if (DEBUG) { Log.d(TAG, "createStream called."); } @@ -518,8 +519,12 @@ public class CameraDeviceUserShim implements ICameraDeviceUser { Log.e(TAG, "Cannot create stream, beginConfigure hasn't been called yet."); return CameraBinderDecorator.INVALID_OPERATION; } + if (outputConfiguration.getRotation() != OutputConfiguration.ROTATION_0) { + Log.e(TAG, "Cannot create stream, stream rotation is not supported."); + return CameraBinderDecorator.INVALID_OPERATION; + } int id = ++mSurfaceIdCounter; - mSurfaces.put(id, surface); + mSurfaces.put(id, outputConfiguration.getSurface()); return id; } } diff --git a/core/java/android/hardware/camera2/legacy/LegacyCameraDevice.java b/core/java/android/hardware/camera2/legacy/LegacyCameraDevice.java index 367a078..b5a019d 100644 --- a/core/java/android/hardware/camera2/legacy/LegacyCameraDevice.java +++ b/core/java/android/hardware/camera2/legacy/LegacyCameraDevice.java @@ -292,6 +292,10 @@ public class LegacyCameraDevice implements AutoCloseable { Log.e(TAG, "configureOutputs - null outputs are not allowed"); return BAD_VALUE; } + if (!output.isValid()) { + Log.e(TAG, "configureOutputs - invalid output surfaces are not allowed"); + return BAD_VALUE; + } StreamConfigurationMap streamConfigurations = mStaticCharacteristics. get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); @@ -522,7 +526,7 @@ public class LegacyCameraDevice implements AutoCloseable { * @return the width and height of the surface * * @throws NullPointerException if the {@code surface} was {@code null} - * @throws IllegalStateException if the {@code surface} was invalid + * @throws BufferQueueAbandonedException if the {@code surface} was invalid */ public static Size getSurfaceSize(Surface surface) throws BufferQueueAbandonedException { checkNotNull(surface); diff --git a/core/java/android/hardware/camera2/legacy/LegacyExceptionUtils.java b/core/java/android/hardware/camera2/legacy/LegacyExceptionUtils.java index 7e0c01b..4b7cfbf 100644 --- a/core/java/android/hardware/camera2/legacy/LegacyExceptionUtils.java +++ b/core/java/android/hardware/camera2/legacy/LegacyExceptionUtils.java @@ -60,7 +60,7 @@ public class LegacyExceptionUtils { case CameraBinderDecorator.NO_ERROR: { return CameraBinderDecorator.NO_ERROR; } - case CameraBinderDecorator.ENODEV: { + case CameraBinderDecorator.BAD_VALUE: { throw new BufferQueueAbandonedException(); } } diff --git a/core/java/android/hardware/camera2/legacy/LegacyFaceDetectMapper.java b/core/java/android/hardware/camera2/legacy/LegacyFaceDetectMapper.java index 6215a8f..e576beb 100644 --- a/core/java/android/hardware/camera2/legacy/LegacyFaceDetectMapper.java +++ b/core/java/android/hardware/camera2/legacy/LegacyFaceDetectMapper.java @@ -33,7 +33,6 @@ import android.util.Size; import com.android.internal.util.ArrayUtils; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import static android.hardware.camera2.CaptureRequest.*; diff --git a/core/java/android/hardware/camera2/legacy/LegacyMetadataMapper.java b/core/java/android/hardware/camera2/legacy/LegacyMetadataMapper.java index 347db05..8776418 100644 --- a/core/java/android/hardware/camera2/legacy/LegacyMetadataMapper.java +++ b/core/java/android/hardware/camera2/legacy/LegacyMetadataMapper.java @@ -42,7 +42,6 @@ import android.util.SizeF; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Comparator; import java.util.List; import static com.android.internal.util.Preconditions.*; @@ -474,6 +473,15 @@ public class LegacyMetadataMapper { m.set(CONTROL_AE_COMPENSATION_STEP, ParamsUtils.createRational(step)); } + + /* + * control.aeLockAvailable + */ + { + boolean aeLockAvailable = p.isAutoExposureLockSupported(); + + m.set(CONTROL_AE_LOCK_AVAILABLE, aeLockAvailable); + } } @@ -571,6 +579,16 @@ public class LegacyMetadataMapper { Log.v(TAG, "mapControlAwb - control.awbAvailableModes set to " + ListUtils.listToString(awbAvail)); } + + + /* + * control.awbLockAvailable + */ + { + boolean awbLockAvailable = p.isAutoWhiteBalanceLockSupported(); + + m.set(CONTROL_AWB_LOCK_AVAILABLE, awbLockAvailable); + } } } @@ -618,17 +636,44 @@ public class LegacyMetadataMapper { /* * android.control.availableSceneModes */ + int maxNumDetectedFaces = p.getMaxNumDetectedFaces(); List<String> sceneModes = p.getSupportedSceneModes(); List<Integer> supportedSceneModes = ArrayUtils.convertStringListToIntList(sceneModes, sLegacySceneModes, sSceneModes); - if (supportedSceneModes == null) { // camera1 doesn't support scene mode settings - supportedSceneModes = new ArrayList<Integer>(); - supportedSceneModes.add(CONTROL_SCENE_MODE_DISABLED); // disabled is always available + + // Special case where the only scene mode listed is AUTO => no scene mode + if (sceneModes != null && sceneModes.size() == 1 && + sceneModes.get(0) == Parameters.SCENE_MODE_AUTO) { + supportedSceneModes = null; } - if (p.getMaxNumDetectedFaces() > 0) { // always supports FACE_PRIORITY when face detecting - supportedSceneModes.add(CONTROL_SCENE_MODE_FACE_PRIORITY); + + boolean sceneModeSupported = true; + if (supportedSceneModes == null && maxNumDetectedFaces == 0) { + sceneModeSupported = false; } - m.set(CONTROL_AVAILABLE_SCENE_MODES, ArrayUtils.toIntArray(supportedSceneModes)); + + if (sceneModeSupported) { + if (supportedSceneModes == null) { + supportedSceneModes = new ArrayList<Integer>(); + } + if (maxNumDetectedFaces > 0) { // always supports FACE_PRIORITY when face detecting + supportedSceneModes.add(CONTROL_SCENE_MODE_FACE_PRIORITY); + } + // Remove all DISABLED occurrences + if (supportedSceneModes.contains(CONTROL_SCENE_MODE_DISABLED)) { + while(supportedSceneModes.remove(new Integer(CONTROL_SCENE_MODE_DISABLED))) {} + } + m.set(CONTROL_AVAILABLE_SCENE_MODES, ArrayUtils.toIntArray(supportedSceneModes)); + } else { + m.set(CONTROL_AVAILABLE_SCENE_MODES, new int[] {CONTROL_SCENE_MODE_DISABLED}); + } + + /* + * android.control.availableModes + */ + m.set(CONTROL_AVAILABLE_MODES, sceneModeSupported ? + new int[] { CONTROL_MODE_AUTO, CONTROL_MODE_USE_SCENE_MODE } : + new int[] { CONTROL_MODE_AUTO }); } private static void mapLens(CameraMetadataNative m, Camera.Parameters p) { diff --git a/core/java/android/hardware/camera2/legacy/LegacyRequestMapper.java b/core/java/android/hardware/camera2/legacy/LegacyRequestMapper.java index 61f7b8b..3688610 100644 --- a/core/java/android/hardware/camera2/legacy/LegacyRequestMapper.java +++ b/core/java/android/hardware/camera2/legacy/LegacyRequestMapper.java @@ -34,7 +34,6 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; -import static com.android.internal.util.Preconditions.*; import static android.hardware.camera2.CaptureRequest.*; /** diff --git a/core/java/android/hardware/camera2/legacy/ParameterUtils.java b/core/java/android/hardware/camera2/legacy/ParameterUtils.java index 3b10eb5..9e9a6fe 100644 --- a/core/java/android/hardware/camera2/legacy/ParameterUtils.java +++ b/core/java/android/hardware/camera2/legacy/ParameterUtils.java @@ -22,8 +22,6 @@ import android.graphics.Rect; import android.graphics.RectF; import android.hardware.Camera; import android.hardware.Camera.Area; -import android.hardware.camera2.legacy.ParameterUtils.MeteringData; -import android.hardware.camera2.legacy.ParameterUtils.ZoomData; import android.hardware.camera2.params.Face; import android.hardware.camera2.params.MeteringRectangle; import android.hardware.camera2.utils.ListUtils; diff --git a/core/java/android/hardware/camera2/legacy/PerfMeasurement.java b/core/java/android/hardware/camera2/legacy/PerfMeasurement.java index b930ec2..53278c7 100644 --- a/core/java/android/hardware/camera2/legacy/PerfMeasurement.java +++ b/core/java/android/hardware/camera2/legacy/PerfMeasurement.java @@ -20,7 +20,6 @@ import android.os.SystemClock; import android.util.Log; import java.io.BufferedWriter; -import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.util.ArrayList; diff --git a/core/java/android/hardware/camera2/legacy/RequestHandlerThread.java b/core/java/android/hardware/camera2/legacy/RequestHandlerThread.java index 0699ffb..e19ebf2 100644 --- a/core/java/android/hardware/camera2/legacy/RequestHandlerThread.java +++ b/core/java/android/hardware/camera2/legacy/RequestHandlerThread.java @@ -96,15 +96,15 @@ public class RequestHandlerThread extends HandlerThread { // Blocks until thread is idling public void waitUntilIdle() { Handler handler = waitAndGetHandler(); - Looper looper = handler.getLooper(); - if (looper.isIdling()) { + MessageQueue queue = handler.getLooper().getQueue(); + if (queue.isIdle()) { return; } mIdle.close(); - looper.getQueue().addIdleHandler(mIdleHandler); + queue.addIdleHandler(mIdleHandler); // Ensure that the idle handler gets run even if the looper already went idle handler.sendEmptyMessage(MSG_POKE_IDLE_HANDLER); - if (looper.isIdling()) { + if (queue.isIdle()) { return; } mIdle.block(); diff --git a/core/java/android/hardware/camera2/legacy/RequestHolder.java b/core/java/android/hardware/camera2/legacy/RequestHolder.java index edd8e4e..9b628fb 100644 --- a/core/java/android/hardware/camera2/legacy/RequestHolder.java +++ b/core/java/android/hardware/camera2/legacy/RequestHolder.java @@ -17,7 +17,6 @@ package android.hardware.camera2.legacy; import android.hardware.camera2.CaptureRequest; -import android.hardware.camera2.impl.CameraMetadataNative; import android.util.Log; import android.view.Surface; diff --git a/core/java/android/hardware/camera2/legacy/RequestThreadManager.java b/core/java/android/hardware/camera2/legacy/RequestThreadManager.java index f1f2f0c..691798f 100644 --- a/core/java/android/hardware/camera2/legacy/RequestThreadManager.java +++ b/core/java/android/hardware/camera2/legacy/RequestThreadManager.java @@ -16,13 +16,11 @@ package android.hardware.camera2.legacy; -import android.graphics.ImageFormat; import android.graphics.SurfaceTexture; import android.hardware.Camera; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.impl.CameraDeviceImpl; -import android.hardware.camera2.params.StreamConfigurationMap; import android.hardware.camera2.utils.LongParcelable; import android.hardware.camera2.utils.SizeAreaComparator; import android.hardware.camera2.impl.CameraMetadataNative; @@ -38,7 +36,6 @@ import android.view.Surface; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Iterator; @@ -85,7 +82,7 @@ public class RequestThreadManager { private static final int PREVIEW_FRAME_TIMEOUT = 1000; // ms private static final int JPEG_FRAME_TIMEOUT = 4000; // ms (same as CTS for API2) - private static final int REQUEST_COMPLETE_TIMEOUT = JPEG_FRAME_TIMEOUT; // ms (same as JPEG timeout) + private static final int REQUEST_COMPLETE_TIMEOUT = JPEG_FRAME_TIMEOUT; private static final float ASPECT_RATIO_TOLERANCE = 0.01f; private boolean mPreviewRunning = false; @@ -501,6 +498,10 @@ public class RequestThreadManager { return; } for(Surface s : surfaces) { + if (s == null || !s.isValid()) { + Log.w(TAG, "Jpeg surface is invalid, skipping..."); + continue; + } try { LegacyCameraDevice.setSurfaceFormat(s, LegacyMetadataMapper.HAL_PIXEL_FORMAT_BLOB); } catch (LegacyExceptionUtils.BufferQueueAbandonedException e) { diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableArray.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableArray.java index 22b87ef..d89518b 100644 --- a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableArray.java +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableArray.java @@ -25,9 +25,6 @@ import java.lang.reflect.Array; import java.nio.ByteBuffer; import java.util.ArrayList; -import static android.hardware.camera2.impl.CameraMetadataNative.*; -import static android.hardware.camera2.marshal.MarshalHelpers.*; - /** * Marshal any array {@code T}. * diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableParcelable.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableParcelable.java index 1fd6a1d..0b7a4bf 100644 --- a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableParcelable.java +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableParcelable.java @@ -25,9 +25,6 @@ import android.util.Log; import java.lang.reflect.Field; import java.nio.ByteBuffer; -import static android.hardware.camera2.impl.CameraMetadataNative.*; -import static android.hardware.camera2.marshal.MarshalHelpers.*; - /** * Marshal any {@code T extends Parcelable} to/from any native type * diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryablePrimitive.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryablePrimitive.java index 189b597..090dd48 100644 --- a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryablePrimitive.java +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryablePrimitive.java @@ -23,8 +23,6 @@ import android.util.Rational; import static android.hardware.camera2.impl.CameraMetadataNative.*; import static android.hardware.camera2.marshal.MarshalHelpers.*; -import static com.android.internal.util.Preconditions.*; - import java.nio.ByteBuffer; /** diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableRange.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableRange.java index 8512804..64763e7 100644 --- a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableRange.java +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableRange.java @@ -27,9 +27,6 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.nio.ByteBuffer; -import static android.hardware.camera2.impl.CameraMetadataNative.*; -import static android.hardware.camera2.marshal.MarshalHelpers.*; - /** * Marshal {@link Range} to/from any native type */ diff --git a/core/java/android/hardware/camera2/params/LensShadingMap.java b/core/java/android/hardware/camera2/params/LensShadingMap.java index 9bbc33a..d6b84f2 100644 --- a/core/java/android/hardware/camera2/params/LensShadingMap.java +++ b/core/java/android/hardware/camera2/params/LensShadingMap.java @@ -19,7 +19,6 @@ package android.hardware.camera2.params; import static com.android.internal.util.Preconditions.*; import static android.hardware.camera2.params.RggbChannelVector.*; -import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CaptureResult; import android.hardware.camera2.utils.HashCodeHelpers; @@ -238,6 +237,51 @@ public final class LensShadingMap { return HashCodeHelpers.hashCode(mRows, mColumns, elemsHash); } + /** + * Return the LensShadingMap as a string representation. + * + * <p> {@code "LensShadingMap{R:([%f, %f, ... %f], ... [%f, %f, ... %f]), G_even:([%f, %f, ... + * %f], ... [%f, %f, ... %f]), G_odd:([%f, %f, ... %f], ... [%f, %f, ... %f]), B:([%f, %f, ... + * %f], ... [%f, %f, ... %f])}"}, + * where each {@code %f} represents one gain factor and each {@code [%f, %f, ... %f]} represents + * a row of the lens shading map</p> + * + * @return string representation of {@link LensShadingMap} + */ + @Override + public String toString() { + StringBuilder str = new StringBuilder(); + str.append("LensShadingMap{"); + + final String channelPrefix[] = {"R:(", "G_even:(", "G_odd:(", "B:("}; + + for (int ch = 0; ch < COUNT; ch++) { + str.append(channelPrefix[ch]); + + for (int r = 0; r < mRows; r++) { + str.append("["); + for (int c = 0; c < mColumns; c++) { + float gain = getGainFactor(ch, c, r); + str.append(gain); + if (c < mColumns - 1) { + str.append(", "); + } + } + str.append("]"); + if (r < mRows - 1) { + str.append(", "); + } + } + + str.append(")"); + if (ch < COUNT - 1) { + str.append(", "); + } + } + + str.append("}"); + return str.toString(); + } private final int mRows; private final int mColumns; diff --git a/core/java/android/hardware/camera2/params/OutputConfiguration.aidl b/core/java/android/hardware/camera2/params/OutputConfiguration.aidl new file mode 100644 index 0000000..0921cd8 --- /dev/null +++ b/core/java/android/hardware/camera2/params/OutputConfiguration.aidl @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.camera2.params; + +/** @hide */ +parcelable OutputConfiguration; diff --git a/core/java/android/hardware/camera2/params/OutputConfiguration.java b/core/java/android/hardware/camera2/params/OutputConfiguration.java new file mode 100644 index 0000000..0a4ed39 --- /dev/null +++ b/core/java/android/hardware/camera2/params/OutputConfiguration.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.hardware.camera2.params; + +import android.hardware.camera2.CameraDevice; +import android.util.Log; +import android.view.Surface; +import android.os.Parcel; +import android.os.Parcelable; + +import static com.android.internal.util.Preconditions.*; + +/** + * A class for describing camera output, which contains a {@link Surface} and its specific + * configuration for creating capture session. + * + * @see CameraDevice#createCaptureSession + * + * @hide + */ +public final class OutputConfiguration implements Parcelable { + + /** + * Rotation constant: 0 degree rotation (no rotation) + */ + public static final int ROTATION_0 = 0; + + /** + * Rotation constant: 90 degree counterclockwise rotation. + */ + public static final int ROTATION_90 = 1; + + /** + * Rotation constant: 180 degree counterclockwise rotation. + */ + public static final int ROTATION_180 = 2; + + /** + * Rotation constant: 270 degree counterclockwise rotation. + */ + public static final int ROTATION_270 = 3; + + /** + * Create a new immutable SurfaceConfiguration instance. + * + * @param surface + * A Surface for camera to output to. + * + * <p>This constructor creates a default configuration</p> + * + */ + public OutputConfiguration(Surface surface) { + checkNotNull(surface, "Surface must not be null"); + mSurface = surface; + mRotation = ROTATION_0; + } + + /** + * Create a new immutable SurfaceConfiguration instance. + * + * <p>This constructor takes an argument for desired camera rotation</p> + * + * @param surface + * A Surface for camera to output to. + * @param rotation + * The desired rotation to be applied on camera output. Value must be one of + * ROTATION_[0, 90, 180, 270]. Note that when the rotation is 90 or 270 degree, + * application should make sure corresponding surface size has width and height + * transposed corresponding to the width and height without rotation. For example, + * if application needs camera to capture 1280x720 picture and rotate it by 90 degree, + * application should set rotation to {@code ROTATION_90} and make sure the + * corresponding Surface size is 720x1280. Note that {@link CameraDevice} might + * throw {@code IllegalArgumentException} if device cannot perform such rotation. + * + */ + public OutputConfiguration(Surface surface, int rotation) { + checkNotNull(surface, "Surface must not be null"); + checkArgumentInRange(rotation, ROTATION_0, ROTATION_270, "Rotation constant"); + mSurface = surface; + mRotation = rotation; + } + + /** + * Create an OutputConfiguration from Parcel. + */ + private OutputConfiguration(Parcel source) { + int rotation = source.readInt(); + Surface surface = Surface.CREATOR.createFromParcel(source); + checkNotNull(surface, "Surface must not be null"); + checkArgumentInRange(rotation, ROTATION_0, ROTATION_270, "Rotation constant"); + mSurface = surface; + mRotation = rotation; + } + + /** + * Get the {@link Surface} associated with this {@link OutputConfiguration}. + * + * @return the {@link Surface} associated with this {@link OutputConfiguration}. + */ + public Surface getSurface() { + return mSurface; + } + + /** + * Get the rotation associated with this {@link OutputConfiguration}. + * + * @return the rotation associated with this {@link OutputConfiguration}. + * Value will be one of ROTATION_[0, 90, 180, 270] + */ + public int getRotation() { + return mRotation; + } + + public static final Parcelable.Creator<OutputConfiguration> CREATOR = + new Parcelable.Creator<OutputConfiguration>() { + @Override + public OutputConfiguration createFromParcel(Parcel source) { + try { + OutputConfiguration outputConfiguration = new OutputConfiguration(source); + return outputConfiguration; + } catch (Exception e) { + Log.e(TAG, "Exception creating OutputConfiguration from parcel", e); + return null; + } + } + + @Override + public OutputConfiguration[] newArray(int size) { + return new OutputConfiguration[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + if (dest == null) { + throw new IllegalArgumentException("dest must not be null"); + } + dest.writeInt(mRotation); + mSurface.writeToParcel(dest, flags); + } + + private static final String TAG = "OutputConfiguration"; + private final Surface mSurface; + private final int mRotation; +} diff --git a/core/java/android/hardware/camera2/params/StreamConfigurationMap.java b/core/java/android/hardware/camera2/params/StreamConfigurationMap.java index 479c842..f5304f8 100644 --- a/core/java/android/hardware/camera2/params/StreamConfigurationMap.java +++ b/core/java/android/hardware/camera2/params/StreamConfigurationMap.java @@ -26,7 +26,6 @@ import android.hardware.camera2.legacy.LegacyCameraDevice; import android.hardware.camera2.legacy.LegacyMetadataMapper; import android.hardware.camera2.legacy.LegacyExceptionUtils.BufferQueueAbandonedException; import android.view.Surface; -import android.util.Log; import android.util.Range; import android.util.Size; diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java index 0051ef5..d9f9c1e 100644 --- a/core/java/android/hardware/display/DisplayManagerGlobal.java +++ b/core/java/android/hardware/display/DisplayManagerGlobal.java @@ -17,10 +17,10 @@ package android.hardware.display; import android.content.Context; +import android.content.res.Configuration; import android.hardware.display.DisplayManager.DisplayListener; import android.media.projection.MediaProjection; import android.media.projection.IMediaProjection; -import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.Looper; @@ -196,11 +196,11 @@ public final class DisplayManagerGlobal { * Gets information about a logical display without applying any compatibility metrics. * * @param displayId The logical display id. - * @param IBinder the activity token for this display. + * @param configuration the configuration. * @return The display object, or null if there is no display with the given id. */ - public Display getRealDisplay(int displayId, IBinder token) { - return getCompatibleDisplay(displayId, new DisplayAdjustments(token)); + public Display getRealDisplay(int displayId, Configuration configuration) { + return getCompatibleDisplay(displayId, new DisplayAdjustments(configuration)); } public void registerDisplayListener(DisplayListener listener, Handler handler) { diff --git a/core/java/android/hardware/display/VirtualDisplay.java b/core/java/android/hardware/display/VirtualDisplay.java index 4ddf10f..d354666 100644 --- a/core/java/android/hardware/display/VirtualDisplay.java +++ b/core/java/android/hardware/display/VirtualDisplay.java @@ -15,7 +15,6 @@ */ package android.hardware.display; -import android.os.IBinder; import android.view.Display; import android.view.Surface; diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index e5995b0..444f020 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -27,8 +27,6 @@ import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; -import android.os.Parcel; -import android.os.Parcelable; import android.os.RemoteException; import android.os.ServiceManager; import android.os.Vibrator; diff --git a/core/java/android/hardware/radio/RadioManager.java b/core/java/android/hardware/radio/RadioManager.java new file mode 100644 index 0000000..32930a7 --- /dev/null +++ b/core/java/android/hardware/radio/RadioManager.java @@ -0,0 +1,1308 @@ +/** + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.radio; + +import android.annotation.SystemApi; +import android.content.Context; +import android.os.Handler; +import android.os.Parcel; +import android.os.Parcelable; +import java.util.List; +import java.util.Arrays; + +/** + * The RadioManager class allows to control a broadcast radio tuner present on the device. + * It provides data structures and methods to query for available radio modules, list their + * properties and open an interface to control tuning operations and receive callbacks when + * asynchronous operations complete or events occur. + * @hide + */ +@SystemApi +public class RadioManager { + + /** Method return status: successful operation */ + public static final int STATUS_OK = 0; + /** Method return status: unspecified error */ + public static final int STATUS_ERROR = Integer.MIN_VALUE; + /** Method return status: permission denied */ + public static final int STATUS_PERMISSION_DENIED = -1; + /** Method return status: initialization failure */ + public static final int STATUS_NO_INIT = -19; + /** Method return status: invalid argument provided */ + public static final int STATUS_BAD_VALUE = -22; + /** Method return status: cannot reach service */ + public static final int STATUS_DEAD_OBJECT = -32; + /** Method return status: invalid or out of sequence operation */ + public static final int STATUS_INVALID_OPERATION = -38; + /** Method return status: time out before operation completion */ + public static final int STATUS_TIMED_OUT = -110; + + + // keep in sync with radio_class_t in /system/core/incluse/system/radio.h + /** Radio module class supporting FM (including HD radio) and AM */ + public static final int CLASS_AM_FM = 0; + /** Radio module class supporting satellite radio */ + public static final int CLASS_SAT = 1; + /** Radio module class supporting Digital terrestrial radio */ + public static final int CLASS_DT = 2; + + // keep in sync with radio_band_t in /system/core/incluse/system/radio.h + /** AM radio band (LW/MW/SW). + * @see BandDescriptor */ + public static final int BAND_AM = 0; + /** FM radio band. + * @see BandDescriptor */ + public static final int BAND_FM = 1; + /** FM HD radio or DRM band. + * @see BandDescriptor */ + public static final int BAND_FM_HD = 2; + /** AM HD radio or DRM band. + * @see BandDescriptor */ + public static final int BAND_AM_HD = 3; + + // keep in sync with radio_region_t in /system/core/incluse/system/radio.h + /** Africa, Europe. + * @see BandDescriptor */ + public static final int REGION_ITU_1 = 0; + /** Americas. + * @see BandDescriptor */ + public static final int REGION_ITU_2 = 1; + /** Russia. + * @see BandDescriptor */ + public static final int REGION_OIRT = 2; + /** Japan. + * @see BandDescriptor */ + public static final int REGION_JAPAN = 3; + /** Korea. + * @see BandDescriptor */ + public static final int REGION_KOREA = 4; + + /***************************************************************************** + * Lists properties, options and radio bands supported by a given broadcast radio module. + * Each module has a unique ID used to address it when calling RadioManager APIs. + * Module properties are returned by {@link #listModules(List <ModuleProperties>)} method. + ****************************************************************************/ + public static class ModuleProperties implements Parcelable { + + private final int mId; + private final int mClassId; + private final String mImplementor; + private final String mProduct; + private final String mVersion; + private final String mSerial; + private final int mNumTuners; + private final int mNumAudioSources; + private final boolean mIsCaptureSupported; + private final BandDescriptor[] mBands; + + ModuleProperties(int id, int classId, String implementor, String product, String version, + String serial, int numTuners, int numAudioSources, boolean isCaptureSupported, + BandDescriptor[] bands) { + mId = id; + mClassId = classId; + mImplementor = implementor; + mProduct = product; + mVersion = version; + mSerial = serial; + mNumTuners = numTuners; + mNumAudioSources = numAudioSources; + mIsCaptureSupported = isCaptureSupported; + mBands = bands; + } + + + /** Unique module identifier provided by the native service. + * For use with {@link #openTuner(int, BandConfig, boolean, Callback, Handler)}. + * @return the radio module unique identifier. + */ + public int getId() { + return mId; + } + + /** Module class identifier: {@link #CLASS_AM_FM}, {@link #CLASS_SAT}, {@link #CLASS_DT} + * @return the radio module class identifier. + */ + public int getClassId() { + return mClassId; + } + + /** Human readable broadcast radio module implementor + * @return the name of the radio module implementator. + */ + public String getImplementor() { + return mImplementor; + } + + /** Human readable broadcast radio module product name + * @return the radio module product name. + */ + public String getProduct() { + return mProduct; + } + + /** Human readable broadcast radio module version number + * @return the radio module version. + */ + public String getVersion() { + return mVersion; + } + + /** Radio module serial number. + * Can be used for subscription services. + * @return the radio module serial number. + */ + public String getSerial() { + return mSerial; + } + + /** Number of tuners available. + * This is the number of tuners that can be open simultaneously. + * @return the number of tuners supported. + */ + public int getNumTuners() { + return mNumTuners; + } + + /** Number tuner audio sources available. Must be less or equal to getNumTuners(). + * When more than one tuner is supported, one is usually for playback and has one + * associated audio source and the other is for pre scanning and building a + * program list. + * @return the number of audio sources available. + */ + public int getNumAudioSources() { + return mNumAudioSources; + } + + /** {@code true} if audio capture is possible from radio tuner output. + * This indicates if routing to audio devices not connected to the same HAL as the FM radio + * is possible (e.g. to USB) or DAR (Digital Audio Recorder) feature can be implemented. + * @return {@code true} if audio capture is possible, {@code false} otherwise. + */ + public boolean isCaptureSupported() { + return mIsCaptureSupported; + } + + /** List of descriptors for all bands supported by this module. + * @return an array of {@link BandDescriptor}. + */ + public BandDescriptor[] getBands() { + return mBands; + } + + private ModuleProperties(Parcel in) { + mId = in.readInt(); + mClassId = in.readInt(); + mImplementor = in.readString(); + mProduct = in.readString(); + mVersion = in.readString(); + mSerial = in.readString(); + mNumTuners = in.readInt(); + mNumAudioSources = in.readInt(); + mIsCaptureSupported = in.readInt() == 1; + Parcelable[] tmp = in.readParcelableArray(BandDescriptor.class.getClassLoader()); + mBands = new BandDescriptor[tmp.length]; + for (int i = 0; i < tmp.length; i++) { + mBands[i] = (BandDescriptor) tmp[i]; + } + } + + public static final Parcelable.Creator<ModuleProperties> CREATOR + = new Parcelable.Creator<ModuleProperties>() { + public ModuleProperties createFromParcel(Parcel in) { + return new ModuleProperties(in); + } + + public ModuleProperties[] newArray(int size) { + return new ModuleProperties[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mId); + dest.writeInt(mClassId); + dest.writeString(mImplementor); + dest.writeString(mProduct); + dest.writeString(mVersion); + dest.writeString(mSerial); + dest.writeInt(mNumTuners); + dest.writeInt(mNumAudioSources); + dest.writeInt(mIsCaptureSupported ? 1 : 0); + dest.writeParcelableArray(mBands, flags); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return "ModuleProperties [mId=" + mId + ", mClassId=" + mClassId + + ", mImplementor=" + mImplementor + ", mProduct=" + mProduct + + ", mVersion=" + mVersion + ", mSerial=" + mSerial + + ", mNumTuners=" + mNumTuners + + ", mNumAudioSources=" + mNumAudioSources + + ", mIsCaptureSupported=" + mIsCaptureSupported + + ", mBands=" + Arrays.toString(mBands) + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + mId; + result = prime * result + mClassId; + result = prime * result + ((mImplementor == null) ? 0 : mImplementor.hashCode()); + result = prime * result + ((mProduct == null) ? 0 : mProduct.hashCode()); + result = prime * result + ((mVersion == null) ? 0 : mVersion.hashCode()); + result = prime * result + ((mSerial == null) ? 0 : mSerial.hashCode()); + result = prime * result + mNumTuners; + result = prime * result + mNumAudioSources; + result = prime * result + (mIsCaptureSupported ? 1 : 0); + result = prime * result + Arrays.hashCode(mBands); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof ModuleProperties)) + return false; + ModuleProperties other = (ModuleProperties) obj; + if (mId != other.getId()) + return false; + if (mClassId != other.getClassId()) + return false; + if (mImplementor == null) { + if (other.getImplementor() != null) + return false; + } else if (!mImplementor.equals(other.getImplementor())) + return false; + if (mProduct == null) { + if (other.getProduct() != null) + return false; + } else if (!mProduct.equals(other.getProduct())) + return false; + if (mVersion == null) { + if (other.getVersion() != null) + return false; + } else if (!mVersion.equals(other.getVersion())) + return false; + if (mSerial == null) { + if (other.getSerial() != null) + return false; + } else if (!mSerial.equals(other.getSerial())) + return false; + if (mNumTuners != other.getNumTuners()) + return false; + if (mNumAudioSources != other.getNumAudioSources()) + return false; + if (mIsCaptureSupported != other.isCaptureSupported()) + return false; + if (!Arrays.equals(mBands, other.getBands())) + return false; + return true; + } + } + + /** Radio band descriptor: an element in ModuleProperties bands array. + * It is either an instance of {@link FmBandDescriptor} or {@link AmBandDescriptor} */ + public static class BandDescriptor implements Parcelable { + + private final int mRegion; + private final int mType; + private final int mLowerLimit; + private final int mUpperLimit; + private final int mSpacing; + + BandDescriptor(int region, int type, int lowerLimit, int upperLimit, int spacing) { + mRegion = region; + mType = type; + mLowerLimit = lowerLimit; + mUpperLimit = upperLimit; + mSpacing = spacing; + } + + /** Region this band applies to. E.g. {@link #REGION_ITU_1} + * @return the region this band is associated to. + */ + public int getRegion() { + return mRegion; + } + /** Band type, e.g {@link #BAND_FM}. Defines the subclass this descriptor can be cast to: + * <ul> + * <li>{@link #BAND_FM} or {@link #BAND_FM_HD} cast to {@link FmBandDescriptor}, </li> + * <li>{@link #BAND_AM} cast to {@link AmBandDescriptor}, </li> + * </ul> + * @return the band type. + */ + public int getType() { + return mType; + } + /** Lower band limit expressed in units according to band type. + * Currently all defined band types express channels as frequency in kHz + * @return the lower band limit. + */ + public int getLowerLimit() { + return mLowerLimit; + } + /** Upper band limit expressed in units according to band type. + * Currently all defined band types express channels as frequency in kHz + * @return the upper band limit. + */ + public int getUpperLimit() { + return mUpperLimit; + } + /** Channel spacing in units according to band type. + * Currently all defined band types express channels as frequency in kHz + * @return the channel spacing. + */ + public int getSpacing() { + return mSpacing; + } + + private BandDescriptor(Parcel in) { + mRegion = in.readInt(); + mType = in.readInt(); + mLowerLimit = in.readInt(); + mUpperLimit = in.readInt(); + mSpacing = in.readInt(); + } + + public static final Parcelable.Creator<BandDescriptor> CREATOR + = new Parcelable.Creator<BandDescriptor>() { + public BandDescriptor createFromParcel(Parcel in) { + return new BandDescriptor(in); + } + + public BandDescriptor[] newArray(int size) { + return new BandDescriptor[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mRegion); + dest.writeInt(mType); + dest.writeInt(mLowerLimit); + dest.writeInt(mUpperLimit); + dest.writeInt(mSpacing); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return "BandDescriptor [mRegion=" + mRegion + ", mType=" + mType + ", mLowerLimit=" + + mLowerLimit + ", mUpperLimit=" + mUpperLimit + ", mSpacing=" + mSpacing + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + mRegion; + result = prime * result + mType; + result = prime * result + mLowerLimit; + result = prime * result + mUpperLimit; + result = prime * result + mSpacing; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof BandDescriptor)) + return false; + BandDescriptor other = (BandDescriptor) obj; + if (mRegion != other.getRegion()) + return false; + if (mType != other.getType()) + return false; + if (mLowerLimit != other.getLowerLimit()) + return false; + if (mUpperLimit != other.getUpperLimit()) + return false; + if (mSpacing != other.getSpacing()) + return false; + return true; + } + } + + /** FM band descriptor + * @see #BAND_FM + * @see #BAND_FM_HD */ + public static class FmBandDescriptor extends BandDescriptor { + private final boolean mStereo; + private final boolean mRds; + private final boolean mTa; + private final boolean mAf; + + FmBandDescriptor(int region, int type, int lowerLimit, int upperLimit, int spacing, + boolean stereo, boolean rds, boolean ta, boolean af) { + super(region, type, lowerLimit, upperLimit, spacing); + mStereo = stereo; + mRds = rds; + mTa = ta; + mAf = af; + } + + /** Stereo is supported + * @return {@code true} if stereo is supported, {@code false} otherwise. + */ + public boolean isStereoSupported() { + return mStereo; + } + /** RDS or RBDS(if region is ITU2) is supported + * @return {@code true} if RDS or RBDS is supported, {@code false} otherwise. + */ + public boolean isRdsSupported() { + return mRds; + } + /** Traffic announcement is supported + * @return {@code true} if TA is supported, {@code false} otherwise. + */ + public boolean isTaSupported() { + return mTa; + } + /** Alternate Frequency Switching is supported + * @return {@code true} if AF switching is supported, {@code false} otherwise. + */ + public boolean isAfSupported() { + return mAf; + } + + /* Parcelable implementation */ + private FmBandDescriptor(Parcel in) { + super(in); + mStereo = in.readByte() == 1; + mRds = in.readByte() == 1; + mTa = in.readByte() == 1; + mAf = in.readByte() == 1; + } + + public static final Parcelable.Creator<FmBandDescriptor> CREATOR + = new Parcelable.Creator<FmBandDescriptor>() { + public FmBandDescriptor createFromParcel(Parcel in) { + return new FmBandDescriptor(in); + } + + public FmBandDescriptor[] newArray(int size) { + return new FmBandDescriptor[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeByte((byte) (mStereo ? 1 : 0)); + dest.writeByte((byte) (mRds ? 1 : 0)); + dest.writeByte((byte) (mTa ? 1 : 0)); + dest.writeByte((byte) (mAf ? 1 : 0)); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return "FmBandDescriptor [ "+ super.toString() + " mStereo=" + mStereo + + ", mRds=" + mRds + ", mTa=" + mTa + ", mAf=" + mAf + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + (mStereo ? 1 : 0); + result = prime * result + (mRds ? 1 : 0); + result = prime * result + (mTa ? 1 : 0); + result = prime * result + (mAf ? 1 : 0); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (!(obj instanceof FmBandDescriptor)) + return false; + FmBandDescriptor other = (FmBandDescriptor) obj; + if (mStereo != other.isStereoSupported()) + return false; + if (mRds != other.isRdsSupported()) + return false; + if (mTa != other.isTaSupported()) + return false; + if (mAf != other.isAfSupported()) + return false; + return true; + } + } + + /** AM band descriptor. + * @see #BAND_AM */ + public static class AmBandDescriptor extends BandDescriptor { + + private final boolean mStereo; + + AmBandDescriptor(int region, int type, int lowerLimit, int upperLimit, int spacing, + boolean stereo) { + super(region, type, lowerLimit, upperLimit, spacing); + mStereo = stereo; + } + + /** Stereo is supported + * @return {@code true} if stereo is supported, {@code false} otherwise. + */ + public boolean isStereoSupported() { + return mStereo; + } + + private AmBandDescriptor(Parcel in) { + super(in); + mStereo = in.readByte() == 1; + } + + public static final Parcelable.Creator<AmBandDescriptor> CREATOR + = new Parcelable.Creator<AmBandDescriptor>() { + public AmBandDescriptor createFromParcel(Parcel in) { + return new AmBandDescriptor(in); + } + + public AmBandDescriptor[] newArray(int size) { + return new AmBandDescriptor[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeByte((byte) (mStereo ? 1 : 0)); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return "AmBandDescriptor [ "+ super.toString() + " mStereo=" + mStereo + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + (mStereo ? 1 : 0); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (!(obj instanceof AmBandDescriptor)) + return false; + AmBandDescriptor other = (AmBandDescriptor) obj; + if (mStereo != other.isStereoSupported()) + return false; + return true; + } + } + + + /** Radio band configuration. */ + public static class BandConfig implements Parcelable { + + final BandDescriptor mDescriptor; + + BandConfig(BandDescriptor descriptor) { + mDescriptor = descriptor; + } + + BandConfig(int region, int type, int lowerLimit, int upperLimit, int spacing) { + mDescriptor = new BandDescriptor(region, type, lowerLimit, upperLimit, spacing); + } + + private BandConfig(Parcel in) { + mDescriptor = new BandDescriptor(in); + } + + BandDescriptor getDescriptor() { + return mDescriptor; + } + + /** Region this band applies to. E.g. {@link #REGION_ITU_1} + * @return the region associated with this band. + */ + public int getRegion() { + return mDescriptor.getRegion(); + } + /** Band type, e.g {@link #BAND_FM}. Defines the subclass this descriptor can be cast to: + * <ul> + * <li>{@link #BAND_FM} or {@link #BAND_FM_HD} cast to {@link FmBandDescriptor}, </li> + * <li>{@link #BAND_AM} cast to {@link AmBandDescriptor}, </li> + * </ul> + * @return the band type. + */ + public int getType() { + return mDescriptor.getType(); + } + /** Lower band limit expressed in units according to band type. + * Currently all defined band types express channels as frequency in kHz + * @return the lower band limit. + */ + public int getLowerLimit() { + return mDescriptor.getLowerLimit(); + } + /** Upper band limit expressed in units according to band type. + * Currently all defined band types express channels as frequency in kHz + * @return the upper band limit. + */ + public int getUpperLimit() { + return mDescriptor.getUpperLimit(); + } + /** Channel spacing in units according to band type. + * Currently all defined band types express channels as frequency in kHz + * @return the channel spacing. + */ + public int getSpacing() { + return mDescriptor.getSpacing(); + } + + + public static final Parcelable.Creator<BandConfig> CREATOR + = new Parcelable.Creator<BandConfig>() { + public BandConfig createFromParcel(Parcel in) { + return new BandConfig(in); + } + + public BandConfig[] newArray(int size) { + return new BandConfig[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + mDescriptor.writeToParcel(dest, flags); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return "BandConfig [ " + mDescriptor.toString() + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + mDescriptor.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof BandConfig)) + return false; + BandConfig other = (BandConfig) obj; + if (mDescriptor != other.getDescriptor()) + return false; + return true; + } + } + + /** FM band configuration. + * @see #BAND_FM + * @see #BAND_FM_HD */ + public static class FmBandConfig extends BandConfig { + private final boolean mStereo; + private final boolean mRds; + private final boolean mTa; + private final boolean mAf; + + FmBandConfig(FmBandDescriptor descriptor) { + super((BandDescriptor)descriptor); + mStereo = descriptor.isStereoSupported(); + mRds = descriptor.isRdsSupported(); + mTa = descriptor.isTaSupported(); + mAf = descriptor.isAfSupported(); + } + + FmBandConfig(int region, int type, int lowerLimit, int upperLimit, int spacing, + boolean stereo, boolean rds, boolean ta, boolean af) { + super(region, type, lowerLimit, upperLimit, spacing); + mStereo = stereo; + mRds = rds; + mTa = ta; + mAf = af; + } + + /** Get stereo enable state + * @return the enable state. + */ + public boolean getStereo() { + return mStereo; + } + + /** Get RDS or RBDS(if region is ITU2) enable state + * @return the enable state. + */ + public boolean getRds() { + return mRds; + } + + /** Get Traffic announcement enable state + * @return the enable state. + */ + public boolean getTa() { + return mTa; + } + + /** Get Alternate Frequency Switching enable state + * @return the enable state. + */ + public boolean getAf() { + return mAf; + } + + private FmBandConfig(Parcel in) { + super(in); + mStereo = in.readByte() == 1; + mRds = in.readByte() == 1; + mTa = in.readByte() == 1; + mAf = in.readByte() == 1; + } + + public static final Parcelable.Creator<FmBandConfig> CREATOR + = new Parcelable.Creator<FmBandConfig>() { + public FmBandConfig createFromParcel(Parcel in) { + return new FmBandConfig(in); + } + + public FmBandConfig[] newArray(int size) { + return new FmBandConfig[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeByte((byte) (mStereo ? 1 : 0)); + dest.writeByte((byte) (mRds ? 1 : 0)); + dest.writeByte((byte) (mTa ? 1 : 0)); + dest.writeByte((byte) (mAf ? 1 : 0)); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return "FmBandConfig [" + super.toString() + + ", mStereo=" + mStereo + ", mRds=" + mRds + ", mTa=" + mTa + + ", mAf=" + mAf + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + (mStereo ? 1 : 0); + result = prime * result + (mRds ? 1 : 0); + result = prime * result + (mTa ? 1 : 0); + result = prime * result + (mAf ? 1 : 0); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (!(obj instanceof FmBandConfig)) + return false; + FmBandConfig other = (FmBandConfig) obj; + if (mStereo != other.mStereo) + return false; + if (mRds != other.mRds) + return false; + if (mTa != other.mTa) + return false; + if (mAf != other.mAf) + return false; + return true; + } + + /** + * Builder class for {@link FmBandConfig} objects. + */ + public static class Builder { + private final BandDescriptor mDescriptor; + private boolean mStereo; + private boolean mRds; + private boolean mTa; + private boolean mAf; + + /** + * Constructs a new Builder with the defaults from an {@link FmBandDescriptor} . + * @param descriptor the FmBandDescriptor defaults are read from . + */ + public Builder(FmBandDescriptor descriptor) { + mDescriptor = new BandDescriptor(descriptor.getRegion(), descriptor.getType(), + descriptor.getLowerLimit(), descriptor.getUpperLimit(), + descriptor.getSpacing()); + mStereo = descriptor.isStereoSupported(); + mRds = descriptor.isRdsSupported(); + mTa = descriptor.isTaSupported(); + mAf = descriptor.isAfSupported(); + } + + /** + * Constructs a new Builder from a given {@link FmBandConfig} + * @param config the FmBandConfig object whose data will be reused in the new Builder. + */ + public Builder(FmBandConfig config) { + mDescriptor = new BandDescriptor(config.getRegion(), config.getType(), + config.getLowerLimit(), config.getUpperLimit(), config.getSpacing()); + mStereo = config.getStereo(); + mRds = config.getRds(); + mTa = config.getTa(); + mAf = config.getAf(); + } + + /** + * Combines all of the parameters that have been set and return a new + * {@link FmBandConfig} object. + * @return a new {@link FmBandConfig} object + */ + public FmBandConfig build() { + FmBandConfig config = new FmBandConfig(mDescriptor.getRegion(), + mDescriptor.getType(), mDescriptor.getLowerLimit(), + mDescriptor.getUpperLimit(), mDescriptor.getSpacing(), + mStereo, mRds, mTa, mAf); + return config; + } + + /** Set stereo enable state + * @param state The new enable state. + * @return the same Builder instance. + */ + public Builder setStereo(boolean state) { + mStereo = state; + return this; + } + + /** Set RDS or RBDS(if region is ITU2) enable state + * @param state The new enable state. + * @return the same Builder instance. + */ + public Builder setRds(boolean state) { + mRds = state; + return this; + } + + /** Set Traffic announcement enable state + * @param state The new enable state. + * @return the same Builder instance. + */ + public Builder setTa(boolean state) { + mTa = state; + return this; + } + + /** Set Alternate Frequency Switching enable state + * @param state The new enable state. + * @return the same Builder instance. + */ + public Builder setAf(boolean state) { + mAf = state; + return this; + } + }; + } + + /** AM band configuration. + * @see #BAND_AM */ + public static class AmBandConfig extends BandConfig { + private final boolean mStereo; + + AmBandConfig(AmBandDescriptor descriptor) { + super((BandDescriptor)descriptor); + mStereo = descriptor.isStereoSupported(); + } + + AmBandConfig(int region, int type, int lowerLimit, int upperLimit, int spacing, + boolean stereo) { + super(region, type, lowerLimit, upperLimit, spacing); + mStereo = stereo; + } + + /** Get stereo enable state + * @return the enable state. + */ + public boolean getStereo() { + return mStereo; + } + + private AmBandConfig(Parcel in) { + super(in); + mStereo = in.readByte() == 1; + } + + public static final Parcelable.Creator<AmBandConfig> CREATOR + = new Parcelable.Creator<AmBandConfig>() { + public AmBandConfig createFromParcel(Parcel in) { + return new AmBandConfig(in); + } + + public AmBandConfig[] newArray(int size) { + return new AmBandConfig[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeByte((byte) (mStereo ? 1 : 0)); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return "AmBandConfig [" + super.toString() + + ", mStereo=" + mStereo + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + (mStereo ? 1 : 0); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (!(obj instanceof AmBandConfig)) + return false; + AmBandConfig other = (AmBandConfig) obj; + if (mStereo != other.getStereo()) + return false; + return true; + } + + /** + * Builder class for {@link AmBandConfig} objects. + */ + public static class Builder { + private final BandDescriptor mDescriptor; + private boolean mStereo; + + /** + * Constructs a new Builder with the defaults from an {@link AmBandDescriptor} . + * @param descriptor the FmBandDescriptor defaults are read from . + */ + public Builder(AmBandDescriptor descriptor) { + mDescriptor = new BandDescriptor(descriptor.getRegion(), descriptor.getType(), + descriptor.getLowerLimit(), descriptor.getUpperLimit(), + descriptor.getSpacing()); + mStereo = descriptor.isStereoSupported(); + } + + /** + * Constructs a new Builder from a given {@link AmBandConfig} + * @param config the FmBandConfig object whose data will be reused in the new Builder. + */ + public Builder(AmBandConfig config) { + mDescriptor = new BandDescriptor(config.getRegion(), config.getType(), + config.getLowerLimit(), config.getUpperLimit(), config.getSpacing()); + mStereo = config.getStereo(); + } + + /** + * Combines all of the parameters that have been set and return a new + * {@link AmBandConfig} object. + * @return a new {@link AmBandConfig} object + */ + public AmBandConfig build() { + AmBandConfig config = new AmBandConfig(mDescriptor.getRegion(), + mDescriptor.getType(), mDescriptor.getLowerLimit(), + mDescriptor.getUpperLimit(), mDescriptor.getSpacing(), + mStereo); + return config; + } + + /** Set stereo enable state + * @param state The new enable state. + * @return the same Builder instance. + */ + public Builder setStereo(boolean state) { + mStereo = state; + return this; + } + }; + } + + /** Radio program information returned by + * {@link RadioTuner#getProgramInformation(RadioManager.ProgramInfo[])} */ + public static class ProgramInfo implements Parcelable { + + private final int mChannel; + private final int mSubChannel; + private final boolean mTuned; + private final boolean mStereo; + private final boolean mDigital; + private final int mSignalStrength; + private final RadioMetadata mMetadata; + + ProgramInfo(int channel, int subChannel, boolean tuned, boolean stereo, + boolean digital, int signalStrength, RadioMetadata metadata) { + mChannel = channel; + mSubChannel = subChannel; + mTuned = tuned; + mStereo = stereo; + mDigital = digital; + mSignalStrength = signalStrength; + mMetadata = metadata; + } + + /** Main channel expressed in units according to band type. + * Currently all defined band types express channels as frequency in kHz + * @return the program channel + */ + public int getChannel() { + return mChannel; + } + /** Sub channel ID. E.g 1 for HD radio HD1 + * @return the program sub channel + */ + public int getSubChannel() { + return mSubChannel; + } + /** {@code true} if the tuner is currently tuned on a valid station + * @return {@code true} if currently tuned, {@code false} otherwise. + */ + public boolean isTuned() { + return mTuned; + } + /** {@code true} if the received program is stereo + * @return {@code true} if stereo, {@code false} otherwise. + */ + public boolean isStereo() { + return mStereo; + } + /** {@code true} if the received program is digital (e.g HD radio) + * @return {@code true} if digital, {@code false} otherwise. + */ + public boolean isDigital() { + return mDigital; + } + /** Signal strength indicator from 0 (no signal) to 100 (excellent) + * @return the signal strength indication. + */ + public int getSignalStrength() { + return mSignalStrength; + } + /** Metadata currently received from this station. + * null if no metadata have been received + * @return current meta data received from this program. + */ + public RadioMetadata getMetadata() { + return mMetadata; + } + + private ProgramInfo(Parcel in) { + mChannel = in.readInt(); + mSubChannel = in.readInt(); + mTuned = in.readByte() == 1; + mStereo = in.readByte() == 1; + mDigital = in.readByte() == 1; + mSignalStrength = in.readInt(); + if (in.readByte() == 1) { + mMetadata = RadioMetadata.CREATOR.createFromParcel(in); + } else { + mMetadata = null; + } + } + + public static final Parcelable.Creator<ProgramInfo> CREATOR + = new Parcelable.Creator<ProgramInfo>() { + public ProgramInfo createFromParcel(Parcel in) { + return new ProgramInfo(in); + } + + public ProgramInfo[] newArray(int size) { + return new ProgramInfo[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mChannel); + dest.writeInt(mSubChannel); + dest.writeByte((byte)(mTuned ? 1 : 0)); + dest.writeByte((byte)(mStereo ? 1 : 0)); + dest.writeByte((byte)(mDigital ? 1 : 0)); + dest.writeInt(mSignalStrength); + if (mMetadata == null) { + dest.writeByte((byte)0); + } else { + dest.writeByte((byte)1); + mMetadata.writeToParcel(dest, flags); + } + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return "ProgramInfo [mChannel=" + mChannel + ", mSubChannel=" + mSubChannel + + ", mTuned=" + mTuned + ", mStereo=" + mStereo + ", mDigital=" + mDigital + + ", mSignalStrength=" + mSignalStrength + + ((mMetadata == null) ? "" : (", mMetadata=" + mMetadata.toString())) + + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + mChannel; + result = prime * result + mSubChannel; + result = prime * result + (mTuned ? 1 : 0); + result = prime * result + (mStereo ? 1 : 0); + result = prime * result + (mDigital ? 1 : 0); + result = prime * result + mSignalStrength; + result = prime * result + ((mMetadata == null) ? 0 : mMetadata.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof ProgramInfo)) + return false; + ProgramInfo other = (ProgramInfo) obj; + if (mChannel != other.getChannel()) + return false; + if (mSubChannel != other.getSubChannel()) + return false; + if (mTuned != other.isTuned()) + return false; + if (mStereo != other.isStereo()) + return false; + if (mDigital != other.isDigital()) + return false; + if (mSignalStrength != other.getSignalStrength()) + return false; + if (mMetadata == null) { + if (other.getMetadata() != null) + return false; + } else if (!mMetadata.equals(other.getMetadata())) + return false; + return true; + } + } + + + /** + * Returns a list of descriptors for all broadcast radio modules present on the device. + * @param modules An List of {@link ModuleProperties} where the list will be returned. + * @return + * <ul> + * <li>{@link #STATUS_OK} in case of success, </li> + * <li>{@link #STATUS_ERROR} in case of unspecified error, </li> + * <li>{@link #STATUS_NO_INIT} if the native service cannot be reached, </li> + * <li>{@link #STATUS_BAD_VALUE} if modules is null, </li> + * <li>{@link #STATUS_DEAD_OBJECT} if the binder transaction to the native service fails, </li> + * </ul> + */ + public native int listModules(List <ModuleProperties> modules); + + /** + * Open an interface to control a tuner on a given broadcast radio module. + * Optionally selects and applies the configuration passed as "config" argument. + * @param moduleId radio module identifier {@link ModuleProperties#getId()}. Mandatory. + * @param config desired band and configuration to apply when enabling the hardware module. + * optional, can be null. + * @param withAudio {@code true} to request a tuner with an audio source. + * This tuner is intended for live listening or recording or a radio program. + * If {@code false}, the tuner can only be used to retrieve program informations. + * @param callback {@link RadioTuner.Callback} interface. Mandatory. + * @param handler the Handler on which the callbacks will be received. + * Can be null if default handler is OK. + * @return a valid {@link RadioTuner} interface in case of success or null in case of error. + */ + public RadioTuner openTuner(int moduleId, BandConfig config, boolean withAudio, + RadioTuner.Callback callback, Handler handler) { + if (callback == null) { + return null; + } + RadioModule module = new RadioModule(moduleId, config, withAudio, callback, handler); + if (module != null) { + if (!module.initCheck()) { + module = null; + } + } + return (RadioTuner)module; + } + + private final Context mContext; + + /** + * @hide + */ + public RadioManager(Context context) { + mContext = context; + } +} diff --git a/core/java/android/hardware/radio/RadioMetadata.java b/core/java/android/hardware/radio/RadioMetadata.java new file mode 100644 index 0000000..8b1851b --- /dev/null +++ b/core/java/android/hardware/radio/RadioMetadata.java @@ -0,0 +1,449 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.hardware.radio; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.content.ContentResolver; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Log; +import android.util.SparseArray; + +import java.util.ArrayList; +import java.util.Set; + +/** + * Contains meta data about a radio program such as station name, song title, artist etc... + * @hide + */ +@SystemApi +public final class RadioMetadata implements Parcelable { + private static final String TAG = "RadioMetadata"; + + /** + * The RDS Program Information. + */ + public static final String METADATA_KEY_RDS_PI = "android.hardware.radio.metadata.RDS_PI"; + + /** + * The RDS Program Service. + */ + public static final String METADATA_KEY_RDS_PS = "android.hardware.radio.metadata.RDS_PS"; + + /** + * The RDS PTY. + */ + public static final String METADATA_KEY_RDS_PTY = "android.hardware.radio.metadata.RDS_PTY"; + + /** + * The RBDS PTY. + */ + public static final String METADATA_KEY_RBDS_PTY = "android.hardware.radio.metadata.RBDS_PTY"; + + /** + * The RBDS Radio Text. + */ + public static final String METADATA_KEY_RDS_RT = "android.hardware.radio.metadata.RDS_RT"; + + /** + * The song title. + */ + public static final String METADATA_KEY_TITLE = "android.hardware.radio.metadata.TITLE"; + + /** + * The artist name. + */ + public static final String METADATA_KEY_ARTIST = "android.hardware.radio.metadata.ARTIST"; + + /** + * The album name. + */ + public static final String METADATA_KEY_ALBUM = "android.hardware.radio.metadata.ALBUM"; + + /** + * The music genre. + */ + public static final String METADATA_KEY_GENRE = "android.hardware.radio.metadata.GENRE"; + + /** + * The radio station icon {@link Bitmap}. + */ + public static final String METADATA_KEY_ICON = "android.hardware.radio.metadata.ICON"; + + /** + * The artwork for the song/album {@link Bitmap}. + */ + public static final String METADATA_KEY_ART = "android.hardware.radio.metadata.ART"; + + + private static final int METADATA_TYPE_INVALID = -1; + private static final int METADATA_TYPE_INT = 0; + private static final int METADATA_TYPE_TEXT = 1; + private static final int METADATA_TYPE_BITMAP = 2; + + private static final ArrayMap<String, Integer> METADATA_KEYS_TYPE; + + static { + METADATA_KEYS_TYPE = new ArrayMap<String, Integer>(); + METADATA_KEYS_TYPE.put(METADATA_KEY_RDS_PI, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_RDS_PS, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_RDS_PTY, METADATA_TYPE_INT); + METADATA_KEYS_TYPE.put(METADATA_KEY_RBDS_PTY, METADATA_TYPE_INT); + METADATA_KEYS_TYPE.put(METADATA_KEY_RDS_RT, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_TITLE, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_ARTIST, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_GENRE, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_ICON, METADATA_TYPE_BITMAP); + METADATA_KEYS_TYPE.put(METADATA_KEY_ART, METADATA_TYPE_BITMAP); + } + + // keep in sync with: system/media/radio/include/system/radio_metadata.h + private static final int NATIVE_KEY_INVALID = -1; + private static final int NATIVE_KEY_RDS_PI = 0; + private static final int NATIVE_KEY_RDS_PS = 1; + private static final int NATIVE_KEY_RDS_PTY = 2; + private static final int NATIVE_KEY_RBDS_PTY = 3; + private static final int NATIVE_KEY_RDS_RT = 4; + private static final int NATIVE_KEY_TITLE = 5; + private static final int NATIVE_KEY_ARTIST = 6; + private static final int NATIVE_KEY_ALBUM = 7; + private static final int NATIVE_KEY_GENRE = 8; + private static final int NATIVE_KEY_ICON = 9; + private static final int NATIVE_KEY_ART = 10; + + private static final SparseArray<String> NATIVE_KEY_MAPPING; + + static { + NATIVE_KEY_MAPPING = new SparseArray<String>(); + NATIVE_KEY_MAPPING.put(NATIVE_KEY_RDS_PI, METADATA_KEY_RDS_PI); + NATIVE_KEY_MAPPING.put(NATIVE_KEY_RDS_PS, METADATA_KEY_RDS_PS); + NATIVE_KEY_MAPPING.put(NATIVE_KEY_RDS_PTY, METADATA_KEY_RDS_PTY); + NATIVE_KEY_MAPPING.put(NATIVE_KEY_RBDS_PTY, METADATA_KEY_RBDS_PTY); + NATIVE_KEY_MAPPING.put(NATIVE_KEY_RDS_RT, METADATA_KEY_RDS_RT); + NATIVE_KEY_MAPPING.put(NATIVE_KEY_TITLE, METADATA_KEY_TITLE); + NATIVE_KEY_MAPPING.put(NATIVE_KEY_ARTIST, METADATA_KEY_ARTIST); + NATIVE_KEY_MAPPING.put(NATIVE_KEY_ALBUM, METADATA_KEY_ALBUM); + NATIVE_KEY_MAPPING.put(NATIVE_KEY_GENRE, METADATA_KEY_GENRE); + NATIVE_KEY_MAPPING.put(NATIVE_KEY_ICON, METADATA_KEY_ICON); + NATIVE_KEY_MAPPING.put(NATIVE_KEY_ART, METADATA_KEY_ART); + } + + private final Bundle mBundle; + + RadioMetadata() { + mBundle = new Bundle(); + } + + private RadioMetadata(Bundle bundle) { + mBundle = new Bundle(bundle); + } + + private RadioMetadata(Parcel in) { + mBundle = in.readBundle(); + } + + /** + * Returns {@code true} if the given key is contained in the meta data + * + * @param key a String key + * @return {@code true} if the key exists in this meta data, {@code false} otherwise + */ + public boolean containsKey(String key) { + return mBundle.containsKey(key); + } + + /** + * Returns the text value associated with the given key as a String, or null + * if the key is not found in the meta data. + * + * @param key The key the value is stored under + * @return a String value, or null + */ + public String getString(String key) { + return mBundle.getString(key); + } + + /** + * Returns the value associated with the given key, + * or 0 if the key is not found in the meta data. + * + * @param key The key the value is stored under + * @return an int value + */ + public int getInt(String key) { + return mBundle.getInt(key, 0); + } + + /** + * Returns a {@link Bitmap} for the given key or null if the key is not found in the meta data. + * + * @param key The key the value is stored under + * @return a {@link Bitmap} or null + */ + public Bitmap getBitmap(String key) { + Bitmap bmp = null; + try { + bmp = mBundle.getParcelable(key); + } catch (Exception e) { + // ignore, value was not a bitmap + Log.w(TAG, "Failed to retrieve a key as Bitmap.", e); + } + return bmp; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeBundle(mBundle); + } + + /** + * Returns the number of fields in this meta data. + * + * @return the number of fields in the meta data. + */ + public int size() { + return mBundle.size(); + } + + /** + * Returns a Set containing the Strings used as keys in this meta data. + * + * @return a Set of String keys + */ + public Set<String> keySet() { + return mBundle.keySet(); + } + + /** + * Helper for getting the String key used by {@link RadioMetadata} from the + * corrsponding native integer key. + * + * @param editorKey The key used by the editor + * @return the key used by this class or null if no mapping exists + * @hide + */ + public static String getKeyFromNativeKey(int nativeKey) { + return NATIVE_KEY_MAPPING.get(nativeKey, null); + } + + public static final Parcelable.Creator<RadioMetadata> CREATOR = + new Parcelable.Creator<RadioMetadata>() { + @Override + public RadioMetadata createFromParcel(Parcel in) { + return new RadioMetadata(in); + } + + @Override + public RadioMetadata[] newArray(int size) { + return new RadioMetadata[size]; + } + }; + + /** + * Use to build RadioMetadata objects. + */ + public static final class Builder { + private final Bundle mBundle; + + /** + * Create an empty Builder. Any field that should be included in the + * {@link RadioMetadata} must be added. + */ + public Builder() { + mBundle = new Bundle(); + } + + /** + * Create a Builder using a {@link RadioMetadata} instance to set the + * initial values. All fields in the source meta data will be included in + * the new meta data. Fields can be overwritten by adding the same key. + * + * @param source + */ + public Builder(RadioMetadata source) { + mBundle = new Bundle(source.mBundle); + } + + /** + * Create a Builder using a {@link RadioMetadata} instance to set + * initial values, but replace bitmaps with a scaled down copy if they + * are larger than maxBitmapSize. + * + * @param source The original meta data to copy. + * @param maxBitmapSize The maximum height/width for bitmaps contained + * in the meta data. + * @hide + */ + public Builder(RadioMetadata source, int maxBitmapSize) { + this(source); + for (String key : mBundle.keySet()) { + Object value = mBundle.get(key); + if (value != null && value instanceof Bitmap) { + Bitmap bmp = (Bitmap) value; + if (bmp.getHeight() > maxBitmapSize || bmp.getWidth() > maxBitmapSize) { + putBitmap(key, scaleBitmap(bmp, maxBitmapSize)); + } + } + } + } + + /** + * Put a String value into the meta data. Custom keys may be used, but if + * the METADATA_KEYs defined in this class are used they may only be one + * of the following: + * <ul> + * <li>{@link #METADATA_KEY_RDS_PI}</li> + * <li>{@link #METADATA_KEY_RDS_PS}</li> + * <li>{@link #METADATA_KEY_RDS_RT}</li> + * <li>{@link #METADATA_KEY_TITLE}</li> + * <li>{@link #METADATA_KEY_ARTIST}</li> + * <li>{@link #METADATA_KEY_ALBUM}</li> + * <li>{@link #METADATA_KEY_GENRE}</li> + * </ul> + * + * @param key The key for referencing this value + * @param value The String value to store + * @return the same Builder instance + */ + public Builder putString(String key, String value) { + if (!METADATA_KEYS_TYPE.containsKey(key) || + METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_TEXT) { + throw new IllegalArgumentException("The " + key + + " key cannot be used to put a String"); + } + mBundle.putString(key, value); + return this; + } + + /** + * Put an int value into the meta data. Custom keys may be used, but if + * the METADATA_KEYs defined in this class are used they may only be one + * of the following: + * <ul> + * <li>{@link #METADATA_KEY_RDS_PTY}</li> + * <li>{@link #METADATA_KEY_RBDS_PTY}</li> + * </ul> + * + * @param key The key for referencing this value + * @param value The int value to store + * @return the same Builder instance + */ + public Builder putInt(String key, int value) { + if (!METADATA_KEYS_TYPE.containsKey(key) || + METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_INT) { + throw new IllegalArgumentException("The " + key + + " key cannot be used to put a long"); + } + mBundle.putInt(key, value); + return this; + } + + /** + * Put a {@link Bitmap} into the meta data. Custom keys may be used, but + * if the METADATA_KEYs defined in this class are used they may only be + * one of the following: + * <ul> + * <li>{@link #METADATA_KEY_ICON}</li> + * <li>{@link #METADATA_KEY_ART}</li> + * </ul> + * <p> + * + * @param key The key for referencing this value + * @param value The Bitmap to store + * @return the same Builder instance + */ + public Builder putBitmap(String key, Bitmap value) { + if (!METADATA_KEYS_TYPE.containsKey(key) || + METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_BITMAP) { + throw new IllegalArgumentException("The " + key + + " key cannot be used to put a Bitmap"); + } + mBundle.putParcelable(key, value); + return this; + } + + /** + * Creates a {@link RadioMetadata} instance with the specified fields. + * + * @return a new {@link RadioMetadata} object + */ + public RadioMetadata build() { + return new RadioMetadata(mBundle); + } + + private Bitmap scaleBitmap(Bitmap bmp, int maxSize) { + float maxSizeF = maxSize; + float widthScale = maxSizeF / bmp.getWidth(); + float heightScale = maxSizeF / bmp.getHeight(); + float scale = Math.min(widthScale, heightScale); + int height = (int) (bmp.getHeight() * scale); + int width = (int) (bmp.getWidth() * scale); + return Bitmap.createScaledBitmap(bmp, width, height, true); + } + } + + int putIntFromNative(int nativeKey, int value) { + String key = getKeyFromNativeKey(nativeKey); + if (!METADATA_KEYS_TYPE.containsKey(key) || + METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_INT) { + return -1; + } + mBundle.putInt(key, value); + return 0; + } + + int putStringFromNative(int nativeKey, String value) { + String key = getKeyFromNativeKey(nativeKey); + if (!METADATA_KEYS_TYPE.containsKey(key) || + METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_TEXT) { + return -1; + } + mBundle.putString(key, value); + return 0; + } + + int putBitmapFromNative(int nativeKey, byte[] value) { + String key = getKeyFromNativeKey(nativeKey); + if (!METADATA_KEYS_TYPE.containsKey(key) || + METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_BITMAP) { + return -1; + } + Bitmap bmp = null; + try { + bmp = BitmapFactory.decodeByteArray(value, 0, value.length); + } catch (Exception e) { + } finally { + if (bmp == null) { + return -1; + } + mBundle.putParcelable(key, bmp); + return 0; + } + } +} diff --git a/core/java/android/hardware/radio/RadioModule.java b/core/java/android/hardware/radio/RadioModule.java new file mode 100644 index 0000000..15916ae --- /dev/null +++ b/core/java/android/hardware/radio/RadioModule.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.radio; + +import android.annotation.SystemApi; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import java.lang.ref.WeakReference; +import java.util.UUID; + +/** + * A RadioModule implements the RadioTuner interface for a broadcast radio tuner physically + * present on the device and exposed by the radio HAL. + * + * @hide + */ +public class RadioModule extends RadioTuner { + private long mNativeContext = 0; + private int mId; + private NativeEventHandlerDelegate mEventHandlerDelegate; + + RadioModule(int moduleId, RadioManager.BandConfig config, boolean withAudio, + RadioTuner.Callback callback, Handler handler) { + mId = moduleId; + mEventHandlerDelegate = new NativeEventHandlerDelegate(callback, handler); + native_setup(new WeakReference<RadioModule>(this), config, withAudio); + } + private native void native_setup(Object module_this, + RadioManager.BandConfig config, boolean withAudio); + + @Override + protected void finalize() { + native_finalize(); + } + private native void native_finalize(); + + boolean initCheck() { + return mNativeContext != 0; + } + + // RadioTuner implementation + public native void close(); + + public native int setConfiguration(RadioManager.BandConfig config); + + public native int getConfiguration(RadioManager.BandConfig[] config); + + public native int setMute(boolean mute); + + public native boolean getMute(); + + public native int step(int direction, boolean skipSubChannel); + + public native int scan(int direction, boolean skipSubChannel); + + public native int tune(int channel, int subChannel); + + public native int cancel(); + + public native int getProgramInformation(RadioManager.ProgramInfo[] info); + + public native boolean isAntennaConnected(); + + public native boolean hasControl(); + + + /* keep in sync with radio_event_type_t in system/core/include/system/radio.h */ + static final int EVENT_HW_FAILURE = 0; + static final int EVENT_CONFIG = 1; + static final int EVENT_ANTENNA = 2; + static final int EVENT_TUNED = 3; + static final int EVENT_METADATA = 4; + static final int EVENT_TA = 5; + static final int EVENT_AF_SWITCH = 6; + static final int EVENT_CONTROL = 100; + static final int EVENT_SERVER_DIED = 101; + + private class NativeEventHandlerDelegate { + private final Handler mHandler; + + NativeEventHandlerDelegate(final RadioTuner.Callback callback, + Handler handler) { + // find the looper for our new event handler + Looper looper; + if (handler != null) { + looper = handler.getLooper(); + } else { + looper = Looper.getMainLooper(); + } + + // construct the event handler with this looper + if (looper != null) { + // implement the event handler delegate + mHandler = new Handler(looper) { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case EVENT_HW_FAILURE: + if (callback != null) { + callback.onError(RadioTuner.ERROR_HARDWARE_FAILURE); + } + break; + case EVENT_CONFIG: { + RadioManager.BandConfig config = (RadioManager.BandConfig)msg.obj; + switch(msg.arg1) { + case RadioManager.STATUS_OK: + if (callback != null) { + callback.onConfigurationChanged(config); + } + break; + default: + if (callback != null) { + callback.onError(RadioTuner.ERROR_CONFIG); + } + break; + } + } break; + case EVENT_ANTENNA: + if (callback != null) { + callback.onAntennaState(msg.arg2 == 1); + } + break; + case EVENT_AF_SWITCH: + case EVENT_TUNED: { + RadioManager.ProgramInfo info = (RadioManager.ProgramInfo)msg.obj; + switch (msg.arg1) { + case RadioManager.STATUS_OK: + if (callback != null) { + callback.onProgramInfoChanged(info); + } + break; + case RadioManager.STATUS_TIMED_OUT: + if (callback != null) { + callback.onError(RadioTuner.ERROR_SCAN_TIMEOUT); + } + break; + case RadioManager.STATUS_INVALID_OPERATION: + default: + if (callback != null) { + callback.onError(RadioTuner.ERROR_CANCELLED); + } + break; + } + } break; + case EVENT_METADATA: { + RadioMetadata metadata = (RadioMetadata)msg.obj; + if (callback != null) { + callback.onMetadataChanged(metadata); + } + } break; + case EVENT_TA: + if (callback != null) { + callback.onTrafficAnnouncement(msg.arg2 == 1); + } + break; + case EVENT_CONTROL: + if (callback != null) { + callback.onControlChanged(msg.arg2 == 1); + } + break; + case EVENT_SERVER_DIED: + if (callback != null) { + callback.onError(RadioTuner.ERROR_SERVER_DIED); + } + break; + default: + // Should not happen + break; + } + } + }; + } else { + mHandler = null; + } + } + + Handler handler() { + return mHandler; + } + } + + + @SuppressWarnings("unused") + private static void postEventFromNative(Object module_ref, + int what, int arg1, int arg2, Object obj) { + RadioModule module = (RadioModule)((WeakReference)module_ref).get(); + if (module == null) { + return; + } + + NativeEventHandlerDelegate delegate = module.mEventHandlerDelegate; + if (delegate != null) { + Handler handler = delegate.handler(); + if (handler != null) { + Message m = handler.obtainMessage(what, arg1, arg2, obj); + handler.sendMessage(m); + } + } + } +} + diff --git a/core/java/android/hardware/radio/RadioTuner.java b/core/java/android/hardware/radio/RadioTuner.java new file mode 100644 index 0000000..376900a --- /dev/null +++ b/core/java/android/hardware/radio/RadioTuner.java @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.radio; + +import android.annotation.SystemApi; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import java.lang.ref.WeakReference; +import java.util.UUID; + +/** + * RadioTuner interface provides methods to control a radio tuner on the device: selecting and + * configuring the active band, muting/unmuting, scanning and tuning, etc... + * + * Obtain a RadioTuner interface by calling {@link RadioManager#openTuner(int, + * RadioManager.BandConfig, boolean, RadioTuner.Callback, Handler)}. + * @hide + */ +@SystemApi +public abstract class RadioTuner { + + /** Scanning direction UP for {@link #step(int, boolean)}, {@link #scan(int, boolean)} */ + public static final int DIRECTION_UP = 0; + + /** Scanning directions DOWN for {@link #step(int, boolean)}, {@link #scan(int, boolean)} */ + public static final int DIRECTION_DOWN = 1; + + /** + * Close the tuner interface. The {@link Callback} callback will not be called + * anymore and associated resources will be released. + * Must be called when the tuner is not needed to make hardware resources available to others. + * */ + public abstract void close(); + + /** + * Set the active band configuration for this module. + * Must be a valid configuration obtained via buildConfig() from a valid BandDescriptor listed + * in the ModuleProperties of the module with the specified ID. + * @param config The desired band configuration (FmBandConfig or AmBandConfig). + * @return + * <ul> + * <li>{@link RadioManager#STATUS_OK} in case of success, </li> + * <li>{@link RadioManager#STATUS_ERROR} in case of unspecified error, </li> + * <li>{@link RadioManager#STATUS_NO_INIT} if the native service cannot be reached, </li> + * <li>{@link RadioManager#STATUS_BAD_VALUE} if parameters are invalid, </li> + * <li>{@link RadioManager#STATUS_INVALID_OPERATION} if the call is out of sequence, </li> + * <li>{@link RadioManager#STATUS_DEAD_OBJECT} if the binder transaction to the native + * service fails, </li> + * </ul> + */ + public abstract int setConfiguration(RadioManager.BandConfig config); + + /** + * Get current configuration. + * @param config a BandConfig array of lengh 1 where the configuration is returned. + * @return + * <ul> + * <li>{@link RadioManager#STATUS_OK} in case of success, </li> + * <li>{@link RadioManager#STATUS_ERROR} in case of unspecified error, </li> + * <li>{@link RadioManager#STATUS_NO_INIT} if the native service cannot be reached, </li> + * <li>{@link RadioManager#STATUS_BAD_VALUE} if parameters are invalid, </li> + * <li>{@link RadioManager#STATUS_INVALID_OPERATION} if the call is out of sequence, </li> + * <li>{@link RadioManager#STATUS_DEAD_OBJECT} if the binder transaction to the native + * service fails, </li> + * </ul> + */ + public abstract int getConfiguration(RadioManager.BandConfig[] config); + + + /** + * Set mute state. When muted, the radio tuner audio source is not available for playback on + * any audio device. when unmuted, the radio tuner audio source is output as a media source + * and renderd over the audio device selected for media use case. + * The radio tuner audio source is muted by default when the tuner is first attached. + * Only effective if the tuner is attached with audio enabled. + * + * @param mute the requested mute state. + * @return + * <ul> + * <li>{@link RadioManager#STATUS_OK} in case of success, </li> + * <li>{@link RadioManager#STATUS_ERROR} in case of unspecified error, </li> + * <li>{@link RadioManager#STATUS_NO_INIT} if the native service cannot be reached, </li> + * <li>{@link RadioManager#STATUS_INVALID_OPERATION} if the call is out of sequence, </li> + * <li>{@link RadioManager#STATUS_DEAD_OBJECT} if the binder transaction to the native + * service fails, </li> + * </ul> + */ + public abstract int setMute(boolean mute); + + /** + * Get mute state. + * + * @return {@code true} if the radio tuner audio source is muted or a problem occured + * retrieving the mute state, {@code false} otherwise. + */ + public abstract boolean getMute(); + + /** + * Step up or down by one channel spacing. + * The operation is asynchronous and {@link Callback} + * onProgramInfoChanged() will be called when step completes or + * onError() when cancelled or timeout. + * @param direction {@link #DIRECTION_UP} or {@link #DIRECTION_DOWN}. + * @param skipSubChannel indicates to skip sub channels when the configuration currently + * selected supports sub channel (e.g HD Radio). N/A otherwise. + * @return + * <ul> + * <li>{@link RadioManager#STATUS_OK} in case of success, </li> + * <li>{@link RadioManager#STATUS_ERROR} in case of unspecified error, </li> + * <li>{@link RadioManager#STATUS_NO_INIT} if the native service cannot be reached, </li> + * <li>{@link RadioManager#STATUS_BAD_VALUE} if parameters are invalid, </li> + * <li>{@link RadioManager#STATUS_INVALID_OPERATION} if the call is out of sequence, </li> + * <li>{@link RadioManager#STATUS_DEAD_OBJECT} if the binder transaction to the native + * service fails, </li> + * </ul> + */ + public abstract int step(int direction, boolean skipSubChannel); + + /** + * Scan up or down to next valid station. + * The operation is asynchronous and {@link Callback} + * onProgramInfoChanged() will be called when scan completes or + * onError() when cancelled or timeout. + * @param direction {@link #DIRECTION_UP} or {@link #DIRECTION_DOWN}. + * @param skipSubChannel indicates to skip sub channels when the configuration currently + * selected supports sub channel (e.g HD Radio). N/A otherwise. + * @return + * <ul> + * <li>{@link RadioManager#STATUS_OK} in case of success, </li> + * <li>{@link RadioManager#STATUS_ERROR} in case of unspecified error, </li> + * <li>{@link RadioManager#STATUS_NO_INIT} if the native service cannot be reached, </li> + * <li>{@link RadioManager#STATUS_BAD_VALUE} if parameters are invalid, </li> + * <li>{@link RadioManager#STATUS_INVALID_OPERATION} if the call is out of sequence, </li> + * <li>{@link RadioManager#STATUS_DEAD_OBJECT} if the binder transaction to the native + * service fails, </li> + * </ul> + */ + public abstract int scan(int direction, boolean skipSubChannel); + + /** + * Tune to a specific frequency. + * The operation is asynchronous and {@link Callback} + * onProgramInfoChanged() will be called when tune completes or + * onError() when cancelled or timeout. + * @param channel the specific channel or frequency to tune to. + * @param subChannel the specific sub-channel to tune to. N/A if the selected configuration + * does not support cub channels. + * @return + * <ul> + * <li>{@link RadioManager#STATUS_OK} in case of success, </li> + * <li>{@link RadioManager#STATUS_ERROR} in case of unspecified error, </li> + * <li>{@link RadioManager#STATUS_NO_INIT} if the native service cannot be reached, </li> + * <li>{@link RadioManager#STATUS_BAD_VALUE} if parameters are invalid, </li> + * <li>{@link RadioManager#STATUS_INVALID_OPERATION} if the call is out of sequence, </li> + * <li>{@link RadioManager#STATUS_DEAD_OBJECT} if the binder transaction to the native + * service fails, </li> + * </ul> + */ + public abstract int tune(int channel, int subChannel); + + /** + * Cancel a pending scan or tune operation. + * If an operation is pending, {@link Callback} onError() will be called with + * {@link #ERROR_CANCELLED}. + * @return + * <ul> + * <li>{@link RadioManager#STATUS_OK} in case of success, </li> + * <li>{@link RadioManager#STATUS_ERROR} in case of unspecified error, </li> + * <li>{@link RadioManager#STATUS_NO_INIT} if the native service cannot be reached, </li> + * <li>{@link RadioManager#STATUS_BAD_VALUE} if parameters are invalid, </li> + * <li>{@link RadioManager#STATUS_INVALID_OPERATION} if the call is out of sequence, </li> + * <li>{@link RadioManager#STATUS_DEAD_OBJECT} if the binder transaction to the native + * service fails, </li> + * </ul> + */ + public abstract int cancel(); + + /** + * Get current station information. + * @param info a ProgramInfo array of lengh 1 where the information is returned. + * @return + * <ul> + * <li>{@link RadioManager#STATUS_OK} in case of success, </li> + * <li>{@link RadioManager#STATUS_ERROR} in case of unspecified error, </li> + * <li>{@link RadioManager#STATUS_NO_INIT} if the native service cannot be reached, </li> + * <li>{@link RadioManager#STATUS_BAD_VALUE} if parameters are invalid, </li> + * <li>{@link RadioManager#STATUS_INVALID_OPERATION} if the call is out of sequence, </li> + * <li>{@link RadioManager#STATUS_DEAD_OBJECT} if the binder transaction to the native + * service fails, </li> + * </ul> + */ + public abstract int getProgramInformation(RadioManager.ProgramInfo[] info); + + /** + * Get current antenna connection state for current configuration. + * Only valid if a configuration has been applied. + * @return {@code true} if the antenna is connected, {@code false} otherwise. + */ + public abstract boolean isAntennaConnected(); + + /** + * Indicates if this client actually controls the tuner. + * Control is always granted after + * {@link RadioManager#openTuner(int, + * RadioManager.BandConfig, boolean, Callback, Handler)} + * returns a non null tuner interface. + * Control is lost when another client opens an interface on the same tuner. + * When this happens, {@link Callback#onControlChanged(boolean)} is received. + * The client can either wait for control to be returned (which is indicated by the same + * callback) or close and reopen the tuner interface. + * @return {@code true} if this interface controls the tuner, + * {@code false} otherwise or if a problem occured retrieving the state. + */ + public abstract boolean hasControl(); + + /** Indicates a failure of radio IC or driver. + * The application must close and re open the tuner */ + public static final int ERROR_HARDWARE_FAILURE = 0; + /** Indicates a failure of the radio service. + * The application must close and re open the tuner */ + public static final int ERROR_SERVER_DIED = 1; + /** A pending seek or tune operation was cancelled */ + public static final int ERROR_CANCELLED = 2; + /** A pending seek or tune operation timed out */ + public static final int ERROR_SCAN_TIMEOUT = 3; + /** The requested configuration could not be applied */ + public static final int ERROR_CONFIG = 4; + + /** + * Callback provided by the client application when opening a {@link RadioTuner} + * to receive asynchronous operation results, updates and error notifications. + */ + public static abstract class Callback { + /** + * onError() is called when an error occured while performing an asynchronous + * operation of when the hardware or system service experiences a problem. + * status is one of {@link #ERROR_HARDWARE_FAILURE}, {@link #ERROR_SERVER_DIED}, + * {@link #ERROR_CANCELLED}, {@link #ERROR_SCAN_TIMEOUT}, + * {@link #ERROR_CONFIG} + */ + public void onError(int status) {} + /** + * onConfigurationChanged() is called upon successful completion of + * {@link RadioManager#openTuner(int, RadioManager.BandConfig, boolean, Callback, Handler)} + * or {@link RadioTuner#setConfiguration(RadioManager.BandConfig)} + */ + public void onConfigurationChanged(RadioManager.BandConfig config) {} + /** + * onProgramInfoChanged() is called upon successful completion of + * {@link RadioTuner#step(int, boolean)}, {@link RadioTuner#scan(int, boolean)}, + * {@link RadioTuner#tune(int, int)} or when a switching to alternate frequency occurs. + * Note that if metadata only are updated, {@link #onMetadataChanged(RadioMetadata)} will + * be called. + */ + public void onProgramInfoChanged(RadioManager.ProgramInfo info) {} + /** + * onMetadataChanged() is called when new meta data are received on current program. + * Meta data are also received in {@link RadioManager.ProgramInfo} when + * {@link #onProgramInfoChanged(RadioManager.ProgramInfo)} is called. + */ + public void onMetadataChanged(RadioMetadata metadata) {} + /** + * onTrafficAnnouncement() is called when a traffic announcement starts and stops. + */ + public void onTrafficAnnouncement(boolean active) {} + /** + * onAntennaState() is called when the antenna is connected or disconnected. + */ + public void onAntennaState(boolean connected) {} + /** + * onControlChanged() is called when the client loses or gains control of the radio tuner. + * The control is always granted after a successful call to + * {@link RadioManager#openTuner(int, RadioManager.BandConfig, boolean, Callback, Handler)}. + * If another client opens the same tuner, onControlChanged() will be called with + * control set to {@code false} to indicate loss of control. + * At this point, RadioTuner APIs other than getters will return + * {@link RadioManager#STATUS_INVALID_OPERATION}. + * When the other client releases the tuner, onControlChanged() will be called + * with control set to {@code true}. + */ + public void onControlChanged(boolean control) {} + } + +} + diff --git a/core/java/android/hardware/soundtrigger/SoundTriggerModule.java b/core/java/android/hardware/soundtrigger/SoundTriggerModule.java index 1a8723d..e23a2bb 100644 --- a/core/java/android/hardware/soundtrigger/SoundTriggerModule.java +++ b/core/java/android/hardware/soundtrigger/SoundTriggerModule.java @@ -16,13 +16,10 @@ package android.hardware.soundtrigger; -import android.content.Context; -import android.content.Intent; import android.os.Handler; import android.os.Looper; import android.os.Message; import java.lang.ref.WeakReference; -import java.util.UUID; /** * The SoundTriggerModule provides APIs to control sound models and sound detection diff --git a/core/java/android/hardware/usb/UsbDevice.java b/core/java/android/hardware/usb/UsbDevice.java index d90e06e..1a42319 100644 --- a/core/java/android/hardware/usb/UsbDevice.java +++ b/core/java/android/hardware/usb/UsbDevice.java @@ -40,6 +40,7 @@ import android.os.Parcelable; public class UsbDevice implements Parcelable { private static final String TAG = "UsbDevice"; + private static final boolean DEBUG = false; private final String mName; private final String mManufacturerName; diff --git a/core/java/android/hardware/usb/UsbManager.java b/core/java/android/hardware/usb/UsbManager.java index f64ef87..f283051 100644 --- a/core/java/android/hardware/usb/UsbManager.java +++ b/core/java/android/hardware/usb/UsbManager.java @@ -68,6 +68,8 @@ public class UsbManager { * accessory function is enabled * <li> {@link #USB_FUNCTION_AUDIO_SOURCE} boolean extra indicating whether the * audio source function is enabled + * <li> {@link #USB_FUNCTION_MIDI} boolean extra indicating whether the + * MIDI function is enabled * </ul> * * {@hide} @@ -188,6 +190,14 @@ public class UsbManager { public static final String USB_FUNCTION_AUDIO_SOURCE = "audio_source"; /** + * Name of the MIDI USB function. + * Used in extras for the {@link #ACTION_USB_STATE} broadcast + * + * {@hide} + */ + public static final String USB_FUNCTION_MIDI = "midi"; + + /** * Name of the Accessory USB function. * Used in extras for the {@link #ACTION_USB_STATE} broadcast * diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index f218b65..481fc2f 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -19,6 +19,7 @@ package android.inputmethodservice; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; +import android.annotation.DrawableRes; import android.app.ActivityManager; import android.app.Dialog; import android.content.Context; @@ -1178,7 +1179,7 @@ public class InputMethodService extends AbstractInputMethodService { return isExtractViewShown() ? View.GONE : View.INVISIBLE; } - public void showStatusIcon(int iconResId) { + public void showStatusIcon(@DrawableRes int iconResId) { mStatusIcon = iconResId; mImm.showStatusIcon(mToken, getPackageName(), iconResId); } diff --git a/core/java/android/inputmethodservice/Keyboard.java b/core/java/android/inputmethodservice/Keyboard.java index 4fe54c0..45f1889 100644 --- a/core/java/android/inputmethodservice/Keyboard.java +++ b/core/java/android/inputmethodservice/Keyboard.java @@ -18,6 +18,7 @@ package android.inputmethodservice; import org.xmlpull.v1.XmlPullParserException; +import android.annotation.XmlRes; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; @@ -519,7 +520,8 @@ public class Keyboard { * @param width sets width of keyboard * @param height sets height of keyboard */ - public Keyboard(Context context, int xmlLayoutResId, int modeId, int width, int height) { + public Keyboard(Context context, @XmlRes int xmlLayoutResId, int modeId, int width, + int height) { mDisplayWidth = width; mDisplayHeight = height; @@ -540,7 +542,7 @@ public class Keyboard { * @param xmlLayoutResId the resource file that contains the keyboard layout and keys. * @param modeId keyboard mode identifier */ - public Keyboard(Context context, int xmlLayoutResId, int modeId) { + public Keyboard(Context context, @XmlRes int xmlLayoutResId, int modeId) { DisplayMetrics dm = context.getResources().getDisplayMetrics(); mDisplayWidth = dm.widthPixels; mDisplayHeight = dm.heightPixels; diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index 0832d81..34a0727 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -25,7 +25,6 @@ import android.content.Intent; import android.net.NetworkUtils; import android.os.Binder; import android.os.Build.VERSION_CODES; -import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; @@ -38,7 +37,6 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.provider.Settings; import android.telephony.SubscriptionManager; -import android.telephony.TelephonyManager; import android.util.ArrayMap; import android.util.Log; @@ -504,6 +502,8 @@ public class ConnectivityManager { return "MOBILE_EMERGENCY"; case TYPE_PROXY: return "PROXY"; + case TYPE_VPN: + return "VPN"; default: return Integer.toString(type); } diff --git a/core/java/android/net/DhcpResults.java b/core/java/android/net/DhcpResults.java index 6159e1e..87c063f 100644 --- a/core/java/android/net/DhcpResults.java +++ b/core/java/android/net/DhcpResults.java @@ -17,7 +17,6 @@ package android.net; import android.net.NetworkUtils; -import android.os.Parcelable; import android.os.Parcel; import android.text.TextUtils; import android.util.Log; diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl index d8852f8..3c09978 100644 --- a/core/java/android/net/IConnectivityManager.aidl +++ b/core/java/android/net/IConnectivityManager.aidl @@ -33,6 +33,7 @@ import android.os.ResultReceiver; import com.android.internal.net.LegacyVpnInfo; import com.android.internal.net.VpnConfig; +import com.android.internal.net.VpnInfo; import com.android.internal.net.VpnProfile; /** @@ -114,6 +115,8 @@ interface IConnectivityManager LegacyVpnInfo getLegacyVpnInfo(); + VpnInfo[] getAllVpnInfo(); + boolean updateLockdownVpn(); void captivePortalCheckCompleted(in NetworkInfo info, boolean isCaptivePortal); diff --git a/core/java/android/net/Network.java b/core/java/android/net/Network.java index dfe2413..ab57c9b 100644 --- a/core/java/android/net/Network.java +++ b/core/java/android/net/Network.java @@ -26,7 +26,6 @@ import java.net.DatagramSocket; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.MalformedURLException; -import java.net.ProxySelector; import java.net.Socket; import java.net.SocketAddress; import java.net.SocketException; diff --git a/core/java/android/net/NetworkAgent.java b/core/java/android/net/NetworkAgent.java index 74d4ac2..24aaf0d 100644 --- a/core/java/android/net/NetworkAgent.java +++ b/core/java/android/net/NetworkAgent.java @@ -21,15 +21,12 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.Messenger; -import android.os.Parcel; -import android.os.Parcelable; import android.util.Log; import com.android.internal.util.AsyncChannel; import com.android.internal.util.Protocol; import java.util.ArrayList; -import java.util.concurrent.atomic.AtomicBoolean; /** * A Utility class for handling for communicating between bearer-specific diff --git a/core/java/android/net/NetworkFactory.java b/core/java/android/net/NetworkFactory.java index 64d0fcf..e47220b 100644 --- a/core/java/android/net/NetworkFactory.java +++ b/core/java/android/net/NetworkFactory.java @@ -21,12 +21,9 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.Messenger; -import android.os.Parcel; -import android.os.Parcelable; import android.util.Log; import android.util.SparseArray; -import com.android.internal.util.AsyncChannel; import com.android.internal.util.Protocol; /** diff --git a/core/java/android/net/NetworkRequest.java b/core/java/android/net/NetworkRequest.java index 5a09b46..7838b47 100644 --- a/core/java/android/net/NetworkRequest.java +++ b/core/java/android/net/NetworkRequest.java @@ -19,8 +19,6 @@ package android.net; import android.os.Parcel; import android.os.Parcelable; -import java.util.concurrent.atomic.AtomicInteger; - /** * Defines a request for a network, made through {@link NetworkRequest.Builder} and used * to request a network via {@link ConnectivityManager#requestNetwork} or listen for changes diff --git a/core/java/android/net/NetworkStats.java b/core/java/android/net/NetworkStats.java index 2afe578..0766253 100644 --- a/core/java/android/net/NetworkStats.java +++ b/core/java/android/net/NetworkStats.java @@ -19,6 +19,7 @@ package android.net; import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; +import android.util.Slog; import android.util.SparseBooleanArray; import com.android.internal.annotations.VisibleForTesting; @@ -42,6 +43,7 @@ import java.util.Objects; * @hide */ public class NetworkStats implements Parcelable { + private static final String TAG = "NetworkStats"; /** {@link #iface} value when interface details unavailable. */ public static final String IFACE_ALL = null; /** {@link #uid} value when UID details unavailable. */ @@ -783,4 +785,162 @@ public class NetworkStats implements Parcelable { public void foundNonMonotonic( NetworkStats left, int leftIndex, NetworkStats right, int rightIndex, C cookie); } + + /** + * VPN accounting. Move some VPN's underlying traffic to other UIDs that use tun0 iface. + * + * This method should only be called on delta NetworkStats. Do not call this method on a + * snapshot {@link NetworkStats} object because the tunUid and/or the underlyingIface may + * change over time. + * + * This method performs adjustments for one active VPN package and one VPN iface at a time. + * + * It is possible for the VPN software to use multiple underlying networks. This method + * only migrates traffic for the primary underlying network. + * + * @param tunUid uid of the VPN application + * @param tunIface iface of the vpn tunnel + * @param underlyingIface the primary underlying network iface used by the VPN application + * @return true if it successfully adjusts the accounting for VPN, false otherwise + */ + public boolean migrateTun(int tunUid, String tunIface, String underlyingIface) { + Entry tunIfaceTotal = new Entry(); + Entry underlyingIfaceTotal = new Entry(); + + tunAdjustmentInit(tunUid, tunIface, underlyingIface, tunIfaceTotal, underlyingIfaceTotal); + + // If tunIface < underlyingIface, it leaves the overhead traffic in the VPN app. + // If tunIface > underlyingIface, the VPN app doesn't get credit for data compression. + // Negative stats should be avoided. + Entry pool = tunGetPool(tunIfaceTotal, underlyingIfaceTotal); + if (pool.isEmpty()) { + return true; + } + Entry moved = addTrafficToApplications(tunIface, underlyingIface, tunIfaceTotal, pool); + deductTrafficFromVpnApp(tunUid, underlyingIface, moved); + + if (!moved.isEmpty()) { + Slog.wtf(TAG, "Failed to deduct underlying network traffic from VPN package. Moved=" + + moved); + return false; + } + return true; + } + + /** + * Initializes the data used by the migrateTun() method. + * + * This is the first pass iteration which does the following work: + * (1) Adds up all the traffic through tun0. + * (2) Adds up all the traffic through the tunUid's underlyingIface + * (both foreground and background). + */ + private void tunAdjustmentInit(int tunUid, String tunIface, String underlyingIface, + Entry tunIfaceTotal, Entry underlyingIfaceTotal) { + Entry recycle = new Entry(); + for (int i = 0; i < size; i++) { + getValues(i, recycle); + if (recycle.uid == UID_ALL) { + throw new IllegalStateException( + "Cannot adjust VPN accounting on an iface aggregated NetworkStats."); + } + + if (recycle.uid == tunUid && recycle.tag == TAG_NONE + && Objects.equals(underlyingIface, recycle.iface)) { + underlyingIfaceTotal.add(recycle); + } + + if (recycle.tag == TAG_NONE && Objects.equals(tunIface, recycle.iface)) { + // Add up all tunIface traffic. + tunIfaceTotal.add(recycle); + } + } + } + + private static Entry tunGetPool(Entry tunIfaceTotal, Entry underlyingIfaceTotal) { + Entry pool = new Entry(); + pool.rxBytes = Math.min(tunIfaceTotal.rxBytes, underlyingIfaceTotal.rxBytes); + pool.rxPackets = Math.min(tunIfaceTotal.rxPackets, underlyingIfaceTotal.rxPackets); + pool.txBytes = Math.min(tunIfaceTotal.txBytes, underlyingIfaceTotal.txBytes); + pool.txPackets = Math.min(tunIfaceTotal.txPackets, underlyingIfaceTotal.txPackets); + pool.operations = Math.min(tunIfaceTotal.operations, underlyingIfaceTotal.operations); + return pool; + } + + private Entry addTrafficToApplications(String tunIface, String underlyingIface, + Entry tunIfaceTotal, Entry pool) { + Entry moved = new Entry(); + Entry tmpEntry = new Entry(); + tmpEntry.iface = underlyingIface; + for (int i = 0; i < size; i++) { + if (Objects.equals(iface[i], tunIface)) { + if (tunIfaceTotal.rxBytes > 0) { + tmpEntry.rxBytes = pool.rxBytes * rxBytes[i] / tunIfaceTotal.rxBytes; + } else { + tmpEntry.rxBytes = 0; + } + if (tunIfaceTotal.rxPackets > 0) { + tmpEntry.rxPackets = pool.rxPackets * rxPackets[i] / tunIfaceTotal.rxPackets; + } else { + tmpEntry.rxPackets = 0; + } + if (tunIfaceTotal.txBytes > 0) { + tmpEntry.txBytes = pool.txBytes * txBytes[i] / tunIfaceTotal.txBytes; + } else { + tmpEntry.txBytes = 0; + } + if (tunIfaceTotal.txPackets > 0) { + tmpEntry.txPackets = pool.txPackets * txPackets[i] / tunIfaceTotal.txPackets; + } else { + tmpEntry.txPackets = 0; + } + if (tunIfaceTotal.operations > 0) { + tmpEntry.operations = + pool.operations * operations[i] / tunIfaceTotal.operations; + } else { + tmpEntry.operations = 0; + } + tmpEntry.uid = uid[i]; + tmpEntry.tag = tag[i]; + tmpEntry.set = set[i]; + combineValues(tmpEntry); + if (tag[i] == TAG_NONE) { + moved.add(tmpEntry); + } + } + } + return moved; + } + + private void deductTrafficFromVpnApp(int tunUid, String underlyingIface, Entry moved) { + // Caveat: if the vpn software uses tag, the total tagged traffic may be greater than + // the TAG_NONE traffic. + int idxVpnBackground = findIndex(underlyingIface, tunUid, SET_DEFAULT, TAG_NONE); + if (idxVpnBackground != -1) { + tunSubtract(idxVpnBackground, this, moved); + } + + int idxVpnForeground = findIndex(underlyingIface, tunUid, SET_FOREGROUND, TAG_NONE); + if (idxVpnForeground != -1) { + tunSubtract(idxVpnForeground, this, moved); + } + } + + private static void tunSubtract(int i, NetworkStats left, Entry right) { + long rxBytes = Math.min(left.rxBytes[i], right.rxBytes); + left.rxBytes[i] -= rxBytes; + right.rxBytes -= rxBytes; + + long rxPackets = Math.min(left.rxPackets[i], right.rxPackets); + left.rxPackets[i] -= rxPackets; + right.rxPackets -= rxPackets; + + long txBytes = Math.min(left.txBytes[i], right.txBytes); + left.txBytes[i] -= txBytes; + right.txBytes -= txBytes; + + long txPackets = Math.min(left.txPackets[i], right.txPackets); + left.txPackets[i] -= txPackets; + right.txPackets -= txPackets; + } } diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java index d2a2997..8003afb 100644 --- a/core/java/android/net/NetworkUtils.java +++ b/core/java/android/net/NetworkUtils.java @@ -56,6 +56,30 @@ public class NetworkUtils { /** * Start the DHCP client daemon, in order to have it request addresses + * for the named interface. This returns {@code true} if the DHCPv4 daemon + * starts, {@code false} otherwise. This call blocks until such time as a + * result is available or the default discovery timeout has been reached. + * Callers should check {@link #getDhcpResults} to determine whether DHCP + * succeeded or failed, and if it succeeded, to fetch the {@link DhcpResults}. + * @param interfaceName the name of the interface to configure + * @return {@code true} for success, {@code false} for failure + */ + public native static boolean startDhcp(String interfaceName); + + /** + * Initiate renewal on the DHCP client daemon for the named interface. This + * returns {@code true} if the DHCPv4 daemon has been notified, {@code false} + * otherwise. This call blocks until such time as a result is available or + * the default renew timeout has been reached. Callers should check + * {@link #getDhcpResults} to determine whether DHCP succeeded or failed, + * and if it succeeded, to fetch the {@link DhcpResults}. + * @param interfaceName the name of the interface to configure + * @return {@code true} for success, {@code false} for failure + */ + public native static boolean startDhcpRenew(String interfaceName); + + /** + * Start the DHCP client daemon, in order to have it request addresses * for the named interface, and then configure the interface with those * addresses. This call blocks until it obtains a result (either success * or failure) from the daemon. @@ -64,17 +88,31 @@ public class NetworkUtils { * the IP address information. * @return {@code true} for success, {@code false} for failure */ - public native static boolean runDhcp(String interfaceName, DhcpResults dhcpResults); + public static boolean runDhcp(String interfaceName, DhcpResults dhcpResults) { + return startDhcp(interfaceName) && getDhcpResults(interfaceName, dhcpResults); + } /** - * Initiate renewal on the Dhcp client daemon. This call blocks until it obtains + * Initiate renewal on the DHCP client daemon. This call blocks until it obtains * a result (either success or failure) from the daemon. * @param interfaceName the name of the interface to configure * @param dhcpResults if the request succeeds, this object is filled in with * the IP address information. * @return {@code true} for success, {@code false} for failure */ - public native static boolean runDhcpRenew(String interfaceName, DhcpResults dhcpResults); + public static boolean runDhcpRenew(String interfaceName, DhcpResults dhcpResults) { + return startDhcpRenew(interfaceName) && getDhcpResults(interfaceName, dhcpResults); + } + + /** + * Fetch results from the DHCP client daemon. This call returns {@code true} if + * if there are results available to be read, {@code false} otherwise. + * @param interfaceName the name of the interface to configure + * @param dhcpResults if the request succeeds, this object is filled in with + * the IP address information. + * @return {@code true} for success, {@code false} for failure + */ + public native static boolean getDhcpResults(String interfaceName, DhcpResults dhcpResults); /** * Shut down the DHCP client daemon. diff --git a/core/java/android/net/PacProxySelector.java b/core/java/android/net/PacProxySelector.java index 8626d08..9bdf4f6 100644 --- a/core/java/android/net/PacProxySelector.java +++ b/core/java/android/net/PacProxySelector.java @@ -16,7 +16,6 @@ package android.net; -import android.os.RemoteException; import android.os.ServiceManager; import android.util.Log; @@ -74,8 +73,8 @@ public class PacProxySelector extends ProxySelector { } try { response = mProxyService.resolvePacFile(uri.getHost(), urlString); - } catch (RemoteException e) { - e.printStackTrace(); + } catch (Exception e) { + Log.e(TAG, "Error resolving PAC File", e); } if (response == null) { return mDefaultList; diff --git a/core/java/android/net/ProxyInfo.java b/core/java/android/net/ProxyInfo.java index a3cad77..2c90909 100644 --- a/core/java/android/net/ProxyInfo.java +++ b/core/java/android/net/ProxyInfo.java @@ -21,8 +21,6 @@ import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; -import org.apache.http.client.HttpClient; - import java.net.InetSocketAddress; import java.net.URLConnection; import java.util.List; @@ -31,8 +29,9 @@ import java.util.Locale; /** * Describes a proxy configuration. * - * Proxy configurations are already integrated within the Apache HTTP stack. - * So {@link URLConnection} and {@link HttpClient} will use them automatically. + * Proxy configurations are already integrated within the {@code java.net} and + * Apache HTTP stack. So {@link URLConnection} and Apache's {@code HttpClient} will use + * them automatically. * * 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}. diff --git a/core/java/android/net/SSLCertificateSocketFactory.java b/core/java/android/net/SSLCertificateSocketFactory.java index 6654577..27096b1 100644 --- a/core/java/android/net/SSLCertificateSocketFactory.java +++ b/core/java/android/net/SSLCertificateSocketFactory.java @@ -159,6 +159,8 @@ public class SSLCertificateSocketFactory extends SSLSocketFactory { * 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. + * + * @removed */ @Deprecated public static org.apache.http.conn.ssl.SSLSocketFactory getHttpSocketFactory( diff --git a/core/java/android/net/StaticIpConfiguration.java b/core/java/android/net/StaticIpConfiguration.java index 37ee961..7f1b179 100644 --- a/core/java/android/net/StaticIpConfiguration.java +++ b/core/java/android/net/StaticIpConfiguration.java @@ -21,7 +21,6 @@ import android.os.Parcelable; import android.os.Parcel; import java.net.InetAddress; -import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import java.util.Objects; diff --git a/core/java/android/net/Uri.java b/core/java/android/net/Uri.java index 2099c3f..bf3d9aa 100644 --- a/core/java/android/net/Uri.java +++ b/core/java/android/net/Uri.java @@ -366,6 +366,7 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { public String toSafeString() { String scheme = getScheme(); String ssp = getSchemeSpecificPart(); + String authority = null; if (scheme != null) { if (scheme.equalsIgnoreCase("tel") || scheme.equalsIgnoreCase("sip") || scheme.equalsIgnoreCase("sms") || scheme.equalsIgnoreCase("smsto") @@ -384,6 +385,9 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { } } return builder.toString(); + } else if (scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https")) { + ssp = null; + authority = "//" + getAuthority() + "/..."; } } // Not a sensitive scheme, but let's still be conservative about @@ -397,6 +401,9 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { if (ssp != null) { builder.append(ssp); } + if (authority != null) { + builder.append(authority); + } return builder.toString(); } @@ -1742,7 +1749,7 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { * * @return normalized Uri (never null) * @see {@link android.content.Intent#setData} - * @see {@link #setNormalizedData} + * @see {@link android.content.Intent#setDataAndNormalize} */ public Uri normalizeScheme() { String scheme = getScheme(); diff --git a/core/java/android/net/http/AndroidHttpClient.java b/core/java/android/net/http/AndroidHttpClient.java deleted file mode 100644 index a262076..0000000 --- a/core/java/android/net/http/AndroidHttpClient.java +++ /dev/null @@ -1,527 +0,0 @@ -/* - * Copyright (C) 2007 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.net.http; - -import com.android.internal.http.HttpDateTime; - -import org.apache.http.Header; -import org.apache.http.HttpEntity; -import org.apache.http.HttpEntityEnclosingRequest; -import org.apache.http.HttpException; -import org.apache.http.HttpHost; -import org.apache.http.HttpRequest; -import org.apache.http.HttpRequestInterceptor; -import org.apache.http.HttpResponse; -import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.HttpClient; -import org.apache.http.client.ResponseHandler; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.client.params.HttpClientParams; -import org.apache.http.client.protocol.ClientContext; -import org.apache.http.conn.ClientConnectionManager; -import org.apache.http.conn.scheme.PlainSocketFactory; -import org.apache.http.conn.scheme.Scheme; -import org.apache.http.conn.scheme.SchemeRegistry; -import org.apache.http.entity.AbstractHttpEntity; -import org.apache.http.entity.ByteArrayEntity; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.impl.client.RequestWrapper; -import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; -import org.apache.http.params.BasicHttpParams; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; -import org.apache.http.params.HttpProtocolParams; -import org.apache.http.protocol.BasicHttpContext; -import org.apache.http.protocol.BasicHttpProcessor; -import org.apache.http.protocol.HttpContext; - -import android.content.ContentResolver; -import android.content.Context; -import android.net.SSLCertificateSocketFactory; -import android.net.SSLSessionCache; -import android.os.Looper; -import android.util.Base64; -import android.util.Log; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URI; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; - -/** - * Implementation of the Apache {@link DefaultHttpClient} that is configured with - * reasonable default settings and registered schemes for Android. - * Don't create this directly, use the {@link #newInstance} factory method. - * - * <p>This client processes cookies but does not retain them by default. - * 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 - public static long DEFAULT_SYNC_MIN_GZIP_BYTES = 256; - - // Default connection and socket timeout of 60 seconds. Tweak to taste. - private static final int SOCKET_OPERATION_TIMEOUT = 60 * 1000; - - private static final String TAG = "AndroidHttpClient"; - - private static String[] textContentTypes = new String[] { - "text/", - "application/xml", - "application/json" - }; - - /** Interceptor throws an exception if the executing thread is blocked */ - private static final HttpRequestInterceptor sThreadCheckInterceptor = - new HttpRequestInterceptor() { - public void process(HttpRequest request, HttpContext context) { - // Prevent the HttpRequest from being sent on the main thread - if (Looper.myLooper() != null && Looper.myLooper() == Looper.getMainLooper() ) { - throw new RuntimeException("This thread forbids HTTP requests"); - } - } - }; - - /** - * Create a new HttpClient with reasonable defaults (which you can update). - * - * @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(); - - // Turn off stale checking. Our connections break all the time anyway, - // and it's not worth it to pay the penalty of checking every time. - HttpConnectionParams.setStaleCheckingEnabled(params, false); - - HttpConnectionParams.setConnectionTimeout(params, SOCKET_OPERATION_TIMEOUT); - HttpConnectionParams.setSoTimeout(params, SOCKET_OPERATION_TIMEOUT); - HttpConnectionParams.setSocketBufferSize(params, 8192); - - // Don't handle redirects -- return them to the caller. Our code - // often wants to re-POST after a redirect, which we must do ourselves. - HttpClientParams.setRedirecting(params, false); - - // Use a session cache for SSL sockets - SSLSessionCache sessionCache = context == null ? null : new SSLSessionCache(context); - - // Set the specified user agent and register standard protocols. - HttpProtocolParams.setUserAgent(params, userAgent); - SchemeRegistry schemeRegistry = new SchemeRegistry(); - schemeRegistry.register(new Scheme("http", - PlainSocketFactory.getSocketFactory(), 80)); - schemeRegistry.register(new Scheme("https", - SSLCertificateSocketFactory.getHttpSocketFactory( - SOCKET_OPERATION_TIMEOUT, sessionCache), 443)); - - ClientConnectionManager manager = - new ThreadSafeClientConnManager(params, schemeRegistry); - - // We use a factory method to modify superclass initialization - // parameters without the funny call-a-static-method dance. - return new AndroidHttpClient(manager, params); - } - - /** - * 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 */); - } - - private final HttpClient delegate; - - private RuntimeException mLeakedException = new IllegalStateException( - "AndroidHttpClient created and never closed"); - - private AndroidHttpClient(ClientConnectionManager ccm, HttpParams params) { - this.delegate = new DefaultHttpClient(ccm, params) { - @Override - protected BasicHttpProcessor createHttpProcessor() { - // Add interceptor to prevent making requests from main thread. - BasicHttpProcessor processor = super.createHttpProcessor(); - processor.addRequestInterceptor(sThreadCheckInterceptor); - processor.addRequestInterceptor(new CurlLogger()); - - return processor; - } - - @Override - protected HttpContext createHttpContext() { - // Same as DefaultHttpClient.createHttpContext() minus the - // cookie store. - HttpContext context = new BasicHttpContext(); - context.setAttribute( - ClientContext.AUTHSCHEME_REGISTRY, - getAuthSchemes()); - context.setAttribute( - ClientContext.COOKIESPEC_REGISTRY, - getCookieSpecs()); - context.setAttribute( - ClientContext.CREDS_PROVIDER, - getCredentialsProvider()); - return context; - } - }; - } - - @Override - protected void finalize() throws Throwable { - super.finalize(); - if (mLeakedException != null) { - Log.e(TAG, "Leak found", mLeakedException); - mLeakedException = null; - } - } - - /** - * Modifies a request to indicate to the server that we would like a - * gzipped response. (Uses the "Accept-Encoding" HTTP header.) - * @param request the request to modify - * @see #getUngzippedContent - */ - public static void modifyRequestToAcceptGzipResponse(HttpRequest request) { - request.addHeader("Accept-Encoding", "gzip"); - } - - /** - * Gets the input stream from a response entity. If the entity is gzipped - * then this will get a stream over the uncompressed data. - * - * @param entity the entity whose content should be read - * @return the input stream to read from - * @throws IOException - */ - public static InputStream getUngzippedContent(HttpEntity entity) - throws IOException { - InputStream responseStream = entity.getContent(); - if (responseStream == null) return responseStream; - Header header = entity.getContentEncoding(); - if (header == null) return responseStream; - String contentEncoding = header.getValue(); - if (contentEncoding == null) return responseStream; - if (contentEncoding.contains("gzip")) responseStream - = new GZIPInputStream(responseStream); - return responseStream; - } - - /** - * Release resources associated with this client. You must call this, - * or significant resources (sockets and memory) may be leaked. - */ - public void close() { - if (mLeakedException != null) { - getConnectionManager().shutdown(); - mLeakedException = null; - } - } - - public HttpParams getParams() { - return delegate.getParams(); - } - - public ClientConnectionManager getConnectionManager() { - return delegate.getConnectionManager(); - } - - public HttpResponse execute(HttpUriRequest request) throws IOException { - return delegate.execute(request); - } - - public HttpResponse execute(HttpUriRequest request, HttpContext context) - throws IOException { - return delegate.execute(request, context); - } - - public HttpResponse execute(HttpHost target, HttpRequest request) - throws IOException { - return delegate.execute(target, request); - } - - public HttpResponse execute(HttpHost target, HttpRequest request, - HttpContext context) throws IOException { - return delegate.execute(target, request, context); - } - - public <T> T execute(HttpUriRequest request, - ResponseHandler<? extends T> responseHandler) - throws IOException, ClientProtocolException { - return delegate.execute(request, responseHandler); - } - - public <T> T execute(HttpUriRequest request, - ResponseHandler<? extends T> responseHandler, HttpContext context) - throws IOException, ClientProtocolException { - return delegate.execute(request, responseHandler, context); - } - - public <T> T execute(HttpHost target, HttpRequest request, - ResponseHandler<? extends T> responseHandler) throws IOException, - ClientProtocolException { - return delegate.execute(target, request, responseHandler); - } - - public <T> T execute(HttpHost target, HttpRequest request, - ResponseHandler<? extends T> responseHandler, HttpContext context) - throws IOException, ClientProtocolException { - return delegate.execute(target, request, responseHandler, context); - } - - /** - * Compress data to send to server. - * Creates a Http Entity holding the gzipped data. - * The data will not be compressed if it is too short. - * @param data The bytes to compress - * @return Entity holding the data - */ - public static AbstractHttpEntity getCompressedEntity(byte data[], ContentResolver resolver) - throws IOException { - AbstractHttpEntity entity; - if (data.length < getMinGzipSize(resolver)) { - entity = new ByteArrayEntity(data); - } else { - ByteArrayOutputStream arr = new ByteArrayOutputStream(); - OutputStream zipper = new GZIPOutputStream(arr); - zipper.write(data); - zipper.close(); - entity = new ByteArrayEntity(arr.toByteArray()); - entity.setContentEncoding("gzip"); - } - return entity; - } - - /** - * Retrieves the minimum size for compressing data. - * Shorter data will not be compressed. - */ - public static long getMinGzipSize(ContentResolver resolver) { - return DEFAULT_SYNC_MIN_GZIP_BYTES; // For now, this is just a constant. - } - - /* cURL logging support. */ - - /** - * Logging tag and level. - */ - private static class LoggingConfiguration { - - private final String tag; - private final int level; - - private LoggingConfiguration(String tag, int level) { - this.tag = tag; - this.level = level; - } - - /** - * Returns true if logging is turned on for this configuration. - */ - private boolean isLoggable() { - return Log.isLoggable(tag, level); - } - - /** - * Prints a message using this configuration. - */ - private void println(String message) { - Log.println(level, tag, message); - } - } - - /** cURL logging configuration. */ - private volatile LoggingConfiguration curlConfiguration; - - /** - * Enables cURL request logging for this client. - * - * @param name to log messages with - * @param level at which to log messages (see {@link android.util.Log}) - */ - public void enableCurlLogging(String name, int level) { - if (name == null) { - throw new NullPointerException("name"); - } - if (level < Log.VERBOSE || level > Log.ASSERT) { - throw new IllegalArgumentException("Level is out of range [" - + Log.VERBOSE + ".." + Log.ASSERT + "]"); - } - - curlConfiguration = new LoggingConfiguration(name, level); - } - - /** - * Disables cURL logging for this client. - */ - public void disableCurlLogging() { - curlConfiguration = null; - } - - /** - * Logs cURL commands equivalent to requests. - */ - private class CurlLogger implements HttpRequestInterceptor { - public void process(HttpRequest request, HttpContext context) - throws HttpException, IOException { - LoggingConfiguration configuration = curlConfiguration; - if (configuration != null - && configuration.isLoggable() - && request instanceof HttpUriRequest) { - // Never print auth token -- we used to check ro.secure=0 to - // enable that, but can't do that in unbundled code. - configuration.println(toCurl((HttpUriRequest) request, false)); - } - } - } - - /** - * Generates a cURL command equivalent to the given request. - */ - private static String toCurl(HttpUriRequest request, boolean logAuthToken) throws IOException { - StringBuilder builder = new StringBuilder(); - - builder.append("curl "); - - // add in the method - builder.append("-X "); - builder.append(request.getMethod()); - builder.append(" "); - - for (Header header: request.getAllHeaders()) { - if (!logAuthToken - && (header.getName().equals("Authorization") || - header.getName().equals("Cookie"))) { - continue; - } - builder.append("--header \""); - builder.append(header.toString().trim()); - builder.append("\" "); - } - - URI uri = request.getURI(); - - // If this is a wrapped request, use the URI from the original - // request instead. getURI() on the wrapper seems to return a - // relative URI. We want an absolute URI. - if (request instanceof RequestWrapper) { - HttpRequest original = ((RequestWrapper) request).getOriginal(); - if (original instanceof HttpUriRequest) { - uri = ((HttpUriRequest) original).getURI(); - } - } - - builder.append("\""); - builder.append(uri); - builder.append("\""); - - if (request instanceof HttpEntityEnclosingRequest) { - HttpEntityEnclosingRequest entityRequest = - (HttpEntityEnclosingRequest) request; - HttpEntity entity = entityRequest.getEntity(); - if (entity != null && entity.isRepeatable()) { - if (entity.getContentLength() < 1024) { - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - entity.writeTo(stream); - - if (isBinaryContent(request)) { - String base64 = Base64.encodeToString(stream.toByteArray(), Base64.NO_WRAP); - builder.insert(0, "echo '" + base64 + "' | base64 -d > /tmp/$$.bin; "); - builder.append(" --data-binary @/tmp/$$.bin"); - } else { - String entityString = stream.toString(); - builder.append(" --data-ascii \"") - .append(entityString) - .append("\""); - } - } else { - builder.append(" [TOO MUCH DATA TO INCLUDE]"); - } - } - } - - return builder.toString(); - } - - private static boolean isBinaryContent(HttpUriRequest request) { - Header[] headers; - headers = request.getHeaders(Headers.CONTENT_ENCODING); - if (headers != null) { - for (Header header : headers) { - if ("gzip".equalsIgnoreCase(header.getValue())) { - return true; - } - } - } - - headers = request.getHeaders(Headers.CONTENT_TYPE); - if (headers != null) { - for (Header header : headers) { - for (String contentType : textContentTypes) { - if (header.getValue().startsWith(contentType)) { - return false; - } - } - } - } - return true; - } - - /** - * Returns the date of the given HTTP date string. This method can identify - * and parse the date formats emitted by common HTTP servers, such as - * <a href="http://www.ietf.org/rfc/rfc0822.txt">RFC 822</a>, - * <a href="http://www.ietf.org/rfc/rfc0850.txt">RFC 850</a>, - * <a href="http://www.ietf.org/rfc/rfc1036.txt">RFC 1036</a>, - * <a href="http://www.ietf.org/rfc/rfc1123.txt">RFC 1123</a> and - * <a href="http://www.opengroup.org/onlinepubs/007908799/xsh/asctime.html">ANSI - * C's asctime()</a>. - * - * @return the number of milliseconds since Jan. 1, 1970, midnight GMT. - * @throws IllegalArgumentException if {@code dateString} is not a date or - * of an unsupported format. - */ - public static long parseDate(String dateString) { - return HttpDateTime.parse(dateString); - } -} diff --git a/core/java/android/net/http/AndroidHttpClientConnection.java b/core/java/android/net/http/AndroidHttpClientConnection.java deleted file mode 100644 index 6d48fce..0000000 --- a/core/java/android/net/http/AndroidHttpClientConnection.java +++ /dev/null @@ -1,460 +0,0 @@ -/* - * Copyright (C) 2008 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.net.http; - -import org.apache.http.HttpConnection; -import org.apache.http.HttpClientConnection; -import org.apache.http.HttpConnectionMetrics; -import org.apache.http.HttpEntity; -import org.apache.http.HttpEntityEnclosingRequest; -import org.apache.http.HttpException; -import org.apache.http.HttpInetConnection; -import org.apache.http.HttpRequest; -import org.apache.http.HttpResponse; -import org.apache.http.NoHttpResponseException; -import org.apache.http.StatusLine; -import org.apache.http.entity.BasicHttpEntity; -import org.apache.http.entity.ContentLengthStrategy; -import org.apache.http.impl.HttpConnectionMetricsImpl; -import org.apache.http.impl.entity.EntitySerializer; -import org.apache.http.impl.entity.StrictContentLengthStrategy; -import org.apache.http.impl.io.ChunkedInputStream; -import org.apache.http.impl.io.ContentLengthInputStream; -import org.apache.http.impl.io.HttpRequestWriter; -import org.apache.http.impl.io.IdentityInputStream; -import org.apache.http.impl.io.SocketInputBuffer; -import org.apache.http.impl.io.SocketOutputBuffer; -import org.apache.http.io.HttpMessageWriter; -import org.apache.http.io.SessionInputBuffer; -import org.apache.http.io.SessionOutputBuffer; -import org.apache.http.message.BasicLineParser; -import org.apache.http.message.ParserCursor; -import org.apache.http.params.CoreConnectionPNames; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; -import org.apache.http.ParseException; -import org.apache.http.util.CharArrayBuffer; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.Socket; -import java.net.SocketException; - -/** - * A alternate class for (@link DefaultHttpClientConnection). - * It has better performance than DefaultHttpClientConnection - * - * {@hide} - */ -public class AndroidHttpClientConnection - implements HttpInetConnection, HttpConnection { - - private SessionInputBuffer inbuffer = null; - private SessionOutputBuffer outbuffer = null; - private int maxHeaderCount; - // store CoreConnectionPNames.MAX_LINE_LENGTH for performance - private int maxLineLength; - - private final EntitySerializer entityserializer; - - private HttpMessageWriter requestWriter = null; - private HttpConnectionMetricsImpl metrics = null; - private volatile boolean open; - private Socket socket = null; - - public AndroidHttpClientConnection() { - this.entityserializer = new EntitySerializer( - new StrictContentLengthStrategy()); - } - - /** - * Bind socket and set HttpParams to AndroidHttpClientConnection - * @param socket outgoing socket - * @param params HttpParams - * @throws IOException - */ - public void bind( - final Socket socket, - final HttpParams params) throws IOException { - if (socket == null) { - throw new IllegalArgumentException("Socket may not be null"); - } - if (params == null) { - throw new IllegalArgumentException("HTTP parameters may not be null"); - } - assertNotOpen(); - socket.setTcpNoDelay(HttpConnectionParams.getTcpNoDelay(params)); - socket.setSoTimeout(HttpConnectionParams.getSoTimeout(params)); - - int linger = HttpConnectionParams.getLinger(params); - if (linger >= 0) { - socket.setSoLinger(linger > 0, linger); - } - this.socket = socket; - - int buffersize = HttpConnectionParams.getSocketBufferSize(params); - this.inbuffer = new SocketInputBuffer(socket, buffersize, params); - this.outbuffer = new SocketOutputBuffer(socket, buffersize, params); - - maxHeaderCount = params.getIntParameter( - CoreConnectionPNames.MAX_HEADER_COUNT, -1); - maxLineLength = params.getIntParameter( - CoreConnectionPNames.MAX_LINE_LENGTH, -1); - - this.requestWriter = new HttpRequestWriter(outbuffer, null, params); - - this.metrics = new HttpConnectionMetricsImpl( - inbuffer.getMetrics(), - outbuffer.getMetrics()); - - this.open = true; - } - - @Override - public String toString() { - StringBuilder buffer = new StringBuilder(); - buffer.append(getClass().getSimpleName()).append("["); - if (isOpen()) { - buffer.append(getRemotePort()); - } else { - buffer.append("closed"); - } - buffer.append("]"); - return buffer.toString(); - } - - - private void assertNotOpen() { - if (this.open) { - throw new IllegalStateException("Connection is already open"); - } - } - - private void assertOpen() { - if (!this.open) { - throw new IllegalStateException("Connection is not open"); - } - } - - public boolean isOpen() { - // to make this method useful, we want to check if the socket is connected - return (this.open && this.socket != null && this.socket.isConnected()); - } - - public InetAddress getLocalAddress() { - if (this.socket != null) { - return this.socket.getLocalAddress(); - } else { - return null; - } - } - - public int getLocalPort() { - if (this.socket != null) { - return this.socket.getLocalPort(); - } else { - return -1; - } - } - - public InetAddress getRemoteAddress() { - if (this.socket != null) { - return this.socket.getInetAddress(); - } else { - return null; - } - } - - public int getRemotePort() { - if (this.socket != null) { - return this.socket.getPort(); - } else { - return -1; - } - } - - public void setSocketTimeout(int timeout) { - assertOpen(); - if (this.socket != null) { - try { - this.socket.setSoTimeout(timeout); - } catch (SocketException ignore) { - // It is not quite clear from the original documentation if there are any - // other legitimate cases for a socket exception to be thrown when setting - // SO_TIMEOUT besides the socket being already closed - } - } - } - - public int getSocketTimeout() { - if (this.socket != null) { - try { - return this.socket.getSoTimeout(); - } catch (SocketException ignore) { - return -1; - } - } else { - return -1; - } - } - - public void shutdown() throws IOException { - this.open = false; - Socket tmpsocket = this.socket; - if (tmpsocket != null) { - tmpsocket.close(); - } - } - - public void close() throws IOException { - if (!this.open) { - return; - } - this.open = false; - doFlush(); - try { - try { - this.socket.shutdownOutput(); - } catch (IOException ignore) { - } - try { - this.socket.shutdownInput(); - } catch (IOException ignore) { - } - } catch (UnsupportedOperationException ignore) { - // if one isn't supported, the other one isn't either - } - this.socket.close(); - } - - /** - * Sends the request line and all headers over the connection. - * @param request the request whose headers to send. - * @throws HttpException - * @throws IOException - */ - public void sendRequestHeader(final HttpRequest request) - throws HttpException, IOException { - if (request == null) { - throw new IllegalArgumentException("HTTP request may not be null"); - } - assertOpen(); - this.requestWriter.write(request); - this.metrics.incrementRequestCount(); - } - - /** - * Sends the request entity over the connection. - * @param request the request whose entity to send. - * @throws HttpException - * @throws IOException - */ - public void sendRequestEntity(final HttpEntityEnclosingRequest request) - throws HttpException, IOException { - if (request == null) { - throw new IllegalArgumentException("HTTP request may not be null"); - } - assertOpen(); - if (request.getEntity() == null) { - return; - } - this.entityserializer.serialize( - this.outbuffer, - request, - request.getEntity()); - } - - protected void doFlush() throws IOException { - this.outbuffer.flush(); - } - - public void flush() throws IOException { - assertOpen(); - doFlush(); - } - - /** - * Parses the response headers and adds them to the - * given {@code headers} object, and returns the response StatusLine - * @param headers store parsed header to headers. - * @throws IOException - * @return StatusLine - * @see HttpClientConnection#receiveResponseHeader() - */ - public StatusLine parseResponseHeader(Headers headers) - throws IOException, ParseException { - assertOpen(); - - CharArrayBuffer current = new CharArrayBuffer(64); - - if (inbuffer.readLine(current) == -1) { - throw new NoHttpResponseException("The target server failed to respond"); - } - - // Create the status line from the status string - StatusLine statusline = BasicLineParser.DEFAULT.parseStatusLine( - current, new ParserCursor(0, current.length())); - - if (HttpLog.LOGV) HttpLog.v("read: " + statusline); - int statusCode = statusline.getStatusCode(); - - // Parse header body - CharArrayBuffer previous = null; - int headerNumber = 0; - while(true) { - if (current == null) { - current = new CharArrayBuffer(64); - } else { - // This must be he buffer used to parse the status - current.clear(); - } - int l = inbuffer.readLine(current); - if (l == -1 || current.length() < 1) { - break; - } - // Parse the header name and value - // Check for folded headers first - // Detect LWS-char see HTTP/1.0 or HTTP/1.1 Section 2.2 - // discussion on folded headers - char first = current.charAt(0); - if ((first == ' ' || first == '\t') && previous != null) { - // we have continuation folded header - // so append value - int start = 0; - int length = current.length(); - while (start < length) { - char ch = current.charAt(start); - if (ch != ' ' && ch != '\t') { - break; - } - start++; - } - if (maxLineLength > 0 && - previous.length() + 1 + current.length() - start > - maxLineLength) { - throw new IOException("Maximum line length limit exceeded"); - } - previous.append(' '); - previous.append(current, start, current.length() - start); - } else { - if (previous != null) { - headers.parseHeader(previous); - } - headerNumber++; - previous = current; - current = null; - } - if (maxHeaderCount > 0 && headerNumber >= maxHeaderCount) { - throw new IOException("Maximum header count exceeded"); - } - } - - if (previous != null) { - headers.parseHeader(previous); - } - - if (statusCode >= 200) { - this.metrics.incrementResponseCount(); - } - return statusline; - } - - /** - * Return the next response entity. - * @param headers contains values for parsing entity - * @see HttpClientConnection#receiveResponseEntity(HttpResponse response) - */ - public HttpEntity receiveResponseEntity(final Headers headers) { - assertOpen(); - BasicHttpEntity entity = new BasicHttpEntity(); - - long len = determineLength(headers); - if (len == ContentLengthStrategy.CHUNKED) { - entity.setChunked(true); - entity.setContentLength(-1); - entity.setContent(new ChunkedInputStream(inbuffer)); - } else if (len == ContentLengthStrategy.IDENTITY) { - entity.setChunked(false); - entity.setContentLength(-1); - entity.setContent(new IdentityInputStream(inbuffer)); - } else { - entity.setChunked(false); - entity.setContentLength(len); - entity.setContent(new ContentLengthInputStream(inbuffer, len)); - } - - String contentTypeHeader = headers.getContentType(); - if (contentTypeHeader != null) { - entity.setContentType(contentTypeHeader); - } - String contentEncodingHeader = headers.getContentEncoding(); - if (contentEncodingHeader != null) { - entity.setContentEncoding(contentEncodingHeader); - } - - return entity; - } - - private long determineLength(final Headers headers) { - long transferEncoding = headers.getTransferEncoding(); - // We use Transfer-Encoding if present and ignore Content-Length. - // RFC2616, 4.4 item number 3 - if (transferEncoding < Headers.NO_TRANSFER_ENCODING) { - return transferEncoding; - } else { - long contentlen = headers.getContentLength(); - if (contentlen > Headers.NO_CONTENT_LENGTH) { - return contentlen; - } else { - return ContentLengthStrategy.IDENTITY; - } - } - } - - /** - * Checks whether this connection has gone down. - * Network connections may get closed during some time of inactivity - * for several reasons. The next time a read is attempted on such a - * connection it will throw an IOException. - * This method tries to alleviate this inconvenience by trying to - * find out if a connection is still usable. Implementations may do - * that by attempting a read with a very small timeout. Thus this - * method may block for a small amount of time before returning a result. - * It is therefore an <i>expensive</i> operation. - * - * @return <code>true</code> if attempts to use this connection are - * likely to succeed, or <code>false</code> if they are likely - * to fail and this connection should be closed - */ - public boolean isStale() { - assertOpen(); - try { - this.inbuffer.isDataAvailable(1); - return false; - } catch (IOException ex) { - return true; - } - } - - /** - * Returns a collection of connection metrcis - * @return HttpConnectionMetrics - */ - public HttpConnectionMetrics getMetrics() { - return this.metrics; - } -} diff --git a/core/java/android/net/http/CertificateChainValidator.java b/core/java/android/net/http/CertificateChainValidator.java deleted file mode 100644 index bf3fe02..0000000 --- a/core/java/android/net/http/CertificateChainValidator.java +++ /dev/null @@ -1,279 +0,0 @@ -/* - * Copyright (C) 2008 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.net.http; - -import com.android.org.conscrypt.SSLParametersImpl; -import com.android.org.conscrypt.TrustManagerImpl; - -import android.util.Slog; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.lang.reflect.Method; -import java.security.GeneralSecurityException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLHandshakeException; -import javax.net.ssl.SSLSession; -import javax.net.ssl.SSLSocket; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; - -/** - * Class responsible for all server certificate validation functionality - * - * {@hide} - */ -public class CertificateChainValidator { - private static final String TAG = "CertificateChainValidator"; - - private static class NoPreloadHolder { - /** - * The singleton instance of the certificate chain validator. - */ - private static final CertificateChainValidator sInstance = new CertificateChainValidator(); - - /** - * The singleton instance of the hostname verifier. - */ - private static final HostnameVerifier sVerifier = HttpsURLConnection - .getDefaultHostnameVerifier(); - } - - private X509TrustManager mTrustManager; - - /** - * @return The singleton instance of the certificates chain validator - */ - public static CertificateChainValidator getInstance() { - return NoPreloadHolder.sInstance; - } - - /** - * Creates a new certificate chain validator. This is a private constructor. - * If you need a Certificate chain validator, call getInstance(). - */ - private CertificateChainValidator() { - try { - TrustManagerFactory tmf = TrustManagerFactory.getInstance("X.509"); - tmf.init((KeyStore) null); - for (TrustManager tm : tmf.getTrustManagers()) { - if (tm instanceof X509TrustManager) { - mTrustManager = (X509TrustManager) tm; - } - } - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException("X.509 TrustManagerFactory must be available", e); - } catch (KeyStoreException e) { - throw new RuntimeException("X.509 TrustManagerFactory cannot be initialized", e); - } - - if (mTrustManager == null) { - throw new RuntimeException( - "None of the X.509 TrustManagers are X509TrustManager"); - } - } - - /** - * Performs the handshake and server certificates validation - * Notice a new chain will be rebuilt by tracing the issuer and subject - * before calling checkServerTrusted(). - * And if the last traced certificate is self issued and it is expired, it - * will be dropped. - * @param sslSocket The secure connection socket - * @param domain The website domain - * @return An SSL error object if there is an error and null otherwise - */ - public SslError doHandshakeAndValidateServerCertificates( - HttpsConnection connection, SSLSocket sslSocket, String domain) - throws IOException { - // get a valid SSLSession, close the socket if we fail - SSLSession sslSession = sslSocket.getSession(); - if (!sslSession.isValid()) { - closeSocketThrowException(sslSocket, "failed to perform SSL handshake"); - } - - // retrieve the chain of the server peer certificates - Certificate[] peerCertificates = - sslSocket.getSession().getPeerCertificates(); - - if (peerCertificates == null || peerCertificates.length == 0) { - closeSocketThrowException( - sslSocket, "failed to retrieve peer certificates"); - } else { - // update the SSL certificate associated with the connection - if (connection != null) { - if (peerCertificates[0] != null) { - connection.setCertificate( - new SslCertificate((X509Certificate)peerCertificates[0])); - } - } - } - - return verifyServerDomainAndCertificates((X509Certificate[]) peerCertificates, domain, "RSA"); - } - - /** - * Similar to doHandshakeAndValidateServerCertificates but exposed to JNI for use - * by Chromium HTTPS stack to validate the cert chain. - * @param certChain The bytes for certificates in ASN.1 DER encoded certificates format. - * @param domain The full website hostname and domain - * @param authType The authentication type for the cert chain - * @return An SSL error object if there is an error and null otherwise - */ - public static SslError verifyServerCertificates( - byte[][] certChain, String domain, String authType) - throws IOException { - - if (certChain == null || certChain.length == 0) { - throw new IllegalArgumentException("bad certificate chain"); - } - - X509Certificate[] serverCertificates = new X509Certificate[certChain.length]; - - try { - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - for (int i = 0; i < certChain.length; ++i) { - serverCertificates[i] = (X509Certificate) cf.generateCertificate( - new ByteArrayInputStream(certChain[i])); - } - } catch (CertificateException e) { - throw new IOException("can't read certificate", e); - } - - return verifyServerDomainAndCertificates(serverCertificates, domain, authType); - } - - /** - * Handles updates to credential storage. - */ - public static void handleTrustStorageUpdate() { - TrustManagerFactory tmf; - try { - tmf = TrustManagerFactory.getInstance("X.509"); - tmf.init((KeyStore) null); - } catch (NoSuchAlgorithmException e) { - Slog.w(TAG, "Couldn't find default X.509 TrustManagerFactory"); - return; - } catch (KeyStoreException e) { - Slog.w(TAG, "Couldn't initialize default X.509 TrustManagerFactory", e); - return; - } - - TrustManager[] tms = tmf.getTrustManagers(); - boolean sentUpdate = false; - for (TrustManager tm : tms) { - try { - Method updateMethod = tm.getClass().getDeclaredMethod("handleTrustStorageUpdate"); - updateMethod.setAccessible(true); - updateMethod.invoke(tm); - sentUpdate = true; - } catch (Exception e) { - } - } - if (!sentUpdate) { - Slog.w(TAG, "Didn't find a TrustManager to handle CA list update"); - } - } - - /** - * Common code of doHandshakeAndValidateServerCertificates and verifyServerCertificates. - * Calls DomainNamevalidator to verify the domain, and TrustManager to verify the certs. - * @param chain the cert chain in X509 cert format. - * @param domain The full website hostname and domain - * @param authType The authentication type for the cert chain - * @return An SSL error object if there is an error and null otherwise - */ - private static SslError verifyServerDomainAndCertificates( - X509Certificate[] chain, String domain, String authType) - throws IOException { - // check if the first certificate in the chain is for this site - X509Certificate currCertificate = chain[0]; - if (currCertificate == null) { - throw new IllegalArgumentException("certificate for this site is null"); - } - - boolean valid = domain != null - && !domain.isEmpty() - && NoPreloadHolder.sVerifier.verify(domain, - new DelegatingSSLSession.CertificateWrap(currCertificate)); - if (!valid) { - if (HttpLog.LOGV) { - HttpLog.v("certificate not for this host: " + domain); - } - return new SslError(SslError.SSL_IDMISMATCH, currCertificate); - } - - try { - X509TrustManager x509TrustManager = SSLParametersImpl.getDefaultX509TrustManager(); - if (x509TrustManager instanceof TrustManagerImpl) { - TrustManagerImpl trustManager = (TrustManagerImpl) x509TrustManager; - trustManager.checkServerTrusted(chain, authType, domain); - } else { - x509TrustManager.checkServerTrusted(chain, authType); - } - return null; // No errors. - } catch (GeneralSecurityException e) { - if (HttpLog.LOGV) { - HttpLog.v("failed to validate the certificate chain, error: " + - e.getMessage()); - } - return new SslError(SslError.SSL_UNTRUSTED, currCertificate); - } - } - - /** - * Returns the platform default {@link X509TrustManager}. - */ - private X509TrustManager getTrustManager() { - return mTrustManager; - } - - private void closeSocketThrowException( - SSLSocket socket, String errorMessage, String defaultErrorMessage) - throws IOException { - closeSocketThrowException( - socket, errorMessage != null ? errorMessage : defaultErrorMessage); - } - - private void closeSocketThrowException(SSLSocket socket, - String errorMessage) throws IOException { - if (HttpLog.LOGV) { - HttpLog.v("validation error: " + errorMessage); - } - - if (socket != null) { - SSLSession session = socket.getSession(); - if (session != null) { - session.invalidate(); - } - - socket.close(); - } - - throw new SSLHandshakeException(errorMessage); - } -} diff --git a/core/java/android/net/http/Connection.java b/core/java/android/net/http/Connection.java deleted file mode 100644 index 831bd0e..0000000 --- a/core/java/android/net/http/Connection.java +++ /dev/null @@ -1,575 +0,0 @@ -/* - * Copyright (C) 2007 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.net.http; - -import android.content.Context; -import android.os.SystemClock; - -import java.io.IOException; -import java.net.UnknownHostException; -import java.util.LinkedList; - -import javax.net.ssl.SSLHandshakeException; - -import org.apache.http.ConnectionReuseStrategy; -import org.apache.http.HttpEntity; -import org.apache.http.HttpException; -import org.apache.http.HttpHost; -import org.apache.http.HttpVersion; -import org.apache.http.ParseException; -import org.apache.http.ProtocolVersion; -import org.apache.http.protocol.ExecutionContext; -import org.apache.http.protocol.HttpContext; -import org.apache.http.protocol.BasicHttpContext; - -/** - * {@hide} - */ -abstract class Connection { - - /** - * Allow a TCP connection 60 idle seconds before erroring out - */ - static final int SOCKET_TIMEOUT = 60000; - - private static final int SEND = 0; - private static final int READ = 1; - private static final int DRAIN = 2; - private static final int DONE = 3; - private static final String[] states = {"SEND", "READ", "DRAIN", "DONE"}; - - Context mContext; - - /** The low level connection */ - protected AndroidHttpClientConnection mHttpClientConnection = null; - - /** - * The server SSL certificate associated with this connection - * (null if the connection is not secure) - * It would be nice to store the whole certificate chain, but - * we want to keep things as light-weight as possible - */ - protected SslCertificate mCertificate = null; - - /** - * The host this connection is connected to. If using proxy, - * this is set to the proxy address - */ - HttpHost mHost; - - /** true if the connection can be reused for sending more requests */ - private boolean mCanPersist; - - /** context required by ConnectionReuseStrategy. */ - private HttpContext mHttpContext; - - /** set when cancelled */ - private static int STATE_NORMAL = 0; - private static int STATE_CANCEL_REQUESTED = 1; - private int mActive = STATE_NORMAL; - - /** The number of times to try to re-connect (if connect fails). */ - private final static int RETRY_REQUEST_LIMIT = 2; - - private static final int MIN_PIPE = 2; - private static final int MAX_PIPE = 3; - - /** - * Doesn't seem to exist anymore in the new HTTP client, so copied here. - */ - private static final String HTTP_CONNECTION = "http.connection"; - - RequestFeeder mRequestFeeder; - - /** - * Buffer for feeding response blocks to webkit. One block per - * connection reduces memory churn. - */ - private byte[] mBuf; - - protected Connection(Context context, HttpHost host, - RequestFeeder requestFeeder) { - mContext = context; - mHost = host; - mRequestFeeder = requestFeeder; - - mCanPersist = false; - mHttpContext = new BasicHttpContext(null); - } - - HttpHost getHost() { - return mHost; - } - - /** - * connection factory: returns an HTTP or HTTPS connection as - * necessary - */ - static Connection getConnection( - Context context, HttpHost host, HttpHost proxy, - RequestFeeder requestFeeder) { - - if (host.getSchemeName().equals("http")) { - return new HttpConnection(context, host, requestFeeder); - } - - // Otherwise, default to https - return new HttpsConnection(context, host, proxy, requestFeeder); - } - - /** - * @return The server SSL certificate associated with this - * connection (null if the connection is not secure) - */ - /* package */ SslCertificate getCertificate() { - return mCertificate; - } - - /** - * Close current network connection - * Note: this runs in non-network thread - */ - void cancel() { - mActive = STATE_CANCEL_REQUESTED; - closeConnection(); - if (HttpLog.LOGV) HttpLog.v( - "Connection.cancel(): connection closed " + mHost); - } - - /** - * Process requests in queue - * pipelines requests - */ - void processRequests(Request firstRequest) { - Request req = null; - boolean empty; - int error = EventHandler.OK; - Exception exception = null; - - LinkedList<Request> pipe = new LinkedList<Request>(); - - int minPipe = MIN_PIPE, maxPipe = MAX_PIPE; - int state = SEND; - - while (state != DONE) { - if (HttpLog.LOGV) HttpLog.v( - states[state] + " pipe " + pipe.size()); - - /* If a request was cancelled, give other cancel requests - some time to go through so we don't uselessly restart - connections */ - if (mActive == STATE_CANCEL_REQUESTED) { - try { - Thread.sleep(100); - } catch (InterruptedException x) { /* ignore */ } - mActive = STATE_NORMAL; - } - - switch (state) { - case SEND: { - if (pipe.size() == maxPipe) { - state = READ; - break; - } - /* get a request */ - if (firstRequest == null) { - req = mRequestFeeder.getRequest(mHost); - } else { - req = firstRequest; - firstRequest = null; - } - if (req == null) { - state = DRAIN; - break; - } - req.setConnection(this); - - /* Don't work on cancelled requests. */ - if (req.mCancelled) { - if (HttpLog.LOGV) HttpLog.v( - "processRequests(): skipping cancelled request " - + req); - req.complete(); - break; - } - - if (mHttpClientConnection == null || - !mHttpClientConnection.isOpen()) { - /* If this call fails, the address is bad or - the net is down. Punt for now. - - FIXME: blow out entire queue here on - connection failure if net up? */ - - if (!openHttpConnection(req)) { - state = DONE; - break; - } - } - - /* we have a connection, let the event handler - * know of any associated certificate, - * potentially none. - */ - req.mEventHandler.certificate(mCertificate); - - try { - /* FIXME: don't increment failure count if old - connection? There should not be a penalty for - attempting to reuse an old connection */ - req.sendRequest(mHttpClientConnection); - } catch (HttpException e) { - exception = e; - error = EventHandler.ERROR; - } catch (IOException e) { - exception = e; - error = EventHandler.ERROR_IO; - } catch (IllegalStateException e) { - exception = e; - error = EventHandler.ERROR_IO; - } - if (exception != null) { - if (httpFailure(req, error, exception) && - !req.mCancelled) { - /* retry request if not permanent failure - or cancelled */ - pipe.addLast(req); - } - exception = null; - state = clearPipe(pipe) ? DONE : SEND; - minPipe = maxPipe = 1; - break; - } - - pipe.addLast(req); - if (!mCanPersist) state = READ; - break; - - } - case DRAIN: - case READ: { - empty = !mRequestFeeder.haveRequest(mHost); - int pipeSize = pipe.size(); - if (state != DRAIN && pipeSize < minPipe && - !empty && mCanPersist) { - state = SEND; - break; - } else if (pipeSize == 0) { - /* Done if no other work to do */ - state = empty ? DONE : SEND; - break; - } - - req = (Request)pipe.removeFirst(); - if (HttpLog.LOGV) HttpLog.v( - "processRequests() reading " + req); - - try { - req.readResponse(mHttpClientConnection); - } catch (ParseException e) { - exception = e; - error = EventHandler.ERROR_IO; - } catch (IOException e) { - exception = e; - error = EventHandler.ERROR_IO; - } catch (IllegalStateException e) { - exception = e; - error = EventHandler.ERROR_IO; - } - if (exception != null) { - if (httpFailure(req, error, exception) && - !req.mCancelled) { - /* retry request if not permanent failure - or cancelled */ - req.reset(); - pipe.addFirst(req); - } - exception = null; - mCanPersist = false; - } - if (!mCanPersist) { - if (HttpLog.LOGV) HttpLog.v( - "processRequests(): no persist, closing " + - mHost); - - closeConnection(); - - mHttpContext.removeAttribute(HTTP_CONNECTION); - clearPipe(pipe); - minPipe = maxPipe = 1; - state = SEND; - } - break; - } - } - } - } - - /** - * After a send/receive failure, any pipelined requests must be - * cleared back to the mRequest queue - * @return true if mRequests is empty after pipe cleared - */ - private boolean clearPipe(LinkedList<Request> pipe) { - boolean empty = true; - if (HttpLog.LOGV) HttpLog.v( - "Connection.clearPipe(): clearing pipe " + pipe.size()); - synchronized (mRequestFeeder) { - Request tReq; - while (!pipe.isEmpty()) { - tReq = (Request)pipe.removeLast(); - if (HttpLog.LOGV) HttpLog.v( - "clearPipe() adding back " + mHost + " " + tReq); - mRequestFeeder.requeueRequest(tReq); - empty = false; - } - if (empty) empty = !mRequestFeeder.haveRequest(mHost); - } - return empty; - } - - /** - * @return true on success - */ - private boolean openHttpConnection(Request req) { - - long now = SystemClock.uptimeMillis(); - int error = EventHandler.OK; - Exception exception = null; - - try { - // reset the certificate to null before opening a connection - mCertificate = null; - mHttpClientConnection = openConnection(req); - if (mHttpClientConnection != null) { - mHttpClientConnection.setSocketTimeout(SOCKET_TIMEOUT); - mHttpContext.setAttribute(HTTP_CONNECTION, - mHttpClientConnection); - } else { - // we tried to do SSL tunneling, failed, - // and need to drop the request; - // we have already informed the handler - req.mFailCount = RETRY_REQUEST_LIMIT; - return false; - } - } catch (UnknownHostException e) { - if (HttpLog.LOGV) HttpLog.v("Failed to open connection"); - error = EventHandler.ERROR_LOOKUP; - exception = e; - } catch (IllegalArgumentException e) { - if (HttpLog.LOGV) HttpLog.v("Illegal argument exception"); - error = EventHandler.ERROR_CONNECT; - req.mFailCount = RETRY_REQUEST_LIMIT; - exception = e; - } catch (SSLConnectionClosedByUserException e) { - // hack: if we have an SSL connection failure, - // we don't want to reconnect - req.mFailCount = RETRY_REQUEST_LIMIT; - // no error message - return false; - } catch (SSLHandshakeException e) { - // hack: if we have an SSL connection failure, - // we don't want to reconnect - req.mFailCount = RETRY_REQUEST_LIMIT; - if (HttpLog.LOGV) HttpLog.v( - "SSL exception performing handshake"); - error = EventHandler.ERROR_FAILED_SSL_HANDSHAKE; - exception = e; - } catch (IOException e) { - error = EventHandler.ERROR_CONNECT; - exception = e; - } - - if (HttpLog.LOGV) { - long now2 = SystemClock.uptimeMillis(); - HttpLog.v("Connection.openHttpConnection() " + - (now2 - now) + " " + mHost); - } - - if (error == EventHandler.OK) { - return true; - } else { - if (req.mFailCount < RETRY_REQUEST_LIMIT) { - // requeue - mRequestFeeder.requeueRequest(req); - req.mFailCount++; - } else { - httpFailure(req, error, exception); - } - return error == EventHandler.OK; - } - } - - /** - * Helper. Calls the mEventHandler's error() method only if - * request failed permanently. Increments mFailcount on failure. - * - * Increments failcount only if the network is believed to be - * connected - * - * @return true if request can be retried (less than - * RETRY_REQUEST_LIMIT failures have occurred). - */ - private boolean httpFailure(Request req, int errorId, Exception e) { - boolean ret = true; - - // e.printStackTrace(); - if (HttpLog.LOGV) HttpLog.v( - "httpFailure() ******* " + e + " count " + req.mFailCount + - " " + mHost + " " + req.getUri()); - - if (++req.mFailCount >= RETRY_REQUEST_LIMIT) { - ret = false; - String error; - if (errorId < 0) { - error = getEventHandlerErrorString(errorId); - } else { - Throwable cause = e.getCause(); - error = cause != null ? cause.toString() : e.getMessage(); - } - req.mEventHandler.error(errorId, error); - req.complete(); - } - - closeConnection(); - mHttpContext.removeAttribute(HTTP_CONNECTION); - - return ret; - } - - private static String getEventHandlerErrorString(int errorId) { - switch (errorId) { - case EventHandler.OK: - return "OK"; - - case EventHandler.ERROR: - return "ERROR"; - - case EventHandler.ERROR_LOOKUP: - return "ERROR_LOOKUP"; - - case EventHandler.ERROR_UNSUPPORTED_AUTH_SCHEME: - return "ERROR_UNSUPPORTED_AUTH_SCHEME"; - - case EventHandler.ERROR_AUTH: - return "ERROR_AUTH"; - - case EventHandler.ERROR_PROXYAUTH: - return "ERROR_PROXYAUTH"; - - case EventHandler.ERROR_CONNECT: - return "ERROR_CONNECT"; - - case EventHandler.ERROR_IO: - return "ERROR_IO"; - - case EventHandler.ERROR_TIMEOUT: - return "ERROR_TIMEOUT"; - - case EventHandler.ERROR_REDIRECT_LOOP: - return "ERROR_REDIRECT_LOOP"; - - case EventHandler.ERROR_UNSUPPORTED_SCHEME: - return "ERROR_UNSUPPORTED_SCHEME"; - - case EventHandler.ERROR_FAILED_SSL_HANDSHAKE: - return "ERROR_FAILED_SSL_HANDSHAKE"; - - case EventHandler.ERROR_BAD_URL: - return "ERROR_BAD_URL"; - - case EventHandler.FILE_ERROR: - return "FILE_ERROR"; - - case EventHandler.FILE_NOT_FOUND_ERROR: - return "FILE_NOT_FOUND_ERROR"; - - case EventHandler.TOO_MANY_REQUESTS_ERROR: - return "TOO_MANY_REQUESTS_ERROR"; - - default: - return "UNKNOWN_ERROR"; - } - } - - HttpContext getHttpContext() { - return mHttpContext; - } - - /** - * Use same logic as ConnectionReuseStrategy - * @see ConnectionReuseStrategy - */ - private boolean keepAlive(HttpEntity entity, - ProtocolVersion ver, int connType, final HttpContext context) { - org.apache.http.HttpConnection conn = (org.apache.http.HttpConnection) - context.getAttribute(ExecutionContext.HTTP_CONNECTION); - - if (conn != null && !conn.isOpen()) - return false; - // do NOT check for stale connection, that is an expensive operation - - if (entity != null) { - if (entity.getContentLength() < 0) { - if (!entity.isChunked() || ver.lessEquals(HttpVersion.HTTP_1_0)) { - // if the content length is not known and is not chunk - // encoded, the connection cannot be reused - return false; - } - } - } - // Check for 'Connection' directive - if (connType == Headers.CONN_CLOSE) { - return false; - } else if (connType == Headers.CONN_KEEP_ALIVE) { - return true; - } - // Resorting to protocol version default close connection policy - return !ver.lessEquals(HttpVersion.HTTP_1_0); - } - - void setCanPersist(HttpEntity entity, ProtocolVersion ver, int connType) { - mCanPersist = keepAlive(entity, ver, connType, mHttpContext); - } - - void setCanPersist(boolean canPersist) { - mCanPersist = canPersist; - } - - boolean getCanPersist() { - return mCanPersist; - } - - /** typically http or https... set by subclass */ - abstract String getScheme(); - abstract void closeConnection(); - abstract AndroidHttpClientConnection openConnection(Request req) throws IOException; - - /** - * Prints request queue to log, for debugging. - * returns request count - */ - public synchronized String toString() { - return mHost.toString(); - } - - byte[] getBuf() { - if (mBuf == null) mBuf = new byte[8192]; - return mBuf; - } - -} diff --git a/core/java/android/net/http/ConnectionThread.java b/core/java/android/net/http/ConnectionThread.java deleted file mode 100644 index d825530..0000000 --- a/core/java/android/net/http/ConnectionThread.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (C) 2008 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.net.http; - -import android.content.Context; -import android.os.SystemClock; - -import java.lang.Thread; - -/** - * {@hide} - */ -class ConnectionThread extends Thread { - - static final int WAIT_TIMEOUT = 5000; - static final int WAIT_TICK = 1000; - - // Performance probe - long mCurrentThreadTime; - long mTotalThreadTime; - - private boolean mWaiting; - private volatile boolean mRunning = true; - private Context mContext; - private RequestQueue.ConnectionManager mConnectionManager; - private RequestFeeder mRequestFeeder; - - private int mId; - Connection mConnection; - - ConnectionThread(Context context, - int id, - RequestQueue.ConnectionManager connectionManager, - RequestFeeder requestFeeder) { - super(); - mContext = context; - setName("http" + id); - mId = id; - mConnectionManager = connectionManager; - mRequestFeeder = requestFeeder; - } - - void requestStop() { - synchronized (mRequestFeeder) { - mRunning = false; - mRequestFeeder.notify(); - } - } - - /** - * Loop until app shutdown. Runs connections in priority - * order. - */ - public void run() { - android.os.Process.setThreadPriority( - android.os.Process.THREAD_PRIORITY_DEFAULT + - android.os.Process.THREAD_PRIORITY_LESS_FAVORABLE); - - // these are used to get performance data. When it is not in the timing, - // mCurrentThreadTime is 0. When it starts timing, mCurrentThreadTime is - // first set to -1, it will be set to the current thread time when the - // next request starts. - mCurrentThreadTime = 0; - mTotalThreadTime = 0; - - while (mRunning) { - if (mCurrentThreadTime == -1) { - mCurrentThreadTime = SystemClock.currentThreadTimeMillis(); - } - - Request request; - - /* Get a request to process */ - request = mRequestFeeder.getRequest(); - - /* wait for work */ - if (request == null) { - synchronized(mRequestFeeder) { - if (HttpLog.LOGV) HttpLog.v("ConnectionThread: Waiting for work"); - mWaiting = true; - try { - mRequestFeeder.wait(); - } catch (InterruptedException e) { - } - mWaiting = false; - if (mCurrentThreadTime != 0) { - mCurrentThreadTime = SystemClock - .currentThreadTimeMillis(); - } - } - } else { - if (HttpLog.LOGV) HttpLog.v("ConnectionThread: new request " + - request.mHost + " " + request ); - - mConnection = mConnectionManager.getConnection(mContext, - request.mHost); - mConnection.processRequests(request); - if (mConnection.getCanPersist()) { - if (!mConnectionManager.recycleConnection(mConnection)) { - mConnection.closeConnection(); - } - } else { - mConnection.closeConnection(); - } - mConnection = null; - - if (mCurrentThreadTime > 0) { - long start = mCurrentThreadTime; - mCurrentThreadTime = SystemClock.currentThreadTimeMillis(); - mTotalThreadTime += mCurrentThreadTime - start; - } - } - - } - } - - public synchronized String toString() { - String con = mConnection == null ? "" : mConnection.toString(); - String active = mWaiting ? "w" : "a"; - return "cid " + mId + " " + active + " " + con; - } - -} diff --git a/core/java/android/net/http/DelegatingSSLSession.java b/core/java/android/net/http/DelegatingSSLSession.java deleted file mode 100644 index 98fbe21..0000000 --- a/core/java/android/net/http/DelegatingSSLSession.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright 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.net.http; - -import java.security.Principal; -import java.security.cert.Certificate; -import java.security.cert.X509Certificate; - -import javax.net.ssl.SSLPeerUnverifiedException; -import javax.net.ssl.SSLSession; -import javax.net.ssl.SSLSessionContext; -import javax.net.ssl.SSLSocket; -import javax.net.ssl.X509TrustManager; - -/** - * This is only used when a {@code certificate} is available but usage - * requires a {@link SSLSession}. - * - * @hide - */ -public class DelegatingSSLSession implements SSLSession { - protected DelegatingSSLSession() { - } - - public static class CertificateWrap extends DelegatingSSLSession { - private final Certificate mCertificate; - - public CertificateWrap(Certificate certificate) { - mCertificate = certificate; - } - - @Override - public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException { - return new Certificate[] { mCertificate }; - } - } - - - @Override - public int getApplicationBufferSize() { - throw new UnsupportedOperationException(); - } - - @Override - public String getCipherSuite() { - throw new UnsupportedOperationException(); - } - - @Override - public long getCreationTime() { - throw new UnsupportedOperationException(); - } - - @Override - public byte[] getId() { - throw new UnsupportedOperationException(); - } - - @Override - public long getLastAccessedTime() { - throw new UnsupportedOperationException(); - } - - @Override - public Certificate[] getLocalCertificates() { - throw new UnsupportedOperationException(); - } - - @Override - public Principal getLocalPrincipal() { - throw new UnsupportedOperationException(); - } - - @Override - public int getPacketBufferSize() { - throw new UnsupportedOperationException(); - } - - @Override - public javax.security.cert.X509Certificate[] getPeerCertificateChain() - throws SSLPeerUnverifiedException { - throw new UnsupportedOperationException(); - } - - @Override - public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException { - throw new UnsupportedOperationException(); - } - - @Override - public String getPeerHost() { - throw new UnsupportedOperationException(); - } - - @Override - public int getPeerPort() { - throw new UnsupportedOperationException(); - } - - @Override - public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { - throw new UnsupportedOperationException(); - } - - @Override - public String getProtocol() { - throw new UnsupportedOperationException(); - } - - @Override - public SSLSessionContext getSessionContext() { - throw new UnsupportedOperationException(); - } - - @Override - public Object getValue(String name) { - throw new UnsupportedOperationException(); - } - - @Override - public String[] getValueNames() { - throw new UnsupportedOperationException(); - } - - @Override - public void invalidate() { - throw new UnsupportedOperationException(); - } - - @Override - public boolean isValid() { - throw new UnsupportedOperationException(); - } - - @Override - public void putValue(String name, Object value) { - throw new UnsupportedOperationException(); - } - - @Override - public void removeValue(String name) { - throw new UnsupportedOperationException(); - } -} diff --git a/core/java/android/net/http/EventHandler.java b/core/java/android/net/http/EventHandler.java deleted file mode 100644 index 3fd471d..0000000 --- a/core/java/android/net/http/EventHandler.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright (C) 2006 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.net.http; - - -/** - * Callbacks in this interface are made as an HTTP request is - * processed. The normal order of callbacks is status(), headers(), - * then multiple data() then endData(). handleSslErrorRequest(), if - * there is an SSL certificate error. error() can occur anywhere - * in the transaction. - * - * {@hide} - */ - -public interface EventHandler { - - /** - * Error codes used in the error() callback. Positive error codes - * are reserved for codes sent by http servers. Negative error - * codes are connection/parsing failures, etc. - */ - - /** Success */ - public static final int OK = 0; - /** Generic error */ - public static final int ERROR = -1; - /** Server or proxy hostname lookup failed */ - public static final int ERROR_LOOKUP = -2; - /** Unsupported authentication scheme (ie, not basic or digest) */ - public static final int ERROR_UNSUPPORTED_AUTH_SCHEME = -3; - /** User authentication failed on server */ - public static final int ERROR_AUTH = -4; - /** User authentication failed on proxy */ - public static final int ERROR_PROXYAUTH = -5; - /** Could not connect to server */ - public static final int ERROR_CONNECT = -6; - /** Failed to write to or read from server */ - public static final int ERROR_IO = -7; - /** Connection timed out */ - public static final int ERROR_TIMEOUT = -8; - /** Too many redirects */ - public static final int ERROR_REDIRECT_LOOP = -9; - /** Unsupported URI scheme (ie, not http, https, etc) */ - public static final int ERROR_UNSUPPORTED_SCHEME = -10; - /** Failed to perform SSL handshake */ - public static final int ERROR_FAILED_SSL_HANDSHAKE = -11; - /** Bad URL */ - public static final int ERROR_BAD_URL = -12; - /** Generic file error for file:/// loads */ - public static final int FILE_ERROR = -13; - /** File not found error for file:/// loads */ - public static final int FILE_NOT_FOUND_ERROR = -14; - /** Too many requests queued */ - public static final int TOO_MANY_REQUESTS_ERROR = -15; - - /** - * Called after status line has been sucessfully processed. - * @param major_version HTTP version advertised by server. major - * is the part before the "." - * @param minor_version HTTP version advertised by server. minor - * is the part after the "." - * @param code HTTP Status code. See RFC 2616. - * @param reason_phrase Textual explanation sent by server - */ - public void status(int major_version, - int minor_version, - int code, - String reason_phrase); - - /** - * Called after all headers are successfully processed. - */ - public void headers(Headers headers); - - /** - * An array containing all or part of the http body as read from - * the server. - * @param data A byte array containing the content - * @param len The length of valid content in data - * - * Note: chunked and compressed encodings are handled within - * android.net.http. Decoded data is passed through this - * interface. - */ - public void data(byte[] data, int len); - - /** - * Called when the document is completely read. No more data() - * callbacks will be made after this call - */ - public void endData(); - - /** - * SSL certificate callback called before resource request is - * made, which will be null for insecure connection. - */ - public void certificate(SslCertificate certificate); - - /** - * There was trouble. - * @param id One of the error codes defined below - * @param description of error - */ - public void error(int id, String description); - - /** - * SSL certificate error callback. Handles SSL error(s) on the way - * up to the user. The callback has to make sure that restartConnection() is called, - * otherwise the connection will be suspended indefinitely. - * @return True if the callback can handle the error, which means it will - * call restartConnection() to unblock the thread later, - * otherwise return false. - */ - public boolean handleSslErrorRequest(SslError error); - -} diff --git a/core/java/android/net/http/Headers.java b/core/java/android/net/http/Headers.java deleted file mode 100644 index 0f8b105..0000000 --- a/core/java/android/net/http/Headers.java +++ /dev/null @@ -1,521 +0,0 @@ -/* - * Copyright (C) 2006 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.net.http; - -import android.util.Log; - -import java.util.ArrayList; - -import org.apache.http.HeaderElement; -import org.apache.http.entity.ContentLengthStrategy; -import org.apache.http.message.BasicHeaderValueParser; -import org.apache.http.message.ParserCursor; -import org.apache.http.protocol.HTTP; -import org.apache.http.util.CharArrayBuffer; - -/** - * Manages received headers - * - * {@hide} - */ -public final class Headers { - private static final String LOGTAG = "Http"; - - // header parsing constant - /** - * indicate HTTP 1.0 connection close after the response - */ - public final static int CONN_CLOSE = 1; - /** - * indicate HTTP 1.1 connection keep alive - */ - public final static int CONN_KEEP_ALIVE = 2; - - // initial values. - public final static int NO_CONN_TYPE = 0; - public final static long NO_TRANSFER_ENCODING = 0; - public final static long NO_CONTENT_LENGTH = -1; - - // header strings - public final static String TRANSFER_ENCODING = "transfer-encoding"; - public final static String CONTENT_LEN = "content-length"; - public final static String CONTENT_TYPE = "content-type"; - public final static String CONTENT_ENCODING = "content-encoding"; - public final static String CONN_DIRECTIVE = "connection"; - - public final static String LOCATION = "location"; - public final static String PROXY_CONNECTION = "proxy-connection"; - - public final static String WWW_AUTHENTICATE = "www-authenticate"; - public final static String PROXY_AUTHENTICATE = "proxy-authenticate"; - public final static String CONTENT_DISPOSITION = "content-disposition"; - public final static String ACCEPT_RANGES = "accept-ranges"; - public final static String EXPIRES = "expires"; - public final static String CACHE_CONTROL = "cache-control"; - public final static String LAST_MODIFIED = "last-modified"; - public final static String ETAG = "etag"; - public final static String SET_COOKIE = "set-cookie"; - public final static String PRAGMA = "pragma"; - public final static String REFRESH = "refresh"; - public final static String X_PERMITTED_CROSS_DOMAIN_POLICIES = "x-permitted-cross-domain-policies"; - - // following hash are generated by String.hashCode() - private final static int HASH_TRANSFER_ENCODING = 1274458357; - private final static int HASH_CONTENT_LEN = -1132779846; - private final static int HASH_CONTENT_TYPE = 785670158; - private final static int HASH_CONTENT_ENCODING = 2095084583; - private final static int HASH_CONN_DIRECTIVE = -775651618; - private final static int HASH_LOCATION = 1901043637; - private final static int HASH_PROXY_CONNECTION = 285929373; - private final static int HASH_WWW_AUTHENTICATE = -243037365; - private final static int HASH_PROXY_AUTHENTICATE = -301767724; - private final static int HASH_CONTENT_DISPOSITION = -1267267485; - private final static int HASH_ACCEPT_RANGES = 1397189435; - private final static int HASH_EXPIRES = -1309235404; - private final static int HASH_CACHE_CONTROL = -208775662; - private final static int HASH_LAST_MODIFIED = 150043680; - private final static int HASH_ETAG = 3123477; - private final static int HASH_SET_COOKIE = 1237214767; - private final static int HASH_PRAGMA = -980228804; - private final static int HASH_REFRESH = 1085444827; - private final static int HASH_X_PERMITTED_CROSS_DOMAIN_POLICIES = -1345594014; - - // keep any headers that require direct access in a presized - // string array - private final static int IDX_TRANSFER_ENCODING = 0; - private final static int IDX_CONTENT_LEN = 1; - private final static int IDX_CONTENT_TYPE = 2; - private final static int IDX_CONTENT_ENCODING = 3; - private final static int IDX_CONN_DIRECTIVE = 4; - private final static int IDX_LOCATION = 5; - private final static int IDX_PROXY_CONNECTION = 6; - private final static int IDX_WWW_AUTHENTICATE = 7; - private final static int IDX_PROXY_AUTHENTICATE = 8; - private final static int IDX_CONTENT_DISPOSITION = 9; - private final static int IDX_ACCEPT_RANGES = 10; - private final static int IDX_EXPIRES = 11; - private final static int IDX_CACHE_CONTROL = 12; - private final static int IDX_LAST_MODIFIED = 13; - private final static int IDX_ETAG = 14; - private final static int IDX_SET_COOKIE = 15; - private final static int IDX_PRAGMA = 16; - private final static int IDX_REFRESH = 17; - private final static int IDX_X_PERMITTED_CROSS_DOMAIN_POLICIES = 18; - - private final static int HEADER_COUNT = 19; - - /* parsed values */ - private long transferEncoding; - private long contentLength; // Content length of the incoming data - private int connectionType; - private ArrayList<String> cookies = new ArrayList<String>(2); - - private String[] mHeaders = new String[HEADER_COUNT]; - private final static String[] sHeaderNames = { - TRANSFER_ENCODING, - CONTENT_LEN, - CONTENT_TYPE, - CONTENT_ENCODING, - CONN_DIRECTIVE, - LOCATION, - PROXY_CONNECTION, - WWW_AUTHENTICATE, - PROXY_AUTHENTICATE, - CONTENT_DISPOSITION, - ACCEPT_RANGES, - EXPIRES, - CACHE_CONTROL, - LAST_MODIFIED, - ETAG, - SET_COOKIE, - PRAGMA, - REFRESH, - X_PERMITTED_CROSS_DOMAIN_POLICIES - }; - - // Catch-all for headers not explicitly handled - private ArrayList<String> mExtraHeaderNames = new ArrayList<String>(4); - private ArrayList<String> mExtraHeaderValues = new ArrayList<String>(4); - - public Headers() { - transferEncoding = NO_TRANSFER_ENCODING; - contentLength = NO_CONTENT_LENGTH; - connectionType = NO_CONN_TYPE; - } - - public void parseHeader(CharArrayBuffer buffer) { - int pos = setLowercaseIndexOf(buffer, ':'); - if (pos == -1) { - return; - } - String name = buffer.substringTrimmed(0, pos); - if (name.length() == 0) { - return; - } - pos++; - - String val = buffer.substringTrimmed(pos, buffer.length()); - if (HttpLog.LOGV) { - HttpLog.v("hdr " + buffer.length() + " " + buffer); - } - - switch (name.hashCode()) { - case HASH_TRANSFER_ENCODING: - if (name.equals(TRANSFER_ENCODING)) { - mHeaders[IDX_TRANSFER_ENCODING] = val; - HeaderElement[] encodings = BasicHeaderValueParser.DEFAULT - .parseElements(buffer, new ParserCursor(pos, - buffer.length())); - // The chunked encoding must be the last one applied RFC2616, - // 14.41 - int len = encodings.length; - if (HTTP.IDENTITY_CODING.equalsIgnoreCase(val)) { - transferEncoding = ContentLengthStrategy.IDENTITY; - } else if ((len > 0) - && (HTTP.CHUNK_CODING - .equalsIgnoreCase(encodings[len - 1].getName()))) { - transferEncoding = ContentLengthStrategy.CHUNKED; - } else { - transferEncoding = ContentLengthStrategy.IDENTITY; - } - } - break; - case HASH_CONTENT_LEN: - if (name.equals(CONTENT_LEN)) { - mHeaders[IDX_CONTENT_LEN] = val; - try { - contentLength = Long.parseLong(val); - } catch (NumberFormatException e) { - if (false) { - Log.v(LOGTAG, "Headers.headers(): error parsing" - + " content length: " + buffer.toString()); - } - } - } - break; - case HASH_CONTENT_TYPE: - if (name.equals(CONTENT_TYPE)) { - mHeaders[IDX_CONTENT_TYPE] = val; - } - break; - case HASH_CONTENT_ENCODING: - if (name.equals(CONTENT_ENCODING)) { - mHeaders[IDX_CONTENT_ENCODING] = val; - } - break; - case HASH_CONN_DIRECTIVE: - if (name.equals(CONN_DIRECTIVE)) { - mHeaders[IDX_CONN_DIRECTIVE] = val; - setConnectionType(buffer, pos); - } - break; - case HASH_LOCATION: - if (name.equals(LOCATION)) { - mHeaders[IDX_LOCATION] = val; - } - break; - case HASH_PROXY_CONNECTION: - if (name.equals(PROXY_CONNECTION)) { - mHeaders[IDX_PROXY_CONNECTION] = val; - setConnectionType(buffer, pos); - } - break; - case HASH_WWW_AUTHENTICATE: - if (name.equals(WWW_AUTHENTICATE)) { - mHeaders[IDX_WWW_AUTHENTICATE] = val; - } - break; - case HASH_PROXY_AUTHENTICATE: - if (name.equals(PROXY_AUTHENTICATE)) { - mHeaders[IDX_PROXY_AUTHENTICATE] = val; - } - break; - case HASH_CONTENT_DISPOSITION: - if (name.equals(CONTENT_DISPOSITION)) { - mHeaders[IDX_CONTENT_DISPOSITION] = val; - } - break; - case HASH_ACCEPT_RANGES: - if (name.equals(ACCEPT_RANGES)) { - mHeaders[IDX_ACCEPT_RANGES] = val; - } - break; - case HASH_EXPIRES: - if (name.equals(EXPIRES)) { - mHeaders[IDX_EXPIRES] = val; - } - break; - case HASH_CACHE_CONTROL: - if (name.equals(CACHE_CONTROL)) { - // In case where we receive more than one header, create a ',' separated list. - // This should be ok, according to RFC 2616 chapter 4.2 - if (mHeaders[IDX_CACHE_CONTROL] != null && - mHeaders[IDX_CACHE_CONTROL].length() > 0) { - mHeaders[IDX_CACHE_CONTROL] += (',' + val); - } else { - mHeaders[IDX_CACHE_CONTROL] = val; - } - } - break; - case HASH_LAST_MODIFIED: - if (name.equals(LAST_MODIFIED)) { - mHeaders[IDX_LAST_MODIFIED] = val; - } - break; - case HASH_ETAG: - if (name.equals(ETAG)) { - mHeaders[IDX_ETAG] = val; - } - break; - case HASH_SET_COOKIE: - if (name.equals(SET_COOKIE)) { - mHeaders[IDX_SET_COOKIE] = val; - cookies.add(val); - } - break; - case HASH_PRAGMA: - if (name.equals(PRAGMA)) { - mHeaders[IDX_PRAGMA] = val; - } - break; - case HASH_REFRESH: - if (name.equals(REFRESH)) { - mHeaders[IDX_REFRESH] = val; - } - break; - case HASH_X_PERMITTED_CROSS_DOMAIN_POLICIES: - if (name.equals(X_PERMITTED_CROSS_DOMAIN_POLICIES)) { - mHeaders[IDX_X_PERMITTED_CROSS_DOMAIN_POLICIES] = val; - } - break; - default: - mExtraHeaderNames.add(name); - mExtraHeaderValues.add(val); - } - } - - public long getTransferEncoding() { - return transferEncoding; - } - - public long getContentLength() { - return contentLength; - } - - public int getConnectionType() { - return connectionType; - } - - public String getContentType() { - return mHeaders[IDX_CONTENT_TYPE]; - } - - public String getContentEncoding() { - return mHeaders[IDX_CONTENT_ENCODING]; - } - - public String getLocation() { - return mHeaders[IDX_LOCATION]; - } - - public String getWwwAuthenticate() { - return mHeaders[IDX_WWW_AUTHENTICATE]; - } - - public String getProxyAuthenticate() { - return mHeaders[IDX_PROXY_AUTHENTICATE]; - } - - public String getContentDisposition() { - return mHeaders[IDX_CONTENT_DISPOSITION]; - } - - public String getAcceptRanges() { - return mHeaders[IDX_ACCEPT_RANGES]; - } - - public String getExpires() { - return mHeaders[IDX_EXPIRES]; - } - - public String getCacheControl() { - return mHeaders[IDX_CACHE_CONTROL]; - } - - public String getLastModified() { - return mHeaders[IDX_LAST_MODIFIED]; - } - - public String getEtag() { - return mHeaders[IDX_ETAG]; - } - - public ArrayList<String> getSetCookie() { - return this.cookies; - } - - public String getPragma() { - return mHeaders[IDX_PRAGMA]; - } - - public String getRefresh() { - return mHeaders[IDX_REFRESH]; - } - - public String getXPermittedCrossDomainPolicies() { - return mHeaders[IDX_X_PERMITTED_CROSS_DOMAIN_POLICIES]; - } - - public void setContentLength(long value) { - this.contentLength = value; - } - - public void setContentType(String value) { - mHeaders[IDX_CONTENT_TYPE] = value; - } - - public void setContentEncoding(String value) { - mHeaders[IDX_CONTENT_ENCODING] = value; - } - - public void setLocation(String value) { - mHeaders[IDX_LOCATION] = value; - } - - public void setWwwAuthenticate(String value) { - mHeaders[IDX_WWW_AUTHENTICATE] = value; - } - - public void setProxyAuthenticate(String value) { - mHeaders[IDX_PROXY_AUTHENTICATE] = value; - } - - public void setContentDisposition(String value) { - mHeaders[IDX_CONTENT_DISPOSITION] = value; - } - - public void setAcceptRanges(String value) { - mHeaders[IDX_ACCEPT_RANGES] = value; - } - - public void setExpires(String value) { - mHeaders[IDX_EXPIRES] = value; - } - - public void setCacheControl(String value) { - mHeaders[IDX_CACHE_CONTROL] = value; - } - - public void setLastModified(String value) { - mHeaders[IDX_LAST_MODIFIED] = value; - } - - public void setEtag(String value) { - mHeaders[IDX_ETAG] = value; - } - - public void setXPermittedCrossDomainPolicies(String value) { - mHeaders[IDX_X_PERMITTED_CROSS_DOMAIN_POLICIES] = value; - } - - public interface HeaderCallback { - public void header(String name, String value); - } - - /** - * Reports all non-null headers to the callback - */ - public void getHeaders(HeaderCallback hcb) { - for (int i = 0; i < HEADER_COUNT; i++) { - String h = mHeaders[i]; - if (h != null) { - hcb.header(sHeaderNames[i], h); - } - } - int extraLen = mExtraHeaderNames.size(); - for (int i = 0; i < extraLen; i++) { - if (false) { - HttpLog.v("Headers.getHeaders() extra: " + i + " " + - mExtraHeaderNames.get(i) + " " + mExtraHeaderValues.get(i)); - } - hcb.header(mExtraHeaderNames.get(i), - mExtraHeaderValues.get(i)); - } - - } - - private void setConnectionType(CharArrayBuffer buffer, int pos) { - if (containsIgnoreCaseTrimmed(buffer, pos, HTTP.CONN_CLOSE)) { - connectionType = CONN_CLOSE; - } else if (containsIgnoreCaseTrimmed( - buffer, pos, HTTP.CONN_KEEP_ALIVE)) { - connectionType = CONN_KEEP_ALIVE; - } - } - - - /** - * Returns true if the buffer contains the given string. Ignores leading - * whitespace and case. - * - * @param buffer to search - * @param beginIndex index at which we should start - * @param str to search for - */ - static boolean containsIgnoreCaseTrimmed(CharArrayBuffer buffer, - int beginIndex, final String str) { - int len = buffer.length(); - char[] chars = buffer.buffer(); - while (beginIndex < len && HTTP.isWhitespace(chars[beginIndex])) { - beginIndex++; - } - int size = str.length(); - boolean ok = len >= (beginIndex + size); - for (int j=0; ok && (j < size); j++) { - char a = chars[beginIndex + j]; - char b = str.charAt(j); - if (a != b) { - a = Character.toLowerCase(a); - b = Character.toLowerCase(b); - ok = a == b; - } - } - - return true; - } - - /** - * Returns index of first occurence ch. Lower cases characters leading up - * to first occurrence of ch. - */ - static int setLowercaseIndexOf(CharArrayBuffer buffer, final int ch) { - - int beginIndex = 0; - int endIndex = buffer.length(); - char[] chars = buffer.buffer(); - - for (int i = beginIndex; i < endIndex; i++) { - char current = chars[i]; - if (current == ch) { - return i; - } else { - chars[i] = Character.toLowerCase(current); - } - } - return -1; - } -} diff --git a/core/java/android/net/http/HttpAuthHeader.java b/core/java/android/net/http/HttpAuthHeader.java deleted file mode 100644 index 3abac23..0000000 --- a/core/java/android/net/http/HttpAuthHeader.java +++ /dev/null @@ -1,424 +0,0 @@ -/* - * Copyright (C) 2007 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.net.http; - -import java.util.Locale; - -/** - * HttpAuthHeader: a class to store HTTP authentication-header parameters. - * For more information, see: RFC 2617: HTTP Authentication. - * - * {@hide} - */ -public class HttpAuthHeader { - /** - * Possible HTTP-authentication header tokens to search for: - */ - public final static String BASIC_TOKEN = "Basic"; - public final static String DIGEST_TOKEN = "Digest"; - - private final static String REALM_TOKEN = "realm"; - private final static String NONCE_TOKEN = "nonce"; - private final static String STALE_TOKEN = "stale"; - private final static String OPAQUE_TOKEN = "opaque"; - private final static String QOP_TOKEN = "qop"; - private final static String ALGORITHM_TOKEN = "algorithm"; - - /** - * An authentication scheme. We currently support two different schemes: - * HttpAuthHeader.BASIC - basic, and - * HttpAuthHeader.DIGEST - digest (algorithm=MD5, QOP="auth"). - */ - private int mScheme; - - public static final int UNKNOWN = 0; - public static final int BASIC = 1; - public static final int DIGEST = 2; - - /** - * A flag, indicating that the previous request from the client was - * rejected because the nonce value was stale. If stale is TRUE - * (case-insensitive), the client may wish to simply retry the request - * with a new encrypted response, without reprompting the user for a - * new username and password. - */ - private boolean mStale; - - /** - * A string to be displayed to users so they know which username and - * password to use. - */ - private String mRealm; - - /** - * A server-specified data string which should be uniquely generated - * each time a 401 response is made. - */ - private String mNonce; - - /** - * A string of data, specified by the server, which should be returned - * by the client unchanged in the Authorization header of subsequent - * requests with URIs in the same protection space. - */ - private String mOpaque; - - /** - * This directive is optional, but is made so only for backward - * compatibility with RFC 2069 [6]; it SHOULD be used by all - * implementations compliant with this version of the Digest scheme. - * If present, it is a quoted string of one or more tokens indicating - * the "quality of protection" values supported by the server. The - * value "auth" indicates authentication; the value "auth-int" - * indicates authentication with integrity protection. - */ - private String mQop; - - /** - * A string indicating a pair of algorithms used to produce the digest - * and a checksum. If this is not present it is assumed to be "MD5". - */ - private String mAlgorithm; - - /** - * Is this authentication request a proxy authentication request? - */ - private boolean mIsProxy; - - /** - * Username string we get from the user. - */ - private String mUsername; - - /** - * Password string we get from the user. - */ - private String mPassword; - - /** - * Creates a new HTTP-authentication header object from the - * input header string. - * The header string is assumed to contain parameters of at - * most one authentication-scheme (ensured by the caller). - */ - public HttpAuthHeader(String header) { - if (header != null) { - parseHeader(header); - } - } - - /** - * @return True iff this is a proxy authentication header. - */ - public boolean isProxy() { - return mIsProxy; - } - - /** - * Marks this header as a proxy authentication header. - */ - public void setProxy() { - mIsProxy = true; - } - - /** - * @return The username string. - */ - public String getUsername() { - return mUsername; - } - - /** - * Sets the username string. - */ - public void setUsername(String username) { - mUsername = username; - } - - /** - * @return The password string. - */ - public String getPassword() { - return mPassword; - } - - /** - * Sets the password string. - */ - public void setPassword(String password) { - mPassword = password; - } - - /** - * @return True iff this is the BASIC-authentication request. - */ - public boolean isBasic () { - return mScheme == BASIC; - } - - /** - * @return True iff this is the DIGEST-authentication request. - */ - public boolean isDigest() { - return mScheme == DIGEST; - } - - /** - * @return The authentication scheme requested. We currently - * support two schemes: - * HttpAuthHeader.BASIC - basic, and - * HttpAuthHeader.DIGEST - digest (algorithm=MD5, QOP="auth"). - */ - public int getScheme() { - return mScheme; - } - - /** - * @return True if indicating that the previous request from - * the client was rejected because the nonce value was stale. - */ - public boolean getStale() { - return mStale; - } - - /** - * @return The realm value or null if there is none. - */ - public String getRealm() { - return mRealm; - } - - /** - * @return The nonce value or null if there is none. - */ - public String getNonce() { - return mNonce; - } - - /** - * @return The opaque value or null if there is none. - */ - public String getOpaque() { - return mOpaque; - } - - /** - * @return The QOP ("quality-of_protection") value or null if - * there is none. The QOP value is always lower-case. - */ - public String getQop() { - return mQop; - } - - /** - * @return The name of the algorithm used or null if there is - * none. By default, MD5 is used. - */ - public String getAlgorithm() { - return mAlgorithm; - } - - /** - * @return True iff the authentication scheme requested by the - * server is supported; currently supported schemes: - * BASIC, - * DIGEST (only algorithm="md5", no qop or qop="auth). - */ - public boolean isSupportedScheme() { - // it is a good idea to enforce non-null realms! - if (mRealm != null) { - if (mScheme == BASIC) { - return true; - } else { - if (mScheme == DIGEST) { - return - mAlgorithm.equals("md5") && - (mQop == null || mQop.equals("auth")); - } - } - } - - return false; - } - - /** - * Parses the header scheme name and then scheme parameters if - * the scheme is supported. - */ - private void parseHeader(String header) { - if (HttpLog.LOGV) { - HttpLog.v("HttpAuthHeader.parseHeader(): header: " + header); - } - - if (header != null) { - String parameters = parseScheme(header); - if (parameters != null) { - // if we have a supported scheme - if (mScheme != UNKNOWN) { - parseParameters(parameters); - } - } - } - } - - /** - * Parses the authentication scheme name. If we have a Digest - * scheme, sets the algorithm value to the default of MD5. - * @return The authentication scheme parameters string to be - * parsed later (if the scheme is supported) or null if failed - * to parse the scheme (the header value is null?). - */ - private String parseScheme(String header) { - if (header != null) { - int i = header.indexOf(' '); - if (i >= 0) { - String scheme = header.substring(0, i).trim(); - if (scheme.equalsIgnoreCase(DIGEST_TOKEN)) { - mScheme = DIGEST; - - // md5 is the default algorithm!!! - mAlgorithm = "md5"; - } else { - if (scheme.equalsIgnoreCase(BASIC_TOKEN)) { - mScheme = BASIC; - } - } - - return header.substring(i + 1); - } - } - - return null; - } - - /** - * Parses a comma-separated list of authentification scheme - * parameters. - */ - private void parseParameters(String parameters) { - if (HttpLog.LOGV) { - HttpLog.v("HttpAuthHeader.parseParameters():" + - " parameters: " + parameters); - } - - if (parameters != null) { - int i; - do { - i = parameters.indexOf(','); - if (i < 0) { - // have only one parameter - parseParameter(parameters); - } else { - parseParameter(parameters.substring(0, i)); - parameters = parameters.substring(i + 1); - } - } while (i >= 0); - } - } - - /** - * Parses a single authentication scheme parameter. The parameter - * string is expected to follow the format: PARAMETER=VALUE. - */ - private void parseParameter(String parameter) { - if (parameter != null) { - // here, we are looking for the 1st occurence of '=' only!!! - int i = parameter.indexOf('='); - if (i >= 0) { - String token = parameter.substring(0, i).trim(); - String value = - trimDoubleQuotesIfAny(parameter.substring(i + 1).trim()); - - if (HttpLog.LOGV) { - HttpLog.v("HttpAuthHeader.parseParameter():" + - " token: " + token + - " value: " + value); - } - - if (token.equalsIgnoreCase(REALM_TOKEN)) { - mRealm = value; - } else { - if (mScheme == DIGEST) { - parseParameter(token, value); - } - } - } - } - } - - /** - * If the token is a known parameter name, parses and initializes - * the token value. - */ - private void parseParameter(String token, String value) { - if (token != null && value != null) { - if (token.equalsIgnoreCase(NONCE_TOKEN)) { - mNonce = value; - return; - } - - if (token.equalsIgnoreCase(STALE_TOKEN)) { - parseStale(value); - return; - } - - if (token.equalsIgnoreCase(OPAQUE_TOKEN)) { - mOpaque = value; - return; - } - - if (token.equalsIgnoreCase(QOP_TOKEN)) { - mQop = value.toLowerCase(Locale.ROOT); - return; - } - - if (token.equalsIgnoreCase(ALGORITHM_TOKEN)) { - mAlgorithm = value.toLowerCase(Locale.ROOT); - return; - } - } - } - - /** - * Parses and initializes the 'stale' paramer value. Any value - * different from case-insensitive "true" is considered "false". - */ - private void parseStale(String value) { - if (value != null) { - if (value.equalsIgnoreCase("true")) { - mStale = true; - } - } - } - - /** - * Trims double-quotes around a parameter value if there are any. - * @return The string value without the outermost pair of double- - * quotes or null if the original value is null. - */ - static private String trimDoubleQuotesIfAny(String value) { - if (value != null) { - int len = value.length(); - if (len > 2 && - value.charAt(0) == '\"' && value.charAt(len - 1) == '\"') { - return value.substring(1, len - 1); - } - } - - return value; - } -} diff --git a/core/java/android/net/http/HttpConnection.java b/core/java/android/net/http/HttpConnection.java deleted file mode 100644 index edf8fed3..0000000 --- a/core/java/android/net/http/HttpConnection.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2007 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.net.http; - -import android.content.Context; - -import java.net.Socket; -import java.io.IOException; - -import org.apache.http.HttpHost; -import org.apache.http.params.BasicHttpParams; -import org.apache.http.params.HttpConnectionParams; - -/** - * A requestConnection connecting to a normal (non secure) http server - * - * {@hide} - */ -class HttpConnection extends Connection { - - HttpConnection(Context context, HttpHost host, - RequestFeeder requestFeeder) { - super(context, host, requestFeeder); - } - - /** - * Opens the connection to a http server - * - * @return the opened low level connection - * @throws IOException if the connection fails for any reason. - */ - @Override - AndroidHttpClientConnection openConnection(Request req) throws IOException { - - // Update the certificate info (connection not secure - set to null) - EventHandler eventHandler = req.getEventHandler(); - mCertificate = null; - eventHandler.certificate(mCertificate); - - AndroidHttpClientConnection conn = new AndroidHttpClientConnection(); - BasicHttpParams params = new BasicHttpParams(); - Socket sock = new Socket(mHost.getHostName(), mHost.getPort()); - params.setIntParameter(HttpConnectionParams.SOCKET_BUFFER_SIZE, 8192); - conn.bind(sock, params); - return conn; - } - - /** - * Closes the low level connection. - * - * If an exception is thrown then it is assumed that the - * connection will have been closed (to the extent possible) - * anyway and the caller does not need to take any further action. - * - */ - void closeConnection() { - try { - if (mHttpClientConnection != null && mHttpClientConnection.isOpen()) { - mHttpClientConnection.close(); - } - } catch (IOException e) { - if (HttpLog.LOGV) HttpLog.v( - "closeConnection(): failed closing connection " + - mHost); - e.printStackTrace(); - } - } - - /** - * Restart a secure connection suspended waiting for user interaction. - */ - void restartConnection(boolean abort) { - // not required for plain http connections - } - - String getScheme() { - return "http"; - } -} diff --git a/core/java/android/net/http/HttpResponseCache.java b/core/java/android/net/http/HttpResponseCache.java index c6c22e7..188287f 100644 --- a/core/java/android/net/http/HttpResponseCache.java +++ b/core/java/android/net/http/HttpResponseCache.java @@ -35,8 +35,8 @@ import java.util.Map; * Caches HTTP and HTTPS responses to the filesystem so they may be reused, * saving time and bandwidth. This class supports {@link * java.net.HttpURLConnection} and {@link javax.net.ssl.HttpsURLConnection}; - * there is no platform-provided cache for {@link - * org.apache.http.impl.client.DefaultHttpClient} or {@link AndroidHttpClient}. + * there is no platform-provided cache for {@code DefaultHttpClient} or + * {@code AndroidHttpClient}. * * <h3>Installing an HTTP response cache</h3> * Enable caching of all of your application's HTTP requests by installing the diff --git a/core/java/android/net/http/HttpsConnection.java b/core/java/android/net/http/HttpsConnection.java deleted file mode 100644 index a8674de..0000000 --- a/core/java/android/net/http/HttpsConnection.java +++ /dev/null @@ -1,433 +0,0 @@ -/* - * Copyright (C) 2007 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.net.http; - -import android.content.Context; -import android.util.Log; -import com.android.org.conscrypt.FileClientSessionCache; -import com.android.org.conscrypt.OpenSSLContextImpl; -import com.android.org.conscrypt.SSLClientSessionCache; -import org.apache.http.Header; -import org.apache.http.HttpException; -import org.apache.http.HttpHost; -import org.apache.http.HttpStatus; -import org.apache.http.ParseException; -import org.apache.http.ProtocolVersion; -import org.apache.http.StatusLine; -import org.apache.http.message.BasicHttpRequest; -import org.apache.http.params.BasicHttpParams; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; - -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLSocket; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; -import java.io.File; -import java.io.IOException; -import java.net.Socket; -import java.security.KeyManagementException; -import java.security.cert.X509Certificate; -import java.util.Locale; - -/** - * A Connection connecting to a secure http server or tunneling through - * a http proxy server to a https server. - * - * @hide - */ -public class HttpsConnection extends Connection { - - /** - * SSL socket factory - */ - private static SSLSocketFactory mSslSocketFactory = null; - - static { - // This initialization happens in the zygote. It triggers some - // lazy initialization that can will benefit later invocations of - // initializeEngine(). - initializeEngine(null); - } - - /** - * @hide - * - * @param sessionDir directory to cache SSL sessions - */ - public static void initializeEngine(File sessionDir) { - try { - SSLClientSessionCache cache = null; - if (sessionDir != null) { - Log.d("HttpsConnection", "Caching SSL sessions in " - + sessionDir + "."); - cache = FileClientSessionCache.usingDirectory(sessionDir); - } - - OpenSSLContextImpl sslContext = OpenSSLContextImpl.getPreferred(); - - // here, trust managers is a single trust-all manager - TrustManager[] trustManagers = new TrustManager[] { - new X509TrustManager() { - public X509Certificate[] getAcceptedIssuers() { - return null; - } - - public void checkClientTrusted( - X509Certificate[] certs, String authType) { - } - - public void checkServerTrusted( - X509Certificate[] certs, String authType) { - } - } - }; - - sslContext.engineInit(null, trustManagers, null); - sslContext.engineGetClientSessionContext().setPersistentCache(cache); - - synchronized (HttpsConnection.class) { - mSslSocketFactory = sslContext.engineGetSocketFactory(); - } - } catch (KeyManagementException e) { - throw new RuntimeException(e); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private synchronized static SSLSocketFactory getSocketFactory() { - return mSslSocketFactory; - } - - /** - * Object to wait on when suspending the SSL connection - */ - private Object mSuspendLock = new Object(); - - /** - * True if the connection is suspended pending the result of asking the - * user about an error. - */ - private boolean mSuspended = false; - - /** - * True if the connection attempt should be aborted due to an ssl - * error. - */ - private boolean mAborted = false; - - // Used when connecting through a proxy. - private HttpHost mProxyHost; - - /** - * Contructor for a https connection. - */ - HttpsConnection(Context context, HttpHost host, HttpHost proxy, - RequestFeeder requestFeeder) { - super(context, host, requestFeeder); - mProxyHost = proxy; - } - - /** - * Sets the server SSL certificate associated with this - * connection. - * @param certificate The SSL certificate - */ - /* package */ void setCertificate(SslCertificate certificate) { - mCertificate = certificate; - } - - /** - * Opens the connection to a http server or proxy. - * - * @return the opened low level connection - * @throws IOException if the connection fails for any reason. - */ - @Override - AndroidHttpClientConnection openConnection(Request req) throws IOException { - SSLSocket sslSock = null; - - if (mProxyHost != null) { - // If we have a proxy set, we first send a CONNECT request - // to the proxy; if the proxy returns 200 OK, we negotiate - // a secure connection to the target server via the proxy. - // If the request fails, we drop it, but provide the event - // handler with the response status and headers. The event - // handler is then responsible for cancelling the load or - // issueing a new request. - AndroidHttpClientConnection proxyConnection = null; - Socket proxySock = null; - try { - proxySock = new Socket - (mProxyHost.getHostName(), mProxyHost.getPort()); - - proxySock.setSoTimeout(60 * 1000); - - proxyConnection = new AndroidHttpClientConnection(); - HttpParams params = new BasicHttpParams(); - HttpConnectionParams.setSocketBufferSize(params, 8192); - - proxyConnection.bind(proxySock, params); - } catch(IOException e) { - if (proxyConnection != null) { - proxyConnection.close(); - } - - String errorMessage = e.getMessage(); - if (errorMessage == null) { - errorMessage = - "failed to establish a connection to the proxy"; - } - - throw new IOException(errorMessage); - } - - StatusLine statusLine = null; - int statusCode = 0; - Headers headers = new Headers(); - try { - BasicHttpRequest proxyReq = new BasicHttpRequest - ("CONNECT", mHost.toHostString()); - - // add all 'proxy' headers from the original request, we also need - // to add 'host' header unless we want proxy to answer us with a - // 400 Bad Request - for (Header h : req.mHttpRequest.getAllHeaders()) { - String headerName = h.getName().toLowerCase(Locale.ROOT); - if (headerName.startsWith("proxy") || headerName.equals("keep-alive") - || headerName.equals("host")) { - proxyReq.addHeader(h); - } - } - - proxyConnection.sendRequestHeader(proxyReq); - proxyConnection.flush(); - - // it is possible to receive informational status - // codes prior to receiving actual headers; - // all those status codes are smaller than OK 200 - // a loop is a standard way of dealing with them - do { - statusLine = proxyConnection.parseResponseHeader(headers); - statusCode = statusLine.getStatusCode(); - } while (statusCode < HttpStatus.SC_OK); - } catch (ParseException e) { - String errorMessage = e.getMessage(); - if (errorMessage == null) { - errorMessage = - "failed to send a CONNECT request"; - } - - throw new IOException(errorMessage); - } catch (HttpException e) { - String errorMessage = e.getMessage(); - if (errorMessage == null) { - errorMessage = - "failed to send a CONNECT request"; - } - - throw new IOException(errorMessage); - } catch (IOException e) { - String errorMessage = e.getMessage(); - if (errorMessage == null) { - errorMessage = - "failed to send a CONNECT request"; - } - - throw new IOException(errorMessage); - } - - if (statusCode == HttpStatus.SC_OK) { - try { - sslSock = (SSLSocket) getSocketFactory().createSocket( - proxySock, mHost.getHostName(), mHost.getPort(), true); - } catch(IOException e) { - if (sslSock != null) { - sslSock.close(); - } - - String errorMessage = e.getMessage(); - if (errorMessage == null) { - errorMessage = - "failed to create an SSL socket"; - } - throw new IOException(errorMessage); - } - } else { - // if the code is not OK, inform the event handler - ProtocolVersion version = statusLine.getProtocolVersion(); - - req.mEventHandler.status(version.getMajor(), - version.getMinor(), - statusCode, - statusLine.getReasonPhrase()); - req.mEventHandler.headers(headers); - req.mEventHandler.endData(); - - proxyConnection.close(); - - // here, we return null to indicate that the original - // request needs to be dropped - return null; - } - } else { - // if we do not have a proxy, we simply connect to the host - try { - sslSock = (SSLSocket) getSocketFactory().createSocket( - mHost.getHostName(), mHost.getPort()); - sslSock.setSoTimeout(SOCKET_TIMEOUT); - } catch(IOException e) { - if (sslSock != null) { - sslSock.close(); - } - - String errorMessage = e.getMessage(); - if (errorMessage == null) { - errorMessage = "failed to create an SSL socket"; - } - - throw new IOException(errorMessage); - } - } - - // do handshake and validate server certificates - SslError error = CertificateChainValidator.getInstance(). - doHandshakeAndValidateServerCertificates(this, sslSock, mHost.getHostName()); - - // Inform the user if there is a problem - if (error != null) { - // handleSslErrorRequest may immediately unsuspend if it wants to - // allow the certificate anyway. - // So we mark the connection as suspended, call handleSslErrorRequest - // then check if we're still suspended and only wait if we actually - // need to. - synchronized (mSuspendLock) { - mSuspended = true; - } - // don't hold the lock while calling out to the event handler - boolean canHandle = req.getEventHandler().handleSslErrorRequest(error); - if(!canHandle) { - throw new IOException("failed to handle "+ error); - } - synchronized (mSuspendLock) { - if (mSuspended) { - try { - // Put a limit on how long we are waiting; if the timeout - // expires (which should never happen unless you choose - // to ignore the SSL error dialog for a very long time), - // we wake up the thread and abort the request. This is - // to prevent us from stalling the network if things go - // very bad. - mSuspendLock.wait(10 * 60 * 1000); - if (mSuspended) { - // mSuspended is true if we have not had a chance to - // restart the connection yet (ie, the wait timeout - // has expired) - mSuspended = false; - mAborted = true; - if (HttpLog.LOGV) { - HttpLog.v("HttpsConnection.openConnection():" + - " SSL timeout expired and request was cancelled!!!"); - } - } - } catch (InterruptedException e) { - // ignore - } - } - if (mAborted) { - // The user decided not to use this unverified connection - // so close it immediately. - sslSock.close(); - throw new SSLConnectionClosedByUserException("connection closed by the user"); - } - } - } - - // All went well, we have an open, verified connection. - AndroidHttpClientConnection conn = new AndroidHttpClientConnection(); - BasicHttpParams params = new BasicHttpParams(); - params.setIntParameter(HttpConnectionParams.SOCKET_BUFFER_SIZE, 8192); - conn.bind(sslSock, params); - - return conn; - } - - /** - * Closes the low level connection. - * - * If an exception is thrown then it is assumed that the connection will - * have been closed (to the extent possible) anyway and the caller does not - * need to take any further action. - * - */ - @Override - void closeConnection() { - // if the connection has been suspended due to an SSL error - if (mSuspended) { - // wake up the network thread - restartConnection(false); - } - - try { - if (mHttpClientConnection != null && mHttpClientConnection.isOpen()) { - mHttpClientConnection.close(); - } - } catch (IOException e) { - if (HttpLog.LOGV) - HttpLog.v("HttpsConnection.closeConnection():" + - " failed closing connection " + mHost); - e.printStackTrace(); - } - } - - /** - * Restart a secure connection suspended waiting for user interaction. - */ - void restartConnection(boolean proceed) { - if (HttpLog.LOGV) { - HttpLog.v("HttpsConnection.restartConnection():" + - " proceed: " + proceed); - } - - synchronized (mSuspendLock) { - if (mSuspended) { - mSuspended = false; - mAborted = !proceed; - mSuspendLock.notify(); - } - } - } - - @Override - String getScheme() { - return "https"; - } -} - -/** - * Simple exception we throw if the SSL connection is closed by the user. - * - * {@hide} - */ -class SSLConnectionClosedByUserException extends SSLException { - - public SSLConnectionClosedByUserException(String reason) { - super(reason); - } -} diff --git a/core/java/android/net/http/IdleCache.java b/core/java/android/net/http/IdleCache.java deleted file mode 100644 index fda6009..0000000 --- a/core/java/android/net/http/IdleCache.java +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright (C) 2008 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. - */ - -/** - * Hangs onto idle live connections for a little while - */ - -package android.net.http; - -import org.apache.http.HttpHost; - -import android.os.SystemClock; - -/** - * {@hide} - */ -class IdleCache { - - class Entry { - HttpHost mHost; - Connection mConnection; - long mTimeout; - }; - - private final static int IDLE_CACHE_MAX = 8; - - /* Allow five consecutive empty queue checks before shutdown */ - private final static int EMPTY_CHECK_MAX = 5; - - /* six second timeout for connections */ - private final static int TIMEOUT = 6 * 1000; - private final static int CHECK_INTERVAL = 2 * 1000; - private Entry[] mEntries = new Entry[IDLE_CACHE_MAX]; - - private int mCount = 0; - - private IdleReaper mThread = null; - - /* stats */ - private int mCached = 0; - private int mReused = 0; - - IdleCache() { - for (int i = 0; i < IDLE_CACHE_MAX; i++) { - mEntries[i] = new Entry(); - } - } - - /** - * Caches connection, if there is room. - * @return true if connection cached - */ - synchronized boolean cacheConnection( - HttpHost host, Connection connection) { - - boolean ret = false; - - if (HttpLog.LOGV) { - HttpLog.v("IdleCache size " + mCount + " host " + host); - } - - if (mCount < IDLE_CACHE_MAX) { - long time = SystemClock.uptimeMillis(); - for (int i = 0; i < IDLE_CACHE_MAX; i++) { - Entry entry = mEntries[i]; - if (entry.mHost == null) { - entry.mHost = host; - entry.mConnection = connection; - entry.mTimeout = time + TIMEOUT; - mCount++; - if (HttpLog.LOGV) mCached++; - ret = true; - if (mThread == null) { - mThread = new IdleReaper(); - mThread.start(); - } - break; - } - } - } - return ret; - } - - synchronized Connection getConnection(HttpHost host) { - Connection ret = null; - - if (mCount > 0) { - for (int i = 0; i < IDLE_CACHE_MAX; i++) { - Entry entry = mEntries[i]; - HttpHost eHost = entry.mHost; - if (eHost != null && eHost.equals(host)) { - ret = entry.mConnection; - entry.mHost = null; - entry.mConnection = null; - mCount--; - if (HttpLog.LOGV) mReused++; - break; - } - } - } - return ret; - } - - synchronized void clear() { - for (int i = 0; mCount > 0 && i < IDLE_CACHE_MAX; i++) { - Entry entry = mEntries[i]; - if (entry.mHost != null) { - entry.mHost = null; - entry.mConnection.closeConnection(); - entry.mConnection = null; - mCount--; - } - } - } - - private synchronized void clearIdle() { - if (mCount > 0) { - long time = SystemClock.uptimeMillis(); - for (int i = 0; i < IDLE_CACHE_MAX; i++) { - Entry entry = mEntries[i]; - if (entry.mHost != null && time > entry.mTimeout) { - entry.mHost = null; - entry.mConnection.closeConnection(); - entry.mConnection = null; - mCount--; - } - } - } - } - - private class IdleReaper extends Thread { - - public void run() { - int check = 0; - - setName("IdleReaper"); - android.os.Process.setThreadPriority( - android.os.Process.THREAD_PRIORITY_BACKGROUND); - synchronized (IdleCache.this) { - while (check < EMPTY_CHECK_MAX) { - try { - IdleCache.this.wait(CHECK_INTERVAL); - } catch (InterruptedException ex) { - } - if (mCount == 0) { - check++; - } else { - check = 0; - clearIdle(); - } - } - mThread = null; - } - if (HttpLog.LOGV) { - HttpLog.v("IdleCache IdleReaper shutdown: cached " + mCached + - " reused " + mReused); - mCached = 0; - mReused = 0; - } - } - } -} diff --git a/core/java/android/net/http/LoggingEventHandler.java b/core/java/android/net/http/LoggingEventHandler.java deleted file mode 100644 index bdafa0b..0000000 --- a/core/java/android/net/http/LoggingEventHandler.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (C) 2006 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. - */ - -/** - * A test EventHandler: Logs everything received - */ - -package android.net.http; - -import android.net.http.Headers; - -/** - * {@hide} - */ -public class LoggingEventHandler implements EventHandler { - - public void requestSent() { - HttpLog.v("LoggingEventHandler:requestSent()"); - } - - public void status(int major_version, - int minor_version, - int code, /* Status-Code value */ - String reason_phrase) { - if (HttpLog.LOGV) { - HttpLog.v("LoggingEventHandler:status() major: " + major_version + - " minor: " + minor_version + - " code: " + code + - " reason: " + reason_phrase); - } - } - - public void headers(Headers headers) { - if (HttpLog.LOGV) { - HttpLog.v("LoggingEventHandler:headers()"); - HttpLog.v(headers.toString()); - } - } - - public void locationChanged(String newLocation, boolean permanent) { - if (HttpLog.LOGV) { - HttpLog.v("LoggingEventHandler: locationChanged() " + newLocation + - " permanent " + permanent); - } - } - - public void data(byte[] data, int len) { - if (HttpLog.LOGV) { - HttpLog.v("LoggingEventHandler: data() " + len + " bytes"); - } - // HttpLog.v(new String(data, 0, len)); - } - public void endData() { - if (HttpLog.LOGV) { - HttpLog.v("LoggingEventHandler: endData() called"); - } - } - - public void certificate(SslCertificate certificate) { - if (HttpLog.LOGV) { - HttpLog.v("LoggingEventHandler: certificate(): " + certificate); - } - } - - public void error(int id, String description) { - if (HttpLog.LOGV) { - HttpLog.v("LoggingEventHandler: error() called Id:" + id + - " description " + description); - } - } - - public boolean handleSslErrorRequest(SslError error) { - if (HttpLog.LOGV) { - HttpLog.v("LoggingEventHandler: handleSslErrorRequest():" + error); - } - // return false so that the caller thread won't wait forever - return false; - } -} diff --git a/core/java/android/net/http/Request.java b/core/java/android/net/http/Request.java deleted file mode 100644 index 76d7bb9..0000000 --- a/core/java/android/net/http/Request.java +++ /dev/null @@ -1,526 +0,0 @@ -/* - * Copyright (C) 2006 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.net.http; - -import java.io.EOFException; -import java.io.InputStream; -import java.io.IOException; -import java.util.Iterator; -import java.util.Map; -import java.util.Map.Entry; -import java.util.zip.GZIPInputStream; - -import org.apache.http.entity.InputStreamEntity; -import org.apache.http.Header; -import org.apache.http.HttpEntity; -import org.apache.http.HttpEntityEnclosingRequest; -import org.apache.http.HttpException; -import org.apache.http.HttpHost; -import org.apache.http.HttpRequest; -import org.apache.http.HttpStatus; -import org.apache.http.ParseException; -import org.apache.http.ProtocolVersion; - -import org.apache.http.StatusLine; -import org.apache.http.message.BasicHttpRequest; -import org.apache.http.message.BasicHttpEntityEnclosingRequest; -import org.apache.http.protocol.RequestContent; - -/** - * Represents an HTTP request for a given host. - * - * {@hide} - */ - -class Request { - - /** The eventhandler to call as the request progresses */ - EventHandler mEventHandler; - - private Connection mConnection; - - /** The Apache http request */ - BasicHttpRequest mHttpRequest; - - /** The path component of this request */ - String mPath; - - /** Host serving this request */ - HttpHost mHost; - - /** Set if I'm using a proxy server */ - HttpHost mProxyHost; - - /** True if request has been cancelled */ - volatile boolean mCancelled = false; - - int mFailCount = 0; - - // This will be used to set the Range field if we retry a connection. This - // is http/1.1 feature. - private int mReceivedBytes = 0; - - private InputStream mBodyProvider; - private int mBodyLength; - - private final static String HOST_HEADER = "Host"; - private final static String ACCEPT_ENCODING_HEADER = "Accept-Encoding"; - private final static String CONTENT_LENGTH_HEADER = "content-length"; - - /* Used to synchronize waitUntilComplete() requests */ - private final Object mClientResource = new Object(); - - /** True if loading should be paused **/ - private boolean mLoadingPaused = false; - - /** - * Processor used to set content-length and transfer-encoding - * headers. - */ - private static RequestContent requestContentProcessor = - new RequestContent(); - - /** - * Instantiates a new Request. - * @param method GET/POST/PUT - * @param host The server that will handle this request - * @param path path part of URI - * @param bodyProvider InputStream providing HTTP body, null if none - * @param bodyLength length of body, must be 0 if bodyProvider is null - * @param eventHandler request will make progress callbacks on - * this interface - * @param headers reqeust headers - */ - Request(String method, HttpHost host, HttpHost proxyHost, String path, - InputStream bodyProvider, int bodyLength, - EventHandler eventHandler, - Map<String, String> headers) { - mEventHandler = eventHandler; - mHost = host; - mProxyHost = proxyHost; - mPath = path; - mBodyProvider = bodyProvider; - mBodyLength = bodyLength; - - if (bodyProvider == null && !"POST".equalsIgnoreCase(method)) { - mHttpRequest = new BasicHttpRequest(method, getUri()); - } else { - mHttpRequest = new BasicHttpEntityEnclosingRequest( - method, getUri()); - // it is ok to have null entity for BasicHttpEntityEnclosingRequest. - // By using BasicHttpEntityEnclosingRequest, it will set up the - // correct content-length, content-type and content-encoding. - if (bodyProvider != null) { - setBodyProvider(bodyProvider, bodyLength); - } - } - addHeader(HOST_HEADER, getHostPort()); - - /* FIXME: if webcore will make the root document a - high-priority request, we can ask for gzip encoding only on - high priority reqs (saving the trouble for images, etc) */ - addHeader(ACCEPT_ENCODING_HEADER, "gzip"); - addHeaders(headers); - } - - /** - * @param pause True if the load should be paused. - */ - synchronized void setLoadingPaused(boolean pause) { - mLoadingPaused = pause; - - // Wake up the paused thread if we're unpausing the load. - if (!mLoadingPaused) { - notify(); - } - } - - /** - * @param connection Request served by this connection - */ - void setConnection(Connection connection) { - mConnection = connection; - } - - /* package */ EventHandler getEventHandler() { - return mEventHandler; - } - - /** - * Add header represented by given pair to request. Header will - * be formatted in request as "name: value\r\n". - * @param name of header - * @param value of header - */ - void addHeader(String name, String value) { - if (name == null) { - String damage = "Null http header name"; - HttpLog.e(damage); - throw new NullPointerException(damage); - } - if (value == null || value.length() == 0) { - String damage = "Null or empty value for header \"" + name + "\""; - HttpLog.e(damage); - throw new RuntimeException(damage); - } - mHttpRequest.addHeader(name, value); - } - - /** - * Add all headers in given map to this request. This is a helper - * method: it calls addHeader for each pair in the map. - */ - void addHeaders(Map<String, String> headers) { - if (headers == null) { - return; - } - - Entry<String, String> entry; - Iterator<Entry<String, String>> i = headers.entrySet().iterator(); - while (i.hasNext()) { - entry = i.next(); - addHeader(entry.getKey(), entry.getValue()); - } - } - - /** - * Send the request line and headers - */ - void sendRequest(AndroidHttpClientConnection httpClientConnection) - throws HttpException, IOException { - - if (mCancelled) return; // don't send cancelled requests - - if (HttpLog.LOGV) { - HttpLog.v("Request.sendRequest() " + mHost.getSchemeName() + "://" + getHostPort()); - // HttpLog.v(mHttpRequest.getRequestLine().toString()); - if (false) { - Iterator i = mHttpRequest.headerIterator(); - while (i.hasNext()) { - Header header = (Header)i.next(); - HttpLog.v(header.getName() + ": " + header.getValue()); - } - } - } - - requestContentProcessor.process(mHttpRequest, - mConnection.getHttpContext()); - httpClientConnection.sendRequestHeader(mHttpRequest); - if (mHttpRequest instanceof HttpEntityEnclosingRequest) { - httpClientConnection.sendRequestEntity( - (HttpEntityEnclosingRequest) mHttpRequest); - } - - if (HttpLog.LOGV) { - HttpLog.v("Request.requestSent() " + mHost.getSchemeName() + "://" + getHostPort() + mPath); - } - } - - - /** - * Receive a single http response. - * - * @param httpClientConnection the request to receive the response for. - */ - void readResponse(AndroidHttpClientConnection httpClientConnection) - throws IOException, ParseException { - - if (mCancelled) return; // don't send cancelled requests - - StatusLine statusLine = null; - boolean hasBody = false; - httpClientConnection.flush(); - int statusCode = 0; - - Headers header = new Headers(); - do { - statusLine = httpClientConnection.parseResponseHeader(header); - statusCode = statusLine.getStatusCode(); - } while (statusCode < HttpStatus.SC_OK); - if (HttpLog.LOGV) HttpLog.v( - "Request.readResponseStatus() " + - statusLine.toString().length() + " " + statusLine); - - ProtocolVersion v = statusLine.getProtocolVersion(); - mEventHandler.status(v.getMajor(), v.getMinor(), - statusCode, statusLine.getReasonPhrase()); - mEventHandler.headers(header); - HttpEntity entity = null; - hasBody = canResponseHaveBody(mHttpRequest, statusCode); - - if (hasBody) - entity = httpClientConnection.receiveResponseEntity(header); - - // restrict the range request to the servers claiming that they are - // accepting ranges in bytes - boolean supportPartialContent = "bytes".equalsIgnoreCase(header - .getAcceptRanges()); - - if (entity != null) { - InputStream is = entity.getContent(); - - // process gzip content encoding - Header contentEncoding = entity.getContentEncoding(); - InputStream nis = null; - byte[] buf = null; - int count = 0; - try { - if (contentEncoding != null && - contentEncoding.getValue().equals("gzip")) { - nis = new GZIPInputStream(is); - } else { - nis = is; - } - - /* accumulate enough data to make it worth pushing it - * up the stack */ - buf = mConnection.getBuf(); - int len = 0; - int lowWater = buf.length / 2; - while (len != -1) { - synchronized(this) { - while (mLoadingPaused) { - // Put this (network loading) thread to sleep if WebCore - // has asked us to. This can happen with plugins for - // example, if we are streaming data but the plugin has - // filled its internal buffers. - try { - wait(); - } catch (InterruptedException e) { - HttpLog.e("Interrupted exception whilst " - + "network thread paused at WebCore's request." - + " " + e.getMessage()); - } - } - } - - len = nis.read(buf, count, buf.length - count); - - if (len != -1) { - count += len; - if (supportPartialContent) mReceivedBytes += len; - } - if (len == -1 || count >= lowWater) { - if (HttpLog.LOGV) HttpLog.v("Request.readResponse() " + count); - mEventHandler.data(buf, count); - count = 0; - } - } - } catch (EOFException e) { - /* InflaterInputStream throws an EOFException when the - server truncates gzipped content. Handle this case - as we do truncated non-gzipped content: no error */ - if (count > 0) { - // if there is uncommited content, we should commit them - mEventHandler.data(buf, count); - } - if (HttpLog.LOGV) HttpLog.v( "readResponse() handling " + e); - } catch(IOException e) { - // don't throw if we have a non-OK status code - if (statusCode == HttpStatus.SC_OK - || statusCode == HttpStatus.SC_PARTIAL_CONTENT) { - if (supportPartialContent && count > 0) { - // if there is uncommited content, we should commit them - // as we will continue the request - mEventHandler.data(buf, count); - } - throw e; - } - } finally { - if (nis != null) { - nis.close(); - } - } - } - mConnection.setCanPersist(entity, statusLine.getProtocolVersion(), - header.getConnectionType()); - mEventHandler.endData(); - complete(); - - if (HttpLog.LOGV) HttpLog.v("Request.readResponse(): done " + - mHost.getSchemeName() + "://" + getHostPort() + mPath); - } - - /** - * Data will not be sent to or received from server after cancel() - * call. Does not close connection--use close() below for that. - * - * Called by RequestHandle from non-network thread - */ - synchronized void cancel() { - if (HttpLog.LOGV) { - HttpLog.v("Request.cancel(): " + getUri()); - } - - // Ensure that the network thread is not blocked by a hanging request from WebCore to - // pause the load. - mLoadingPaused = false; - notify(); - - mCancelled = true; - if (mConnection != null) { - mConnection.cancel(); - } - } - - String getHostPort() { - String myScheme = mHost.getSchemeName(); - int myPort = mHost.getPort(); - - // Only send port when we must... many servers can't deal with it - if (myPort != 80 && myScheme.equals("http") || - myPort != 443 && myScheme.equals("https")) { - return mHost.toHostString(); - } else { - return mHost.getHostName(); - } - } - - String getUri() { - if (mProxyHost == null || - mHost.getSchemeName().equals("https")) { - return mPath; - } - return mHost.getSchemeName() + "://" + getHostPort() + mPath; - } - - /** - * for debugging - */ - public String toString() { - return mPath; - } - - - /** - * If this request has been sent once and failed, it must be reset - * before it can be sent again. - */ - void reset() { - /* clear content-length header */ - mHttpRequest.removeHeaders(CONTENT_LENGTH_HEADER); - - if (mBodyProvider != null) { - try { - mBodyProvider.reset(); - } catch (IOException ex) { - if (HttpLog.LOGV) HttpLog.v( - "failed to reset body provider " + - getUri()); - } - setBodyProvider(mBodyProvider, mBodyLength); - } - - if (mReceivedBytes > 0) { - // reset the fail count as we continue the request - mFailCount = 0; - // set the "Range" header to indicate that the retry will continue - // instead of restarting the request - HttpLog.v("*** Request.reset() to range:" + mReceivedBytes); - mHttpRequest.setHeader("Range", "bytes=" + mReceivedBytes + "-"); - } - } - - /** - * Pause thread request completes. Used for synchronous requests, - * and testing - */ - void waitUntilComplete() { - synchronized (mClientResource) { - try { - if (HttpLog.LOGV) HttpLog.v("Request.waitUntilComplete()"); - mClientResource.wait(); - if (HttpLog.LOGV) HttpLog.v("Request.waitUntilComplete() done waiting"); - } catch (InterruptedException e) { - } - } - } - - void complete() { - synchronized (mClientResource) { - mClientResource.notifyAll(); - } - } - - /** - * Decide whether a response comes with an entity. - * The implementation in this class is based on RFC 2616. - * Unknown methods and response codes are supposed to - * indicate responses with an entity. - * <br/> - * Derived executors can override this method to handle - * methods and response codes not specified in RFC 2616. - * - * @param request the request, to obtain the executed method - * @param response the response, to obtain the status code - */ - - private static boolean canResponseHaveBody(final HttpRequest request, - final int status) { - - if ("HEAD".equalsIgnoreCase(request.getRequestLine().getMethod())) { - return false; - } - return status >= HttpStatus.SC_OK - && status != HttpStatus.SC_NO_CONTENT - && status != HttpStatus.SC_NOT_MODIFIED; - } - - /** - * Supply an InputStream that provides the body of a request. It's - * not great that the caller must also provide the length of the data - * returned by that InputStream, but the client needs to know up - * front, and I'm not sure how to get this out of the InputStream - * itself without a costly readthrough. I'm not sure skip() would - * do what we want. If you know a better way, please let me know. - */ - private void setBodyProvider(InputStream bodyProvider, int bodyLength) { - if (!bodyProvider.markSupported()) { - throw new IllegalArgumentException( - "bodyProvider must support mark()"); - } - // Mark beginning of stream - bodyProvider.mark(Integer.MAX_VALUE); - - ((BasicHttpEntityEnclosingRequest)mHttpRequest).setEntity( - new InputStreamEntity(bodyProvider, bodyLength)); - } - - - /** - * Handles SSL error(s) on the way down from the user (the user - * has already provided their feedback). - */ - public void handleSslErrorResponse(boolean proceed) { - HttpsConnection connection = (HttpsConnection)(mConnection); - if (connection != null) { - connection.restartConnection(proceed); - } - } - - /** - * Helper: calls error() on eventhandler with appropriate message - * This should not be called before the mConnection is set. - */ - void error(int errorId, int resourceId) { - mEventHandler.error( - errorId, - mConnection.mContext.getText( - resourceId).toString()); - } - -} diff --git a/core/java/android/net/http/RequestHandle.java b/core/java/android/net/http/RequestHandle.java deleted file mode 100644 index f23f69c..0000000 --- a/core/java/android/net/http/RequestHandle.java +++ /dev/null @@ -1,466 +0,0 @@ -/* - * Copyright (C) 2006 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.net.http; - -import android.net.ParseException; -import android.net.WebAddress; -import junit.framework.Assert; -import android.webkit.CookieManager; - -import org.apache.commons.codec.binary.Base64; - -import java.io.InputStream; -import java.lang.Math; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.HashMap; -import java.util.Map; -import java.util.Random; - -/** - * RequestHandle: handles a request session that may include multiple - * redirects, HTTP authentication requests, etc. - * - * {@hide} - */ -public class RequestHandle { - - private String mUrl; - private WebAddress mUri; - private String mMethod; - private Map<String, String> mHeaders; - private RequestQueue mRequestQueue; - private Request mRequest; - private InputStream mBodyProvider; - private int mBodyLength; - private int mRedirectCount = 0; - // Used only with synchronous requests. - private Connection mConnection; - - private final static String AUTHORIZATION_HEADER = "Authorization"; - private final static String PROXY_AUTHORIZATION_HEADER = "Proxy-Authorization"; - - public final static int MAX_REDIRECT_COUNT = 16; - - /** - * Creates a new request session. - */ - public RequestHandle(RequestQueue requestQueue, String url, WebAddress uri, - String method, Map<String, String> headers, - InputStream bodyProvider, int bodyLength, Request request) { - - if (headers == null) { - headers = new HashMap<String, String>(); - } - mHeaders = headers; - mBodyProvider = bodyProvider; - mBodyLength = bodyLength; - mMethod = method == null? "GET" : method; - - mUrl = url; - mUri = uri; - - mRequestQueue = requestQueue; - - mRequest = request; - } - - /** - * Creates a new request session with a given Connection. This connection - * is used during a synchronous load to handle this request. - */ - public RequestHandle(RequestQueue requestQueue, String url, WebAddress uri, - String method, Map<String, String> headers, - InputStream bodyProvider, int bodyLength, Request request, - Connection conn) { - this(requestQueue, url, uri, method, headers, bodyProvider, bodyLength, - request); - mConnection = conn; - } - - /** - * Cancels this request - */ - public void cancel() { - if (mRequest != null) { - mRequest.cancel(); - } - } - - /** - * Pauses the loading of this request. For example, called from the WebCore thread - * when the plugin can take no more data. - */ - public void pauseRequest(boolean pause) { - if (mRequest != null) { - mRequest.setLoadingPaused(pause); - } - } - - /** - * Handles SSL error(s) on the way down from the user (the user - * has already provided their feedback). - */ - public void handleSslErrorResponse(boolean proceed) { - if (mRequest != null) { - mRequest.handleSslErrorResponse(proceed); - } - } - - /** - * @return true if we've hit the max redirect count - */ - public boolean isRedirectMax() { - return mRedirectCount >= MAX_REDIRECT_COUNT; - } - - public int getRedirectCount() { - return mRedirectCount; - } - - public void setRedirectCount(int count) { - mRedirectCount = count; - } - - /** - * Create and queue a redirect request. - * - * @param redirectTo URL to redirect to - * @param statusCode HTTP status code returned from original request - * @param cacheHeaders Cache header for redirect URL - * @return true if setup succeeds, false otherwise (redirect loop - * count exceeded, body provider unable to rewind on 307 redirect) - */ - public boolean setupRedirect(String redirectTo, int statusCode, - Map<String, String> cacheHeaders) { - if (HttpLog.LOGV) { - HttpLog.v("RequestHandle.setupRedirect(): redirectCount " + - mRedirectCount); - } - - // be careful and remove authentication headers, if any - mHeaders.remove(AUTHORIZATION_HEADER); - mHeaders.remove(PROXY_AUTHORIZATION_HEADER); - - if (++mRedirectCount == MAX_REDIRECT_COUNT) { - // Way too many redirects -- fail out - if (HttpLog.LOGV) HttpLog.v( - "RequestHandle.setupRedirect(): too many redirects " + - mRequest); - mRequest.error(EventHandler.ERROR_REDIRECT_LOOP, - com.android.internal.R.string.httpErrorRedirectLoop); - return false; - } - - if (mUrl.startsWith("https:") && redirectTo.startsWith("http:")) { - // implement http://www.w3.org/Protocols/rfc2616/rfc2616-sec15.html#sec15.1.3 - if (HttpLog.LOGV) { - HttpLog.v("blowing away the referer on an https -> http redirect"); - } - mHeaders.remove("Referer"); - } - - mUrl = redirectTo; - try { - mUri = new WebAddress(mUrl); - } catch (ParseException e) { - e.printStackTrace(); - } - - // update the "Cookie" header based on the redirected url - mHeaders.remove("Cookie"); - String cookie = CookieManager.getInstance().getCookie(mUri); - if (cookie != null && cookie.length() > 0) { - mHeaders.put("Cookie", cookie); - } - - if ((statusCode == 302 || statusCode == 303) && mMethod.equals("POST")) { - if (HttpLog.LOGV) { - HttpLog.v("replacing POST with GET on redirect to " + redirectTo); - } - mMethod = "GET"; - } - /* Only repost content on a 307. If 307, reset the body - provider so we can replay the body */ - if (statusCode == 307) { - try { - if (mBodyProvider != null) mBodyProvider.reset(); - } catch (java.io.IOException ex) { - if (HttpLog.LOGV) { - HttpLog.v("setupRedirect() failed to reset body provider"); - } - return false; - } - - } else { - mHeaders.remove("Content-Type"); - mBodyProvider = null; - } - - // Update the cache headers for this URL - mHeaders.putAll(cacheHeaders); - - createAndQueueNewRequest(); - return true; - } - - /** - * Create and queue an HTTP authentication-response (basic) request. - */ - public void setupBasicAuthResponse(boolean isProxy, String username, String password) { - String response = computeBasicAuthResponse(username, password); - if (HttpLog.LOGV) { - HttpLog.v("setupBasicAuthResponse(): response: " + response); - } - mHeaders.put(authorizationHeader(isProxy), "Basic " + response); - setupAuthResponse(); - } - - /** - * Create and queue an HTTP authentication-response (digest) request. - */ - public void setupDigestAuthResponse(boolean isProxy, - String username, - String password, - String realm, - String nonce, - String QOP, - String algorithm, - String opaque) { - - String response = computeDigestAuthResponse( - username, password, realm, nonce, QOP, algorithm, opaque); - if (HttpLog.LOGV) { - HttpLog.v("setupDigestAuthResponse(): response: " + response); - } - mHeaders.put(authorizationHeader(isProxy), "Digest " + response); - setupAuthResponse(); - } - - private void setupAuthResponse() { - try { - if (mBodyProvider != null) mBodyProvider.reset(); - } catch (java.io.IOException ex) { - if (HttpLog.LOGV) { - HttpLog.v("setupAuthResponse() failed to reset body provider"); - } - } - createAndQueueNewRequest(); - } - - /** - * @return HTTP request method (GET, PUT, etc). - */ - public String getMethod() { - return mMethod; - } - - /** - * @return Basic-scheme authentication response: BASE64(username:password). - */ - public static String computeBasicAuthResponse(String username, String password) { - Assert.assertNotNull(username); - Assert.assertNotNull(password); - - // encode username:password to base64 - return new String(Base64.encodeBase64((username + ':' + password).getBytes())); - } - - public void waitUntilComplete() { - mRequest.waitUntilComplete(); - } - - public void processRequest() { - if (mConnection != null) { - mConnection.processRequests(mRequest); - } - } - - /** - * @return Digest-scheme authentication response. - */ - private String computeDigestAuthResponse(String username, - String password, - String realm, - String nonce, - String QOP, - String algorithm, - String opaque) { - - Assert.assertNotNull(username); - Assert.assertNotNull(password); - Assert.assertNotNull(realm); - - String A1 = username + ":" + realm + ":" + password; - String A2 = mMethod + ":" + mUrl; - - // because we do not preemptively send authorization headers, nc is always 1 - String nc = "00000001"; - String cnonce = computeCnonce(); - String digest = computeDigest(A1, A2, nonce, QOP, nc, cnonce); - - String response = ""; - response += "username=" + doubleQuote(username) + ", "; - response += "realm=" + doubleQuote(realm) + ", "; - response += "nonce=" + doubleQuote(nonce) + ", "; - response += "uri=" + doubleQuote(mUrl) + ", "; - response += "response=" + doubleQuote(digest) ; - - if (opaque != null) { - response += ", opaque=" + doubleQuote(opaque); - } - - if (algorithm != null) { - response += ", algorithm=" + algorithm; - } - - if (QOP != null) { - response += ", qop=" + QOP + ", nc=" + nc + ", cnonce=" + doubleQuote(cnonce); - } - - return response; - } - - /** - * @return The right authorization header (dependeing on whether it is a proxy or not). - */ - public static String authorizationHeader(boolean isProxy) { - if (!isProxy) { - return AUTHORIZATION_HEADER; - } else { - return PROXY_AUTHORIZATION_HEADER; - } - } - - /** - * @return Double-quoted MD5 digest. - */ - private String computeDigest( - String A1, String A2, String nonce, String QOP, String nc, String cnonce) { - if (HttpLog.LOGV) { - HttpLog.v("computeDigest(): QOP: " + QOP); - } - - if (QOP == null) { - return KD(H(A1), nonce + ":" + H(A2)); - } else { - if (QOP.equalsIgnoreCase("auth")) { - return KD(H(A1), nonce + ":" + nc + ":" + cnonce + ":" + QOP + ":" + H(A2)); - } - } - - return null; - } - - /** - * @return MD5 hash of concat(secret, ":", data). - */ - private String KD(String secret, String data) { - return H(secret + ":" + data); - } - - /** - * @return MD5 hash of param. - */ - private String H(String param) { - if (param != null) { - try { - MessageDigest md5 = MessageDigest.getInstance("MD5"); - - byte[] d = md5.digest(param.getBytes()); - if (d != null) { - return bufferToHex(d); - } - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - } - - return null; - } - - /** - * @return HEX buffer representation. - */ - private String bufferToHex(byte[] buffer) { - final char hexChars[] = - { '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f' }; - - if (buffer != null) { - int length = buffer.length; - if (length > 0) { - StringBuilder hex = new StringBuilder(2 * length); - - for (int i = 0; i < length; ++i) { - byte l = (byte) (buffer[i] & 0x0F); - byte h = (byte)((buffer[i] & 0xF0) >> 4); - - hex.append(hexChars[h]); - hex.append(hexChars[l]); - } - - return hex.toString(); - } else { - return ""; - } - } - - return null; - } - - /** - * Computes a random cnonce value based on the current time. - */ - private String computeCnonce() { - Random rand = new Random(); - int nextInt = rand.nextInt(); - nextInt = (nextInt == Integer.MIN_VALUE) ? - Integer.MAX_VALUE : Math.abs(nextInt); - return Integer.toString(nextInt, 16); - } - - /** - * "Double-quotes" the argument. - */ - private String doubleQuote(String param) { - if (param != null) { - return "\"" + param + "\""; - } - - return null; - } - - /** - * Creates and queues new request. - */ - private void createAndQueueNewRequest() { - // mConnection is non-null if and only if the requests are synchronous. - if (mConnection != null) { - RequestHandle newHandle = mRequestQueue.queueSynchronousRequest( - mUrl, mUri, mMethod, mHeaders, mRequest.mEventHandler, - mBodyProvider, mBodyLength); - mRequest = newHandle.mRequest; - mConnection = newHandle.mConnection; - newHandle.processRequest(); - return; - } - mRequest = mRequestQueue.queueRequest( - mUrl, mUri, mMethod, mHeaders, mRequest.mEventHandler, - mBodyProvider, - mBodyLength).mRequest; - } -} diff --git a/core/java/android/net/http/RequestQueue.java b/core/java/android/net/http/RequestQueue.java deleted file mode 100644 index 7d2da1b..0000000 --- a/core/java/android/net/http/RequestQueue.java +++ /dev/null @@ -1,542 +0,0 @@ -/* - * Copyright (C) 2006 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. - */ - -/** - * High level HTTP Interface - * Queues requests as necessary - */ - -package android.net.http; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.net.Proxy; -import android.net.WebAddress; -import android.util.Log; - -import java.io.InputStream; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.ListIterator; -import java.util.Map; - -import org.apache.http.HttpHost; - -/** - * {@hide} - */ -public class RequestQueue implements RequestFeeder { - - - /** - * Requests, indexed by HttpHost (scheme, host, port) - */ - private final LinkedHashMap<HttpHost, LinkedList<Request>> mPending; - private final Context mContext; - private final ActivePool mActivePool; - private final ConnectivityManager mConnectivityManager; - - private HttpHost mProxyHost = null; - private BroadcastReceiver mProxyChangeReceiver; - - /* default simultaneous connection count */ - private static final int CONNECTION_COUNT = 4; - - /** - * This class maintains active connection threads - */ - class ActivePool implements ConnectionManager { - /** Threads used to process requests */ - ConnectionThread[] mThreads; - - IdleCache mIdleCache; - - private int mTotalRequest; - private int mTotalConnection; - private int mConnectionCount; - - ActivePool(int connectionCount) { - mIdleCache = new IdleCache(); - mConnectionCount = connectionCount; - mThreads = new ConnectionThread[mConnectionCount]; - - for (int i = 0; i < mConnectionCount; i++) { - mThreads[i] = new ConnectionThread( - mContext, i, this, RequestQueue.this); - } - } - - void startup() { - for (int i = 0; i < mConnectionCount; i++) { - mThreads[i].start(); - } - } - - void shutdown() { - for (int i = 0; i < mConnectionCount; i++) { - mThreads[i].requestStop(); - } - } - - void startConnectionThread() { - synchronized (RequestQueue.this) { - RequestQueue.this.notify(); - } - } - - public void startTiming() { - for (int i = 0; i < mConnectionCount; i++) { - ConnectionThread rt = mThreads[i]; - rt.mCurrentThreadTime = -1; - rt.mTotalThreadTime = 0; - } - mTotalRequest = 0; - mTotalConnection = 0; - } - - public void stopTiming() { - int totalTime = 0; - for (int i = 0; i < mConnectionCount; i++) { - ConnectionThread rt = mThreads[i]; - if (rt.mCurrentThreadTime != -1) { - totalTime += rt.mTotalThreadTime; - } - rt.mCurrentThreadTime = 0; - } - Log.d("Http", "Http thread used " + totalTime + " ms " + " for " - + mTotalRequest + " requests and " + mTotalConnection - + " new connections"); - } - - void logState() { - StringBuilder dump = new StringBuilder(); - for (int i = 0; i < mConnectionCount; i++) { - dump.append(mThreads[i] + "\n"); - } - HttpLog.v(dump.toString()); - } - - - public HttpHost getProxyHost() { - return mProxyHost; - } - - /** - * Turns off persistence on all live connections - */ - void disablePersistence() { - for (int i = 0; i < mConnectionCount; i++) { - Connection connection = mThreads[i].mConnection; - if (connection != null) connection.setCanPersist(false); - } - mIdleCache.clear(); - } - - /* Linear lookup -- okay for small thread counts. Might use - private HashMap<HttpHost, LinkedList<ConnectionThread>> mActiveMap; - if this turns out to be a hotspot */ - ConnectionThread getThread(HttpHost host) { - synchronized(RequestQueue.this) { - for (int i = 0; i < mThreads.length; i++) { - ConnectionThread ct = mThreads[i]; - Connection connection = ct.mConnection; - if (connection != null && connection.mHost.equals(host)) { - return ct; - } - } - } - return null; - } - - public Connection getConnection(Context context, HttpHost host) { - host = RequestQueue.this.determineHost(host); - Connection con = mIdleCache.getConnection(host); - if (con == null) { - mTotalConnection++; - con = Connection.getConnection(mContext, host, mProxyHost, - RequestQueue.this); - } - return con; - } - public boolean recycleConnection(Connection connection) { - return mIdleCache.cacheConnection(connection.getHost(), connection); - } - - } - - /** - * A RequestQueue class instance maintains a set of queued - * requests. It orders them, makes the requests against HTTP - * servers, and makes callbacks to supplied eventHandlers as data - * is read. It supports request prioritization, connection reuse - * and pipelining. - * - * @param context application context - */ - public RequestQueue(Context context) { - this(context, CONNECTION_COUNT); - } - - /** - * A RequestQueue class instance maintains a set of queued - * requests. It orders them, makes the requests against HTTP - * servers, and makes callbacks to supplied eventHandlers as data - * is read. It supports request prioritization, connection reuse - * and pipelining. - * - * @param context application context - * @param connectionCount The number of simultaneous connections - */ - public RequestQueue(Context context, int connectionCount) { - mContext = context; - - mPending = new LinkedHashMap<HttpHost, LinkedList<Request>>(32); - - mActivePool = new ActivePool(connectionCount); - mActivePool.startup(); - - mConnectivityManager = (ConnectivityManager) - context.getSystemService(Context.CONNECTIVITY_SERVICE); - } - - /** - * Enables data state and proxy tracking - */ - public synchronized void enablePlatformNotifications() { - if (HttpLog.LOGV) HttpLog.v("RequestQueue.enablePlatformNotifications() network"); - - if (mProxyChangeReceiver == null) { - mProxyChangeReceiver = - new BroadcastReceiver() { - @Override - public void onReceive(Context ctx, Intent intent) { - setProxyConfig(); - } - }; - mContext.registerReceiver(mProxyChangeReceiver, - new IntentFilter(Proxy.PROXY_CHANGE_ACTION)); - } - // we need to resample the current proxy setup - setProxyConfig(); - } - - /** - * If platform notifications have been enabled, call this method - * to disable before destroying RequestQueue - */ - public synchronized void disablePlatformNotifications() { - if (HttpLog.LOGV) HttpLog.v("RequestQueue.disablePlatformNotifications() network"); - - if (mProxyChangeReceiver != null) { - mContext.unregisterReceiver(mProxyChangeReceiver); - mProxyChangeReceiver = null; - } - } - - /** - * Because our IntentReceiver can run within a different thread, - * synchronize setting the proxy - */ - private synchronized void setProxyConfig() { - NetworkInfo info = mConnectivityManager.getActiveNetworkInfo(); - if (info != null && info.getType() == ConnectivityManager.TYPE_WIFI) { - mProxyHost = null; - } else { - String host = Proxy.getHost(mContext); - if (HttpLog.LOGV) HttpLog.v("RequestQueue.setProxyConfig " + host); - if (host == null) { - mProxyHost = null; - } else { - mActivePool.disablePersistence(); - mProxyHost = new HttpHost(host, Proxy.getPort(mContext), "http"); - } - } - } - - /** - * used by webkit - * @return proxy host if set, null otherwise - */ - public HttpHost getProxyHost() { - return mProxyHost; - } - - /** - * Queues an HTTP request - * @param url The url to load. - * @param method "GET" or "POST." - * @param headers A hashmap of http headers. - * @param eventHandler The event handler for handling returned - * data. Callbacks will be made on the supplied instance. - * @param bodyProvider InputStream providing HTTP body, null if none - * @param bodyLength length of body, must be 0 if bodyProvider is null - */ - public RequestHandle queueRequest( - String url, String method, - Map<String, String> headers, EventHandler eventHandler, - InputStream bodyProvider, int bodyLength) { - WebAddress uri = new WebAddress(url); - return queueRequest(url, uri, method, headers, eventHandler, - bodyProvider, bodyLength); - } - - /** - * Queues an HTTP request - * @param url The url to load. - * @param uri The uri of the url to load. - * @param method "GET" or "POST." - * @param headers A hashmap of http headers. - * @param eventHandler The event handler for handling returned - * data. Callbacks will be made on the supplied instance. - * @param bodyProvider InputStream providing HTTP body, null if none - * @param bodyLength length of body, must be 0 if bodyProvider is null - */ - public RequestHandle queueRequest( - String url, WebAddress uri, String method, Map<String, String> headers, - EventHandler eventHandler, - InputStream bodyProvider, int bodyLength) { - - if (HttpLog.LOGV) HttpLog.v("RequestQueue.queueRequest " + uri); - - // Ensure there is an eventHandler set - if (eventHandler == null) { - eventHandler = new LoggingEventHandler(); - } - - /* Create and queue request */ - Request req; - HttpHost httpHost = new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()); - - // set up request - req = new Request(method, httpHost, mProxyHost, uri.getPath(), bodyProvider, - bodyLength, eventHandler, headers); - - queueRequest(req, false); - - mActivePool.mTotalRequest++; - - // dump(); - mActivePool.startConnectionThread(); - - return new RequestHandle( - this, url, uri, method, headers, bodyProvider, bodyLength, - req); - } - - private static class SyncFeeder implements RequestFeeder { - // This is used in the case where the request fails and needs to be - // requeued into the RequestFeeder. - private Request mRequest; - SyncFeeder() { - } - public Request getRequest() { - Request r = mRequest; - mRequest = null; - return r; - } - public Request getRequest(HttpHost host) { - return getRequest(); - } - public boolean haveRequest(HttpHost host) { - return mRequest != null; - } - public void requeueRequest(Request r) { - mRequest = r; - } - } - - public RequestHandle queueSynchronousRequest(String url, WebAddress uri, - String method, Map<String, String> headers, - EventHandler eventHandler, InputStream bodyProvider, - int bodyLength) { - if (HttpLog.LOGV) { - HttpLog.v("RequestQueue.dispatchSynchronousRequest " + uri); - } - - HttpHost host = new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()); - - Request req = new Request(method, host, mProxyHost, uri.getPath(), - bodyProvider, bodyLength, eventHandler, headers); - - // Open a new connection that uses our special RequestFeeder - // implementation. - host = determineHost(host); - Connection conn = Connection.getConnection(mContext, host, mProxyHost, - new SyncFeeder()); - - // TODO: I would like to process the request here but LoadListener - // needs a RequestHandle to process some messages. - return new RequestHandle(this, url, uri, method, headers, bodyProvider, - bodyLength, req, conn); - - } - - // Chooses between the proxy and the request's host. - private HttpHost determineHost(HttpHost host) { - // There used to be a comment in ConnectionThread about t-mob's proxy - // being really bad about https. But, HttpsConnection actually looks - // for a proxy and connects through it anyway. I think that this check - // is still valid because if a site is https, we will use - // HttpsConnection rather than HttpConnection if the proxy address is - // not secure. - return (mProxyHost == null || "https".equals(host.getSchemeName())) - ? host : mProxyHost; - } - - /** - * @return true iff there are any non-active requests pending - */ - synchronized boolean requestsPending() { - return !mPending.isEmpty(); - } - - - /** - * debug tool: prints request queue to log - */ - synchronized void dump() { - HttpLog.v("dump()"); - StringBuilder dump = new StringBuilder(); - int count = 0; - Iterator<Map.Entry<HttpHost, LinkedList<Request>>> iter; - - // mActivePool.log(dump); - - if (!mPending.isEmpty()) { - iter = mPending.entrySet().iterator(); - while (iter.hasNext()) { - Map.Entry<HttpHost, LinkedList<Request>> entry = iter.next(); - String hostName = entry.getKey().getHostName(); - StringBuilder line = new StringBuilder("p" + count++ + " " + hostName + " "); - - LinkedList<Request> reqList = entry.getValue(); - ListIterator reqIter = reqList.listIterator(0); - while (iter.hasNext()) { - Request request = (Request)iter.next(); - line.append(request + " "); - } - dump.append(line); - dump.append("\n"); - } - } - HttpLog.v(dump.toString()); - } - - /* - * RequestFeeder implementation - */ - public synchronized Request getRequest() { - Request ret = null; - - if (!mPending.isEmpty()) { - ret = removeFirst(mPending); - } - if (HttpLog.LOGV) HttpLog.v("RequestQueue.getRequest() => " + ret); - return ret; - } - - /** - * @return a request for given host if possible - */ - public synchronized Request getRequest(HttpHost host) { - Request ret = null; - - if (mPending.containsKey(host)) { - LinkedList<Request> reqList = mPending.get(host); - ret = reqList.removeFirst(); - if (reqList.isEmpty()) { - mPending.remove(host); - } - } - if (HttpLog.LOGV) HttpLog.v("RequestQueue.getRequest(" + host + ") => " + ret); - return ret; - } - - /** - * @return true if a request for this host is available - */ - public synchronized boolean haveRequest(HttpHost host) { - return mPending.containsKey(host); - } - - /** - * Put request back on head of queue - */ - public void requeueRequest(Request request) { - queueRequest(request, true); - } - - /** - * This must be called to cleanly shutdown RequestQueue - */ - public void shutdown() { - mActivePool.shutdown(); - } - - protected synchronized void queueRequest(Request request, boolean head) { - HttpHost host = request.mProxyHost == null ? request.mHost : request.mProxyHost; - LinkedList<Request> reqList; - if (mPending.containsKey(host)) { - reqList = mPending.get(host); - } else { - reqList = new LinkedList<Request>(); - mPending.put(host, reqList); - } - if (head) { - reqList.addFirst(request); - } else { - reqList.add(request); - } - } - - - public void startTiming() { - mActivePool.startTiming(); - } - - public void stopTiming() { - mActivePool.stopTiming(); - } - - /* helper */ - private Request removeFirst(LinkedHashMap<HttpHost, LinkedList<Request>> requestQueue) { - Request ret = null; - Iterator<Map.Entry<HttpHost, LinkedList<Request>>> iter = requestQueue.entrySet().iterator(); - if (iter.hasNext()) { - Map.Entry<HttpHost, LinkedList<Request>> entry = iter.next(); - LinkedList<Request> reqList = entry.getValue(); - ret = reqList.removeFirst(); - if (reqList.isEmpty()) { - requestQueue.remove(entry.getKey()); - } - } - return ret; - } - - /** - * This interface is exposed to each connection - */ - interface ConnectionManager { - HttpHost getProxyHost(); - Connection getConnection(Context context, HttpHost host); - boolean recycleConnection(Connection connection); - } -} diff --git a/core/java/android/net/http/X509TrustManagerExtensions.java b/core/java/android/net/http/X509TrustManagerExtensions.java index bb36c20..eb4ceda 100644 --- a/core/java/android/net/http/X509TrustManagerExtensions.java +++ b/core/java/android/net/http/X509TrustManagerExtensions.java @@ -22,8 +22,6 @@ import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.List; -import javax.net.ssl.SSLParameters; -import javax.net.ssl.SSLSocket; import javax.net.ssl.X509TrustManager; /** diff --git a/core/java/android/os/AsyncTask.java b/core/java/android/os/AsyncTask.java index 7785f2b..47e8e69 100644 --- a/core/java/android/os/AsyncTask.java +++ b/core/java/android/os/AsyncTask.java @@ -301,7 +301,7 @@ public abstract class AsyncTask<Params, Progress, Result> { } catch (InterruptedException e) { android.util.Log.w(LOG_TAG, e); } catch (ExecutionException e) { - throw new RuntimeException("An error occured while executing doInBackground()", + throw new RuntimeException("An error occurred while executing doInBackground()", e.getCause()); } catch (CancellationException e) { postResultIfNotInvoked(null); diff --git a/core/java/android/os/BaseBundle.java b/core/java/android/os/BaseBundle.java index 1b02141..c373308 100644 --- a/core/java/android/os/BaseBundle.java +++ b/core/java/android/os/BaseBundle.java @@ -16,12 +16,12 @@ package android.os; +import android.annotation.Nullable; import android.util.ArrayMap; import android.util.Log; import java.io.Serializable; import java.util.ArrayList; -import java.util.Map; import java.util.Set; /** @@ -63,7 +63,7 @@ public class BaseBundle { * inside of the Bundle. * @param capacity Initial size of the ArrayMap. */ - BaseBundle(ClassLoader loader, int capacity) { + BaseBundle(@Nullable ClassLoader loader, int capacity) { mMap = capacity > 0 ? new ArrayMap<String, Object>(capacity) : new ArrayMap<String, Object>(); mClassLoader = loader == null ? getClass().getClassLoader() : loader; @@ -276,6 +276,7 @@ public class BaseBundle { * @param key a String key * @return an Object, or null */ + @Nullable public Object get(String key) { unparcel(); return mMap.get(key); @@ -307,7 +308,7 @@ public class BaseBundle { * * @param map a Map */ - void putAll(Map map) { + void putAll(ArrayMap map) { unparcel(); mMap.putAll(map); } @@ -327,9 +328,9 @@ public class BaseBundle { * any existing value for the given key. Either key or value may be null. * * @param key a String, or null - * @param value a Boolean, or null + * @param value a boolean */ - public void putBoolean(String key, boolean value) { + public void putBoolean(@Nullable String key, boolean value) { unparcel(); mMap.put(key, value); } @@ -341,7 +342,7 @@ public class BaseBundle { * @param key a String, or null * @param value a byte */ - void putByte(String key, byte value) { + void putByte(@Nullable String key, byte value) { unparcel(); mMap.put(key, value); } @@ -351,9 +352,9 @@ public class BaseBundle { * any existing value for the given key. * * @param key a String, or null - * @param value a char, or null + * @param value a char */ - void putChar(String key, char value) { + void putChar(@Nullable String key, char value) { unparcel(); mMap.put(key, value); } @@ -365,7 +366,7 @@ public class BaseBundle { * @param key a String, or null * @param value a short */ - void putShort(String key, short value) { + void putShort(@Nullable String key, short value) { unparcel(); mMap.put(key, value); } @@ -375,9 +376,9 @@ public class BaseBundle { * any existing value for the given key. * * @param key a String, or null - * @param value an int, or null + * @param value an int */ - public void putInt(String key, int value) { + public void putInt(@Nullable String key, int value) { unparcel(); mMap.put(key, value); } @@ -389,7 +390,7 @@ public class BaseBundle { * @param key a String, or null * @param value a long */ - public void putLong(String key, long value) { + public void putLong(@Nullable String key, long value) { unparcel(); mMap.put(key, value); } @@ -401,7 +402,7 @@ public class BaseBundle { * @param key a String, or null * @param value a float */ - void putFloat(String key, float value) { + void putFloat(@Nullable String key, float value) { unparcel(); mMap.put(key, value); } @@ -413,7 +414,7 @@ public class BaseBundle { * @param key a String, or null * @param value a double */ - public void putDouble(String key, double value) { + public void putDouble(@Nullable String key, double value) { unparcel(); mMap.put(key, value); } @@ -425,7 +426,7 @@ public class BaseBundle { * @param key a String, or null * @param value a String, or null */ - public void putString(String key, String value) { + public void putString(@Nullable String key, @Nullable String value) { unparcel(); mMap.put(key, value); } @@ -437,7 +438,7 @@ public class BaseBundle { * @param key a String, or null * @param value a CharSequence, or null */ - void putCharSequence(String key, CharSequence value) { + void putCharSequence(@Nullable String key, @Nullable CharSequence value) { unparcel(); mMap.put(key, value); } @@ -449,7 +450,7 @@ public class BaseBundle { * @param key a String, or null * @param value an ArrayList<Integer> object, or null */ - void putIntegerArrayList(String key, ArrayList<Integer> value) { + void putIntegerArrayList(@Nullable String key, @Nullable ArrayList<Integer> value) { unparcel(); mMap.put(key, value); } @@ -461,7 +462,7 @@ public class BaseBundle { * @param key a String, or null * @param value an ArrayList<String> object, or null */ - void putStringArrayList(String key, ArrayList<String> value) { + void putStringArrayList(@Nullable String key, @Nullable ArrayList<String> value) { unparcel(); mMap.put(key, value); } @@ -473,7 +474,7 @@ public class BaseBundle { * @param key a String, or null * @param value an ArrayList<CharSequence> object, or null */ - void putCharSequenceArrayList(String key, ArrayList<CharSequence> value) { + void putCharSequenceArrayList(@Nullable String key, @Nullable ArrayList<CharSequence> value) { unparcel(); mMap.put(key, value); } @@ -485,7 +486,7 @@ public class BaseBundle { * @param key a String, or null * @param value a Serializable object, or null */ - void putSerializable(String key, Serializable value) { + void putSerializable(@Nullable String key, @Nullable Serializable value) { unparcel(); mMap.put(key, value); } @@ -497,7 +498,7 @@ public class BaseBundle { * @param key a String, or null * @param value a boolean array object, or null */ - public void putBooleanArray(String key, boolean[] value) { + public void putBooleanArray(@Nullable String key, @Nullable boolean[] value) { unparcel(); mMap.put(key, value); } @@ -509,7 +510,7 @@ public class BaseBundle { * @param key a String, or null * @param value a byte array object, or null */ - void putByteArray(String key, byte[] value) { + void putByteArray(@Nullable String key, @Nullable byte[] value) { unparcel(); mMap.put(key, value); } @@ -521,7 +522,7 @@ public class BaseBundle { * @param key a String, or null * @param value a short array object, or null */ - void putShortArray(String key, short[] value) { + void putShortArray(@Nullable String key, @Nullable short[] value) { unparcel(); mMap.put(key, value); } @@ -533,7 +534,7 @@ public class BaseBundle { * @param key a String, or null * @param value a char array object, or null */ - void putCharArray(String key, char[] value) { + void putCharArray(@Nullable String key, @Nullable char[] value) { unparcel(); mMap.put(key, value); } @@ -545,7 +546,7 @@ public class BaseBundle { * @param key a String, or null * @param value an int array object, or null */ - public void putIntArray(String key, int[] value) { + public void putIntArray(@Nullable String key, @Nullable int[] value) { unparcel(); mMap.put(key, value); } @@ -557,7 +558,7 @@ public class BaseBundle { * @param key a String, or null * @param value a long array object, or null */ - public void putLongArray(String key, long[] value) { + public void putLongArray(@Nullable String key, @Nullable long[] value) { unparcel(); mMap.put(key, value); } @@ -569,7 +570,7 @@ public class BaseBundle { * @param key a String, or null * @param value a float array object, or null */ - void putFloatArray(String key, float[] value) { + void putFloatArray(@Nullable String key, @Nullable float[] value) { unparcel(); mMap.put(key, value); } @@ -581,7 +582,7 @@ public class BaseBundle { * @param key a String, or null * @param value a double array object, or null */ - public void putDoubleArray(String key, double[] value) { + public void putDoubleArray(@Nullable String key, @Nullable double[] value) { unparcel(); mMap.put(key, value); } @@ -593,7 +594,7 @@ public class BaseBundle { * @param key a String, or null * @param value a String array object, or null */ - public void putStringArray(String key, String[] value) { + public void putStringArray(@Nullable String key, @Nullable String[] value) { unparcel(); mMap.put(key, value); } @@ -605,7 +606,7 @@ public class BaseBundle { * @param key a String, or null * @param value a CharSequence array object, or null */ - void putCharSequenceArray(String key, CharSequence[] value) { + void putCharSequenceArray(@Nullable String key, @Nullable CharSequence[] value) { unparcel(); mMap.put(key, value); } @@ -914,7 +915,8 @@ public class BaseBundle { * @param key a String, or null * @return a String value, or null */ - public String getString(String key) { + @Nullable + public String getString(@Nullable String key) { unparcel(); final Object o = mMap.get(key); try { @@ -936,7 +938,7 @@ public class BaseBundle { * @return the String value associated with the given key, or defaultValue * if no valid String object is currently mapped to that key. */ - public String getString(String key, String defaultValue) { + public String getString(@Nullable String key, String defaultValue) { final String s = getString(key); return (s == null) ? defaultValue : s; } @@ -949,7 +951,8 @@ public class BaseBundle { * @param key a String, or null * @return a CharSequence value, or null */ - CharSequence getCharSequence(String key) { + @Nullable + CharSequence getCharSequence(@Nullable String key) { unparcel(); final Object o = mMap.get(key); try { @@ -971,7 +974,7 @@ public class BaseBundle { * @return the CharSequence value associated with the given key, or defaultValue * if no valid CharSequence object is currently mapped to that key. */ - CharSequence getCharSequence(String key, CharSequence defaultValue) { + CharSequence getCharSequence(@Nullable String key, CharSequence defaultValue) { final CharSequence cs = getCharSequence(key); return (cs == null) ? defaultValue : cs; } @@ -984,7 +987,8 @@ public class BaseBundle { * @param key a String, or null * @return a Serializable value, or null */ - Serializable getSerializable(String key) { + @Nullable + Serializable getSerializable(@Nullable String key) { unparcel(); Object o = mMap.get(key); if (o == null) { @@ -1006,7 +1010,8 @@ public class BaseBundle { * @param key a String, or null * @return an ArrayList<String> value, or null */ - ArrayList<Integer> getIntegerArrayList(String key) { + @Nullable + ArrayList<Integer> getIntegerArrayList(@Nullable String key) { unparcel(); Object o = mMap.get(key); if (o == null) { @@ -1028,7 +1033,8 @@ public class BaseBundle { * @param key a String, or null * @return an ArrayList<String> value, or null */ - ArrayList<String> getStringArrayList(String key) { + @Nullable + ArrayList<String> getStringArrayList(@Nullable String key) { unparcel(); Object o = mMap.get(key); if (o == null) { @@ -1050,7 +1056,8 @@ public class BaseBundle { * @param key a String, or null * @return an ArrayList<CharSequence> value, or null */ - ArrayList<CharSequence> getCharSequenceArrayList(String key) { + @Nullable + ArrayList<CharSequence> getCharSequenceArrayList(@Nullable String key) { unparcel(); Object o = mMap.get(key); if (o == null) { @@ -1072,7 +1079,8 @@ public class BaseBundle { * @param key a String, or null * @return a boolean[] value, or null */ - public boolean[] getBooleanArray(String key) { + @Nullable + public boolean[] getBooleanArray(@Nullable String key) { unparcel(); Object o = mMap.get(key); if (o == null) { @@ -1094,7 +1102,8 @@ public class BaseBundle { * @param key a String, or null * @return a byte[] value, or null */ - byte[] getByteArray(String key) { + @Nullable + byte[] getByteArray(@Nullable String key) { unparcel(); Object o = mMap.get(key); if (o == null) { @@ -1116,7 +1125,8 @@ public class BaseBundle { * @param key a String, or null * @return a short[] value, or null */ - short[] getShortArray(String key) { + @Nullable + short[] getShortArray(@Nullable String key) { unparcel(); Object o = mMap.get(key); if (o == null) { @@ -1138,7 +1148,8 @@ public class BaseBundle { * @param key a String, or null * @return a char[] value, or null */ - char[] getCharArray(String key) { + @Nullable + char[] getCharArray(@Nullable String key) { unparcel(); Object o = mMap.get(key); if (o == null) { @@ -1160,7 +1171,8 @@ public class BaseBundle { * @param key a String, or null * @return an int[] value, or null */ - public int[] getIntArray(String key) { + @Nullable + public int[] getIntArray(@Nullable String key) { unparcel(); Object o = mMap.get(key); if (o == null) { @@ -1182,7 +1194,8 @@ public class BaseBundle { * @param key a String, or null * @return a long[] value, or null */ - public long[] getLongArray(String key) { + @Nullable + public long[] getLongArray(@Nullable String key) { unparcel(); Object o = mMap.get(key); if (o == null) { @@ -1204,7 +1217,8 @@ public class BaseBundle { * @param key a String, or null * @return a float[] value, or null */ - float[] getFloatArray(String key) { + @Nullable + float[] getFloatArray(@Nullable String key) { unparcel(); Object o = mMap.get(key); if (o == null) { @@ -1226,7 +1240,8 @@ public class BaseBundle { * @param key a String, or null * @return a double[] value, or null */ - public double[] getDoubleArray(String key) { + @Nullable + public double[] getDoubleArray(@Nullable String key) { unparcel(); Object o = mMap.get(key); if (o == null) { @@ -1248,7 +1263,8 @@ public class BaseBundle { * @param key a String, or null * @return a String[] value, or null */ - public String[] getStringArray(String key) { + @Nullable + public String[] getStringArray(@Nullable String key) { unparcel(); Object o = mMap.get(key); if (o == null) { @@ -1270,7 +1286,8 @@ public class BaseBundle { * @param key a String, or null * @return a CharSequence[] value, or null */ - CharSequence[] getCharSequenceArray(String key) { + @Nullable + CharSequence[] getCharSequenceArray(@Nullable String key) { unparcel(); Object o = mMap.get(key); if (o == null) { diff --git a/core/java/android/os/BatteryManager.java b/core/java/android/os/BatteryManager.java index 537e993..bd5a392 100644 --- a/core/java/android/os/BatteryManager.java +++ b/core/java/android/os/BatteryManager.java @@ -162,7 +162,15 @@ public class BatteryManager { */ public static final int BATTERY_PROPERTY_ENERGY_COUNTER = 5; - private IBatteryPropertiesRegistrar mBatteryPropertiesRegistrar; + private final IBatteryPropertiesRegistrar mBatteryPropertiesRegistrar; + + /** + * @removed Was previously made visible by accident. + */ + public BatteryManager() { + mBatteryPropertiesRegistrar = IBatteryPropertiesRegistrar.Stub.asInterface( + ServiceManager.getService("batteryproperties")); + } /** * Query a battery property from the batteryproperties service. @@ -174,12 +182,7 @@ public class BatteryManager { long ret; if (mBatteryPropertiesRegistrar == null) { - IBinder b = ServiceManager.getService("batteryproperties"); - mBatteryPropertiesRegistrar = - IBatteryPropertiesRegistrar.Stub.asInterface(b); - - if (mBatteryPropertiesRegistrar == null) - return Long.MIN_VALUE; + return Long.MIN_VALUE; } try { diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java index cd45cfb..d96a0e9 100644 --- a/core/java/android/os/BatteryStats.java +++ b/core/java/android/os/BatteryStats.java @@ -30,6 +30,7 @@ import android.content.pm.ApplicationInfo; import android.telephony.SignalStrength; import android.text.format.DateFormat; import android.util.Printer; +import android.util.Slog; import android.util.SparseArray; import android.util.SparseIntArray; import android.util.TimeUtils; @@ -85,10 +86,10 @@ public abstract class BatteryStats implements Parcelable { */ public static final int WIFI_SCAN = 6; - /** - * A constant indicating a wifi multicast timer - */ - public static final int WIFI_MULTICAST_ENABLED = 7; + /** + * A constant indicating a wifi multicast timer + */ + public static final int WIFI_MULTICAST_ENABLED = 7; /** * A constant indicating a video turn on timer @@ -352,7 +353,9 @@ public abstract class BatteryStats implements Parcelable { public abstract long getWifiRunningTime(long elapsedRealtimeUs, int which); public abstract long getFullWifiLockTime(long elapsedRealtimeUs, int which); public abstract long getWifiScanTime(long elapsedRealtimeUs, int which); + public abstract int getWifiScanCount(int which); public abstract long getWifiBatchedScanTime(int csphBin, long elapsedRealtimeUs, int which); + public abstract int getWifiBatchedScanCount(int csphBin, int which); public abstract long getWifiMulticastTime(long elapsedRealtimeUs, int which); public abstract long getAudioTurnedOnTime(long elapsedRealtimeUs, int which); public abstract long getVideoTurnedOnTime(long elapsedRealtimeUs, int which); @@ -438,14 +441,14 @@ public abstract class BatteryStats implements Parcelable { public abstract boolean isActive(); /** - * Returns the total time (in 1/100 sec) spent executing in user code. + * Returns the total time (in milliseconds) spent executing in user code. * * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. */ public abstract long getUserTime(int which); /** - * Returns the total time (in 1/100 sec) spent executing in system code. + * Returns the total time (in milliseconds) spent executing in system code. * * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. */ @@ -473,14 +476,14 @@ public abstract class BatteryStats implements Parcelable { public abstract int getNumAnrs(int which); /** - * Returns the cpu time spent in microseconds while the process was in the foreground. + * Returns the cpu time (milliseconds) spent while the process was in the foreground. * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. * @return foreground cpu time in microseconds */ public abstract long getForegroundTime(int which); /** - * Returns the approximate cpu time spent in microseconds, at a certain CPU speed. + * Returns the approximate cpu time (in milliseconds) spent at a certain CPU speed. * @param speedStep the index of the CPU speed. This is not the actual speed of the * CPU. * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. @@ -542,6 +545,297 @@ public abstract class BatteryStats implements Parcelable { } } + public static final class LevelStepTracker { + public long mLastStepTime = -1; + public int mNumStepDurations; + public final long[] mStepDurations; + + public LevelStepTracker(int maxLevelSteps) { + mStepDurations = new long[maxLevelSteps]; + } + + public LevelStepTracker(int numSteps, long[] steps) { + mNumStepDurations = numSteps; + mStepDurations = new long[numSteps]; + System.arraycopy(steps, 0, mStepDurations, 0, numSteps); + } + + public long getDurationAt(int index) { + return mStepDurations[index] & STEP_LEVEL_TIME_MASK; + } + + public int getLevelAt(int index) { + return (int)((mStepDurations[index] & STEP_LEVEL_LEVEL_MASK) + >> STEP_LEVEL_LEVEL_SHIFT); + } + + public int getInitModeAt(int index) { + return (int)((mStepDurations[index] & STEP_LEVEL_INITIAL_MODE_MASK) + >> STEP_LEVEL_INITIAL_MODE_SHIFT); + } + + public int getModModeAt(int index) { + return (int)((mStepDurations[index] & STEP_LEVEL_MODIFIED_MODE_MASK) + >> STEP_LEVEL_MODIFIED_MODE_SHIFT); + } + + private void appendHex(long val, int topOffset, StringBuilder out) { + boolean hasData = false; + while (topOffset >= 0) { + int digit = (int)( (val>>topOffset) & 0xf ); + topOffset -= 4; + if (!hasData && digit == 0) { + continue; + } + hasData = true; + if (digit >= 0 && digit <= 9) { + out.append((char)('0' + digit)); + } else { + out.append((char)('a' + digit - 10)); + } + } + } + + public void encodeEntryAt(int index, StringBuilder out) { + long item = mStepDurations[index]; + long duration = item & STEP_LEVEL_TIME_MASK; + int level = (int)((item & STEP_LEVEL_LEVEL_MASK) + >> STEP_LEVEL_LEVEL_SHIFT); + int initMode = (int)((item & STEP_LEVEL_INITIAL_MODE_MASK) + >> STEP_LEVEL_INITIAL_MODE_SHIFT); + int modMode = (int)((item & STEP_LEVEL_MODIFIED_MODE_MASK) + >> STEP_LEVEL_MODIFIED_MODE_SHIFT); + switch ((initMode&STEP_LEVEL_MODE_SCREEN_STATE) + 1) { + case Display.STATE_OFF: out.append('f'); break; + case Display.STATE_ON: out.append('o'); break; + case Display.STATE_DOZE: out.append('d'); break; + case Display.STATE_DOZE_SUSPEND: out.append('z'); break; + } + if ((initMode&STEP_LEVEL_MODE_POWER_SAVE) != 0) { + out.append('p'); + } + switch ((modMode&STEP_LEVEL_MODE_SCREEN_STATE) + 1) { + case Display.STATE_OFF: out.append('F'); break; + case Display.STATE_ON: out.append('O'); break; + case Display.STATE_DOZE: out.append('D'); break; + case Display.STATE_DOZE_SUSPEND: out.append('Z'); break; + } + if ((modMode&STEP_LEVEL_MODE_POWER_SAVE) != 0) { + out.append('P'); + } + out.append('-'); + appendHex(level, 4, out); + out.append('-'); + appendHex(duration, STEP_LEVEL_LEVEL_SHIFT-4, out); + } + + public void decodeEntryAt(int index, String value) { + final int N = value.length(); + int i = 0; + char c; + long out = 0; + while (i < N && (c=value.charAt(i)) != '-') { + i++; + switch (c) { + case 'f': out |= (((long)Display.STATE_OFF-1)<<STEP_LEVEL_INITIAL_MODE_SHIFT); + break; + case 'o': out |= (((long)Display.STATE_ON-1)<<STEP_LEVEL_INITIAL_MODE_SHIFT); + break; + case 'd': out |= (((long)Display.STATE_DOZE-1)<<STEP_LEVEL_INITIAL_MODE_SHIFT); + break; + case 'z': out |= (((long)Display.STATE_DOZE_SUSPEND-1) + << STEP_LEVEL_INITIAL_MODE_SHIFT); + break; + case 'p': out |= (((long)STEP_LEVEL_MODE_POWER_SAVE) + << STEP_LEVEL_INITIAL_MODE_SHIFT); + break; + case 'F': out |= (((long)Display.STATE_OFF-1)<<STEP_LEVEL_MODIFIED_MODE_SHIFT); + break; + case 'O': out |= (((long)Display.STATE_ON-1)<<STEP_LEVEL_MODIFIED_MODE_SHIFT); + break; + case 'D': out |= (((long)Display.STATE_DOZE-1)<<STEP_LEVEL_MODIFIED_MODE_SHIFT); + break; + case 'Z': out |= (((long)Display.STATE_DOZE_SUSPEND-1) + << STEP_LEVEL_MODIFIED_MODE_SHIFT); + break; + case 'P': out |= (((long)STEP_LEVEL_MODE_POWER_SAVE) + << STEP_LEVEL_MODIFIED_MODE_SHIFT); + break; + } + } + i++; + long level = 0; + while (i < N && (c=value.charAt(i)) != '-') { + i++; + level <<= 4; + if (c >= '0' && c <= '9') { + level += c - '0'; + } else if (c >= 'a' && c <= 'f') { + level += c - 'a' + 10; + } else if (c >= 'A' && c <= 'F') { + level += c - 'A' + 10; + } + } + i++; + out |= (level << STEP_LEVEL_LEVEL_SHIFT) & STEP_LEVEL_LEVEL_MASK; + long duration = 0; + while (i < N && (c=value.charAt(i)) != '-') { + i++; + duration <<= 4; + if (c >= '0' && c <= '9') { + duration += c - '0'; + } else if (c >= 'a' && c <= 'f') { + duration += c - 'a' + 10; + } else if (c >= 'A' && c <= 'F') { + duration += c - 'A' + 10; + } + } + mStepDurations[index] = out | (duration & STEP_LEVEL_TIME_MASK); + } + + public void init() { + mLastStepTime = -1; + mNumStepDurations = 0; + } + + public void clearTime() { + mLastStepTime = -1; + } + + public long computeTimePerLevel() { + final long[] steps = mStepDurations; + final int numSteps = mNumStepDurations; + + // For now we'll do a simple average across all steps. + if (numSteps <= 0) { + return -1; + } + long total = 0; + for (int i=0; i<numSteps; i++) { + total += steps[i] & STEP_LEVEL_TIME_MASK; + } + return total / numSteps; + /* + long[] buckets = new long[numSteps]; + int numBuckets = 0; + int numToAverage = 4; + int i = 0; + while (i < numSteps) { + long totalTime = 0; + int num = 0; + for (int j=0; j<numToAverage && (i+j)<numSteps; j++) { + totalTime += steps[i+j] & STEP_LEVEL_TIME_MASK; + num++; + } + buckets[numBuckets] = totalTime / num; + numBuckets++; + numToAverage *= 2; + i += num; + } + if (numBuckets < 1) { + return -1; + } + long averageTime = buckets[numBuckets-1]; + for (i=numBuckets-2; i>=0; i--) { + averageTime = (averageTime + buckets[i]) / 2; + } + return averageTime; + */ + } + + public long computeTimeEstimate(long modesOfInterest, long modeValues, + int[] outNumOfInterest) { + final long[] steps = mStepDurations; + final int count = mNumStepDurations; + if (count <= 0) { + return -1; + } + long total = 0; + int numOfInterest = 0; + for (int i=0; i<count; i++) { + long initMode = (steps[i] & STEP_LEVEL_INITIAL_MODE_MASK) + >> STEP_LEVEL_INITIAL_MODE_SHIFT; + long modMode = (steps[i] & STEP_LEVEL_MODIFIED_MODE_MASK) + >> STEP_LEVEL_MODIFIED_MODE_SHIFT; + // If the modes of interest didn't change during this step period... + if ((modMode&modesOfInterest) == 0) { + // And the mode values during this period match those we are measuring... + if ((initMode&modesOfInterest) == modeValues) { + // Then this can be used to estimate the total time! + numOfInterest++; + total += steps[i] & STEP_LEVEL_TIME_MASK; + } + } + } + if (numOfInterest <= 0) { + return -1; + } + + if (outNumOfInterest != null) { + outNumOfInterest[0] = numOfInterest; + } + + // The estimated time is the average time we spend in each level, multipled + // by 100 -- the total number of battery levels + return (total / numOfInterest) * 100; + } + + public void addLevelSteps(int numStepLevels, long modeBits, long elapsedRealtime) { + int stepCount = mNumStepDurations; + final long lastStepTime = mLastStepTime; + if (lastStepTime >= 0 && numStepLevels > 0) { + final long[] steps = mStepDurations; + long duration = elapsedRealtime - lastStepTime; + for (int i=0; i<numStepLevels; i++) { + System.arraycopy(steps, 0, steps, 1, steps.length-1); + long thisDuration = duration / (numStepLevels-i); + duration -= thisDuration; + if (thisDuration > STEP_LEVEL_TIME_MASK) { + thisDuration = STEP_LEVEL_TIME_MASK; + } + steps[0] = thisDuration | modeBits; + } + stepCount += numStepLevels; + if (stepCount > steps.length) { + stepCount = steps.length; + } + } + mNumStepDurations = stepCount; + mLastStepTime = elapsedRealtime; + } + + public void readFromParcel(Parcel in) { + final int N = in.readInt(); + mNumStepDurations = N; + for (int i=0; i<N; i++) { + mStepDurations[i] = in.readLong(); + } + } + + public void writeToParcel(Parcel out) { + final int N = mNumStepDurations; + out.writeInt(N); + for (int i=0; i<N; i++) { + out.writeLong(mStepDurations[i]); + } + } + } + + public static final class DailyItem { + public long mStartTime; + public long mEndTime; + public LevelStepTracker mDischargeSteps; + public LevelStepTracker mChargeSteps; + } + + public abstract DailyItem getDailyItemLocked(int daysAgo); + + public abstract long getCurrentDailyStartTime(); + + public abstract long getNextMinDailyDeadline(); + + public abstract long getNextMaxDailyDeadline(); + public final static class HistoryTag { public String string; public int uid; @@ -592,6 +886,86 @@ public abstract class BatteryStats implements Parcelable { } } + /** + * Optional detailed information that can go into a history step. This is typically + * generated each time the battery level changes. + */ + public final static class HistoryStepDetails { + // Time (in 1/100 second) spent in user space and the kernel since the last step. + public int userTime; + public int systemTime; + + // Top three apps using CPU in the last step, with times in 1/100 second. + public int appCpuUid1; + public int appCpuUTime1; + public int appCpuSTime1; + public int appCpuUid2; + public int appCpuUTime2; + public int appCpuSTime2; + public int appCpuUid3; + public int appCpuUTime3; + public int appCpuSTime3; + + // Information from /proc/stat + public int statUserTime; + public int statSystemTime; + public int statIOWaitTime; + public int statIrqTime; + public int statSoftIrqTime; + public int statIdlTime; + + public HistoryStepDetails() { + clear(); + } + + public void clear() { + userTime = systemTime = 0; + appCpuUid1 = appCpuUid2 = appCpuUid3 = -1; + appCpuUTime1 = appCpuSTime1 = appCpuUTime2 = appCpuSTime2 + = appCpuUTime3 = appCpuSTime3 = 0; + } + + public void writeToParcel(Parcel out) { + out.writeInt(userTime); + out.writeInt(systemTime); + out.writeInt(appCpuUid1); + out.writeInt(appCpuUTime1); + out.writeInt(appCpuSTime1); + out.writeInt(appCpuUid2); + out.writeInt(appCpuUTime2); + out.writeInt(appCpuSTime2); + out.writeInt(appCpuUid3); + out.writeInt(appCpuUTime3); + out.writeInt(appCpuSTime3); + out.writeInt(statUserTime); + out.writeInt(statSystemTime); + out.writeInt(statIOWaitTime); + out.writeInt(statIrqTime); + out.writeInt(statSoftIrqTime); + out.writeInt(statIdlTime); + } + + public void readFromParcel(Parcel in) { + userTime = in.readInt(); + systemTime = in.readInt(); + appCpuUid1 = in.readInt(); + appCpuUTime1 = in.readInt(); + appCpuSTime1 = in.readInt(); + appCpuUid2 = in.readInt(); + appCpuUTime2 = in.readInt(); + appCpuSTime2 = in.readInt(); + appCpuUid3 = in.readInt(); + appCpuUTime3 = in.readInt(); + appCpuSTime3 = in.readInt(); + statUserTime = in.readInt(); + statSystemTime = in.readInt(); + statIOWaitTime = in.readInt(); + statIrqTime = in.readInt(); + statSoftIrqTime = in.readInt(); + statIdlTime = in.readInt(); + } + } + public final static class HistoryItem implements Parcelable { public HistoryItem next; @@ -687,6 +1061,9 @@ public abstract class BatteryStats implements Parcelable { // Kernel wakeup reason at this point. public HistoryTag wakeReasonTag; + // Non-null when there is more detailed information at this step. + public HistoryStepDetails stepDetails; + public static final int EVENT_FLAG_START = 0x8000; public static final int EVENT_FLAG_FINISH = 0x4000; @@ -1481,6 +1858,15 @@ public abstract class BatteryStats implements Parcelable { public abstract long getNetworkActivityBytes(int type, int which); public abstract long getNetworkActivityPackets(int type, int which); + public static final int CONTROLLER_IDLE_TIME = 0; + public static final int CONTROLLER_RX_TIME = 1; + public static final int CONTROLLER_TX_TIME = 2; + public static final int CONTROLLER_ENERGY = 3; + public static final int NUM_CONTROLLER_ACTIVITY_TYPES = CONTROLLER_ENERGY + 1; + + public abstract long getBluetoothControllerActivity(int type, int which); + public abstract long getWifiControllerActivity(int type, int which); + /** * Return the wall clock time when battery stats data collection started. */ @@ -1641,15 +2027,15 @@ public abstract class BatteryStats implements Parcelable { // Bits in a step duration that are the new battery level we are at. public static final long STEP_LEVEL_LEVEL_MASK = 0x0000ff0000000000L; - public static final long STEP_LEVEL_LEVEL_SHIFT = 40; + public static final int STEP_LEVEL_LEVEL_SHIFT = 40; // Bits in a step duration that are the initial mode we were in at that step. public static final long STEP_LEVEL_INITIAL_MODE_MASK = 0x00ff000000000000L; - public static final long STEP_LEVEL_INITIAL_MODE_SHIFT = 48; + public static final int STEP_LEVEL_INITIAL_MODE_SHIFT = 48; // Bits in a step duration that indicate which modes changed during that step. public static final long STEP_LEVEL_MODIFIED_MODE_MASK = 0xff00000000000000L; - public static final long STEP_LEVEL_MODIFIED_MODE_SHIFT = 56; + public static final int STEP_LEVEL_MODIFIED_MODE_SHIFT = 56; // Step duration mode: the screen is on, off, dozed, etc; value is Display.STATE_* - 1. public static final int STEP_LEVEL_MODE_SCREEN_STATE = 0x03; @@ -1657,17 +2043,56 @@ public abstract class BatteryStats implements Parcelable { // Step duration mode: power save is on. public static final int STEP_LEVEL_MODE_POWER_SAVE = 0x04; + public static final int[] STEP_LEVEL_MODES_OF_INTEREST = new int[] { + STEP_LEVEL_MODE_SCREEN_STATE|STEP_LEVEL_MODE_POWER_SAVE, + STEP_LEVEL_MODE_SCREEN_STATE|STEP_LEVEL_MODE_POWER_SAVE, + STEP_LEVEL_MODE_SCREEN_STATE|STEP_LEVEL_MODE_POWER_SAVE, + STEP_LEVEL_MODE_SCREEN_STATE|STEP_LEVEL_MODE_POWER_SAVE, + STEP_LEVEL_MODE_SCREEN_STATE|STEP_LEVEL_MODE_POWER_SAVE, + STEP_LEVEL_MODE_SCREEN_STATE|STEP_LEVEL_MODE_POWER_SAVE, + STEP_LEVEL_MODE_SCREEN_STATE|STEP_LEVEL_MODE_POWER_SAVE, + STEP_LEVEL_MODE_SCREEN_STATE|STEP_LEVEL_MODE_POWER_SAVE, + }; + public static final int[] STEP_LEVEL_MODE_VALUES = new int[] { + (Display.STATE_OFF-1), + (Display.STATE_OFF-1)|STEP_LEVEL_MODE_POWER_SAVE, + (Display.STATE_ON-1), + (Display.STATE_ON-1)|STEP_LEVEL_MODE_POWER_SAVE, + (Display.STATE_DOZE-1), + (Display.STATE_DOZE-1)|STEP_LEVEL_MODE_POWER_SAVE, + (Display.STATE_DOZE_SUSPEND-1), + (Display.STATE_DOZE_SUSPEND-1)|STEP_LEVEL_MODE_POWER_SAVE, + }; + public static final String[] STEP_LEVEL_MODE_LABELS = new String[] { + "screen off", + "screen off power save", + "screen on", + "screen on power save", + "screen doze", + "screen doze power save", + "screen doze-suspend", + "screen doze-suspend power save", + }; + public static final String[] STEP_LEVEL_MODE_TAGS = new String[] { + "off", + "off-save", + "on", + "on-save", + "doze", + "doze-save", + "susp", + "susp-save", + }; + /** - * Return the historical number of discharge steps we currently have. + * Return the array of discharge step durations. */ - public abstract int getNumDischargeStepDurations(); + public abstract LevelStepTracker getDischargeLevelStepTracker(); /** - * Return the array of discharge step durations; the number of valid - * items in it is returned by {@link #getNumDischargeStepDurations()}. - * These values are in milliseconds. + * Return the array of daily discharge step durations. */ - public abstract long[] getDischargeStepDurationsArray(); + public abstract LevelStepTracker getDailyDischargeLevelStepTracker(); /** * Compute an approximation for how much time (in microseconds) remains until the battery @@ -1680,16 +2105,14 @@ public abstract class BatteryStats implements Parcelable { public abstract long computeChargeTimeRemaining(long curTime); /** - * Return the historical number of charge steps we currently have. + * Return the array of charge step durations. */ - public abstract int getNumChargeStepDurations(); + public abstract LevelStepTracker getChargeLevelStepTracker(); /** - * Return the array of charge step durations; the number of valid - * items in it is returned by {@link #getNumChargeStepDurations()}. - * These values are in milliseconds. + * Return the array of daily charge step durations. */ - public abstract long[] getChargeStepDurationsArray(); + public abstract LevelStepTracker getDailyChargeLevelStepTracker(); public abstract Map<String, ? extends Timer> getWakeupReasonStats(); @@ -1728,13 +2151,6 @@ public abstract class BatteryStats implements Parcelable { } } - public final static void formatTime(StringBuilder sb, long time) { - long sec = time / 100; - formatTimeRaw(sb, sec); - sb.append((time - (sec * 100)) * 10); - sb.append("ms "); - } - public final static void formatTimeMs(StringBuilder sb, long time) { long sec = time / 1000; formatTimeRaw(sb, sec); @@ -2160,6 +2576,7 @@ public abstract class BatteryStats implements Parcelable { long wifiPacketsTx = u.getNetworkActivityPackets(NETWORK_WIFI_TX_DATA, which); long fullWifiLockOnTime = u.getFullWifiLockTime(rawRealtime, which); long wifiScanTime = u.getWifiScanTime(rawRealtime, which); + int wifiScanCount = u.getWifiScanCount(which); long uidWifiRunningTime = u.getWifiRunningTime(rawRealtime, which); if (mobileBytesRx > 0 || mobileBytesTx > 0 || wifiBytesRx > 0 || wifiBytesTx > 0 @@ -2172,10 +2589,10 @@ public abstract class BatteryStats implements Parcelable { mobileActiveTime, mobileActiveCount); } - if (fullWifiLockOnTime != 0 || wifiScanTime != 0 + if (fullWifiLockOnTime != 0 || wifiScanTime != 0 || wifiScanCount != 0 || uidWifiRunningTime != 0) { dumpLine(pw, uid, category, WIFI_DATA, - fullWifiLockOnTime, wifiScanTime, uidWifiRunningTime); + fullWifiLockOnTime, wifiScanTime, uidWifiRunningTime, wifiScanCount); } if (u.hasUserActivity()) { @@ -2293,9 +2710,9 @@ public abstract class BatteryStats implements Parcelable { : processStats.entrySet()) { Uid.Proc ps = ent.getValue(); - final long userMillis = ps.getUserTime(which) * 10; - final long systemMillis = ps.getSystemTime(which) * 10; - final long foregroundMillis = ps.getForegroundTime(which) * 10; + final long userMillis = ps.getUserTime(which); + final long systemMillis = ps.getSystemTime(which); + final long foregroundMillis = ps.getForegroundTime(which); final int starts = ps.getStarts(which); final int numCrashes = ps.getNumCrashes(which); final int numAnrs = ps.getNumAnrs(which); @@ -2734,6 +3151,35 @@ public abstract class BatteryStats implements Parcelable { if (!didOne) sb.append(" (no activity)"); pw.println(sb.toString()); + final long wifiIdleTimeMs = getBluetoothControllerActivity(CONTROLLER_IDLE_TIME, which); + final long wifiRxTimeMs = getBluetoothControllerActivity(CONTROLLER_RX_TIME, which); + final long wifiTxTimeMs = getBluetoothControllerActivity(CONTROLLER_TX_TIME, which); + final long wifiTotalTimeMs = wifiIdleTimeMs + wifiRxTimeMs + wifiTxTimeMs; + + sb.setLength(0); + sb.append(prefix); + sb.append(" WiFi Idle time: "); formatTimeMs(sb, wifiIdleTimeMs); + sb.append(" ("); + sb.append(formatRatioLocked(wifiIdleTimeMs, wifiTotalTimeMs)); + sb.append(")"); + pw.println(sb.toString()); + + sb.setLength(0); + sb.append(prefix); + sb.append(" WiFi Rx time: "); formatTimeMs(sb, wifiRxTimeMs); + sb.append(" ("); + sb.append(formatRatioLocked(wifiRxTimeMs, wifiTotalTimeMs)); + sb.append(")"); + pw.println(sb.toString()); + + sb.setLength(0); + sb.append(prefix); + sb.append(" WiFi Tx time: "); formatTimeMs(sb, wifiTxTimeMs); + sb.append(" ("); + sb.append(formatRatioLocked(wifiTxTimeMs, wifiTotalTimeMs)); + sb.append(")"); + pw.println(sb.toString()); + sb.setLength(0); sb.append(prefix); sb.append(" Bluetooth on: "); formatTimeMs(sb, bluetoothOnTime / 1000); @@ -2761,9 +3207,41 @@ public abstract class BatteryStats implements Parcelable { sb.append(getPhoneDataConnectionCount(i, which)); sb.append("x"); } + if (!didOne) sb.append(" (no activity)"); pw.println(sb.toString()); + final long bluetoothIdleTimeMs = + getBluetoothControllerActivity(CONTROLLER_IDLE_TIME, which); + final long bluetoothRxTimeMs = getBluetoothControllerActivity(CONTROLLER_RX_TIME, which); + final long bluetoothTxTimeMs = getBluetoothControllerActivity(CONTROLLER_TX_TIME, which); + final long bluetoothTotalTimeMs = bluetoothIdleTimeMs + bluetoothRxTimeMs + + bluetoothTxTimeMs; + + sb.setLength(0); + sb.append(prefix); + sb.append(" Bluetooth Idle time: "); formatTimeMs(sb, bluetoothIdleTimeMs); + sb.append(" ("); + sb.append(formatRatioLocked(bluetoothIdleTimeMs, bluetoothTotalTimeMs)); + sb.append(")"); + pw.println(sb.toString()); + + sb.setLength(0); + sb.append(prefix); + sb.append(" Bluetooth Rx time: "); formatTimeMs(sb, bluetoothRxTimeMs); + sb.append(" ("); + sb.append(formatRatioLocked(bluetoothRxTimeMs, bluetoothTotalTimeMs)); + sb.append(")"); + pw.println(sb.toString()); + + sb.setLength(0); + sb.append(prefix); + sb.append(" Bluetooth Tx time: "); formatTimeMs(sb, bluetoothTxTimeMs); + sb.append(" ("); + sb.append(formatRatioLocked(bluetoothTxTimeMs, bluetoothTotalTimeMs)); + sb.append(")"); + pw.println(sb.toString()); + pw.println(); if (which == STATS_SINCE_UNPLUGGED) { @@ -3008,6 +3486,7 @@ public abstract class BatteryStats implements Parcelable { long wifiTxPackets = u.getNetworkActivityPackets(NETWORK_WIFI_TX_DATA, which); long fullWifiLockOnTime = u.getFullWifiLockTime(rawRealtime, which); long wifiScanTime = u.getWifiScanTime(rawRealtime, which); + int wifiScanCount = u.getWifiScanCount(which); long uidWifiRunningTime = u.getWifiRunningTime(rawRealtime, which); if (mobileRxBytes > 0 || mobileTxBytes > 0 @@ -3043,7 +3522,7 @@ public abstract class BatteryStats implements Parcelable { pw.print(" received, "); pw.print(wifiTxPackets); pw.println(" sent)"); } - if (fullWifiLockOnTime != 0 || wifiScanTime != 0 + if (fullWifiLockOnTime != 0 || wifiScanTime != 0 || wifiScanCount != 0 || uidWifiRunningTime != 0) { sb.setLength(0); sb.append(prefix); sb.append(" Wifi Running: "); @@ -3057,7 +3536,9 @@ public abstract class BatteryStats implements Parcelable { sb.append(prefix); sb.append(" Wifi Scan: "); formatTimeMs(sb, wifiScanTime / 1000); sb.append("("); sb.append(formatRatioLocked(wifiScanTime, - whichBatteryRealtime)); sb.append(")"); + whichBatteryRealtime)); sb.append(") "); + sb.append(wifiScanCount); + sb.append("x"); pw.println(sb.toString()); } @@ -3316,9 +3797,9 @@ public abstract class BatteryStats implements Parcelable { sb.append(prefix); sb.append(" Proc "); sb.append(ent.getKey()); sb.append(":\n"); sb.append(prefix); sb.append(" CPU: "); - formatTime(sb, userTime); sb.append("usr + "); - formatTime(sb, systemTime); sb.append("krn ; "); - formatTime(sb, foregroundTime); sb.append("fg"); + formatTimeMs(sb, userTime); sb.append("usr + "); + formatTimeMs(sb, systemTime); sb.append("krn ; "); + formatTimeMs(sb, foregroundTime); sb.append("fg"); if (starts != 0 || numCrashes != 0 || numAnrs != 0) { sb.append("\n"); sb.append(prefix); sb.append(" "); boolean hasOne = false; @@ -3692,10 +4173,115 @@ public abstract class BatteryStats implements Parcelable { } } pw.println(); + if (rec.stepDetails != null) { + if (!checkin) { + pw.print(" Details: cpu="); + pw.print(rec.stepDetails.userTime); + pw.print("u+"); + pw.print(rec.stepDetails.systemTime); + pw.print("s"); + if (rec.stepDetails.appCpuUid1 >= 0) { + pw.print(" ("); + printStepCpuUidDetails(pw, rec.stepDetails.appCpuUid1, + rec.stepDetails.appCpuUTime1, rec.stepDetails.appCpuSTime1); + if (rec.stepDetails.appCpuUid2 >= 0) { + pw.print(", "); + printStepCpuUidDetails(pw, rec.stepDetails.appCpuUid2, + rec.stepDetails.appCpuUTime2, rec.stepDetails.appCpuSTime2); + } + if (rec.stepDetails.appCpuUid3 >= 0) { + pw.print(", "); + printStepCpuUidDetails(pw, rec.stepDetails.appCpuUid3, + rec.stepDetails.appCpuUTime3, rec.stepDetails.appCpuSTime3); + } + pw.print(')'); + } + pw.println(); + pw.print(" /proc/stat="); + pw.print(rec.stepDetails.statUserTime); + pw.print(" usr, "); + pw.print(rec.stepDetails.statSystemTime); + pw.print(" sys, "); + pw.print(rec.stepDetails.statIOWaitTime); + pw.print(" io, "); + pw.print(rec.stepDetails.statIrqTime); + pw.print(" irq, "); + pw.print(rec.stepDetails.statSoftIrqTime); + pw.print(" sirq, "); + pw.print(rec.stepDetails.statIdlTime); + pw.print(" idle"); + int totalRun = rec.stepDetails.statUserTime + rec.stepDetails.statSystemTime + + rec.stepDetails.statIOWaitTime + rec.stepDetails.statIrqTime + + rec.stepDetails.statSoftIrqTime; + int total = totalRun + rec.stepDetails.statIdlTime; + if (total > 0) { + pw.print(" ("); + float perc = ((float)totalRun) / ((float)total) * 100; + pw.print(String.format("%.1f%%", perc)); + pw.print(" of "); + StringBuilder sb = new StringBuilder(64); + formatTimeMsNoSpace(sb, total*10); + pw.print(sb); + pw.print(")"); + } + pw.println(); + } else { + pw.print(BATTERY_STATS_CHECKIN_VERSION); pw.print(','); + pw.print(HISTORY_DATA); pw.print(",0,Dcpu="); + pw.print(rec.stepDetails.userTime); + pw.print(":"); + pw.print(rec.stepDetails.systemTime); + if (rec.stepDetails.appCpuUid1 >= 0) { + printStepCpuUidCheckinDetails(pw, rec.stepDetails.appCpuUid1, + rec.stepDetails.appCpuUTime1, rec.stepDetails.appCpuSTime1); + if (rec.stepDetails.appCpuUid2 >= 0) { + printStepCpuUidCheckinDetails(pw, rec.stepDetails.appCpuUid2, + rec.stepDetails.appCpuUTime2, rec.stepDetails.appCpuSTime2); + } + if (rec.stepDetails.appCpuUid3 >= 0) { + printStepCpuUidCheckinDetails(pw, rec.stepDetails.appCpuUid3, + rec.stepDetails.appCpuUTime3, rec.stepDetails.appCpuSTime3); + } + } + pw.println(); + pw.print(BATTERY_STATS_CHECKIN_VERSION); pw.print(','); + pw.print(HISTORY_DATA); pw.print(",0,Dpst="); + pw.print(rec.stepDetails.statUserTime); + pw.print(','); + pw.print(rec.stepDetails.statSystemTime); + pw.print(','); + pw.print(rec.stepDetails.statIOWaitTime); + pw.print(','); + pw.print(rec.stepDetails.statIrqTime); + pw.print(','); + pw.print(rec.stepDetails.statSoftIrqTime); + pw.print(','); + pw.print(rec.stepDetails.statIdlTime); + pw.println(); + } + } oldState = rec.states; oldState2 = rec.states2; } } + + private void printStepCpuUidDetails(PrintWriter pw, int uid, int utime, int stime) { + UserHandle.formatUid(pw, uid); + pw.print("="); + pw.print(utime); + pw.print("u+"); + pw.print(stime); + pw.print("s"); + } + + private void printStepCpuUidCheckinDetails(PrintWriter pw, int uid, int utime, int stime) { + pw.print('/'); + pw.print(uid); + pw.print(":"); + pw.print(utime); + pw.print(":"); + pw.print(stime); + } } private void printSizeValue(PrintWriter pw, long size) { @@ -3725,47 +4311,27 @@ public abstract class BatteryStats implements Parcelable { pw.print(suffix); } - private static boolean dumpTimeEstimate(PrintWriter pw, String label, long[] steps, - int count, long modesOfInterest, long modeValues) { - if (count <= 0) { + private static boolean dumpTimeEstimate(PrintWriter pw, String label1, String label2, + String label3, long estimatedTime) { + if (estimatedTime < 0) { return false; } - long total = 0; - int numOfInterest = 0; - for (int i=0; i<count; i++) { - long initMode = (steps[i] & STEP_LEVEL_INITIAL_MODE_MASK) - >> STEP_LEVEL_INITIAL_MODE_SHIFT; - long modMode = (steps[i] & STEP_LEVEL_MODIFIED_MODE_MASK) - >> STEP_LEVEL_MODIFIED_MODE_SHIFT; - // If the modes of interest didn't change during this step period... - if ((modMode&modesOfInterest) == 0) { - // And the mode values during this period match those we are measuring... - if ((initMode&modesOfInterest) == modeValues) { - // Then this can be used to estimate the total time! - numOfInterest++; - total += steps[i] & STEP_LEVEL_TIME_MASK; - } - } - } - if (numOfInterest <= 0) { - return false; - } - - // The estimated time is the average time we spend in each level, multipled - // by 100 -- the total number of battery levels - long estimatedTime = (total / numOfInterest) * 100; - - pw.print(label); + pw.print(label1); + pw.print(label2); + pw.print(label3); StringBuilder sb = new StringBuilder(64); formatTimeMs(sb, estimatedTime); pw.print(sb); pw.println(); - return true; } - private static boolean dumpDurationSteps(PrintWriter pw, String header, long[] steps, - int count, boolean checkin) { + private static boolean dumpDurationSteps(PrintWriter pw, String prefix, String header, + LevelStepTracker steps, boolean checkin) { + if (steps == null) { + return false; + } + int count = steps.mNumStepDurations; if (count <= 0) { return false; } @@ -3774,13 +4340,10 @@ public abstract class BatteryStats implements Parcelable { } String[] lineArgs = new String[4]; for (int i=0; i<count; i++) { - long duration = steps[i] & STEP_LEVEL_TIME_MASK; - int level = (int)((steps[i] & STEP_LEVEL_LEVEL_MASK) - >> STEP_LEVEL_LEVEL_SHIFT); - long initMode = (steps[i] & STEP_LEVEL_INITIAL_MODE_MASK) - >> STEP_LEVEL_INITIAL_MODE_SHIFT; - long modMode = (steps[i] & STEP_LEVEL_MODIFIED_MODE_MASK) - >> STEP_LEVEL_MODIFIED_MODE_SHIFT; + long duration = steps.getDurationAt(i); + int level = steps.getLevelAt(i); + long initMode = steps.getInitModeAt(i); + long modMode = steps.getModModeAt(i); if (checkin) { lineArgs[0] = Long.toString(duration); lineArgs[1] = Integer.toString(level); @@ -3802,7 +4365,8 @@ public abstract class BatteryStats implements Parcelable { } dumpLine(pw, 0 /* uid */, "i" /* category */, header, (Object[])lineArgs); } else { - pw.print(" #"); pw.print(i); pw.print(": "); + pw.print(prefix); + pw.print("#"); pw.print(i); pw.print(": "); TimeUtils.formatDuration(duration, pw); pw.print(" to "); pw.print(level); boolean haveModes = false; @@ -3834,10 +4398,11 @@ public abstract class BatteryStats implements Parcelable { public static final int DUMP_UNPLUGGED_ONLY = 1<<0; public static final int DUMP_CHARGED_ONLY = 1<<1; - public static final int DUMP_HISTORY_ONLY = 1<<2; - public static final int DUMP_INCLUDE_HISTORY = 1<<3; - public static final int DUMP_VERBOSE = 1<<4; - public static final int DUMP_DEVICE_WIFI_ONLY = 1<<5; + public static final int DUMP_DAILY_ONLY = 1<<2; + public static final int DUMP_HISTORY_ONLY = 1<<3; + public static final int DUMP_INCLUDE_HISTORY = 1<<4; + public static final int DUMP_VERBOSE = 1<<5; + public static final int DUMP_DEVICE_WIFI_ONLY = 1<<6; private void dumpHistoryLocked(PrintWriter pw, int flags, long histStart, boolean checkin) { final HistoryPrinter hprinter = new HistoryPrinter(); @@ -3923,6 +4488,36 @@ public abstract class BatteryStats implements Parcelable { } } + private void dumpDailyLevelStepSummary(PrintWriter pw, String prefix, String label, + LevelStepTracker steps, StringBuilder tmpSb, int[] tmpOutInt) { + if (steps == null) { + return; + } + long timeRemaining = steps.computeTimeEstimate(0, 0, tmpOutInt); + if (timeRemaining >= 0) { + pw.print(prefix); pw.print(label); pw.print(" total time: "); + tmpSb.setLength(0); + formatTimeMs(tmpSb, timeRemaining); + pw.print(tmpSb); + pw.print(" (from "); pw.print(tmpOutInt[0]); + pw.println(" steps)"); + } + for (int i=0; i< STEP_LEVEL_MODES_OF_INTEREST.length; i++) { + long estimatedTime = steps.computeTimeEstimate(STEP_LEVEL_MODES_OF_INTEREST[i], + STEP_LEVEL_MODE_VALUES[i], tmpOutInt); + if (estimatedTime > 0) { + pw.print(prefix); pw.print(label); pw.print(" "); + pw.print(STEP_LEVEL_MODE_LABELS[i]); + pw.print(" time: "); + tmpSb.setLength(0); + formatTimeMs(tmpSb, estimatedTime); + pw.print(tmpSb); + pw.print(" (from "); pw.print(tmpOutInt[0]); + pw.println(" steps)"); + } + } + } + /** * Dumps a human-readable summary of the battery statistics to the given PrintWriter. * @@ -3932,8 +4527,8 @@ public abstract class BatteryStats implements Parcelable { public void dumpLocked(Context context, PrintWriter pw, int flags, int reqUid, long histStart) { prepareForDumpLocked(); - final boolean filtering = - (flags&(DUMP_HISTORY_ONLY|DUMP_UNPLUGGED_ONLY|DUMP_CHARGED_ONLY)) != 0; + final boolean filtering = (flags + & (DUMP_HISTORY_ONLY|DUMP_UNPLUGGED_ONLY|DUMP_CHARGED_ONLY|DUMP_DAILY_ONLY)) != 0; if ((flags&DUMP_HISTORY_ONLY) != 0 || !filtering) { final long historyTotalSize = getHistoryTotalSize(); @@ -3977,7 +4572,7 @@ public abstract class BatteryStats implements Parcelable { } } - if (filtering && (flags&(DUMP_UNPLUGGED_ONLY|DUMP_CHARGED_ONLY)) == 0) { + if (filtering && (flags&(DUMP_UNPLUGGED_ONLY|DUMP_CHARGED_ONLY|DUMP_DAILY_ONLY)) == 0) { return; } @@ -4011,50 +4606,24 @@ public abstract class BatteryStats implements Parcelable { } if (!filtering || (flags&DUMP_CHARGED_ONLY) != 0) { - if (dumpDurationSteps(pw, "Discharge step durations:", getDischargeStepDurationsArray(), - getNumDischargeStepDurations(), false)) { + if (dumpDurationSteps(pw, " ", "Discharge step durations:", + getDischargeLevelStepTracker(), false)) { long timeRemaining = computeBatteryTimeRemaining(SystemClock.elapsedRealtime()); if (timeRemaining >= 0) { pw.print(" Estimated discharge time remaining: "); TimeUtils.formatDuration(timeRemaining / 1000, pw); pw.println(); } - dumpTimeEstimate(pw, " Estimated screen off time: ", - getDischargeStepDurationsArray(), getNumDischargeStepDurations(), - STEP_LEVEL_MODE_SCREEN_STATE|STEP_LEVEL_MODE_POWER_SAVE, - (Display.STATE_OFF-1)); - dumpTimeEstimate(pw, " Estimated screen off power save time: ", - getDischargeStepDurationsArray(), getNumDischargeStepDurations(), - STEP_LEVEL_MODE_SCREEN_STATE|STEP_LEVEL_MODE_POWER_SAVE, - (Display.STATE_OFF-1)|STEP_LEVEL_MODE_POWER_SAVE); - dumpTimeEstimate(pw, " Estimated screen on time: ", - getDischargeStepDurationsArray(), getNumDischargeStepDurations(), - STEP_LEVEL_MODE_SCREEN_STATE|STEP_LEVEL_MODE_POWER_SAVE, - (Display.STATE_ON-1)); - dumpTimeEstimate(pw, " Estimated screen on power save time: ", - getDischargeStepDurationsArray(), getNumDischargeStepDurations(), - STEP_LEVEL_MODE_SCREEN_STATE|STEP_LEVEL_MODE_POWER_SAVE, - (Display.STATE_ON-1)|STEP_LEVEL_MODE_POWER_SAVE); - dumpTimeEstimate(pw, " Estimated screen doze time: ", - getDischargeStepDurationsArray(), getNumDischargeStepDurations(), - STEP_LEVEL_MODE_SCREEN_STATE|STEP_LEVEL_MODE_POWER_SAVE, - (Display.STATE_DOZE-1)); - dumpTimeEstimate(pw, " Estimated screen doze power save time: ", - getDischargeStepDurationsArray(), getNumDischargeStepDurations(), - STEP_LEVEL_MODE_SCREEN_STATE|STEP_LEVEL_MODE_POWER_SAVE, - (Display.STATE_DOZE-1)|STEP_LEVEL_MODE_POWER_SAVE); - dumpTimeEstimate(pw, " Estimated screen doze suspend time: ", - getDischargeStepDurationsArray(), getNumDischargeStepDurations(), - STEP_LEVEL_MODE_SCREEN_STATE|STEP_LEVEL_MODE_POWER_SAVE, - (Display.STATE_DOZE_SUSPEND-1)); - dumpTimeEstimate(pw, " Estimated screen doze suspend power save time: ", - getDischargeStepDurationsArray(), getNumDischargeStepDurations(), - STEP_LEVEL_MODE_SCREEN_STATE|STEP_LEVEL_MODE_POWER_SAVE, - (Display.STATE_DOZE_SUSPEND-1)|STEP_LEVEL_MODE_POWER_SAVE); + final LevelStepTracker steps = getDischargeLevelStepTracker(); + for (int i=0; i< STEP_LEVEL_MODES_OF_INTEREST.length; i++) { + dumpTimeEstimate(pw, " Estimated ", STEP_LEVEL_MODE_LABELS[i], " time: ", + steps.computeTimeEstimate(STEP_LEVEL_MODES_OF_INTEREST[i], + STEP_LEVEL_MODE_VALUES[i], null)); + } pw.println(); } - if (dumpDurationSteps(pw, "Charge step durations:", getChargeStepDurationsArray(), - getNumChargeStepDurations(), false)) { + if (dumpDurationSteps(pw, " ", "Charge step durations:", + getChargeLevelStepTracker(), false)) { long timeRemaining = computeChargeTimeRemaining(SystemClock.elapsedRealtime()); if (timeRemaining >= 0) { pw.print(" Estimated charge time remaining: "); @@ -4063,6 +4632,75 @@ public abstract class BatteryStats implements Parcelable { } pw.println(); } + } + if (!filtering || (flags&(DUMP_CHARGED_ONLY|DUMP_DAILY_ONLY)) != 0) { + pw.println("Daily stats:"); + pw.print(" Current start time: "); + pw.println(DateFormat.format("yyyy-MM-dd-HH-mm-ss", + getCurrentDailyStartTime()).toString()); + pw.print(" Next min deadline: "); + pw.println(DateFormat.format("yyyy-MM-dd-HH-mm-ss", + getNextMinDailyDeadline()).toString()); + pw.print(" Next max deadline: "); + pw.println(DateFormat.format("yyyy-MM-dd-HH-mm-ss", + getNextMaxDailyDeadline()).toString()); + StringBuilder sb = new StringBuilder(64); + int[] outInt = new int[1]; + LevelStepTracker dsteps = getDailyDischargeLevelStepTracker(); + LevelStepTracker csteps = getDailyChargeLevelStepTracker(); + if (dsteps.mNumStepDurations > 0 || csteps.mNumStepDurations > 0) { + if ((flags&DUMP_DAILY_ONLY) != 0) { + if (dumpDurationSteps(pw, " ", " Current daily discharge step durations:", + dsteps, false)) { + dumpDailyLevelStepSummary(pw, " ", "Discharge", dsteps, + sb, outInt); + } + if (dumpDurationSteps(pw, " ", " Current daily charge step durations:", + csteps, false)) { + dumpDailyLevelStepSummary(pw, " ", "Charge", csteps, + sb, outInt); + } + } else { + pw.println(" Current daily steps:"); + dumpDailyLevelStepSummary(pw, " ", "Discharge", dsteps, + sb, outInt); + dumpDailyLevelStepSummary(pw, " ", "Charge", csteps, + sb, outInt); + } + } + DailyItem dit; + int curIndex = 0; + while ((dit=getDailyItemLocked(curIndex)) != null) { + curIndex++; + if ((flags&DUMP_DAILY_ONLY) != 0) { + pw.println(); + } + pw.print(" Daily from "); + pw.print(DateFormat.format("yyyy-MM-dd-HH-mm-ss", dit.mStartTime).toString()); + pw.print(" to "); + pw.print(DateFormat.format("yyyy-MM-dd-HH-mm-ss", dit.mEndTime).toString()); + pw.println(":"); + if ((flags&DUMP_DAILY_ONLY) != 0) { + if (dumpDurationSteps(pw, " ", + " Discharge step durations:", dit.mDischargeSteps, false)) { + dumpDailyLevelStepSummary(pw, " ", "Discharge", dit.mDischargeSteps, + sb, outInt); + } + if (dumpDurationSteps(pw, " ", + " Charge step durations:", dit.mChargeSteps, false)) { + dumpDailyLevelStepSummary(pw, " ", "Charge", dit.mChargeSteps, + sb, outInt); + } + } else { + dumpDailyLevelStepSummary(pw, " ", "Discharge", dit.mDischargeSteps, + sb, outInt); + dumpDailyLevelStepSummary(pw, " ", "Charge", dit.mChargeSteps, + sb, outInt); + } + } + pw.println(); + } + if (!filtering || (flags&DUMP_CHARGED_ONLY) != 0) { pw.println("Statistics since last charge:"); pw.println(" System starts: " + getStartCount() + ", currently on battery: " + getIsOnBattery()); @@ -4087,8 +4725,8 @@ public abstract class BatteryStats implements Parcelable { long now = getHistoryBaseTime() + SystemClock.elapsedRealtime(); - final boolean filtering = - (flags&(DUMP_HISTORY_ONLY|DUMP_UNPLUGGED_ONLY|DUMP_CHARGED_ONLY)) != 0; + final boolean filtering = (flags & + (DUMP_HISTORY_ONLY|DUMP_UNPLUGGED_ONLY|DUMP_CHARGED_ONLY|DUMP_DAILY_ONLY)) != 0; if ((flags&DUMP_INCLUDE_HISTORY) != 0 || (flags&DUMP_HISTORY_ONLY) != 0) { if (startIteratingHistoryLocked()) { @@ -4114,7 +4752,7 @@ public abstract class BatteryStats implements Parcelable { } } - if (filtering && (flags&(DUMP_UNPLUGGED_ONLY|DUMP_CHARGED_ONLY)) == 0) { + if (filtering && (flags&(DUMP_UNPLUGGED_ONLY|DUMP_CHARGED_ONLY|DUMP_DAILY_ONLY)) == 0) { return; } @@ -4146,8 +4784,7 @@ public abstract class BatteryStats implements Parcelable { } } if (!filtering || (flags&DUMP_CHARGED_ONLY) != 0) { - dumpDurationSteps(pw, DISCHARGE_STEP_DATA, getDischargeStepDurationsArray(), - getNumDischargeStepDurations(), true); + dumpDurationSteps(pw, "", DISCHARGE_STEP_DATA, getDischargeLevelStepTracker(), true); String[] lineArgs = new String[1]; long timeRemaining = computeBatteryTimeRemaining(SystemClock.elapsedRealtime()); if (timeRemaining >= 0) { @@ -4155,8 +4792,7 @@ public abstract class BatteryStats implements Parcelable { dumpLine(pw, 0 /* uid */, "i" /* category */, DISCHARGE_TIME_REMAIN_DATA, (Object[])lineArgs); } - dumpDurationSteps(pw, CHARGE_STEP_DATA, getChargeStepDurationsArray(), - getNumChargeStepDurations(), true); + dumpDurationSteps(pw, "", CHARGE_STEP_DATA, getChargeLevelStepTracker(), true); timeRemaining = computeChargeTimeRemaining(SystemClock.elapsedRealtime()); if (timeRemaining >= 0) { lineArgs[0] = Long.toString(timeRemaining); diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java index b209690..36fc4f9 100644 --- a/core/java/android/os/Build.java +++ b/core/java/android/os/Build.java @@ -91,7 +91,7 @@ public class Build { /** The name of the hardware (from the kernel command line or /proc). */ public static final String HARDWARE = getString("ro.hardware"); - /** A hardware serial number, if available. Alphanumeric only, case-insensitive. */ + /** A hardware serial number, if available. Alphanumeric only, case-insensitive. */ public static final String SERIAL = getString("ro.serialno"); /** @@ -159,7 +159,7 @@ public class Build { /** * The user-visible SDK version of the framework in its raw String * representation; use {@link #SDK_INT} instead. - * + * * @deprecated Use {@link #SDK_INT} to easily get this as an integer. */ @Deprecated @@ -207,25 +207,25 @@ public class Build { * not yet turned into an official release. */ public static final int CUR_DEVELOPMENT = 10000; - + /** * October 2008: The original, first, version of Android. Yay! */ public static final int BASE = 1; - + /** * February 2009: First Android update, officially called 1.1. */ public static final int BASE_1_1 = 2; - + /** * May 2009: Android 1.5. */ public static final int CUPCAKE = 3; - + /** * September 2009: Android 1.6. - * + * * <p>Applications targeting this or a later release will get these * new changes in behavior:</p> * <ul> @@ -247,10 +247,10 @@ public class Build { * </ul> */ public static final int DONUT = 4; - + /** * November 2009: Android 2.0 - * + * * <p>Applications targeting this or a later release will get these * new changes in behavior:</p> * <ul> @@ -267,22 +267,22 @@ public class Build { * </ul> */ public static final int ECLAIR = 5; - + /** * December 2009: Android 2.0.1 */ public static final int ECLAIR_0_1 = 6; - + /** * January 2010: Android 2.1 */ public static final int ECLAIR_MR1 = 7; - + /** * June 2010: Android 2.2 */ public static final int FROYO = 8; - + /** * November 2010: Android 2.3 * @@ -294,7 +294,7 @@ public class Build { * </ul> */ public static final int GINGERBREAD = 9; - + /** * February 2011: Android 2.3.3. */ @@ -339,12 +339,12 @@ public class Build { * </ul> */ public static final int HONEYCOMB = 11; - + /** * May 2011: Android 3.1. */ public static final int HONEYCOMB_MR1 = 12; - + /** * June 2011: Android 3.2. * @@ -597,8 +597,13 @@ public class Build { * Lollipop with an extra sugar coating on the outside! */ public static final int LOLLIPOP_MR1 = 22; + + /** + * M comes after L. + */ + public static final int MNC = CUR_DEVELOPMENT; } - + /** The type of build, like "user" or "eng". */ public static final String TYPE = getString("ro.build.type"); @@ -645,14 +650,22 @@ public class Build { } /** - * Check that device fingerprint is defined and that it matches across - * various partitions. + * Verifies the the current flash of the device is consistent with what + * was expected at build time. + * 1) Checks that device fingerprint is defined and that it matches across + * various partitions. + * 2) Verifies radio and bootloader partitions are those expected in the build. * * @hide */ - public static boolean isFingerprintConsistent() { + public static boolean isBuildConsistent() { final String system = SystemProperties.get("ro.build.fingerprint"); final String vendor = SystemProperties.get("ro.vendor.build.fingerprint"); + final String bootimage = SystemProperties.get("ro.bootimage.build.fingerprint"); + final String requiredBootloader = SystemProperties.get("ro.build.expect.bootloader"); + final String currentBootloader = SystemProperties.get("ro.bootloader"); + final String requiredRadio = SystemProperties.get("ro.build.expect.baseband"); + final String currentRadio = SystemProperties.get("gsm.version.baseband"); if (TextUtils.isEmpty(system)) { Slog.e(TAG, "Required ro.build.fingerprint is empty!"); @@ -667,6 +680,32 @@ public class Build { } } + /* TODO: Figure out issue with checks failing + if (!TextUtils.isEmpty(bootimage)) { + if (!Objects.equals(system, bootimage)) { + Slog.e(TAG, "Mismatched fingerprints; system reported " + system + + " but bootimage reported " + bootimage); + return false; + } + } + + if (!TextUtils.isEmpty(requiredBootloader)) { + if (!Objects.equals(currentBootloader, requiredBootloader)) { + Slog.e(TAG, "Mismatched bootloader version: build requires " + requiredBootloader + + " but runtime reports " + currentBootloader); + return false; + } + } + + if (!TextUtils.isEmpty(requiredRadio)) { + if (!Objects.equals(currentRadio, requiredRadio)) { + Slog.e(TAG, "Mismatched radio version: build requires " + requiredRadio + + " but runtime reports " + currentRadio); + return false; + } + } + */ + return true; } diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java index c5c5372..5e9b8c1 100644 --- a/core/java/android/os/Bundle.java +++ b/core/java/android/os/Bundle.java @@ -16,6 +16,7 @@ package android.os; +import android.annotation.Nullable; import android.util.ArrayMap; import android.util.Size; import android.util.SizeF; @@ -24,7 +25,6 @@ import android.util.SparseArray; import java.io.Serializable; import java.util.ArrayList; import java.util.List; -import java.util.Set; /** * A mapping from String values to various Parcelable types. @@ -252,6 +252,29 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { } /** + * Filter values in Bundle to only basic types. + * @hide + */ + public void filterValues() { + unparcel(); + if (mMap != null) { + for (int i = mMap.size() - 1; i >= 0; i--) { + Object value = mMap.valueAt(i); + if (PersistableBundle.isValidType(value)) { + continue; + } + if (value instanceof Bundle) { + ((Bundle)value).filterValues(); + } + if (value.getClass().getName().startsWith("android.")) { + continue; + } + mMap.removeAt(i); + } + } + } + + /** * Inserts a byte value into the mapping of this Bundle, replacing * any existing value for the given key. * @@ -259,7 +282,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @param value a byte */ @Override - public void putByte(String key, byte value) { + public void putByte(@Nullable String key, byte value) { super.putByte(key, value); } @@ -268,10 +291,10 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * any existing value for the given key. * * @param key a String, or null - * @param value a char, or null + * @param value a char */ @Override - public void putChar(String key, char value) { + public void putChar(@Nullable String key, char value) { super.putChar(key, value); } @@ -283,7 +306,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @param value a short */ @Override - public void putShort(String key, short value) { + public void putShort(@Nullable String key, short value) { super.putShort(key, value); } @@ -295,7 +318,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @param value a float */ @Override - public void putFloat(String key, float value) { + public void putFloat(@Nullable String key, float value) { super.putFloat(key, value); } @@ -307,7 +330,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @param value a CharSequence, or null */ @Override - public void putCharSequence(String key, CharSequence value) { + public void putCharSequence(@Nullable String key, @Nullable CharSequence value) { super.putCharSequence(key, value); } @@ -318,7 +341,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @param key a String, or null * @param value a Parcelable object, or null */ - public void putParcelable(String key, Parcelable value) { + public void putParcelable(@Nullable String key, @Nullable Parcelable value) { unparcel(); mMap.put(key, value); mFdsKnown = false; @@ -331,7 +354,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @param key a String, or null * @param value a Size object, or null */ - public void putSize(String key, Size value) { + public void putSize(@Nullable String key, @Nullable Size value) { unparcel(); mMap.put(key, value); } @@ -343,7 +366,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @param key a String, or null * @param value a SizeF object, or null */ - public void putSizeF(String key, SizeF value) { + public void putSizeF(@Nullable String key, @Nullable SizeF value) { unparcel(); mMap.put(key, value); } @@ -356,7 +379,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @param key a String, or null * @param value an array of Parcelable objects, or null */ - public void putParcelableArray(String key, Parcelable[] value) { + public void putParcelableArray(@Nullable String key, @Nullable Parcelable[] value) { unparcel(); mMap.put(key, value); mFdsKnown = false; @@ -370,8 +393,8 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @param key a String, or null * @param value an ArrayList of Parcelable objects, or null */ - public void putParcelableArrayList(String key, - ArrayList<? extends Parcelable> value) { + public void putParcelableArrayList(@Nullable String key, + @Nullable ArrayList<? extends Parcelable> value) { unparcel(); mMap.put(key, value); mFdsKnown = false; @@ -392,8 +415,8 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @param key a String, or null * @param value a SparseArray of Parcelable objects, or null */ - public void putSparseParcelableArray(String key, - SparseArray<? extends Parcelable> value) { + public void putSparseParcelableArray(@Nullable String key, + @Nullable SparseArray<? extends Parcelable> value) { unparcel(); mMap.put(key, value); mFdsKnown = false; @@ -407,7 +430,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @param value an ArrayList<Integer> object, or null */ @Override - public void putIntegerArrayList(String key, ArrayList<Integer> value) { + public void putIntegerArrayList(@Nullable String key, @Nullable ArrayList<Integer> value) { super.putIntegerArrayList(key, value); } @@ -419,7 +442,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @param value an ArrayList<String> object, or null */ @Override - public void putStringArrayList(String key, ArrayList<String> value) { + public void putStringArrayList(@Nullable String key, @Nullable ArrayList<String> value) { super.putStringArrayList(key, value); } @@ -431,7 +454,8 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @param value an ArrayList<CharSequence> object, or null */ @Override - public void putCharSequenceArrayList(String key, ArrayList<CharSequence> value) { + public void putCharSequenceArrayList(@Nullable String key, + @Nullable ArrayList<CharSequence> value) { super.putCharSequenceArrayList(key, value); } @@ -443,7 +467,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @param value a Serializable object, or null */ @Override - public void putSerializable(String key, Serializable value) { + public void putSerializable(@Nullable String key, @Nullable Serializable value) { super.putSerializable(key, value); } @@ -455,7 +479,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @param value a byte array object, or null */ @Override - public void putByteArray(String key, byte[] value) { + public void putByteArray(@Nullable String key, @Nullable byte[] value) { super.putByteArray(key, value); } @@ -467,7 +491,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @param value a short array object, or null */ @Override - public void putShortArray(String key, short[] value) { + public void putShortArray(@Nullable String key, @Nullable short[] value) { super.putShortArray(key, value); } @@ -479,7 +503,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @param value a char array object, or null */ @Override - public void putCharArray(String key, char[] value) { + public void putCharArray(@Nullable String key, @Nullable char[] value) { super.putCharArray(key, value); } @@ -491,7 +515,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @param value a float array object, or null */ @Override - public void putFloatArray(String key, float[] value) { + public void putFloatArray(@Nullable String key, @Nullable float[] value) { super.putFloatArray(key, value); } @@ -503,7 +527,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @param value a CharSequence array object, or null */ @Override - public void putCharSequenceArray(String key, CharSequence[] value) { + public void putCharSequenceArray(@Nullable String key, @Nullable CharSequence[] value) { super.putCharSequenceArray(key, value); } @@ -514,7 +538,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @param key a String, or null * @param value a Bundle object, or null */ - public void putBundle(String key, Bundle value) { + public void putBundle(@Nullable String key, @Nullable Bundle value) { unparcel(); mMap.put(key, value); } @@ -533,7 +557,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @param key a String, or null * @param value an IBinder object, or null */ - public void putBinder(String key, IBinder value) { + public void putBinder(@Nullable String key, @Nullable IBinder value) { unparcel(); mMap.put(key, value); } @@ -549,7 +573,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @hide This is the old name of the function. */ @Deprecated - public void putIBinder(String key, IBinder value) { + public void putIBinder(@Nullable String key, @Nullable IBinder value) { unparcel(); mMap.put(key, value); } @@ -663,7 +687,8 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @return a CharSequence value, or null */ @Override - public CharSequence getCharSequence(String key) { + @Nullable + public CharSequence getCharSequence(@Nullable String key) { return super.getCharSequence(key); } @@ -679,7 +704,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * if no valid CharSequence object is currently mapped to that key. */ @Override - public CharSequence getCharSequence(String key, CharSequence defaultValue) { + public CharSequence getCharSequence(@Nullable String key, CharSequence defaultValue) { return super.getCharSequence(key, defaultValue); } @@ -691,7 +716,8 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @param key a String, or null * @return a Size value, or null */ - public Size getSize(String key) { + @Nullable + public Size getSize(@Nullable String key) { unparcel(); final Object o = mMap.get(key); try { @@ -710,7 +736,8 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @param key a String, or null * @return a Size value, or null */ - public SizeF getSizeF(String key) { + @Nullable + public SizeF getSizeF(@Nullable String key) { unparcel(); final Object o = mMap.get(key); try { @@ -729,7 +756,8 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @param key a String, or null * @return a Bundle value, or null */ - public Bundle getBundle(String key) { + @Nullable + public Bundle getBundle(@Nullable String key) { unparcel(); Object o = mMap.get(key); if (o == null) { @@ -751,7 +779,8 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @param key a String, or null * @return a Parcelable value, or null */ - public <T extends Parcelable> T getParcelable(String key) { + @Nullable + public <T extends Parcelable> T getParcelable(@Nullable String key) { unparcel(); Object o = mMap.get(key); if (o == null) { @@ -773,7 +802,8 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @param key a String, or null * @return a Parcelable[] value, or null */ - public Parcelable[] getParcelableArray(String key) { + @Nullable + public Parcelable[] getParcelableArray(@Nullable String key) { unparcel(); Object o = mMap.get(key); if (o == null) { @@ -795,7 +825,8 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @param key a String, or null * @return an ArrayList<T> value, or null */ - public <T extends Parcelable> ArrayList<T> getParcelableArrayList(String key) { + @Nullable + public <T extends Parcelable> ArrayList<T> getParcelableArrayList(@Nullable String key) { unparcel(); Object o = mMap.get(key); if (o == null) { @@ -818,7 +849,8 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * * @return a SparseArray of T values, or null */ - public <T extends Parcelable> SparseArray<T> getSparseParcelableArray(String key) { + @Nullable + public <T extends Parcelable> SparseArray<T> getSparseParcelableArray(@Nullable String key) { unparcel(); Object o = mMap.get(key); if (o == null) { @@ -841,7 +873,8 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @return a Serializable value, or null */ @Override - public Serializable getSerializable(String key) { + @Nullable + public Serializable getSerializable(@Nullable String key) { return super.getSerializable(key); } @@ -854,7 +887,8 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @return an ArrayList<String> value, or null */ @Override - public ArrayList<Integer> getIntegerArrayList(String key) { + @Nullable + public ArrayList<Integer> getIntegerArrayList(@Nullable String key) { return super.getIntegerArrayList(key); } @@ -867,7 +901,8 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @return an ArrayList<String> value, or null */ @Override - public ArrayList<String> getStringArrayList(String key) { + @Nullable + public ArrayList<String> getStringArrayList(@Nullable String key) { return super.getStringArrayList(key); } @@ -880,7 +915,8 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @return an ArrayList<CharSequence> value, or null */ @Override - public ArrayList<CharSequence> getCharSequenceArrayList(String key) { + @Nullable + public ArrayList<CharSequence> getCharSequenceArrayList(@Nullable String key) { return super.getCharSequenceArrayList(key); } @@ -893,7 +929,8 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @return a byte[] value, or null */ @Override - public byte[] getByteArray(String key) { + @Nullable + public byte[] getByteArray(@Nullable String key) { return super.getByteArray(key); } @@ -906,7 +943,8 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @return a short[] value, or null */ @Override - public short[] getShortArray(String key) { + @Nullable + public short[] getShortArray(@Nullable String key) { return super.getShortArray(key); } @@ -919,7 +957,8 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @return a char[] value, or null */ @Override - public char[] getCharArray(String key) { + @Nullable + public char[] getCharArray(@Nullable String key) { return super.getCharArray(key); } @@ -932,7 +971,8 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @return a float[] value, or null */ @Override - public float[] getFloatArray(String key) { + @Nullable + public float[] getFloatArray(@Nullable String key) { return super.getFloatArray(key); } @@ -945,7 +985,8 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @return a CharSequence[] value, or null */ @Override - public CharSequence[] getCharSequenceArray(String key) { + @Nullable + public CharSequence[] getCharSequenceArray(@Nullable String key) { return super.getCharSequenceArray(key); } @@ -957,7 +998,8 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @param key a String, or null * @return an IBinder value, or null */ - public IBinder getBinder(String key) { + @Nullable + public IBinder getBinder(@Nullable String key) { unparcel(); Object o = mMap.get(key); if (o == null) { @@ -983,7 +1025,8 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @hide This is the old name of the function. */ @Deprecated - public IBinder getIBinder(String key) { + @Nullable + public IBinder getIBinder(@Nullable String key) { unparcel(); Object o = mMap.get(key); if (o == null) { diff --git a/core/java/android/os/CommonClock.java b/core/java/android/os/CommonClock.java index f83a90b..2ecf317 100644 --- a/core/java/android/os/CommonClock.java +++ b/core/java/android/os/CommonClock.java @@ -23,7 +23,6 @@ import android.os.IBinder; import android.os.Parcel; import android.os.RemoteException; import android.os.ServiceManager; -import static android.system.OsConstants.*; /** * Used for accessing the android common time service's common clock and receiving notifications diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java index 2d92c7b..512e212 100644 --- a/core/java/android/os/Debug.java +++ b/core/java/android/os/Debug.java @@ -63,7 +63,10 @@ public final class Debug * * TRACE_COUNT_ALLOCS adds the results from startAllocCounting to the * trace key file. + * + * @deprecated Accurate counting is a burden on the runtime and may be removed. */ + @Deprecated public static final int TRACE_COUNT_ALLOCS = VMDebug.TRACE_COUNT_ALLOCS; /** @@ -760,7 +763,7 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo /** * Stop counting the number and aggregate size of memory allocations. * - * @see #startAllocCounting() + * @deprecated Accurate counting is a burden on the runtime and may be removed. */ @Deprecated public static void stopAllocCounting() { @@ -770,7 +773,10 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo /** * Returns the global count of objects allocated by the runtime between a * {@link #startAllocCounting() start} and {@link #stopAllocCounting() stop}. + * + * @deprecated Accurate counting is a burden on the runtime and may be removed. */ + @Deprecated public static int getGlobalAllocCount() { return VMDebug.getAllocCount(VMDebug.KIND_GLOBAL_ALLOCATED_OBJECTS); } @@ -778,7 +784,10 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo /** * Clears the global count of objects allocated. * @see #getGlobalAllocCount() + * + * @deprecated Accurate counting is a burden on the runtime and may be removed. */ + @Deprecated public static void resetGlobalAllocCount() { VMDebug.resetAllocCount(VMDebug.KIND_GLOBAL_ALLOCATED_OBJECTS); } @@ -786,7 +795,10 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo /** * Returns the global size, in bytes, of objects allocated by the runtime between a * {@link #startAllocCounting() start} and {@link #stopAllocCounting() stop}. + * + * @deprecated Accurate counting is a burden on the runtime and may be removed. */ + @Deprecated public static int getGlobalAllocSize() { return VMDebug.getAllocCount(VMDebug.KIND_GLOBAL_ALLOCATED_BYTES); } @@ -794,7 +806,10 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo /** * Clears the global size of objects allocated. * @see #getGlobalAllocSize() + * + * @deprecated Accurate counting is a burden on the runtime and may be removed. */ + @Deprecated public static void resetGlobalAllocSize() { VMDebug.resetAllocCount(VMDebug.KIND_GLOBAL_ALLOCATED_BYTES); } @@ -802,7 +817,10 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo /** * Returns the global count of objects freed by the runtime between a * {@link #startAllocCounting() start} and {@link #stopAllocCounting() stop}. + * + * @deprecated Accurate counting is a burden on the runtime and may be removed. */ + @Deprecated public static int getGlobalFreedCount() { return VMDebug.getAllocCount(VMDebug.KIND_GLOBAL_FREED_OBJECTS); } @@ -810,7 +828,10 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo /** * Clears the global count of objects freed. * @see #getGlobalFreedCount() + * + * @deprecated Accurate counting is a burden on the runtime and may be removed. */ + @Deprecated public static void resetGlobalFreedCount() { VMDebug.resetAllocCount(VMDebug.KIND_GLOBAL_FREED_OBJECTS); } @@ -818,7 +839,10 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo /** * Returns the global size, in bytes, of objects freed by the runtime between a * {@link #startAllocCounting() start} and {@link #stopAllocCounting() stop}. + * + * @deprecated Accurate counting is a burden on the runtime and may be removed. */ + @Deprecated public static int getGlobalFreedSize() { return VMDebug.getAllocCount(VMDebug.KIND_GLOBAL_FREED_BYTES); } @@ -826,7 +850,10 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo /** * Clears the global size of objects freed. * @see #getGlobalFreedSize() + * + * @deprecated Accurate counting is a burden on the runtime and may be removed. */ + @Deprecated public static void resetGlobalFreedSize() { VMDebug.resetAllocCount(VMDebug.KIND_GLOBAL_FREED_BYTES); } @@ -834,7 +861,10 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo /** * Returns the number of non-concurrent GC invocations between a * {@link #startAllocCounting() start} and {@link #stopAllocCounting() stop}. + * + * @deprecated Accurate counting is a burden on the runtime and may be removed. */ + @Deprecated public static int getGlobalGcInvocationCount() { return VMDebug.getAllocCount(VMDebug.KIND_GLOBAL_GC_INVOCATIONS); } @@ -842,7 +872,10 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo /** * Clears the count of non-concurrent GC invocations. * @see #getGlobalGcInvocationCount() + * + * @deprecated Accurate counting is a burden on the runtime and may be removed. */ + @Deprecated public static void resetGlobalGcInvocationCount() { VMDebug.resetAllocCount(VMDebug.KIND_GLOBAL_GC_INVOCATIONS); } @@ -851,7 +884,10 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo * Returns the number of classes successfully initialized (ie those that executed without * throwing an exception) between a {@link #startAllocCounting() start} and * {@link #stopAllocCounting() stop}. + * + * @deprecated Accurate counting is a burden on the runtime and may be removed. */ + @Deprecated public static int getGlobalClassInitCount() { return VMDebug.getAllocCount(VMDebug.KIND_GLOBAL_CLASS_INIT_COUNT); } @@ -859,7 +895,10 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo /** * Clears the count of classes initialized. * @see #getGlobalClassInitCount() + * + * @deprecated Accurate counting is a burden on the runtime and may be removed. */ + @Deprecated public static void resetGlobalClassInitCount() { VMDebug.resetAllocCount(VMDebug.KIND_GLOBAL_CLASS_INIT_COUNT); } @@ -867,7 +906,10 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo /** * Returns the time spent successfully initializing classes between a * {@link #startAllocCounting() start} and {@link #stopAllocCounting() stop}. + * + * @deprecated Accurate counting is a burden on the runtime and may be removed. */ + @Deprecated public static int getGlobalClassInitTime() { /* cumulative elapsed time for class initialization, in usec */ return VMDebug.getAllocCount(VMDebug.KIND_GLOBAL_CLASS_INIT_TIME); @@ -876,7 +918,10 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo /** * Clears the count of time spent initializing classes. * @see #getGlobalClassInitTime() + * + * @deprecated Accurate counting is a burden on the runtime and may be removed. */ + @Deprecated public static void resetGlobalClassInitTime() { VMDebug.resetAllocCount(VMDebug.KIND_GLOBAL_CLASS_INIT_TIME); } @@ -948,7 +993,10 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo /** * Returns the thread-local count of objects allocated by the runtime between a * {@link #startAllocCounting() start} and {@link #stopAllocCounting() stop}. + * + * @deprecated Accurate counting is a burden on the runtime and may be removed. */ + @Deprecated public static int getThreadAllocCount() { return VMDebug.getAllocCount(VMDebug.KIND_THREAD_ALLOCATED_OBJECTS); } @@ -956,7 +1004,10 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo /** * Clears the thread-local count of objects allocated. * @see #getThreadAllocCount() + * + * @deprecated Accurate counting is a burden on the runtime and may be removed. */ + @Deprecated public static void resetThreadAllocCount() { VMDebug.resetAllocCount(VMDebug.KIND_THREAD_ALLOCATED_OBJECTS); } @@ -965,7 +1016,10 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo * Returns the thread-local size of objects allocated by the runtime between a * {@link #startAllocCounting() start} and {@link #stopAllocCounting() stop}. * @return The allocated size in bytes. + * + * @deprecated Accurate counting is a burden on the runtime and may be removed. */ + @Deprecated public static int getThreadAllocSize() { return VMDebug.getAllocCount(VMDebug.KIND_THREAD_ALLOCATED_BYTES); } @@ -973,7 +1027,10 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo /** * Clears the thread-local count of objects allocated. * @see #getThreadAllocSize() + * + * @deprecated Accurate counting is a burden on the runtime and may be removed. */ + @Deprecated public static void resetThreadAllocSize() { VMDebug.resetAllocCount(VMDebug.KIND_THREAD_ALLOCATED_BYTES); } @@ -1013,7 +1070,10 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo /** * Returns the number of thread-local non-concurrent GC invocations between a * {@link #startAllocCounting() start} and {@link #stopAllocCounting() stop}. + * + * @deprecated Accurate counting is a burden on the runtime and may be removed. */ + @Deprecated public static int getThreadGcInvocationCount() { return VMDebug.getAllocCount(VMDebug.KIND_THREAD_GC_INVOCATIONS); } @@ -1021,7 +1081,10 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo /** * Clears the thread-local count of non-concurrent GC invocations. * @see #getThreadGcInvocationCount() + * + * @deprecated Accurate counting is a burden on the runtime and may be removed. */ + @Deprecated public static void resetThreadGcInvocationCount() { VMDebug.resetAllocCount(VMDebug.KIND_THREAD_GC_INVOCATIONS); } @@ -1029,7 +1092,10 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo /** * Clears all the global and thread-local memory allocation counters. * @see #startAllocCounting() + * + * @deprecated Accurate counting is a burden on the runtime and may be removed. */ + @Deprecated public static void resetAllCounts() { VMDebug.resetAllocCount(VMDebug.KIND_ALL_COUNTS); } @@ -1286,7 +1352,10 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo * + icount.globalMethodInvocations()); * } * </pre> + * + * @deprecated Instruction counting is no longer supported. */ + @Deprecated public static class InstructionCount { private static final int NUM_INSTR = OpcodeInfo.MAXIMUM_PACKED_VALUE + 1; diff --git a/core/java/android/os/INetworkManagementService.aidl b/core/java/android/os/INetworkManagementService.aidl index f0660eb..f93550a 100644 --- a/core/java/android/os/INetworkManagementService.aidl +++ b/core/java/android/os/INetworkManagementService.aidl @@ -178,6 +178,18 @@ interface INetworkManagementService String[] getDnsForwarders(); /** + * Enables unidirectional packet forwarding from {@code fromIface} to + * {@code toIface}. + */ + void startInterfaceForwarding(String fromIface, String toIface); + + /** + * Disables unidirectional packet forwarding from {@code fromIface} to + * {@code toIface}. + */ + void stopInterfaceForwarding(String fromIface, String toIface); + + /** * Enables Network Address Translation between two interfaces. * The address and netmask of the external interface is used for * the NAT'ed network. diff --git a/core/java/android/net/http/RequestFeeder.java b/core/java/android/os/IProcessInfoService.aidl index 34ca267..c98daa2 100644 --- a/core/java/android/net/http/RequestFeeder.java +++ b/core/java/android/os/IProcessInfoService.aidl @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008 The Android Open Source Project + * Copyright 2015 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. @@ -14,29 +14,16 @@ * limitations under the License. */ -/** - * Supplies Requests to a Connection - */ - -package android.net.http; - -import org.apache.http.HttpHost; - -/** - * {@hide} - */ -interface RequestFeeder { - - Request getRequest(); - Request getRequest(HttpHost host); +package android.os; +/** {@hide} */ +interface IProcessInfoService +{ /** - * @return true if a request for this host is available + * For each PID in the given input array, write the current process state + * for that process into the output array, or ActivityManager.PROCESS_STATE_NONEXISTENT + * to indicate that no process with the given PID exists. */ - boolean haveRequest(HttpHost host); - - /** - * Put request back on head of queue - */ - void requeueRequest(Request request); + void getProcessStatesFromPids(in int[] pids, out int[] states); } + diff --git a/core/java/android/os/IUserManager.aidl b/core/java/android/os/IUserManager.aidl index b5295fb..236003b 100644 --- a/core/java/android/os/IUserManager.aidl +++ b/core/java/android/os/IUserManager.aidl @@ -21,6 +21,7 @@ import android.os.Bundle; import android.content.pm.UserInfo; import android.content.RestrictionEntry; import android.graphics.Bitmap; +import android.os.ParcelFileDescriptor; /** * {@hide} @@ -32,7 +33,7 @@ interface IUserManager { boolean removeUser(int userHandle); void setUserName(int userHandle, String name); void setUserIcon(int userHandle, in Bitmap icon); - Bitmap getUserIcon(int userHandle); + ParcelFileDescriptor getUserIcon(int userHandle); List<UserInfo> getUsers(boolean excludeDying); List<UserInfo> getProfiles(int userHandle, boolean enabledOnly); UserInfo getProfileParent(int userHandle); diff --git a/core/java/android/os/Looper.java b/core/java/android/os/Looper.java index 6d7c9cf..34c880f 100644 --- a/core/java/android/os/Looper.java +++ b/core/java/android/os/Looper.java @@ -16,6 +16,8 @@ package android.os; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.util.Log; import android.util.Printer; @@ -24,10 +26,10 @@ import android.util.Printer; * not have a message loop associated with them; to create one, call * {@link #prepare} in the thread that is to run the loop, and then * {@link #loop} to have it process messages until the loop is stopped. - * + * * <p>Most interaction with a message loop is through the * {@link Handler} class. - * + * * <p>This is a typical example of the implementation of a Looper thread, * using the separation of {@link #prepare} and {@link #loop} to create an * initial Handler to communicate with the Looper. @@ -50,6 +52,16 @@ import android.util.Printer; * }</pre> */ public final class Looper { + /* + * API Implementation Note: + * + * This class contains the code required to set up and manage an event loop + * based on MessageQueue. APIs that affect the state of the queue should be + * defined on MessageQueue or Handler rather than on Looper itself. For example, + * idle handlers and sync barriers are defined on the queue whereas preparing the + * thread, looping, and quitting are defined on the looper. + */ + private static final String TAG = "Looper"; // sThreadLocal.get() will return null unless you've called prepare(). @@ -94,7 +106,8 @@ public final class Looper { } } - /** Returns the application's main looper, which lives in the main thread of the application. + /** + * Returns the application's main looper, which lives in the main thread of the application. */ public static Looper getMainLooper() { synchronized (Looper.class) { @@ -157,29 +170,16 @@ public final class Looper { * Return the Looper object associated with the current thread. Returns * null if the calling thread is not associated with a Looper. */ - public static Looper myLooper() { + public static @Nullable Looper myLooper() { return sThreadLocal.get(); } /** - * Control logging of messages as they are processed by this Looper. If - * enabled, a log message will be written to <var>printer</var> - * at the beginning and ending of each message dispatch, identifying the - * target Handler and message contents. - * - * @param printer A Printer object that will receive log messages, or - * null to disable message logging. - */ - public void setMessageLogging(Printer printer) { - mLogging = printer; - } - - /** * Return the {@link MessageQueue} object associated with the current * thread. This must be called from a thread running a Looper, or a * NullPointerException will be thrown. */ - public static MessageQueue myQueue() { + public static @NonNull MessageQueue myQueue() { return myLooper().mQueue; } @@ -190,13 +190,25 @@ public final class Looper { /** * Returns true if the current thread is this looper's thread. - * @hide */ public boolean isCurrentThread() { return Thread.currentThread() == mThread; } /** + * Control logging of messages as they are processed by this Looper. If + * enabled, a log message will be written to <var>printer</var> + * at the beginning and ending of each message dispatch, identifying the + * target Handler and message contents. + * + * @param printer A Printer object that will receive log messages, or + * null to disable message logging. + */ + public void setMessageLogging(@Nullable Printer printer) { + mLogging = printer; + } + + /** * Quits the looper. * <p> * Causes the {@link #loop} method to terminate without processing any @@ -233,74 +245,35 @@ public final class Looper { } /** - * Posts a synchronization barrier to the Looper's message queue. - * - * Message processing occurs as usual until the message queue encounters the - * synchronization barrier that has been posted. When the barrier is encountered, - * later synchronous messages in the queue are stalled (prevented from being executed) - * until the barrier is released by calling {@link #removeSyncBarrier} and specifying - * the token that identifies the synchronization barrier. - * - * This method is used to immediately postpone execution of all subsequently posted - * synchronous messages until a condition is met that releases the barrier. - * Asynchronous messages (see {@link Message#isAsynchronous} are exempt from the barrier - * and continue to be processed as usual. + * Gets the Thread associated with this Looper. * - * This call must be always matched by a call to {@link #removeSyncBarrier} with - * the same token to ensure that the message queue resumes normal operation. - * Otherwise the application will probably hang! - * - * @return A token that uniquely identifies the barrier. This token must be - * passed to {@link #removeSyncBarrier} to release the barrier. - * - * @hide + * @return The looper's thread. */ - public int postSyncBarrier() { - return mQueue.enqueueSyncBarrier(SystemClock.uptimeMillis()); + public @NonNull Thread getThread() { + return mThread; } - /** - * Removes a synchronization barrier. - * - * @param token The synchronization barrier token that was returned by - * {@link #postSyncBarrier}. + * Gets this looper's message queue. * - * @throws IllegalStateException if the barrier was not found. - * - * @hide - */ - public void removeSyncBarrier(int token) { - mQueue.removeSyncBarrier(token); - } - - /** - * Return the Thread associated with this Looper. + * @return The looper's message queue. */ - public Thread getThread() { - return mThread; - } - - /** @hide */ - public MessageQueue getQueue() { + public @NonNull MessageQueue getQueue() { return mQueue; } /** - * Return whether this looper's thread is currently idle, waiting for new work - * to do. This is intrinsically racy, since its state can change before you get - * the result back. - * @hide + * Dumps the state of the looper for debugging purposes. + * + * @param pw A printer to receive the contents of the dump. + * @param prefix A prefix to prepend to each line which is printed. */ - public boolean isIdling() { - return mQueue.isIdling(); - } - - public void dump(Printer pw, String prefix) { + public void dump(@NonNull Printer pw, @NonNull String prefix) { pw.println(prefix + toString()); mQueue.dump(pw, prefix + " "); } + @Override public String toString() { return "Looper (" + mThread.getName() + ", tid " + mThread.getId() + ") {" + Integer.toHexString(System.identityHashCode(this)) + "}"; diff --git a/core/java/android/os/MessageQueue.java b/core/java/android/os/MessageQueue.java index 01a23ce..7dd4f94 100644 --- a/core/java/android/os/MessageQueue.java +++ b/core/java/android/os/MessageQueue.java @@ -16,9 +16,15 @@ package android.os; +import android.annotation.IntDef; +import android.annotation.NonNull; import android.util.Log; import android.util.Printer; +import android.util.SparseArray; +import java.io.FileDescriptor; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; /** @@ -30,6 +36,9 @@ import java.util.ArrayList; * {@link Looper#myQueue() Looper.myQueue()}. */ public final class MessageQueue { + private static final String TAG = "MessageQueue"; + private static final boolean DEBUG = false; + // True if the message queue can be quit. private final boolean mQuitAllowed; @@ -38,6 +47,7 @@ public final class MessageQueue { Message mMessages; private final ArrayList<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>(); + private SparseArray<FileDescriptorRecord> mFileDescriptorRecords; private IdleHandler[] mPendingIdleHandlers; private boolean mQuitting; @@ -50,23 +60,46 @@ public final class MessageQueue { private native static long nativeInit(); private native static void nativeDestroy(long ptr); - private native static void nativePollOnce(long ptr, int timeoutMillis); + private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/ private native static void nativeWake(long ptr); - private native static boolean nativeIsIdling(long ptr); + private native static boolean nativeIsPolling(long ptr); + private native static void nativeSetFileDescriptorEvents(long ptr, int fd, int events); + + MessageQueue(boolean quitAllowed) { + mQuitAllowed = quitAllowed; + mPtr = nativeInit(); + } + + @Override + protected void finalize() throws Throwable { + try { + dispose(); + } finally { + super.finalize(); + } + } + + // Disposes of the underlying message queue. + // Must only be called on the looper thread or the finalizer. + private void dispose() { + if (mPtr != 0) { + nativeDestroy(mPtr); + mPtr = 0; + } + } /** - * Callback interface for discovering when a thread is going to block - * waiting for more messages. + * Returns true if the looper has no pending messages which are due to be processed. + * + * <p>This method is safe to call from any thread. + * + * @return True if the looper is idle. */ - public static interface IdleHandler { - /** - * Called when the message queue has run out of messages and will now - * wait for more. Return true to keep your idle handler active, false - * to have it removed. This may be called if there are still messages - * pending in the queue, but they are all scheduled to be dispatched - * after the current time. - */ - boolean queueIdle(); + public boolean isIdle() { + synchronized (this) { + final long now = SystemClock.uptimeMillis(); + return mMessages == null || now < mMessages.when; + } } /** @@ -74,12 +107,12 @@ public final class MessageQueue { * removed automatically for you by returning false from * {@link IdleHandler#queueIdle IdleHandler.queueIdle()} when it is * invoked, or explicitly removing it with {@link #removeIdleHandler}. - * + * * <p>This method is safe to call from any thread. - * + * * @param handler The IdleHandler to be added. */ - public void addIdleHandler(IdleHandler handler) { + public void addIdleHandler(@NonNull IdleHandler handler) { if (handler == null) { throw new NullPointerException("Can't add a null IdleHandler"); } @@ -92,38 +125,185 @@ public final class MessageQueue { * Remove an {@link IdleHandler} from the queue that was previously added * with {@link #addIdleHandler}. If the given object is not currently * in the idle list, nothing is done. - * + * + * <p>This method is safe to call from any thread. + * * @param handler The IdleHandler to be removed. */ - public void removeIdleHandler(IdleHandler handler) { + public void removeIdleHandler(@NonNull IdleHandler handler) { synchronized (this) { mIdleHandlers.remove(handler); } } - MessageQueue(boolean quitAllowed) { - mQuitAllowed = quitAllowed; - mPtr = nativeInit(); + /** + * Returns whether this looper's thread is currently polling for more work to do. + * This is a good signal that the loop is still alive rather than being stuck + * handling a callback. Note that this method is intrinsically racy, since the + * state of the loop can change before you get the result back. + * + * <p>This method is safe to call from any thread. + * + * @return True if the looper is currently polling for events. + * @hide + */ + public boolean isPolling() { + synchronized (this) { + return isPollingLocked(); + } } - @Override - protected void finalize() throws Throwable { - try { - dispose(); - } finally { - super.finalize(); + private boolean isPollingLocked() { + // If the loop is quitting then it must not be idling. + // We can assume mPtr != 0 when mQuitting is false. + return !mQuitting && nativeIsPolling(mPtr); + } + + /** + * Registers a file descriptor callback to receive notification when file descriptor + * related events occur. + * <p> + * If the file descriptor has already been registered, the specified events + * and callback will replace any that were previously associated with it. + * It is not possible to set more than one callback per file descriptor. + * </p><p> + * It is important to always unregister the callback when the file descriptor + * is no longer of use. + * </p> + * + * @param fd The file descriptor for which a callback will be registered. + * @param events The set of events to receive: a combination of the + * {@link FileDescriptorCallback#EVENT_INPUT}, + * {@link FileDescriptorCallback#EVENT_OUTPUT}, and + * {@link FileDescriptorCallback#EVENT_ERROR} event masks. If the requested + * set of events is zero, then the callback is unregistered. + * @param callback The callback to invoke when file descriptor events occur. + * + * @see FileDescriptorCallback + * @see #unregisterFileDescriptorCallback + */ + public void registerFileDescriptorCallback(@NonNull FileDescriptor fd, + @FileDescriptorCallback.Events int events, + @NonNull FileDescriptorCallback callback) { + if (fd == null) { + throw new IllegalArgumentException("fd must not be null"); + } + if (callback == null) { + throw new IllegalArgumentException("callback must not be null"); + } + + synchronized (this) { + setFileDescriptorCallbackLocked(fd, events, callback); } } - // Disposes of the underlying message queue. - // Must only be called on the looper thread or the finalizer. - private void dispose() { - if (mPtr != 0) { - nativeDestroy(mPtr); - mPtr = 0; + /** + * Unregisters a file descriptor callback. + * <p> + * This method does nothing if no callback has been registered for the + * specified file descriptor. + * </p> + * + * @param fd The file descriptor whose callback will be unregistered. + * + * @see FileDescriptorCallback + * @see #registerFileDescriptorCallback + */ + public void unregisterFileDescriptorCallback(@NonNull FileDescriptor fd) { + if (fd == null) { + throw new IllegalArgumentException("fd must not be null"); + } + + synchronized (this) { + setFileDescriptorCallbackLocked(fd, 0, null); } } + private void setFileDescriptorCallbackLocked(FileDescriptor fd, int events, + FileDescriptorCallback callback) { + final int fdNum = fd.getInt$(); + + int index = -1; + FileDescriptorRecord record = null; + if (mFileDescriptorRecords != null) { + index = mFileDescriptorRecords.indexOfKey(fdNum); + if (index >= 0) { + record = mFileDescriptorRecords.valueAt(index); + if (record != null && record.mEvents == events) { + return; + } + } + } + + if (events != 0) { + events |= FileDescriptorCallback.EVENT_ERROR; + if (record == null) { + if (mFileDescriptorRecords == null) { + mFileDescriptorRecords = new SparseArray<FileDescriptorRecord>(); + } + record = new FileDescriptorRecord(fd, events, callback); + mFileDescriptorRecords.put(fdNum, record); + } else { + record.mCallback = callback; + record.mEvents = events; + record.mSeq += 1; + } + nativeSetFileDescriptorEvents(mPtr, fdNum, events); + } else if (record != null) { + record.mEvents = 0; + mFileDescriptorRecords.removeAt(index); + } + } + + // Called from native code. + private int dispatchEvents(int fd, int events) { + // Get the file descriptor record and any state that might change. + final FileDescriptorRecord record; + final int oldWatchedEvents; + final FileDescriptorCallback callback; + final int seq; + synchronized (this) { + record = mFileDescriptorRecords.get(fd); + if (record == null) { + return 0; // spurious, no callback registered + } + + oldWatchedEvents = record.mEvents; + events &= oldWatchedEvents; // filter events based on current watched set + if (events == 0) { + return oldWatchedEvents; // spurious, watched events changed + } + + callback = record.mCallback; + seq = record.mSeq; + } + + // Invoke the callback outside of the lock. + int newWatchedEvents = callback.onFileDescriptorEvents( + record.mDescriptor, events); + if (newWatchedEvents != 0) { + newWatchedEvents |= FileDescriptorCallback.EVENT_ERROR; + } + + // Update the file descriptor record if the callback changed the set of + // events to watch and the callback itself hasn't been updated since. + if (newWatchedEvents != oldWatchedEvents) { + synchronized (this) { + int index = mFileDescriptorRecords.indexOfKey(fd); + if (index >= 0 && mFileDescriptorRecords.valueAt(index) == record + && record.mSeq == seq) { + record.mEvents = newWatchedEvents; + if (newWatchedEvents == 0) { + mFileDescriptorRecords.removeAt(index); + } + } + } + } + + // Return the new set of events to watch for native code to take care of. + return newWatchedEvents; + } + Message next() { // Return here if the message loop has already quit and been disposed. // This can happen if the application tries to restart a looper after quit @@ -167,7 +347,8 @@ public final class MessageQueue { mMessages = msg.next; } msg.next = null; - if (false) Log.v("MessageQueue", "Returning message: " + msg); + if (DEBUG) Log.v(TAG, "Returning message: " + msg); + msg.markInUse(); return msg; } } else { @@ -210,7 +391,7 @@ public final class MessageQueue { try { keep = idler.queueIdle(); } catch (Throwable t) { - Log.wtf("MessageQueue", "IdleHandler threw exception", t); + Log.wtf(TAG, "IdleHandler threw exception", t); } if (!keep) { @@ -251,7 +432,34 @@ public final class MessageQueue { } } - int enqueueSyncBarrier(long when) { + /** + * Posts a synchronization barrier to the Looper's message queue. + * + * Message processing occurs as usual until the message queue encounters the + * synchronization barrier that has been posted. When the barrier is encountered, + * later synchronous messages in the queue are stalled (prevented from being executed) + * until the barrier is released by calling {@link #removeSyncBarrier} and specifying + * the token that identifies the synchronization barrier. + * + * This method is used to immediately postpone execution of all subsequently posted + * synchronous messages until a condition is met that releases the barrier. + * Asynchronous messages (see {@link Message#isAsynchronous} are exempt from the barrier + * and continue to be processed as usual. + * + * This call must be always matched by a call to {@link #removeSyncBarrier} with + * the same token to ensure that the message queue resumes normal operation. + * Otherwise the application will probably hang! + * + * @return A token that uniquely identifies the barrier. This token must be + * passed to {@link #removeSyncBarrier} to release the barrier. + * + * @hide + */ + public int postSyncBarrier() { + return postSyncBarrier(SystemClock.uptimeMillis()); + } + + private int postSyncBarrier(long when) { // Enqueue a new sync barrier token. // We don't need to wake the queue because the purpose of a barrier is to stall it. synchronized (this) { @@ -280,7 +488,17 @@ public final class MessageQueue { } } - void removeSyncBarrier(int token) { + /** + * Removes a synchronization barrier. + * + * @param token The synchronization barrier token that was returned by + * {@link #postSyncBarrier}. + * + * @throws IllegalStateException if the barrier was not found. + * + * @hide + */ + public void removeSyncBarrier(int token) { // Remove a sync barrier token from the queue. // If the queue is no longer stalled by a barrier then wake it. synchronized (this) { @@ -324,7 +542,7 @@ public final class MessageQueue { if (mQuitting) { IllegalStateException e = new IllegalStateException( msg.target + " sending message to a Handler on a dead thread"); - Log.w("MessageQueue", e.getMessage(), e); + Log.w(TAG, e.getMessage(), e); msg.recycle(); return false; } @@ -400,18 +618,6 @@ public final class MessageQueue { } } - boolean isIdling() { - synchronized (this) { - return isIdlingLocked(); - } - } - - private boolean isIdlingLocked() { - // If the loop is quitting then it must not be idling. - // We can assume mPtr != 0 when mQuitting is false. - return !mQuitting && nativeIsIdling(mPtr); - } - void removeMessages(Handler h, int what, Object object) { if (h == null) { return; @@ -559,8 +765,113 @@ public final class MessageQueue { pw.println(prefix + "Message " + n + ": " + msg.toString(now)); n++; } - pw.println(prefix + "(Total messages: " + n + ", idling=" + isIdlingLocked() + pw.println(prefix + "(Total messages: " + n + ", polling=" + isPollingLocked() + ", quitting=" + mQuitting + ")"); } } + + /** + * Callback interface for discovering when a thread is going to block + * waiting for more messages. + */ + public static interface IdleHandler { + /** + * Called when the message queue has run out of messages and will now + * wait for more. Return true to keep your idle handler active, false + * to have it removed. This may be called if there are still messages + * pending in the queue, but they are all scheduled to be dispatched + * after the current time. + */ + boolean queueIdle(); + } + + /** + * A callback which is invoked when file descriptor related events occur. + */ + public static abstract class FileDescriptorCallback { + /** + * File descriptor event: Indicates that the file descriptor is ready for input + * operations, such as reading. + * <p> + * The callback should read all available data from the file descriptor + * then return <code>true</code> to keep the callback active or <code>false</code> + * to remove the callback. + * </p><p> + * In the case of a socket, this event may be generated to indicate + * that there is at least one incoming connection that the callback + * should accept. + * </p><p> + * This event will only be generated if the {@link #EVENT_INPUT} event mask was + * specified when the callback was added. + * </p> + */ + public static final int EVENT_INPUT = 1 << 0; + + /** + * File descriptor event: Indicates that the file descriptor is ready for output + * operations, such as writing. + * <p> + * The callback should write as much data as it needs. If it could not + * write everything at once, then it should return <code>true</code> to + * keep the callback active. Otherwise, it should return <code>false</code> + * to remove the callback then re-register it later when it needs to write + * something else. + * </p><p> + * This event will only be generated if the {@link #EVENT_OUTPUT} event mask was + * specified when the callback was added. + * </p> + */ + public static final int EVENT_OUTPUT = 1 << 1; + + /** + * File descriptor event: Indicates that the file descriptor encountered a + * fatal error. + * <p> + * File descriptor errors can occur for various reasons. One common error + * is when the remote peer of a socket or pipe closes its end of the connection. + * </p><p> + * This event may be generated at any time regardless of whether the + * {@link #EVENT_ERROR} event mask was specified when the callback was added. + * </p> + */ + public static final int EVENT_ERROR = 1 << 2; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag=true, value={EVENT_INPUT, EVENT_OUTPUT, EVENT_ERROR}) + public @interface Events {} + + /** + * Called when a file descriptor receives events. + * <p> + * The default implementation does nothing and returns 0 to unregister the callback. + * </p> + * + * @param fd The file descriptor. + * @param events The set of events that occurred: a combination of the + * {@link #EVENT_INPUT}, {@link #EVENT_OUTPUT}, and {@link #EVENT_ERROR} event masks. + * @return The new set of events to watch, or 0 to unregister the callback. + * + * @see #EVENT_INPUT + * @see #EVENT_OUTPUT + * @see #EVENT_ERROR + */ + public @Events int onFileDescriptorEvents(@NonNull FileDescriptor fd, @Events int events) { + return 0; + } + } + + private static final class FileDescriptorRecord { + public final FileDescriptor mDescriptor; + public int mEvents; + public FileDescriptorCallback mCallback; + public int mSeq; + + public FileDescriptorRecord(FileDescriptor descriptor, + int events, FileDescriptorCallback callback) { + mDescriptor = descriptor; + mEvents = events; + mCallback = callback; + } + } } diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index 3d5215b..9d8a1ba 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -1059,6 +1059,21 @@ public final class Parcel { } } + /** + * @hide + */ + public final void writeCharSequenceList(ArrayList<CharSequence> val) { + if (val != null) { + int N = val.size(); + writeInt(N); + for (int i=0; i<N; i++) { + writeCharSequence(val.get(i)); + } + } else { + writeInt(-1); + } + } + public final IBinder[] createBinderArray() { int N = readInt(); if (N >= 0) { @@ -1828,6 +1843,25 @@ public final class Parcel { } /** + * Read and return an ArrayList<CharSequence> object from the parcel. + * {@hide} + */ + public final ArrayList<CharSequence> readCharSequenceList() { + ArrayList<CharSequence> array = null; + + int length = readInt(); + if (length >= 0) { + array = new ArrayList<CharSequence>(length); + + for (int i = 0 ; i < length ; i++) { + array.add(readCharSequence()); + } + } + + return array; + } + + /** * Read and return a new ArrayList object from the parcel at the current * dataPosition(). Returns null if the previously written list object was * null. The given class loader will be used to load any enclosed diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java index c6b2151..5b26304 100644 --- a/core/java/android/os/ParcelFileDescriptor.java +++ b/core/java/android/os/ParcelFileDescriptor.java @@ -19,11 +19,13 @@ package android.os; import static android.system.OsConstants.AF_UNIX; import static android.system.OsConstants.SEEK_SET; import static android.system.OsConstants.SOCK_STREAM; +import static android.system.OsConstants.SOCK_SEQPACKET; import static android.system.OsConstants.S_ISLNK; import static android.system.OsConstants.S_ISREG; import android.content.BroadcastReceiver; import android.content.ContentProvider; +import android.os.MessageQueue.FileDescriptorCallback; import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; @@ -31,7 +33,6 @@ import android.system.StructStat; import android.util.Log; import dalvik.system.CloseGuard; - import libcore.io.IoUtils; import libcore.io.Memory; @@ -220,8 +221,8 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { * be opened with the requested mode. * @see #parseMode(String) */ - public static ParcelFileDescriptor open( - File file, int mode, Handler handler, OnCloseListener listener) throws IOException { + public static ParcelFileDescriptor open(File file, int mode, Handler handler, + final OnCloseListener listener) throws IOException { if (handler == null) { throw new IllegalArgumentException("Handler must not be null"); } @@ -234,11 +235,27 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { final FileDescriptor[] comm = createCommSocketPair(); final ParcelFileDescriptor pfd = new ParcelFileDescriptor(fd, comm[0]); - - // Kick off thread to watch for status updates - IoUtils.setBlocking(comm[1], true); - final ListenerBridge bridge = new ListenerBridge(comm[1], handler.getLooper(), listener); - bridge.start(); + final MessageQueue queue = handler.getLooper().getQueue(); + queue.registerFileDescriptorCallback(comm[1], + FileDescriptorCallback.EVENT_INPUT, new FileDescriptorCallback() { + @Override + public int onFileDescriptorEvents(FileDescriptor fd, int events) { + Status status = null; + if ((events & FileDescriptorCallback.EVENT_INPUT) != 0) { + final byte[] buf = new byte[MAX_STATUS]; + status = readCommStatus(fd, buf); + } else if ((events & FileDescriptorCallback.EVENT_ERROR) != 0) { + status = new Status(Status.DEAD); + } + if (status != null) { + queue.unregisterFileDescriptorCallback(fd); + IoUtils.closeQuietly(fd); + listener.onClose(status.asIOException()); + return 0; + } + return EVENT_INPUT; + } + }); return pfd; } @@ -395,10 +412,17 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { * connected to each other. The two sockets are indistinguishable. */ public static ParcelFileDescriptor[] createSocketPair() throws IOException { + return createSocketPair(SOCK_STREAM); + } + + /** + * @hide + */ + public static ParcelFileDescriptor[] createSocketPair(int type) throws IOException { try { final FileDescriptor fd0 = new FileDescriptor(); final FileDescriptor fd1 = new FileDescriptor(); - Os.socketpair(AF_UNIX, SOCK_STREAM, 0, fd0, fd1); + Os.socketpair(AF_UNIX, type, 0, fd0, fd1); return new ParcelFileDescriptor[] { new ParcelFileDescriptor(fd0), new ParcelFileDescriptor(fd1) }; @@ -417,11 +441,18 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { * This can also be used to detect remote crashes. */ public static ParcelFileDescriptor[] createReliableSocketPair() throws IOException { + return createReliableSocketPair(SOCK_STREAM); + } + + /** + * @hide + */ + public static ParcelFileDescriptor[] createReliableSocketPair(int type) throws IOException { try { final FileDescriptor[] comm = createCommSocketPair(); final FileDescriptor fd0 = new FileDescriptor(); final FileDescriptor fd1 = new FileDescriptor(); - Os.socketpair(AF_UNIX, SOCK_STREAM, 0, fd0, fd1); + Os.socketpair(AF_UNIX, type, 0, fd0, fd1); return new ParcelFileDescriptor[] { new ParcelFileDescriptor(fd0, comm[0]), new ParcelFileDescriptor(fd1, comm[1]) }; @@ -432,9 +463,12 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { private static FileDescriptor[] createCommSocketPair() throws IOException { try { + // Use SOCK_SEQPACKET so that we have a guarantee that the status + // is written and read atomically as one unit and is not split + // across multiple IO operations. final FileDescriptor comm1 = new FileDescriptor(); final FileDescriptor comm2 = new FileDescriptor(); - Os.socketpair(AF_UNIX, SOCK_STREAM, 0, comm1, comm2); + Os.socketpair(AF_UNIX, SOCK_SEQPACKET, 0, comm1, comm2); IoUtils.setBlocking(comm1, false); IoUtils.setBlocking(comm2, false); return new FileDescriptor[] { comm1, comm2 }; @@ -695,6 +729,7 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { writePtr += len; } + // Must write the entire status as a single operation. Os.write(mCommFd, buf, 0, writePtr); } catch (ErrnoException e) { // Reporting status is best-effort @@ -712,6 +747,7 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { private static Status readCommStatus(FileDescriptor comm, byte[] buf) { try { + // Must read the entire status as a single operation. final int n = Os.read(comm, buf, 0, buf.length); if (n == 0) { // EOF means they're dead @@ -1000,39 +1036,10 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { return new IOException("Unknown status: " + status); } } - } - - /** - * Bridge to watch for remote status, and deliver to listener. Currently - * requires that communication socket is <em>blocking</em>. - */ - private static final class ListenerBridge extends Thread { - // TODO: switch to using Looper to avoid burning a thread - - private FileDescriptor mCommFd; - private final Handler mHandler; - - public ListenerBridge(FileDescriptor comm, Looper looper, final OnCloseListener listener) { - mCommFd = comm; - mHandler = new Handler(looper) { - @Override - public void handleMessage(Message msg) { - final Status s = (Status) msg.obj; - listener.onClose(s != null ? s.asIOException() : null); - } - }; - } @Override - public void run() { - try { - final byte[] buf = new byte[MAX_STATUS]; - final Status status = readCommStatus(mCommFd, buf); - mHandler.obtainMessage(0, status).sendToTarget(); - } finally { - IoUtils.closeQuietly(mCommFd); - mCommFd = null; - } + public String toString() { + return "{" + status + ": " + msg + "}"; } } } diff --git a/core/java/android/os/PersistableBundle.java b/core/java/android/os/PersistableBundle.java index 3a44428..ea180b2 100644 --- a/core/java/android/os/PersistableBundle.java +++ b/core/java/android/os/PersistableBundle.java @@ -16,6 +16,7 @@ package android.os; +import android.annotation.Nullable; import android.util.ArrayMap; import com.android.internal.util.XmlUtils; import org.xmlpull.v1.XmlPullParser; @@ -44,6 +45,16 @@ public final class PersistableBundle extends BaseBundle implements Cloneable, Pa EMPTY_PARCEL = BaseBundle.EMPTY_PARCEL; } + /** @hide */ + public static boolean isValidType(Object value) { + return (value instanceof Integer) || (value instanceof Long) || + (value instanceof Double) || (value instanceof String) || + (value instanceof int[]) || (value instanceof long[]) || + (value instanceof double[]) || (value instanceof String[]) || + (value instanceof PersistableBundle) || (value == null) || + (value instanceof Boolean) || (value instanceof boolean[]); + } + /** * Constructs a new, empty PersistableBundle. */ @@ -77,29 +88,22 @@ public final class PersistableBundle extends BaseBundle implements Cloneable, Pa * @param map a Map containing only those items that can be persisted. * @throws IllegalArgumentException if any element of #map cannot be persisted. */ - private PersistableBundle(Map<String, Object> map) { + private PersistableBundle(ArrayMap<String, Object> map) { super(); // First stuff everything in. putAll(map); // Now verify each item throwing an exception if there is a violation. - Set<String> keys = map.keySet(); - Iterator<String> iterator = keys.iterator(); - while (iterator.hasNext()) { - String key = iterator.next(); - Object value = map.get(key); - if (value instanceof Map) { + final int N = mMap.size(); + for (int i=0; i<N; i++) { + Object value = mMap.valueAt(i); + if (value instanceof ArrayMap) { // Fix up any Maps by replacing them with PersistableBundles. - putPersistableBundle(key, new PersistableBundle((Map<String, Object>) value)); - } else if (!(value instanceof Integer) && !(value instanceof Long) && - !(value instanceof Double) && !(value instanceof String) && - !(value instanceof int[]) && !(value instanceof long[]) && - !(value instanceof double[]) && !(value instanceof String[]) && - !(value instanceof PersistableBundle) && (value != null) && - !(value instanceof Boolean) && !(value instanceof boolean[])) { - throw new IllegalArgumentException("Bad value in PersistableBundle key=" + key + - " value=" + value); + mMap.setValueAt(i, new PersistableBundle((ArrayMap<String, Object>) value)); + } else if (!isValidType(value)) { + throw new IllegalArgumentException("Bad value in PersistableBundle key=" + + mMap.keyAt(i) + " value=" + value); } } } @@ -135,7 +139,7 @@ public final class PersistableBundle extends BaseBundle implements Cloneable, Pa * @param key a String, or null * @param value a Bundle object, or null */ - public void putPersistableBundle(String key, PersistableBundle value) { + public void putPersistableBundle(@Nullable String key, @Nullable PersistableBundle value) { unparcel(); mMap.put(key, value); } @@ -148,7 +152,8 @@ public final class PersistableBundle extends BaseBundle implements Cloneable, Pa * @param key a String, or null * @return a Bundle value, or null */ - public PersistableBundle getPersistableBundle(String key) { + @Nullable + public PersistableBundle getPersistableBundle(@Nullable String key) { unparcel(); Object o = mMap.get(key); if (o == null) { @@ -240,8 +245,9 @@ public final class PersistableBundle extends BaseBundle implements Cloneable, Pa while (((event = in.next()) != XmlPullParser.END_DOCUMENT) && (event != XmlPullParser.END_TAG || in.getDepth() < outerDepth)) { if (event == XmlPullParser.START_TAG) { - return new PersistableBundle((Map<String, Object>) - XmlUtils.readThisMapXml(in, startTag, tagName, new MyReadMapCallback())); + return new PersistableBundle((ArrayMap<String, Object>) + XmlUtils.readThisArrayMapXml(in, startTag, tagName, + new MyReadMapCallback())); } } return EMPTY; diff --git a/core/java/android/os/PooledStringReader.java b/core/java/android/os/PooledStringReader.java new file mode 100644 index 0000000..7795957 --- /dev/null +++ b/core/java/android/os/PooledStringReader.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2015 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.os; + +/** + * Helper class for reading pooling strings from a Parcel. It must be used + * in conjunction with {@link android.os.PooledStringWriter}. This really needs + * to be pushed in to Parcel itself, but doing that is... complicated. + * @hide + */ +public class PooledStringReader { + private final Parcel mIn; + + /** + * The pool of strings we have collected so far. + */ + private final String[] mPool; + + public PooledStringReader(Parcel in) { + mIn = in; + final int size = in.readInt(); + mPool = new String[size]; + } + + public String readString() { + int idx = mIn.readInt(); + if (idx >= 0) { + return mPool[idx]; + } else { + idx = (-idx) - 1; + String str = mIn.readString(); + mPool[idx] = str; + return str; + } + } +} diff --git a/core/java/android/os/PooledStringWriter.java b/core/java/android/os/PooledStringWriter.java new file mode 100644 index 0000000..eac297d --- /dev/null +++ b/core/java/android/os/PooledStringWriter.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2015 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.os; + +import java.util.HashMap; + +/** + * Helper class for writing pooled strings into a Parcel. It must be used + * in conjunction with {@link android.os.PooledStringReader}. This really needs + * to be pushed in to Parcel itself, but doing that is... complicated. + * @hide + */ +public class PooledStringWriter { + private final Parcel mOut; + + /** + * Book-keeping for writing pooled string objects, mapping strings we have + * written so far to their index in the pool. We deliberately use HashMap + * here since performance is critical, we expect to be doing lots of adds to + * it, and it is only a temporary object so its overall memory footprint is + * not a signifciant issue. + */ + private final HashMap<String, Integer> mPool; + + /** + * Book-keeping for writing pooling string objects, indicating where we + * started writing the pool, which is where we need to ultimately store + * how many strings are in the pool. + */ + private int mStart; + + /** + * Next available index in the pool. + */ + private int mNext; + + public PooledStringWriter(Parcel out) { + mOut = out; + mPool = new HashMap<>(); + mStart = out.dataPosition(); + out.writeInt(0); // reserve space for final pool size. + } + + public void writeString(String str) { + final Integer cur = mPool.get(str); + if (cur != null) { + mOut.writeInt(cur); + } else { + mPool.put(str, mNext); + mOut.writeInt(-(mNext+1)); + mOut.writeString(str); + mNext++; + } + } + + public void finish() { + final int pos = mOut.dataPosition(); + mOut.setDataPosition(mStart); + mOut.writeInt(mNext); + mOut.setDataPosition(pos); + } +} diff --git a/core/java/android/os/StrictMode.java b/core/java/android/os/StrictMode.java index 5018711..0b55998 100644 --- a/core/java/android/os/StrictMode.java +++ b/core/java/android/os/StrictMode.java @@ -145,7 +145,7 @@ public final class StrictMode { * in {@link VmPolicy.Builder#detectAll()}. Apps can still always opt-into * detection using {@link VmPolicy.Builder#detectCleartextNetwork()}. */ - private static final String CLEARTEXT_PROPERTY = "persist.sys.strictmode.nonssl"; + private static final String CLEARTEXT_PROPERTY = "persist.sys.strictmode.clear"; // Only log a duplicate stack trace to the logs every second. private static final long MIN_LOG_INTERVAL_MS = 1000; @@ -184,8 +184,16 @@ public final class StrictMode { */ public static final int DETECT_CUSTOM = 0x08; // for ThreadPolicy + /** + * For StrictMode.noteResourceMismatch() + * + * @hide + */ + public static final int DETECT_RESOURCE_MISMATCH = 0x10; // for ThreadPolicy + private static final int ALL_THREAD_DETECT_BITS = - DETECT_DISK_WRITE | DETECT_DISK_READ | DETECT_NETWORK | DETECT_CUSTOM; + DETECT_DISK_WRITE | DETECT_DISK_READ | DETECT_NETWORK | DETECT_CUSTOM | + DETECT_RESOURCE_MISMATCH; // Byte 2: Process-policy @@ -460,6 +468,22 @@ public final class StrictMode { } /** + * Disable detection of mismatches between defined resource types + * and getter calls. + */ + public Builder permitResourceMismatches() { + return disable(DETECT_RESOURCE_MISMATCH); + } + + /** + * Enable detection of mismatches between defined resource types + * and getter calls. + */ + public Builder detectResourceMismatches() { + return enable(DETECT_RESOURCE_MISMATCH); + } + + /** * Enable detection of disk writes. */ public Builder detectDiskWrites() { @@ -739,8 +763,6 @@ public final class StrictMode { * This inspects both IPv4/IPv6 and TCP/UDP network traffic, but it * may be subject to false positives, such as when STARTTLS * protocols or HTTP proxies are used. - * - * @hide */ public Builder detectCleartextNetwork() { return enable(DETECT_VM_CLEARTEXT_NETWORK); @@ -760,7 +782,6 @@ public final class StrictMode { * detected. * * @see #detectCleartextNetwork() - * @hide */ public Builder penaltyDeathOnCleartextNetwork() { return enable(PENALTY_DEATH_ON_CLEARTEXT_NETWORK); @@ -923,6 +944,15 @@ public final class StrictMode { } /** + * @hide + */ + private static class StrictModeResourceMismatchViolation extends StrictModeViolation { + public StrictModeResourceMismatchViolation(int policyMask, Object tag) { + super(policyMask, DETECT_RESOURCE_MISMATCH, tag != null ? tag.toString() : null); + } + } + + /** * Returns the bitmask of the current thread's policy. * * @return the bitmask of all the DETECT_* and PENALTY_* bits currently enabled @@ -1197,6 +1227,20 @@ public final class StrictMode { startHandlingViolationException(e); } + // Not part of BlockGuard.Policy; just part of StrictMode: + void onResourceMismatch(Object tag) { + if ((mPolicyMask & DETECT_RESOURCE_MISMATCH) == 0) { + return; + } + if (tooManyViolationsThisLoop()) { + return; + } + BlockGuard.BlockGuardPolicyException e = + new StrictModeResourceMismatchViolation(mPolicyMask, tag); + e.fillInStackTrace(); + startHandlingViolationException(e); + } + // Part of BlockGuard.Policy interface: public void onReadFromDisk() { if ((mPolicyMask & DETECT_DISK_READ) == 0) { @@ -2083,6 +2127,26 @@ public final class StrictMode { } /** + * For code to note that a resource was obtained using a type other than + * its defined type. This is a no-op unless the current thread's + * {@link android.os.StrictMode.ThreadPolicy} has + * {@link android.os.StrictMode.ThreadPolicy.Builder#detectResourceMismatches()} + * enabled. + * + * @param tag an object for the exception stack trace that's + * built if when this fires. + * @hide + */ + public static void noteResourceMismatch(Object tag) { + BlockGuard.Policy policy = BlockGuard.getThreadPolicy(); + if (!(policy instanceof AndroidBlockGuardPolicy)) { + // StrictMode not enabled. + return; + } + ((AndroidBlockGuardPolicy) policy).onResourceMismatch(tag); + } + + /** * @hide */ public static void noteDiskRead() { diff --git a/core/java/android/os/UserHandle.java b/core/java/android/os/UserHandle.java index 74e064e..6874e77 100644 --- a/core/java/android/os/UserHandle.java +++ b/core/java/android/os/UserHandle.java @@ -20,7 +20,6 @@ import android.annotation.SystemApi; import android.util.SparseArray; import java.io.PrintWriter; -import java.util.HashMap; /** * Representation of a user on the device. diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index d124a49..3601a1c 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -22,6 +22,7 @@ import android.content.Context; import android.content.pm.UserInfo; import android.content.res.Resources; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.provider.Settings; @@ -30,6 +31,7 @@ import android.view.WindowManager.LayoutParams; import com.android.internal.R; +import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -114,6 +116,7 @@ public class UserManager { /** * Specifies if a user is disallowed from configuring bluetooth. + * This does <em>not</em> restrict the user from turning bluetooth on or off. * The default value is <code>false</code>. * <p/>This restriction has no effect in a managed profile. * @@ -388,6 +391,27 @@ public class UserManager { public static final String DISALLOW_OUTGOING_BEAM = "no_outgoing_beam"; /** + * Hidden user restriction to disallow access to wallpaper manager APIs. This user restriction + * is always set for managed profiles. + * @hide + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_WALLPAPER = "no_wallpaper"; + + /** + * Specifies if the user is not allowed to reboot the device into safe boot mode. + * This can only be set by device owners and profile owners on the primary user. + * The default value is <code>false</code>. + * + * <p/>Key for user restrictions. + * <p/>Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_SAFE_BOOT = "no_safe_boot"; + + /** * Application restriction key that is used to indicate the pending arrival * of real restrictions for the app. * @@ -1083,11 +1107,21 @@ public class UserManager { */ public Bitmap getUserIcon(int userHandle) { try { - return mService.getUserIcon(userHandle); + ParcelFileDescriptor fd = mService.getUserIcon(userHandle); + if (fd != null) { + try { + return BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor()); + } finally { + try { + fd.close(); + } catch (IOException e) { + } + } + } } catch (RemoteException re) { Log.w(TAG, "Could not get the user icon ", re); - return null; } + return null; } /** diff --git a/core/java/android/preference/DialogPreference.java b/core/java/android/preference/DialogPreference.java index b65eac7..3d57b4d 100644 --- a/core/java/android/preference/DialogPreference.java +++ b/core/java/android/preference/DialogPreference.java @@ -17,6 +17,9 @@ package android.preference; +import android.annotation.CallSuper; +import android.annotation.DrawableRes; +import android.annotation.StringRes; import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; @@ -168,7 +171,7 @@ public abstract class DialogPreference extends Preference implements * * @param dialogIconRes The icon, as a resource ID. */ - public void setDialogIcon(int dialogIconRes) { + public void setDialogIcon(@DrawableRes int dialogIconRes) { mDialogIcon = getContext().getDrawable(dialogIconRes); } @@ -194,7 +197,7 @@ public abstract class DialogPreference extends Preference implements * @see #setPositiveButtonText(CharSequence) * @param positiveButtonTextResId The positive button text as a resource. */ - public void setPositiveButtonText(int positiveButtonTextResId) { + public void setPositiveButtonText(@StringRes int positiveButtonTextResId) { setPositiveButtonText(getContext().getString(positiveButtonTextResId)); } @@ -222,7 +225,7 @@ public abstract class DialogPreference extends Preference implements * @see #setNegativeButtonText(CharSequence) * @param negativeButtonTextResId The negative button text as a resource. */ - public void setNegativeButtonText(int negativeButtonTextResId) { + public void setNegativeButtonText(@StringRes int negativeButtonTextResId) { setNegativeButtonText(getContext().getString(negativeButtonTextResId)); } @@ -358,6 +361,7 @@ public abstract class DialogPreference extends Preference implements * * @param view The content View of the dialog, if it is custom. */ + @CallSuper protected void onBindDialogView(View view) { View dialogMessageView = view.findViewById(com.android.internal.R.id.message); diff --git a/core/java/android/preference/GenericInflater.java b/core/java/android/preference/GenericInflater.java index 7de7d1c..918933b 100644 --- a/core/java/android/preference/GenericInflater.java +++ b/core/java/android/preference/GenericInflater.java @@ -23,6 +23,7 @@ import java.util.HashMap; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import android.annotation.XmlRes; import android.content.Context; import android.content.res.XmlResourceParser; import android.util.AttributeSet; @@ -216,7 +217,7 @@ abstract class GenericInflater<T, P extends GenericInflater.Parent> { * this is the root item; otherwise it is the root of the inflated * XML file. */ - public T inflate(int resource, P root) { + public T inflate(@XmlRes int resource, P root) { return inflate(resource, root, root != null); } @@ -256,7 +257,7 @@ abstract class GenericInflater<T, P extends GenericInflater.Parent> { * attachToRoot is true, this is root; otherwise it is the root of * the inflated XML file. */ - public T inflate(int resource, P root, boolean attachToRoot) { + public T inflate(@XmlRes int resource, P root, boolean attachToRoot) { if (DEBUG) System.out.println("INFLATING from resource: " + resource); XmlResourceParser parser = getContext().getResources().getXml(resource); try { diff --git a/core/java/android/preference/ListPreference.java b/core/java/android/preference/ListPreference.java index 9482a72..2700373 100644 --- a/core/java/android/preference/ListPreference.java +++ b/core/java/android/preference/ListPreference.java @@ -16,6 +16,7 @@ package android.preference; +import android.annotation.ArrayRes; import android.app.AlertDialog.Builder; import android.content.Context; import android.content.DialogInterface; @@ -91,7 +92,7 @@ public class ListPreference extends DialogPreference { * @see #setEntries(CharSequence[]) * @param entriesResId The entries array as a resource. */ - public void setEntries(int entriesResId) { + public void setEntries(@ArrayRes int entriesResId) { setEntries(getContext().getResources().getTextArray(entriesResId)); } @@ -119,7 +120,7 @@ public class ListPreference extends DialogPreference { * @see #setEntryValues(CharSequence[]) * @param entryValuesResId The entry values array as a resource. */ - public void setEntryValues(int entryValuesResId) { + public void setEntryValues(@ArrayRes int entryValuesResId) { setEntryValues(getContext().getResources().getTextArray(entryValuesResId)); } diff --git a/core/java/android/preference/MultiCheckPreference.java b/core/java/android/preference/MultiCheckPreference.java index 57c906d..c1260a4 100644 --- a/core/java/android/preference/MultiCheckPreference.java +++ b/core/java/android/preference/MultiCheckPreference.java @@ -18,6 +18,7 @@ package android.preference; import java.util.Arrays; +import android.annotation.ArrayRes; import android.app.AlertDialog.Builder; import android.content.Context; import android.content.DialogInterface; @@ -96,7 +97,7 @@ public class MultiCheckPreference extends DialogPreference { * @see #setEntries(CharSequence[]) * @param entriesResId The entries array as a resource. */ - public void setEntries(int entriesResId) { + public void setEntries(@ArrayRes int entriesResId) { setEntries(getContext().getResources().getTextArray(entriesResId)); } @@ -126,7 +127,7 @@ public class MultiCheckPreference extends DialogPreference { * @see #setEntryValues(CharSequence[]) * @param entryValuesResId The entry values array as a resource. */ - public void setEntryValues(int entryValuesResId) { + public void setEntryValues(@ArrayRes int entryValuesResId) { setEntryValuesCS(getContext().getResources().getTextArray(entryValuesResId)); } diff --git a/core/java/android/preference/MultiSelectListPreference.java b/core/java/android/preference/MultiSelectListPreference.java index 6c4c20f..138bd87 100644 --- a/core/java/android/preference/MultiSelectListPreference.java +++ b/core/java/android/preference/MultiSelectListPreference.java @@ -16,6 +16,7 @@ package android.preference; +import android.annotation.ArrayRes; import android.app.AlertDialog.Builder; import android.content.Context; import android.content.DialogInterface; @@ -87,7 +88,7 @@ public class MultiSelectListPreference extends DialogPreference { * @see #setEntries(CharSequence[]) * @param entriesResId The entries array as a resource. */ - public void setEntries(int entriesResId) { + public void setEntries(@ArrayRes int entriesResId) { setEntries(getContext().getResources().getTextArray(entriesResId)); } @@ -115,7 +116,7 @@ public class MultiSelectListPreference extends DialogPreference { * @see #setEntryValues(CharSequence[]) * @param entryValuesResId The entry values array as a resource. */ - public void setEntryValues(int entryValuesResId) { + public void setEntryValues(@ArrayRes int entryValuesResId) { setEntryValues(getContext().getResources().getTextArray(entryValuesResId)); } diff --git a/core/java/android/preference/Preference.java b/core/java/android/preference/Preference.java index 0224c73..ccf2cfa 100644 --- a/core/java/android/preference/Preference.java +++ b/core/java/android/preference/Preference.java @@ -16,8 +16,12 @@ package android.preference; +import android.annotation.CallSuper; import com.android.internal.util.CharSequences; +import android.annotation.DrawableRes; +import android.annotation.LayoutRes; +import android.annotation.StringRes; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -424,7 +428,7 @@ public class Preference implements Comparable<Preference> { * a {@link View}. * @see #setWidgetLayoutResource(int) */ - public void setLayoutResource(int layoutResId) { + public void setLayoutResource(@LayoutRes int layoutResId) { if (layoutResId != mLayoutResId) { // Layout changed mCanRecycleLayout = false; @@ -438,6 +442,7 @@ public class Preference implements Comparable<Preference> { * * @return The layout resource ID. */ + @LayoutRes public int getLayoutResource() { return mLayoutResId; } @@ -452,7 +457,7 @@ public class Preference implements Comparable<Preference> { * main layout. * @see #setLayoutResource(int) */ - public void setWidgetLayoutResource(int widgetLayoutResId) { + public void setWidgetLayoutResource(@LayoutRes int widgetLayoutResId) { if (widgetLayoutResId != mWidgetLayoutResId) { // Layout changed mCanRecycleLayout = false; @@ -465,6 +470,7 @@ public class Preference implements Comparable<Preference> { * * @return The layout resource ID. */ + @LayoutRes public int getWidgetLayoutResource() { return mWidgetLayoutResId; } @@ -503,6 +509,7 @@ public class Preference implements Comparable<Preference> { * @return The View that displays this Preference. * @see #onBindView(View) */ + @CallSuper protected View onCreateView(ViewGroup parent) { final LayoutInflater layoutInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); @@ -532,6 +539,7 @@ public class Preference implements Comparable<Preference> { * @param view The View that shows this Preference. * @see #onCreateView(ViewGroup) */ + @CallSuper protected void onBindView(View view) { final TextView titleView = (TextView) view.findViewById(com.android.internal.R.id.title); if (titleView != null) { @@ -648,7 +656,7 @@ public class Preference implements Comparable<Preference> { * @see #setTitle(CharSequence) * @param titleResId The title as a resource ID. */ - public void setTitle(int titleResId) { + public void setTitle(@StringRes int titleResId) { setTitle(mContext.getString(titleResId)); mTitleRes = titleResId; } @@ -660,6 +668,7 @@ public class Preference implements Comparable<Preference> { * @return The title resource. * @see #setTitle(int) */ + @StringRes public int getTitleRes() { return mTitleRes; } @@ -696,7 +705,7 @@ public class Preference implements Comparable<Preference> { * @see #setIcon(Drawable) * @param iconResId The icon as a resource ID. */ - public void setIcon(int iconResId) { + public void setIcon(@DrawableRes int iconResId) { mIconResId = iconResId; setIcon(mContext.getDrawable(iconResId)); } @@ -739,7 +748,7 @@ public class Preference implements Comparable<Preference> { * @see #setSummary(CharSequence) * @param summaryResId The summary as a resource. */ - public void setSummary(int summaryResId) { + public void setSummary(@StringRes int summaryResId) { setSummary(mContext.getString(summaryResId)); } @@ -1350,6 +1359,7 @@ public class Preference implements Comparable<Preference> { * should remove any references to this Preference that you know about. Make * sure to call through to the superclass implementation. */ + @CallSuper protected void onPrepareForRemoval() { unregisterDependency(); } diff --git a/core/java/android/preference/PreferenceActivity.java b/core/java/android/preference/PreferenceActivity.java index 04cd7d5..06666f4 100644 --- a/core/java/android/preference/PreferenceActivity.java +++ b/core/java/android/preference/PreferenceActivity.java @@ -16,6 +16,9 @@ package android.preference; +import android.annotation.Nullable; +import android.annotation.StringRes; +import android.annotation.XmlRes; import android.app.Fragment; import android.app.FragmentBreadCrumbs; import android.app.FragmentManager; @@ -337,6 +340,7 @@ public abstract class PreferenceActivity extends ListActivity implements * Resource ID of title of the header that is shown to the user. * @attr ref android.R.styleable#PreferenceHeader_title */ + @StringRes public int titleRes; /** @@ -349,6 +353,7 @@ public abstract class PreferenceActivity extends ListActivity implements * Resource ID of optional summary describing what this header controls. * @attr ref android.R.styleable#PreferenceHeader_summary */ + @StringRes public int summaryRes; /** @@ -361,6 +366,7 @@ public abstract class PreferenceActivity extends ListActivity implements * Resource ID of optional text to show as the title in the bread crumb. * @attr ref android.R.styleable#PreferenceHeader_breadCrumbTitle */ + @StringRes public int breadCrumbTitleRes; /** @@ -373,6 +379,7 @@ public abstract class PreferenceActivity extends ListActivity implements * Resource ID of optional text to show as the short title in the bread crumb. * @attr ref android.R.styleable#PreferenceHeader_breadCrumbShortTitle */ + @StringRes public int breadCrumbShortTitleRes; /** @@ -525,7 +532,7 @@ public abstract class PreferenceActivity extends ListActivity implements } @Override - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Theming for the PreferenceActivity layout and for the Preference Header(s) layout @@ -797,7 +804,7 @@ public abstract class PreferenceActivity extends ListActivity implements * @param resid The XML resource to load and parse. * @param target The list in which the parsed headers should be placed. */ - public void loadHeadersFromResource(int resid, List<Header> target) { + public void loadHeadersFromResource(@XmlRes int resid, List<Header> target) { XmlResourceParser parser = null; try { parser = getResources().getXml(resid); @@ -1086,7 +1093,7 @@ public abstract class PreferenceActivity extends ListActivity implements * fragment. */ public Intent onBuildStartFragmentIntent(String fragmentName, Bundle args, - int titleRes, int shortTitleRes) { + @StringRes int titleRes, int shortTitleRes) { Intent intent = new Intent(Intent.ACTION_MAIN); intent.setClass(this, getClass()); intent.putExtra(EXTRA_SHOW_FRAGMENT, fragmentName); @@ -1124,7 +1131,8 @@ public abstract class PreferenceActivity extends ListActivity implements * this set of preferences. */ public void startWithFragment(String fragmentName, Bundle args, - Fragment resultTo, int resultRequestCode, int titleRes, int shortTitleRes) { + Fragment resultTo, int resultRequestCode, @StringRes int titleRes, + @StringRes int shortTitleRes) { Intent intent = onBuildStartFragmentIntent(fragmentName, args, titleRes, shortTitleRes); if (resultTo == null) { startActivity(intent); @@ -1343,9 +1351,9 @@ public abstract class PreferenceActivity extends ListActivity implements * preference panel is done. The launched panel must use * {@link #finishPreferencePanel(Fragment, int, Intent)} when done. * @param resultRequestCode If resultTo is non-null, this is the caller's - * request code to be received with the resut. + * request code to be received with the result. */ - public void startPreferencePanel(String fragmentClass, Bundle args, int titleRes, + public void startPreferencePanel(String fragmentClass, Bundle args, @StringRes int titleRes, CharSequence titleText, Fragment resultTo, int resultRequestCode) { if (mSinglePane) { startWithFragment(fragmentClass, args, resultTo, resultRequestCode, titleRes, 0); diff --git a/core/java/android/preference/PreferenceFragment.java b/core/java/android/preference/PreferenceFragment.java index e95e6e2..66642de 100644 --- a/core/java/android/preference/PreferenceFragment.java +++ b/core/java/android/preference/PreferenceFragment.java @@ -16,6 +16,8 @@ package android.preference; +import android.annotation.Nullable; +import android.annotation.XmlRes; import android.app.Activity; import android.app.Fragment; import android.content.Intent; @@ -153,15 +155,15 @@ public abstract class PreferenceFragment extends Fragment implements } @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mPreferenceManager = new PreferenceManager(getActivity(), FIRST_REQUEST_CODE); mPreferenceManager.setFragment(this); } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { TypedArray a = getActivity().obtainStyledAttributes(null, com.android.internal.R.styleable.PreferenceFragment, @@ -177,7 +179,7 @@ public abstract class PreferenceFragment extends Fragment implements } @Override - public void onActivityCreated(Bundle savedInstanceState) { + public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); if (mHavePrefs) { @@ -293,7 +295,7 @@ public abstract class PreferenceFragment extends Fragment implements * * @param preferencesResId The XML resource ID to inflate. */ - public void addPreferencesFromResource(int preferencesResId) { + public void addPreferencesFromResource(@XmlRes int preferencesResId) { requirePreferenceManager(); setPreferenceScreen(mPreferenceManager.inflateFromResource(getActivity(), diff --git a/core/java/android/preference/PreferenceGroup.java b/core/java/android/preference/PreferenceGroup.java index 2d35b1b..5e84086 100644 --- a/core/java/android/preference/PreferenceGroup.java +++ b/core/java/android/preference/PreferenceGroup.java @@ -19,12 +19,9 @@ package android.preference; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Map; - import android.content.Context; import android.content.res.TypedArray; import android.os.Bundle; -import android.os.Parcelable; import android.text.TextUtils; import android.util.AttributeSet; diff --git a/core/java/android/preference/PreferenceManager.java b/core/java/android/preference/PreferenceManager.java index 0a0e625..55ee77a 100644 --- a/core/java/android/preference/PreferenceManager.java +++ b/core/java/android/preference/PreferenceManager.java @@ -16,6 +16,7 @@ package android.preference; +import android.annotation.XmlRes; import android.app.Activity; import android.content.Context; import android.content.DialogInterface; @@ -263,7 +264,7 @@ public class PreferenceManager { * root). * @hide */ - public PreferenceScreen inflateFromResource(Context context, int resId, + public PreferenceScreen inflateFromResource(Context context, @XmlRes int resId, PreferenceScreen rootPreferences) { // Block commits setNoCommit(true); @@ -438,7 +439,7 @@ public class PreferenceManager { * and clear it followed by a call to this method with this * parameter set to true. */ - public static void setDefaultValues(Context context, int resId, boolean readAgain) { + public static void setDefaultValues(Context context, @XmlRes int resId, boolean readAgain) { // Use the default shared preferences name and mode setDefaultValues(context, getDefaultSharedPreferencesName(context), diff --git a/core/java/android/preference/SeekBarVolumizer.java b/core/java/android/preference/SeekBarVolumizer.java index c3dd4ce..30da0e7 100644 --- a/core/java/android/preference/SeekBarVolumizer.java +++ b/core/java/android/preference/SeekBarVolumizer.java @@ -21,6 +21,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.database.ContentObserver; +import android.media.AudioAttributes; import android.media.AudioManager; import android.media.Ringtone; import android.media.RingtoneManager; @@ -174,6 +175,11 @@ public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callba } if (mRingtone != null) { try { + mRingtone.setAudioAttributes(new AudioAttributes.Builder(mRingtone + .getAudioAttributes()) + .setFlags(AudioAttributes.FLAG_BYPASS_INTERRUPTION_POLICY | + AudioAttributes.FLAG_BYPASS_MUTE) + .build()); mRingtone.play(); } catch (Throwable e) { Log.w(TAG, "Error playing ringtone, stream " + mStreamType, e); diff --git a/core/java/android/preference/SwitchPreference.java b/core/java/android/preference/SwitchPreference.java index 53b5aad..9c3cefc 100644 --- a/core/java/android/preference/SwitchPreference.java +++ b/core/java/android/preference/SwitchPreference.java @@ -16,6 +16,7 @@ package android.preference; +import android.annotation.StringRes; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; @@ -169,7 +170,7 @@ public class SwitchPreference extends TwoStatePreference { * * @param resId The text as a string resource ID */ - public void setSwitchTextOn(int resId) { + public void setSwitchTextOn(@StringRes int resId) { setSwitchTextOn(getContext().getString(resId)); } @@ -179,7 +180,7 @@ public class SwitchPreference extends TwoStatePreference { * * @param resId The text as a string resource ID */ - public void setSwitchTextOff(int resId) { + public void setSwitchTextOff(@StringRes int resId) { setSwitchTextOff(getContext().getString(resId)); } diff --git a/core/java/android/preference/TwoStatePreference.java b/core/java/android/preference/TwoStatePreference.java index 3823b27..7037aca 100644 --- a/core/java/android/preference/TwoStatePreference.java +++ b/core/java/android/preference/TwoStatePreference.java @@ -16,6 +16,7 @@ package android.preference; +import android.annotation.StringRes; import android.content.Context; import android.content.SharedPreferences; import android.content.res.TypedArray; @@ -116,7 +117,7 @@ public abstract class TwoStatePreference extends Preference { * @see #setSummaryOn(CharSequence) * @param summaryResId The summary as a resource. */ - public void setSummaryOn(int summaryResId) { + public void setSummaryOn(@StringRes int summaryResId) { setSummaryOn(getContext().getString(summaryResId)); } @@ -144,7 +145,7 @@ public abstract class TwoStatePreference extends Preference { * @see #setSummaryOff(CharSequence) * @param summaryResId The summary as a resource. */ - public void setSummaryOff(int summaryResId) { + public void setSummaryOff(@StringRes int summaryResId) { setSummaryOff(getContext().getString(summaryResId)); } diff --git a/core/java/android/print/PrintAttributes.java b/core/java/android/print/PrintAttributes.java index 30f0c6a..90d30d6 100644 --- a/core/java/android/print/PrintAttributes.java +++ b/core/java/android/print/PrintAttributes.java @@ -45,11 +45,22 @@ public final class PrintAttributes implements Parcelable { private static final int VALID_COLOR_MODES = COLOR_MODE_MONOCHROME | COLOR_MODE_COLOR; + /** Duplex mode: No duplexing. */ + public static final int DUPLEX_MODE_NONE = 1 << 0; + /** Duplex mode: Pages are turned sideways along the long edge - like a book. */ + public static final int DUPLEX_MODE_LONG_EDGE = 1 << 1; + /** Duplex mode: Pages are turned upwards along the short edge - like a notpad. */ + public static final int DUPLEX_MODE_SHORT_EDGE = 1 << 2; + + private static final int VALID_DUPLEX_MODES = + DUPLEX_MODE_NONE | DUPLEX_MODE_LONG_EDGE | DUPLEX_MODE_SHORT_EDGE; + private MediaSize mMediaSize; private Resolution mResolution; private Margins mMinMargins; private int mColorMode; + private int mDuplexMode = DUPLEX_MODE_NONE; PrintAttributes() { /* hide constructor */ @@ -60,6 +71,7 @@ public final class PrintAttributes implements Parcelable { mResolution = (parcel.readInt() == 1) ? Resolution.createFromParcel(parcel) : null; mMinMargins = (parcel.readInt() == 1) ? Margins.createFromParcel(parcel) : null; mColorMode = parcel.readInt(); + mDuplexMode = parcel.readInt(); } /** @@ -74,7 +86,7 @@ public final class PrintAttributes implements Parcelable { /** * Sets the media size. * - * @param The media size. + * @param mediaSize The media size. * * @hide */ @@ -94,7 +106,7 @@ public final class PrintAttributes implements Parcelable { /** * Sets the resolution. * - * @param The resolution. + * @param resolution The resolution. * * @hide */ @@ -130,7 +142,7 @@ public final class PrintAttributes implements Parcelable { * </strong> * </p> * - * @param The margins. + * @param margins The margins. * * @hide */ @@ -153,7 +165,7 @@ public final class PrintAttributes implements Parcelable { /** * Sets the color mode. * - * @param The color mode. + * @param colorMode The color mode. * * @see #COLOR_MODE_MONOCHROME * @see #COLOR_MODE_COLOR @@ -179,6 +191,35 @@ public final class PrintAttributes implements Parcelable { } /** + * Gets the duplex mode. + * + * @return The duplex mode. + * + * @see #DUPLEX_MODE_NONE + * @see #DUPLEX_MODE_LONG_EDGE + * @see #DUPLEX_MODE_SHORT_EDGE + */ + public int getDuplexMode() { + return mDuplexMode; + } + + /** + * Sets the duplex mode. + * + * @param duplexMode The duplex mode. + * + * @see #DUPLEX_MODE_NONE + * @see #DUPLEX_MODE_LONG_EDGE + * @see #DUPLEX_MODE_SHORT_EDGE + * + * @hide + */ + public void setDuplexMode(int duplexMode) { + enforceValidDuplexMode(duplexMode); + mDuplexMode = duplexMode; + } + + /** * Gets a new print attributes instance which is in portrait orientation, * which is the media size is in portrait and all orientation dependent * attributes such as resolution and margins are properly adjusted. @@ -211,6 +252,7 @@ public final class PrintAttributes implements Parcelable { attributes.setMinMargins(getMinMargins()); attributes.setColorMode(getColorMode()); + attributes.setDuplexMode(getDuplexMode()); return attributes; } @@ -248,6 +290,7 @@ public final class PrintAttributes implements Parcelable { attributes.setMinMargins(getMinMargins()); attributes.setColorMode(getColorMode()); + attributes.setDuplexMode(getDuplexMode()); return attributes; } @@ -273,6 +316,7 @@ public final class PrintAttributes implements Parcelable { parcel.writeInt(0); } parcel.writeInt(mColorMode); + parcel.writeInt(mDuplexMode); } @Override @@ -285,6 +329,7 @@ public final class PrintAttributes implements Parcelable { final int prime = 31; int result = 1; result = prime * result + mColorMode; + result = prime * result + mDuplexMode; result = prime * result + ((mMinMargins == null) ? 0 : mMinMargins.hashCode()); result = prime * result + ((mMediaSize == null) ? 0 : mMediaSize.hashCode()); result = prime * result + ((mResolution == null) ? 0 : mResolution.hashCode()); @@ -306,6 +351,9 @@ public final class PrintAttributes implements Parcelable { if (mColorMode != other.mColorMode) { return false; } + if (mDuplexMode != other.mDuplexMode) { + return false; + } if (mMinMargins == null) { if (other.mMinMargins != null) { return false; @@ -344,6 +392,7 @@ public final class PrintAttributes implements Parcelable { builder.append(", resolution: ").append(mResolution); builder.append(", minMargins: ").append(mMinMargins); builder.append(", colorMode: ").append(colorModeToString(mColorMode)); + builder.append(", duplexMode: ").append(duplexModeToString(mDuplexMode)); builder.append("}"); return builder.toString(); } @@ -354,6 +403,7 @@ public final class PrintAttributes implements Parcelable { mResolution = null; mMinMargins = null; mColorMode = 0; + mDuplexMode = DUPLEX_MODE_NONE; } /** @@ -364,6 +414,7 @@ public final class PrintAttributes implements Parcelable { mResolution = other.mResolution; mMinMargins = other.mMinMargins; mColorMode = other.mColorMode; + mDuplexMode = other.mDuplexMode; } /** @@ -1270,17 +1321,41 @@ public final class PrintAttributes implements Parcelable { case COLOR_MODE_COLOR: { return "COLOR_MODE_COLOR"; } - default: + default: { return "COLOR_MODE_UNKNOWN"; + } + } + } + + static String duplexModeToString(int duplexMode) { + switch (duplexMode) { + case DUPLEX_MODE_NONE: { + return "DUPLEX_MODE_NONE"; + } + case DUPLEX_MODE_LONG_EDGE: { + return "DUPLEX_MODE_LONG_EDGE"; + } + case DUPLEX_MODE_SHORT_EDGE: { + return "DUPLEX_MODE_SHORT_EDGE"; + } + default: { + return "DUPLEX_MODE_UNKNOWN"; + } } } static void enforceValidColorMode(int colorMode) { - if ((colorMode & VALID_COLOR_MODES) == 0 && Integer.bitCount(colorMode) == 1) { + if ((colorMode & VALID_COLOR_MODES) == 0 || Integer.bitCount(colorMode) != 1) { throw new IllegalArgumentException("invalid color mode: " + colorMode); } } + static void enforceValidDuplexMode(int duplexMode) { + if ((duplexMode & VALID_DUPLEX_MODES) == 0 || Integer.bitCount(duplexMode) != 1) { + throw new IllegalArgumentException("invalid duplex mode: " + duplexMode); + } + } + /** * Builder for creating {@link PrintAttributes}. */ @@ -1331,15 +1406,31 @@ public final class PrintAttributes implements Parcelable { * @see PrintAttributes#COLOR_MODE_COLOR */ public Builder setColorMode(int colorMode) { - if (Integer.bitCount(colorMode) > 1) { - throw new IllegalArgumentException("can specify at most one colorMode bit."); - } mAttributes.setColorMode(colorMode); return this; } /** + * Sets the duplex mode. + * + * @param duplexMode A valid duplex mode or zero. + * @return This builder. + * + * @see PrintAttributes#DUPLEX_MODE_NONE + * @see PrintAttributes#DUPLEX_MODE_LONG_EDGE + * @see PrintAttributes#DUPLEX_MODE_SHORT_EDGE + */ + public Builder setDuplexMode(int duplexMode) { + mAttributes.setDuplexMode(duplexMode); + return this; + } + + /** * Creates a new {@link PrintAttributes} instance. + * <p> + * If you do not specify a duplex mode, the default + * {@link #DUPLEX_MODE_NONE} will be used. + * </p> * * @return The new instance. */ diff --git a/core/java/android/print/PrinterCapabilitiesInfo.java b/core/java/android/print/PrinterCapabilitiesInfo.java index 806a89d..96f3185 100644 --- a/core/java/android/print/PrinterCapabilitiesInfo.java +++ b/core/java/android/print/PrinterCapabilitiesInfo.java @@ -47,7 +47,8 @@ public final class PrinterCapabilitiesInfo implements Parcelable { private static final int PROPERTY_MEDIA_SIZE = 0; private static final int PROPERTY_RESOLUTION = 1; private static final int PROPERTY_COLOR_MODE = 2; - private static final int PROPERTY_COUNT = 3; + private static final int PROPERTY_DUPLEX_MODE = 3; + private static final int PROPERTY_COUNT = 4; private static final Margins DEFAULT_MARGINS = new Margins(0, 0, 0, 0); @@ -56,6 +57,7 @@ public final class PrinterCapabilitiesInfo implements Parcelable { private List<Resolution> mResolutions; private int mColorModes; + private int mDuplexModes; private final int[] mDefaults = new int[PROPERTY_COUNT]; @@ -106,6 +108,7 @@ public final class PrinterCapabilitiesInfo implements Parcelable { } mColorModes = other.mColorModes; + mDuplexModes = other.mDuplexModes; final int defaultCount = other.mDefaults.length; for (int i = 0; i < defaultCount; i++) { @@ -154,6 +157,19 @@ public final class PrinterCapabilitiesInfo implements Parcelable { } /** + * Gets the bit mask of supported duplex modes. + * + * @return The bit mask of supported duplex modes. + * + * @see PrintAttributes#DUPLEX_MODE_NONE + * @see PrintAttributes#DUPLEX_MODE_LONG_EDGE + * @see PrintAttributes#DUPLEX_MODE_SHORT_EDGE + */ + public int getDuplexModes() { + return mDuplexModes; + } + + /** * Gets the default print attributes. * * @return The default attributes. @@ -178,6 +194,11 @@ public final class PrinterCapabilitiesInfo implements Parcelable { builder.setColorMode(colorMode); } + final int duplexMode = mDefaults[PROPERTY_DUPLEX_MODE]; + if (duplexMode > 0) { + builder.setDuplexMode(duplexMode); + } + return builder.build(); } @@ -187,6 +208,7 @@ public final class PrinterCapabilitiesInfo implements Parcelable { readResolutions(parcel); mColorModes = parcel.readInt(); + mDuplexModes = parcel.readInt(); readDefaults(parcel); } @@ -203,6 +225,7 @@ public final class PrinterCapabilitiesInfo implements Parcelable { writeResolutions(parcel); parcel.writeInt(mColorModes); + parcel.writeInt(mDuplexModes); writeDefaults(parcel); } @@ -215,6 +238,7 @@ public final class PrinterCapabilitiesInfo implements Parcelable { result = prime * result + ((mMediaSizes == null) ? 0 : mMediaSizes.hashCode()); result = prime * result + ((mResolutions == null) ? 0 : mResolutions.hashCode()); result = prime * result + mColorModes; + result = prime * result + mDuplexModes; result = prime * result + Arrays.hashCode(mDefaults); return result; } @@ -255,6 +279,9 @@ public final class PrinterCapabilitiesInfo implements Parcelable { if (mColorModes != other.mColorModes) { return false; } + if (mDuplexModes != other.mDuplexModes) { + return false; + } if (!Arrays.equals(mDefaults, other.mDefaults)) { return false; } @@ -269,6 +296,7 @@ public final class PrinterCapabilitiesInfo implements Parcelable { builder.append(", mediaSizes=").append(mMediaSizes); builder.append(", resolutions=").append(mResolutions); builder.append(", colorModes=").append(colorModesToString()); + builder.append(", duplexModes=").append(duplexModesToString()); builder.append("\"}"); return builder.toString(); } @@ -289,6 +317,22 @@ public final class PrinterCapabilitiesInfo implements Parcelable { return builder.toString(); } + private String duplexModesToString() { + StringBuilder builder = new StringBuilder(); + builder.append('['); + int duplexModes = mDuplexModes; + while (duplexModes != 0) { + final int duplexMode = 1 << Integer.numberOfTrailingZeros(duplexModes); + duplexModes &= ~duplexMode; + if (builder.length() > 1) { + builder.append(", "); + } + builder.append(PrintAttributes.duplexModeToString(duplexMode)); + } + builder.append(']'); + return builder.toString(); + } + private void writeMediaSizes(Parcel parcel) { if (mMediaSizes == null) { parcel.writeInt(0); @@ -495,19 +539,51 @@ public final class PrinterCapabilitiesInfo implements Parcelable { currentModes &= ~currentMode; PrintAttributes.enforceValidColorMode(currentMode); } - if ((colorModes & defaultColorMode) == 0) { - throw new IllegalArgumentException("Default color mode not in color modes."); - } - PrintAttributes.enforceValidColorMode(colorModes); + PrintAttributes.enforceValidColorMode(defaultColorMode); mPrototype.mColorModes = colorModes; mPrototype.mDefaults[PROPERTY_COLOR_MODE] = defaultColorMode; return this; } /** + * Sets the duplex modes. + * <p> + * <strong>Required:</strong> No + * </p> + * + * @param duplexModes The duplex mode bit mask. + * @param defaultDuplexMode The default duplex mode. + * @return This builder. + * + * @throws IllegalArgumentException If duplex modes contains an invalid + * mode bit or if the default duplex mode is invalid. + * + * @see PrintAttributes#DUPLEX_MODE_NONE + * @see PrintAttributes#DUPLEX_MODE_LONG_EDGE + * @see PrintAttributes#DUPLEX_MODE_SHORT_EDGE + */ + public Builder setDuplexModes(int duplexModes, int defaultDuplexMode) { + int currentModes = duplexModes; + while (currentModes > 0) { + final int currentMode = (1 << Integer.numberOfTrailingZeros(currentModes)); + currentModes &= ~currentMode; + PrintAttributes.enforceValidDuplexMode(currentMode); + } + PrintAttributes.enforceValidDuplexMode(defaultDuplexMode); + mPrototype.mDuplexModes = duplexModes; + mPrototype.mDefaults[PROPERTY_DUPLEX_MODE] = defaultDuplexMode; + return this; + } + + /** * Crates a new {@link PrinterCapabilitiesInfo} enforcing that all * required properties have been specified. See individual methods * in this class for reference about required attributes. + * <p> + * <strong>Note:</strong> If you do not add supported duplex modes, + * {@link android.print.PrintAttributes#DUPLEX_MODE_NONE} will set + * as the only supported mode and also as the default duplex mode. + * </p> * * @return A new {@link PrinterCapabilitiesInfo}. * @@ -532,6 +608,10 @@ public final class PrinterCapabilitiesInfo implements Parcelable { if (mPrototype.mDefaults[PROPERTY_COLOR_MODE] == DEFAULT_UNDEFINED) { throw new IllegalStateException("No default color mode specified."); } + if (mPrototype.mDuplexModes == 0) { + setDuplexModes(PrintAttributes.DUPLEX_MODE_NONE, + PrintAttributes.DUPLEX_MODE_NONE); + } if (mPrototype.mMinMargins == null) { throw new IllegalArgumentException("margins cannot be null"); } @@ -558,4 +638,3 @@ public final class PrinterCapabilitiesInfo implements Parcelable { } }; } - diff --git a/core/java/android/printservice/PrintService.java b/core/java/android/printservice/PrintService.java index ae95854..527c8ae 100644 --- a/core/java/android/printservice/PrintService.java +++ b/core/java/android/printservice/PrintService.java @@ -16,7 +16,6 @@ package android.printservice; -import android.R; import android.app.Service; import android.content.ComponentName; import android.content.Context; @@ -192,7 +191,7 @@ public abstract class PrintService extends Service { /** * If you declared an optional activity with advanced print options via the - * {@link R.attr#advancedPrintOptionsActivity advancedPrintOptionsActivity} + * {@link android.R.attr#advancedPrintOptionsActivity advancedPrintOptionsActivity} * attribute, this extra is used to pass in the currently constructed {@link * PrintJobInfo} to your activity allowing you to modify it. After you are * done, you must return the modified {@link PrintJobInfo} via the same extra. @@ -224,7 +223,7 @@ public abstract class PrintService extends Service { /** * If you declared an optional activity with advanced print options via the - * {@link R.attr#advancedPrintOptionsActivity advancedPrintOptionsActivity} + * {@link android.R.attr#advancedPrintOptionsActivity advancedPrintOptionsActivity} * attribute, this extra is used to pass in the currently selected printer's * {@link android.print.PrinterInfo} to your activity allowing you to inspect it. * diff --git a/core/java/android/provider/Browser.java b/core/java/android/provider/Browser.java index 3853003..69a05c4 100644 --- a/core/java/android/provider/Browser.java +++ b/core/java/android/provider/Browser.java @@ -25,7 +25,6 @@ import android.database.Cursor; import android.database.DatabaseUtils; import android.graphics.BitmapFactory; import android.net.Uri; -import android.os.Build; import android.provider.BrowserContract.Bookmarks; import android.provider.BrowserContract.Combined; import android.provider.BrowserContract.History; diff --git a/core/java/android/provider/CallLog.java b/core/java/android/provider/CallLog.java index f023df7..6517f35 100644 --- a/core/java/android/provider/CallLog.java +++ b/core/java/android/provider/CallLog.java @@ -33,9 +33,12 @@ 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.PhoneAccount; import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; import android.telephony.PhoneNumberUtils; import android.text.TextUtils; +import android.util.Log; import com.android.internal.telephony.CallerInfo; import com.android.internal.telephony.PhoneConstants; @@ -46,6 +49,8 @@ import java.util.List; * The CallLog provider contains information about placed and received calls. */ public class CallLog { + private static final String LOG_TAG = "CallLog"; + public static final String AUTHORITY = "call_log"; /** @@ -323,6 +328,14 @@ public class CallLog { public static final String CACHED_PHOTO_ID = "photo_id"; /** + * The cached photo URI of the picture associated with the phone number, if it exists. + * This value may not be current if the contact information associated with this number + * has changed. + * <P>Type: TEXT (URI)</P> + */ + public static final String CACHED_PHOTO_URI = "photo_uri"; + + /** * The cached phone number, formatted with formatting rules based on the country the * user was in when the call was made or received. * This value is not guaranteed to be present, and may not be current if the contact @@ -336,24 +349,44 @@ public class CallLog { // that was encoded into call log databases. /** - * The component name of the account in string form. + * The component name of the account used to place or receive the call; in string form. * <P>Type: TEXT</P> */ public static final String PHONE_ACCOUNT_COMPONENT_NAME = "subscription_component_name"; /** - * The identifier of a account that is unique to a specified component. + * The identifier for the account used to place or receive the call. * <P>Type: TEXT</P> */ public static final String PHONE_ACCOUNT_ID = "subscription_id"; /** - * The identifier of a account that is unique to a specified component. Equivalent value - * to {@link #PHONE_ACCOUNT_ID}. For ContactsProvider internal use only. + * The address associated with the account used to place or receive the call; in string + * form. For SIM-based calls, this is the user's own phone number. + * <P>Type: TEXT</P> + * + * @hide + */ + public static final String PHONE_ACCOUNT_ADDRESS = "phone_account_address"; + + /** + * Indicates that the entry will be hidden from all queries until the associated + * {@link android.telecom.PhoneAccount} is registered with the system. * <P>Type: INTEGER</P> * * @hide */ + public static final String PHONE_ACCOUNT_HIDDEN = "phone_account_hidden"; + + /** + * The subscription ID used to place this call. This is no longer used and has been + * replaced with PHONE_ACCOUNT_COMPONENT_NAME/PHONE_ACCOUNT_ID. + * For ContactsProvider internal use only. + * <P>Type: INTEGER</P> + * + * @Deprecated + * @hide + */ public static final String SUB_ID = "sub_id"; /** @@ -421,6 +454,29 @@ public class CallLog { long start, int duration, Long dataUsage, boolean addForAllUsers) { final ContentResolver resolver = context.getContentResolver(); int numberPresentation = PRESENTATION_ALLOWED; + boolean isHidden = false; + + TelecomManager tm = null; + try { + tm = TelecomManager.from(context); + } catch (UnsupportedOperationException e) {} + + String accountAddress = null; + if (tm != null && accountHandle != null) { + PhoneAccount account = tm.getPhoneAccount(accountHandle); + if (account != null) { + Uri address = account.getSubscriptionAddress(); + if (address != null) { + accountAddress = address.getSchemeSpecificPart(); + } + } else { + // We could not find the account through telecom. For call log entries that + // are added with a phone account which is not registered, we automatically + // mark them as hidden. They are unhidden once the account is registered. + Log.i(LOG_TAG, "Marking call log entry as hidden."); + isHidden = true; + } + } // Remap network specified number presentation types // PhoneConstants.PRESENTATION_xxx to calllog number presentation types @@ -463,6 +519,8 @@ public class CallLog { } values.put(PHONE_ACCOUNT_COMPONENT_NAME, accountComponentString); values.put(PHONE_ACCOUNT_ID, accountId); + values.put(PHONE_ACCOUNT_ADDRESS, accountAddress); + values.put(PHONE_ACCOUNT_HIDDEN, Integer.valueOf(isHidden ? 1 : 0)); values.put(NEW, Integer.valueOf(1)); if (callType == MISSED_TYPE) { diff --git a/core/java/android/provider/Contacts.java b/core/java/android/provider/Contacts.java index d4c5cfb..3aa526d 100644 --- a/core/java/android/provider/Contacts.java +++ b/core/java/android/provider/Contacts.java @@ -2077,12 +2077,12 @@ public class Contacts { /** * Intents related to the Contacts app UI. - * @deprecated see {@link android.provider.ContactsContract} + * @deprecated Do not use. This is not supported. */ @Deprecated public static final class UI { /** - * @deprecated see {@link android.provider.ContactsContract} + * @deprecated Do not use. This is not supported. */ @Deprecated public UI() { @@ -2090,76 +2090,77 @@ public class Contacts { /** * The action for the default contacts list tab. - * @deprecated see {@link android.provider.ContactsContract} + * @deprecated Do not use. This is not supported. */ @Deprecated - public static final String LIST_DEFAULT = ContactsContract.Intents.UI.LIST_DEFAULT; + public static final String LIST_DEFAULT + = "com.android.contacts.action.LIST_DEFAULT"; /** * The action for the contacts list tab. - * @deprecated see {@link android.provider.ContactsContract} + * @deprecated Do not use. This is not supported. */ @Deprecated public static final String LIST_GROUP_ACTION = - ContactsContract.Intents.UI.LIST_GROUP_ACTION; + "com.android.contacts.action.LIST_GROUP"; /** * When in LIST_GROUP_ACTION mode, this is the group to display. - * @deprecated see {@link android.provider.ContactsContract} + * @deprecated Do not use. This is not supported. */ @Deprecated public static final String GROUP_NAME_EXTRA_KEY = - ContactsContract.Intents.UI.GROUP_NAME_EXTRA_KEY; + "com.android.contacts.extra.GROUP"; /** * The action for the all contacts list tab. - * @deprecated see {@link android.provider.ContactsContract} + * @deprecated Do not use. This is not supported. */ @Deprecated public static final String LIST_ALL_CONTACTS_ACTION = - ContactsContract.Intents.UI.LIST_ALL_CONTACTS_ACTION; + "com.android.contacts.action.LIST_ALL_CONTACTS"; /** * The action for the contacts with phone numbers list tab. - * @deprecated see {@link android.provider.ContactsContract} + * @deprecated Do not use. This is not supported. */ @Deprecated public static final String LIST_CONTACTS_WITH_PHONES_ACTION = - ContactsContract.Intents.UI.LIST_CONTACTS_WITH_PHONES_ACTION; + "com.android.contacts.action.LIST_CONTACTS_WITH_PHONES"; /** * The action for the starred contacts list tab. - * @deprecated see {@link android.provider.ContactsContract} + * @deprecated Do not use. This is not supported. */ @Deprecated public static final String LIST_STARRED_ACTION = - ContactsContract.Intents.UI.LIST_STARRED_ACTION; + "com.android.contacts.action.LIST_STARRED"; /** * The action for the frequent contacts list tab. - * @deprecated see {@link android.provider.ContactsContract} + * @deprecated Do not use. This is not supported. */ @Deprecated public static final String LIST_FREQUENT_ACTION = - ContactsContract.Intents.UI.LIST_FREQUENT_ACTION; + "com.android.contacts.action.LIST_FREQUENT"; /** * The action for the "strequent" contacts list tab. It first lists the starred * contacts in alphabetical order and then the frequent contacts in descending * order of the number of times they have been contacted. - * @deprecated see {@link android.provider.ContactsContract} + * @deprecated Do not use. This is not supported. */ @Deprecated public static final String LIST_STREQUENT_ACTION = - ContactsContract.Intents.UI.LIST_STREQUENT_ACTION; + "com.android.contacts.action.LIST_STREQUENT"; /** * A key for to be used as an intent extra to set the activity * title to a custom String value. - * @deprecated see {@link android.provider.ContactsContract} + * @deprecated Do not use. This is not supported. */ @Deprecated public static final String TITLE_EXTRA_KEY = - ContactsContract.Intents.UI.TITLE_EXTRA_KEY; + "com.android.contacts.extra.TITLE_EXTRA"; /** * Activity Action: Display a filtered list of contacts @@ -2168,20 +2169,20 @@ public class Contacts { * filtering * <p> * Output: Nothing. - * @deprecated see {@link android.provider.ContactsContract} + * @deprecated Do not use. This is not supported. */ @Deprecated public static final String FILTER_CONTACTS_ACTION = - ContactsContract.Intents.UI.FILTER_CONTACTS_ACTION; + "com.android.contacts.action.FILTER_CONTACTS"; /** * Used as an int extra field in {@link #FILTER_CONTACTS_ACTION} * intents to supply the text on which to filter. - * @deprecated see {@link android.provider.ContactsContract} + * @deprecated Do not use. This is not supported. */ @Deprecated public static final String FILTER_TEXT_EXTRA_KEY = - ContactsContract.Intents.UI.FILTER_TEXT_EXTRA_KEY; + "com.android.contacts.extra.FILTER_TEXT"; } /** diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java index 18a9eb1..06862d7 100644 --- a/core/java/android/provider/ContactsContract.java +++ b/core/java/android/provider/ContactsContract.java @@ -197,12 +197,15 @@ public final class ContactsContract { * API for obtaining a pre-authorized version of a URI that normally requires special * permission (beyond READ_CONTACTS) to read. The caller obtaining the pre-authorized URI * must already have the necessary permissions to access the URI; otherwise a - * {@link SecurityException} will be thrown. + * {@link SecurityException} will be thrown. Unlike {@link Context#grantUriPermission}, + * this can be used to grant permissions that aren't explicitly required for the URI inside + * AndroidManifest.xml. For example, permissions that are only required when reading URIs + * that refer to the user's profile. * </p> * <p> * The authorized URI returned in the bundle contains an expiring token that allows the * caller to execute the query without having the special permissions that would normally - * be required. + * be required. The token expires in five minutes. * </p> * <p> * This API does not access disk, and should be safe to invoke from the UI thread. @@ -226,7 +229,6 @@ public final class ContactsContract { * } * </pre> * </p> - * @hide */ public static final class Authorization { /** @@ -1008,7 +1010,8 @@ public final class ContactsContract { /** * Types of data used to produce the display name for a contact. In the order * of increasing priority: {@link #EMAIL}, {@link #PHONE}, - * {@link #ORGANIZATION}, {@link #NICKNAME}, {@link #STRUCTURED_NAME}. + * {@link #ORGANIZATION}, {@link #NICKNAME}, {@link #STRUCTURED_PHONETIC_NAME}, + * {@link #STRUCTURED_NAME}. */ public interface DisplayNameSources { public static final int UNDEFINED = 0; @@ -1016,6 +1019,8 @@ public final class ContactsContract { public static final int PHONE = 20; public static final int ORGANIZATION = 30; public static final int NICKNAME = 35; + /** Display name comes from a structured name that only has phonetic components. */ + public static final int STRUCTURED_PHONETIC_NAME = 37; public static final int STRUCTURED_NAME = 40; } @@ -1432,9 +1437,9 @@ public final class ContactsContract { * and {@link #CONTENT_MULTI_VCARD_URI} to indicate that the returned * vcard should not contain a photo. * - * @hide + * This is useful for obtaining a space efficient vcard. */ - public static final String QUERY_PARAMETER_VCARD_NO_PHOTO = "nophoto"; + public static final String QUERY_PARAMETER_VCARD_NO_PHOTO = "no_photo"; /** * Base {@link Uri} for referencing multiple {@link Contacts} entry, @@ -1790,52 +1795,26 @@ public final class ContactsContract { public static final String CONTENT_DIRECTORY = "suggestions"; /** - * Used with {@link Builder#addParameter} to specify what kind of data is - * supplied for the suggestion query. + * Used to specify what kind of data is supplied for the suggestion query. * * @hide */ public static final String PARAMETER_MATCH_NAME = "name"; /** - * Used with {@link Builder#addParameter} to specify what kind of data is - * supplied for the suggestion query. - * - * @hide - */ - public static final String PARAMETER_MATCH_EMAIL = "email"; - - /** - * Used with {@link Builder#addParameter} to specify what kind of data is - * supplied for the suggestion query. - * - * @hide - */ - public static final String PARAMETER_MATCH_PHONE = "phone"; - - /** - * Used with {@link Builder#addParameter} to specify what kind of data is - * supplied for the suggestion query. - * - * @hide - */ - public static final String PARAMETER_MATCH_NICKNAME = "nickname"; - - /** * A convenience builder for aggregation suggestion content URIs. - * - * TODO: change documentation for this class to use the builder. - * @hide */ public static final class Builder { private long mContactId; - private ArrayList<String> mKinds = new ArrayList<String>(); - private ArrayList<String> mValues = new ArrayList<String>(); + private final ArrayList<String> mValues = new ArrayList<String>(); private int mLimit; /** * Optional existing contact ID. If it is not provided, the search - * will be based exclusively on the values supplied with {@link #addParameter}. + * will be based exclusively on the values supplied with {@link #addNameParameter}. + * + * @param contactId contact to find aggregation suggestions for + * @return This Builder object to allow for chaining of calls to builder methods */ public Builder setContactId(long contactId) { this.mContactId = contactId; @@ -1843,28 +1822,31 @@ public final class ContactsContract { } /** - * A value that can be used when searching for an aggregation - * suggestion. + * Add a name to be used when searching for aggregation suggestions. * - * @param kind can be one of - * {@link AggregationSuggestions#PARAMETER_MATCH_NAME}, - * {@link AggregationSuggestions#PARAMETER_MATCH_EMAIL}, - * {@link AggregationSuggestions#PARAMETER_MATCH_NICKNAME}, - * {@link AggregationSuggestions#PARAMETER_MATCH_PHONE} + * @param name name to find aggregation suggestions for + * @return This Builder object to allow for chaining of calls to builder methods */ - public Builder addParameter(String kind, String value) { - if (!TextUtils.isEmpty(value)) { - mKinds.add(kind); - mValues.add(value); - } + public Builder addNameParameter(String name) { + mValues.add(name); return this; } + /** + * Sets the Maximum number of suggested aggregations that should be returned. + * @param limit The maximum number of suggested aggregations + * + * @return This Builder object to allow for chaining of calls to builder methods + */ public Builder setLimit(int limit) { mLimit = limit; return this; } + /** + * Combine all of the options that have been set and return a new {@link Uri} + * object for fetching aggregation suggestions. + */ public Uri build() { android.net.Uri.Builder builder = Contacts.CONTENT_URI.buildUpon(); builder.appendEncodedPath(String.valueOf(mContactId)); @@ -1873,9 +1855,10 @@ public final class ContactsContract { builder.appendQueryParameter("limit", String.valueOf(mLimit)); } - int count = mKinds.size(); + int count = mValues.size(); for (int i = 0; i < count; i++) { - builder.appendQueryParameter("query", mKinds.get(i) + ":" + mValues.get(i)); + builder.appendQueryParameter("query", PARAMETER_MATCH_NAME + + ":" + mValues.get(i)); } return builder.build(); @@ -2214,6 +2197,16 @@ public final class ContactsContract { public static final String CONTACT_ID = "contact_id"; /** + * Persistent unique id for each raw_contact within its account. + * This id is provided by its own data source, and can be used to backup metadata + * to the server. + * This should be unique within each set of account_name/account_type/data_set + * + * @hide + */ + public static final String BACKUP_ID = "backup_id"; + + /** * The data set within the account that this row belongs to. This allows * multiple sync adapters for the same account type to distinguish between * each others' data. @@ -2258,33 +2251,6 @@ public final class ContactsContract { public static final String DELETED = "deleted"; /** - * The "name_verified" flag: "1" means that the name fields on this raw - * contact can be trusted and therefore should be used for the entire - * aggregated contact. - * <p> - * If an aggregated contact contains more than one raw contact with a - * verified name, one of those verified names is chosen at random. - * If an aggregated contact contains no verified names, the - * name is chosen randomly from the constituent raw contacts. - * </p> - * <p> - * Updating this flag from "0" to "1" automatically resets it to "0" on - * all other raw contacts in the same aggregated contact. - * </p> - * <p> - * Sync adapters should only specify a value for this column when - * inserting a raw contact and leave it out when doing an update. - * </p> - * <p> - * The default value is "0" - * </p> - * <p>Type: INTEGER</p> - * - * @hide - */ - public static final String NAME_VERIFIED = "name_verified"; - - /** * The "read-only" flag: "0" by default, "1" if the row cannot be modified or * deleted except by a sync adapter. See {@link ContactsContract#CALLER_IS_SYNCADAPTER}. * <P>Type: INTEGER</P> @@ -2980,7 +2946,6 @@ public final class ContactsContract { DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, DELETED); DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, CONTACT_ID); DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, STARRED); - DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, NAME_VERIFIED); android.content.Entity contact = new android.content.Entity(cv); // read data rows until the contact id changes @@ -4006,6 +3971,13 @@ public final class ContactsContract { public static final String MIMETYPE = "mimetype"; /** + * Hash id on the data fields, used for backup and restore. + * + * @hide + */ + public static final String HASH_ID = "hash_id"; + + /** * A reference to the {@link RawContacts#_ID} * that this data belongs to. */ @@ -4628,6 +4600,15 @@ public final class ContactsContract { public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "data"); /** + * The content:// style URI for this table in managed profile, which requests a directory + * of data rows matching the selection criteria. + * + * @hide + */ + static final Uri ENTERPRISE_CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, + "data_enterprise"); + + /** * A boolean parameter for {@link Data#CONTENT_URI}. * This specifies whether or not the returned data items should be filtered to show * data items belonging to visible contacts only. @@ -5013,14 +4994,16 @@ public final class ContactsContract { "phone_lookup"); /** - * URI used for the "enterprise caller-id". + * <p>URI used for the "enterprise caller-id".</p> * + * <p> * It supports the same semantics as {@link #CONTENT_FILTER_URI} and returns the same * columns. If the device has no corp profile that is linked to the current profile, it * behaves in the exact same way as {@link #CONTENT_FILTER_URI}. If there is a corp profile * linked to the current profile, it first queries against the personal contact database, * and if no matching contacts are found there, then queries against the * corp contacts database. + * </p> * <p> * If a result is from the corp profile, it makes the following changes to the data: * <ul> @@ -5779,6 +5762,20 @@ public final class ContactsContract { "phones"); /** + * URI used for getting all contacts from primary and managed profile. + * + * It supports the same semantics as {@link #CONTENT_URI} and returns the same + * columns. If the device has no corp profile that is linked to the current profile, it + * behaves in the exact same way as {@link #CONTENT_URI}. If there is a corp profile + * linked to the current profile, it will merge corp profile and current profile's + * results and return + * + * @hide + */ + public static final Uri ENTERPRISE_CONTENT_URI = + Uri.withAppendedPath(Data.ENTERPRISE_CONTENT_URI, "phones"); + + /** * The content:// style URL for phone lookup using a filter. The filter returns * records of MIME type {@link #CONTENT_ITEM_TYPE}. The filter is applied * to display names as well as phone numbers. The filter argument should be passed @@ -5989,6 +5986,45 @@ public final class ContactsContract { "lookup"); /** + * <p>URI used for enterprise email lookup.</p> + * + * <p> + * It supports the same semantics as {@link #CONTENT_LOOKUP_URI} and returns the same + * columns. If the device has no corp profile that is linked to the current profile, it + * behaves in the exact same way as {@link #CONTENT_LOOKUP_URI}. If there is a + * corp profile linked to the current profile, it first queries against the personal contact database, + * and if no matching contacts are found there, then queries against the + * corp contacts database. + * </p> + * <p> + * If a result is from the corp profile, it makes the following changes to the data: + * <ul> + * <li> + * {@link #PHOTO_THUMBNAIL_URI} and {@link #PHOTO_URI} will be rewritten to special + * URIs. Use {@link ContentResolver#openAssetFileDescriptor} or its siblings to + * load pictures from them. + * {@link #PHOTO_ID} and {@link #PHOTO_FILE_ID} will be set to null. Do not + * use them. + * </li> + * <li> + * Corp contacts will get artificial {@link #CONTACT_ID}s. In order to tell whether + * a contact + * is from the corp profile, use + * {@link ContactsContract.Contacts#isEnterpriseContactId(long)}. + * </li> + * </ul> + * <p> + * This URI does NOT support selection nor order-by. + * + * <pre> + * Uri lookupUri = Uri.withAppendedPath(Email.ENTERPRISE_CONTENT_LOOKUP_URI, + * Uri.encode(email)); + * </pre> + */ + public static final Uri ENTERPRISE_CONTENT_LOOKUP_URI = + Uri.withAppendedPath(CONTENT_URI, "lookup_enterprise"); + + /** * <p> * The content:// style URL for email lookup using a filter. The filter returns * records of MIME type {@link #CONTENT_ITEM_TYPE}. The filter is applied @@ -7839,9 +7875,7 @@ public final class ContactsContract { } /** - * Private API for inquiring about the general status of the provider. - * - * @hide + * API for inquiring about the general status of the provider. */ public static final class ProviderStatus { @@ -7854,8 +7888,6 @@ public final class ContactsContract { /** * The content:// style URI for this table. Requests to this URI can be * performed on the UI thread because they are always unblocking. - * - * @hide */ public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "provider_status"); @@ -7863,64 +7895,35 @@ public final class ContactsContract { /** * The MIME-type of {@link #CONTENT_URI} providing a directory of * settings. - * - * @hide */ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/provider_status"; /** * An integer representing the current status of the provider. - * - * @hide */ public static final String STATUS = "status"; /** * Default status of the provider. - * - * @hide */ public static final int STATUS_NORMAL = 0; /** * The status used when the provider is in the process of upgrading. Contacts * are temporarily unaccessible. - * - * @hide */ public static final int STATUS_UPGRADING = 1; /** - * The status used if the provider was in the process of upgrading but ran - * out of storage. The DATA1 column will contain the estimated amount of - * storage required (in bytes). Update status to STATUS_NORMAL to force - * the provider to retry the upgrade. - * - * @hide - */ - public static final int STATUS_UPGRADE_OUT_OF_MEMORY = 2; - - /** * The status used during a locale change. - * - * @hide */ public static final int STATUS_CHANGING_LOCALE = 3; /** * The status that indicates that there are no accounts and no contacts * on the device. - * - * @hide */ public static final int STATUS_NO_ACCOUNTS_NO_CONTACTS = 4; - - /** - * Additional data associated with the status. - * - * @hide - */ - public static final String DATA1 = "data1"; } /** @@ -8125,12 +8128,22 @@ public final class ContactsContract { public static final String EXTRA_TARGET_RECT = "android.provider.extra.TARGET_RECT"; /** - * Extra used to specify size of pivot dialog. - * @hide + * Extra used to specify size of QuickContacts. Not all implementations of QuickContacts + * will respect this extra's value. + * + * One of {@link #MODE_SMALL}, {@link #MODE_MEDIUM}, or {@link #MODE_LARGE}. */ public static final String EXTRA_MODE = "android.provider.extra.MODE"; /** + * Extra used to specify which mimetype should be prioritized in the QuickContacts UI. + * For example, passing the value {@link CommonDataKinds.Phone#CONTENT_ITEM_TYPE} can + * cause phone numbers to be displayed more prominently in QuickContacts. + */ + public static final String EXTRA_PRIORITIZED_MIMETYPE + = "android.provider.extra.PRIORITIZED_MIMETYPE"; + + /** * Extra used to indicate a list of specific MIME-types to exclude and not display in the * QuickContacts dialog. Stored as a {@link String} array. */ @@ -8268,6 +8281,80 @@ public final class ContactsContract { startActivityWithErrorToast(context, intent); } + /** + * Trigger a dialog that lists the various methods of interacting with + * the requested {@link Contacts} entry. This may be based on available + * {@link ContactsContract.Data} rows under that contact, and may also + * include social status and presence details. + * + * @param context The parent {@link Context} that may be used as the + * parent for this dialog. + * @param target Specific {@link View} from your layout that this dialog + * should be centered around. In particular, if the dialog + * has a "callout" arrow, it will be pointed and centered + * around this {@link View}. + * @param lookupUri A + * {@link ContactsContract.Contacts#CONTENT_LOOKUP_URI} style + * {@link Uri} that describes a specific contact to feature + * in this dialog. + * @param excludeMimes Optional list of {@link Data#MIMETYPE} MIME-types + * to exclude when showing this dialog. For example, when + * already viewing the contact details card, this can be used + * to omit the details entry from the dialog. + * @param prioritizedMimeType This mimetype should be prioritized in the QuickContacts UI. + * For example, passing the value + * {@link CommonDataKinds.Phone#CONTENT_ITEM_TYPE} can cause phone numbers to be + * displayed more prominently in QuickContacts. + */ + public static void showQuickContact(Context context, View target, Uri lookupUri, + String[] excludeMimes, String prioritizedMimeType) { + // Use MODE_LARGE instead of accepting mode as a parameter. The different mode + // values defined in ContactsContract only affect very old implementations + // of QuickContacts. + Intent intent = composeQuickContactsIntent(context, target, lookupUri, MODE_LARGE, + excludeMimes); + intent.putExtra(EXTRA_PRIORITIZED_MIMETYPE, prioritizedMimeType); + startActivityWithErrorToast(context, intent); + } + + /** + * Trigger a dialog that lists the various methods of interacting with + * the requested {@link Contacts} entry. This may be based on available + * {@link ContactsContract.Data} rows under that contact, and may also + * include social status and presence details. + * + * @param context The parent {@link Context} that may be used as the + * parent for this dialog. + * @param target Specific {@link Rect} that this dialog should be + * centered around, in screen coordinates. In particular, if + * the dialog has a "callout" arrow, it will be pointed and + * centered around this {@link Rect}. If you are running at a + * non-native density, you need to manually adjust using + * {@link DisplayMetrics#density} before calling. + * @param lookupUri A + * {@link ContactsContract.Contacts#CONTENT_LOOKUP_URI} style + * {@link Uri} that describes a specific contact to feature + * in this dialog. + * @param excludeMimes Optional list of {@link Data#MIMETYPE} MIME-types + * to exclude when showing this dialog. For example, when + * already viewing the contact details card, this can be used + * to omit the details entry from the dialog. + * @param prioritizedMimeType This mimetype should be prioritized in the QuickContacts UI. + * For example, passing the value + * {@link CommonDataKinds.Phone#CONTENT_ITEM_TYPE} can cause phone numbers to be + * displayed more prominently in QuickContacts. + */ + public static void showQuickContact(Context context, Rect target, Uri lookupUri, + String[] excludeMimes, String prioritizedMimeType) { + // Use MODE_LARGE instead of accepting mode as a parameter. The different mode + // values defined in ContactsContract only affect very old implementations + // of QuickContacts. + Intent intent = composeQuickContactsIntent(context, target, lookupUri, MODE_LARGE, + excludeMimes); + intent.putExtra(EXTRA_PRIORITIZED_MIMETYPE, prioritizedMimeType); + startActivityWithErrorToast(context, intent); + } + private static void startActivityWithErrorToast(Context context, Intent intent) { try { context.startActivity(intent); @@ -8538,102 +8625,6 @@ public final class ContactsContract { public static final String EXTRA_EXCLUDE_MIMES = "exclude_mimes"; /** - * Intents related to the Contacts app UI. - * - * @hide - */ - public static final class UI { - /** - * The action for the default contacts list tab. - */ - public static final String LIST_DEFAULT = - "com.android.contacts.action.LIST_DEFAULT"; - - /** - * The action for the contacts list tab. - */ - public static final String LIST_GROUP_ACTION = - "com.android.contacts.action.LIST_GROUP"; - - /** - * When in LIST_GROUP_ACTION mode, this is the group to display. - */ - public static final String GROUP_NAME_EXTRA_KEY = "com.android.contacts.extra.GROUP"; - - /** - * The action for the all contacts list tab. - */ - public static final String LIST_ALL_CONTACTS_ACTION = - "com.android.contacts.action.LIST_ALL_CONTACTS"; - - /** - * The action for the contacts with phone numbers list tab. - */ - public static final String LIST_CONTACTS_WITH_PHONES_ACTION = - "com.android.contacts.action.LIST_CONTACTS_WITH_PHONES"; - - /** - * The action for the starred contacts list tab. - */ - public static final String LIST_STARRED_ACTION = - "com.android.contacts.action.LIST_STARRED"; - - /** - * The action for the frequent contacts list tab. - */ - public static final String LIST_FREQUENT_ACTION = - "com.android.contacts.action.LIST_FREQUENT"; - - /** - * The action for the "Join Contact" picker. - */ - public static final String PICK_JOIN_CONTACT_ACTION = - "com.android.contacts.action.JOIN_CONTACT"; - - /** - * The action for the "strequent" contacts list tab. It first lists the starred - * contacts in alphabetical order and then the frequent contacts in descending - * order of the number of times they have been contacted. - */ - public static final String LIST_STREQUENT_ACTION = - "com.android.contacts.action.LIST_STREQUENT"; - - /** - * A key for to be used as an intent extra to set the activity - * title to a custom String value. - */ - public static final String TITLE_EXTRA_KEY = - "com.android.contacts.extra.TITLE_EXTRA"; - - /** - * Activity Action: Display a filtered list of contacts - * <p> - * Input: Extra field {@link #FILTER_TEXT_EXTRA_KEY} is the text to use for - * filtering - * <p> - * Output: Nothing. - */ - public static final String FILTER_CONTACTS_ACTION = - "com.android.contacts.action.FILTER_CONTACTS"; - - /** - * Used as an int extra field in {@link #FILTER_CONTACTS_ACTION} - * intents to supply the text on which to filter. - */ - public static final String FILTER_TEXT_EXTRA_KEY = - "com.android.contacts.extra.FILTER_TEXT"; - - /** - * Used with JOIN_CONTACT action to set the target for aggregation. This action type - * uses contact ids instead of contact uris for the sake of backwards compatibility. - * <p> - * Type: LONG - */ - public static final String TARGET_CONTACT_ID_EXTRA_KEY - = "com.android.contacts.action.CONTACT_ID"; - } - - /** * Convenience class that contains string constants used * to create contact {@link android.content.Intent Intents}. */ @@ -8858,10 +8849,8 @@ public final class ContactsContract { * dialog to chose an account * <p> * Type: {@link Account} - * - * @hide */ - public static final String ACCOUNT = "com.android.contacts.extra.ACCOUNT"; + public static final String EXTRA_ACCOUNT = "android.provider.extra.ACCOUNT"; /** * Used to specify the data set within the account in which to create the @@ -8871,10 +8860,8 @@ public final class ContactsContract { * created in the base account, with no data set. * <p> * Type: String - * - * @hide */ - public static final String DATA_SET = "com.android.contacts.extra.DATA_SET"; + public static final String EXTRA_DATA_SET = "android.provider.extra.DATA_SET"; } } } diff --git a/core/java/android/provider/DocumentsProvider.java b/core/java/android/provider/DocumentsProvider.java index 1316471..6979bee 100644 --- a/core/java/android/provider/DocumentsProvider.java +++ b/core/java/android/provider/DocumentsProvider.java @@ -28,6 +28,7 @@ import static android.provider.DocumentsContract.getSearchDocumentsQuery; import static android.provider.DocumentsContract.getTreeDocumentId; import static android.provider.DocumentsContract.isTreeUri; +import android.annotation.CallSuper; import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentValues; @@ -541,6 +542,7 @@ public abstract class DocumentsProvider extends ContentProvider { * * @see DocumentsContract#buildDocumentUriUsingTree(Uri, String) */ + @CallSuper @Override public Uri canonicalize(Uri uri) { final Context context = getContext(); @@ -616,6 +618,7 @@ public abstract class DocumentsProvider extends ContentProvider { * call the superclass. If the superclass returns {@code null}, the subclass * may implement custom behavior. */ + @CallSuper @Override public Bundle call(String method, String arg, Bundle extras) { if (!method.startsWith("android:")) { diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index ef0f72f..3813277 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -51,14 +51,20 @@ import android.os.Build.VERSION_CODES; import android.speech.tts.TextToSpeech; import android.text.TextUtils; import android.util.AndroidException; +import android.util.ArrayMap; +import android.util.ArraySet; import android.util.Log; +import com.android.internal.util.ArrayUtils; import com.android.internal.widget.ILockSettings; import java.net.URISyntaxException; +import java.text.SimpleDateFormat; import java.util.HashMap; import java.util.HashSet; import java.util.Locale; +import java.util.Map; +import java.util.Set; /** * The Settings provider contains global system-level device preferences. @@ -132,7 +138,6 @@ public final class Settings { "android.settings.AIRPLANE_MODE_SETTINGS"; /** - * @hide * Activity Action: Modify Airplane mode settings using the users voice. * <p> * In some cases, a matching Activity may not exist, so ensure you safeguard against this. @@ -154,7 +159,6 @@ public final class Settings { * Output: Nothing. */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) - @SystemApi public static final String ACTION_VOICE_CONTROL_AIRPLANE_MODE = "android.settings.VOICE_CONTROL_AIRPLANE_MODE"; @@ -308,7 +312,7 @@ public final class Settings { /** * Activity Action: Show settings to allow configuration of - * cast endpoints. + * {@link android.media.routing.MediaRouteService media route providers}. * <p> * In some cases, a matching Activity may not exist, so ensure you * safeguard against this. @@ -991,13 +995,11 @@ public final class Settings { public static final String EXTRA_INPUT_DEVICE_IDENTIFIER = "input_device_identifier"; /** - * @hide * Activity Extra: Enable or disable Airplane Mode. * <p> * This can be passed as an extra field to the {@link #ACTION_VOICE_CONTROL_AIRPLANE_MODE} * intent as a boolean. */ - @SystemApi public static final String EXTRA_AIRPLANE_MODE_ENABLED = "airplane_mode_enabled"; private static final String JID_RESOURCE_PREFIX = "android"; @@ -1196,6 +1198,11 @@ public final class Settings { public static final class System extends NameValueTable { public static final String SYS_PROP_SETTING_VERSION = "sys.settings_system_version"; + /** @hide */ + public static interface Validator { + public boolean validate(String value); + } + /** * The content:// style URL for this table */ @@ -1298,13 +1305,53 @@ public final class Settings { MOVED_TO_GLOBAL.add(Settings.Global.CERT_PIN_UPDATE_METADATA_URL); } + private static final Validator sBooleanValidator = + new DiscreteValueValidator(new String[] {"0", "1"}); + + private static final Validator sNonNegativeIntegerValidator = new Validator() { + @Override + public boolean validate(String value) { + try { + return Integer.parseInt(value) >= 0; + } catch (NumberFormatException e) { + return false; + } + } + }; + + private static final Validator sUriValidator = new Validator() { + @Override + public boolean validate(String value) { + try { + Uri.decode(value); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + }; + + private static final Validator sLenientIpAddressValidator = new Validator() { + private static final int MAX_IPV6_LENGTH = 45; + + @Override + public boolean validate(String value) { + return value.length() <= MAX_IPV6_LENGTH; + } + }; + /** @hide */ - public static void getMovedKeys(HashSet<String> outKeySet) { + public static void getMovedToGlobalSettings(Set<String> outKeySet) { outKeySet.addAll(MOVED_TO_GLOBAL); outKeySet.addAll(MOVED_TO_SECURE_THEN_GLOBAL); } /** @hide */ + public static void getMovedToSecureSettings(Set<String> outKeySet) { + outKeySet.addAll(MOVED_TO_SECURE); + } + + /** @hide */ public static void getNonLegacyMovedKeys(HashSet<String> outKeySet) { outKeySet.addAll(MOVED_TO_GLOBAL); } @@ -1727,6 +1774,59 @@ public final class Settings { putIntForUser(cr, SHOW_GTALK_SERVICE_STATUS, flag ? 1 : 0, userHandle); } + private static final class DiscreteValueValidator implements Validator { + private final String[] mValues; + + public DiscreteValueValidator(String[] values) { + mValues = values; + } + + @Override + public boolean validate(String value) { + return ArrayUtils.contains(mValues, value); + } + } + + private static final class InclusiveIntegerRangeValidator implements Validator { + private final int mMin; + private final int mMax; + + public InclusiveIntegerRangeValidator(int min, int max) { + mMin = min; + mMax = max; + } + + @Override + public boolean validate(String value) { + try { + final int intValue = Integer.parseInt(value); + return intValue >= mMin && intValue <= mMax; + } catch (NumberFormatException e) { + return false; + } + } + } + + private static final class InclusiveFloatRangeValidator implements Validator { + private final float mMin; + private final float mMax; + + public InclusiveFloatRangeValidator(float min, float max) { + mMin = min; + mMax = max; + } + + @Override + public boolean validate(String value) { + try { + final float floatValue = Float.parseFloat(value); + return floatValue >= mMin && floatValue <= mMax; + } catch (NumberFormatException e) { + return false; + } + } + } + /** * @deprecated Use {@link android.provider.Settings.Global#STAY_ON_WHILE_PLUGGED_IN} instead */ @@ -1745,6 +1845,9 @@ public final class Settings { */ public static final String END_BUTTON_BEHAVIOR = "end_button_behavior"; + private static final Validator END_BUTTON_BEHAVIOR_VALIDATOR = + new InclusiveIntegerRangeValidator(0, 3); + /** * END_BUTTON_BEHAVIOR value for "go home". * @hide @@ -1769,6 +1872,8 @@ public final class Settings { */ public static final String ADVANCED_SETTINGS = "advanced_settings"; + private static final Validator ADVANCED_SETTINGS_VALIDATOR = sBooleanValidator; + /** * ADVANCED_SETTINGS default value. * @hide @@ -1868,6 +1973,8 @@ public final class Settings { @Deprecated public static final String WIFI_USE_STATIC_IP = "wifi_use_static_ip"; + private static final Validator WIFI_USE_STATIC_IP_VALIDATOR = sBooleanValidator; + /** * The static IP address. * <p> @@ -1878,6 +1985,8 @@ public final class Settings { @Deprecated public static final String WIFI_STATIC_IP = "wifi_static_ip"; + private static final Validator WIFI_STATIC_IP_VALIDATOR = sLenientIpAddressValidator; + /** * If using static IP, the gateway's IP address. * <p> @@ -1888,6 +1997,8 @@ public final class Settings { @Deprecated public static final String WIFI_STATIC_GATEWAY = "wifi_static_gateway"; + private static final Validator WIFI_STATIC_GATEWAY_VALIDATOR = sLenientIpAddressValidator; + /** * If using static IP, the net mask. * <p> @@ -1898,6 +2009,8 @@ public final class Settings { @Deprecated public static final String WIFI_STATIC_NETMASK = "wifi_static_netmask"; + private static final Validator WIFI_STATIC_NETMASK_VALIDATOR = sLenientIpAddressValidator; + /** * If using static IP, the primary DNS's IP address. * <p> @@ -1908,6 +2021,8 @@ public final class Settings { @Deprecated public static final String WIFI_STATIC_DNS1 = "wifi_static_dns1"; + private static final Validator WIFI_STATIC_DNS1_VALIDATOR = sLenientIpAddressValidator; + /** * If using static IP, the secondary DNS's IP address. * <p> @@ -1918,6 +2033,7 @@ public final class Settings { @Deprecated public static final String WIFI_STATIC_DNS2 = "wifi_static_dns2"; + private static final Validator WIFI_STATIC_DNS2_VALIDATOR = sLenientIpAddressValidator; /** * Determines whether remote devices may discover and/or connect to @@ -1930,6 +2046,9 @@ public final class Settings { public static final String BLUETOOTH_DISCOVERABILITY = "bluetooth_discoverability"; + private static final Validator BLUETOOTH_DISCOVERABILITY_VALIDATOR = + new InclusiveIntegerRangeValidator(0, 2); + /** * Bluetooth discoverability timeout. If this value is nonzero, then * Bluetooth becomes discoverable for a certain number of seconds, @@ -1938,6 +2057,9 @@ public final class Settings { public static final String BLUETOOTH_DISCOVERABILITY_TIMEOUT = "bluetooth_discoverability_timeout"; + private static final Validator BLUETOOTH_DISCOVERABILITY_TIMEOUT_VALIDATOR = + sNonNegativeIntegerValidator; + /** * @deprecated Use {@link android.provider.Settings.Secure#LOCK_PATTERN_ENABLED} * instead @@ -1961,7 +2083,6 @@ public final class Settings { public static final String LOCK_PATTERN_TACTILE_FEEDBACK_ENABLED = "lock_pattern_tactile_feedback_enabled"; - /** * A formatted string of the next alarm that is set, or the empty string * if there is no alarm set. @@ -1971,11 +2092,32 @@ public final class Settings { @Deprecated public static final String NEXT_ALARM_FORMATTED = "next_alarm_formatted"; + private static final Validator NEXT_ALARM_FORMATTED_VALIDATOR = new Validator() { + private static final int MAX_LENGTH = 1000; + + @Override + public boolean validate(String value) { + // TODO: No idea what the correct format is. + return value == null || value.length() < MAX_LENGTH; + } + }; + /** * Scaling factor for fonts, float. */ public static final String FONT_SCALE = "font_scale"; + private static final Validator FONT_SCALE_VALIDATOR = new Validator() { + @Override + public boolean validate(String value) { + try { + return Float.parseFloat(value) >= 0; + } catch (NumberFormatException e) { + return false; + } + } + }; + /** * Name of an application package to be debugged. * @@ -2000,6 +2142,8 @@ public final class Settings { @Deprecated public static final String DIM_SCREEN = "dim_screen"; + private static final Validator DIM_SCREEN_VALIDATOR = sBooleanValidator; + /** * The amount of time in milliseconds before the device goes to sleep or begins * to dream after a period of inactivity. This value is also known as the @@ -2008,16 +2152,23 @@ public final class Settings { */ public static final String SCREEN_OFF_TIMEOUT = "screen_off_timeout"; + private static final Validator SCREEN_OFF_TIMEOUT_VALIDATOR = sNonNegativeIntegerValidator; + /** * The screen backlight brightness between 0 and 255. */ public static final String SCREEN_BRIGHTNESS = "screen_brightness"; + private static final Validator SCREEN_BRIGHTNESS_VALIDATOR = + new InclusiveIntegerRangeValidator(0, 255); + /** * Control whether to enable automatic brightness mode. */ public static final String SCREEN_BRIGHTNESS_MODE = "screen_brightness_mode"; + private static final Validator SCREEN_BRIGHTNESS_MODE_VALIDATOR = sBooleanValidator; + /** * Adjustment to auto-brightness to make it generally more (>0.0 <1.0) * or less (<0.0 >-1.0) bright. @@ -2025,6 +2176,9 @@ public final class Settings { */ public static final String SCREEN_AUTO_BRIGHTNESS_ADJ = "screen_auto_brightness_adj"; + private static final Validator SCREEN_AUTO_BRIGHTNESS_ADJ_VALIDATOR = + new InclusiveFloatRangeValidator(-1, 1); + /** * SCREEN_BRIGHTNESS_MODE value for manual mode. */ @@ -2060,12 +2214,18 @@ public final class Settings { */ public static final String MODE_RINGER_STREAMS_AFFECTED = "mode_ringer_streams_affected"; - /** + private static final Validator MODE_RINGER_STREAMS_AFFECTED_VALIDATOR = + sNonNegativeIntegerValidator; + + /** * Determines which streams are affected by mute. The * stream type's bit should be set to 1 if it should be muted when a mute request * is received. */ - public static final String MUTE_STREAMS_AFFECTED = "mute_streams_affected"; + public static final String MUTE_STREAMS_AFFECTED = "mute_streams_affected"; + + private static final Validator MUTE_STREAMS_AFFECTED_VALIDATOR = + sNonNegativeIntegerValidator; /** * Whether vibrate is on for different events. This is used internally, @@ -2073,6 +2233,8 @@ public final class Settings { */ public static final String VIBRATE_ON = "vibrate_on"; + private static final Validator VIBRATE_ON_VALIDATOR = sBooleanValidator; + /** * If 1, redirects the system vibrator to all currently attached input devices * that support vibration. If there are no such input devices, then the system @@ -2087,50 +2249,67 @@ public final class Settings { */ public static final String VIBRATE_INPUT_DEVICES = "vibrate_input_devices"; + private static final Validator VIBRATE_INPUT_DEVICES_VALIDATOR = sBooleanValidator; + /** * Ringer volume. This is used internally, changing this value will not * change the volume. See AudioManager. + * + * @removed Not used by anything since API 2. */ public static final String VOLUME_RING = "volume_ring"; /** * System/notifications volume. This is used internally, changing this * value will not change the volume. See AudioManager. + * + * @removed Not used by anything since API 2. */ public static final String VOLUME_SYSTEM = "volume_system"; /** * Voice call volume. This is used internally, changing this value will * not change the volume. See AudioManager. + * + * @removed Not used by anything since API 2. */ public static final String VOLUME_VOICE = "volume_voice"; /** * Music/media/gaming volume. This is used internally, changing this * value will not change the volume. See AudioManager. + * + * @removed Not used by anything since API 2. */ public static final String VOLUME_MUSIC = "volume_music"; /** * Alarm volume. This is used internally, changing this * value will not change the volume. See AudioManager. + * + * @removed Not used by anything since API 2. */ public static final String VOLUME_ALARM = "volume_alarm"; /** * Notification volume. This is used internally, changing this * value will not change the volume. See AudioManager. + * + * @removed Not used by anything since API 2. */ public static final String VOLUME_NOTIFICATION = "volume_notification"; /** * Bluetooth Headset volume. This is used internally, changing this value will * not change the volume. See AudioManager. + * + * @removed Not used by anything since API 2. */ public static final String VOLUME_BLUETOOTH_SCO = "volume_bluetooth_sco"; /** * Master volume (float in the range 0.0f to 1.0f). + * * @hide */ public static final String VOLUME_MASTER = "volume_master"; @@ -2142,6 +2321,8 @@ public final class Settings { */ public static final String VOLUME_MASTER_MUTE = "volume_master_mute"; + private static final Validator VOLUME_MASTER_MUTE_VALIDATOR = sBooleanValidator; + /** * Microphone mute (int 1 = mute, 0 = not muted). * @@ -2149,6 +2330,8 @@ public final class Settings { */ public static final String MICROPHONE_MUTE = "microphone_mute"; + private static final Validator MICROPHONE_MUTE_VALIDATOR = sBooleanValidator; + /** * Whether the notifications should use the ring volume (value of 1) or * a separate notification volume (value of 0). In most cases, users @@ -2167,6 +2350,8 @@ public final class Settings { public static final String NOTIFICATIONS_USE_RING_VOLUME = "notifications_use_ring_volume"; + private static final Validator NOTIFICATIONS_USE_RING_VOLUME_VALIDATOR = sBooleanValidator; + /** * Whether silent mode should allow vibration feedback. This is used * internally in AudioService and the Sound settings activity to @@ -2181,8 +2366,12 @@ public final class Settings { */ public static final String VIBRATE_IN_SILENT = "vibrate_in_silent"; + private static final Validator VIBRATE_IN_SILENT_VALIDATOR = sBooleanValidator; + /** * The mapping of stream type (integer) to its setting. + * + * @removed Not used by anything since API 2. */ public static final String[] VOLUME_SETTINGS = { VOLUME_VOICE, VOLUME_SYSTEM, VOLUME_RING, VOLUME_MUSIC, @@ -2193,6 +2382,8 @@ public final class Settings { * Appended to various volume related settings to record the previous * values before they the settings were affected by a silent/vibrate * ringer mode change. + * + * @removed Not used by anything since API 2. */ public static final String APPEND_FOR_LAST_AUDIBLE = "_last_audible"; @@ -2207,6 +2398,8 @@ public final class Settings { */ public static final String RINGTONE = "ringtone"; + private static final Validator RINGTONE_VALIDATOR = sUriValidator; + /** * A {@link Uri} that will point to the current default ringtone at any * given time. @@ -2225,6 +2418,8 @@ public final class Settings { */ public static final String NOTIFICATION_SOUND = "notification_sound"; + private static final Validator NOTIFICATION_SOUND_VALIDATOR = sUriValidator; + /** * A {@link Uri} that will point to the current default notification * sound at any given time. @@ -2241,6 +2436,8 @@ public final class Settings { */ public static final String ALARM_ALERT = "alarm_alert"; + private static final Validator ALARM_ALERT_VALIDATOR = sUriValidator; + /** * A {@link Uri} that will point to the current default alarm alert at * any given time. @@ -2256,30 +2453,52 @@ public final class Settings { */ public static final String MEDIA_BUTTON_RECEIVER = "media_button_receiver"; + private static final Validator MEDIA_BUTTON_RECEIVER_VALIDATOR = new Validator() { + @Override + public boolean validate(String value) { + try { + ComponentName.unflattenFromString(value); + return true; + } catch (NullPointerException e) { + return false; + } + } + }; + /** * Setting to enable Auto Replace (AutoText) in text editors. 1 = On, 0 = Off */ public static final String TEXT_AUTO_REPLACE = "auto_replace"; + private static final Validator TEXT_AUTO_REPLACE_VALIDATOR = sBooleanValidator; + /** * Setting to enable Auto Caps in text editors. 1 = On, 0 = Off */ public static final String TEXT_AUTO_CAPS = "auto_caps"; + private static final Validator TEXT_AUTO_CAPS_VALIDATOR = sBooleanValidator; + /** * Setting to enable Auto Punctuate in text editors. 1 = On, 0 = Off. This * feature converts two spaces to a "." and space. */ public static final String TEXT_AUTO_PUNCTUATE = "auto_punctuate"; + private static final Validator TEXT_AUTO_PUNCTUATE_VALIDATOR = sBooleanValidator; + /** * Setting to showing password characters in text editors. 1 = On, 0 = Off */ public static final String TEXT_SHOW_PASSWORD = "show_password"; + private static final Validator TEXT_SHOW_PASSWORD_VALIDATOR = sBooleanValidator; + public static final String SHOW_GTALK_SERVICE_STATUS = "SHOW_GTALK_SERVICE_STATUS"; + private static final Validator SHOW_GTALK_SERVICE_STATUS_VALIDATOR = sBooleanValidator; + /** * Name of activity to use for wallpaper on the home screen. * @@ -2288,6 +2507,18 @@ public final class Settings { @Deprecated public static final String WALLPAPER_ACTIVITY = "wallpaper_activity"; + private static final Validator WALLPAPER_ACTIVITY_VALIDATOR = new Validator() { + private static final int MAX_LENGTH = 1000; + + @Override + public boolean validate(String value) { + if (value != null && value.length() > MAX_LENGTH) { + return false; + } + return ComponentName.unflattenFromString(value) != null; + } + }; + /** * @deprecated Use {@link android.provider.Settings.Global#AUTO_TIME} * instead @@ -2309,6 +2540,10 @@ public final class Settings { */ public static final String TIME_12_24 = "time_12_24"; + /** @hide */ + public static final Validator TIME_12_24_VALIDATOR = + new DiscreteValueValidator(new String[] {"12", "24"}); + /** * Date format string * mm/dd/yyyy @@ -2317,6 +2552,19 @@ public final class Settings { */ public static final String DATE_FORMAT = "date_format"; + /** @hide */ + public static final Validator DATE_FORMAT_VALIDATOR = new Validator() { + @Override + public boolean validate(String value) { + try { + new SimpleDateFormat(value); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + }; + /** * Whether the setup wizard has been run before (on first boot), or if * it still needs to be run. @@ -2326,6 +2574,9 @@ public final class Settings { */ public static final String SETUP_WIZARD_HAS_RUN = "setup_wizard_has_run"; + /** @hide */ + public static final Validator SETUP_WIZARD_HAS_RUN_VALIDATOR = sBooleanValidator; + /** * Scaling factor for normal window animations. Setting to 0 will disable window * animations. @@ -2362,6 +2613,9 @@ public final class Settings { */ public static final String ACCELEROMETER_ROTATION = "accelerometer_rotation"; + /** @hide */ + public static final Validator ACCELEROMETER_ROTATION_VALIDATOR = sBooleanValidator; + /** * Default screen rotation when no other policy applies. * When {@link #ACCELEROMETER_ROTATION} is zero and no on-screen Activity expresses a @@ -2372,6 +2626,10 @@ public final class Settings { */ public static final String USER_ROTATION = "user_rotation"; + /** @hide */ + public static final Validator USER_ROTATION_VALIDATOR = + new InclusiveIntegerRangeValidator(0, 3); + /** * Control whether the rotation lock toggle in the System UI should be hidden. * Typically this is done for accessibility purposes to make it harder for @@ -2386,6 +2644,10 @@ public final class Settings { public static final String HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY = "hide_rotation_lock_toggle_for_accessibility"; + /** @hide */ + public static final Validator HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY_VALIDATOR = + sBooleanValidator; + /** * Whether the phone vibrates when it is ringing due to an incoming call. This will * be used by Phone and Setting apps; it shouldn't affect other apps. @@ -2400,12 +2662,18 @@ public final class Settings { */ public static final String VIBRATE_WHEN_RINGING = "vibrate_when_ringing"; + /** @hide */ + public static final Validator VIBRATE_WHEN_RINGING_VALIDATOR = sBooleanValidator; + /** * Whether the audible DTMF tones are played by the dialer when dialing. The value is * boolean (1 or 0). */ public static final String DTMF_TONE_WHEN_DIALING = "dtmf_tone"; + /** @hide */ + public static final Validator DTMF_TONE_WHEN_DIALING_VALIDATOR = sBooleanValidator; + /** * CDMA only settings * DTMF tone type played by the dialer when dialing. @@ -2415,6 +2683,9 @@ public final class Settings { */ public static final String DTMF_TONE_TYPE_WHEN_DIALING = "dtmf_tone_type"; + /** @hide */ + public static final Validator DTMF_TONE_TYPE_WHEN_DIALING_VALIDATOR = sBooleanValidator; + /** * Whether the hearing aid is enabled. The value is * boolean (1 or 0). @@ -2422,6 +2693,9 @@ public final class Settings { */ public static final String HEARING_AID = "hearing_aid"; + /** @hide */ + public static final Validator HEARING_AID_VALIDATOR = sBooleanValidator; + /** * CDMA only settings * TTY Mode @@ -2433,18 +2707,27 @@ public final class Settings { */ public static final String TTY_MODE = "tty_mode"; + /** @hide */ + public static final Validator TTY_MODE_VALIDATOR = new InclusiveIntegerRangeValidator(0, 3); + /** * Whether the sounds effects (key clicks, lid open ...) are enabled. The value is * boolean (1 or 0). */ public static final String SOUND_EFFECTS_ENABLED = "sound_effects_enabled"; + /** @hide */ + public static final Validator SOUND_EFFECTS_ENABLED_VALIDATOR = sBooleanValidator; + /** * Whether the haptic feedback (long presses, ...) are enabled. The value is * boolean (1 or 0). */ public static final String HAPTIC_FEEDBACK_ENABLED = "haptic_feedback_enabled"; + /** @hide */ + public static final Validator HAPTIC_FEEDBACK_ENABLED_VALIDATOR = sBooleanValidator; + /** * @deprecated Each application that shows web suggestions should have its own * setting for this. @@ -2452,6 +2735,9 @@ public final class Settings { @Deprecated public static final String SHOW_WEB_SUGGESTIONS = "show_web_suggestions"; + /** @hide */ + public static final Validator SHOW_WEB_SUGGESTIONS_VALIDATOR = sBooleanValidator; + /** * Whether the notification LED should repeatedly flash when a notification is * pending. The value is boolean (1 or 0). @@ -2459,6 +2745,9 @@ public final class Settings { */ public static final String NOTIFICATION_LIGHT_PULSE = "notification_light_pulse"; + /** @hide */ + public static final Validator NOTIFICATION_LIGHT_PULSE_VALIDATOR = sBooleanValidator; + /** * Show pointer location on screen? * 0 = no @@ -2467,6 +2756,9 @@ public final class Settings { */ public static final String POINTER_LOCATION = "pointer_location"; + /** @hide */ + public static final Validator POINTER_LOCATION_VALIDATOR = sBooleanValidator; + /** * Show touch positions on screen? * 0 = no @@ -2475,9 +2767,12 @@ public final class Settings { */ public static final String SHOW_TOUCHES = "show_touches"; + /** @hide */ + public static final Validator SHOW_TOUCHES_VALIDATOR = sBooleanValidator; + /** * Log raw orientation data from - * {@link com.android.internal.policy.impl.WindowOrientationListener} for use with the + * {@link com.android.server.policy.WindowOrientationListener} for use with the * orientationplot.py tool. * 0 = no * 1 = yes @@ -2486,6 +2781,9 @@ public final class Settings { public static final String WINDOW_ORIENTATION_LISTENER_LOG = "window_orientation_listener_log"; + /** @hide */ + public static final Validator WINDOW_ORIENTATION_LISTENER_LOG_VALIDATOR = sBooleanValidator; + /** * @deprecated Use {@link android.provider.Settings.Global#POWER_SOUNDS_ENABLED} * instead @@ -2508,12 +2806,18 @@ public final class Settings { */ public static final String LOCKSCREEN_SOUNDS_ENABLED = "lockscreen_sounds_enabled"; + /** @hide */ + public static final Validator LOCKSCREEN_SOUNDS_ENABLED_VALIDATOR = sBooleanValidator; + /** * Whether the lockscreen should be completely disabled. * @hide */ public static final String LOCKSCREEN_DISABLED = "lockscreen.disabled"; + /** @hide */ + public static final Validator LOCKSCREEN_DISABLED_VALIDATOR = sBooleanValidator; + /** * @deprecated Use {@link android.provider.Settings.Global#LOW_BATTERY_SOUND} * instead @@ -2578,6 +2882,9 @@ public final class Settings { */ public static final String SIP_RECEIVE_CALLS = "sip_receive_calls"; + /** @hide */ + public static final Validator SIP_RECEIVE_CALLS_VALIDATOR = sBooleanValidator; + /** * Call Preference String. * "SIP_ALWAYS" : Always use SIP with network access @@ -2586,18 +2893,28 @@ public final class Settings { */ public static final String SIP_CALL_OPTIONS = "sip_call_options"; + /** @hide */ + public static final Validator SIP_CALL_OPTIONS_VALIDATOR = new DiscreteValueValidator( + new String[] {"SIP_ALWAYS", "SIP_ADDRESS_ONLY"}); + /** * One of the sip call options: Always use SIP with network access. * @hide */ public static final String SIP_ALWAYS = "SIP_ALWAYS"; + /** @hide */ + public static final Validator SIP_ALWAYS_VALIDATOR = sBooleanValidator; + /** * One of the sip call options: Only if destination is a SIP address. * @hide */ public static final String SIP_ADDRESS_ONLY = "SIP_ADDRESS_ONLY"; + /** @hide */ + public static final Validator SIP_ADDRESS_ONLY_VALIDATOR = sBooleanValidator; + /** * @deprecated Use SIP_ALWAYS or SIP_ADDRESS_ONLY instead. Formerly used to indicate that * the user should be prompted each time a call is made whether it should be placed using @@ -2608,6 +2925,9 @@ public final class Settings { @Deprecated public static final String SIP_ASK_ME_EACH_TIME = "SIP_ASK_ME_EACH_TIME"; + /** @hide */ + public static final Validator SIP_ASK_ME_EACH_TIME_VALIDATOR = sBooleanValidator; + /** * Pointer speed setting. * This is an integer value in a range between -7 and +7, so there are 15 possible values. @@ -2618,12 +2938,19 @@ public final class Settings { */ public static final String POINTER_SPEED = "pointer_speed"; + /** @hide */ + public static final Validator POINTER_SPEED_VALIDATOR = + new InclusiveFloatRangeValidator(-7, 7); + /** * Whether lock-to-app will be triggered by long-press on recents. * @hide */ public static final String LOCK_TO_APP_ENABLED = "lock_to_app_enabled"; + /** @hide */ + public static final Validator LOCK_TO_APP_ENABLED_VALIDATOR = sBooleanValidator; + /** * I am the lolrus. * <p> @@ -2633,6 +2960,16 @@ public final class Settings { */ public static final String EGG_MODE = "egg_mode"; + /** @hide */ + public static final Validator EGG_MODE_VALIDATOR = sBooleanValidator; + + /** + * IMPORTANT: If you add a new public settings you also have to add it to + * PUBLIC_SETTINGS below. If the new setting is hidden you have to add + * it to PRIVATE_SETTINGS below. Also add a validator that can validate + * the setting value. See an example above. + */ + /** * Settings to backup. This is here so that it's in the same place as the settings * keys and easy to update. @@ -2660,20 +2997,6 @@ public final class Settings { SCREEN_AUTO_BRIGHTNESS_ADJ, VIBRATE_INPUT_DEVICES, MODE_RINGER_STREAMS_AFFECTED, - VOLUME_VOICE, - VOLUME_SYSTEM, - VOLUME_RING, - VOLUME_MUSIC, - VOLUME_ALARM, - VOLUME_NOTIFICATION, - VOLUME_BLUETOOTH_SCO, - VOLUME_VOICE + APPEND_FOR_LAST_AUDIBLE, - VOLUME_SYSTEM + APPEND_FOR_LAST_AUDIBLE, - VOLUME_RING + APPEND_FOR_LAST_AUDIBLE, - VOLUME_MUSIC + APPEND_FOR_LAST_AUDIBLE, - VOLUME_ALARM + APPEND_FOR_LAST_AUDIBLE, - VOLUME_NOTIFICATION + APPEND_FOR_LAST_AUDIBLE, - VOLUME_BLUETOOTH_SCO + APPEND_FOR_LAST_AUDIBLE, TEXT_AUTO_REPLACE, TEXT_AUTO_CAPS, TEXT_AUTO_PUNCTUATE, @@ -2703,17 +3026,199 @@ public final class Settings { }; /** + * These are all pulbic system settings + * + * @hide + */ + public static final Set<String> PUBLIC_SETTINGS = new ArraySet<>(); + static { + PUBLIC_SETTINGS.add(END_BUTTON_BEHAVIOR); + PUBLIC_SETTINGS.add(WIFI_USE_STATIC_IP); + PUBLIC_SETTINGS.add(WIFI_STATIC_IP); + PUBLIC_SETTINGS.add(WIFI_STATIC_GATEWAY); + PUBLIC_SETTINGS.add(WIFI_STATIC_NETMASK); + PUBLIC_SETTINGS.add(WIFI_STATIC_DNS1); + PUBLIC_SETTINGS.add(WIFI_STATIC_DNS2); + PUBLIC_SETTINGS.add(BLUETOOTH_DISCOVERABILITY); + PUBLIC_SETTINGS.add(BLUETOOTH_DISCOVERABILITY_TIMEOUT); + PUBLIC_SETTINGS.add(NEXT_ALARM_FORMATTED); + PUBLIC_SETTINGS.add(FONT_SCALE); + PUBLIC_SETTINGS.add(DIM_SCREEN); + PUBLIC_SETTINGS.add(SCREEN_OFF_TIMEOUT); + PUBLIC_SETTINGS.add(SCREEN_BRIGHTNESS); + PUBLIC_SETTINGS.add(SCREEN_BRIGHTNESS_MODE); + PUBLIC_SETTINGS.add(MODE_RINGER_STREAMS_AFFECTED); + PUBLIC_SETTINGS.add(MUTE_STREAMS_AFFECTED); + PUBLIC_SETTINGS.add(VIBRATE_ON); + PUBLIC_SETTINGS.add(VOLUME_RING); + PUBLIC_SETTINGS.add(VOLUME_SYSTEM); + PUBLIC_SETTINGS.add(VOLUME_VOICE); + PUBLIC_SETTINGS.add(VOLUME_MUSIC); + PUBLIC_SETTINGS.add(VOLUME_ALARM); + PUBLIC_SETTINGS.add(VOLUME_NOTIFICATION); + PUBLIC_SETTINGS.add(VOLUME_BLUETOOTH_SCO); + PUBLIC_SETTINGS.add(RINGTONE); + PUBLIC_SETTINGS.add(NOTIFICATION_SOUND); + PUBLIC_SETTINGS.add(ALARM_ALERT); + PUBLIC_SETTINGS.add(TEXT_AUTO_REPLACE); + PUBLIC_SETTINGS.add(TEXT_AUTO_CAPS); + PUBLIC_SETTINGS.add(TEXT_AUTO_PUNCTUATE); + PUBLIC_SETTINGS.add(TEXT_SHOW_PASSWORD); + PUBLIC_SETTINGS.add(SHOW_GTALK_SERVICE_STATUS); + PUBLIC_SETTINGS.add(WALLPAPER_ACTIVITY); + PUBLIC_SETTINGS.add(TIME_12_24); + PUBLIC_SETTINGS.add(DATE_FORMAT); + PUBLIC_SETTINGS.add(SETUP_WIZARD_HAS_RUN); + PUBLIC_SETTINGS.add(ACCELEROMETER_ROTATION); + PUBLIC_SETTINGS.add(USER_ROTATION); + PUBLIC_SETTINGS.add(DTMF_TONE_WHEN_DIALING); + PUBLIC_SETTINGS.add(SOUND_EFFECTS_ENABLED); + PUBLIC_SETTINGS.add(HAPTIC_FEEDBACK_ENABLED); + PUBLIC_SETTINGS.add(SHOW_WEB_SUGGESTIONS); + } + + /** + * These are all hidden system settings. + * + * @hide + */ + public static final Set<String> PRIVATE_SETTINGS = new ArraySet<>(); + static { + PRIVATE_SETTINGS.add(WIFI_USE_STATIC_IP); + PRIVATE_SETTINGS.add(END_BUTTON_BEHAVIOR); + PRIVATE_SETTINGS.add(ADVANCED_SETTINGS); + PRIVATE_SETTINGS.add(SCREEN_AUTO_BRIGHTNESS_ADJ); + PRIVATE_SETTINGS.add(VIBRATE_INPUT_DEVICES); + PRIVATE_SETTINGS.add(VOLUME_MASTER); + PRIVATE_SETTINGS.add(VOLUME_MASTER_MUTE); + PRIVATE_SETTINGS.add(MICROPHONE_MUTE); + PRIVATE_SETTINGS.add(NOTIFICATIONS_USE_RING_VOLUME); + PRIVATE_SETTINGS.add(VIBRATE_IN_SILENT); + PRIVATE_SETTINGS.add(MEDIA_BUTTON_RECEIVER); + PRIVATE_SETTINGS.add(HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY); + PRIVATE_SETTINGS.add(VIBRATE_WHEN_RINGING); + PRIVATE_SETTINGS.add(DTMF_TONE_TYPE_WHEN_DIALING); + PRIVATE_SETTINGS.add(HEARING_AID); + PRIVATE_SETTINGS.add(TTY_MODE); + PRIVATE_SETTINGS.add(NOTIFICATION_LIGHT_PULSE); + PRIVATE_SETTINGS.add(POINTER_LOCATION); + PRIVATE_SETTINGS.add(SHOW_TOUCHES); + PRIVATE_SETTINGS.add(WINDOW_ORIENTATION_LISTENER_LOG); + PRIVATE_SETTINGS.add(POWER_SOUNDS_ENABLED); + PRIVATE_SETTINGS.add(DOCK_SOUNDS_ENABLED); + PRIVATE_SETTINGS.add(LOCKSCREEN_SOUNDS_ENABLED); + PRIVATE_SETTINGS.add(LOCKSCREEN_DISABLED); + PRIVATE_SETTINGS.add(LOW_BATTERY_SOUND); + PRIVATE_SETTINGS.add(DESK_DOCK_SOUND); + PRIVATE_SETTINGS.add(DESK_UNDOCK_SOUND); + PRIVATE_SETTINGS.add(CAR_DOCK_SOUND); + PRIVATE_SETTINGS.add(CAR_UNDOCK_SOUND); + PRIVATE_SETTINGS.add(LOCK_SOUND); + PRIVATE_SETTINGS.add(UNLOCK_SOUND); + PRIVATE_SETTINGS.add(SIP_RECEIVE_CALLS); + PRIVATE_SETTINGS.add(SIP_CALL_OPTIONS); + PRIVATE_SETTINGS.add(SIP_ALWAYS); + PRIVATE_SETTINGS.add(SIP_ADDRESS_ONLY); + PRIVATE_SETTINGS.add(SIP_ASK_ME_EACH_TIME); + PRIVATE_SETTINGS.add(POINTER_SPEED); + PRIVATE_SETTINGS.add(LOCK_TO_APP_ENABLED); + PRIVATE_SETTINGS.add(EGG_MODE); + } + + /** + * These are all pulbic system settings + * + * @hide + */ + public static final Map<String, Validator> VALIDATORS = new ArrayMap<>(); + static { + VALIDATORS.put(END_BUTTON_BEHAVIOR,END_BUTTON_BEHAVIOR_VALIDATOR); + VALIDATORS.put(WIFI_USE_STATIC_IP, WIFI_USE_STATIC_IP_VALIDATOR); + VALIDATORS.put(BLUETOOTH_DISCOVERABILITY, BLUETOOTH_DISCOVERABILITY_VALIDATOR); + VALIDATORS.put(BLUETOOTH_DISCOVERABILITY_TIMEOUT, + BLUETOOTH_DISCOVERABILITY_TIMEOUT_VALIDATOR); + VALIDATORS.put(NEXT_ALARM_FORMATTED, NEXT_ALARM_FORMATTED_VALIDATOR); + VALIDATORS.put(FONT_SCALE, FONT_SCALE_VALIDATOR); + VALIDATORS.put(DIM_SCREEN, DIM_SCREEN_VALIDATOR); + VALIDATORS.put(SCREEN_OFF_TIMEOUT, SCREEN_OFF_TIMEOUT_VALIDATOR); + VALIDATORS.put(SCREEN_BRIGHTNESS, SCREEN_BRIGHTNESS_VALIDATOR); + VALIDATORS.put(SCREEN_BRIGHTNESS_MODE, SCREEN_BRIGHTNESS_MODE_VALIDATOR); + VALIDATORS.put(MODE_RINGER_STREAMS_AFFECTED, MODE_RINGER_STREAMS_AFFECTED_VALIDATOR); + VALIDATORS.put(MUTE_STREAMS_AFFECTED, MUTE_STREAMS_AFFECTED_VALIDATOR); + VALIDATORS.put(VIBRATE_ON, VIBRATE_ON_VALIDATOR); + VALIDATORS.put(RINGTONE, RINGTONE_VALIDATOR); + VALIDATORS.put(NOTIFICATION_SOUND, NOTIFICATION_SOUND_VALIDATOR); + VALIDATORS.put(ALARM_ALERT, ALARM_ALERT_VALIDATOR); + VALIDATORS.put(TEXT_AUTO_REPLACE, TEXT_AUTO_REPLACE_VALIDATOR); + VALIDATORS.put(TEXT_AUTO_CAPS, TEXT_AUTO_CAPS_VALIDATOR); + VALIDATORS.put(TEXT_AUTO_PUNCTUATE, TEXT_AUTO_PUNCTUATE_VALIDATOR); + VALIDATORS.put(TEXT_SHOW_PASSWORD, TEXT_SHOW_PASSWORD_VALIDATOR); + VALIDATORS.put(SHOW_GTALK_SERVICE_STATUS, SHOW_GTALK_SERVICE_STATUS_VALIDATOR); + VALIDATORS.put(WALLPAPER_ACTIVITY, WALLPAPER_ACTIVITY_VALIDATOR); + VALIDATORS.put(TIME_12_24, TIME_12_24_VALIDATOR); + VALIDATORS.put(DATE_FORMAT, DATE_FORMAT_VALIDATOR); + VALIDATORS.put(SETUP_WIZARD_HAS_RUN, SETUP_WIZARD_HAS_RUN_VALIDATOR); + VALIDATORS.put(ACCELEROMETER_ROTATION, ACCELEROMETER_ROTATION_VALIDATOR); + VALIDATORS.put(USER_ROTATION, USER_ROTATION_VALIDATOR); + VALIDATORS.put(DTMF_TONE_WHEN_DIALING, DTMF_TONE_WHEN_DIALING_VALIDATOR); + VALIDATORS.put(SOUND_EFFECTS_ENABLED, SOUND_EFFECTS_ENABLED_VALIDATOR); + VALIDATORS.put(HAPTIC_FEEDBACK_ENABLED, HAPTIC_FEEDBACK_ENABLED_VALIDATOR); + VALIDATORS.put(SHOW_WEB_SUGGESTIONS, SHOW_WEB_SUGGESTIONS_VALIDATOR); + VALIDATORS.put(WIFI_USE_STATIC_IP, WIFI_USE_STATIC_IP_VALIDATOR); + VALIDATORS.put(END_BUTTON_BEHAVIOR, END_BUTTON_BEHAVIOR_VALIDATOR); + VALIDATORS.put(ADVANCED_SETTINGS, ADVANCED_SETTINGS_VALIDATOR); + VALIDATORS.put(SCREEN_AUTO_BRIGHTNESS_ADJ, SCREEN_AUTO_BRIGHTNESS_ADJ_VALIDATOR); + VALIDATORS.put(VIBRATE_INPUT_DEVICES, VIBRATE_INPUT_DEVICES_VALIDATOR); + VALIDATORS.put(VOLUME_MASTER_MUTE, VOLUME_MASTER_MUTE_VALIDATOR); + VALIDATORS.put(MICROPHONE_MUTE, MICROPHONE_MUTE_VALIDATOR); + VALIDATORS.put(NOTIFICATIONS_USE_RING_VOLUME, NOTIFICATIONS_USE_RING_VOLUME_VALIDATOR); + VALIDATORS.put(VIBRATE_IN_SILENT, VIBRATE_IN_SILENT_VALIDATOR); + VALIDATORS.put(MEDIA_BUTTON_RECEIVER, MEDIA_BUTTON_RECEIVER_VALIDATOR); + VALIDATORS.put(HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY, + HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY_VALIDATOR); + VALIDATORS.put(VIBRATE_WHEN_RINGING, VIBRATE_WHEN_RINGING_VALIDATOR); + VALIDATORS.put(DTMF_TONE_TYPE_WHEN_DIALING, DTMF_TONE_TYPE_WHEN_DIALING_VALIDATOR); + VALIDATORS.put(HEARING_AID, HEARING_AID_VALIDATOR); + VALIDATORS.put(TTY_MODE, TTY_MODE_VALIDATOR); + VALIDATORS.put(NOTIFICATION_LIGHT_PULSE, NOTIFICATION_LIGHT_PULSE_VALIDATOR); + VALIDATORS.put(POINTER_LOCATION, POINTER_LOCATION_VALIDATOR); + VALIDATORS.put(SHOW_TOUCHES, SHOW_TOUCHES_VALIDATOR); + VALIDATORS.put(WINDOW_ORIENTATION_LISTENER_LOG, + WINDOW_ORIENTATION_LISTENER_LOG_VALIDATOR); + VALIDATORS.put(LOCKSCREEN_SOUNDS_ENABLED, LOCKSCREEN_SOUNDS_ENABLED_VALIDATOR); + VALIDATORS.put(LOCKSCREEN_DISABLED, LOCKSCREEN_DISABLED_VALIDATOR); + VALIDATORS.put(SIP_RECEIVE_CALLS, SIP_RECEIVE_CALLS_VALIDATOR); + VALIDATORS.put(SIP_CALL_OPTIONS, SIP_CALL_OPTIONS_VALIDATOR); + VALIDATORS.put(SIP_ALWAYS, SIP_ALWAYS_VALIDATOR); + VALIDATORS.put(SIP_ADDRESS_ONLY, SIP_ADDRESS_ONLY_VALIDATOR); + VALIDATORS.put(SIP_ASK_ME_EACH_TIME, SIP_ASK_ME_EACH_TIME_VALIDATOR); + VALIDATORS.put(POINTER_SPEED, POINTER_SPEED_VALIDATOR); + VALIDATORS.put(LOCK_TO_APP_ENABLED, LOCK_TO_APP_ENABLED_VALIDATOR); + VALIDATORS.put(EGG_MODE, EGG_MODE_VALIDATOR); + VALIDATORS.put(WIFI_STATIC_IP, WIFI_STATIC_IP_VALIDATOR); + VALIDATORS.put(WIFI_STATIC_GATEWAY, WIFI_STATIC_GATEWAY_VALIDATOR); + VALIDATORS.put(WIFI_STATIC_NETMASK, WIFI_STATIC_NETMASK_VALIDATOR); + VALIDATORS.put(WIFI_STATIC_DNS1, WIFI_STATIC_DNS1_VALIDATOR); + VALIDATORS.put(WIFI_STATIC_DNS2, WIFI_STATIC_DNS2_VALIDATOR); + } + + /** * These entries are considered common between the personal and the managed profile, * since the managed profile doesn't get to change them. - * @hide */ - public static final String[] CLONE_TO_MANAGED_PROFILE = { - DATE_FORMAT, - HAPTIC_FEEDBACK_ENABLED, - SOUND_EFFECTS_ENABLED, - TEXT_SHOW_PASSWORD, - TIME_12_24 - }; + private static final Set<String> CLONE_TO_MANAGED_PROFILE = new ArraySet<>(); + static { + CLONE_TO_MANAGED_PROFILE.add(DATE_FORMAT); + CLONE_TO_MANAGED_PROFILE.add(HAPTIC_FEEDBACK_ENABLED); + CLONE_TO_MANAGED_PROFILE.add(SOUND_EFFECTS_ENABLED); + CLONE_TO_MANAGED_PROFILE.add(TEXT_SHOW_PASSWORD); + CLONE_TO_MANAGED_PROFILE.add(TIME_12_24); + } + + /** @hide */ + public static void getCloneToManagedProfileSettings(Set<String> outKeySet) { + outKeySet.addAll(CLONE_TO_MANAGED_PROFILE); + } /** * When to use Wi-Fi calling @@ -3103,7 +3608,7 @@ public final class Settings { } /** @hide */ - public static void getMovedKeys(HashSet<String> outKeySet) { + public static void getMovedToGlobalSettings(Set<String> outKeySet) { outKeySet.addAll(MOVED_TO_GLOBAL); } @@ -3657,6 +4162,7 @@ public final class Settings { * A flag containing settings used for biometric weak * @hide */ + @Deprecated public static final String LOCK_BIOMETRIC_WEAK_FLAGS = "lock_biometric_weak_flags"; @@ -3668,7 +4174,11 @@ public final class Settings { /** * Whether autolock is enabled (0 = false, 1 = true) + * + * @deprecated Use {@link android.app.KeyguardManager} to determine the state and security + * level of the keyguard. */ + @Deprecated public static final String LOCK_PATTERN_ENABLED = "lock_pattern_autolock"; /** @@ -3707,6 +4217,7 @@ public final class Settings { * Ids of the user-selected appwidgets on the lockscreen (comma-delimited). * @hide */ + @Deprecated public static final String LOCK_SCREEN_APPWIDGET_IDS = "lock_screen_appwidget_ids"; @@ -3720,6 +4231,7 @@ public final class Settings { * Id of the appwidget shown on the lock screen when appwidgets are disabled. * @hide */ + @Deprecated public static final String LOCK_SCREEN_FALLBACK_APPWIDGET_ID = "lock_screen_fallback_appwidget_id"; @@ -3727,6 +4239,7 @@ public final class Settings { * Index of the lockscreen appwidget to restore, -1 if none. * @hide */ + @Deprecated public static final String LOCK_SCREEN_STICKY_APPWIDGET = "lock_screen_sticky_appwidget"; @@ -4758,6 +5271,10 @@ public final class Settings { public static final String BAR_SERVICE_COMPONENT = "bar_service_component"; /** @hide */ + public static final String VOLUME_CONTROLLER_SERVICE_COMPONENT + = "volume_controller_service_component"; + + /** @hide */ public static final String IMMERSIVE_MODE_CONFIRMATIONS = "immersive_mode_confirmations"; /** @@ -4857,6 +5374,7 @@ public final class Settings { ACCESSIBILITY_SCRIPT_INJECTION, BACKUP_AUTO_RESTORE, ENABLED_ACCESSIBILITY_SERVICES, + ENABLED_NOTIFICATION_LISTENERS, TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES, TOUCH_EXPLORATION_ENABLED, ACCESSIBILITY_ENABLED, @@ -4881,6 +5399,9 @@ public final class Settings { WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON, // moved to global WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY, // moved to global WIFI_NUM_OPEN_NETWORKS_KEPT, // moved to global + SELECTED_SPELL_CHECKER, + SELECTED_SPELL_CHECKER_SUBTYPE, + SPELL_CHECKER_ENABLED, MOUNT_PLAY_NOTIFICATION_SND, MOUNT_UMS_AUTOSTART, MOUNT_UMS_PROMPT, @@ -4892,22 +5413,27 @@ public final class Settings { /** * These entries are considered common between the personal and the managed profile, * since the managed profile doesn't get to change them. - * @hide */ - public static final String[] CLONE_TO_MANAGED_PROFILE = { - ACCESSIBILITY_ENABLED, - ALLOW_MOCK_LOCATION, - ALLOWED_GEOLOCATION_ORIGINS, - DEFAULT_INPUT_METHOD, - ENABLED_ACCESSIBILITY_SERVICES, - ENABLED_INPUT_METHODS, - LOCATION_MODE, - LOCATION_PROVIDERS_ALLOWED, - LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, - SELECTED_INPUT_METHOD_SUBTYPE, - SELECTED_SPELL_CHECKER, - SELECTED_SPELL_CHECKER_SUBTYPE - }; + private static final Set<String> CLONE_TO_MANAGED_PROFILE = new ArraySet<>(); + static { + CLONE_TO_MANAGED_PROFILE.add(ACCESSIBILITY_ENABLED); + CLONE_TO_MANAGED_PROFILE.add(ALLOW_MOCK_LOCATION); + CLONE_TO_MANAGED_PROFILE.add(ALLOWED_GEOLOCATION_ORIGINS); + CLONE_TO_MANAGED_PROFILE.add(DEFAULT_INPUT_METHOD); + CLONE_TO_MANAGED_PROFILE.add(ENABLED_ACCESSIBILITY_SERVICES); + CLONE_TO_MANAGED_PROFILE.add(ENABLED_INPUT_METHODS); + CLONE_TO_MANAGED_PROFILE.add(LOCATION_MODE); + CLONE_TO_MANAGED_PROFILE.add(LOCATION_PROVIDERS_ALLOWED); + CLONE_TO_MANAGED_PROFILE.add(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS); + CLONE_TO_MANAGED_PROFILE.add(SELECTED_INPUT_METHOD_SUBTYPE); + CLONE_TO_MANAGED_PROFILE.add(SELECTED_SPELL_CHECKER); + CLONE_TO_MANAGED_PROFILE.add(SELECTED_SPELL_CHECKER_SUBTYPE); + } + + /** @hide */ + public static void getCloneToManagedProfileSettings(Set<String> outKeySet) { + outKeySet.addAll(CLONE_TO_MANAGED_PROFILE); + } /** * Helper method for determining if a location provider is enabled. @@ -6541,7 +7067,7 @@ public final class Settings { /** * Defines global runtime overrides to window policy. * - * See {@link com.android.internal.policy.impl.PolicyControl} for value format. + * See {@link com.android.server.policy.PolicyControl} for value format. * * @hide */ @@ -6624,6 +7150,33 @@ public final class Settings { public static final String ENHANCED_4G_MODE_ENABLED = "volte_vt_enabled"; /** + * Whether WFC is enabled + * <p> + * Type: int (0 for false, 1 for true) + * + * @hide + */ + public static final String WFC_IMS_ENABLED = "wfc_ims_enabled"; + + /** + * WFC Mode. + * <p> + * Type: int - 2=Wi-Fi preferred, 1=Cellular preferred, 0=Wi-Fi only + * + * @hide + */ + public static final String WFC_IMS_MODE = "wfc_ims_mode"; + + /** + * Whether WFC roaming is enabled + * <p> + * Type: int (0 for false, 1 for true) + * + * @hide + */ + public static final String WFC_IMS_ROAMING_ENABLED = "wfc_ims_roaming_enabled"; + + /** * Global override to disable VoLTE (independent of user setting) * <p> * Type: int (1 for disable VoLTE, 0 to use user configuration) @@ -6689,6 +7242,11 @@ public final class Settings { MOVED_TO_SECURE.add(Settings.Global.INSTALL_NON_MARKET_APPS); } + /** @hide */ + public static void getMovedToSecureSettings(Set<String> outKeySet) { + outKeySet.addAll(MOVED_TO_SECURE); + } + /** * Look up a name in the database. * @param resolver to access the database with diff --git a/core/java/android/provider/VoicemailContract.java b/core/java/android/provider/VoicemailContract.java index d71ad03..0da4fd5 100644 --- a/core/java/android/provider/VoicemailContract.java +++ b/core/java/android/provider/VoicemailContract.java @@ -19,10 +19,18 @@ package android.provider; import android.Manifest; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; import android.content.Intent; import android.database.ContentObserver; +import android.database.Cursor; import android.net.Uri; import android.provider.CallLog.Calls; +import android.telecom.PhoneAccountHandle; +import android.telecom.Voicemail; + +import java.util.List; /** * The contract between the voicemail provider and applications. Contains @@ -199,13 +207,100 @@ public class VoicemailContract { */ public static final String _DATA = "_data"; + // Note: PHONE_ACCOUNT_* constant values are "subscription_*" due to a historic naming + // that was encoded into call log databases. + + /** + * The component name of the account in string form. + * <P>Type: TEXT</P> + */ + public static final String PHONE_ACCOUNT_COMPONENT_NAME = "subscription_component_name"; + + /** + * The identifier of a account that is unique to a specified component. + * <P>Type: TEXT</P> + */ + public static final String PHONE_ACCOUNT_ID = "subscription_id"; + + /** + * Flag used to indicate that local, unsynced changes are present. + * Currently, this is used to indicate that the voicemail was read or deleted. + * The value will be 1 if dirty is true, 0 if false. + * <P>Type: INTEGER (boolean)</P> + */ + public static final String DIRTY = "dirty"; + + /** + * Flag used to indicate that the voicemail was deleted but not synced to the server. + * A deleted row should be ignored. + * The value will be 1 if deleted is true, 0 if false. + * <P>Type: INTEGER (boolean)</P> + */ + public static final String DELETED = "deleted"; + /** * A convenience method to build voicemail URI specific to a source package by appending * {@link VoicemailContract#PARAM_KEY_SOURCE_PACKAGE} param to the base URI. */ public static Uri buildSourceUri(String packageName) { return Voicemails.CONTENT_URI.buildUpon() - .appendQueryParameter(PARAM_KEY_SOURCE_PACKAGE, packageName).build(); + .appendQueryParameter(PARAM_KEY_SOURCE_PACKAGE, packageName) + .build(); + } + + /** + * Inserts a new voicemail into the voicemail content provider. + * + * @param context The context of the app doing the inserting + * @param voicemail Data to be inserted + * @return {@link Uri} of the newly inserted {@link Voicemail} + */ + public static Uri insert(Context context, Voicemail voicemail) { + ContentResolver contentResolver = context.getContentResolver(); + ContentValues contentValues = getContentValues(voicemail); + return contentResolver.insert(Voicemails.CONTENT_URI, contentValues); + } + + /** + * Inserts a list of voicemails into the voicemail content provider. + * + * @param context The context of the app doing the inserting + * @param voicemails Data to be inserted + * @return the number of voicemails inserted + */ + public static int insert(Context context, List<Voicemail> voicemails) { + ContentResolver contentResolver = context.getContentResolver(); + int count = voicemails.size(); + for (int i = 0; i < count; i++) { + ContentValues contentValues = getContentValues(voicemails.get(i)); + contentResolver.insert(Voicemails.CONTENT_URI, contentValues); + } + return count; + } + + /** + * Clears all voicemails accessible to this voicemail content provider for the calling + * package. By default, a package only has permission to delete voicemails it inserted. + * + * @return the number of voicemails deleted + */ + public static int deleteAll(Context context) { + return context.getContentResolver().delete( + buildSourceUri(context.getPackageName()), "", new String[0]); + } + + /** + * Maps structured {@link Voicemail} to {@link ContentValues} in content provider. + */ + private static ContentValues getContentValues(Voicemail voicemail) { + ContentValues contentValues = new ContentValues(); + contentValues.put(Voicemails.DATE, String.valueOf(voicemail.getTimestampMillis())); + contentValues.put(Voicemails.NUMBER, voicemail.getNumber()); + contentValues.put(Voicemails.DURATION, String.valueOf(voicemail.getDuration())); + contentValues.put(Voicemails.SOURCE_PACKAGE, voicemail.getSourcePackage()); + contentValues.put(Voicemails.SOURCE_DATA, voicemail.getSourceData()); + contentValues.put(Voicemails.IS_READ, voicemail.isRead() ? 1 : 0); + return contentValues; } } @@ -222,10 +317,27 @@ public class VoicemailContract { private Status() { } /** - * The package name of the voicemail source. There can only be a one entry per source. + * The package name of the voicemail source. There can only be a one entry per account + * per source. * <P>Type: TEXT</P> */ public static final String SOURCE_PACKAGE = SOURCE_PACKAGE_FIELD; + + // Note: Multiple entries may exist for a single source if they are differentiated by the + // PHONE_ACCOUNT_* fields. + + /** + * The component name of the account in string form. + * <P>Type: TEXT</P> + */ + public static final String PHONE_ACCOUNT_COMPONENT_NAME = "phone_account_component_name"; + + /** + * The identifier of a account that is unique to a specified component. + * <P>Type: TEXT</P> + */ + public static final String PHONE_ACCOUNT_ID = "phone_account_id"; + /** * The URI to call to invoke source specific voicemail settings screen. On a user request * to setup voicemail an intent with action VIEW with this URI will be fired by the system. @@ -318,5 +430,50 @@ public class VoicemailContract { return Status.CONTENT_URI.buildUpon() .appendQueryParameter(PARAM_KEY_SOURCE_PACKAGE, packageName).build(); } + + /** + * A helper method to set the status of a voicemail source. + * + * @param context The context from the package calling the method. This will be the source. + * @param accountHandle The handle for the account the source is associated with. + * @param configurationState See {@link Status#CONFIGURATION_STATE} + * @param dataChannelState See {@link Status#DATA_CHANNEL_STATE} + * @param notificationChannelState See {@link Status#NOTIFICATION_CHANNEL_STATE} + */ + public static void setStatus(Context context, PhoneAccountHandle accountHandle, + int configurationState, int dataChannelState, int notificationChannelState) { + ContentResolver contentResolver = context.getContentResolver(); + Uri statusUri = buildSourceUri(context.getPackageName()); + ContentValues values = new ContentValues(); + values.put(Status.PHONE_ACCOUNT_COMPONENT_NAME, + accountHandle.getComponentName().toString()); + values.put(Status.PHONE_ACCOUNT_ID, accountHandle.getId()); + values.put(Status.CONFIGURATION_STATE, configurationState); + values.put(Status.DATA_CHANNEL_STATE, dataChannelState); + values.put(Status.NOTIFICATION_CHANNEL_STATE, notificationChannelState); + + if (isStatusPresent(contentResolver, statusUri)) { + contentResolver.update(statusUri, values, null, null); + } else { + contentResolver.insert(statusUri, values); + } + } + + /** + * Determines if a voicemail source exists in the status table. + * + * @param contentResolver A content resolver constructed from the appropriate context. + * @param statusUri The content uri for the source. + * @return {@code true} if a status entry for this source exists + */ + private static boolean isStatusPresent(ContentResolver contentResolver, Uri statusUri) { + Cursor cursor = null; + try { + cursor = contentResolver.query(statusUri, null, null, null, null); + return cursor != null && cursor.getCount() != 0; + } finally { + if (cursor != null) cursor.close(); + } + } } } diff --git a/core/java/android/security/keymaster/KeyCharacteristics.java b/core/java/android/security/keymaster/KeyCharacteristics.java index b803a1b..0f1d422 100644 --- a/core/java/android/security/keymaster/KeyCharacteristics.java +++ b/core/java/android/security/keymaster/KeyCharacteristics.java @@ -19,8 +19,6 @@ package android.security.keymaster; import android.os.Parcel; import android.os.Parcelable; -import java.util.List; - /** * @hide */ diff --git a/core/java/android/security/keymaster/KeymasterBlobArgument.java b/core/java/android/security/keymaster/KeymasterBlobArgument.java index 27f1153..9af4445 100644 --- a/core/java/android/security/keymaster/KeymasterBlobArgument.java +++ b/core/java/android/security/keymaster/KeymasterBlobArgument.java @@ -17,7 +17,6 @@ package android.security.keymaster; import android.os.Parcel; -import android.os.Parcelable; /** * @hide diff --git a/core/java/android/security/keymaster/KeymasterBooleanArgument.java b/core/java/android/security/keymaster/KeymasterBooleanArgument.java index 8e17db4..5481e8f 100644 --- a/core/java/android/security/keymaster/KeymasterBooleanArgument.java +++ b/core/java/android/security/keymaster/KeymasterBooleanArgument.java @@ -17,7 +17,6 @@ package android.security.keymaster; import android.os.Parcel; -import android.os.Parcelable; /** * @hide diff --git a/core/java/android/security/keymaster/KeymasterDateArgument.java b/core/java/android/security/keymaster/KeymasterDateArgument.java index e8f4055..310f546 100644 --- a/core/java/android/security/keymaster/KeymasterDateArgument.java +++ b/core/java/android/security/keymaster/KeymasterDateArgument.java @@ -17,8 +17,6 @@ package android.security.keymaster; import android.os.Parcel; -import android.os.Parcelable; - import java.util.Date; /** diff --git a/core/java/android/security/keymaster/KeymasterIntArgument.java b/core/java/android/security/keymaster/KeymasterIntArgument.java index 71797ae..c3738d7 100644 --- a/core/java/android/security/keymaster/KeymasterIntArgument.java +++ b/core/java/android/security/keymaster/KeymasterIntArgument.java @@ -17,7 +17,6 @@ package android.security.keymaster; import android.os.Parcel; -import android.os.Parcelable; /** * @hide diff --git a/core/java/android/security/keymaster/KeymasterLongArgument.java b/core/java/android/security/keymaster/KeymasterLongArgument.java index 781b1ab..3c565b8 100644 --- a/core/java/android/security/keymaster/KeymasterLongArgument.java +++ b/core/java/android/security/keymaster/KeymasterLongArgument.java @@ -17,7 +17,6 @@ package android.security.keymaster; import android.os.Parcel; -import android.os.Parcelable; /** * @hide diff --git a/core/java/android/security/keymaster/OperationResult.java b/core/java/android/security/keymaster/OperationResult.java index ad54c96..4fc9d24 100644 --- a/core/java/android/security/keymaster/OperationResult.java +++ b/core/java/android/security/keymaster/OperationResult.java @@ -20,8 +20,6 @@ import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; -import java.util.List; - /** * Class for handling the parceling of return values from keymaster crypto operations * (begin/update/finish). diff --git a/core/java/android/service/carrier/CarrierMessagingService.java b/core/java/android/service/carrier/CarrierMessagingService.java index 3d6ebca..0592a84 100644 --- a/core/java/android/service/carrier/CarrierMessagingService.java +++ b/core/java/android/service/carrier/CarrierMessagingService.java @@ -23,11 +23,8 @@ import android.app.Service; import android.content.Intent; import android.net.Uri; import android.os.IBinder; -import android.os.Parcel; -import android.os.Parcelable; import android.os.RemoteException; -import java.util.ArrayList; import java.util.List; /** diff --git a/core/java/android/service/chooser/ChooserTarget.aidl b/core/java/android/service/chooser/ChooserTarget.aidl new file mode 100644 index 0000000..ca91bc8 --- /dev/null +++ b/core/java/android/service/chooser/ChooserTarget.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2015 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.service.chooser; + +parcelable ChooserTarget; diff --git a/core/java/android/service/chooser/ChooserTarget.java b/core/java/android/service/chooser/ChooserTarget.java new file mode 100644 index 0000000..7fd1d10 --- /dev/null +++ b/core/java/android/service/chooser/ChooserTarget.java @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2015 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.service.chooser; + +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.IntentSender; +import android.graphics.Bitmap; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +/** + * A ChooserTarget represents a deep-link into an application as returned by a + * {@link android.service.chooser.ChooserTargetService}. + */ +public final class ChooserTarget implements Parcelable { + private static final String TAG = "ChooserTarget"; + + /** + * The title of this target that will be shown to the user. The title may be truncated + * if it is too long to display in the space provided. + */ + private CharSequence mTitle; + + /** + * The icon that will be shown to the user to represent this target. + * The system may resize this icon as appropriate. + */ + private Bitmap mIcon; + + /** + * The IntentSender that will be used to deliver the intent to the target. + * It will be {@link android.content.Intent#fillIn(android.content.Intent, int)} filled in} + * by the real intent sent by the application. + */ + private IntentSender mIntentSender; + + /** + * The score given to this item. It can be normalized. + */ + private float mScore; + + /** + * Construct a deep link target for presentation by a chooser UI. + * + * <p>A target is composed of a title and an icon for presentation to the user. + * The UI presenting this target may truncate the title if it is too long to be presented + * in the available space, as well as crop, resize or overlay the supplied icon.</p> + * + * <p>The creator of a target may supply a ranking score. This score is assumed to be relative + * to the other targets supplied by the same + * {@link ChooserTargetService#onGetChooserTargets(ComponentName, IntentFilter) query}. + * Scores should be in the range from 0.0f (unlikely match) to 1.0f (very relevant match).</p> + * + * <p>Before being sent, the PendingIntent supplied will be + * {@link Intent#fillIn(Intent, int) filled in} by the Intent originally supplied + * to the chooser. When constructing a PendingIntent for use in a ChooserTarget, make sure + * that you permit the relevant fields to be filled in using the appropriate flags such as + * {@link Intent#FILL_IN_ACTION}, {@link Intent#FILL_IN_CATEGORIES}, + * {@link Intent#FILL_IN_DATA} and {@link Intent#FILL_IN_CLIP_DATA}. Note that + * {@link Intent#FILL_IN_CLIP_DATA} is required to appropriately receive URI permission grants + * for {@link Intent#ACTION_SEND} intents.</p> + * + * <p>Take care not to place custom {@link android.os.Parcelable} types into + * the PendingIntent as extras, as the system will not be able to unparcel it to merge + * additional extras.</p> + * + * @param title title of this target that will be shown to a user + * @param icon icon to represent this target + * @param score ranking score for this target between 0.0f and 1.0f, inclusive + * @param pendingIntent PendingIntent to fill in and send if the user chooses this target + */ + public ChooserTarget(CharSequence title, Bitmap icon, float score, + PendingIntent pendingIntent) { + this(title, icon, score, pendingIntent.getIntentSender()); + } + + /** + * Construct a deep link target for presentation by a chooser UI. + * + * <p>A target is composed of a title and an icon for presentation to the user. + * The UI presenting this target may truncate the title if it is too long to be presented + * in the available space, as well as crop, resize or overlay the supplied icon.</p> + * + * <p>The creator of a target may supply a ranking score. This score is assumed to be relative + * to the other targets supplied by the same + * {@link ChooserTargetService#onGetChooserTargets(ComponentName, IntentFilter) query}. + * Scores should be in the range from 0.0f (unlikely match) to 1.0f (very relevant match).</p> + * + * <p>Before being sent, the IntentSender supplied will be + * {@link Intent#fillIn(Intent, int) filled in} by the Intent originally supplied + * to the chooser. When constructing an IntentSender for use in a ChooserTarget, make sure + * that you permit the relevant fields to be filled in using the appropriate flags such as + * {@link Intent#FILL_IN_ACTION}, {@link Intent#FILL_IN_CATEGORIES}, + * {@link Intent#FILL_IN_DATA} and {@link Intent#FILL_IN_CLIP_DATA}. Note that + * {@link Intent#FILL_IN_CLIP_DATA} is required to appropriately receive URI permission grants + * for {@link Intent#ACTION_SEND} intents.</p> + * + * <p>Take care not to place custom {@link android.os.Parcelable} types into + * the IntentSender as extras, as the system will not be able to unparcel it to merge + * additional extras.</p> + * + * @param title title of this target that will be shown to a user + * @param icon icon to represent this target + * @param score ranking score for this target between 0.0f and 1.0f, inclusive + * @param intentSender IntentSender to fill in and send if the user chooses this target + */ + public ChooserTarget(CharSequence title, Bitmap icon, float score, IntentSender intentSender) { + mTitle = title; + mIcon = icon; + if (score > 1.f || score < 0.f) { + throw new IllegalArgumentException("Score " + score + " out of range; " + + "must be between 0.0f and 1.0f"); + } + mScore = score; + mIntentSender = intentSender; + } + + ChooserTarget(Parcel in) { + mTitle = in.readCharSequence(); + if (in.readInt() != 0) { + mIcon = Bitmap.CREATOR.createFromParcel(in); + } else { + mIcon = null; + } + mScore = in.readFloat(); + mIntentSender = IntentSender.readIntentSenderOrNullFromParcel(in); + } + + /** + * Returns the title of this target for display to a user. The UI displaying the title + * may truncate this title if it is too long to be displayed in full. + * + * @return the title of this target, intended to be shown to a user + */ + public CharSequence getTitle() { + return mTitle; + } + + /** + * Returns the icon representing this target for display to a user. The UI displaying the icon + * may crop, resize or overlay this icon. + * + * @return the icon representing this target, intended to be shown to a user + */ + public Bitmap getIcon() { + return mIcon; + } + + /** + * Returns the ranking score supplied by the creator of this ChooserTarget. + * Values are between 0.0f and 1.0f. The UI displaying the target may + * take this score into account when sorting and merging targets from multiple sources. + * + * @return the ranking score for this target between 0.0f and 1.0f, inclusive + */ + public float getScore() { + return mScore; + } + + /** + * Returns the raw IntentSender supplied by the ChooserTarget's creator. + * + * <p>To fill in and send the intent, see {@link #sendIntent(Context, Intent)}.</p> + * + * @return the IntentSender supplied by the ChooserTarget's creator + */ + public IntentSender getIntentSender() { + return mIntentSender; + } + + /** + * Fill in the IntentSender supplied by the ChooserTarget's creator and send it. + * + * @param context the sending Context; generally the Activity presenting the chooser UI + * @param fillInIntent the Intent provided to the Chooser to be sent to a selected target + * @return true if sending the Intent was successful + */ + public boolean sendIntent(Context context, Intent fillInIntent) { + if (fillInIntent != null) { + fillInIntent.migrateExtraStreamToClipData(); + fillInIntent.prepareToLeaveProcess(); + } + try { + mIntentSender.sendIntent(context, 0, fillInIntent, null, null); + return true; + } catch (IntentSender.SendIntentException e) { + Log.e(TAG, "sendIntent " + this + " failed", e); + return false; + } + } + + @Override + public String toString() { + return "ChooserTarget{" + mIntentSender.getCreatorPackage() + "'" + mTitle + + "', " + mScore + "}"; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeCharSequence(mTitle); + if (mIcon != null) { + dest.writeInt(1); + mIcon.writeToParcel(dest, 0); + } else { + dest.writeInt(0); + } + dest.writeFloat(mScore); + IntentSender.writeIntentSenderOrNullToParcel(mIntentSender, dest); + } + + public static final Creator<ChooserTarget> CREATOR + = new Creator<ChooserTarget>() { + @Override + public ChooserTarget createFromParcel(Parcel source) { + return new ChooserTarget(source); + } + + @Override + public ChooserTarget[] newArray(int size) { + return new ChooserTarget[size]; + } + }; +} diff --git a/core/java/android/service/chooser/ChooserTargetService.java b/core/java/android/service/chooser/ChooserTargetService.java new file mode 100644 index 0000000..9188806 --- /dev/null +++ b/core/java/android/service/chooser/ChooserTargetService.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2015 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.service.chooser; + +import android.annotation.SdkConstant; +import android.app.Service; +import android.content.ComponentName; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.IBinder; +import android.os.RemoteException; + +import java.util.List; + +/** + * A service that receives calls from the system when the user is asked to choose + * a target for an intent explicitly by another app. The calling app must have invoked + * {@link android.content.Intent#ACTION_CHOOSER ACTION_CHOOSER} as handled by the system; + * applications do not have the ability to query a ChooserTargetService directly. + * + * <p>Which ChooserTargetServices are queried depends on a system-level policy decision + * made at the moment the chooser is invoked, including but not limited to user time + * spent with the app package or associated components in the foreground, recency of usage + * or frequency of usage. These will generally correlate with the order that app targets + * are shown in the list of intent handlers shown in the system chooser or resolver.</p> + * + * <p>To extend this class, you must declare the service in your manifest file with + * the {@link android.Manifest.permission#BIND_CHOOSER_TARGET_SERVICE} permission + * and include an intent filter with the {@link #SERVICE_INTERFACE} action. For example:</p> + * <pre> + * <service android:name=".ChooserTargetService" + * android:label="@string/service_name" + * android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE"> + * <intent-filter> + * <action android:name="android.service.chooser.ChooserTargetService" /> + * </intent-filter> + * </service> + * </pre> + * + * <p>For the system to query your service, you must add a <meta-data> element to the + * Activity in your manifest that can handle Intents that you would also like to provide + * optional deep links for. For example, a chat app might offer deep links to recent active + * conversations instead of invoking a generic picker after the app itself is chosen as a target. + * </p> + * + * <p>The meta-data element should have the name + * <code>android.service.chooser.chooser_target_service</code> and a value corresponding to + * the component name of your service. Example:</p> + * <pre> + * <activity android:name=".MyShareActivity" + * android:label="@string/share_activity_label"> + * <intent-filter> + * <action android:name="android.intent.action.SEND" /> + * </intent-filter> + * <meta-data android:name="android.service.chooser.chooser_target_service" + * android:value=".ChooserTargetService" /> + * </activity> + * </pre> + */ +public abstract class ChooserTargetService extends Service { + // TAG = "ChooserTargetService[MySubclass]"; + private final String TAG = ChooserTargetService.class.getSimpleName() + + '[' + getClass().getSimpleName() + ']'; + + @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) + public static final String SERVICE_INTERFACE = "android.service.chooser.ChooserTargetService"; + + private IChooserTargetServiceWrapper mWrapper = null; + + /** + * Called by the system to retrieve a set of deep-link {@link ChooserTarget targets} that + * can handle an intent. + * + * <p>The returned list should be sorted such that the most relevant targets appear first. + * Any PendingIntents used to construct the resulting ChooserTargets should always be prepared + * to have the relevant data fields filled in by the sender. See + * {@link ChooserTarget#ChooserTarget(CharSequence, android.graphics.Bitmap, float, android.app.PendingIntent) ChooserTarget}.</p> + * + * <p><em>Important:</em> Calls to this method from other applications will occur on + * a binder thread, not on your app's main thread. Make sure that access to relevant data + * within your app is thread-safe.</p> + * + * @param targetActivityName the ComponentName of the matched activity that referred the system + * to this ChooserTargetService + * @param matchedFilter the specific IntentFilter on the component that was matched + * @return a list of deep-link targets to fulfill the intent match, sorted by relevance + */ + public abstract List<ChooserTarget> onGetChooserTargets(ComponentName targetActivityName, + IntentFilter matchedFilter); + + @Override + public IBinder onBind(Intent intent) { + if (!SERVICE_INTERFACE.equals(intent.getAction())) { + return null; + } + + if (mWrapper == null) { + mWrapper = new IChooserTargetServiceWrapper(); + } + return mWrapper; + } + + private class IChooserTargetServiceWrapper extends IChooserTargetService.Stub { + @Override + public void getChooserTargets(ComponentName targetComponentName, + IntentFilter matchedFilter, IChooserTargetResult result) throws RemoteException { + List<ChooserTarget> targets = null; + try { + targets = onGetChooserTargets(targetComponentName, matchedFilter); + } finally { + result.sendResult(targets); + } + } + } +} diff --git a/core/java/android/net/http/HttpLog.java b/core/java/android/service/chooser/IChooserTargetResult.aidl index 0934664..dbd7cbd 100644 --- a/core/java/android/net/http/HttpLog.java +++ b/core/java/android/service/chooser/IChooserTargetResult.aidl @@ -1,5 +1,5 @@ /* - * Copyright (C) 2007 The Android Open Source Project + * Copyright (C) 2015 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. @@ -14,30 +14,14 @@ * limitations under the License. */ -/** - * package-level logging flag - */ - -package android.net.http; - -import android.os.SystemClock; +package android.service.chooser; -import android.util.Log; +import android.service.chooser.ChooserTarget; /** - * {@hide} + * @hide */ -class HttpLog { - private final static String LOGTAG = "http"; - - private static final boolean DEBUG = false; - static final boolean LOGV = false; - - static void v(String logMe) { - Log.v(LOGTAG, SystemClock.uptimeMillis() + " " + Thread.currentThread().getName() + " " + logMe); - } - - static void e(String logMe) { - Log.e(LOGTAG, logMe); - } +interface IChooserTargetResult +{ + void sendResult(in List<ChooserTarget> targets); } diff --git a/core/java/android/service/chooser/IChooserTargetService.aidl b/core/java/android/service/chooser/IChooserTargetService.aidl new file mode 100644 index 0000000..6cfa9a2 --- /dev/null +++ b/core/java/android/service/chooser/IChooserTargetService.aidl @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2015 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.service.chooser; + +import android.content.ComponentName; +import android.content.IntentFilter; +import android.service.chooser.IChooserTargetResult; + +/** + * @hide + */ +oneway interface IChooserTargetService +{ + void getChooserTargets(in ComponentName targetComponentName, + in IntentFilter matchedFilter, IChooserTargetResult result); +}
\ No newline at end of file diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java index 38b0439..822bfcc 100644 --- a/core/java/android/service/dreams/DreamService.java +++ b/core/java/android/service/dreams/DreamService.java @@ -18,6 +18,9 @@ package android.service.dreams; import java.io.FileDescriptor; import java.io.PrintWriter; +import android.annotation.IdRes; +import android.annotation.LayoutRes; +import android.annotation.Nullable; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.app.AlarmManager; @@ -37,6 +40,7 @@ import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; +import android.view.PhoneWindow; import android.view.View; import android.view.ViewGroup; import android.view.Window; @@ -46,7 +50,6 @@ import android.view.WindowManager.LayoutParams; import android.view.accessibility.AccessibilityEvent; import android.util.MathUtils; -import com.android.internal.policy.PolicyManager; import com.android.internal.util.DumpUtils; import com.android.internal.util.DumpUtils.Dump; @@ -341,6 +344,13 @@ public class DreamService extends Service implements Window.Callback { /** {@inheritDoc} */ @Override + public ActionMode onWindowStartingActionMode( + android.view.ActionMode.Callback callback, int type) { + return null; + } + + /** {@inheritDoc} */ + @Override public void onActionModeStarted(ActionMode mode) { } @@ -382,7 +392,7 @@ public class DreamService extends Service implements Window.Callback { * @see #setContentView(android.view.View) * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams) */ - public void setContentView(int layoutResID) { + public void setContentView(@LayoutRes int layoutResID) { getWindow().setContentView(layoutResID); } @@ -442,7 +452,8 @@ public class DreamService extends Service implements Window.Callback { * * @return The view if found or null otherwise. */ - public View findViewById(int id) { + @Nullable + public View findViewById(@IdRes int id) { return getWindow().findViewById(id); } @@ -945,7 +956,7 @@ public class DreamService extends Service implements Window.Callback { throw new IllegalStateException("Only doze dreams can be windowless"); } if (!mWindowless) { - mWindow = PolicyManager.makeNewWindow(this); + mWindow = new PhoneWindow(this); mWindow.setCallback(this); mWindow.requestFeature(Window.FEATURE_NO_TITLE); mWindow.setBackgroundDrawable(new ColorDrawable(0xFF000000)); @@ -1037,10 +1048,10 @@ public class DreamService extends Service implements Window.Callback { protected void dump(final FileDescriptor fd, PrintWriter pw, final String[] args) { DumpUtils.dumpAsync(mHandler, new Dump() { @Override - public void dump(PrintWriter pw) { + public void dump(PrintWriter pw, String prefix) { dumpOnHandler(fd, pw, args); } - }, pw, 1000); + }, pw, "", 1000); } /** @hide */ diff --git a/core/java/android/service/fingerprint/FingerprintManager.java b/core/java/android/service/fingerprint/FingerprintManager.java index 178cc8b..6375668 100644 --- a/core/java/android/service/fingerprint/FingerprintManager.java +++ b/core/java/android/service/fingerprint/FingerprintManager.java @@ -17,11 +17,8 @@ package android.service.fingerprint; import android.app.ActivityManagerNative; -import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; import android.os.Binder; import android.os.Handler; import android.os.IBinder; @@ -31,6 +28,9 @@ import android.provider.Settings; import android.util.Log; import android.util.Slog; +import java.util.ArrayList; +import java.util.List; + /** * A class that coordinates access to the fingerprint hardware. * @hide @@ -97,6 +97,15 @@ public class FingerprintManager { } }; + public static final class FingerprintItem { + public CharSequence name; + public int id; + FingerprintItem(CharSequence name, int id) { + this.name = name; + this.id = id; + } + } + /** * @hide */ @@ -248,4 +257,57 @@ public class FingerprintManager { private void sendError(int msg, int arg1, int arg2) { mHandler.obtainMessage(msg, arg1, arg2); } + + /** + * @return list of current fingerprint items + * @hide + */ + public List<FingerprintItem> getEnrolledFingerprints() { + int[] ids = FingerprintUtils.getFingerprintIdsForUser(mContext.getContentResolver(), + getCurrentUserId()); + List<FingerprintItem> result = new ArrayList<FingerprintItem>(); + for (int i = 0; i < ids.length; i++) { + // TODO: persist names in Settings + FingerprintItem item = new FingerprintItem("Finger" + ids[i], ids[i]); + result.add(item); + } + return result; + } + + /** + * Determine if fingerprint hardware is present and functional. + * @return true if hardware is present and functional, false otherwise. + * @hide + */ + public boolean isHardwareDetected() { + if (mService != null) { + try { + return mService.isHardwareDetected(); + } catch (RemoteException e) { + Log.v(TAG, "Remote exception in isFingerprintHardwareDetected(): ", e); + } + } else { + Log.w(TAG, "isFingerprintHardwareDetected(): Service not connected!"); + } + return false; + } + + /** + * Renames the given fingerprint template + * @param fpId the fingerprint id + * @param newName the new name + * @hide + */ + public void rename(int fpId, String newName) { + // Renames the given fpId + if (mService != null) { + try { + mService.rename(fpId, newName); + } catch (RemoteException e) { + Log.v(TAG, "Remote exception in rename(): ", e); + } + } else { + Log.w(TAG, "rename(): Service not connected!"); + } + } }
\ No newline at end of file diff --git a/core/java/android/service/fingerprint/FingerprintUtils.java b/core/java/android/service/fingerprint/FingerprintUtils.java index a4caf8e..cc17b99 100644 --- a/core/java/android/service/fingerprint/FingerprintUtils.java +++ b/core/java/android/service/fingerprint/FingerprintUtils.java @@ -21,7 +21,11 @@ import android.provider.Settings; import android.text.TextUtils; import android.util.Log; +import com.android.internal.util.ArrayUtils; + +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; /** * Utility class for dealing with fingerprints and fingerprint settings. @@ -32,34 +36,50 @@ class FingerprintUtils { private static final boolean DEBUG = true; private static final String TAG = "FingerprintUtils"; + private static int[] toIntArray(List<Integer> list) { + if (list == null) { + return null; + } + int[] arr = new int[list.size()]; + int i = 0; + for (int elem : list) { + arr[i] = elem; + i++; + } + return arr; + } + public static int[] getFingerprintIdsForUser(ContentResolver res, int userId) { String fingerIdsRaw = Settings.Secure.getStringForUser(res, Settings.Secure.USER_FINGERPRINT_IDS, userId); - - int result[] = {}; + ArrayList<Integer> tmp = new ArrayList<Integer>(); if (!TextUtils.isEmpty(fingerIdsRaw)) { String[] fingerStringIds = fingerIdsRaw.replace("[","").replace("]","").split(", "); - result = new int[fingerStringIds.length]; - for (int i = 0; i < result.length; i++) { + int length = fingerStringIds.length; + for (int i = 0; i < length; i++) { try { - result[i] = Integer.decode(fingerStringIds[i]); + tmp.add(Integer.decode(fingerStringIds[i])); } catch (NumberFormatException e) { - if (DEBUG) Log.d(TAG, "Error when parsing finger id " + fingerStringIds[i]); + if (DEBUG) Log.w(TAG, "Error parsing finger id: '" + fingerStringIds[i] + "'"); } } } - return result; + return toIntArray(tmp); } public static void addFingerprintIdForUser(int fingerId, ContentResolver res, int userId) { - int[] fingerIds = getFingerprintIdsForUser(res, userId); - // FingerId 0 has special meaning. - if (fingerId == 0) return; + if (fingerId == 0) { + Log.w(TAG, "Tried to add fingerId 0"); + return; + } + + int[] fingerIds = getFingerprintIdsForUser(res, userId); // Don't allow dups - for (int i = 0; i < fingerIds.length; i++) { - if (fingerIds[i] == fingerId) return; + if (ArrayUtils.contains(fingerIds, fingerId)) { + Log.w(TAG, "finger already added " + fingerId); + return; } int[] newList = Arrays.copyOf(fingerIds, fingerIds.length + 1); newList[fingerIds.length] = fingerId; @@ -72,19 +92,13 @@ class FingerprintUtils { // FingerId 0 has special meaning. The HAL layer is supposed to remove each finger one // at a time and invoke notify() for each fingerId. If we get called with 0 here, it means // something bad has happened. - if (fingerId == 0) throw new IllegalStateException("Bad fingerId"); + if (fingerId == 0) throw new IllegalArgumentException("fingerId can't be 0"); - int[] fingerIds = getFingerprintIdsForUser(res, userId); - int[] resultIds = Arrays.copyOf(fingerIds, fingerIds.length); - int resultCount = 0; - for (int i = 0; i < fingerIds.length; i++) { - if (fingerId != fingerIds[i]) { - resultIds[resultCount++] = fingerIds[i]; - } - } - if (resultCount > 0) { + final int[] fingerIds = getFingerprintIdsForUser(res, userId); + if (ArrayUtils.contains(fingerIds, fingerId)) { + final int[] result = ArrayUtils.removeInt(fingerIds, fingerId); Settings.Secure.putStringForUser(res, Settings.Secure.USER_FINGERPRINT_IDS, - Arrays.toString(Arrays.copyOf(resultIds, resultCount)), userId); + Arrays.toString(result), userId); return true; } return false; diff --git a/core/java/android/service/fingerprint/IFingerprintService.aidl b/core/java/android/service/fingerprint/IFingerprintService.aidl index 43d5e9a..9b4750b 100644 --- a/core/java/android/service/fingerprint/IFingerprintService.aidl +++ b/core/java/android/service/fingerprint/IFingerprintService.aidl @@ -22,10 +22,10 @@ import android.service.fingerprint.IFingerprintServiceReceiver; * Communication channel from client to the fingerprint service. * @hide */ -oneway interface IFingerprintService { +interface IFingerprintService { // Any errors resulting from this call will be returned to the listener void enroll(IBinder token, long timeout, int userId); - + // Any errors resulting from this call will be returned to the listener void enrollCancel(IBinder token, int userId); @@ -38,4 +38,10 @@ oneway interface IFingerprintService { // Stops listening for fingerprints void stopListening(IBinder token, int userId); + + // Determine if HAL is loaded and ready + boolean isHardwareDetected(); + + // Rename the given fingerprint id + void rename(int fpId, String name); } diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java index 3d39b18..0860153 100644 --- a/core/java/android/service/notification/NotificationListenerService.java +++ b/core/java/android/service/notification/NotificationListenerService.java @@ -77,6 +77,14 @@ public abstract class NotificationListenerService extends Service { */ public static final int INTERRUPTION_FILTER_NONE = 3; + /** {@link #getCurrentInterruptionFilter() Interruption filter} constant - returned when + * the value is unavailable for any reason. For example, before the notification listener + * is connected. + * + * {@see #onListenerConnected()} + */ + public static final int INTERRUPTION_FILTER_UNKNOWN = 0; + /** {@link #getCurrentListenerHints() Listener hints} constant - the primary device UI * should disable notification sound, vibrating and other visual or aural effects. * This does not change the interruption filter, only the effects. **/ @@ -473,15 +481,16 @@ public abstract class NotificationListenerService extends Service { * <p> * Listen for updates using {@link #onInterruptionFilterChanged(int)}. * - * @return One of the INTERRUPTION_FILTER_ constants, or 0 on errors. + * @return One of the INTERRUPTION_FILTER_ constants, or INTERRUPTION_FILTER_UNKNOWN when + * unavailable. */ public final int getCurrentInterruptionFilter() { - if (!isBound()) return 0; + if (!isBound()) return INTERRUPTION_FILTER_UNKNOWN; try { return getNotificationInterface().getInterruptionFilterFromListener(mWrapper); } catch (android.os.RemoteException ex) { Log.v(TAG, "Unable to contact notification manager", ex); - return 0; + return INTERRUPTION_FILTER_UNKNOWN; } } diff --git a/core/java/android/service/restrictions/RestrictionsReceiver.java b/core/java/android/service/restrictions/RestrictionsReceiver.java index 7c6e1f6..b830cb1 100644 --- a/core/java/android/service/restrictions/RestrictionsReceiver.java +++ b/core/java/android/service/restrictions/RestrictionsReceiver.java @@ -21,7 +21,6 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.RestrictionsManager; -import android.os.IBinder; import android.os.PersistableBundle; /** diff --git a/core/java/android/service/trust/TrustAgentService.java b/core/java/android/service/trust/TrustAgentService.java index 62fa978..a3178e2 100644 --- a/core/java/android/service/trust/TrustAgentService.java +++ b/core/java/android/service/trust/TrustAgentService.java @@ -25,13 +25,10 @@ import android.content.ComponentName; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; -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; @@ -125,12 +122,14 @@ public class TrustAgentService extends Service { case MSG_CONFIGURE: ConfigurationData data = (ConfigurationData) msg.obj; boolean result = onConfigure(data.options); - try { - synchronized (mLock) { - mCallback.onConfigureCompleted(result, data.token); + if (data.token != null) { + try { + synchronized (mLock) { + mCallback.onConfigureCompleted(result, data.token); + } + } catch (RemoteException e) { + onError("calling onSetTrustAgentFeaturesEnabledCompleted()"); } - } catch (RemoteException e) { - onError("calling onSetTrustAgentFeaturesEnabledCompleted()"); } break; case MSG_TRUST_TIMEOUT: @@ -206,7 +205,7 @@ public class TrustAgentService extends Service { * PersistableBundle)}. * <p>Agents that support configuration options should overload this method and return 'true'. * - * @param options bundle containing all options or null if none. + * @param options The aggregated list of options or an empty list if no restrictions apply. * @return true if the {@link TrustAgentService} supports configuration options. */ public boolean onConfigure(List<PersistableBundle> options) { diff --git a/core/java/android/service/voice/IVoiceInteractionSession.aidl b/core/java/android/service/voice/IVoiceInteractionSession.aidl index 9f9c312..4f4b2d5 100644 --- a/core/java/android/service/voice/IVoiceInteractionSession.aidl +++ b/core/java/android/service/voice/IVoiceInteractionSession.aidl @@ -17,11 +17,17 @@ package android.service.voice; import android.content.Intent; +import android.graphics.Bitmap; +import android.os.Bundle; /** * @hide */ oneway interface IVoiceInteractionSession { + void show(in Bundle sessionArgs, int flags); + void hide(); + void handleAssist(in Bundle assistData); + void handleScreenshot(in Bitmap screenshot); void taskStarted(in Intent intent, int taskId); void taskFinished(in Intent intent, int taskId); void closeSystemDialogs(); diff --git a/core/java/android/service/voice/IVoiceInteractionSessionService.aidl b/core/java/android/service/voice/IVoiceInteractionSessionService.aidl index 2519442..7f8158f 100644 --- a/core/java/android/service/voice/IVoiceInteractionSessionService.aidl +++ b/core/java/android/service/voice/IVoiceInteractionSessionService.aidl @@ -24,5 +24,5 @@ import android.service.voice.IVoiceInteractionSession; * @hide */ oneway interface IVoiceInteractionSessionService { - void newSession(IBinder token, in Bundle args); + void newSession(IBinder token, in Bundle args, int startFlags); } diff --git a/core/java/android/service/voice/VoiceInteractionService.java b/core/java/android/service/voice/VoiceInteractionService.java index 0cde4f2..419b92b 100644 --- a/core/java/android/service/voice/VoiceInteractionService.java +++ b/core/java/android/service/voice/VoiceInteractionService.java @@ -40,15 +40,16 @@ import java.util.Locale; /** * Top-level service of the current global voice interactor, which is providing - * support for hotwording etc. + * support for hotwording, the back-end of a {@link android.app.VoiceInteractor}, etc. * The current VoiceInteractionService that has been selected by the user is kept * always running by the system, to allow it to do things like listen for hotwords - * in the background. + * in the background to instigate voice interactions. * * <p>Because this service is always running, it should be kept as lightweight as * possible. Heavy-weight operations (including showing UI) should be implemented - * in the associated {@link android.service.voice.VoiceInteractionSessionService} - * that only runs while the operation is active. + * in the associated {@link android.service.voice.VoiceInteractionSessionService} when + * an actual voice interaction is taking place, and that service should run in a + * separate process from this one. */ public class VoiceInteractionService extends Service { /** @@ -69,6 +70,18 @@ public class VoiceInteractionService extends Service { */ public static final String SERVICE_META_DATA = "android.voice_interaction"; + /** + * Flag for use with {@link #showSession}: request that the session be started with + * assist data from the currently focused activity. + */ + public static final int START_WITH_ASSIST = 1<<0; + + /** + * Flag for use with {@link #showSession}: request that the session be started with + * a screen shot of the currently focused activity. + */ + public static final int START_WITH_SCREENSHOT = 1<<1; + IVoiceInteractionService mInterface = new IVoiceInteractionService.Stub() { @Override public void ready() { mHandler.sendEmptyMessage(MSG_READY); @@ -132,19 +145,29 @@ public class VoiceInteractionService extends Service { } /** - * Initiate the execution of a new {@link android.service.voice.VoiceInteractionSession}. + * Request that the associated {@link android.service.voice.VoiceInteractionSession} be + * shown to the user, starting it if necessary. * @param args Arbitrary arguments that will be propagated to the session. */ - public void startSession(Bundle args) { + public void showSession(Bundle args, int flags) { if (mSystemService == null) { throw new IllegalStateException("Not available until onReady() is called"); } try { - mSystemService.startSession(mInterface, args); + mSystemService.showSession(mInterface, args, flags); } catch (RemoteException e) { } } + /** @hide */ + public void startSession(Bundle args, int flags) { + showSession(args, flags); + } + /** @hide */ + public void startSession(Bundle args) { + startSession(args, 0); + } + @Override public void onCreate() { super.onCreate(); @@ -162,8 +185,8 @@ public class VoiceInteractionService extends Service { /** * Called during service initialization to tell you when the system is ready * to receive interaction from it. You should generally do initialization here - * rather than in {@link #onCreate()}. Methods such as {@link #startSession(Bundle)} and - * {@link #createAlwaysOnHotwordDetector(String, Locale, android.service.voice.AlwaysOnHotwordDetector.Callback)} + * rather than in {@link #onCreate}. Methods such as {@link #showSession} and + * {@link #createAlwaysOnHotwordDetector} * will not be operational until this point. */ public void onReady() { diff --git a/core/java/android/service/voice/VoiceInteractionServiceInfo.java b/core/java/android/service/voice/VoiceInteractionServiceInfo.java index e6e9413..ebc7507 100644 --- a/core/java/android/service/voice/VoiceInteractionServiceInfo.java +++ b/core/java/android/service/voice/VoiceInteractionServiceInfo.java @@ -99,6 +99,10 @@ public class VoiceInteractionServiceInfo { mParseError = "No sessionService specified"; return; } + if (mRecognitionService == null) { + mParseError = "No recognitionService specified"; + return; + } /* Not yet time if (mRecognitionService == null) { mParseError = "No recogitionService specified"; diff --git a/core/java/android/service/voice/VoiceInteractionSession.java b/core/java/android/service/voice/VoiceInteractionSession.java index 749f813..7a5bb90 100644 --- a/core/java/android/service/voice/VoiceInteractionSession.java +++ b/core/java/android/service/voice/VoiceInteractionSession.java @@ -16,12 +16,13 @@ package android.service.voice; -import android.annotation.SystemApi; import android.app.Dialog; import android.app.Instrumentation; +import android.app.VoiceInteractor; import android.content.Context; import android.content.Intent; import android.content.res.TypedArray; +import android.graphics.Bitmap; import android.graphics.Rect; import android.graphics.Region; import android.inputmethodservice.SoftInputWindow; @@ -51,10 +52,16 @@ import com.android.internal.os.SomeArgs; import java.lang.ref.WeakReference; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; -import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; /** - * An active interaction session, started by a {@link VoiceInteractionService}. + * An active voice interaction session, providing a facility for the implementation + * to interact with the user in the voice interaction layer. The user interface is + * initially shown by default, and can be created be overriding {@link #onCreateContentView()} + * in which the UI can be built. + * + * <p>A voice interaction session can be self-contained, ultimately calling {@link #finish} + * when done. It can also initiate voice interactions with applications by calling + * {@link #startVoiceActivity}</p>. */ public abstract class VoiceInteractionSession implements KeyEvent.Callback { static final String TAG = "VoiceInteractionSession"; @@ -84,7 +91,6 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { final ArrayMap<IBinder, Request> mActiveRequests = new ArrayMap<IBinder, Request>(); final Insets mTmpInsets = new Insets(); - final int[] mTmpLocation = new int[2]; final WeakReference<VoiceInteractionSession> mWeakRef = new WeakReference<VoiceInteractionSession>(this); @@ -101,6 +107,17 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { } @Override + public IVoiceInteractorRequest startPickOption(String callingPackage, + IVoiceInteractorCallback callback, CharSequence prompt, + VoiceInteractor.PickOptionRequest.Option[] options, Bundle extras) { + Request request = newRequest(callback); + mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOOOOO(MSG_START_PICK_OPTION, + new Caller(callingPackage, Binder.getCallingUid()), request, + prompt, options, extras)); + return request.mInterface; + } + + @Override public IVoiceInteractorRequest startCompleteVoice(String callingPackage, IVoiceInteractorCallback callback, CharSequence message, Bundle extras) { Request request = newRequest(callback); @@ -146,6 +163,29 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { final IVoiceInteractionSession mSession = new IVoiceInteractionSession.Stub() { @Override + public void show(Bundle sessionArgs, int flags) { + mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIO(MSG_SHOW, + flags, sessionArgs)); + } + + @Override + public void hide() { + mHandlerCaller.sendMessage(mHandlerCaller.obtainMessage(MSG_HIDE)); + } + + @Override + public void handleAssist(Bundle assistBundle) { + mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageO(MSG_HANDLE_ASSIST, + assistBundle)); + } + + @Override + public void handleScreenshot(Bitmap screenshot) { + mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageO(MSG_HANDLE_SCREENSHOT, + screenshot)); + } + + @Override public void taskStarted(Intent intent, int taskId) { mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIO(MSG_TASK_STARTED, taskId, intent)); @@ -168,10 +208,6 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { } }; - /** - * @hide - */ - @SystemApi public static class Request { final IVoiceInteractorRequest mInterface = new IVoiceInteractorRequest.Stub() { @Override @@ -215,6 +251,20 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { } } + public void sendPickOptionResult(boolean finished, + VoiceInteractor.PickOptionRequest.Option[] selections, Bundle result) { + try { + if (DEBUG) Log.d(TAG, "sendPickOptionResult: req=" + mInterface + + " finished=" + finished + " selections=" + selections + + " result=" + result); + if (finished) { + finishRequest(); + } + mCallback.deliverPickOptionResult(mInterface, finished, selections, result); + } catch (RemoteException e) { + } + } + public void sendCompleteVoiceResult(Bundle result) { try { if (DEBUG) Log.d(TAG, "sendCompleteVoiceResult: req=" + mInterface @@ -235,12 +285,14 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { } } - public void sendCommandResult(boolean complete, Bundle result) { + public void sendCommandResult(boolean finished, Bundle result) { try { if (DEBUG) Log.d(TAG, "sendCommandResult: req=" + mInterface + " result=" + result); - finishRequest(); - mCallback.deliverCommandResult(mInterface, complete, result); + if (finished) { + finishRequest(); + } + mCallback.deliverCommandResult(mInterface, finished, result); } catch (RemoteException e) { } } @@ -255,10 +307,6 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { } } - /** - * @hide - */ - @SystemApi public static class Caller { final String packageName; final int uid; @@ -270,16 +318,21 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { } static final int MSG_START_CONFIRMATION = 1; - static final int MSG_START_COMPLETE_VOICE = 2; - static final int MSG_START_ABORT_VOICE = 3; - static final int MSG_START_COMMAND = 4; - static final int MSG_SUPPORTS_COMMANDS = 5; - static final int MSG_CANCEL = 6; + static final int MSG_START_PICK_OPTION = 2; + static final int MSG_START_COMPLETE_VOICE = 3; + static final int MSG_START_ABORT_VOICE = 4; + static final int MSG_START_COMMAND = 5; + static final int MSG_SUPPORTS_COMMANDS = 6; + static final int MSG_CANCEL = 7; static final int MSG_TASK_STARTED = 100; static final int MSG_TASK_FINISHED = 101; static final int MSG_CLOSE_SYSTEM_DIALOGS = 102; static final int MSG_DESTROY = 103; + static final int MSG_HANDLE_ASSIST = 104; + static final int MSG_HANDLE_SCREENSHOT = 105; + static final int MSG_SHOW = 106; + static final int MSG_HIDE = 107; class MyCallbacks implements HandlerCaller.Callback, SoftInputWindow.Callback { @Override @@ -293,6 +346,15 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { onConfirm((Caller)args.arg1, (Request)args.arg2, (CharSequence)args.arg3, (Bundle)args.arg4); break; + case MSG_START_PICK_OPTION: + args = (SomeArgs)msg.obj; + if (DEBUG) Log.d(TAG, "onPickOption: req=" + ((Request) args.arg2).mInterface + + " prompt=" + args.arg3 + " options=" + args.arg4 + + " extras=" + args.arg5); + onPickOption((Caller)args.arg1, (Request)args.arg2, (CharSequence)args.arg3, + (VoiceInteractor.PickOptionRequest.Option[])args.arg4, + (Bundle)args.arg5); + break; case MSG_START_COMPLETE_VOICE: args = (SomeArgs)msg.obj; if (DEBUG) Log.d(TAG, "onCompleteVoice: req=" + ((Request) args.arg2).mInterface @@ -320,9 +382,8 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { args.arg1 = onGetSupportedCommands((Caller) args.arg1, (String[]) args.arg2); break; case MSG_CANCEL: - args = (SomeArgs)msg.obj; - if (DEBUG) Log.d(TAG, "onCancel: req=" + ((Request) args.arg1).mInterface); - onCancel((Request)args.arg1); + if (DEBUG) Log.d(TAG, "onCancel: req=" + ((Request)msg.obj)); + onCancel((Request)msg.obj); break; case MSG_TASK_STARTED: if (DEBUG) Log.d(TAG, "onTaskStarted: intent=" + msg.obj @@ -342,6 +403,23 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { if (DEBUG) Log.d(TAG, "doDestroy"); doDestroy(); break; + case MSG_HANDLE_ASSIST: + if (DEBUG) Log.d(TAG, "onHandleAssist: " + msg.obj); + onHandleAssist((Bundle) msg.obj); + break; + case MSG_HANDLE_SCREENSHOT: + if (DEBUG) Log.d(TAG, "onHandleScreenshot: " + msg.obj); + onHandleScreenshot((Bitmap) msg.obj); + break; + case MSG_SHOW: + if (DEBUG) Log.d(TAG, "doShow: args=" + msg.obj + + " flags=" + msg.arg1); + doShow((Bundle) msg.obj, msg.arg1); + break; + case MSG_HIDE: + if (DEBUG) Log.d(TAG, "doHide"); + doHide(); + break; } } @@ -354,10 +432,8 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { final MyCallbacks mCallbacks = new MyCallbacks(); /** - * @hide * Information about where interesting parts of the input method UI appear. */ - @SystemApi public static final class Insets { /** * This is the part of the UI that is the main content. It is @@ -444,10 +520,50 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { } } - void doCreate(IVoiceInteractionManagerService service, IBinder token, Bundle args) { + void doCreate(IVoiceInteractionManagerService service, IBinder token, Bundle args, + int startFlags) { mSystemService = service; mToken = token; - onCreate(args); + onCreate(args, startFlags); + } + + void doShow(Bundle args, int flags) { + if (DEBUG) Log.v(TAG, "Showing window: mWindowAdded=" + mWindowAdded + + " mWindowVisible=" + mWindowVisible); + + if (mInShowWindow) { + Log.w(TAG, "Re-entrance in to showWindow"); + return; + } + + try { + mInShowWindow = true; + if (!mWindowVisible) { + if (!mWindowAdded) { + mWindowAdded = true; + View v = onCreateContentView(); + if (v != null) { + setContentView(v); + } + } + } + onShow(args, flags); + if (!mWindowVisible) { + mWindowVisible = true; + mWindow.show(); + } + } finally { + mWindowWasVisible = true; + mInShowWindow = false; + } + } + + void doHide() { + if (mWindowVisible) { + mWindow.hide(); + mWindowVisible = false; + onHide(); + } } void doDestroy() { @@ -477,57 +593,34 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { mContentFrame = (FrameLayout)mRootView.findViewById(android.R.id.content); } - /** - * @hide - */ - @SystemApi - public void showWindow() { - if (DEBUG) Log.v(TAG, "Showing window: mWindowAdded=" + mWindowAdded - + " mWindowVisible=" + mWindowVisible); - - if (mInShowWindow) { - Log.w(TAG, "Re-entrance in to showWindow"); - return; + public void show() { + try { + mSystemService.showSessionFromSession(mToken, null, 0); + } catch (RemoteException e) { } + } + public void hide() { try { - mInShowWindow = true; - if (!mWindowVisible) { - mWindowVisible = true; - if (!mWindowAdded) { - mWindowAdded = true; - View v = onCreateContentView(); - if (v != null) { - setContentView(v); - } - } - mWindow.show(); - } - } finally { - mWindowWasVisible = true; - mInShowWindow = false; + mSystemService.hideSessionFromSession(mToken); + } catch (RemoteException e) { } } - /** - * @hide - */ - @SystemApi + /** TODO: remove */ + public void showWindow() { + } + + /** TODO: remove */ public void hideWindow() { - if (mWindowVisible) { - mWindow.hide(); - mWindowVisible = false; - } } /** - * @hide * You can call this to customize the theme used by your IME's window. * This must be set before {@link #onCreate}, so you * will typically call it in your constructor with the resource ID * of your custom theme. */ - @SystemApi public void setTheme(int theme) { if (mWindow != null) { throw new IllegalStateException("Must be called before onCreate()"); @@ -536,7 +629,6 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { } /** - * @hide * Ask that a new activity be started for voice interaction. This will create a * new dedicated task in the activity manager for this voice interaction session; * this means that {@link Intent#FLAG_ACTIVITY_NEW_TASK Intent.FLAG_ACTIVITY_NEW_TASK} @@ -557,7 +649,6 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { * always have {@link Intent#CATEGORY_VOICE Intent.CATEGORY_VOICE} added to it, since * this is part of a voice interaction. */ - @SystemApi public void startVoiceActivity(Intent intent) { if (mToken == null) { throw new IllegalStateException("Can't call before onCreate()"); @@ -573,19 +664,35 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { } /** - * @hide + * Set whether this session will keep the device awake while it is running a voice + * activity. By default, the system holds a wake lock for it while in this state, + * so that it can work even if the screen is off. Setting this to false removes that + * wake lock, allowing the CPU to go to sleep. This is typically used if the + * session decides it has been waiting too long for a response from the user and + * doesn't want to let this continue to drain the battery. + * + * <p>Passing false here will release the wake lock, and you can call later with + * true to re-acquire it. It will also be automatically re-acquired for you each + * time you start a new voice activity task -- that is when you call + * {@link #startVoiceActivity}.</p> + */ + public void setKeepAwake(boolean keepAwake) { + try { + mSystemService.setKeepAwake(mToken, keepAwake); + } catch (RemoteException e) { + } + } + + /** * Convenience for inflating views. */ - @SystemApi public LayoutInflater getLayoutInflater() { return mInflater; } /** - * @hide * Retrieve the window being used to show the session's UI. */ - @SystemApi public Dialog getWindow() { return mWindow; } @@ -604,12 +711,7 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { } } - /** - * Initiatize a new session. - * - * @param args The arguments that were supplied to - * {@link VoiceInteractionService#startSession VoiceInteractionService.startSession}. - */ + /** @hide */ public void onCreate(Bundle args) { mTheme = mTheme != 0 ? mTheme : com.android.internal.R.style.Theme_DeviceDefault_VoiceInteractionSession; @@ -617,24 +719,52 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { Context.LAYOUT_INFLATER_SERVICE); mWindow = new SoftInputWindow(mContext, "VoiceInteractionSession", mTheme, mCallbacks, this, mDispatcherState, - WindowManager.LayoutParams.TYPE_VOICE_INTERACTION, Gravity.TOP, true); + WindowManager.LayoutParams.TYPE_VOICE_INTERACTION, Gravity.BOTTOM, true); mWindow.getWindow().addFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); initViews(); - mWindow.getWindow().setLayout(MATCH_PARENT, WRAP_CONTENT); + mWindow.getWindow().setLayout(MATCH_PARENT, MATCH_PARENT); mWindow.setToken(mToken); } /** + * Initiatize a new session. The given args and showFlags are the initial values + * passed to {@link VoiceInteractionService#showSession VoiceInteractionService.showSession}, + * if possible. Normally you should handle these in {@link #onShow}. + */ + public void onCreate(Bundle args, int showFlags) { + onCreate(args); + } + + /** + * Called when the session UI is going to be shown. This is called after + * {@link #onCreateContentView} (if the session's content UI needed to be created) and + * immediately prior to the window being shown. This may be called while the window + * is already shown, if a show request has come in while it is shown, to allow you to + * update the UI to match the new show arguments. + * + * @param args The arguments that were supplied to + * {@link VoiceInteractionService#showSession VoiceInteractionService.showSession}. + * @param showFlags The show flags originally provided to + * {@link VoiceInteractionService#showSession VoiceInteractionService.showSession}. + */ + public void onShow(Bundle args, int showFlags) { + } + + /** + * Called immediately after stopping to show the session UI. + */ + public void onHide() { + } + + /** * Last callback to the session as it is being finished. */ public void onDestroy() { } /** - * @hide * Hook in which to create the session's UI. */ - @SystemApi public View onCreateContentView() { return null; } @@ -643,48 +773,34 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { mContentFrame.removeAllViews(); mContentFrame.addView(view, new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT)); + ViewGroup.LayoutParams.MATCH_PARENT)); } - /** - * @hide - */ - @SystemApi + public void onHandleAssist(Bundle assistBundle) { + } + + public void onHandleScreenshot(Bitmap screenshot) { + } + public boolean onKeyDown(int keyCode, KeyEvent event) { return false; } - /** - * @hide - */ - @SystemApi public boolean onKeyLongPress(int keyCode, KeyEvent event) { return false; } - /** - * @hide - */ - @SystemApi public boolean onKeyUp(int keyCode, KeyEvent event) { return false; } - /** - * @hide - */ - @SystemApi public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) { return false; } - /** - * @hide - */ - @SystemApi public void onBackPressed() { - finish(); + hide(); } /** @@ -693,33 +809,29 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { * calls {@link #finish}. */ public void onCloseSystemDialogs() { - finish(); + hide(); } /** - * @hide * Compute the interesting insets into your UI. The default implementation - * uses the entire window frame as the insets. The default touchable - * insets are {@link Insets#TOUCHABLE_INSETS_FRAME}. + * sets {@link Insets#contentInsets outInsets.contentInsets.top} to the height + * of the window, meaning it should not adjust content underneath. The default touchable + * insets are {@link Insets#TOUCHABLE_INSETS_FRAME}, meaning it consumes all touch + * events within its window frame. * * @param outInsets Fill in with the current UI insets. */ - @SystemApi public void onComputeInsets(Insets outInsets) { - int[] loc = mTmpLocation; - View decor = getWindow().getWindow().getDecorView(); - decor.getLocationInWindow(loc); - outInsets.contentInsets.top = 0; outInsets.contentInsets.left = 0; - outInsets.contentInsets.right = 0; outInsets.contentInsets.bottom = 0; + outInsets.contentInsets.right = 0; + View decor = getWindow().getWindow().getDecorView(); + outInsets.contentInsets.top = decor.getHeight(); outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_FRAME; outInsets.touchableRegion.setEmpty(); } /** - * @hide - * @SystemApi * Called when a task initiated by {@link #startVoiceActivity(android.content.Intent)} * has actually started. * @@ -731,8 +843,6 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { } /** - * @hide - * @SystemApi * Called when the last activity of a task initiated by * {@link #startVoiceActivity(android.content.Intent)} has finished. The default * implementation calls {@link #finish()} on the assumption that this represents @@ -744,12 +854,10 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { * @param taskId Unique ID of the finished task. */ public void onTaskFinished(Intent intent, int taskId) { - finish(); + hide(); } /** - * @hide - * @SystemApi * Request to query for what extended commands the session supports. * * @param caller Who is making the request. @@ -764,8 +872,6 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { } /** - * @hide - * @SystemApi * Request to confirm with the user before proceeding with an unrecoverable operation, * corresponding to a {@link android.app.VoiceInteractor.ConfirmationRequest * VoiceInteractor.ConfirmationRequest}. @@ -781,8 +887,22 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { Bundle extras); /** - * @hide - * @SystemApi + * Request for the user to pick one of N options, corresponding to a + * {@link android.app.VoiceInteractor.PickOptionRequest VoiceInteractor.PickOptionRequest}. + * + * @param caller Who is making the request. + * @param request The active request. + * @param prompt The prompt informing the user of what they are picking, as per + * {@link android.app.VoiceInteractor.PickOptionRequest VoiceInteractor.PickOptionRequest}. + * @param options The set of options the user is picking from, as per + * {@link android.app.VoiceInteractor.PickOptionRequest VoiceInteractor.PickOptionRequest}. + * @param extras Any additional information, as per + * {@link android.app.VoiceInteractor.PickOptionRequest VoiceInteractor.PickOptionRequest}. + */ + public abstract void onPickOption(Caller caller, Request request, CharSequence prompt, + VoiceInteractor.PickOptionRequest.Option[] options, Bundle extras); + + /** * Request to complete the voice interaction session because the voice activity successfully * completed its interaction using voice. Corresponds to * {@link android.app.VoiceInteractor.CompleteVoiceRequest @@ -804,8 +924,6 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { } /** - * @hide - * @SystemApi * Request to abort the voice interaction session because the voice activity can not * complete its interaction using voice. Corresponds to * {@link android.app.VoiceInteractor.AbortVoiceRequest @@ -824,8 +942,6 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { } /** - * @hide - * @SystemApi * Process an arbitrary extended command from the caller, * corresponding to a {@link android.app.VoiceInteractor.CommandRequest * VoiceInteractor.CommandRequest}. @@ -840,8 +956,6 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { public abstract void onCommand(Caller caller, Request request, String command, Bundle extras); /** - * @hide - * @SystemApi * Called when the {@link android.app.VoiceInteractor} has asked to cancel a {@link Request} * that was previously delivered to {@link #onConfirm} or {@link #onCommand}. * diff --git a/core/java/android/service/voice/VoiceInteractionSessionService.java b/core/java/android/service/voice/VoiceInteractionSessionService.java index e793849..008d55f 100644 --- a/core/java/android/service/voice/VoiceInteractionSessionService.java +++ b/core/java/android/service/voice/VoiceInteractionSessionService.java @@ -40,9 +40,9 @@ public abstract class VoiceInteractionSessionService extends Service { VoiceInteractionSession mSession; IVoiceInteractionSessionService mInterface = new IVoiceInteractionSessionService.Stub() { - public void newSession(IBinder token, Bundle args) { - mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOO(MSG_NEW_SESSION, - token, args)); + public void newSession(IBinder token, Bundle args, int startFlags) { + mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIOO(MSG_NEW_SESSION, + startFlags, token, args)); } }; @@ -54,7 +54,7 @@ public abstract class VoiceInteractionSessionService extends Service { SomeArgs args = (SomeArgs)msg.obj; switch (msg.what) { case MSG_NEW_SESSION: - doNewSession((IBinder)args.arg1, (Bundle)args.arg2); + doNewSession((IBinder)args.arg1, (Bundle)args.arg2, args.argi1); break; } } @@ -76,7 +76,7 @@ public abstract class VoiceInteractionSessionService extends Service { return mInterface.asBinder(); } - void doNewSession(IBinder token, Bundle args) { + void doNewSession(IBinder token, Bundle args, int startFlags) { if (mSession != null) { mSession.doDestroy(); mSession = null; @@ -84,7 +84,7 @@ public abstract class VoiceInteractionSessionService extends Service { mSession = onNewSession(args); try { mSystemService.deliverNewSession(token, mSession.mSession, mSession.mInteractor); - mSession.doCreate(mSystemService, token, args); + mSession.doCreate(mSystemService, token, args, startFlags); } catch (RemoteException e) { } } diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 9496b53..4902a71 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -64,8 +64,6 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; -import static android.view.WindowManager.LayoutParams.FLAG_FULLSCREEN; - /** * A wallpaper service is responsible for showing a live wallpaper behind * applications that would like to sit on top of it. This service object diff --git a/core/java/android/speech/tts/AudioPlaybackQueueItem.java b/core/java/android/speech/tts/AudioPlaybackQueueItem.java index b4ac429..d4fea53 100644 --- a/core/java/android/speech/tts/AudioPlaybackQueueItem.java +++ b/core/java/android/speech/tts/AudioPlaybackQueueItem.java @@ -17,7 +17,6 @@ package android.speech.tts; import android.content.Context; import android.media.AudioSystem; -import android.media.AudioTrack; import android.media.MediaPlayer; import android.net.Uri; import android.os.ConditionVariable; diff --git a/core/java/android/speech/tts/BlockingAudioTrack.java b/core/java/android/speech/tts/BlockingAudioTrack.java index dc4e9d3..9920ea1 100644 --- a/core/java/android/speech/tts/BlockingAudioTrack.java +++ b/core/java/android/speech/tts/BlockingAudioTrack.java @@ -2,7 +2,6 @@ package android.speech.tts; -import android.media.AudioAttributes; import android.media.AudioFormat; import android.media.AudioTrack; import android.speech.tts.TextToSpeechService.AudioOutputParams; diff --git a/core/java/android/speech/tts/ITextToSpeechCallback.aidl b/core/java/android/speech/tts/ITextToSpeechCallback.aidl index 899515f..d785c3f 100644 --- a/core/java/android/speech/tts/ITextToSpeechCallback.aidl +++ b/core/java/android/speech/tts/ITextToSpeechCallback.aidl @@ -40,7 +40,7 @@ oneway interface ITextToSpeechCallback { * * @param utteranceId Unique id identifying synthesis request. */ - void onStop(String utteranceId); + void onStop(String utteranceId, boolean isStarted); /** * Tells the client that the synthesis has failed. diff --git a/core/java/android/speech/tts/TextToSpeech.java b/core/java/android/speech/tts/TextToSpeech.java index c59ca8a..13fb657 100644 --- a/core/java/android/speech/tts/TextToSpeech.java +++ b/core/java/android/speech/tts/TextToSpeech.java @@ -15,6 +15,7 @@ */ package android.speech.tts; +import android.annotation.RawRes; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.content.ComponentName; @@ -37,7 +38,6 @@ import android.util.Log; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; -import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -884,7 +884,7 @@ public class TextToSpeech { * * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. */ - public int addSpeech(String text, String packagename, int resourceId) { + public int addSpeech(String text, String packagename, @RawRes int resourceId) { synchronized (mStartLock) { mUtterances.put(text, makeResourceUri(packagename, resourceId)); return SUCCESS; @@ -918,7 +918,7 @@ public class TextToSpeech { * * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. */ - public int addSpeech(CharSequence text, String packagename, int resourceId) { + public int addSpeech(CharSequence text, String packagename, @RawRes int resourceId) { synchronized (mStartLock) { mUtterances.put(text, makeResourceUri(packagename, resourceId)); return SUCCESS; @@ -993,7 +993,7 @@ public class TextToSpeech { * * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. */ - public int addEarcon(String earcon, String packagename, int resourceId) { + public int addEarcon(String earcon, String packagename, @RawRes int resourceId) { synchronized(mStartLock) { mEarcons.put(earcon, makeResourceUri(packagename, resourceId)); return SUCCESS; @@ -2066,10 +2066,10 @@ public class TextToSpeech { private boolean mEstablished; private final ITextToSpeechCallback.Stub mCallback = new ITextToSpeechCallback.Stub() { - public void onStop(String utteranceId) throws RemoteException { + public void onStop(String utteranceId, boolean isStarted) throws RemoteException { UtteranceProgressListener listener = mUtteranceProgressListener; if (listener != null) { - listener.onDone(utteranceId); + listener.onStop(utteranceId, isStarted); } }; diff --git a/core/java/android/speech/tts/TextToSpeechService.java b/core/java/android/speech/tts/TextToSpeechService.java index 9bb7f02..ba98f27 100644 --- a/core/java/android/speech/tts/TextToSpeechService.java +++ b/core/java/android/speech/tts/TextToSpeechService.java @@ -39,11 +39,9 @@ import android.util.Log; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.MissingResourceException; import java.util.Set; @@ -455,10 +453,37 @@ public abstract class TextToSpeechService extends Service { private class SynthHandler extends Handler { private SpeechItem mCurrentSpeechItem = null; + private ArrayList<Object> mFlushedObjects = new ArrayList<Object>(); + private boolean mFlushAll; + public SynthHandler(Looper looper) { super(looper); } + private void startFlushingSpeechItems(Object callerIdentity) { + synchronized (mFlushedObjects) { + if (callerIdentity == null) { + mFlushAll = true; + } else { + mFlushedObjects.add(callerIdentity); + } + } + } + private void endFlushingSpeechItems(Object callerIdentity) { + synchronized (mFlushedObjects) { + if (callerIdentity == null) { + mFlushAll = false; + } else { + mFlushedObjects.remove(callerIdentity); + } + } + } + private boolean isFlushed(SpeechItem speechItem) { + synchronized (mFlushedObjects) { + return mFlushAll || mFlushedObjects.contains(speechItem.getCallerIdentity()); + } + } + private synchronized SpeechItem getCurrentSpeechItem() { return mCurrentSpeechItem; } @@ -522,9 +547,13 @@ public abstract class TextToSpeechService extends Service { Runnable runnable = new Runnable() { @Override public void run() { - setCurrentSpeechItem(speechItem); - speechItem.play(); - setCurrentSpeechItem(null); + if (isFlushed(speechItem)) { + speechItem.stop(); + } else { + setCurrentSpeechItem(speechItem); + speechItem.play(); + setCurrentSpeechItem(null); + } } }; Message msg = Message.obtain(this, runnable); @@ -552,12 +581,14 @@ public abstract class TextToSpeechService extends Service { * * Called on a service binder thread. */ - public int stopForApp(Object callerIdentity) { + public int stopForApp(final Object callerIdentity) { if (callerIdentity == null) { return TextToSpeech.ERROR; } - removeCallbacksAndMessages(callerIdentity); + // Flush pending messages from callerIdentity + startFlushingSpeechItems(callerIdentity); + // This stops writing data to the file / or publishing // items to the audio playback handler. // @@ -573,20 +604,39 @@ public abstract class TextToSpeechService extends Service { // Remove any enqueued audio too. mAudioPlaybackHandler.stopForApp(callerIdentity); + // Stop flushing pending messages + Runnable runnable = new Runnable() { + @Override + public void run() { + endFlushingSpeechItems(callerIdentity); + } + }; + sendMessage(Message.obtain(this, runnable)); return TextToSpeech.SUCCESS; } public int stopAll() { + // Order to flush pending messages + startFlushingSpeechItems(null); + // Stop the current speech item unconditionally . SpeechItem current = setCurrentSpeechItem(null); if (current != null) { current.stop(); } - // Remove all other items from the queue. - removeCallbacksAndMessages(null); // Remove all pending playback as well. mAudioPlaybackHandler.stop(); + // Message to stop flushing pending messages + Runnable runnable = new Runnable() { + @Override + public void run() { + endFlushingSpeechItems(null); + } + }; + sendMessage(Message.obtain(this, runnable)); + + return TextToSpeech.SUCCESS; } } @@ -698,7 +748,6 @@ public abstract class TextToSpeechService extends Service { return mCallerIdentity; } - public int getCallerUid() { return mCallerUid; } @@ -752,6 +801,10 @@ public abstract class TextToSpeechService extends Service { protected synchronized boolean isStopped() { return mStopped; } + + protected synchronized boolean isStarted() { + return mStarted; + } } /** @@ -777,7 +830,7 @@ public abstract class TextToSpeechService extends Service { public void dispatchOnStop() { final String utteranceId = getUtteranceId(); if (utteranceId != null) { - mCallbacks.dispatchOnStop(getCallerIdentity(), utteranceId); + mCallbacks.dispatchOnStop(getCallerIdentity(), utteranceId, isStarted()); } } @@ -940,6 +993,8 @@ public abstract class TextToSpeechService extends Service { // turn implies that synthesis would not have started. synthesisCallback.stop(); TextToSpeechService.this.onStop(); + } else { + dispatchOnStop(); } } @@ -1345,11 +1400,11 @@ public abstract class TextToSpeechService extends Service { } } - public void dispatchOnStop(Object callerIdentity, String utteranceId) { + public void dispatchOnStop(Object callerIdentity, String utteranceId, boolean started) { ITextToSpeechCallback cb = getCallbackFor(callerIdentity); if (cb == null) return; try { - cb.onStop(utteranceId); + cb.onStop(utteranceId, started); } catch (RemoteException e) { Log.e(TAG, "Callback onStop failed: " + e); } diff --git a/core/java/android/speech/tts/TtsEngines.java b/core/java/android/speech/tts/TtsEngines.java index df6c010..412eba3 100644 --- a/core/java/android/speech/tts/TtsEngines.java +++ b/core/java/android/speech/tts/TtsEngines.java @@ -17,7 +17,6 @@ package android.speech.tts; import org.xmlpull.v1.XmlPullParserException; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; diff --git a/core/java/android/speech/tts/UtteranceProgressListener.java b/core/java/android/speech/tts/UtteranceProgressListener.java index 6769794..9eb22ef 100644 --- a/core/java/android/speech/tts/UtteranceProgressListener.java +++ b/core/java/android/speech/tts/UtteranceProgressListener.java @@ -60,6 +60,20 @@ public abstract class UtteranceProgressListener { } /** + * Called when an utterance has been stopped while in progress or flushed from the + * synthesis queue. This can happen if client calls {@link TextToSpeech#stop()} + * or use {@link TextToSpeech#QUEUE_FLUSH} as an argument in + * {@link TextToSpeech#speak} or {@link TextToSpeech#synthesizeToFile} methods. + * + * @param utteranceId the utterance ID of the utterance. + * @param isStarted If true, then utterance was interrupted while being synthesized + * and it's output is incomplete. If it's false, then utterance was flushed + * before the synthesis started. + */ + public void onStop(String utteranceId, boolean isStarted) { + } + + /** * Wraps an old deprecated OnUtteranceCompletedListener with a shiny new * progress listener. * @@ -83,6 +97,11 @@ public abstract class UtteranceProgressListener { // Left unimplemented, has no equivalent in the old // API. } + + @Override + public void onStop(String utteranceId, boolean isStarted) { + listener.onUtteranceCompleted(utteranceId); + } }; } } diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java index 77ef1da..1bdaef0 100644 --- a/core/java/android/text/DynamicLayout.java +++ b/core/java/android/text/DynamicLayout.java @@ -270,22 +270,30 @@ public class DynamicLayout extends Layout // generate new layout for affected text StaticLayout reflowed; + StaticLayout.Builder b; synchronized (sLock) { reflowed = sStaticLayout; + b = sBuilder; sStaticLayout = null; + sBuilder = null; } + // TODO: make sure reflowed is properly initialized if (reflowed == null) { reflowed = new StaticLayout(null); - } else { - reflowed.prepare(); - } - - reflowed.generate(text, where, where + after, - getPaint(), getWidth(), getTextDirectionHeuristic(), getSpacingMultiplier(), - getSpacingAdd(), false, - true, mEllipsizedWidth, mEllipsizeAt); + b = StaticLayout.Builder.obtain(); + } + + b.setText(text, where, where + after) + .setPaint(getPaint()) + .setWidth(getWidth()) + .setTextDir(getTextDirectionHeuristic()) + .setSpacingMult(getSpacingMultiplier()) + .setSpacingAdd(getSpacingAdd()) + .setEllipsizedWidth(mEllipsizedWidth) + .setEllipsize(mEllipsizeAt); + reflowed.generate(b, false, true); int n = reflowed.getLineCount(); // If the new layout has a blank line at the end, but it is not @@ -359,9 +367,10 @@ public class DynamicLayout extends Layout updateBlocks(startline, endline - 1, n); + b.finish(); synchronized (sLock) { sStaticLayout = reflowed; - reflowed.finish(); + sBuilder = b; } } @@ -720,7 +729,8 @@ public class DynamicLayout extends Layout private int mTopPadding, mBottomPadding; - private static StaticLayout sStaticLayout = new StaticLayout(null); + private static StaticLayout sStaticLayout = null; + private static StaticLayout.Builder sBuilder = null; private static final Object[] sLock = new Object[0]; diff --git a/core/java/android/text/Html.java b/core/java/android/text/Html.java index dc93bc2..8cf1b4b 100644 --- a/core/java/android/text/Html.java +++ b/core/java/android/text/Html.java @@ -672,7 +672,7 @@ class HtmlToSpannedConverter implements ContentHandler { String name = f.mColor.substring(1); int colorRes = res.getIdentifier(name, "color", "android"); if (colorRes != 0) { - ColorStateList colors = res.getColorStateList(colorRes); + ColorStateList colors = res.getColorStateList(colorRes, null); text.setSpan(new TextAppearanceSpan(null, 0, 0, colors, null), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index b84c3aa..fcf1828 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -1564,7 +1564,7 @@ public abstract class Layout { MeasuredText mt = MeasuredText.obtain(); TextLine tl = TextLine.obtain(); try { - mt.setPara(text, start, end, TextDirectionHeuristics.LTR); + mt.setPara(text, start, end, TextDirectionHeuristics.LTR, null); Directions directions; int dir; if (mt.mEasy) { diff --git a/core/java/android/text/MeasuredText.java b/core/java/android/text/MeasuredText.java index 2415b11..55df206 100644 --- a/core/java/android/text/MeasuredText.java +++ b/core/java/android/text/MeasuredText.java @@ -16,7 +16,6 @@ package android.text; -import android.graphics.Canvas; import android.graphics.Paint; import android.text.style.MetricAffectingSpan; import android.text.style.ReplacementSpan; @@ -40,6 +39,7 @@ class MeasuredText { private int mPos; private TextPaint mWorkPaint; + private StaticLayout.Builder mBuilder; private MeasuredText() { mWorkPaint = new TextPaint(); @@ -67,21 +67,29 @@ class MeasuredText { } static MeasuredText recycle(MeasuredText mt) { - mt.mText = null; - if (mt.mLen < 1000) { - synchronized(sLock) { - for (int i = 0; i < sCached.length; ++i) { - if (sCached[i] == null) { - sCached[i] = mt; - mt.mText = null; - break; - } + mt.finish(); + synchronized(sLock) { + for (int i = 0; i < sCached.length; ++i) { + if (sCached[i] == null) { + sCached[i] = mt; + mt.mText = null; + break; } } } return null; } + void finish() { + mText = null; + mBuilder = null; + if (mLen > 1000) { + mWidths = null; + mChars = null; + mLevels = null; + } + } + void setPos(int pos) { mPos = pos - mTextStart; } @@ -89,7 +97,9 @@ class MeasuredText { /** * Analyzes text for bidirectional runs. Allocates working buffers. */ - void setPara(CharSequence text, int start, int end, TextDirectionHeuristic textDir) { + void setPara(CharSequence text, int start, int end, TextDirectionHeuristic textDir, + StaticLayout.Builder builder) { + mBuilder = builder; mText = text; mTextStart = start; @@ -158,9 +168,24 @@ class MeasuredText { int p = mPos; mPos = p + len; + // try to do widths measurement in native code, but use Java if paint has been subclassed + // FIXME: may want to eliminate special case for subclass + float[] widths = null; + if (mBuilder == null || paint.getClass() != TextPaint.class) { + widths = mWidths; + } if (mEasy) { boolean isRtl = mDir != Layout.DIR_LEFT_TO_RIGHT; - return paint.getTextRunAdvances(mChars, p, len, p, len, isRtl, mWidths, p); + float width = 0; + if (widths != null) { + width = paint.getTextRunAdvances(mChars, p, len, p, len, isRtl, widths, p); + if (mBuilder != null) { + mBuilder.addMeasuredRun(p, p + len, widths); + } + } else { + width = mBuilder.addStyleRun(paint, p, p + len, isRtl); + } + return width; } float totalAdvance = 0; @@ -168,8 +193,15 @@ class MeasuredText { for (int q = p, i = p + 1, e = p + len;; ++i) { if (i == e || mLevels[i] != level) { boolean isRtl = (level & 0x1) != 0; - totalAdvance += - paint.getTextRunAdvances(mChars, q, i - q, q, i - q, isRtl, mWidths, q); + if (widths != null) { + totalAdvance += + paint.getTextRunAdvances(mChars, q, i - q, q, i - q, isRtl, widths, q); + if (mBuilder != null) { + mBuilder.addMeasuredRun(q, i, widths); + } + } else { + totalAdvance += mBuilder.addStyleRun(paint, q, i, isRtl); + } if (i == e) { break; } @@ -205,10 +237,14 @@ class MeasuredText { // Use original text. Shouldn't matter. wid = replacement.getSize(workPaint, mText, mTextStart + mPos, mTextStart + mPos + len, fm); - float[] w = mWidths; - w[mPos] = wid; - for (int i = mPos + 1, e = mPos + len; i < e; i++) - w[i] = 0; + if (mBuilder == null) { + float[] w = mWidths; + w[mPos] = wid; + for (int i = mPos + 1, e = mPos + len; i < e; i++) + w[i] = 0; + } else { + mBuilder.addReplacementRun(mPos, mPos + len, wid); + } mPos += len; } diff --git a/core/java/android/text/SpannableStringBuilder.java b/core/java/android/text/SpannableStringBuilder.java index 06df683..992dc4d 100644 --- a/core/java/android/text/SpannableStringBuilder.java +++ b/core/java/android/text/SpannableStringBuilder.java @@ -26,6 +26,7 @@ import com.android.internal.util.GrowingArrayUtils; import libcore.util.EmptyArray; import java.lang.reflect.Array; +import java.util.IdentityHashMap; /** * This is the class for text whose content and markup can both be changed. @@ -68,6 +69,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable mSpanStarts = EmptyArray.INT; mSpanEnds = EmptyArray.INT; mSpanFlags = EmptyArray.INT; + mSpanMax = EmptyArray.INT; if (text instanceof Spanned) { Spanned sp = (Spanned) text; @@ -94,6 +96,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable setSpan(false, spans[i], st, en, fl); } + restoreInvariants(); } } @@ -147,9 +150,12 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable if (mGapLength < 1) new Exception("mGapLength < 1").printStackTrace(); - for (int i = 0; i < mSpanCount; i++) { - if (mSpanStarts[i] > mGapStart) mSpanStarts[i] += delta; - if (mSpanEnds[i] > mGapStart) mSpanEnds[i] += delta; + if (mSpanCount != 0) { + for (int i = 0; i < mSpanCount; i++) { + if (mSpanStarts[i] > mGapStart) mSpanStarts[i] += delta; + if (mSpanEnds[i] > mGapStart) mSpanEnds[i] += delta; + } + calcMax(treeRoot()); } } @@ -167,35 +173,38 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable System.arraycopy(mText, where + mGapLength - overlap, mText, mGapStart, overlap); } - // XXX be more clever - for (int i = 0; i < mSpanCount; i++) { - int start = mSpanStarts[i]; - int end = mSpanEnds[i]; - - if (start > mGapStart) - start -= mGapLength; - if (start > where) - start += mGapLength; - else if (start == where) { - int flag = (mSpanFlags[i] & START_MASK) >> START_SHIFT; + // TODO: be more clever (although the win really isn't that big) + if (mSpanCount != 0) { + for (int i = 0; i < mSpanCount; i++) { + int start = mSpanStarts[i]; + int end = mSpanEnds[i]; - if (flag == POINT || (atEnd && flag == PARAGRAPH)) + if (start > mGapStart) + start -= mGapLength; + if (start > where) start += mGapLength; - } + else if (start == where) { + int flag = (mSpanFlags[i] & START_MASK) >> START_SHIFT; - if (end > mGapStart) - end -= mGapLength; - if (end > where) - end += mGapLength; - else if (end == where) { - int flag = (mSpanFlags[i] & END_MASK); + if (flag == POINT || (atEnd && flag == PARAGRAPH)) + start += mGapLength; + } - if (flag == POINT || (atEnd && flag == PARAGRAPH)) + if (end > mGapStart) + end -= mGapLength; + if (end > where) end += mGapLength; - } + else if (end == where) { + int flag = (mSpanFlags[i] & END_MASK); + + if (flag == POINT || (atEnd && flag == PARAGRAPH)) + end += mGapLength; + } - mSpanStarts[i] = start; - mSpanEnds[i] = end; + mSpanStarts[i] = start; + mSpanEnds[i] = end; + } + calcMax(treeRoot()); } mGapStart = where; @@ -243,6 +252,9 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable sendSpanRemoved(what, ostart, oend); } + if (mIndexOfSpan != null) { + mIndexOfSpan.clear(); + } } // Documentation from interface @@ -277,12 +289,39 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable return append(String.valueOf(text)); } + // Returns true if a node was removed (so we can restart search from root) + private boolean removeSpansForChange(int start, int end, boolean textIsRemoved, int i) { + if ((i & 1) != 0) { + // internal tree node + if (resolveGap(mSpanMax[i]) >= start && + removeSpansForChange(start, end, textIsRemoved, leftChild(i))) { + return true; + } + } + if (i < mSpanCount) { + if ((mSpanFlags[i] & Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) == + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE && + mSpanStarts[i] >= start && mSpanStarts[i] < mGapStart + mGapLength && + mSpanEnds[i] >= start && mSpanEnds[i] < mGapStart + mGapLength && + // The following condition indicates that the span would become empty + (textIsRemoved || mSpanStarts[i] > start || mSpanEnds[i] < mGapStart)) { + mIndexOfSpan.remove(mSpans[i]); + removeSpan(i); + return true; + } + return resolveGap(mSpanStarts[i]) <= end && (i & 1) != 0 && + removeSpansForChange(start, end, textIsRemoved, rightChild(i)); + } + return false; + } + private void change(int start, int end, CharSequence cs, int csStart, int csEnd) { // Can be negative final int replacedLength = end - start; final int replacementLength = csEnd - csStart; final int nbNewChars = replacementLength - replacedLength; + boolean changed = false; for (int i = mSpanCount - 1; i >= 0; i--) { int spanStart = mSpanStarts[i]; if (spanStart > mGapStart) @@ -309,8 +348,10 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable break; } - if (spanStart != ost || spanEnd != oen) + if (spanStart != ost || spanEnd != oen) { setSpan(false, mSpans[i], spanStart, spanEnd, mSpanFlags[i]); + changed = true; + } } int flags = 0; @@ -320,6 +361,9 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable else if (spanEnd == end + nbNewChars) flags |= SPAN_END_AT_END; mSpanFlags[i] |= flags; } + if (changed) { + restoreInvariants(); + } moveGapTo(end); @@ -331,23 +375,10 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable // The removal pass needs to be done before the gap is updated in order to broadcast the // correct previous positions to the correct intersecting SpanWatchers if (replacedLength > 0) { // no need for span fixup on pure insertion - // A for loop will not work because the array is being modified - // Do not iterate in reverse to keep the SpanWatchers notified in ordering - // Also, a removed SpanWatcher should not get notified of removed spans located - // further in the span array. - int i = 0; - while (i < mSpanCount) { - if ((mSpanFlags[i] & Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) == - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE && - mSpanStarts[i] >= start && mSpanStarts[i] < mGapStart + mGapLength && - mSpanEnds[i] >= start && mSpanEnds[i] < mGapStart + mGapLength && - // This condition indicates that the span would become empty - (textIsRemoved || mSpanStarts[i] > start || mSpanEnds[i] < mGapStart)) { - removeSpan(i); - continue; // do not increment i, spans will be shifted left in the array - } - - i++; + while (mSpanCount > 0 && + removeSpansForChange(start, end, textIsRemoved, treeRoot())) { + // keep deleting spans as needed, and restart from root after every deletion + // because deletion can invalidate an index. } } @@ -360,6 +391,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable TextUtils.getChars(cs, csStart, csEnd, mText, start); if (replacedLength > 0) { // no need for span fixup on pure insertion + // TODO potential optimization: only update bounds on intersecting spans final boolean atEnd = (mGapStart + mGapLength == mText.length); for (int i = 0; i < mSpanCount; i++) { @@ -371,10 +403,10 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable mSpanEnds[i] = updatedIntervalBound(mSpanEnds[i], start, nbNewChars, endFlag, atEnd, textIsRemoved); } + // TODO potential optimization: only fix up invariants when bounds actually changed + restoreInvariants(); } - mSpanCountBeforeAdd = mSpanCount; - if (cs instanceof Spanned) { Spanned sp = (Spanned) cs; Object[] spans = sp.getSpans(csStart, csEnd, Object.class); @@ -389,9 +421,10 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable // Add span only if this object is not yet used as a span in this string if (getSpanStart(spans[i]) < 0) { setSpan(false, spans[i], st - csStart + start, en - csStart + start, - sp.getSpanFlags(spans[i])); + sp.getSpanFlags(spans[i]) | SPAN_ADDED); } } + restoreInvariants(); } } @@ -427,6 +460,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable return offset; } + // Note: caller is responsible for removing the mIndexOfSpan entry. private void removeSpan(int i) { Object object = mSpans[i]; @@ -444,8 +478,12 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable mSpanCount--; + invalidateIndex(i); mSpans[mSpanCount] = null; + // Invariants must be restored before sending span removed notifications. + restoreInvariants(); + sendSpanRemoved(object, start, end); } @@ -496,10 +534,12 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable change(start, end, tb, tbstart, tbend); if (adjustSelection) { + boolean changed = false; if (selectionStart > start && selectionStart < end) { final int offset = (selectionStart - start) * newLen / origLen; selectionStart = start + offset; + changed = true; setSpan(false, Selection.SELECTION_START, selectionStart, selectionStart, Spanned.SPAN_POINT_POINT); } @@ -507,9 +547,13 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable final int offset = (selectionEnd - start) * newLen / origLen; selectionEnd = start + offset; + changed = true; setSpan(false, Selection.SELECTION_END, selectionEnd, selectionEnd, Spanned.SPAN_POINT_POINT); } + if (changed) { + restoreInvariants(); + } } sendTextChanged(textWatchers, start, origLen, newLen); @@ -536,12 +580,15 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable } private void sendToSpanWatchers(int replaceStart, int replaceEnd, int nbNewChars) { - for (int i = 0; i < mSpanCountBeforeAdd; i++) { + for (int i = 0; i < mSpanCount; i++) { + int spanFlags = mSpanFlags[i]; + + // This loop handles only modified (not added) spans. + if ((spanFlags & SPAN_ADDED) != 0) continue; int spanStart = mSpanStarts[i]; int spanEnd = mSpanEnds[i]; if (spanStart > mGapStart) spanStart -= mGapLength; if (spanEnd > mGapStart) spanEnd -= mGapLength; - int spanFlags = mSpanFlags[i]; int newReplaceEnd = replaceEnd + nbNewChars; boolean spanChanged = false; @@ -588,13 +635,17 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable mSpanFlags[i] &= ~SPAN_START_END_MASK; } - // The spans starting at mIntermediateSpanCount were added from the replacement text - for (int i = mSpanCountBeforeAdd; i < mSpanCount; i++) { - int spanStart = mSpanStarts[i]; - int spanEnd = mSpanEnds[i]; - if (spanStart > mGapStart) spanStart -= mGapLength; - if (spanEnd > mGapStart) spanEnd -= mGapLength; - sendSpanAdded(mSpans[i], spanStart, spanEnd); + // Handle added spans + for (int i = 0; i < mSpanCount; i++) { + int spanFlags = mSpanFlags[i]; + if ((spanFlags & SPAN_ADDED) != 0) { + mSpanFlags[i] &= ~SPAN_ADDED; + int spanStart = mSpanStarts[i]; + int spanEnd = mSpanEnds[i]; + if (spanStart > mGapStart) spanStart -= mGapLength; + if (spanEnd > mGapStart) spanEnd -= mGapLength; + sendSpanAdded(mSpans[i], spanStart, spanEnd); + } } } @@ -607,6 +658,9 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable setSpan(true, what, start, end, flags); } + // Note: if send is false, then it is the caller's responsibility to restore + // invariants. If send is false and the span already exists, then this method + // will not change the index of any spans. private void setSpan(boolean send, Object what, int start, int end, int flags) { checkRange("setSpan", start, end); @@ -661,8 +715,10 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable int count = mSpanCount; Object[] spans = mSpans; - for (int i = 0; i < count; i++) { - if (spans[i] == what) { + if (mIndexOfSpan != null) { + Integer index = mIndexOfSpan.get(what); + if (index != null) { + int i = index; int ostart = mSpanStarts[i]; int oend = mSpanEnds[i]; @@ -675,7 +731,10 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable mSpanEnds[i] = end; mSpanFlags[i] = flags; - if (send) sendSpanChanged(what, ostart, oend, nstart, nend); + if (send) { + restoreInvariants(); + sendSpanChanged(what, ostart, oend, nstart, nend); + } return; } @@ -685,43 +744,48 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable mSpanStarts = GrowingArrayUtils.append(mSpanStarts, mSpanCount, start); mSpanEnds = GrowingArrayUtils.append(mSpanEnds, mSpanCount, end); mSpanFlags = GrowingArrayUtils.append(mSpanFlags, mSpanCount, flags); + invalidateIndex(mSpanCount); mSpanCount++; + // Make sure there is enough room for empty interior nodes. + // This magic formula computes the size of the smallest perfect binary + // tree no smaller than mSpanCount. + int sizeOfMax = 2 * treeRoot() + 1; + if (mSpanMax.length < sizeOfMax) { + mSpanMax = new int[sizeOfMax]; + } - if (send) sendSpanAdded(what, nstart, nend); + if (send) { + restoreInvariants(); + sendSpanAdded(what, nstart, nend); + } } /** * Remove the specified markup object from the buffer. */ public void removeSpan(Object what) { - for (int i = mSpanCount - 1; i >= 0; i--) { - if (mSpans[i] == what) { - removeSpan(i); - return; - } + if (mIndexOfSpan == null) return; + Integer i = mIndexOfSpan.remove(what); + if (i != null) { + removeSpan(i.intValue()); } } /** + * Return externally visible offset given offset into gapped buffer. + */ + private int resolveGap(int i) { + return i > mGapStart ? i - mGapLength : i; + } + + /** * Return the buffer offset of the beginning of the specified * markup object, or -1 if it is not attached to this buffer. */ public int getSpanStart(Object what) { - int count = mSpanCount; - Object[] spans = mSpans; - - for (int i = count - 1; i >= 0; i--) { - if (spans[i] == what) { - int where = mSpanStarts[i]; - - if (where > mGapStart) - where -= mGapLength; - - return where; - } - } - - return -1; + if (mIndexOfSpan == null) return -1; + Integer i = mIndexOfSpan.get(what); + return i == null ? -1 : resolveGap(mSpanStarts[i]); } /** @@ -729,21 +793,9 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable * markup object, or -1 if it is not attached to this buffer. */ public int getSpanEnd(Object what) { - int count = mSpanCount; - Object[] spans = mSpans; - - for (int i = count - 1; i >= 0; i--) { - if (spans[i] == what) { - int where = mSpanEnds[i]; - - if (where > mGapStart) - where -= mGapLength; - - return where; - } - } - - return -1; + if (mIndexOfSpan == null) return -1; + Integer i = mIndexOfSpan.get(what); + return i == null ? -1 : resolveGap(mSpanEnds[i]); } /** @@ -751,16 +803,9 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable * markup object, or 0 if it is not attached to this buffer. */ public int getSpanFlags(Object what) { - int count = mSpanCount; - Object[] spans = mSpans; - - for (int i = count - 1; i >= 0; i--) { - if (spans[i] == what) { - return mSpanFlags[i]; - } - } - - return 0; + if (mIndexOfSpan == null) return 0; + Integer i = mIndexOfSpan.get(what); + return i == null ? 0 : mSpanFlags[i]; } /** @@ -770,59 +815,84 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable */ @SuppressWarnings("unchecked") public <T> T[] getSpans(int queryStart, int queryEnd, Class<T> kind) { - if (kind == null) return ArrayUtils.emptyArray(kind); + if (kind == null || mSpanCount == 0) return ArrayUtils.emptyArray(kind); + int count = countSpans(queryStart, queryEnd, kind, treeRoot()); + if (count == 0) { + return ArrayUtils.emptyArray(kind); + } - int spanCount = mSpanCount; - Object[] spans = mSpans; - int[] starts = mSpanStarts; - int[] ends = mSpanEnds; - int[] flags = mSpanFlags; - int gapstart = mGapStart; - int gaplen = mGapLength; + // Safe conversion, but requires a suppressWarning + T[] ret = (T[]) Array.newInstance(kind, count); + getSpansRec(queryStart, queryEnd, kind, treeRoot(), ret, 0); + return ret; + } + private int countSpans(int queryStart, int queryEnd, Class kind, int i) { int count = 0; - T[] ret = null; - T ret1 = null; - - for (int i = 0; i < spanCount; i++) { - int spanStart = starts[i]; - if (spanStart > gapstart) { - spanStart -= gaplen; + if ((i & 1) != 0) { + // internal tree node + int left = leftChild(i); + int spanMax = mSpanMax[left]; + if (spanMax > mGapStart) { + spanMax -= mGapLength; } - if (spanStart > queryEnd) { - continue; + if (spanMax >= queryStart) { + count = countSpans(queryStart, queryEnd, kind, left); } - - int spanEnd = ends[i]; - if (spanEnd > gapstart) { - spanEnd -= gaplen; + } + if (i < mSpanCount) { + int spanStart = mSpanStarts[i]; + if (spanStart > mGapStart) { + spanStart -= mGapLength; } - if (spanEnd < queryStart) { - continue; + if (spanStart <= queryEnd) { + int spanEnd = mSpanEnds[i]; + if (spanEnd > mGapStart) { + spanEnd -= mGapLength; + } + if (spanEnd >= queryStart && + (spanStart == spanEnd || queryStart == queryEnd || + (spanStart != queryEnd && spanEnd != queryStart)) && + kind.isInstance(mSpans[i])) { + count++; + } + if ((i & 1) != 0) { + count += countSpans(queryStart, queryEnd, kind, rightChild(i)); + } } + } + return count; + } - if (spanStart != spanEnd && queryStart != queryEnd) { - if (spanStart == queryEnd) - continue; - if (spanEnd == queryStart) - continue; + @SuppressWarnings("unchecked") + private <T> int getSpansRec(int queryStart, int queryEnd, Class<T> kind, + int i, T[] ret, int count) { + if ((i & 1) != 0) { + // internal tree node + int left = leftChild(i); + int spanMax = mSpanMax[left]; + if (spanMax > mGapStart) { + spanMax -= mGapLength; } - - // Expensive test, should be performed after the previous tests - if (!kind.isInstance(spans[i])) continue; - - if (count == 0) { - // Safe conversion thanks to the isInstance test above - ret1 = (T) spans[i]; - count++; - } else { - if (count == 1) { - // Safe conversion, but requires a suppressWarning - ret = (T[]) Array.newInstance(kind, spanCount - i + 1); - ret[0] = ret1; - } - - int prio = flags[i] & SPAN_PRIORITY; + if (spanMax >= queryStart) { + count = getSpansRec(queryStart, queryEnd, kind, left, ret, count); + } + } + if (i >= mSpanCount) return count; + int spanStart = mSpanStarts[i]; + if (spanStart > mGapStart) { + spanStart -= mGapLength; + } + if (spanStart <= queryEnd) { + int spanEnd = mSpanEnds[i]; + if (spanEnd > mGapStart) { + spanEnd -= mGapLength; + } + if (spanEnd >= queryStart && + (spanStart == spanEnd || queryStart == queryEnd || + (spanStart != queryEnd && spanEnd != queryStart)) && + kind.isInstance(mSpans[i])) { + int prio = mSpanFlags[i] & SPAN_PRIORITY; if (prio != 0) { int j; @@ -836,32 +906,18 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable System.arraycopy(ret, j, ret, j + 1, count - j); // Safe conversion thanks to the isInstance test above - ret[j] = (T) spans[i]; - count++; + ret[j] = (T) mSpans[i]; } else { // Safe conversion thanks to the isInstance test above - ret[count++] = (T) spans[i]; + ret[count] = (T) mSpans[i]; } + count++; + } + if (count < ret.length && (i & 1) != 0) { + count = getSpansRec(queryStart, queryEnd, kind, rightChild(i), ret, count); } } - - if (count == 0) { - return ArrayUtils.emptyArray(kind); - } - if (count == 1) { - // Safe conversion, but requires a suppressWarning - ret = (T[]) Array.newInstance(kind, 1); - ret[0] = ret1; - return ret; - } - if (count == ret.length) { - return ret; - } - - // Safe conversion, but requires a suppressWarning - T[] nret = (T[]) Array.newInstance(kind, count); - System.arraycopy(ret, 0, nret, 0, count); - return nret; + return count; } /** @@ -870,30 +926,31 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable * begins or ends. */ public int nextSpanTransition(int start, int limit, Class kind) { - int count = mSpanCount; - Object[] spans = mSpans; - int[] starts = mSpanStarts; - int[] ends = mSpanEnds; - int gapstart = mGapStart; - int gaplen = mGapLength; - + if (mSpanCount == 0) return limit; if (kind == null) { kind = Object.class; } + return nextSpanTransitionRec(start, limit, kind, treeRoot()); + } - for (int i = 0; i < count; i++) { - int st = starts[i]; - int en = ends[i]; - - if (st > gapstart) - st -= gaplen; - if (en > gapstart) - en -= gaplen; - - if (st > start && st < limit && kind.isInstance(spans[i])) + private int nextSpanTransitionRec(int start, int limit, Class kind, int i) { + if ((i & 1) != 0) { + // internal tree node + int left = leftChild(i); + if (resolveGap(mSpanMax[left]) > start) { + limit = nextSpanTransitionRec(start, limit, kind, left); + } + } + if (i < mSpanCount) { + int st = resolveGap(mSpanStarts[i]); + int en = resolveGap(mSpanEnds[i]); + if (st > start && st < limit && kind.isInstance(mSpans[i])) limit = st; - if (en > start && en < limit && kind.isInstance(spans[i])) + if (en > start && en < limit && kind.isInstance(mSpans[i])) limit = en; + if (st < limit && (i & 1) != 0) { + limit = nextSpanTransitionRec(start, limit, kind, rightChild(i)); + } } return limit; @@ -949,28 +1006,43 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable return new String(buf); } + /** + * Returns the depth of TextWatcher callbacks. Returns 0 when the object is not handling + * TextWatchers. A return value greater than 1 implies that a TextWatcher caused a change that + * recursively triggered a TextWatcher. + */ + public int getTextWatcherDepth() { + return mTextWatcherDepth; + } + private void sendBeforeTextChanged(TextWatcher[] watchers, int start, int before, int after) { int n = watchers.length; + mTextWatcherDepth++; for (int i = 0; i < n; i++) { watchers[i].beforeTextChanged(this, start, before, after); } + mTextWatcherDepth--; } private void sendTextChanged(TextWatcher[] watchers, int start, int before, int after) { int n = watchers.length; + mTextWatcherDepth++; for (int i = 0; i < n; i++) { watchers[i].onTextChanged(this, start, before, after); } + mTextWatcherDepth--; } private void sendAfterTextChanged(TextWatcher[] watchers) { int n = watchers.length; + mTextWatcherDepth++; for (int i = 0; i < n; i++) { watchers[i].afterTextChanged(this); } + mTextWatcherDepth--; } private void sendSpanAdded(Object what, int start, int end) { @@ -1339,6 +1411,118 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable return hash; } + // Primitives for treating span list as binary tree + + // The spans (along with start and end offsets and flags) are stored in linear arrays sorted + // by start offset. For fast searching, there is a binary search structure imposed over these + // arrays. This structure is inorder traversal of a perfect binary tree, a slightly unusual + // but advantageous approach. + + // The value-containing nodes are indexed 0 <= i < n (where n = mSpanCount), thus preserving + // logic that accesses the values as a contiguous array. Other balanced binary tree approaches + // (such as a complete binary tree) would require some shuffling of node indices. + + // Basic properties of this structure: For a perfect binary tree of height m: + // The tree has 2^(m+1) - 1 total nodes. + // The root of the tree has index 2^m - 1. + // All leaf nodes have even index, all interior nodes odd. + // The height of a node of index i is the number of trailing ones in i's binary representation. + // The left child of a node i of height h is i - 2^(h - 1). + // The right child of a node i of height h is i + 2^(h - 1). + + // Note that for arbitrary n, interior nodes of this tree may be >= n. Thus, the general + // structure of a recursive traversal of node i is: + // * traverse left child if i is an interior node + // * process i if i < n + // * traverse right child if i is an interior node and i < n + + private int treeRoot() { + return Integer.highestOneBit(mSpanCount) - 1; + } + + // (i+1) & ~i is equal to 2^(the number of trailing ones in i) + private static int leftChild(int i) { + return i - (((i + 1) & ~i) >> 1); + } + + private static int rightChild(int i) { + return i + (((i + 1) & ~i) >> 1); + } + + // The span arrays are also augmented by an mSpanMax[] array that represents an interval tree + // over the binary tree structure described above. For each node, the mSpanMax[] array contains + // the maximum value of mSpanEnds of that node and its descendants. Thus, traversals can + // easily reject subtrees that contain no spans overlapping the area of interest. + + // Note that mSpanMax[] also has a valid valuefor interior nodes of index >= n, but which have + // descendants of index < n. In these cases, it simply represents the maximum span end of its + // descendants. This is a consequence of the perfect binary tree structure. + private int calcMax(int i) { + int max = 0; + if ((i & 1) != 0) { + // internal tree node + max = calcMax(leftChild(i)); + } + if (i < mSpanCount) { + max = Math.max(max, mSpanEnds[i]); + if ((i & 1) != 0) { + max = Math.max(max, calcMax(rightChild(i))); + } + } + mSpanMax[i] = max; + return max; + } + + // restores binary interval tree invariants after any mutation of span structure + private void restoreInvariants() { + if (mSpanCount == 0) return; + + // invariant 1: span starts are nondecreasing + + // This is a simple insertion sort because we expect it to be mostly sorted. + for (int i = 1; i < mSpanCount; i++) { + if (mSpanStarts[i] < mSpanStarts[i - 1]) { + Object span = mSpans[i]; + int start = mSpanStarts[i]; + int end = mSpanEnds[i]; + int flags = mSpanFlags[i]; + int j = i; + do { + mSpans[j] = mSpans[j - 1]; + mSpanStarts[j] = mSpanStarts[j - 1]; + mSpanEnds[j] = mSpanEnds[j - 1]; + mSpanFlags[j] = mSpanFlags[j - 1]; + j--; + } while (j > 0 && start < mSpanStarts[j - 1]); + mSpans[j] = span; + mSpanStarts[j] = start; + mSpanEnds[j] = end; + mSpanFlags[j] = flags; + invalidateIndex(j); + } + } + + // invariant 2: max is max span end for each node and its descendants + calcMax(treeRoot()); + + // invariant 3: mIndexOfSpan maps spans back to indices + if (mIndexOfSpan == null) { + mIndexOfSpan = new IdentityHashMap<Object, Integer>(); + } + for (int i = mLowWaterMark; i < mSpanCount; i++) { + Integer existing = mIndexOfSpan.get(mSpans[i]); + if (existing == null || existing != i) { + mIndexOfSpan.put(mSpans[i], i); + } + } + mLowWaterMark = Integer.MAX_VALUE; + } + + // Call this on any update to mSpans[], so that mIndexOfSpan can be updated + private void invalidateIndex(int i) { + mLowWaterMark = Math.min(i, mLowWaterMark); + } + private static final InputFilter[] NO_FILTERS = new InputFilter[0]; private InputFilter[] mFilters = NO_FILTERS; @@ -1349,9 +1533,15 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable private Object[] mSpans; private int[] mSpanStarts; private int[] mSpanEnds; + private int[] mSpanMax; // see calcMax() for an explanation of what this array stores private int[] mSpanFlags; private int mSpanCount; - private int mSpanCountBeforeAdd; + private IdentityHashMap<Object, Integer> mIndexOfSpan; + private int mLowWaterMark; // indices below this have not been touched + + // TextWatcher callbacks may trigger changes that trigger more callbacks. This keeps track of + // how deep the callbacks go. + private int mTextWatcherDepth; // TODO These value are tightly related to the public SPAN_MARK/POINT values in {@link Spanned} private static final int MARK = 1; @@ -1363,6 +1553,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable private static final int START_SHIFT = 4; // These bits are not (currently) used by SPANNED flags + private static final int SPAN_ADDED = 0x800; private static final int SPAN_START_AT_START = 0x1000; private static final int SPAN_START_AT_END = 0x2000; private static final int SPAN_END_AT_START = 0x4000; diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index 07505a9..ee39e27 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -16,7 +16,6 @@ package android.text; -import android.graphics.Bitmap; import android.graphics.Paint; import android.text.style.LeadingMarginSpan; import android.text.style.LeadingMarginSpan.LeadingMarginSpan2; @@ -28,6 +27,9 @@ import android.util.Log; import com.android.internal.util.ArrayUtils; import com.android.internal.util.GrowingArrayUtils; +import java.util.Arrays; +import java.util.Locale; + /** * StaticLayout is a Layout for text that will not be edited after it * is laid out. Use {@link DynamicLayout} for text that may change. @@ -42,6 +44,209 @@ public class StaticLayout extends Layout { static final String TAG = "StaticLayout"; + /** + * Builder for static layouts. It would be better if this were a public + * API (as it would offer much greater flexibility for adding new options) + * but for the time being it's just internal. + * + * @hide + */ + public final static class Builder { + private Builder() { + mNativePtr = nNewBuilder(); + } + + static Builder obtain() { + Builder b = null; + synchronized (sLock) { + for (int i = 0; i < sCached.length; i++) { + if (sCached[i] != null) { + b = sCached[i]; + sCached[i] = null; + break; + } + } + } + if (b == null) { + b = new Builder(); + } + + // set default initial values + b.mWidth = 0; + b.mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR; + b.mSpacingMult = 1.0f; + b.mSpacingAdd = 0.0f; + b.mIncludePad = true; + b.mEllipsizedWidth = 0; + b.mEllipsize = null; + b.mMaxLines = Integer.MAX_VALUE; + + b.mMeasuredText = MeasuredText.obtain(); + return b; + } + + static void recycle(Builder b) { + b.mPaint = null; + b.mText = null; + MeasuredText.recycle(b.mMeasuredText); + synchronized (sLock) { + for (int i = 0; i < sCached.length; i++) { + if (sCached[i] == null) { + sCached[i] = b; + break; + } + } + } + } + + // release any expensive state + /* package */ void finish() { + nFinishBuilder(mNativePtr); + mMeasuredText.finish(); + } + + public Builder setText(CharSequence source) { + return setText(source, 0, source.length()); + } + + public Builder setText(CharSequence source, int start, int end) { + mText = source; + mStart = start; + mEnd = end; + return this; + } + + public Builder setPaint(TextPaint paint) { + mPaint = paint; + return this; + } + + public Builder setWidth(int width) { + mWidth = width; + if (mEllipsize == null) { + mEllipsizedWidth = width; + } + return this; + } + + public Builder setTextDir(TextDirectionHeuristic textDir) { + mTextDir = textDir; + return this; + } + + // TODO: combine the following, as they're almost always set together? + public Builder setSpacingMult(float spacingMult) { + mSpacingMult = spacingMult; + return this; + } + + public Builder setSpacingAdd(float spacingAdd) { + mSpacingAdd = spacingAdd; + return this; + } + + public Builder setIncludePad(boolean includePad) { + mIncludePad = includePad; + return this; + } + + // TODO: combine the following? + public Builder setEllipsizedWidth(int ellipsizedWidth) { + mEllipsizedWidth = ellipsizedWidth; + return this; + } + + public Builder setEllipsize(TextUtils.TruncateAt ellipsize) { + mEllipsize = ellipsize; + return this; + } + + public Builder setMaxLines(int maxLines) { + mMaxLines = maxLines; + return this; + } + + /** + * Measurement and break iteration is done in native code. The protocol for using + * the native code is as follows. + * + * For each paragraph, do a nSetText of the paragraph text. Then, for each run within the + * paragraph: + * - setLocale (this must be done at least for the first run, optional afterwards) + * - one of the following, depending on the type of run: + * + addStyleRun (a text run, to be measured in native code) + * + addMeasuredRun (a run already measured in Java, passed into native code) + * + addReplacementRun (a replacement run, width is given) + * + * After measurement, nGetWidths() is valid if the widths are needed (eg for ellipsis). + * Run nComputeLineBreaks() to obtain line breaks for the paragraph. + * + * After all paragraphs, call finish() to release expensive buffers. + */ + + private void setLocale(Locale locale) { + if (!locale.equals(mLocale)) { + nSetLocale(mNativePtr, locale.toLanguageTag()); + mLocale = locale; + } + } + + /* package */ float addStyleRun(TextPaint paint, int start, int end, boolean isRtl) { + return nAddStyleRun(mNativePtr, paint.getNativeInstance(), paint.mNativeTypeface, + start, end, isRtl); + } + + /* package */ void addMeasuredRun(int start, int end, float[] widths) { + nAddMeasuredRun(mNativePtr, start, end, widths); + } + + /* package */ void addReplacementRun(int start, int end, float width) { + nAddReplacementRun(mNativePtr, start, end, width); + } + + public StaticLayout build() { + // TODO: can optimize based on whether ellipsis is needed + StaticLayout result = new StaticLayout(mText); + result.generate(this, this.mIncludePad, this.mIncludePad); + recycle(this); + return result; + } + + @Override + protected void finalize() throws Throwable { + try { + nFreeBuilder(mNativePtr); + } finally { + super.finalize(); + } + } + + /* package */ long mNativePtr; + + CharSequence mText; + int mStart; + int mEnd; + TextPaint mPaint; + int mWidth; + TextDirectionHeuristic mTextDir; + float mSpacingMult; + float mSpacingAdd; + boolean mIncludePad; + int mEllipsizedWidth; + TextUtils.TruncateAt mEllipsize; + int mMaxLines; + + Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt(); + + // This will go away and be subsumed by native builder code + MeasuredText mMeasuredText; + + Locale mLocale; + + private static final Object sLock = new Object(); + private static final Builder[] sCached = new Builder[3]; + } + public StaticLayout(CharSequence source, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd, @@ -109,6 +314,17 @@ public class StaticLayout extends Layout { : new Ellipsizer(source), paint, outerwidth, align, textDir, spacingmult, spacingadd); + Builder b = Builder.obtain(); + b.setText(source, bufstart, bufend) + .setPaint(paint) + .setWidth(outerwidth) + .setTextDir(textDir) + .setSpacingMult(spacingmult) + .setSpacingAdd(spacingadd) + .setIncludePad(includepad) + .setEllipsizedWidth(ellipsizedWidth) + .setEllipsize(ellipsize) + .setMaxLines(maxLines); /* * This is annoying, but we can't refer to the layout until * superclass construction is finished, and the superclass @@ -135,14 +351,9 @@ public class StaticLayout extends Layout { mLines = new int[mLineDirections.length]; mMaximumVisibleLineCount = maxLines; - mMeasured = MeasuredText.obtain(); - - generate(source, bufstart, bufend, paint, outerwidth, textDir, spacingmult, - spacingadd, includepad, includepad, ellipsizedWidth, - ellipsize); + generate(b, b.mIncludePad, b.mIncludePad); - mMeasured = MeasuredText.recycle(mMeasured); - mFontMetricsInt = null; + Builder.recycle(b); } /* package */ StaticLayout(CharSequence text) { @@ -151,28 +362,36 @@ public class StaticLayout extends Layout { mColumns = COLUMNS_ELLIPSIZE; mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2 * mColumns); mLines = new int[mLineDirections.length]; - // FIXME This is never recycled - mMeasured = MeasuredText.obtain(); } - /* package */ void generate(CharSequence source, int bufStart, int bufEnd, - TextPaint paint, int outerWidth, - TextDirectionHeuristic textDir, float spacingmult, - float spacingadd, boolean includepad, - boolean trackpad, float ellipsizedWidth, - TextUtils.TruncateAt ellipsize) { - int[] breakOpp = null; - final String localeLanguageTag = paint.getTextLocale().toLanguageTag(); + /* package */ void generate(Builder b, boolean includepad, boolean trackpad) { + CharSequence source = b.mText; + int bufStart = b.mStart; + int bufEnd = b.mEnd; + TextPaint paint = b.mPaint; + int outerWidth = b.mWidth; + TextDirectionHeuristic textDir = b.mTextDir; + float spacingmult = b.mSpacingMult; + float spacingadd = b.mSpacingAdd; + float ellipsizedWidth = b.mEllipsizedWidth; + TextUtils.TruncateAt ellipsize = b.mEllipsize; + LineBreaks lineBreaks = new LineBreaks(); // TODO: move to builder to avoid allocation costs + // store span end locations + int[] spanEndCache = new int[4]; + // store fontMetrics per span range + // must be a multiple of 4 (and > 0) (store top, bottom, ascent, and descent per range) + int[] fmCache = new int[4 * 4]; + b.setLocale(paint.getTextLocale()); // TODO: also respect LocaleSpan within the text mLineCount = 0; int v = 0; boolean needMultiply = (spacingmult != 1 || spacingadd != 0); - Paint.FontMetricsInt fm = mFontMetricsInt; + Paint.FontMetricsInt fm = b.mFontMetricsInt; int[] chooseHtv = null; - MeasuredText measured = mMeasured; + MeasuredText measured = b.mMeasuredText; Spanned spanned = null; if (source instanceof Spanned) @@ -186,7 +405,7 @@ public class StaticLayout extends Layout { else paraEnd++; - int firstWidthLineLimit = mLineCount + 1; + int firstWidthLineCount = 1; int firstWidth = outerWidth; int restWidth = outerWidth; @@ -204,9 +423,8 @@ public class StaticLayout extends Layout { // leading margin spans, not just this particular one if (lms instanceof LeadingMarginSpan2) { LeadingMarginSpan2 lms2 = (LeadingMarginSpan2) lms; - int lmsFirstLine = getLineForOffset(spanned.getSpanStart(lms2)); - firstWidthLineLimit = Math.max(firstWidthLineLimit, - lmsFirstLine + lms2.getLeadingMarginLineCount()); + firstWidthLineCount = Math.max(firstWidthLineCount, + lms2.getLeadingMarginLineCount()); } } @@ -235,41 +453,31 @@ public class StaticLayout extends Layout { } } - measured.setPara(source, paraStart, paraEnd, textDir); + measured.setPara(source, paraStart, paraEnd, textDir, b); char[] chs = measured.mChars; float[] widths = measured.mWidths; byte[] chdirs = measured.mLevels; int dir = measured.mDir; boolean easy = measured.mEasy; + nSetText(b.mNativePtr, chs, paraEnd - paraStart); - breakOpp = nLineBreakOpportunities(localeLanguageTag, chs, paraEnd - paraStart, breakOpp); - int breakOppIndex = 0; - - int width = firstWidth; - - float w = 0; - // here is the offset of the starting character of the line we are currently measuring - int here = paraStart; - - // ok is a character offset located after a word separator (space, tab, number...) where - // we would prefer to cut the current line. Equals to here when no such break was found. - int ok = paraStart; - float okWidth = w; - int okAscent = 0, okDescent = 0, okTop = 0, okBottom = 0; - - // fit is a character offset such that the [here, fit[ range fits in the allowed width. - // We will cut the line there if no ok position is found. - int fit = paraStart; - float fitWidth = w; - int fitAscent = 0, fitDescent = 0, fitTop = 0, fitBottom = 0; - // same as fitWidth but not including any trailing whitespace - float fitWidthGraphing = w; - - boolean hasTabOrEmoji = false; - boolean hasTab = false; - TabStops tabStops = null; - + // measurement has to be done before performing line breaking + // but we don't want to recompute fontmetrics or span ranges the + // second time, so we cache those and then use those stored values + int fmCacheCount = 0; + int spanEndCacheCount = 0; for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) { + if (fmCacheCount * 4 >= fmCache.length) { + int[] grow = new int[fmCacheCount * 4 * 2]; + System.arraycopy(fmCache, 0, grow, 0, fmCacheCount * 4); + fmCache = grow; + } + + if (spanEndCacheCount >= spanEndCache.length) { + int[] grow = new int[spanEndCacheCount * 2]; + System.arraycopy(spanEndCache, 0, grow, 0, spanEndCacheCount); + spanEndCache = grow; + } if (spanned == null) { spanEnd = paraEnd; @@ -285,200 +493,108 @@ public class StaticLayout extends Layout { measured.addStyleRun(paint, spans, spanLen, fm); } - int fmTop = fm.top; - int fmBottom = fm.bottom; - int fmAscent = fm.ascent; - int fmDescent = fm.descent; - - for (int j = spanStart; j < spanEnd; j++) { - char c = chs[j - paraStart]; - - if (c == CHAR_NEW_LINE) { - // intentionally left empty - } else if (c == CHAR_TAB) { - if (hasTab == false) { - hasTab = true; - hasTabOrEmoji = true; - if (spanned != null) { - // First tab this para, check for tabstops - TabStopSpan[] spans = getParagraphSpans(spanned, paraStart, - paraEnd, TabStopSpan.class); - if (spans.length > 0) { - tabStops = new TabStops(TAB_INCREMENT, spans); - } - } - } - if (tabStops != null) { - w = tabStops.nextTab(w); - } else { - w = TabStops.nextDefaultStop(w, TAB_INCREMENT); - } - } else if (c >= CHAR_FIRST_HIGH_SURROGATE && c <= CHAR_LAST_LOW_SURROGATE - && j + 1 < spanEnd) { - int emoji = Character.codePointAt(chs, j - paraStart); - - if (emoji >= MIN_EMOJI && emoji <= MAX_EMOJI) { - Bitmap bm = EMOJI_FACTORY.getBitmapFromAndroidPua(emoji); - - if (bm != null) { - Paint whichPaint; - - if (spanned == null) { - whichPaint = paint; - } else { - whichPaint = mWorkPaint; - } - - float wid = bm.getWidth() * -whichPaint.ascent() / bm.getHeight(); - - w += wid; - hasTabOrEmoji = true; - j++; - } else { - w += widths[j - paraStart]; - } - } else { - w += widths[j - paraStart]; - } - } else { - w += widths[j - paraStart]; + // the order of storage here (top, bottom, ascent, descent) has to match the code below + // where these values are retrieved + fmCache[fmCacheCount * 4 + 0] = fm.top; + fmCache[fmCacheCount * 4 + 1] = fm.bottom; + fmCache[fmCacheCount * 4 + 2] = fm.ascent; + fmCache[fmCacheCount * 4 + 3] = fm.descent; + fmCacheCount++; + + spanEndCache[spanEndCacheCount] = spanEnd; + spanEndCacheCount++; + } + + // tab stop locations + int[] variableTabStops = null; + if (spanned != null) { + TabStopSpan[] spans = getParagraphSpans(spanned, paraStart, + paraEnd, TabStopSpan.class); + if (spans.length > 0) { + int[] stops = new int[spans.length]; + for (int i = 0; i < spans.length; i++) { + stops[i] = spans[i].getTabStop(); } + Arrays.sort(stops, 0, stops.length); + variableTabStops = stops; + } + } - boolean isSpaceOrTab = c == CHAR_SPACE || c == CHAR_TAB || c == CHAR_ZWSP; + nGetWidths(b.mNativePtr, widths); + int breakCount = nComputeLineBreaks(b.mNativePtr, paraEnd - paraStart, firstWidth, + firstWidthLineCount, restWidth, variableTabStops, TAB_INCREMENT, false, lineBreaks, + lineBreaks.breaks, lineBreaks.widths, lineBreaks.flags, lineBreaks.breaks.length); - if (w <= width || isSpaceOrTab) { - fitWidth = w; - if (!isSpaceOrTab) { - fitWidthGraphing = w; - } - fit = j + 1; - - if (fmTop < fitTop) - fitTop = fmTop; - if (fmAscent < fitAscent) - fitAscent = fmAscent; - if (fmDescent > fitDescent) - fitDescent = fmDescent; - if (fmBottom > fitBottom) - fitBottom = fmBottom; - - while (breakOpp[breakOppIndex] != -1 - && breakOpp[breakOppIndex] < j - paraStart + 1) { - breakOppIndex++; - } - boolean isLineBreak = breakOppIndex < breakOpp.length && - breakOpp[breakOppIndex] == j - paraStart + 1; - - if (isLineBreak) { - okWidth = fitWidthGraphing; - ok = j + 1; - - if (fitTop < okTop) - okTop = fitTop; - if (fitAscent < okAscent) - okAscent = fitAscent; - if (fitDescent > okDescent) - okDescent = fitDescent; - if (fitBottom > okBottom) - okBottom = fitBottom; - } - } else { - int endPos; - int above, below, top, bottom; - float currentTextWidth; - - if (ok != here) { - endPos = ok; - above = okAscent; - below = okDescent; - top = okTop; - bottom = okBottom; - currentTextWidth = okWidth; - } else if (fit != here) { - endPos = fit; - above = fitAscent; - below = fitDescent; - top = fitTop; - bottom = fitBottom; - currentTextWidth = fitWidth; - } else { - // must make progress, so take next character - endPos = here + 1; - // but to deal properly with clusters - // take all zero width characters following that - while (endPos < spanEnd && widths[endPos - paraStart] == 0) { - endPos++; - } - above = fmAscent; - below = fmDescent; - top = fmTop; - bottom = fmBottom; - currentTextWidth = widths[here - paraStart]; - } + int[] breaks = lineBreaks.breaks; + float[] lineWidths = lineBreaks.widths; + boolean[] flags = lineBreaks.flags; - int ellipseEnd = endPos; - if (mMaximumVisibleLineCount == 1 && ellipsize == TextUtils.TruncateAt.MIDDLE) { - ellipseEnd = paraEnd; - } - v = out(source, here, ellipseEnd, - above, below, top, bottom, - v, spacingmult, spacingadd, chooseHt,chooseHtv, fm, hasTabOrEmoji, - needMultiply, chdirs, dir, easy, bufEnd, includepad, trackpad, - chs, widths, paraStart, ellipsize, ellipsizedWidth, - currentTextWidth, paint, true); - - here = endPos; - j = here - 1; // restart j-span loop from here, compensating for the j++ - ok = fit = here; - w = 0; - fitWidthGraphing = w; - fitAscent = fitDescent = fitTop = fitBottom = 0; - okAscent = okDescent = okTop = okBottom = 0; - - if (--firstWidthLineLimit <= 0) { - width = restWidth; - } + // here is the offset of the starting character of the line we are currently measuring + int here = paraStart; - if (here < spanStart) { - // The text was cut before the beginning of the current span range. - // Exit the span loop, and get spanStart to start over from here. - measured.setPos(here); - spanEnd = here; - break; - } + int fmTop = 0, fmBottom = 0, fmAscent = 0, fmDescent = 0; + int fmCacheIndex = 0; + int spanEndCacheIndex = 0; + int breakIndex = 0; + for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) { + // retrieve end of span + spanEnd = spanEndCache[spanEndCacheIndex++]; + + // retrieve cached metrics, order matches above + fm.top = fmCache[fmCacheIndex * 4 + 0]; + fm.bottom = fmCache[fmCacheIndex * 4 + 1]; + fm.ascent = fmCache[fmCacheIndex * 4 + 2]; + fm.descent = fmCache[fmCacheIndex * 4 + 3]; + fmCacheIndex++; + + if (fm.top < fmTop) { + fmTop = fm.top; + } + if (fm.ascent < fmAscent) { + fmAscent = fm.ascent; + } + if (fm.descent > fmDescent) { + fmDescent = fm.descent; + } + if (fm.bottom > fmBottom) { + fmBottom = fm.bottom; + } - if (mLineCount >= mMaximumVisibleLineCount) { - return; - } - } + // skip breaks ending before current span range + while (breakIndex < breakCount && paraStart + breaks[breakIndex] < spanStart) { + breakIndex++; } - } - if (paraEnd != here && mLineCount < mMaximumVisibleLineCount) { - if ((fitTop | fitBottom | fitDescent | fitAscent) == 0) { - paint.getFontMetricsInt(fm); + while (breakIndex < breakCount && paraStart + breaks[breakIndex] <= spanEnd) { + int endPos = paraStart + breaks[breakIndex]; - fitTop = fm.top; - fitBottom = fm.bottom; - fitAscent = fm.ascent; - fitDescent = fm.descent; - } + boolean moreChars = (endPos < bufEnd); - // Log.e("text", "output rest " + here + " to " + end); - - v = out(source, - here, paraEnd, fitAscent, fitDescent, - fitTop, fitBottom, - v, - spacingmult, spacingadd, chooseHt, - chooseHtv, fm, hasTabOrEmoji, - needMultiply, chdirs, dir, easy, bufEnd, - includepad, trackpad, chs, - widths, paraStart, ellipsize, - ellipsizedWidth, w, paint, paraEnd != bufEnd); - } + v = out(source, here, endPos, + fmAscent, fmDescent, fmTop, fmBottom, + v, spacingmult, spacingadd, chooseHt,chooseHtv, fm, flags[breakIndex], + needMultiply, chdirs, dir, easy, bufEnd, includepad, trackpad, + chs, widths, paraStart, ellipsize, ellipsizedWidth, + lineWidths[breakIndex], paint, moreChars); + + if (endPos < spanEnd) { + // preserve metrics for current span + fmTop = fm.top; + fmBottom = fm.bottom; + fmAscent = fm.ascent; + fmDescent = fm.descent; + } else { + fmTop = fmBottom = fmAscent = fmDescent = 0; + } - paraStart = paraEnd; + here = endPos; + breakIndex++; + + if (mLineCount >= mMaximumVisibleLineCount) { + return; + } + } + } if (paraEnd == bufEnd) break; @@ -488,7 +604,7 @@ public class StaticLayout extends Layout { mLineCount < mMaximumVisibleLineCount) { // Log.e("text", "output last " + bufEnd); - measured.setPara(source, bufStart, bufEnd, textDir); + measured.setPara(source, bufEnd, bufEnd, textDir, b); paint.getFontMetricsInt(fm); @@ -845,18 +961,32 @@ public class StaticLayout extends Layout { return mEllipsizedWidth; } - void prepare() { - mMeasured = MeasuredText.obtain(); - } + private static native long nNewBuilder(); + private static native void nFreeBuilder(long nativePtr); + private static native void nFinishBuilder(long nativePtr); + private static native void nSetLocale(long nativePtr, String locale); - void finish() { - mMeasured = MeasuredText.recycle(mMeasured); - } + private static native void nSetText(long nativePtr, char[] text, int length); + + private static native float nAddStyleRun(long nativePtr, long nativePaint, + long nativeTypeface, int start, int end, boolean isRtl); + + private static native void nAddMeasuredRun(long nativePtr, + int start, int end, float[] widths); - // returns an array with terminal sentinel value -1 to indicate end - // this is so that arrays can be recycled instead of allocating new arrays - // every time - private static native int[] nLineBreakOpportunities(String locale, char[] text, int length, int[] recycle); + private static native void nAddReplacementRun(long nativePtr, int start, int end, float width); + + private static native void nGetWidths(long nativePtr, float[] widths); + + // populates LineBreaks and returns the number of breaks found + // + // the arrays inside the LineBreaks objects are passed in as well + // to reduce the number of JNI calls in the common case where the + // arrays do not have to be resized + private static native int nComputeLineBreaks(long nativePtr, + int length, float firstWidth, int firstWidthLineCount, float restWidth, + int[] variableTabStops, int defaultTabStop, boolean optimize, LineBreaks recycle, + int[] recycleBreaks, float[] recycleWidths, boolean[] recycleFlags, int recycleLength); private int mLineCount; private int mTopPadding, mBottomPadding; @@ -884,18 +1014,17 @@ public class StaticLayout extends Layout { private static final int TAB_INCREMENT = 20; // same as Layout, but that's private private static final char CHAR_NEW_LINE = '\n'; - private static final char CHAR_TAB = '\t'; - private static final char CHAR_SPACE = ' '; - private static final char CHAR_ZWSP = '\u200B'; private static final double EXTRA_ROUNDING = 0.5; - private static final int CHAR_FIRST_HIGH_SURROGATE = 0xD800; - private static final int CHAR_LAST_LOW_SURROGATE = 0xDFFF; + // This is used to return three arrays from a single JNI call when + // performing line breaking + /*package*/ static class LineBreaks { + private static final int INITIAL_SIZE = 16; + public int[] breaks = new int[INITIAL_SIZE]; + public float[] widths = new float[INITIAL_SIZE]; + public boolean[] flags = new boolean[INITIAL_SIZE]; // hasTabOrEmoji + // breaks, widths, and flags should all have the same length + } - /* - * This is reused across calls to generate() - */ - private MeasuredText mMeasured; - private Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt(); } diff --git a/core/java/android/text/TextPaint.java b/core/java/android/text/TextPaint.java index 0447117..4f8cff0 100644 --- a/core/java/android/text/TextPaint.java +++ b/core/java/android/text/TextPaint.java @@ -16,6 +16,7 @@ package android.text; +import android.annotation.ColorInt; import android.graphics.Paint; /** @@ -25,8 +26,10 @@ import android.graphics.Paint; public class TextPaint extends Paint { // Special value 0 means no background paint + @ColorInt public int bgColor; public int baselineShift; + @ColorInt public int linkColor; public int[] drawableState; public float density = 1.0f; @@ -34,6 +37,7 @@ public class TextPaint extends Paint { * Special value 0 means no custom underline * @hide */ + @ColorInt public int underlineColor = 0; /** * Defined as a multiplier of the default underline thickness. Use 1.0f for default thickness. diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java index 48bb5dd..676986d 100644 --- a/core/java/android/text/TextUtils.java +++ b/core/java/android/text/TextUtils.java @@ -16,6 +16,7 @@ package android.text; +import android.annotation.Nullable; import android.content.res.Resources; import android.os.Parcel; import android.os.Parcelable; @@ -457,7 +458,7 @@ public class TextUtils { * @param str the string to be examined * @return true if str is null or zero length */ - public static boolean isEmpty(CharSequence str) { + public static boolean isEmpty(@Nullable CharSequence str) { if (str == null || str.length() == 0) return true; else @@ -1258,7 +1259,7 @@ public class TextUtils { } // XXX this is probably ok, but need to look at it more - tempMt.setPara(format, 0, format.length(), textDir); + tempMt.setPara(format, 0, format.length(), textDir, null); float moreWid = tempMt.addStyleRun(p, tempMt.mLen, null); if (w + moreWid <= avail) { @@ -1280,7 +1281,7 @@ public class TextUtils { private static float setPara(MeasuredText mt, TextPaint paint, CharSequence text, int start, int end, TextDirectionHeuristic textDir) { - mt.setPara(text, start, end, textDir); + mt.setPara(text, start, end, textDir, null); float width; Spanned sp = text instanceof Spanned ? (Spanned) text : null; diff --git a/core/java/android/text/format/DateFormat.java b/core/java/android/text/format/DateFormat.java index c03f7a6..3ed37b3 100755 --- a/core/java/android/text/format/DateFormat.java +++ b/core/java/android/text/format/DateFormat.java @@ -23,8 +23,6 @@ import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.SpannedString; -import com.android.internal.R; - import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; diff --git a/core/java/android/text/style/ForegroundColorSpan.java b/core/java/android/text/style/ForegroundColorSpan.java index c9e09bd..f167aab 100644 --- a/core/java/android/text/style/ForegroundColorSpan.java +++ b/core/java/android/text/style/ForegroundColorSpan.java @@ -16,6 +16,7 @@ package android.text.style; +import android.annotation.ColorInt; import android.os.Parcel; import android.text.ParcelableSpan; import android.text.TextPaint; @@ -26,7 +27,7 @@ public class ForegroundColorSpan extends CharacterStyle private final int mColor; - public ForegroundColorSpan(int color) { + public ForegroundColorSpan(@ColorInt int color) { mColor = color; } @@ -46,6 +47,7 @@ public class ForegroundColorSpan extends CharacterStyle dest.writeInt(mColor); } + @ColorInt public int getForegroundColor() { return mColor; } diff --git a/core/java/android/text/style/ImageSpan.java b/core/java/android/text/style/ImageSpan.java index 3d6f8e6..856dd0b 100644 --- a/core/java/android/text/style/ImageSpan.java +++ b/core/java/android/text/style/ImageSpan.java @@ -16,6 +16,7 @@ package android.text.style; +import android.annotation.DrawableRes; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -110,7 +111,7 @@ public class ImageSpan extends DynamicDrawableSpan { mSource = uri.toString(); } - public ImageSpan(Context context, int resourceId) { + public ImageSpan(Context context, @DrawableRes int resourceId) { this(context, resourceId, ALIGN_BOTTOM); } @@ -118,7 +119,7 @@ public class ImageSpan extends DynamicDrawableSpan { * @param verticalAlignment one of {@link DynamicDrawableSpan#ALIGN_BOTTOM} or * {@link DynamicDrawableSpan#ALIGN_BASELINE}. */ - public ImageSpan(Context context, int resourceId, int verticalAlignment) { + public ImageSpan(Context context, @DrawableRes int resourceId, int verticalAlignment) { super(verticalAlignment); mContext = context; mResourceId = resourceId; diff --git a/core/java/android/text/style/QuoteSpan.java b/core/java/android/text/style/QuoteSpan.java index 29dd273..17748ca 100644 --- a/core/java/android/text/style/QuoteSpan.java +++ b/core/java/android/text/style/QuoteSpan.java @@ -16,6 +16,7 @@ package android.text.style; +import android.annotation.ColorInt; import android.graphics.Paint; import android.graphics.Canvas; import android.os.Parcel; @@ -34,7 +35,7 @@ public class QuoteSpan implements LeadingMarginSpan, ParcelableSpan { mColor = 0xff0000ff; } - public QuoteSpan(int color) { + public QuoteSpan(@ColorInt int color) { super(); mColor = color; } @@ -55,6 +56,7 @@ public class QuoteSpan implements LeadingMarginSpan, ParcelableSpan { dest.writeInt(mColor); } + @ColorInt public int getColor() { return mColor; } diff --git a/core/java/android/text/util/Linkify.java b/core/java/android/text/util/Linkify.java index 0f401a4..c119277 100644 --- a/core/java/android/text/util/Linkify.java +++ b/core/java/android/text/util/Linkify.java @@ -78,7 +78,10 @@ public class Linkify { /** * Bit field indicating that street addresses should be matched in methods that - * take an options mask + * take an options mask. Note that this uses the + * {@link android.webkit.WebView#findAddress(String) findAddress()} method in + * {@link android.webkit.WebView} for finding addresses, which has various + * limitations. */ public static final int MAP_ADDRESSES = 0x08; diff --git a/core/java/android/text/util/Rfc822Token.java b/core/java/android/text/util/Rfc822Token.java index 0edeeb5..058757a 100644 --- a/core/java/android/text/util/Rfc822Token.java +++ b/core/java/android/text/util/Rfc822Token.java @@ -16,18 +16,21 @@ package android.text.util; +import android.annotation.Nullable; + /** * This class stores an RFC 822-like name, address, and comment, * and provides methods to convert them to quoted strings. */ public class Rfc822Token { + @Nullable private String mName, mAddress, mComment; /** * Creates a new Rfc822Token with the specified name, address, * and comment. */ - public Rfc822Token(String name, String address, String comment) { + public Rfc822Token(@Nullable String name, @Nullable String address, @Nullable String comment) { mName = name; mAddress = address; mComment = comment; @@ -36,6 +39,7 @@ public class Rfc822Token { /** * Returns the name part. */ + @Nullable public String getName() { return mName; } @@ -43,6 +47,7 @@ public class Rfc822Token { /** * Returns the address part. */ + @Nullable public String getAddress() { return mAddress; } @@ -50,6 +55,7 @@ public class Rfc822Token { /** * Returns the comment part. */ + @Nullable public String getComment() { return mComment; } @@ -57,21 +63,21 @@ public class Rfc822Token { /** * Changes the name to the specified name. */ - public void setName(String name) { + public void setName(@Nullable String name) { mName = name; } /** * Changes the address to the specified address. */ - public void setAddress(String address) { + public void setAddress(@Nullable String address) { mAddress = address; } /** * Changes the comment to the specified comment. */ - public void setComment(String comment) { + public void setComment(@Nullable String comment) { mComment = comment; } diff --git a/core/java/android/transition/ChangeScroll.java b/core/java/android/transition/ChangeScroll.java index 39291bf..5a78b94 100644 --- a/core/java/android/transition/ChangeScroll.java +++ b/core/java/android/transition/ChangeScroll.java @@ -28,8 +28,6 @@ import android.view.ViewGroup; /** * This transition captures the scroll properties of targets before and after * the scene change and animates any changes. - * - * @hide */ public class ChangeScroll extends Transition { diff --git a/core/java/android/transition/CircularPropagation.java b/core/java/android/transition/CircularPropagation.java index 1e44cfa..c9faa0f 100644 --- a/core/java/android/transition/CircularPropagation.java +++ b/core/java/android/transition/CircularPropagation.java @@ -16,7 +16,6 @@ package android.transition; import android.graphics.Rect; -import android.util.Log; import android.view.View; import android.view.ViewGroup; diff --git a/core/java/android/transition/SidePropagation.java b/core/java/android/transition/SidePropagation.java index 5dd1fff..b10f6a0 100644 --- a/core/java/android/transition/SidePropagation.java +++ b/core/java/android/transition/SidePropagation.java @@ -16,7 +16,6 @@ package android.transition; import android.graphics.Rect; -import android.util.Log; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; diff --git a/core/java/android/transition/Transition.java b/core/java/android/transition/Transition.java index 2705bcf..c942042 100644 --- a/core/java/android/transition/Transition.java +++ b/core/java/android/transition/Transition.java @@ -1762,7 +1762,17 @@ public abstract class Transition implements Cloneable { runAnimators(); } - boolean areValuesChanged(TransitionValues oldValues, TransitionValues newValues) { + /** + * Returns whether transition values have changed between the start scene and the end scene + * (thus determining whether animation is required). The default implementation compares the + * property values returned from {@link #getTransitionProperties()}, or all property values if + * {@code getTransitionProperties()} returns null. Subclasses may override this method to + * provide logic more specific to their transition implementation. + * + * @param oldValues the first set of values, may be {@code null} + * @param newValues the second set of values, may be {@code null} + */ + protected boolean areValuesChanged(TransitionValues oldValues, TransitionValues newValues) { boolean valuesChanged = false; // if oldValues null, then transition didn't care to stash values, // and won't get canceled diff --git a/core/java/android/transition/TransitionInflater.java b/core/java/android/transition/TransitionInflater.java index 9009d6a..a7d9503 100644 --- a/core/java/android/transition/TransitionInflater.java +++ b/core/java/android/transition/TransitionInflater.java @@ -16,6 +16,7 @@ package android.transition; +import android.annotation.TransitionRes; import com.android.internal.R; import org.xmlpull.v1.XmlPullParser; @@ -71,7 +72,8 @@ public class TransitionInflater { * @throws android.content.res.Resources.NotFoundException when the * transition cannot be loaded */ - public Transition inflateTransition(int resource) { + public Transition inflateTransition(@TransitionRes int resource) { + //noinspection ResourceType XmlResourceParser parser = mContext.getResources().getXml(resource); try { return createTransitionFromXml(parser, Xml.asAttributeSet(parser), null); @@ -98,7 +100,9 @@ public class TransitionInflater { * @throws android.content.res.Resources.NotFoundException when the * transition manager cannot be loaded */ - public TransitionManager inflateTransitionManager(int resource, ViewGroup sceneRoot) { + public TransitionManager inflateTransitionManager(@TransitionRes int resource, + ViewGroup sceneRoot) { + //noinspection ResourceType XmlResourceParser parser = mContext.getResources().getXml(resource); try { return createTransitionManagerFromXml(parser, Xml.asAttributeSet(parser), sceneRoot); diff --git a/core/java/android/transition/TransitionManager.java b/core/java/android/transition/TransitionManager.java index 7bd6287..0b70fdb 100644 --- a/core/java/android/transition/TransitionManager.java +++ b/core/java/android/transition/TransitionManager.java @@ -181,24 +181,27 @@ public class TransitionManager { private static void changeScene(Scene scene, Transition transition) { final ViewGroup sceneRoot = scene.getSceneRoot(); + if (!sPendingTransitions.contains(sceneRoot)) { + sPendingTransitions.add(sceneRoot); - Transition transitionClone = null; - if (transition != null) { - transitionClone = transition.clone(); - transitionClone.setSceneRoot(sceneRoot); - } + Transition transitionClone = null; + if (transition != null) { + transitionClone = transition.clone(); + transitionClone.setSceneRoot(sceneRoot); + } - Scene oldScene = Scene.getCurrentScene(sceneRoot); - if (oldScene != null && transitionClone != null && - oldScene.isCreatedFromLayoutResource()) { - transitionClone.setCanRemoveViews(true); - } + Scene oldScene = Scene.getCurrentScene(sceneRoot); + if (oldScene != null && transitionClone != null && + oldScene.isCreatedFromLayoutResource()) { + transitionClone.setCanRemoveViews(true); + } - sceneChangeSetup(sceneRoot, transitionClone); + sceneChangeSetup(sceneRoot, transitionClone); - scene.enter(); + scene.enter(); - sceneChangeRunTransition(sceneRoot, transitionClone); + sceneChangeRunTransition(sceneRoot, transitionClone); + } } private static ArrayMap<ViewGroup, ArrayList<Transition>> getRunningTransitions() { @@ -268,7 +271,12 @@ public class TransitionManager { @Override public boolean onPreDraw() { removeListeners(); - sPendingTransitions.remove(mSceneRoot); + + // Don't start the transition if it's no longer pending. + if (!sPendingTransitions.remove(mSceneRoot)) { + return true; + } + // Add to running list, handle end to remove it final ArrayMap<ViewGroup, ArrayList<Transition>> runningTransitions = getRunningTransitions(); @@ -417,4 +425,24 @@ public class TransitionManager { sceneChangeRunTransition(sceneRoot, transitionClone); } } + + /** + * Ends all pending and ongoing transitions on the specified scene root. + * + * @param sceneRoot The root of the View hierarchy to end transitions on. + * @hide + */ + public static void endTransitions(final ViewGroup sceneRoot) { + sPendingTransitions.remove(sceneRoot); + + final ArrayList<Transition> runningTransitions = getRunningTransitions().get(sceneRoot); + if (runningTransitions != null) { + final int count = runningTransitions.size(); + for (int i = 0; i < count; i++) { + final Transition transition = runningTransitions.get(i); + transition.end(); + } + } + + } } diff --git a/core/java/android/transition/TransitionPropagation.java b/core/java/android/transition/TransitionPropagation.java index 9a481c2..b831038 100644 --- a/core/java/android/transition/TransitionPropagation.java +++ b/core/java/android/transition/TransitionPropagation.java @@ -15,7 +15,6 @@ */ package android.transition; -import android.graphics.Rect; import android.view.ViewGroup; /** diff --git a/core/java/android/transition/Visibility.java b/core/java/android/transition/Visibility.java index 8779229..cd68fd1 100644 --- a/core/java/android/transition/Visibility.java +++ b/core/java/android/transition/Visibility.java @@ -22,9 +22,6 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.content.Context; import android.content.res.TypedArray; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.drawable.BitmapDrawable; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; @@ -182,7 +179,7 @@ public abstract class Visibility extends Transition { return visibility == View.VISIBLE && parent != null; } - private VisibilityInfo getVisibilityChangeInfo(TransitionValues startValues, + private static VisibilityInfo getVisibilityChangeInfo(TransitionValues startValues, TransitionValues endValues) { final VisibilityInfo visInfo = new VisibilityInfo(); visInfo.visibilityChange = false; @@ -484,7 +481,7 @@ public abstract class Visibility extends Transition { } @Override - boolean areValuesChanged(TransitionValues oldValues, TransitionValues newValues) { + protected boolean areValuesChanged(TransitionValues oldValues, TransitionValues newValues) { if (oldValues == null && newValues == null) { return false; } diff --git a/core/java/android/util/AtomicFile.java b/core/java/android/util/AtomicFile.java index a6466fc..3aa3447 100644 --- a/core/java/android/util/AtomicFile.java +++ b/core/java/android/util/AtomicFile.java @@ -102,7 +102,7 @@ public class AtomicFile { str = new FileOutputStream(mBaseName); } catch (FileNotFoundException e) { File parent = mBaseName.getParentFile(); - if (!parent.mkdir()) { + if (!parent.mkdirs()) { throw new IOException("Couldn't create directory " + mBaseName); } FileUtils.setPermissions( diff --git a/core/java/android/util/AttributeSet.java b/core/java/android/util/AttributeSet.java index 74942ba..eb8c168 100644 --- a/core/java/android/util/AttributeSet.java +++ b/core/java/android/util/AttributeSet.java @@ -39,7 +39,7 @@ package android.util; * is more useful in conjunction with compiled XML resources: * * <pre> - * XmlPullParser parser = resources.getXml(myResouce); + * XmlPullParser parser = resources.getXml(myResource); * AttributeSet attributes = Xml.asAttributeSet(parser);</pre> * * <p>The implementation returned here, unlike using diff --git a/core/java/android/util/DebugUtils.java b/core/java/android/util/DebugUtils.java index f607207..84d9ce8 100644 --- a/core/java/android/util/DebugUtils.java +++ b/core/java/android/util/DebugUtils.java @@ -16,6 +16,7 @@ package android.util; +import java.io.PrintWriter; import java.lang.reflect.Method; import java.lang.reflect.InvocationTargetException; import java.util.Locale; @@ -123,4 +124,83 @@ public class DebugUtils { } } + /** @hide */ + public static void printSizeValue(PrintWriter pw, long number) { + float result = number; + String suffix = ""; + if (result > 900) { + suffix = "KB"; + result = result / 1024; + } + if (result > 900) { + suffix = "MB"; + result = result / 1024; + } + if (result > 900) { + suffix = "GB"; + result = result / 1024; + } + if (result > 900) { + suffix = "TB"; + result = result / 1024; + } + if (result > 900) { + suffix = "PB"; + result = result / 1024; + } + String value; + if (result < 1) { + value = String.format("%.2f", result); + } else if (result < 10) { + value = String.format("%.1f", result); + } else if (result < 100) { + value = String.format("%.0f", result); + } else { + value = String.format("%.0f", result); + } + pw.print(value); + pw.print(suffix); + } + + /** @hide */ + public static String sizeValueToString(long number, StringBuilder outBuilder) { + if (outBuilder == null) { + outBuilder = new StringBuilder(32); + } + float result = number; + String suffix = ""; + if (result > 900) { + suffix = "KB"; + result = result / 1024; + } + if (result > 900) { + suffix = "MB"; + result = result / 1024; + } + if (result > 900) { + suffix = "GB"; + result = result / 1024; + } + if (result > 900) { + suffix = "TB"; + result = result / 1024; + } + if (result > 900) { + suffix = "PB"; + result = result / 1024; + } + String value; + if (result < 1) { + value = String.format("%.2f", result); + } else if (result < 10) { + value = String.format("%.1f", result); + } else if (result < 100) { + value = String.format("%.0f", result); + } else { + value = String.format("%.0f", result); + } + outBuilder.append(value); + outBuilder.append(suffix); + return outBuilder.toString(); + } } diff --git a/core/java/android/util/LocalLog.java b/core/java/android/util/LocalLog.java index e49b8c3..cab5d19 100644 --- a/core/java/android/util/LocalLog.java +++ b/core/java/android/util/LocalLog.java @@ -16,8 +16,6 @@ package android.util; -import android.text.format.Time; - import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.Calendar; diff --git a/core/java/android/util/StateSet.java b/core/java/android/util/StateSet.java index 2623638..83dfc47 100644 --- a/core/java/android/util/StateSet.java +++ b/core/java/android/util/StateSet.java @@ -36,7 +36,88 @@ import com.android.internal.R; */ public class StateSet { - /** @hide */ public StateSet() {} + /** + * The order here is very important to + * {@link android.view.View#getDrawableState()} + */ + private static final int[][] VIEW_STATE_SETS; + + /** @hide */ + public static final int VIEW_STATE_WINDOW_FOCUSED = 1; + /** @hide */ + public static final int VIEW_STATE_SELECTED = 1 << 1; + /** @hide */ + public static final int VIEW_STATE_FOCUSED = 1 << 2; + /** @hide */ + public static final int VIEW_STATE_ENABLED = 1 << 3; + /** @hide */ + public static final int VIEW_STATE_PRESSED = 1 << 4; + /** @hide */ + public static final int VIEW_STATE_ACTIVATED = 1 << 5; + /** @hide */ + public static final int VIEW_STATE_ACCELERATED = 1 << 6; + /** @hide */ + public static final int VIEW_STATE_HOVERED = 1 << 7; + /** @hide */ + public static final int VIEW_STATE_DRAG_CAN_ACCEPT = 1 << 8; + /** @hide */ + public static final int VIEW_STATE_DRAG_HOVERED = 1 << 9; + + static final int[] VIEW_STATE_IDS = new int[] { + R.attr.state_window_focused, VIEW_STATE_WINDOW_FOCUSED, + R.attr.state_selected, VIEW_STATE_SELECTED, + R.attr.state_focused, VIEW_STATE_FOCUSED, + R.attr.state_enabled, VIEW_STATE_ENABLED, + R.attr.state_pressed, VIEW_STATE_PRESSED, + R.attr.state_activated, VIEW_STATE_ACTIVATED, + R.attr.state_accelerated, VIEW_STATE_ACCELERATED, + R.attr.state_hovered, VIEW_STATE_HOVERED, + R.attr.state_drag_can_accept, VIEW_STATE_DRAG_CAN_ACCEPT, + R.attr.state_drag_hovered, VIEW_STATE_DRAG_HOVERED + }; + + static { + if ((VIEW_STATE_IDS.length / 2) != R.styleable.ViewDrawableStates.length) { + throw new IllegalStateException( + "VIEW_STATE_IDs array length does not match ViewDrawableStates style array"); + } + + final int[] orderedIds = new int[VIEW_STATE_IDS.length]; + for (int i = 0; i < R.styleable.ViewDrawableStates.length; i++) { + final int viewState = R.styleable.ViewDrawableStates[i]; + for (int j = 0; j < VIEW_STATE_IDS.length; j += 2) { + if (VIEW_STATE_IDS[j] == viewState) { + orderedIds[i * 2] = viewState; + orderedIds[i * 2 + 1] = VIEW_STATE_IDS[j + 1]; + } + } + } + + final int NUM_BITS = VIEW_STATE_IDS.length / 2; + VIEW_STATE_SETS = new int[1 << NUM_BITS][]; + for (int i = 0; i < VIEW_STATE_SETS.length; i++) { + final int numBits = Integer.bitCount(i); + final int[] set = new int[numBits]; + int pos = 0; + for (int j = 0; j < orderedIds.length; j += 2) { + if ((i & orderedIds[j + 1]) != 0) { + set[pos++] = orderedIds[j]; + } + } + VIEW_STATE_SETS[i] = set; + } + } + + /** @hide */ + public static int[] get(int mask) { + if (mask >= VIEW_STATE_SETS.length) { + throw new IllegalArgumentException("Invalid state set mask"); + } + return VIEW_STATE_SETS[mask]; + } + + /** @hide */ + public StateSet() {} public static final int[] WILD_CARD = new int[0]; public static final int[] NOTHING = new int[] { 0 }; diff --git a/core/java/android/util/TypedValue.java b/core/java/android/util/TypedValue.java index 74d4245..98aaa81 100644 --- a/core/java/android/util/TypedValue.java +++ b/core/java/android/util/TypedValue.java @@ -16,6 +16,8 @@ package android.util; +import android.annotation.AnyRes; + /** * Container for a dynamically typed data value. Primarily used with * {@link android.content.res.Resources} for holding resource values. @@ -178,6 +180,7 @@ public class TypedValue { public int assetCookie; /** If Value came from a resource, this holds the corresponding resource id. */ + @AnyRes public int resourceId; /** If Value came from a resource, these are the configurations for which diff --git a/core/java/android/view/ActionMode.java b/core/java/android/view/ActionMode.java index a359952..9f202a9 100644 --- a/core/java/android/view/ActionMode.java +++ b/core/java/android/view/ActionMode.java @@ -17,6 +17,9 @@ package android.view; +import android.annotation.StringRes; +import android.graphics.Rect; + /** * Represents a contextual mode of the user interface. Action modes can be used to provide * alternative interaction modes and replace parts of the normal UI until finished. @@ -29,8 +32,21 @@ package android.view; * </div> */ public abstract class ActionMode { + + /** + * The action mode is treated as a Primary mode. This is the default. + * Use with {@link #setType}. + */ + public static final int TYPE_PRIMARY = 0; + /** + * The action mode is treated as a Floating Toolbar. + * Use with {@link #setType}. + */ + public static final int TYPE_FLOATING = 1; + private Object mTag; private boolean mTitleOptionalHint; + private int mType = TYPE_PRIMARY; /** * Set a tag object associated with this ActionMode. @@ -80,7 +96,7 @@ public abstract class ActionMode { * @see #setTitle(CharSequence) * @see #setCustomView(View) */ - public abstract void setTitle(int resId); + public abstract void setTitle(@StringRes int resId); /** * Set the subtitle of the action mode. This method will have no visible effect if @@ -102,7 +118,7 @@ public abstract class ActionMode { * @see #setSubtitle(CharSequence) * @see #setCustomView(View) */ - public abstract void setSubtitle(int resId); + public abstract void setSubtitle(@StringRes int resId); /** * Set whether or not the title/subtitle display for this action mode @@ -154,6 +170,25 @@ public abstract class ActionMode { public abstract void setCustomView(View view); /** + * Set a type for this action mode. This will affect how the system renders the action mode if + * it has to. + * + * @param type One of {@link #TYPE_PRIMARY} or {@link #TYPE_FLOATING}. + */ + public void setType(int type) { + mType = type; + } + + /** + * Returns the type for this action mode. + * + * @return One of {@link #TYPE_PRIMARY} or {@link #TYPE_FLOATING}. + */ + public int getType() { + return mType; + } + + /** * Invalidate the action mode and refresh menu content. The mode's * {@link ActionMode.Callback} will have its * {@link Callback#onPrepareActionMode(ActionMode, Menu)} method called. @@ -163,6 +198,15 @@ public abstract class ActionMode { public abstract void invalidate(); /** + * Invalidate the content rect associated to this ActionMode. This only makes sense for + * action modes that support dynamic positioning on the screen, and provides a more efficient + * way to reposition it without invalidating the whole action mode. + * + * @see Callback2#onGetContentRect(ActionMode, View, Rect) . + */ + public void invalidateContentRect() {} + + /** * Finish and close this action mode. The action mode's {@link ActionMode.Callback} will * have its {@link Callback#onDestroyActionMode(ActionMode)} method called. */ @@ -264,4 +308,31 @@ public abstract class ActionMode { */ public void onDestroyActionMode(ActionMode mode); } -}
\ No newline at end of file + + /** + * Extension of {@link ActionMode.Callback} to provide content rect information. This is + * required for ActionModes with dynamic positioning such as the ones with type + * {@link ActionMode#TYPE_FLOATING} to ensure the positioning doesn't obscure app content. If + * an app fails to provide a subclass of this class, a default implementation will be used. + */ + public static abstract class Callback2 implements ActionMode.Callback { + + /** + * Called when an ActionMode needs to be positioned on screen, potentially occluding view + * content. Note this may be called on a per-frame basis. + * + * @param mode The ActionMode that requires positioning. + * @param view The View that originated the ActionMode, in whose coordinates the Rect should + * be provided. + * @param outRect The Rect to be populated with the content position. + */ + public void onGetContentRect(ActionMode mode, View view, Rect outRect) { + if (view != null) { + outRect.set(0, 0, view.getWidth(), view.getHeight()); + } else { + outRect.set(0, 0, 0, 0); + } + } + + } +} diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java index f41afcf..c8149d9 100644 --- a/core/java/android/view/Choreographer.java +++ b/core/java/android/view/Choreographer.java @@ -141,6 +141,19 @@ public final class Choreographer { private long mFrameIntervalNanos; /** + * Contains information about the current frame for jank-tracking, + * mainly timings of key events along with a bit of metadata about + * view tree state + * + * TODO: Is there a better home for this? Currently Choreographer + * is the only one with CALLBACK_ANIMATION start time, hence why this + * resides here. + * + * @hide + */ + FrameInfo mFrameInfo = new FrameInfo(); + + /** * Callback type: Input callback. Runs first. * @hide */ @@ -513,6 +526,7 @@ public final class Choreographer { return; // no work to do } + long intendedFrameTimeNanos = frameTimeNanos; startNanos = System.nanoTime(); final long jitterNanos = startNanos - frameTimeNanos; if (jitterNanos >= mFrameIntervalNanos) { @@ -541,12 +555,18 @@ public final class Choreographer { return; } + mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos); mFrameScheduled = false; mLastFrameTimeNanos = frameTimeNanos; } + mFrameInfo.markInputHandlingStart(); doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos); + + mFrameInfo.markAnimationsStart(); doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos); + + mFrameInfo.markPerformTraversalsStart(); doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos); if (DEBUG) { diff --git a/core/java/android/view/ContextMenu.java b/core/java/android/view/ContextMenu.java index decabcb..85fe421 100644 --- a/core/java/android/view/ContextMenu.java +++ b/core/java/android/view/ContextMenu.java @@ -16,6 +16,8 @@ package android.view; +import android.annotation.DrawableRes; +import android.annotation.StringRes; import android.app.Activity; import android.graphics.drawable.Drawable; import android.widget.AdapterView; @@ -44,7 +46,7 @@ public interface ContextMenu extends Menu { * @param titleRes The string resource identifier used for the title. * @return This ContextMenu so additional setters can be called. */ - public ContextMenu setHeaderTitle(int titleRes); + public ContextMenu setHeaderTitle(@StringRes int titleRes); /** * Sets the context menu header's title to the title given in <var>title</var>. @@ -61,7 +63,7 @@ public interface ContextMenu extends Menu { * @param iconRes The resource identifier used for the icon. * @return This ContextMenu so additional setters can be called. */ - public ContextMenu setHeaderIcon(int iconRes); + public ContextMenu setHeaderIcon(@DrawableRes int iconRes); /** * Sets the context menu header's icon to the icon given in <var>icon</var> diff --git a/core/java/android/view/ContextThemeWrapper.java b/core/java/android/view/ContextThemeWrapper.java index 0afbde9..9047b1d 100644 --- a/core/java/android/view/ContextThemeWrapper.java +++ b/core/java/android/view/ContextThemeWrapper.java @@ -16,6 +16,7 @@ package android.view; +import android.annotation.StyleRes; import android.content.Context; import android.content.ContextWrapper; import android.content.res.Configuration; @@ -35,13 +36,19 @@ public class ContextThemeWrapper extends ContextWrapper { public ContextThemeWrapper() { super(null); } - - public ContextThemeWrapper(Context base, int themeres) { + + public ContextThemeWrapper(Context base, @StyleRes int themeResId) { + super(base); + mThemeResource = themeResId; + } + + public ContextThemeWrapper(Context base, Resources.Theme theme) { super(base); - mThemeResource = themeres; + mTheme = theme; } - @Override protected void attachBaseContext(Context newBase) { + @Override + protected void attachBaseContext(Context newBase) { super.attachBaseContext(newBase); } diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java index cfb0297..71863b7 100644 --- a/core/java/android/view/Display.java +++ b/core/java/android/view/Display.java @@ -716,7 +716,7 @@ public final class Display { updateDisplayInfoLocked(); mDisplayInfo.getLogicalMetrics(outMetrics, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, - mDisplayAdjustments.getActivityToken()); + mDisplayAdjustments.getConfiguration()); } } diff --git a/core/java/android/view/DisplayAdjustments.java b/core/java/android/view/DisplayAdjustments.java index 35fb504..272740f 100644 --- a/core/java/android/view/DisplayAdjustments.java +++ b/core/java/android/view/DisplayAdjustments.java @@ -17,7 +17,7 @@ package android.view; import android.content.res.CompatibilityInfo; -import android.os.IBinder; +import android.content.res.Configuration; import java.util.Objects; @@ -28,22 +28,18 @@ public class DisplayAdjustments { public static final DisplayAdjustments DEFAULT_DISPLAY_ADJUSTMENTS = new DisplayAdjustments(); private volatile CompatibilityInfo mCompatInfo = CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO; - private volatile IBinder mActivityToken; + private Configuration mConfiguration = Configuration.EMPTY; public DisplayAdjustments() { } - public DisplayAdjustments(IBinder token) { - mActivityToken = token; + public DisplayAdjustments(Configuration configuration) { + mConfiguration = configuration; } public DisplayAdjustments(DisplayAdjustments daj) { - this (daj.getCompatibilityInfo(), daj.getActivityToken()); - } - - public DisplayAdjustments(CompatibilityInfo compatInfo, IBinder token) { - setCompatibilityInfo(compatInfo); - mActivityToken = token; + setCompatibilityInfo(daj.mCompatInfo); + mConfiguration = daj.mConfiguration; } public void setCompatibilityInfo(CompatibilityInfo compatInfo) { @@ -63,16 +59,16 @@ public class DisplayAdjustments { return mCompatInfo; } - public void setActivityToken(IBinder token) { + public void setConfiguration(Configuration configuration) { if (this == DEFAULT_DISPLAY_ADJUSTMENTS) { throw new IllegalArgumentException( - "setActivityToken: Cannot modify DEFAULT_DISPLAY_ADJUSTMENTS"); + "setConfiguration: Cannot modify DEFAULT_DISPLAY_ADJUSTMENTS"); } - mActivityToken = token; + mConfiguration = configuration; } - public IBinder getActivityToken() { - return mActivityToken; + public Configuration getConfiguration() { + return mConfiguration; } @Override @@ -80,7 +76,7 @@ public class DisplayAdjustments { int hash = 17; hash = hash * 31 + mCompatInfo.hashCode(); if (DEVELOPMENT_RESOURCES_DEPEND_ON_ACTIVITY_TOKEN) { - hash = hash * 31 + (mActivityToken == null ? 0 : mActivityToken.hashCode()); + hash = hash * 31 + (mConfiguration == null ? 0 : mConfiguration.hashCode()); } return hash; } @@ -92,6 +88,6 @@ public class DisplayAdjustments { } DisplayAdjustments daj = (DisplayAdjustments)o; return Objects.equals(daj.mCompatInfo, mCompatInfo) && - Objects.equals(daj.mActivityToken, mActivityToken); + Objects.equals(daj.mConfiguration, mConfiguration); } } diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java index 9feb681..ecf45b4 100644 --- a/core/java/android/view/DisplayInfo.java +++ b/core/java/android/view/DisplayInfo.java @@ -17,7 +17,7 @@ package android.view; import android.content.res.CompatibilityInfo; -import android.os.IBinder; +import android.content.res.Configuration; import android.os.Parcel; import android.os.Parcelable; import android.util.DisplayMetrics; @@ -401,16 +401,17 @@ public final class DisplayInfo implements Parcelable { public void getAppMetrics(DisplayMetrics outMetrics, DisplayAdjustments displayAdjustments) { getMetricsWithSize(outMetrics, displayAdjustments.getCompatibilityInfo(), - displayAdjustments.getActivityToken(), appWidth, appHeight); + displayAdjustments.getConfiguration(), appWidth, appHeight); } - public void getAppMetrics(DisplayMetrics outMetrics, CompatibilityInfo ci, IBinder token) { - getMetricsWithSize(outMetrics, ci, token, appWidth, appHeight); + public void getAppMetrics(DisplayMetrics outMetrics, CompatibilityInfo ci, + Configuration configuration) { + getMetricsWithSize(outMetrics, ci, configuration, appWidth, appHeight); } public void getLogicalMetrics(DisplayMetrics outMetrics, CompatibilityInfo compatInfo, - IBinder token) { - getMetricsWithSize(outMetrics, compatInfo, token, logicalWidth, logicalHeight); + Configuration configuration) { + getMetricsWithSize(outMetrics, compatInfo, configuration, logicalWidth, logicalHeight); } public int getNaturalWidth() { @@ -431,17 +432,24 @@ public final class DisplayInfo implements Parcelable { } private void getMetricsWithSize(DisplayMetrics outMetrics, CompatibilityInfo compatInfo, - IBinder token, int width, int height) { + Configuration configuration, int width, int height) { outMetrics.densityDpi = outMetrics.noncompatDensityDpi = logicalDensityDpi; - outMetrics.noncompatWidthPixels = outMetrics.widthPixels = width; - outMetrics.noncompatHeightPixels = outMetrics.heightPixels = height; - outMetrics.density = outMetrics.noncompatDensity = logicalDensityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE; outMetrics.scaledDensity = outMetrics.noncompatScaledDensity = outMetrics.density; outMetrics.xdpi = outMetrics.noncompatXdpi = physicalXDpi; outMetrics.ydpi = outMetrics.noncompatYdpi = physicalYDpi; + width = (configuration != null + && configuration.screenWidthDp != Configuration.SCREEN_WIDTH_DP_UNDEFINED) + ? (int)((configuration.screenWidthDp * outMetrics.density) + 0.5f) : width; + height = (configuration != null + && configuration.screenHeightDp != Configuration.SCREEN_HEIGHT_DP_UNDEFINED) + ? (int)((configuration.screenHeightDp * outMetrics.density) + 0.5f) : height; + + outMetrics.noncompatWidthPixels = outMetrics.widthPixels = width; + outMetrics.noncompatHeightPixels = outMetrics.heightPixels = height; + if (!compatInfo.equals(CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO)) { compatInfo.applyToDisplayMetrics(outMetrics); } diff --git a/core/java/android/view/DisplayListCanvas.java b/core/java/android/view/DisplayListCanvas.java new file mode 100644 index 0000000..3caf6f0 --- /dev/null +++ b/core/java/android/view/DisplayListCanvas.java @@ -0,0 +1,344 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.annotation.NonNull; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.CanvasProperty; +import android.graphics.NinePatch; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Picture; +import android.graphics.Rect; +import android.graphics.RectF; +import android.util.Pools.SynchronizedPool; + +/** + * An implementation of a GL canvas that records drawing operations. + * This is intended for use with a DisplayList. This class keeps a list of all the Paint and + * Bitmap objects that it draws, preventing the backing memory of Bitmaps from being freed while + * the DisplayList is still holding a native reference to the memory. + * + * @hide + */ +public class DisplayListCanvas extends Canvas { + // The recording canvas pool should be large enough to handle a deeply nested + // view hierarchy because display lists are generated recursively. + private static final int POOL_LIMIT = 25; + + private static final SynchronizedPool<DisplayListCanvas> sPool = + new SynchronizedPool<DisplayListCanvas>(POOL_LIMIT); + + RenderNode mNode; + private int mWidth; + private int mHeight; + + + static DisplayListCanvas obtain(@NonNull RenderNode node) { + if (node == null) throw new IllegalArgumentException("node cannot be null"); + DisplayListCanvas canvas = sPool.acquire(); + if (canvas == null) { + canvas = new DisplayListCanvas(); + } + canvas.mNode = node; + return canvas; + } + + void recycle() { + mNode = null; + sPool.release(this); + } + + long finishRecording() { + return nFinishRecording(mNativeCanvasWrapper); + } + + @Override + public boolean isRecordingFor(Object o) { + return o == mNode; + } + + /////////////////////////////////////////////////////////////////////////// + // JNI + /////////////////////////////////////////////////////////////////////////// + + private static native boolean nIsAvailable(); + private static boolean sIsAvailable = nIsAvailable(); + + static boolean isAvailable() { + return sIsAvailable; + } + + /////////////////////////////////////////////////////////////////////////// + // Constructors + /////////////////////////////////////////////////////////////////////////// + + private DisplayListCanvas() { + super(nCreateDisplayListRenderer()); + } + + private static native long nCreateDisplayListRenderer(); + + public static void setProperty(String name, String value) { + nSetProperty(name, value); + } + + private static native void nSetProperty(String name, String value); + + /////////////////////////////////////////////////////////////////////////// + // Canvas management + /////////////////////////////////////////////////////////////////////////// + + @Override + public boolean isHardwareAccelerated() { + return true; + } + + @Override + public void setBitmap(Bitmap bitmap) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isOpaque() { + return false; + } + + @Override + public int getWidth() { + return mWidth; + } + + @Override + public int getHeight() { + return mHeight; + } + + @Override + public int getMaximumBitmapWidth() { + return nGetMaximumTextureWidth(); + } + + @Override + public int getMaximumBitmapHeight() { + return nGetMaximumTextureHeight(); + } + + private static native int nGetMaximumTextureWidth(); + private static native int nGetMaximumTextureHeight(); + + /** + * Returns the native OpenGLRenderer object. + */ + long getRenderer() { + return mNativeCanvasWrapper; + } + + /////////////////////////////////////////////////////////////////////////// + // Setup + /////////////////////////////////////////////////////////////////////////// + + @Override + public void setViewport(int width, int height) { + mWidth = width; + mHeight = height; + + nSetViewport(mNativeCanvasWrapper, width, height); + } + + private static native void nSetViewport(long renderer, + int width, int height); + + @Override + public void setHighContrastText(boolean highContrastText) { + nSetHighContrastText(mNativeCanvasWrapper, highContrastText); + } + + private static native void nSetHighContrastText(long renderer, boolean highContrastText); + + @Override + public void insertReorderBarrier() { + nInsertReorderBarrier(mNativeCanvasWrapper, true); + } + + @Override + public void insertInorderBarrier() { + nInsertReorderBarrier(mNativeCanvasWrapper, false); + } + + private static native void nInsertReorderBarrier(long renderer, boolean enableReorder); + + /** + * Invoked before any drawing operation is performed in this canvas. + * + * @param dirty The dirty rectangle to update, can be null. + */ + public void onPreDraw(Rect dirty) { + if (dirty != null) { + nPrepareDirty(mNativeCanvasWrapper, dirty.left, dirty.top, dirty.right, dirty.bottom); + } else { + nPrepare(mNativeCanvasWrapper); + } + } + + private static native void nPrepare(long renderer); + private static native void nPrepareDirty(long renderer, int left, int top, int right, int bottom); + + /** + * Invoked after all drawing operation have been performed. + */ + public void onPostDraw() { + nFinish(mNativeCanvasWrapper); + } + + private static native void nFinish(long renderer); + + /////////////////////////////////////////////////////////////////////////// + // Functor + /////////////////////////////////////////////////////////////////////////// + + /** + * Calls the function specified with the drawGLFunction function pointer. This is + * functionality used by webkit for calling into their renderer from our display lists. + * This function may return true if an invalidation is needed after the call. + * + * @param drawGLFunction A native function pointer + */ + public void callDrawGLFunction2(long drawGLFunction) { + nCallDrawGLFunction(mNativeCanvasWrapper, drawGLFunction); + } + + private static native void nCallDrawGLFunction(long renderer, long drawGLFunction); + + /////////////////////////////////////////////////////////////////////////// + // Display list + /////////////////////////////////////////////////////////////////////////// + + protected static native long nFinishRecording(long renderer); + + /** + * Draws the specified display list onto this canvas. The display list can only + * be drawn if {@link android.view.RenderNode#isValid()} returns true. + * + * @param renderNode The RenderNode to replay. + */ + public void drawRenderNode(RenderNode renderNode) { + drawRenderNode(renderNode, RenderNode.FLAG_CLIP_CHILDREN); + } + + /** + * Draws the specified display list onto this canvas. + * + * @param renderNode The RenderNode to replay. + * @param flags Optional flags about drawing, see {@link RenderNode} for + * the possible flags. + */ + public void drawRenderNode(RenderNode renderNode, int flags) { + nDrawRenderNode(mNativeCanvasWrapper, renderNode.getNativeDisplayList(), flags); + } + + private static native void nDrawRenderNode(long renderer, long renderNode, + int flags); + + /////////////////////////////////////////////////////////////////////////// + // Hardware layer + /////////////////////////////////////////////////////////////////////////// + + /** + * Draws the specified layer onto this canvas. + * + * @param layer The layer to composite on this canvas + * @param x The left coordinate of the layer + * @param y The top coordinate of the layer + * @param paint The paint used to draw the layer + */ + void drawHardwareLayer(HardwareLayer layer, float x, float y, Paint paint) { + layer.setLayerPaint(paint); + nDrawLayer(mNativeCanvasWrapper, layer.getLayerHandle(), x, y); + } + + private static native void nDrawLayer(long renderer, long layer, float x, float y); + + /////////////////////////////////////////////////////////////////////////// + // Drawing + /////////////////////////////////////////////////////////////////////////// + + // TODO: move to Canvas.java + @Override + public void drawPatch(NinePatch patch, Rect dst, Paint paint) { + Bitmap bitmap = patch.getBitmap(); + throwIfCannotDraw(bitmap); + final long nativePaint = paint == null ? 0 : paint.getNativeInstance(); + nDrawPatch(mNativeCanvasWrapper, bitmap.getSkBitmap(), patch.mNativeChunk, + dst.left, dst.top, dst.right, dst.bottom, nativePaint); + } + + // TODO: move to Canvas.java + @Override + public void drawPatch(NinePatch patch, RectF dst, Paint paint) { + Bitmap bitmap = patch.getBitmap(); + throwIfCannotDraw(bitmap); + final long nativePaint = paint == null ? 0 : paint.getNativeInstance(); + nDrawPatch(mNativeCanvasWrapper, bitmap.getSkBitmap(), patch.mNativeChunk, + dst.left, dst.top, dst.right, dst.bottom, nativePaint); + } + + private static native void nDrawPatch(long renderer, long bitmap, long chunk, + float left, float top, float right, float bottom, long paint); + + public void drawCircle(CanvasProperty<Float> cx, CanvasProperty<Float> cy, + CanvasProperty<Float> radius, CanvasProperty<Paint> paint) { + nDrawCircle(mNativeCanvasWrapper, cx.getNativeContainer(), cy.getNativeContainer(), + radius.getNativeContainer(), paint.getNativeContainer()); + } + + private static native void nDrawCircle(long renderer, long propCx, + long propCy, long propRadius, long propPaint); + + public void drawRoundRect(CanvasProperty<Float> left, CanvasProperty<Float> top, + CanvasProperty<Float> right, CanvasProperty<Float> bottom, CanvasProperty<Float> rx, + CanvasProperty<Float> ry, CanvasProperty<Paint> paint) { + nDrawRoundRect(mNativeCanvasWrapper, left.getNativeContainer(), top.getNativeContainer(), + right.getNativeContainer(), bottom.getNativeContainer(), + rx.getNativeContainer(), ry.getNativeContainer(), + paint.getNativeContainer()); + } + + private static native void nDrawRoundRect(long renderer, long propLeft, long propTop, + long propRight, long propBottom, long propRx, long propRy, long propPaint); + + // TODO: move this optimization to Canvas.java + @Override + public void drawPath(Path path, Paint paint) { + if (path.isSimplePath) { + if (path.rects != null) { + nDrawRects(mNativeCanvasWrapper, path.rects.mNativeRegion, paint.getNativeInstance()); + } + } else { + super.drawPath(path, paint); + } + } + + private static native void nDrawRects(long renderer, long region, long paint); + + @Override + public void drawPicture(Picture picture) { + picture.endRecording(); + // TODO: Implement rendering + } +} diff --git a/core/java/android/view/FrameInfo.java b/core/java/android/view/FrameInfo.java new file mode 100644 index 0000000..c79547c --- /dev/null +++ b/core/java/android/view/FrameInfo.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Class that contains all the timing information for the current frame. This + * is used in conjunction with the hardware renderer to provide + * continous-monitoring jank events + * + * All times in nanoseconds from CLOCK_MONOTONIC/System.nanoTime() + * + * To minimize overhead from System.nanoTime() calls we infer durations of + * things by knowing the ordering of the events. For example, to know how + * long layout & measure took it's displayListRecordStart - performTraversalsStart. + * + * These constants must be kept in sync with FrameInfo.h in libhwui and are + * used for indexing into AttachInfo's mFrameInfo long[], which is intended + * to be quick to pass down to native via JNI, hence a pre-packed format + * + * @hide + */ +final class FrameInfo { + + long[] mFrameInfo = new long[9]; + + // Various flags set to provide extra metadata about the current frame + private static final int FLAGS = 0; + + // Is this the first-draw following a window layout? + public static final long FLAG_WINDOW_LAYOUT_CHANGED = 1; + + @IntDef(flag = true, value = { + FLAG_WINDOW_LAYOUT_CHANGED }) + @Retention(RetentionPolicy.SOURCE) + public @interface FrameInfoFlags {} + + // The intended vsync time, unadjusted by jitter + private static final int INTENDED_VSYNC = 1; + + // Jitter-adjusted vsync time, this is what was used as input into the + // animation & drawing system + private static final int VSYNC = 2; + + // The time of the oldest input event + private static final int OLDEST_INPUT_EVENT = 3; + + // The time of the newest input event + private static final int NEWEST_INPUT_EVENT = 4; + + // When input event handling started + private static final int HANDLE_INPUT_START = 5; + + // When animation evaluations started + private static final int ANIMATION_START = 6; + + // When ViewRootImpl#performTraversals() started + private static final int PERFORM_TRAVERSALS_START = 7; + + // When View:draw() started + private static final int DRAW_START = 8; + + public void setVsync(long intendedVsync, long usedVsync) { + mFrameInfo[INTENDED_VSYNC] = intendedVsync; + mFrameInfo[VSYNC] = usedVsync; + mFrameInfo[OLDEST_INPUT_EVENT] = Long.MAX_VALUE; + mFrameInfo[NEWEST_INPUT_EVENT] = 0; + mFrameInfo[FLAGS] = 0; + } + + public void updateInputEventTime(long inputEventTime, long inputEventOldestTime) { + if (inputEventOldestTime < mFrameInfo[OLDEST_INPUT_EVENT]) { + mFrameInfo[OLDEST_INPUT_EVENT] = inputEventOldestTime; + } + if (inputEventTime > mFrameInfo[NEWEST_INPUT_EVENT]) { + mFrameInfo[NEWEST_INPUT_EVENT] = inputEventTime; + } + } + + public void markInputHandlingStart() { + mFrameInfo[HANDLE_INPUT_START] = System.nanoTime(); + } + + public void markAnimationsStart() { + mFrameInfo[ANIMATION_START] = System.nanoTime(); + } + + public void markPerformTraversalsStart() { + mFrameInfo[PERFORM_TRAVERSALS_START] = System.nanoTime(); + } + + public void markDrawStart() { + mFrameInfo[DRAW_START] = System.nanoTime(); + } + + public void addFlags(@FrameInfoFlags long flags) { + mFrameInfo[FLAGS] |= flags; + } + +} diff --git a/core/java/android/view/FrameStats.java b/core/java/android/view/FrameStats.java index b3ac1db..3fbe6fe 100644 --- a/core/java/android/view/FrameStats.java +++ b/core/java/android/view/FrameStats.java @@ -16,9 +16,6 @@ package android.view; -import android.os.Parcel; -import android.os.Parcelable; - /** * This is the base class for frame statistics. */ diff --git a/core/java/android/view/GLES20Canvas.java b/core/java/android/view/GLES20Canvas.java deleted file mode 100644 index 60a489b..0000000 --- a/core/java/android/view/GLES20Canvas.java +++ /dev/null @@ -1,999 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.view; - -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.CanvasProperty; -import android.graphics.DrawFilter; -import android.graphics.Matrix; -import android.graphics.NinePatch; -import android.graphics.Paint; -import android.graphics.PaintFlagsDrawFilter; -import android.graphics.Path; -import android.graphics.Picture; -import android.graphics.PorterDuff; -import android.graphics.Rect; -import android.graphics.RectF; -import android.graphics.Region; -import android.graphics.Shader; -import android.graphics.TemporaryBuffer; -import android.text.GraphicsOperations; -import android.text.SpannableString; -import android.text.SpannedString; -import android.text.TextUtils; - -/** - * An implementation of Canvas on top of OpenGL ES 2.0. - */ -class GLES20Canvas extends HardwareCanvas { - private final boolean mOpaque; - protected long mRenderer; - - // The native renderer will be destroyed when this object dies. - // DO NOT overwrite this reference once it is set. - @SuppressWarnings({"unused", "FieldCanBeLocal"}) - private CanvasFinalizer mFinalizer; - - private int mWidth; - private int mHeight; - - private float[] mPoint; - private float[] mLine; - - private Rect mClipBounds; - private RectF mPathBounds; - - private DrawFilter mFilter; - - /////////////////////////////////////////////////////////////////////////// - // JNI - /////////////////////////////////////////////////////////////////////////// - - private static native boolean nIsAvailable(); - private static boolean sIsAvailable = nIsAvailable(); - - static boolean isAvailable() { - return sIsAvailable; - } - - /////////////////////////////////////////////////////////////////////////// - // Constructors - /////////////////////////////////////////////////////////////////////////// - - // TODO: Merge with GLES20RecordingCanvas - protected GLES20Canvas() { - mOpaque = false; - mRenderer = nCreateDisplayListRenderer(); - setupFinalizer(); - } - - private void setupFinalizer() { - if (mRenderer == 0) { - throw new IllegalStateException("Could not create GLES20Canvas renderer"); - } else { - mFinalizer = new CanvasFinalizer(mRenderer); - } - } - - private static native long nCreateDisplayListRenderer(); - private static native void nResetDisplayListRenderer(long renderer); - private static native void nDestroyRenderer(long renderer); - - private static final class CanvasFinalizer { - private final long mRenderer; - - public CanvasFinalizer(long renderer) { - mRenderer = renderer; - } - - @Override - protected void finalize() throws Throwable { - try { - nDestroyRenderer(mRenderer); - } finally { - super.finalize(); - } - } - } - - public static void setProperty(String name, String value) { - nSetProperty(name, value); - } - - private static native void nSetProperty(String name, String value); - - /////////////////////////////////////////////////////////////////////////// - // Canvas management - /////////////////////////////////////////////////////////////////////////// - - @Override - public boolean isOpaque() { - return mOpaque; - } - - @Override - public int getWidth() { - return mWidth; - } - - @Override - public int getHeight() { - return mHeight; - } - - @Override - public int getMaximumBitmapWidth() { - return nGetMaximumTextureWidth(); - } - - @Override - public int getMaximumBitmapHeight() { - return nGetMaximumTextureHeight(); - } - - private static native int nGetMaximumTextureWidth(); - private static native int nGetMaximumTextureHeight(); - - /** - * Returns the native OpenGLRenderer object. - */ - long getRenderer() { - return mRenderer; - } - - /////////////////////////////////////////////////////////////////////////// - // Setup - /////////////////////////////////////////////////////////////////////////// - - @Override - public void setViewport(int width, int height) { - mWidth = width; - mHeight = height; - - nSetViewport(mRenderer, width, height); - } - - private static native void nSetViewport(long renderer, - int width, int height); - - @Override - public void setHighContrastText(boolean highContrastText) { - nSetHighContrastText(mRenderer, highContrastText); - } - - private static native void nSetHighContrastText(long renderer, boolean highContrastText); - - @Override - public void insertReorderBarrier() { - nInsertReorderBarrier(mRenderer, true); - } - - @Override - public void insertInorderBarrier() { - nInsertReorderBarrier(mRenderer, false); - } - - private static native void nInsertReorderBarrier(long renderer, boolean enableReorder); - - @Override - public int onPreDraw(Rect dirty) { - if (dirty != null) { - return nPrepareDirty(mRenderer, dirty.left, dirty.top, dirty.right, dirty.bottom, - mOpaque); - } else { - return nPrepare(mRenderer, mOpaque); - } - } - - private static native int nPrepare(long renderer, boolean opaque); - private static native int nPrepareDirty(long renderer, int left, int top, int right, int bottom, - boolean opaque); - - @Override - public void onPostDraw() { - nFinish(mRenderer); - } - - private static native void nFinish(long renderer); - - /////////////////////////////////////////////////////////////////////////// - // Functor - /////////////////////////////////////////////////////////////////////////// - - @Override - public int callDrawGLFunction2(long drawGLFunction) { - return nCallDrawGLFunction(mRenderer, drawGLFunction); - } - - private static native int nCallDrawGLFunction(long renderer, long drawGLFunction); - - /////////////////////////////////////////////////////////////////////////// - // Display list - /////////////////////////////////////////////////////////////////////////// - - protected static native long nFinishRecording(long renderer); - - @Override - public int drawRenderNode(RenderNode renderNode, Rect dirty, int flags) { - return nDrawRenderNode(mRenderer, renderNode.getNativeDisplayList(), dirty, flags); - } - - private static native int nDrawRenderNode(long renderer, long renderNode, - Rect dirty, int flags); - - /////////////////////////////////////////////////////////////////////////// - // Hardware layer - /////////////////////////////////////////////////////////////////////////// - - void drawHardwareLayer(HardwareLayer layer, float x, float y, Paint paint) { - layer.setLayerPaint(paint); - nDrawLayer(mRenderer, layer.getLayerHandle(), x, y); - } - - private static native void nDrawLayer(long renderer, long layer, float x, float y); - - /////////////////////////////////////////////////////////////////////////// - // Support - /////////////////////////////////////////////////////////////////////////// - - private Rect getInternalClipBounds() { - if (mClipBounds == null) mClipBounds = new Rect(); - return mClipBounds; - } - - - private RectF getPathBounds() { - if (mPathBounds == null) mPathBounds = new RectF(); - return mPathBounds; - } - - private float[] getPointStorage() { - if (mPoint == null) mPoint = new float[2]; - return mPoint; - } - - private float[] getLineStorage() { - if (mLine == null) mLine = new float[4]; - return mLine; - } - - /////////////////////////////////////////////////////////////////////////// - // Clipping - /////////////////////////////////////////////////////////////////////////// - - @Override - public boolean clipPath(Path path) { - return nClipPath(mRenderer, path.mNativePath, Region.Op.INTERSECT.nativeInt); - } - - @Override - public boolean clipPath(Path path, Region.Op op) { - return nClipPath(mRenderer, path.mNativePath, op.nativeInt); - } - - private static native boolean nClipPath(long renderer, long path, int op); - - @Override - public boolean clipRect(float left, float top, float right, float bottom) { - return nClipRect(mRenderer, left, top, right, bottom, Region.Op.INTERSECT.nativeInt); - } - - private static native boolean nClipRect(long renderer, float left, float top, - float right, float bottom, int op); - - @Override - public boolean clipRect(float left, float top, float right, float bottom, Region.Op op) { - return nClipRect(mRenderer, left, top, right, bottom, op.nativeInt); - } - - @Override - public boolean clipRect(int left, int top, int right, int bottom) { - return nClipRect(mRenderer, left, top, right, bottom, Region.Op.INTERSECT.nativeInt); - } - - private static native boolean nClipRect(long renderer, int left, int top, - int right, int bottom, int op); - - @Override - public boolean clipRect(Rect rect) { - return nClipRect(mRenderer, rect.left, rect.top, rect.right, rect.bottom, - Region.Op.INTERSECT.nativeInt); - } - - @Override - public boolean clipRect(Rect rect, Region.Op op) { - return nClipRect(mRenderer, rect.left, rect.top, rect.right, rect.bottom, op.nativeInt); - } - - @Override - public boolean clipRect(RectF rect) { - return nClipRect(mRenderer, rect.left, rect.top, rect.right, rect.bottom, - Region.Op.INTERSECT.nativeInt); - } - - @Override - public boolean clipRect(RectF rect, Region.Op op) { - return nClipRect(mRenderer, rect.left, rect.top, rect.right, rect.bottom, op.nativeInt); - } - - @Override - public boolean clipRegion(Region region) { - return nClipRegion(mRenderer, region.mNativeRegion, Region.Op.INTERSECT.nativeInt); - } - - @Override - public boolean clipRegion(Region region, Region.Op op) { - return nClipRegion(mRenderer, region.mNativeRegion, op.nativeInt); - } - - private static native boolean nClipRegion(long renderer, long region, int op); - - @Override - public boolean getClipBounds(Rect bounds) { - return nGetClipBounds(mRenderer, bounds); - } - - private static native boolean nGetClipBounds(long renderer, Rect bounds); - - @Override - public boolean quickReject(float left, float top, float right, float bottom, EdgeType type) { - return nQuickReject(mRenderer, left, top, right, bottom); - } - - private static native boolean nQuickReject(long renderer, float left, float top, - float right, float bottom); - - @Override - public boolean quickReject(Path path, EdgeType type) { - RectF pathBounds = getPathBounds(); - path.computeBounds(pathBounds, true); - return nQuickReject(mRenderer, pathBounds.left, pathBounds.top, - pathBounds.right, pathBounds.bottom); - } - - @Override - public boolean quickReject(RectF rect, EdgeType type) { - return nQuickReject(mRenderer, rect.left, rect.top, rect.right, rect.bottom); - } - - /////////////////////////////////////////////////////////////////////////// - // Transformations - /////////////////////////////////////////////////////////////////////////// - - @Override - public void translate(float dx, float dy) { - if (dx != 0.0f || dy != 0.0f) nTranslate(mRenderer, dx, dy); - } - - private static native void nTranslate(long renderer, float dx, float dy); - - @Override - public void skew(float sx, float sy) { - nSkew(mRenderer, sx, sy); - } - - private static native void nSkew(long renderer, float sx, float sy); - - @Override - public void rotate(float degrees) { - nRotate(mRenderer, degrees); - } - - private static native void nRotate(long renderer, float degrees); - - @Override - public void scale(float sx, float sy) { - nScale(mRenderer, sx, sy); - } - - private static native void nScale(long renderer, float sx, float sy); - - @Override - public void setMatrix(Matrix matrix) { - nSetMatrix(mRenderer, matrix == null ? 0 : matrix.native_instance); - } - - private static native void nSetMatrix(long renderer, long matrix); - - @SuppressWarnings("deprecation") - @Override - public void getMatrix(Matrix matrix) { - nGetMatrix(mRenderer, matrix.native_instance); - } - - private static native void nGetMatrix(long renderer, long matrix); - - @Override - public void concat(Matrix matrix) { - if (matrix != null) nConcatMatrix(mRenderer, matrix.native_instance); - } - - private static native void nConcatMatrix(long renderer, long matrix); - - /////////////////////////////////////////////////////////////////////////// - // State management - /////////////////////////////////////////////////////////////////////////// - - @Override - public int save() { - return nSave(mRenderer, Canvas.CLIP_SAVE_FLAG | Canvas.MATRIX_SAVE_FLAG); - } - - @Override - public int save(int saveFlags) { - return nSave(mRenderer, saveFlags); - } - - private static native int nSave(long renderer, int flags); - - @Override - public int saveLayer(RectF bounds, Paint paint, int saveFlags) { - if (bounds != null) { - return saveLayer(bounds.left, bounds.top, bounds.right, bounds.bottom, paint, saveFlags); - } - - final long nativePaint = paint == null ? 0 : paint.mNativePaint; - return nSaveLayer(mRenderer, nativePaint, saveFlags); - } - - private static native int nSaveLayer(long renderer, long paint, int saveFlags); - - @Override - public int saveLayer(float left, float top, float right, float bottom, Paint paint, - int saveFlags) { - if (left < right && top < bottom) { - final long nativePaint = paint == null ? 0 : paint.mNativePaint; - return nSaveLayer(mRenderer, left, top, right, bottom, nativePaint, saveFlags); - } - return save(saveFlags); - } - - private static native int nSaveLayer(long renderer, float left, float top, - float right, float bottom, long paint, int saveFlags); - - @Override - public int saveLayerAlpha(RectF bounds, int alpha, int saveFlags) { - if (bounds != null) { - return saveLayerAlpha(bounds.left, bounds.top, bounds.right, bounds.bottom, - alpha, saveFlags); - } - return nSaveLayerAlpha(mRenderer, alpha, saveFlags); - } - - private static native int nSaveLayerAlpha(long renderer, int alpha, int saveFlags); - - @Override - public int saveLayerAlpha(float left, float top, float right, float bottom, int alpha, - int saveFlags) { - if (left < right && top < bottom) { - return nSaveLayerAlpha(mRenderer, left, top, right, bottom, alpha, saveFlags); - } - return save(saveFlags); - } - - private static native int nSaveLayerAlpha(long renderer, float left, float top, float right, - float bottom, int alpha, int saveFlags); - - @Override - public void restore() { - nRestore(mRenderer); - } - - private static native void nRestore(long renderer); - - @Override - public void restoreToCount(int saveCount) { - nRestoreToCount(mRenderer, saveCount); - } - - private static native void nRestoreToCount(long renderer, int saveCount); - - @Override - public int getSaveCount() { - return nGetSaveCount(mRenderer); - } - - private static native int nGetSaveCount(long renderer); - - /////////////////////////////////////////////////////////////////////////// - // Filtering - /////////////////////////////////////////////////////////////////////////// - - @Override - public void setDrawFilter(DrawFilter filter) { - mFilter = filter; - if (filter == null) { - nResetPaintFilter(mRenderer); - } else if (filter instanceof PaintFlagsDrawFilter) { - PaintFlagsDrawFilter flagsFilter = (PaintFlagsDrawFilter) filter; - nSetupPaintFilter(mRenderer, flagsFilter.clearBits, flagsFilter.setBits); - } - } - - private static native void nResetPaintFilter(long renderer); - private static native void nSetupPaintFilter(long renderer, int clearBits, int setBits); - - @Override - public DrawFilter getDrawFilter() { - return mFilter; - } - - /////////////////////////////////////////////////////////////////////////// - // Drawing - /////////////////////////////////////////////////////////////////////////// - - @Override - public void drawArc(float left, float top, float right, float bottom, - float startAngle, float sweepAngle, boolean useCenter, Paint paint) { - nDrawArc(mRenderer, left, top, right, bottom, - startAngle, sweepAngle, useCenter, paint.mNativePaint); - } - - private static native void nDrawArc(long renderer, float left, float top, - float right, float bottom, float startAngle, float sweepAngle, - boolean useCenter, long paint); - - @Override - public void drawARGB(int a, int r, int g, int b) { - drawColor((a & 0xFF) << 24 | (r & 0xFF) << 16 | (g & 0xFF) << 8 | (b & 0xFF)); - } - - @Override - public void drawPatch(NinePatch patch, Rect dst, Paint paint) { - Bitmap bitmap = patch.getBitmap(); - throwIfCannotDraw(bitmap); - final long nativePaint = paint == null ? 0 : paint.mNativePaint; - nDrawPatch(mRenderer, bitmap.mNativeBitmap, patch.mNativeChunk, - dst.left, dst.top, dst.right, dst.bottom, nativePaint); - } - - @Override - public void drawPatch(NinePatch patch, RectF dst, Paint paint) { - Bitmap bitmap = patch.getBitmap(); - throwIfCannotDraw(bitmap); - final long nativePaint = paint == null ? 0 : paint.mNativePaint; - nDrawPatch(mRenderer, bitmap.mNativeBitmap, patch.mNativeChunk, - dst.left, dst.top, dst.right, dst.bottom, nativePaint); - } - - private static native void nDrawPatch(long renderer, long bitmap, long chunk, - float left, float top, float right, float bottom, long paint); - - @Override - public void drawBitmap(Bitmap bitmap, float left, float top, Paint paint) { - throwIfCannotDraw(bitmap); - final long nativePaint = paint == null ? 0 : paint.mNativePaint; - nDrawBitmap(mRenderer, bitmap.mNativeBitmap, left, top, nativePaint); - } - - private static native void nDrawBitmap(long renderer, long bitmap, float left, - float top, long paint); - - @Override - public void drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint) { - throwIfCannotDraw(bitmap); - final long nativePaint = paint == null ? 0 : paint.mNativePaint; - nDrawBitmap(mRenderer, bitmap.mNativeBitmap, matrix.native_instance, nativePaint); - } - - private static native void nDrawBitmap(long renderer, long bitmap, - long matrix, long paint); - - @Override - public void drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint) { - throwIfCannotDraw(bitmap); - final long nativePaint = paint == null ? 0 : paint.mNativePaint; - - int left, top, right, bottom; - if (src == null) { - left = top = 0; - right = bitmap.getWidth(); - bottom = bitmap.getHeight(); - } else { - left = src.left; - right = src.right; - top = src.top; - bottom = src.bottom; - } - - nDrawBitmap(mRenderer, bitmap.mNativeBitmap, left, top, right, bottom, - dst.left, dst.top, dst.right, dst.bottom, nativePaint); - } - - @Override - public void drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint) { - throwIfCannotDraw(bitmap); - final long nativePaint = paint == null ? 0 : paint.mNativePaint; - - float left, top, right, bottom; - if (src == null) { - left = top = 0; - right = bitmap.getWidth(); - bottom = bitmap.getHeight(); - } else { - left = src.left; - right = src.right; - top = src.top; - bottom = src.bottom; - } - - nDrawBitmap(mRenderer, bitmap.mNativeBitmap, left, top, right, bottom, - dst.left, dst.top, dst.right, dst.bottom, nativePaint); - } - - private static native void nDrawBitmap(long renderer, long bitmap, - float srcLeft, float srcTop, float srcRight, float srcBottom, - float left, float top, float right, float bottom, long paint); - - @Override - public void drawBitmap(int[] colors, int offset, int stride, float x, float y, - int width, int height, boolean hasAlpha, Paint paint) { - if (width < 0) { - throw new IllegalArgumentException("width must be >= 0"); - } - - if (height < 0) { - throw new IllegalArgumentException("height must be >= 0"); - } - - if (Math.abs(stride) < width) { - throw new IllegalArgumentException("abs(stride) must be >= width"); - } - - int lastScanline = offset + (height - 1) * stride; - int length = colors.length; - - if (offset < 0 || (offset + width > length) || lastScanline < 0 || - (lastScanline + width > length)) { - throw new ArrayIndexOutOfBoundsException(); - } - - final long nativePaint = paint == null ? 0 : paint.mNativePaint; - nDrawBitmap(mRenderer, colors, offset, stride, x, y, - width, height, hasAlpha, nativePaint); - } - - private static native void nDrawBitmap(long renderer, int[] colors, int offset, int stride, - float x, float y, int width, int height, boolean hasAlpha, long nativePaint); - - @Override - public void drawBitmap(int[] colors, int offset, int stride, int x, int y, - int width, int height, boolean hasAlpha, Paint paint) { - drawBitmap(colors, offset, stride, (float) x, (float) y, width, height, hasAlpha, paint); - } - - @Override - public void drawBitmapMesh(Bitmap bitmap, int meshWidth, int meshHeight, float[] verts, - int vertOffset, int[] colors, int colorOffset, Paint paint) { - throwIfCannotDraw(bitmap); - if (meshWidth < 0 || meshHeight < 0 || vertOffset < 0 || colorOffset < 0) { - throw new ArrayIndexOutOfBoundsException(); - } - - if (meshWidth == 0 || meshHeight == 0) { - return; - } - - final int count = (meshWidth + 1) * (meshHeight + 1); - checkRange(verts.length, vertOffset, count * 2); - - if (colors != null) { - checkRange(colors.length, colorOffset, count); - } - - final long nativePaint = paint == null ? 0 : paint.mNativePaint; - nDrawBitmapMesh(mRenderer, bitmap.mNativeBitmap, meshWidth, meshHeight, - verts, vertOffset, colors, colorOffset, nativePaint); - } - - private static native void nDrawBitmapMesh(long renderer, long bitmap, - int meshWidth, int meshHeight, float[] verts, int vertOffset, - int[] colors, int colorOffset, long paint); - - @Override - public void drawCircle(float cx, float cy, float radius, Paint paint) { - nDrawCircle(mRenderer, cx, cy, radius, paint.mNativePaint); - } - - private static native void nDrawCircle(long renderer, float cx, float cy, - float radius, long paint); - - @Override - public void drawCircle(CanvasProperty<Float> cx, CanvasProperty<Float> cy, - CanvasProperty<Float> radius, CanvasProperty<Paint> paint) { - nDrawCircle(mRenderer, cx.getNativeContainer(), cy.getNativeContainer(), - radius.getNativeContainer(), paint.getNativeContainer()); - } - - private static native void nDrawCircle(long renderer, long propCx, - long propCy, long propRadius, long propPaint); - - @Override - public void drawRoundRect(CanvasProperty<Float> left, CanvasProperty<Float> top, - CanvasProperty<Float> right, CanvasProperty<Float> bottom, CanvasProperty<Float> rx, - CanvasProperty<Float> ry, CanvasProperty<Paint> paint) { - nDrawRoundRect(mRenderer, left.getNativeContainer(), top.getNativeContainer(), - right.getNativeContainer(), bottom.getNativeContainer(), - rx.getNativeContainer(), ry.getNativeContainer(), - paint.getNativeContainer()); - } - - private static native void nDrawRoundRect(long renderer, long propLeft, long propTop, - long propRight, long propBottom, long propRx, long propRy, long propPaint); - - @Override - public void drawColor(int color) { - drawColor(color, PorterDuff.Mode.SRC_OVER); - } - - @Override - public void drawColor(int color, PorterDuff.Mode mode) { - nDrawColor(mRenderer, color, mode.nativeInt); - } - - private static native void nDrawColor(long renderer, int color, int mode); - - @Override - public void drawLine(float startX, float startY, float stopX, float stopY, Paint paint) { - float[] line = getLineStorage(); - line[0] = startX; - line[1] = startY; - line[2] = stopX; - line[3] = stopY; - drawLines(line, 0, 4, paint); - } - - @Override - public void drawLines(float[] pts, int offset, int count, Paint paint) { - if (count < 4) return; - - if ((offset | count) < 0 || offset + count > pts.length) { - throw new IllegalArgumentException("The lines array must contain 4 elements per line."); - } - nDrawLines(mRenderer, pts, offset, count, paint.mNativePaint); - } - - private static native void nDrawLines(long renderer, float[] points, - int offset, int count, long paint); - - @Override - public void drawLines(float[] pts, Paint paint) { - drawLines(pts, 0, pts.length, paint); - } - - @Override - public void drawOval(float left, float top, float right, float bottom, Paint paint) { - nDrawOval(mRenderer, left, top, right, bottom, paint.mNativePaint); - } - - private static native void nDrawOval(long renderer, float left, float top, - float right, float bottom, long paint); - - @Override - public void drawPaint(Paint paint) { - final Rect r = getInternalClipBounds(); - nGetClipBounds(mRenderer, r); - drawRect(r.left, r.top, r.right, r.bottom, paint); - } - - @Override - public void drawPath(Path path, Paint paint) { - if (path.isSimplePath) { - if (path.rects != null) { - nDrawRects(mRenderer, path.rects.mNativeRegion, paint.mNativePaint); - } - } else { - nDrawPath(mRenderer, path.mNativePath, paint.mNativePaint); - } - } - - private static native void nDrawPath(long renderer, long path, long paint); - private static native void nDrawRects(long renderer, long region, long paint); - - @Override - public void drawPicture(Picture picture) { - picture.endRecording(); - // TODO: Implement rendering - } - - @Override - public void drawPoint(float x, float y, Paint paint) { - float[] point = getPointStorage(); - point[0] = x; - point[1] = y; - drawPoints(point, 0, 2, paint); - } - - @Override - public void drawPoints(float[] pts, Paint paint) { - drawPoints(pts, 0, pts.length, paint); - } - - @Override - public void drawPoints(float[] pts, int offset, int count, Paint paint) { - if (count < 2) return; - - nDrawPoints(mRenderer, pts, offset, count, paint.mNativePaint); - } - - private static native void nDrawPoints(long renderer, float[] points, - int offset, int count, long paint); - - // Note: drawPosText just uses implementation in Canvas - - @Override - public void drawRect(float left, float top, float right, float bottom, Paint paint) { - if (left == right || top == bottom) return; - nDrawRect(mRenderer, left, top, right, bottom, paint.mNativePaint); - } - - private static native void nDrawRect(long renderer, float left, float top, - float right, float bottom, long paint); - - @Override - public void drawRect(Rect r, Paint paint) { - drawRect(r.left, r.top, r.right, r.bottom, paint); - } - - @Override - public void drawRect(RectF r, Paint paint) { - drawRect(r.left, r.top, r.right, r.bottom, paint); - } - - @Override - public void drawRGB(int r, int g, int b) { - drawColor(0xFF000000 | (r & 0xFF) << 16 | (g & 0xFF) << 8 | (b & 0xFF)); - } - - @Override - public void drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, - Paint paint) { - nDrawRoundRect(mRenderer, left, top, right, bottom, rx, ry, paint.mNativePaint); - } - - private static native void nDrawRoundRect(long renderer, float left, float top, - float right, float bottom, float rx, float y, long paint); - - @Override - public void drawText(char[] text, int index, int count, float x, float y, Paint paint) { - if ((index | count | (index + count) | (text.length - index - count)) < 0) { - throw new IndexOutOfBoundsException(); - } - - nDrawText(mRenderer, text, index, count, x, y, - paint.mBidiFlags, paint.mNativePaint, paint.mNativeTypeface); - } - - private static native void nDrawText(long renderer, char[] text, int index, int count, - float x, float y, int bidiFlags, long paint, long typeface); - - @Override - public void drawText(CharSequence text, int start, int end, float x, float y, Paint paint) { - if ((start | end | (end - start) | (text.length() - end)) < 0) { - throw new IndexOutOfBoundsException(); - } - if (text instanceof String || text instanceof SpannedString || - text instanceof SpannableString) { - nDrawText(mRenderer, text.toString(), start, end, x, y, paint.mBidiFlags, - paint.mNativePaint, paint.mNativeTypeface); - } else if (text instanceof GraphicsOperations) { - ((GraphicsOperations) text).drawText(this, start, end, x, y, paint); - } else { - char[] buf = TemporaryBuffer.obtain(end - start); - TextUtils.getChars(text, start, end, buf, 0); - nDrawText(mRenderer, buf, 0, end - start, x, y, - paint.mBidiFlags, paint.mNativePaint, paint.mNativeTypeface); - TemporaryBuffer.recycle(buf); - } - } - - @Override - public void drawText(String text, int start, int end, float x, float y, Paint paint) { - if ((start | end | (end - start) | (text.length() - end)) < 0) { - throw new IndexOutOfBoundsException(); - } - - nDrawText(mRenderer, text, start, end, x, y, - paint.mBidiFlags, paint.mNativePaint, paint.mNativeTypeface); - } - - private static native void nDrawText(long renderer, String text, int start, int end, - float x, float y, int bidiFlags, long paint, long typeface); - - @Override - public void drawText(String text, float x, float y, Paint paint) { - nDrawText(mRenderer, text, 0, text.length(), x, y, - paint.mBidiFlags, paint.mNativePaint, paint.mNativeTypeface); - } - - @Override - public void drawTextOnPath(char[] text, int index, int count, Path path, float hOffset, - float vOffset, Paint paint) { - if (index < 0 || index + count > text.length) { - throw new ArrayIndexOutOfBoundsException(); - } - - nDrawTextOnPath(mRenderer, text, index, count, path.mNativePath, hOffset, vOffset, - paint.mBidiFlags, paint.mNativePaint, paint.mNativeTypeface); - } - - private static native void nDrawTextOnPath(long renderer, char[] text, int index, int count, - long path, float hOffset, float vOffset, int bidiFlags, long nativePaint, - long typeface); - - @Override - public void drawTextOnPath(String text, Path path, float hOffset, float vOffset, Paint paint) { - if (text.length() == 0) return; - - nDrawTextOnPath(mRenderer, text, 0, text.length(), path.mNativePath, hOffset, vOffset, - paint.mBidiFlags, paint.mNativePaint, paint.mNativeTypeface); - } - - private static native void nDrawTextOnPath(long renderer, String text, int start, int end, - long path, float hOffset, float vOffset, int bidiFlags, long nativePaint, - long typeface); - - @Override - public void drawTextRun(char[] text, int index, int count, int contextIndex, int contextCount, - float x, float y, boolean isRtl, Paint paint) { - if ((index | count | text.length - index - count) < 0) { - throw new IndexOutOfBoundsException(); - } - - nDrawTextRun(mRenderer, text, index, count, contextIndex, contextCount, x, y, isRtl, - paint.mNativePaint, paint.mNativeTypeface); - } - - private static native void nDrawTextRun(long renderer, char[] text, int index, int count, - int contextIndex, int contextCount, float x, float y, boolean isRtl, long nativePaint, long nativeTypeface); - - @Override - public void drawTextRun(CharSequence text, int start, int end, int contextStart, int contextEnd, - float x, float y, boolean isRtl, Paint paint) { - if ((start | end | end - start | text.length() - end) < 0) { - throw new IndexOutOfBoundsException(); - } - - if (text instanceof String || text instanceof SpannedString || - text instanceof SpannableString) { - nDrawTextRun(mRenderer, text.toString(), start, end, contextStart, - contextEnd, x, y, isRtl, paint.mNativePaint, paint.mNativeTypeface); - } else if (text instanceof GraphicsOperations) { - ((GraphicsOperations) text).drawTextRun(this, start, end, - contextStart, contextEnd, x, y, isRtl, paint); - } else { - int contextLen = contextEnd - contextStart; - int len = end - start; - char[] buf = TemporaryBuffer.obtain(contextLen); - TextUtils.getChars(text, contextStart, contextEnd, buf, 0); - nDrawTextRun(mRenderer, buf, start - contextStart, len, 0, contextLen, - x, y, isRtl, paint.mNativePaint, paint.mNativeTypeface); - TemporaryBuffer.recycle(buf); - } - } - - private static native void nDrawTextRun(long renderer, String text, int start, int end, - int contextStart, int contextEnd, float x, float y, boolean isRtl, long nativePaint, long nativeTypeface); - - @Override - public void drawVertices(VertexMode mode, int vertexCount, float[] verts, int vertOffset, - float[] texs, int texOffset, int[] colors, int colorOffset, short[] indices, - int indexOffset, int indexCount, Paint paint) { - // TODO: Implement - } -} diff --git a/core/java/android/view/GLES20RecordingCanvas.java b/core/java/android/view/GLES20RecordingCanvas.java deleted file mode 100644 index 5e49d8e..0000000 --- a/core/java/android/view/GLES20RecordingCanvas.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.view; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.util.Pools.SynchronizedPool; - -/** - * An implementation of a GL canvas that records drawing operations. - * This is intended for use with a DisplayList. This class keeps a list of all the Paint and - * Bitmap objects that it draws, preventing the backing memory of Bitmaps from being freed while - * the DisplayList is still holding a native reference to the memory. - */ -class GLES20RecordingCanvas extends GLES20Canvas { - // The recording canvas pool should be large enough to handle a deeply nested - // view hierarchy because display lists are generated recursively. - private static final int POOL_LIMIT = 25; - - private static final SynchronizedPool<GLES20RecordingCanvas> sPool = - new SynchronizedPool<GLES20RecordingCanvas>(POOL_LIMIT); - - RenderNode mNode; - - private GLES20RecordingCanvas() { - super(); - } - - static GLES20RecordingCanvas obtain(@NonNull RenderNode node) { - if (node == null) throw new IllegalArgumentException("node cannot be null"); - GLES20RecordingCanvas canvas = sPool.acquire(); - if (canvas == null) { - canvas = new GLES20RecordingCanvas(); - } - canvas.mNode = node; - return canvas; - } - - void recycle() { - mNode = null; - sPool.release(this); - } - - long finishRecording() { - return nFinishRecording(mRenderer); - } - - @Override - public boolean isRecordingFor(Object o) { - return o == mNode; - } -} diff --git a/core/java/android/view/GhostView.java b/core/java/android/view/GhostView.java index 20baad0..d58e7c0 100644 --- a/core/java/android/view/GhostView.java +++ b/core/java/android/view/GhostView.java @@ -46,14 +46,14 @@ public class GhostView extends View { @Override protected void onDraw(Canvas canvas) { - if (canvas instanceof HardwareCanvas) { - HardwareCanvas hwCanvas = (HardwareCanvas) canvas; + if (canvas instanceof DisplayListCanvas) { + DisplayListCanvas dlCanvas = (DisplayListCanvas) canvas; mView.mRecreateDisplayList = true; RenderNode renderNode = mView.getDisplayList(); if (renderNode.isValid()) { - hwCanvas.insertReorderBarrier(); // enable shadow for this rendernode - hwCanvas.drawRenderNode(renderNode); - hwCanvas.insertInorderBarrier(); // re-disable reordering/shadows + dlCanvas.insertReorderBarrier(); // enable shadow for this rendernode + dlCanvas.drawRenderNode(renderNode); + dlCanvas.insertInorderBarrier(); // re-disable reordering/shadows } } } diff --git a/core/java/android/view/HardwareCanvas.java b/core/java/android/view/HardwareCanvas.java deleted file mode 100644 index 18accb8..0000000 --- a/core/java/android/view/HardwareCanvas.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.view; - -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.CanvasProperty; -import android.graphics.Paint; -import android.graphics.Rect; - -/** - * Hardware accelerated canvas. - * - * @hide - */ -public abstract class HardwareCanvas extends Canvas { - - @Override - public boolean isHardwareAccelerated() { - return true; - } - - @Override - public void setBitmap(Bitmap bitmap) { - throw new UnsupportedOperationException(); - } - - /** - * Invoked before any drawing operation is performed in this canvas. - * - * @param dirty The dirty rectangle to update, can be null. - * @return {@link RenderNode#STATUS_DREW} if anything was drawn (such as a call to clear - * the canvas). - * - * @hide - */ - public abstract int onPreDraw(Rect dirty); - - /** - * Invoked after all drawing operation have been performed. - * - * @hide - */ - public abstract void onPostDraw(); - - /** - * Draws the specified display list onto this canvas. The display list can only - * be drawn if {@link android.view.RenderNode#isValid()} returns true. - * - * @param renderNode The RenderNode to replay. - */ - public void drawRenderNode(RenderNode renderNode) { - drawRenderNode(renderNode, null, RenderNode.FLAG_CLIP_CHILDREN); - } - - /** - * Draws the specified display list onto this canvas. - * - * @param renderNode The RenderNode to replay. - * @param dirty Ignored, can be null. - * @param flags Optional flags about drawing, see {@link RenderNode} for - * the possible flags. - * - * @return One of {@link RenderNode#STATUS_DONE} or {@link RenderNode#STATUS_DREW} - * if anything was drawn. - * - * @hide - */ - public abstract int drawRenderNode(RenderNode renderNode, Rect dirty, int flags); - - /** - * Draws the specified layer onto this canvas. - * - * @param layer The layer to composite on this canvas - * @param x The left coordinate of the layer - * @param y The top coordinate of the layer - * @param paint The paint used to draw the layer - * - * @hide - */ - abstract void drawHardwareLayer(HardwareLayer layer, float x, float y, Paint paint); - - /** - * Calls the function specified with the drawGLFunction function pointer. This is - * functionality used by webkit for calling into their renderer from our display lists. - * This function may return true if an invalidation is needed after the call. - * - * @param drawGLFunction A native function pointer - * - * @return {@link RenderNode#STATUS_DONE} - * - * @hide - */ - public abstract int callDrawGLFunction2(long drawGLFunction); - - public abstract void drawCircle(CanvasProperty<Float> cx, CanvasProperty<Float> cy, - CanvasProperty<Float> radius, CanvasProperty<Paint> paint); - - public abstract void drawRoundRect(CanvasProperty<Float> left, CanvasProperty<Float> top, - CanvasProperty<Float> right, CanvasProperty<Float> bottom, - CanvasProperty<Float> rx, CanvasProperty<Float> ry, - CanvasProperty<Paint> paint); - - public static void setProperty(String name, String value) { - GLES20Canvas.setProperty(name, value); - } -} diff --git a/core/java/android/view/HardwareLayer.java b/core/java/android/view/HardwareLayer.java index a130bda..65ae8a6 100644 --- a/core/java/android/view/HardwareLayer.java +++ b/core/java/android/view/HardwareLayer.java @@ -52,7 +52,7 @@ final class HardwareLayer { * @see View#setLayerPaint(android.graphics.Paint) */ public void setLayerPaint(Paint paint) { - nSetLayerPaint(mFinalizer.get(), paint.mNativePaint); + nSetLayerPaint(mFinalizer.get(), paint.getNativeInstance()); mRenderer.pushLayerUpdate(this); } diff --git a/core/java/android/view/HardwareRenderer.java b/core/java/android/view/HardwareRenderer.java index c5c3f83..6632f39 100644 --- a/core/java/android/view/HardwareRenderer.java +++ b/core/java/android/view/HardwareRenderer.java @@ -19,7 +19,6 @@ package android.view; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Rect; -import android.util.DisplayMetrics; import android.view.Surface.OutOfResourcesException; import java.io.File; @@ -206,7 +205,7 @@ public abstract class HardwareRenderer { * false otherwise */ public static boolean isAvailable() { - return GLES20Canvas.isAvailable(); + return DisplayListCanvas.isAvailable(); } /** @@ -278,7 +277,7 @@ public abstract class HardwareRenderer { /** * Outputs extra debugging information in the specified file descriptor. */ - abstract void dumpGfxInfo(PrintWriter pw, FileDescriptor fd); + abstract void dumpGfxInfo(PrintWriter pw, FileDescriptor fd, String[] args); /** * Loads system properties used by the renderer. This method is invoked @@ -327,7 +326,7 @@ public abstract class HardwareRenderer { * * @param canvas The Canvas used to render the view. */ - void onHardwarePreDraw(HardwareCanvas canvas); + void onHardwarePreDraw(DisplayListCanvas canvas); /** * Invoked after a view is drawn by a hardware renderer. @@ -335,7 +334,7 @@ public abstract class HardwareRenderer { * * @param canvas The Canvas used to render the view. */ - void onHardwarePostDraw(HardwareCanvas canvas); + void onHardwarePostDraw(DisplayListCanvas canvas); } /** @@ -424,7 +423,7 @@ public abstract class HardwareRenderer { */ static HardwareRenderer create(Context context, boolean translucent) { HardwareRenderer renderer = null; - if (GLES20Canvas.isAvailable()) { + if (DisplayListCanvas.isAvailable()) { renderer = new ThreadedRenderer(context, translucent); } return renderer; diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index 7b20e72..d6625c8 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -16,6 +16,7 @@ package android.view; +import com.android.internal.app.IAssistScreenshotReceiver; import com.android.internal.view.IInputContext; import com.android.internal.view.IInputMethodClient; @@ -81,7 +82,7 @@ interface IWindowManager void addAppToken(int addPos, IApplicationToken token, int groupId, int stackId, int requestedOrientation, boolean fullscreen, boolean showWhenLocked, int userId, int configChanges, boolean voiceInteraction, boolean launchTaskBehind); - void setAppGroupId(IBinder token, int groupId); + void setAppTask(IBinder token, int taskId); void setAppOrientation(IApplicationToken token, int requestedOrientation); int getAppOrientation(IApplicationToken token); void setFocusedApp(IBinder token, boolean moveFocusNow); @@ -91,6 +92,8 @@ interface IWindowManager IRemoteCallback startedCallback); void overridePendingAppTransitionScaleUp(int startX, int startY, int startWidth, int startHeight); + void overridePendingAppTransitionClipReveal(int startX, int startY, + int startWidth, int startHeight); void overridePendingAppTransitionThumb(in Bitmap srcThumb, int startX, int startY, IRemoteCallback startedCallback, boolean scaleUp); void overridePendingAppTransitionAspectScaledThumb(in Bitmap srcThumb, int startX, @@ -218,10 +221,14 @@ interface IWindowManager boolean isRotationFrozen(); /** + * Used only for assist -- request a screenshot of the current application. + */ + boolean requestAssistScreenshot(IAssistScreenshotReceiver receiver); + + /** * Create a screenshot of the applications currently displayed. */ - Bitmap screenshotApplications(IBinder appToken, int displayId, int maxWidth, - int maxHeight, boolean force565); + Bitmap screenshotApplications(IBinder appToken, int displayId, int maxWidth, int maxHeight); /** * Called by the status bar to notify Views of changes to System UI visiblity. diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl index 63e1a85..fc0148f 100644 --- a/core/java/android/view/IWindowSession.aidl +++ b/core/java/android/view/IWindowSession.aidl @@ -188,9 +188,6 @@ interface IWindowSession { void wallpaperCommandComplete(IBinder window, in Bundle result); - void setUniverseTransform(IBinder window, float alpha, float offx, float offy, - float dsdx, float dtdx, float dsdy, float dtdy); - /** * Notifies that a rectangle on the screen has been requested. */ diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java index 243a0fc..779560c 100644 --- a/core/java/android/view/KeyEvent.java +++ b/core/java/android/view/KeyEvent.java @@ -20,7 +20,6 @@ import android.os.Parcel; import android.os.Parcelable; import android.text.method.MetaKeyKeyListener; import android.util.Log; -import android.util.SparseArray; import android.util.SparseIntArray; import android.view.KeyCharacterMap; import android.view.KeyCharacterMap.KeyData; diff --git a/core/java/android/view/LayoutInflater.java b/core/java/android/view/LayoutInflater.java index 1546877..1a07aee 100644 --- a/core/java/android/view/LayoutInflater.java +++ b/core/java/android/view/LayoutInflater.java @@ -16,22 +16,26 @@ package android.view; -import android.graphics.Canvas; -import android.os.Handler; -import android.os.Message; -import android.os.Trace; -import android.widget.FrameLayout; +import com.android.internal.R; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import android.annotation.LayoutRes; +import android.annotation.Nullable; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; +import android.graphics.Canvas; +import android.os.Handler; +import android.os.Message; +import android.os.Trace; import android.util.AttributeSet; import android.util.Log; +import android.util.TypedValue; import android.util.Xml; +import android.widget.FrameLayout; import java.io.IOException; import java.lang.reflect.Constructor; @@ -64,6 +68,7 @@ import java.util.HashMap; * @see Context#getSystemService */ public abstract class LayoutInflater { + private static final String TAG = LayoutInflater.class.getSimpleName(); private static final boolean DEBUG = false; @@ -90,12 +95,16 @@ public abstract class LayoutInflater { private HashMap<String, Boolean> mFilterMap; + private TypedValue mTempValue; + private static final String TAG_MERGE = "merge"; private static final String TAG_INCLUDE = "include"; private static final String TAG_1995 = "blink"; private static final String TAG_REQUEST_FOCUS = "requestFocus"; private static final String TAG_TAG = "tag"; + private static final String ATTR_LAYOUT = "layout"; + private static final int[] ATTRS_THEME = new int[] { com.android.internal.R.attr.theme }; @@ -361,7 +370,7 @@ public abstract class LayoutInflater { * this is the root View; otherwise it is the root of the inflated * XML file. */ - public View inflate(int resource, ViewGroup root) { + public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) { return inflate(resource, root, root != null); } @@ -381,7 +390,7 @@ public abstract class LayoutInflater { * this is the root View; otherwise it is the root of the inflated * XML file. */ - public View inflate(XmlPullParser parser, ViewGroup root) { + public View inflate(XmlPullParser parser, @Nullable ViewGroup root) { return inflate(parser, root, root != null); } @@ -402,7 +411,7 @@ public abstract class LayoutInflater { * attachToRoot is true, this is root; otherwise it is the root of * the inflated XML file. */ - public View inflate(int resource, ViewGroup root, boolean attachToRoot) { + public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) { final Resources res = getContext().getResources(); if (DEBUG) { Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" (" @@ -439,7 +448,7 @@ public abstract class LayoutInflater { * attachToRoot is true, this is root; otherwise it is the root of * the inflated XML file. */ - public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) { + public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) { synchronized (mConstructorArgs) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate"); @@ -523,10 +532,10 @@ public abstract class LayoutInflater { InflateException ex = new InflateException(e.getMessage()); ex.initCause(e); throw ex; - } catch (IOException e) { + } catch (Exception e) { InflateException ex = new InflateException( parser.getPositionDescription() - + ": " + e.getMessage()); + + ": " + e.getMessage()); ex.initCause(e); throw ex; } finally { @@ -837,7 +846,7 @@ public abstract class LayoutInflater { throws XmlPullParserException, IOException { int type; - final TypedArray ta = mContext.obtainStyledAttributes( + final TypedArray ta = view.getContext().obtainStyledAttributes( attrs, com.android.internal.R.styleable.ViewTag); final int key = ta.getResourceId(com.android.internal.R.styleable.ViewTag_id, 0); final CharSequence value = ta.getText(com.android.internal.R.styleable.ViewTag_value); @@ -856,16 +865,41 @@ public abstract class LayoutInflater { int type; if (parent instanceof ViewGroup) { - final int layout = attrs.getAttributeResourceValue(null, "layout", 0); + Context context = inheritContext ? parent.getContext() : mContext; + + // Apply a theme wrapper, if requested. + final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME); + final int themeResId = ta.getResourceId(0, 0); + if (themeResId != 0) { + context = new ContextThemeWrapper(context, themeResId); + } + ta.recycle(); + + // If the layout is pointing to a theme attribute, we have to + // massage the value to get a resource identifier out of it. + int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0); if (layout == 0) { - final String value = attrs.getAttributeValue(null, "layout"); - if (value == null) { - throw new InflateException("You must specifiy a layout in the" + final String value = attrs.getAttributeValue(null, ATTR_LAYOUT); + if (value == null || value.length() < 1) { + throw new InflateException("You must specify a layout in the" + " include tag: <include layout=\"@layout/layoutID\" />"); - } else { - throw new InflateException("You must specifiy a valid layout " - + "reference. The layout ID " + value + " is not valid."); } + + layout = context.getResources().getIdentifier(value.substring(1), null, null); + } + + // The layout might be referencing a theme attribute. + if (mTempValue == null) { + mTempValue = new TypedValue(); + } + if (layout != 0 && context.getTheme().resolveAttribute(layout, mTempValue, true)) { + layout = mTempValue.resourceId; + } + + if (layout == 0) { + final String value = attrs.getAttributeValue(null, ATTR_LAYOUT); + throw new InflateException("You must specify a valid layout " + + "reference. The layout ID " + value + " is not valid."); } else { final XmlResourceParser childParser = getContext().getResources().getLayout(layout); @@ -893,6 +927,14 @@ public abstract class LayoutInflater { inheritContext); final ViewGroup group = (ViewGroup) parent; + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.Include); + final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID); + final int visibility = a.getInt(R.styleable.Include_visibility, -1); + final boolean hasWidth = a.hasValue(R.styleable.Include_layout_width); + final boolean hasHeight = a.hasValue(R.styleable.Include_layout_height); + a.recycle(); + // We try to load the layout params set in the <include /> tag. If // they don't exist, we will rely on the layout params set in the // included XML file. @@ -902,28 +944,21 @@ public abstract class LayoutInflater { // successfully loaded layout params from the <include /> tag, // false means we need to rely on the included layout params. ViewGroup.LayoutParams params = null; - try { - params = group.generateLayoutParams(attrs); - } catch (RuntimeException e) { - params = group.generateLayoutParams(childAttrs); - } finally { - if (params != null) { - view.setLayoutParams(params); + if (hasWidth && hasHeight) { + try { + params = group.generateLayoutParams(attrs); + } catch (RuntimeException e) { + // Ignore, just fail over to child attrs. } } + if (params == null) { + params = group.generateLayoutParams(childAttrs); + } + view.setLayoutParams(params); // Inflate all children. rInflate(childParser, view, childAttrs, true, true); - // Attempt to override the included layout's android:id with the - // one set on the <include /> tag itself. - TypedArray a = mContext.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.View, 0, 0); - int id = a.getResourceId(com.android.internal.R.styleable.View_id, View.NO_ID); - // While we're at it, let's try to override android:visibility. - int visibility = a.getInt(com.android.internal.R.styleable.View_visibility, -1); - a.recycle(); - if (id != View.NO_ID) { view.setId(id); } diff --git a/core/java/android/view/Menu.java b/core/java/android/view/Menu.java index 7157bc5..0c2e9cf 100644 --- a/core/java/android/view/Menu.java +++ b/core/java/android/view/Menu.java @@ -16,6 +16,7 @@ package android.view; +import android.annotation.StringRes; import android.app.Activity; import android.content.ComponentName; import android.content.Intent; @@ -148,7 +149,7 @@ public interface Menu { * @param titleRes Resource identifier of title string. * @return The newly added menu item. */ - public MenuItem add(int titleRes); + public MenuItem add(@StringRes int titleRes); /** * Add a new item to the menu. This item displays the given title for its @@ -182,7 +183,7 @@ public interface Menu { * @param titleRes Resource identifier of title string. * @return The newly added menu item. */ - public MenuItem add(int groupId, int itemId, int order, int titleRes); + public MenuItem add(int groupId, int itemId, int order, @StringRes int titleRes); /** * Add a new sub-menu to the menu. This item displays the given title for @@ -202,7 +203,7 @@ public interface Menu { * @param titleRes Resource identifier of title string. * @return The newly added sub-menu */ - SubMenu addSubMenu(final int titleRes); + SubMenu addSubMenu(@StringRes final int titleRes); /** * Add a new sub-menu to the menu. This item displays the given @@ -239,7 +240,7 @@ public interface Menu { * @param titleRes Resource identifier of title string. * @return The newly added sub-menu */ - SubMenu addSubMenu(int groupId, int itemId, int order, int titleRes); + SubMenu addSubMenu(int groupId, int itemId, int order, @StringRes int titleRes); /** * Add a group of menu items corresponding to actions that can be performed diff --git a/core/java/android/view/MenuInflater.java b/core/java/android/view/MenuInflater.java index 5811c17..b49a59e 100644 --- a/core/java/android/view/MenuInflater.java +++ b/core/java/android/view/MenuInflater.java @@ -21,11 +21,15 @@ import com.android.internal.view.menu.MenuItemImpl; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import android.annotation.MenuRes; import android.app.Activity; import android.content.Context; import android.content.ContextWrapper; +import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.Log; import android.util.Xml; @@ -101,7 +105,7 @@ public class MenuInflater { * @param menu The Menu to inflate into. The items and submenus will be * added to this Menu. */ - public void inflate(int menuRes, Menu menu) { + public void inflate(@MenuRes int menuRes, Menu menu) { XmlResourceParser parser = null; try { parser = mContext.getResources().getLayout(menuRes); @@ -333,6 +337,11 @@ public class MenuInflater { private ActionProvider itemActionProvider; + private ColorStateList itemIconTintList; + private boolean itemIconTintListSet; + private PorterDuff.Mode itemIconTintMode; + private boolean itemIconTintModeSet; + private static final int defaultGroupId = NO_ID; private static final int defaultItemId = NO_ID; private static final int defaultItemCategory = 0; @@ -423,6 +432,23 @@ public class MenuInflater { itemActionProvider = null; } + if (a.hasValueOrEmpty(com.android.internal.R.styleable.MenuItem_iconTint)) { + itemIconTintList = a.getColorStateList( + com.android.internal.R.styleable.MenuItem_iconTint); + itemIconTintListSet = true; + } else { + itemIconTintList = null; + itemIconTintListSet = false; + } + if (a.hasValueOrEmpty(com.android.internal.R.styleable.MenuItem_iconTintMode)) { + itemIconTintMode = Drawable.parseTintMode( + a.getInt(com.android.internal.R.styleable.MenuItem_iconTintMode, -1), null); + itemIconTintModeSet = true; + } else { + itemIconTintMode = null; + itemIconTintModeSet = false; + } + a.recycle(); itemAdded = false; @@ -485,6 +511,13 @@ public class MenuInflater { if (itemActionProvider != null) { item.setActionProvider(itemActionProvider); } + + if (itemIconTintListSet) { + item.setIconTintList(itemIconTintList); + } + if (itemIconTintModeSet) { + item.setIconTintMode(itemIconTintMode); + } } public MenuItem addItem() { diff --git a/core/java/android/view/MenuItem.java b/core/java/android/view/MenuItem.java index e706c9c..2948007 100644 --- a/core/java/android/view/MenuItem.java +++ b/core/java/android/view/MenuItem.java @@ -16,8 +16,13 @@ package android.view; +import android.annotation.DrawableRes; +import android.annotation.LayoutRes; +import android.annotation.StringRes; import android.app.Activity; import android.content.Intent; +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.view.ContextMenu.ContextMenuInfo; import android.view.View.OnCreateContextMenuListener; @@ -165,7 +170,7 @@ public interface MenuItem { * @see #setTitleCondensed(CharSequence) */ - public MenuItem setTitle(int title); + public MenuItem setTitle(@StringRes int title); /** * Retrieve the current title of the item. @@ -214,7 +219,7 @@ public interface MenuItem { * @param iconRes The new icon (as a resource ID) to be displayed. * @return This Item so additional setters can be called. */ - public MenuItem setIcon(int iconRes); + public MenuItem setIcon(@DrawableRes int iconRes); /** * Returns the icon for this item as a Drawable (getting it from resources if it hasn't been @@ -511,7 +516,7 @@ public interface MenuItem { * * @see #setShowAsAction(int) */ - public MenuItem setActionView(int resId); + public MenuItem setActionView(@LayoutRes int resId); /** * Returns the currently set action view for this menu item. @@ -596,4 +601,26 @@ public interface MenuItem { * @return This menu item instance for call chaining */ public MenuItem setOnActionExpandListener(OnActionExpandListener listener); + + /** + * Applies a tint to the icon drawable. Does not modify the current tint + * mode, which is {@link PorterDuff.Mode#SRC_IN} by default. + * <p> + * Subsequent calls to {@link android.view.MenuItem#setIcon(android.graphics.drawable.Drawable)} + * will automatically mutate the drawable and apply the specified tint and tint mode. + * + * @param tint the tint to apply, may be {@code null} to clear tint + * @return This menu item instance for call chaining + */ + public MenuItem setIconTintList(ColorStateList tint); + + /** + * Specifies the blending mode used to apply the tint specified by {@link + * #setIconTintList(ColorStateList)} to the icon drawable. The default mode is {@link + * PorterDuff.Mode#SRC_IN}. + * + * @param tintMode the blending mode used to apply the tint, may be {@code null} to clear tint + * @return This menu item instance for call chaining + */ + public MenuItem setIconTintMode(PorterDuff.Mode tintMode); } diff --git a/core/java/android/view/PhoneFallbackEventHandler.java b/core/java/android/view/PhoneFallbackEventHandler.java new file mode 100644 index 0000000..fbf5732 --- /dev/null +++ b/core/java/android/view/PhoneFallbackEventHandler.java @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.app.KeyguardManager; +import android.app.SearchManager; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.media.AudioManager; +import android.media.session.MediaSessionLegacyHelper; +import android.os.UserHandle; +import android.telephony.TelephonyManager; +import android.util.Slog; + +/** + * @hide + */ +public class PhoneFallbackEventHandler implements FallbackEventHandler { + private static String TAG = "PhoneFallbackEventHandler"; + private static final boolean DEBUG = false; + + Context mContext; + View mView; + + AudioManager mAudioManager; + KeyguardManager mKeyguardManager; + SearchManager mSearchManager; + TelephonyManager mTelephonyManager; + + public PhoneFallbackEventHandler(Context context) { + mContext = context; + } + + public void setView(View v) { + mView = v; + } + + public void preDispatchKeyEvent(KeyEvent event) { + getAudioManager().preDispatchKeyEvent(event, AudioManager.USE_DEFAULT_STREAM_TYPE); + } + + public boolean dispatchKeyEvent(KeyEvent event) { + + final int action = event.getAction(); + final int keyCode = event.getKeyCode(); + + if (action == KeyEvent.ACTION_DOWN) { + return onKeyDown(keyCode, event); + } else { + return onKeyUp(keyCode, event); + } + } + + boolean onKeyDown(int keyCode, KeyEvent event) { + /* **************************************************************************** + * HOW TO DECIDE WHERE YOUR KEY HANDLING GOES. + * See the comment in PhoneWindow.onKeyDown + * ****************************************************************************/ + final KeyEvent.DispatcherState dispatcher = mView.getKeyDispatcherState(); + + switch (keyCode) { + case KeyEvent.KEYCODE_VOLUME_UP: + case KeyEvent.KEYCODE_VOLUME_DOWN: + case KeyEvent.KEYCODE_VOLUME_MUTE: { + MediaSessionLegacyHelper.getHelper(mContext).sendVolumeKeyEvent(event, false); + return true; + } + + + case KeyEvent.KEYCODE_MEDIA_PLAY: + case KeyEvent.KEYCODE_MEDIA_PAUSE: + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + /* Suppress PLAY/PAUSE toggle when phone is ringing or in-call + * to avoid music playback */ + if (getTelephonyManager().getCallState() != TelephonyManager.CALL_STATE_IDLE) { + return true; // suppress key event + } + case KeyEvent.KEYCODE_MUTE: + case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MEDIA_STOP: + case KeyEvent.KEYCODE_MEDIA_NEXT: + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + case KeyEvent.KEYCODE_MEDIA_REWIND: + case KeyEvent.KEYCODE_MEDIA_RECORD: + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: + case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK: { + handleMediaKeyEvent(event); + return true; + } + + case KeyEvent.KEYCODE_CALL: { + if (getKeyguardManager().inKeyguardRestrictedInputMode() || dispatcher == null) { + break; + } + if (event.getRepeatCount() == 0) { + dispatcher.startTracking(event, this); + } else if (event.isLongPress() && dispatcher.isTracking(event)) { + dispatcher.performedLongPress(event); + mView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + // launch the VoiceDialer + Intent intent = new Intent(Intent.ACTION_VOICE_COMMAND); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + try { + sendCloseSystemWindows(); + mContext.startActivity(intent); + } catch (ActivityNotFoundException e) { + startCallActivity(); + } + } + return true; + } + + case KeyEvent.KEYCODE_CAMERA: { + if (getKeyguardManager().inKeyguardRestrictedInputMode() || dispatcher == null) { + break; + } + if (event.getRepeatCount() == 0) { + dispatcher.startTracking(event, this); + } else if (event.isLongPress() && dispatcher.isTracking(event)) { + dispatcher.performedLongPress(event); + mView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + sendCloseSystemWindows(); + // Broadcast an intent that the Camera button was longpressed + Intent intent = new Intent(Intent.ACTION_CAMERA_BUTTON, null); + intent.putExtra(Intent.EXTRA_KEY_EVENT, event); + mContext.sendOrderedBroadcastAsUser(intent, UserHandle.CURRENT_OR_SELF, + null, null, null, 0, null, null); + } + return true; + } + + case KeyEvent.KEYCODE_SEARCH: { + if (getKeyguardManager().inKeyguardRestrictedInputMode() || dispatcher == null) { + break; + } + if (event.getRepeatCount() == 0) { + dispatcher.startTracking(event, this); + } else if (event.isLongPress() && dispatcher.isTracking(event)) { + Configuration config = mContext.getResources().getConfiguration(); + if (config.keyboard == Configuration.KEYBOARD_NOKEYS + || config.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES) { + // launch the search activity + Intent intent = new Intent(Intent.ACTION_SEARCH_LONG_PRESS); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + try { + mView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + sendCloseSystemWindows(); + getSearchManager().stopSearch(); + mContext.startActivity(intent); + // Only clear this if we successfully start the + // activity; otherwise we will allow the normal short + // press action to be performed. + dispatcher.performedLongPress(event); + return true; + } catch (ActivityNotFoundException e) { + // Ignore + } + } + } + break; + } + } + return false; + } + + boolean onKeyUp(int keyCode, KeyEvent event) { + if (DEBUG) { + Slog.d(TAG, "up " + keyCode); + } + final KeyEvent.DispatcherState dispatcher = mView.getKeyDispatcherState(); + if (dispatcher != null) { + dispatcher.handleUpEvent(event); + } + + switch (keyCode) { + case KeyEvent.KEYCODE_VOLUME_UP: + case KeyEvent.KEYCODE_VOLUME_DOWN: + case KeyEvent.KEYCODE_VOLUME_MUTE: { + if (!event.isCanceled()) { + MediaSessionLegacyHelper.getHelper(mContext).sendVolumeKeyEvent(event, false); + } + return true; + } + + case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MUTE: + case KeyEvent.KEYCODE_MEDIA_PLAY: + case KeyEvent.KEYCODE_MEDIA_PAUSE: + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + case KeyEvent.KEYCODE_MEDIA_STOP: + case KeyEvent.KEYCODE_MEDIA_NEXT: + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + case KeyEvent.KEYCODE_MEDIA_REWIND: + case KeyEvent.KEYCODE_MEDIA_RECORD: + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: + case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK: { + handleMediaKeyEvent(event); + return true; + } + + case KeyEvent.KEYCODE_CAMERA: { + if (getKeyguardManager().inKeyguardRestrictedInputMode()) { + break; + } + if (event.isTracking() && !event.isCanceled()) { + // Add short press behavior here if desired + } + return true; + } + + case KeyEvent.KEYCODE_CALL: { + if (getKeyguardManager().inKeyguardRestrictedInputMode()) { + break; + } + if (event.isTracking() && !event.isCanceled()) { + startCallActivity(); + } + return true; + } + } + return false; + } + + void startCallActivity() { + sendCloseSystemWindows(); + Intent intent = new Intent(Intent.ACTION_CALL_BUTTON); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + try { + mContext.startActivity(intent); + } catch (ActivityNotFoundException e) { + Slog.w(TAG, "No activity found for android.intent.action.CALL_BUTTON."); + } + } + + SearchManager getSearchManager() { + if (mSearchManager == null) { + mSearchManager = (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE); + } + return mSearchManager; + } + + TelephonyManager getTelephonyManager() { + if (mTelephonyManager == null) { + mTelephonyManager = (TelephonyManager)mContext.getSystemService( + Context.TELEPHONY_SERVICE); + } + return mTelephonyManager; + } + + KeyguardManager getKeyguardManager() { + if (mKeyguardManager == null) { + mKeyguardManager = (KeyguardManager)mContext.getSystemService(Context.KEYGUARD_SERVICE); + } + return mKeyguardManager; + } + + AudioManager getAudioManager() { + if (mAudioManager == null) { + mAudioManager = (AudioManager)mContext.getSystemService(Context.AUDIO_SERVICE); + } + return mAudioManager; + } + + void sendCloseSystemWindows() { + PhoneWindow.sendCloseSystemWindows(mContext, null); + } + + private void handleMediaKeyEvent(KeyEvent keyEvent) { + MediaSessionLegacyHelper.getHelper(mContext).sendMediaButtonEvent(keyEvent, false); + } +} + diff --git a/core/java/android/view/PhoneLayoutInflater.java b/core/java/android/view/PhoneLayoutInflater.java new file mode 100644 index 0000000..7d89a0b --- /dev/null +++ b/core/java/android/view/PhoneLayoutInflater.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.content.Context; +import android.util.AttributeSet; + +/** + * @hide + */ +public class PhoneLayoutInflater extends LayoutInflater { + private static final String[] sClassPrefixList = { + "android.widget.", + "android.webkit.", + "android.app." + }; + + /** + * Instead of instantiating directly, you should retrieve an instance + * through {@link Context#getSystemService} + * + * @param context The Context in which in which to find resources and other + * application-specific things. + * + * @see Context#getSystemService + */ + public PhoneLayoutInflater(Context context) { + super(context); + } + + protected PhoneLayoutInflater(LayoutInflater original, Context newContext) { + super(original, newContext); + } + + /** Override onCreateView to instantiate names that correspond to the + widgets known to the Widget factory. If we don't find a match, + call through to our super class. + */ + @Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { + for (String prefix : sClassPrefixList) { + try { + View view = createView(name, prefix, attrs); + if (view != null) { + return view; + } + } catch (ClassNotFoundException e) { + // In this case we want to let the base class take a crack + // at it. + } + } + + return super.onCreateView(name, attrs); + } + + public LayoutInflater cloneInContext(Context newContext) { + return new PhoneLayoutInflater(this, newContext); + } +} + diff --git a/core/java/android/view/PhoneWindow.java b/core/java/android/view/PhoneWindow.java new file mode 100644 index 0000000..05796bb --- /dev/null +++ b/core/java/android/view/PhoneWindow.java @@ -0,0 +1,4818 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import static android.view.View.MeasureSpec.AT_MOST; +import static android.view.View.MeasureSpec.EXACTLY; +import static android.view.View.MeasureSpec.getMode; +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; +import static android.view.WindowManager.LayoutParams.*; + +import android.app.ActivityManagerNative; +import android.app.SearchManager; +import android.os.UserHandle; + +import com.android.internal.R; +import com.android.internal.view.RootViewSurfaceTaker; +import com.android.internal.view.StandaloneActionMode; +import com.android.internal.view.menu.ContextMenuBuilder; +import com.android.internal.view.menu.IconMenuPresenter; +import com.android.internal.view.menu.ListMenuPresenter; +import com.android.internal.view.menu.MenuBuilder; +import com.android.internal.view.menu.MenuDialogHelper; +import com.android.internal.view.menu.MenuPresenter; +import com.android.internal.view.menu.MenuView; +import com.android.internal.widget.ActionBarContextView; +import com.android.internal.widget.BackgroundFallback; +import com.android.internal.widget.DecorContentParent; +import com.android.internal.widget.SwipeDismissLayout; + +import android.app.ActivityManager; +import android.app.KeyguardManager; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.content.res.Resources.Theme; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.media.AudioManager; +import android.media.session.MediaController; +import android.media.session.MediaSessionLegacyHelper; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.transition.Scene; +import android.transition.Transition; +import android.transition.TransitionInflater; +import android.transition.TransitionManager; +import android.transition.TransitionSet; +import android.util.AndroidRuntimeException; +import android.util.DisplayMetrics; +import android.util.EventLog; +import android.util.Log; +import android.util.SparseArray; +import android.util.TypedValue; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.PopupWindow; +import android.widget.ProgressBar; +import android.widget.TextView; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +/** + * Android-specific Window. + * <p> + * todo: need to pull the generic functionality out into a base class + * in android.widget. + * + * @hide + */ +public class PhoneWindow extends Window implements MenuBuilder.Callback { + + private final static String TAG = "PhoneWindow"; + + private final static boolean SWEEP_OPEN_MENU = false; + + private final static int DEFAULT_BACKGROUND_FADE_DURATION_MS = 300; + + private static final int CUSTOM_TITLE_COMPATIBLE_FEATURES = DEFAULT_FEATURES | + (1 << FEATURE_CUSTOM_TITLE) | + (1 << FEATURE_CONTENT_TRANSITIONS) | + (1 << FEATURE_ACTIVITY_TRANSITIONS) | + (1 << FEATURE_ACTION_MODE_OVERLAY); + + private static final Transition USE_DEFAULT_TRANSITION = new TransitionSet(); + + /** + * Simple callback used by the context menu and its submenus. The options + * menu submenus do not use this (their behavior is more complex). + */ + final DialogMenuCallback mContextMenuCallback = new DialogMenuCallback(FEATURE_CONTEXT_MENU); + + final TypedValue mMinWidthMajor = new TypedValue(); + final TypedValue mMinWidthMinor = new TypedValue(); + TypedValue mFixedWidthMajor; + TypedValue mFixedWidthMinor; + TypedValue mFixedHeightMajor; + TypedValue mFixedHeightMinor; + TypedValue mOutsetBottom; + + // This is the top-level view of the window, containing the window decor. + private DecorView mDecor; + + // This is the view in which the window contents are placed. It is either + // mDecor itself, or a child of mDecor where the contents go. + private ViewGroup mContentParent; + + private ViewGroup mContentRoot; + + SurfaceHolder.Callback2 mTakeSurfaceCallback; + + InputQueue.Callback mTakeInputQueueCallback; + + private boolean mIsFloating; + + private LayoutInflater mLayoutInflater; + + private TextView mTitleView; + + private DecorContentParent mDecorContentParent; + private ActionMenuPresenterCallback mActionMenuPresenterCallback; + private PanelMenuPresenterCallback mPanelMenuPresenterCallback; + + private TransitionManager mTransitionManager; + private Scene mContentScene; + + // The icon resource has been explicitly set elsewhere + // and should not be overwritten with a default. + static final int FLAG_RESOURCE_SET_ICON = 1 << 0; + + // The logo resource has been explicitly set elsewhere + // and should not be overwritten with a default. + static final int FLAG_RESOURCE_SET_LOGO = 1 << 1; + + // The icon resource is currently configured to use the system fallback + // as no default was previously specified. Anything can override this. + static final int FLAG_RESOURCE_SET_ICON_FALLBACK = 1 << 2; + + int mResourcesSetFlags; + int mIconRes; + int mLogoRes; + + private DrawableFeatureState[] mDrawables; + + private PanelFeatureState[] mPanels; + + /** + * The panel that is prepared or opened (the most recent one if there are + * multiple panels). Shortcuts will go to this panel. It gets set in + * {@link #preparePanel} and cleared in {@link #closePanel}. + */ + private PanelFeatureState mPreparedPanel; + + /** + * The keycode that is currently held down (as a modifier) for chording. If + * this is 0, there is no key held down. + */ + private int mPanelChordingKey; + + private ImageView mLeftIconView; + + private ImageView mRightIconView; + + private ProgressBar mCircularProgressBar; + + private ProgressBar mHorizontalProgressBar; + + private int mBackgroundResource = 0; + private int mBackgroundFallbackResource = 0; + + private Drawable mBackgroundDrawable; + + private float mElevation; + + /** Whether window content should be clipped to the background outline. */ + private boolean mClipToOutline; + + private int mFrameResource = 0; + + private int mTextColor = 0; + private int mStatusBarColor = 0; + private int mNavigationBarColor = 0; + private boolean mForcedStatusBarColor = false; + private boolean mForcedNavigationBarColor = false; + + private CharSequence mTitle = null; + + private int mTitleColor = 0; + + private boolean mAlwaysReadCloseOnTouchAttr = false; + + private ContextMenuBuilder mContextMenu; + private MenuDialogHelper mContextMenuHelper; + private boolean mClosingActionMenu; + + private int mVolumeControlStreamType = AudioManager.USE_DEFAULT_STREAM_TYPE; + private MediaController mMediaController; + + private AudioManager mAudioManager; + private KeyguardManager mKeyguardManager; + + private int mUiOptions = 0; + + private boolean mInvalidatePanelMenuPosted; + private int mInvalidatePanelMenuFeatures; + private final Runnable mInvalidatePanelMenuRunnable = new Runnable() { + @Override public void run() { + for (int i = 0; i <= FEATURE_MAX; i++) { + if ((mInvalidatePanelMenuFeatures & 1 << i) != 0) { + doInvalidatePanelMenu(i); + } + } + mInvalidatePanelMenuPosted = false; + mInvalidatePanelMenuFeatures = 0; + } + }; + + private Transition mEnterTransition = null; + private Transition mReturnTransition = USE_DEFAULT_TRANSITION; + private Transition mExitTransition = null; + private Transition mReenterTransition = USE_DEFAULT_TRANSITION; + private Transition mSharedElementEnterTransition = null; + private Transition mSharedElementReturnTransition = USE_DEFAULT_TRANSITION; + private Transition mSharedElementExitTransition = null; + private Transition mSharedElementReenterTransition = USE_DEFAULT_TRANSITION; + private Boolean mAllowReturnTransitionOverlap; + private Boolean mAllowEnterTransitionOverlap; + private long mBackgroundFadeDurationMillis = -1; + private Boolean mSharedElementsUseOverlay; + + private Rect mTempRect; + + static class WindowManagerHolder { + static final IWindowManager sWindowManager = IWindowManager.Stub.asInterface( + ServiceManager.getService("window")); + } + + static final RotationWatcher sRotationWatcher = new RotationWatcher(); + + public PhoneWindow(Context context) { + super(context); + mLayoutInflater = LayoutInflater.from(context); + } + + @Override + public final void setContainer(Window container) { + super.setContainer(container); + } + + @Override + public boolean requestFeature(int featureId) { + if (mContentParent != null) { + throw new AndroidRuntimeException("requestFeature() must be called before adding content"); + } + final int features = getFeatures(); + final int newFeatures = features | (1 << featureId); + if ((newFeatures & (1 << FEATURE_CUSTOM_TITLE)) != 0 && + (newFeatures & ~CUSTOM_TITLE_COMPATIBLE_FEATURES) != 0) { + // Another feature is enabled and the user is trying to enable the custom title feature + // or custom title feature is enabled and the user is trying to enable another feature + throw new AndroidRuntimeException( + "You cannot combine custom titles with other title features"); + } + if ((features & (1 << FEATURE_NO_TITLE)) != 0 && featureId == FEATURE_ACTION_BAR) { + return false; // Ignore. No title dominates. + } + if ((features & (1 << FEATURE_ACTION_BAR)) != 0 && featureId == FEATURE_NO_TITLE) { + // Remove the action bar feature if we have no title. No title dominates. + removeFeature(FEATURE_ACTION_BAR); + } + + if ((features & (1 << FEATURE_ACTION_BAR)) != 0 && featureId == FEATURE_SWIPE_TO_DISMISS) { + throw new AndroidRuntimeException( + "You cannot combine swipe dismissal and the action bar."); + } + if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0 && featureId == FEATURE_ACTION_BAR) { + throw new AndroidRuntimeException( + "You cannot combine swipe dismissal and the action bar."); + } + + if (featureId == FEATURE_INDETERMINATE_PROGRESS && + getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)) { + throw new AndroidRuntimeException("You cannot use indeterminate progress on a watch."); + } + return super.requestFeature(featureId); + } + + @Override + public void setUiOptions(int uiOptions) { + mUiOptions = uiOptions; + } + + @Override + public void setUiOptions(int uiOptions, int mask) { + mUiOptions = (mUiOptions & ~mask) | (uiOptions & mask); + } + + @Override + public TransitionManager getTransitionManager() { + return mTransitionManager; + } + + @Override + public void setTransitionManager(TransitionManager tm) { + mTransitionManager = tm; + } + + @Override + public Scene getContentScene() { + return mContentScene; + } + + @Override + public void setContentView(int layoutResID) { + // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window + // decor, when theme attributes and the like are crystalized. Do not check the feature + // before this happens. + if (mContentParent == null) { + installDecor(); + } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { + mContentParent.removeAllViews(); + } + + if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { + final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID, + getContext()); + transitionTo(newScene); + } else { + mLayoutInflater.inflate(layoutResID, mContentParent); + } + final Callback cb = getCallback(); + if (cb != null && !isDestroyed()) { + cb.onContentChanged(); + } + } + + @Override + public void setContentView(View view) { + setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); + } + + @Override + public void setContentView(View view, ViewGroup.LayoutParams params) { + // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window + // decor, when theme attributes and the like are crystalized. Do not check the feature + // before this happens. + if (mContentParent == null) { + installDecor(); + } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { + mContentParent.removeAllViews(); + } + + if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { + view.setLayoutParams(params); + final Scene newScene = new Scene(mContentParent, view); + transitionTo(newScene); + } else { + mContentParent.addView(view, params); + } + final Callback cb = getCallback(); + if (cb != null && !isDestroyed()) { + cb.onContentChanged(); + } + } + + @Override + public void addContentView(View view, ViewGroup.LayoutParams params) { + if (mContentParent == null) { + installDecor(); + } + if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { + // TODO Augment the scenes/transitions API to support this. + Log.v(TAG, "addContentView does not support content transitions"); + } + mContentParent.addView(view, params); + final Callback cb = getCallback(); + if (cb != null && !isDestroyed()) { + cb.onContentChanged(); + } + } + + private void transitionTo(Scene scene) { + if (mContentScene == null) { + scene.enter(); + } else { + mTransitionManager.transitionTo(scene); + } + mContentScene = scene; + } + + @Override + public View getCurrentFocus() { + return mDecor != null ? mDecor.findFocus() : null; + } + + @Override + public void takeSurface(SurfaceHolder.Callback2 callback) { + mTakeSurfaceCallback = callback; + } + + public void takeInputQueue(InputQueue.Callback callback) { + mTakeInputQueueCallback = callback; + } + + @Override + public boolean isFloating() { + return mIsFloating; + } + + /** + * Return a LayoutInflater instance that can be used to inflate XML view layout + * resources for use in this Window. + * + * @return LayoutInflater The shared LayoutInflater. + */ + @Override + public LayoutInflater getLayoutInflater() { + return mLayoutInflater; + } + + @Override + public void setTitle(CharSequence title) { + if (mTitleView != null) { + mTitleView.setText(title); + } else if (mDecorContentParent != null) { + mDecorContentParent.setWindowTitle(title); + } + mTitle = title; + } + + @Override + @Deprecated + public void setTitleColor(int textColor) { + if (mTitleView != null) { + mTitleView.setTextColor(textColor); + } + mTitleColor = textColor; + } + + /** + * Prepares the panel to either be opened or chorded. This creates the Menu + * instance for the panel and populates it via the Activity callbacks. + * + * @param st The panel state to prepare. + * @param event The event that triggered the preparing of the panel. + * @return Whether the panel was prepared. If the panel should not be shown, + * returns false. + */ + public final boolean preparePanel(PanelFeatureState st, KeyEvent event) { + if (isDestroyed()) { + return false; + } + + // Already prepared (isPrepared will be reset to false later) + if (st.isPrepared) { + return true; + } + + if ((mPreparedPanel != null) && (mPreparedPanel != st)) { + // Another Panel is prepared and possibly open, so close it + closePanel(mPreparedPanel, false); + } + + final Callback cb = getCallback(); + + if (cb != null) { + st.createdPanelView = cb.onCreatePanelView(st.featureId); + } + + final boolean isActionBarMenu = + (st.featureId == FEATURE_OPTIONS_PANEL || st.featureId == FEATURE_ACTION_BAR); + + if (isActionBarMenu && mDecorContentParent != null) { + // Enforce ordering guarantees around events so that the action bar never + // dispatches menu-related events before the panel is prepared. + mDecorContentParent.setMenuPrepared(); + } + + if (st.createdPanelView == null) { + // Init the panel state's menu--return false if init failed + if (st.menu == null || st.refreshMenuContent) { + if (st.menu == null) { + if (!initializePanelMenu(st) || (st.menu == null)) { + return false; + } + } + + if (isActionBarMenu && mDecorContentParent != null) { + if (mActionMenuPresenterCallback == null) { + mActionMenuPresenterCallback = new ActionMenuPresenterCallback(); + } + mDecorContentParent.setMenu(st.menu, mActionMenuPresenterCallback); + } + + // Call callback, and return if it doesn't want to display menu. + + // Creating the panel menu will involve a lot of manipulation; + // don't dispatch change events to presenters until we're done. + st.menu.stopDispatchingItemsChanged(); + if ((cb == null) || !cb.onCreatePanelMenu(st.featureId, st.menu)) { + // Ditch the menu created above + st.setMenu(null); + + if (isActionBarMenu && mDecorContentParent != null) { + // Don't show it in the action bar either + mDecorContentParent.setMenu(null, mActionMenuPresenterCallback); + } + + return false; + } + + st.refreshMenuContent = false; + } + + // Callback and return if the callback does not want to show the menu + + // Preparing the panel menu can involve a lot of manipulation; + // don't dispatch change events to presenters until we're done. + st.menu.stopDispatchingItemsChanged(); + + // Restore action view state before we prepare. This gives apps + // an opportunity to override frozen/restored state in onPrepare. + if (st.frozenActionViewState != null) { + st.menu.restoreActionViewStates(st.frozenActionViewState); + st.frozenActionViewState = null; + } + + if (!cb.onPreparePanel(st.featureId, st.createdPanelView, st.menu)) { + if (isActionBarMenu && mDecorContentParent != null) { + // The app didn't want to show the menu for now but it still exists. + // Clear it out of the action bar. + mDecorContentParent.setMenu(null, mActionMenuPresenterCallback); + } + st.menu.startDispatchingItemsChanged(); + return false; + } + + // Set the proper keymap + KeyCharacterMap kmap = KeyCharacterMap.load( + event != null ? event.getDeviceId() : KeyCharacterMap.VIRTUAL_KEYBOARD); + st.qwertyMode = kmap.getKeyboardType() != KeyCharacterMap.NUMERIC; + st.menu.setQwertyMode(st.qwertyMode); + st.menu.startDispatchingItemsChanged(); + } + + // Set other state + st.isPrepared = true; + st.isHandled = false; + mPreparedPanel = st; + + return true; + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + // Action bars handle their own menu state + if (mDecorContentParent == null) { + PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false); + if ((st != null) && (st.menu != null)) { + if (st.isOpen) { + // Freeze state + final Bundle state = new Bundle(); + if (st.iconMenuPresenter != null) { + st.iconMenuPresenter.saveHierarchyState(state); + } + if (st.listMenuPresenter != null) { + st.listMenuPresenter.saveHierarchyState(state); + } + + // Remove the menu views since they need to be recreated + // according to the new configuration + clearMenuViews(st); + + // Re-open the same menu + reopenMenu(false); + + // Restore state + if (st.iconMenuPresenter != null) { + st.iconMenuPresenter.restoreHierarchyState(state); + } + if (st.listMenuPresenter != null) { + st.listMenuPresenter.restoreHierarchyState(state); + } + + } else { + // Clear menu views so on next menu opening, it will use + // the proper layout + clearMenuViews(st); + } + } + } + } + + private static void clearMenuViews(PanelFeatureState st) { + // This can be called on config changes, so we should make sure + // the views will be reconstructed based on the new orientation, etc. + + // Allow the callback to create a new panel view + st.createdPanelView = null; + + // Causes the decor view to be recreated + st.refreshDecorView = true; + + st.clearMenuPresenters(); + } + + @Override + public final void openPanel(int featureId, KeyEvent event) { + if (featureId == FEATURE_OPTIONS_PANEL && mDecorContentParent != null && + mDecorContentParent.canShowOverflowMenu() && + !ViewConfiguration.get(getContext()).hasPermanentMenuKey()) { + mDecorContentParent.showOverflowMenu(); + } else { + openPanel(getPanelState(featureId, true), event); + } + } + + private void openPanel(final PanelFeatureState st, KeyEvent event) { + // System.out.println("Open panel: isOpen=" + st.isOpen); + + // Already open, return + if (st.isOpen || isDestroyed()) { + return; + } + + // Don't open an options panel for honeycomb apps on xlarge devices. + // (The app should be using an action bar for menu items.) + if (st.featureId == FEATURE_OPTIONS_PANEL) { + Context context = getContext(); + Configuration config = context.getResources().getConfiguration(); + boolean isXLarge = (config.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == + Configuration.SCREENLAYOUT_SIZE_XLARGE; + boolean isHoneycombApp = context.getApplicationInfo().targetSdkVersion >= + android.os.Build.VERSION_CODES.HONEYCOMB; + + if (isXLarge && isHoneycombApp) { + return; + } + } + + Callback cb = getCallback(); + if ((cb != null) && (!cb.onMenuOpened(st.featureId, st.menu))) { + // Callback doesn't want the menu to open, reset any state + closePanel(st, true); + return; + } + + final WindowManager wm = getWindowManager(); + if (wm == null) { + return; + } + + // Prepare panel (should have been done before, but just in case) + if (!preparePanel(st, event)) { + return; + } + + int width = WRAP_CONTENT; + if (st.decorView == null || st.refreshDecorView) { + if (st.decorView == null) { + // Initialize the panel decor, this will populate st.decorView + if (!initializePanelDecor(st) || (st.decorView == null)) + return; + } else if (st.refreshDecorView && (st.decorView.getChildCount() > 0)) { + // Decor needs refreshing, so remove its views + st.decorView.removeAllViews(); + } + + // This will populate st.shownPanelView + if (!initializePanelContent(st) || !st.hasPanelItems()) { + return; + } + + ViewGroup.LayoutParams lp = st.shownPanelView.getLayoutParams(); + if (lp == null) { + lp = new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT); + } + + int backgroundResId; + if (lp.width == ViewGroup.LayoutParams.MATCH_PARENT) { + // If the contents is fill parent for the width, set the + // corresponding background + backgroundResId = st.fullBackground; + width = MATCH_PARENT; + } else { + // Otherwise, set the normal panel background + backgroundResId = st.background; + } + st.decorView.setWindowBackground(getContext().getDrawable( + backgroundResId)); + + ViewParent shownPanelParent = st.shownPanelView.getParent(); + if (shownPanelParent != null && shownPanelParent instanceof ViewGroup) { + ((ViewGroup) shownPanelParent).removeView(st.shownPanelView); + } + st.decorView.addView(st.shownPanelView, lp); + + /* + * Give focus to the view, if it or one of its children does not + * already have it. + */ + if (!st.shownPanelView.hasFocus()) { + st.shownPanelView.requestFocus(); + } + } else if (!st.isInListMode()) { + width = MATCH_PARENT; + } else if (st.createdPanelView != null) { + // If we already had a panel view, carry width=MATCH_PARENT through + // as we did above when it was created. + ViewGroup.LayoutParams lp = st.createdPanelView.getLayoutParams(); + if (lp != null && lp.width == ViewGroup.LayoutParams.MATCH_PARENT) { + width = MATCH_PARENT; + } + } + + st.isHandled = false; + + WindowManager.LayoutParams lp = new WindowManager.LayoutParams( + width, WRAP_CONTENT, + st.x, st.y, WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG, + WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM + | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH, + st.decorView.mDefaultOpacity); + + if (st.isCompact) { + lp.gravity = getOptionsPanelGravity(); + sRotationWatcher.addWindow(this); + } else { + lp.gravity = st.gravity; + } + + lp.windowAnimations = st.windowAnimations; + + wm.addView(st.decorView, lp); + st.isOpen = true; + // Log.v(TAG, "Adding main menu to window manager."); + } + + @Override + public final void closePanel(int featureId) { + if (featureId == FEATURE_OPTIONS_PANEL && mDecorContentParent != null && + mDecorContentParent.canShowOverflowMenu() && + !ViewConfiguration.get(getContext()).hasPermanentMenuKey()) { + mDecorContentParent.hideOverflowMenu(); + } else if (featureId == FEATURE_CONTEXT_MENU) { + closeContextMenu(); + } else { + closePanel(getPanelState(featureId, true), true); + } + } + + /** + * Closes the given panel. + * + * @param st The panel to be closed. + * @param doCallback Whether to notify the callback that the panel was + * closed. If the panel is in the process of re-opening or + * opening another panel (e.g., menu opening a sub menu), the + * callback should not happen and this variable should be false. + * In addition, this method internally will only perform the + * callback if the panel is open. + */ + public final void closePanel(PanelFeatureState st, boolean doCallback) { + // System.out.println("Close panel: isOpen=" + st.isOpen); + if (doCallback && st.featureId == FEATURE_OPTIONS_PANEL && + mDecorContentParent != null && mDecorContentParent.isOverflowMenuShowing()) { + checkCloseActionMenu(st.menu); + return; + } + + final ViewManager wm = getWindowManager(); + if ((wm != null) && st.isOpen) { + if (st.decorView != null) { + wm.removeView(st.decorView); + // Log.v(TAG, "Removing main menu from window manager."); + if (st.isCompact) { + sRotationWatcher.removeWindow(this); + } + } + + if (doCallback) { + callOnPanelClosed(st.featureId, st, null); + } + } + + st.isPrepared = false; + st.isHandled = false; + st.isOpen = false; + + // This view is no longer shown, so null it out + st.shownPanelView = null; + + if (st.isInExpandedMode) { + // Next time the menu opens, it should not be in expanded mode, so + // force a refresh of the decor + st.refreshDecorView = true; + st.isInExpandedMode = false; + } + + if (mPreparedPanel == st) { + mPreparedPanel = null; + mPanelChordingKey = 0; + } + } + + void checkCloseActionMenu(Menu menu) { + if (mClosingActionMenu) { + return; + } + + mClosingActionMenu = true; + mDecorContentParent.dismissPopups(); + Callback cb = getCallback(); + if (cb != null && !isDestroyed()) { + cb.onPanelClosed(FEATURE_ACTION_BAR, menu); + } + mClosingActionMenu = false; + } + + @Override + public final void togglePanel(int featureId, KeyEvent event) { + PanelFeatureState st = getPanelState(featureId, true); + if (st.isOpen) { + closePanel(st, true); + } else { + openPanel(st, event); + } + } + + @Override + public void invalidatePanelMenu(int featureId) { + mInvalidatePanelMenuFeatures |= 1 << featureId; + + if (!mInvalidatePanelMenuPosted && mDecor != null) { + mDecor.postOnAnimation(mInvalidatePanelMenuRunnable); + mInvalidatePanelMenuPosted = true; + } + } + + void doPendingInvalidatePanelMenu() { + if (mInvalidatePanelMenuPosted) { + mDecor.removeCallbacks(mInvalidatePanelMenuRunnable); + mInvalidatePanelMenuRunnable.run(); + } + } + + void doInvalidatePanelMenu(int featureId) { + PanelFeatureState st = getPanelState(featureId, false); + if (st == null) { + return; + } + Bundle savedActionViewStates = null; + if (st.menu != null) { + savedActionViewStates = new Bundle(); + st.menu.saveActionViewStates(savedActionViewStates); + if (savedActionViewStates.size() > 0) { + st.frozenActionViewState = savedActionViewStates; + } + // This will be started again when the panel is prepared. + st.menu.stopDispatchingItemsChanged(); + st.menu.clear(); + } + st.refreshMenuContent = true; + st.refreshDecorView = true; + + // Prepare the options panel if we have an action bar + if ((featureId == FEATURE_ACTION_BAR || featureId == FEATURE_OPTIONS_PANEL) + && mDecorContentParent != null) { + st = getPanelState(Window.FEATURE_OPTIONS_PANEL, false); + if (st != null) { + st.isPrepared = false; + preparePanel(st, null); + } + } + } + + /** + * Called when the panel key is pushed down. + * @param featureId The feature ID of the relevant panel (defaults to FEATURE_OPTIONS_PANEL}. + * @param event The key event. + * @return Whether the key was handled. + */ + public final boolean onKeyDownPanel(int featureId, KeyEvent event) { + final int keyCode = event.getKeyCode(); + + if (event.getRepeatCount() == 0) { + // The panel key was pushed, so set the chording key + mPanelChordingKey = keyCode; + + PanelFeatureState st = getPanelState(featureId, false); + if (st != null && !st.isOpen) { + return preparePanel(st, event); + } + } + + return false; + } + + /** + * Called when the panel key is released. + * @param featureId The feature ID of the relevant panel (defaults to FEATURE_OPTIONS_PANEL}. + * @param event The key event. + */ + public final void onKeyUpPanel(int featureId, KeyEvent event) { + // The panel key was released, so clear the chording key + if (mPanelChordingKey != 0) { + mPanelChordingKey = 0; + + final PanelFeatureState st = getPanelState(featureId, false); + + if (event.isCanceled() || (mDecor != null && mDecor.mPrimaryActionMode != null) || + (st == null)) { + return; + } + + boolean playSoundEffect = false; + if (featureId == FEATURE_OPTIONS_PANEL && mDecorContentParent != null && + mDecorContentParent.canShowOverflowMenu() && + !ViewConfiguration.get(getContext()).hasPermanentMenuKey()) { + if (!mDecorContentParent.isOverflowMenuShowing()) { + if (!isDestroyed() && preparePanel(st, event)) { + playSoundEffect = mDecorContentParent.showOverflowMenu(); + } + } else { + playSoundEffect = mDecorContentParent.hideOverflowMenu(); + } + } else { + if (st.isOpen || st.isHandled) { + + // Play the sound effect if the user closed an open menu (and not if + // they just released a menu shortcut) + playSoundEffect = st.isOpen; + + // Close menu + closePanel(st, true); + + } else if (st.isPrepared) { + boolean show = true; + if (st.refreshMenuContent) { + // Something may have invalidated the menu since we prepared it. + // Re-prepare it to refresh. + st.isPrepared = false; + show = preparePanel(st, event); + } + + if (show) { + // Write 'menu opened' to event log + EventLog.writeEvent(50001, 0); + + // Show menu + openPanel(st, event); + + playSoundEffect = true; + } + } + } + + if (playSoundEffect) { + AudioManager audioManager = (AudioManager) getContext().getSystemService( + Context.AUDIO_SERVICE); + if (audioManager != null) { + audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK); + } else { + Log.w(TAG, "Couldn't get audio manager"); + } + } + } + } + + @Override + public final void closeAllPanels() { + final ViewManager wm = getWindowManager(); + if (wm == null) { + return; + } + + final PanelFeatureState[] panels = mPanels; + final int N = panels != null ? panels.length : 0; + for (int i = 0; i < N; i++) { + final PanelFeatureState panel = panels[i]; + if (panel != null) { + closePanel(panel, true); + } + } + + closeContextMenu(); + } + + /** + * Closes the context menu. This notifies the menu logic of the close, along + * with dismissing it from the UI. + */ + private synchronized void closeContextMenu() { + if (mContextMenu != null) { + mContextMenu.close(); + dismissContextMenu(); + } + } + + /** + * Dismisses just the context menu UI. To close the context menu, use + * {@link #closeContextMenu()}. + */ + private synchronized void dismissContextMenu() { + mContextMenu = null; + + if (mContextMenuHelper != null) { + mContextMenuHelper.dismiss(); + mContextMenuHelper = null; + } + } + + @Override + public boolean performPanelShortcut(int featureId, int keyCode, KeyEvent event, int flags) { + return performPanelShortcut(getPanelState(featureId, false), keyCode, event, flags); + } + + private boolean performPanelShortcut(PanelFeatureState st, int keyCode, KeyEvent event, + int flags) { + if (event.isSystem() || (st == null)) { + return false; + } + + boolean handled = false; + + // Only try to perform menu shortcuts if preparePanel returned true (possible false + // return value from application not wanting to show the menu). + if ((st.isPrepared || preparePanel(st, event)) && st.menu != null) { + // The menu is prepared now, perform the shortcut on it + handled = st.menu.performShortcut(keyCode, event, flags); + } + + if (handled) { + // Mark as handled + st.isHandled = true; + + // Only close down the menu if we don't have an action bar keeping it open. + if ((flags & Menu.FLAG_PERFORM_NO_CLOSE) == 0 && mDecorContentParent == null) { + closePanel(st, true); + } + } + + return handled; + } + + @Override + public boolean performPanelIdentifierAction(int featureId, int id, int flags) { + + PanelFeatureState st = getPanelState(featureId, true); + if (!preparePanel(st, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MENU))) { + return false; + } + if (st.menu == null) { + return false; + } + + boolean res = st.menu.performIdentifierAction(id, flags); + + // Only close down the menu if we don't have an action bar keeping it open. + if (mDecorContentParent == null) { + closePanel(st, true); + } + + return res; + } + + public PanelFeatureState findMenuPanel(Menu menu) { + final PanelFeatureState[] panels = mPanels; + final int N = panels != null ? panels.length : 0; + for (int i = 0; i < N; i++) { + final PanelFeatureState panel = panels[i]; + if (panel != null && panel.menu == menu) { + return panel; + } + } + return null; + } + + public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) { + final Callback cb = getCallback(); + if (cb != null && !isDestroyed()) { + final PanelFeatureState panel = findMenuPanel(menu.getRootMenu()); + if (panel != null) { + return cb.onMenuItemSelected(panel.featureId, item); + } + } + return false; + } + + public void onMenuModeChange(MenuBuilder menu) { + reopenMenu(true); + } + + private void reopenMenu(boolean toggleMenuMode) { + if (mDecorContentParent != null && mDecorContentParent.canShowOverflowMenu() && + (!ViewConfiguration.get(getContext()).hasPermanentMenuKey() || + mDecorContentParent.isOverflowMenuShowPending())) { + final Callback cb = getCallback(); + if (!mDecorContentParent.isOverflowMenuShowing() || !toggleMenuMode) { + if (cb != null && !isDestroyed()) { + // If we have a menu invalidation pending, do it now. + if (mInvalidatePanelMenuPosted && + (mInvalidatePanelMenuFeatures & (1 << FEATURE_OPTIONS_PANEL)) != 0) { + mDecor.removeCallbacks(mInvalidatePanelMenuRunnable); + mInvalidatePanelMenuRunnable.run(); + } + + final PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false); + + // If we don't have a menu or we're waiting for a full content refresh, + // forget it. This is a lingering event that no longer matters. + if (st != null && st.menu != null && !st.refreshMenuContent && + cb.onPreparePanel(FEATURE_OPTIONS_PANEL, st.createdPanelView, st.menu)) { + cb.onMenuOpened(FEATURE_ACTION_BAR, st.menu); + mDecorContentParent.showOverflowMenu(); + } + } + } else { + mDecorContentParent.hideOverflowMenu(); + final PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false); + if (st != null && cb != null && !isDestroyed()) { + cb.onPanelClosed(FEATURE_ACTION_BAR, st.menu); + } + } + return; + } + + PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false); + + if (st == null) { + return; + } + + // Save the future expanded mode state since closePanel will reset it + boolean newExpandedMode = toggleMenuMode ? !st.isInExpandedMode : st.isInExpandedMode; + + st.refreshDecorView = true; + closePanel(st, false); + + // Set the expanded mode state + st.isInExpandedMode = newExpandedMode; + + openPanel(st, null); + } + + /** + * Initializes the menu associated with the given panel feature state. You + * must at the very least set PanelFeatureState.menu to the Menu to be + * associated with the given panel state. The default implementation creates + * a new menu for the panel state. + * + * @param st The panel whose menu is being initialized. + * @return Whether the initialization was successful. + */ + protected boolean initializePanelMenu(final PanelFeatureState st) { + Context context = getContext(); + + // If we have an action bar, initialize the menu with the right theme. + if ((st.featureId == FEATURE_OPTIONS_PANEL || st.featureId == FEATURE_ACTION_BAR) && + mDecorContentParent != null) { + final TypedValue outValue = new TypedValue(); + final Theme baseTheme = context.getTheme(); + baseTheme.resolveAttribute(R.attr.actionBarTheme, outValue, true); + + Theme widgetTheme = null; + if (outValue.resourceId != 0) { + widgetTheme = context.getResources().newTheme(); + widgetTheme.setTo(baseTheme); + widgetTheme.applyStyle(outValue.resourceId, true); + widgetTheme.resolveAttribute( + R.attr.actionBarWidgetTheme, outValue, true); + } else { + baseTheme.resolveAttribute( + R.attr.actionBarWidgetTheme, outValue, true); + } + + if (outValue.resourceId != 0) { + if (widgetTheme == null) { + widgetTheme = context.getResources().newTheme(); + widgetTheme.setTo(baseTheme); + } + widgetTheme.applyStyle(outValue.resourceId, true); + } + + if (widgetTheme != null) { + context = new ContextThemeWrapper(context, 0); + context.getTheme().setTo(widgetTheme); + } + } + + final MenuBuilder menu = new MenuBuilder(context); + menu.setCallback(this); + st.setMenu(menu); + + return true; + } + + /** + * Perform initial setup of a panel. This should at the very least set the + * style information in the PanelFeatureState and must set + * PanelFeatureState.decor to the panel's window decor view. + * + * @param st The panel being initialized. + */ + protected boolean initializePanelDecor(PanelFeatureState st) { + st.decorView = new DecorView(getContext(), st.featureId); + st.gravity = Gravity.CENTER | Gravity.BOTTOM; + st.setStyle(getContext()); + TypedArray a = getContext().obtainStyledAttributes(null, + R.styleable.Window, 0, st.listPresenterTheme); + final float elevation = a.getDimension(R.styleable.Window_windowElevation, 0); + if (elevation != 0) { + st.decorView.setElevation(elevation); + } + a.recycle(); + + return true; + } + + /** + * Determine the gravity value for the options panel. This can + * differ in compact mode. + * + * @return gravity value to use for the panel window + */ + private int getOptionsPanelGravity() { + try { + return WindowManagerHolder.sWindowManager.getPreferredOptionsPanelGravity(); + } catch (RemoteException ex) { + Log.e(TAG, "Couldn't getOptionsPanelGravity; using default", ex); + return Gravity.CENTER | Gravity.BOTTOM; + } + } + + void onOptionsPanelRotationChanged() { + final PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false); + if (st == null) return; + + final WindowManager.LayoutParams lp = st.decorView != null ? + (WindowManager.LayoutParams) st.decorView.getLayoutParams() : null; + if (lp != null) { + lp.gravity = getOptionsPanelGravity(); + final ViewManager wm = getWindowManager(); + if (wm != null) { + wm.updateViewLayout(st.decorView, lp); + } + } + } + + /** + * Initializes the panel associated with the panel feature state. You must + * at the very least set PanelFeatureState.panel to the View implementing + * its contents. The default implementation gets the panel from the menu. + * + * @param st The panel state being initialized. + * @return Whether the initialization was successful. + */ + protected boolean initializePanelContent(PanelFeatureState st) { + if (st.createdPanelView != null) { + st.shownPanelView = st.createdPanelView; + return true; + } + + if (st.menu == null) { + return false; + } + + if (mPanelMenuPresenterCallback == null) { + mPanelMenuPresenterCallback = new PanelMenuPresenterCallback(); + } + + MenuView menuView = st.isInListMode() + ? st.getListMenuView(getContext(), mPanelMenuPresenterCallback) + : st.getIconMenuView(getContext(), mPanelMenuPresenterCallback); + + st.shownPanelView = (View) menuView; + + if (st.shownPanelView != null) { + // Use the menu View's default animations if it has any + final int defaultAnimations = menuView.getWindowAnimations(); + if (defaultAnimations != 0) { + st.windowAnimations = defaultAnimations; + } + return true; + } else { + return false; + } + } + + @Override + public boolean performContextMenuIdentifierAction(int id, int flags) { + return (mContextMenu != null) ? mContextMenu.performIdentifierAction(id, flags) : false; + } + + @Override + public final void setElevation(float elevation) { + mElevation = elevation; + if (mDecor != null) { + mDecor.setElevation(elevation); + } + dispatchWindowAttributesChanged(getAttributes()); + } + + @Override + public final void setClipToOutline(boolean clipToOutline) { + mClipToOutline = clipToOutline; + if (mDecor != null) { + mDecor.setClipToOutline(clipToOutline); + } + } + + @Override + public final void setBackgroundDrawable(Drawable drawable) { + if (drawable != mBackgroundDrawable || mBackgroundResource != 0) { + mBackgroundResource = 0; + mBackgroundDrawable = drawable; + if (mDecor != null) { + mDecor.setWindowBackground(drawable); + } + if (mBackgroundFallbackResource != 0) { + mDecor.setBackgroundFallback(drawable != null ? 0 : mBackgroundFallbackResource); + } + } + } + + @Override + public final void setFeatureDrawableResource(int featureId, int resId) { + if (resId != 0) { + DrawableFeatureState st = getDrawableState(featureId, true); + if (st.resid != resId) { + st.resid = resId; + st.uri = null; + st.local = getContext().getDrawable(resId); + updateDrawable(featureId, st, false); + } + } else { + setFeatureDrawable(featureId, null); + } + } + + @Override + public final void setFeatureDrawableUri(int featureId, Uri uri) { + if (uri != null) { + DrawableFeatureState st = getDrawableState(featureId, true); + if (st.uri == null || !st.uri.equals(uri)) { + st.resid = 0; + st.uri = uri; + st.local = loadImageURI(uri); + updateDrawable(featureId, st, false); + } + } else { + setFeatureDrawable(featureId, null); + } + } + + @Override + public final void setFeatureDrawable(int featureId, Drawable drawable) { + DrawableFeatureState st = getDrawableState(featureId, true); + st.resid = 0; + st.uri = null; + if (st.local != drawable) { + st.local = drawable; + updateDrawable(featureId, st, false); + } + } + + @Override + public void setFeatureDrawableAlpha(int featureId, int alpha) { + DrawableFeatureState st = getDrawableState(featureId, true); + if (st.alpha != alpha) { + st.alpha = alpha; + updateDrawable(featureId, st, false); + } + } + + protected final void setFeatureDefaultDrawable(int featureId, Drawable drawable) { + DrawableFeatureState st = getDrawableState(featureId, true); + if (st.def != drawable) { + st.def = drawable; + updateDrawable(featureId, st, false); + } + } + + @Override + public final void setFeatureInt(int featureId, int value) { + // XXX Should do more management (as with drawable features) to + // deal with interactions between multiple window policies. + updateInt(featureId, value, false); + } + + /** + * Update the state of a drawable feature. This should be called, for every + * drawable feature supported, as part of onActive(), to make sure that the + * contents of a containing window is properly updated. + * + * @see #onActive + * @param featureId The desired drawable feature to change. + * @param fromActive Always true when called from onActive(). + */ + protected final void updateDrawable(int featureId, boolean fromActive) { + final DrawableFeatureState st = getDrawableState(featureId, false); + if (st != null) { + updateDrawable(featureId, st, fromActive); + } + } + + /** + * Called when a Drawable feature changes, for the window to update its + * graphics. + * + * @param featureId The feature being changed. + * @param drawable The new Drawable to show, or null if none. + * @param alpha The new alpha blending of the Drawable. + */ + protected void onDrawableChanged(int featureId, Drawable drawable, int alpha) { + ImageView view; + if (featureId == FEATURE_LEFT_ICON) { + view = getLeftIconView(); + } else if (featureId == FEATURE_RIGHT_ICON) { + view = getRightIconView(); + } else { + return; + } + + if (drawable != null) { + drawable.setAlpha(alpha); + view.setImageDrawable(drawable); + view.setVisibility(View.VISIBLE); + } else { + view.setVisibility(View.GONE); + } + } + + /** + * Called when an int feature changes, for the window to update its + * graphics. + * + * @param featureId The feature being changed. + * @param value The new integer value. + */ + protected void onIntChanged(int featureId, int value) { + if (featureId == FEATURE_PROGRESS || featureId == FEATURE_INDETERMINATE_PROGRESS) { + updateProgressBars(value); + } else if (featureId == FEATURE_CUSTOM_TITLE) { + FrameLayout titleContainer = (FrameLayout) findViewById(R.id.title_container); + if (titleContainer != null) { + mLayoutInflater.inflate(value, titleContainer); + } + } + } + + /** + * Updates the progress bars that are shown in the title bar. + * + * @param value Can be one of {@link Window#PROGRESS_VISIBILITY_ON}, + * {@link Window#PROGRESS_VISIBILITY_OFF}, + * {@link Window#PROGRESS_INDETERMINATE_ON}, + * {@link Window#PROGRESS_INDETERMINATE_OFF}, or a value + * starting at {@link Window#PROGRESS_START} through + * {@link Window#PROGRESS_END} for setting the default + * progress (if {@link Window#PROGRESS_END} is given, + * the progress bar widgets in the title will be hidden after an + * animation), a value between + * {@link Window#PROGRESS_SECONDARY_START} - + * {@link Window#PROGRESS_SECONDARY_END} for the + * secondary progress (if + * {@link Window#PROGRESS_SECONDARY_END} is given, the + * progress bar widgets will still be shown with the secondary + * progress bar will be completely filled in.) + */ + private void updateProgressBars(int value) { + ProgressBar circularProgressBar = getCircularProgressBar(true); + ProgressBar horizontalProgressBar = getHorizontalProgressBar(true); + + final int features = getLocalFeatures(); + if (value == PROGRESS_VISIBILITY_ON) { + if ((features & (1 << FEATURE_PROGRESS)) != 0) { + if (horizontalProgressBar != null) { + int level = horizontalProgressBar.getProgress(); + int visibility = (horizontalProgressBar.isIndeterminate() || level < 10000) ? + View.VISIBLE : View.INVISIBLE; + horizontalProgressBar.setVisibility(visibility); + } else { + Log.e(TAG, "Horizontal progress bar not located in current window decor"); + } + } + if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) { + if (circularProgressBar != null) { + circularProgressBar.setVisibility(View.VISIBLE); + } else { + Log.e(TAG, "Circular progress bar not located in current window decor"); + } + } + } else if (value == PROGRESS_VISIBILITY_OFF) { + if ((features & (1 << FEATURE_PROGRESS)) != 0) { + if (horizontalProgressBar != null) { + horizontalProgressBar.setVisibility(View.GONE); + } else { + Log.e(TAG, "Horizontal progress bar not located in current window decor"); + } + } + if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) { + if (circularProgressBar != null) { + circularProgressBar.setVisibility(View.GONE); + } else { + Log.e(TAG, "Circular progress bar not located in current window decor"); + } + } + } else if (value == PROGRESS_INDETERMINATE_ON) { + if (horizontalProgressBar != null) { + horizontalProgressBar.setIndeterminate(true); + } else { + Log.e(TAG, "Horizontal progress bar not located in current window decor"); + } + } else if (value == PROGRESS_INDETERMINATE_OFF) { + if (horizontalProgressBar != null) { + horizontalProgressBar.setIndeterminate(false); + } else { + Log.e(TAG, "Horizontal progress bar not located in current window decor"); + } + } else if (PROGRESS_START <= value && value <= PROGRESS_END) { + // We want to set the progress value before testing for visibility + // so that when the progress bar becomes visible again, it has the + // correct level. + if (horizontalProgressBar != null) { + horizontalProgressBar.setProgress(value - PROGRESS_START); + } else { + Log.e(TAG, "Horizontal progress bar not located in current window decor"); + } + + if (value < PROGRESS_END) { + showProgressBars(horizontalProgressBar, circularProgressBar); + } else { + hideProgressBars(horizontalProgressBar, circularProgressBar); + } + } else if (PROGRESS_SECONDARY_START <= value && value <= PROGRESS_SECONDARY_END) { + if (horizontalProgressBar != null) { + horizontalProgressBar.setSecondaryProgress(value - PROGRESS_SECONDARY_START); + } else { + Log.e(TAG, "Horizontal progress bar not located in current window decor"); + } + + showProgressBars(horizontalProgressBar, circularProgressBar); + } + + } + + private void showProgressBars(ProgressBar horizontalProgressBar, ProgressBar spinnyProgressBar) { + final int features = getLocalFeatures(); + if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0 && + spinnyProgressBar != null && spinnyProgressBar.getVisibility() == View.INVISIBLE) { + spinnyProgressBar.setVisibility(View.VISIBLE); + } + // Only show the progress bars if the primary progress is not complete + if ((features & (1 << FEATURE_PROGRESS)) != 0 && horizontalProgressBar != null && + horizontalProgressBar.getProgress() < 10000) { + horizontalProgressBar.setVisibility(View.VISIBLE); + } + } + + private void hideProgressBars(ProgressBar horizontalProgressBar, ProgressBar spinnyProgressBar) { + final int features = getLocalFeatures(); + Animation anim = AnimationUtils.loadAnimation(getContext(), R.anim.fade_out); + anim.setDuration(1000); + if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0 && + spinnyProgressBar != null && + spinnyProgressBar.getVisibility() == View.VISIBLE) { + spinnyProgressBar.startAnimation(anim); + spinnyProgressBar.setVisibility(View.INVISIBLE); + } + if ((features & (1 << FEATURE_PROGRESS)) != 0 && horizontalProgressBar != null && + horizontalProgressBar.getVisibility() == View.VISIBLE) { + horizontalProgressBar.startAnimation(anim); + horizontalProgressBar.setVisibility(View.INVISIBLE); + } + } + + @Override + public void setIcon(int resId) { + mIconRes = resId; + mResourcesSetFlags |= FLAG_RESOURCE_SET_ICON; + mResourcesSetFlags &= ~FLAG_RESOURCE_SET_ICON_FALLBACK; + if (mDecorContentParent != null) { + mDecorContentParent.setIcon(resId); + } + } + + @Override + public void setDefaultIcon(int resId) { + if ((mResourcesSetFlags & FLAG_RESOURCE_SET_ICON) != 0) { + return; + } + mIconRes = resId; + if (mDecorContentParent != null && (!mDecorContentParent.hasIcon() || + (mResourcesSetFlags & FLAG_RESOURCE_SET_ICON_FALLBACK) != 0)) { + if (resId != 0) { + mDecorContentParent.setIcon(resId); + mResourcesSetFlags &= ~FLAG_RESOURCE_SET_ICON_FALLBACK; + } else { + mDecorContentParent.setIcon( + getContext().getPackageManager().getDefaultActivityIcon()); + mResourcesSetFlags |= FLAG_RESOURCE_SET_ICON_FALLBACK; + } + } + } + + @Override + public void setLogo(int resId) { + mLogoRes = resId; + mResourcesSetFlags |= FLAG_RESOURCE_SET_LOGO; + if (mDecorContentParent != null) { + mDecorContentParent.setLogo(resId); + } + } + + @Override + public void setDefaultLogo(int resId) { + if ((mResourcesSetFlags & FLAG_RESOURCE_SET_LOGO) != 0) { + return; + } + mLogoRes = resId; + if (mDecorContentParent != null && !mDecorContentParent.hasLogo()) { + mDecorContentParent.setLogo(resId); + } + } + + @Override + public void setLocalFocus(boolean hasFocus, boolean inTouchMode) { + getViewRootImpl().windowFocusChanged(hasFocus, inTouchMode); + + } + + @Override + public void injectInputEvent(InputEvent event) { + getViewRootImpl().dispatchInputEvent(event); + } + + private ViewRootImpl getViewRootImpl() { + if (mDecor != null) { + ViewRootImpl viewRootImpl = mDecor.getViewRootImpl(); + if (viewRootImpl != null) { + return viewRootImpl; + } + } + throw new IllegalStateException("view not added"); + } + + /** + * Request that key events come to this activity. Use this if your activity + * has no views with focus, but the activity still wants a chance to process + * key events. + */ + @Override + public void takeKeyEvents(boolean get) { + mDecor.setFocusable(get); + } + + @Override + public boolean superDispatchKeyEvent(KeyEvent event) { + return mDecor.superDispatchKeyEvent(event); + } + + @Override + public boolean superDispatchKeyShortcutEvent(KeyEvent event) { + return mDecor.superDispatchKeyShortcutEvent(event); + } + + @Override + public boolean superDispatchTouchEvent(MotionEvent event) { + return mDecor.superDispatchTouchEvent(event); + } + + @Override + public boolean superDispatchTrackballEvent(MotionEvent event) { + return mDecor.superDispatchTrackballEvent(event); + } + + @Override + public boolean superDispatchGenericMotionEvent(MotionEvent event) { + return mDecor.superDispatchGenericMotionEvent(event); + } + + /** + * A key was pressed down and not handled by anything else in the window. + * + * @see #onKeyUp + * @see android.view.KeyEvent + */ + protected boolean onKeyDown(int featureId, int keyCode, KeyEvent event) { + /* **************************************************************************** + * HOW TO DECIDE WHERE YOUR KEY HANDLING GOES. + * + * If your key handling must happen before the app gets a crack at the event, + * it goes in PhoneWindowManager. + * + * If your key handling should happen in all windows, and does not depend on + * the state of the current application, other than that the current + * application can override the behavior by handling the event itself, it + * should go in PhoneFallbackEventHandler. + * + * Only if your handling depends on the window, and the fact that it has + * a DecorView, should it go here. + * ****************************************************************************/ + + final KeyEvent.DispatcherState dispatcher = + mDecor != null ? mDecor.getKeyDispatcherState() : null; + //Log.i(TAG, "Key down: repeat=" + event.getRepeatCount() + // + " flags=0x" + Integer.toHexString(event.getFlags())); + + switch (keyCode) { + case KeyEvent.KEYCODE_VOLUME_UP: + case KeyEvent.KEYCODE_VOLUME_DOWN: + case KeyEvent.KEYCODE_VOLUME_MUTE: { + int direction = 0; + switch (keyCode) { + case KeyEvent.KEYCODE_VOLUME_UP: + direction = AudioManager.ADJUST_RAISE; + break; + case KeyEvent.KEYCODE_VOLUME_DOWN: + direction = AudioManager.ADJUST_LOWER; + break; + case KeyEvent.KEYCODE_VOLUME_MUTE: + direction = AudioManager.ADJUST_TOGGLE_MUTE; + break; + } + // If we have a session send it the volume command, otherwise + // use the suggested stream. + if (mMediaController != null) { + mMediaController.adjustVolume(direction, AudioManager.FLAG_SHOW_UI); + } else { + MediaSessionLegacyHelper.getHelper(getContext()).sendAdjustVolumeBy( + mVolumeControlStreamType, direction, + AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_VIBRATE + | AudioManager.FLAG_FROM_KEY); + } + return true; + } + // These are all the recognized media key codes in + // KeyEvent.isMediaKey() + case KeyEvent.KEYCODE_MEDIA_PLAY: + case KeyEvent.KEYCODE_MEDIA_PAUSE: + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + case KeyEvent.KEYCODE_MUTE: + case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MEDIA_STOP: + case KeyEvent.KEYCODE_MEDIA_NEXT: + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + case KeyEvent.KEYCODE_MEDIA_REWIND: + case KeyEvent.KEYCODE_MEDIA_RECORD: + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: { + if (mMediaController != null) { + if (mMediaController.dispatchMediaButtonEvent(event)) { + return true; + } + } + return false; + } + + case KeyEvent.KEYCODE_MENU: { + onKeyDownPanel((featureId < 0) ? FEATURE_OPTIONS_PANEL : featureId, event); + return true; + } + + case KeyEvent.KEYCODE_BACK: { + if (event.getRepeatCount() > 0) break; + if (featureId < 0) break; + // Currently don't do anything with long press. + if (dispatcher != null) { + dispatcher.startTracking(event, this); + } + return true; + } + + } + + return false; + } + + private KeyguardManager getKeyguardManager() { + if (mKeyguardManager == null) { + mKeyguardManager = (KeyguardManager) getContext().getSystemService( + Context.KEYGUARD_SERVICE); + } + return mKeyguardManager; + } + + AudioManager getAudioManager() { + if (mAudioManager == null) { + mAudioManager = (AudioManager)getContext().getSystemService(Context.AUDIO_SERVICE); + } + return mAudioManager; + } + + /** + * A key was released and not handled by anything else in the window. + * + * @see #onKeyDown + * @see android.view.KeyEvent + */ + protected boolean onKeyUp(int featureId, int keyCode, KeyEvent event) { + final KeyEvent.DispatcherState dispatcher = + mDecor != null ? mDecor.getKeyDispatcherState() : null; + if (dispatcher != null) { + dispatcher.handleUpEvent(event); + } + //Log.i(TAG, "Key up: repeat=" + event.getRepeatCount() + // + " flags=0x" + Integer.toHexString(event.getFlags())); + + switch (keyCode) { + case KeyEvent.KEYCODE_VOLUME_UP: + case KeyEvent.KEYCODE_VOLUME_DOWN: { + final int flags = AudioManager.FLAG_PLAY_SOUND | AudioManager.FLAG_VIBRATE + | AudioManager.FLAG_FROM_KEY; + // If we have a session send it the volume command, otherwise + // use the suggested stream. + if (mMediaController != null) { + mMediaController.adjustVolume(0, flags); + } else { + MediaSessionLegacyHelper.getHelper(getContext()).sendAdjustVolumeBy( + mVolumeControlStreamType, 0, flags); + } + return true; + } + case KeyEvent.KEYCODE_VOLUME_MUTE: { + // Similar code is in PhoneFallbackEventHandler in case the window + // doesn't have one of these. In this case, we execute it here and + // eat the event instead, because we have mVolumeControlStreamType + // and they don't. + getAudioManager().handleKeyUp(event, mVolumeControlStreamType); + return true; + } + // These are all the recognized media key codes in + // KeyEvent.isMediaKey() + case KeyEvent.KEYCODE_MEDIA_PLAY: + case KeyEvent.KEYCODE_MEDIA_PAUSE: + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + case KeyEvent.KEYCODE_MUTE: + case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MEDIA_STOP: + case KeyEvent.KEYCODE_MEDIA_NEXT: + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + case KeyEvent.KEYCODE_MEDIA_REWIND: + case KeyEvent.KEYCODE_MEDIA_RECORD: + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: { + if (mMediaController != null) { + if (mMediaController.dispatchMediaButtonEvent(event)) { + return true; + } + } + return false; + } + + case KeyEvent.KEYCODE_MENU: { + onKeyUpPanel(featureId < 0 ? FEATURE_OPTIONS_PANEL : featureId, + event); + return true; + } + + case KeyEvent.KEYCODE_BACK: { + if (featureId < 0) break; + if (event.isTracking() && !event.isCanceled()) { + if (featureId == FEATURE_OPTIONS_PANEL) { + PanelFeatureState st = getPanelState(featureId, false); + if (st != null && st.isInExpandedMode) { + // If the user is in an expanded menu and hits back, it + // should go back to the icon menu + reopenMenu(true); + return true; + } + } + closePanel(featureId); + return true; + } + break; + } + + case KeyEvent.KEYCODE_SEARCH: { + /* + * Do this in onKeyUp since the Search key is also used for + * chording quick launch shortcuts. + */ + if (getKeyguardManager().inKeyguardRestrictedInputMode()) { + break; + } + if (event.isTracking() && !event.isCanceled()) { + launchDefaultSearch(); + } + return true; + } + } + + return false; + } + + @Override + protected void onActive() { + } + + @Override + public final View getDecorView() { + if (mDecor == null) { + installDecor(); + } + return mDecor; + } + + @Override + public final View peekDecorView() { + return mDecor; + } + + static private final String FOCUSED_ID_TAG = "android:focusedViewId"; + static private final String VIEWS_TAG = "android:views"; + static private final String PANELS_TAG = "android:Panels"; + static private final String ACTION_BAR_TAG = "android:ActionBar"; + + /** {@inheritDoc} */ + @Override + public Bundle saveHierarchyState() { + Bundle outState = new Bundle(); + if (mContentParent == null) { + return outState; + } + + SparseArray<Parcelable> states = new SparseArray<Parcelable>(); + mContentParent.saveHierarchyState(states); + outState.putSparseParcelableArray(VIEWS_TAG, states); + + // save the focused view id + View focusedView = mContentParent.findFocus(); + if (focusedView != null) { + if (focusedView.getId() != View.NO_ID) { + outState.putInt(FOCUSED_ID_TAG, focusedView.getId()); + } else { + if (false) { + Log.d(TAG, "couldn't save which view has focus because the focused view " + + focusedView + " has no id."); + } + } + } + + // save the panels + SparseArray<Parcelable> panelStates = new SparseArray<Parcelable>(); + savePanelState(panelStates); + if (panelStates.size() > 0) { + outState.putSparseParcelableArray(PANELS_TAG, panelStates); + } + + if (mDecorContentParent != null) { + SparseArray<Parcelable> actionBarStates = new SparseArray<Parcelable>(); + mDecorContentParent.saveToolbarHierarchyState(actionBarStates); + outState.putSparseParcelableArray(ACTION_BAR_TAG, actionBarStates); + } + + return outState; + } + + /** {@inheritDoc} */ + @Override + public void restoreHierarchyState(Bundle savedInstanceState) { + if (mContentParent == null) { + return; + } + + SparseArray<Parcelable> savedStates + = savedInstanceState.getSparseParcelableArray(VIEWS_TAG); + if (savedStates != null) { + mContentParent.restoreHierarchyState(savedStates); + } + + // restore the focused view + int focusedViewId = savedInstanceState.getInt(FOCUSED_ID_TAG, View.NO_ID); + if (focusedViewId != View.NO_ID) { + View needsFocus = mContentParent.findViewById(focusedViewId); + if (needsFocus != null) { + needsFocus.requestFocus(); + } else { + Log.w(TAG, + "Previously focused view reported id " + focusedViewId + + " during save, but can't be found during restore."); + } + } + + // restore the panels + SparseArray<Parcelable> panelStates = savedInstanceState.getSparseParcelableArray(PANELS_TAG); + if (panelStates != null) { + restorePanelState(panelStates); + } + + if (mDecorContentParent != null) { + SparseArray<Parcelable> actionBarStates = + savedInstanceState.getSparseParcelableArray(ACTION_BAR_TAG); + if (actionBarStates != null) { + doPendingInvalidatePanelMenu(); + mDecorContentParent.restoreToolbarHierarchyState(actionBarStates); + } else { + Log.w(TAG, "Missing saved instance states for action bar views! " + + "State will not be restored."); + } + } + } + + /** + * Invoked when the panels should freeze their state. + * + * @param icicles Save state into this. This is usually indexed by the + * featureId. This will be given to {@link #restorePanelState} in the + * future. + */ + private void savePanelState(SparseArray<Parcelable> icicles) { + PanelFeatureState[] panels = mPanels; + if (panels == null) { + return; + } + + for (int curFeatureId = panels.length - 1; curFeatureId >= 0; curFeatureId--) { + if (panels[curFeatureId] != null) { + icicles.put(curFeatureId, panels[curFeatureId].onSaveInstanceState()); + } + } + } + + /** + * Invoked when the panels should thaw their state from a previously frozen state. + * + * @param icicles The state saved by {@link #savePanelState} that needs to be thawed. + */ + private void restorePanelState(SparseArray<Parcelable> icicles) { + PanelFeatureState st; + int curFeatureId; + for (int i = icicles.size() - 1; i >= 0; i--) { + curFeatureId = icicles.keyAt(i); + st = getPanelState(curFeatureId, false /* required */); + if (st == null) { + // The panel must not have been required, and is currently not around, skip it + continue; + } + + st.onRestoreInstanceState(icicles.get(curFeatureId)); + invalidatePanelMenu(curFeatureId); + } + + /* + * Implementation note: call openPanelsAfterRestore later to actually open the + * restored panels. + */ + } + + /** + * Opens the panels that have had their state restored. This should be + * called sometime after {@link #restorePanelState} when it is safe to add + * to the window manager. + */ + private void openPanelsAfterRestore() { + PanelFeatureState[] panels = mPanels; + + if (panels == null) { + return; + } + + PanelFeatureState st; + for (int i = panels.length - 1; i >= 0; i--) { + st = panels[i]; + // We restore the panel if it was last open; we skip it if it + // now is open, to avoid a race condition if the user immediately + // opens it when we are resuming. + if (st != null) { + st.applyFrozenState(); + if (!st.isOpen && st.wasLastOpen) { + st.isInExpandedMode = st.wasLastExpanded; + openPanel(st, null); + } + } + } + } + + private class PanelMenuPresenterCallback implements MenuPresenter.Callback { + @Override + public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { + final Menu parentMenu = menu.getRootMenu(); + final boolean isSubMenu = parentMenu != menu; + final PanelFeatureState panel = findMenuPanel(isSubMenu ? parentMenu : menu); + if (panel != null) { + if (isSubMenu) { + callOnPanelClosed(panel.featureId, panel, parentMenu); + closePanel(panel, true); + } else { + // Close the panel and only do the callback if the menu is being + // closed completely, not if opening a sub menu + closePanel(panel, allMenusAreClosing); + } + } + } + + @Override + public boolean onOpenSubMenu(MenuBuilder subMenu) { + if (subMenu == null && hasFeature(FEATURE_ACTION_BAR)) { + Callback cb = getCallback(); + if (cb != null && !isDestroyed()) { + cb.onMenuOpened(FEATURE_ACTION_BAR, subMenu); + } + } + + return true; + } + } + + private final class ActionMenuPresenterCallback implements MenuPresenter.Callback { + @Override + public boolean onOpenSubMenu(MenuBuilder subMenu) { + Callback cb = getCallback(); + if (cb != null) { + cb.onMenuOpened(FEATURE_ACTION_BAR, subMenu); + return true; + } + return false; + } + + @Override + public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { + checkCloseActionMenu(menu); + } + } + + private final class DecorView extends FrameLayout implements RootViewSurfaceTaker { + + /* package */int mDefaultOpacity = PixelFormat.OPAQUE; + + /** The feature ID of the panel, or -1 if this is the application's DecorView */ + private final int mFeatureId; + + private final Rect mDrawingBounds = new Rect(); + + private final Rect mBackgroundPadding = new Rect(); + + private final Rect mFramePadding = new Rect(); + + private final Rect mFrameOffsets = new Rect(); + + private boolean mChanging; + + private Drawable mMenuBackground; + private boolean mWatchingForMenu; + private int mDownY; + + private ActionMode mPrimaryActionMode; + private ActionMode mFloatingActionMode; + private ActionBarContextView mPrimaryActionModeView; + private PopupWindow mPrimaryActionModePopup; + private Runnable mShowPrimaryActionModePopup; + + // View added at runtime to draw under the status bar area + private View mStatusGuard; + // View added at runtime to draw under the navigation bar area + private View mNavigationGuard; + + private final ColorViewState mStatusColorViewState = new ColorViewState( + SYSTEM_UI_FLAG_FULLSCREEN, FLAG_TRANSLUCENT_STATUS, + Gravity.TOP, + STATUS_BAR_BACKGROUND_TRANSITION_NAME, + com.android.internal.R.id.statusBarBackground, + FLAG_FULLSCREEN); + private final ColorViewState mNavigationColorViewState = new ColorViewState( + SYSTEM_UI_FLAG_HIDE_NAVIGATION, FLAG_TRANSLUCENT_NAVIGATION, + Gravity.BOTTOM, + NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME, + com.android.internal.R.id.navigationBarBackground, + 0 /* hideWindowFlag */); + + private final Interpolator mShowInterpolator; + private final Interpolator mHideInterpolator; + private final int mBarEnterExitDuration; + + private final BackgroundFallback mBackgroundFallback = new BackgroundFallback(); + + private int mLastTopInset = 0; + private int mLastBottomInset = 0; + private int mLastRightInset = 0; + private boolean mLastHasTopStableInset = false; + private boolean mLastHasBottomStableInset = false; + private int mLastWindowFlags = 0; + + private int mRootScrollY = 0; + + public DecorView(Context context, int featureId) { + super(context); + mFeatureId = featureId; + + mShowInterpolator = AnimationUtils.loadInterpolator(context, + android.R.interpolator.linear_out_slow_in); + mHideInterpolator = AnimationUtils.loadInterpolator(context, + android.R.interpolator.fast_out_linear_in); + + mBarEnterExitDuration = context.getResources().getInteger( + R.integer.dock_enter_exit_duration); + } + + public void setBackgroundFallback(int resId) { + mBackgroundFallback.setDrawable(resId != 0 ? getContext().getDrawable(resId) : null); + setWillNotDraw(getBackground() == null && !mBackgroundFallback.hasFallback()); + } + + @Override + public void onDraw(Canvas c) { + super.onDraw(c); + mBackgroundFallback.draw(mContentRoot, c, mContentParent); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + final int keyCode = event.getKeyCode(); + final int action = event.getAction(); + final boolean isDown = action == KeyEvent.ACTION_DOWN; + + if (isDown && (event.getRepeatCount() == 0)) { + // First handle chording of panel key: if a panel key is held + // but not released, try to execute a shortcut in it. + if ((mPanelChordingKey > 0) && (mPanelChordingKey != keyCode)) { + boolean handled = dispatchKeyShortcutEvent(event); + if (handled) { + return true; + } + } + + // If a panel is open, perform a shortcut on it without the + // chorded panel key + if ((mPreparedPanel != null) && mPreparedPanel.isOpen) { + if (performPanelShortcut(mPreparedPanel, keyCode, event, 0)) { + return true; + } + } + } + + if (!isDestroyed()) { + final Callback cb = getCallback(); + final boolean handled = cb != null && mFeatureId < 0 ? cb.dispatchKeyEvent(event) + : super.dispatchKeyEvent(event); + if (handled) { + return true; + } + } + + return isDown ? PhoneWindow.this.onKeyDown(mFeatureId, event.getKeyCode(), event) + : PhoneWindow.this.onKeyUp(mFeatureId, event.getKeyCode(), event); + } + + @Override + public boolean dispatchKeyShortcutEvent(KeyEvent ev) { + // If the panel is already prepared, then perform the shortcut using it. + boolean handled; + if (mPreparedPanel != null) { + handled = performPanelShortcut(mPreparedPanel, ev.getKeyCode(), ev, + Menu.FLAG_PERFORM_NO_CLOSE); + if (handled) { + if (mPreparedPanel != null) { + mPreparedPanel.isHandled = true; + } + return true; + } + } + + // Shortcut not handled by the panel. Dispatch to the view hierarchy. + final Callback cb = getCallback(); + handled = cb != null && !isDestroyed() && mFeatureId < 0 + ? cb.dispatchKeyShortcutEvent(ev) : super.dispatchKeyShortcutEvent(ev); + if (handled) { + return true; + } + + // If the panel is not prepared, then we may be trying to handle a shortcut key + // combination such as Control+C. Temporarily prepare the panel then mark it + // unprepared again when finished to ensure that the panel will again be prepared + // the next time it is shown for real. + PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false); + if (st != null && mPreparedPanel == null) { + preparePanel(st, ev); + handled = performPanelShortcut(st, ev.getKeyCode(), ev, + Menu.FLAG_PERFORM_NO_CLOSE); + st.isPrepared = false; + if (handled) { + return true; + } + } + return false; + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + final Callback cb = getCallback(); + return cb != null && !isDestroyed() && mFeatureId < 0 ? cb.dispatchTouchEvent(ev) + : super.dispatchTouchEvent(ev); + } + + @Override + public boolean dispatchTrackballEvent(MotionEvent ev) { + final Callback cb = getCallback(); + return cb != null && !isDestroyed() && mFeatureId < 0 ? cb.dispatchTrackballEvent(ev) + : super.dispatchTrackballEvent(ev); + } + + @Override + public boolean dispatchGenericMotionEvent(MotionEvent ev) { + final Callback cb = getCallback(); + return cb != null && !isDestroyed() && mFeatureId < 0 ? cb.dispatchGenericMotionEvent(ev) + : super.dispatchGenericMotionEvent(ev); + } + + public boolean superDispatchKeyEvent(KeyEvent event) { + // Give priority to closing action modes if applicable. + if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { + final int action = event.getAction(); + // Back cancels action modes first. + if (mPrimaryActionMode != null) { + if (action == KeyEvent.ACTION_UP) { + mPrimaryActionMode.finish(); + } + return true; + } + } + + return super.dispatchKeyEvent(event); + } + + public boolean superDispatchKeyShortcutEvent(KeyEvent event) { + return super.dispatchKeyShortcutEvent(event); + } + + public boolean superDispatchTouchEvent(MotionEvent event) { + return super.dispatchTouchEvent(event); + } + + public boolean superDispatchTrackballEvent(MotionEvent event) { + return super.dispatchTrackballEvent(event); + } + + public boolean superDispatchGenericMotionEvent(MotionEvent event) { + return super.dispatchGenericMotionEvent(event); + } + + @Override + public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) { + if (mOutsetBottom != null) { + final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics(); + int bottom = (int) mOutsetBottom.getDimension(metrics); + WindowInsets newInsets = insets.replaceSystemWindowInsets( + insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), + insets.getSystemWindowInsetRight(), bottom); + return super.dispatchApplyWindowInsets(newInsets); + } else { + return super.dispatchApplyWindowInsets(insets); + } + } + + + @Override + public boolean onTouchEvent(MotionEvent event) { + return onInterceptTouchEvent(event); + } + + private boolean isOutOfBounds(int x, int y) { + return x < -5 || y < -5 || x > (getWidth() + 5) + || y > (getHeight() + 5); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + int action = event.getAction(); + if (mFeatureId >= 0) { + if (action == MotionEvent.ACTION_DOWN) { + int x = (int)event.getX(); + int y = (int)event.getY(); + if (isOutOfBounds(x, y)) { + closePanel(mFeatureId); + return true; + } + } + } + + if (!SWEEP_OPEN_MENU) { + return false; + } + + if (mFeatureId >= 0) { + if (action == MotionEvent.ACTION_DOWN) { + Log.i(TAG, "Watchiing!"); + mWatchingForMenu = true; + mDownY = (int) event.getY(); + return false; + } + + if (!mWatchingForMenu) { + return false; + } + + int y = (int)event.getY(); + if (action == MotionEvent.ACTION_MOVE) { + if (y > (mDownY+30)) { + Log.i(TAG, "Closing!"); + closePanel(mFeatureId); + mWatchingForMenu = false; + return true; + } + } else if (action == MotionEvent.ACTION_UP) { + mWatchingForMenu = false; + } + + return false; + } + + //Log.i(TAG, "Intercept: action=" + action + " y=" + event.getY() + // + " (in " + getHeight() + ")"); + + if (action == MotionEvent.ACTION_DOWN) { + int y = (int)event.getY(); + if (y >= (getHeight()-5) && !hasChildren()) { + Log.i(TAG, "Watchiing!"); + mWatchingForMenu = true; + } + return false; + } + + if (!mWatchingForMenu) { + return false; + } + + int y = (int)event.getY(); + if (action == MotionEvent.ACTION_MOVE) { + if (y < (getHeight()-30)) { + Log.i(TAG, "Opening!"); + openPanel(FEATURE_OPTIONS_PANEL, new KeyEvent( + KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MENU)); + mWatchingForMenu = false; + return true; + } + } else if (action == MotionEvent.ACTION_UP) { + mWatchingForMenu = false; + } + + return false; + } + + @Override + public void sendAccessibilityEvent(int eventType) { + if (!AccessibilityManager.getInstance(mContext).isEnabled()) { + return; + } + + // if we are showing a feature that should be announced and one child + // make this child the event source since this is the feature itself + // otherwise the callback will take over and announce its client + if ((mFeatureId == FEATURE_OPTIONS_PANEL || + mFeatureId == FEATURE_CONTEXT_MENU || + mFeatureId == FEATURE_PROGRESS || + mFeatureId == FEATURE_INDETERMINATE_PROGRESS) + && getChildCount() == 1) { + getChildAt(0).sendAccessibilityEvent(eventType); + } else { + super.sendAccessibilityEvent(eventType); + } + } + + @Override + public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) { + final Callback cb = getCallback(); + if (cb != null && !isDestroyed()) { + if (cb.dispatchPopulateAccessibilityEvent(event)) { + return true; + } + } + return super.dispatchPopulateAccessibilityEventInternal(event); + } + + @Override + protected boolean setFrame(int l, int t, int r, int b) { + boolean changed = super.setFrame(l, t, r, b); + if (changed) { + final Rect drawingBounds = mDrawingBounds; + getDrawingRect(drawingBounds); + + Drawable fg = getForeground(); + if (fg != null) { + final Rect frameOffsets = mFrameOffsets; + drawingBounds.left += frameOffsets.left; + drawingBounds.top += frameOffsets.top; + drawingBounds.right -= frameOffsets.right; + drawingBounds.bottom -= frameOffsets.bottom; + fg.setBounds(drawingBounds); + final Rect framePadding = mFramePadding; + drawingBounds.left += framePadding.left - frameOffsets.left; + drawingBounds.top += framePadding.top - frameOffsets.top; + drawingBounds.right -= framePadding.right - frameOffsets.right; + drawingBounds.bottom -= framePadding.bottom - frameOffsets.bottom; + } + + Drawable bg = getBackground(); + if (bg != null) { + bg.setBounds(drawingBounds); + } + + if (SWEEP_OPEN_MENU) { + if (mMenuBackground == null && mFeatureId < 0 + && getAttributes().height + == WindowManager.LayoutParams.MATCH_PARENT) { + mMenuBackground = getContext().getDrawable( + R.drawable.menu_background); + } + if (mMenuBackground != null) { + mMenuBackground.setBounds(drawingBounds.left, + drawingBounds.bottom-6, drawingBounds.right, + drawingBounds.bottom+20); + } + } + } + return changed; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics(); + final boolean isPortrait = metrics.widthPixels < metrics.heightPixels; + + final int widthMode = getMode(widthMeasureSpec); + final int heightMode = getMode(heightMeasureSpec); + + boolean fixedWidth = false; + if (widthMode == AT_MOST) { + final TypedValue tvw = isPortrait ? mFixedWidthMinor : mFixedWidthMajor; + if (tvw != null && tvw.type != TypedValue.TYPE_NULL) { + final int w; + if (tvw.type == TypedValue.TYPE_DIMENSION) { + w = (int) tvw.getDimension(metrics); + } else if (tvw.type == TypedValue.TYPE_FRACTION) { + w = (int) tvw.getFraction(metrics.widthPixels, metrics.widthPixels); + } else { + w = 0; + } + + if (w > 0) { + final int widthSize = MeasureSpec.getSize(widthMeasureSpec); + widthMeasureSpec = MeasureSpec.makeMeasureSpec( + Math.min(w, widthSize), EXACTLY); + fixedWidth = true; + } + } + } + + if (heightMode == AT_MOST) { + final TypedValue tvh = isPortrait ? mFixedHeightMajor : mFixedHeightMinor; + if (tvh != null && tvh.type != TypedValue.TYPE_NULL) { + final int h; + if (tvh.type == TypedValue.TYPE_DIMENSION) { + h = (int) tvh.getDimension(metrics); + } else if (tvh.type == TypedValue.TYPE_FRACTION) { + h = (int) tvh.getFraction(metrics.heightPixels, metrics.heightPixels); + } else { + h = 0; + } + if (h > 0) { + final int heightSize = MeasureSpec.getSize(heightMeasureSpec); + heightMeasureSpec = MeasureSpec.makeMeasureSpec( + Math.min(h, heightSize), EXACTLY); + } + } + } + + if (mOutsetBottom != null) { + int mode = MeasureSpec.getMode(heightMeasureSpec); + if (mode != MeasureSpec.UNSPECIFIED) { + int outset = (int) mOutsetBottom.getDimension(metrics); + int height = MeasureSpec.getSize(heightMeasureSpec); + heightMeasureSpec = MeasureSpec.makeMeasureSpec(height + outset, mode); + } + } + + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + int width = getMeasuredWidth(); + boolean measure = false; + + widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, EXACTLY); + + if (!fixedWidth && widthMode == AT_MOST) { + final TypedValue tv = isPortrait ? mMinWidthMinor : mMinWidthMajor; + if (tv.type != TypedValue.TYPE_NULL) { + final int min; + if (tv.type == TypedValue.TYPE_DIMENSION) { + min = (int)tv.getDimension(metrics); + } else if (tv.type == TypedValue.TYPE_FRACTION) { + min = (int)tv.getFraction(metrics.widthPixels, metrics.widthPixels); + } else { + min = 0; + } + + if (width < min) { + widthMeasureSpec = MeasureSpec.makeMeasureSpec(min, EXACTLY); + measure = true; + } + } + } + + // TODO: Support height? + + if (measure) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + + if (mMenuBackground != null) { + mMenuBackground.draw(canvas); + } + } + + + @Override + public boolean showContextMenuForChild(View originalView) { + // Reuse the context menu builder + if (mContextMenu == null) { + mContextMenu = new ContextMenuBuilder(getContext()); + mContextMenu.setCallback(mContextMenuCallback); + } else { + mContextMenu.clearAll(); + } + + final MenuDialogHelper helper = mContextMenu.show(originalView, + originalView.getWindowToken()); + if (helper != null) { + helper.setPresenterCallback(mContextMenuCallback); + } else if (mContextMenuHelper != null) { + // No menu to show, but if we have a menu currently showing it just became blank. + // Close it. + mContextMenuHelper.dismiss(); + } + mContextMenuHelper = helper; + return helper != null; + } + + @Override + public ActionMode startActionModeForChild(View originalView, + ActionMode.Callback callback) { + return startActionModeForChild(originalView, callback, ActionMode.TYPE_PRIMARY); + } + + @Override + public ActionMode startActionModeForChild( + View child, ActionMode.Callback callback, int type) { + return startActionMode(child, callback, type); + } + + @Override + public ActionMode startActionMode(ActionMode.Callback callback) { + return startActionMode(callback, ActionMode.TYPE_PRIMARY); + } + + @Override + public ActionMode startActionMode(ActionMode.Callback callback, int type) { + return startActionMode(this, callback, type); + } + + private ActionMode startActionMode( + View originatingView, ActionMode.Callback callback, int type) { + ActionMode.Callback2 wrappedCallback = new ActionModeCallback2Wrapper(callback); + ActionMode mode = null; + if (getCallback() != null && !isDestroyed()) { + try { + mode = getCallback().onWindowStartingActionMode(wrappedCallback, type); + } catch (AbstractMethodError ame) { + // Older apps might not implement this callback method. + } + } + if (mode != null) { + if (mode.getType() == ActionMode.TYPE_PRIMARY) { + cleanupPrimaryActionMode(); + mPrimaryActionMode = mode; + } else { + mFloatingActionMode = mode; + } + } else { + if (type == ActionMode.TYPE_PRIMARY) { + cleanupPrimaryActionMode(); + mode = createStandaloneActionMode(wrappedCallback); + if (mode != null && callback.onCreateActionMode(mode, mode.getMenu())) { + setHandledPrimaryActionMode(mode); + } else { + mode = null; + } + } + } + if (mode != null && getCallback() != null && !isDestroyed()) { + try { + getCallback().onActionModeStarted(mode); + } catch (AbstractMethodError ame) { + // Older apps might not implement this callback method. + } + } + return mode; + } + + private void cleanupPrimaryActionMode() { + if (mPrimaryActionMode != null) { + mPrimaryActionMode.finish(); + mPrimaryActionMode = null; + } + if (mPrimaryActionModeView != null) { + mPrimaryActionModeView.killMode(); + } + } + + public void startChanging() { + mChanging = true; + } + + public void finishChanging() { + mChanging = false; + drawableChanged(); + } + + public void setWindowBackground(Drawable drawable) { + if (getBackground() != drawable) { + setBackgroundDrawable(drawable); + if (drawable != null) { + drawable.getPadding(mBackgroundPadding); + } else { + mBackgroundPadding.setEmpty(); + } + drawableChanged(); + } + } + + @Override + public void setBackgroundDrawable(Drawable d) { + super.setBackgroundDrawable(d); + if (getWindowToken() != null) { + updateWindowResizeState(); + } + } + + public void setWindowFrame(Drawable drawable) { + if (getForeground() != drawable) { + setForeground(drawable); + if (drawable != null) { + drawable.getPadding(mFramePadding); + } else { + mFramePadding.setEmpty(); + } + drawableChanged(); + } + } + + @Override + public void onWindowSystemUiVisibilityChanged(int visible) { + updateColorViews(null /* insets */, true /* animate */); + } + + @Override + public WindowInsets onApplyWindowInsets(WindowInsets insets) { + mFrameOffsets.set(insets.getSystemWindowInsets()); + insets = updateColorViews(insets, true /* animate */); + insets = updateStatusGuard(insets); + updateNavigationGuard(insets); + if (getForeground() != null) { + drawableChanged(); + } + return insets; + } + + @Override + public boolean isTransitionGroup() { + return false; + } + + private WindowInsets updateColorViews(WindowInsets insets, boolean animate) { + WindowManager.LayoutParams attrs = getAttributes(); + int sysUiVisibility = attrs.systemUiVisibility | getWindowSystemUiVisibility(); + + if (!mIsFloating && ActivityManager.isHighEndGfx()) { + boolean disallowAnimate = !isLaidOut(); + disallowAnimate |= ((mLastWindowFlags ^ attrs.flags) + & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0; + mLastWindowFlags = attrs.flags; + + if (insets != null) { + mLastTopInset = Math.min(insets.getStableInsetTop(), + insets.getSystemWindowInsetTop()); + mLastBottomInset = Math.min(insets.getStableInsetBottom(), + insets.getSystemWindowInsetBottom()); + mLastRightInset = Math.min(insets.getStableInsetRight(), + insets.getSystemWindowInsetRight()); + + // Don't animate if the presence of stable insets has changed, because that + // indicates that the window was either just added and received them for the + // first time, or the window size or position has changed. + boolean hasTopStableInset = insets.getStableInsetTop() != 0; + disallowAnimate |= (hasTopStableInset != mLastHasTopStableInset); + mLastHasTopStableInset = hasTopStableInset; + + boolean hasBottomStableInset = insets.getStableInsetBottom() != 0; + disallowAnimate |= (hasBottomStableInset != mLastHasBottomStableInset); + mLastHasBottomStableInset = hasBottomStableInset; + } + + updateColorViewInt(mStatusColorViewState, sysUiVisibility, mStatusBarColor, + mLastTopInset, animate && !disallowAnimate); + updateColorViewInt(mNavigationColorViewState, sysUiVisibility, mNavigationBarColor, + mLastBottomInset, animate && !disallowAnimate); + } + + // When we expand the window with FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, we still need + // to ensure that the rest of the view hierarchy doesn't notice it, unless they've + // explicitly asked for it. + + boolean consumingNavBar = + (attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0 + && (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0 + && (sysUiVisibility & SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0; + + int consumedRight = consumingNavBar ? mLastRightInset : 0; + int consumedBottom = consumingNavBar ? mLastBottomInset : 0; + + if (mContentRoot != null + && mContentRoot.getLayoutParams() instanceof MarginLayoutParams) { + MarginLayoutParams lp = (MarginLayoutParams) mContentRoot.getLayoutParams(); + if (lp.rightMargin != consumedRight || lp.bottomMargin != consumedBottom) { + lp.rightMargin = consumedRight; + lp.bottomMargin = consumedBottom; + mContentRoot.setLayoutParams(lp); + + if (insets == null) { + // The insets have changed, but we're not currently in the process + // of dispatching them. + requestApplyInsets(); + } + } + if (insets != null) { + insets = insets.replaceSystemWindowInsets( + insets.getSystemWindowInsetLeft(), + insets.getSystemWindowInsetTop(), + insets.getSystemWindowInsetRight() - consumedRight, + insets.getSystemWindowInsetBottom() - consumedBottom); + } + } + + if (insets != null) { + insets = insets.consumeStableInsets(); + } + return insets; + } + + private void updateColorViewInt(final ColorViewState state, int sysUiVis, int color, + int height, boolean animate) { + boolean show = height > 0 && (sysUiVis & state.systemUiHideFlag) == 0 + && (getAttributes().flags & state.hideWindowFlag) == 0 + && (getAttributes().flags & state.translucentFlag) == 0 + && (color & Color.BLACK) != 0 + && (getAttributes().flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0; + + boolean visibilityChanged = false; + View view = state.view; + + if (view == null) { + if (show) { + state.view = view = new View(mContext); + view.setBackgroundColor(color); + view.setTransitionName(state.transitionName); + view.setId(state.id); + visibilityChanged = true; + view.setVisibility(INVISIBLE); + state.targetVisibility = VISIBLE; + + addView(view, new LayoutParams(LayoutParams.MATCH_PARENT, height, + Gravity.START | state.verticalGravity)); + updateColorViewTranslations(); + } + } else { + int vis = show ? VISIBLE : INVISIBLE; + visibilityChanged = state.targetVisibility != vis; + state.targetVisibility = vis; + if (show) { + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + if (lp.height != height) { + lp.height = height; + view.setLayoutParams(lp); + } + view.setBackgroundColor(color); + } + } + if (visibilityChanged) { + view.animate().cancel(); + if (animate) { + if (show) { + if (view.getVisibility() != VISIBLE) { + view.setVisibility(VISIBLE); + view.setAlpha(0.0f); + } + view.animate().alpha(1.0f).setInterpolator(mShowInterpolator). + setDuration(mBarEnterExitDuration); + } else { + view.animate().alpha(0.0f).setInterpolator(mHideInterpolator) + .setDuration(mBarEnterExitDuration) + .withEndAction(new Runnable() { + @Override + public void run() { + state.view.setAlpha(1.0f); + state.view.setVisibility(INVISIBLE); + } + }); + } + } else { + view.setAlpha(1.0f); + view.setVisibility(show ? VISIBLE : INVISIBLE); + } + } + } + + private void updateColorViewTranslations() { + // Put the color views back in place when they get moved off the screen + // due to the the ViewRootImpl panning. + int rootScrollY = mRootScrollY; + if (mStatusColorViewState.view != null) { + mStatusColorViewState.view.setTranslationY(rootScrollY > 0 ? rootScrollY : 0); + } + if (mNavigationColorViewState.view != null) { + mNavigationColorViewState.view.setTranslationY(rootScrollY < 0 ? rootScrollY : 0); + } + } + + private WindowInsets updateStatusGuard(WindowInsets insets) { + boolean showStatusGuard = false; + // Show the status guard when the non-overlay contextual action bar is showing + if (mPrimaryActionModeView != null) { + if (mPrimaryActionModeView.getLayoutParams() instanceof MarginLayoutParams) { + // Insets are magic! + final MarginLayoutParams mlp = (MarginLayoutParams) + mPrimaryActionModeView.getLayoutParams(); + boolean mlpChanged = false; + if (mPrimaryActionModeView.isShown()) { + if (mTempRect == null) { + mTempRect = new Rect(); + } + final Rect rect = mTempRect; + + // If the parent doesn't consume the insets, manually + // apply the default system window insets. + mContentParent.computeSystemWindowInsets(insets, rect); + final int newMargin = rect.top == 0 ? insets.getSystemWindowInsetTop() : 0; + if (mlp.topMargin != newMargin) { + mlpChanged = true; + mlp.topMargin = insets.getSystemWindowInsetTop(); + + if (mStatusGuard == null) { + mStatusGuard = new View(mContext); + mStatusGuard.setBackgroundColor(mContext.getColor( + R.color.input_method_navigation_guard)); + addView(mStatusGuard, indexOfChild(mStatusColorViewState.view), + new LayoutParams(LayoutParams.MATCH_PARENT, + mlp.topMargin, Gravity.START | Gravity.TOP)); + } else { + final LayoutParams lp = (LayoutParams) + mStatusGuard.getLayoutParams(); + if (lp.height != mlp.topMargin) { + lp.height = mlp.topMargin; + mStatusGuard.setLayoutParams(lp); + } + } + } + + // The action mode's theme may differ from the app, so + // always show the status guard above it if we have one. + showStatusGuard = mStatusGuard != null; + + // We only need to consume the insets if the action + // mode is overlaid on the app content (e.g. it's + // sitting in a FrameLayout, see + // screen_simple_overlay_action_mode.xml). + final boolean nonOverlay = (getLocalFeatures() + & (1 << FEATURE_ACTION_MODE_OVERLAY)) == 0; + insets = insets.consumeSystemWindowInsets( + false, nonOverlay && showStatusGuard /* top */, false, false); + } else { + // reset top margin + if (mlp.topMargin != 0) { + mlpChanged = true; + mlp.topMargin = 0; + } + } + if (mlpChanged) { + mPrimaryActionModeView.setLayoutParams(mlp); + } + } + } + if (mStatusGuard != null) { + mStatusGuard.setVisibility(showStatusGuard ? View.VISIBLE : View.GONE); + } + return insets; + } + + private void updateNavigationGuard(WindowInsets insets) { + // IMEs lay out below the nav bar, but the content view must not (for back compat) + if (getAttributes().type == WindowManager.LayoutParams.TYPE_INPUT_METHOD) { + // prevent the content view from including the nav bar height + if (mContentParent != null) { + if (mContentParent.getLayoutParams() instanceof MarginLayoutParams) { + MarginLayoutParams mlp = + (MarginLayoutParams) mContentParent.getLayoutParams(); + mlp.bottomMargin = insets.getSystemWindowInsetBottom(); + mContentParent.setLayoutParams(mlp); + } + } + // position the navigation guard view, creating it if necessary + if (mNavigationGuard == null) { + mNavigationGuard = new View(mContext); + mNavigationGuard.setBackgroundColor(mContext.getColor( + R.color.input_method_navigation_guard)); + addView(mNavigationGuard, indexOfChild(mNavigationColorViewState.view), + new LayoutParams(LayoutParams.MATCH_PARENT, + insets.getSystemWindowInsetBottom(), + Gravity.START | Gravity.BOTTOM)); + } else { + LayoutParams lp = (LayoutParams) mNavigationGuard.getLayoutParams(); + lp.height = insets.getSystemWindowInsetBottom(); + mNavigationGuard.setLayoutParams(lp); + } + } + } + + private void drawableChanged() { + if (mChanging) { + return; + } + + setPadding(mFramePadding.left + mBackgroundPadding.left, mFramePadding.top + + mBackgroundPadding.top, mFramePadding.right + mBackgroundPadding.right, + mFramePadding.bottom + mBackgroundPadding.bottom); + requestLayout(); + invalidate(); + + int opacity = PixelFormat.OPAQUE; + // Note: if there is no background, we will assume opaque. The + // common case seems to be that an application sets there to be + // no background so it can draw everything itself. For that, + // we would like to assume OPAQUE and let the app force it to + // the slower TRANSLUCENT mode if that is really what it wants. + Drawable bg = getBackground(); + Drawable fg = getForeground(); + if (bg != null) { + if (fg == null) { + opacity = bg.getOpacity(); + } else if (mFramePadding.left <= 0 && mFramePadding.top <= 0 + && mFramePadding.right <= 0 && mFramePadding.bottom <= 0) { + // If the frame padding is zero, then we can be opaque + // if either the frame -or- the background is opaque. + int fop = fg.getOpacity(); + int bop = bg.getOpacity(); + if (false) + Log.v(TAG, "Background opacity: " + bop + ", Frame opacity: " + fop); + if (fop == PixelFormat.OPAQUE || bop == PixelFormat.OPAQUE) { + opacity = PixelFormat.OPAQUE; + } else if (fop == PixelFormat.UNKNOWN) { + opacity = bop; + } else if (bop == PixelFormat.UNKNOWN) { + opacity = fop; + } else { + opacity = Drawable.resolveOpacity(fop, bop); + } + } else { + // For now we have to assume translucent if there is a + // frame with padding... there is no way to tell if the + // frame and background together will draw all pixels. + if (false) + Log.v(TAG, "Padding: " + mFramePadding); + opacity = PixelFormat.TRANSLUCENT; + } + } + + if (false) + Log.v(TAG, "Background: " + bg + ", Frame: " + fg); + if (false) + Log.v(TAG, "Selected default opacity: " + opacity); + + mDefaultOpacity = opacity; + if (mFeatureId < 0) { + setDefaultWindowFormat(opacity); + } + } + + @Override + public void onWindowFocusChanged(boolean hasWindowFocus) { + super.onWindowFocusChanged(hasWindowFocus); + + // If the user is chording a menu shortcut, release the chord since + // this window lost focus + if (hasFeature(FEATURE_OPTIONS_PANEL) && !hasWindowFocus && mPanelChordingKey != 0) { + closePanel(FEATURE_OPTIONS_PANEL); + } + + final Callback cb = getCallback(); + if (cb != null && !isDestroyed() && mFeatureId < 0) { + cb.onWindowFocusChanged(hasWindowFocus); + } + } + + void updateWindowResizeState() { + Drawable bg = getBackground(); + hackTurnOffWindowResizeAnim(bg == null || bg.getOpacity() + != PixelFormat.OPAQUE); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + updateWindowResizeState(); + + final Callback cb = getCallback(); + if (cb != null && !isDestroyed() && mFeatureId < 0) { + cb.onAttachedToWindow(); + } + + if (mFeatureId == -1) { + /* + * The main window has been attached, try to restore any panels + * that may have been open before. This is called in cases where + * an activity is being killed for configuration change and the + * menu was open. When the activity is recreated, the menu + * should be shown again. + */ + openPanelsAfterRestore(); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + final Callback cb = getCallback(); + if (cb != null && mFeatureId < 0) { + cb.onDetachedFromWindow(); + } + + if (mDecorContentParent != null) { + mDecorContentParent.dismissPopups(); + } + + if (mPrimaryActionModePopup != null) { + removeCallbacks(mShowPrimaryActionModePopup); + if (mPrimaryActionModePopup.isShowing()) { + mPrimaryActionModePopup.dismiss(); + } + mPrimaryActionModePopup = null; + } + + PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false); + if (st != null && st.menu != null && mFeatureId < 0) { + st.menu.close(); + } + } + + @Override + public void onCloseSystemDialogs(String reason) { + if (mFeatureId >= 0) { + closeAllPanels(); + } + } + + public android.view.SurfaceHolder.Callback2 willYouTakeTheSurface() { + return mFeatureId < 0 ? mTakeSurfaceCallback : null; + } + + public InputQueue.Callback willYouTakeTheInputQueue() { + return mFeatureId < 0 ? mTakeInputQueueCallback : null; + } + + public void setSurfaceType(int type) { + PhoneWindow.this.setType(type); + } + + public void setSurfaceFormat(int format) { + PhoneWindow.this.setFormat(format); + } + + public void setSurfaceKeepScreenOn(boolean keepOn) { + if (keepOn) PhoneWindow.this.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + else PhoneWindow.this.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + @Override + public void onRootViewScrollYChanged(int rootScrollY) { + mRootScrollY = rootScrollY; + updateColorViewTranslations(); + } + + private ActionMode createStandaloneActionMode(ActionMode.Callback callback) { + if (mPrimaryActionModeView == null) { + if (isFloating()) { + // Use the action bar theme. + final TypedValue outValue = new TypedValue(); + final Theme baseTheme = mContext.getTheme(); + baseTheme.resolveAttribute(R.attr.actionBarTheme, outValue, true); + + final Context actionBarContext; + if (outValue.resourceId != 0) { + final Theme actionBarTheme = mContext.getResources().newTheme(); + actionBarTheme.setTo(baseTheme); + actionBarTheme.applyStyle(outValue.resourceId, true); + + actionBarContext = new ContextThemeWrapper(mContext, 0); + actionBarContext.getTheme().setTo(actionBarTheme); + } else { + actionBarContext = mContext; + } + + mPrimaryActionModeView = new ActionBarContextView(actionBarContext); + mPrimaryActionModePopup = new PopupWindow(actionBarContext, null, + R.attr.actionModePopupWindowStyle); + mPrimaryActionModePopup.setWindowLayoutType( + WindowManager.LayoutParams.TYPE_APPLICATION); + mPrimaryActionModePopup.setContentView(mPrimaryActionModeView); + mPrimaryActionModePopup.setWidth(MATCH_PARENT); + + actionBarContext.getTheme().resolveAttribute( + R.attr.actionBarSize, outValue, true); + final int height = TypedValue.complexToDimensionPixelSize(outValue.data, + actionBarContext.getResources().getDisplayMetrics()); + mPrimaryActionModeView.setContentHeight(height); + mPrimaryActionModePopup.setHeight(WRAP_CONTENT); + mShowPrimaryActionModePopup = new Runnable() { + public void run() { + mPrimaryActionModePopup.showAtLocation( + mPrimaryActionModeView.getApplicationWindowToken(), + Gravity.TOP | Gravity.FILL_HORIZONTAL, 0, 0); + } + }; + } else { + ViewStub stub = (ViewStub) findViewById( + R.id.action_mode_bar_stub); + if (stub != null) { + mPrimaryActionModeView = (ActionBarContextView) stub.inflate(); + } + } + } + if (mPrimaryActionModeView != null) { + mPrimaryActionModeView.killMode(); + ActionMode mode = new StandaloneActionMode( + mPrimaryActionModeView.getContext(), mPrimaryActionModeView, + callback, mPrimaryActionModePopup == null); + return mode; + } + return null; + } + + private void setHandledPrimaryActionMode(ActionMode mode) { + mPrimaryActionMode = mode; + mPrimaryActionMode.invalidate(); + mPrimaryActionModeView.initForMode(mPrimaryActionMode); + mPrimaryActionModeView.setVisibility(View.VISIBLE); + if (mPrimaryActionModePopup != null) { + post(mShowPrimaryActionModePopup); + } + mPrimaryActionModeView.sendAccessibilityEvent( + AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } + + /** + * Clears out internal references when the action mode is destroyed. + */ + private class ActionModeCallback2Wrapper extends ActionMode.Callback2 { + private final ActionMode.Callback mWrapped; + + public ActionModeCallback2Wrapper(ActionMode.Callback wrapped) { + mWrapped = wrapped; + } + + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + return mWrapped.onCreateActionMode(mode, menu); + } + + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + requestFitSystemWindows(); + return mWrapped.onPrepareActionMode(mode, menu); + } + + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return mWrapped.onActionItemClicked(mode, item); + } + + public void onDestroyActionMode(ActionMode mode) { + mWrapped.onDestroyActionMode(mode); + if (mode == mPrimaryActionMode) { + if (mPrimaryActionModePopup != null) { + removeCallbacks(mShowPrimaryActionModePopup); + mPrimaryActionModePopup.dismiss(); + } else if (mPrimaryActionModeView != null) { + mPrimaryActionModeView.setVisibility(GONE); + } + if (mPrimaryActionModeView != null) { + mPrimaryActionModeView.removeAllViews(); + } + mPrimaryActionMode = null; + } else if (mode == mFloatingActionMode) { + mFloatingActionMode = null; + } + if (getCallback() != null && !isDestroyed()) { + try { + getCallback().onActionModeFinished(mode); + } catch (AbstractMethodError ame) { + // Older apps might not implement this callback method. + } + } + requestFitSystemWindows(); + } + } + } + + protected DecorView generateDecor() { + return new DecorView(getContext(), -1); + } + + protected void setFeatureFromAttrs(int featureId, TypedArray attrs, + int drawableAttr, int alphaAttr) { + Drawable d = attrs.getDrawable(drawableAttr); + if (d != null) { + requestFeature(featureId); + setFeatureDefaultDrawable(featureId, d); + } + if ((getFeatures() & (1 << featureId)) != 0) { + int alpha = attrs.getInt(alphaAttr, -1); + if (alpha >= 0) { + setFeatureDrawableAlpha(featureId, alpha); + } + } + } + + protected ViewGroup generateLayout(DecorView decor) { + // Apply data from current theme. + + TypedArray a = getWindowStyle(); + + if (false) { + System.out.println("From style:"); + String s = "Attrs:"; + for (int i = 0; i < R.styleable.Window.length; i++) { + s = s + " " + Integer.toHexString(R.styleable.Window[i]) + "=" + + a.getString(i); + } + System.out.println(s); + } + + mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false); + int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR) + & (~getForcedWindowFlags()); + if (mIsFloating) { + setLayout(WRAP_CONTENT, WRAP_CONTENT); + setFlags(0, flagsToUpdate); + } else { + setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate); + } + + if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) { + requestFeature(FEATURE_NO_TITLE); + } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) { + // Don't allow an action bar if there is no title. + requestFeature(FEATURE_ACTION_BAR); + } + + if (a.getBoolean(R.styleable.Window_windowActionBarOverlay, false)) { + requestFeature(FEATURE_ACTION_BAR_OVERLAY); + } + + if (a.getBoolean(R.styleable.Window_windowActionModeOverlay, false)) { + requestFeature(FEATURE_ACTION_MODE_OVERLAY); + } + + if (a.getBoolean(R.styleable.Window_windowSwipeToDismiss, false)) { + requestFeature(FEATURE_SWIPE_TO_DISMISS); + } + + if (a.getBoolean(R.styleable.Window_windowFullscreen, false)) { + setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN & (~getForcedWindowFlags())); + } + + if (a.getBoolean(R.styleable.Window_windowTranslucentStatus, + false)) { + setFlags(FLAG_TRANSLUCENT_STATUS, FLAG_TRANSLUCENT_STATUS + & (~getForcedWindowFlags())); + } + + if (a.getBoolean(R.styleable.Window_windowTranslucentNavigation, + false)) { + setFlags(FLAG_TRANSLUCENT_NAVIGATION, FLAG_TRANSLUCENT_NAVIGATION + & (~getForcedWindowFlags())); + } + + if (a.getBoolean(R.styleable.Window_windowOverscan, false)) { + setFlags(FLAG_LAYOUT_IN_OVERSCAN, FLAG_LAYOUT_IN_OVERSCAN&(~getForcedWindowFlags())); + } + + if (a.getBoolean(R.styleable.Window_windowShowWallpaper, false)) { + setFlags(FLAG_SHOW_WALLPAPER, FLAG_SHOW_WALLPAPER&(~getForcedWindowFlags())); + } + + if (a.getBoolean(R.styleable.Window_windowEnableSplitTouch, + getContext().getApplicationInfo().targetSdkVersion + >= android.os.Build.VERSION_CODES.HONEYCOMB)) { + setFlags(FLAG_SPLIT_TOUCH, FLAG_SPLIT_TOUCH&(~getForcedWindowFlags())); + } + + a.getValue(R.styleable.Window_windowMinWidthMajor, mMinWidthMajor); + a.getValue(R.styleable.Window_windowMinWidthMinor, mMinWidthMinor); + if (a.hasValue(R.styleable.Window_windowFixedWidthMajor)) { + if (mFixedWidthMajor == null) mFixedWidthMajor = new TypedValue(); + a.getValue(R.styleable.Window_windowFixedWidthMajor, + mFixedWidthMajor); + } + if (a.hasValue(R.styleable.Window_windowFixedWidthMinor)) { + if (mFixedWidthMinor == null) mFixedWidthMinor = new TypedValue(); + a.getValue(R.styleable.Window_windowFixedWidthMinor, + mFixedWidthMinor); + } + if (a.hasValue(R.styleable.Window_windowFixedHeightMajor)) { + if (mFixedHeightMajor == null) mFixedHeightMajor = new TypedValue(); + a.getValue(R.styleable.Window_windowFixedHeightMajor, + mFixedHeightMajor); + } + if (a.hasValue(R.styleable.Window_windowFixedHeightMinor)) { + if (mFixedHeightMinor == null) mFixedHeightMinor = new TypedValue(); + a.getValue(R.styleable.Window_windowFixedHeightMinor, + mFixedHeightMinor); + } + if (a.getBoolean(R.styleable.Window_windowContentTransitions, false)) { + requestFeature(FEATURE_CONTENT_TRANSITIONS); + } + if (a.getBoolean(R.styleable.Window_windowActivityTransitions, false)) { + requestFeature(FEATURE_ACTIVITY_TRANSITIONS); + } + + final WindowManager windowService = (WindowManager) getContext().getSystemService( + Context.WINDOW_SERVICE); + if (windowService != null) { + final Display display = windowService.getDefaultDisplay(); + final boolean shouldUseBottomOutset = + display.getDisplayId() == Display.DEFAULT_DISPLAY + || (getForcedWindowFlags() & FLAG_FULLSCREEN) != 0; + if (shouldUseBottomOutset && a.hasValue(R.styleable.Window_windowOutsetBottom)) { + if (mOutsetBottom == null) mOutsetBottom = new TypedValue(); + a.getValue(R.styleable.Window_windowOutsetBottom, + mOutsetBottom); + } + } + + final Context context = getContext(); + final int targetSdk = context.getApplicationInfo().targetSdkVersion; + final boolean targetPreHoneycomb = targetSdk < android.os.Build.VERSION_CODES.HONEYCOMB; + final boolean targetPreIcs = targetSdk < android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH; + final boolean targetPreL = targetSdk < android.os.Build.VERSION_CODES.LOLLIPOP; + final boolean targetHcNeedsOptions = context.getResources().getBoolean( + R.bool.target_honeycomb_needs_options_menu); + final boolean noActionBar = !hasFeature(FEATURE_ACTION_BAR) || hasFeature(FEATURE_NO_TITLE); + + if (targetPreHoneycomb || (targetPreIcs && targetHcNeedsOptions && noActionBar)) { + setNeedsMenuKey(WindowManager.LayoutParams.NEEDS_MENU_SET_TRUE); + } else { + setNeedsMenuKey(WindowManager.LayoutParams.NEEDS_MENU_SET_FALSE); + } + + // Non-floating windows on high end devices must put up decor beneath the system bars and + // therefore must know about visibility changes of those. + if (!mIsFloating && ActivityManager.isHighEndGfx()) { + if (!targetPreL && a.getBoolean( + R.styleable.Window_windowDrawsSystemBarBackgrounds, + false)) { + setFlags(FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, + FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS & ~getForcedWindowFlags()); + } + } + if (!mForcedStatusBarColor) { + mStatusBarColor = a.getColor(R.styleable.Window_statusBarColor, 0xFF000000); + } + if (!mForcedNavigationBarColor) { + mNavigationBarColor = a.getColor(R.styleable.Window_navigationBarColor, 0xFF000000); + } + if (a.getBoolean(R.styleable.Window_windowHasLightStatusBar, false)) { + decor.setSystemUiVisibility( + decor.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); + } + + if (mAlwaysReadCloseOnTouchAttr || getContext().getApplicationInfo().targetSdkVersion + >= android.os.Build.VERSION_CODES.HONEYCOMB) { + if (a.getBoolean( + R.styleable.Window_windowCloseOnTouchOutside, + false)) { + setCloseOnTouchOutsideIfNotSet(true); + } + } + + WindowManager.LayoutParams params = getAttributes(); + + if (!hasSoftInputMode()) { + params.softInputMode = a.getInt( + R.styleable.Window_windowSoftInputMode, + params.softInputMode); + } + + if (a.getBoolean(R.styleable.Window_backgroundDimEnabled, + mIsFloating)) { + /* All dialogs should have the window dimmed */ + if ((getForcedWindowFlags()&WindowManager.LayoutParams.FLAG_DIM_BEHIND) == 0) { + params.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND; + } + if (!haveDimAmount()) { + params.dimAmount = a.getFloat( + android.R.styleable.Window_backgroundDimAmount, 0.5f); + } + } + + if (params.windowAnimations == 0) { + params.windowAnimations = a.getResourceId( + R.styleable.Window_windowAnimationStyle, 0); + } + + // The rest are only done if this window is not embedded; otherwise, + // the values are inherited from our container. + if (getContainer() == null) { + if (mBackgroundDrawable == null) { + if (mBackgroundResource == 0) { + mBackgroundResource = a.getResourceId( + R.styleable.Window_windowBackground, 0); + } + if (mFrameResource == 0) { + mFrameResource = a.getResourceId(R.styleable.Window_windowFrame, 0); + } + mBackgroundFallbackResource = a.getResourceId( + R.styleable.Window_windowBackgroundFallback, 0); + if (false) { + System.out.println("Background: " + + Integer.toHexString(mBackgroundResource) + " Frame: " + + Integer.toHexString(mFrameResource)); + } + } + mElevation = a.getDimension(R.styleable.Window_windowElevation, 0); + mClipToOutline = a.getBoolean(R.styleable.Window_windowClipToOutline, false); + mTextColor = a.getColor(R.styleable.Window_textColor, Color.TRANSPARENT); + } + + // Inflate the window decor. + + int layoutResource; + int features = getLocalFeatures(); + // System.out.println("Features: 0x" + Integer.toHexString(features)); + if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) { + layoutResource = R.layout.screen_swipe_dismiss; + } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) { + if (mIsFloating) { + TypedValue res = new TypedValue(); + getContext().getTheme().resolveAttribute( + R.attr.dialogTitleIconsDecorLayout, res, true); + layoutResource = res.resourceId; + } else { + layoutResource = R.layout.screen_title_icons; + } + // XXX Remove this once action bar supports these features. + removeFeature(FEATURE_ACTION_BAR); + // System.out.println("Title Icons!"); + } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0 + && (features & (1 << FEATURE_ACTION_BAR)) == 0) { + // Special case for a window with only a progress bar (and title). + // XXX Need to have a no-title version of embedded windows. + layoutResource = R.layout.screen_progress; + // System.out.println("Progress!"); + } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) { + // Special case for a window with a custom title. + // If the window is floating, we need a dialog layout + if (mIsFloating) { + TypedValue res = new TypedValue(); + getContext().getTheme().resolveAttribute( + R.attr.dialogCustomTitleDecorLayout, res, true); + layoutResource = res.resourceId; + } else { + layoutResource = R.layout.screen_custom_title; + } + // XXX Remove this once action bar supports these features. + removeFeature(FEATURE_ACTION_BAR); + } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) { + // If no other features and not embedded, only need a title. + // If the window is floating, we need a dialog layout + if (mIsFloating) { + TypedValue res = new TypedValue(); + getContext().getTheme().resolveAttribute( + R.attr.dialogTitleDecorLayout, res, true); + layoutResource = res.resourceId; + } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) { + layoutResource = a.getResourceId( + R.styleable.Window_windowActionBarFullscreenDecorLayout, + R.layout.screen_action_bar); + } else { + layoutResource = R.layout.screen_title; + } + // System.out.println("Title!"); + } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) { + layoutResource = R.layout.screen_simple_overlay_action_mode; + } else { + // Embedded, so no decoration is needed. + layoutResource = R.layout.screen_simple; + // System.out.println("Simple!"); + } + + mDecor.startChanging(); + + View in = mLayoutInflater.inflate(layoutResource, null); + decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); + mContentRoot = (ViewGroup) in; + + ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); + if (contentParent == null) { + throw new RuntimeException("Window couldn't find content container view"); + } + + if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) { + ProgressBar progress = getCircularProgressBar(false); + if (progress != null) { + progress.setIndeterminate(true); + } + } + + if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) { + registerSwipeCallbacks(); + } + + // Remaining setup -- of background and title -- that only applies + // to top-level windows. + if (getContainer() == null) { + final Drawable background; + if (mBackgroundResource != 0) { + background = getContext().getDrawable(mBackgroundResource); + } else { + background = mBackgroundDrawable; + } + mDecor.setWindowBackground(background); + + final Drawable frame; + if (mFrameResource != 0) { + frame = getContext().getDrawable(mFrameResource); + } else { + frame = null; + } + mDecor.setWindowFrame(frame); + + mDecor.setElevation(mElevation); + mDecor.setClipToOutline(mClipToOutline); + + if (mTitle != null) { + setTitle(mTitle); + } + + if (mTitleColor == 0) { + mTitleColor = mTextColor; + } + setTitleColor(mTitleColor); + } + + mDecor.finishChanging(); + + return contentParent; + } + + /** @hide */ + public void alwaysReadCloseOnTouchAttr() { + mAlwaysReadCloseOnTouchAttr = true; + } + + private void installDecor() { + if (mDecor == null) { + mDecor = generateDecor(); + mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); + mDecor.setIsRootNamespace(true); + if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) { + mDecor.postOnAnimation(mInvalidatePanelMenuRunnable); + } + } + if (mContentParent == null) { + mContentParent = generateLayout(mDecor); + + // Set up decor part of UI to ignore fitsSystemWindows if appropriate. + mDecor.makeOptionalFitsSystemWindows(); + + final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById( + R.id.decor_content_parent); + + if (decorContentParent != null) { + mDecorContentParent = decorContentParent; + mDecorContentParent.setWindowCallback(getCallback()); + if (mDecorContentParent.getTitle() == null) { + mDecorContentParent.setWindowTitle(mTitle); + } + + final int localFeatures = getLocalFeatures(); + for (int i = 0; i < FEATURE_MAX; i++) { + if ((localFeatures & (1 << i)) != 0) { + mDecorContentParent.initFeature(i); + } + } + + mDecorContentParent.setUiOptions(mUiOptions); + + if ((mResourcesSetFlags & FLAG_RESOURCE_SET_ICON) != 0 || + (mIconRes != 0 && !mDecorContentParent.hasIcon())) { + mDecorContentParent.setIcon(mIconRes); + } else if ((mResourcesSetFlags & FLAG_RESOURCE_SET_ICON) == 0 && + mIconRes == 0 && !mDecorContentParent.hasIcon()) { + mDecorContentParent.setIcon( + getContext().getPackageManager().getDefaultActivityIcon()); + mResourcesSetFlags |= FLAG_RESOURCE_SET_ICON_FALLBACK; + } + if ((mResourcesSetFlags & FLAG_RESOURCE_SET_LOGO) != 0 || + (mLogoRes != 0 && !mDecorContentParent.hasLogo())) { + mDecorContentParent.setLogo(mLogoRes); + } + + // Invalidate if the panel menu hasn't been created before this. + // Panel menu invalidation is deferred avoiding application onCreateOptionsMenu + // being called in the middle of onCreate or similar. + // A pending invalidation will typically be resolved before the posted message + // would run normally in order to satisfy instance state restoration. + PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false); + if (!isDestroyed() && (st == null || st.menu == null)) { + invalidatePanelMenu(FEATURE_ACTION_BAR); + } + } else { + mTitleView = (TextView)findViewById(R.id.title); + if (mTitleView != null) { + mTitleView.setLayoutDirection(mDecor.getLayoutDirection()); + if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) { + View titleContainer = findViewById( + R.id.title_container); + if (titleContainer != null) { + titleContainer.setVisibility(View.GONE); + } else { + mTitleView.setVisibility(View.GONE); + } + if (mContentParent instanceof FrameLayout) { + ((FrameLayout)mContentParent).setForeground(null); + } + } else { + mTitleView.setText(mTitle); + } + } + } + + if (mDecor.getBackground() == null && mBackgroundFallbackResource != 0) { + mDecor.setBackgroundFallback(mBackgroundFallbackResource); + } + + // Only inflate or create a new TransitionManager if the caller hasn't + // already set a custom one. + if (hasFeature(FEATURE_ACTIVITY_TRANSITIONS)) { + if (mTransitionManager == null) { + final int transitionRes = getWindowStyle().getResourceId( + R.styleable.Window_windowContentTransitionManager, + 0); + if (transitionRes != 0) { + final TransitionInflater inflater = TransitionInflater.from(getContext()); + mTransitionManager = inflater.inflateTransitionManager(transitionRes, + mContentParent); + } else { + mTransitionManager = new TransitionManager(); + } + } + + mEnterTransition = getTransition(mEnterTransition, null, + R.styleable.Window_windowEnterTransition); + mReturnTransition = getTransition(mReturnTransition, USE_DEFAULT_TRANSITION, + R.styleable.Window_windowReturnTransition); + mExitTransition = getTransition(mExitTransition, null, + R.styleable.Window_windowExitTransition); + mReenterTransition = getTransition(mReenterTransition, USE_DEFAULT_TRANSITION, + R.styleable.Window_windowReenterTransition); + mSharedElementEnterTransition = getTransition(mSharedElementEnterTransition, null, + R.styleable.Window_windowSharedElementEnterTransition); + mSharedElementReturnTransition = getTransition(mSharedElementReturnTransition, + USE_DEFAULT_TRANSITION, + R.styleable.Window_windowSharedElementReturnTransition); + mSharedElementExitTransition = getTransition(mSharedElementExitTransition, null, + R.styleable.Window_windowSharedElementExitTransition); + mSharedElementReenterTransition = getTransition(mSharedElementReenterTransition, + USE_DEFAULT_TRANSITION, + R.styleable.Window_windowSharedElementReenterTransition); + if (mAllowEnterTransitionOverlap == null) { + mAllowEnterTransitionOverlap = getWindowStyle().getBoolean( + R.styleable.Window_windowAllowEnterTransitionOverlap, true); + } + if (mAllowReturnTransitionOverlap == null) { + mAllowReturnTransitionOverlap = getWindowStyle().getBoolean( + R.styleable.Window_windowAllowReturnTransitionOverlap, true); + } + if (mBackgroundFadeDurationMillis < 0) { + mBackgroundFadeDurationMillis = getWindowStyle().getInteger( + R.styleable.Window_windowTransitionBackgroundFadeDuration, + DEFAULT_BACKGROUND_FADE_DURATION_MS); + } + if (mSharedElementsUseOverlay == null) { + mSharedElementsUseOverlay = getWindowStyle().getBoolean( + R.styleable.Window_windowSharedElementsUseOverlay, true); + } + } + } + } + + private Transition getTransition(Transition currentValue, Transition defaultValue, int id) { + if (currentValue != defaultValue) { + return currentValue; + } + int transitionId = getWindowStyle().getResourceId(id, -1); + Transition transition = defaultValue; + if (transitionId != -1 && transitionId != R.transition.no_transition) { + TransitionInflater inflater = TransitionInflater.from(getContext()); + transition = inflater.inflateTransition(transitionId); + if (transition instanceof TransitionSet && + ((TransitionSet)transition).getTransitionCount() == 0) { + transition = null; + } + } + return transition; + } + + private Drawable loadImageURI(Uri uri) { + try { + return Drawable.createFromStream( + getContext().getContentResolver().openInputStream(uri), null); + } catch (Exception e) { + Log.w(TAG, "Unable to open content: " + uri); + } + return null; + } + + private DrawableFeatureState getDrawableState(int featureId, boolean required) { + if ((getFeatures() & (1 << featureId)) == 0) { + if (!required) { + return null; + } + throw new RuntimeException("The feature has not been requested"); + } + + DrawableFeatureState[] ar; + if ((ar = mDrawables) == null || ar.length <= featureId) { + DrawableFeatureState[] nar = new DrawableFeatureState[featureId + 1]; + if (ar != null) { + System.arraycopy(ar, 0, nar, 0, ar.length); + } + mDrawables = ar = nar; + } + + DrawableFeatureState st = ar[featureId]; + if (st == null) { + ar[featureId] = st = new DrawableFeatureState(featureId); + } + return st; + } + + /** + * Gets a panel's state based on its feature ID. + * + * @param featureId The feature ID of the panel. + * @param required Whether the panel is required (if it is required and it + * isn't in our features, this throws an exception). + * @return The panel state. + */ + private PanelFeatureState getPanelState(int featureId, boolean required) { + return getPanelState(featureId, required, null); + } + + /** + * Gets a panel's state based on its feature ID. + * + * @param featureId The feature ID of the panel. + * @param required Whether the panel is required (if it is required and it + * isn't in our features, this throws an exception). + * @param convertPanelState Optional: If the panel state does not exist, use + * this as the panel state. + * @return The panel state. + */ + private PanelFeatureState getPanelState(int featureId, boolean required, + PanelFeatureState convertPanelState) { + if ((getFeatures() & (1 << featureId)) == 0) { + if (!required) { + return null; + } + throw new RuntimeException("The feature has not been requested"); + } + + PanelFeatureState[] ar; + if ((ar = mPanels) == null || ar.length <= featureId) { + PanelFeatureState[] nar = new PanelFeatureState[featureId + 1]; + if (ar != null) { + System.arraycopy(ar, 0, nar, 0, ar.length); + } + mPanels = ar = nar; + } + + PanelFeatureState st = ar[featureId]; + if (st == null) { + ar[featureId] = st = (convertPanelState != null) + ? convertPanelState + : new PanelFeatureState(featureId); + } + return st; + } + + @Override + public final void setChildDrawable(int featureId, Drawable drawable) { + DrawableFeatureState st = getDrawableState(featureId, true); + st.child = drawable; + updateDrawable(featureId, st, false); + } + + @Override + public final void setChildInt(int featureId, int value) { + updateInt(featureId, value, false); + } + + @Override + public boolean isShortcutKey(int keyCode, KeyEvent event) { + PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false); + return st != null && st.menu != null && st.menu.isShortcutKey(keyCode, event); + } + + private void updateDrawable(int featureId, DrawableFeatureState st, boolean fromResume) { + // Do nothing if the decor is not yet installed... an update will + // need to be forced when we eventually become active. + if (mContentParent == null) { + return; + } + + final int featureMask = 1 << featureId; + + if ((getFeatures() & featureMask) == 0 && !fromResume) { + return; + } + + Drawable drawable = null; + if (st != null) { + drawable = st.child; + if (drawable == null) + drawable = st.local; + if (drawable == null) + drawable = st.def; + } + if ((getLocalFeatures() & featureMask) == 0) { + if (getContainer() != null) { + if (isActive() || fromResume) { + getContainer().setChildDrawable(featureId, drawable); + } + } + } else if (st != null && (st.cur != drawable || st.curAlpha != st.alpha)) { + // System.out.println("Drawable changed: old=" + st.cur + // + ", new=" + drawable); + st.cur = drawable; + st.curAlpha = st.alpha; + onDrawableChanged(featureId, drawable, st.alpha); + } + } + + private void updateInt(int featureId, int value, boolean fromResume) { + + // Do nothing if the decor is not yet installed... an update will + // need to be forced when we eventually become active. + if (mContentParent == null) { + return; + } + + final int featureMask = 1 << featureId; + + if ((getFeatures() & featureMask) == 0 && !fromResume) { + return; + } + + if ((getLocalFeatures() & featureMask) == 0) { + if (getContainer() != null) { + getContainer().setChildInt(featureId, value); + } + } else { + onIntChanged(featureId, value); + } + } + + private ImageView getLeftIconView() { + if (mLeftIconView != null) { + return mLeftIconView; + } + if (mContentParent == null) { + installDecor(); + } + return (mLeftIconView = (ImageView)findViewById(R.id.left_icon)); + } + + @Override + protected void dispatchWindowAttributesChanged(WindowManager.LayoutParams attrs) { + super.dispatchWindowAttributesChanged(attrs); + if (mDecor != null) { + mDecor.updateColorViews(null /* insets */, true /* animate */); + } + } + + private ProgressBar getCircularProgressBar(boolean shouldInstallDecor) { + if (mCircularProgressBar != null) { + return mCircularProgressBar; + } + if (mContentParent == null && shouldInstallDecor) { + installDecor(); + } + mCircularProgressBar = (ProgressBar) findViewById(R.id.progress_circular); + if (mCircularProgressBar != null) { + mCircularProgressBar.setVisibility(View.INVISIBLE); + } + return mCircularProgressBar; + } + + private ProgressBar getHorizontalProgressBar(boolean shouldInstallDecor) { + if (mHorizontalProgressBar != null) { + return mHorizontalProgressBar; + } + if (mContentParent == null && shouldInstallDecor) { + installDecor(); + } + mHorizontalProgressBar = (ProgressBar) findViewById(R.id.progress_horizontal); + if (mHorizontalProgressBar != null) { + mHorizontalProgressBar.setVisibility(View.INVISIBLE); + } + return mHorizontalProgressBar; + } + + private ImageView getRightIconView() { + if (mRightIconView != null) { + return mRightIconView; + } + if (mContentParent == null) { + installDecor(); + } + return (mRightIconView = (ImageView)findViewById(R.id.right_icon)); + } + + private void registerSwipeCallbacks() { + SwipeDismissLayout swipeDismiss = + (SwipeDismissLayout) findViewById(R.id.content); + swipeDismiss.setOnDismissedListener(new SwipeDismissLayout.OnDismissedListener() { + @Override + public void onDismissed(SwipeDismissLayout layout) { + dispatchOnWindowDismissed(); + } + }); + swipeDismiss.setOnSwipeProgressChangedListener( + new SwipeDismissLayout.OnSwipeProgressChangedListener() { + private static final float ALPHA_DECREASE = 0.5f; + private boolean mIsTranslucent = false; + @Override + public void onSwipeProgressChanged( + SwipeDismissLayout layout, float progress, float translate) { + WindowManager.LayoutParams newParams = getAttributes(); + newParams.x = (int) translate; + newParams.alpha = 1 - (progress * ALPHA_DECREASE); + setAttributes(newParams); + + int flags = 0; + if (newParams.x == 0) { + flags = FLAG_FULLSCREEN; + } else { + flags = FLAG_LAYOUT_NO_LIMITS; + } + setFlags(flags, FLAG_FULLSCREEN | FLAG_LAYOUT_NO_LIMITS); + } + + @Override + public void onSwipeCancelled(SwipeDismissLayout layout) { + WindowManager.LayoutParams newParams = getAttributes(); + newParams.x = 0; + newParams.alpha = 1; + setAttributes(newParams); + setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN | FLAG_LAYOUT_NO_LIMITS); + } + }); + } + + /** + * Helper method for calling the {@link Callback#onPanelClosed(int, Menu)} + * callback. This method will grab whatever extra state is needed for the + * callback that isn't given in the parameters. If the panel is not open, + * this will not perform the callback. + * + * @param featureId Feature ID of the panel that was closed. Must be given. + * @param panel Panel that was closed. Optional but useful if there is no + * menu given. + * @param menu The menu that was closed. Optional, but give if you have. + */ + private void callOnPanelClosed(int featureId, PanelFeatureState panel, Menu menu) { + final Callback cb = getCallback(); + if (cb == null) + return; + + // Try to get a menu + if (menu == null) { + // Need a panel to grab the menu, so try to get that + if (panel == null) { + if ((featureId >= 0) && (featureId < mPanels.length)) { + panel = mPanels[featureId]; + } + } + + if (panel != null) { + // menu still may be null, which is okay--we tried our best + menu = panel.menu; + } + } + + // If the panel is not open, do not callback + if ((panel != null) && (!panel.isOpen)) + return; + + if (!isDestroyed()) { + cb.onPanelClosed(featureId, menu); + } + } + + /** + * Helper method for adding launch-search to most applications. Opens the + * search window using default settings. + * + * @return true if search window opened + */ + private boolean launchDefaultSearch() { + boolean result; + final Callback cb = getCallback(); + if (cb == null || isDestroyed()) { + result = false; + } else { + sendCloseSystemWindows("search"); + result = cb.onSearchRequested(); + } + if (!result && (getContext().getResources().getConfiguration().uiMode + & Configuration.UI_MODE_TYPE_MASK) == Configuration.UI_MODE_TYPE_TELEVISION) { + // On TVs, if the app doesn't implement search, we want to launch assist. + return ((SearchManager)getContext().getSystemService(Context.SEARCH_SERVICE)) + .launchAssistAction(null, UserHandle.myUserId()); + } + return result; + } + + @Override + public void setVolumeControlStream(int streamType) { + mVolumeControlStreamType = streamType; + } + + @Override + public int getVolumeControlStream() { + return mVolumeControlStreamType; + } + + @Override + public void setMediaController(MediaController controller) { + mMediaController = controller; + } + + @Override + public MediaController getMediaController() { + return mMediaController; + } + + @Override + public void setEnterTransition(Transition enterTransition) { + mEnterTransition = enterTransition; + } + + @Override + public void setReturnTransition(Transition transition) { + mReturnTransition = transition; + } + + @Override + public void setExitTransition(Transition exitTransition) { + mExitTransition = exitTransition; + } + + @Override + public void setReenterTransition(Transition transition) { + mReenterTransition = transition; + } + + @Override + public void setSharedElementEnterTransition(Transition sharedElementEnterTransition) { + mSharedElementEnterTransition = sharedElementEnterTransition; + } + + @Override + public void setSharedElementReturnTransition(Transition transition) { + mSharedElementReturnTransition = transition; + } + + @Override + public void setSharedElementExitTransition(Transition sharedElementExitTransition) { + mSharedElementExitTransition = sharedElementExitTransition; + } + + @Override + public void setSharedElementReenterTransition(Transition transition) { + mSharedElementReenterTransition = transition; + } + + @Override + public Transition getEnterTransition() { + return mEnterTransition; + } + + @Override + public Transition getReturnTransition() { + return mReturnTransition == USE_DEFAULT_TRANSITION ? getEnterTransition() + : mReturnTransition; + } + + @Override + public Transition getExitTransition() { + return mExitTransition; + } + + @Override + public Transition getReenterTransition() { + return mReenterTransition == USE_DEFAULT_TRANSITION ? getExitTransition() + : mReenterTransition; + } + + @Override + public Transition getSharedElementEnterTransition() { + return mSharedElementEnterTransition; + } + + @Override + public Transition getSharedElementReturnTransition() { + return mSharedElementReturnTransition == USE_DEFAULT_TRANSITION + ? getSharedElementEnterTransition() : mSharedElementReturnTransition; + } + + @Override + public Transition getSharedElementExitTransition() { + return mSharedElementExitTransition; + } + + @Override + public Transition getSharedElementReenterTransition() { + return mSharedElementReenterTransition == USE_DEFAULT_TRANSITION + ? getSharedElementExitTransition() : mSharedElementReenterTransition; + } + + @Override + public void setAllowEnterTransitionOverlap(boolean allow) { + mAllowEnterTransitionOverlap = allow; + } + + @Override + public boolean getAllowEnterTransitionOverlap() { + return (mAllowEnterTransitionOverlap == null) ? true : mAllowEnterTransitionOverlap; + } + + @Override + public void setAllowReturnTransitionOverlap(boolean allowExitTransitionOverlap) { + mAllowReturnTransitionOverlap = allowExitTransitionOverlap; + } + + @Override + public boolean getAllowReturnTransitionOverlap() { + return (mAllowReturnTransitionOverlap == null) ? true : mAllowReturnTransitionOverlap; + } + + @Override + public long getTransitionBackgroundFadeDuration() { + return (mBackgroundFadeDurationMillis < 0) ? DEFAULT_BACKGROUND_FADE_DURATION_MS + : mBackgroundFadeDurationMillis; + } + + @Override + public void setTransitionBackgroundFadeDuration(long fadeDurationMillis) { + if (fadeDurationMillis < 0) { + throw new IllegalArgumentException("negative durations are not allowed"); + } + mBackgroundFadeDurationMillis = fadeDurationMillis; + } + + @Override + public void setSharedElementsUseOverlay(boolean sharedElementsUseOverlay) { + mSharedElementsUseOverlay = sharedElementsUseOverlay; + } + + @Override + public boolean getSharedElementsUseOverlay() { + return (mSharedElementsUseOverlay == null) ? true : mSharedElementsUseOverlay; + } + + private static final class DrawableFeatureState { + DrawableFeatureState(int _featureId) { + featureId = _featureId; + } + + final int featureId; + + int resid; + + Uri uri; + + Drawable local; + + Drawable child; + + Drawable def; + + Drawable cur; + + int alpha = 255; + + int curAlpha = 255; + } + + private static final class PanelFeatureState { + + /** Feature ID for this panel. */ + int featureId; + + // Information pulled from the style for this panel. + + int background; + + /** The background when the panel spans the entire available width. */ + int fullBackground; + + int gravity; + + int x; + + int y; + + int windowAnimations; + + /** Dynamic state of the panel. */ + DecorView decorView; + + /** The panel that was returned by onCreatePanelView(). */ + View createdPanelView; + + /** The panel that we are actually showing. */ + View shownPanelView; + + /** Use {@link #setMenu} to set this. */ + MenuBuilder menu; + + IconMenuPresenter iconMenuPresenter; + ListMenuPresenter listMenuPresenter; + + /** true if this menu will show in single-list compact mode */ + boolean isCompact; + + /** Theme resource ID for list elements of the panel menu */ + int listPresenterTheme; + + /** + * Whether the panel has been prepared (see + * {@link PhoneWindow#preparePanel}). + */ + boolean isPrepared; + + /** + * Whether an item's action has been performed. This happens in obvious + * scenarios (user clicks on menu item), but can also happen with + * chording menu+(shortcut key). + */ + boolean isHandled; + + boolean isOpen; + + /** + * True if the menu is in expanded mode, false if the menu is in icon + * mode + */ + boolean isInExpandedMode; + + public boolean qwertyMode; + + boolean refreshDecorView; + + boolean refreshMenuContent; + + boolean wasLastOpen; + + boolean wasLastExpanded; + + /** + * Contains the state of the menu when told to freeze. + */ + Bundle frozenMenuState; + + /** + * Contains the state of associated action views when told to freeze. + * These are saved across invalidations. + */ + Bundle frozenActionViewState; + + PanelFeatureState(int featureId) { + this.featureId = featureId; + + refreshDecorView = false; + } + + public boolean isInListMode() { + return isInExpandedMode || isCompact; + } + + public boolean hasPanelItems() { + if (shownPanelView == null) return false; + if (createdPanelView != null) return true; + + if (isCompact || isInExpandedMode) { + return listMenuPresenter.getAdapter().getCount() > 0; + } else { + return ((ViewGroup) shownPanelView).getChildCount() > 0; + } + } + + /** + * Unregister and free attached MenuPresenters. They will be recreated as needed. + */ + public void clearMenuPresenters() { + if (menu != null) { + menu.removeMenuPresenter(iconMenuPresenter); + menu.removeMenuPresenter(listMenuPresenter); + } + iconMenuPresenter = null; + listMenuPresenter = null; + } + + void setStyle(Context context) { + TypedArray a = context.obtainStyledAttributes(R.styleable.Theme); + background = a.getResourceId( + R.styleable.Theme_panelBackground, 0); + fullBackground = a.getResourceId( + R.styleable.Theme_panelFullBackground, 0); + windowAnimations = a.getResourceId( + R.styleable.Theme_windowAnimationStyle, 0); + isCompact = a.getBoolean( + R.styleable.Theme_panelMenuIsCompact, false); + listPresenterTheme = a.getResourceId( + R.styleable.Theme_panelMenuListTheme, + R.style.Theme_ExpandedMenu); + a.recycle(); + } + + void setMenu(MenuBuilder menu) { + if (menu == this.menu) return; + + if (this.menu != null) { + this.menu.removeMenuPresenter(iconMenuPresenter); + this.menu.removeMenuPresenter(listMenuPresenter); + } + this.menu = menu; + if (menu != null) { + if (iconMenuPresenter != null) menu.addMenuPresenter(iconMenuPresenter); + if (listMenuPresenter != null) menu.addMenuPresenter(listMenuPresenter); + } + } + + MenuView getListMenuView(Context context, MenuPresenter.Callback cb) { + if (menu == null) return null; + + if (!isCompact) { + getIconMenuView(context, cb); // Need this initialized to know where our offset goes + } + + if (listMenuPresenter == null) { + listMenuPresenter = new ListMenuPresenter( + R.layout.list_menu_item_layout, listPresenterTheme); + listMenuPresenter.setCallback(cb); + listMenuPresenter.setId(R.id.list_menu_presenter); + menu.addMenuPresenter(listMenuPresenter); + } + + if (iconMenuPresenter != null) { + listMenuPresenter.setItemIndexOffset( + iconMenuPresenter.getNumActualItemsShown()); + } + MenuView result = listMenuPresenter.getMenuView(decorView); + + return result; + } + + MenuView getIconMenuView(Context context, MenuPresenter.Callback cb) { + if (menu == null) return null; + + if (iconMenuPresenter == null) { + iconMenuPresenter = new IconMenuPresenter(context); + iconMenuPresenter.setCallback(cb); + iconMenuPresenter.setId(R.id.icon_menu_presenter); + menu.addMenuPresenter(iconMenuPresenter); + } + + MenuView result = iconMenuPresenter.getMenuView(decorView); + + return result; + } + + Parcelable onSaveInstanceState() { + SavedState savedState = new SavedState(); + savedState.featureId = featureId; + savedState.isOpen = isOpen; + savedState.isInExpandedMode = isInExpandedMode; + + if (menu != null) { + savedState.menuState = new Bundle(); + menu.savePresenterStates(savedState.menuState); + } + + return savedState; + } + + void onRestoreInstanceState(Parcelable state) { + SavedState savedState = (SavedState) state; + featureId = savedState.featureId; + wasLastOpen = savedState.isOpen; + wasLastExpanded = savedState.isInExpandedMode; + frozenMenuState = savedState.menuState; + + /* + * A LocalActivityManager keeps the same instance of this class around. + * The first time the menu is being shown after restoring, the + * Activity.onCreateOptionsMenu should be called. But, if it is the + * same instance then menu != null and we won't call that method. + * We clear any cached views here. The caller should invalidatePanelMenu. + */ + createdPanelView = null; + shownPanelView = null; + decorView = null; + } + + void applyFrozenState() { + if (menu != null && frozenMenuState != null) { + menu.restorePresenterStates(frozenMenuState); + frozenMenuState = null; + } + } + + private static class SavedState implements Parcelable { + int featureId; + boolean isOpen; + boolean isInExpandedMode; + Bundle menuState; + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(featureId); + dest.writeInt(isOpen ? 1 : 0); + dest.writeInt(isInExpandedMode ? 1 : 0); + + if (isOpen) { + dest.writeBundle(menuState); + } + } + + private static SavedState readFromParcel(Parcel source) { + SavedState savedState = new SavedState(); + savedState.featureId = source.readInt(); + savedState.isOpen = source.readInt() == 1; + savedState.isInExpandedMode = source.readInt() == 1; + + if (savedState.isOpen) { + savedState.menuState = source.readBundle(); + } + + return savedState; + } + + public static final Parcelable.Creator<SavedState> CREATOR + = new Parcelable.Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return readFromParcel(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + } + + static class RotationWatcher extends IRotationWatcher.Stub { + private Handler mHandler; + private final Runnable mRotationChanged = new Runnable() { + public void run() { + dispatchRotationChanged(); + } + }; + private final ArrayList<WeakReference<PhoneWindow>> mWindows = + new ArrayList<WeakReference<PhoneWindow>>(); + private boolean mIsWatching; + + @Override + public void onRotationChanged(int rotation) throws RemoteException { + mHandler.post(mRotationChanged); + } + + public void addWindow(PhoneWindow phoneWindow) { + synchronized (mWindows) { + if (!mIsWatching) { + try { + WindowManagerHolder.sWindowManager.watchRotation(this); + mHandler = new Handler(); + mIsWatching = true; + } catch (RemoteException ex) { + Log.e(TAG, "Couldn't start watching for device rotation", ex); + } + } + mWindows.add(new WeakReference<PhoneWindow>(phoneWindow)); + } + } + + public void removeWindow(PhoneWindow phoneWindow) { + synchronized (mWindows) { + int i = 0; + while (i < mWindows.size()) { + final WeakReference<PhoneWindow> ref = mWindows.get(i); + final PhoneWindow win = ref.get(); + if (win == null || win == phoneWindow) { + mWindows.remove(i); + } else { + i++; + } + } + } + } + + void dispatchRotationChanged() { + synchronized (mWindows) { + int i = 0; + while (i < mWindows.size()) { + final WeakReference<PhoneWindow> ref = mWindows.get(i); + final PhoneWindow win = ref.get(); + if (win != null) { + win.onOptionsPanelRotationChanged(); + i++; + } else { + mWindows.remove(i); + } + } + } + } + } + + /** + * Simple implementation of MenuBuilder.Callback that: + * <li> Opens a submenu when selected. + * <li> Calls back to the callback's onMenuItemSelected when an item is + * selected. + */ + private final class DialogMenuCallback implements MenuBuilder.Callback, MenuPresenter.Callback { + private int mFeatureId; + private MenuDialogHelper mSubMenuHelper; + + public DialogMenuCallback(int featureId) { + mFeatureId = featureId; + } + + public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { + if (menu.getRootMenu() != menu) { + onCloseSubMenu(menu); + } + + if (allMenusAreClosing) { + Callback callback = getCallback(); + if (callback != null && !isDestroyed()) { + callback.onPanelClosed(mFeatureId, menu); + } + + if (menu == mContextMenu) { + dismissContextMenu(); + } + + // Dismiss the submenu, if it is showing + if (mSubMenuHelper != null) { + mSubMenuHelper.dismiss(); + mSubMenuHelper = null; + } + } + } + + public void onCloseSubMenu(MenuBuilder menu) { + Callback callback = getCallback(); + if (callback != null && !isDestroyed()) { + callback.onPanelClosed(mFeatureId, menu.getRootMenu()); + } + } + + public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) { + Callback callback = getCallback(); + return (callback != null && !isDestroyed()) + && callback.onMenuItemSelected(mFeatureId, item); + } + + public void onMenuModeChange(MenuBuilder menu) { + } + + public boolean onOpenSubMenu(MenuBuilder subMenu) { + if (subMenu == null) return false; + + // Set a simple callback for the submenu + subMenu.setCallback(this); + + // The window manager will give us a valid window token + mSubMenuHelper = new MenuDialogHelper(subMenu); + mSubMenuHelper.show(null); + + return true; + } + } + + private static class ColorViewState { + View view = null; + int targetVisibility = View.INVISIBLE; + + final int id; + final int systemUiHideFlag; + final int translucentFlag; + final int verticalGravity; + final String transitionName; + final int hideWindowFlag; + + ColorViewState(int systemUiHideFlag, + int translucentFlag, int verticalGravity, + String transitionName, int id, int hideWindowFlag) { + this.id = id; + this.systemUiHideFlag = systemUiHideFlag; + this.translucentFlag = translucentFlag; + this.verticalGravity = verticalGravity; + this.transitionName = transitionName; + this.hideWindowFlag = hideWindowFlag; + } + } + + void sendCloseSystemWindows() { + sendCloseSystemWindows(getContext(), null); + } + + void sendCloseSystemWindows(String reason) { + sendCloseSystemWindows(getContext(), reason); + } + + public static void sendCloseSystemWindows(Context context, String reason) { + if (ActivityManagerNative.isSystemReady()) { + try { + ActivityManagerNative.getDefault().closeSystemDialogs(reason); + } catch (RemoteException e) { + } + } + } + + @Override + public int getStatusBarColor() { + return mStatusBarColor; + } + + @Override + public void setStatusBarColor(int color) { + mStatusBarColor = color; + mForcedStatusBarColor = true; + if (mDecor != null) { + mDecor.updateColorViews(null, false /* animate */); + } + } + + @Override + public int getNavigationBarColor() { + return mNavigationBarColor; + } + + @Override + public void setNavigationBarColor(int color) { + mNavigationBarColor = color; + mForcedNavigationBarColor = true; + if (mDecor != null) { + mDecor.updateColorViews(null, false /* animate */); + } + } +} diff --git a/core/java/android/view/PointerIcon.java b/core/java/android/view/PointerIcon.java index 7dcad68..cf35ce5 100644 --- a/core/java/android/view/PointerIcon.java +++ b/core/java/android/view/PointerIcon.java @@ -18,6 +18,7 @@ package android.view; import com.android.internal.util.XmlUtils; +import android.annotation.XmlRes; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; @@ -192,7 +193,7 @@ public final class PointerIcon implements Parcelable { * @throws Resources.NotFoundException if the resource was not found or the drawable * linked in the resource was not found. */ - public static PointerIcon loadCustomIcon(Resources resources, int resourceId) { + public static PointerIcon loadCustomIcon(Resources resources, @XmlRes int resourceId) { if (resources == null) { throw new IllegalArgumentException("resources must not be null"); } @@ -373,7 +374,7 @@ public final class PointerIcon implements Parcelable { return true; } - private void loadResource(Context context, Resources resources, int resourceId) { + private void loadResource(Context context, Resources resources, @XmlRes int resourceId) { final XmlResourceParser parser = resources.getXml(resourceId); final int bitmapRes; final float hotSpotX; diff --git a/core/java/android/view/RenderNode.java b/core/java/android/view/RenderNode.java index 47f72a8..ef98bbc 100644 --- a/core/java/android/view/RenderNode.java +++ b/core/java/android/view/RenderNode.java @@ -26,7 +26,7 @@ import android.graphics.Rect; /** * <p>A display list records a series of graphics related operations and can replay * them later. Display lists are usually built by recording operations on a - * {@link HardwareCanvas}. Replaying the operations from a display list avoids + * {@link DisplayListCanvas}. Replaying the operations from a display list avoids * executing application code on every frame, and is thus much more efficient.</p> * * <p>Display lists are used internally for all views by default, and are not @@ -43,7 +43,7 @@ import android.graphics.Rect; * affected paragraph needs to be recorded again.</p> * * <h3>Hardware acceleration</h3> - * <p>Display lists can only be replayed using a {@link HardwareCanvas}. They are not + * <p>Display lists can only be replayed using a {@link DisplayListCanvas}. They are not * supported in software. Always make sure that the {@link android.graphics.Canvas} * you are using to render a display list is hardware accelerated using * {@link android.graphics.Canvas#isHardwareAccelerated()}.</p> @@ -53,7 +53,7 @@ import android.graphics.Rect; * HardwareRenderer renderer = myView.getHardwareRenderer(); * if (renderer != null) { * DisplayList displayList = renderer.createDisplayList(); - * HardwareCanvas canvas = displayList.start(width, height); + * DisplayListCanvas canvas = displayList.start(width, height); * try { * // Draw onto the canvas * // For instance: canvas.drawBitmap(...); @@ -67,8 +67,8 @@ import android.graphics.Rect; * <pre class="prettyprint"> * protected void onDraw(Canvas canvas) { * if (canvas.isHardwareAccelerated()) { - * HardwareCanvas hardwareCanvas = (HardwareCanvas) canvas; - * hardwareCanvas.drawDisplayList(mDisplayList); + * DisplayListCanvas displayListCanvas = (DisplayListCanvas) canvas; + * displayListCanvas.drawDisplayList(mDisplayList); * } * } * </pre> @@ -92,7 +92,7 @@ import android.graphics.Rect; * <pre class="prettyprint"> * private void createDisplayList() { * mDisplayList = DisplayList.create("MyDisplayList"); - * HardwareCanvas canvas = mDisplayList.start(width, height); + * DisplayListCanvas canvas = mDisplayList.start(width, height); * try { * for (Bitmap b : mBitmaps) { * canvas.drawBitmap(b, 0.0f, 0.0f, null); @@ -105,8 +105,8 @@ import android.graphics.Rect; * * protected void onDraw(Canvas canvas) { * if (canvas.isHardwareAccelerated()) { - * HardwareCanvas hardwareCanvas = (HardwareCanvas) canvas; - * hardwareCanvas.drawDisplayList(mDisplayList); + * DisplayListCanvas displayListCanvas = (DisplayListCanvas) canvas; + * displayListCanvas.drawDisplayList(mDisplayList); * } * } * @@ -128,7 +128,7 @@ import android.graphics.Rect; public class RenderNode { /** * Flag used when calling - * {@link HardwareCanvas#drawRenderNode(RenderNode, android.graphics.Rect, int)} + * {@link DisplayListCanvas#drawRenderNode * When this flag is set, draw operations lying outside of the bounds of the * display list will be culled early. It is recommeneded to always set this * flag. @@ -140,29 +140,29 @@ public class RenderNode { /** * Indicates that the display list is done drawing. * - * @see HardwareCanvas#drawRenderNode(RenderNode, android.graphics.Rect, int) + * @see DisplayListCanvas#drawRenderNode(RenderNode, int) */ public static final int STATUS_DONE = 0x0; /** * Indicates that the display list needs another drawing pass. * - * @see HardwareCanvas#drawRenderNode(RenderNode, android.graphics.Rect, int) + * @see DisplayListCanvas#drawRenderNode(RenderNode, int) */ public static final int STATUS_DRAW = 0x1; /** * Indicates that the display list needs to re-execute its GL functors. * - * @see HardwareCanvas#drawRenderNode(RenderNode, android.graphics.Rect, int) - * @see HardwareCanvas#callDrawGLFunction(long) + * @see DisplayListCanvas#drawRenderNode(RenderNode, int) + * @see DisplayListCanvas#callDrawGLFunction2(long) */ public static final int STATUS_INVOKE = 0x2; /** * Indicates that the display list performed GL drawing operations. * - * @see HardwareCanvas#drawRenderNode(RenderNode, android.graphics.Rect, int) + * @see DisplayListCanvas#drawRenderNode(RenderNode, int) */ public static final int STATUS_DREW = 0x4; @@ -213,7 +213,7 @@ public class RenderNode { * stored in this display list. * * Calling this method will mark the render node invalid until - * {@link #end(HardwareCanvas)} is called. + * {@link #end(DisplayListCanvas)} is called. * Only valid render nodes can be replayed. * * @param width The width of the recording viewport @@ -221,11 +221,11 @@ public class RenderNode { * * @return A canvas to record drawing operations. * - * @see #end(HardwareCanvas) + * @see #end(DisplayListCanvas) * @see #isValid() */ - public HardwareCanvas start(int width, int height) { - HardwareCanvas canvas = GLES20RecordingCanvas.obtain(this); + public DisplayListCanvas start(int width, int height) { + DisplayListCanvas canvas = DisplayListCanvas.obtain(this); canvas.setViewport(width, height); // The dirty rect should always be null for a display list canvas.onPreDraw(null); @@ -240,12 +240,12 @@ public class RenderNode { * @see #start(int, int) * @see #isValid() */ - public void end(HardwareCanvas endCanvas) { - if (!(endCanvas instanceof GLES20RecordingCanvas)) { + public void end(DisplayListCanvas endCanvas) { + if (!(endCanvas instanceof DisplayListCanvas)) { throw new IllegalArgumentException("Passed an invalid canvas to end!"); } - GLES20RecordingCanvas canvas = (GLES20RecordingCanvas) endCanvas; + DisplayListCanvas canvas = (DisplayListCanvas) endCanvas; canvas.onPostDraw(); long renderNodeData = canvas.finishRecording(); nSetDisplayListData(mNativeRenderNode, renderNodeData); @@ -305,7 +305,7 @@ public class RenderNode { } public boolean setLayerPaint(Paint paint) { - return nSetLayerPaint(mNativeRenderNode, paint != null ? paint.mNativePaint : 0); + return nSetLayerPaint(mNativeRenderNode, paint != null ? paint.getNativeInstance() : 0); } public boolean setClipBounds(@Nullable Rect rect) { diff --git a/core/java/android/view/RenderNodeAnimator.java b/core/java/android/view/RenderNodeAnimator.java index 7b35a3b..379796d 100644 --- a/core/java/android/view/RenderNodeAnimator.java +++ b/core/java/android/view/RenderNodeAnimator.java @@ -283,10 +283,10 @@ public class RenderNodeAnimator extends Animator { } public void setTarget(Canvas canvas) { - if (!(canvas instanceof GLES20RecordingCanvas)) { + if (!(canvas instanceof DisplayListCanvas)) { throw new IllegalArgumentException("Not a GLES20RecordingCanvas"); } - final GLES20RecordingCanvas recordingCanvas = (GLES20RecordingCanvas) canvas; + final DisplayListCanvas recordingCanvas = (DisplayListCanvas) canvas; setTarget(recordingCanvas.mNode); } diff --git a/core/java/android/view/SubMenu.java b/core/java/android/view/SubMenu.java index 196a183..38662b0 100644 --- a/core/java/android/view/SubMenu.java +++ b/core/java/android/view/SubMenu.java @@ -16,6 +16,8 @@ package android.view; +import android.annotation.DrawableRes; +import android.annotation.StringRes; import android.graphics.drawable.Drawable; /** @@ -38,7 +40,7 @@ public interface SubMenu extends Menu { * @param titleRes The string resource identifier used for the title. * @return This SubMenu so additional setters can be called. */ - public SubMenu setHeaderTitle(int titleRes); + public SubMenu setHeaderTitle(@StringRes int titleRes); /** * Sets the submenu header's title to the title given in <var>title</var>. @@ -55,7 +57,7 @@ public interface SubMenu extends Menu { * @param iconRes The resource identifier used for the icon. * @return This SubMenu so additional setters can be called. */ - public SubMenu setHeaderIcon(int iconRes); + public SubMenu setHeaderIcon(@DrawableRes int iconRes); /** * Sets the submenu header's icon to the icon given in <var>icon</var> @@ -88,7 +90,7 @@ public interface SubMenu extends Menu { * @param iconRes The new icon (as a resource ID) to be displayed. * @return This SubMenu so additional setters can be called. */ - public SubMenu setIcon(int iconRes); + public SubMenu setIcon(@DrawableRes int iconRes); /** * Change the icon associated with this submenu's item in its parent menu. diff --git a/core/java/android/view/Surface.java b/core/java/android/view/Surface.java index 33ce517..6de4d3e 100644 --- a/core/java/android/view/Surface.java +++ b/core/java/android/view/Surface.java @@ -322,7 +322,6 @@ public class Surface implements Parcelable { * @return A canvas for drawing into the surface. * * @throws IllegalStateException If the canvas cannot be locked. - * @hide */ public Canvas lockHardwareCanvas() { synchronized (mLock) { @@ -573,7 +572,7 @@ public class Surface implements Parcelable { private final class HwuiContext { private final RenderNode mRenderNode; private long mHwuiRenderer; - private HardwareCanvas mCanvas; + private DisplayListCanvas mCanvas; HwuiContext() { mRenderNode = RenderNode.create("HwuiCanvas", null); diff --git a/core/java/android/view/ThreadedRenderer.java b/core/java/android/view/ThreadedRenderer.java index ad4a048..031be07 100644 --- a/core/java/android/view/ThreadedRenderer.java +++ b/core/java/android/view/ThreadedRenderer.java @@ -16,8 +16,7 @@ package android.view; -import com.android.internal.R; - +import android.annotation.IntDef; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; @@ -27,16 +26,18 @@ import android.graphics.drawable.Drawable; import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; -import android.os.SystemProperties; import android.os.Trace; import android.util.Log; import android.util.LongSparseArray; -import android.util.TimeUtils; import android.view.Surface.OutOfResourcesException; import android.view.View.AttachInfo; +import com.android.internal.R; + import java.io.FileDescriptor; import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.HashSet; @@ -74,6 +75,14 @@ public class ThreadedRenderer extends HardwareRenderer { PROFILE_PROPERTY_VISUALIZE_BARS, }; + private static final int FLAG_DUMP_FRAMESTATS = 1 << 0; + private static final int FLAG_DUMP_RESET = 1 << 1; + + @IntDef(flag = true, value = { + FLAG_DUMP_FRAMESTATS, FLAG_DUMP_RESET }) + @Retention(RetentionPolicy.SOURCE) + public @interface DumpFlags {} + // Size of the rendered content. private int mWidth, mHeight; @@ -98,7 +107,6 @@ public class ThreadedRenderer extends HardwareRenderer { private boolean mInitialized = false; private RenderNode mRootNode; private Choreographer mChoreographer; - private boolean mProfilingEnabled; private boolean mRootNodeNeedsUpdate; ThreadedRenderer(Context context, boolean translucent) { @@ -118,10 +126,6 @@ public class ThreadedRenderer extends HardwareRenderer { AtlasInitializer.sInstance.init(context, mNativeProxy); - // Setup timing - mChoreographer = Choreographer.getInstance(); - nSetFrameInterval(mNativeProxy, mChoreographer.getFrameIntervalNanos()); - loadSystemProperties(); } @@ -233,32 +237,25 @@ public class ThreadedRenderer extends HardwareRenderer { } @Override - void dumpGfxInfo(PrintWriter pw, FileDescriptor fd) { + void dumpGfxInfo(PrintWriter pw, FileDescriptor fd, String[] args) { pw.flush(); - nDumpProfileInfo(mNativeProxy, fd); - } - - private static int search(String[] values, String value) { - for (int i = 0; i < values.length; i++) { - if (values[i].equals(value)) return i; + int flags = 0; + for (int i = 0; i < args.length; i++) { + switch (args[i]) { + case "framestats": + flags |= FLAG_DUMP_FRAMESTATS; + break; + case "reset": + flags |= FLAG_DUMP_RESET; + break; + } } - return -1; - } - - private static boolean checkIfProfilingRequested() { - String profiling = SystemProperties.get(HardwareRenderer.PROFILE_PROPERTY); - int graphType = search(VISUALIZERS, profiling); - return (graphType >= 0) || Boolean.parseBoolean(profiling); + nDumpProfileInfo(mNativeProxy, fd, flags); } @Override boolean loadSystemProperties() { boolean changed = nLoadSystemProperties(mNativeProxy); - boolean wantProfiling = checkIfProfilingRequested(); - if (wantProfiling != mProfilingEnabled) { - mProfilingEnabled = wantProfiling; - changed = true; - } if (changed) { invalidateRoot(); } @@ -279,7 +276,7 @@ public class ThreadedRenderer extends HardwareRenderer { updateViewTreeDisplayList(view); if (mRootNodeNeedsUpdate || !mRootNode.isValid()) { - HardwareCanvas canvas = mRootNode.start(mSurfaceWidth, mSurfaceHeight); + DisplayListCanvas canvas = mRootNode.start(mSurfaceWidth, mSurfaceHeight); try { final int saveCount = canvas.save(); canvas.translate(mInsetLeft, mInsetTop); @@ -307,20 +304,12 @@ public class ThreadedRenderer extends HardwareRenderer { @Override void draw(View view, AttachInfo attachInfo, HardwareDrawCallbacks callbacks) { attachInfo.mIgnoreDirtyState = true; - long frameTimeNanos = mChoreographer.getFrameTimeNanos(); - attachInfo.mDrawingTime = frameTimeNanos / TimeUtils.NANOS_PER_MS; - long recordDuration = 0; - if (mProfilingEnabled) { - recordDuration = System.nanoTime(); - } + final Choreographer choreographer = attachInfo.mViewRootImpl.mChoreographer; + choreographer.mFrameInfo.markDrawStart(); updateRootDisplayList(view, callbacks); - if (mProfilingEnabled) { - recordDuration = System.nanoTime() - recordDuration; - } - attachInfo.mIgnoreDirtyState = false; // register animating rendernodes which started animating prior to renderer @@ -337,8 +326,8 @@ public class ThreadedRenderer extends HardwareRenderer { attachInfo.mPendingAnimatingRenderNodes = null; } - int syncResult = nSyncAndDrawFrame(mNativeProxy, frameTimeNanos, - recordDuration, view.getResources().getDisplayMetrics().density); + final long[] frameInfo = choreographer.mFrameInfo.mFrameInfo; + int syncResult = nSyncAndDrawFrame(mNativeProxy, frameInfo, frameInfo.length); if ((syncResult & SYNC_LOST_SURFACE_REWARD_IF_FOUND) != 0) { setEnabled(false); attachInfo.mViewRootImpl.mSurface.release(); @@ -369,7 +358,7 @@ public class ThreadedRenderer extends HardwareRenderer { @Override boolean copyLayerInto(final HardwareLayer layer, final Bitmap bitmap) { return nCopyLayerInto(mNativeProxy, - layer.getDeferredLayerUpdater(), bitmap.mNativeBitmap); + layer.getDeferredLayerUpdater(), bitmap.getSkBitmap()); } @Override @@ -384,6 +373,7 @@ public class ThreadedRenderer extends HardwareRenderer { @Override void setName(String name) { + nSetName(mNativeProxy, name); } @Override @@ -470,7 +460,7 @@ public class ThreadedRenderer extends HardwareRenderer { for (int i = 0; i < count; i++) { drawables.valueAt(i).addAtlasableBitmaps(tmpList); for (int j = 0; j < tmpList.size(); j++) { - preloadedPointers.add(tmpList.get(j).mNativeBitmap); + preloadedPointers.add(tmpList.get(j).getSkBitmap()); } tmpList.clear(); } @@ -492,8 +482,8 @@ public class ThreadedRenderer extends HardwareRenderer { private static native long nCreateProxy(boolean translucent, long rootRenderNode); private static native void nDeleteProxy(long nativeProxy); - private static native void nSetFrameInterval(long nativeProxy, long frameIntervalNanos); private static native boolean nLoadSystemProperties(long nativeProxy); + private static native void nSetName(long nativeProxy, String name); private static native boolean nInitialize(long nativeProxy, Surface window); private static native void nUpdateSurface(long nativeProxy, Surface window); @@ -502,8 +492,7 @@ public class ThreadedRenderer extends HardwareRenderer { float lightX, float lightY, float lightZ, float lightRadius, int ambientShadowAlpha, int spotShadowAlpha); private static native void nSetOpaque(long nativeProxy, boolean opaque); - private static native int nSyncAndDrawFrame(long nativeProxy, - long frameTimeNanos, long recordDuration, float density); + private static native int nSyncAndDrawFrame(long nativeProxy, long[] frameInfo, int size); private static native void nDestroy(long nativeProxy); private static native void nRegisterAnimatingRenderNode(long rootRenderNode, long animatingNode); @@ -523,5 +512,6 @@ public class ThreadedRenderer extends HardwareRenderer { private static native void nStopDrawing(long nativeProxy); private static native void nNotifyFramePending(long nativeProxy); - private static native void nDumpProfileInfo(long nativeProxy, FileDescriptor fd); + private static native void nDumpProfileInfo(long nativeProxy, FileDescriptor fd, + @DumpFlags int dumpFlags); } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 3c05872..f5de8e3 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -18,9 +18,16 @@ package android.view; import android.animation.AnimatorInflater; import android.animation.StateListAnimator; +import android.annotation.CallSuper; +import android.annotation.ColorInt; +import android.annotation.DrawableRes; +import android.annotation.FloatRange; +import android.annotation.IdRes; import android.annotation.IntDef; +import android.annotation.LayoutRes; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.Size; import android.content.ClipData; import android.content.Context; import android.content.res.ColorStateList; @@ -35,8 +42,6 @@ import android.graphics.LinearGradient; import android.graphics.Matrix; import android.graphics.Outline; import android.graphics.Paint; -import android.graphics.Path; -import android.graphics.PathMeasure; import android.graphics.PixelFormat; import android.graphics.Point; import android.graphics.PorterDuff; @@ -66,6 +71,7 @@ import android.util.LongSparseLongArray; import android.util.Pools.SynchronizedPool; import android.util.Property; import android.util.SparseArray; +import android.util.StateSet; import android.util.SuperNotCalledException; import android.util.TypedValue; import android.view.ContextMenu.ContextMenuInfo; @@ -439,7 +445,7 @@ import java.util.concurrent.atomic.AtomicInteger; * </p> * * <p> - * To intiate a layout, call {@link #requestLayout}. This method is typically + * To initiate a layout, call {@link #requestLayout}. This method is typically * called by a view on itself when it believes that is can no longer fit within * its current bounds. * </p> @@ -1438,140 +1444,87 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ protected static final int[] PRESSED_ENABLED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET; - /** - * The order here is very important to {@link #getDrawableState()} - */ - private static final int[][] VIEW_STATE_SETS; - - static final int VIEW_STATE_WINDOW_FOCUSED = 1; - static final int VIEW_STATE_SELECTED = 1 << 1; - static final int VIEW_STATE_FOCUSED = 1 << 2; - static final int VIEW_STATE_ENABLED = 1 << 3; - static final int VIEW_STATE_PRESSED = 1 << 4; - static final int VIEW_STATE_ACTIVATED = 1 << 5; - static final int VIEW_STATE_ACCELERATED = 1 << 6; - static final int VIEW_STATE_HOVERED = 1 << 7; - static final int VIEW_STATE_DRAG_CAN_ACCEPT = 1 << 8; - static final int VIEW_STATE_DRAG_HOVERED = 1 << 9; - - static final int[] VIEW_STATE_IDS = new int[] { - R.attr.state_window_focused, VIEW_STATE_WINDOW_FOCUSED, - R.attr.state_selected, VIEW_STATE_SELECTED, - R.attr.state_focused, VIEW_STATE_FOCUSED, - R.attr.state_enabled, VIEW_STATE_ENABLED, - R.attr.state_pressed, VIEW_STATE_PRESSED, - R.attr.state_activated, VIEW_STATE_ACTIVATED, - R.attr.state_accelerated, VIEW_STATE_ACCELERATED, - R.attr.state_hovered, VIEW_STATE_HOVERED, - R.attr.state_drag_can_accept, VIEW_STATE_DRAG_CAN_ACCEPT, - R.attr.state_drag_hovered, VIEW_STATE_DRAG_HOVERED - }; - static { - if ((VIEW_STATE_IDS.length/2) != R.styleable.ViewDrawableStates.length) { - throw new IllegalStateException( - "VIEW_STATE_IDs array length does not match ViewDrawableStates style array"); - } - int[] orderedIds = new int[VIEW_STATE_IDS.length]; - for (int i = 0; i < R.styleable.ViewDrawableStates.length; i++) { - int viewState = R.styleable.ViewDrawableStates[i]; - for (int j = 0; j<VIEW_STATE_IDS.length; j += 2) { - if (VIEW_STATE_IDS[j] == viewState) { - orderedIds[i * 2] = viewState; - orderedIds[i * 2 + 1] = VIEW_STATE_IDS[j + 1]; - } - } - } - final int NUM_BITS = VIEW_STATE_IDS.length / 2; - VIEW_STATE_SETS = new int[1 << NUM_BITS][]; - for (int i = 0; i < VIEW_STATE_SETS.length; i++) { - int numBits = Integer.bitCount(i); - int[] set = new int[numBits]; - int pos = 0; - for (int j = 0; j < orderedIds.length; j += 2) { - if ((i & orderedIds[j+1]) != 0) { - set[pos++] = orderedIds[j]; - } - } - VIEW_STATE_SETS[i] = set; - } - - EMPTY_STATE_SET = VIEW_STATE_SETS[0]; - WINDOW_FOCUSED_STATE_SET = VIEW_STATE_SETS[VIEW_STATE_WINDOW_FOCUSED]; - SELECTED_STATE_SET = VIEW_STATE_SETS[VIEW_STATE_SELECTED]; - SELECTED_WINDOW_FOCUSED_STATE_SET = VIEW_STATE_SETS[ - VIEW_STATE_WINDOW_FOCUSED | VIEW_STATE_SELECTED]; - FOCUSED_STATE_SET = VIEW_STATE_SETS[VIEW_STATE_FOCUSED]; - FOCUSED_WINDOW_FOCUSED_STATE_SET = VIEW_STATE_SETS[ - VIEW_STATE_WINDOW_FOCUSED | VIEW_STATE_FOCUSED]; - FOCUSED_SELECTED_STATE_SET = VIEW_STATE_SETS[ - VIEW_STATE_SELECTED | VIEW_STATE_FOCUSED]; - FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET = VIEW_STATE_SETS[ - VIEW_STATE_WINDOW_FOCUSED | VIEW_STATE_SELECTED - | VIEW_STATE_FOCUSED]; - ENABLED_STATE_SET = VIEW_STATE_SETS[VIEW_STATE_ENABLED]; - ENABLED_WINDOW_FOCUSED_STATE_SET = VIEW_STATE_SETS[ - VIEW_STATE_WINDOW_FOCUSED | VIEW_STATE_ENABLED]; - ENABLED_SELECTED_STATE_SET = VIEW_STATE_SETS[ - VIEW_STATE_SELECTED | VIEW_STATE_ENABLED]; - ENABLED_SELECTED_WINDOW_FOCUSED_STATE_SET = VIEW_STATE_SETS[ - VIEW_STATE_WINDOW_FOCUSED | VIEW_STATE_SELECTED - | VIEW_STATE_ENABLED]; - ENABLED_FOCUSED_STATE_SET = VIEW_STATE_SETS[ - VIEW_STATE_FOCUSED | VIEW_STATE_ENABLED]; - ENABLED_FOCUSED_WINDOW_FOCUSED_STATE_SET = VIEW_STATE_SETS[ - VIEW_STATE_WINDOW_FOCUSED | VIEW_STATE_FOCUSED - | VIEW_STATE_ENABLED]; - ENABLED_FOCUSED_SELECTED_STATE_SET = VIEW_STATE_SETS[ - VIEW_STATE_SELECTED | VIEW_STATE_FOCUSED - | VIEW_STATE_ENABLED]; - ENABLED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET = VIEW_STATE_SETS[ - VIEW_STATE_WINDOW_FOCUSED | VIEW_STATE_SELECTED - | VIEW_STATE_FOCUSED| VIEW_STATE_ENABLED]; - - PRESSED_STATE_SET = VIEW_STATE_SETS[VIEW_STATE_PRESSED]; - PRESSED_WINDOW_FOCUSED_STATE_SET = VIEW_STATE_SETS[ - VIEW_STATE_WINDOW_FOCUSED | VIEW_STATE_PRESSED]; - PRESSED_SELECTED_STATE_SET = VIEW_STATE_SETS[ - VIEW_STATE_SELECTED | VIEW_STATE_PRESSED]; - PRESSED_SELECTED_WINDOW_FOCUSED_STATE_SET = VIEW_STATE_SETS[ - VIEW_STATE_WINDOW_FOCUSED | VIEW_STATE_SELECTED - | VIEW_STATE_PRESSED]; - PRESSED_FOCUSED_STATE_SET = VIEW_STATE_SETS[ - VIEW_STATE_FOCUSED | VIEW_STATE_PRESSED]; - PRESSED_FOCUSED_WINDOW_FOCUSED_STATE_SET = VIEW_STATE_SETS[ - VIEW_STATE_WINDOW_FOCUSED | VIEW_STATE_FOCUSED - | VIEW_STATE_PRESSED]; - PRESSED_FOCUSED_SELECTED_STATE_SET = VIEW_STATE_SETS[ - VIEW_STATE_SELECTED | VIEW_STATE_FOCUSED - | VIEW_STATE_PRESSED]; - PRESSED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET = VIEW_STATE_SETS[ - VIEW_STATE_WINDOW_FOCUSED | VIEW_STATE_SELECTED - | VIEW_STATE_FOCUSED | VIEW_STATE_PRESSED]; - PRESSED_ENABLED_STATE_SET = VIEW_STATE_SETS[ - VIEW_STATE_ENABLED | VIEW_STATE_PRESSED]; - PRESSED_ENABLED_WINDOW_FOCUSED_STATE_SET = VIEW_STATE_SETS[ - VIEW_STATE_WINDOW_FOCUSED | VIEW_STATE_ENABLED - | VIEW_STATE_PRESSED]; - PRESSED_ENABLED_SELECTED_STATE_SET = VIEW_STATE_SETS[ - VIEW_STATE_SELECTED | VIEW_STATE_ENABLED - | VIEW_STATE_PRESSED]; - PRESSED_ENABLED_SELECTED_WINDOW_FOCUSED_STATE_SET = VIEW_STATE_SETS[ - VIEW_STATE_WINDOW_FOCUSED | VIEW_STATE_SELECTED - | VIEW_STATE_ENABLED | VIEW_STATE_PRESSED]; - PRESSED_ENABLED_FOCUSED_STATE_SET = VIEW_STATE_SETS[ - VIEW_STATE_FOCUSED | VIEW_STATE_ENABLED - | VIEW_STATE_PRESSED]; - PRESSED_ENABLED_FOCUSED_WINDOW_FOCUSED_STATE_SET = VIEW_STATE_SETS[ - VIEW_STATE_WINDOW_FOCUSED | VIEW_STATE_FOCUSED - | VIEW_STATE_ENABLED | VIEW_STATE_PRESSED]; - PRESSED_ENABLED_FOCUSED_SELECTED_STATE_SET = VIEW_STATE_SETS[ - VIEW_STATE_SELECTED | VIEW_STATE_FOCUSED - | VIEW_STATE_ENABLED | VIEW_STATE_PRESSED]; - PRESSED_ENABLED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET = VIEW_STATE_SETS[ - VIEW_STATE_WINDOW_FOCUSED | VIEW_STATE_SELECTED - | VIEW_STATE_FOCUSED| VIEW_STATE_ENABLED - | VIEW_STATE_PRESSED]; + EMPTY_STATE_SET = StateSet.get(0); + + WINDOW_FOCUSED_STATE_SET = StateSet.get(StateSet.VIEW_STATE_WINDOW_FOCUSED); + + SELECTED_STATE_SET = StateSet.get(StateSet.VIEW_STATE_SELECTED); + SELECTED_WINDOW_FOCUSED_STATE_SET = StateSet.get( + StateSet.VIEW_STATE_WINDOW_FOCUSED | StateSet.VIEW_STATE_SELECTED); + + FOCUSED_STATE_SET = StateSet.get(StateSet.VIEW_STATE_FOCUSED); + FOCUSED_WINDOW_FOCUSED_STATE_SET = StateSet.get( + StateSet.VIEW_STATE_WINDOW_FOCUSED | StateSet.VIEW_STATE_FOCUSED); + FOCUSED_SELECTED_STATE_SET = StateSet.get( + StateSet.VIEW_STATE_SELECTED | StateSet.VIEW_STATE_FOCUSED); + FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET = StateSet.get( + StateSet.VIEW_STATE_WINDOW_FOCUSED | StateSet.VIEW_STATE_SELECTED + | StateSet.VIEW_STATE_FOCUSED); + + ENABLED_STATE_SET = StateSet.get(StateSet.VIEW_STATE_ENABLED); + ENABLED_WINDOW_FOCUSED_STATE_SET = StateSet.get( + StateSet.VIEW_STATE_WINDOW_FOCUSED | StateSet.VIEW_STATE_ENABLED); + ENABLED_SELECTED_STATE_SET = StateSet.get( + StateSet.VIEW_STATE_SELECTED | StateSet.VIEW_STATE_ENABLED); + ENABLED_SELECTED_WINDOW_FOCUSED_STATE_SET = StateSet.get( + StateSet.VIEW_STATE_WINDOW_FOCUSED | StateSet.VIEW_STATE_SELECTED + | StateSet.VIEW_STATE_ENABLED); + ENABLED_FOCUSED_STATE_SET = StateSet.get( + StateSet.VIEW_STATE_FOCUSED | StateSet.VIEW_STATE_ENABLED); + ENABLED_FOCUSED_WINDOW_FOCUSED_STATE_SET = StateSet.get( + StateSet.VIEW_STATE_WINDOW_FOCUSED | StateSet.VIEW_STATE_FOCUSED + | StateSet.VIEW_STATE_ENABLED); + ENABLED_FOCUSED_SELECTED_STATE_SET = StateSet.get( + StateSet.VIEW_STATE_SELECTED | StateSet.VIEW_STATE_FOCUSED + | StateSet.VIEW_STATE_ENABLED); + ENABLED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET = StateSet.get( + StateSet.VIEW_STATE_WINDOW_FOCUSED | StateSet.VIEW_STATE_SELECTED + | StateSet.VIEW_STATE_FOCUSED| StateSet.VIEW_STATE_ENABLED); + + PRESSED_STATE_SET = StateSet.get(StateSet.VIEW_STATE_PRESSED); + PRESSED_WINDOW_FOCUSED_STATE_SET = StateSet.get( + StateSet.VIEW_STATE_WINDOW_FOCUSED | StateSet.VIEW_STATE_PRESSED); + PRESSED_SELECTED_STATE_SET = StateSet.get( + StateSet.VIEW_STATE_SELECTED | StateSet.VIEW_STATE_PRESSED); + PRESSED_SELECTED_WINDOW_FOCUSED_STATE_SET = StateSet.get( + StateSet.VIEW_STATE_WINDOW_FOCUSED | StateSet.VIEW_STATE_SELECTED + | StateSet.VIEW_STATE_PRESSED); + PRESSED_FOCUSED_STATE_SET = StateSet.get( + StateSet.VIEW_STATE_FOCUSED | StateSet.VIEW_STATE_PRESSED); + PRESSED_FOCUSED_WINDOW_FOCUSED_STATE_SET = StateSet.get( + StateSet.VIEW_STATE_WINDOW_FOCUSED | StateSet.VIEW_STATE_FOCUSED + | StateSet.VIEW_STATE_PRESSED); + PRESSED_FOCUSED_SELECTED_STATE_SET = StateSet.get( + StateSet.VIEW_STATE_SELECTED | StateSet.VIEW_STATE_FOCUSED + | StateSet.VIEW_STATE_PRESSED); + PRESSED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET = StateSet.get( + StateSet.VIEW_STATE_WINDOW_FOCUSED | StateSet.VIEW_STATE_SELECTED + | StateSet.VIEW_STATE_FOCUSED | StateSet.VIEW_STATE_PRESSED); + PRESSED_ENABLED_STATE_SET = StateSet.get( + StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_PRESSED); + PRESSED_ENABLED_WINDOW_FOCUSED_STATE_SET = StateSet.get( + StateSet.VIEW_STATE_WINDOW_FOCUSED | StateSet.VIEW_STATE_ENABLED + | StateSet.VIEW_STATE_PRESSED); + PRESSED_ENABLED_SELECTED_STATE_SET = StateSet.get( + StateSet.VIEW_STATE_SELECTED | StateSet.VIEW_STATE_ENABLED + | StateSet.VIEW_STATE_PRESSED); + PRESSED_ENABLED_SELECTED_WINDOW_FOCUSED_STATE_SET = StateSet.get( + StateSet.VIEW_STATE_WINDOW_FOCUSED | StateSet.VIEW_STATE_SELECTED + | StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_PRESSED); + PRESSED_ENABLED_FOCUSED_STATE_SET = StateSet.get( + StateSet.VIEW_STATE_FOCUSED | StateSet.VIEW_STATE_ENABLED + | StateSet.VIEW_STATE_PRESSED); + PRESSED_ENABLED_FOCUSED_WINDOW_FOCUSED_STATE_SET = StateSet.get( + StateSet.VIEW_STATE_WINDOW_FOCUSED | StateSet.VIEW_STATE_FOCUSED + | StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_PRESSED); + PRESSED_ENABLED_FOCUSED_SELECTED_STATE_SET = StateSet.get( + StateSet.VIEW_STATE_SELECTED | StateSet.VIEW_STATE_FOCUSED + | StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_PRESSED); + PRESSED_ENABLED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET = StateSet.get( + StateSet.VIEW_STATE_WINDOW_FOCUSED | StateSet.VIEW_STATE_SELECTED + | StateSet.VIEW_STATE_FOCUSED| StateSet.VIEW_STATE_ENABLED + | StateSet.VIEW_STATE_PRESSED); } /** @@ -1645,6 +1598,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #setId(int) * @see #getId() */ + @IdRes @ViewDebug.ExportedProperty(resolveId = true) int mID = NO_ID; @@ -1977,7 +1931,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, static final int LAYOUT_DIRECTION_RESOLVED_DEFAULT = LAYOUT_DIRECTION_LTR; /** - * Text direction is inherited thru {@link ViewGroup} + * Text direction is inherited through {@link ViewGroup} */ public static final int TEXT_DIRECTION_INHERIT = 0; @@ -2545,7 +2499,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, /** * Flag for {@link #setSystemUiVisibility(int)}: View would like its window - * to be layed out as if it has requested + * to be laid out as if it has requested * {@link #SYSTEM_UI_FLAG_HIDE_NAVIGATION}, even if it currently hasn't. This * allows it to avoid artifacts when switching in and out of that mode, at * the expense that some of its user interface may be covered by screen @@ -2557,7 +2511,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, /** * Flag for {@link #setSystemUiVisibility(int)}: View would like its window - * to be layed out as if it has requested + * to be laid out as if it has requested * {@link #SYSTEM_UI_FLAG_FULLSCREEN}, even if it currently hasn't. This * allows it to avoid artifacts when switching in and out of that mode, at * the expense that some of its user interface may be covered by screen @@ -2596,6 +2550,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback, public static final int SYSTEM_UI_FLAG_IMMERSIVE_STICKY = 0x00001000; /** + * Flag for {@link #setSystemUiVisibility(int)}: Requests the status bar to draw in a mode that + * is compatible with light status bar backgrounds. + * + * <p>For this to take effect, the window must request + * {@link android.view.WindowManager.LayoutParams#FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS + * FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS} but not + * {@link android.view.WindowManager.LayoutParams#FLAG_TRANSLUCENT_STATUS + * FLAG_TRANSLUCENT_STATUS}. + * + * @see android.R.attr#windowHasLightStatusBar + */ + public static final int SYSTEM_UI_FLAG_LIGHT_STATUS_BAR = 0x00002000; + + /** * @deprecated Use {@link #SYSTEM_UI_FLAG_LOW_PROFILE} instead. */ public static final int STATUS_BAR_HIDDEN = SYSTEM_UI_FLAG_LOW_PROFILE; @@ -3620,7 +3588,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param attrs The attributes of the XML tag that is inflating the view. * @see #View(Context, AttributeSet, int) */ - public View(Context context, AttributeSet attrs) { + public View(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } @@ -3641,7 +3609,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * the view. Can be 0 to not look for defaults. * @see #View(Context, AttributeSet) */ - public View(Context context, AttributeSet attrs, int defStyleAttr) { + public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } @@ -3678,7 +3646,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * to not look for defaults. * @see #View(Context, AttributeSet, int) */ - public View(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { this(context); final TypedArray a = context.obtainStyledAttributes( @@ -4508,6 +4476,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (scrollabilityCache.scrollBar == null) { scrollabilityCache.scrollBar = new ScrollBarDrawable(); + scrollabilityCache.scrollBar.setCallback(this); + scrollabilityCache.scrollBar.setState(getDrawableState()); } final boolean fadeScrollbars = a.getBoolean(R.styleable.View_fadeScrollbars, true); @@ -4619,11 +4589,18 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * Register a callback to be invoked when the scroll position of this view - * changed. + * Register a callback to be invoked when the scroll X or Y positions of + * this view change. + * <p> + * <b>Note:</b> Some views handle scrolling independently from View and may + * have their own separate listeners for scroll-type events. For example, + * {@link android.widget.ListView ListView} allows clients to register an + * {@link android.widget.ListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener) AbsListView.OnScrollListener} + * to listen for changes in list scroll position. * - * @param l The callback that will run. - * @hide Only used internally. + * @param l The listener to notify when the scroll X or Y position changes. + * @see android.view.View#getScrollX() + * @see android.view.View#getScrollY() */ public void setOnScrollChangeListener(OnScrollChangeListener l) { getListenerInfo().mOnScrollChangeListener = l; @@ -4719,7 +4696,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @see #setClickable(boolean) */ - public void setOnClickListener(OnClickListener l) { + public void setOnClickListener(@Nullable OnClickListener l) { if (!isClickable()) { setClickable(true); } @@ -4743,7 +4720,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @see #setLongClickable(boolean) */ - public void setOnLongClickListener(OnLongClickListener l) { + public void setOnLongClickListener(@Nullable OnLongClickListener l) { if (!isLongClickable()) { setLongClickable(true); } @@ -4868,17 +4845,36 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * Start an action mode. + * Start an action mode with the default type {@link ActionMode#TYPE_PRIMARY}. * * @param callback Callback that will control the lifecycle of the action mode * @return The new action mode if it is started, null otherwise * * @see ActionMode + * @see #startActionMode(android.view.ActionMode.Callback, int) */ public ActionMode startActionMode(ActionMode.Callback callback) { + return startActionMode(callback, ActionMode.TYPE_PRIMARY); + } + + /** + * Start an action mode with the given type. + * + * @param callback Callback that will control the lifecycle of the action mode + * @param type One of {@link ActionMode#TYPE_PRIMARY} or {@link ActionMode#TYPE_FLOATING}. + * @return The new action mode if it is started, null otherwise + * + * @see ActionMode + */ + public ActionMode startActionMode(ActionMode.Callback callback, int type) { ViewParent parent = getParent(); if (parent == null) return null; - return parent.startActionModeForChild(this, callback); + try { + return parent.startActionModeForChild(this, callback, type); + } catch (AbstractMethodError ame) { + // Older implementations of custom views might not implement this. + return parent.startActionModeForChild(this, callback); + } } /** @@ -5123,7 +5119,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * Returns true if this view has focus iteself, or is the ancestor of the + * Returns true if this view has focus itself, or is the ancestor of the * view that has focus. * * @return True if this view has or contains focus, false otherwise. @@ -5176,6 +5172,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * passed in as finer grained information about where the focus is coming * from (in addition to direction). Will be <code>null</code> otherwise. */ + @CallSuper protected void onFocusChanged(boolean gainFocus, @FocusDirection int direction, @Nullable Rect previouslyFocusedRect) { if (gainFocus) { @@ -5220,7 +5217,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * populate the text content of the event source including its descendants, * and last calls * {@link ViewParent#requestSendAccessibilityEvent(View, AccessibilityEvent)} - * on its parent to resuest sending of the event to interested parties. + * on its parent to request sending of the event to interested parties. * <p> * If an {@link AccessibilityDelegate} has been specified via calling * {@link #setAccessibilityDelegate(AccessibilityDelegate)} its @@ -5270,8 +5267,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #sendAccessibilityEvent(int) * * Note: Called from the default {@link AccessibilityDelegate}. + * + * @hide */ - void sendAccessibilityEventInternal(int eventType) { + public void sendAccessibilityEventInternal(int eventType) { if (AccessibilityManager.getInstance(mContext).isEnabled()) { sendAccessibilityEventUnchecked(AccessibilityEvent.obtain(eventType)); } @@ -5304,8 +5303,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #sendAccessibilityEventUnchecked(AccessibilityEvent) * * Note: Called from the default {@link AccessibilityDelegate}. + * + * @hide */ - void sendAccessibilityEventUncheckedInternal(AccessibilityEvent event) { + public void sendAccessibilityEventUncheckedInternal(AccessibilityEvent event) { if (!isShown()) { return; } @@ -5355,8 +5356,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #dispatchPopulateAccessibilityEvent(AccessibilityEvent) * * Note: Called from the default {@link AccessibilityDelegate}. + * + * @hide */ - boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) { + public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) { onPopulateAccessibilityEvent(event); return false; } @@ -5392,6 +5395,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #sendAccessibilityEvent(int) * @see #dispatchPopulateAccessibilityEvent(AccessibilityEvent) */ + @CallSuper public void onPopulateAccessibilityEvent(AccessibilityEvent event) { if (mAccessibilityDelegate != null) { mAccessibilityDelegate.onPopulateAccessibilityEvent(this, event); @@ -5404,8 +5408,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #onPopulateAccessibilityEvent(AccessibilityEvent) * * Note: Called from the default {@link AccessibilityDelegate}. + * + * @hide */ - void onPopulateAccessibilityEventInternal(AccessibilityEvent event) { + public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) { } /** @@ -5434,6 +5440,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #sendAccessibilityEvent(int) * @see #dispatchPopulateAccessibilityEvent(AccessibilityEvent) */ + @CallSuper public void onInitializeAccessibilityEvent(AccessibilityEvent event) { if (mAccessibilityDelegate != null) { mAccessibilityDelegate.onInitializeAccessibilityEvent(this, event); @@ -5446,10 +5453,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #onInitializeAccessibilityEvent(AccessibilityEvent) * * Note: Called from the default {@link AccessibilityDelegate}. + * + * @hide */ - void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { + public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { event.setSource(this); - event.setClassName(View.class.getName()); + event.setClassName(getAccessibilityClassName()); event.setPackageName(getContext().getPackageName()); event.setEnabled(isEnabled()); event.setContentDescription(mContentDescription); @@ -5502,8 +5511,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, /** * @see #createAccessibilityNodeInfo() + * + * @hide */ - AccessibilityNodeInfo createAccessibilityNodeInfoInternal() { + public AccessibilityNodeInfo createAccessibilityNodeInfoInternal() { AccessibilityNodeProvider provider = getAccessibilityNodeProvider(); if (provider != null) { return provider.createAccessibilityNodeInfo(View.NO_ID); @@ -5544,6 +5555,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @param info The instance to initialize. */ + @CallSuper public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { if (mAccessibilityDelegate != null) { mAccessibilityDelegate.onInitializeAccessibilityNodeInfo(this, info); @@ -5617,11 +5629,33 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Return the class name of this object to be used for accessibility purposes. + * Subclasses should only override this if they are implementing something that + * should be seen as a completely new class of view when used by accessibility, + * unrelated to the class it is deriving from. This is used to fill in + * {@link AccessibilityNodeInfo#setClassName AccessibilityNodeInfo.setClassName}. + */ + public CharSequence getAccessibilityClassName() { + return View.class.getName(); + } + + /** + * Called when assist structure is being retrieved from a view as part of + * {@link android.app.Activity#onProvideAssistData Activity.onProvideAssistData}. + * @param structure Additional standard structured view structure to supply. + * @param extras Non-standard extensions. + */ + public void onProvideAssistStructure(ViewAssistStructure structure, Bundle extras) { + } + + /** * @see #onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo) * * Note: Called from the default {@link AccessibilityDelegate}. + * + * @hide */ - void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { + public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { Rect bounds = mAttachInfo.mTmpInvalRect; getDrawingRect(bounds); @@ -5696,7 +5730,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, info.setVisibleToUser(isVisibleToUser()); info.setPackageName(mContext.getPackageName()); - info.setClassName(View.class.getName()); + info.setClassName(getAccessibilityClassName()); info.setContentDescription(getContentDescription()); info.setEnabled(isEnabled()); @@ -5843,7 +5877,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @see AccessibilityDelegate */ - public void setAccessibilityDelegate(AccessibilityDelegate delegate) { + public void setAccessibilityDelegate(@Nullable AccessibilityDelegate delegate) { mAccessibilityDelegate = delegate; } @@ -6057,7 +6091,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param id The labeled view id. */ @RemotableViewMethod - public void setLabelFor(int id) { + public void setLabelFor(@IdRes int id) { if (mLabelForId == id) { return; } @@ -6083,6 +6117,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @hide pending API council approval */ + @CallSuper protected void onFocusLost() { resetPressedState(); } @@ -6549,6 +6584,19 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Provide original WindowInsets that are dispatched to the view hierarchy. The insets are + * only available if the view is attached. + * + * @return WindowInsets from the top of the view hierarchy or null if View is detached + */ + public WindowInsets getRootWindowInsets() { + if (mAttachInfo != null) { + return mAttachInfo.mViewRootImpl.getWindowInsets(false /* forceConstruct */); + } + return null; + } + + /** * @hide Compute the insets that should be consumed by this view and the ones * that should propagate to those under it. */ @@ -6691,7 +6739,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @RemotableViewMethod public void setVisibility(@Visibility int visibility) { setFlags(visibility, VISIBILITY_MASK); - if (mBackground != null) mBackground.setVisible(visibility == VISIBLE, false); } /** @@ -7394,8 +7441,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * including this view if it is focusable itself) to views. This method * adds all focusable views regardless if we are in touch mode or * only views focusable in touch mode if we are in touch mode or - * only views that can take accessibility focus if accessibility is enabeld - * depending on the focusable mode paramater. + * only views that can take accessibility focus if accessibility is enabled + * depending on the focusable mode parameter. * * @param views Focusable views found so far or null if all we are interested is * the number of focusables. @@ -7681,7 +7728,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, /** * Call this to try to give focus to a specific view or to one of its descendants. This is a - * special variant of {@link #requestFocus() } that will allow views that are not focuable in + * special variant of {@link #requestFocus() } that will allow views that are not focusable in * touch mode to request focus when they are touched. * * @return Whether this view or one of its descendants actually took focus. @@ -8083,7 +8130,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * Note: Called from the default {@link AccessibilityDelegate}. * - * @hide Until we've refactored all accessibility delegation methods. + * @hide */ public boolean performAccessibilityActionInternal(int action, Bundle arguments) { if (isNestedScrollingEnabled() @@ -8732,20 +8779,28 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * Called when the visibility of the view or an ancestor of the view is changed. - * @param changedView The view whose visibility changed. Could be 'this' or - * an ancestor view. - * @param visibility The new visibility of changedView: {@link #VISIBLE}, - * {@link #INVISIBLE} or {@link #GONE}. + * Called when the visibility of the view or an ancestor of the view has + * changed. + * + * @param changedView The view whose visibility changed. May be + * {@code this} or an ancestor view. + * @param visibility The new visibility, one of {@link #VISIBLE}, + * {@link #INVISIBLE} or {@link #GONE}. */ protected void onVisibilityChanged(@NonNull View changedView, @Visibility int visibility) { - if (visibility == VISIBLE) { + final boolean visible = visibility == VISIBLE && getVisibility() == VISIBLE; + if (visible) { if (mAttachInfo != null) { initialAwakenScrollBars(); } else { mPrivateFlags |= PFLAG_AWAKEN_SCROLL_BARS_ON_ATTACH; } } + + final Drawable dr = mBackground; + if (dr != null && visible != dr.isVisible()) { + dr.setVisible(visible, false); + } } /** @@ -8868,7 +8923,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * Called when the current configuration of the resources being used * by the application have changed. You can use this to decide when * to reload resources that can changed based on orientation and other - * configuration characterstics. You only need to use this if you are + * configuration characteristics. You only need to use this if you are * not relying on the normal {@link android.app.Activity} mechanism of * recreating the activity instance upon a configuration change. * @@ -9875,9 +9930,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback, /** * Interface definition for a callback to be invoked when the scroll - * position of a view changes. + * X or Y positions of a view change. + * <p> + * <b>Note:</b> Some views handle scrolling independently from View and may + * have their own separate listeners for scroll-type events. For example, + * {@link android.widget.ListView ListView} allows clients to register an + * {@link android.widget.ListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener) AbsListView.OnScrollListener} + * to listen for changes in list scroll position. * - * @hide Only used internally. + * @see #setOnScrollChangeListener(View.OnScrollChangeListener) */ public interface OnScrollChangeListener { /** @@ -10046,6 +10107,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @return The measured width of this view as a bit mask. */ + @ViewDebug.ExportedProperty(category = "measurement", flagMapping = { + @ViewDebug.FlagToString(mask = MEASURED_STATE_MASK, equals = MEASURED_STATE_TOO_SMALL, + name = "MEASURED_STATE_TOO_SMALL"), + }) public final int getMeasuredWidthAndState() { return mMeasuredWidth; } @@ -10070,6 +10135,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @return The measured width of this view as a bit mask. */ + @ViewDebug.ExportedProperty(category = "measurement", flagMapping = { + @ViewDebug.FlagToString(mask = MEASURED_STATE_MASK, equals = MEASURED_STATE_TOO_SMALL, + name = "MEASURED_STATE_TOO_SMALL"), + }) public final int getMeasuredHeightAndState() { return mMeasuredHeight; } @@ -10537,7 +10606,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * <p>Note that if the view is backed by a * {@link #setLayerType(int, android.graphics.Paint) layer} and is associated with a * {@link #setLayerPaint(android.graphics.Paint) layer paint}, setting an alpha value less than - * 1.0 will supercede the alpha of the layer paint.</p> + * 1.0 will supersede the alpha of the layer paint.</p> * * @param alpha The opacity of the view. * @@ -10546,7 +10615,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @attr ref android.R.styleable#View_alpha */ - public void setAlpha(float alpha) { + public void setAlpha(@FloatRange(from=0.0, to=1.0) float alpha) { ensureTransformationInfo(); if (mTransformationInfo.mAlpha != alpha) { mTransformationInfo.mAlpha = alpha; @@ -11585,7 +11654,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * </p> * * <p> - * This method should be invoked everytime a subclass directly updates the + * This method should be invoked every time a subclass directly updates the * scroll parameters. * </p> * @@ -11624,7 +11693,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * </p> * * <p> - * This method should be invoked everytime a subclass directly updates the + * This method should be invoked every time a subclass directly updates the * scroll parameters. * </p> * @@ -11632,7 +11701,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * should start; when the delay is 0, the animation starts * immediately * - * @param invalidate Wheter this method should call invalidate + * @param invalidate Whether this method should call invalidate * * @return true if the animation is played, false otherwise * @@ -11652,6 +11721,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (scrollCache.scrollBar == null) { scrollCache.scrollBar = new ScrollBarDrawable(); + scrollCache.scrollBar.setCallback(this); + scrollCache.scrollBar.setState(getDrawableState()); } if (isHorizontalScrollBarEnabled() || isVerticalScrollBarEnabled()) { @@ -12537,7 +12608,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, /** * Define whether scrollbars will fade when the view is not scrolling. * - * @param fadeScrollbars wheter to enable fading + * @param fadeScrollbars whether to enable fading * * @attr ref android.R.styleable#View_fadeScrollbars */ @@ -13051,6 +13122,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @see #onDetachedFromWindow() */ + @CallSuper protected void onAttachedToWindow() { if ((mPrivateFlags & PFLAG_REQUEST_TRANSPARENT_REGIONS) != 0) { mParent.requestTransparentRegion(this); @@ -13072,7 +13144,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (isFocused()) { InputMethodManager imm = InputMethodManager.peekInstance(); - imm.focusIn(this); + if (imm != null) { + imm.focusIn(this); + } } } @@ -13381,6 +13455,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @see #onAttachedToWindow() */ + @CallSuper protected void onDetachedFromWindow() { } @@ -13389,11 +13464,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * after onDetachedFromWindow(). * * If you override this you *MUST* call super.onDetachedFromWindowInternal()! - * The super method should be called at the end of the overriden method to ensure + * The super method should be called at the end of the overridden method to ensure * subclasses are destroyed first * * @hide */ + @CallSuper protected void onDetachedFromWindowInternal() { mPrivateFlags &= ~PFLAG_CANCEL_NEXT_UP_EVENT; mPrivateFlags3 &= ~PFLAG3_IS_LAID_OUT; @@ -13700,6 +13776,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #dispatchSaveInstanceState(android.util.SparseArray) * @see #setSaveEnabled(boolean) */ + @CallSuper protected Parcelable onSaveInstanceState() { mPrivateFlags |= PFLAG_SAVE_STATE_CALLED; return BaseSavedState.EMPTY_STATE; @@ -13758,6 +13835,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #restoreHierarchyState(android.util.SparseArray) * @see #dispatchRestoreInstanceState(android.util.SparseArray) */ + @CallSuper protected void onRestoreInstanceState(Parcelable state) { mPrivateFlags |= PFLAG_SAVE_STATE_CALLED; if (state != BaseSavedState.EMPTY_STATE && state != null) { @@ -13790,7 +13868,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * <p>Note: if this view's parent addStateFromChildren property is enabled and this * property is enabled, an exception will be thrown.</p> * - * <p>Note: if the child view uses and updates additionnal states which are unknown to the + * <p>Note: if the child view uses and updates additional states which are unknown to the * parent, these states should not be affected by this method.</p> * * @param enabled True to enable duplication of the parent's drawable state, false @@ -13831,7 +13909,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * </ul> * * <p>If this view has an alpha value set to < 1.0 by calling - * {@link #setAlpha(float)}, the alpha value of the layer's paint is superceded + * {@link #setAlpha(float)}, the alpha value of the layer's paint is superseded * by this view's alpha value.</p> * * <p>Refer to the documentation of {@link #LAYER_TYPE_NONE}, @@ -13899,7 +13977,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * </ul> * * <p>If this view has an alpha value set to < 1.0 by calling {@link #setAlpha(float)}, the - * alpha value of the layer's paint is superceded by this view's alpha value.</p> + * alpha value of the layer's paint is superseded by this view's alpha value.</p> * * @param paint The paint used to compose the layer. This argument is optional * and can be null. It is ignored when the layer type is @@ -14008,6 +14086,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @hide */ + @CallSuper protected void destroyHardwareResources() { // Although the Layer will be destroyed by RenderNode, we want to release // the staging display list, which is also a signal to RenderNode that it's @@ -14137,7 +14216,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, int height = mBottom - mTop; int layerType = getLayerType(); - final HardwareCanvas canvas = renderNode.start(width, height); + final DisplayListCanvas canvas = renderNode.start(width, height); canvas.setHighContrastText(mAttachInfo.mHighContrastText); try { @@ -14280,7 +14359,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #buildDrawingCache() * @see #getDrawingCache() */ - public void setDrawingCacheBackgroundColor(int color) { + public void setDrawingCacheBackgroundColor(@ColorInt int color) { if (color != mDrawingCacheBackgroundColor) { mDrawingCacheBackgroundColor = color; mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID; @@ -14292,6 +14371,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @return The background color to used for the drawing cache's bitmap */ + @ColorInt public int getDrawingCacheBackgroundColor() { return mDrawingCacheBackgroundColor; } @@ -14803,10 +14883,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, void setDisplayListProperties(RenderNode renderNode) { if (renderNode != null) { renderNode.setHasOverlappingRendering(hasOverlappingRendering()); - if (mParent instanceof ViewGroup) { - renderNode.setClipToBounds( - (((ViewGroup) mParent).mGroupFlags & ViewGroup.FLAG_CLIP_CHILDREN) != 0); - } + renderNode.setClipToBounds(mParent instanceof ViewGroup + && ((ViewGroup) mParent).getClipChildren()); + float alpha = 1; if (mParent instanceof ViewGroup && (((ViewGroup) mParent).mGroupFlags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) { @@ -15118,7 +15197,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (layer != null && layer.isValid()) { int restoreAlpha = mLayerPaint.getAlpha(); mLayerPaint.setAlpha((int) (alpha * 255)); - ((HardwareCanvas) canvas).drawHardwareLayer(layer, 0, 0, mLayerPaint); + ((DisplayListCanvas) canvas).drawHardwareLayer(layer, 0, 0, mLayerPaint); mLayerPaint.setAlpha(restoreAlpha); layerRendered = true; } else { @@ -15141,7 +15220,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } } else { mPrivateFlags &= ~PFLAG_DIRTY_MASK; - ((HardwareCanvas) canvas).drawRenderNode(renderNode, null, flags); + ((DisplayListCanvas) canvas).drawRenderNode(renderNode, flags); } } } else if (cache != null) { @@ -15197,6 +15276,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @param canvas The Canvas to which the View is rendered. */ + @CallSuper public void draw(Canvas canvas) { final int privateFlags = mPrivateFlags; final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && @@ -15414,7 +15494,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, final RenderNode renderNode = mBackgroundRenderNode; if (renderNode != null && renderNode.isValid()) { setBackgroundRenderNodeProperties(renderNode); - ((HardwareCanvas) canvas).drawRenderNode(renderNode); + ((DisplayListCanvas) canvas).drawRenderNode(renderNode); return; } } @@ -15451,7 +15531,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, final Rect bounds = drawable.getBounds(); final int width = bounds.width(); final int height = bounds.height(); - final HardwareCanvas canvas = renderNode.start(width, height); + final DisplayListCanvas canvas = renderNode.start(width, height); // Reverse left/top translation done by drawable canvas, which will // instead be applied by rendernode's LTRB bounds below. This way, the @@ -15506,6 +15586,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @return The known solid color background for this view, or 0 if the color may vary */ @ViewDebug.ExportedProperty(category = "drawing") + @ColorInt public int getSolidColor() { return 0; } @@ -15798,6 +15879,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * <p>Even if the subclass overrides onFinishInflate, they should always be * sure to call the super method, so that we get called. */ + @CallSuper protected void onFinishInflate() { } @@ -15963,8 +16045,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #unscheduleDrawable(android.graphics.drawable.Drawable) * @see #drawableStateChanged() */ + @CallSuper protected boolean verifyDrawable(Drawable who) { - return who == mBackground; + return who == mBackground || (mScrollCache != null && mScrollCache.scrollBar == who); } /** @@ -15978,14 +16061,24 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @see Drawable#setState(int[]) */ + @CallSuper protected void drawableStateChanged() { + final int[] state = getDrawableState(); + final Drawable d = mBackground; if (d != null && d.isStateful()) { - d.setState(getDrawableState()); + d.setState(state); + } + + if (mScrollCache != null) { + final Drawable scrollBar = mScrollCache.scrollBar; + if (scrollBar != null && scrollBar.isStateful()) { + scrollBar.setState(state); + } } if (mStateListAnimator != null) { - mStateListAnimator.setState(getDrawableState()); + mStateListAnimator.setState(state); } } @@ -16001,6 +16094,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param x hotspot x coordinate * @param y hotspot y coordinate */ + @CallSuper public void drawableHotspotChanged(float x, float y) { if (mBackground != null) { mBackground.setHotspot(x, y); @@ -16083,26 +16177,30 @@ public class View implements Drawable.Callback, KeyEvent.Callback, int privateFlags = mPrivateFlags; int viewStateIndex = 0; - if ((privateFlags & PFLAG_PRESSED) != 0) viewStateIndex |= VIEW_STATE_PRESSED; - if ((mViewFlags & ENABLED_MASK) == ENABLED) viewStateIndex |= VIEW_STATE_ENABLED; - if (isFocused()) viewStateIndex |= VIEW_STATE_FOCUSED; - if ((privateFlags & PFLAG_SELECTED) != 0) viewStateIndex |= VIEW_STATE_SELECTED; - if (hasWindowFocus()) viewStateIndex |= VIEW_STATE_WINDOW_FOCUSED; - if ((privateFlags & PFLAG_ACTIVATED) != 0) viewStateIndex |= VIEW_STATE_ACTIVATED; + if ((privateFlags & PFLAG_PRESSED) != 0) viewStateIndex |= StateSet.VIEW_STATE_PRESSED; + if ((mViewFlags & ENABLED_MASK) == ENABLED) viewStateIndex |= StateSet.VIEW_STATE_ENABLED; + if (isFocused()) viewStateIndex |= StateSet.VIEW_STATE_FOCUSED; + if ((privateFlags & PFLAG_SELECTED) != 0) viewStateIndex |= StateSet.VIEW_STATE_SELECTED; + if (hasWindowFocus()) viewStateIndex |= StateSet.VIEW_STATE_WINDOW_FOCUSED; + if ((privateFlags & PFLAG_ACTIVATED) != 0) viewStateIndex |= StateSet.VIEW_STATE_ACTIVATED; if (mAttachInfo != null && mAttachInfo.mHardwareAccelerationRequested && HardwareRenderer.isAvailable()) { // This is set if HW acceleration is requested, even if the current // process doesn't allow it. This is just to allow app preview // windows to better match their app. - viewStateIndex |= VIEW_STATE_ACCELERATED; + viewStateIndex |= StateSet.VIEW_STATE_ACCELERATED; } - if ((privateFlags & PFLAG_HOVERED) != 0) viewStateIndex |= VIEW_STATE_HOVERED; + if ((privateFlags & PFLAG_HOVERED) != 0) viewStateIndex |= StateSet.VIEW_STATE_HOVERED; final int privateFlags2 = mPrivateFlags2; - if ((privateFlags2 & PFLAG2_DRAG_CAN_ACCEPT) != 0) viewStateIndex |= VIEW_STATE_DRAG_CAN_ACCEPT; - if ((privateFlags2 & PFLAG2_DRAG_HOVERED) != 0) viewStateIndex |= VIEW_STATE_DRAG_HOVERED; + if ((privateFlags2 & PFLAG2_DRAG_CAN_ACCEPT) != 0) { + viewStateIndex |= StateSet.VIEW_STATE_DRAG_CAN_ACCEPT; + } + if ((privateFlags2 & PFLAG2_DRAG_HOVERED) != 0) { + viewStateIndex |= StateSet.VIEW_STATE_DRAG_HOVERED; + } - drawableState = VIEW_STATE_SETS[viewStateIndex]; + drawableState = StateSet.get(viewStateIndex); //noinspection ConstantIfStatement if (false) { @@ -16179,7 +16277,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param color the color of the background */ @RemotableViewMethod - public void setBackgroundColor(int color) { + public void setBackgroundColor(@ColorInt int color) { if (mBackground instanceof ColorDrawable) { ((ColorDrawable) mBackground.mutate()).setColor(color); computeOpaqueFlags(); @@ -16190,6 +16288,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * If the view has a ColorDrawable background, returns the color of that + * drawable. + * + * @return The color of the ColorDrawable background, if set, otherwise 0. + */ + @ColorInt + public int getBackgroundColor() { + if (mBackground instanceof ColorDrawable) { + return ((ColorDrawable) mBackground).getColor(); + } + return 0; + } + + /** * Set the background to a given resource. The resource should refer to * a Drawable object or 0 to remove the background. * @param resid The identifier of the resource. @@ -16197,7 +16309,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @attr ref android.R.styleable#View_background */ @RemotableViewMethod - public void setBackgroundResource(int resid) { + public void setBackgroundResource(@DrawableRes int resid) { if (resid != 0 && resid == mBackgroundResource) { return; } @@ -16652,8 +16764,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * Return if the padding as been set thru relative values - * {@link #setPaddingRelative(int, int, int, int)} or thru + * Return if the padding has been set through relative values + * {@link #setPaddingRelative(int, int, int, int)} or through * @attr ref android.R.styleable#View_paddingStart or * @attr ref android.R.styleable#View_paddingEnd * @@ -16955,7 +17067,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @param location an array of two integers in which to hold the coordinates */ - public void getLocationOnScreen(int[] location) { + public void getLocationOnScreen(@Size(2) int[] location) { getLocationInWindow(location); final AttachInfo info = mAttachInfo; @@ -16972,7 +17084,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @param location an array of two integers in which to hold the coordinates */ - public void getLocationInWindow(int[] location) { + public void getLocationInWindow(@Size(2) int[] location) { if (location == null || location.length < 2) { throw new IllegalArgumentException("location must be an array of two integers"); } @@ -17025,7 +17137,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param id the id of the view to be found * @return the view of the specified id, null if cannot be found */ - protected View findViewTraversal(int id) { + protected View findViewTraversal(@IdRes int id) { if (id == mID) { return this; } @@ -17064,7 +17176,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param id The id to search for. * @return The view that has the given id in the hierarchy or null */ - public final View findViewById(int id) { + @Nullable + public final View findViewById(@IdRes int id) { if (id < 0) { return null; } @@ -17179,7 +17292,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @attr ref android.R.styleable#View_id */ - public void setId(int id) { + public void setId(@IdRes int id) { mID = id; if (mID == View.NO_ID && mLabelForId != View.NO_ID) { mID = generateViewId(); @@ -17219,6 +17332,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #findViewById(int) * @attr ref android.R.styleable#View_id */ + @IdRes @ViewDebug.CapturedViewProperty public int getId() { return mID; @@ -17277,7 +17391,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * The specified key should be an id declared in the resources of the * application to ensure it is unique (see the <a - * href={@docRoot}guide/topics/resources/more-resources.html#Id">ID resource type</a>). + * href="{@docRoot}guide/topics/resources/more-resources.html#Id">ID resource type</a>). * Keys identified as belonging to * the Android framework or not associated with any package will cause * an {@link IllegalArgumentException} to be thrown. @@ -17454,6 +17568,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * <p>Subclasses which override this method should call the superclass method to * handle possible request-during-layout errors correctly.</p> */ + @CallSuper public void requestLayout() { if (mMeasureCache != null) mMeasureCache.clear(); @@ -17575,7 +17690,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * <p> * Measure the view and its content to determine the measured width and the * measured height. This method is invoked by {@link #measure(int, int)} and - * should be overriden by subclasses to provide accurate and efficient + * should be overridden by subclasses to provide accurate and efficient * measurement of their contents. * </p> * @@ -17688,37 +17803,40 @@ public class View implements Drawable.Callback, KeyEvent.Callback, /** * Utility to reconcile a desired size and state, with constraints imposed - * by a MeasureSpec. Will take the desired size, unless a different size - * is imposed by the constraints. The returned value is a compound integer, + * by a MeasureSpec. Will take the desired size, unless a different size + * is imposed by the constraints. The returned value is a compound integer, * with the resolved size in the {@link #MEASURED_SIZE_MASK} bits and - * optionally the bit {@link #MEASURED_STATE_TOO_SMALL} set if the resulting - * size is smaller than the size the view wants to be. + * optionally the bit {@link #MEASURED_STATE_TOO_SMALL} set if the + * resulting size is smaller than the size the view wants to be. * - * @param size How big the view wants to be - * @param measureSpec Constraints imposed by the parent + * @param size How big the view wants to be. + * @param measureSpec Constraints imposed by the parent. + * @param childMeasuredState Size information bit mask for the view's + * children. * @return Size information bit mask as defined by - * {@link #MEASURED_SIZE_MASK} and {@link #MEASURED_STATE_TOO_SMALL}. + * {@link #MEASURED_SIZE_MASK} and + * {@link #MEASURED_STATE_TOO_SMALL}. */ public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) { - int result = size; - int specMode = MeasureSpec.getMode(measureSpec); - int specSize = MeasureSpec.getSize(measureSpec); + final int specMode = MeasureSpec.getMode(measureSpec); + final int specSize = MeasureSpec.getSize(measureSpec); + final int result; switch (specMode) { - case MeasureSpec.UNSPECIFIED: - result = size; - break; - case MeasureSpec.AT_MOST: - if (specSize < size) { - result = specSize | MEASURED_STATE_TOO_SMALL; - } else { + case MeasureSpec.AT_MOST: + if (specSize < size) { + result = specSize | MEASURED_STATE_TOO_SMALL; + } else { + result = size; + } + break; + case MeasureSpec.EXACTLY: + result = specSize; + break; + case MeasureSpec.UNSPECIFIED: + default: result = size; - } - break; - case MeasureSpec.EXACTLY: - result = specSize; - break; } - return result | (childMeasuredState&MEASURED_STATE_MASK); + return result | (childMeasuredState & MEASURED_STATE_MASK); } /** @@ -17906,6 +18024,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #setAnimation(android.view.animation.Animation) * @see #getAnimation() */ + @CallSuper protected void onAnimationStart() { mPrivateFlags |= PFLAG_ANIMATION_STARTED; } @@ -17918,6 +18037,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #setAnimation(android.view.animation.Animation) * @see #getAnimation() */ + @CallSuper protected void onAnimationEnd() { mPrivateFlags &= ~PFLAG_ANIMATION_STARTED; } @@ -18181,7 +18301,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * appearance as the given View. The default also positions the center of the drag shadow * directly under the touch point. If no View is provided (the constructor with no parameters * is used), and {@link #onProvideShadowMetrics(Point,Point) onProvideShadowMetrics()} and - * {@link #onDrawShadow(Canvas) onDrawShadow()} are not overriden, then the + * {@link #onDrawShadow(Canvas) onDrawShadow()} are not overridden, then the * default is an invisible drag shadow. * <p> * You are not required to use the View you provide to the constructor as the basis of the @@ -18527,7 +18647,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * layout_* parameters. * @see LayoutInflater */ - public static View inflate(Context context, int resource, ViewGroup root) { + public static View inflate(Context context, @LayoutRes int resource, ViewGroup root) { LayoutInflater factory = LayoutInflater.from(context); return factory.inflate(resource, root); } @@ -18810,7 +18930,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #dispatchNestedPreScroll(int, int, int[], int[]) */ public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, - int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { + int dxUnconsumed, int dyUnconsumed, @Nullable @Size(2) int[] offsetInWindow) { if (isNestedScrollingEnabled() && mNestedScrollingParent != null) { if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) { int startX = 0; @@ -18858,7 +18978,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @return true if the parent consumed some or all of the scroll delta * @see #dispatchNestedScroll(int, int, int, int, int[]) */ - public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { + public boolean dispatchNestedPreScroll(int dx, int dy, + @Nullable @Size(2) int[] consumed, @Nullable @Size(2) int[] offsetInWindow) { if (isNestedScrollingEnabled() && mNestedScrollingParent != null) { if (dx != 0 || dy != 0) { int startX = 0; diff --git a/core/java/android/view/ViewAssistStructure.java b/core/java/android/view/ViewAssistStructure.java new file mode 100644 index 0000000..5132bb9 --- /dev/null +++ b/core/java/android/view/ViewAssistStructure.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.text.TextPaint; + +/** + * Container for storing additional per-view data generated by {@link View#onProvideAssistStructure + * View.onProvideAssistStructure}. + */ +public abstract class ViewAssistStructure { + public abstract void setText(CharSequence text); + public abstract void setText(CharSequence text, int selectionStart, int selectionEnd); + public abstract void setTextPaint(TextPaint paint); + public abstract void setHint(CharSequence hint); + + public abstract CharSequence getText(); + public abstract int getTextSelectionStart(); + public abstract int getTextSelectionEnd(); + public abstract CharSequence getHint(); +} diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index f8026d1..87f3e94 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -17,6 +17,7 @@ package android.view; import android.animation.LayoutTransition; +import android.annotation.IdRes; import android.content.Context; import android.content.pm.PackageManager; import android.content.res.Configuration; @@ -52,11 +53,8 @@ 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; /** @@ -371,6 +369,26 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager static final int FLAG_TOUCHSCREEN_BLOCKS_FOCUS = 0x4000000; /** + * When true, indicates that a call to startActionModeForChild was made with the type parameter + * and should not be ignored. This helps in backwards compatibility with the existing method + * without a type. + * + * @see #startActionModeForChild(View, android.view.ActionMode.Callback) + * @see #startActionModeForChild(View, android.view.ActionMode.Callback, int) + */ + private static final int FLAG_START_ACTION_MODE_FOR_CHILD_IS_TYPED = 0x8000000; + + /** + * When true, indicates that a call to startActionModeForChild was made without the type + * parameter. This helps in backwards compatibility with the existing method + * without a type. + * + * @see #startActionModeForChild(View, android.view.ActionMode.Callback) + * @see #startActionModeForChild(View, android.view.ActionMode.Callback, int) + */ + private static final int FLAG_START_ACTION_MODE_FOR_CHILD_IS_NOT_TYPED = 0x10000000; + + /** * Indicates which types of drawing caches are to be kept in memory. * This field should be made private, so it is hidden from the SDK. * {@hide} @@ -481,6 +499,60 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager */ private int mNestedScrollAxes; + /** + * Empty ActionMode used as a sentinel in recursive entries to startActionModeForChild. + * + * @see #startActionModeForChild(View, android.view.ActionMode.Callback) + * @see #startActionModeForChild(View, android.view.ActionMode.Callback, int) + */ + private static final ActionMode SENTINEL_ACTION_MODE = new ActionMode() { + @Override + public void setTitle(CharSequence title) {} + + @Override + public void setTitle(int resId) {} + + @Override + public void setSubtitle(CharSequence subtitle) {} + + @Override + public void setSubtitle(int resId) {} + + @Override + public void setCustomView(View view) {} + + @Override + public void invalidate() {} + + @Override + public void finish() {} + + @Override + public Menu getMenu() { + return null; + } + + @Override + public CharSequence getTitle() { + return null; + } + + @Override + public CharSequence getSubtitle() { + return null; + } + + @Override + public View getCustomView() { + return null; + } + + @Override + public MenuInflater getMenuInflater() { + return null; + } + }; + public ViewGroup(Context context) { this(context, null); } @@ -696,8 +768,49 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager /** * {@inheritDoc} */ + @Override public ActionMode startActionModeForChild(View originalView, ActionMode.Callback callback) { - return mParent != null ? mParent.startActionModeForChild(originalView, callback) : null; + if ((mGroupFlags & FLAG_START_ACTION_MODE_FOR_CHILD_IS_TYPED) == 0) { + // This is the original call. + try { + mGroupFlags |= FLAG_START_ACTION_MODE_FOR_CHILD_IS_NOT_TYPED; + return startActionModeForChild(originalView, callback, ActionMode.TYPE_PRIMARY); + } finally { + mGroupFlags &= ~FLAG_START_ACTION_MODE_FOR_CHILD_IS_NOT_TYPED; + } + } else { + // We are being called from the new method with type. + return SENTINEL_ACTION_MODE; + } + } + + /** + * {@inheritDoc} + */ + @Override + public ActionMode startActionModeForChild( + View originalView, ActionMode.Callback callback, int type) { + if ((mGroupFlags & FLAG_START_ACTION_MODE_FOR_CHILD_IS_NOT_TYPED) == 0) { + ActionMode mode; + try { + mGroupFlags |= FLAG_START_ACTION_MODE_FOR_CHILD_IS_TYPED; + mode = startActionModeForChild(originalView, callback); + } finally { + mGroupFlags &= ~FLAG_START_ACTION_MODE_FOR_CHILD_IS_TYPED; + } + if (mode != SENTINEL_ACTION_MODE) { + return mode; + } + } + if (mParent != null) { + try { + return mParent.startActionModeForChild(originalView, callback, type); + } catch (AbstractMethodError ame) { + // Custom view parents might not implement this method. + return mParent.startActionModeForChild(originalView, callback); + } + } + return null; } /** @@ -771,8 +884,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * @see #onRequestSendAccessibilityEvent(View, AccessibilityEvent) * * Note: Called from the default {@link View.AccessibilityDelegate}. + * + * @hide */ - boolean onRequestSendAccessibilityEventInternal(View child, AccessibilityEvent event) { + public boolean onRequestSendAccessibilityEventInternal(View child, AccessibilityEvent event) { return true; } @@ -1206,7 +1321,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * {@inheritDoc} */ public void bringChildToFront(View child) { - int index = indexOfChild(child); + final int index = indexOfChild(child); if (index >= 0) { removeFromArray(index); addInArray(child, mChildrenCount); @@ -2708,8 +2823,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } } + /** @hide */ @Override - boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) { + public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) { boolean handled = false; if (includeForAccessibility()) { handled = super.dispatchPopulateAccessibilityEventInternal(event); @@ -2736,8 +2852,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager return false; } + /** @hide */ @Override - void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { + public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfoInternal(info); if (getAccessibilityNodeProvider() != null) { return; @@ -2755,8 +2872,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } } + /** @hide */ @Override - void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { + public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { super.onInitializeAccessibilityEventInternal(event); event.setClassName(ViewGroup.class.getName()); } @@ -3607,7 +3725,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * {@hide} */ @Override - protected View findViewTraversal(int id) { + protected View findViewTraversal(@IdRes int id) { if (id == mID) { return this; } @@ -3766,7 +3884,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p> * * @param child the child view to add - * @param index the position at which to add the child + * @param index the position at which to add the child or -1 to add last * @param params the layout parameters to set on the child */ public void addView(View child, int index, LayoutParams params) { @@ -3882,7 +4000,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * If index is negative, it means put it at the end of the list. * * @param child the view to add to the group - * @param index the index at which the child must be added + * @param index the index at which the child must be added or -1 to add last * @param params the layout parameters to associate with the child * @return true if the child was added, false otherwise */ @@ -3897,7 +4015,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * If index is negative, it means put it at the end of the list. * * @param child the view to add to the group - * @param index the index at which the child must be added + * @param index the index at which the child must be added or -1 to add last * @param params the layout parameters to associate with the child * @param preventRequestLayout if true, calling this method will not trigger a * layout request on child diff --git a/core/java/android/view/ViewParent.java b/core/java/android/view/ViewParent.java index 035871d..15b86d1 100644 --- a/core/java/android/view/ViewParent.java +++ b/core/java/android/view/ViewParent.java @@ -190,7 +190,8 @@ public interface ViewParent { public void createContextMenu(ContextMenu menu); /** - * Start an action mode for the specified view. + * Start an action mode for the specified view with the default type + * {@link ActionMode#TYPE_PRIMARY}. * * <p>In most cases, a subclass does not need to override this. However, if the * subclass is added directly to the window manager (for example, @@ -200,17 +201,35 @@ public interface ViewParent { * @param originalView The source view where the action mode was first invoked * @param callback The callback that will handle lifecycle events for the action mode * @return The new action mode if it was started, null otherwise + * + * @see #startActionModeForChild(View, android.view.ActionMode.Callback, int) */ public ActionMode startActionModeForChild(View originalView, ActionMode.Callback callback); /** + * Start an action mode of a specific type for the specified view. + * + * <p>In most cases, a subclass does not need to override this. However, if the + * subclass is added directly to the window manager (for example, + * {@link ViewManager#addView(View, android.view.ViewGroup.LayoutParams)}) + * then it should override this and start the action mode.</p> + * + * @param originalView The source view where the action mode was first invoked + * @param callback The callback that will handle lifecycle events for the action mode + * @param type One of {@link ActionMode#TYPE_PRIMARY} or {@link ActionMode#TYPE_FLOATING}. + * @return The new action mode if it was started, null otherwise + */ + public ActionMode startActionModeForChild( + View originalView, ActionMode.Callback callback, int type); + + /** * This method is called on the parent when a child's drawable state * has changed. * * @param child The child whose drawable state has changed. */ public void childDrawableStateChanged(View child); - + /** * Called when a child does not want this parent and its ancestors to * intercept touch events with diff --git a/core/java/android/view/ViewPropertyAnimator.java b/core/java/android/view/ViewPropertyAnimator.java index b73b9fa..f18b7ac 100644 --- a/core/java/android/view/ViewPropertyAnimator.java +++ b/core/java/android/view/ViewPropertyAnimator.java @@ -19,8 +19,6 @@ package android.view; import android.animation.Animator; import android.animation.ValueAnimator; import android.animation.TimeInterpolator; -import android.os.Build; - import java.util.ArrayList; import java.util.HashMap; import java.util.Set; diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 90c2bd1..e790d4c 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -47,7 +47,6 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.ParcelFileDescriptor; -import android.os.PowerManager; import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; @@ -57,6 +56,7 @@ import android.util.AndroidRuntimeException; import android.util.DisplayMetrics; import android.util.Log; import android.util.Slog; +import android.util.TimeUtils; import android.util.TypedValue; import android.view.Surface.OutOfResourcesException; import android.view.View.AttachInfo; @@ -77,7 +77,6 @@ import android.widget.Scroller; import com.android.internal.R; import com.android.internal.os.SomeArgs; -import com.android.internal.policy.PolicyManager; import com.android.internal.view.BaseSurfaceHolder; import com.android.internal.view.RootViewSurfaceTaker; @@ -266,6 +265,8 @@ public final class ViewRootImpl implements ViewParent, final Rect mDispatchContentInsets = new Rect(); final Rect mDispatchStableInsets = new Rect(); + private WindowInsets mLastWindowInsets; + final Configuration mLastConfiguration = new Configuration(); final Configuration mPendingConfiguration = new Configuration(); @@ -387,7 +388,7 @@ public final class ViewRootImpl implements ViewParent, mViewConfiguration = ViewConfiguration.get(context); mDensity = context.getResources().getDisplayMetrics().densityDpi; mNoncompatDensity = context.getResources().getDisplayMetrics().noncompatDensityDpi; - mFallbackEventHandler = PolicyManager.makeNewFallbackEventHandler(context); + mFallbackEventHandler = new PhoneFallbackEventHandler(context); mChoreographer = Choreographer.getInstance(); mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE); loadSystemProperties(); @@ -473,12 +474,13 @@ public final class ViewRootImpl implements ViewParent, // Compute surface insets required to draw at specified Z value. // TODO: Use real shadow insets for a constant max Z. - final int surfaceInset = (int) Math.ceil(view.getZ() * 2); - attrs.surfaceInsets.set(surfaceInset, surfaceInset, surfaceInset, surfaceInset); + if (!attrs.hasManualSurfaceInsets) { + final int surfaceInset = (int) Math.ceil(view.getZ() * 2); + attrs.surfaceInsets.set(surfaceInset, surfaceInset, surfaceInset, surfaceInset); + } CompatibilityInfo compatibilityInfo = mDisplayAdjustments.getCompatibilityInfo(); mTranslator = compatibilityInfo.getTranslator(); - mDisplayAdjustments.setActivityToken(attrs.token); // If the application owns the surface, don't enable hardware acceleration if (mSurfaceHolder == null) { @@ -550,6 +552,11 @@ public final class ViewRootImpl implements ViewParent, mPendingContentInsets.set(mAttachInfo.mContentInsets); mPendingStableInsets.set(mAttachInfo.mStableInsets); mPendingVisibleInsets.set(0, 0, 0, 0); + try { + relayoutWindow(attrs, getHostVisibility(), false); + } catch (RemoteException e) { + if (DEBUG_LAYOUT) Log.e(TAG, "failed to relayoutWindow", e); + } if (DEBUG_LAYOUT) Log.v(TAG, "Added window " + mWindow); if (res < WindowManagerGlobal.ADD_OKAY) { mAttachInfo.mRootView = null; @@ -650,6 +657,10 @@ public final class ViewRootImpl implements ViewParent, return (mWindowAttributes.flags & WindowManager.LayoutParams.FLAG_LOCAL_FOCUS_MODE) != 0; } + public CharSequence getTitle() { + return mWindowAttributes.getTitle(); + } + void destroyHardwareResources() { if (mAttachInfo.mHardwareRenderer != null) { mAttachInfo.mHardwareRenderer.destroyHardwareResources(mView); @@ -761,6 +772,7 @@ public final class ViewRootImpl implements ViewParent, final int oldInsetRight = mWindowAttributes.surfaceInsets.right; final int oldInsetBottom = mWindowAttributes.surfaceInsets.bottom; final int oldSoftInputMode = mWindowAttributes.softInputMode; + final boolean oldHasManualSurfaceInsets = mWindowAttributes.hasManualSurfaceInsets; // Keep track of the actual window flags supplied by the client. mClientWindowLayoutFlags = attrs.flags; @@ -787,6 +799,7 @@ public final class ViewRootImpl implements ViewParent, // Restore old surface insets. mWindowAttributes.surfaceInsets.set( oldInsetLeft, oldInsetTop, oldInsetRight, oldInsetBottom); + mWindowAttributes.hasManualSurfaceInsets = oldHasManualSurfaceInsets; applyKeepScreenOnFlag(mWindowAttributes); @@ -1043,7 +1056,7 @@ public final class ViewRootImpl implements ViewParent, void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; - mTraversalBarrier = mHandler.getLooper().postSyncBarrier(); + mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); if (!mUnbufferedInputDispatch) { @@ -1057,7 +1070,7 @@ public final class ViewRootImpl implements ViewParent, void unscheduleTraversals() { if (mTraversalScheduled) { mTraversalScheduled = false; - mHandler.getLooper().removeSyncBarrier(mTraversalBarrier); + mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); mChoreographer.removeCallbacks( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); } @@ -1066,7 +1079,7 @@ public final class ViewRootImpl implements ViewParent, void doTraversal() { if (mTraversalScheduled) { mTraversalScheduled = false; - mHandler.getLooper().removeSyncBarrier(mTraversalBarrier); + mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); if (mProfile) { Debug.startMethodTracing("ViewAncestor"); @@ -1221,13 +1234,29 @@ public final class ViewRootImpl implements ViewParent, m.postTranslate(-mAttachInfo.mWindowLeft, -mAttachInfo.mWindowTop); } + /* package */ WindowInsets getWindowInsets(boolean forceConstruct) { + if (mLastWindowInsets == null || forceConstruct) { + mDispatchContentInsets.set(mAttachInfo.mContentInsets); + mDispatchStableInsets.set(mAttachInfo.mStableInsets); + Rect contentInsets = mDispatchContentInsets; + Rect stableInsets = mDispatchStableInsets; + // For dispatch we preserve old logic, but for direct requests from Views we allow to + // immediately use pending insets. + if (!forceConstruct + && (!mPendingContentInsets.equals(contentInsets) || + !mPendingStableInsets.equals(stableInsets))) { + contentInsets = mPendingContentInsets; + stableInsets = mPendingStableInsets; + } + final boolean isRound = (mIsEmulator && mIsCircularEmulator) || mWindowIsRound; + mLastWindowInsets = new WindowInsets(contentInsets, + null /* windowDecorInsets */, stableInsets, isRound); + } + return mLastWindowInsets; + } + void dispatchApplyInsets(View host) { - mDispatchContentInsets.set(mAttachInfo.mContentInsets); - mDispatchStableInsets.set(mAttachInfo.mStableInsets); - final boolean isRound = (mIsEmulator && mIsCircularEmulator) || mWindowIsRound; - host.dispatchApplyWindowInsets(new WindowInsets( - mDispatchContentInsets, null /* windowDecorInsets */, - mDispatchStableInsets, isRound)); + host.dispatchApplyWindowInsets(getWindowInsets(true /* forceConstruct */)); } private void performTraversals() { @@ -1342,12 +1371,17 @@ public final class ViewRootImpl implements ViewParent, } } + // Non-visible windows can't hold accessibility focus. + if (mAttachInfo.mWindowVisibility != View.VISIBLE) { + host.clearAccessibilityFocus(); + } + // Execute enqueued actions on every traversal in case a detached view enqueued an action getRunQueue().executeActions(mAttachInfo.mHandler); boolean insetsChanged = false; - boolean layoutRequested = mLayoutRequested && !mStopped; + boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw); if (layoutRequested) { final Resources res = mView.getContext().getResources(); @@ -1518,6 +1552,7 @@ public final class ViewRootImpl implements ViewParent, // to resume them mDirty.set(0, 0, mWidth, mHeight); } + mChoreographer.mFrameInfo.addFlags(FrameInfo.FLAG_WINDOW_LAYOUT_CHANGED); } final int surfaceGenerationId = mSurface.getGenerationId(); relayoutResult = relayoutWindow(params, viewVisibility, insetsPending); @@ -1779,7 +1814,7 @@ public final class ViewRootImpl implements ViewParent, } } - if (!mStopped) { + if (!mStopped || mReportNextDraw) { boolean focusChangedDueToTouchMode = ensureTouchModeLocally( (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0); if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth() @@ -1852,7 +1887,7 @@ public final class ViewRootImpl implements ViewParent, } } - final boolean didLayout = layoutRequested && !mStopped; + final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw); boolean triggerGlobalLayoutListener = didLayout || mAttachInfo.mRecomputeGlobalAttributes; if (didLayout) { @@ -2274,12 +2309,12 @@ public final class ViewRootImpl implements ViewParent, final Paint mResizePaint = new Paint(); @Override - public void onHardwarePreDraw(HardwareCanvas canvas) { + public void onHardwarePreDraw(DisplayListCanvas canvas) { canvas.translate(-mHardwareXOffset, -mHardwareYOffset); } @Override - public void onHardwarePostDraw(HardwareCanvas canvas) { + public void onHardwarePostDraw(DisplayListCanvas canvas) { if (mResizeBuffer != null) { mResizePaint.setAlpha(mResizeAlpha); canvas.drawHardwareLayer(mResizeBuffer, mHardwareXOffset, mHardwareYOffset, @@ -2521,6 +2556,9 @@ public final class ViewRootImpl implements ViewParent, } } + mAttachInfo.mDrawingTime = + mChoreographer.getFrameTimeNanos() / TimeUtils.NANOS_PER_MS; + if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) { if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) { // If accessibility focus moved, always invalidate the root. @@ -2640,7 +2678,6 @@ public final class ViewRootImpl implements ViewParent, dirty.setEmpty(); mIsAnimating = false; - attachInfo.mDrawingTime = SystemClock.uptimeMillis(); mView.mPrivateFlags |= View.PFLAG_DRAWN; if (DEBUG_DRAW) { @@ -3085,17 +3122,6 @@ public final class ViewRootImpl implements ViewParent, return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent); } - private static void forceLayout(View view) { - view.forceLayout(); - if (view instanceof ViewGroup) { - ViewGroup group = (ViewGroup) view; - final int count = group.getChildCount(); - for (int i = 0; i < count; i++) { - forceLayout(group.getChildAt(i)); - } - } - } - private final static int MSG_INVALIDATE = 1; private final static int MSG_INVALIDATE_RECT = 2; private final static int MSG_DIE = 3; @@ -3229,10 +3255,6 @@ public final class ViewRootImpl implements ViewParent, mReportNextDraw = true; } - if (mView != null) { - forceLayout(mView); - } - requestLayout(); } break; @@ -3247,9 +3269,6 @@ public final class ViewRootImpl implements ViewParent, mWinFrame.top = t; mWinFrame.bottom = t + h; - if (mView != null) { - forceLayout(mView); - } requestLayout(); } break; @@ -5794,6 +5813,16 @@ public final class ViewRootImpl implements ViewParent, Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName, mPendingInputEventCount); + long eventTime = q.mEvent.getEventTimeNano(); + long oldestEventTime = eventTime; + if (q.mEvent instanceof MotionEvent) { + MotionEvent me = (MotionEvent)q.mEvent; + if (me.getHistorySize() > 0) { + oldestEventTime = me.getHistoricalEventTimeNano(0); + } + } + mChoreographer.mFrameInfo.updateInputEventTime(eventTime, oldestEventTime); + deliverInputEvent(q); } @@ -6198,6 +6227,12 @@ public final class ViewRootImpl implements ViewParent, } @Override + public ActionMode startActionModeForChild( + View originalView, ActionMode.Callback callback, int type) { + return null; + } + + @Override public void createContextMenu(ContextMenu menu) { } diff --git a/core/java/android/view/ViewStub.java b/core/java/android/view/ViewStub.java index d68a860..ec852e8 100644 --- a/core/java/android/view/ViewStub.java +++ b/core/java/android/view/ViewStub.java @@ -16,6 +16,8 @@ package android.view; +import android.annotation.IdRes; +import android.annotation.LayoutRes; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; @@ -69,8 +71,8 @@ import java.lang.ref.WeakReference; */ @RemoteView public final class ViewStub extends View { - private int mLayoutResource = 0; private int mInflatedId; + private int mLayoutResource; private WeakReference<View> mInflatedViewRef; @@ -78,7 +80,7 @@ public final class ViewStub extends View { private OnInflateListener mInflateListener; public ViewStub(Context context) { - initialize(context); + this(context, 0); } /** @@ -87,39 +89,30 @@ public final class ViewStub extends View { * @param context The application's environment. * @param layoutResource The reference to a layout resource that will be inflated. */ - public ViewStub(Context context, int layoutResource) { + public ViewStub(Context context, @LayoutRes int layoutResource) { + this(context, null); + mLayoutResource = layoutResource; - initialize(context); } public ViewStub(Context context, AttributeSet attrs) { this(context, attrs, 0); } - @SuppressWarnings({"UnusedDeclaration"}) public ViewStub(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - TypedArray a = context.obtainStyledAttributes( - attrs, com.android.internal.R.styleable.ViewStub, defStyleAttr, defStyleRes); + super(context); + final TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.ViewStub, defStyleAttr, defStyleRes); mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID); mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0); - - a.recycle(); - - a = context.obtainStyledAttributes( - attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes); - mID = a.getResourceId(R.styleable.View_id, NO_ID); + mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID); a.recycle(); - initialize(context); - } - - private void initialize(Context context) { - mContext = context; setVisibility(GONE); setWillNotDraw(true); } @@ -134,6 +127,7 @@ public final class ViewStub extends View { * @see #setInflatedId(int) * @attr ref android.R.styleable#ViewStub_inflatedId */ + @IdRes public int getInflatedId() { return mInflatedId; } @@ -149,7 +143,7 @@ public final class ViewStub extends View { * @attr ref android.R.styleable#ViewStub_inflatedId */ @android.view.RemotableViewMethod - public void setInflatedId(int inflatedId) { + public void setInflatedId(@IdRes int inflatedId) { mInflatedId = inflatedId; } @@ -165,6 +159,7 @@ public final class ViewStub extends View { * @see #inflate() * @attr ref android.R.styleable#ViewStub_layout */ + @LayoutRes public int getLayoutResource() { return mLayoutResource; } @@ -182,7 +177,7 @@ public final class ViewStub extends View { * @attr ref android.R.styleable#ViewStub_layout */ @android.view.RemotableViewMethod - public void setLayoutResource(int layoutResource) { + public void setLayoutResource(@LayoutRes int layoutResource) { mLayoutResource = layoutResource; } diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java index 8704356..9a92932 100644 --- a/core/java/android/view/Window.java +++ b/core/java/android/view/Window.java @@ -16,8 +16,13 @@ package android.view; +import android.annotation.ColorInt; +import android.annotation.DrawableRes; +import android.annotation.IdRes; +import android.annotation.LayoutRes; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.StyleRes; import android.annotation.SystemApi; import android.content.Context; import android.content.res.Configuration; @@ -42,10 +47,8 @@ import android.view.accessibility.AccessibilityEvent; * area, default key processing, etc. * * <p>The only existing implementation of this abstract class is - * android.policy.PhoneWindow, which you should instantiate when needing a - * Window. Eventually that class will be refactored and a factory method - * added for creating Window instances without knowing about a particular - * implementation. + * android.view.PhoneWindow, which you should instantiate when needing a + * Window. */ public abstract class Window { /** Flag for the "options panel" feature. This is enabled by default. */ @@ -414,7 +417,9 @@ public abstract class Window { * Called when an action mode is being started for this window. Gives the * callback an opportunity to handle the action mode in its own unique and * beautiful way. If this method returns null the system can choose a way - * to present the mode or choose not to start the mode at all. + * to present the mode or choose not to start the mode at all. This is equivalent + * to {@link #onWindowStartingActionMode(android.view.ActionMode.Callback, int)} + * with type {@link ActionMode#TYPE_PRIMARY}. * * @param callback Callback to control the lifecycle of this action mode * @return The ActionMode that was started, or null if the system should present it @@ -423,6 +428,19 @@ public abstract class Window { public ActionMode onWindowStartingActionMode(ActionMode.Callback callback); /** + * Called when an action mode is being started for this window. Gives the + * callback an opportunity to handle the action mode in its own unique and + * beautiful way. If this method returns null the system can choose a way + * to present the mode or choose not to start the mode at all. + * + * @param callback Callback to control the lifecycle of this action mode + * @param type One of {@link ActionMode#TYPE_PRIMARY} or {@link ActionMode#TYPE_FLOATING}. + * @return The ActionMode that was started, or null if the system should present it + */ + @Nullable + public ActionMode onWindowStartingActionMode(ActionMode.Callback callback, int type); + + /** * Called when an action mode has been started. The appropriate mode callback * method will have already been invoked. * @@ -736,7 +754,7 @@ public abstract class Window { * 0 here will override the animations the window would * normally retrieve from its theme. */ - public void setWindowAnimations(int resId) { + public void setWindowAnimations(@StyleRes int resId) { final WindowManager.LayoutParams attrs = getAttributes(); attrs.windowAnimations = resId; dispatchWindowAttributesChanged(attrs); @@ -986,7 +1004,8 @@ public abstract class Window { * * @return The view if found or null otherwise. */ - public View findViewById(int id) { + @Nullable + public View findViewById(@IdRes int id) { return getDecorView().findViewById(id); } @@ -999,7 +1018,7 @@ public abstract class Window { * @param layoutResID Resource ID to be inflated. * @see #setContentView(View, android.view.ViewGroup.LayoutParams) */ - public abstract void setContentView(int layoutResID); + public abstract void setContentView(@LayoutRes int layoutResID); /** * Convenience for @@ -1067,7 +1086,7 @@ public abstract class Window { public abstract void setTitle(CharSequence title); @Deprecated - public abstract void setTitleColor(int textColor); + public abstract void setTitleColor(@ColorInt int textColor); public abstract void openPanel(int featureId, KeyEvent event); @@ -1129,7 +1148,7 @@ public abstract class Window { * @param resId The resource identifier of a drawable resource which will * be installed as the new background. */ - public void setBackgroundDrawableResource(int resId) { + public void setBackgroundDrawableResource(@DrawableRes int resId) { setBackgroundDrawable(mContext.getDrawable(resId)); } @@ -1145,7 +1164,7 @@ public abstract class Window { /** * Set the value for a drawable feature of this window, from a resource - * identifier. You must have called requestFeauture(featureId) before + * identifier. You must have called requestFeature(featureId) before * calling this function. * * @see android.content.res.Resources#getDrawable(int) @@ -1154,7 +1173,7 @@ public abstract class Window { * constant by Window. * @param resId Resource identifier of the desired image. */ - public abstract void setFeatureDrawableResource(int featureId, int resId); + public abstract void setFeatureDrawableResource(int featureId, @DrawableRes int resId); /** * Set the value for a drawable feature of this window, from a URI. You @@ -1424,7 +1443,7 @@ public abstract class Window { * * @param resId resource ID of a drawable to set */ - public void setIcon(int resId) { } + public void setIcon(@DrawableRes int resId) { } /** * Set the default icon for this window. @@ -1433,7 +1452,7 @@ public abstract class Window { * * @hide */ - public void setDefaultIcon(int resId) { } + public void setDefaultIcon(@DrawableRes int resId) { } /** * Set the logo for this window. A logo is often shown in place of an @@ -1442,7 +1461,7 @@ public abstract class Window { * * @param resId resource ID of a drawable to set */ - public void setLogo(int resId) { } + public void setLogo(@DrawableRes int resId) { } /** * Set the default logo for this window. @@ -1451,7 +1470,7 @@ public abstract class Window { * * @hide */ - public void setDefaultLogo(int resId) { } + public void setDefaultLogo(@DrawableRes int resId) { } /** * Set focus locally. The window should have the @@ -1833,6 +1852,7 @@ public abstract class Window { /** * @return the color of the status bar. */ + @ColorInt public abstract int getStatusBarColor(); /** @@ -1850,11 +1870,12 @@ public abstract class Window { * The transitionName for the view background will be "android:status:background". * </p> */ - public abstract void setStatusBarColor(int color); + public abstract void setStatusBarColor(@ColorInt int color); /** * @return the color of the navigation bar. */ + @ColorInt public abstract int getNavigationBarColor(); /** @@ -1872,7 +1893,7 @@ public abstract class Window { * The transitionName for the view background will be "android:navigation:background". * </p> */ - public abstract void setNavigationBarColor(int color); + public abstract void setNavigationBarColor(@ColorInt int color); } diff --git a/core/java/android/view/WindowCallbackWrapper.java b/core/java/android/view/WindowCallbackWrapper.java index 35a6a76..979ee95 100644 --- a/core/java/android/view/WindowCallbackWrapper.java +++ b/core/java/android/view/WindowCallbackWrapper.java @@ -132,6 +132,11 @@ public class WindowCallbackWrapper implements Window.Callback { } @Override + public ActionMode onWindowStartingActionMode(ActionMode.Callback callback, int type) { + return mWrapped.onWindowStartingActionMode(callback, type); + } + + @Override public void onActionModeStarted(ActionMode mode) { mWrapped.onActionModeStarted(mode); } diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index d26bcd3..66dae7b 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -502,13 +502,6 @@ public interface WindowManager extends ViewManager { public static final int TYPE_NAVIGATION_BAR_PANEL = FIRST_SYSTEM_WINDOW+24; /** - * Window type: Behind the universe of the real windows. - * In multiuser systems shows on all users' windows. - * @hide - */ - public static final int TYPE_UNIVERSE_BACKGROUND = FIRST_SYSTEM_WINDOW+25; - - /** * Window type: Display overlay window. Used to simulate secondary display devices. * In multiuser systems shows on all users' windows. * @hide @@ -1333,6 +1326,16 @@ public interface WindowManager extends ViewManager { * @hide */ public final Rect surfaceInsets = new Rect(); + + /** + * Whether the surface insets have been manually set. When set to + * {@code false}, the view root will automatically determine the + * appropriate surface insets. + * + * @see #surfaceInsets + * @hide + */ + public boolean hasManualSurfaceInsets; /** * The desired bitmap format. May be one of the constants in @@ -1641,6 +1644,7 @@ public interface WindowManager extends ViewManager { out.writeInt(surfaceInsets.top); out.writeInt(surfaceInsets.right); out.writeInt(surfaceInsets.bottom); + out.writeInt(hasManualSurfaceInsets ? 1 : 0); out.writeInt(needsMenuKey); } @@ -1689,6 +1693,7 @@ public interface WindowManager extends ViewManager { surfaceInsets.top = in.readInt(); surfaceInsets.right = in.readInt(); surfaceInsets.bottom = in.readInt(); + hasManualSurfaceInsets = in.readInt() != 0; needsMenuKey = in.readInt(); } @@ -1871,6 +1876,11 @@ public interface WindowManager extends ViewManager { changes |= SURFACE_INSETS_CHANGED; } + if (hasManualSurfaceInsets != o.hasManualSurfaceInsets) { + hasManualSurfaceInsets = o.hasManualSurfaceInsets; + changes |= SURFACE_INSETS_CHANGED; + } + if (needsMenuKey != o.needsMenuKey) { needsMenuKey = o.needsMenuKey; changes |= NEEDS_MENU_KEY_CHANGED; @@ -1980,8 +1990,11 @@ public interface WindowManager extends ViewManager { sb.append(" userActivityTimeout=").append(userActivityTimeout); } if (surfaceInsets.left != 0 || surfaceInsets.top != 0 || surfaceInsets.right != 0 || - surfaceInsets.bottom != 0) { + surfaceInsets.bottom != 0 || hasManualSurfaceInsets) { sb.append(" surfaceInsets=").append(surfaceInsets); + if (hasManualSurfaceInsets) { + sb.append(" (manual)"); + } } if (needsMenuKey != NEEDS_MENU_UNSET) { sb.append(" needsMenuKey="); diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java index a14c766..57558ff 100644 --- a/core/java/android/view/WindowManagerGlobal.java +++ b/core/java/android/view/WindowManagerGlobal.java @@ -190,6 +190,39 @@ public final class WindowManagerGlobal { } } + public ArrayList<ViewRootImpl> getRootViews(IBinder token) { + ArrayList<ViewRootImpl> views = new ArrayList<>(); + synchronized (mLock) { + final int numRoots = mRoots.size(); + for (int i = 0; i < numRoots; ++i) { + WindowManager.LayoutParams params = mParams.get(i); + if (params.token == null) { + continue; + } + if (params.token != token) { + boolean isChild = false; + if (params.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW + && params.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) { + for (int j = 0 ; j < numRoots; ++j) { + View viewj = mViews.get(j); + WindowManager.LayoutParams paramsj = mParams.get(j); + if (params.token == viewj.getWindowToken() + && paramsj.token == token) { + isChild = true; + break; + } + } + } + if (!isChild) { + continue; + } + } + views.add(mRoots.get(i)); + } + } + return views; + } + public View getRootView(String name) { synchronized (mLock) { for (int i = mRoots.size() - 1; i >= 0; --i) { @@ -213,15 +246,15 @@ public final class WindowManagerGlobal { throw new IllegalArgumentException("Params must be WindowManager.LayoutParams"); } - final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params; + final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params; if (parentWindow != null) { parentWindow.adjustLayoutParamsForSubWindow(wparams); } else { - // If there's no parent and we're running on L or above (or in the - // system context), assume we want hardware acceleration. + // If there's no parent, then hardware acceleration for this view is + // set from the application's hardware acceleration setting. final Context context = view.getContext(); if (context != null - && context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP) { + && context.getApplicationInfo().hardwareAccelerated) { wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; } } @@ -459,7 +492,7 @@ public final class WindowManagerGlobal { } } - public void dumpGfxInfo(FileDescriptor fd) { + public void dumpGfxInfo(FileDescriptor fd, String[] args) { FileOutputStream fout = new FileOutputStream(fd); PrintWriter pw = new FastPrintWriter(fout); try { @@ -476,7 +509,7 @@ public final class WindowManagerGlobal { HardwareRenderer renderer = root.getView().mAttachInfo.mHardwareRenderer; if (renderer != null) { - renderer.dumpGfxInfo(pw, fd); + renderer.dumpGfxInfo(pw, fd, args); } } diff --git a/core/java/android/view/WindowManagerInternal.java b/core/java/android/view/WindowManagerInternal.java index f557b97..7b4640b 100644 --- a/core/java/android/view/WindowManagerInternal.java +++ b/core/java/android/view/WindowManagerInternal.java @@ -20,7 +20,7 @@ import android.graphics.Rect; import android.graphics.Region; import android.hardware.display.DisplayManagerInternal; import android.os.IBinder; -import android.os.IRemoteCallback; +import android.view.animation.Animation; import java.util.List; @@ -85,6 +85,41 @@ public abstract class WindowManagerInternal { } /** + * Abstract class to be notified about {@link com.android.server.wm.AppTransition} events. Held + * as an abstract class so a listener only needs to implement the methods of its interest. + */ + public static abstract class AppTransitionListener { + + /** + * Called when an app transition is being setup and about to be executed. + */ + public void onAppTransitionPendingLocked() {} + + /** + * Called when a pending app transition gets cancelled. + */ + public void onAppTransitionCancelledLocked() {} + + /** + * Called when an app transition gets started + * + * @param openToken the token for the opening app + * @param closeToken the token for the closing app + * @param openAnimation the animation for the opening app + * @param closeAnimation the animation for the closing app + */ + public void onAppTransitionStartingLocked(IBinder openToken, IBinder closeToken, + Animation openAnimation, Animation closeAnimation) {} + + /** + * Called when an app transition is finished running. + * + * @param token the token for app whose transition has finished + */ + public void onAppTransitionFinishedLocked(IBinder token) {} + } + + /** * Request that the window manager call * {@link DisplayManagerInternal#performTraversalInTransactionFromWindowManager} * within a surface transaction at a later time. @@ -189,4 +224,11 @@ public abstract class WindowManagerInternal { * @param removeWindows Whether to also remove the windows associated with the token. */ public abstract void removeWindowToken(android.os.IBinder token, boolean removeWindows); + + /** + * Registers a listener to be notified about app transition events. + * + * @param listener The listener to register. + */ + public abstract void registerAppTransitionListener(AppTransitionListener listener); } diff --git a/core/java/android/view/WindowManagerPolicy.java b/core/java/android/view/WindowManagerPolicy.java index 780ca99..9199af1 100644 --- a/core/java/android/view/WindowManagerPolicy.java +++ b/core/java/android/view/WindowManagerPolicy.java @@ -365,6 +365,11 @@ public interface WindowManagerPolicy { * @return true if window is on default display. */ public boolean isDefaultDisplay(); + + /** + * Check whether the window is currently dimming. + */ + public boolean isDimming(); } /** @@ -587,13 +592,6 @@ public interface WindowManagerPolicy { public int getMaxWallpaperLayer(); /** - * Return the window layer at which windows appear above the normal - * universe (that is no longer impacted by the universe background - * transform). - */ - public int getAboveUniverseLayer(); - - /** * Return the display width available after excluding any screen * decorations that can never be removed. That is, system bar or * button bar. diff --git a/core/java/android/view/accessibility/AccessibilityInteractionClient.java b/core/java/android/view/accessibility/AccessibilityInteractionClient.java index cefd34d..db78ec5 100644 --- a/core/java/android/view/accessibility/AccessibilityInteractionClient.java +++ b/core/java/android/view/accessibility/AccessibilityInteractionClient.java @@ -17,7 +17,6 @@ package android.view.accessibility; import android.accessibilityservice.IAccessibilityServiceConnection; -import android.graphics.Point; import android.os.Binder; import android.os.Build; import android.os.Bundle; diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java index b5afdf7..6096d7d 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -721,7 +721,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @return Whether the refresh succeeded. */ public boolean refresh() { - return refresh(false); + return refresh(true); } /** diff --git a/core/java/android/view/animation/Animation.java b/core/java/android/view/animation/Animation.java index 85d77cb..be43952 100644 --- a/core/java/android/view/animation/Animation.java +++ b/core/java/android/view/animation/Animation.java @@ -16,6 +16,8 @@ package android.view.animation; +import android.annotation.ColorInt; +import android.annotation.InterpolatorRes; import android.content.Context; import android.content.res.TypedArray; import android.graphics.RectF; @@ -387,7 +389,7 @@ public abstract class Animation implements Cloneable { * @param resID The resource identifier of the interpolator to load * @attr ref android.R.styleable#Animation_interpolator */ - public void setInterpolator(Context context, int resID) { + public void setInterpolator(Context context, @InterpolatorRes int resID) { setInterpolator(AnimationUtils.loadInterpolator(context, resID)); } @@ -622,7 +624,7 @@ public abstract class Animation implements Cloneable { * @param bg The background color. If 0, no background. Currently must * be black, with any desired alpha level. */ - public void setBackgroundColor(int bg) { + public void setBackgroundColor(@ColorInt int bg) { mBackgroundColor = bg; } @@ -753,6 +755,7 @@ public abstract class Animation implements Cloneable { /** * Returns the background color behind the animation. */ + @ColorInt public int getBackgroundColor() { return mBackgroundColor; } diff --git a/core/java/android/view/animation/AnimationUtils.java b/core/java/android/view/animation/AnimationUtils.java index 606c83e..4d1209a 100644 --- a/core/java/android/view/animation/AnimationUtils.java +++ b/core/java/android/view/animation/AnimationUtils.java @@ -19,6 +19,8 @@ package android.view.animation; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import android.annotation.AnimRes; +import android.annotation.InterpolatorRes; import android.content.Context; import android.content.res.Resources; import android.content.res.Resources.Theme; @@ -65,7 +67,7 @@ public class AnimationUtils { * @return The animation object reference by the specified id * @throws NotFoundException when the animation cannot be loaded */ - public static Animation loadAnimation(Context context, int id) + public static Animation loadAnimation(Context context, @AnimRes int id) throws NotFoundException { XmlResourceParser parser = null; @@ -143,7 +145,7 @@ public class AnimationUtils { * @return The animation object reference by the specified id * @throws NotFoundException when the layout animation controller cannot be loaded */ - public static LayoutAnimationController loadLayoutAnimation(Context context, int id) + public static LayoutAnimationController loadLayoutAnimation(Context context, @AnimRes int id) throws NotFoundException { XmlResourceParser parser = null; @@ -266,7 +268,8 @@ public class AnimationUtils { * @return The animation object reference by the specified id * @throws NotFoundException */ - public static Interpolator loadInterpolator(Context context, int id) throws NotFoundException { + public static Interpolator loadInterpolator(Context context, @InterpolatorRes int id) + throws NotFoundException { XmlResourceParser parser = null; try { parser = context.getResources().getAnimation(id); diff --git a/core/java/android/view/animation/ClipRectAnimation.java b/core/java/android/view/animation/ClipRectAnimation.java index 2361501..e194927 100644 --- a/core/java/android/view/animation/ClipRectAnimation.java +++ b/core/java/android/view/animation/ClipRectAnimation.java @@ -26,8 +26,8 @@ import android.graphics.Rect; * @hide */ public class ClipRectAnimation extends Animation { - private Rect mFromRect = new Rect(); - private Rect mToRect = new Rect(); + protected Rect mFromRect = new Rect(); + protected Rect mToRect = new Rect(); /** * Constructor to use when building a ClipRectAnimation from code @@ -43,6 +43,15 @@ public class ClipRectAnimation extends Animation { mToRect.set(toClip); } + /** + * Constructor to use when building a ClipRectAnimation from code + */ + public ClipRectAnimation(int fromL, int fromT, int fromR, int fromB, + int toL, int toT, int toR, int toB) { + mFromRect.set(fromL, fromT, fromR, fromB); + mToRect.set(toL, toT, toR, toB); + } + @Override protected void applyTransformation(float it, Transformation tr) { int l = mFromRect.left + (int) ((mToRect.left - mFromRect.left) * it); diff --git a/core/java/android/view/animation/ClipRectLRAnimation.java b/core/java/android/view/animation/ClipRectLRAnimation.java new file mode 100644 index 0000000..8993cd3 --- /dev/null +++ b/core/java/android/view/animation/ClipRectLRAnimation.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2015 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; + +import android.graphics.Rect; + +/** + * Special case of ClipRectAnimation that animates only the left/right + * dimensions of the clip, picking up the other dimensions from whatever is + * set on the transform already. + * + * @hide + */ +public class ClipRectLRAnimation extends ClipRectAnimation { + + /** + * Constructor. Passes in 0 for Top/Bottom parameters of ClipRectAnimation + */ + public ClipRectLRAnimation(int fromL, int fromR, int toL, int toR) { + super(fromL, 0, fromR, 0, toL, 0, toR, 0); + } + + /** + * Calculates and sets clip rect on given transformation. It uses existing values + * on the Transformation for Top/Bottom clip parameters. + */ + @Override + protected void applyTransformation(float it, Transformation tr) { + Rect oldClipRect = tr.getClipRect(); + tr.setClipRect(mFromRect.left + (int) ((mToRect.left - mFromRect.left) * it), + oldClipRect.top, + mFromRect.right + (int) ((mToRect.right - mFromRect.right) * it), + oldClipRect.bottom); + } +} diff --git a/core/java/android/view/animation/ClipRectTBAnimation.java b/core/java/android/view/animation/ClipRectTBAnimation.java new file mode 100644 index 0000000..06f86ce --- /dev/null +++ b/core/java/android/view/animation/ClipRectTBAnimation.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2015 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; + +import android.graphics.Rect; + +/** + * Special case of ClipRectAnimation that animates only the top/bottom + * dimensions of the clip, picking up the other dimensions from whatever is + * set on the transform already. + * + * @hide + */ +public class ClipRectTBAnimation extends ClipRectAnimation { + + /** + * Constructor. Passes in 0 for Left/Right parameters of ClipRectAnimation + */ + public ClipRectTBAnimation(int fromT, int fromB, int toT, int toB) { + super(0, fromT, 0, fromB, 0, toT, 0, toB); + } + + /** + * Calculates and sets clip rect on given transformation. It uses existing values + * on the Transformation for Left/Right clip parameters. + */ + @Override + protected void applyTransformation(float it, Transformation tr) { + Rect oldClipRect = tr.getClipRect(); + tr.setClipRect(oldClipRect.left, mFromRect.top + (int) ((mToRect.top - mFromRect.top) * it), + oldClipRect.right, + mFromRect.bottom + (int) ((mToRect.bottom - mFromRect.bottom) * it)); + } + +} diff --git a/core/java/android/view/animation/LayoutAnimationController.java b/core/java/android/view/animation/LayoutAnimationController.java index 882e738..df2f18c 100644 --- a/core/java/android/view/animation/LayoutAnimationController.java +++ b/core/java/android/view/animation/LayoutAnimationController.java @@ -16,6 +16,8 @@ package android.view.animation; +import android.annotation.AnimRes; +import android.annotation.InterpolatorRes; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; @@ -180,7 +182,7 @@ public class LayoutAnimationController { * * @attr ref android.R.styleable#LayoutAnimation_animation */ - public void setAnimation(Context context, int resourceID) { + public void setAnimation(Context context, @AnimRes int resourceID) { setAnimation(AnimationUtils.loadAnimation(context, resourceID)); } @@ -225,7 +227,7 @@ public class LayoutAnimationController { * * @attr ref android.R.styleable#LayoutAnimation_interpolator */ - public void setInterpolator(Context context, int resourceID) { + public void setInterpolator(Context context, @InterpolatorRes int resourceID) { setInterpolator(AnimationUtils.loadInterpolator(context, resourceID)); } diff --git a/core/java/android/view/animation/Transformation.java b/core/java/android/view/animation/Transformation.java index 2f4fe73..8eb5b5c 100644 --- a/core/java/android/view/animation/Transformation.java +++ b/core/java/android/view/animation/Transformation.java @@ -16,6 +16,7 @@ package android.view.animation; +import android.annotation.FloatRange; import android.graphics.Matrix; import android.graphics.Rect; @@ -122,7 +123,13 @@ public class Transformation { mAlpha *= t.getAlpha(); mMatrix.preConcat(t.getMatrix()); if (t.mHasClipRect) { - setClipRect(t.getClipRect()); + Rect bounds = t.getClipRect(); + if (mHasClipRect) { + setClipRect(mClipRect.left + bounds.left, mClipRect.top + bounds.top, + mClipRect.right + bounds.right, mClipRect.bottom + bounds.bottom); + } else { + setClipRect(bounds); + } } } @@ -135,7 +142,13 @@ public class Transformation { mAlpha *= t.getAlpha(); mMatrix.postConcat(t.getMatrix()); if (t.mHasClipRect) { - setClipRect(t.getClipRect()); + Rect bounds = t.getClipRect(); + if (mHasClipRect) { + setClipRect(mClipRect.left + bounds.left, mClipRect.top + bounds.top, + mClipRect.right + bounds.right, mClipRect.bottom + bounds.bottom); + } else { + setClipRect(bounds); + } } } @@ -151,7 +164,7 @@ public class Transformation { * Sets the degree of transparency * @param alpha 1.0 means fully opaqe and 0.0 means fully transparent */ - public void setAlpha(float alpha) { + public void setAlpha(@FloatRange(from=0.0, to=1.0) float alpha) { mAlpha = alpha; } diff --git a/core/java/android/view/animation/TranslateAnimation.java b/core/java/android/view/animation/TranslateAnimation.java index d2ff754..216022b 100644 --- a/core/java/android/view/animation/TranslateAnimation.java +++ b/core/java/android/view/animation/TranslateAnimation.java @@ -24,7 +24,7 @@ import android.util.AttributeSet; * An animation that controls the position of an object. See the * {@link android.view.animation full package} description for details and * sample code. - * + * */ public class TranslateAnimation extends Animation { private int mFromXType = ABSOLUTE; @@ -33,20 +33,28 @@ public class TranslateAnimation extends Animation { private int mFromYType = ABSOLUTE; private int mToYType = ABSOLUTE; - private float mFromXValue = 0.0f; - private float mToXValue = 0.0f; - - private float mFromYValue = 0.0f; - private float mToYValue = 0.0f; - - private float mFromXDelta; - private float mToXDelta; - private float mFromYDelta; - private float mToYDelta; + /** @hide */ + protected float mFromXValue = 0.0f; + /** @hide */ + protected float mToXValue = 0.0f; + + /** @hide */ + protected float mFromYValue = 0.0f; + /** @hide */ + protected float mToYValue = 0.0f; + + /** @hide */ + protected float mFromXDelta; + /** @hide */ + protected float mToXDelta; + /** @hide */ + protected float mFromYDelta; + /** @hide */ + protected float mToYDelta; /** * Constructor used when a TranslateAnimation is loaded from a resource. - * + * * @param context Application context to use * @param attrs Attribute set from which to read values */ @@ -81,7 +89,7 @@ public class TranslateAnimation extends Animation { /** * Constructor to use when building a TranslateAnimation from code - * + * * @param fromXDelta Change in X coordinate to apply at the start of the * animation * @param toXDelta Change in X coordinate to apply at the end of the diff --git a/core/java/android/view/animation/TranslateXAnimation.java b/core/java/android/view/animation/TranslateXAnimation.java new file mode 100644 index 0000000..d75323f --- /dev/null +++ b/core/java/android/view/animation/TranslateXAnimation.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2015 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; + +import android.graphics.Matrix; + +/** + * Special case of TranslateAnimation that translates only horizontally, picking up the + * vertical values from whatever is set on the Transformation already. When used in + * conjunction with a TranslateYAnimation, allows independent animation of x and y + * position. + * @hide + */ +public class TranslateXAnimation extends TranslateAnimation { + float[] mTmpValues = new float[9]; + + /** + * Constructor. Passes in 0 for the y parameters of TranslateAnimation + */ + public TranslateXAnimation(float fromXDelta, float toXDelta) { + super(fromXDelta, toXDelta, 0, 0); + } + + /** + * Constructor. Passes in 0 for the y parameters of TranslateAnimation + */ + public TranslateXAnimation(int fromXType, float fromXValue, int toXType, float toXValue) { + super(fromXType, fromXValue, toXType, toXValue, ABSOLUTE, 0, ABSOLUTE, 0); + } + + /** + * Calculates and sets x translation values on given transformation. + */ + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + Matrix m = t.getMatrix(); + m.getValues(mTmpValues); + float dx = mFromXDelta + ((mToXDelta - mFromXDelta) * interpolatedTime); + t.getMatrix().setTranslate(dx, mTmpValues[Matrix.MTRANS_Y]); + } +} diff --git a/core/java/android/view/animation/TranslateYAnimation.java b/core/java/android/view/animation/TranslateYAnimation.java new file mode 100644 index 0000000..714558d --- /dev/null +++ b/core/java/android/view/animation/TranslateYAnimation.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2015 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; + +import android.graphics.Matrix; + +/** + * Special case of TranslateAnimation that translates only vertically, picking up the + * horizontal values from whatever is set on the Transformation already. When used in + * conjunction with a TranslateXAnimation, allows independent animation of x and y + * position. + * @hide + */ +public class TranslateYAnimation extends TranslateAnimation { + float[] mTmpValues = new float[9]; + + /** + * Constructor. Passes in 0 for the x parameters of TranslateAnimation + */ + public TranslateYAnimation(float fromYDelta, float toYDelta) { + super(0, 0, fromYDelta, toYDelta); + } + + /** + * Constructor. Passes in 0 for the x parameters of TranslateAnimation + */ + public TranslateYAnimation(int fromYType, float fromYValue, int toYType, float toYValue) { + super(ABSOLUTE, 0, ABSOLUTE, 0, fromYType, fromYValue, toYType, toYValue); + } + + /** + * Calculates and sets y translation values on given transformation. + */ + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + Matrix m = t.getMatrix(); + m.getValues(mTmpValues); + float dy = mFromYDelta + ((mToYDelta - mFromYDelta) * interpolatedTime); + t.getMatrix().setTranslate(mTmpValues[Matrix.MTRANS_X], dy); + } +} diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 1416e1b..78604bf 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -27,7 +27,6 @@ import com.android.internal.view.InputBindResult; import android.content.Context; import android.graphics.Matrix; import android.graphics.Rect; -import android.graphics.RectF; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; diff --git a/core/java/android/view/inputmethod/InputMethodSubtype.java b/core/java/android/view/inputmethod/InputMethodSubtype.java index 1671faa..c2f3777 100644 --- a/core/java/android/view/inputmethod/InputMethodSubtype.java +++ b/core/java/android/view/inputmethod/InputMethodSubtype.java @@ -23,6 +23,8 @@ import android.os.Parcelable; import android.text.TextUtils; import android.util.Slog; +import com.android.internal.inputmethod.InputMethodUtils; + import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -379,7 +381,7 @@ public final class InputMethodSubtype implements Parcelable { */ public CharSequence getDisplayName( Context context, String packageName, ApplicationInfo appInfo) { - final Locale locale = constructLocaleFromString(mSubtypeLocale); + final Locale locale = InputMethodUtils.constructLocaleFromString(mSubtypeLocale); final String localeStr = locale != null ? locale.getDisplayName() : mSubtypeLocale; if (mSubtypeNameResId == 0) { return localeStr; @@ -503,22 +505,6 @@ public final class InputMethodSubtype implements Parcelable { } }; - private static Locale constructLocaleFromString(String localeStr) { - if (TextUtils.isEmpty(localeStr)) - return null; - String[] localeParams = localeStr.split("_", 3); - // The length of localeStr is guaranteed to always return a 1 <= value <= 3 - // because localeStr is not empty. - if (localeParams.length == 1) { - return new Locale(localeParams[0]); - } else if (localeParams.length == 2) { - return new Locale(localeParams[0], localeParams[1]); - } else if (localeParams.length == 3) { - return new Locale(localeParams[0], localeParams[1], localeParams[2]); - } - return null; - } - private static int hashCodeInternal(String locale, String mode, String extraValue, boolean isAuxiliary, boolean overridesImplicitlyEnabledSubtype, boolean isAsciiCapable) { diff --git a/core/java/android/view/inputmethod/InputMethodSubtypeArray.java b/core/java/android/view/inputmethod/InputMethodSubtypeArray.java index 5bef71f..6a748ce 100644 --- a/core/java/android/view/inputmethod/InputMethodSubtypeArray.java +++ b/core/java/android/view/inputmethod/InputMethodSubtypeArray.java @@ -17,15 +17,10 @@ package android.view.inputmethod; import android.os.Parcel; -import android.os.Parcelable; -import android.util.AndroidRuntimeException; import android.util.Slog; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; import java.util.List; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; @@ -203,43 +198,20 @@ public class InputMethodSubtypeArray { } private static byte[] compress(final byte[] data) { - ByteArrayOutputStream resultStream = null; - GZIPOutputStream zipper = null; - try { - resultStream = new ByteArrayOutputStream(); - zipper = new GZIPOutputStream(resultStream); + try (final ByteArrayOutputStream resultStream = new ByteArrayOutputStream(); + final GZIPOutputStream zipper = new GZIPOutputStream(resultStream)) { zipper.write(data); - } catch(IOException e) { + zipper.finish(); + return resultStream.toByteArray(); + } catch(Exception e) { + Slog.e(TAG, "Failed to compress the data.", e); return null; - } finally { - try { - if (zipper != null) { - zipper.close(); - } - } catch (IOException e) { - zipper = null; - Slog.e(TAG, "Failed to close the stream.", e); - // swallowed, not propagated back to the caller - } - try { - if (resultStream != null) { - resultStream.close(); - } - } catch (IOException e) { - resultStream = null; - Slog.e(TAG, "Failed to close the stream.", e); - // swallowed, not propagated back to the caller - } } - return resultStream != null ? resultStream.toByteArray() : null; } private static byte[] decompress(final byte[] data, final int expectedSize) { - ByteArrayInputStream inputStream = null; - GZIPInputStream unzipper = null; - try { - inputStream = new ByteArrayInputStream(data); - unzipper = new GZIPInputStream(inputStream); + try (final ByteArrayInputStream inputStream = new ByteArrayInputStream(data); + final GZIPInputStream unzipper = new GZIPInputStream(inputStream)) { final byte [] result = new byte[expectedSize]; int totalReadBytes = 0; while (totalReadBytes < result.length) { @@ -254,25 +226,9 @@ public class InputMethodSubtypeArray { return null; } return result; - } catch(IOException e) { + } catch(Exception e) { + Slog.e(TAG, "Failed to decompress the data.", e); return null; - } finally { - try { - if (unzipper != null) { - unzipper.close(); - } - } catch (IOException e) { - Slog.e(TAG, "Failed to close the stream.", e); - // swallowed, not propagated back to the caller - } - try { - if (inputStream != null) { - inputStream.close(); - } - } catch (IOException e) { - Slog.e(TAG, "Failed to close the stream.", e); - // swallowed, not propagated back to the caller - } } } } diff --git a/core/java/android/webkit/CookieManager.java b/core/java/android/webkit/CookieManager.java index eca96f9..6d5fac1 100644 --- a/core/java/android/webkit/CookieManager.java +++ b/core/java/android/webkit/CookieManager.java @@ -31,10 +31,7 @@ public abstract class CookieManager { } /** - * Gets the singleton CookieManager instance. If this method is used - * before the application instantiates a {@link WebView} instance, - * {@link CookieSyncManager#createInstance CookieSyncManager.createInstance(Context)} - * must be called first. + * Gets the singleton CookieManager instance. * * @return the singleton CookieManager instance */ diff --git a/core/java/android/webkit/CookieSyncManager.java b/core/java/android/webkit/CookieSyncManager.java index d9546ca..eda8d36 100644 --- a/core/java/android/webkit/CookieSyncManager.java +++ b/core/java/android/webkit/CookieSyncManager.java @@ -17,7 +17,6 @@ package android.webkit; import android.content.Context; -import android.util.Log; /** diff --git a/core/java/android/webkit/LegacyErrorStrings.java b/core/java/android/webkit/LegacyErrorStrings.java index 11fc05d..60a6ee1 100644 --- a/core/java/android/webkit/LegacyErrorStrings.java +++ b/core/java/android/webkit/LegacyErrorStrings.java @@ -17,7 +17,6 @@ package android.webkit; import android.content.Context; -import android.net.http.EventHandler; import android.util.Log; /** @@ -44,52 +43,52 @@ class LegacyErrorStrings { */ private static int getResource(int errorCode) { switch(errorCode) { - case EventHandler.OK: + case 0: /* EventHandler.OK: */ return com.android.internal.R.string.httpErrorOk; - case EventHandler.ERROR: + case -1: /* EventHandler.ERROR: */ return com.android.internal.R.string.httpError; - case EventHandler.ERROR_LOOKUP: + case -2: /* EventHandler.ERROR_LOOKUP: */ return com.android.internal.R.string.httpErrorLookup; - case EventHandler.ERROR_UNSUPPORTED_AUTH_SCHEME: + case -3: /* EventHandler.ERROR_UNSUPPORTED_AUTH_SCHEME: */ return com.android.internal.R.string.httpErrorUnsupportedAuthScheme; - case EventHandler.ERROR_AUTH: + case -4: /* EventHandler.ERROR_AUTH: */ return com.android.internal.R.string.httpErrorAuth; - case EventHandler.ERROR_PROXYAUTH: + case -5: /* EventHandler.ERROR_PROXYAUTH: */ return com.android.internal.R.string.httpErrorProxyAuth; - case EventHandler.ERROR_CONNECT: + case -6: /* EventHandler.ERROR_CONNECT: */ return com.android.internal.R.string.httpErrorConnect; - case EventHandler.ERROR_IO: + case -7: /* EventHandler.ERROR_IO: */ return com.android.internal.R.string.httpErrorIO; - case EventHandler.ERROR_TIMEOUT: + case -8: /* EventHandler.ERROR_TIMEOUT: */ return com.android.internal.R.string.httpErrorTimeout; - case EventHandler.ERROR_REDIRECT_LOOP: + case -9: /* EventHandler.ERROR_REDIRECT_LOOP: */ return com.android.internal.R.string.httpErrorRedirectLoop; - case EventHandler.ERROR_UNSUPPORTED_SCHEME: + case -10: /* EventHandler.ERROR_UNSUPPORTED_SCHEME: */ return com.android.internal.R.string.httpErrorUnsupportedScheme; - case EventHandler.ERROR_FAILED_SSL_HANDSHAKE: + case -11: /* EventHandler.ERROR_FAILED_SSL_HANDSHAKE: */ return com.android.internal.R.string.httpErrorFailedSslHandshake; - case EventHandler.ERROR_BAD_URL: + case -12: /* EventHandler.ERROR_BAD_URL: */ return com.android.internal.R.string.httpErrorBadUrl; - case EventHandler.FILE_ERROR: + case -13: /* EventHandler.FILE_ERROR: */ return com.android.internal.R.string.httpErrorFile; - case EventHandler.FILE_NOT_FOUND_ERROR: + case -14: /* EventHandler.FILE_NOT_FOUND_ERROR: */ return com.android.internal.R.string.httpErrorFileNotFound; - case EventHandler.TOO_MANY_REQUESTS_ERROR: + case -15: /* EventHandler.TOO_MANY_REQUESTS_ERROR: */ return com.android.internal.R.string.httpErrorTooManyRequests; default: diff --git a/core/java/android/webkit/WebBackForwardList.java b/core/java/android/webkit/WebBackForwardList.java index e671376..6f763dc 100644 --- a/core/java/android/webkit/WebBackForwardList.java +++ b/core/java/android/webkit/WebBackForwardList.java @@ -16,7 +16,6 @@ package android.webkit; -import android.annotation.SystemApi; import java.io.Serializable; /** diff --git a/core/java/android/webkit/WebChromeClient.java b/core/java/android/webkit/WebChromeClient.java index 768dc9f..4737e9b 100644 --- a/core/java/android/webkit/WebChromeClient.java +++ b/core/java/android/webkit/WebChromeClient.java @@ -70,13 +70,14 @@ public class WebChromeClient { } /** - * Notify the host application that the current page would - * like to show a custom View. This is used for Fullscreen - * video playback; see "HTML5 Video support" documentation on + * Notify the host application that the current page has entered full + * screen mode. The host application must show the custom View which + * contains the web contents — video or other HTML content — + * in full screen mode. Also see "Full screen support" documentation on * {@link WebView}. * @param view is the View object to be shown. - * @param callback is the callback to be invoked if and when the view - * is dismissed. + * @param callback invoke this callback to request the page to exit + * full screen mode. */ public void onShowCustomView(View view, CustomViewCallback callback) {}; @@ -96,8 +97,10 @@ public class WebChromeClient { CustomViewCallback callback) {}; /** - * Notify the host application that the current page would - * like to hide its custom view. + * Notify the host application that the current page has exited full + * screen mode. The host application must hide the custom View, ie. the + * View passed to {@link #onShowCustomView} when the content entered fullscreen. + * Also see "Full screen support" documentation on {@link WebView}. */ public void onHideCustomView() {} @@ -377,10 +380,9 @@ public class WebChromeClient { } /** - * When the user starts to playback a video element, it may take time for enough - * data to be buffered before the first frames can be rendered. While this buffering - * is taking place, the ChromeClient can use this function to provide a View to be - * displayed. For example, the ChromeClient could show a spinner animation. + * Obtains a View to be displayed while buffering of full screen video is taking + * place. The host application can override this method to provide a View + * containing a spinner or similar. * * @return View The View to be displayed whilst the video is loading. */ diff --git a/core/java/android/webkit/WebMessage.java b/core/java/android/webkit/WebMessage.java new file mode 100644 index 0000000..7683a40 --- /dev/null +++ b/core/java/android/webkit/WebMessage.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2015 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.webkit; + +/** + * The Java representation of the HTML5 PostMessage event. See + * https://html.spec.whatwg.org/multipage/comms.html#the-messageevent-interfaces + * for definition of a MessageEvent in HTML5. + * + */ +public class WebMessage { + + private String mData; + private WebMessagePort[] mPorts; + + /** + * Creates a WebMessage. + * @param data the data of the message. + */ + public WebMessage(String data) { + mData = data; + } + + /** + * Creates a WebMessage. + * @param data the data of the message. + * @param ports the ports that are sent with the message. + */ + public WebMessage(String data, WebMessagePort[] ports) { + mData = data; + mPorts = ports; + } + + /** + * Returns the data of the message. + */ + public String getData() { + return mData; + } + + /** + * Returns the ports that are sent with the message, or null if no port + * is sent. + */ + public WebMessagePort[] getPorts() { + return mPorts; + } +} diff --git a/core/java/android/webkit/WebMessagePort.java b/core/java/android/webkit/WebMessagePort.java new file mode 100644 index 0000000..eab27bd --- /dev/null +++ b/core/java/android/webkit/WebMessagePort.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2015 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.webkit; + +import android.os.Handler; + +/** + * The Java representation of the HTML5 Message Port. See + * https://html.spec.whatwg.org/multipage/comms.html#messageport + * for definition of MessagePort in HTML5. + * + * A Message port represents one endpoint of a Message Channel. In Android + * webview, there is no separate Message Channel object. When a message channel + * is created, both ports are tangled to each other and started, and then + * returned in a MessagePort array, see {@link WebView#createWebMessageChannel} + * for creating a message channel. + * + * When a message port is first created or received via transfer, it does not + * have a WebMessageCallback to receive web messages. The messages are queued until + * a WebMessageCallback is set. + */ +public abstract class WebMessagePort { + + /** + * The listener for handling MessagePort events. The message callback + * methods are called on the main thread. If the embedder application + * wants to receive the messages on a different thread, it can do this + * by passing a Handler in + * {@link WebMessagePort#setWebMessageCallback(WebMessageCallback, Handler)}. + * In the latter case, the application should be extra careful for thread safety + * since WebMessagePort methods should be called on main thread. + */ + public static abstract class WebMessageCallback { + /** + * Message callback for receiving onMessage events. + * + * @param port the WebMessagePort that the message is destined for + * @param message the message from the entangled port. + */ + public void onMessage(WebMessagePort port, WebMessage message) { } + } + + /** + * Post a WebMessage to the entangled port. + * + * @param message the message from Java to JS. + * + * @throws IllegalStateException If message port is already transferred or closed. + */ + public abstract void postMessage(WebMessage message); + + /** + * Close the message port and free any resources associated with it. + */ + public abstract void close(); + + /** + * Sets a callback to receive message events on the main thread. + * + * @param callback the message callback. + */ + public abstract void setWebMessageCallback(WebMessageCallback callback); + + /** + * Sets a callback to receive message events on the handler that is provided + * by the application. + * + * @param callback the message callback. + * @param handler the handler to receive the message messages. + */ + public abstract void setWebMessageCallback(WebMessageCallback callback, Handler handler); +} diff --git a/core/java/android/webkit/WebResourceError.java b/core/java/android/webkit/WebResourceError.java new file mode 100644 index 0000000..080d174 --- /dev/null +++ b/core/java/android/webkit/WebResourceError.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2015 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.webkit; + +/** + * Encapsulates information about errors occured during loading of web resources. See + * {@link WebViewClient#onReceivedError(WebView, WebResourceRequest, WebResourceError) WebViewClient.onReceivedError(WebView, WebResourceRequest, WebResourceError)} + */ +public abstract class WebResourceError { + /** + * Gets the error code of the error. The code corresponds to one + * of the ERROR_* constants in {@link WebViewClient}. + * + * @return The error code of the error + */ + public abstract int getErrorCode(); + + /** + * Gets the string describing the error. Descriptions are localized, + * and thus can be used for communicating the problem to the user. + * + * @return The description of the error + */ + public abstract String getDescription(); +} diff --git a/core/java/android/webkit/WebResourceRequest.java b/core/java/android/webkit/WebResourceRequest.java index 2185658..0760d2b 100644 --- a/core/java/android/webkit/WebResourceRequest.java +++ b/core/java/android/webkit/WebResourceRequest.java @@ -18,7 +18,6 @@ package android.webkit; import android.net.Uri; -import java.io.InputStream; import java.util.Map; /** @@ -42,6 +41,8 @@ public interface WebResourceRequest { /** * Gets whether a gesture (such as a click) was associated with the request. + * For security reasons in certain situations this method may return false even though the + * sequence of events which caused the request to be created was initiated by a user gesture. * * @return whether a gesture was associated with the request. */ diff --git a/core/java/android/webkit/WebResourceResponse.java b/core/java/android/webkit/WebResourceResponse.java index ad6e9aa..a42aaa7 100644 --- a/core/java/android/webkit/WebResourceResponse.java +++ b/core/java/android/webkit/WebResourceResponse.java @@ -17,6 +17,7 @@ package android.webkit; import java.io.InputStream; +import java.io.StringBufferInputStream; import java.util.Map; /** @@ -24,7 +25,7 @@ import java.util.Map; * class from {@link WebViewClient#shouldInterceptRequest} to provide a custom * response when the WebView requests a particular resource. */ -public class WebResourceResponse { +public class WebResourceResponse extends WebResourceResponseBase { private String mMimeType; private String mEncoding; private int mStatusCode; @@ -40,13 +41,14 @@ public class WebResourceResponse { * * @param mimeType the resource response's MIME type, for example text/html * @param encoding the resource response's encoding - * @param data the input stream that provides the resource response's data + * @param data the input stream that provides the resource response's data. Must not be a + * StringBufferInputStream. */ public WebResourceResponse(String mimeType, String encoding, InputStream data) { mMimeType = mimeType; mEncoding = encoding; - mInputStream = data; + setData(data); } /** @@ -62,7 +64,8 @@ public class WebResourceResponse { * and not empty. * @param responseHeaders the resource response's headers represented as a mapping of header * name -> header value. - * @param data the input stream that provides the resource response's data + * @param data the input stream that provides the resource response's data. Must not be a + * StringBufferInputStream. */ public WebResourceResponse(String mimeType, String encoding, int statusCode, String reasonPhrase, Map<String, String> responseHeaders, InputStream data) { @@ -72,38 +75,36 @@ public class WebResourceResponse { } /** - * Sets the resource response's MIME type, for example text/html. + * Sets the resource response's MIME type, for example "text/html". * - * @param mimeType the resource response's MIME type + * @param mimeType The resource response's MIME type */ public void setMimeType(String mimeType) { mMimeType = mimeType; } /** - * Gets the resource response's MIME type. - * - * @return the resource response's MIME type + * {@inheritDoc} */ + @Override public String getMimeType() { return mMimeType; } /** - * Sets the resource response's encoding, for example UTF-8. This is used + * Sets the resource response's encoding, for example "UTF-8". This is used * to decode the data from the input stream. * - * @param encoding the resource response's encoding + * @param encoding The resource response's encoding */ public void setEncoding(String encoding) { mEncoding = encoding; } /** - * Gets the resource response's encoding. - * - * @return the resource response's encoding + * {@inheritDoc} */ + @Override public String getEncoding() { return mEncoding; } @@ -139,19 +140,17 @@ public class WebResourceResponse { } /** - * Gets the resource response's status code. - * - * @return the resource response's status code. + * {@inheritDoc} */ + @Override public int getStatusCode() { return mStatusCode; } /** - * Gets the description of the resource response's status code. - * - * @return the description of the resource response's status code. + * {@inheritDoc} */ + @Override public String getReasonPhrase() { return mReasonPhrase; } @@ -159,17 +158,16 @@ public class WebResourceResponse { /** * Sets the headers for the resource response. * - * @param headers mapping of header name -> header value. + * @param headers Mapping of header name -> header value. */ public void setResponseHeaders(Map<String, String> headers) { mResponseHeaders = headers; } /** - * Gets the headers for the resource response. - * - * @return the headers for the resource response. + * {@inheritDoc} */ + @Override public Map<String, String> getResponseHeaders() { return mResponseHeaders; } @@ -178,17 +176,22 @@ public class WebResourceResponse { * Sets the input stream that provides the resource response's data. Callers * must implement {@link InputStream#read(byte[]) InputStream.read(byte[])}. * - * @param data the input stream that provides the resource response's data + * @param data the input stream that provides the resource response's data. Must not be a + * StringBufferInputStream. */ public void setData(InputStream data) { + // If data is (or is a subclass of) StringBufferInputStream + if (data != null && StringBufferInputStream.class.isAssignableFrom(data.getClass())) { + throw new IllegalArgumentException("StringBufferInputStream is deprecated and must " + + "not be passed to a WebResourceResponse"); + } mInputStream = data; } /** - * Gets the input stream that provides the resource response's data. - * - * @return the input stream that provides the resource response's data + * {@inheritDoc} */ + @Override public InputStream getData() { return mInputStream; } diff --git a/core/java/android/webkit/WebResourceResponseBase.java b/core/java/android/webkit/WebResourceResponseBase.java new file mode 100644 index 0000000..cffde82 --- /dev/null +++ b/core/java/android/webkit/WebResourceResponseBase.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2015 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.webkit; + +import java.io.InputStream; +import java.util.Map; + +/** + * Encapsulates a resource response received from the server. + * This is an abstract class used by WebView callbacks. + */ +public abstract class WebResourceResponseBase { + /** + * Gets the resource response's MIME type. + * + * @return The resource response's MIME type + */ + public abstract String getMimeType(); + + /** + * Gets the resource response's encoding. + * + * @return The resource response's encoding + */ + public abstract String getEncoding(); + + /** + * Gets the resource response's status code. + * + * @return The resource response's status code. + */ + public abstract int getStatusCode(); + + /** + * Gets the description of the resource response's status code. + * + * @return The description of the resource response's status code. + */ + public abstract String getReasonPhrase(); + + /** + * Gets the headers for the resource response. + * + * @return The headers for the resource response. + */ + public abstract Map<String, String> getResponseHeaders(); + + /** + * Gets the input stream that provides the resource response's data. + * + * @return The input stream that provides the resource response's data + */ + public abstract InputStream getData(); +} diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java index 1d2c311..943beb0 100644 --- a/core/java/android/webkit/WebSettings.java +++ b/core/java/android/webkit/WebSettings.java @@ -1285,7 +1285,7 @@ public abstract class WebSettings { * strongly discouraged. * * @param mode The mixed content mode to use. One of {@link #MIXED_CONTENT_NEVER_ALLOW}, - * {@link #MIXED_CONTENT_NEVER_ALLOW} or {@link #MIXED_CONTENT_COMPATIBILITY_MODE}. + * {@link #MIXED_CONTENT_ALWAYS_ALLOW} or {@link #MIXED_CONTENT_COMPATIBILITY_MODE}. */ public abstract void setMixedContentMode(int mode); @@ -1293,7 +1293,7 @@ public abstract class WebSettings { * Gets the current behavior of the WebView with regard to loading insecure content from a * secure origin. * @return The current setting, one of {@link #MIXED_CONTENT_NEVER_ALLOW}, - * {@link #MIXED_CONTENT_NEVER_ALLOW} or {@link #MIXED_CONTENT_COMPATIBILITY_MODE}. + * {@link #MIXED_CONTENT_ALWAYS_ALLOW} or {@link #MIXED_CONTENT_COMPATIBILITY_MODE}. */ public abstract int getMixedContentMode(); @@ -1330,4 +1330,25 @@ public abstract class WebSettings { */ @SystemApi public abstract boolean getVideoOverlayForEmbeddedEncryptedVideoEnabled(); + + /** + * Sets whether this WebView should raster tiles when it is + * offscreen but attached to a window. Turning this on can avoid + * rendering artifacts when animating an offscreen WebView on-screen. + * Offscreen WebViews in this mode use more memory. The default value is + * false. + * Please follow these guidelines to limit memory usage: + * - WebView size should be not be larger than the device screen size. + * - Limit use of this mode to a small number of WebViews. Use it for + * visible WebViews and WebViews about to be animated to visible. + */ + public abstract void setOffscreenPreRaster(boolean enabled); + + /** + * Gets whether this WebView should raster tiles when it is + * offscreen but attached to a window. + * @return true if this WebView will raster tiles when it is + * offscreen but attached to a window. + */ + public abstract boolean getOffscreenPreRaster(); } diff --git a/core/java/android/webkit/WebSyncManager.java b/core/java/android/webkit/WebSyncManager.java index 402394f..801be12 100644 --- a/core/java/android/webkit/WebSyncManager.java +++ b/core/java/android/webkit/WebSyncManager.java @@ -17,11 +17,6 @@ package android.webkit; import android.content.Context; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.os.Process; -import android.util.Log; /* * @deprecated The WebSyncManager no longer does anything. diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index bab1f3b..6711a6b 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -27,6 +27,7 @@ import android.graphics.Picture; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.net.http.SslCertificate; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Looper; @@ -233,12 +234,48 @@ import java.util.Map; * * <h3>HTML5 Video support</h3> * - * <p>In order to support inline HTML5 video in your application, you need to have hardware - * acceleration turned on, and set a {@link android.webkit.WebChromeClient}. For full screen support, - * implementations of {@link WebChromeClient#onShowCustomView(View, WebChromeClient.CustomViewCallback)} - * and {@link WebChromeClient#onHideCustomView()} are required, - * {@link WebChromeClient#getVideoLoadingProgressView()} is optional. + * <p>In order to support inline HTML5 video in your application you need to have hardware + * acceleration turned on. * </p> + * + * <h3>Full screen support</h3> + * + * <p>In order to support full screen — for video or other HTML content — you need to set a + * {@link android.webkit.WebChromeClient} and implement both + * {@link WebChromeClient#onShowCustomView(View, WebChromeClient.CustomViewCallback)} + * and {@link WebChromeClient#onHideCustomView()}. If the implementation of either of these two methods is + * missing then the web contents will not be allowed to enter full screen. Optionally you can implement + * {@link WebChromeClient#getVideoLoadingProgressView()} to customize the View displayed whilst a video + * is loading. + * </p> + * + * <h3>Layout size</h3> + * <p> + * It is recommended to set the WebView layout height to a fixed value or to + * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT} instead of using + * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}. + * When using {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT} + * for the height none of the WebView's parents should use a + * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} layout height since that could result in + * incorrect sizing of the views. + * </p> + * + * <p>Setting the WebView's height to {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} + * enables the following behaviors: + * <ul> + * <li>The HTML body layout height is set to a fixed value. This means that elements with a height + * relative to the HTML body may not be sized correctly. </li> + * <li>For applications targetting {@link android.os.Build.VERSION_CODES#KITKAT} and earlier SDKs the + * HTML viewport meta tag will be ignored in order to preserve backwards compatibility. </li> + * </ul> + * </p> + * + * <p> + * Using a layout width of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} is not + * supported. If such a width is used the WebView will attempt to use the width of the parent + * instead. + * </p> + * */ // Implementation notes. // The WebView is a thin API class that delegates its public API to a backend WebViewProvider @@ -327,6 +364,20 @@ public class WebView extends AbsoluteLayout } /** + * Callback interface supplied to {@link #insertVisualStateCallback} for receiving + * notifications about the visual state. + */ + public static abstract class VisualStateCallback { + /** + * Invoked when the visual state is ready to be drawn in the next {@link #onDraw}. + * + * @param requestId the id supplied to the corresponding {@link #insertVisualStateCallback} + * request + */ + public abstract void onComplete(long requestId); + } + + /** * Interface to listen for new pictures as they change. * * @deprecated This interface is now obsolete. @@ -1107,6 +1158,60 @@ public class WebView extends AbsoluteLayout } /** + * Inserts a {@link VisualStateCallback}. + * + * <p>Updates to the the DOM are reflected asynchronously such that when the DOM is updated the + * subsequent {@link WebView#onDraw} invocation might not reflect those updates. The + * {@link VisualStateCallback} provides a mechanism to notify the caller when the contents of + * the DOM at the current time are ready to be drawn the next time the {@link WebView} draws. + * By current time we mean the time at which this API was called. The next draw after the + * callback completes is guaranteed to reflect all the updates to the DOM applied before the + * current time, but it may also contain updates applied after the current time.</p> + * + * <p>The state of the DOM covered by this API includes the following: + * <ul> + * <li>primitive HTML elements (div, img, span, etc..)</li> + * <li>images</li> + * <li>CSS animations</li> + * <li>WebGL</li> + * <li>canvas</li> + * </ul> + * It does not include the state of: + * <ul> + * <li>the video tag</li> + * </ul></p> + * + * <p>To guarantee that the {@link WebView} will successfully render the first frame + * after the {@link VisualStateCallback#onComplete} method has been called a set of conditions + * must be met: + * <ul> + * <li>If the {@link WebView}'s visibility is set to {@link View#VISIBLE VISIBLE} then + * the {@link WebView} must be attached to the view hierarchy.</li> + * <li>If the {@link WebView}'s visibility is set to {@link View#INVISIBLE INVISIBLE} + * then the {@link WebView} must be attached to the view hierarchy and must be made + * {@link View#VISIBLE VISIBLE} from the {@link VisualStateCallback#onComplete} method.</li> + * <li>If the {@link WebView}'s visibility is set to {@link View#GONE GONE} then the + * {@link WebView} must be attached to the view hierarchy and its + * {@link AbsoluteLayout.LayoutParams LayoutParams}'s width and height need to be set to fixed + * values and must be made {@link View#VISIBLE VISIBLE} from the + * {@link VisualStateCallback#onComplete} method.</li> + * </ul></p> + * + * <p>When using this API it is also recommended to enable pre-rasterization if the + * {@link WebView} is offscreen to avoid flickering. See WebSettings#setOffscreenPreRaster for + * more details and do consider its caveats.</p> + * + * @param requestId an id that will be returned in the callback to allow callers to match + * requests with callbacks. + * @param callback the callback to be invoked. + */ + public void insertVisualStateCallback(long requestId, VisualStateCallback callback) { + checkThread(); + if (TRACE) Log.d(LOGTAG, "insertVisualStateCallback"); + mProvider.insertVisualStateCallback(requestId, callback); + } + + /** * Clears this WebView so that onDraw() will draw nothing but white background, * and onMeasure() will return 0 if MeasureSpec is not MeasureSpec.EXACTLY. * @deprecated Use WebView.loadUrl("about:blank") to reliably reset the view state @@ -1789,6 +1894,37 @@ public class WebView extends AbsoluteLayout } /** + * Creates a message channel to communicate with JS and returns the message + * ports that represent the endpoints of this message channel. The HTML5 message + * channel functionality is described here: + * https://html.spec.whatwg.org/multipage/comms.html#messagechannel + * + * The returned message channels are entangled and already in started state. + * + * @return the two message ports that form the message channel. + */ + public WebMessagePort[] createWebMessageChannel() { + checkThread(); + if (TRACE) Log.d(LOGTAG, "createWebMessageChannel"); + return mProvider.createWebMessageChannel(); + } + + /** + * Post a message to main frame. The embedded application can restrict the + * messages to a certain target origin. See + * https://html.spec.whatwg.org/multipage/comms.html#posting-messages + * for how target origin can be used. + * + * @param message the WebMessage + * @param targetOrigin the target origin. + */ + public void postMessageToMainFrame(WebMessage message, Uri targetOrigin) { + checkThread(); + if (TRACE) Log.d(LOGTAG, "postMessageToMainFrame. TargetOrigin=" + targetOrigin); + mProvider.postMessageToMainFrame(message, targetOrigin); + } + + /** * Gets the WebSettings object used to control the settings for this * WebView. * @@ -2043,7 +2179,7 @@ public class WebView extends AbsoluteLayout } public boolean super_performAccessibilityAction(int action, Bundle arguments) { - return WebView.super.performAccessibilityAction(action, arguments); + return WebView.super.performAccessibilityActionInternal(action, arguments); } public boolean super_performLongClick() { @@ -2351,22 +2487,27 @@ public class WebView extends AbsoluteLayout return mProvider.getViewDelegate().shouldDelayChildPressedState(); } + public CharSequence getAccessibilityClassName() { + return WebView.class.getName(); + } + + /** @hide */ @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(WebView.class.getName()); + public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfoInternal(info); mProvider.getViewDelegate().onInitializeAccessibilityNodeInfo(info); } + /** @hide */ @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(WebView.class.getName()); + public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { + super.onInitializeAccessibilityEventInternal(event); mProvider.getViewDelegate().onInitializeAccessibilityEvent(event); } + /** @hide */ @Override - public boolean performAccessibilityAction(int action, Bundle arguments) { + public boolean performAccessibilityActionInternal(int action, Bundle arguments) { return mProvider.getViewDelegate().performAccessibilityAction(action, arguments); } diff --git a/core/java/android/webkit/WebViewClient.java b/core/java/android/webkit/WebViewClient.java index d52dd60..53c7e04 100644 --- a/core/java/android/webkit/WebViewClient.java +++ b/core/java/android/webkit/WebViewClient.java @@ -23,8 +23,6 @@ import android.view.InputEvent; import android.view.KeyEvent; import android.view.ViewRootImpl; -import java.security.Principal; - public class WebViewClient { /** @@ -50,7 +48,9 @@ public class WebViewClient { * is called once for each main frame load so a page with iframes or * framesets will call onPageStarted one time for the main frame. This also * means that onPageStarted will not be called when the contents of an - * embedded frame changes, i.e. clicking a link whose target is an iframe. + * embedded frame changes, i.e. clicking a link whose target is an iframe, + * it will also not be called for fragment navigations (navigations to + * #fragment_id). * * @param view The WebView that is initiating the callback. * @param url The url to be loaded. @@ -83,6 +83,32 @@ public class WebViewClient { } /** + * Notify the host application that the page commit is visible. + * + * <p>This is the earliest point at which we can guarantee that the contents of the previously + * loaded page will not longer be drawn in the next {@link WebView#onDraw}. The next draw will + * render the {@link WebView#setBackgroundColor background color} of the WebView or some of the + * contents from the committed page already. This callback may be useful when reusing + * {@link WebView}s to ensure that no stale content is shown. This method is only called for + * the main frame.</p> + * + * <p>This method is called when the state of the DOM at the point at which the + * body of the HTTP response (commonly the string of html) had started loading will be visible. + * If you set a background color for the page in the HTTP response body this will most likely + * be visible and perhaps some other elements. At that point no other resources had usually + * been loaded, so you can expect images for example to not be visible. If you want + * a finer level of granularity consider calling {@link WebView#insertVisualStateCallback} + * directly.</p> + * + * <p>Please note that all the conditions and recommendations presented in + * {@link WebView#insertVisualStateCallback} also apply to this API.<p> + * + * @param url the url of the committed page + */ + public void onPageCommitVisible(WebView view, String url) { + } + + /** * Notify the host application of a resource request and allow the * application to return the data. If the return value is null, the WebView * will continue to load the resource as usual. Otherwise, the return @@ -174,6 +200,8 @@ public class WebViewClient { public static final int ERROR_FILE_NOT_FOUND = -14; /** Too many requests during this load */ public static final int ERROR_TOO_MANY_REQUESTS = -15; + /** Request blocked by the browser */ + public static final int ERROR_BLOCKED = -16; /** * Report an error to the host application. These errors are unrecoverable @@ -183,12 +211,45 @@ public class WebViewClient { * @param errorCode The error code corresponding to an ERROR_* value. * @param description A String describing the error. * @param failingUrl The url that failed to load. + * @deprecated Use {@link #onReceivedError(WebView, WebResourceRequest, WebResourceError) + * onReceivedError(WebView, WebResourceRequest, WebResourceError)} instead. */ + @Deprecated public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { } /** + * Report web resource loading error to the host application. These errors usually indicate + * inability to connect to the server. Note that unlike the deprecated version of the callback, + * the new version will be called for any resource (iframe, image, etc), not just for the main + * page. Thus, it is recommended to perform minimum required work in this callback. + * @param view The WebView that is initiating the callback. + * @param request The originating request. + * @param error Information about the error occured. + */ + public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { + if (request.isForMainFrame()) { + onReceivedError(view, + error.getErrorCode(), error.getDescription(), request.getUrl().toString()); + } + } + + /** + * Notify the host application that an HTTP error has been received from the server while + * loading a resource. HTTP errors have status codes >= 400. This callback will be called + * for any resource (iframe, image, etc), not just for the main page. Thus, it is recommended to + * perform minimum required work in this callback. Note that the content of the server + * response may not be provided within the <b>errorResponse</b> parameter. + * @param view The WebView that is initiating the callback. + * @param request The originating request. + * @param errorResponse Information about the error occured. + */ + public void onReceivedHttpError( + WebView view, WebResourceRequest request, WebResourceResponseBase errorResponse) { + } + + /** * As the host application if the browser should resend data as the * requested page was a result of a POST. The default is to not resend the * data. diff --git a/core/java/android/webkit/WebViewDatabase.java b/core/java/android/webkit/WebViewDatabase.java index bfea481..cdff416 100644 --- a/core/java/android/webkit/WebViewDatabase.java +++ b/core/java/android/webkit/WebViewDatabase.java @@ -16,7 +16,6 @@ package android.webkit; -import android.annotation.SystemApi; import android.content.Context; /** diff --git a/core/java/android/webkit/WebViewDelegate.java b/core/java/android/webkit/WebViewDelegate.java index ac360fa..23af384 100644 --- a/core/java/android/webkit/WebViewDelegate.java +++ b/core/java/android/webkit/WebViewDelegate.java @@ -25,7 +25,7 @@ import android.graphics.Canvas; import android.os.SystemProperties; import android.os.Trace; import android.util.SparseArray; -import android.view.HardwareCanvas; +import android.view.DisplayListCanvas; import android.view.View; import android.view.ViewRootImpl; @@ -101,12 +101,12 @@ public final class WebViewDelegate { * @throws IllegalArgumentException if the canvas is not hardware accelerated */ public void callDrawGlFunction(Canvas canvas, long nativeDrawGLFunctor) { - if (!(canvas instanceof HardwareCanvas)) { + if (!(canvas instanceof DisplayListCanvas)) { // Canvas#isHardwareAccelerated() is only true for subclasses of HardwareCanvas. throw new IllegalArgumentException(canvas.getClass().getName() - + " is not hardware accelerated"); + + " is not a DisplayList canvas"); } - ((HardwareCanvas) canvas).callDrawGLFunction2(nativeDrawGLFunctor); + ((DisplayListCanvas) canvas).callDrawGLFunction2(nativeDrawGLFunctor); } /** @@ -153,7 +153,7 @@ public final class WebViewDelegate { } /** - * Adds the WebView asset path to {@link AssetManager}. + * Adds the WebView asset path to {@link android.content.res.AssetManager}. */ public void addWebViewAssetPath(Context context) { context.getAssets().addAssetPath( diff --git a/core/java/android/webkit/WebViewFactory.java b/core/java/android/webkit/WebViewFactory.java index 474ef42..cafe053 100644 --- a/core/java/android/webkit/WebViewFactory.java +++ b/core/java/android/webkit/WebViewFactory.java @@ -117,12 +117,8 @@ public final class WebViewFactory { StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads(); Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "providerClass.newInstance()"); try { - try { - sProviderInstance = providerClass.getConstructor(WebViewDelegate.class) - .newInstance(new WebViewDelegate()); - } catch (Exception e) { - sProviderInstance = providerClass.newInstance(); - } + sProviderInstance = providerClass.getConstructor(WebViewDelegate.class) + .newInstance(new WebViewDelegate()); if (DEBUG) Log.v(LOGTAG, "Loaded provider: " + sProviderInstance); return sProviderInstance; } catch (Exception e) { diff --git a/core/java/android/webkit/WebViewProvider.java b/core/java/android/webkit/WebViewProvider.java index 2aee57b..fa2ce1b 100644 --- a/core/java/android/webkit/WebViewProvider.java +++ b/core/java/android/webkit/WebViewProvider.java @@ -24,8 +24,8 @@ import android.graphics.Paint; import android.graphics.Picture; import android.graphics.Rect; import android.graphics.drawable.Drawable; -import android.net.Uri; import android.net.http.SslCertificate; +import android.net.Uri; import android.os.Bundle; import android.os.Message; import android.print.PrintDocumentAdapter; @@ -40,6 +40,8 @@ import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.webkit.WebView.HitTestResult; import android.webkit.WebView.PictureListener; +import android.webkit.WebView.VisualStateCallback; + import java.io.BufferedWriter; import java.io.File; @@ -146,6 +148,8 @@ public interface WebViewProvider { public boolean pageDown(boolean bottom); + public void insertVisualStateCallback(long requestId, VisualStateCallback callback); + public void clearView(); public Picture capturePicture(); @@ -228,6 +232,10 @@ public interface WebViewProvider { public void removeJavascriptInterface(String interfaceName); + public WebMessagePort[] createWebMessageChannel(); + + public void postMessageToMainFrame(WebMessage message, Uri targetOrigin); + public WebSettings getSettings(); public void setMapTrackballToArrowKeys(boolean setMap); diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 1e269a3..168066a 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -16,11 +16,12 @@ package android.widget; +import android.annotation.ColorInt; +import android.annotation.DrawableRes; import android.content.Context; import android.content.Intent; import android.content.res.TypedArray; import android.graphics.Canvas; -import android.graphics.PointF; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.TransitionDrawable; @@ -578,6 +579,12 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te private boolean mIsChildViewEnabled; /** + * The cached drawable state for the selector. Accounts for child enabled + * state, but otherwise identical to the view's own drawable state. + */ + private int[] mSelectorState; + + /** * The last scroll state reported to clients through {@link OnScrollListener}. */ private int mLastScrollState = OnScrollListener.SCROLL_STATE_IDLE; @@ -1466,8 +1473,9 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te onScrollChanged(0, 0, 0, 0); // dummy values, View's implementation does not use these. } + /** @hide */ @Override - public void sendAccessibilityEvent(int eventType) { + public void sendAccessibilityEventInternal(int eventType) { // Since this class calls onScrollChanged even if the mFirstPosition and the // child count have not changed we will avoid sending duplicate accessibility // events. @@ -1482,19 +1490,18 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mLastAccessibilityScrollEventToIndex = lastVisiblePosition; } } - super.sendAccessibilityEvent(eventType); + super.sendAccessibilityEventInternal(eventType); } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(AbsListView.class.getName()); + public CharSequence getAccessibilityClassName() { + return AbsListView.class.getName(); } + /** @hide */ @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(AbsListView.class.getName()); + public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfoInternal(info); if (isEnabled()) { if (canScrollUp()) { info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); @@ -1522,9 +1529,10 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } } + /** @hide */ @Override - public boolean performAccessibilityAction(int action, Bundle arguments) { - if (super.performAccessibilityAction(action, arguments)) { + public boolean performAccessibilityActionInternal(int action, Bundle arguments) { + if (super.performAccessibilityActionInternal(action, arguments)) { return true; } switch (action) { @@ -2705,7 +2713,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * * @attr ref android.R.styleable#AbsListView_listSelector */ - public void setSelector(int resID) { + public void setSelector(@DrawableRes int resID) { setSelector(getContext().getDrawable(resID)); } @@ -2785,7 +2793,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te void updateSelectorState() { if (mSelector != null) { if (shouldShowSelector()) { - mSelector.setState(getDrawableState()); + mSelector.setState(getDrawableStateForSelector()); } else { mSelector.setState(StateSet.NOTHING); } @@ -2798,12 +2806,11 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te updateSelectorState(); } - @Override - protected int[] onCreateDrawableState(int extraSpace) { + private int[] getDrawableStateForSelector() { // If the child view is enabled then do the default behavior. if (mIsChildViewEnabled) { // Common case - return super.onCreateDrawableState(extraSpace); + return super.getDrawableState(); } // The selector uses this View's drawable state. The selected child view @@ -2811,10 +2818,12 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te // states. final int enabledState = ENABLED_STATE_SET[0]; - // If we don't have any extra space, it will return one of the static state arrays, - // and clearing the enabled state on those arrays is a bad thing! If we specify - // we need extra space, it will create+copy into a new array that safely mutable. - int[] state = super.onCreateDrawableState(extraSpace + 1); + // If we don't have any extra space, it will return one of the static + // state arrays, and clearing the enabled state on those arrays is a + // bad thing! If we specify we need extra space, it will create+copy + // into a new array that is safely mutable. + final int[] state = onCreateDrawableState(1); + int enabledPos = -1; for (int i = state.length - 1; i >= 0; i--) { if (state[i] == enabledState) { @@ -5974,7 +5983,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * * @param color The background color */ - public void setCacheColorHint(int color) { + public void setCacheColorHint(@ColorInt int color) { if (color != mCacheColorHint) { mCacheColorHint = color; int count = getChildCount(); @@ -5992,6 +6001,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * @return The cache color hint */ @ViewDebug.ExportedProperty(category = "drawing") + @ColorInt public int getCacheColorHint() { return mCacheColorHint; } diff --git a/core/java/android/widget/AbsSeekBar.java b/core/java/android/widget/AbsSeekBar.java index a3ce808..d6f9f78 100644 --- a/core/java/android/widget/AbsSeekBar.java +++ b/core/java/android/widget/AbsSeekBar.java @@ -31,7 +31,6 @@ import android.util.AttributeSet; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ViewConfiguration; -import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import com.android.internal.R; @@ -381,8 +380,8 @@ public abstract class AbsSeekBar extends ProgressBar { } @Override - void onProgressRefresh(float scale, boolean fromUser) { - super.onProgressRefresh(scale, fromUser); + void onProgressRefresh(float scale, boolean fromUser, int progress) { + super.onProgressRefresh(scale, fromUser, progress); final Drawable thumb = mThumb; if (thumb != null) { @@ -403,28 +402,31 @@ public abstract class AbsSeekBar extends ProgressBar { } private void updateThumbAndTrackPos(int w, int h) { + final int paddedHeight = h - mPaddingTop - mPaddingBottom; final Drawable track = getCurrentDrawable(); final Drawable thumb = mThumb; // The max height does not incorporate padding, whereas the height // parameter does. - final int trackHeight = Math.min(mMaxHeight, h - mPaddingTop - mPaddingBottom); + final int trackHeight = Math.min(mMaxHeight, paddedHeight); final int thumbHeight = thumb == null ? 0 : thumb.getIntrinsicHeight(); // Apply offset to whichever item is taller. final int trackOffset; final int thumbOffset; if (thumbHeight > trackHeight) { - trackOffset = (thumbHeight - trackHeight) / 2; - thumbOffset = 0; + final int offsetHeight = (paddedHeight - thumbHeight) / 2; + trackOffset = offsetHeight + (thumbHeight - trackHeight) / 2; + thumbOffset = offsetHeight + 0; } else { - trackOffset = 0; - thumbOffset = (trackHeight - thumbHeight) / 2; + final int offsetHeight = (paddedHeight - trackHeight) / 2; + trackOffset = offsetHeight + 0; + thumbOffset = offsetHeight + (trackHeight - thumbHeight) / 2; } if (track != null) { - track.setBounds(0, trackOffset, w - mPaddingRight - mPaddingLeft, - h - mPaddingBottom - trackOffset - mPaddingTop); + final int trackWidth = w - mPaddingRight - mPaddingLeft; + track.setBounds(0, trackOffset, trackWidth, trackOffset + trackHeight); } if (thumb != null) { @@ -472,7 +474,6 @@ public abstract class AbsSeekBar extends ProgressBar { final Drawable background = getBackground(); if (background != null) { - final Rect bounds = thumb.getBounds(); final int offsetX = mPaddingLeft - mThumbOffset; final int offsetY = mPaddingTop; background.setHotspotBounds(left + offsetX, top + offsetY, @@ -716,15 +717,14 @@ public abstract class AbsSeekBar extends ProgressBar { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(AbsSeekBar.class.getName()); + public CharSequence getAccessibilityClassName() { + return AbsSeekBar.class.getName(); } + /** @hide */ @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(AbsSeekBar.class.getName()); + public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfoInternal(info); if (isEnabled()) { final int progress = getProgress(); @@ -737,9 +737,10 @@ public abstract class AbsSeekBar extends ProgressBar { } } + /** @hide */ @Override - public boolean performAccessibilityAction(int action, Bundle arguments) { - if (super.performAccessibilityAction(action, arguments)) { + public boolean performAccessibilityActionInternal(int action, Bundle arguments) { + if (super.performAccessibilityActionInternal(action, arguments)) { return true; } if (!isEnabled()) { diff --git a/core/java/android/widget/AbsSpinner.java b/core/java/android/widget/AbsSpinner.java index 6a4ad75..1cb7f2a 100644 --- a/core/java/android/widget/AbsSpinner.java +++ b/core/java/android/widget/AbsSpinner.java @@ -28,8 +28,6 @@ import android.util.AttributeSet; import android.util.SparseArray; import android.view.View; import android.view.ViewGroup; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; /** * An abstract base class for spinner widgets. SDK users will probably not @@ -73,13 +71,12 @@ public abstract class AbsSpinner extends AdapterView<SpinnerAdapter> { initAbsSpinner(); final TypedArray a = context.obtainStyledAttributes( - attrs, com.android.internal.R.styleable.AbsSpinner, defStyleAttr, defStyleRes); + attrs, R.styleable.AbsSpinner, defStyleAttr, defStyleRes); - CharSequence[] entries = a.getTextArray(R.styleable.AbsSpinner_entries); + final CharSequence[] entries = a.getTextArray(R.styleable.AbsSpinner_entries); if (entries != null) { - ArrayAdapter<CharSequence> adapter = - new ArrayAdapter<CharSequence>(context, - R.layout.simple_spinner_item, entries); + final ArrayAdapter<CharSequence> adapter = new ArrayAdapter<CharSequence>( + context, R.layout.simple_spinner_item, entries); adapter.setDropDownViewResource(R.layout.simple_spinner_dropdown_item); setAdapter(adapter); } @@ -472,14 +469,7 @@ public abstract class AbsSpinner extends AdapterView<SpinnerAdapter> { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(AbsSpinner.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(AbsSpinner.class.getName()); + public CharSequence getAccessibilityClassName() { + return AbsSpinner.class.getName(); } } diff --git a/core/java/android/widget/ActionMenuPresenter.java b/core/java/android/widget/ActionMenuPresenter.java index 18f15a0..4fadc19 100644 --- a/core/java/android/widget/ActionMenuPresenter.java +++ b/core/java/android/widget/ActionMenuPresenter.java @@ -17,10 +17,10 @@ package android.widget; import android.content.Context; +import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.Resources; -import android.graphics.Matrix; -import android.graphics.Rect; +import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.os.Parcel; import android.os.Parcelable; @@ -55,7 +55,7 @@ public class ActionMenuPresenter extends BaseMenuPresenter implements ActionProvider.SubUiVisibilityListener { private static final String TAG = "ActionMenuPresenter"; - private View mOverflowButton; + private OverflowMenuButton mOverflowButton; private boolean mReserveOverflow; private boolean mReserveOverflowSet; private int mWidthLimit; @@ -79,6 +79,8 @@ public class ActionMenuPresenter extends BaseMenuPresenter private OpenOverflowRunnable mPostedOpenRunnable; private ActionMenuPopupCallback mPopupCallback; + private TintInfo mOverflowTintInfo; + final PopupPresenterCallback mPopupPresenterCallback = new PopupPresenterCallback(); int mOpenSubMenuId; @@ -113,6 +115,7 @@ public class ActionMenuPresenter extends BaseMenuPresenter mOverflowButton = new OverflowMenuButton(mSystemContext); final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); mOverflowButton.measure(spec, spec); + applyOverflowTint(); } width -= mOverflowButton.getMeasuredWidth(); } else { @@ -236,6 +239,7 @@ public class ActionMenuPresenter extends BaseMenuPresenter if (hasOverflow) { if (mOverflowButton == null) { mOverflowButton = new OverflowMenuButton(mSystemContext); + applyOverflowTint(); } ViewGroup parent = (ViewGroup) mOverflowButton.getParent(); if (parent != mMenuView) { @@ -550,6 +554,40 @@ public class ActionMenuPresenter extends BaseMenuPresenter menuView.initialize(mMenu); } + public void setOverflowTintList(ColorStateList tint) { + if (mOverflowTintInfo == null) { + mOverflowTintInfo = new TintInfo(); + } + mOverflowTintInfo.mTintList = tint; + mOverflowTintInfo.mHasTintList = true; + + applyOverflowTint(); + } + + public void setOverflowTintMode(PorterDuff.Mode tintMode) { + if (mOverflowTintInfo == null) { + mOverflowTintInfo = new TintInfo(); + } + mOverflowTintInfo.mTintMode = tintMode; + mOverflowTintInfo.mHasTintMode = true; + + applyOverflowTint(); + } + + private void applyOverflowTint() { + final TintInfo tintInfo = mOverflowTintInfo; + if (tintInfo != null && (tintInfo.mHasTintList || tintInfo.mHasTintMode)) { + if (mOverflowButton != null) { + if (tintInfo.mHasTintList) { + mOverflowButton.setImageTintList(tintInfo.mTintList); + } + if (tintInfo.mHasTintMode) { + mOverflowButton.setImageTintMode(tintInfo.mTintMode); + } + } + } + } + private static class SavedState implements Parcelable { public int openSubMenuId; @@ -645,9 +683,10 @@ public class ActionMenuPresenter extends BaseMenuPresenter return false; } + /** @hide */ @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); + public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfoInternal(info); info.setCanOpenPopup(true); } @@ -773,4 +812,11 @@ public class ActionMenuPresenter extends BaseMenuPresenter return mActionButtonPopup != null ? mActionButtonPopup.getPopup() : null; } } + + private static class TintInfo { + ColorStateList mTintList; + PorterDuff.Mode mTintMode; + boolean mHasTintMode; + boolean mHasTintList; + } } diff --git a/core/java/android/widget/ActionMenuView.java b/core/java/android/widget/ActionMenuView.java index 0a8a01f..d6f2276 100644 --- a/core/java/android/widget/ActionMenuView.java +++ b/core/java/android/widget/ActionMenuView.java @@ -15,8 +15,11 @@ */ package android.widget; +import android.annotation.StyleRes; import android.content.Context; +import android.content.res.ColorStateList; import android.content.res.Configuration; +import android.graphics.PorterDuff; import android.util.AttributeSet; import android.view.ContextThemeWrapper; import android.view.Gravity; @@ -84,7 +87,7 @@ public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvo * @param resId theme used to inflate popup menus * @see #getPopupTheme() */ - public void setPopupTheme(int resId) { + public void setPopupTheme(@StyleRes int resId) { if (mPopupTheme != resId) { mPopupTheme = resId; if (resId == 0) { @@ -116,11 +119,14 @@ public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvo @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - mPresenter.updateMenuView(false); - if (mPresenter != null && mPresenter.isOverflowMenuShowing()) { - mPresenter.hideOverflowMenu(); - mPresenter.showOverflowMenu(); + if (mPresenter != null) { + mPresenter.updateMenuView(false); + + if (mPresenter.isOverflowMenuShowing()) { + mPresenter.hideOverflowMenu(); + mPresenter.showOverflowMenu(); + } } } @@ -543,6 +549,31 @@ public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvo mReserveOverflow = reserveOverflow; } + /** + * Applies a tint to the overflow drawable. Does not modify the current tint + * mode, which is {@link PorterDuff.Mode#SRC_IN} by default. + * + * @param tint the tint to apply, may be {@code null} to clear tint + */ + public void setOverflowTintList(ColorStateList tint) { + if (mPresenter != null) { + mPresenter.setOverflowTintList(tint); + } + } + + /** + * Specifies the blending mode used to apply the tint specified by {@link + * #setOverflowTintList(ColorStateList)} to the overflow drawable. + * The default mode is {@link PorterDuff.Mode#SRC_IN}. + * + * @param tintMode the blending mode used to apply the tint, may be {@code null} to clear tint + */ + public void setOverflowTintMode(PorterDuff.Mode tintMode) { + if (mPresenter != null) { + mPresenter.setOverflowTintMode(tintMode); + } + } + @Override protected LayoutParams generateDefaultLayoutParams() { LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, @@ -700,7 +731,8 @@ public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvo return result; } - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + /** @hide */ + public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) { return false; } diff --git a/core/java/android/widget/ActivityChooserView.java b/core/java/android/widget/ActivityChooserView.java index f9af2f9..f34ad71 100644 --- a/core/java/android/widget/ActivityChooserView.java +++ b/core/java/android/widget/ActivityChooserView.java @@ -18,6 +18,7 @@ package android.widget; import com.android.internal.R; +import android.annotation.StringRes; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -334,7 +335,7 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod * * @param resourceId The content description resource id. */ - public void setExpandActivityOverflowButtonContentDescription(int resourceId) { + public void setExpandActivityOverflowButtonContentDescription(@StringRes int resourceId) { CharSequence contentDescription = mContext.getString(resourceId); mExpandActivityOverflowButtonImage.setContentDescription(contentDescription); } @@ -514,7 +515,7 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod * * @param resourceId The resource id. */ - public void setDefaultActionButtonContentDescription(int resourceId) { + public void setDefaultActionButtonContentDescription(@StringRes int resourceId) { mDefaultActionButtonContentDescription = resourceId; } diff --git a/core/java/android/widget/AdapterView.java b/core/java/android/widget/AdapterView.java index 5e2394c..72cb0b5 100644 --- a/core/java/android/widget/AdapterView.java +++ b/core/java/android/widget/AdapterView.java @@ -16,6 +16,7 @@ package android.widget; +import android.annotation.Nullable; import android.content.Context; import android.database.DataSetObserver; import android.os.Parcelable; @@ -276,7 +277,7 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { * * @param listener The callback that will be invoked. */ - public void setOnItemClickListener(OnItemClickListener listener) { + public void setOnItemClickListener(@Nullable OnItemClickListener listener) { mOnItemClickListener = listener; } @@ -284,6 +285,7 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { * @return The callback to be invoked with an item in this AdapterView has * been clicked, or null id no callback has been set. */ + @Nullable public final OnItemClickListener getOnItemClickListener() { return mOnItemClickListener; } @@ -394,10 +396,11 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { * * @param listener The callback that will run */ - public void setOnItemSelectedListener(OnItemSelectedListener listener) { + public void setOnItemSelectedListener(@Nullable OnItemSelectedListener listener) { mOnItemSelectedListener = listener; } + @Nullable public final OnItemSelectedListener getOnItemSelectedListener() { return mOnItemSelectedListener; } @@ -929,8 +932,9 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { } } + /** @hide */ @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) { View selectedView = getSelectedView(); if (selectedView != null && selectedView.getVisibility() == VISIBLE && selectedView.dispatchPopulateAccessibilityEvent(event)) { @@ -939,9 +943,10 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { return false; } + /** @hide */ @Override - public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { - if (super.onRequestSendAccessibilityEvent(child, event)) { + public boolean onRequestSendAccessibilityEventInternal(View child, AccessibilityEvent event) { + if (super.onRequestSendAccessibilityEventInternal(child, event)) { // Add a record for ourselves as well. AccessibilityEvent record = AccessibilityEvent.obtain(); onInitializeAccessibilityEvent(record); @@ -954,9 +959,14 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { } @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(AdapterView.class.getName()); + public CharSequence getAccessibilityClassName() { + return AdapterView.class.getName(); + } + + /** @hide */ + @Override + public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfoInternal(info); info.setScrollable(isScrollableForAccessibility()); View selectedView = getSelectedView(); if (selectedView != null) { @@ -964,10 +974,10 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { } } + /** @hide */ @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(AdapterView.class.getName()); + public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { + super.onInitializeAccessibilityEventInternal(event); event.setScrollable(isScrollableForAccessibility()); View selectedView = getSelectedView(); if (selectedView != null) { diff --git a/core/java/android/widget/AdapterViewAnimator.java b/core/java/android/widget/AdapterViewAnimator.java index 1bc2f4b..932b354 100644 --- a/core/java/android/widget/AdapterViewAnimator.java +++ b/core/java/android/widget/AdapterViewAnimator.java @@ -29,8 +29,6 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.OnClickHandler; import java.util.ArrayList; @@ -1085,14 +1083,7 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(AdapterViewAnimator.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(AdapterViewAnimator.class.getName()); + public CharSequence getAccessibilityClassName() { + return AdapterViewAnimator.class.getName(); } } diff --git a/core/java/android/widget/AdapterViewFlipper.java b/core/java/android/widget/AdapterViewFlipper.java index 285dee8..a105b40 100644 --- a/core/java/android/widget/AdapterViewFlipper.java +++ b/core/java/android/widget/AdapterViewFlipper.java @@ -26,8 +26,6 @@ import android.os.Message; import android.util.AttributeSet; import android.util.Log; import android.view.RemotableViewMethod; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; /** @@ -305,14 +303,7 @@ public class AdapterViewFlipper extends AdapterViewAnimator { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(AdapterViewFlipper.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(AdapterViewFlipper.class.getName()); + public CharSequence getAccessibilityClassName() { + return AdapterViewFlipper.class.getName(); } } diff --git a/core/java/android/widget/ArrayAdapter.java b/core/java/android/widget/ArrayAdapter.java index 97926a7..ae94a10 100644 --- a/core/java/android/widget/ArrayAdapter.java +++ b/core/java/android/widget/ArrayAdapter.java @@ -16,8 +16,14 @@ package android.widget; +import android.annotation.ArrayRes; +import android.annotation.IdRes; +import android.annotation.LayoutRes; +import android.annotation.NonNull; import android.content.Context; +import android.content.res.Resources; import android.util.Log; +import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -44,7 +50,8 @@ import java.util.List; * or to have some of data besides toString() results fill the views, * override {@link #getView(int, View, ViewGroup)} to return the type of view you want. */ -public class ArrayAdapter<T> extends BaseAdapter implements Filterable { +public class ArrayAdapter<T> extends BaseAdapter implements Filterable, + Spinner.ThemedSpinnerAdapter { /** * Contains the list of objects that represent the data of this ArrayAdapter. * The content of this list is referred to as "the array" in the documentation. @@ -93,6 +100,9 @@ public class ArrayAdapter<T> extends BaseAdapter implements Filterable { private LayoutInflater mInflater; + /** Layout inflater used for {@link #getDropDownView(int, View, ViewGroup)}. */ + private LayoutInflater mDropDownInflater; + /** * Constructor * @@ -100,8 +110,8 @@ public class ArrayAdapter<T> extends BaseAdapter implements Filterable { * @param resource The resource ID for a layout file containing a TextView to use when * instantiating views. */ - public ArrayAdapter(Context context, int resource) { - init(context, resource, 0, new ArrayList<T>()); + public ArrayAdapter(Context context, @LayoutRes int resource) { + this(context, resource, 0, new ArrayList<T>()); } /** @@ -112,8 +122,8 @@ public class ArrayAdapter<T> extends BaseAdapter implements Filterable { * instantiating views. * @param textViewResourceId The id of the TextView within the layout resource to be populated */ - public ArrayAdapter(Context context, int resource, int textViewResourceId) { - init(context, resource, textViewResourceId, new ArrayList<T>()); + public ArrayAdapter(Context context, @LayoutRes int resource, @IdRes int textViewResourceId) { + this(context, resource, textViewResourceId, new ArrayList<T>()); } /** @@ -124,8 +134,8 @@ public class ArrayAdapter<T> extends BaseAdapter implements Filterable { * instantiating views. * @param objects The objects to represent in the ListView. */ - public ArrayAdapter(Context context, int resource, T[] objects) { - init(context, resource, 0, Arrays.asList(objects)); + public ArrayAdapter(Context context, @LayoutRes int resource, @NonNull T[] objects) { + this(context, resource, 0, Arrays.asList(objects)); } /** @@ -137,8 +147,9 @@ public class ArrayAdapter<T> extends BaseAdapter implements Filterable { * @param textViewResourceId The id of the TextView within the layout resource to be populated * @param objects The objects to represent in the ListView. */ - public ArrayAdapter(Context context, int resource, int textViewResourceId, T[] objects) { - init(context, resource, textViewResourceId, Arrays.asList(objects)); + public ArrayAdapter(Context context, @LayoutRes int resource, @IdRes int textViewResourceId, + @NonNull T[] objects) { + this(context, resource, textViewResourceId, Arrays.asList(objects)); } /** @@ -149,8 +160,8 @@ public class ArrayAdapter<T> extends BaseAdapter implements Filterable { * instantiating views. * @param objects The objects to represent in the ListView. */ - public ArrayAdapter(Context context, int resource, List<T> objects) { - init(context, resource, 0, objects); + public ArrayAdapter(Context context, @LayoutRes int resource, @NonNull List<T> objects) { + this(context, resource, 0, objects); } /** @@ -162,8 +173,13 @@ public class ArrayAdapter<T> extends BaseAdapter implements Filterable { * @param textViewResourceId The id of the TextView within the layout resource to be populated * @param objects The objects to represent in the ListView. */ - public ArrayAdapter(Context context, int resource, int textViewResourceId, List<T> objects) { - init(context, resource, textViewResourceId, objects); + public ArrayAdapter(Context context, @LayoutRes int resource, @IdRes int textViewResourceId, + @NonNull List<T> objects) { + mContext = context; + mInflater = LayoutInflater.from(context); + mResource = mDropDownResource = resource; + mObjects = objects; + mFieldId = textViewResourceId; } /** @@ -305,14 +321,6 @@ public class ArrayAdapter<T> extends BaseAdapter implements Filterable { mNotifyOnChange = notifyOnChange; } - private void init(Context context, int resource, int textViewResourceId, List<T> objects) { - mContext = context; - mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - mResource = mDropDownResource = resource; - mObjects = objects; - mFieldId = textViewResourceId; - } - /** * Returns the context associated with this array adapter. The context is used * to create views from the resource passed to the constructor. @@ -359,16 +367,16 @@ public class ArrayAdapter<T> extends BaseAdapter implements Filterable { * {@inheritDoc} */ public View getView(int position, View convertView, ViewGroup parent) { - return createViewFromResource(position, convertView, parent, mResource); + return createViewFromResource(mInflater, position, convertView, parent, mResource); } - private View createViewFromResource(int position, View convertView, ViewGroup parent, - int resource) { + private View createViewFromResource(LayoutInflater inflater, int position, View convertView, + ViewGroup parent, int resource) { View view; TextView text; if (convertView == null) { - view = mInflater.inflate(resource, parent, false); + view = inflater.inflate(resource, parent, false); } else { view = convertView; } @@ -403,16 +411,45 @@ public class ArrayAdapter<T> extends BaseAdapter implements Filterable { * @param resource the layout resource defining the drop down views * @see #getDropDownView(int, android.view.View, android.view.ViewGroup) */ - public void setDropDownViewResource(int resource) { + public void setDropDownViewResource(@LayoutRes int resource) { this.mDropDownResource = resource; } /** + * Sets the {@link Resources.Theme} against which drop-down views are + * inflated. + * <p> + * By default, drop-down views are inflated against the theme of the + * {@link Context} passed to the adapter's constructor. + * + * @param theme the theme against which to inflate drop-down views or + * {@code null} to use the theme from the adapter's context + * @see #getDropDownView(int, View, ViewGroup) + */ + @Override + public void setDropDownViewTheme(Resources.Theme theme) { + if (theme == null) { + mDropDownInflater = null; + } else if (theme == mInflater.getContext().getTheme()) { + mDropDownInflater = mInflater; + } else { + final Context context = new ContextThemeWrapper(mContext, theme); + mDropDownInflater = LayoutInflater.from(context); + } + } + + @Override + public Resources.Theme getDropDownViewTheme() { + return mDropDownInflater == null ? null : mDropDownInflater.getContext().getTheme(); + } + + /** * {@inheritDoc} */ @Override public View getDropDownView(int position, View convertView, ViewGroup parent) { - return createViewFromResource(position, convertView, parent, mDropDownResource); + final LayoutInflater inflater = mDropDownInflater == null ? mInflater : mDropDownInflater; + return createViewFromResource(inflater, position, convertView, parent, mDropDownResource); } /** @@ -426,7 +463,7 @@ public class ArrayAdapter<T> extends BaseAdapter implements Filterable { * @return An ArrayAdapter<CharSequence>. */ public static ArrayAdapter<CharSequence> createFromResource(Context context, - int textArrayResId, int textViewResId) { + @ArrayRes int textArrayResId, @LayoutRes int textViewResId) { CharSequence[] strings = context.getResources().getTextArray(textArrayResId); return new ArrayAdapter<CharSequence>(context, textViewResId, strings); } diff --git a/core/java/android/widget/AutoCompleteTextView.java b/core/java/android/widget/AutoCompleteTextView.java index e6392b9..01767d5 100644 --- a/core/java/android/widget/AutoCompleteTextView.java +++ b/core/java/android/widget/AutoCompleteTextView.java @@ -16,6 +16,7 @@ package android.widget; +import android.annotation.DrawableRes; import android.content.Context; import android.content.res.TypedArray; import android.database.DataSetObserver; @@ -356,7 +357,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * * @attr ref android.R.styleable#PopupWindow_popupBackground */ - public void setDropDownBackgroundResource(int id) { + public void setDropDownBackgroundResource(@DrawableRes int id) { mPopup.setBackgroundDrawable(getContext().getDrawable(id)); } diff --git a/core/java/android/widget/Button.java b/core/java/android/widget/Button.java index 1663620..154cc33 100644 --- a/core/java/android/widget/Button.java +++ b/core/java/android/widget/Button.java @@ -18,8 +18,6 @@ package android.widget; import android.content.Context; import android.util.AttributeSet; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; @@ -112,14 +110,7 @@ public class Button extends TextView { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(Button.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(Button.class.getName()); + public CharSequence getAccessibilityClassName() { + return Button.class.getName(); } } diff --git a/core/java/android/widget/CalendarView.java b/core/java/android/widget/CalendarView.java index ed59ea6..5bc16cb 100644 --- a/core/java/android/widget/CalendarView.java +++ b/core/java/android/widget/CalendarView.java @@ -16,6 +16,8 @@ package android.widget; +import android.annotation.ColorInt; +import android.annotation.DrawableRes; import android.annotation.Widget; import android.content.Context; import android.content.res.Configuration; @@ -23,9 +25,6 @@ import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.Log; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; - import com.android.internal.R; import java.text.DateFormat; @@ -142,7 +141,7 @@ public class CalendarView extends FrameLayout { * * @attr ref android.R.styleable#CalendarView_selectedWeekBackgroundColor */ - public void setSelectedWeekBackgroundColor(int color) { + public void setSelectedWeekBackgroundColor(@ColorInt int color) { mDelegate.setSelectedWeekBackgroundColor(color); } @@ -153,6 +152,7 @@ public class CalendarView extends FrameLayout { * * @attr ref android.R.styleable#CalendarView_selectedWeekBackgroundColor */ + @ColorInt public int getSelectedWeekBackgroundColor() { return mDelegate.getSelectedWeekBackgroundColor(); } @@ -164,7 +164,7 @@ public class CalendarView extends FrameLayout { * * @attr ref android.R.styleable#CalendarView_focusedMonthDateColor */ - public void setFocusedMonthDateColor(int color) { + public void setFocusedMonthDateColor(@ColorInt int color) { mDelegate.setFocusedMonthDateColor(color); } @@ -175,6 +175,7 @@ public class CalendarView extends FrameLayout { * * @attr ref android.R.styleable#CalendarView_focusedMonthDateColor */ + @ColorInt public int getFocusedMonthDateColor() { return mDelegate.getFocusedMonthDateColor(); } @@ -186,7 +187,7 @@ public class CalendarView extends FrameLayout { * * @attr ref android.R.styleable#CalendarView_unfocusedMonthDateColor */ - public void setUnfocusedMonthDateColor(int color) { + public void setUnfocusedMonthDateColor(@ColorInt int color) { mDelegate.setUnfocusedMonthDateColor(color); } @@ -197,6 +198,7 @@ public class CalendarView extends FrameLayout { * * @attr ref android.R.styleable#CalendarView_unfocusedMonthDateColor */ + @ColorInt public int getUnfocusedMonthDateColor() { return mDelegate.getUnfocusedMonthDateColor(); } @@ -208,7 +210,7 @@ public class CalendarView extends FrameLayout { * * @attr ref android.R.styleable#CalendarView_weekNumberColor */ - public void setWeekNumberColor(int color) { + public void setWeekNumberColor(@ColorInt int color) { mDelegate.setWeekNumberColor(color); } @@ -219,6 +221,7 @@ public class CalendarView extends FrameLayout { * * @attr ref android.R.styleable#CalendarView_weekNumberColor */ + @ColorInt public int getWeekNumberColor() { return mDelegate.getWeekNumberColor(); } @@ -230,7 +233,7 @@ public class CalendarView extends FrameLayout { * * @attr ref android.R.styleable#CalendarView_weekSeparatorLineColor */ - public void setWeekSeparatorLineColor(int color) { + public void setWeekSeparatorLineColor(@ColorInt int color) { mDelegate.setWeekSeparatorLineColor(color); } @@ -241,6 +244,7 @@ public class CalendarView extends FrameLayout { * * @attr ref android.R.styleable#CalendarView_weekSeparatorLineColor */ + @ColorInt public int getWeekSeparatorLineColor() { return mDelegate.getWeekSeparatorLineColor(); } @@ -253,7 +257,7 @@ public class CalendarView extends FrameLayout { * * @attr ref android.R.styleable#CalendarView_selectedDateVerticalBar */ - public void setSelectedDateVerticalBar(int resourceId) { + public void setSelectedDateVerticalBar(@DrawableRes int resourceId) { mDelegate.setSelectedDateVerticalBar(resourceId); } @@ -502,13 +506,8 @@ public class CalendarView extends FrameLayout { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - event.setClassName(CalendarView.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - info.setClassName(CalendarView.class.getName()); + public CharSequence getAccessibilityClassName() { + return CalendarView.class.getName(); } /** diff --git a/core/java/android/widget/CheckBox.java b/core/java/android/widget/CheckBox.java index 71438c9..15bbdd2 100644 --- a/core/java/android/widget/CheckBox.java +++ b/core/java/android/widget/CheckBox.java @@ -18,8 +18,6 @@ package android.widget; import android.content.Context; import android.util.AttributeSet; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; /** @@ -73,14 +71,7 @@ public class CheckBox extends CompoundButton { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(CheckBox.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(CheckBox.class.getName()); + public CharSequence getAccessibilityClassName() { + return CheckBox.class.getName(); } } diff --git a/core/java/android/widget/CheckedTextView.java b/core/java/android/widget/CheckedTextView.java index 69969a9..22e079c 100644 --- a/core/java/android/widget/CheckedTextView.java +++ b/core/java/android/widget/CheckedTextView.java @@ -18,6 +18,7 @@ package android.widget; import com.android.internal.R; +import android.annotation.DrawableRes; import android.annotation.Nullable; import android.content.Context; import android.content.res.ColorStateList; @@ -32,12 +33,14 @@ import android.view.ViewDebug; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; - /** - * An extension to TextView that supports the {@link android.widget.Checkable} interface. - * This is useful when used in a {@link android.widget.ListView ListView} where the it's - * {@link android.widget.ListView#setChoiceMode(int) setChoiceMode} has been set to - * something other than {@link android.widget.ListView#CHOICE_MODE_NONE CHOICE_MODE_NONE}. + * An extension to {@link TextView} that supports the {@link Checkable} + * interface and displays. + * <p> + * This is useful when used in a {@link android.widget.ListView ListView} where + * the {@link android.widget.ListView#setChoiceMode(int) setChoiceMode} has + * been set to something other than + * {@link android.widget.ListView#CHOICE_MODE_NONE CHOICE_MODE_NONE}. * * @attr ref android.R.styleable#CheckedTextView_checked * @attr ref android.R.styleable#CheckedTextView_checkMark @@ -116,9 +119,10 @@ public class CheckedTextView extends TextView implements Checkable { } /** - * <p>Changes the checked state of this text view.</p> + * Sets the checked state of this view. * - * @param checked true to check the text, false to uncheck it + * @param checked {@code true} set the state to checked, {@code false} to + * uncheck */ public void setChecked(boolean checked) { if (mChecked != checked) { @@ -129,24 +133,24 @@ public class CheckedTextView extends TextView implements Checkable { } } - /** - * Set the checkmark to a given Drawable, identified by its resourece id. This will be drawn - * when {@link #isChecked()} is true. - * - * @param resid The Drawable to use for the checkmark. + * Sets the check mark to the drawable with the specified resource ID. + * <p> + * When this view is checked, the drawable's state set will include + * {@link android.R.attr#state_checked}. * + * @param resId the resource identifier of drawable to use as the check + * mark + * @attr ref android.R.styleable#CheckedTextView_checkMark * @see #setCheckMarkDrawable(Drawable) * @see #getCheckMarkDrawable() - * - * @attr ref android.R.styleable#CheckedTextView_checkMark */ - public void setCheckMarkDrawable(int resid) { - if (resid != 0 && resid == mCheckMarkResource) { + public void setCheckMarkDrawable(@DrawableRes int resId) { + if (resId != 0 && resId == mCheckMarkResource) { return; } - mCheckMarkResource = resid; + mCheckMarkResource = resId; Drawable d = null; if (mCheckMarkResource != 0) { @@ -156,14 +160,15 @@ public class CheckedTextView extends TextView implements Checkable { } /** - * Set the checkmark to a given Drawable. This will be drawn when {@link #isChecked()} is true. - * - * @param d The Drawable to use for the checkmark. + * Set the check mark to the specified drawable. + * <p> + * When this view is checked, the drawable's state set will include + * {@link android.R.attr#state_checked}. * + * @param d the drawable to use for the check mark + * @attr ref android.R.styleable#CheckedTextView_checkMark * @see #setCheckMarkDrawable(int) * @see #getCheckMarkDrawable() - * - * @attr ref android.R.styleable#CheckedTextView_checkMark */ public void setCheckMarkDrawable(Drawable d) { if (mCheckMarkDrawable != null) { @@ -436,16 +441,21 @@ public class CheckedTextView extends TextView implements Checkable { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(CheckedTextView.class.getName()); + public CharSequence getAccessibilityClassName() { + return CheckedTextView.class.getName(); + } + + /** @hide */ + @Override + public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { + super.onInitializeAccessibilityEventInternal(event); event.setChecked(mChecked); } + /** @hide */ @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(CheckedTextView.class.getName()); + public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfoInternal(info); info.setCheckable(true); info.setChecked(mChecked); } diff --git a/core/java/android/widget/Chronometer.java b/core/java/android/widget/Chronometer.java index f94789d..a15080e 100644 --- a/core/java/android/widget/Chronometer.java +++ b/core/java/android/widget/Chronometer.java @@ -24,8 +24,6 @@ import android.os.SystemClock; import android.text.format.DateUtils; import android.util.AttributeSet; import android.util.Log; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; import java.util.Formatter; @@ -282,14 +280,7 @@ public class Chronometer extends TextView { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(Chronometer.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(Chronometer.class.getName()); + public CharSequence getAccessibilityClassName() { + return Chronometer.class.getName(); } } diff --git a/core/java/android/widget/CompoundButton.java b/core/java/android/widget/CompoundButton.java index 447ccc2..f2afeeb 100644 --- a/core/java/android/widget/CompoundButton.java +++ b/core/java/android/widget/CompoundButton.java @@ -16,6 +16,7 @@ package android.widget; +import android.annotation.DrawableRes; import android.annotation.Nullable; import android.graphics.PorterDuff; import com.android.internal.R; @@ -50,7 +51,6 @@ import android.view.accessibility.AccessibilityNodeInfo; */ public abstract class CompoundButton extends Button implements Checkable { private boolean mChecked; - private int mButtonResource; private boolean mBroadcasting; private Drawable mButtonDrawable; @@ -197,54 +197,62 @@ public abstract class CompoundButton extends Button implements Checkable { } /** - * Set the button graphic to a given Drawable, identified by its resource - * id. + * Sets a drawable as the compound button image given its resource + * identifier. * - * @param resid the resource id of the drawable to use as the button - * graphic + * @param resId the resource identifier of the drawable + * @attr ref android.R.styleable#CompoundButton_button */ - public void setButtonDrawable(int resid) { - if (resid != 0 && resid == mButtonResource) { - return; - } - - mButtonResource = resid; - - Drawable d = null; - if (mButtonResource != 0) { - d = getContext().getDrawable(mButtonResource); + public void setButtonDrawable(@DrawableRes int resId) { + final Drawable d; + if (resId != 0) { + d = getContext().getDrawable(resId); + } else { + d = null; } setButtonDrawable(d); } /** - * Set the button graphic to a given Drawable + * Sets a drawable as the compound button image. * - * @param d The Drawable to use as the button graphic + * @param drawable the drawable to set + * @attr ref android.R.styleable#CompoundButton_button */ - public void setButtonDrawable(Drawable d) { - if (mButtonDrawable != d) { + @Nullable + public void setButtonDrawable(@Nullable Drawable drawable) { + if (mButtonDrawable != drawable) { if (mButtonDrawable != null) { mButtonDrawable.setCallback(null); unscheduleDrawable(mButtonDrawable); } - mButtonDrawable = d; + mButtonDrawable = drawable; - if (d != null) { - d.setCallback(this); - d.setLayoutDirection(getLayoutDirection()); - if (d.isStateful()) { - d.setState(getDrawableState()); + if (drawable != null) { + drawable.setCallback(this); + drawable.setLayoutDirection(getLayoutDirection()); + if (drawable.isStateful()) { + drawable.setState(getDrawableState()); } - d.setVisible(getVisibility() == VISIBLE, false); - setMinHeight(d.getIntrinsicHeight()); + drawable.setVisible(getVisibility() == VISIBLE, false); + setMinHeight(drawable.getIntrinsicHeight()); applyButtonTint(); } } } /** + * @return the drawable used as the compound button image + * @see #setButtonDrawable(Drawable) + * @see #setButtonDrawable(int) + */ + @Nullable + public Drawable getButtonDrawable() { + return mButtonDrawable; + } + + /** * Applies a tint to the button drawable. Does not modify the current tint * mode, which is {@link PorterDuff.Mode#SRC_IN} by default. * <p> @@ -325,16 +333,21 @@ public abstract class CompoundButton extends Button implements Checkable { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(CompoundButton.class.getName()); + public CharSequence getAccessibilityClassName() { + return CompoundButton.class.getName(); + } + + /** @hide */ + @Override + public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { + super.onInitializeAccessibilityEventInternal(event); event.setChecked(mChecked); } + /** @hide */ @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(CompoundButton.class.getName()); + public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfoInternal(info); info.setCheckable(true); info.setChecked(mChecked); } diff --git a/core/java/android/widget/CursorAdapter.java b/core/java/android/widget/CursorAdapter.java index d4c5be0..30c74c0 100644 --- a/core/java/android/widget/CursorAdapter.java +++ b/core/java/android/widget/CursorAdapter.java @@ -17,11 +17,13 @@ package android.widget; import android.content.Context; +import android.content.res.Resources; import android.database.ContentObserver; import android.database.Cursor; import android.database.DataSetObserver; import android.os.Handler; import android.util.Log; +import android.view.ContextThemeWrapper; import android.view.View; import android.view.ViewGroup; @@ -35,7 +37,7 @@ import android.view.ViewGroup; * columns. */ public abstract class CursorAdapter extends BaseAdapter implements Filterable, - CursorFilter.CursorFilterClient { + CursorFilter.CursorFilterClient, Spinner.ThemedSpinnerAdapter { /** * This field should be made private, so it is hidden from the SDK. * {@hide} @@ -57,6 +59,11 @@ public abstract class CursorAdapter extends BaseAdapter implements Filterable, */ protected Context mContext; /** + * Context used for {@link #getDropDownView(int, View, ViewGroup)}. + * {@hide} + */ + protected Context mDropDownContext; + /** * This field should be made private, so it is hidden from the SDK. * {@hide} */ @@ -185,6 +192,33 @@ public abstract class CursorAdapter extends BaseAdapter implements Filterable, } /** + * Sets the {@link Resources.Theme} against which drop-down views are + * inflated. + * <p> + * By default, drop-down views are inflated against the theme of the + * {@link Context} passed to the adapter's constructor. + * + * @param theme the theme against which to inflate drop-down views or + * {@code null} to use the theme from the adapter's context + * @see #newDropDownView(Context, Cursor, ViewGroup) + */ + @Override + public void setDropDownViewTheme(Resources.Theme theme) { + if (theme == null) { + mDropDownContext = null; + } else if (theme == mContext.getTheme()) { + mDropDownContext = mContext; + } else { + mDropDownContext = new ContextThemeWrapper(mContext, theme); + } + } + + @Override + public Resources.Theme getDropDownViewTheme() { + return mDropDownContext == null ? null : mDropDownContext.getTheme(); + } + + /** * Returns the cursor. * @return the cursor. */ @@ -258,20 +292,21 @@ public abstract class CursorAdapter extends BaseAdapter implements Filterable, @Override public View getDropDownView(int position, View convertView, ViewGroup parent) { if (mDataValid) { + final Context context = mDropDownContext == null ? mContext : mDropDownContext; mCursor.moveToPosition(position); - View v; + final View v; if (convertView == null) { - v = newDropDownView(mContext, mCursor, parent); + v = newDropDownView(context, mCursor, parent); } else { v = convertView; } - bindView(v, mContext, mCursor); + bindView(v, context, mCursor); return v; } else { return null; } } - + /** * Makes a new view to hold the data pointed to by cursor. * @param context Interface to application's global information diff --git a/core/java/android/widget/DatePicker.java b/core/java/android/widget/DatePicker.java index 0ca08e1..45998f7 100644 --- a/core/java/android/widget/DatePicker.java +++ b/core/java/android/widget/DatePicker.java @@ -33,7 +33,6 @@ import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.NumberPicker.OnValueChangeListener; @@ -283,27 +282,22 @@ public class DatePicker extends FrameLayout { return mDelegate.isEnabled(); } + /** @hide */ @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) { return mDelegate.dispatchPopulateAccessibilityEvent(event); } + /** @hide */ @Override - public void onPopulateAccessibilityEvent(AccessibilityEvent event) { - super.onPopulateAccessibilityEvent(event); + public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) { + super.onPopulateAccessibilityEventInternal(event); mDelegate.onPopulateAccessibilityEvent(event); } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - mDelegate.onInitializeAccessibilityEvent(event); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - mDelegate.onInitializeAccessibilityNodeInfo(info); + public CharSequence getAccessibilityClassName() { + return DatePicker.class.getName(); } @Override @@ -472,8 +466,6 @@ public class DatePicker extends FrameLayout { boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event); void onPopulateAccessibilityEvent(AccessibilityEvent event); - void onInitializeAccessibilityEvent(AccessibilityEvent event); - void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info); } /** @@ -888,16 +880,6 @@ public class DatePicker extends FrameLayout { event.getText().add(selectedDateUtterance); } - @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - event.setClassName(DatePicker.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - info.setClassName(DatePicker.class.getName()); - } - /** * Sets the current locale. * diff --git a/core/java/android/widget/DatePickerCalendarDelegate.java b/core/java/android/widget/DatePickerCalendarDelegate.java index 1d50eb2..0e3ec7f 100755 --- a/core/java/android/widget/DatePickerCalendarDelegate.java +++ b/core/java/android/widget/DatePickerCalendarDelegate.java @@ -30,7 +30,6 @@ import android.view.HapticFeedbackConstants; import android.view.LayoutInflater; import android.view.View; import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; @@ -145,8 +144,8 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate i // Use Theme attributes if possible final int dayOfWeekTextAppearanceResId = a.getResourceId( - R.styleable.DatePicker_dayOfWeekTextAppearance, -1); - if (dayOfWeekTextAppearanceResId != -1) { + R.styleable.DatePicker_dayOfWeekTextAppearance, 0); + if (dayOfWeekTextAppearanceResId != 0) { mDayOfWeekView.setTextAppearance(context, dayOfWeekTextAppearanceResId); } @@ -154,34 +153,23 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate i dateLayout.setBackground(a.getDrawable(R.styleable.DatePicker_headerBackground)); - final int headerSelectedTextColor = a.getColor( - R.styleable.DatePicker_headerSelectedTextColor, defaultHighlightColor); final int monthTextAppearanceResId = a.getResourceId( - R.styleable.DatePicker_headerMonthTextAppearance, -1); - if (monthTextAppearanceResId != -1) { + R.styleable.DatePicker_headerMonthTextAppearance, 0); + if (monthTextAppearanceResId != 0) { mHeaderMonthTextView.setTextAppearance(context, monthTextAppearanceResId); } - mHeaderMonthTextView.setTextColor(ColorStateList.addFirstIfMissing( - mHeaderMonthTextView.getTextColors(), R.attr.state_selected, - headerSelectedTextColor)); final int dayOfMonthTextAppearanceResId = a.getResourceId( - R.styleable.DatePicker_headerDayOfMonthTextAppearance, -1); - if (dayOfMonthTextAppearanceResId != -1) { + R.styleable.DatePicker_headerDayOfMonthTextAppearance, 0); + if (dayOfMonthTextAppearanceResId != 0) { mHeaderDayOfMonthTextView.setTextAppearance(context, dayOfMonthTextAppearanceResId); } - mHeaderDayOfMonthTextView.setTextColor(ColorStateList.addFirstIfMissing( - mHeaderDayOfMonthTextView.getTextColors(), R.attr.state_selected, - headerSelectedTextColor)); - final int yearTextAppearanceResId = a.getResourceId( - R.styleable.DatePicker_headerYearTextAppearance, -1); - if (yearTextAppearanceResId != -1) { - mHeaderYearTextView.setTextAppearance(context, yearTextAppearanceResId); + final int headerYearTextAppearanceResId = a.getResourceId( + R.styleable.DatePicker_headerYearTextAppearance, 0); + if (headerYearTextAppearanceResId != 0) { + mHeaderYearTextView.setTextAppearance(context, headerYearTextAppearanceResId); } - mHeaderYearTextView.setTextColor(ColorStateList.addFirstIfMissing( - mHeaderYearTextView.getTextColors(), R.attr.state_selected, - headerSelectedTextColor)); mDayPickerView = new DayPickerView(mContext); mDayPickerView.setFirstDayOfWeek(mFirstDayOfWeek); @@ -194,16 +182,23 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate i mYearPickerView.init(this); mYearPickerView.setRange(mMinDate, mMaxDate); - final int yearSelectedCircleColor = a.getColor(R.styleable.DatePicker_yearListSelectorColor, - defaultHighlightColor); - mYearPickerView.setYearSelectedCircleColor(yearSelectedCircleColor); + final ColorStateList yearBackgroundColor = a.getColorStateList( + R.styleable.DatePicker_yearListSelectorColor); + mYearPickerView.setYearBackgroundColor(yearBackgroundColor); + + final int yearTextAppearanceResId = a.getResourceId( + R.styleable.DatePicker_yearListItemTextAppearance, 0); + if (yearTextAppearanceResId != 0) { + mYearPickerView.setYearTextAppearance(yearTextAppearanceResId); + } final ColorStateList calendarTextColor = a.getColorStateList( R.styleable.DatePicker_calendarTextColor); - final int calendarSelectedTextColor = a.getColor( - R.styleable.DatePicker_calendarSelectedTextColor, defaultHighlightColor); - mDayPickerView.setCalendarTextColor(ColorStateList.addFirstIfMissing( - calendarTextColor, R.attr.state_selected, calendarSelectedTextColor)); + mDayPickerView.setCalendarTextColor(calendarTextColor); + + final ColorStateList calendarDayBackgroundColor = a.getColorStateList( + R.styleable.DatePicker_calendarDayBackgroundColor); + mDayPickerView.setCalendarDayBackgroundColor(calendarDayBackgroundColor); mDayPickerDescription = res.getString(R.string.day_picker_description); mSelectDay = res.getString(R.string.select_day); @@ -578,14 +573,8 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate i event.getText().add(mCurrentDate.getTime().toString()); } - @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - event.setClassName(DatePicker.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - info.setClassName(DatePicker.class.getName()); + public CharSequence getAccessibilityClassName() { + return DatePicker.class.getName(); } @Override diff --git a/core/java/android/widget/DateTimeView.java b/core/java/android/widget/DateTimeView.java index db17df7..0b5824a 100644 --- a/core/java/android/widget/DateTimeView.java +++ b/core/java/android/widget/DateTimeView.java @@ -21,17 +21,14 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.BroadcastReceiver; import android.database.ContentObserver; -import android.net.Uri; import android.os.Handler; import android.text.format.Time; import android.util.AttributeSet; import android.util.Log; -import android.provider.Settings; import android.widget.TextView; import android.widget.RemoteViews.RemoteView; import java.text.DateFormat; -import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; diff --git a/core/java/android/widget/DayPickerView.java b/core/java/android/widget/DayPickerView.java index 7db3fb9..65af45d 100644 --- a/core/java/android/widget/DayPickerView.java +++ b/core/java/android/widget/DayPickerView.java @@ -305,6 +305,10 @@ class DayPickerView extends ListView implements AbsListView.OnScrollListener { mAdapter.setCalendarTextColor(colors); } + void setCalendarDayBackgroundColor(ColorStateList dayBackgroundColor) { + mAdapter.setCalendarDayBackgroundColor(dayBackgroundColor); + } + void setCalendarTextAppearance(int resId) { mAdapter.setCalendarTextAppearance(resId); } @@ -459,9 +463,10 @@ class DayPickerView extends ListView implements AbsListView.OnScrollListener { mYearFormat = new SimpleDateFormat("yyyy", Locale.getDefault()); } + /** @hide */ @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); + public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { + super.onInitializeAccessibilityEventInternal(event); event.setItemCount(-1); } @@ -476,22 +481,26 @@ class DayPickerView extends ListView implements AbsListView.OnScrollListener { /** * Necessary for accessibility, to ensure we support "scrolling" forward and backward * in the month list. + * + * @hide */ @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); + public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfoInternal(info); info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD); info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD); } /** * When scroll forward/backward events are received, announce the newly scrolled-to month. + * + * @hide */ @Override - public boolean performAccessibilityAction(int action, Bundle arguments) { + public boolean performAccessibilityActionInternal(int action, Bundle arguments) { if (action != AccessibilityNodeInfo.ACTION_SCROLL_FORWARD && action != AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) { - return super.performAccessibilityAction(action, arguments); + return super.performAccessibilityActionInternal(action, arguments); } // Figure out what month is showing. diff --git a/core/java/android/widget/DigitalClock.java b/core/java/android/widget/DigitalClock.java index b6c1e5b..9e442f9 100644 --- a/core/java/android/widget/DigitalClock.java +++ b/core/java/android/widget/DigitalClock.java @@ -23,9 +23,6 @@ import android.os.SystemClock; import android.provider.Settings; import android.text.format.DateFormat; import android.util.AttributeSet; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; - import java.util.Calendar; /** @@ -116,16 +113,8 @@ public class DigitalClock extends TextView { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - //noinspection deprecation - event.setClassName(DigitalClock.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); + public CharSequence getAccessibilityClassName() { //noinspection deprecation - info.setClassName(DigitalClock.class.getName()); + return DigitalClock.class.getName(); } } diff --git a/core/java/android/widget/EdgeEffect.java b/core/java/android/widget/EdgeEffect.java index 391347e..9019f31 100644 --- a/core/java/android/widget/EdgeEffect.java +++ b/core/java/android/widget/EdgeEffect.java @@ -16,6 +16,7 @@ package android.widget; +import android.annotation.ColorInt; import android.content.res.TypedArray; import android.graphics.Paint; import android.graphics.PorterDuff; @@ -292,7 +293,7 @@ public class EdgeEffect { * * @param color Color in argb */ - public void setColor(int color) { + public void setColor(@ColorInt int color) { mPaint.setColor(color); } @@ -300,6 +301,7 @@ public class EdgeEffect { * Return the color of this edge effect in argb. * @return The color of this edge effect in argb */ + @ColorInt public int getColor() { return mPaint.getColor(); } diff --git a/core/java/android/widget/EditText.java b/core/java/android/widget/EditText.java index a8ff562..d21a5f7 100644 --- a/core/java/android/widget/EditText.java +++ b/core/java/android/widget/EditText.java @@ -25,7 +25,6 @@ import android.text.TextUtils; import android.text.method.ArrowKeyMovementMethod; import android.text.method.MovementMethod; import android.util.AttributeSet; -import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; @@ -123,19 +122,13 @@ public class EditText extends TextView { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(EditText.class.getName()); + public CharSequence getAccessibilityClassName() { + return EditText.class.getName(); } + /** @hide */ @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(EditText.class.getName()); - } - - @Override - public boolean performAccessibilityAction(int action, Bundle arguments) { + public boolean performAccessibilityActionInternal(int action, Bundle arguments) { switch (action) { case AccessibilityNodeInfo.ACTION_SET_TEXT: { CharSequence text = (arguments != null) ? arguments.getCharSequence( @@ -147,7 +140,7 @@ public class EditText extends TextView { return true; } default: { - return super.performAccessibilityAction(action, arguments); + return super.performAccessibilityActionInternal(action, arguments); } } } diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index 2aaad7a..fd34415 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -23,8 +23,6 @@ import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.text.InputFilter; -import android.text.SpannableString; - import com.android.internal.util.ArrayUtils; import com.android.internal.util.GrowingArrayUtils; import com.android.internal.view.menu.MenuBuilder; @@ -50,6 +48,7 @@ import android.graphics.drawable.Drawable; import android.inputmethodservice.ExtractEditText; import android.os.Bundle; import android.os.Handler; +import android.os.ParcelableParcel; import android.os.SystemClock; import android.provider.Settings; import android.text.DynamicLayout; @@ -78,14 +77,14 @@ import android.util.DisplayMetrics; import android.util.Log; import android.view.ActionMode; import android.view.ActionMode.Callback; -import android.view.RenderNode; +import android.view.DisplayListCanvas; import android.view.DragEvent; import android.view.Gravity; -import android.view.HardwareCanvas; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; +import android.view.RenderNode; import android.view.View; import android.view.View.DragShadowBuilder; import android.view.View.OnClickListener; @@ -118,15 +117,19 @@ import java.util.HashMap; */ public class Editor { private static final String TAG = "Editor"; - static final boolean DEBUG_UNDO = false; + private static final boolean DEBUG_UNDO = false; static final int BLINK = 500; private static final float[] TEMP_POSITION = new float[2]; private static int DRAG_SHADOW_MAX_TEXT_LENGTH = 20; + // Tag used when the Editor maintains its own separate UndoManager. + private static final String UNDO_OWNER_TAG = "Editor"; - UndoManager mUndoManager; - UndoOwner mUndoOwner; - InputFilter mUndoInputFilter; + // Each Editor manages its own undo stack. + private final UndoManager mUndoManager = new UndoManager(); + private UndoOwner mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this); + final UndoInputFilter mUndoInputFilter = new UndoInputFilter(this); + boolean mAllowUndo = true; // Cursor Controllers. InsertionPointCursorController mInsertionPointCursorController; @@ -214,6 +217,12 @@ public class Editor { WordIterator mWordIterator; SpellChecker mSpellChecker; + // This word iterator is set with text and used to determine word boundaries + // when a user is selecting text. + private WordIterator mWordIteratorWithText; + // Indicate that the text in the word iterator needs to be updated. + private boolean mUpdateWordIteratorText; + private Rect mTempRect; private TextView mTextView; @@ -222,6 +231,59 @@ public class Editor { Editor(TextView textView) { mTextView = textView; + // Synchronize the filter list, which places the undo input filter at the end. + mTextView.setFilters(mTextView.getFilters()); + } + + ParcelableParcel saveInstanceState() { + ParcelableParcel state = new ParcelableParcel(getClass().getClassLoader()); + Parcel parcel = state.getParcel(); + mUndoManager.saveInstanceState(parcel); + mUndoInputFilter.saveInstanceState(parcel); + return state; + } + + void restoreInstanceState(ParcelableParcel state) { + Parcel parcel = state.getParcel(); + mUndoManager.restoreInstanceState(parcel, state.getClassLoader()); + mUndoInputFilter.restoreInstanceState(parcel); + // Re-associate this object as the owner of undo state. + mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this); + } + + /** + * Forgets all undo and redo operations for this Editor. + */ + void forgetUndoRedo() { + UndoOwner[] owners = { mUndoOwner }; + mUndoManager.forgetUndos(owners, -1 /* all */); + mUndoManager.forgetRedos(owners, -1 /* all */); + } + + boolean canUndo() { + UndoOwner[] owners = { mUndoOwner }; + return mAllowUndo && mUndoManager.countUndos(owners) > 0; + } + + boolean canRedo() { + UndoOwner[] owners = { mUndoOwner }; + return mAllowUndo && mUndoManager.countRedos(owners) > 0; + } + + void undo() { + if (!mAllowUndo) { + return; + } + UndoOwner[] owners = { mUndoOwner }; + mUndoManager.undo(owners, 1); // Undo 1 action. + } + + void redo() { + if (!mAllowUndo) { + return; + } + UndoOwner[] owners = { mUndoOwner }; + mUndoManager.redo(owners, 1); // Redo 1 action. } void onAttachedToWindow() { @@ -252,7 +314,7 @@ public class Editor { mTextView.setHasTransientState(false); // We had an active selection from before, start the selection mode. - startSelectionActionMode(); + startSelectionActionModeWithSelection(); } getPositionListener().addSubscriber(mCursorAnchorInfoNotifier, true); @@ -647,9 +709,52 @@ public class Editor { return mTextView.getTransformationMethod() instanceof PasswordTransformationMethod; } + private int getWordStart(int offset) { + // FIXME - For this and similar methods we're not doing anything to check if there's + // a LocaleSpan in the text, this may be something we should try handling or checking for. + int retOffset = getWordIteratorWithText().getBeginning(offset); + if (retOffset == BreakIterator.DONE) retOffset = offset; + return retOffset; + } + + private int getWordEnd(int offset, boolean includePunctuation) { + int retOffset = getWordIteratorWithText().getEnd(offset); + if (retOffset == BreakIterator.DONE) { + retOffset = offset; + } else if (includePunctuation) { + retOffset = handlePunctuation(retOffset); + } + return retOffset; + } + + private boolean isEndBoundary(int offset) { + int thisEnd = getWordEnd(offset, false); + return offset == thisEnd; + } + + private boolean isStartBoundary(int offset) { + int thisStart = getWordStart(offset); + return thisStart == offset; + } + + private int handlePunctuation(int offset) { + // FIXME - Check with UX how repeated ending punctuation should be handled. + // FIXME - Check with UX if / how we would handle non sentence ending characters. + // FIXME - Consider punctuation in different languages. + CharSequence text = mTextView.getText(); + if (offset < text.length()) { + int c = Character.codePointAt(text, offset); + if (c == 0x002e /* period */|| c == 0x003f /* question mark */ + || c == 0x0021 /* exclamation mark */) { + offset = Character.offsetByCodePoints(text, offset, 1); + } + } + return offset; + } + /** - * Adjusts selection to the word under last touch offset. - * Return true if the operation was successfully performed. + * Adjusts selection to the word under last touch offset. Return true if the operation was + * successfully performed. */ private boolean selectCurrentWord() { if (!canSelectText()) { @@ -696,6 +801,8 @@ public class Editor { selectionStart = ((Spanned) mTextView.getText()).getSpanStart(urlSpan); selectionEnd = ((Spanned) mTextView.getText()).getSpanEnd(urlSpan); } else { + // FIXME - We should check if there's a LocaleSpan in the text, this may be + // something we should try handling or checking for. final WordIterator wordIterator = getWordIterator(); wordIterator.setCharSequence(mTextView.getText(), minOffset, maxOffset); @@ -718,6 +825,7 @@ public class Editor { void onLocaleChanged() { // Will be re-created on demand in getWordIterator with the proper new locale mWordIterator = null; + mWordIteratorWithText = null; } /** @@ -730,6 +838,23 @@ public class Editor { return mWordIterator; } + private WordIterator getWordIteratorWithText() { + if (mWordIteratorWithText == null) { + mWordIteratorWithText = new WordIterator(mTextView.getTextServicesLocale()); + mUpdateWordIteratorText = true; + } + if (mUpdateWordIteratorText) { + // FIXME - Shouldn't copy all of the text as only the area of the text relevant + // to the user's selection is needed. A possible solution would be to + // copy some number N of characters near the selection and then when the + // user approaches N then we'd do another copy of the next N characters. + CharSequence text = mTextView.getText(); + mWordIteratorWithText.setCharSequence(text, 0, text.length()); + mUpdateWordIteratorText = false; + } + return mWordIteratorWithText; + } + private long getCharRange(int offset) { final int textLength = mTextView.getText().length(); if (offset + 1 < textLength) { @@ -856,14 +981,15 @@ public class Editor { } public boolean performLongClick(boolean handled) { - // Long press in empty space moves cursor and shows the Paste affordance if available. + // Long press in empty space moves cursor and starts the selection action mode. if (!handled && !isPositionOnText(mLastDownPositionX, mLastDownPositionY) && mInsertionControllerEnabled) { final int offset = mTextView.getOffsetForPosition(mLastDownPositionX, mLastDownPositionY); stopSelectionActionMode(); Selection.setSelection((Spannable) mTextView.getText(), offset); - getInsertionController().showWithActionPopup(); + getInsertionController().show(); + startSelectionActionModeWithoutSelection(); handled = true; } @@ -878,16 +1004,15 @@ public class Editor { mTextView.startDrag(data, getTextThumbnailBuilder(selectedText), localState, 0); stopSelectionActionMode(); } else { - getSelectionController().hide(); - selectCurrentWord(); - getSelectionController().show(); + stopSelectionActionMode(); + startSelectionActionModeWithSelection(); } handled = true; } // Start a new selection if (!handled) { - handled = startSelectionActionMode(); + handled = startSelectionActionModeWithSelection(); } return handled; @@ -1016,6 +1141,9 @@ public class Editor { void sendOnTextChanged(int start, int after) { updateSpellCheckSpans(start, start + after, false); + // Flip flag to indicate the word iterator needs to have the text reset. + mUpdateWordIteratorText = true; + // Hide the controllers as soon as text is modified (typing, procedural...) // We do not hide the span controllers, since they can be added when a new text is // inserted into the text view (voice IME). @@ -1101,6 +1229,7 @@ public class Editor { ims.mChangedEnd = EXTRACT_UNKNOWN; ims.mContentChanged = false; } + mUndoInputFilter.beginBatchEdit(); mTextView.onBeginBatchEdit(); } } @@ -1127,6 +1256,7 @@ public class Editor { void finishBatchEdit(final InputMethodState ims) { mTextView.onEndBatchEdit(); + mUndoInputFilter.endBatchEdit(); if (ims.mContentChanged || ims.mSelectionModeChanged) { mTextView.updateAfterEdit(); @@ -1352,6 +1482,9 @@ public class Editor { searchStartIndex); // Note how dynamic layout's internal block indices get updated from Editor blockIndices[i] = blockIndex; + if (mTextDisplayLists[blockIndex] != null) { + mTextDisplayLists[blockIndex].isDirty = true; + } searchStartIndex = blockIndex + 1; } @@ -1381,17 +1514,18 @@ public class Editor { // Rebuild display list if it is invalid if (blockDisplayListIsInvalid) { - final HardwareCanvas hardwareCanvas = blockDisplayList.start( + final DisplayListCanvas displayListCanvas = blockDisplayList.start( right - left, bottom - top); try { // drawText is always relative to TextView's origin, this translation // brings this range of text back to the top left corner of the viewport - hardwareCanvas.translate(-left, -top); - layout.drawText(hardwareCanvas, blockBeginLine, blockEndLine); + displayListCanvas.translate(-left, -top); + layout.drawText(displayListCanvas, blockBeginLine, blockEndLine); + mTextDisplayLists[blockIndex].isDirty = false; // No need to untranslate, previous context is popped after // drawDisplayList } finally { - blockDisplayList.end(hardwareCanvas); + blockDisplayList.end(displayListCanvas); // Same as drawDisplayList below, handled by our TextView's parent blockDisplayList.setClipToBounds(false); } @@ -1402,7 +1536,7 @@ public class Editor { blockDisplayList.setLeftTopRightBottom(left, top, right, bottom); } - ((HardwareCanvas) canvas).drawRenderNode(blockDisplayList, null, + ((DisplayListCanvas) canvas).drawRenderNode(blockDisplayList, 0 /* no child clipping, our TextView parent enforces it */); endOfPreviousBlock = blockEndLine; @@ -1529,7 +1663,21 @@ public class Editor { /** * @return true if the selection mode was actually started. */ - boolean startSelectionActionMode() { + private boolean startSelectionActionModeWithoutSelection() { + if (mSelectionActionMode != null) { + // Selection action mode is already started + // TODO: revisit invocations to minimize this case. + return false; + } + ActionMode.Callback actionModeCallback = new SelectionActionModeCallback(); + mSelectionActionMode = mTextView.startActionMode(actionModeCallback); + return mSelectionActionMode != null; + } + + /** + * @return true if the selection mode was actually started. + */ + boolean startSelectionActionModeWithSelection() { if (mSelectionActionMode != null) { // Selection action mode is already started return false; @@ -1567,6 +1715,9 @@ public class Editor { } } + if (selectionStarted) { + getSelectionController().enterDrag(); + } return selectionStarted; } @@ -1702,7 +1853,7 @@ public class Editor { /** * Called by the framework in response to a text auto-correction (such as fixing a typo using a - * a dictionnary) from the current input method, provided by it calling + * a dictionary) from the current input method, provided by it calling * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default * implementation flashes the background of the corrected word to provide feedback to the user. * @@ -2838,6 +2989,12 @@ public class Editor { MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); } + if (mTextView.isSuggestionsEnabled() && isCursorInsideSuggestionSpan()) { + menu.add(0, TextView.ID_REPLACE, 0, com.android.internal.R.string.replace). + setShowAsAction( + MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + } + styledAttributes.recycle(); if (mCustomSelectionActionModeCallback != null) { @@ -2848,7 +3005,6 @@ public class Editor { } if (menu.hasVisibleItems() || mode.getCustomView() != null) { - getSelectionController().show(); mTextView.setHasTransientState(true); return true; } else { @@ -2870,6 +3026,10 @@ public class Editor { mCustomSelectionActionModeCallback.onActionItemClicked(mode, item)) { return true; } + if (item.getItemId() == TextView.ID_REPLACE) { + onReplace(); + return true; + } return mTextView.onTextContextMenuItem(item.getItemId()); } @@ -2898,6 +3058,13 @@ public class Editor { } } + private void onReplace() { + int middle = (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2; + stopSelectionActionMode(); + Selection.setSelection((Spannable) mTextView.getText(), middle); + showSuggestions(); + } + private class ActionPopupWindow extends PinnedPopupWindow implements OnClickListener { private static final int POPUP_TEXT_LAYOUT = com.android.internal.R.layout.text_edit_action_popup_text; @@ -2956,10 +3123,7 @@ public class Editor { mTextView.onTextContextMenuItem(TextView.ID_PASTE); hide(); } else if (view == mReplaceTextView) { - int middle = (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2; - stopSelectionActionMode(); - Selection.setSelection((Spannable) mTextView.getText(), middle); - showSuggestions(); + onReplace(); } } @@ -3176,16 +3340,14 @@ public class Editor { private float mIdealVerticalOffset; // Parent's (TextView) previous position in window private int mLastParentX, mLastParentY; - // Transient action popup window for Paste and Replace actions - protected ActionPopupWindow mActionPopupWindow; // Previous text character offset private int mPreviousOffset = -1; // Previous text character offset private boolean mPositionHasChanged = true; - // Used to delay the appearance of the action popup window - private Runnable mActionPopupShower; // Minimum touch target size for handles private int mMinSize; + // Indicates the line of text that the handle is on. + protected int mLine = -1; public HandleView(Drawable drawableLtr, Drawable drawableRtl) { super(mTextView.getContext()); @@ -3194,6 +3356,8 @@ public class Editor { mContainer.setSplitTouchEnabled(true); mContainer.setClippingEnabled(false); mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL); + mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT); + mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); mContainer.setContentView(this); mDrawableLtr = drawableLtr; @@ -3281,8 +3445,6 @@ public class Editor { // Make sure the offset is always considered new, even when focusing at same position mPreviousOffset = -1; positionAtCursorOffset(getCurrentCursorOffset(), false); - - hideActionPopupWindow(); } protected void dismiss() { @@ -3297,31 +3459,6 @@ public class Editor { getPositionListener().removeSubscriber(this); } - void showActionPopupWindow(int delay) { - if (mActionPopupWindow == null) { - mActionPopupWindow = new ActionPopupWindow(); - } - if (mActionPopupShower == null) { - mActionPopupShower = new Runnable() { - public void run() { - mActionPopupWindow.show(); - } - }; - } else { - mTextView.removeCallbacks(mActionPopupShower); - } - mTextView.postDelayed(mActionPopupShower, delay); - } - - protected void hideActionPopupWindow() { - if (mActionPopupShower != null) { - mTextView.removeCallbacks(mActionPopupShower); - } - if (mActionPopupWindow != null) { - mActionPopupWindow.hide(); - } - } - public boolean isShowing() { return mContainer.isShowing(); } @@ -3361,6 +3498,7 @@ public class Editor { addPositionToTouchUpFilter(offset); } final int line = layout.getLineForOffset(offset); + mLine = line; mPositionX = (int) (layout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX - getHorizontalOffset() + getCursorOffset()); @@ -3410,6 +3548,30 @@ public class Editor { } } + public void showAtLocation(int offset) { + // TODO - investigate if there's a better way to show the handles + // after the drag accelerator has occured. + int[] tmpCords = new int[2]; + mTextView.getLocationInWindow(tmpCords); + + Layout layout = mTextView.getLayout(); + int posX = tmpCords[0]; + int posY = tmpCords[1]; + + final int line = layout.getLineForOffset(offset); + + int startX = (int) (layout.getPrimaryHorizontal(offset) - 0.5f + - mHotspotX - getHorizontalOffset() + getCursorOffset()); + int startY = layout.getLineBottom(line); + + // Take TextView's padding and scroll into account. + startX += mTextView.viewportToContentHorizontalOffset(); + startY += mTextView.viewportToContentVerticalOffset(); + + mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY, + startX + posX, startY + posY); + } + @Override protected void onDraw(Canvas c) { final int drawWidth = mDrawable.getIntrinsicWidth(); @@ -3497,20 +3659,16 @@ public class Editor { return mIsDragging; } - void onHandleMoved() { - hideActionPopupWindow(); - } + void onHandleMoved() {} - public void onDetached() { - hideActionPopupWindow(); - } + public void onDetached() {} } private class InsertionHandleView extends HandleView { private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000; private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds - // Used to detect taps on the insertion handle, which will affect the ActionPopupWindow + // Used to detect taps on the insertion handle, which will affect the selection action mode private float mDownPositionX, mDownPositionY; private Runnable mHider; @@ -3525,17 +3683,12 @@ public class Editor { final long durationSinceCutOrCopy = SystemClock.uptimeMillis() - TextView.LAST_CUT_OR_COPY_TIME; if (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION) { - showActionPopupWindow(0); + startSelectionActionModeWithoutSelection(); } hideAfterDelay(); } - public void showWithActionPopup() { - show(); - showActionPopupWindow(0); - } - private void hideAfterDelay() { if (mHider == null) { mHider = new Runnable() { @@ -3597,11 +3750,11 @@ public class Editor { final int touchSlop = viewConfiguration.getScaledTouchSlop(); if (distanceSquared < touchSlop * touchSlop) { - if (mActionPopupWindow != null && mActionPopupWindow.isShowing()) { - // Tapping on the handle dismisses the displayed action popup - mActionPopupWindow.hide(); + // Tapping on the handle toggles the selection action mode. + if (mSelectionActionMode != null) { + mSelectionActionMode.finish(); } else { - showWithActionPopup(); + startSelectionActionModeWithoutSelection(); } } } @@ -3648,6 +3801,12 @@ public class Editor { } private class SelectionStartHandleView extends HandleView { + // The previous offset this handle was at. + private int mPrevOffset; + // Indicates whether the cursor is making adjustments within a word. + private boolean mInWord = false; + // Offset to track difference between touch and word boundary. + protected int mTouchWordOffset; public SelectionStartHandleView(Drawable drawableLtr, Drawable drawableRtl) { super(drawableLtr, drawableRtl); @@ -3655,11 +3814,7 @@ public class Editor { @Override protected int getHotspotX(Drawable drawable, boolean isRtlRun) { - if (isRtlRun) { - return drawable.getIntrinsicWidth() / 4; - } else { - return (drawable.getIntrinsicWidth() * 3) / 4; - } + return isRtlRun ? 0 : drawable.getIntrinsicWidth(); } @Override @@ -3681,21 +3836,77 @@ public class Editor { @Override public void updatePosition(float x, float y) { - int offset = mTextView.getOffsetForPosition(x, y); - - // Handles can not cross and selection is at least one character - final int selectionEnd = mTextView.getSelectionEnd(); - if (offset >= selectionEnd) offset = Math.max(0, selectionEnd - 1); + final int trueOffset = mTextView.getOffsetForPosition(x, y); + final int currLine = mTextView.getLineAtCoordinate(y); + int offset = trueOffset; + boolean positionCursor = false; + + int end = getWordEnd(offset, true); + int start = getWordStart(offset); + + if (offset < mPrevOffset) { + // User is increasing the selection. + if (!mInWord || currLine < mLine) { + // We're not in a word, or we're on a different line so we'll expand by + // word. First ensure the user has at least entered the next word. + int offsetToWord = Math.min((end - start) / 2, 2); + if (offset <= end - offsetToWord || currLine < mLine) { + offset = start; + } else { + offset = mPrevOffset; + } + } + mPrevOffset = offset; + mTouchWordOffset = Math.max(trueOffset - offset, 0); + mInWord = !isStartBoundary(offset); + positionCursor = true; + } else if (offset - mTouchWordOffset > mPrevOffset) { + // User is shrinking the selection. + if (currLine > mLine) { + // We're on a different line, so we'll snap to word boundaries. + offset = end; + } + offset -= mTouchWordOffset; + mPrevOffset = offset; + mInWord = !isEndBoundary(offset); + positionCursor = true; + } - positionAtCursorOffset(offset, false); + // Handles can not cross and selection is at least one character. + if (positionCursor) { + final int selectionEnd = mTextView.getSelectionEnd(); + if (offset >= selectionEnd) { + // We can't cross the handles so let's just constrain the Y value. + int alteredOffset = mTextView.getOffsetAtCoordinate(mLine, x); + if (alteredOffset >= selectionEnd) { + // Can't pass the other drag handle. + offset = Math.max(0, selectionEnd - 1); + } else { + offset = alteredOffset; + } + } + positionAtCursorOffset(offset, false); + } } - public ActionPopupWindow getActionPopupWindow() { - return mActionPopupWindow; + @Override + public boolean onTouchEvent(MotionEvent event) { + boolean superResult = super.onTouchEvent(event); + if (event.getActionMasked() == MotionEvent.ACTION_UP) { + // Reset the touch word offset when the user has lifted their finger. + mTouchWordOffset = 0; + } + return superResult; } } private class SelectionEndHandleView extends HandleView { + // The previous offset this handle was at. + private int mPrevOffset; + // Indicates whether the cursor is making adjustments within a word. + private boolean mInWord = false; + // Offset to track difference between touch and word boundary. + protected int mTouchWordOffset; public SelectionEndHandleView(Drawable drawableLtr, Drawable drawableRtl) { super(drawableLtr, drawableRtl); @@ -3703,11 +3914,7 @@ public class Editor { @Override protected int getHotspotX(Drawable drawable, boolean isRtlRun) { - if (isRtlRun) { - return (drawable.getIntrinsicWidth() * 3) / 4; - } else { - return drawable.getIntrinsicWidth() / 4; - } + return isRtlRun ? drawable.getIntrinsicWidth() : 0; } @Override @@ -3729,19 +3936,67 @@ public class Editor { @Override public void updatePosition(float x, float y) { - int offset = mTextView.getOffsetForPosition(x, y); - - // Handles can not cross and selection is at least one character - final int selectionStart = mTextView.getSelectionStart(); - if (offset <= selectionStart) { - offset = Math.min(selectionStart + 1, mTextView.getText().length()); + final int trueOffset = mTextView.getOffsetForPosition(x, y); + final int currLine = mTextView.getLineAtCoordinate(y); + int offset = trueOffset; + boolean positionCursor = false; + + int end = getWordEnd(offset, true); + int start = getWordStart(offset); + + if (offset > mPrevOffset) { + // User is increasing the selection. + if (!mInWord || currLine > mLine) { + // We're not in a word, or we're on a different line so we'll expand by + // word. First ensure the user has at least entered the next word. + int midPoint = Math.min((end - start) / 2, 2); + if (offset >= start + midPoint || currLine > mLine) { + offset = end; + } else { + offset = mPrevOffset; + } + } + mPrevOffset = offset; + mTouchWordOffset = Math.max(offset - trueOffset, 0); + mInWord = !isEndBoundary(offset); + positionCursor = true; + } else if (offset + mTouchWordOffset < mPrevOffset) { + // User is shrinking the selection. + if (currLine > mLine) { + // We're on a different line, so we'll snap to word boundaries. + offset = getWordStart(offset); + } + offset += mTouchWordOffset; + mPrevOffset = offset; + positionCursor = true; + mInWord = !isStartBoundary(offset); } - positionAtCursorOffset(offset, false); + if (positionCursor) { + final int selectionStart = mTextView.getSelectionStart(); + if (offset <= selectionStart) { + // We can't cross the handles so let's just constrain the Y value. + int alteredOffset = mTextView.getOffsetAtCoordinate(mLine, x); + int length = mTextView.getText().length(); + if (alteredOffset <= selectionStart) { + // Can't pass the other drag handle. + offset = Math.min(selectionStart + 1, length); + } else { + offset = Math.min(alteredOffset, length); + } + } + positionAtCursorOffset(offset, false); + } } - public void setActionPopupWindow(ActionPopupWindow actionPopupWindow) { - mActionPopupWindow = actionPopupWindow; + @Override + public boolean onTouchEvent(MotionEvent event) { + boolean superResult = super.onTouchEvent(event); + if (event.getActionMasked() == MotionEvent.ACTION_UP) { + // Reset the touch word offset when the user has lifted their finger. + mTouchWordOffset = 0; + } + return superResult; } } @@ -3776,10 +4031,6 @@ public class Editor { getHandle().show(); } - public void showWithActionPopup() { - getHandle().showWithActionPopup(); - } - public void hide() { if (mHandle != null) { mHandle.hide(); @@ -3825,6 +4076,11 @@ public class Editor { private float mDownPositionX, mDownPositionY; private boolean mGestureStayedInTapRegion; + // Where the user first starts the drag motion. + private int mStartOffset = -1; + // Indicates whether the user is selecting text and using the drag accelerator. + private boolean mDragAcceleratorActive; + SelectionModifierCursorController() { resetTouchOffsets(); } @@ -3861,11 +4117,6 @@ public class Editor { mStartHandle.show(); mEndHandle.show(); - // Make sure both left and right handles share the same ActionPopupWindow (so that - // moving any of the handles hides the action popup). - mStartHandle.showActionPopupWindow(DELAY_BEFORE_REPLACE_ACTION); - mEndHandle.setActionPopupWindow(mStartHandle.getActionPopupWindow()); - hideInsertionPointCursorController(); } @@ -3874,6 +4125,22 @@ public class Editor { if (mEndHandle != null) mEndHandle.hide(); } + public void enterDrag() { + // Just need to init the handles / hide insertion cursor. + show(); + mDragAcceleratorActive = true; + // Start location of selection. + mStartOffset = mTextView.getOffsetForPosition(mLastDownPositionX, + mLastDownPositionY); + // Don't show the handles until user has lifted finger. + hide(); + + // This stops scrolling parents from intercepting the touch event, allowing + // the user to continue dragging across the screen to select text; TextView will + // scroll as necessary. + mTextView.getParent().requestDisallowInterceptTouchEvent(true); + } + public void onTouchEvent(MotionEvent event) { // This is done even when the View does not have focus, so that long presses can start // selection and tap can move cursor from this tap position. @@ -3882,7 +4149,7 @@ public class Editor { final float x = event.getX(); final float y = event.getY(); - // Remember finger down position, to be able to start selection from there + // Remember finger down position, to be able to start selection from there. mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition(x, y); // Double tap detection @@ -3899,7 +4166,7 @@ public class Editor { boolean stayedInArea = distanceSquared < doubleTapSlop * doubleTapSlop; if (stayedInArea && isPositionOnText(x, y)) { - startSelectionActionMode(); + startSelectionActionModeWithSelection(); mDiscardNextActionUp = true; } } @@ -3921,23 +4188,112 @@ public class Editor { break; case MotionEvent.ACTION_MOVE: + final ViewConfiguration viewConfiguration = ViewConfiguration.get( + mTextView.getContext()); + if (mGestureStayedInTapRegion) { final float deltaX = event.getX() - mDownPositionX; final float deltaY = event.getY() - mDownPositionY; final float distanceSquared = deltaX * deltaX + deltaY * deltaY; - final ViewConfiguration viewConfiguration = ViewConfiguration.get( - mTextView.getContext()); int doubleTapTouchSlop = viewConfiguration.getScaledDoubleTapTouchSlop(); if (distanceSquared > doubleTapTouchSlop * doubleTapTouchSlop) { mGestureStayedInTapRegion = false; } } + + if (mStartHandle != null && mStartHandle.isShowing()) { + // Don't do the drag if the handles are showing already. + break; + } + + if (mStartOffset != -1) { + final int rawOffset = mTextView.getOffsetForPosition(event.getX(), + event.getY()); + int offset = rawOffset; + + // We don't start "dragging" until the user is past the initial word that + // gets selected on long press. + int firstWordStart = getWordStart(mStartOffset); + int firstWordEnd = getWordEnd(mStartOffset, false); + if (offset > firstWordEnd || offset < firstWordStart) { + + // Basically the goal in the below code is to have the highlight be + // offset so that your finger isn't covering the end point. + int fingerOffset = viewConfiguration.getScaledTouchSlop(); + float mx = event.getX(); + float my = event.getY(); + if (mx > fingerOffset) mx -= fingerOffset; + if (my > fingerOffset) my -= fingerOffset; + offset = mTextView.getOffsetForPosition(mx, my); + + // Perform the check for closeness at edge of view, if we're very close + // don't adjust the offset to be in front of the finger - otherwise the + // user can't select words at the edge. + if (mTextView.getWidth() - fingerOffset > mx) { + // We're going by word, so we need to make sure that the offset + // that we get is within this, so we'll get the previous boundary. + final WordIterator wordIterator = getWordIteratorWithText(); + + final int precedingOffset = wordIterator.preceding(offset); + if (mStartOffset < offset) { + // Expanding with bottom handle, in this case the selection end + // is before the finger. + offset = Math.max(precedingOffset - 1, 0); + } else { + // Expand with the start handle, in this case the selection + // start is before the finger. + if (precedingOffset == WordIterator.DONE) { + offset = 0; + } else { + offset = wordIterator.preceding(precedingOffset); + } + } + } + if (offset == WordIterator.DONE) + offset = rawOffset; + + // Need to adjust start offset based on direction of movement. + int newStart = mStartOffset < offset ? getWordStart(mStartOffset) + : getWordEnd(mStartOffset, true); + Selection.setSelection((Spannable) mTextView.getText(), newStart, + offset); + } + } break; case MotionEvent.ACTION_UP: mPreviousTapUpTime = SystemClock.uptimeMillis(); + if (mDragAcceleratorActive) { + // No longer dragging to select text, let the parent intercept events. + mTextView.getParent().requestDisallowInterceptTouchEvent(false); + + show(); + int startOffset = mTextView.getSelectionStart(); + int endOffset = mTextView.getSelectionEnd(); + + // Since we don't let drag handles pass once they're visible, we need to + // make sure the start / end locations are correct because the user *can* + // switch directions during the initial drag. + if (endOffset < startOffset) { + int tmp = endOffset; + endOffset = startOffset; + startOffset = tmp; + + // Also update the selection with the right offsets in this case. + Selection.setSelection((Spannable) mTextView.getText(), + startOffset, endOffset); + } + + // Need to do this to display the handles. + mStartHandle.showAtLocation(startOffset); + mEndHandle.showAtLocation(endOffset); + + // No longer the first dragging motion, reset. + mDragAcceleratorActive = false; + mStartOffset = -1; + } break; } } @@ -3964,6 +4320,8 @@ public class Editor { public void resetTouchOffsets() { mMinTouchOffset = mMaxTouchOffset = -1; + mStartOffset = -1; + mDragAcceleratorActive = false; } /** @@ -3973,6 +4331,13 @@ public class Editor { return mStartHandle != null && mStartHandle.isDragging(); } + /** + * @return true if the user is selecting text using the drag accelerator. + */ + public boolean isDragAcceleratorActive() { + return mDragAcceleratorActive; + } + public void onTouchModeChanged(boolean isInTouchMode) { if (!isInTouchMode) { hide(); @@ -4157,103 +4522,279 @@ public class Editor { int mChangedStart, mChangedEnd, mChangedDelta; } + /** + * @return True iff (start, end) is a valid range within the text. + */ + private static boolean isValidRange(CharSequence text, int start, int end) { + return 0 <= start && start <= end && end <= text.length(); + } + + /** + * An InputFilter that monitors text input to maintain undo history. It does not modify the + * text being typed (and hence always returns null from the filter() method). + */ public static class UndoInputFilter implements InputFilter { - final Editor mEditor; + private final Editor mEditor; + + // Whether the current filter pass is directly caused by an end-user text edit. + private boolean mIsUserEdit; + + // Whether the text field is handling an IME composition. Must be parceled in case the user + // rotates the screen during composition. + private boolean mHasComposition; public UndoInputFilter(Editor editor) { mEditor = editor; } + public void saveInstanceState(Parcel parcel) { + parcel.writeInt(mIsUserEdit ? 1 : 0); + parcel.writeInt(mHasComposition ? 1 : 0); + } + + public void restoreInstanceState(Parcel parcel) { + mIsUserEdit = parcel.readInt() != 0; + mHasComposition = parcel.readInt() != 0; + } + + /** + * Signals that a user-triggered edit is starting. + */ + public void beginBatchEdit() { + if (DEBUG_UNDO) Log.d(TAG, "beginBatchEdit"); + mIsUserEdit = true; + } + + public void endBatchEdit() { + if (DEBUG_UNDO) Log.d(TAG, "endBatchEdit"); + mIsUserEdit = false; + } + @Override public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { if (DEBUG_UNDO) { - Log.d(TAG, "filter: source=" + source + " (" + start + "-" + end + ")"); - Log.d(TAG, "filter: dest=" + dest + " (" + dstart + "-" + dend + ")"); + Log.d(TAG, "filter: source=" + source + " (" + start + "-" + end + ") " + + "dest=" + dest + " (" + dstart + "-" + dend + ")"); } - final UndoManager um = mEditor.mUndoManager; - if (um.isInUndo()) { - if (DEBUG_UNDO) Log.d(TAG, "*** skipping, currently performing undo/redo"); + + // Check to see if this edit should be tracked for undo. + if (!canUndoEdit(source, start, end, dest, dstart, dend)) { return null; } - um.beginUpdate("Edit text"); - TextModifyOperation op = um.getLastOperation( - TextModifyOperation.class, mEditor.mUndoOwner, UndoManager.MERGE_MODE_UNIQUE); - if (op != null) { - if (DEBUG_UNDO) Log.d(TAG, "Last op: range=(" + op.mRangeStart + "-" + op.mRangeEnd - + "), oldText=" + op.mOldText); - // See if we can continue modifying this operation. - if (op.mOldText == null) { - // The current operation is an add... are we adding more? We are adding - // more if we are either appending new text to the end of the last edit or - // completely replacing some or all of the last edit. - if (start < end && ((dstart >= op.mRangeStart && dend <= op.mRangeEnd) - || (dstart == op.mRangeEnd && dend == op.mRangeEnd))) { - op.mRangeEnd = dstart + (end-start); - um.endUpdate(); - if (DEBUG_UNDO) Log.d(TAG, "*** merging with last op, mRangeEnd=" - + op.mRangeEnd); - return null; - } - } else { - // The current operation is a delete... can we delete more? - if (start == end && dend == op.mRangeStart-1) { - SpannableStringBuilder str; - if (op.mOldText instanceof SpannableString) { - str = (SpannableStringBuilder)op.mOldText; - } else { - str = new SpannableStringBuilder(op.mOldText); - } - str.insert(0, dest, dstart, dend); - op.mRangeStart = dstart; - op.mOldText = str; - um.endUpdate(); - if (DEBUG_UNDO) Log.d(TAG, "*** merging with last op, range=(" - + op.mRangeStart + "-" + op.mRangeEnd - + "), oldText=" + op.mOldText); - return null; - } + // Check for and handle IME composition edits. + if (handleCompositionEdit(source, start, end, dstart)) { + return null; + } + + // Handle keyboard edits. + handleKeyboardEdit(source, start, end, dest, dstart, dend); + return null; + } + + /** + * Returns true iff the edit was handled, either because it should be ignored or because + * this function created an undo operation for it. + */ + private boolean handleCompositionEdit(CharSequence source, int start, int end, int dstart) { + // Ignore edits while the user is composing. + if (isComposition(source)) { + mHasComposition = true; + return true; + } + final boolean hadComposition = mHasComposition; + mHasComposition = false; + + // Check for the transition out of the composing state. + if (hadComposition) { + // If there was no text the user canceled composition. Ignore the edit. + if (start == end) { + return true; } - // Couldn't add to the current undo operation, need to start a new - // undo state for a new undo operation. - um.commitState(null); - um.setUndoLabel("Edit text"); + // Otherwise the user inserted the composition. + String newText = TextUtils.substring(source, start, end); + EditOperation edit = new EditOperation(mEditor, false, "", dstart, newText); + recordEdit(edit); + return true; } - // Create a new undo state reflecting the operation being performed. - op = new TextModifyOperation(mEditor.mUndoOwner); - op.mRangeStart = dstart; - if (start < end) { - op.mRangeEnd = dstart + (end-start); + // This was neither a composition event nor a transition out of composing. + return false; + } + + private void handleKeyboardEdit(CharSequence source, int start, int end, + Spanned dest, int dstart, int dend) { + // An application may install a TextWatcher to provide additional modifications after + // the initial input filters run (e.g. a credit card formatter that adds spaces to a + // string). This results in multiple filter() calls for what the user considers to be + // a single operation. Always undo the whole set of changes in one step. + final boolean forceMerge = isInTextWatcher(); + + // Build a new operation with all the information from this edit. + String newText = TextUtils.substring(source, start, end); + String oldText = TextUtils.substring(dest, dstart, dend); + EditOperation edit = new EditOperation(mEditor, forceMerge, oldText, dstart, newText); + recordEdit(edit); + } + + private void recordEdit(EditOperation edit) { + // Fetch the last edit operation and attempt to merge in the new edit. + final UndoManager um = mEditor.mUndoManager; + um.beginUpdate("Edit text"); + EditOperation lastEdit = um.getLastOperation( + EditOperation.class, mEditor.mUndoOwner, UndoManager.MERGE_MODE_UNIQUE); + if (lastEdit == null) { + // Add this as the first edit. + if (DEBUG_UNDO) Log.d(TAG, "filter: adding first op " + edit); + um.addOperation(edit, UndoManager.MERGE_MODE_NONE); + } else if (!mIsUserEdit) { + // An application directly modified the Editable outside of a text edit. Treat this + // as a new change and don't attempt to merge. + if (DEBUG_UNDO) Log.d(TAG, "non-user edit, new op " + edit); + um.commitState(mEditor.mUndoOwner); + um.addOperation(edit, UndoManager.MERGE_MODE_NONE); + } else if (lastEdit.mergeWith(edit)) { + // Merge succeeded, nothing else to do. + if (DEBUG_UNDO) Log.d(TAG, "filter: merge succeeded, created " + lastEdit); } else { - op.mRangeEnd = dstart; - } - if (dstart < dend) { - op.mOldText = dest.subSequence(dstart, dend); + // Could not merge with the last edit, so commit the last edit and add this edit. + if (DEBUG_UNDO) Log.d(TAG, "filter: merge failed, adding " + edit); + um.commitState(mEditor.mUndoOwner); + um.addOperation(edit, UndoManager.MERGE_MODE_NONE); } - if (DEBUG_UNDO) Log.d(TAG, "*** adding new op, range=(" + op.mRangeStart - + "-" + op.mRangeEnd + "), oldText=" + op.mOldText); - um.addOperation(op, UndoManager.MERGE_MODE_NONE); um.endUpdate(); - return null; + } + + private boolean canUndoEdit(CharSequence source, int start, int end, + Spanned dest, int dstart, int dend) { + if (!mEditor.mAllowUndo) { + if (DEBUG_UNDO) Log.d(TAG, "filter: undo is disabled"); + return false; + } + + if (mEditor.mUndoManager.isInUndo()) { + if (DEBUG_UNDO) Log.d(TAG, "filter: skipping, currently performing undo/redo"); + return false; + } + + // Text filters run before input operations are applied. However, some input operations + // are invalid and will throw exceptions when applied. This is common in tests. Don't + // attempt to undo invalid operations. + if (!isValidRange(source, start, end) || !isValidRange(dest, dstart, dend)) { + if (DEBUG_UNDO) Log.d(TAG, "filter: invalid op"); + return false; + } + + // Earlier filters can rewrite input to be a no-op, for example due to a length limit + // on an input field. Skip no-op changes. + if (start == end && dstart == dend) { + if (DEBUG_UNDO) Log.d(TAG, "filter: skipping no-op"); + return false; + } + + return true; + } + + private boolean isComposition(CharSequence source) { + if (!(source instanceof Spannable)) { + return false; + } + // This is a composition edit if the source has a non-zero-length composing span. + Spannable text = (Spannable) source; + int composeBegin = EditableInputConnection.getComposingSpanStart(text); + int composeEnd = EditableInputConnection.getComposingSpanEnd(text); + return composeBegin < composeEnd; + } + + private boolean isInTextWatcher() { + CharSequence text = mEditor.mTextView.getText(); + return (text instanceof SpannableStringBuilder) + && ((SpannableStringBuilder) text).getTextWatcherDepth() > 0; } } - public static class TextModifyOperation extends UndoOperation<TextView> { - int mRangeStart, mRangeEnd; - CharSequence mOldText; + /** + * An operation to undo a single "edit" to a text view. + */ + public static class EditOperation extends UndoOperation<Editor> { + private static final int TYPE_INSERT = 0; + private static final int TYPE_DELETE = 1; + private static final int TYPE_REPLACE = 2; + + private int mType; + private boolean mForceMerge; + private String mOldText; + private int mOldTextStart; + private String mNewText; + private int mNewTextStart; + + private int mOldCursorPos; + private int mNewCursorPos; + + /** + * Constructs an edit operation from a text input operation on editor that replaces the + * oldText starting at dstart with newText. If forceMerge is true then always forcibly + * merge this operation with any previous one. + */ + public EditOperation(Editor editor, boolean forceMerge, String oldText, int dstart, + String newText) { + super(editor.mUndoOwner); + mForceMerge = forceMerge; + mOldText = oldText; + mNewText = newText; + + // Determine the type of the edit and store where it occurred. Avoid storing + // irrevelant data (e.g. mNewTextStart for a delete) because that makes the + // merging logic more complex (e.g. merging deletes could lead to mNewTextStart being + // outside the bounds of the final text). + if (mNewText.length() > 0 && mOldText.length() == 0) { + mType = TYPE_INSERT; + mNewTextStart = dstart; + } else if (mNewText.length() == 0 && mOldText.length() > 0) { + mType = TYPE_DELETE; + mOldTextStart = dstart; + } else { + mType = TYPE_REPLACE; + mOldTextStart = mNewTextStart = dstart; + } - public TextModifyOperation(UndoOwner owner) { - super(owner); + // Store cursor data. + mOldCursorPos = editor.mTextView.getSelectionStart(); + mNewCursorPos = dstart + mNewText.length(); } - public TextModifyOperation(Parcel src, ClassLoader loader) { + public EditOperation(Parcel src, ClassLoader loader) { super(src, loader); - mRangeStart = src.readInt(); - mRangeEnd = src.readInt(); - mOldText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(src); + mType = src.readInt(); + mForceMerge = src.readInt() != 0; + mOldText = src.readString(); + mOldTextStart = src.readInt(); + mNewText = src.readString(); + mNewTextStart = src.readInt(); + mOldCursorPos = src.readInt(); + mNewCursorPos = src.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mType); + dest.writeInt(mForceMerge ? 1 : 0); + dest.writeString(mOldText); + dest.writeInt(mOldTextStart); + dest.writeString(mNewText); + dest.writeInt(mNewTextStart); + dest.writeInt(mOldCursorPos); + dest.writeInt(mNewCursorPos); + } + + private int getNewTextEnd() { + return mNewTextStart + mNewText.length(); + } + + private int getOldTextEnd() { + return mOldTextStart + mOldText.length(); } @Override @@ -4262,59 +4803,185 @@ public class Editor { @Override public void undo() { - swapText(); + if (DEBUG_UNDO) Log.d(TAG, "undo"); + // Remove the new text and insert the old. + Editor editor = getOwnerData(); + Editable text = (Editable) editor.mTextView.getText(); + modifyText(text, mNewTextStart, getNewTextEnd(), mOldText, mOldTextStart, + mOldCursorPos); } @Override public void redo() { - swapText(); + if (DEBUG_UNDO) Log.d(TAG, "redo"); + // Remove the old text and insert the new. + Editor editor = getOwnerData(); + Editable text = (Editable) editor.mTextView.getText(); + modifyText(text, mOldTextStart, getOldTextEnd(), mNewText, mNewTextStart, + mNewCursorPos); } - private void swapText() { - // Both undo and redo involves swapping the contents of the range - // in the text view with our local text. - TextView tv = getOwnerData(); - Editable editable = (Editable)tv.getText(); - CharSequence curText; - if (mRangeStart >= mRangeEnd) { - curText = null; - } else { - curText = editable.subSequence(mRangeStart, mRangeEnd); - } + /** + * Attempts to merge this existing operation with a new edit. + * @param edit The new edit operation. + * @return If the merge succeeded, returns true. Otherwise returns false and leaves this + * object unchanged. + */ + private boolean mergeWith(EditOperation edit) { if (DEBUG_UNDO) { - Log.d(TAG, "Swap: range=(" + mRangeStart + "-" + mRangeEnd - + "), oldText=" + mOldText); - Log.d(TAG, "Swap: curText=" + curText); + Log.d(TAG, "mergeWith old " + this); + Log.d(TAG, "mergeWith new " + edit); } - if (mOldText == null) { - editable.delete(mRangeStart, mRangeEnd); - mRangeEnd = mRangeStart; - } else { - editable.replace(mRangeStart, mRangeEnd, mOldText); - mRangeEnd = mRangeStart + mOldText.length(); + if (edit.mForceMerge) { + forceMergeWith(edit); + return true; + } + switch (mType) { + case TYPE_INSERT: + return mergeInsertWith(edit); + case TYPE_DELETE: + return mergeDeleteWith(edit); + case TYPE_REPLACE: + return mergeReplaceWith(edit); + default: + return false; } - mOldText = curText; } - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(mRangeStart); - dest.writeInt(mRangeEnd); - TextUtils.writeToParcel(mOldText, dest, flags); + private boolean mergeInsertWith(EditOperation edit) { + // Only merge continuous insertions. + if (edit.mType != TYPE_INSERT) { + return false; + } + // Only merge insertions that are contiguous. + if (getNewTextEnd() != edit.mNewTextStart) { + return false; + } + mNewText += edit.mNewText; + mNewCursorPos = edit.mNewCursorPos; + return true; } - public static final Parcelable.ClassLoaderCreator<TextModifyOperation> CREATOR - = new Parcelable.ClassLoaderCreator<TextModifyOperation>() { - public TextModifyOperation createFromParcel(Parcel in) { - return new TextModifyOperation(in, null); + // TODO: Support forward delete. + private boolean mergeDeleteWith(EditOperation edit) { + // Only merge continuous deletes. + if (edit.mType != TYPE_DELETE) { + return false; + } + // Only merge deletions that are contiguous. + if (mOldTextStart != edit.getOldTextEnd()) { + return false; } + mOldTextStart = edit.mOldTextStart; + mOldText = edit.mOldText + mOldText; + mNewCursorPos = edit.mNewCursorPos; + return true; + } - public TextModifyOperation createFromParcel(Parcel in, ClassLoader loader) { - return new TextModifyOperation(in, loader); + private boolean mergeReplaceWith(EditOperation edit) { + // Replacements can merge only with adjacent inserts. + if (edit.mType != TYPE_INSERT || getNewTextEnd() != edit.mNewTextStart) { + return false; } + mOldText += edit.mOldText; + mNewText += edit.mNewText; + mNewCursorPos = edit.mNewCursorPos; + return true; + } - public TextModifyOperation[] newArray(int size) { - return new TextModifyOperation[size]; + /** + * Forcibly creates a single merged edit operation by simulating the entire text + * contents being replaced. + */ + private void forceMergeWith(EditOperation edit) { + if (DEBUG_UNDO) Log.d(TAG, "forceMerge"); + Editor editor = getOwnerData(); + + // Copy the text of the current field. + // NOTE: Using StringBuilder instead of SpannableStringBuilder would be somewhat faster, + // but would require two parallel implementations of modifyText() because Editable and + // StringBuilder do not share an interface for replace/delete/insert. + Editable editable = (Editable) editor.mTextView.getText(); + Editable originalText = new SpannableStringBuilder(editable.toString()); + + // Roll back the last operation. + modifyText(originalText, mNewTextStart, getNewTextEnd(), mOldText, mOldTextStart, + mOldCursorPos); + + // Clone the text again and apply the new operation. + Editable finalText = new SpannableStringBuilder(editable.toString()); + modifyText(finalText, edit.mOldTextStart, edit.getOldTextEnd(), edit.mNewText, + edit.mNewTextStart, edit.mNewCursorPos); + + // Convert this operation into a non-mergeable replacement of the entire string. + mType = TYPE_REPLACE; + mNewText = finalText.toString(); + mNewTextStart = 0; + mOldText = originalText.toString(); + mOldTextStart = 0; + mNewCursorPos = edit.mNewCursorPos; + // mOldCursorPos is unchanged. + } + + private static void modifyText(Editable text, int deleteFrom, int deleteTo, + CharSequence newText, int newTextInsertAt, int newCursorPos) { + // Apply the edit if it is still valid. + if (isValidRange(text, deleteFrom, deleteTo) && + newTextInsertAt <= text.length() - (deleteTo - deleteFrom)) { + if (deleteFrom != deleteTo) { + text.delete(deleteFrom, deleteTo); + } + if (newText.length() != 0) { + text.insert(newTextInsertAt, newText); + } + } + // Restore the cursor position. If there wasn't an old cursor (newCursorPos == -1) then + // don't explicitly set it and rely on SpannableStringBuilder to position it. + // TODO: Select all the text that was undone. + if (0 <= newCursorPos && newCursorPos <= text.length()) { + Selection.setSelection(text, newCursorPos); + } + } + + private String getTypeString() { + switch (mType) { + case TYPE_INSERT: + return "insert"; + case TYPE_DELETE: + return "delete"; + case TYPE_REPLACE: + return "replace"; + default: + return ""; + } + } + + @Override + public String toString() { + return "[mType=" + getTypeString() + ", " + + "mOldText=" + mOldText + ", " + + "mOldTextStart=" + mOldTextStart + ", " + + "mNewText=" + mNewText + ", " + + "mNewTextStart=" + mNewTextStart + ", " + + "mOldCursorPos=" + mOldCursorPos + ", " + + "mNewCursorPos=" + mNewCursorPos + "]"; + } + + public static final Parcelable.ClassLoaderCreator<EditOperation> CREATOR + = new Parcelable.ClassLoaderCreator<EditOperation>() { + @Override + public EditOperation createFromParcel(Parcel in) { + return new EditOperation(in, null); + } + + @Override + public EditOperation createFromParcel(Parcel in, ClassLoader loader) { + return new EditOperation(in, loader); + } + + @Override + public EditOperation[] newArray(int size) { + return new EditOperation[size]; } }; } diff --git a/core/java/android/widget/ExpandableListView.java b/core/java/android/widget/ExpandableListView.java index 70089e0..fac36c5 100644 --- a/core/java/android/widget/ExpandableListView.java +++ b/core/java/android/widget/ExpandableListView.java @@ -30,8 +30,6 @@ import android.view.ContextMenu; import android.view.SoundEffectConstants; import android.view.View; import android.view.ContextMenu.ContextMenuInfo; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; import android.widget.ExpandableListConnector.PositionMetadata; import java.util.ArrayList; @@ -1342,14 +1340,7 @@ public class ExpandableListView extends ListView { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(ExpandableListView.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(ExpandableListView.class.getName()); + public CharSequence getAccessibilityClassName() { + return ExpandableListView.class.getName(); } } diff --git a/core/java/android/widget/FastScroller.java b/core/java/android/widget/FastScroller.java index fe143de..21213ac 100644 --- a/core/java/android/widget/FastScroller.java +++ b/core/java/android/widget/FastScroller.java @@ -22,6 +22,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; +import android.annotation.StyleRes; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; @@ -326,7 +327,7 @@ class FastScroller { refreshDrawablePressedState(); } - public void setStyle(int resId) { + public void setStyle(@StyleRes int resId) { final Context context = mList.getContext(); final TypedArray ta = context.obtainStyledAttributes(null, com.android.internal.R.styleable.FastScroll, android.R.attr.fastScrollStyle, resId); @@ -752,13 +753,13 @@ class FastScroller { final View track = mTrackImage; final View thumb = mThumbImage; final Rect container = mContainerRect; - final int containerWidth = container.width(); - final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(containerWidth, MeasureSpec.AT_MOST); + final int maxWidth = container.width(); + final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST); final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); track.measure(widthMeasureSpec, heightMeasureSpec); final int trackWidth = track.getMeasuredWidth(); - final int thumbHalfHeight = thumb == null ? 0 : thumb.getHeight() / 2; + final int thumbHalfHeight = thumb.getHeight() / 2; final int left = thumb.getLeft() + (thumb.getWidth() - trackWidth) / 2; final int right = left + trackWidth; final int top = container.top + thumbHalfHeight; diff --git a/core/java/android/widget/FrameLayout.java b/core/java/android/widget/FrameLayout.java index d974c29..f6d198b 100644 --- a/core/java/android/widget/FrameLayout.java +++ b/core/java/android/widget/FrameLayout.java @@ -18,6 +18,7 @@ package android.widget; import java.util.ArrayList; +import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.content.res.ColorStateList; @@ -29,12 +30,9 @@ import android.graphics.Region; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.Gravity; -import android.view.RemotableViewMethod; import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; import com.android.internal.R; @@ -103,15 +101,16 @@ public class FrameLayout extends ViewGroup { super(context); } - public FrameLayout(Context context, AttributeSet attrs) { + public FrameLayout(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } - public FrameLayout(Context context, AttributeSet attrs, int defStyleAttr) { + public FrameLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } - public FrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + public FrameLayout( + Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); final TypedArray a = context.obtainStyledAttributes( @@ -203,11 +202,15 @@ public class FrameLayout extends ViewGroup { } @Override - @RemotableViewMethod - public void setVisibility(@Visibility int visibility) { - super.setVisibility(visibility); - if (mForeground != null) { - mForeground.setVisible(visibility == VISIBLE, false); + protected void onVisibilityChanged(@NonNull View changedView, @Visibility int visibility) { + super.onVisibilityChanged(changedView, visibility); + + final Drawable dr = mForeground; + if (dr != null) { + final boolean visible = visibility == VISIBLE && getVisibility() == VISIBLE; + if (visible != dr.isVisible()) { + dr.setVisible(visible, false); + } } } @@ -703,17 +706,9 @@ public class FrameLayout extends ViewGroup { return new LayoutParams(p); } - - @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(FrameLayout.class.getName()); - } - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(FrameLayout.class.getName()); + public CharSequence getAccessibilityClassName() { + return FrameLayout.class.getName(); } /** diff --git a/core/java/android/widget/Gallery.java b/core/java/android/widget/Gallery.java index f7c839f..af5a8bf 100644 --- a/core/java/android/widget/Gallery.java +++ b/core/java/android/widget/Gallery.java @@ -33,7 +33,6 @@ import android.view.SoundEffectConstants; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; -import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.Transformation; @@ -1374,15 +1373,14 @@ public class Gallery extends AbsSpinner implements GestureDetector.OnGestureList } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(Gallery.class.getName()); + public CharSequence getAccessibilityClassName() { + return Gallery.class.getName(); } + /** @hide */ @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(Gallery.class.getName()); + public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfoInternal(info); info.setScrollable(mItemCount > 1); if (isEnabled()) { if (mItemCount > 0 && mSelectedPosition < mItemCount - 1) { @@ -1394,9 +1392,10 @@ public class Gallery extends AbsSpinner implements GestureDetector.OnGestureList } } + /** @hide */ @Override - public boolean performAccessibilityAction(int action, Bundle arguments) { - if (super.performAccessibilityAction(action, arguments)) { + public boolean performAccessibilityActionInternal(int action, Bundle arguments) { + if (super.performAccessibilityActionInternal(action, arguments)) { return true; } switch (action) { diff --git a/core/java/android/widget/GridLayout.java b/core/java/android/widget/GridLayout.java index 161ae7e..6cc4bda 100644 --- a/core/java/android/widget/GridLayout.java +++ b/core/java/android/widget/GridLayout.java @@ -31,8 +31,6 @@ import android.util.Printer; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; import com.android.internal.R; @@ -143,7 +141,9 @@ import static java.lang.Math.min; * view was alone in a column, that column would itself collapse to zero width if and only if * no gravity was defined on the view. If gravity was defined, then the gone-marked * view has no effect on the layout and the container should be laid out as if the view - * had never been added to it. + * had never been added to it. GONE views are taken to have zero weight during excess space + * distribution. + * <p> * These statements apply equally to rows as well as columns, and to groups of rows or columns. * * <p> @@ -1012,12 +1012,10 @@ public class GridLayout extends ViewGroup { LayoutParams lp = getLayoutParams(c); if (firstPass) { measureChildWithMargins2(c, widthSpec, heightSpec, lp.width, lp.height); - mHorizontalAxis.recordOriginalMeasurement(i); - mVerticalAxis.recordOriginalMeasurement(i); } else { boolean horizontal = (mOrientation == HORIZONTAL); Spec spec = horizontal ? lp.columnSpec : lp.rowSpec; - if (spec.alignment == FILL) { + if (spec.getAbsoluteAlignment(horizontal) == FILL) { Interval span = spec.span; Axis axis = horizontal ? mHorizontalAxis : mVerticalAxis; int[] locations = axis.getLocations(); @@ -1093,11 +1091,6 @@ public class GridLayout extends ViewGroup { invalidateValues(); } - final Alignment getAlignment(Alignment alignment, boolean horizontal) { - return (alignment != UNDEFINED_ALIGNMENT) ? alignment : - (horizontal ? START : BASELINE); - } - // Layout container /** @@ -1152,8 +1145,8 @@ public class GridLayout extends ViewGroup { int pWidth = getMeasurement(c, true); int pHeight = getMeasurement(c, false); - Alignment hAlign = getAlignment(columnSpec.alignment, true); - Alignment vAlign = getAlignment(rowSpec.alignment, false); + Alignment hAlign = columnSpec.getAbsoluteAlignment(true); + Alignment vAlign = rowSpec.getAbsoluteAlignment(false); Bounds boundsX = mHorizontalAxis.getGroupBounds().getValue(i); Bounds boundsY = mVerticalAxis.getGroupBounds().getValue(i); @@ -1191,15 +1184,8 @@ public class GridLayout extends ViewGroup { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(GridLayout.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(GridLayout.class.getName()); + public CharSequence getAccessibilityClassName() { + return GridLayout.class.getName(); } // Inner classes @@ -1243,7 +1229,6 @@ public class GridLayout extends ViewGroup { public boolean hasWeights; public boolean hasWeightsValid = false; - public int[] originalMeasurements; public int[] deltas; boolean orderPreserved = DEFAULT_ORDER_PRESERVED; @@ -1306,7 +1291,7 @@ public class GridLayout extends ViewGroup { // we must include views that are GONE here, see introductory javadoc LayoutParams lp = getLayoutParams(c); Spec spec = horizontal ? lp.columnSpec : lp.rowSpec; - Bounds bounds = getAlignment(spec.alignment, horizontal).getBounds(); + Bounds bounds = spec.getAbsoluteAlignment(horizontal).getBounds(); assoc.put(spec, bounds); } return assoc.pack(); @@ -1322,9 +1307,8 @@ public class GridLayout extends ViewGroup { // we must include views that are GONE here, see introductory javadoc LayoutParams lp = getLayoutParams(c); Spec spec = horizontal ? lp.columnSpec : lp.rowSpec; - int size = (spec.weight == 0) ? - getMeasurementIncludingMargin(c, horizontal) : - getOriginalMeasurements()[i] + getDeltas()[i]; + int size = getMeasurementIncludingMargin(c, horizontal) + + ((spec.weight == 0) ? 0 : getDeltas()[i]); groupBounds.getValue(i).include(GridLayout.this, c, spec, this, size); } } @@ -1712,7 +1696,11 @@ public class GridLayout extends ViewGroup { private boolean computeHasWeights() { for (int i = 0, N = getChildCount(); i < N; i++) { - LayoutParams lp = getLayoutParams(getChildAt(i)); + final View child = getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + LayoutParams lp = getLayoutParams(child); Spec spec = horizontal ? lp.columnSpec : lp.rowSpec; if (spec.weight != 0) { return true; @@ -1729,19 +1717,6 @@ public class GridLayout extends ViewGroup { return hasWeights; } - public int[] getOriginalMeasurements() { - if (originalMeasurements == null) { - originalMeasurements = new int[getChildCount()]; - } - return originalMeasurements; - } - - private void recordOriginalMeasurement(int i) { - if (hasWeights()) { - getOriginalMeasurements()[i] = getMeasurementIncludingMargin(getChildAt(i), horizontal); - } - } - public int[] getDeltas() { if (deltas == null) { deltas = new int[getChildCount()]; @@ -1752,7 +1727,10 @@ public class GridLayout extends ViewGroup { private void shareOutDelta(int totalDelta, float totalWeight) { Arrays.fill(deltas, 0); for (int i = 0, N = getChildCount(); i < N; i++) { - View c = getChildAt(i); + final View c = getChildAt(i); + if (c.getVisibility() == View.GONE) { + continue; + } LayoutParams lp = getLayoutParams(c); Spec spec = horizontal ? lp.columnSpec : lp.rowSpec; float weight = spec.weight; @@ -1805,6 +1783,9 @@ public class GridLayout extends ViewGroup { float totalWeight = 0f; for (int i = 0, N = getChildCount(); i < N; i++) { View c = getChildAt(i); + if (c.getVisibility() == View.GONE) { + continue; + } LayoutParams lp = getLayoutParams(c); Spec spec = horizontal ? lp.columnSpec : lp.rowSpec; totalWeight += spec.weight; @@ -1900,7 +1881,6 @@ public class GridLayout extends ViewGroup { locations = null; - originalMeasurements = null; deltas = null; hasWeightsValid = false; @@ -2020,7 +2000,6 @@ public class GridLayout extends ViewGroup { R.styleable.ViewGroup_MarginLayout_layout_marginRight; private static final int BOTTOM_MARGIN = R.styleable.ViewGroup_MarginLayout_layout_marginBottom; - private static final int COLUMN = R.styleable.GridLayout_Layout_layout_column; private static final int COLUMN_SPAN = R.styleable.GridLayout_Layout_layout_columnSpan; private static final int COLUMN_WEIGHT = R.styleable.GridLayout_Layout_layout_columnWeight; @@ -2414,7 +2393,7 @@ public class GridLayout extends ViewGroup { protected final void include(GridLayout gl, View c, Spec spec, Axis axis, int size) { this.flexibility &= spec.getFlexibility(); boolean horizontal = axis.horizontal; - Alignment alignment = gl.getAlignment(spec.alignment, horizontal); + Alignment alignment = spec.getAbsoluteAlignment(axis.horizontal); // todo test this works correctly when the returned value is UNDEFINED int before = alignment.getAlignmentValue(c, size, gl.getLayoutMode()); include(before, size - before); @@ -2565,6 +2544,16 @@ public class GridLayout extends ViewGroup { this(startDefined, new Interval(start, start + size), alignment, weight); } + private Alignment getAbsoluteAlignment(boolean horizontal) { + if (alignment != UNDEFINED_ALIGNMENT) { + return alignment; + } + if (weight == 0f) { + return horizontal ? START : BASELINE; + } + return FILL; + } + final Spec copyWriteSpan(Interval span) { return new Spec(startDefined, span, alignment, weight); } diff --git a/core/java/android/widget/GridView.java b/core/java/android/widget/GridView.java index efd6fc0..c6e3dc8 100644 --- a/core/java/android/widget/GridView.java +++ b/core/java/android/widget/GridView.java @@ -31,7 +31,6 @@ import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; import android.view.ViewRootImpl; -import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeProvider; import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo; @@ -2342,15 +2341,14 @@ public class GridView extends AbsListView { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(GridView.class.getName()); + public CharSequence getAccessibilityClassName() { + return GridView.class.getName(); } + /** @hide */ @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(GridView.class.getName()); + public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfoInternal(info); final int columnsCount = getNumColumns(); final int rowsCount = getCount() / columnsCount; diff --git a/core/java/android/widget/HorizontalScrollView.java b/core/java/android/widget/HorizontalScrollView.java index 1b93b97..324c2aa 100644 --- a/core/java/android/widget/HorizontalScrollView.java +++ b/core/java/android/widget/HorizontalScrollView.java @@ -20,7 +20,6 @@ import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; -import android.graphics.RectF; import android.os.Build; import android.os.Bundle; import android.os.Parcel; @@ -762,9 +761,10 @@ public class HorizontalScrollView extends FrameLayout { awakenScrollBars(); } + /** @hide */ @Override - public boolean performAccessibilityAction(int action, Bundle arguments) { - if (super.performAccessibilityAction(action, arguments)) { + public boolean performAccessibilityActionInternal(int action, Bundle arguments) { + if (super.performAccessibilityActionInternal(action, arguments)) { return true; } switch (action) { @@ -795,9 +795,14 @@ public class HorizontalScrollView extends FrameLayout { } @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(HorizontalScrollView.class.getName()); + public CharSequence getAccessibilityClassName() { + return HorizontalScrollView.class.getName(); + } + + /** @hide */ + @Override + public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfoInternal(info); final int scrollRange = getScrollRange(); if (scrollRange > 0) { info.setScrollable(true); @@ -810,10 +815,10 @@ public class HorizontalScrollView extends FrameLayout { } } + /** @hide */ @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(HorizontalScrollView.class.getName()); + public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { + super.onInitializeAccessibilityEventInternal(event); event.setScrollable(getScrollRange() > 0); event.setScrollX(mScrollX); event.setScrollY(mScrollY); diff --git a/core/java/android/widget/ImageButton.java b/core/java/android/widget/ImageButton.java index 3a20628..332b158 100644 --- a/core/java/android/widget/ImageButton.java +++ b/core/java/android/widget/ImageButton.java @@ -18,8 +18,6 @@ package android.widget; import android.content.Context; import android.util.AttributeSet; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; /** @@ -93,14 +91,7 @@ public class ImageButton extends ImageView { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(ImageButton.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(ImageButton.class.getName()); + public CharSequence getAccessibilityClassName() { + return ImageButton.class.getName(); } } diff --git a/core/java/android/widget/ImageSwitcher.java b/core/java/android/widget/ImageSwitcher.java index c048970..08f21a2 100644 --- a/core/java/android/widget/ImageSwitcher.java +++ b/core/java/android/widget/ImageSwitcher.java @@ -16,13 +16,11 @@ package android.widget; +import android.annotation.DrawableRes; import android.content.Context; import android.graphics.drawable.Drawable; import android.net.Uri; import android.util.AttributeSet; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; - public class ImageSwitcher extends ViewSwitcher { @@ -35,7 +33,7 @@ public class ImageSwitcher extends ViewSwitcher super(context, attrs); } - public void setImageResource(int resid) + public void setImageResource(@DrawableRes int resid) { ImageView image = (ImageView)this.getNextView(); image.setImageResource(resid); @@ -57,14 +55,7 @@ public class ImageSwitcher extends ViewSwitcher } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(ImageSwitcher.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(ImageSwitcher.class.getName()); + public CharSequence getAccessibilityClassName() { + return ImageSwitcher.class.getName(); } } diff --git a/core/java/android/widget/ImageView.java b/core/java/android/widget/ImageView.java index c68bfca..041796b 100644 --- a/core/java/android/widget/ImageView.java +++ b/core/java/android/widget/ImageView.java @@ -16,6 +16,7 @@ package android.widget; +import android.annotation.DrawableRes; import android.annotation.Nullable; import android.content.ContentResolver; import android.content.Context; @@ -43,7 +44,6 @@ import android.view.RemotableViewMethod; import android.view.View; import android.view.ViewDebug; import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; import com.android.internal.R; @@ -127,15 +127,16 @@ public class ImageView extends View { initImageView(); } - public ImageView(Context context, AttributeSet attrs) { + public ImageView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } - public ImageView(Context context, AttributeSet attrs, int defStyleAttr) { + public ImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } - public ImageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + public ImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, + int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); initImageView(); @@ -239,9 +240,10 @@ public class ImageView extends View { return (getBackground() != null && getBackground().getCurrent() != null); } + /** @hide */ @Override - public void onPopulateAccessibilityEvent(AccessibilityEvent event) { - super.onPopulateAccessibilityEvent(event); + public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) { + super.onPopulateAccessibilityEventInternal(event); CharSequence contentDescription = getContentDescription(); if (!TextUtils.isEmpty(contentDescription)) { event.getText().add(contentDescription); @@ -385,7 +387,7 @@ public class ImageView extends View { * @attr ref android.R.styleable#ImageView_src */ @android.view.RemotableViewMethod - public void setImageResource(int resId) { + public void setImageResource(@DrawableRes int resId) { // 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; @@ -1419,14 +1421,7 @@ public class ImageView extends View { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(ImageView.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(ImageView.class.getName()); + public CharSequence getAccessibilityClassName() { + return ImageView.class.getName(); } } diff --git a/core/java/android/widget/LinearLayout.java b/core/java/android/widget/LinearLayout.java index 6476cdc..da15302 100644 --- a/core/java/android/widget/LinearLayout.java +++ b/core/java/android/widget/LinearLayout.java @@ -19,6 +19,7 @@ package android.widget; import com.android.internal.R; import android.annotation.IntDef; +import android.annotation.Nullable; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; @@ -28,8 +29,6 @@ import android.view.Gravity; import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; import java.lang.annotation.Retention; @@ -188,11 +187,11 @@ public class LinearLayout extends ViewGroup { this(context, null); } - public LinearLayout(Context context, AttributeSet attrs) { + public LinearLayout(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } - public LinearLayout(Context context, AttributeSet attrs, int defStyleAttr) { + public LinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } @@ -1807,15 +1806,8 @@ public class LinearLayout extends ViewGroup { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(LinearLayout.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(LinearLayout.class.getName()); + public CharSequence getAccessibilityClassName() { + return LinearLayout.class.getName(); } /** diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java index ba6f061..ee2c055 100644 --- a/core/java/android/widget/ListView.java +++ b/core/java/android/widget/ListView.java @@ -21,6 +21,7 @@ import com.android.internal.R; import com.android.internal.util.Predicate; import com.google.android.collect.Lists; +import android.annotation.IdRes; import android.content.Context; import android.content.Intent; import android.content.res.TypedArray; @@ -40,7 +41,6 @@ import android.view.ViewDebug; import android.view.ViewGroup; import android.view.ViewParent; import android.view.ViewRootImpl; -import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo; import android.view.accessibility.AccessibilityNodeInfo.CollectionItemInfo; @@ -3630,7 +3630,7 @@ public class ListView extends AbsListView { * First look in our children, then in any header and footer views that may be scrolled off. */ @Override - protected View findViewTraversal(int id) { + protected View findViewTraversal(@IdRes int id) { View v; v = super.findViewTraversal(id); if (v == null) { @@ -3879,15 +3879,14 @@ public class ListView extends AbsListView { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(ListView.class.getName()); + public CharSequence getAccessibilityClassName() { + return ListView.class.getName(); } + /** @hide */ @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(ListView.class.getName()); + public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfoInternal(info); final int rowsCount = getCount(); final int selectionMode = getSelectionModeForAccessibility(); diff --git a/core/java/android/widget/MediaController.java b/core/java/android/widget/MediaController.java index f1aaa4d..2375089 100644 --- a/core/java/android/widget/MediaController.java +++ b/core/java/android/widget/MediaController.java @@ -28,16 +28,13 @@ import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; +import android.view.PhoneWindow; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; import android.widget.SeekBar.OnSeekBarChangeListener; -import com.android.internal.policy.PolicyManager; - import java.util.Formatter; import java.util.Locale; @@ -128,7 +125,7 @@ public class MediaController extends FrameLayout { private void initFloatingWindow() { mWindowManager = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE); - mWindow = PolicyManager.makeNewWindow(mContext); + mWindow = new PhoneWindow(mContext); mWindow.setWindowManager(mWindowManager, null, null); mWindow.requestFeature(Window.FEATURE_NO_TITLE); mDecor = mWindow.getDecorView(); @@ -626,15 +623,8 @@ public class MediaController extends FrameLayout { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(MediaController.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(MediaController.class.getName()); + public CharSequence getAccessibilityClassName() { + return MediaController.class.getName(); } private View.OnClickListener mRewListener = new View.OnClickListener() { diff --git a/core/java/android/widget/MultiAutoCompleteTextView.java b/core/java/android/widget/MultiAutoCompleteTextView.java index cbd01b0..2152e43 100644 --- a/core/java/android/widget/MultiAutoCompleteTextView.java +++ b/core/java/android/widget/MultiAutoCompleteTextView.java @@ -23,8 +23,6 @@ import android.text.Spanned; import android.text.TextUtils; import android.text.method.QwertyKeyListener; import android.util.AttributeSet; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; /** * An editable text view, extending {@link AutoCompleteTextView}, that @@ -203,15 +201,8 @@ public class MultiAutoCompleteTextView extends AutoCompleteTextView { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(MultiAutoCompleteTextView.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(MultiAutoCompleteTextView.class.getName()); + public CharSequence getAccessibilityClassName() { + return MultiAutoCompleteTextView.class.getName(); } public static interface Tokenizer { diff --git a/core/java/android/widget/NumberPicker.java b/core/java/android/widget/NumberPicker.java index ee17b78..16dc26d 100644 --- a/core/java/android/widget/NumberPicker.java +++ b/core/java/android/widget/NumberPicker.java @@ -1558,9 +1558,10 @@ public class NumberPicker extends LinearLayout { } } + /** @hide */ @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); + public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { + super.onInitializeAccessibilityEventInternal(event); event.setClassName(NumberPicker.class.getName()); event.setScrollable(true); event.setScrollY((mMinValue + mValue) * mSelectorElementHeight); diff --git a/core/java/android/widget/PopupMenu.java b/core/java/android/widget/PopupMenu.java index 2708398..1507dfb 100644 --- a/core/java/android/widget/PopupMenu.java +++ b/core/java/android/widget/PopupMenu.java @@ -22,6 +22,7 @@ import com.android.internal.view.menu.MenuPopupHelper; import com.android.internal.view.menu.MenuPresenter; import com.android.internal.view.menu.SubMenuBuilder; +import android.annotation.MenuRes; import android.content.Context; import android.view.Gravity; import android.view.Menu; @@ -115,6 +116,29 @@ public class PopupMenu implements MenuBuilder.Callback, MenuPresenter.Callback { } /** + * Sets the gravity used to align the popup window to its anchor view. + * <p> + * If the popup is showing, calling this method will take effect only + * the next time the popup is shown. + * + * @param gravity the gravity used to align the popup window + * + * @see #getGravity() + */ + public void setGravity(int gravity) { + mPopup.setGravity(gravity); + } + + /** + * @return the gravity used to align the popup window to its anchor view + * + * @see #setGravity(int) + */ + public int getGravity() { + return mPopup.getGravity(); + } + + /** * Returns an {@link OnTouchListener} that can be added to the anchor view * to implement drag-to-open behavior. * <p> @@ -182,7 +206,7 @@ public class PopupMenu implements MenuBuilder.Callback, MenuPresenter.Callback { * popupMenu.getMenuInflater().inflate(menuRes, popupMenu.getMenu()). * @param menuRes Menu resource to inflate */ - public void inflate(int menuRes) { + public void inflate(@MenuRes int menuRes) { getMenuInflater().inflate(menuRes, mMenu); } diff --git a/core/java/android/widget/PopupWindow.java b/core/java/android/widget/PopupWindow.java index 5419ab6..f676254 100644 --- a/core/java/android/widget/PopupWindow.java +++ b/core/java/android/widget/PopupWindow.java @@ -18,17 +18,22 @@ package android.widget; import com.android.internal.R; -import android.animation.ValueAnimator; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; -import android.graphics.Insets; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.StateListDrawable; import android.os.Build; import android.os.IBinder; +import android.transition.Transition; +import android.transition.Transition.EpicenterCallback; +import android.transition.Transition.TransitionListener; +import android.transition.Transition.TransitionListenerAdapter; +import android.transition.TransitionInflater; +import android.transition.TransitionManager; +import android.transition.TransitionSet; import android.util.AttributeSet; import android.view.Gravity; import android.view.KeyEvent; @@ -36,7 +41,9 @@ import android.view.MotionEvent; import android.view.View; import android.view.View.OnTouchListener; import android.view.ViewGroup; +import android.view.ViewParent; import android.view.ViewTreeObserver; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.view.ViewTreeObserver.OnScrollChangedListener; import android.view.WindowManager; @@ -46,7 +53,7 @@ import java.lang.ref.WeakReference; * <p>A popup window that can be used to display an arbitrary view. The popup * window is a floating container that appears on top of the current * activity.</p> - * + * * @see android.widget.AutoCompleteTextView * @see android.widget.Spinner */ @@ -58,7 +65,7 @@ public class PopupWindow { * it doesn't. */ public static final int INPUT_METHOD_FROM_FOCUSABLE = 0; - + /** * Mode for {@link #setInputMethodMode(int)}: this popup always needs to * work with an input method, regardless of whether it is focusable. This @@ -66,7 +73,7 @@ public class PopupWindow { * the input method while it is shown. */ public static final int INPUT_METHOD_NEEDED = 1; - + /** * Mode for {@link #setInputMethodMode(int)}: this popup never needs to * work with an input method, regardless of whether it is focusable. This @@ -77,14 +84,30 @@ public class PopupWindow { private static final int DEFAULT_ANCHORED_GRAVITY = Gravity.TOP | Gravity.START; + /** + * Default animation style indicating that separate animations should be + * used for top/bottom anchoring states. + */ + private static final int ANIMATION_STYLE_DEFAULT = -1; + + private final int[] mDrawingLocation = new int[2]; + private final int[] mScreenLocation = new int[2]; + private final Rect mTempRect = new Rect(); + private final Rect mAnchorBounds = new Rect(); + private Context mContext; private WindowManager mWindowManager; - + private boolean mIsShowing; + private boolean mIsTransitioningToDismiss; private boolean mIsDropdown; + /** View that handles event dispatch and content transitions. */ + private PopupDecorView mDecorView; + + /** The contents of the popup. */ private View mContentView; - private View mPopupView; + private boolean mFocusable; private int mInputMethodMode = INPUT_METHOD_FROM_FOCUSABLE; private int mSoftInputMode = WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED; @@ -114,49 +137,67 @@ public class PopupWindow { private float mElevation; - private int[] mDrawingLocation = new int[2]; - private int[] mScreenLocation = new int[2]; - private Rect mTempRect = new Rect(); - private Drawable mBackground; private Drawable mAboveAnchorBackgroundDrawable; private Drawable mBelowAnchorBackgroundDrawable; - // Temporary animation centers. Should be moved into window params? - private int mAnchorRelativeX; - private int mAnchorRelativeY; + private Transition mEnterTransition; + private Transition mExitTransition; private boolean mAboveAnchor; private int mWindowLayoutType = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; - + private OnDismissListener mOnDismissListener; private boolean mIgnoreCheekPress = false; - private int mAnimationStyle = -1; - + private int mAnimationStyle = ANIMATION_STYLE_DEFAULT; + private static final int[] ABOVE_ANCHOR_STATE_SET = new int[] { com.android.internal.R.attr.state_above_anchor }; private WeakReference<View> mAnchor; - private final OnScrollChangedListener mOnScrollChangedListener = - new OnScrollChangedListener() { - @Override - public void onScrollChanged() { - final View anchor = mAnchor != null ? mAnchor.get() : null; - if (anchor != null && mPopupView != null) { - final WindowManager.LayoutParams p = (WindowManager.LayoutParams) - mPopupView.getLayoutParams(); - - updateAboveAnchor(findDropDownPosition(anchor, p, mAnchorXoff, mAnchorYoff, - mAnchoredGravity)); - update(p.x, p.y, -1, -1, true); - } + private final EpicenterCallback mEpicenterCallback = new EpicenterCallback() { + @Override + public Rect onGetEpicenter(Transition transition) { + final View anchor = mAnchor != null ? mAnchor.get() : null; + final View decor = mDecorView; + if (anchor == null || decor == null) { + return null; + } + + final Rect anchorBounds = mAnchorBounds; + final int[] anchorLocation = anchor.getLocationOnScreen(); + final int[] popupLocation = mDecorView.getLocationOnScreen(); + + // Compute the position of the anchor relative to the popup. + anchorBounds.set(0, 0, anchor.getWidth(), anchor.getHeight()); + anchorBounds.offset(anchorLocation[0] - popupLocation[0], + anchorLocation[1] - popupLocation[1]); + + return anchorBounds; + } + }; + + private final OnScrollChangedListener mOnScrollChangedListener = new OnScrollChangedListener() { + @Override + public void onScrollChanged() { + final View anchor = mAnchor != null ? mAnchor.get() : null; + if (anchor != null && mDecorView != null) { + final WindowManager.LayoutParams p = (WindowManager.LayoutParams) + mDecorView.getLayoutParams(); + + updateAboveAnchor(findDropDownPosition(anchor, p, mAnchorXoff, mAnchorYoff, + mAnchoredGravity)); + update(p.x, p.y, -1, -1, true); } - }; + } + }; - private int mAnchorXoff, mAnchorYoff, mAnchoredGravity; + private int mAnchorXoff; + private int mAnchorYoff; + private int mAnchoredGravity; private boolean mOverlapAnchor; private boolean mPopupViewInitialLayoutDirectionInherited; @@ -187,15 +228,15 @@ public class PopupWindow { public PopupWindow(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } - + /** * <p>Create a new, empty, non focusable popup window of dimension (0,0).</p> - * + * * <p>The popup does not provide a background.</p> */ public PopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { mContext = context; - mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); + mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); final TypedArray a = context.obtainStyledAttributes( attrs, R.styleable.PopupWindow, defStyleAttr, defStyleRes); @@ -203,11 +244,34 @@ public class PopupWindow { 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 == R.style.Animation_PopupWindow ? -1 : animStyle; + // Preserve default behavior from Gingerbread. If the animation is + // undefined or explicitly specifies the Gingerbread animation style, + // use a sentinel value. + if (a.hasValueOrEmpty(R.styleable.PopupWindow_popupAnimationStyle)) { + final int animStyle = a.getResourceId(R.styleable.PopupWindow_popupAnimationStyle, 0); + if (animStyle == R.style.Animation_PopupWindow) { + mAnimationStyle = ANIMATION_STYLE_DEFAULT; + } else { + mAnimationStyle = animStyle; + } + } else { + mAnimationStyle = ANIMATION_STYLE_DEFAULT; + } + + final Transition enterTransition = getTransition(a.getResourceId( + R.styleable.PopupWindow_popupEnterTransition, 0)); + final Transition exitTransition; + if (a.hasValueOrEmpty(R.styleable.PopupWindow_popupExitTransition)) { + exitTransition = getTransition(a.getResourceId( + R.styleable.PopupWindow_popupExitTransition, 0)); + } else { + exitTransition = enterTransition == null ? null : enterTransition.clone(); + } a.recycle(); + setEnterTransition(enterTransition); + setExitTransition(exitTransition); setBackgroundDrawable(bg); } @@ -288,6 +352,37 @@ public class PopupWindow { setFocusable(focusable); } + public void setEnterTransition(Transition enterTransition) { + mEnterTransition = enterTransition; + + if (mEnterTransition != null) { + mEnterTransition.setEpicenterCallback(mEpicenterCallback); + } + } + + public void setExitTransition(Transition exitTransition) { + mExitTransition = exitTransition; + + if (mExitTransition != null) { + mExitTransition.setEpicenterCallback(mEpicenterCallback); + } + } + + private Transition getTransition(int resId) { + if (resId != 0 && resId != R.transition.no_transition) { + final TransitionInflater inflater = TransitionInflater.from(mContext); + final Transition transition = inflater.inflateTransition(resId); + if (transition != null) { + final boolean isEmpty = transition instanceof TransitionSet + && ((TransitionSet) transition).getTransitionCount() == 0; + if (!isEmpty) { + return transition; + } + } + } + return null; + } + /** * Return the drawable used as the popup window's background. * @@ -381,7 +476,7 @@ public class PopupWindow { * Set the flag on popup to ignore cheek press events; by default this flag * is set to false * which means the popup will not 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> @@ -391,7 +486,7 @@ public class PopupWindow { public void setIgnoreCheekPress() { mIgnoreCheekPress = true; } - + /** * <p>Change the animation style resource for this popup.</p> @@ -403,13 +498,13 @@ public class PopupWindow { * @param animationStyle animation style to use when the popup appears * and disappears. Set to -1 for the default animation, 0 for no * animation, or a resource identifier for an explicit animation. - * + * * @see #update() */ public void setAnimationStyle(int animationStyle) { mAnimationStyle = animationStyle; } - + /** * <p>Return the view used as the content of the popup window.</p> * @@ -493,7 +588,7 @@ public class PopupWindow { * @param focusable true if the popup should grab focus, false otherwise. * * @see #isFocusable() - * @see #isShowing() + * @see #isShowing() * @see #update() */ public void setFocusable(boolean focusable) { @@ -502,23 +597,23 @@ public class PopupWindow { /** * Return the current value in {@link #setInputMethodMode(int)}. - * + * * @see #setInputMethodMode(int) */ public int getInputMethodMode() { return mInputMethodMode; - + } - + /** * Control how the popup operates with an input method: one of * {@link #INPUT_METHOD_FROM_FOCUSABLE}, {@link #INPUT_METHOD_NEEDED}, * or {@link #INPUT_METHOD_NOT_NEEDED}. - * + * * <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 #getInputMethodMode() * @see #update() */ @@ -549,12 +644,12 @@ public class PopupWindow { public int getSoftInputMode() { return mSoftInputMode; } - + /** * <p>Indicates whether the popup window receives touch events.</p> - * + * * @return true if the popup is touchable, false otherwise - * + * * @see #setTouchable(boolean) */ public boolean isTouchable() { @@ -573,7 +668,7 @@ public class PopupWindow { * @param touchable true if the popup should receive touch events, false otherwise * * @see #isTouchable() - * @see #isShowing() + * @see #isShowing() * @see #update() */ public void setTouchable(boolean touchable) { @@ -583,9 +678,9 @@ public class PopupWindow { /** * <p>Indicates whether the popup window will be informed of touch events * outside of its window.</p> - * + * * @return true if the popup is outside touchable, false otherwise - * + * * @see #setOutsideTouchable(boolean) */ public boolean isOutsideTouchable() { @@ -606,7 +701,7 @@ public class PopupWindow { * touch events, false otherwise * * @see #isOutsideTouchable() - * @see #isShowing() + * @see #isShowing() * @see #update() */ public void setOutsideTouchable(boolean touchable) { @@ -615,9 +710,9 @@ public class PopupWindow { /** * <p>Indicates whether clipping of the popup window is enabled.</p> - * + * * @return true if the clipping is enabled, false otherwise - * + * * @see #setClippingEnabled(boolean) */ public boolean isClippingEnabled() { @@ -628,13 +723,13 @@ public class PopupWindow { * <p>Allows the popup window to extend beyond the bounds of the screen. By default the * window is clipped to the screen boundaries. Setting this to false will allow windows to be * accurately positioned.</p> - * + * * <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> * * @param enabled false if the window should be allowed to extend outside of the screen - * @see #isShowing() + * @see #isShowing() * @see #isClippingEnabled() * @see #update() */ @@ -662,12 +757,12 @@ public class PopupWindow { void setAllowScrollingAnchorParent(boolean enabled) { mAllowScrollingAnchorParent = enabled; } - + /** * <p>Indicates whether the popup window supports splitting touches.</p> - * + * * @return true if the touch splitting is enabled, false otherwise - * + * * @see #setSplitTouchEnabled(boolean) */ public boolean isSplitTouchEnabled() { @@ -796,7 +891,7 @@ public class PopupWindow { * window manager by the popup. By default these are 0, meaning that * the current width or height is requested as an explicit size from * the window manager. You can supply - * {@link ViewGroup.LayoutParams#WRAP_CONTENT} or + * {@link ViewGroup.LayoutParams#WRAP_CONTENT} or * {@link ViewGroup.LayoutParams#MATCH_PARENT} to have that measure * spec supplied instead, replacing the absolute width and height that * has been set in the popup.</p> @@ -817,7 +912,7 @@ public class PopupWindow { mWidthMode = widthSpec; mHeightMode = heightSpec; } - + /** * <p>Return this popup's height MeasureSpec</p> * @@ -838,7 +933,7 @@ public class PopupWindow { * @param height the height MeasureSpec of the popup * * @see #getHeight() - * @see #isShowing() + * @see #isShowing() */ public void setHeight(int height) { mHeight = height; @@ -849,7 +944,7 @@ public class PopupWindow { * * @return the width MeasureSpec of the popup * - * @see #setWidth(int) + * @see #setWidth(int) */ public int getWidth() { return mWidth; @@ -871,6 +966,34 @@ public class PopupWindow { } /** + * Sets whether the popup window should overlap its anchor view when + * displayed as a drop-down. + * <p> + * If the popup is showing, calling this method will take effect only + * the next time the popup is shown. + * + * @param overlapAnchor Whether the popup should overlap its anchor. + * + * @see #getOverlapAnchor() + * @see #isShowing() + */ + public void setOverlapAnchor(boolean overlapAnchor) { + mOverlapAnchor = overlapAnchor; + } + + /** + * Returns whether the popup window should overlap its anchor view when + * displayed as a drop-down. + * + * @return Whether the popup should overlap its anchor. + * + * @see #setOverlapAnchor(boolean) + */ + public boolean getOverlapAnchor() { + return mOverlapAnchor; + } + + /** * <p>Indicate whether this popup window is showing on screen.</p> * * @return true if the popup is showing, false otherwise @@ -887,7 +1010,7 @@ public class PopupWindow { * a gravity of {@link android.view.Gravity#NO_GRAVITY} is similar to specifying * <code>Gravity.LEFT | Gravity.TOP</code>. * </p> - * + * * @param parent a parent view to get the {@link android.view.View#getWindowToken()} token from * @param gravity the gravity which controls the placement of the popup window * @param x the popup's x location offset @@ -913,32 +1036,34 @@ public class PopupWindow { return; } + TransitionManager.endTransitions(mDecorView); + unregisterForScrollChanged(); mIsShowing = true; mIsDropdown = false; - WindowManager.LayoutParams p = createPopupLayout(token); - p.windowAnimations = computeAnimationResource(); - + final WindowManager.LayoutParams p = createPopupLayoutParams(token); preparePopup(p); - if (gravity == Gravity.NO_GRAVITY) { - gravity = Gravity.TOP | Gravity.START; + + // Only override the default if some gravity was specified. + if (gravity != Gravity.NO_GRAVITY) { + p.gravity = gravity; } - p.gravity = gravity; + p.x = x; p.y = y; - if (mHeightMode < 0) p.height = mLastHeight = mHeightMode; - if (mWidthMode < 0) p.width = mLastWidth = mWidthMode; + invokePopup(p); } /** - * <p>Display the content view in a popup window anchored to the bottom-left + * Display the content view in a popup window anchored to the bottom-left * corner of the anchor view. If there is not enough room on screen to show * the popup in its entirety, this method tries to find a parent scroll - * view to scroll. If no parent scroll view can be scrolled, the bottom-left - * corner of the popup is pinned at the top left corner of the anchor view.</p> + * view to scroll. If no parent scroll view can be scrolled, the + * bottom-left corner of the popup is pinned at the top left corner of the + * anchor view. * * @param anchor the view on which to pin the popup window * @@ -949,14 +1074,15 @@ public class PopupWindow { } /** - * <p>Display the content view in a popup window anchored to the bottom-left + * Display the content view in a popup window anchored to the bottom-left * corner of the anchor view offset by the specified x and y coordinates. - * If there is not enough room on screen to show - * the popup in its entirety, this method tries to find a parent scroll - * view to scroll. If no parent scroll view can be scrolled, the bottom-left - * corner of the popup is pinned at the top left corner of the anchor view.</p> - * <p>If the view later scrolls to move <code>anchor</code> to a different - * location, the popup will be moved correspondingly.</p> + * If there is not enough room on screen to show the popup in its entirety, + * this method tries to find a parent scroll view to scroll. If no parent + * scroll view can be scrolled, the bottom-left corner of the popup is + * pinned at the top left corner of the anchor view. + * <p> + * If the view later scrolls to move <code>anchor</code> to a different + * location, the popup will be moved correspondingly. * * @param anchor the view on which to pin the popup window * @param xoff A horizontal offset from the anchor in pixels @@ -969,14 +1095,17 @@ public class PopupWindow { } /** - * <p>Display the content view in a popup window anchored to the bottom-left - * corner of the anchor view offset by the specified x and y coordinates. - * If there is not enough room on screen to show - * the popup in its entirety, this method tries to find a parent scroll - * view to scroll. If no parent scroll view can be scrolled, the bottom-left - * corner of the popup is pinned at the top left corner of the anchor view.</p> - * <p>If the view later scrolls to move <code>anchor</code> to a different - * location, the popup will be moved correspondingly.</p> + * Displays the content view in a popup window anchored to the corner of + * another view. The window is positioned according to the specified + * gravity and offset by the specified x and y coordinates. + * <p> + * If there is not enough room on screen to show the popup in its entirety, + * this method tries to find a parent scroll view to scroll. If no parent + * view can be scrolled, the specified vertical gravity will be ignored and + * the popup will anchor itself such that it is visible. + * <p> + * If the view later scrolls to move <code>anchor</code> to a different + * location, the popup will be moved correspondingly. * * @param anchor the view on which to pin the popup window * @param xoff A horizontal offset from the anchor in pixels @@ -990,20 +1119,18 @@ public class PopupWindow { return; } + TransitionManager.endTransitions(mDecorView); + registerForScrollChanged(anchor, xoff, yoff, gravity); mIsShowing = true; mIsDropdown = true; - WindowManager.LayoutParams p = createPopupLayout(anchor.getWindowToken()); + final WindowManager.LayoutParams p = createPopupLayoutParams(anchor.getWindowToken()); preparePopup(p); - updateAboveAnchor(findDropDownPosition(anchor, p, xoff, yoff, gravity)); - - if (mHeightMode < 0) p.height = mLastHeight = mHeightMode; - if (mWidthMode < 0) p.width = mLastWidth = mWidthMode; - - p.windowAnimations = computeAnimationResource(); + final boolean aboveAnchor = findDropDownPosition(anchor, p, xoff, yoff, gravity); + updateAboveAnchor(aboveAnchor); invokePopup(p); } @@ -1018,12 +1145,12 @@ public class PopupWindow { // do the job. if (mAboveAnchorBackgroundDrawable != null) { if (mAboveAnchor) { - mPopupView.setBackground(mAboveAnchorBackgroundDrawable); + mDecorView.setBackground(mAboveAnchorBackgroundDrawable); } else { - mPopupView.setBackground(mBelowAnchorBackgroundDrawable); + mDecorView.setBackground(mBelowAnchorBackgroundDrawable); } } else { - mPopupView.refreshDrawableState(); + mDecorView.refreshDrawableState(); } } } @@ -1045,10 +1172,9 @@ public class PopupWindow { } /** - * <p>Prepare the popup by embedding in into a new ViewGroup if the - * background drawable is not null. If embedding is required, the layout - * parameters' height is modified to take into account the background's - * padding.</p> + * Prepare the popup by embedding it into a new ViewGroup if the background + * drawable is not null. If embedding is required, the layout parameters' + * height is modified to take into account the background's padding. * * @param p the layout parameters of the popup's content view */ @@ -1058,36 +1184,86 @@ public class PopupWindow { + "calling setContentView() before attempting to show the popup."); } - if (mBackground != null) { - final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams(); - int height = ViewGroup.LayoutParams.MATCH_PARENT; - if (layoutParams != null && - layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT) { - height = ViewGroup.LayoutParams.WRAP_CONTENT; - } - - // when a background is available, we embed the content view - // within another view that owns the background drawable - PopupViewContainer popupViewContainer = new PopupViewContainer(mContext); - PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, height - ); - popupViewContainer.setBackground(mBackground); - popupViewContainer.addView(mContentView, listParams); + // The old decor view may be transitioning out. Make sure it finishes + // and cleans up before we try to create another one. + if (mDecorView != null) { + mDecorView.cancelTransitions(); + } - mPopupView = popupViewContainer; + // When a background is available, we embed the content view within + // another view that owns the background drawable. + final View backgroundView; + if (mBackground != null) { + backgroundView = createBackgroundView(mContentView); + backgroundView.setBackground(mBackground); } else { - mPopupView = mContentView; + backgroundView = mContentView; } - mPopupView.setElevation(mElevation); + mDecorView = createDecorView(backgroundView); + + // The background owner should be elevated so that it casts a shadow. + backgroundView.setElevation(mElevation); + + // We may wrap that in another view, so we'll need to manually specify + // the surface insets. + final int surfaceInset = (int) Math.ceil(backgroundView.getZ() * 2); + p.surfaceInsets.set(surfaceInset, surfaceInset, surfaceInset, surfaceInset); + p.hasManualSurfaceInsets = true; + mPopupViewInitialLayoutDirectionInherited = - (mPopupView.getRawLayoutDirection() == View.LAYOUT_DIRECTION_INHERIT); + (mContentView.getRawLayoutDirection() == View.LAYOUT_DIRECTION_INHERIT); mPopupWidth = p.width; mPopupHeight = p.height; } /** + * Wraps a content view in a PopupViewContainer. + * + * @param contentView the content view to wrap + * @return a PopupViewContainer that wraps the content view + */ + private PopupBackgroundView createBackgroundView(View contentView) { + final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams(); + final int height; + if (layoutParams != null && layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT) { + height = ViewGroup.LayoutParams.WRAP_CONTENT; + } else { + height = ViewGroup.LayoutParams.MATCH_PARENT; + } + + final PopupBackgroundView backgroundView = new PopupBackgroundView(mContext); + final PopupBackgroundView.LayoutParams listParams = new PopupBackgroundView.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, height); + backgroundView.addView(contentView, listParams); + + return backgroundView; + } + + /** + * Wraps a content view in a FrameLayout. + * + * @param contentView the content view to wrap + * @return a FrameLayout that wraps the content view + */ + private PopupDecorView createDecorView(View contentView) { + final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams(); + final int height; + if (layoutParams != null && layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT) { + height = ViewGroup.LayoutParams.WRAP_CONTENT; + } else { + height = ViewGroup.LayoutParams.MATCH_PARENT; + } + + final PopupDecorView decorView = new PopupDecorView(mContext); + decorView.addView(contentView, ViewGroup.LayoutParams.MATCH_PARENT, height); + decorView.setClipChildren(false); + decorView.setClipToPadding(false); + + return decorView; + } + + /** * <p>Invoke the popup window by adding the content view to the window * manager.</p> * @@ -1099,16 +1275,21 @@ public class PopupWindow { if (mContext != null) { p.packageName = mContext.getPackageName(); } - mPopupView.setFitsSystemWindows(mLayoutInsetDecor); + + final PopupDecorView decorView = mDecorView; + decorView.setFitsSystemWindows(mLayoutInsetDecor); + decorView.requestEnterTransition(mEnterTransition); + setLayoutDirectionFromAnchor(); - mWindowManager.addView(mPopupView, p); + + mWindowManager.addView(decorView, p); } private void setLayoutDirectionFromAnchor() { if (mAnchor != null) { View anchor = mAnchor.get(); if (anchor != null && mPopupViewInitialLayoutDirectionInherited) { - mPopupView.setLayoutDirection(anchor.getLayoutDirection()); + mDecorView.setLayoutDirection(anchor.getLayoutDirection()); } } } @@ -1120,26 +1301,39 @@ public class PopupWindow { * * @return the layout parameters to pass to the window manager */ - private WindowManager.LayoutParams createPopupLayout(IBinder token) { - // generates the layout parameters for the drop down - // we want a fixed size view located at the bottom left of the anchor - WindowManager.LayoutParams p = new WindowManager.LayoutParams(); - // these gravity settings put the view at the top left corner of the - // screen. The view is then positioned to the appropriate location - // by setting the x and y offsets to match the anchor's bottom - // left corner + private WindowManager.LayoutParams createPopupLayoutParams(IBinder token) { + final WindowManager.LayoutParams p = new WindowManager.LayoutParams(); + + // These gravity settings put the view at the top left corner of the + // screen. The view is then positioned to the appropriate location by + // setting the x and y offsets to match the anchor's bottom-left + // corner. p.gravity = Gravity.START | Gravity.TOP; - p.width = mLastWidth = mWidth; - p.height = mLastHeight = mHeight; + p.flags = computeFlags(p.flags); + p.type = mWindowLayoutType; + p.token = token; + p.softInputMode = mSoftInputMode; + p.windowAnimations = computeAnimationResource(); + if (mBackground != null) { p.format = mBackground.getOpacity(); } else { p.format = PixelFormat.TRANSLUCENT; } - p.flags = computeFlags(p.flags); - p.type = mWindowLayoutType; - p.token = token; - p.softInputMode = mSoftInputMode; + + if (mHeightMode < 0) { + p.height = mLastHeight = mHeightMode; + } else { + p.height = mLastHeight = mHeight; + } + + if (mWidthMode < 0) { + p.width = mLastWidth = mWidthMode; + } else { + p.width = mLastWidth = mWidth; + } + + // Used for debugging. p.setTitle("PopupWindow:" + Integer.toHexString(hashCode())); return p; @@ -1193,7 +1387,7 @@ public class PopupWindow { } private int computeAnimationResource() { - if (mAnimationStyle == -1) { + if (mAnimationStyle == ANIMATION_STYLE_DEFAULT) { if (mIsDropdown) { return mAboveAnchor ? com.android.internal.R.style.Animation_DropDownUp @@ -1212,7 +1406,7 @@ public class PopupWindow { * <p> * The height must have been set on the layout parameters prior to calling * this method. - * + * * @param anchor the view on which the popup window must be anchored * @param p the layout parameters used to display the drop down * @param xoff horizontal offset used to adjust for background padding @@ -1310,19 +1504,15 @@ public class PopupWindow { p.gravity |= Gravity.DISPLAY_CLIP_VERTICAL; - // Compute the position of the anchor relative to the popup. - mAnchorRelativeX = mDrawingLocation[0] - p.x + anchorHeight / 2; - mAnchorRelativeY = mDrawingLocation[1] - p.y + anchorWidth / 2; - return onTop; } - + /** * Returns the maximum height that is available for the popup to be * completely shown. It is recommended that this height be the maximum for * the popup's height, otherwise it is possible that the popup will be * clipped. - * + * * @param anchor The view on which the popup window must be anchored. * @return The maximum available height for the popup to be completely * shown. @@ -1345,14 +1535,14 @@ public class PopupWindow { public int getMaxAvailableHeight(View anchor, int yOffset) { return getMaxAvailableHeight(anchor, yOffset, false); } - + /** * Returns the maximum height that is available for the popup to be * completely shown, optionally ignoring any bottom decorations such as * the input method. It is recommended that this height be the maximum for * the popup's height, otherwise it is possible that the popup will be * clipped. - * + * * @param anchor The view on which the popup window must be anchored. * @param yOffset y offset from the view's bottom edge * @param ignoreBottomDecorations if true, the height returned will be @@ -1360,7 +1550,7 @@ public class PopupWindow { * bottom decorations * @return The maximum available height for the popup to be completely * shown. - * + * * @hide Pending API council approval. */ public int getMaxAvailableHeight(View anchor, int yOffset, boolean ignoreBottomDecorations) { @@ -1369,7 +1559,7 @@ public class PopupWindow { final int[] anchorPos = mDrawingLocation; anchor.getLocationOnScreen(anchorPos); - + int bottomEdge = displayFrame.bottom; if (ignoreBottomDecorations) { Resources res = anchor.getContext().getResources(); @@ -1382,49 +1572,90 @@ public class PopupWindow { int returnedHeight = Math.max(distanceToBottom, distanceToTop); if (mBackground != null) { mBackground.getPadding(mTempRect); - returnedHeight -= mTempRect.top + mTempRect.bottom; + returnedHeight -= mTempRect.top + mTempRect.bottom; } - + return returnedHeight; } - + /** - * <p>Dispose of the popup window. This method can be invoked only after - * {@link #showAsDropDown(android.view.View)} has been executed. Failing that, calling - * this method will have no effect.</p> + * Disposes of the popup window. This method can be invoked only after + * {@link #showAsDropDown(android.view.View)} has been executed. Failing + * that, calling this method will have no effect. * - * @see #showAsDropDown(android.view.View) + * @see #showAsDropDown(android.view.View) */ public void dismiss() { - if (isShowing() && mPopupView != null) { - mIsShowing = false; + if (!isShowing() || mIsTransitioningToDismiss) { + return; + } - unregisterForScrollChanged(); + final PopupDecorView decorView = mDecorView; + final View contentView = mContentView; - try { - mWindowManager.removeViewImmediate(mPopupView); - } finally { - if (mPopupView != mContentView && mPopupView instanceof ViewGroup) { - ((ViewGroup) mPopupView).removeView(mContentView); - } - mPopupView = null; + final ViewGroup contentHolder; + final ViewParent contentParent = contentView.getParent(); + if (contentParent instanceof ViewGroup) { + contentHolder = ((ViewGroup) contentParent); + } else { + contentHolder = null; + } - if (mOnDismissListener != null) { - mOnDismissListener.onDismiss(); + // Ensure any ongoing or pending transitions are canceled. + decorView.cancelTransitions(); + + unregisterForScrollChanged(); + + mIsShowing = false; + mIsTransitioningToDismiss = true; + + if (mExitTransition != null && decorView.isLaidOut()) { + decorView.startExitTransition(mExitTransition, new TransitionListenerAdapter() { + @Override + public void onTransitionEnd(Transition transition) { + dismissImmediate(decorView, contentHolder, contentView); } - } + }); + } else { + dismissImmediate(decorView, contentHolder, contentView); + } + + if (mOnDismissListener != null) { + mOnDismissListener.onDismiss(); } } /** + * Removes the popup from the window manager and tears down the supporting + * view hierarchy, if necessary. + */ + private void dismissImmediate(View decorView, ViewGroup contentHolder, View contentView) { + // If this method gets called and the decor view doesn't have a parent, + // then it was either never added or was already removed. That should + // never happen, but it's worth checking to avoid potential crashes. + if (decorView.getParent() != null) { + mWindowManager.removeViewImmediate(decorView); + } + + if (contentHolder != null) { + contentHolder.removeView(contentView); + } + + // This needs to stay until after all transitions have ended since we + // need the reference to cancel transitions in preparePopup(). + mDecorView = null; + mIsTransitioningToDismiss = false; + } + + /** * Sets the listener to be called when the window is dismissed. - * + * * @param onDismissListener The listener. */ public void setOnDismissListener(OnDismissListener onDismissListener) { mOnDismissListener = onDismissListener; } - + /** * Updates the state of the popup window, if it is currently being displayed, * from the currently set state. This includes: @@ -1436,12 +1667,12 @@ public class PopupWindow { if (!isShowing() || mContentView == null) { return; } - - WindowManager.LayoutParams p = (WindowManager.LayoutParams) - mPopupView.getLayoutParams(); - + + final WindowManager.LayoutParams p = + (WindowManager.LayoutParams) mDecorView.getLayoutParams(); + boolean update = false; - + final int newAnim = computeAnimationResource(); if (newAnim != p.windowAnimations) { p.windowAnimations = newAnim; @@ -1456,7 +1687,7 @@ public class PopupWindow { if (update) { setLayoutDirectionFromAnchor(); - mWindowManager.updateViewLayout(mPopupView, p); + mWindowManager.updateViewLayout(mDecorView, p); } } @@ -1469,11 +1700,11 @@ public class PopupWindow { * @param height the new height */ public void update(int width, int height) { - WindowManager.LayoutParams p = (WindowManager.LayoutParams) - mPopupView.getLayoutParams(); + final WindowManager.LayoutParams p = + (WindowManager.LayoutParams) mDecorView.getLayoutParams(); update(p.x, p.y, width, height, false); } - + /** * <p>Updates the position and the dimension of the popup window. Width and * height can be set to -1 to update location only. Calling this function @@ -1517,7 +1748,8 @@ public class PopupWindow { return; } - WindowManager.LayoutParams p = (WindowManager.LayoutParams) mPopupView.getLayoutParams(); + final WindowManager.LayoutParams p = + (WindowManager.LayoutParams) mDecorView.getLayoutParams(); boolean update = force; @@ -1557,7 +1789,7 @@ public class PopupWindow { if (update) { setLayoutDirectionFromAnchor(); - mWindowManager.updateViewLayout(mPopupView, p); + mWindowManager.updateViewLayout(mDecorView, p); } } @@ -1571,7 +1803,7 @@ public class PopupWindow { * @param height the new height, can be -1 to ignore */ public void update(View anchor, int width, int height) { - update(anchor, false, 0, 0, true, width, height, mAnchoredGravity); + update(anchor, false, 0, 0, true, width, height); } /** @@ -1590,30 +1822,26 @@ public class PopupWindow { * @param height the new height, can be -1 to ignore */ public void update(View anchor, int xoff, int yoff, int width, int height) { - update(anchor, true, xoff, yoff, true, width, height, mAnchoredGravity); + update(anchor, true, xoff, yoff, true, width, height); } private void update(View anchor, boolean updateLocation, int xoff, int yoff, - boolean updateDimension, int width, int height, int gravity) { + boolean updateDimension, int width, int height) { if (!isShowing() || mContentView == null) { return; } - WeakReference<View> oldAnchor = mAnchor; - final boolean needsUpdate = updateLocation - && (mAnchorXoff != xoff || mAnchorYoff != yoff); + final WeakReference<View> oldAnchor = mAnchor; + final boolean needsUpdate = updateLocation && (mAnchorXoff != xoff || mAnchorYoff != yoff); if (oldAnchor == null || oldAnchor.get() != anchor || (needsUpdate && !mIsDropdown)) { - registerForScrollChanged(anchor, xoff, yoff, gravity); + registerForScrollChanged(anchor, xoff, yoff, mAnchoredGravity); } else if (needsUpdate) { // No need to register again if this is a DropDown, showAsDropDown already did. mAnchorXoff = xoff; mAnchorYoff = yoff; - mAnchoredGravity = gravity; } - WindowManager.LayoutParams p = (WindowManager.LayoutParams) mPopupView.getLayoutParams(); - if (updateDimension) { if (width == -1) { width = mPopupWidth; @@ -1627,11 +1855,12 @@ public class PopupWindow { } } - int x = p.x; - int y = p.y; - + final WindowManager.LayoutParams p = + (WindowManager.LayoutParams) mDecorView.getLayoutParams(); + final int x = p.x; + final int y = p.y; if (updateLocation) { - updateAboveAnchor(findDropDownPosition(anchor, p, xoff, yoff, gravity)); + updateAboveAnchor(findDropDownPosition(anchor, p, xoff, yoff, mAnchoredGravity)); } else { updateAboveAnchor(findDropDownPosition(anchor, p, mAnchorXoff, mAnchorYoff, mAnchoredGravity)); @@ -1651,23 +1880,22 @@ public class PopupWindow { } private void unregisterForScrollChanged() { - WeakReference<View> anchorRef = mAnchor; - View anchor = null; - if (anchorRef != null) { - anchor = anchorRef.get(); - } + final WeakReference<View> anchorRef = mAnchor; + final View anchor = anchorRef == null ? null : anchorRef.get(); if (anchor != null) { - ViewTreeObserver vto = anchor.getViewTreeObserver(); + final ViewTreeObserver vto = anchor.getViewTreeObserver(); vto.removeOnScrollChangedListener(mOnScrollChangedListener); } + mAnchor = null; } private void registerForScrollChanged(View anchor, int xoff, int yoff, int gravity) { unregisterForScrollChanged(); - mAnchor = new WeakReference<View>(anchor); - ViewTreeObserver vto = anchor.getViewTreeObserver(); + mAnchor = new WeakReference<>(anchor); + + final ViewTreeObserver vto = anchor.getViewTreeObserver(); if (vto != null) { vto.addOnScrollChangedListener(mOnScrollChangedListener); } @@ -1677,41 +1905,28 @@ public class PopupWindow { mAnchoredGravity = gravity; } - private class PopupViewContainer extends FrameLayout { - private static final String TAG = "PopupWindow.PopupViewContainer"; + private class PopupDecorView extends FrameLayout { + private TransitionListenerAdapter mPendingExitListener; - public PopupViewContainer(Context context) { + public PopupDecorView(Context context) { super(context); } @Override - protected int[] onCreateDrawableState(int extraSpace) { - if (mAboveAnchor) { - // 1 more needed for the above anchor state - final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); - View.mergeDrawableStates(drawableState, ABOVE_ANCHOR_STATE_SET); - return drawableState; - } else { - return super.onCreateDrawableState(extraSpace); - } - } - - @Override public boolean dispatchKeyEvent(KeyEvent event) { if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { if (getKeyDispatcherState() == null) { return super.dispatchKeyEvent(event); } - if (event.getAction() == KeyEvent.ACTION_DOWN - && event.getRepeatCount() == 0) { - KeyEvent.DispatcherState state = getKeyDispatcherState(); + if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { + final KeyEvent.DispatcherState state = getKeyDispatcherState(); if (state != null) { state.startTracking(event, this); } return true; } else if (event.getAction() == KeyEvent.ACTION_UP) { - KeyEvent.DispatcherState state = getKeyDispatcherState(); + final KeyEvent.DispatcherState state = getKeyDispatcherState(); if (state != null && state.isTracking(event) && !event.isCanceled()) { dismiss(); return true; @@ -1735,7 +1950,7 @@ public class PopupWindow { public boolean onTouchEvent(MotionEvent event) { final int x = (int) event.getX(); final int y = (int) event.getY(); - + if ((event.getAction() == MotionEvent.ACTION_DOWN) && ((x < 0) || (x >= getWidth()) || (y < 0) || (y >= getHeight()))) { dismiss(); @@ -1748,15 +1963,115 @@ public class PopupWindow { } } + /** + * Requests that an enter transition run after the next layout pass. + */ + public void requestEnterTransition(Transition transition) { + final ViewTreeObserver observer = getViewTreeObserver(); + if (observer != null && transition != null) { + final Transition enterTransition = transition.clone(); + + // Postpone the enter transition after the first layout pass. + observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + final ViewTreeObserver observer = getViewTreeObserver(); + if (observer != null) { + observer.removeOnGlobalLayoutListener(this); + } + + startEnterTransition(enterTransition); + } + }); + } + } + + /** + * Starts the pending enter transition, if one is set. + */ + private void startEnterTransition(Transition enterTransition) { + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + enterTransition.addTarget(child); + child.setVisibility(View.INVISIBLE); + } + + TransitionManager.beginDelayedTransition(this, enterTransition); + + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + child.setVisibility(View.VISIBLE); + } + } + + /** + * Starts an exit transition immediately. + * <p> + * <strong>Note:</strong> The transition listener is guaranteed to have + * its {@code onTransitionEnd} method called even if the transition + * never starts; however, it may be called with a {@code null} argument. + */ + public void startExitTransition(Transition transition, final TransitionListener listener) { + if (transition == null) { + return; + } + + // The exit listener MUST be called for cleanup, even if the + // transition never starts or ends. Stash it for later. + mPendingExitListener = new TransitionListenerAdapter() { + @Override + public void onTransitionEnd(Transition transition) { + listener.onTransitionEnd(transition); + + // The listener was called. Our job here is done. + mPendingExitListener = null; + } + }; + + final Transition exitTransition = transition.clone(); + exitTransition.addListener(mPendingExitListener); + + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + exitTransition.addTarget(child); + } + + TransitionManager.beginDelayedTransition(this, exitTransition); + + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + child.setVisibility(View.INVISIBLE); + } + } + + /** + * Cancels all pending or current transitions. + */ + public void cancelTransitions() { + TransitionManager.endTransitions(this); + + if (mPendingExitListener != null) { + mPendingExitListener.onTransitionEnd(null); + } + } + } + + private class PopupBackgroundView extends FrameLayout { + public PopupBackgroundView(Context context) { + super(context); + } + @Override - public void sendAccessibilityEvent(int eventType) { - // clinets are interested in the content not the container, make it event source - if (mContentView != null) { - mContentView.sendAccessibilityEvent(eventType); + protected int[] onCreateDrawableState(int extraSpace) { + if (mAboveAnchor) { + final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); + View.mergeDrawableStates(drawableState, ABOVE_ANCHOR_STATE_SET); + return drawableState; } else { - super.sendAccessibilityEvent(eventType); + return super.onCreateDrawableState(extraSpace); } } } - } diff --git a/core/java/android/widget/ProgressBar.java b/core/java/android/widget/ProgressBar.java index de1bbc7..50d701a 100644 --- a/core/java/android/widget/ProgressBar.java +++ b/core/java/android/widget/ProgressBar.java @@ -18,15 +18,16 @@ package android.widget; import android.annotation.Nullable; import android.graphics.PorterDuff; + import com.android.internal.R; +import android.annotation.InterpolatorRes; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapShader; import android.graphics.Canvas; -import android.graphics.PorterDuff.Mode; import android.graphics.Rect; import android.graphics.Shader; import android.graphics.drawable.Animatable; @@ -49,7 +50,6 @@ import android.view.View; import android.view.ViewDebug; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; -import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.AnimationUtils; @@ -64,8 +64,8 @@ import java.util.ArrayList; /** * <p> * Visual indicator of progress in some operation. Displays a bar to the user - * representing how far the operation has progressed; the application can - * change the amount of progress (modifying the length of the bar) as it moves + * representing how far the operation has progressed; the application can + * change the amount of progress (modifying the length of the bar) as it moves * forward. There is also a secondary progress displayable on a progress bar * which is useful for displaying intermediate progress, such as the buffer * level during a streaming playback progress bar. @@ -81,7 +81,7 @@ import java.util.ArrayList; * <p>The following code example shows how a progress bar can be used from * a worker thread to update the user interface to notify the user of progress: * </p> - * + * * <pre> * public class MyActivity extends Activity { * private static final int PROGRESS = 0x1; @@ -169,13 +169,13 @@ import java.util.ArrayList; * </ul> * <p>The "inverse" styles provide an inverse color scheme for the spinner, which may be necessary * if your application uses a light colored theme (a white background).</p> - * - * <p><strong>XML attributes</b></strong> - * <p> - * See {@link android.R.styleable#ProgressBar ProgressBar Attributes}, + * + * <p><strong>XML attributes</b></strong> + * <p> + * See {@link android.R.styleable#ProgressBar ProgressBar Attributes}, * {@link android.R.styleable#View View Attributes} * </p> - * + * * @attr ref android.R.styleable#ProgressBar_animationResolution * @attr ref android.R.styleable#ProgressBar_indeterminate * @attr ref android.R.styleable#ProgressBar_indeterminateBehavior @@ -244,7 +244,7 @@ public class ProgressBar extends View { public ProgressBar(Context context) { this(context, null); } - + public ProgressBar(Context context, AttributeSet attrs) { this(context, attrs, com.android.internal.R.attr.progressBarStyle); } @@ -261,9 +261,9 @@ public class ProgressBar extends View { final TypedArray a = context.obtainStyledAttributes( attrs, R.styleable.ProgressBar, defStyleAttr, defStyleRes); - + mNoInvalidate = true; - + final Drawable progressDrawable = a.getDrawable(R.styleable.ProgressBar_progressDrawable); if (progressDrawable != null) { // Calling this method can set mMaxHeight, make sure the corresponding @@ -282,11 +282,11 @@ public class ProgressBar extends View { mBehavior = a.getInt(R.styleable.ProgressBar_indeterminateBehavior, mBehavior); final int resID = a.getResourceId( - com.android.internal.R.styleable.ProgressBar_interpolator, + com.android.internal.R.styleable.ProgressBar_interpolator, android.R.anim.linear_interpolator); // default to linear interpolator if (resID > 0) { setInterpolator(context, resID); - } + } setMax(a.getInt(R.styleable.ProgressBar_max, mMax)); @@ -399,36 +399,49 @@ public class ProgressBar extends View { * traverse layer and state list drawables. */ private Drawable tileify(Drawable drawable, boolean clip) { - + // TODO: This is a terrible idea that potentially destroys any drawable + // that extends any of these classes. We *really* need to remove this. + if (drawable instanceof LayerDrawable) { - LayerDrawable background = (LayerDrawable) drawable; - final int N = background.getNumberOfLayers(); - Drawable[] outDrawables = new Drawable[N]; - + final LayerDrawable orig = (LayerDrawable) drawable; + final int N = orig.getNumberOfLayers(); + final Drawable[] outDrawables = new Drawable[N]; + for (int i = 0; i < N; i++) { - int id = background.getId(i); - outDrawables[i] = tileify(background.getDrawable(i), + final int id = orig.getId(i); + outDrawables[i] = tileify(orig.getDrawable(i), (id == R.id.progress || id == R.id.secondaryProgress)); } - LayerDrawable newBg = new LayerDrawable(outDrawables); - + final LayerDrawable clone = new LayerDrawable(outDrawables); for (int i = 0; i < N; i++) { - newBg.setId(i, background.getId(i)); + clone.setId(i, orig.getId(i)); + clone.setLayerGravity(i, orig.getLayerGravity(i)); + clone.setLayerWidth(i, orig.getLayerWidth(i)); + clone.setLayerHeight(i, orig.getLayerHeight(i)); + clone.setLayerInsetLeft(i, orig.getLayerInsetLeft(i)); + clone.setLayerInsetRight(i, orig.getLayerInsetRight(i)); + clone.setLayerInsetTop(i, orig.getLayerInsetTop(i)); + clone.setLayerInsetBottom(i, orig.getLayerInsetBottom(i)); + clone.setLayerInsetStart(i, orig.getLayerInsetStart(i)); + clone.setLayerInsetEnd(i, orig.getLayerInsetEnd(i)); } - - return newBg; - - } else if (drawable instanceof StateListDrawable) { - StateListDrawable in = (StateListDrawable) drawable; - StateListDrawable out = new StateListDrawable(); - int numStates = in.getStateCount(); - for (int i = 0; i < numStates; i++) { + + return clone; + } + + if (drawable instanceof StateListDrawable) { + final StateListDrawable in = (StateListDrawable) drawable; + final StateListDrawable out = new StateListDrawable(); + final int N = in.getStateCount(); + for (int i = 0; i < N; i++) { out.addState(in.getStateSet(i), tileify(in.getStateDrawable(i), clip)); } + return out; - - } else if (drawable instanceof BitmapDrawable) { + } + + if (drawable instanceof BitmapDrawable) { final BitmapDrawable bitmap = (BitmapDrawable) drawable; final Bitmap tileBitmap = bitmap.getBitmap(); if (mSampleTile == null) { @@ -448,7 +461,7 @@ public class ProgressBar extends View { return clip ? new ClipDrawable( shapeDrawable, Gravity.LEFT, ClipDrawable.HORIZONTAL) : shapeDrawable; } - + return drawable; } @@ -456,7 +469,7 @@ public class ProgressBar extends View { final float[] roundedCorners = new float[] { 5, 5, 5, 5, 5, 5, 5, 5 }; return new RoundRectShape(roundedCorners, null, null); } - + /** * Convert a AnimationDrawable for use as a barberpole animation. * Each frame of the animation is wrapped in a ClipDrawable and @@ -468,7 +481,7 @@ public class ProgressBar extends View { final int N = background.getNumberOfFrames(); AnimationDrawable newBg = new AnimationDrawable(); newBg.setOneShot(background.isOneShot()); - + for (int i = 0; i < N; i++) { Drawable frame = tileify(background.getFrame(i), true); frame.setLevel(10000); @@ -479,7 +492,7 @@ public class ProgressBar extends View { } return drawable; } - + /** * <p> * Initialize the progress bar's default values: @@ -520,7 +533,7 @@ public class ProgressBar extends View { * <p>Change the indeterminate mode for this progress bar. In indeterminate * mode, the progress is ignored and the progress bar shows an infinite * animation instead.</p> - * + * * If this progress bar's style only supports indeterminate mode (such as the circular * progress bars), then this will be ignored. * @@ -699,7 +712,7 @@ public class ProgressBar extends View { setIndeterminateDrawable(d); } - + /** * <p>Get the drawable used to draw the progress bar in * progress mode.</p> @@ -1135,7 +1148,7 @@ public class ProgressBar extends View { setProgressDrawable(d); } - + /** * @return The drawable currently used to draw the progress bar */ @@ -1214,30 +1227,12 @@ public class ProgressBar extends View { rd.fromUser = fromUser; return rd; } - + public void recycle() { sPool.release(this); } } - private void setDrawableTint(int id, ColorStateList tint, Mode tintMode, boolean fallback) { - Drawable layer = null; - - // We expect a layer drawable, so try to find the target ID. - final Drawable d = mCurrentDrawable; - if (d instanceof LayerDrawable) { - layer = ((LayerDrawable) d).findDrawableByLayerId(id); - } - - if (fallback && layer == null) { - layer = d; - } - - layer.mutate(); - layer.setTintList(tint); - layer.setTintMode(tintMode); - } - private synchronized void doRefreshProgress(int id, int progress, boolean fromUser, boolean callBackToApp) { float scale = mMax > 0 ? (float) progress / (float) mMax : 0; @@ -1257,13 +1252,13 @@ public class ProgressBar extends View { } else { invalidate(); } - + if (callBackToApp && id == R.id.progress) { - onProgressRefresh(scale, fromUser); + onProgressRefresh(scale, fromUser, progress); } } - void onProgressRefresh(float scale, boolean fromUser) { + void onProgressRefresh(float scale, boolean fromUser, int progress) { if (AccessibilityManager.getInstance(mContext).isEnabled()) { scheduleAccessibilityEventSender(); } @@ -1285,7 +1280,7 @@ public class ProgressBar extends View { } } } - + /** * <p>Set the current progress to the specified value. Does not do anything * if the progress bar is in indeterminate mode.</p> @@ -1295,13 +1290,13 @@ public class ProgressBar extends View { * @see #setIndeterminate(boolean) * @see #isIndeterminate() * @see #getProgress() - * @see #incrementProgressBy(int) + * @see #incrementProgressBy(int) */ @android.view.RemotableViewMethod public synchronized void setProgress(int progress) { setProgress(progress, false); } - + @android.view.RemotableViewMethod synchronized void setProgress(int progress, boolean fromUser) { if (mIndeterminate) { @@ -1327,7 +1322,7 @@ public class ProgressBar extends View { * Set the current secondary progress to the specified value. Does not do * anything if the progress bar is in indeterminate mode. * </p> - * + * * @param secondaryProgress the new secondary progress, between 0 and {@link #getMax()} * @see #setIndeterminate(boolean) * @see #isIndeterminate() @@ -1408,8 +1403,8 @@ public class ProgressBar extends View { * @param max the upper range of this progress bar * * @see #getMax() - * @see #setProgress(int) - * @see #setSecondaryProgress(int) + * @see #setProgress(int) + * @see #setSecondaryProgress(int) */ @android.view.RemotableViewMethod public synchronized void setMax(int max) { @@ -1426,13 +1421,13 @@ public class ProgressBar extends View { refreshProgress(R.id.progress, mProgress, false); } } - + /** * <p>Increase the progress bar's progress by the specified amount.</p> * * @param diff the amount by which the progress must be increased * - * @see #setProgress(int) + * @see #setProgress(int) */ public synchronized final void incrementProgressBy(int diff) { setProgress(mProgress + diff); @@ -1443,7 +1438,7 @@ public class ProgressBar extends View { * * @param diff the amount by which the secondary progress must be increased * - * @see #setSecondaryProgress(int) + * @see #setSecondaryProgress(int) */ public synchronized final void incrementSecondaryProgressBy(int diff) { setSecondaryProgress(mSecondaryProgress + diff); @@ -1466,13 +1461,13 @@ public class ProgressBar extends View { if (mInterpolator == null) { mInterpolator = new LinearInterpolator(); } - + if (mTransformation == null) { mTransformation = new Transformation(); } else { mTransformation.clear(); } - + if (mAnimation == null) { mAnimation = new AlphaAnimation(0.0f, 1.0f); } else { @@ -1507,7 +1502,7 @@ public class ProgressBar extends View { * @param context The application environment * @param resID The resource identifier of the interpolator to load */ - public void setInterpolator(Context context, int resID) { + public void setInterpolator(Context context, @InterpolatorRes int resID) { setInterpolator(AnimationUtils.loadInterpolator(context, resID)); } @@ -1623,7 +1618,7 @@ public class ProgressBar extends View { } mIndeterminateDrawable.setBounds(left, top, right, bottom); } - + if (mProgressDrawable != null) { mProgressDrawable.setBounds(0, 0, right, bottom); } @@ -1646,7 +1641,7 @@ public class ProgressBar extends View { // rotates properly in its animation final int saveCount = canvas.save(); - if(isLayoutRtl() && mMirrorForRtl) { + if (isLayoutRtl() && mMirrorForRtl) { canvas.translate(getWidth() - mPaddingRight, mPaddingTop); canvas.scale(-1.0f, 1.0f); } else { @@ -1678,35 +1673,38 @@ public class ProgressBar extends View { @Override protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - Drawable d = mCurrentDrawable; - int dw = 0; int dh = 0; + + final Drawable d = mCurrentDrawable; if (d != null) { dw = Math.max(mMinWidth, Math.min(mMaxWidth, d.getIntrinsicWidth())); dh = Math.max(mMinHeight, Math.min(mMaxHeight, d.getIntrinsicHeight())); } + updateDrawableState(); + dw += mPaddingLeft + mPaddingRight; dh += mPaddingTop + mPaddingBottom; - setMeasuredDimension(resolveSizeAndState(dw, widthMeasureSpec, 0), - resolveSizeAndState(dh, heightMeasureSpec, 0)); + final int measuredWidth = resolveSizeAndState(dw, widthMeasureSpec, 0); + final int measuredHeight = resolveSizeAndState(dh, heightMeasureSpec, 0); + setMeasuredDimension(measuredWidth, measuredHeight); } - + @Override protected void drawableStateChanged() { super.drawableStateChanged(); updateDrawableState(); } - + private void updateDrawableState() { - int[] state = getDrawableState(); - + final int[] state = getDrawableState(); + if (mProgressDrawable != null && mProgressDrawable.isStateful()) { mProgressDrawable.setState(state); } - + if (mIndeterminateDrawable != null && mIndeterminateDrawable.isStateful()) { mIndeterminateDrawable.setState(state); } @@ -1728,14 +1726,14 @@ public class ProgressBar extends View { static class SavedState extends BaseSavedState { int progress; int secondaryProgress; - + /** * Constructor called from {@link ProgressBar#onSaveInstanceState()} */ SavedState(Parcelable superState) { super(superState); } - + /** * Constructor called from {@link #CREATOR} */ @@ -1769,10 +1767,10 @@ public class ProgressBar extends View { // Force our ancestor class to save its state Parcelable superState = super.onSaveInstanceState(); SavedState ss = new SavedState(superState); - + ss.progress = mProgress; ss.secondaryProgress = mSecondaryProgress; - + return ss; } @@ -1780,7 +1778,7 @@ public class ProgressBar extends View { public void onRestoreInstanceState(Parcelable state) { SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); - + setProgress(ss.progress); setSecondaryProgress(ss.secondaryProgress); } @@ -1826,17 +1824,16 @@ public class ProgressBar extends View { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(ProgressBar.class.getName()); - event.setItemCount(mMax); - event.setCurrentItemIndex(mProgress); + public CharSequence getAccessibilityClassName() { + return ProgressBar.class.getName(); } + /** @hide */ @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(ProgressBar.class.getName()); + public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { + super.onInitializeAccessibilityEventInternal(event); + event.setItemCount(mMax); + event.setCurrentItemIndex(mProgress); } /** diff --git a/core/java/android/widget/QuickContactBadge.java b/core/java/android/widget/QuickContactBadge.java index 23fa402..25b301f 100644 --- a/core/java/android/widget/QuickContactBadge.java +++ b/core/java/android/widget/QuickContactBadge.java @@ -37,8 +37,6 @@ import android.provider.ContactsContract.RawContacts; import android.util.AttributeSet; import android.view.View; import android.view.View.OnClickListener; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; /** * Widget used to show an image with the standard QuickContact badge @@ -52,6 +50,7 @@ public class QuickContactBadge extends ImageView implements OnClickListener { private QueryHandler mQueryHandler; private Drawable mDefaultAvatar; private Bundle mExtras = null; + private String mPrioritizedMimeType; protected String[] mExcludeMimes = null; @@ -126,6 +125,15 @@ public class QuickContactBadge extends ImageView implements OnClickListener { public void setMode(int size) { } + /** + * Set which mimetype should be prioritized in the QuickContacts UI. For example, passing the + * value {@link Email#CONTENT_ITEM_TYPE} can cause emails to be displayed more prominently in + * QuickContacts. + */ + public void setPrioritizedMimeType(String prioritizedMimeType) { + mPrioritizedMimeType = prioritizedMimeType; + } + @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); @@ -287,7 +295,7 @@ public class QuickContactBadge extends ImageView implements OnClickListener { final Bundle extras = (mExtras == null) ? new Bundle() : mExtras; if (mContactUri != null) { QuickContact.showQuickContact(getContext(), QuickContactBadge.this, mContactUri, - QuickContact.MODE_LARGE, mExcludeMimes); + mExcludeMimes, mPrioritizedMimeType); } else if (mContactEmail != null && mQueryHandler != null) { extras.putString(EXTRA_URI_CONTENT, mContactEmail); mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP_AND_TRIGGER, extras, @@ -305,15 +313,8 @@ public class QuickContactBadge extends ImageView implements OnClickListener { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(QuickContactBadge.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(QuickContactBadge.class.getName()); + public CharSequence getAccessibilityClassName() { + return QuickContactBadge.class.getName(); } /** @@ -377,10 +378,10 @@ public class QuickContactBadge extends ImageView implements OnClickListener { mContactUri = lookupUri; onContactUriChanged(); - if (trigger && lookupUri != null) { + if (trigger && mContactUri != null) { // Found contact, so trigger QuickContact - QuickContact.showQuickContact(getContext(), QuickContactBadge.this, lookupUri, - QuickContact.MODE_LARGE, mExcludeMimes); + QuickContact.showQuickContact(getContext(), QuickContactBadge.this, mContactUri, + mExcludeMimes, mPrioritizedMimeType); } else if (createUri != null) { // Prompt user to add this person to contacts final Intent intent = new Intent(Intents.SHOW_OR_CREATE_CONTACT, createUri); diff --git a/core/java/android/widget/RadialTimePickerView.java b/core/java/android/widget/RadialTimePickerView.java index 11fda2c..28b4db2 100644 --- a/core/java/android/widget/RadialTimePickerView.java +++ b/core/java/android/widget/RadialTimePickerView.java @@ -23,23 +23,26 @@ import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; 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.Path; import android.graphics.Rect; +import android.graphics.Region; import android.graphics.Typeface; import android.os.Bundle; import android.util.AttributeSet; import android.util.IntArray; import android.util.Log; import android.util.MathUtils; +import android.util.StateSet; import android.util.TypedValue; import android.view.HapticFeedbackConstants; import android.view.MotionEvent; import android.view.View; -import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; @@ -56,14 +59,8 @@ import java.util.Locale; * * @hide */ -public class RadialTimePickerView extends View implements View.OnTouchListener { - private static final String TAG = "ClockView"; - - private static final boolean DEBUG = false; - - private static final int DEBUG_COLOR = 0x20FF0000; - private static final int DEBUG_TEXT_COLOR = 0x60FF0000; - private static final int DEBUG_STROKE_WIDTH = 2; +public class RadialTimePickerView extends View { + private static final String TAG = "RadialTimePickerView"; private static final int HOURS = 0; private static final int MINUTES = 1; @@ -82,12 +79,6 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { // Transparent alpha level private static final int ALPHA_TRANSPARENT = 0; - // Alpha level of color for selector. - private static final int ALPHA_SELECTOR = 60; // was 51 - - private static final float COSINE_30_DEGREES = ((float) Math.sqrt(3)) * 0.5f; - private static final float SINE_30_DEGREES = 0.5f; - private static final int DEGREES_FOR_ONE_HOUR = 30; private static final int DEGREES_FOR_ONE_MINUTE = 6; @@ -95,9 +86,27 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { private static final int[] HOURS_NUMBERS_24 = {0, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}; private static final int[] MINUTES_NUMBERS = {0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55}; - private static final int CENTER_RADIUS = 2; + private static final int FADE_OUT_DURATION = 500; + private static final int FADE_IN_DURATION = 500; + + private static final int[] SNAP_PREFER_30S_MAP = new int[361]; + + private static final int NUM_POSITIONS = 12; + private static final float[] COS_30 = new float[NUM_POSITIONS]; + private static final float[] SIN_30 = new float[NUM_POSITIONS]; + + static { + // Prepare mapping to snap touchable degrees to selectable degrees. + preparePrefer30sMap(); - private static int[] sSnapPrefer30sMap = new int[361]; + final double increment = 2.0 * Math.PI / NUM_POSITIONS; + double angle = Math.PI / 2.0; + for (int i = 0; i < NUM_POSITIONS; i++) { + COS_30[i] = (float) Math.cos(angle); + SIN_30[i] = (float) Math.sin(angle); + angle += increment; + } + } private final InvalidateUpdateListener mInvalidateUpdateListener = new InvalidateUpdateListener(); @@ -108,7 +117,6 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { private final String[] mMinutesTexts = new String[12]; private final Paint[] mPaint = new Paint[2]; - private final int[] mColor = new int[2]; private final IntHolder[] mAlpha = new IntHolder[2]; private final Paint mPaintCenter = new Paint(); @@ -118,42 +126,27 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { private final IntHolder[][] mAlphaSelector = new IntHolder[2][3]; private final Paint mPaintBackground = new Paint(); - private final Paint mPaintDebug = new Paint(); 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 ColorStateList[] mTextColor = new ColorStateList[3]; + private final int[] mTextSize = new int[3]; + private final int[] mTextInset = new int[3]; - private final float[] mTextSizeMultiplier = new float[3]; + private final float[][] mOuterTextX = new float[2][12]; + private final float[][] mOuterTextY = new float[2][12]; - private final float[] mAnimationRadiusMultiplier = new float[3]; - - private final float mTransitionMidRadiusMultiplier; - private final float mTransitionEndRadiusMultiplier; + private final float[] mInnerTextX = new float[12]; + private final float[] mInnerTextY = new float[12]; 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 ArrayList<Animator> mHoursToMinutesAnims = new ArrayList<>(); + private final ArrayList<Animator> mMinuteToHoursAnims = new ArrayList<>(); private final RadialPickerTouchHelper mTouchHelper; - private float mInnerTextSize; - private boolean mIs24HourMode; private boolean mShowHours; @@ -163,8 +156,14 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { */ private boolean mIsOnInnerCircle; + private int mSelectorRadius; + private int mSelectorStroke; + private int mSelectorDotRadius; + private int mCenterDotRadius; + private int mXCenter; private int mYCenter; + private int mCircleRadius; private int mMinHypotenuseForInnerNumber; private int mMaxHypotenuseForOuterNumber; @@ -176,7 +175,8 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { private AnimatorSet mTransition; private int mAmOrPm; - private int mDisabledAlpha; + + private float mDisabledAlpha; private OnValueSelectedListener mListener; @@ -186,11 +186,6 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance); } - static { - // Prepare mapping to snap touchable degrees to selectable degrees. - preparePrefer30sMap(); - } - /** * Split up the 360 degrees of the circle among the 60 selectable values. Assigns a larger * selectable area to each of the 12 visible values, such that the ratio of space apportioned @@ -225,7 +220,7 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { // Iterate through the input. for (int degrees = 0; degrees < 361; degrees++) { // Save the input-output mapping. - sSnapPrefer30sMap[degrees] = snappedOutputDegrees; + SNAP_PREFER_30S_MAP[degrees] = snappedOutputDegrees; // If this is the last input for the specified output, calculate the next output and // the next expected count. if (count == expectedCount) { @@ -252,10 +247,10 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { * mapping. */ private static int snapPrefer30s(int degrees) { - if (sSnapPrefer30sMap == null) { + if (SNAP_PREFER_30S_MAP == null) { return -1; } - return sSnapPrefer30sMap[degrees]; + return SNAP_PREFER_30S_MAP[degrees]; } /** @@ -308,7 +303,7 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { // Pull disabled alpha from theme. final TypedValue outValue = new TypedValue(); context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, outValue, true); - mDisabledAlpha = (int) (outValue.getFloat() * 255 + 0.5f); + mDisabledAlpha = outValue.getFloat(); // process style attributes final Resources res = getResources(); @@ -327,72 +322,73 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { } } - final int numbersTextColor = a.getColor(R.styleable.TimePicker_numbersTextColor, - res.getColor(R.color.timepicker_default_text_color_material)); + mTextColor[HOURS] = a.getColorStateList(R.styleable.TimePicker_numbersTextColor); + mTextColor[HOURS_INNER] = a.getColorStateList(R.styleable.TimePicker_numbersInnerTextColor); + mTextColor[MINUTES] = mTextColor[HOURS]; mPaint[HOURS] = new Paint(); mPaint[HOURS].setAntiAlias(true); mPaint[HOURS].setTextAlign(Paint.Align.CENTER); - mColor[HOURS] = numbersTextColor; mPaint[MINUTES] = new Paint(); mPaint[MINUTES].setAntiAlias(true); mPaint[MINUTES].setTextAlign(Paint.Align.CENTER); - mColor[MINUTES] = numbersTextColor; - mPaintCenter.setColor(numbersTextColor); + final ColorStateList selectorColors = a.getColorStateList( + R.styleable.TimePicker_numbersSelectorColor); + final int selectorActivatedColor = selectorColors.getColorForState( + StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_ACTIVATED), 0); + + mPaintCenter.setColor(selectorActivatedColor); mPaintCenter.setAntiAlias(true); - mPaintCenter.setTextAlign(Paint.Align.CENTER); + + final int[] activatedStateSet = StateSet.get( + StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_ACTIVATED); mPaintSelector[HOURS][SELECTOR_CIRCLE] = new Paint(); mPaintSelector[HOURS][SELECTOR_CIRCLE].setAntiAlias(true); - mColorSelector[HOURS][SELECTOR_CIRCLE] = a.getColor( - R.styleable.TimePicker_numbersSelectorColor, - R.color.timepicker_default_selector_color_material); + mColorSelector[HOURS][SELECTOR_CIRCLE] = selectorActivatedColor; mPaintSelector[HOURS][SELECTOR_DOT] = new Paint(); mPaintSelector[HOURS][SELECTOR_DOT].setAntiAlias(true); - mColorSelector[HOURS][SELECTOR_DOT] = a.getColor( - R.styleable.TimePicker_numbersSelectorColor, - R.color.timepicker_default_selector_color_material); + mColorSelector[HOURS][SELECTOR_DOT] = + mTextColor[HOURS].getColorForState(activatedStateSet, 0); mPaintSelector[HOURS][SELECTOR_LINE] = new Paint(); mPaintSelector[HOURS][SELECTOR_LINE].setAntiAlias(true); mPaintSelector[HOURS][SELECTOR_LINE].setStrokeWidth(2); - mColorSelector[HOURS][SELECTOR_LINE] = a.getColor( - R.styleable.TimePicker_numbersSelectorColor, - R.color.timepicker_default_selector_color_material); + mColorSelector[HOURS][SELECTOR_LINE] = selectorActivatedColor; mPaintSelector[MINUTES][SELECTOR_CIRCLE] = new Paint(); mPaintSelector[MINUTES][SELECTOR_CIRCLE].setAntiAlias(true); - mColorSelector[MINUTES][SELECTOR_CIRCLE] = a.getColor( - R.styleable.TimePicker_numbersSelectorColor, - R.color.timepicker_default_selector_color_material); + mColorSelector[MINUTES][SELECTOR_CIRCLE] = selectorActivatedColor; mPaintSelector[MINUTES][SELECTOR_DOT] = new Paint(); mPaintSelector[MINUTES][SELECTOR_DOT].setAntiAlias(true); - mColorSelector[MINUTES][SELECTOR_DOT] = a.getColor( - R.styleable.TimePicker_numbersSelectorColor, - R.color.timepicker_default_selector_color_material); + mColorSelector[MINUTES][SELECTOR_DOT] = + mTextColor[MINUTES].getColorForState(activatedStateSet, 0); mPaintSelector[MINUTES][SELECTOR_LINE] = new Paint(); mPaintSelector[MINUTES][SELECTOR_LINE].setAntiAlias(true); mPaintSelector[MINUTES][SELECTOR_LINE].setStrokeWidth(2); - mColorSelector[MINUTES][SELECTOR_LINE] = a.getColor( - R.styleable.TimePicker_numbersSelectorColor, - R.color.timepicker_default_selector_color_material); + mColorSelector[MINUTES][SELECTOR_LINE] = selectorActivatedColor; mPaintBackground.setColor(a.getColor(R.styleable.TimePicker_numbersBackgroundColor, - res.getColor(R.color.timepicker_default_numbers_background_color_material))); + context.getColor(R.color.timepicker_default_numbers_background_color_material))); mPaintBackground.setAntiAlias(true); - if (DEBUG) { - mPaintDebug.setColor(DEBUG_COLOR); - mPaintDebug.setAntiAlias(true); - mPaintDebug.setStrokeWidth(DEBUG_STROKE_WIDTH); - mPaintDebug.setStyle(Paint.Style.STROKE); - mPaintDebug.setTextAlign(Paint.Align.CENTER); - } + mSelectorRadius = res.getDimensionPixelSize(R.dimen.timepicker_selector_radius); + mSelectorStroke = res.getDimensionPixelSize(R.dimen.timepicker_selector_stroke); + mSelectorDotRadius = res.getDimensionPixelSize(R.dimen.timepicker_selector_dot_radius); + mCenterDotRadius = res.getDimensionPixelSize(R.dimen.timepicker_center_dot_radius); + + mTextSize[HOURS] = res.getDimensionPixelSize(R.dimen.timepicker_text_size_normal); + mTextSize[MINUTES] = res.getDimensionPixelSize(R.dimen.timepicker_text_size_normal); + mTextSize[HOURS_INNER] = res.getDimensionPixelSize(R.dimen.timepicker_text_size_inner); + + mTextInset[HOURS] = res.getDimensionPixelSize(R.dimen.timepicker_text_inset_normal); + mTextInset[MINUTES] = res.getDimensionPixelSize(R.dimen.timepicker_text_inset_normal); + mTextInset[HOURS_INNER] = res.getDimensionPixelSize(R.dimen.timepicker_text_inset_inner); mShowHours = true; mIs24HourMode = false; @@ -409,22 +405,8 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { initHoursAndMinutesText(); initData(); - mTransitionMidRadiusMultiplier = Float.parseFloat( - res.getString(R.string.timepicker_transition_mid_radius_multiplier)); - mTransitionEndRadiusMultiplier = Float.parseFloat( - res.getString(R.string.timepicker_transition_end_radius_multiplier)); - - mTextGridHeights[HOURS] = new float[7]; - mTextGridHeights[MINUTES] = new float[7]; - - mSelectionRadiusMultiplier = Float.parseFloat( - res.getString(R.string.timepicker_selection_radius_multiplier)); - a.recycle(); - setOnTouchListener(this); - setClickable(true); - // Initial values final Calendar calendar = Calendar.getInstance(Locale.getDefault()); final int currentHour = calendar.get(Calendar.HOUR_OF_DAY); @@ -436,21 +418,6 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { setHapticFeedbackEnabled(true); } - /** - * Measure the view to end up as a square, based on the minimum of the height and width. - */ - @Override - public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int measuredWidth = MeasureSpec.getSize(widthMeasureSpec); - int widthMode = MeasureSpec.getMode(widthMeasureSpec); - int measuredHeight = MeasureSpec.getSize(heightMeasureSpec); - int heightMode = MeasureSpec.getMode(heightMeasureSpec); - int minDimension = Math.min(measuredWidth, measuredHeight); - - super.onMeasure(MeasureSpec.makeMeasureSpec(minDimension, widthMode), - MeasureSpec.makeMeasureSpec(minDimension, heightMode)); - } - public void initialize(int hour, int minute, boolean is24HourMode) { if (mIs24HourMode != is24HourMode) { mIs24HourMode = is24HourMode; @@ -512,7 +479,6 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { mIsOnInnerCircle = isOnInnerCircle; initData(); - updateLayoutData(); mTouchHelper.invalidateRoot(); } @@ -601,24 +567,32 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { } public void showHours(boolean animate) { - if (mShowHours) return; + if (mShowHours) { + return; + } + mShowHours = true; + if (animate) { startMinutesToHoursAnimation(); } + initData(); - updateLayoutData(); invalidate(); } public void showMinutes(boolean animate) { - if (!mShowHours) return; + if (!mShowHours) { + return; + } + mShowHours = false; + if (animate) { startHoursToMinutesAnimation(); } + initData(); - updateLayoutData(); invalidate(); } @@ -643,162 +617,117 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { mOuterTextMinutes = mMinutesTexts; - final Resources res = getResources(); - - if (mShowHours) { - if (mIs24HourMode) { - mCircleRadiusMultiplier[HOURS] = Float.parseFloat( - res.getString(R.string.timepicker_circle_radius_multiplier_24HourMode)); - mNumbersRadiusMultiplier[HOURS] = Float.parseFloat( - res.getString(R.string.timepicker_numbers_radius_multiplier_outer)); - mTextSizeMultiplier[HOURS] = Float.parseFloat( - res.getString(R.string.timepicker_text_size_multiplier_outer)); - - mNumbersRadiusMultiplier[HOURS_INNER] = Float.parseFloat( - res.getString(R.string.timepicker_numbers_radius_multiplier_inner)); - mTextSizeMultiplier[HOURS_INNER] = Float.parseFloat( - res.getString(R.string.timepicker_text_size_multiplier_inner)); - } else { - mCircleRadiusMultiplier[HOURS] = Float.parseFloat( - res.getString(R.string.timepicker_circle_radius_multiplier)); - mNumbersRadiusMultiplier[HOURS] = Float.parseFloat( - res.getString(R.string.timepicker_numbers_radius_multiplier_normal)); - mTextSizeMultiplier[HOURS] = Float.parseFloat( - res.getString(R.string.timepicker_text_size_multiplier_normal)); - } - } else { - mCircleRadiusMultiplier[MINUTES] = Float.parseFloat( - res.getString(R.string.timepicker_circle_radius_multiplier)); - mNumbersRadiusMultiplier[MINUTES] = Float.parseFloat( - res.getString(R.string.timepicker_numbers_radius_multiplier_normal)); - mTextSizeMultiplier[MINUTES] = Float.parseFloat( - res.getString(R.string.timepicker_text_size_multiplier_normal)); - } - - mAnimationRadiusMultiplier[HOURS] = 1; - mAnimationRadiusMultiplier[HOURS_INNER] = 1; - mAnimationRadiusMultiplier[MINUTES] = 1; - - mAlpha[HOURS].setValue(mShowHours ? ALPHA_OPAQUE : ALPHA_TRANSPARENT); - mAlpha[MINUTES].setValue(mShowHours ? ALPHA_TRANSPARENT : ALPHA_OPAQUE); - - mAlphaSelector[HOURS][SELECTOR_CIRCLE].setValue( - mShowHours ? ALPHA_SELECTOR : ALPHA_TRANSPARENT); - mAlphaSelector[HOURS][SELECTOR_DOT].setValue( - mShowHours ? ALPHA_OPAQUE : ALPHA_TRANSPARENT); - mAlphaSelector[HOURS][SELECTOR_LINE].setValue( - mShowHours ? ALPHA_SELECTOR : ALPHA_TRANSPARENT); - - mAlphaSelector[MINUTES][SELECTOR_CIRCLE].setValue( - mShowHours ? ALPHA_TRANSPARENT : ALPHA_SELECTOR); - mAlphaSelector[MINUTES][SELECTOR_DOT].setValue( - mShowHours ? ALPHA_TRANSPARENT : ALPHA_OPAQUE); - mAlphaSelector[MINUTES][SELECTOR_LINE].setValue( - mShowHours ? ALPHA_TRANSPARENT : ALPHA_SELECTOR); + final int hoursAlpha = mShowHours ? ALPHA_OPAQUE : ALPHA_TRANSPARENT; + mAlpha[HOURS].setValue(hoursAlpha); + mAlphaSelector[HOURS][SELECTOR_CIRCLE].setValue(hoursAlpha); + mAlphaSelector[HOURS][SELECTOR_DOT].setValue(hoursAlpha); + mAlphaSelector[HOURS][SELECTOR_LINE].setValue(hoursAlpha); + + final int minutesAlpha = mShowHours ? ALPHA_TRANSPARENT : ALPHA_OPAQUE; + mAlpha[MINUTES].setValue(minutesAlpha); + mAlphaSelector[MINUTES][SELECTOR_CIRCLE].setValue(minutesAlpha); + mAlphaSelector[MINUTES][SELECTOR_DOT].setValue(minutesAlpha); + mAlphaSelector[MINUTES][SELECTOR_LINE].setValue(minutesAlpha); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - updateLayoutData(); - } + if (!changed) { + return; + } - private void updateLayoutData() { mXCenter = getWidth() / 2; mYCenter = getHeight() / 2; + mCircleRadius = Math.min(mXCenter, mYCenter); - final int min = Math.min(mXCenter, mYCenter); + mMinHypotenuseForInnerNumber = mCircleRadius - mTextInset[HOURS_INNER] - mSelectorRadius; + mMaxHypotenuseForOuterNumber = mCircleRadius - mTextInset[HOURS] - mSelectorRadius; + mHalfwayHypotenusePoint = mCircleRadius - (mTextInset[HOURS] + mTextInset[HOURS_INNER]) / 2; - mCircleRadius[HOURS] = min * mCircleRadiusMultiplier[HOURS]; - mCircleRadius[HOURS_INNER] = min * mCircleRadiusMultiplier[HOURS]; - mCircleRadius[MINUTES] = min * mCircleRadiusMultiplier[MINUTES]; - - mMinHypotenuseForInnerNumber = (int) (mCircleRadius[HOURS] - * mNumbersRadiusMultiplier[HOURS_INNER]) - mSelectionRadius[HOURS]; - mMaxHypotenuseForOuterNumber = (int) (mCircleRadius[HOURS] - * mNumbersRadiusMultiplier[HOURS]) + mSelectionRadius[HOURS]; - mHalfwayHypotenusePoint = (int) (mCircleRadius[HOURS] - * ((mNumbersRadiusMultiplier[HOURS] + mNumbersRadiusMultiplier[HOURS_INNER]) / 2)); - - mTextSize[HOURS] = mCircleRadius[HOURS] * mTextSizeMultiplier[HOURS]; - mTextSize[MINUTES] = mCircleRadius[MINUTES] * mTextSizeMultiplier[MINUTES]; - - if (mIs24HourMode) { - mInnerTextSize = mCircleRadius[HOURS] * mTextSizeMultiplier[HOURS_INNER]; - } - - calculateGridSizesHours(); - calculateGridSizesMinutes(); - - mSelectionRadius[HOURS] = (int) (mCircleRadius[HOURS] * mSelectionRadiusMultiplier); - mSelectionRadius[HOURS_INNER] = mSelectionRadius[HOURS]; - mSelectionRadius[MINUTES] = (int) (mCircleRadius[MINUTES] * mSelectionRadiusMultiplier); + calculatePositionsHours(); + calculatePositionsMinutes(); mTouchHelper.invalidateRoot(); } @Override public void onDraw(Canvas canvas) { - if (!mInputEnabled) { - canvas.saveLayerAlpha(0, 0, getWidth(), getHeight(), mDisabledAlpha); - } else { - canvas.save(); - } - - calculateGridSizesHours(); - calculateGridSizesMinutes(); + final float alphaMod = mInputEnabled ? 1 : mDisabledAlpha; drawCircleBackground(canvas); - drawSelector(canvas); - - drawTextElements(canvas, mTextSize[HOURS], mTypeface, mOuterTextHours, - mTextGridWidths[HOURS], mTextGridHeights[HOURS], mPaint[HOURS], - mColor[HOURS], mAlpha[HOURS].getValue()); - - if (mIs24HourMode && mInnerTextHours != null) { - drawTextElements(canvas, mInnerTextSize, mTypeface, mInnerTextHours, - mInnerTextGridWidths, mInnerTextGridHeights, mPaint[HOURS], - mColor[HOURS], mAlpha[HOURS].getValue()); - } - - drawTextElements(canvas, mTextSize[MINUTES], mTypeface, mOuterTextMinutes, - mTextGridWidths[MINUTES], mTextGridHeights[MINUTES], mPaint[MINUTES], - mColor[MINUTES], mAlpha[MINUTES].getValue()); - - drawCenter(canvas); - - if (DEBUG) { - drawDebug(canvas); - } - - canvas.restore(); + drawHours(canvas, alphaMod); + drawMinutes(canvas, alphaMod); + drawCenter(canvas, alphaMod); } private void drawCircleBackground(Canvas canvas) { - canvas.drawCircle(mXCenter, mYCenter, mCircleRadius[HOURS], mPaintBackground); + canvas.drawCircle(mXCenter, mYCenter, mCircleRadius, mPaintBackground); + } + + private void drawHours(Canvas canvas, float alphaMod) { + final int hoursAlpha = (int) (mAlpha[HOURS].getValue() * alphaMod + 0.5f); + if (hoursAlpha > 0) { + // Draw the hour selector under the elements. + drawSelector(canvas, mIsOnInnerCircle ? HOURS_INNER : HOURS, null, alphaMod); + + // Draw outer hours. + drawTextElements(canvas, mTextSize[HOURS], mTypeface, mTextColor[HOURS], + mOuterTextHours, mOuterTextX[HOURS], mOuterTextY[HOURS], mPaint[HOURS], + hoursAlpha, !mIsOnInnerCircle, mSelectionDegrees[HOURS], false); + + // Draw inner hours (12-23) for 24-hour time. + if (mIs24HourMode && mInnerTextHours != null) { + drawTextElements(canvas, mTextSize[HOURS_INNER], mTypeface, mTextColor[HOURS_INNER], + mInnerTextHours, mInnerTextX, mInnerTextY, mPaint[HOURS], hoursAlpha, + mIsOnInnerCircle, mSelectionDegrees[HOURS], false); + } + } } - private void drawCenter(Canvas canvas) { - canvas.drawCircle(mXCenter, mYCenter, CENTER_RADIUS, mPaintCenter); + private void drawMinutes(Canvas canvas, float alphaMod) { + final int minutesAlpha = (int) (mAlpha[MINUTES].getValue() * alphaMod + 0.5f); + if (minutesAlpha > 0) { + drawSelector(canvas, MINUTES, mSelectorPath, alphaMod); + + // Exclude the selector region, then draw minutes with no + // activated states. + canvas.save(Canvas.CLIP_SAVE_FLAG); + canvas.clipPath(mSelectorPath, Region.Op.DIFFERENCE); + drawTextElements(canvas, mTextSize[MINUTES], mTypeface, mTextColor[MINUTES], + mOuterTextMinutes, mOuterTextX[MINUTES], mOuterTextY[MINUTES], mPaint[MINUTES], + minutesAlpha, false, 0, false); + canvas.restore(); + + // Intersect the selector region, then draw minutes with only + // activated states. + canvas.save(Canvas.CLIP_SAVE_FLAG); + canvas.clipPath(mSelectorPath, Region.Op.INTERSECT); + drawTextElements(canvas, mTextSize[MINUTES], mTypeface, mTextColor[MINUTES], + mOuterTextMinutes, mOuterTextX[MINUTES], mOuterTextY[MINUTES], mPaint[MINUTES], + minutesAlpha, true, mSelectionDegrees[MINUTES], true); + canvas.restore(); + } } - private void drawSelector(Canvas canvas) { - drawSelector(canvas, mIsOnInnerCircle ? HOURS_INNER : HOURS); - drawSelector(canvas, MINUTES); + private void drawCenter(Canvas canvas, float alphaMod) { + mPaintCenter.setAlpha((int) (255 * alphaMod + 0.5f)); + canvas.drawCircle(mXCenter, mYCenter, mCenterDotRadius, mPaintCenter); } private int getMultipliedAlpha(int argb, int alpha) { return (int) (Color.alpha(argb) * (alpha / 255.0) + 0.5); } - private void drawSelector(Canvas canvas, int index) { + private final Path mSelectorPath = new Path(); + + private void drawSelector(Canvas canvas, int index, Path selectorPath, float alphaMod) { // Calculate the current radius at which to place the selection circle. - mLineLength[index] = (int) (mCircleRadius[index] - * mNumbersRadiusMultiplier[index] * mAnimationRadiusMultiplier[index]); + mLineLength[index] = mCircleRadius - mTextInset[index]; - double selectionRadians = Math.toRadians(mSelectionDegrees[index]); + final double selectionRadians = Math.toRadians(mSelectionDegrees[index]); - int pointX = mXCenter + (int) (mLineLength[index] * Math.sin(selectionRadians)); - int pointY = mYCenter - (int) (mLineLength[index] * Math.cos(selectionRadians)); + float pointX = mXCenter + (int) (mLineLength[index] * Math.sin(selectionRadians)); + float pointY = mYCenter - (int) (mLineLength[index] * Math.cos(selectionRadians)); int color; int alpha; @@ -806,267 +735,146 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { // Draw the selection circle color = mColorSelector[index % 2][SELECTOR_CIRCLE]; - alpha = mAlphaSelector[index % 2][SELECTOR_CIRCLE].getValue(); + alpha = (int) (mAlphaSelector[index % 2][SELECTOR_CIRCLE].getValue() * alphaMod + 0.5f); paint = mPaintSelector[index % 2][SELECTOR_CIRCLE]; paint.setColor(color); paint.setAlpha(getMultipliedAlpha(color, alpha)); - canvas.drawCircle(pointX, pointY, mSelectionRadius[index], paint); + canvas.drawCircle(pointX, pointY, mSelectorRadius, paint); - // Draw the dot if needed - if (mSelectionDegrees[index] % 30 != 0) { + // If needed, set up the clip path for later. + if (selectorPath != null) { + mSelectorPath.reset(); + mSelectorPath.addCircle(pointX, pointY, mSelectorRadius, Path.Direction.CCW); + } + + // Draw the dot if needed. + final boolean shouldDrawDot = mSelectionDegrees[index] % 30 != 0; + if (shouldDrawDot) { // We're not on a direct tick color = mColorSelector[index % 2][SELECTOR_DOT]; - alpha = mAlphaSelector[index % 2][SELECTOR_DOT].getValue(); + alpha = (int) (mAlphaSelector[index % 2][SELECTOR_DOT].getValue() * alphaMod + 0.5f); paint = mPaintSelector[index % 2][SELECTOR_DOT]; paint.setColor(color); paint.setAlpha(getMultipliedAlpha(color, alpha)); - canvas.drawCircle(pointX, pointY, (mSelectionRadius[index] * 2 / 7), paint); - } else { - // We're not drawing the dot, so shorten the line to only go as far as the edge of the - // selection circle - int lineLength = mLineLength[index] - mSelectionRadius[index]; - pointX = mXCenter + (int) (lineLength * Math.sin(selectionRadians)); - pointY = mYCenter - (int) (lineLength * Math.cos(selectionRadians)); + canvas.drawCircle(pointX, pointY, mSelectorDotRadius, paint); } + // Shorten the line to only go from the edge of the center dot to the + // edge of the selection circle. + final double sin = Math.sin(selectionRadians); + final double cos = Math.cos(selectionRadians); + final int lineLength = mLineLength[index] - mSelectorRadius; + final int centerX = mXCenter + (int) (mCenterDotRadius * sin); + final int centerY = mYCenter - (int) (mCenterDotRadius * cos); + pointX = centerX + (int) (lineLength * sin); + pointY = centerY - (int) (lineLength * cos); + // Draw the line color = mColorSelector[index % 2][SELECTOR_LINE]; - alpha = mAlphaSelector[index % 2][SELECTOR_LINE].getValue(); + alpha = (int) (mAlphaSelector[index % 2][SELECTOR_LINE].getValue() * alphaMod + 0.5f); paint = mPaintSelector[index % 2][SELECTOR_LINE]; paint.setColor(color); + paint.setStrokeWidth(mSelectorStroke); paint.setAlpha(getMultipliedAlpha(color, alpha)); canvas.drawLine(mXCenter, mYCenter, pointX, pointY, paint); } - private void drawDebug(Canvas canvas) { - // Draw outer numbers circle - final float outerRadius = mCircleRadius[HOURS] * mNumbersRadiusMultiplier[HOURS]; - canvas.drawCircle(mXCenter, mYCenter, outerRadius, mPaintDebug); - - // Draw inner numbers circle - final float innerRadius = mCircleRadius[HOURS] * mNumbersRadiusMultiplier[HOURS_INNER]; - canvas.drawCircle(mXCenter, mYCenter, innerRadius, mPaintDebug); - - // Draw outer background circle - canvas.drawCircle(mXCenter, mYCenter, mCircleRadius[HOURS], mPaintDebug); - - // Draw outer rectangle for circles - float left = mXCenter - outerRadius; - float top = mYCenter - outerRadius; - float right = mXCenter + outerRadius; - float bottom = mYCenter + outerRadius; - 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]; - canvas.drawRect(left, top, right, bottom, mPaintDebug); - - // Draw outer view rectangle - canvas.drawRect(0, 0, getWidth(), getHeight(), mPaintDebug); - - // Draw selected time - final String selected = String.format("%02d:%02d", getCurrentHour(), getCurrentMinute()); - - ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT); - TextView tv = new TextView(getContext()); - tv.setLayoutParams(lp); - tv.setText(selected); - tv.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); - Paint paint = tv.getPaint(); - paint.setColor(DEBUG_TEXT_COLOR); - - final int width = tv.getMeasuredWidth(); - - float height = paint.descent() - paint.ascent(); - float x = mXCenter - width / 2; - float y = mYCenter + 1.5f * height; - - canvas.drawText(selected, x, y, paint); - } - - private void calculateGridSizesHours() { + private void calculatePositionsHours() { // Calculate the text positions - float numbersRadius = mCircleRadius[HOURS] - * mNumbersRadiusMultiplier[HOURS] * mAnimationRadiusMultiplier[HOURS]; + final float numbersRadius = mCircleRadius - mTextInset[HOURS]; // Calculate the positions for the 12 numbers in the main circle. - calculateGridSizes(mPaint[HOURS], numbersRadius, mXCenter, mYCenter, - mTextSize[HOURS], mTextGridHeights[HOURS], mTextGridWidths[HOURS]); + calculatePositions(mPaint[HOURS], numbersRadius, mXCenter, mYCenter, + mTextSize[HOURS], mOuterTextX[HOURS], mOuterTextY[HOURS]); // If we have an inner circle, calculate those positions too. if (mIs24HourMode) { - float innerNumbersRadius = mCircleRadius[HOURS_INNER] - * mNumbersRadiusMultiplier[HOURS_INNER] - * mAnimationRadiusMultiplier[HOURS_INNER]; - - calculateGridSizes(mPaint[HOURS], innerNumbersRadius, mXCenter, mYCenter, - mInnerTextSize, mInnerTextGridHeights, mInnerTextGridWidths); + final int innerNumbersRadius = mCircleRadius - mTextInset[HOURS_INNER]; + calculatePositions(mPaint[HOURS], innerNumbersRadius, mXCenter, mYCenter, + mTextSize[HOURS_INNER], mInnerTextX, mInnerTextY); } } - private void calculateGridSizesMinutes() { + private void calculatePositionsMinutes() { // Calculate the text positions - float numbersRadius = mCircleRadius[MINUTES] - * mNumbersRadiusMultiplier[MINUTES] * mAnimationRadiusMultiplier[MINUTES]; + final float numbersRadius = mCircleRadius - mTextInset[MINUTES]; // Calculate the positions for the 12 numbers in the main circle. - calculateGridSizes(mPaint[MINUTES], numbersRadius, mXCenter, mYCenter, - mTextSize[MINUTES], mTextGridHeights[MINUTES], mTextGridWidths[MINUTES]); + calculatePositions(mPaint[MINUTES], numbersRadius, mXCenter, mYCenter, + mTextSize[MINUTES], mOuterTextX[MINUTES], mOuterTextY[MINUTES]); } - /** * Using the trigonometric Unit Circle, calculate the positions that the text will need to be * drawn at based on the specified circle radius. Place the values in the textGridHeights and * textGridWidths parameters. */ - private static void calculateGridSizes(Paint paint, float numbersRadius, float xCenter, - float yCenter, float textSize, float[] textGridHeights, float[] textGridWidths) { - /* - * The numbers need to be drawn in a 7x7 grid, representing the points on the Unit Circle. - */ - final float offset1 = numbersRadius; - // cos(30) = a / r => r * cos(30) - final float offset2 = numbersRadius * COSINE_30_DEGREES; - // sin(30) = o / r => r * sin(30) - final float offset3 = numbersRadius * SINE_30_DEGREES; - + private static void calculatePositions(Paint paint, float radius, float xCenter, float yCenter, + float textSize, float[] x, float[] y) { + // Adjust yCenter to account for the text's baseline. paint.setTextSize(textSize); - // We'll need yTextBase to be slightly lower to account for the text's baseline. yCenter -= (paint.descent() + paint.ascent()) / 2; - textGridHeights[0] = yCenter - offset1; - textGridWidths[0] = xCenter - offset1; - textGridHeights[1] = yCenter - offset2; - textGridWidths[1] = xCenter - offset2; - textGridHeights[2] = yCenter - offset3; - textGridWidths[2] = xCenter - offset3; - textGridHeights[3] = yCenter; - textGridWidths[3] = xCenter; - textGridHeights[4] = yCenter + offset3; - textGridWidths[4] = xCenter + offset3; - textGridHeights[5] = yCenter + offset2; - textGridWidths[5] = xCenter + offset2; - textGridHeights[6] = yCenter + offset1; - textGridWidths[6] = xCenter + offset1; + for (int i = 0; i < NUM_POSITIONS; i++) { + x[i] = xCenter - radius * COS_30[i]; + y[i] = yCenter - radius * SIN_30[i]; + } } /** * Draw the 12 text values at the positions specified by the textGrid parameters. */ - private void drawTextElements(Canvas canvas, float textSize, Typeface typeface, String[] texts, - float[] textGridWidths, float[] textGridHeights, Paint paint, int color, int alpha) { + private void drawTextElements(Canvas canvas, float textSize, Typeface typeface, + ColorStateList textColor, String[] texts, float[] textX, float[] textY, Paint paint, + int alpha, boolean showActivated, int activatedDegrees, boolean activatedOnly) { paint.setTextSize(textSize); paint.setTypeface(typeface); - paint.setColor(color); - paint.setAlpha(getMultipliedAlpha(color, alpha)); - canvas.drawText(texts[0], textGridWidths[3], textGridHeights[0], paint); - canvas.drawText(texts[1], textGridWidths[4], textGridHeights[1], paint); - canvas.drawText(texts[2], textGridWidths[5], textGridHeights[2], paint); - canvas.drawText(texts[3], textGridWidths[6], textGridHeights[3], paint); - canvas.drawText(texts[4], textGridWidths[5], textGridHeights[4], paint); - canvas.drawText(texts[5], textGridWidths[4], textGridHeights[5], paint); - canvas.drawText(texts[6], textGridWidths[3], textGridHeights[6], paint); - canvas.drawText(texts[7], textGridWidths[2], textGridHeights[5], paint); - canvas.drawText(texts[8], textGridWidths[1], textGridHeights[4], paint); - canvas.drawText(texts[9], textGridWidths[0], textGridHeights[3], paint); - canvas.drawText(texts[10], textGridWidths[1], textGridHeights[2], paint); - canvas.drawText(texts[11], textGridWidths[2], textGridHeights[1], paint); - } - // Used for animating the hours by changing their radius - @SuppressWarnings("unused") - private void setAnimationRadiusMultiplierHours(float animationRadiusMultiplier) { - mAnimationRadiusMultiplier[HOURS] = animationRadiusMultiplier; - mAnimationRadiusMultiplier[HOURS_INNER] = animationRadiusMultiplier; - } + // The activated index can touch a range of elements. + final float activatedIndex = activatedDegrees / (360.0f / NUM_POSITIONS); + final int activatedFloor = (int) activatedIndex; + final int activatedCeil = ((int) Math.ceil(activatedIndex)) % NUM_POSITIONS; - // Used for animating the minutes by changing their radius - @SuppressWarnings("unused") - private void setAnimationRadiusMultiplierMinutes(float animationRadiusMultiplier) { - mAnimationRadiusMultiplier[MINUTES] = animationRadiusMultiplier; - } + for (int i = 0; i < 12; i++) { + final boolean activated = (activatedFloor == i || activatedCeil == i); + if (activatedOnly && !activated) { + continue; + } - private static ObjectAnimator getRadiusDisappearAnimator(Object target, - String radiusPropertyName, InvalidateUpdateListener updateListener, - float midRadiusMultiplier, float endRadiusMultiplier) { - Keyframe kf0, kf1, kf2; - float midwayPoint = 0.2f; - int duration = 500; - - kf0 = Keyframe.ofFloat(0f, 1); - kf1 = Keyframe.ofFloat(midwayPoint, midRadiusMultiplier); - kf2 = Keyframe.ofFloat(1f, endRadiusMultiplier); - PropertyValuesHolder radiusDisappear = PropertyValuesHolder.ofKeyframe( - radiusPropertyName, kf0, kf1, kf2); - - ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder( - target, radiusDisappear).setDuration(duration); - animator.addUpdateListener(updateListener); - return animator; - } + final int stateMask = StateSet.VIEW_STATE_ENABLED + | (showActivated && activated ? StateSet.VIEW_STATE_ACTIVATED : 0); + final int color = textColor.getColorForState(StateSet.get(stateMask), 0); + paint.setColor(color); + paint.setAlpha(getMultipliedAlpha(color, alpha)); - private static ObjectAnimator getRadiusReappearAnimator(Object target, - String radiusPropertyName, InvalidateUpdateListener updateListener, - float midRadiusMultiplier, float endRadiusMultiplier) { - Keyframe kf0, kf1, kf2, kf3; - float midwayPoint = 0.2f; - int duration = 500; - - // Set up animator for reappearing. - float delayMultiplier = 0.25f; - float transitionDurationMultiplier = 1f; - float totalDurationMultiplier = transitionDurationMultiplier + delayMultiplier; - int totalDuration = (int) (duration * totalDurationMultiplier); - float delayPoint = (delayMultiplier * duration) / totalDuration; - midwayPoint = 1 - (midwayPoint * (1 - delayPoint)); - - kf0 = Keyframe.ofFloat(0f, endRadiusMultiplier); - kf1 = Keyframe.ofFloat(delayPoint, endRadiusMultiplier); - kf2 = Keyframe.ofFloat(midwayPoint, midRadiusMultiplier); - kf3 = Keyframe.ofFloat(1f, 1); - PropertyValuesHolder radiusReappear = PropertyValuesHolder.ofKeyframe( - radiusPropertyName, kf0, kf1, kf2, kf3); - - ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder( - target, radiusReappear).setDuration(totalDuration); - animator.addUpdateListener(updateListener); - return animator; + canvas.drawText(texts[i], textX[i], textY[i], paint); + } } private static ObjectAnimator getFadeOutAnimator(IntHolder target, int startAlpha, int endAlpha, InvalidateUpdateListener updateListener) { - int duration = 500; - ObjectAnimator animator = ObjectAnimator.ofInt(target, "value", startAlpha, endAlpha); - animator.setDuration(duration); + final ObjectAnimator animator = ObjectAnimator.ofInt(target, "value", startAlpha, endAlpha); + animator.setDuration(FADE_OUT_DURATION); animator.addUpdateListener(updateListener); - return animator; } private static ObjectAnimator getFadeInAnimator(IntHolder target, int startAlpha, int endAlpha, InvalidateUpdateListener updateListener) { - Keyframe kf0, kf1, kf2; - int duration = 500; - - // Set up animator for reappearing. - float delayMultiplier = 0.25f; - float transitionDurationMultiplier = 1f; - float totalDurationMultiplier = transitionDurationMultiplier + delayMultiplier; - int totalDuration = (int) (duration * totalDurationMultiplier); - float delayPoint = (delayMultiplier * duration) / totalDuration; + final float delayMultiplier = 0.25f; + final float transitionDurationMultiplier = 1f; + final float totalDurationMultiplier = transitionDurationMultiplier + delayMultiplier; + final int totalDuration = (int) (FADE_IN_DURATION * totalDurationMultiplier); + final float delayPoint = (delayMultiplier * FADE_IN_DURATION) / totalDuration; + final Keyframe kf0, kf1, kf2; kf0 = Keyframe.ofInt(0f, startAlpha); kf1 = Keyframe.ofInt(delayPoint, startAlpha); kf2 = Keyframe.ofInt(1f, endAlpha); - PropertyValuesHolder fadeIn = PropertyValuesHolder.ofKeyframe("value", kf0, kf1, kf2); + final PropertyValuesHolder fadeIn = PropertyValuesHolder.ofKeyframe("value", kf0, kf1, kf2); - ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder( - target, fadeIn).setDuration(totalDuration); + final ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(target, fadeIn); + animator.setDuration(totalDuration); animator.addUpdateListener(updateListener); return animator; } @@ -1080,29 +888,23 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { private void startHoursToMinutesAnimation() { if (mHoursToMinutesAnims.size() == 0) { - mHoursToMinutesAnims.add(getRadiusDisappearAnimator(this, - "animationRadiusMultiplierHours", mInvalidateUpdateListener, - mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier)); mHoursToMinutesAnims.add(getFadeOutAnimator(mAlpha[HOURS], ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); mHoursToMinutesAnims.add(getFadeOutAnimator(mAlphaSelector[HOURS][SELECTOR_CIRCLE], - ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); + ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); mHoursToMinutesAnims.add(getFadeOutAnimator(mAlphaSelector[HOURS][SELECTOR_DOT], ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); mHoursToMinutesAnims.add(getFadeOutAnimator(mAlphaSelector[HOURS][SELECTOR_LINE], - ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); + ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); - mHoursToMinutesAnims.add(getRadiusReappearAnimator(this, - "animationRadiusMultiplierMinutes", mInvalidateUpdateListener, - mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier)); mHoursToMinutesAnims.add(getFadeInAnimator(mAlpha[MINUTES], ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); mHoursToMinutesAnims.add(getFadeInAnimator(mAlphaSelector[MINUTES][SELECTOR_CIRCLE], - ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener)); + ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); mHoursToMinutesAnims.add(getFadeInAnimator(mAlphaSelector[MINUTES][SELECTOR_DOT], ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); mHoursToMinutesAnims.add(getFadeInAnimator(mAlphaSelector[MINUTES][SELECTOR_LINE], - ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener)); + ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); } if (mTransition != null && mTransition.isRunning()) { @@ -1115,29 +917,23 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { private void startMinutesToHoursAnimation() { if (mMinuteToHoursAnims.size() == 0) { - mMinuteToHoursAnims.add(getRadiusDisappearAnimator(this, - "animationRadiusMultiplierMinutes", mInvalidateUpdateListener, - mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier)); mMinuteToHoursAnims.add(getFadeOutAnimator(mAlpha[MINUTES], ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); mMinuteToHoursAnims.add(getFadeOutAnimator(mAlphaSelector[MINUTES][SELECTOR_CIRCLE], - ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); + ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); mMinuteToHoursAnims.add(getFadeOutAnimator(mAlphaSelector[MINUTES][SELECTOR_DOT], ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); mMinuteToHoursAnims.add(getFadeOutAnimator(mAlphaSelector[MINUTES][SELECTOR_LINE], - ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); + ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); - mMinuteToHoursAnims.add(getRadiusReappearAnimator(this, - "animationRadiusMultiplierHours", mInvalidateUpdateListener, - mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier)); mMinuteToHoursAnims.add(getFadeInAnimator(mAlpha[HOURS], ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); mMinuteToHoursAnims.add(getFadeInAnimator(mAlphaSelector[HOURS][SELECTOR_CIRCLE], - ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener)); + ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); mMinuteToHoursAnims.add(getFadeInAnimator(mAlphaSelector[HOURS][SELECTOR_DOT], ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); mMinuteToHoursAnims.add(getFadeInAnimator(mAlphaSelector[HOURS][SELECTOR_LINE], - ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener)); + ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); } if (mTransition != null && mTransition.isRunning()) { @@ -1148,20 +944,21 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { mTransition.start(); } - private int getDegreesFromXY(float x, float y) { + private int getDegreesFromXY(float x, float y, boolean constrainOutside) { final double hypotenuse = Math.sqrt( (y - mYCenter) * (y - mYCenter) + (x - mXCenter) * (x - mXCenter)); // Basic check if we're outside the range of the disk - if (hypotenuse > mCircleRadius[HOURS]) { + if (constrainOutside && hypotenuse > mCircleRadius) { return -1; } + // Check if (mIs24HourMode && mShowHours) { if (hypotenuse >= mMinHypotenuseForInnerNumber && hypotenuse <= mHalfwayHypotenusePoint) { mIsOnInnerCircle = true; - } else if (hypotenuse <= mMaxHypotenuseForOuterNumber + } else if ((hypotenuse <= mMaxHypotenuseForOuterNumber || !constrainOutside) && hypotenuse >= mHalfwayHypotenusePoint) { mIsOnInnerCircle = false; } else { @@ -1169,11 +966,11 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { } } else { final int index = (mShowHours) ? HOURS : MINUTES; - final float length = (mCircleRadius[index] * mNumbersRadiusMultiplier[index]); - final int distanceToNumber = (int) Math.abs(hypotenuse - length); - final int maxAllowedDistance = - (int) (mCircleRadius[index] * (1 - mNumbersRadiusMultiplier[index])); - if (distanceToNumber > maxAllowedDistance) { + final float length = (mCircleRadius - mTextInset[index]); + final int distanceToNumber = (int) (hypotenuse - length); + final int maxAllowedDistance = mTextInset[index]; + if (distanceToNumber < -maxAllowedDistance + || (constrainOutside && distanceToNumber > maxAllowedDistance)) { return -1; } } @@ -1203,7 +1000,7 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { boolean mChangedDuringTouch = false; @Override - public boolean onTouch(View v, MotionEvent event) { + public boolean onTouchEvent(MotionEvent event) { if (!mInputEnabled) { return true; } @@ -1240,7 +1037,7 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { // Calling getDegreesFromXY has side effects, so cache // whether we used to be on the inner circle. final boolean wasOnInnerCircle = mIsOnInnerCircle; - final int degrees = getDegreesFromXY(x, y); + final int degrees = getDegreesFromXY(x, y, false); if (degrees == -1) { return false; } @@ -1385,7 +1182,7 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { // Calling getDegreesXY() has side-effects, so we need to cache the // current inner circle value and restore after the call. final boolean wasOnInnerCircle = mIsOnInnerCircle; - final int degrees = getDegreesFromXY(x, y); + final int degrees = getDegreesFromXY(x, y, true); final boolean isOnInnerCircle = mIsOnInnerCircle; mIsOnInnerCircle = wasOnInnerCircle; @@ -1541,18 +1338,18 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { 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]; + centerRadius = mCircleRadius - mTextInset[HOURS_INNER]; + radius = mSelectorRadius; } else { - centerRadius = mCircleRadius[HOURS] * mNumbersRadiusMultiplier[HOURS]; - radius = mSelectionRadius[HOURS]; + centerRadius = mCircleRadius - mTextInset[HOURS]; + radius = mSelectorRadius; } degrees = getDegreesForHour(value); } else if (type == TYPE_MINUTE) { - centerRadius = mCircleRadius[MINUTES] * mNumbersRadiusMultiplier[MINUTES]; + centerRadius = mCircleRadius - mTextInset[MINUTES]; degrees = getDegreesForMinute(value); - radius = mSelectionRadius[MINUTES]; + radius = mSelectorRadius; } else { // This should never happen. centerRadius = 0; diff --git a/core/java/android/widget/RadioButton.java b/core/java/android/widget/RadioButton.java index afc4830..d44fbd7 100644 --- a/core/java/android/widget/RadioButton.java +++ b/core/java/android/widget/RadioButton.java @@ -18,8 +18,6 @@ package android.widget; import android.content.Context; import android.util.AttributeSet; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; /** @@ -80,14 +78,7 @@ public class RadioButton extends CompoundButton { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(RadioButton.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(RadioButton.class.getName()); + public CharSequence getAccessibilityClassName() { + return RadioButton.class.getName(); } } diff --git a/core/java/android/widget/RadioGroup.java b/core/java/android/widget/RadioGroup.java index 78d05b0..065feb8 100644 --- a/core/java/android/widget/RadioGroup.java +++ b/core/java/android/widget/RadioGroup.java @@ -18,13 +18,12 @@ package android.widget; import com.android.internal.R; +import android.annotation.IdRes; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; /** @@ -151,7 +150,7 @@ public class RadioGroup extends LinearLayout { * @see #getCheckedRadioButtonId() * @see #clearCheck() */ - public void check(int id) { + public void check(@IdRes int id) { // don't even bother if (id != -1 && (id == mCheckedId)) { return; @@ -168,7 +167,7 @@ public class RadioGroup extends LinearLayout { setCheckedId(id); } - private void setCheckedId(int id) { + private void setCheckedId(@IdRes int id) { mCheckedId = id; if (mOnCheckedChangeListener != null) { mOnCheckedChangeListener.onCheckedChanged(this, mCheckedId); @@ -193,6 +192,7 @@ public class RadioGroup extends LinearLayout { * * @attr ref android.R.styleable#RadioGroup_checkedButton */ + @IdRes public int getCheckedRadioButtonId() { return mCheckedId; } @@ -241,15 +241,8 @@ public class RadioGroup extends LinearLayout { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(RadioGroup.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(RadioGroup.class.getName()); + public CharSequence getAccessibilityClassName() { + return RadioGroup.class.getName(); } /** @@ -338,7 +331,7 @@ public class RadioGroup extends LinearLayout { * @param group the group in which the checked radio button has changed * @param checkedId the unique identifier of the newly checked radio button */ - public void onCheckedChanged(RadioGroup group, int checkedId); + public void onCheckedChanged(RadioGroup group, @IdRes int checkedId); } private class CheckedStateTracker implements CompoundButton.OnCheckedChangeListener { diff --git a/core/java/android/widget/RatingBar.java b/core/java/android/widget/RatingBar.java index 82b490e..b538334 100644 --- a/core/java/android/widget/RatingBar.java +++ b/core/java/android/widget/RatingBar.java @@ -21,9 +21,6 @@ import android.content.res.TypedArray; import android.graphics.drawable.shapes.RectShape; import android.graphics.drawable.shapes.Shape; import android.util.AttributeSet; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; - import com.android.internal.R; /** @@ -43,7 +40,7 @@ import com.android.internal.R; * <p> * The secondary progress should not be modified by the client as it is used * internally as the background for a fractionally filled star. - * + * * @attr ref android.R.styleable#RatingBar_numStars * @attr ref android.R.styleable#RatingBar_rating * @attr ref android.R.styleable#RatingBar_stepSize @@ -58,14 +55,14 @@ public class RatingBar extends AbsSeekBar { * programmatically. */ public interface OnRatingBarChangeListener { - + /** * Notification that the rating has changed. Clients can use the * fromUser parameter to distinguish user-initiated changes from those * that occurred programmatically. This will not be called continuously * while the user is dragging, only when the user finalizes a rating by * lifting the touch. - * + * * @param ratingBar The RatingBar whose rating has changed. * @param rating The current rating. This will be in the range * 0..numStars. @@ -79,9 +76,9 @@ public class RatingBar extends AbsSeekBar { private int mNumStars = 5; private int mProgressOnStartTracking; - + private OnRatingBarChangeListener mOnRatingBarChangeListener; - + public RatingBar(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } @@ -98,19 +95,19 @@ public class RatingBar extends AbsSeekBar { a.recycle(); if (numStars > 0 && numStars != mNumStars) { - setNumStars(numStars); + setNumStars(numStars); } - + if (stepSize >= 0) { setStepSize(stepSize); } else { setStepSize(0.5f); } - + if (rating >= 0) { setRating(rating); } - + // A touch inside a star fill up to that fractional area (slightly more // than 1 so boundaries round up). mTouchProgressOffset = 1.1f; @@ -123,16 +120,16 @@ public class RatingBar extends AbsSeekBar { public RatingBar(Context context) { this(context, null); } - + /** * Sets the listener to be called when the rating changes. - * + * * @param listener The listener. */ public void setOnRatingBarChangeListener(OnRatingBarChangeListener listener) { mOnRatingBarChangeListener = listener; } - + /** * @return The listener (may be null) that is listening for rating change * events. @@ -144,7 +141,7 @@ public class RatingBar extends AbsSeekBar { /** * Whether this rating bar should only be an indicator (thus non-changeable * by the user). - * + * * @param isIndicator Whether it should be an indicator. * * @attr ref android.R.styleable#RatingBar_isIndicator @@ -153,7 +150,7 @@ public class RatingBar extends AbsSeekBar { mIsUserSeekable = !isIndicator; setFocusable(!isIndicator); } - + /** * @return Whether this rating bar is only an indicator. * @@ -162,21 +159,21 @@ public class RatingBar extends AbsSeekBar { public boolean isIndicator() { return !mIsUserSeekable; } - + /** * Sets the number of stars to show. In order for these to be shown * properly, it is recommended the layout width of this widget be wrap * content. - * + * * @param numStars The number of stars. */ public void setNumStars(final int numStars) { if (numStars <= 0) { return; } - + mNumStars = numStars; - + // This causes the width to change, so re-layout requestLayout(); } @@ -188,10 +185,10 @@ public class RatingBar extends AbsSeekBar { public int getNumStars() { return mNumStars; } - + /** * Sets the rating (the number of stars filled). - * + * * @param rating The rating to set. */ public void setRating(float rating) { @@ -200,16 +197,16 @@ public class RatingBar extends AbsSeekBar { /** * Gets the current rating (number of stars filled). - * + * * @return The current rating. */ public float getRating() { - return getProgress() / getProgressPerStar(); + return getProgress() / getProgressPerStar(); } /** * Sets the step size (granularity) of this rating bar. - * + * * @param stepSize The step size of this rating bar. For example, if * half-star granularity is wanted, this would be 0.5. */ @@ -217,7 +214,7 @@ public class RatingBar extends AbsSeekBar { if (stepSize <= 0) { return; } - + final float newMax = mNumStars / stepSize; final int newProgress = (int) (newMax / getMax() * getProgress()); setMax((int) newMax); @@ -226,13 +223,13 @@ public class RatingBar extends AbsSeekBar { /** * Gets the step size of this rating bar. - * + * * @return The step size. */ public float getStepSize() { return (float) getNumStars() / getMax(); } - + /** * @return The amount of progress that fits into a star */ @@ -251,12 +248,12 @@ public class RatingBar extends AbsSeekBar { } @Override - void onProgressRefresh(float scale, boolean fromUser) { - super.onProgressRefresh(scale, fromUser); + void onProgressRefresh(float scale, boolean fromUser, int progress) { + super.onProgressRefresh(scale, fromUser, progress); // Keep secondary progress in sync with primary - updateSecondaryProgress(getProgress()); - + updateSecondaryProgress(progress); + if (!fromUser) { // Callback for non-user rating changes dispatchRatingChange(false); @@ -267,7 +264,7 @@ public class RatingBar extends AbsSeekBar { * The secondary progress is used to differentiate the background of a * partially filled star. This method keeps the secondary progress in sync * with the progress. - * + * * @param progress The primary progress level. */ private void updateSecondaryProgress(int progress) { @@ -278,11 +275,11 @@ public class RatingBar extends AbsSeekBar { setSecondaryProgress(secondaryProgress); } } - + @Override protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); - + if (mSampleTile != null) { // TODO: Once ProgressBar's TODOs are gone, this can be done more // cleanly than mSampleTile @@ -295,7 +292,7 @@ public class RatingBar extends AbsSeekBar { @Override void onStartTrackingTouch() { mProgressOnStartTracking = getProgress(); - + super.onStartTrackingTouch(); } @@ -327,19 +324,12 @@ public class RatingBar extends AbsSeekBar { if (max <= 0) { return; } - + super.setMax(max); } - - @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(RatingBar.class.getName()); - } @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(RatingBar.class.getName()); + public CharSequence getAccessibilityClassName() { + return RatingBar.class.getName(); } } diff --git a/core/java/android/widget/RelativeLayout.java b/core/java/android/widget/RelativeLayout.java index 5b604cd..d12739f 100644 --- a/core/java/android/widget/RelativeLayout.java +++ b/core/java/android/widget/RelativeLayout.java @@ -37,7 +37,6 @@ import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; @@ -201,7 +200,6 @@ public class RelativeLayout extends ViewGroup { private static final int VALUE_NOT_SET = Integer.MIN_VALUE; private View mBaselineView = null; - private boolean mHasBaselineAlignedChild; private int mGravity = Gravity.START | Gravity.TOP; private final Rect mContentBounds = new Rect(); @@ -417,8 +415,6 @@ public class RelativeLayout extends ViewGroup { height = myHeight; } - mHasBaselineAlignedChild = false; - View ignore = null; int gravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK; final boolean horizontalGravity = gravity != Gravity.START && gravity != 0; @@ -473,11 +469,11 @@ public class RelativeLayout extends ViewGroup { final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion; for (int i = 0; i < count; i++) { - View child = views[i]; + final View child = views[i]; if (child.getVisibility() != GONE) { - LayoutParams params = (LayoutParams) child.getLayoutParams(); - - applyVerticalSizeRules(params, myHeight); + final LayoutParams params = (LayoutParams) child.getLayoutParams(); + + applyVerticalSizeRules(params, myHeight, child.getBaseline()); measureChild(child, params, myWidth, myHeight); if (positionChildVertical(child, params, myHeight, isWrapContentHeight)) { offsetVerticalAxis = true; @@ -519,25 +515,22 @@ public class RelativeLayout extends ViewGroup { } } - if (mHasBaselineAlignedChild) { - for (int i = 0; i < count; i++) { - View child = getChildAt(i); - if (child.getVisibility() != GONE) { - LayoutParams params = (LayoutParams) child.getLayoutParams(); - alignBaseline(child, params); - - if (child != ignore || verticalGravity) { - left = Math.min(left, params.mLeft - params.leftMargin); - top = Math.min(top, params.mTop - params.topMargin); - } - - if (child != ignore || horizontalGravity) { - right = Math.max(right, params.mRight + params.rightMargin); - bottom = Math.max(bottom, params.mBottom + params.bottomMargin); - } + // Use the top-start-most laid out view as the baseline. RTL offsets are + // applied later, so we can use the left-most edge as the starting edge. + View baselineView = null; + LayoutParams baselineParams = null; + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final LayoutParams childParams = (LayoutParams) child.getLayoutParams(); + if (baselineView == null || baselineParams == null + || compareLayoutPosition(childParams, baselineParams) < 0) { + baselineView = child; + baselineParams = childParams; } } } + mBaselineView = baselineView; if (isWrapContentWidth) { // Width already has left padding in it since it was calculated by looking at @@ -638,39 +631,23 @@ public class RelativeLayout extends ViewGroup { params.mRight -= offsetWidth; } } - } setMeasuredDimension(width, height); } - private void alignBaseline(View child, LayoutParams params) { - final int layoutDirection = getLayoutDirection(); - int[] rules = params.getRules(layoutDirection); - int anchorBaseline = getRelatedViewBaseline(rules, ALIGN_BASELINE); - - if (anchorBaseline != -1) { - LayoutParams anchorParams = getRelatedViewParams(rules, ALIGN_BASELINE); - if (anchorParams != null) { - int offset = anchorParams.mTop + anchorBaseline; - int baseline = child.getBaseline(); - if (baseline != -1) { - offset -= baseline; - } - int height = params.mBottom - params.mTop; - params.mTop = offset; - params.mBottom = params.mTop + height; - } - } - - if (mBaselineView == null) { - mBaselineView = child; - } else { - LayoutParams lp = (LayoutParams) mBaselineView.getLayoutParams(); - if (params.mTop < lp.mTop || (params.mTop == lp.mTop && params.mLeft < lp.mLeft)) { - mBaselineView = child; - } + /** + * @return a negative number if the top of {@code p1} is above the top of + * {@code p2} or if they have identical top values and the left of + * {@code p1} is to the left of {@code p2}, or a positive number + * otherwise + */ + private int compareLayoutPosition(LayoutParams p1, LayoutParams p2) { + final int topDiff = p1.mTop - p2.mTop; + if (topDiff != 0) { + return topDiff; } + return p1.mLeft - p2.mLeft; } /** @@ -950,8 +927,20 @@ public class RelativeLayout extends ViewGroup { } } - private void applyVerticalSizeRules(LayoutParams childParams, int myHeight) { - int[] rules = childParams.getRules(); + private void applyVerticalSizeRules(LayoutParams childParams, int myHeight, int myBaseline) { + final int[] rules = childParams.getRules(); + + // Baseline alignment overrides any explicitly specified top or bottom. + int baselineOffset = getRelatedViewBaselineOffset(rules); + if (baselineOffset != -1) { + if (myBaseline != -1) { + baselineOffset -= myBaseline; + } + childParams.mTop = baselineOffset; + childParams.mBottom = VALUE_NOT_SET; + return; + } + RelativeLayout.LayoutParams anchorParams; childParams.mTop = VALUE_NOT_SET; @@ -1000,10 +989,6 @@ public class RelativeLayout extends ViewGroup { childParams.mBottom = myHeight - mPaddingBottom - childParams.bottomMargin; } } - - if (rules[ALIGN_BASELINE] != 0) { - mHasBaselineAlignedChild = true; - } } private View getRelatedView(int[] rules, int relation) { @@ -1038,10 +1023,17 @@ public class RelativeLayout extends ViewGroup { return null; } - private int getRelatedViewBaseline(int[] rules, int relation) { - View v = getRelatedView(rules, relation); + private int getRelatedViewBaselineOffset(int[] rules) { + final View v = getRelatedView(rules, ALIGN_BASELINE); if (v != null) { - return v.getBaseline(); + final int baseline = v.getBaseline(); + if (baseline != -1) { + final ViewGroup.LayoutParams params = v.getLayoutParams(); + if (params instanceof LayoutParams) { + final LayoutParams anchorParams = (LayoutParams) v.getLayoutParams(); + return anchorParams.mTop + baseline; + } + } } return -1; } @@ -1104,8 +1096,9 @@ public class RelativeLayout extends ViewGroup { return new LayoutParams(p); } + /** @hide */ @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) { if (mTopToBottomLeftToRightSet == null) { mTopToBottomLeftToRightSet = new TreeSet<View>(new TopToBottomLeftToRightComparator()); } @@ -1128,15 +1121,8 @@ public class RelativeLayout extends ViewGroup { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(RelativeLayout.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(RelativeLayout.class.getName()); + public CharSequence getAccessibilityClassName() { + return RelativeLayout.class.getName(); } /** @@ -1387,6 +1373,7 @@ public class RelativeLayout extends ViewGroup { * {@link android.widget.RelativeLayout RelativeLayout}, such as * ALIGN_WITH_PARENT_LEFT. * @see #addRule(int, int) + * @see #getRule(int) */ public void addRule(int verb) { mRules[verb] = TRUE; @@ -1407,6 +1394,7 @@ public class RelativeLayout extends ViewGroup { * for true or 0 for false). For verbs that don't refer to another sibling * (for example, ALIGN_WITH_PARENT_BOTTOM) just use -1. * @see #addRule(int) + * @see #getRule(int) */ public void addRule(int verb, int anchor) { mRules[verb] = anchor; @@ -1422,6 +1410,7 @@ public class RelativeLayout extends ViewGroup { * ALIGN_WITH_PARENT_LEFT. * @see #addRule(int) * @see #addRule(int, int) + * @see #getRule(int) */ public void removeRule(int verb) { mRules[verb] = 0; @@ -1429,6 +1418,22 @@ public class RelativeLayout extends ViewGroup { mRulesChanged = true; } + /** + * Returns the layout rule associated with a specific verb. + * + * @param verb one of the verbs defined by {@link RelativeLayout}, such + * as ALIGN_WITH_PARENT_LEFT + * @return the id of another view to use as an anchor, a boolean value + * (represented as {@link RelativeLayout#TRUE} for true + * or 0 for false), or -1 for verbs that don't refer to another + * sibling (for example, ALIGN_WITH_PARENT_BOTTOM) + * @see #addRule(int) + * @see #addRule(int, int) + */ + public int getRule(int verb) { + return mRules[verb]; + } + private boolean hasRelativeRules() { return (mInitialRules[START_OF] != 0 || mInitialRules[END_OF] != 0 || mInitialRules[ALIGN_START] != 0 || mInitialRules[ALIGN_END] != 0 || diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index dd7fa18..a10be11 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -16,6 +16,7 @@ package android.widget; +import android.annotation.ColorInt; import android.app.ActivityOptions; import android.app.ActivityThread; import android.app.Application; @@ -2263,7 +2264,7 @@ public class RemoteViews implements Parcelable, Filter { * @param color Sets the text color for all the states (normal, selected, * focused) to be this color. */ - public void setTextColor(int viewId, int color) { + public void setTextColor(int viewId, @ColorInt int color) { setInt(viewId, "setTextColor", color); } diff --git a/core/java/android/widget/RemoteViewsAdapter.java b/core/java/android/widget/RemoteViewsAdapter.java index 56bdb9b..349f3f0 100644 --- a/core/java/android/widget/RemoteViewsAdapter.java +++ b/core/java/android/widget/RemoteViewsAdapter.java @@ -26,14 +26,12 @@ import android.Manifest; import android.appwidget.AppWidgetManager; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.RemoteException; -import android.os.UserHandle; import android.util.Log; import android.util.Slog; import android.view.LayoutInflater; diff --git a/core/java/android/widget/ResourceCursorAdapter.java b/core/java/android/widget/ResourceCursorAdapter.java index 7341c2c..100f919 100644 --- a/core/java/android/widget/ResourceCursorAdapter.java +++ b/core/java/android/widget/ResourceCursorAdapter.java @@ -17,7 +17,9 @@ package android.widget; import android.content.Context; +import android.content.res.Resources; import android.database.Cursor; +import android.view.ContextThemeWrapper; import android.view.View; import android.view.ViewGroup; import android.view.LayoutInflater; @@ -31,9 +33,10 @@ public abstract class ResourceCursorAdapter extends CursorAdapter { private int mLayout; private int mDropDownLayout; - + private LayoutInflater mInflater; - + private LayoutInflater mDropDownInflater; + /** * Constructor the enables auto-requery. * @@ -52,8 +55,9 @@ public abstract class ResourceCursorAdapter extends CursorAdapter { super(context, c); mLayout = mDropDownLayout = layout; mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mDropDownInflater = mInflater; } - + /** * Constructor with default behavior as per * {@link CursorAdapter#CursorAdapter(Context, Cursor, boolean)}; it is recommended @@ -74,6 +78,7 @@ public abstract class ResourceCursorAdapter extends CursorAdapter { super(context, c, autoRequery); mLayout = mDropDownLayout = layout; mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mDropDownInflater = mInflater; } /** @@ -91,11 +96,37 @@ public abstract class ResourceCursorAdapter extends CursorAdapter { super(context, c, flags); mLayout = mDropDownLayout = layout; mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mDropDownInflater = mInflater; + } + + /** + * Sets the {@link android.content.res.Resources.Theme} against which drop-down views are + * inflated. + * <p> + * By default, drop-down views are inflated against the theme of the + * {@link Context} passed to the adapter's constructor. + * + * @param theme the theme against which to inflate drop-down views or + * {@code null} to use the theme from the adapter's context + * @see #newDropDownView(Context, Cursor, ViewGroup) + */ + @Override + public void setDropDownViewTheme(Resources.Theme theme) { + super.setDropDownViewTheme(theme); + + if (theme == null) { + mDropDownInflater = null; + } else if (theme == mInflater.getContext().getTheme()) { + mDropDownInflater = mInflater; + } else { + final Context context = new ContextThemeWrapper(mContext, theme); + mDropDownInflater = LayoutInflater.from(context); + } } /** * Inflates view(s) from the specified XML file. - * + * * @see android.widget.CursorAdapter#newView(android.content.Context, * android.database.Cursor, ViewGroup) */ @@ -106,7 +137,7 @@ public abstract class ResourceCursorAdapter extends CursorAdapter { @Override public View newDropDownView(Context context, Cursor cursor, ViewGroup parent) { - return mInflater.inflate(mDropDownLayout, parent, false); + return mDropDownInflater.inflate(mDropDownLayout, parent, false); } /** diff --git a/core/java/android/widget/ScrollBarDrawable.java b/core/java/android/widget/ScrollBarDrawable.java index 8eff1aa..91d6232 100644 --- a/core/java/android/widget/ScrollBarDrawable.java +++ b/core/java/android/widget/ScrollBarDrawable.java @@ -23,63 +23,74 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; /** - * This is only used by View for displaying its scroll bars. It should probably + * This is only used by View for displaying its scroll bars. It should probably * be moved in to the view package since it is used in that lower-level layer. * For now, we'll hide it so it can be cleaned up later. + * * {@hide} */ -public class ScrollBarDrawable extends Drawable { - private static final int[] STATE_ENABLED = new int[] { android.R.attr.state_enabled }; - +public class ScrollBarDrawable extends Drawable implements Drawable.Callback { private Drawable mVerticalTrack; private Drawable mHorizontalTrack; private Drawable mVerticalThumb; private Drawable mHorizontalThumb; + private int mRange; private int mOffset; private int mExtent; + private boolean mVertical; - private boolean mChanged; + private boolean mBoundsChanged; private boolean mRangeChanged; - private final Rect mTempBounds = new Rect(); private boolean mAlwaysDrawHorizontalTrack; private boolean mAlwaysDrawVerticalTrack; private boolean mMutated; - public ScrollBarDrawable() { - } + private int mAlpha = 255; + private boolean mHasSetAlpha; + + private ColorFilter mColorFilter; + private boolean mHasSetColorFilter; /** - * Indicate whether the horizontal scrollbar track should always be drawn regardless of the - * extent. Defaults to false. + * Indicate whether the horizontal scrollbar track should always be drawn + * regardless of the extent. Defaults to false. + * + * @param alwaysDrawTrack Whether the track should always be drawn * - * @param alwaysDrawTrack Set to true if the track should always be drawn + * @see #getAlwaysDrawHorizontalTrack() */ public void setAlwaysDrawHorizontalTrack(boolean alwaysDrawTrack) { mAlwaysDrawHorizontalTrack = alwaysDrawTrack; } /** - * Indicate whether the vertical scrollbar track should always be drawn regardless of the - * extent. Defaults to false. + * Indicate whether the vertical scrollbar track should always be drawn + * regardless of the extent. Defaults to false. + * + * @param alwaysDrawTrack Whether the track should always be drawn * - * @param alwaysDrawTrack Set to true if the track should always be drawn + * @see #getAlwaysDrawVerticalTrack() */ public void setAlwaysDrawVerticalTrack(boolean alwaysDrawTrack) { mAlwaysDrawVerticalTrack = alwaysDrawTrack; } /** - * Indicates whether the vertical scrollbar track should always be drawn regardless of the - * extent. + * @return whether the vertical scrollbar track should always be drawn + * regardless of the extent. + * + * @see #setAlwaysDrawVerticalTrack(boolean) */ public boolean getAlwaysDrawVerticalTrack() { return mAlwaysDrawVerticalTrack; } /** - * Indicates whether the horizontal scrollbar track should always be drawn regardless of the - * extent. + * @return whether the horizontal scrollbar track should always be drawn + * regardless of the extent. + * + * @see #setAlwaysDrawHorizontalTrack(boolean) */ public boolean getAlwaysDrawHorizontalTrack() { return mAlwaysDrawHorizontalTrack; @@ -87,17 +98,18 @@ public class ScrollBarDrawable extends Drawable { public void setParameters(int range, int offset, int extent, boolean vertical) { if (mVertical != vertical) { - mChanged = true; + mVertical = vertical; + + mBoundsChanged = true; } if (mRange != range || mOffset != offset || mExtent != extent) { + mRange = range; + mOffset = offset; + mExtent = extent; + mRangeChanged = true; } - - mRange = range; - mOffset = offset; - mExtent = extent; - mVertical = vertical; } @Override @@ -113,27 +125,29 @@ public class ScrollBarDrawable extends Drawable { drawThumb = false; } - Rect r = getBounds(); + final Rect r = getBounds(); if (canvas.quickReject(r.left, r.top, r.right, r.bottom, Canvas.EdgeType.AA)) { return; } + if (drawTrack) { drawTrack(canvas, r, vertical); } if (drawThumb) { - int size = vertical ? r.height() : r.width(); - int thickness = vertical ? r.width() : r.height(); - int length = Math.round((float) size * extent / range); - int offset = Math.round((float) (size - length) * mOffset / (range - extent)); + final int size = vertical ? r.height() : r.width(); + final int thickness = vertical ? r.width() : r.height(); + final int minLength = thickness * 2; - // avoid the tiny thumb - int minLength = thickness * 2; + // Avoid the tiny thumb. + int length = Math.round((float) size * extent / range); if (length < minLength) { length = minLength; } - // avoid the too-big thumb - if (offset + length > size) { + + // Avoid the too-big thumb. + int offset = Math.round((float) (size - length) * mOffset / (range - extent)); + if (offset > size - length) { offset = size - length; } @@ -144,90 +158,130 @@ public class ScrollBarDrawable extends Drawable { @Override protected void onBoundsChange(Rect bounds) { super.onBoundsChange(bounds); - mChanged = true; + mBoundsChanged = true; + } + + @Override + public boolean isStateful() { + return (mVerticalTrack != null && mVerticalTrack.isStateful()) + || (mVerticalThumb != null && mVerticalThumb.isStateful()) + || (mHorizontalTrack != null && mHorizontalTrack.isStateful()) + || (mHorizontalThumb != null && mHorizontalThumb.isStateful()) + || super.isStateful(); + } + + @Override + protected boolean onStateChange(int[] state) { + boolean changed = super.onStateChange(state); + if (mVerticalTrack != null) { + changed |= mVerticalTrack.setState(state); + } + if (mVerticalThumb != null) { + changed |= mVerticalThumb.setState(state); + } + if (mHorizontalTrack != null) { + changed |= mHorizontalTrack.setState(state); + } + if (mHorizontalThumb != null) { + changed |= mHorizontalThumb.setState(state); + } + return changed; } - protected void drawTrack(Canvas canvas, Rect bounds, boolean vertical) { - Drawable track; + private void drawTrack(Canvas canvas, Rect bounds, boolean vertical) { + final Drawable track; if (vertical) { track = mVerticalTrack; } else { track = mHorizontalTrack; } + if (track != null) { - if (mChanged) { + if (mBoundsChanged) { track.setBounds(bounds); } track.draw(canvas); } } - protected void drawThumb(Canvas canvas, Rect bounds, int offset, int length, boolean vertical) { - final Rect thumbRect = mTempBounds; - final boolean changed = mRangeChanged || mChanged; - if (changed) { - if (vertical) { - thumbRect.set(bounds.left, bounds.top + offset, - bounds.right, bounds.top + offset + length); - } else { - thumbRect.set(bounds.left + offset, bounds.top, - bounds.left + offset + length, bounds.bottom); - } - } - + private void drawThumb(Canvas canvas, Rect bounds, int offset, int length, boolean vertical) { + final boolean changed = mRangeChanged || mBoundsChanged; if (vertical) { if (mVerticalThumb != null) { final Drawable thumb = mVerticalThumb; - if (changed) thumb.setBounds(thumbRect); + if (changed) { + thumb.setBounds(bounds.left, bounds.top + offset, + bounds.right, bounds.top + offset + length); + } + thumb.draw(canvas); } } else { if (mHorizontalThumb != null) { final Drawable thumb = mHorizontalThumb; - if (changed) thumb.setBounds(thumbRect); + if (changed) { + thumb.setBounds(bounds.left + offset, bounds.top, + bounds.left + offset + length, bounds.bottom); + } + thumb.draw(canvas); } } } public void setVerticalThumbDrawable(Drawable thumb) { - if (thumb != null) { - if (mMutated) { - thumb.mutate(); - } - thumb.setState(STATE_ENABLED); - mVerticalThumb = thumb; + if (mVerticalThumb != null) { + mVerticalThumb.setCallback(null); } + + propagateCurrentState(thumb); + mVerticalThumb = thumb; } public void setVerticalTrackDrawable(Drawable track) { - if (track != null) { - if (mMutated) { - track.mutate(); - } - track.setState(STATE_ENABLED); + if (mVerticalTrack != null) { + mVerticalTrack.setCallback(null); } + + propagateCurrentState(track); mVerticalTrack = track; } public void setHorizontalThumbDrawable(Drawable thumb) { - if (thumb != null) { - if (mMutated) { - thumb.mutate(); - } - thumb.setState(STATE_ENABLED); - mHorizontalThumb = thumb; + if (mHorizontalThumb != null) { + mHorizontalThumb.setCallback(null); } + + propagateCurrentState(thumb); + mHorizontalThumb = thumb; } public void setHorizontalTrackDrawable(Drawable track) { - if (track != null) { + if (mHorizontalTrack != null) { + mHorizontalTrack.setCallback(null); + } + + propagateCurrentState(track); + mHorizontalTrack = track; + } + + private void propagateCurrentState(Drawable d) { + if (d != null) { if (mMutated) { - track.mutate(); + d.mutate(); + } + + d.setState(getState()); + d.setCallback(this); + + if (mHasSetAlpha) { + d.setAlpha(mAlpha); + } + + if (mHasSetColorFilter) { + d.setColorFilter(mColorFilter); } - track.setState(STATE_ENABLED); } - mHorizontalTrack = track; } public int getSize(boolean vertical) { @@ -262,6 +316,9 @@ public class ScrollBarDrawable extends Drawable { @Override public void setAlpha(int alpha) { + mAlpha = alpha; + mHasSetAlpha = true; + if (mVerticalTrack != null) { mVerticalTrack.setAlpha(alpha); } @@ -278,32 +335,54 @@ public class ScrollBarDrawable extends Drawable { @Override public int getAlpha() { - // All elements should have same alpha, just return one of them - return mVerticalThumb.getAlpha(); + return mAlpha; } @Override - public void setColorFilter(ColorFilter cf) { + public void setColorFilter(ColorFilter colorFilter) { + mColorFilter = colorFilter; + mHasSetColorFilter = true; + if (mVerticalTrack != null) { - mVerticalTrack.setColorFilter(cf); + mVerticalTrack.setColorFilter(colorFilter); } if (mVerticalThumb != null) { - mVerticalThumb.setColorFilter(cf); + mVerticalThumb.setColorFilter(colorFilter); } if (mHorizontalTrack != null) { - mHorizontalTrack.setColorFilter(cf); + mHorizontalTrack.setColorFilter(colorFilter); } if (mHorizontalThumb != null) { - mHorizontalThumb.setColorFilter(cf); + mHorizontalThumb.setColorFilter(colorFilter); } } @Override + public ColorFilter getColorFilter() { + return mColorFilter; + } + + @Override public int getOpacity() { return PixelFormat.TRANSLUCENT; } @Override + public void invalidateDrawable(Drawable who) { + invalidateSelf(); + } + + @Override + public void scheduleDrawable(Drawable who, Runnable what, long when) { + scheduleSelf(what, when); + } + + @Override + public void unscheduleDrawable(Drawable who, Runnable what) { + unscheduleSelf(what); + } + + @Override public String toString() { return "ScrollBarDrawable: range=" + mRange + " offset=" + mOffset + " extent=" + mExtent + (mVertical ? " V" : " H"); diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java index a90b392..b95c27d 100644 --- a/core/java/android/widget/ScrollView.java +++ b/core/java/android/widget/ScrollView.java @@ -809,9 +809,10 @@ public class ScrollView extends FrameLayout { awakenScrollBars(); } + /** @hide */ @Override - public boolean performAccessibilityAction(int action, Bundle arguments) { - if (super.performAccessibilityAction(action, arguments)) { + public boolean performAccessibilityActionInternal(int action, Bundle arguments) { + if (super.performAccessibilityActionInternal(action, arguments)) { return true; } if (!isEnabled()) { @@ -839,9 +840,14 @@ public class ScrollView extends FrameLayout { } @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(ScrollView.class.getName()); + public CharSequence getAccessibilityClassName() { + return ScrollView.class.getName(); + } + + /** @hide */ + @Override + public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfoInternal(info); if (isEnabled()) { final int scrollRange = getScrollRange(); if (scrollRange > 0) { @@ -856,10 +862,10 @@ public class ScrollView extends FrameLayout { } } + /** @hide */ @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(ScrollView.class.getName()); + public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { + super.onInitializeAccessibilityEventInternal(event); final boolean scrollable = getScrollRange() > 0; event.setScrollable(scrollable); event.setScrollX(mScrollX); diff --git a/core/java/android/widget/SearchView.java b/core/java/android/widget/SearchView.java index 4ee6418..bbf120a 100644 --- a/core/java/android/widget/SearchView.java +++ b/core/java/android/widget/SearchView.java @@ -49,8 +49,6 @@ import android.view.CollapsibleActionView; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView.OnItemClickListener; @@ -1326,15 +1324,8 @@ public class SearchView extends LinearLayout implements CollapsibleActionView { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(SearchView.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(SearchView.class.getName()); + public CharSequence getAccessibilityClassName() { + return SearchView.class.getName(); } private void adjustDropDownSizeAndPosition() { diff --git a/core/java/android/widget/SeekBar.java b/core/java/android/widget/SeekBar.java index dc7c04c..d010122 100644 --- a/core/java/android/widget/SeekBar.java +++ b/core/java/android/widget/SeekBar.java @@ -18,15 +18,13 @@ package android.widget; import android.content.Context; import android.util.AttributeSet; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; /** * A SeekBar is an extension of ProgressBar that adds a draggable thumb. The user can touch * the thumb and drag left or right to set the current progress level or use the arrow keys. - * Placing focusable widgets to the left or right of a SeekBar is discouraged. + * Placing focusable widgets to the left or right of a SeekBar is discouraged. * <p> * Clients of the SeekBar can attach a {@link SeekBar.OnSeekBarChangeListener} to * be notified of the user's actions. @@ -42,39 +40,39 @@ public class SeekBar extends AbsSeekBar { * programmatically. */ public interface OnSeekBarChangeListener { - + /** * Notification that the progress level has changed. Clients can use the fromUser parameter * to distinguish user-initiated changes from those that occurred programmatically. - * + * * @param seekBar The SeekBar whose progress has changed * @param progress The current progress level. This will be in the range 0..max where max * was set by {@link ProgressBar#setMax(int)}. (The default value for max is 100.) * @param fromUser True if the progress change was initiated by the user. */ void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser); - + /** * Notification that the user has started a touch gesture. Clients may want to use this - * to disable advancing the seekbar. + * to disable advancing the seekbar. * @param seekBar The SeekBar in which the touch gesture began */ void onStartTrackingTouch(SeekBar seekBar); - + /** * Notification that the user has finished a touch gesture. Clients may want to use this - * to re-enable advancing the seekbar. + * to re-enable advancing the seekbar. * @param seekBar The SeekBar in which the touch gesture began */ void onStopTrackingTouch(SeekBar seekBar); } private OnSeekBarChangeListener mOnSeekBarChangeListener; - + public SeekBar(Context context) { this(context, null); } - + public SeekBar(Context context, AttributeSet attrs) { this(context, attrs, com.android.internal.R.attr.seekBarStyle); } @@ -88,26 +86,26 @@ public class SeekBar extends AbsSeekBar { } @Override - void onProgressRefresh(float scale, boolean fromUser) { - super.onProgressRefresh(scale, fromUser); + void onProgressRefresh(float scale, boolean fromUser, int progress) { + super.onProgressRefresh(scale, fromUser, progress); if (mOnSeekBarChangeListener != null) { - mOnSeekBarChangeListener.onProgressChanged(this, getProgress(), fromUser); + mOnSeekBarChangeListener.onProgressChanged(this, progress, fromUser); } } /** * Sets a listener to receive notifications of changes to the SeekBar's progress level. Also * provides notifications of when the user starts and stops a touch gesture within the SeekBar. - * + * * @param l The seek bar notification listener - * + * * @see SeekBar.OnSeekBarChangeListener */ public void setOnSeekBarChangeListener(OnSeekBarChangeListener l) { mOnSeekBarChangeListener = l; } - + @Override void onStartTrackingTouch() { super.onStartTrackingTouch(); @@ -115,7 +113,7 @@ public class SeekBar extends AbsSeekBar { mOnSeekBarChangeListener.onStartTrackingTouch(this); } } - + @Override void onStopTrackingTouch() { super.onStopTrackingTouch(); @@ -125,14 +123,7 @@ public class SeekBar extends AbsSeekBar { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(SeekBar.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(SeekBar.class.getName()); + public CharSequence getAccessibilityClassName() { + return SeekBar.class.getName(); } } diff --git a/core/java/android/widget/SimpleAdapter.java b/core/java/android/widget/SimpleAdapter.java index 98bcfff..2008ba8 100644 --- a/core/java/android/widget/SimpleAdapter.java +++ b/core/java/android/widget/SimpleAdapter.java @@ -16,7 +16,11 @@ package android.widget; +import android.annotation.IdRes; +import android.annotation.LayoutRes; import android.content.Context; +import android.content.res.Resources; +import android.view.ContextThemeWrapper; import android.view.View; import android.view.ViewGroup; import android.view.LayoutInflater; @@ -40,14 +44,14 @@ import java.util.Map; * If the returned value is false, the following views are then tried in order: * <ul> * <li> A view that implements Checkable (e.g. CheckBox). The expected bind value is a boolean. - * <li> TextView. The expected bind value is a string and {@link #setViewText(TextView, String)} + * <li> TextView. The expected bind value is a string and {@link #setViewText(TextView, String)} * is invoked. - * <li> ImageView. The expected bind value is a resource id or a string and - * {@link #setViewImage(ImageView, int)} or {@link #setViewImage(ImageView, String)} is invoked. + * <li> ImageView. The expected bind value is a resource id or a string and + * {@link #setViewImage(ImageView, int)} or {@link #setViewImage(ImageView, String)} is invoked. * </ul> * If no appropriate binding can be found, an {@link IllegalStateException} is thrown. */ -public class SimpleAdapter extends BaseAdapter implements Filterable { +public class SimpleAdapter extends BaseAdapter implements Filterable, Spinner.ThemedSpinnerAdapter { private int[] mTo; private String[] mFrom; private ViewBinder mViewBinder; @@ -58,12 +62,15 @@ public class SimpleAdapter extends BaseAdapter implements Filterable { private int mDropDownResource; private LayoutInflater mInflater; + /** Layout inflater used for {@link #getDropDownView(int, View, ViewGroup)}. */ + private LayoutInflater mDropDownInflater; + private SimpleFilter mFilter; private ArrayList<Map<String, ?>> mUnfilteredData; /** * Constructor - * + * * @param context The context where the View associated with this SimpleAdapter is running * @param data A List of Maps. Each entry in the List corresponds to one row in the list. The * Maps contain the data for each row, and should include all the entries specified in @@ -77,7 +84,7 @@ public class SimpleAdapter extends BaseAdapter implements Filterable { * in the from parameter. */ public SimpleAdapter(Context context, List<? extends Map<String, ?>> data, - int resource, String[] from, int[] to) { + @LayoutRes int resource, String[] from, @IdRes int[] to) { mData = data; mResource = mDropDownResource = resource; mFrom = from; @@ -85,7 +92,6 @@ public class SimpleAdapter extends BaseAdapter implements Filterable { mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); } - /** * @see android.widget.Adapter#getCount() */ @@ -111,14 +117,14 @@ public class SimpleAdapter extends BaseAdapter implements Filterable { * @see android.widget.Adapter#getView(int, View, ViewGroup) */ public View getView(int position, View convertView, ViewGroup parent) { - return createViewFromResource(position, convertView, parent, mResource); + return createViewFromResource(mInflater, position, convertView, parent, mResource); } - private View createViewFromResource(int position, View convertView, + private View createViewFromResource(LayoutInflater inflater, int position, View convertView, ViewGroup parent, int resource) { View v; if (convertView == null) { - v = mInflater.inflate(resource, parent, false); + v = inflater.inflate(resource, parent, false); } else { v = convertView; } @@ -135,12 +141,41 @@ public class SimpleAdapter extends BaseAdapter implements Filterable { * @see #getDropDownView(int, android.view.View, android.view.ViewGroup) */ public void setDropDownViewResource(int resource) { - this.mDropDownResource = resource; + mDropDownResource = resource; + } + + /** + * Sets the {@link android.content.res.Resources.Theme} against which drop-down views are + * inflated. + * <p> + * By default, drop-down views are inflated against the theme of the + * {@link Context} passed to the adapter's constructor. + * + * @param theme the theme against which to inflate drop-down views or + * {@code null} to use the theme from the adapter's context + * @see #getDropDownView(int, View, ViewGroup) + */ + @Override + public void setDropDownViewTheme(Resources.Theme theme) { + if (theme == null) { + mDropDownInflater = null; + } else if (theme == mInflater.getContext().getTheme()) { + mDropDownInflater = mInflater; + } else { + final Context context = new ContextThemeWrapper(mInflater.getContext(), theme); + mDropDownInflater = LayoutInflater.from(context); + } + } + + @Override + public Resources.Theme getDropDownViewTheme() { + return mDropDownInflater == null ? null : mDropDownInflater.getContext().getTheme(); } @Override public View getDropDownView(int position, View convertView, ViewGroup parent) { - return createViewFromResource(position, convertView, parent, mDropDownResource); + return createViewFromResource( + mDropDownInflater, position, convertView, parent, mDropDownResource); } private void bindView(int position, View view) { diff --git a/core/java/android/widget/SimpleMonthAdapter.java b/core/java/android/widget/SimpleMonthAdapter.java index 24ebb2c..c807d56 100644 --- a/core/java/android/widget/SimpleMonthAdapter.java +++ b/core/java/android/widget/SimpleMonthAdapter.java @@ -39,6 +39,7 @@ class SimpleMonthAdapter extends BaseAdapter { private Calendar mSelectedDay = Calendar.getInstance(); private ColorStateList mCalendarTextColors = ColorStateList.valueOf(Color.BLACK); + private ColorStateList mCalendarDayBackgroundColor = ColorStateList.valueOf(Color.MAGENTA); private OnDaySelectedListener mOnDaySelectedListener; private int mFirstDayOfWeek; @@ -88,6 +89,10 @@ class SimpleMonthAdapter extends BaseAdapter { mCalendarTextColors = colors; } + void setCalendarDayBackgroundColor(ColorStateList dayBackgroundColor) { + mCalendarDayBackgroundColor = dayBackgroundColor; + } + /** * Sets the text color, size, style, hint color, and highlight color from * the specified TextAppearance resource. This is mostly copied from @@ -144,9 +149,11 @@ class SimpleMonthAdapter extends BaseAdapter { v.setClickable(true); v.setOnDayClickListener(mOnDayClickListener); - if (mCalendarTextColors != null) { - v.setTextColor(mCalendarTextColors); - } + v.setMonthTextColor(mCalendarTextColors); + v.setDayOfWeekTextColor(mCalendarTextColors); + v.setDayTextColor(mCalendarTextColors); + + v.setDayBackgroundColor(mCalendarDayBackgroundColor); } final int minMonth = mMinDate.get(Calendar.MONTH); diff --git a/core/java/android/widget/SimpleMonthView.java b/core/java/android/widget/SimpleMonthView.java index d2a37ac..58ad515 100644 --- a/core/java/android/widget/SimpleMonthView.java +++ b/core/java/android/widget/SimpleMonthView.java @@ -27,12 +27,13 @@ import android.graphics.Paint.Style; import android.graphics.Rect; import android.graphics.Typeface; import android.os.Bundle; +import android.text.TextPaint; 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.util.StateSet; import android.view.MotionEvent; import android.view.View; import android.view.accessibility.AccessibilityEvent; @@ -44,7 +45,6 @@ import com.android.internal.widget.ExploreByTouchHelper; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Formatter; -import java.util.List; import java.util.Locale; /** @@ -52,8 +52,7 @@ import java.util.Locale; * within the specified month. */ class SimpleMonthView extends View { - private static final int DEFAULT_HEIGHT = 32; - private static final int MIN_HEIGHT = 10; + private static final int MIN_ROW_HEIGHT = 10; private static final int DEFAULT_SELECTED_DAY = -1; private static final int DEFAULT_WEEK_START = Calendar.SUNDAY; @@ -61,18 +60,21 @@ class SimpleMonthView extends View { private static final int DEFAULT_NUM_ROWS = 6; private static final int MAX_NUM_ROWS = 6; - private static final int SELECTED_CIRCLE_ALPHA = 60; - - 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; + private final int mMonthTextSize; + private final int mDayOfWeekTextSize; + private final int mDayTextSize; + + /** Height of the header containing the month and day of week labels. */ + private final int mMonthHeaderHeight; + + private final TextPaint mMonthPaint = new TextPaint(); + private final TextPaint mDayOfWeekPaint = new TextPaint(); + private final TextPaint mDayPaint = new TextPaint(); + + private final Paint mDayBackgroundPaint = new Paint(); /** Single-letter (when available) formatter for the day of week label. */ private SimpleDateFormat mDayFormatter = new SimpleDateFormat("EEEEE", Locale.getDefault()); @@ -81,14 +83,7 @@ class SimpleMonthView extends View { private int mPadding = 0; private String mDayOfWeekTypeface; - private String mMonthTitleTypeface; - - private Paint mDayNumberPaint; - private Paint mDayNumberDisabledPaint; - private Paint mDayNumberSelectedPaint; - - private Paint mMonthTitlePaint; - private Paint mMonthDayLabelPaint; + private String mMonthTypeface; private int mMonth; private int mYear; @@ -97,13 +92,13 @@ class SimpleMonthView extends View { private int mWidth; // The height this view should draw at in pixels, set by height param - private int mRowHeight = DEFAULT_HEIGHT; + private final int mRowHeight; // If this view contains the today private boolean mHasToday = false; // Which day is selected [0-6] or -1 if no day is selected - private int mSelectedDay = -1; + private int mActivatedDay = -1; // Which day is today [0-6] or -1 if no day is today private int mToday = DEFAULT_SELECTED_DAY; @@ -142,6 +137,8 @@ class SimpleMonthView extends View { private int mDisabledTextColor; private int mSelectedDayColor; + private ColorStateList mDayTextColor; + public SimpleMonthView(Context context) { this(context, null); } @@ -159,22 +156,21 @@ class SimpleMonthView extends View { final Resources res = context.getResources(); mDayOfWeekTypeface = res.getString(R.string.day_of_week_label_typeface); - mMonthTitleTypeface = res.getString(R.string.sans_serif); + mMonthTypeface = res.getString(R.string.sans_serif); mStringBuilder = new StringBuilder(50); mFormatter = new Formatter(mStringBuilder, Locale.getDefault()); - mMiniDayNumberTextSize = res.getDimensionPixelSize(R.dimen.datepicker_day_number_size); - mMonthLabelTextSize = res.getDimensionPixelSize(R.dimen.datepicker_month_label_size); - mMonthDayLabelTextSize = res.getDimensionPixelSize( + mDayTextSize = res.getDimensionPixelSize(R.dimen.datepicker_day_number_size); + mMonthTextSize = res.getDimensionPixelSize(R.dimen.datepicker_month_label_size); + mDayOfWeekTextSize = res.getDimensionPixelSize( R.dimen.datepicker_month_day_label_text_size); - mMonthHeaderSize = res.getDimensionPixelOffset( + mMonthHeaderHeight = res.getDimensionPixelOffset( R.dimen.datepicker_month_list_item_header_height); - mDaySelectedCircleSize = res.getDimensionPixelSize( - R.dimen.datepicker_day_number_select_circle_radius); - mRowHeight = (res.getDimensionPixelOffset(R.dimen.datepicker_view_animator_height) - - mMonthHeaderSize) / MAX_NUM_ROWS; + mRowHeight = Math.max(MIN_ROW_HEIGHT, + (res.getDimensionPixelOffset(R.dimen.datepicker_view_animator_height) + - mMonthHeaderHeight) / MAX_NUM_ROWS); // Set up accessibility components. mTouchHelper = new MonthViewTouchHelper(this); @@ -182,8 +178,32 @@ class SimpleMonthView extends View { setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); mLockAccessibilityDelegate = true; - // Sets up any standard paints that will be used - initView(); + initPaints(); + } + + /** + * Sets up the text and style properties for painting. + */ + private void initPaints() { + mMonthPaint.setAntiAlias(true); + mMonthPaint.setTextSize(mMonthTextSize); + mMonthPaint.setTypeface(Typeface.create(mMonthTypeface, Typeface.BOLD)); + mMonthPaint.setTextAlign(Align.CENTER); + mMonthPaint.setStyle(Style.FILL); + + mDayOfWeekPaint.setAntiAlias(true); + mDayOfWeekPaint.setTextSize(mDayOfWeekTextSize); + mDayOfWeekPaint.setTypeface(Typeface.create(mDayOfWeekTypeface, Typeface.BOLD)); + mDayOfWeekPaint.setTextAlign(Align.CENTER); + mDayOfWeekPaint.setStyle(Style.FILL); + + mDayBackgroundPaint.setAntiAlias(true); + mDayBackgroundPaint.setStyle(Style.FILL); + + mDayPaint.setAntiAlias(true); + mDayPaint.setTextSize(mDayTextSize); + mDayPaint.setTextAlign(Align.CENTER); + mDayPaint.setStyle(Style.FILL); } @Override @@ -193,22 +213,28 @@ class SimpleMonthView extends View { mDayFormatter = new SimpleDateFormat("EEEEE", newConfig.locale); } - void setTextColor(ColorStateList colors) { - final Resources res = getContext().getResources(); + void setMonthTextColor(ColorStateList monthTextColor) { + final int enabledColor = monthTextColor.getColorForState(ENABLED_STATE_SET, 0); + mMonthPaint.setColor(enabledColor); + invalidate(); + } - mNormalTextColor = colors.getColorForState(ENABLED_STATE_SET, - res.getColor(R.color.datepicker_default_normal_text_color_holo_light)); - mMonthTitlePaint.setColor(mNormalTextColor); - mMonthDayLabelPaint.setColor(mNormalTextColor); + void setDayOfWeekTextColor(ColorStateList dayOfWeekTextColor) { + final int enabledColor = dayOfWeekTextColor.getColorForState(ENABLED_STATE_SET, 0); + mDayOfWeekPaint.setColor(enabledColor); + invalidate(); + } - mDisabledTextColor = colors.getColorForState(EMPTY_STATE_SET, - res.getColor(R.color.datepicker_default_disabled_text_color_holo_light)); - mDayNumberDisabledPaint.setColor(mDisabledTextColor); + void setDayTextColor(ColorStateList dayTextColor) { + mDayTextColor = dayTextColor; + invalidate(); + } - mSelectedDayColor = colors.getColorForState(ENABLED_SELECTED_STATE_SET, - res.getColor(R.color.holo_blue_light)); - mDayNumberSelectedPaint.setColor(mSelectedDayColor); - mDayNumberSelectedPaint.setAlpha(SELECTED_CIRCLE_ALPHA); + void setDayBackgroundColor(ColorStateList dayBackgroundColor) { + final int activatedColor = dayBackgroundColor.getColorForState( + StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_ACTIVATED), 0); + mDayBackgroundPaint.setColor(activatedColor); + invalidate(); } @Override @@ -246,52 +272,6 @@ class SimpleMonthView extends View { return true; } - /** - * Sets up the text and style properties for painting. - */ - private void initView() { - mMonthTitlePaint = new Paint(); - mMonthTitlePaint.setAntiAlias(true); - mMonthTitlePaint.setColor(mNormalTextColor); - mMonthTitlePaint.setTextSize(mMonthLabelTextSize); - mMonthTitlePaint.setTypeface(Typeface.create(mMonthTitleTypeface, Typeface.BOLD)); - mMonthTitlePaint.setTextAlign(Align.CENTER); - mMonthTitlePaint.setStyle(Style.FILL); - mMonthTitlePaint.setFakeBoldText(true); - - mMonthDayLabelPaint = new Paint(); - mMonthDayLabelPaint.setAntiAlias(true); - mMonthDayLabelPaint.setColor(mNormalTextColor); - mMonthDayLabelPaint.setTextSize(mMonthDayLabelTextSize); - mMonthDayLabelPaint.setTypeface(Typeface.create(mDayOfWeekTypeface, Typeface.NORMAL)); - mMonthDayLabelPaint.setTextAlign(Align.CENTER); - mMonthDayLabelPaint.setStyle(Style.FILL); - mMonthDayLabelPaint.setFakeBoldText(true); - - mDayNumberSelectedPaint = new Paint(); - mDayNumberSelectedPaint.setAntiAlias(true); - mDayNumberSelectedPaint.setColor(mSelectedDayColor); - mDayNumberSelectedPaint.setAlpha(SELECTED_CIRCLE_ALPHA); - mDayNumberSelectedPaint.setTextAlign(Align.CENTER); - mDayNumberSelectedPaint.setStyle(Style.FILL); - mDayNumberSelectedPaint.setFakeBoldText(true); - - mDayNumberPaint = new Paint(); - mDayNumberPaint.setAntiAlias(true); - mDayNumberPaint.setTextSize(mMiniDayNumberTextSize); - mDayNumberPaint.setTextAlign(Align.CENTER); - mDayNumberPaint.setStyle(Style.FILL); - mDayNumberPaint.setFakeBoldText(false); - - mDayNumberDisabledPaint = new Paint(); - mDayNumberDisabledPaint.setAntiAlias(true); - mDayNumberDisabledPaint.setColor(mDisabledTextColor); - mDayNumberDisabledPaint.setTextSize(mMiniDayNumberTextSize); - mDayNumberDisabledPaint.setTextAlign(Align.CENTER); - mDayNumberDisabledPaint.setStyle(Style.FILL); - mDayNumberDisabledPaint.setFakeBoldText(false); - } - @Override protected void onDraw(Canvas canvas) { drawMonthTitle(canvas); @@ -323,11 +303,7 @@ class SimpleMonthView extends View { */ void setMonthParams(int selectedDay, int month, int year, int weekStart, int enabledDayStart, int enabledDayEnd) { - if (mRowHeight < MIN_HEIGHT) { - mRowHeight = MIN_HEIGHT; - } - - mSelectedDay = selectedDay; + mActivatedDay = selectedDay; if (isValidMonth(month)) { mMonth = month; @@ -415,7 +391,7 @@ class SimpleMonthView extends View { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * mNumRows - + mMonthHeaderSize); + + mMonthHeaderHeight); } @Override @@ -437,21 +413,28 @@ class SimpleMonthView extends View { private void drawMonthTitle(Canvas canvas) { final float x = (mWidth + 2 * mPadding) / 2f; - final float y = (mMonthHeaderSize - mMonthDayLabelTextSize) / 2f; - canvas.drawText(getMonthAndYearString(), x, y, mMonthTitlePaint); + + // Centered on the upper half of the month header. + final float lineHeight = mMonthPaint.ascent() + mMonthPaint.descent(); + final float y = mMonthHeaderHeight * 0.25f - lineHeight / 2f; + + canvas.drawText(getMonthAndYearString(), x, y, mMonthPaint); } private void drawWeekDayLabels(Canvas canvas) { - final int y = mMonthHeaderSize - (mMonthDayLabelTextSize / 2); - final int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2); + final float dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2); + + // Centered on the lower half of the month header. + final float lineHeight = mDayOfWeekPaint.ascent() + mDayOfWeekPaint.descent(); + final float y = mMonthHeaderHeight * 0.75f - lineHeight / 2f; for (int i = 0; i < mNumDays; i++) { final int calendarDay = (i + mWeekStart) % mNumDays; mDayLabelCalendar.set(Calendar.DAY_OF_WEEK, calendarDay); final String dayLabel = mDayFormatter.format(mDayLabelCalendar.getTime()); - final int x = (2 * i + 1) * dayWidthHalf + mPadding; - canvas.drawText(dayLabel, x, y, mMonthDayLabelPaint); + final float x = (2 * i + 1) * dayWidthHalf + mPadding; + canvas.drawText(dayLabel, x, y, mDayOfWeekPaint); } } @@ -459,26 +442,40 @@ class SimpleMonthView extends View { * Draws the month days. */ private void drawDays(Canvas canvas) { - int y = (((mRowHeight + mMiniDayNumberTextSize) / 2) - DAY_SEPARATOR_WIDTH) - + mMonthHeaderSize; - int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2); - int j = findDayOffset(); - for (int day = 1; day <= mNumCells; day++) { - int x = (2 * j + 1) * dayWidthHalf + mPadding; - if (mSelectedDay == day) { - canvas.drawCircle(x, y - (mMiniDayNumberTextSize / 3), mDaySelectedCircleSize, - mDayNumberSelectedPaint); + final int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2); + + // Centered within the row. + final float lineHeight = mDayOfWeekPaint.ascent() + mDayOfWeekPaint.descent(); + float y = mMonthHeaderHeight + (mRowHeight - lineHeight) / 2f; + + for (int day = 1, j = findDayOffset(); day <= mNumCells; day++) { + final int x = (2 * j + 1) * dayWidthHalf + mPadding; + int stateMask = 0; + + if (day >= mEnabledDayStart && day <= mEnabledDayEnd) { + stateMask |= StateSet.VIEW_STATE_ENABLED; } - if (mHasToday && mToday == day) { - mDayNumberPaint.setColor(mSelectedDayColor); - } else { - mDayNumberPaint.setColor(mNormalTextColor); + if (mActivatedDay == day) { + stateMask |= StateSet.VIEW_STATE_ACTIVATED; + + // Adjust the circle to be centered the row. + final float rowCenterY = y + lineHeight / 2; + canvas.drawCircle(x, rowCenterY, mRowHeight / 2, + mDayBackgroundPaint); } - final Paint paint = (day < mEnabledDayStart || day > mEnabledDayEnd) ? - mDayNumberDisabledPaint : mDayNumberPaint; - canvas.drawText(String.format("%d", day), x, y, paint); + + final int[] stateSet = StateSet.get(stateMask); + final int dayTextColor = mDayTextColor.getColorForState(stateSet, 0); + mDayPaint.setColor(dayTextColor); + + final boolean isDayToday = mHasToday && mToday == day; + mDayPaint.setFakeBoldText(isDayToday); + + canvas.drawText(String.format("%d", day), x, y, mDayPaint); + j++; + if (j == mNumDays) { j = 0; y += mRowHeight; @@ -504,7 +501,7 @@ class SimpleMonthView extends View { return -1; } // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels - int row = (int) (y - mMonthHeaderSize) / mRowHeight; + int row = (int) (y - mMonthHeaderHeight) / mRowHeight; int column = (int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding)); int day = column - findDayOffset() + 1; @@ -628,7 +625,7 @@ class SimpleMonthView extends View { node.setBoundsInParent(mTempRect); node.addAction(AccessibilityNodeInfo.ACTION_CLICK); - if (virtualViewId == mSelectedDay) { + if (virtualViewId == mActivatedDay) { node.setSelected(true); } @@ -654,7 +651,7 @@ class SimpleMonthView extends View { */ private void getItemBounds(int day, Rect rect) { final int offsetX = mPadding; - final int offsetY = mMonthHeaderSize; + final int offsetY = mMonthHeaderHeight; final int cellHeight = mRowHeight; final int cellWidth = ((mWidth - (2 * mPadding)) / mNumDays); final int index = ((day - 1) + findDayOffset()); @@ -679,7 +676,7 @@ class SimpleMonthView extends View { final CharSequence date = DateFormat.format(DATE_FORMAT, mTempCalendar.getTimeInMillis()); - if (day == mSelectedDay) { + if (day == mActivatedDay) { return getContext().getString(R.string.item_is_selected, date); } diff --git a/core/java/android/widget/SlidingDrawer.java b/core/java/android/widget/SlidingDrawer.java index ec06c02..9c44236 100644 --- a/core/java/android/widget/SlidingDrawer.java +++ b/core/java/android/widget/SlidingDrawer.java @@ -32,7 +32,6 @@ import android.view.VelocityTracker; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; /** * SlidingDrawer hides content out of the screen and allows the user to drag a handle @@ -838,15 +837,8 @@ public class SlidingDrawer extends ViewGroup { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(SlidingDrawer.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(SlidingDrawer.class.getName()); + public CharSequence getAccessibilityClassName() { + return SlidingDrawer.class.getName(); } private void closeDrawer() { diff --git a/core/java/android/widget/SpellChecker.java b/core/java/android/widget/SpellChecker.java index 3592687..e7031fe 100644 --- a/core/java/android/widget/SpellChecker.java +++ b/core/java/android/widget/SpellChecker.java @@ -727,14 +727,10 @@ public class SpellChecker implements SpellCheckerSessionListener { } } - if (scheduleOtherSpellCheck && wordStart <= end) { + if (scheduleOtherSpellCheck && wordStart != BreakIterator.DONE && wordStart <= end) { // Update range span: start new spell check from last wordStart setRangeSpan(editable, wordStart, end); } else { - if (DBG && scheduleOtherSpellCheck) { - Log.w(TAG, "Trying to schedule spellcheck for invalid region, from " - + wordStart + " to " + end); - } removeRangeSpan(editable); } diff --git a/core/java/android/widget/Spinner.java b/core/java/android/widget/Spinner.java index 0d76239..3746ec6 100644 --- a/core/java/android/widget/Spinner.java +++ b/core/java/android/widget/Spinner.java @@ -16,11 +16,16 @@ package android.widget; +import com.android.internal.R; + +import android.annotation.DrawableRes; +import android.annotation.Nullable; import android.annotation.Widget; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; +import android.content.res.Resources; import android.content.res.TypedArray; import android.database.DataSetObserver; import android.graphics.Rect; @@ -30,18 +35,17 @@ import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.util.Log; +import android.view.ContextThemeWrapper; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.ViewTreeObserver.OnGlobalLayoutListener; -import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.ListPopupWindow.ForwardingListener; import android.widget.PopupWindow.OnDismissListener; - /** * A view that displays one child at a time and lets the user pick among them. * The items in the Spinner come from the {@link Adapter} associated with @@ -74,17 +78,22 @@ public class Spinner extends AbsSpinner implements OnClickListener { * Use a dropdown anchored to the Spinner for selecting spinner options. */ public static final int MODE_DROPDOWN = 1; - + /** * Use the theme-supplied value to select the dropdown mode. */ private static final int MODE_THEME = -1; + /** Context used to inflate the popup window or dialog. */ + private Context mPopupContext; + /** Forwarding listener used to implement drag-to-open. */ private ForwardingListener mForwardingListener; + /** Temporary holder for setAdapter() calls from the super constructor. */ + private SpinnerAdapter mTempAdapter; + private SpinnerPopup mPopup; - private DropDownAdapter mTempAdapter; int mDropDownWidth; private int mGravity; @@ -184,69 +193,120 @@ public class Spinner extends AbsSpinner implements OnClickListener { * @see #MODE_DIALOG * @see #MODE_DROPDOWN */ - public Spinner( - Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes, int mode) { + public Spinner(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes, + int mode) { + this(context, attrs, defStyleAttr, defStyleRes, mode, null); + } + + /** + * Constructs a new spinner with the given context's theme, the supplied + * attribute set, default styles, popup mode (one of {@link #MODE_DIALOG} + * or {@link #MODE_DROPDOWN}), and the context against which the popup + * should be inflated. + * + * @param context The context against which the view is inflated, which + * provides access to the current theme, resources, etc. + * @param attrs The attributes of the XML tag that is inflating the view. + * @param defStyleAttr An attribute in the current theme that contains a + * reference to a style resource that supplies default + * values for the view. Can be 0 to not look for + * defaults. + * @param defStyleRes A resource identifier of a style resource that + * supplies default values for the view, used only if + * defStyleAttr is 0 or can not be found in the theme. + * Can be 0 to not look for defaults. + * @param mode Constant describing how the user will select choices from + * the spinner. + * @param popupContext The context against which the dialog or dropdown + * popup will be inflated. Can be null to use the view + * context. If set, this will override any value + * specified by + * {@link android.R.styleable#Spinner_popupTheme}. + * + * @see #MODE_DIALOG + * @see #MODE_DROPDOWN + */ + public Spinner(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes, int mode, + Context popupContext) { super(context, attrs, defStyleAttr, defStyleRes); final TypedArray a = context.obtainStyledAttributes( - attrs, com.android.internal.R.styleable.Spinner, defStyleAttr, defStyleRes); + attrs, R.styleable.Spinner, defStyleAttr, defStyleRes); - if (mode == MODE_THEME) { - mode = a.getInt(com.android.internal.R.styleable.Spinner_spinnerMode, MODE_DIALOG); - } - - switch (mode) { - case MODE_DIALOG: { - mPopup = new DialogPopup(); - break; + if (popupContext != null) { + mPopupContext = popupContext; + } else { + final int popupThemeResId = a.getResourceId(R.styleable.Spinner_popupTheme, 0); + if (popupThemeResId != 0) { + mPopupContext = new ContextThemeWrapper(context, popupThemeResId); + } else { + mPopupContext = context; + } } - case MODE_DROPDOWN: { - final DropdownPopup popup = new DropdownPopup(context, attrs, defStyleAttr, defStyleRes); + if (mode == MODE_THEME) { + mode = a.getInt(R.styleable.Spinner_spinnerMode, MODE_DIALOG); + } - mDropDownWidth = a.getLayoutDimension( - com.android.internal.R.styleable.Spinner_dropDownWidth, - ViewGroup.LayoutParams.WRAP_CONTENT); - popup.setBackgroundDrawable(a.getDrawable( - com.android.internal.R.styleable.Spinner_popupBackground)); + switch (mode) { + case MODE_DIALOG: { + mPopup = new DialogPopup(); + mPopup.setPromptText(a.getString(R.styleable.Spinner_prompt)); + break; + } - mPopup = popup; - mForwardingListener = new ForwardingListener(this) { - @Override - public ListPopupWindow getPopup() { - return popup; - } + case MODE_DROPDOWN: { + final DropdownPopup popup = new DropdownPopup( + mPopupContext, attrs, defStyleAttr, defStyleRes); + final TypedArray pa = mPopupContext.obtainStyledAttributes( + attrs, R.styleable.Spinner, defStyleAttr, defStyleRes); + mDropDownWidth = pa.getLayoutDimension(R.styleable.Spinner_dropDownWidth, + ViewGroup.LayoutParams.WRAP_CONTENT); + popup.setBackgroundDrawable(pa.getDrawable(R.styleable.Spinner_popupBackground)); + popup.setPromptText(a.getString(R.styleable.Spinner_prompt)); + pa.recycle(); + + mPopup = popup; + mForwardingListener = new ForwardingListener(this) { + @Override + public ListPopupWindow getPopup() { + return popup; + } - @Override - public boolean onForwardingStarted() { - if (!mPopup.isShowing()) { - mPopup.show(getTextDirection(), getTextAlignment()); + @Override + public boolean onForwardingStarted() { + if (!mPopup.isShowing()) { + mPopup.show(getTextDirection(), getTextAlignment()); + } + return true; } - return true; - } - }; - break; - } + }; + break; + } } - mGravity = a.getInt(com.android.internal.R.styleable.Spinner_gravity, Gravity.CENTER); - - mPopup.setPromptText(a.getString(com.android.internal.R.styleable.Spinner_prompt)); - + mGravity = a.getInt(R.styleable.Spinner_gravity, Gravity.CENTER); mDisableChildrenWhenDisabled = a.getBoolean( - com.android.internal.R.styleable.Spinner_disableChildrenWhenDisabled, false); + R.styleable.Spinner_disableChildrenWhenDisabled, false); a.recycle(); // Base constructor can call setAdapter before we initialize mPopup. // Finish setting things up if this happened. if (mTempAdapter != null) { - mPopup.setAdapter(mTempAdapter); + setAdapter(mTempAdapter); mTempAdapter = null; } } /** + * @return the context used to inflate the Spinner's popup or dialog window + */ + public Context getPopupContext() { + return mPopupContext; + } + + /** * Set the background drawable for the spinner's popup window of choices. * Only valid in {@link #MODE_DROPDOWN}; this method is a no-op in other modes. * @@ -259,7 +319,7 @@ public class Spinner extends AbsSpinner implements OnClickListener { Log.e(TAG, "setPopupBackgroundDrawable: incompatible spinner mode; ignoring..."); return; } - ((DropdownPopup) mPopup).setBackgroundDrawable(background); + mPopup.setBackgroundDrawable(background); } /** @@ -270,8 +330,8 @@ public class Spinner extends AbsSpinner implements OnClickListener { * * @attr ref android.R.styleable#Spinner_popupBackground */ - public void setPopupBackgroundResource(int resId) { - setPopupBackgroundDrawable(getContext().getDrawable(resId)); + public void setPopupBackgroundResource(@DrawableRes int resId) { + setPopupBackgroundDrawable(getPopupContext().getDrawable(resId)); } /** @@ -410,9 +470,17 @@ public class Spinner extends AbsSpinner implements OnClickListener { } /** - * Sets the Adapter used to provide the data which backs this Spinner. + * Sets the {@link SpinnerAdapter} used to provide the data which backs + * this Spinner. * <p> - * Note that Spinner overrides {@link Adapter#getViewTypeCount()} on the + * If this Spinner has a popup theme set in XML via the + * {@link android.R.styleable#Spinner_popupTheme popupTheme} attribute, the + * adapter should inflate drop-down views using the same theme. The easiest + * way to achieve this is by using {@link #getPopupContext()} to obtain a + * layout inflater for use in + * {@link SpinnerAdapter#getDropDownView(int, View, ViewGroup)}. + * <p> + * Spinner overrides {@link Adapter#getViewTypeCount()} on the * Adapter associated with this view. Calling * {@link Adapter#getItemViewType(int) getItemViewType(int)} on the object * returned from {@link #getAdapter()} will always return 0. Calling @@ -429,6 +497,13 @@ public class Spinner extends AbsSpinner implements OnClickListener { */ @Override public void setAdapter(SpinnerAdapter adapter) { + // The super constructor may call setAdapter before we're prepared. + // Postpone doing anything until we've finished construction. + if (mPopup == null) { + mTempAdapter = adapter; + return; + } + super.setAdapter(adapter); mRecycler.clear(); @@ -439,11 +514,8 @@ public class Spinner extends AbsSpinner implements OnClickListener { throw new IllegalArgumentException("Spinner adapter view type count must be 1"); } - if (mPopup != null) { - mPopup.setAdapter(new DropDownAdapter(adapter)); - } else { - mTempAdapter = new DropDownAdapter(adapter); - } + final Context popupContext = mPopupContext == null ? mContext : mPopupContext; + mPopup.setAdapter(new DropDownAdapter(adapter, popupContext.getTheme())); } @Override @@ -693,15 +765,14 @@ public class Spinner extends AbsSpinner implements OnClickListener { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(Spinner.class.getName()); + public CharSequence getAccessibilityClassName() { + return Spinner.class.getName(); } + /** @hide */ @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(Spinner.class.getName()); + public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfoInternal(info); if (mAdapter != null) { info.setCanOpenPopup(true); @@ -847,14 +918,26 @@ public class Spinner extends AbsSpinner implements OnClickListener { private ListAdapter mListAdapter; /** - * <p>Creates a new ListAdapter wrapper for the specified adapter.</p> + * Creates a new ListAdapter wrapper for the specified adapter. * - * @param adapter the Adapter to transform into a ListAdapter + * @param adapter the SpinnerAdapter to transform into a ListAdapter + * @param dropDownTheme the theme against which to inflate drop-down + * views, may be {@null} to use default theme */ - public DropDownAdapter(SpinnerAdapter adapter) { - this.mAdapter = adapter; + public DropDownAdapter(@Nullable SpinnerAdapter adapter, + @Nullable Resources.Theme dropDownTheme) { + mAdapter = adapter; + if (adapter instanceof ListAdapter) { - this.mListAdapter = (ListAdapter) adapter; + mListAdapter = (ListAdapter) adapter; + } + + if (dropDownTheme != null && adapter instanceof Spinner.ThemedSpinnerAdapter) { + final Spinner.ThemedSpinnerAdapter themedAdapter = + (Spinner.ThemedSpinnerAdapter) adapter; + if (themedAdapter.getDropDownViewTheme() == null) { + themedAdapter.setDropDownViewTheme(dropDownTheme); + } } } @@ -970,7 +1053,7 @@ public class Spinner extends AbsSpinner implements OnClickListener { public int getVerticalOffset(); public int getHorizontalOffset(); } - + private class DialogPopup implements SpinnerPopup, DialogInterface.OnClickListener { private AlertDialog mPopup; private ListAdapter mListAdapter; @@ -994,7 +1077,7 @@ public class Spinner extends AbsSpinner implements OnClickListener { public void setPromptText(CharSequence hintText) { mPrompt = hintText; } - + public CharSequence getHintText() { return mPrompt; } @@ -1003,7 +1086,7 @@ public class Spinner extends AbsSpinner implements OnClickListener { if (mListAdapter == null) { return; } - AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); + AlertDialog.Builder builder = new AlertDialog.Builder(getPopupContext()); if (mPrompt != null) { builder.setTitle(mPrompt); } @@ -1179,4 +1262,21 @@ public class Spinner extends AbsSpinner implements OnClickListener { } } } + + public interface ThemedSpinnerAdapter { + /** + * Sets the {@link Resources.Theme} against which drop-down views are + * inflated. + * + * @param theme the context against which to inflate drop-down views + * @see SpinnerAdapter#getDropDownView(int, View, ViewGroup) + */ + public void setDropDownViewTheme(Resources.Theme theme); + + /** + * @return The {@link Resources.Theme} against which drop-down views are + * inflated. + */ + public Resources.Theme getDropDownViewTheme(); + } } diff --git a/core/java/android/widget/SpinnerAdapter.java b/core/java/android/widget/SpinnerAdapter.java index 91504cf..f99f45b 100644 --- a/core/java/android/widget/SpinnerAdapter.java +++ b/core/java/android/widget/SpinnerAdapter.java @@ -22,16 +22,17 @@ import android.view.ViewGroup; /** * Extended {@link Adapter} that is the bridge between a * {@link android.widget.Spinner} and its data. A spinner adapter allows to - * define two different views: one that shows the data in the spinner itself and - * one that shows the data in the drop down list when the spinner is pressed.</p> + * define two different views: one that shows the data in the spinner itself + * and one that shows the data in the drop down list when the spinner is + * pressed. */ public interface SpinnerAdapter extends Adapter { /** - * <p>Get a {@link android.view.View} that displays in the drop down popup - * the data at the specified position in the data set.</p> + * Gets a {@link android.view.View} that displays in the drop down popup + * the data at the specified position in the data set. * - * @param position index of the item whose view we want. - * @param convertView the old view to reuse, if possible. Note: You should + * @param position index of the item whose view we want. + * @param convertView the old view to reuse, if possible. Note: You should * check that this view is non-null and of an appropriate type before * using. If it is not possible to convert this view to display the * correct data, this method can create a new view. diff --git a/core/java/android/widget/StackView.java b/core/java/android/widget/StackView.java index 9e168b8..2bd3143 100644 --- a/core/java/android/widget/StackView.java +++ b/core/java/android/widget/StackView.java @@ -41,7 +41,6 @@ import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; -import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.LinearInterpolator; import android.widget.RemoteViews.RemoteView; @@ -1225,15 +1224,14 @@ public class StackView extends AdapterViewAnimator { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(StackView.class.getName()); + public CharSequence getAccessibilityClassName() { + return StackView.class.getName(); } + /** @hide */ @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(StackView.class.getName()); + public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfoInternal(info); info.setScrollable(getChildCount() > 1); if (isEnabled()) { if (getDisplayedChild() < getChildCount() - 1) { @@ -1245,9 +1243,10 @@ public class StackView extends AdapterViewAnimator { } } + /** @hide */ @Override - public boolean performAccessibilityAction(int action, Bundle arguments) { - if (super.performAccessibilityAction(action, arguments)) { + public boolean performAccessibilityActionInternal(int action, Bundle arguments) { + if (super.performAccessibilityActionInternal(action, arguments)) { return true; } if (!isEnabled()) { diff --git a/core/java/android/widget/SuggestionsAdapter.java b/core/java/android/widget/SuggestionsAdapter.java index 6349347..aad0625 100644 --- a/core/java/android/widget/SuggestionsAdapter.java +++ b/core/java/android/widget/SuggestionsAdapter.java @@ -145,8 +145,9 @@ class SuggestionsAdapter extends ResourceCursorAdapter implements OnClickListene * copied to the query text field. * <p> * - * @param refine which queries to refine. Possible values are {@link #REFINE_NONE}, - * {@link #REFINE_BY_ENTRY}, and {@link #REFINE_ALL}. + * @param refineWhat which queries to refine. Possible values are + * {@link #REFINE_NONE}, {@link #REFINE_BY_ENTRY}, and + * {@link #REFINE_ALL}. */ public void setQueryRefinement(int refineWhat) { mQueryRefinement = refineWhat; @@ -327,7 +328,7 @@ class SuggestionsAdapter extends ResourceCursorAdapter implements OnClickListene // First check TEXT_2_URL CharSequence text2 = getStringOrNull(cursor, mText2UrlCol); if (text2 != null) { - text2 = formatUrl(text2); + text2 = formatUrl(context, text2); } else { text2 = getStringOrNull(cursor, mText2Col); } @@ -372,12 +373,12 @@ class SuggestionsAdapter extends ResourceCursorAdapter implements OnClickListene } } - private CharSequence formatUrl(CharSequence url) { + private CharSequence formatUrl(Context context, CharSequence url) { if (mUrlColor == null) { // Lazily get the URL color from the current theme. TypedValue colorValue = new TypedValue(); - mContext.getTheme().resolveAttribute(R.attr.textColorSearchUrl, colorValue, true); - mUrlColor = mContext.getResources().getColorStateList(colorValue.resourceId); + context.getTheme().resolveAttribute(R.attr.textColorSearchUrl, colorValue, true); + mUrlColor = context.getColorStateList(colorValue.resourceId); } SpannableString text = new SpannableString(url); @@ -502,6 +503,30 @@ class SuggestionsAdapter extends ResourceCursorAdapter implements OnClickListene } /** + * This method is overridden purely to provide a bit of protection against + * flaky content providers. + * + * @see android.widget.CursorAdapter#getDropDownView(int, View, ViewGroup) + */ + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + try { + return super.getDropDownView(position, convertView, parent); + } catch (RuntimeException e) { + Log.w(LOG_TAG, "Search suggestions cursor threw exception.", e); + // Put exception string in item title + final Context context = mDropDownContext == null ? mContext : mDropDownContext; + final View v = newDropDownView(context, mCursor, parent); + if (v != null) { + final ChildViewCache views = (ChildViewCache) v.getTag(); + final TextView tv = views.mText1; + tv.setText(e.toString()); + } + return v; + } + } + + /** * Gets a drawable given a value provided by a suggestion provider. * * This value could be just the string value of a resource id @@ -570,7 +595,7 @@ class SuggestionsAdapter extends ResourceCursorAdapter implements OnClickListene OpenResourceIdResult r = mProviderContext.getContentResolver().getResourceId(uri); try { - return r.r.getDrawable(r.id, mContext.getTheme()); + return r.r.getDrawable(r.id, mProviderContext.getTheme()); } catch (Resources.NotFoundException ex) { throw new FileNotFoundException("Resource does not exist: " + uri); } diff --git a/core/java/android/widget/Switch.java b/core/java/android/widget/Switch.java index 7a22224..bb290e7 100644 --- a/core/java/android/widget/Switch.java +++ b/core/java/android/widget/Switch.java @@ -17,6 +17,9 @@ package android.widget; import android.animation.ObjectAnimator; +import android.annotation.DrawableRes; +import android.annotation.Nullable; +import android.annotation.StyleRes; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; @@ -24,10 +27,12 @@ import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Insets; import android.graphics.Paint; +import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.Region.Op; import android.graphics.drawable.Drawable; +import android.os.Bundle; import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; @@ -41,6 +46,7 @@ import android.view.Gravity; import android.view.MotionEvent; import android.view.SoundEffectConstants; import android.view.VelocityTracker; +import android.view.ViewAssistStructure; import android.view.ViewConfiguration; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; @@ -84,7 +90,17 @@ public class Switch extends CompoundButton { private static final int MONOSPACE = 3; private Drawable mThumbDrawable; + private ColorStateList mThumbTintList = null; + private PorterDuff.Mode mThumbTintMode = null; + private boolean mHasThumbTint = false; + private boolean mHasThumbTintMode = false; + private Drawable mTrackDrawable; + private ColorStateList mTrackTintList = null; + private PorterDuff.Mode mTrackTintMode = null; + private boolean mHasTrackTint = false; + private boolean mHasTrackTintMode = false; + private int mThumbTextPadding; private int mSwitchMinWidth; private int mSwitchPadding; @@ -249,7 +265,7 @@ public class Switch extends CompoundButton { * * @attr ref android.R.styleable#Switch_switchTextAppearance */ - public void setSwitchTextAppearance(Context context, int resid) { + public void setSwitchTextAppearance(Context context, @StyleRes int resid) { TypedArray appearance = context.obtainStyledAttributes(resid, com.android.internal.R.styleable.TextAppearance); @@ -457,7 +473,7 @@ public class Switch extends CompoundButton { * * @attr ref android.R.styleable#Switch_track */ - public void setTrackResource(int resId) { + public void setTrackResource(@DrawableRes int resId) { setTrackDrawable(getContext().getDrawable(resId)); } @@ -473,6 +489,86 @@ public class Switch extends CompoundButton { } /** + * Applies a tint to the track drawable. Does not modify the current + * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default. + * <p> + * Subsequent calls to {@link #setTrackDrawable(Drawable)} will + * automatically mutate the drawable and apply the specified tint and tint + * mode using {@link Drawable#setTintList(ColorStateList)}. + * + * @param tint the tint to apply, may be {@code null} to clear tint + * + * @attr ref android.R.styleable#Switch_trackTint + * @see #getTrackTintList() + * @see Drawable#setTintList(ColorStateList) + */ + public void setTrackTintList(@Nullable ColorStateList tint) { + mTrackTintList = tint; + mHasTrackTint = true; + + applyTrackTint(); + } + + /** + * @return the tint applied to the track drawable + * @attr ref android.R.styleable#Switch_trackTint + * @see #setTrackTintList(ColorStateList) + */ + @Nullable + public ColorStateList getTrackTintList() { + return mTrackTintList; + } + + /** + * Specifies the blending mode used to apply the tint specified by + * {@link #setTrackTintList(ColorStateList)}} to the track drawable. + * The default mode is {@link PorterDuff.Mode#SRC_IN}. + * + * @param tintMode the blending mode used to apply the tint, may be + * {@code null} to clear tint + * @attr ref android.R.styleable#Switch_trackTintMode + * @see #getTrackTintMode() + * @see Drawable#setTintMode(PorterDuff.Mode) + */ + public void setTrackTintMode(@Nullable PorterDuff.Mode tintMode) { + mTrackTintMode = tintMode; + mHasTrackTintMode = true; + + applyTrackTint(); + } + + /** + * @return the blending mode used to apply the tint to the track + * drawable + * @attr ref android.R.styleable#Switch_trackTintMode + * @see #setTrackTintMode(PorterDuff.Mode) + */ + @Nullable + public PorterDuff.Mode getTrackTintMode() { + return mTrackTintMode; + } + + private void applyTrackTint() { + if (mTrackDrawable != null && (mHasTrackTint || mHasTrackTintMode)) { + mTrackDrawable = mTrackDrawable.mutate(); + + if (mHasTrackTint) { + mTrackDrawable.setTintList(mTrackTintList); + } + + if (mHasTrackTintMode) { + mTrackDrawable.setTintMode(mTrackTintMode); + } + + // The drawable (or one of its children) may not have been + // stateful before applying the tint, so let's try again. + if (mTrackDrawable.isStateful()) { + mTrackDrawable.setState(getDrawableState()); + } + } + } + + /** * Set the drawable used for the switch "thumb" - the piece that the user * can physically touch and drag along the track. * @@ -499,7 +595,7 @@ public class Switch extends CompoundButton { * * @attr ref android.R.styleable#Switch_thumb */ - public void setThumbResource(int resId) { + public void setThumbResource(@DrawableRes int resId) { setThumbDrawable(getContext().getDrawable(resId)); } @@ -516,6 +612,86 @@ public class Switch extends CompoundButton { } /** + * Applies a tint to the thumb drawable. Does not modify the current + * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default. + * <p> + * Subsequent calls to {@link #setThumbDrawable(Drawable)} will + * automatically mutate the drawable and apply the specified tint and tint + * mode using {@link Drawable#setTintList(ColorStateList)}. + * + * @param tint the tint to apply, may be {@code null} to clear tint + * + * @attr ref android.R.styleable#Switch_thumbTint + * @see #getThumbTintList() + * @see Drawable#setTintList(ColorStateList) + */ + public void setThumbTintList(@Nullable ColorStateList tint) { + mThumbTintList = tint; + mHasThumbTint = true; + + applyThumbTint(); + } + + /** + * @return the tint applied to the thumb drawable + * @attr ref android.R.styleable#Switch_thumbTint + * @see #setThumbTintList(ColorStateList) + */ + @Nullable + public ColorStateList getThumbTintList() { + return mThumbTintList; + } + + /** + * Specifies the blending mode used to apply the tint specified by + * {@link #setThumbTintList(ColorStateList)}} to the thumb drawable. + * The default mode is {@link PorterDuff.Mode#SRC_IN}. + * + * @param tintMode the blending mode used to apply the tint, may be + * {@code null} to clear tint + * @attr ref android.R.styleable#Switch_thumbTintMode + * @see #getThumbTintMode() + * @see Drawable#setTintMode(PorterDuff.Mode) + */ + public void setThumbTintMode(@Nullable PorterDuff.Mode tintMode) { + mThumbTintMode = tintMode; + mHasThumbTintMode = true; + + applyThumbTint(); + } + + /** + * @return the blending mode used to apply the tint to the thumb + * drawable + * @attr ref android.R.styleable#Switch_thumbTintMode + * @see #setThumbTintMode(PorterDuff.Mode) + */ + @Nullable + public PorterDuff.Mode getThumbTintMode() { + return mThumbTintMode; + } + + private void applyThumbTint() { + if (mThumbDrawable != null && (mHasThumbTint || mHasThumbTintMode)) { + mThumbDrawable = mThumbDrawable.mutate(); + + if (mHasThumbTint) { + mThumbDrawable.setTintList(mThumbTintList); + } + + if (mHasThumbTintMode) { + mThumbDrawable.setTintMode(mThumbTintMode); + } + + // The drawable (or one of its children) may not have been + // stateful before applying the tint, so let's try again. + if (mThumbDrawable.isStateful()) { + mThumbDrawable.setState(getDrawableState()); + } + } + } + + /** * Specifies whether the track should be split by the thumb. When true, * the thumb's optical bounds will be clipped out of the track drawable, * then the thumb will be drawn into the resulting gap. @@ -665,9 +841,10 @@ public class Switch extends CompoundButton { } } + /** @hide */ @Override - public void onPopulateAccessibilityEvent(AccessibilityEvent event) { - super.onPopulateAccessibilityEvent(event); + public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) { + super.onPopulateAccessibilityEventInternal(event); final CharSequence text = isChecked() ? mTextOn : mTextOff; if (text != null) { @@ -1181,15 +1358,31 @@ public class Switch extends CompoundButton { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(Switch.class.getName()); + public CharSequence getAccessibilityClassName() { + return Switch.class.getName(); + } + + @Override + public void onProvideAssistStructure(ViewAssistStructure structure, Bundle extras) { + super.onProvideAssistStructure(structure, extras); + CharSequence switchText = isChecked() ? mTextOn : mTextOff; + if (!TextUtils.isEmpty(switchText)) { + CharSequence oldText = structure.getText(); + if (TextUtils.isEmpty(oldText)) { + structure.setText(switchText); + } else { + StringBuilder newText = new StringBuilder(); + newText.append(oldText).append(' ').append(switchText); + structure.setText(newText); + } + structure.setTextPaint(mTextPaint); + } } + /** @hide */ @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(Switch.class.getName()); + public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfoInternal(info); CharSequence switchText = isChecked() ? mTextOn : mTextOff; if (!TextUtils.isEmpty(switchText)) { CharSequence oldText = info.getText(); diff --git a/core/java/android/widget/TabHost.java b/core/java/android/widget/TabHost.java index 89df51a..c521f72 100644 --- a/core/java/android/widget/TabHost.java +++ b/core/java/android/widget/TabHost.java @@ -33,9 +33,6 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.Window; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; - import java.util.ArrayList; import java.util.List; @@ -173,8 +170,9 @@ mTabHost.addTab(TAB_TAG_1, "Hello, world!", "Tab 1"); } } + /** @hide */ @Override - public void sendAccessibilityEvent(int eventType) { + public void sendAccessibilityEventInternal(int eventType) { /* avoid super class behavior - TabWidget sends the right events */ } @@ -384,15 +382,8 @@ mTabHost.addTab(TAB_TAG_1, "Hello, world!", "Tab 1"); } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(TabHost.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(TabHost.class.getName()); + public CharSequence getAccessibilityClassName() { + return TabHost.class.getName(); } public void setCurrentTab(int index) { @@ -612,7 +603,7 @@ mTabHost.addTab(TAB_TAG_1, "Hello, world!", "Tab 1"); if (context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.DONUT) { // Donut apps get old color scheme tabIndicator.setBackgroundResource(R.drawable.tab_indicator_v4); - tv.setTextColor(context.getResources().getColorStateList(R.color.tab_indicator_text_v4)); + tv.setTextColor(context.getColorStateList(R.color.tab_indicator_text_v4)); } return tabIndicator; @@ -657,7 +648,7 @@ mTabHost.addTab(TAB_TAG_1, "Hello, world!", "Tab 1"); if (context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.DONUT) { // Donut apps get old color scheme tabIndicator.setBackgroundResource(R.drawable.tab_indicator_v4); - tv.setTextColor(context.getResources().getColorStateList(R.color.tab_indicator_text_v4)); + tv.setTextColor(context.getColorStateList(R.color.tab_indicator_text_v4)); } return tabIndicator; diff --git a/core/java/android/widget/TabWidget.java b/core/java/android/widget/TabWidget.java index 47a5449..9496e62 100644 --- a/core/java/android/widget/TabWidget.java +++ b/core/java/android/widget/TabWidget.java @@ -17,8 +17,8 @@ package android.widget; import android.R; +import android.annotation.DrawableRes; import android.content.Context; -import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; @@ -29,7 +29,6 @@ import android.view.View; import android.view.View.OnFocusChangeListener; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; /** * @@ -244,7 +243,7 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener { * @param resId the resource identifier of the drawable to use as a * divider. */ - public void setDividerDrawable(int resId) { + public void setDividerDrawable(@DrawableRes int resId) { setDividerDrawable(mContext.getDrawable(resId)); } @@ -265,7 +264,7 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener { * @param resId the resource identifier of the drawable to use as the * left strip drawable */ - public void setLeftStripDrawable(int resId) { + public void setLeftStripDrawable(@DrawableRes int resId) { setLeftStripDrawable(mContext.getDrawable(resId)); } @@ -286,7 +285,7 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener { * @param resId the resource identifier of the drawable to use as the * right strip drawable */ - public void setRightStripDrawable(int resId) { + public void setRightStripDrawable(@DrawableRes int resId) { setRightStripDrawable(mContext.getDrawable(resId)); } @@ -401,8 +400,9 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener { } } + /** @hide */ @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) { onPopulateAccessibilityEvent(event); // Dispatch only to the selected tab. if (mSelectedTab != -1) { @@ -415,28 +415,28 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(TabWidget.class.getName()); + public CharSequence getAccessibilityClassName() { + return TabWidget.class.getName(); + } + + /** @hide */ + @Override + public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { + super.onInitializeAccessibilityEventInternal(event); event.setItemCount(getTabCount()); event.setCurrentItemIndex(mSelectedTab); } + /** @hide */ @Override - public void sendAccessibilityEventUnchecked(AccessibilityEvent event) { + public void sendAccessibilityEventUncheckedInternal(AccessibilityEvent event) { // this class fires events only when tabs are focused or selected if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED && isFocused()) { event.recycle(); return; } - super.sendAccessibilityEventUnchecked(event); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(TabWidget.class.getName()); + super.sendAccessibilityEventUncheckedInternal(event); } /** diff --git a/core/java/android/widget/TableLayout.java b/core/java/android/widget/TableLayout.java index f4b2ce0..093bdcf 100644 --- a/core/java/android/widget/TableLayout.java +++ b/core/java/android/widget/TableLayout.java @@ -24,9 +24,6 @@ import android.util.AttributeSet; import android.util.SparseBooleanArray; import android.view.View; import android.view.ViewGroup; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; - import java.util.regex.Pattern; /** @@ -667,15 +664,8 @@ public class TableLayout extends LinearLayout { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(TableLayout.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(TableLayout.class.getName()); + public CharSequence getAccessibilityClassName() { + return TableLayout.class.getName(); } /** diff --git a/core/java/android/widget/TableRow.java b/core/java/android/widget/TableRow.java index fe3631a..faf5b84 100644 --- a/core/java/android/widget/TableRow.java +++ b/core/java/android/widget/TableRow.java @@ -24,8 +24,6 @@ import android.view.Gravity; import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; /** @@ -380,15 +378,8 @@ public class TableRow extends LinearLayout { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(TableRow.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(TableRow.class.getName()); + public CharSequence getAccessibilityClassName() { + return TableRow.class.getName(); } /** diff --git a/core/java/android/widget/TextSwitcher.java b/core/java/android/widget/TextSwitcher.java index 1aefd2b..ecd9a8c 100644 --- a/core/java/android/widget/TextSwitcher.java +++ b/core/java/android/widget/TextSwitcher.java @@ -21,8 +21,6 @@ import android.content.Context; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; /** * Specialized {@link android.widget.ViewSwitcher} that contains @@ -92,14 +90,7 @@ public class TextSwitcher extends ViewSwitcher { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(TextSwitcher.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(TextSwitcher.class.getName()); + public CharSequence getAccessibilityClassName() { + return TextSwitcher.class.getName(); } } diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index dd8280b..718ef93 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -17,14 +17,20 @@ package android.widget; import android.R; +import android.annotation.ColorInt; +import android.annotation.DrawableRes; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.StringRes; +import android.annotation.StyleRes; +import android.annotation.XmlRes; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.UndoManager; import android.content.res.ColorStateList; import android.content.res.CompatibilityInfo; +import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; @@ -32,6 +38,7 @@ import android.graphics.Canvas; import android.graphics.Insets; import android.graphics.Paint; import android.graphics.Path; +import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Typeface; @@ -41,6 +48,7 @@ import android.os.AsyncTask; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; +import android.os.ParcelableParcel; import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings; @@ -103,10 +111,9 @@ import android.view.Gravity; import android.view.HapticFeedbackConstants; import android.view.KeyCharacterMap; import android.view.KeyEvent; -import android.view.Menu; -import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; +import android.view.ViewAssistStructure; import android.view.ViewConfiguration; import android.view.ViewDebug; import android.view.ViewGroup.LayoutParams; @@ -214,6 +221,8 @@ import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; * @attr ref android.R.styleable#TextView_drawableStart * @attr ref android.R.styleable#TextView_drawableEnd * @attr ref android.R.styleable#TextView_drawablePadding + * @attr ref android.R.styleable#TextView_drawableTint + * @attr ref android.R.styleable#TextView_drawableTintMode * @attr ref android.R.styleable#TextView_lineSpacingExtra * @attr ref android.R.styleable#TextView_lineSpacingMultiplier * @attr ref android.R.styleable#TextView_marqueeRepeatLimit @@ -298,7 +307,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private float mShadowRadius, mShadowDx, mShadowDy; private int mShadowColor; - private boolean mPreDrawRegistered; private boolean mPreDrawListenerDetached; @@ -312,16 +320,27 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private TextUtils.TruncateAt mEllipsize; static class Drawables { - final static int DRAWABLE_NONE = -1; - final static int DRAWABLE_RIGHT = 0; - final static int DRAWABLE_LEFT = 1; + static final int LEFT = 0; + static final int TOP = 1; + static final int RIGHT = 2; + static final int BOTTOM = 3; + + static final int DRAWABLE_NONE = -1; + static final int DRAWABLE_RIGHT = 0; + static final int DRAWABLE_LEFT = 1; final Rect mCompoundRect = new Rect(); - Drawable mDrawableTop, mDrawableBottom, mDrawableLeft, mDrawableRight, - mDrawableStart, mDrawableEnd, mDrawableError, mDrawableTemp; + final Drawable[] mShowing = new Drawable[4]; + + ColorStateList mTintList; + PorterDuff.Mode mTintMode; + boolean mHasTint; + boolean mHasTintMode; + Drawable mDrawableStart, mDrawableEnd, mDrawableError, mDrawableTemp; Drawable mDrawableLeftInitial, mDrawableRightInitial; + boolean mIsRtlCompatibilityMode; boolean mOverride; @@ -344,19 +363,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public void resolveWithLayoutDirection(int layoutDirection) { // First reset "left" and "right" drawables to their initial values - mDrawableLeft = mDrawableLeftInitial; - mDrawableRight = mDrawableRightInitial; + mShowing[Drawables.LEFT] = mDrawableLeftInitial; + mShowing[Drawables.RIGHT] = mDrawableRightInitial; if (mIsRtlCompatibilityMode) { // Use "start" drawable as "left" drawable if the "left" drawable was not defined - if (mDrawableStart != null && mDrawableLeft == null) { - mDrawableLeft = mDrawableStart; + if (mDrawableStart != null && mShowing[Drawables.LEFT] == null) { + mShowing[Drawables.LEFT] = mDrawableStart; mDrawableSizeLeft = mDrawableSizeStart; mDrawableHeightLeft = mDrawableHeightStart; } // Use "end" drawable as "right" drawable if the "right" drawable was not defined - if (mDrawableEnd != null && mDrawableRight == null) { - mDrawableRight = mDrawableEnd; + if (mDrawableEnd != null && mShowing[Drawables.RIGHT] == null) { + mShowing[Drawables.RIGHT] = mDrawableEnd; mDrawableSizeRight = mDrawableSizeEnd; mDrawableHeightRight = mDrawableHeightEnd; } @@ -366,11 +385,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener switch(layoutDirection) { case LAYOUT_DIRECTION_RTL: if (mOverride) { - mDrawableRight = mDrawableStart; + mShowing[Drawables.RIGHT] = mDrawableStart; mDrawableSizeRight = mDrawableSizeStart; mDrawableHeightRight = mDrawableHeightStart; - mDrawableLeft = mDrawableEnd; + mShowing[Drawables.LEFT] = mDrawableEnd; mDrawableSizeLeft = mDrawableSizeEnd; mDrawableHeightLeft = mDrawableHeightEnd; } @@ -379,11 +398,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener case LAYOUT_DIRECTION_LTR: default: if (mOverride) { - mDrawableLeft = mDrawableStart; + mShowing[Drawables.LEFT] = mDrawableStart; mDrawableSizeLeft = mDrawableSizeStart; mDrawableHeightLeft = mDrawableHeightStart; - mDrawableRight = mDrawableEnd; + mShowing[Drawables.RIGHT] = mDrawableEnd; mDrawableSizeRight = mDrawableSizeEnd; mDrawableHeightRight = mDrawableHeightEnd; } @@ -395,17 +414,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } private void updateDrawablesLayoutDirection(int layoutDirection) { - if (mDrawableLeft != null) { - mDrawableLeft.setLayoutDirection(layoutDirection); - } - if (mDrawableRight != null) { - mDrawableRight.setLayoutDirection(layoutDirection); - } - if (mDrawableTop != null) { - mDrawableTop.setLayoutDirection(layoutDirection); - } - if (mDrawableBottom != null) { - mDrawableBottom.setLayoutDirection(layoutDirection); + for (Drawable dr : mShowing) { + if (dr != null) { + dr.setLayoutDirection(layoutDirection); + } } } @@ -415,10 +427,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } mDrawableError = dr; - final Rect compoundRect = mCompoundRect; - int[] state = tv.getDrawableState(); - if (mDrawableError != null) { + final Rect compoundRect = mCompoundRect; + final int[] state = tv.getDrawableState(); + mDrawableError.setState(state); mDrawableError.copyBounds(compoundRect); mDrawableError.setCallback(tv); @@ -433,12 +445,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // first restore the initial state if needed switch (mDrawableSaved) { case DRAWABLE_LEFT: - mDrawableLeft = mDrawableTemp; + mShowing[Drawables.LEFT] = mDrawableTemp; mDrawableSizeLeft = mDrawableSizeTemp; mDrawableHeightLeft = mDrawableHeightTemp; break; case DRAWABLE_RIGHT: - mDrawableRight = mDrawableTemp; + mShowing[Drawables.RIGHT] = mDrawableTemp; mDrawableSizeRight = mDrawableSizeTemp; mDrawableHeightRight = mDrawableHeightTemp; break; @@ -451,11 +463,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener case LAYOUT_DIRECTION_RTL: mDrawableSaved = DRAWABLE_LEFT; - mDrawableTemp = mDrawableLeft; + mDrawableTemp = mShowing[Drawables.LEFT]; mDrawableSizeTemp = mDrawableSizeLeft; mDrawableHeightTemp = mDrawableHeightLeft; - mDrawableLeft = mDrawableError; + mShowing[Drawables.LEFT] = mDrawableError; mDrawableSizeLeft = mDrawableSizeError; mDrawableHeightLeft = mDrawableHeightError; break; @@ -463,11 +475,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener default: mDrawableSaved = DRAWABLE_RIGHT; - mDrawableTemp = mDrawableRight; + mDrawableTemp = mShowing[Drawables.RIGHT]; mDrawableSizeTemp = mDrawableSizeRight; mDrawableHeightTemp = mDrawableHeightRight; - mDrawableRight = mDrawableError; + mShowing[Drawables.RIGHT] = mDrawableError; mDrawableSizeRight = mDrawableSizeError; mDrawableHeightRight = mDrawableHeightError; break; @@ -520,6 +532,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private final TextPaint mTextPaint; private boolean mUserSetTextScaleX; private Layout mLayout; + private boolean mLocaleChanged = false; private int mGravity = Gravity.TOP | Gravity.START; private boolean mHorizontallyScrolling; @@ -623,16 +636,17 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener this(context, null); } - public TextView(Context context, AttributeSet attrs) { + public TextView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, com.android.internal.R.attr.textViewStyle); } - public TextView(Context context, AttributeSet attrs, int defStyleAttr) { + public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } @SuppressWarnings("deprecation") - public TextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + public TextView( + Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); mText = ""; @@ -771,6 +785,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener boolean selectallonfocus = false; Drawable drawableLeft = null, drawableTop = null, drawableRight = null, drawableBottom = null, drawableStart = null, drawableEnd = null; + ColorStateList drawableTint = null; + PorterDuff.Mode drawableTintMode = null; int drawablePadding = 0; int ellipsize = -1; boolean singleLine = false; @@ -856,6 +872,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener drawableEnd = a.getDrawable(attr); break; + case com.android.internal.R.styleable.TextView_drawableTint: + drawableTint = a.getColorStateList(attr); + break; + + case com.android.internal.R.styleable.TextView_drawableTintMode: + drawableTintMode = Drawable.parseTintMode(a.getInt(attr, -1), drawableTintMode); + break; + case com.android.internal.R.styleable.TextView_drawablePadding: drawablePadding = a.getDimensionPixelSize(attr, drawablePadding); break; @@ -1031,6 +1055,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener inputType = a.getInt(attr, EditorInfo.TYPE_NULL); break; + case com.android.internal.R.styleable.TextView_allowUndo: + createEditorIfNeeded(); + mEditor.mAllowUndo = a.getBoolean(attr, true); + break; + case com.android.internal.R.styleable.TextView_imeOptions: createEditorIfNeeded(); mEditor.createInputContentTypeIfNeeded(); @@ -1240,6 +1269,22 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener bufferType = BufferType.SPANNABLE; } + // Set up the tint (if needed) before setting the drawables so that it + // gets applied correctly. + if (drawableTint != null || drawableTintMode != null) { + if (mDrawables == null) { + mDrawables = new Drawables(context); + } + if (drawableTint != null) { + mDrawables.mTintList = drawableTint; + mDrawables.mHasTint = true; + } + if (drawableTintMode != null) { + mDrawables.mTintMode = drawableTintMode; + mDrawables.mHasTintMode = true; + } + } + // This call will save the initial left/right drawables setCompoundDrawablesWithIntrinsicBounds( drawableLeft, drawableTop, drawableRight, drawableBottom); @@ -1425,6 +1470,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } resetResolvedDrawables(); resolveDrawables(); + applyCompoundDrawableTint(); } } @@ -1572,7 +1618,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @hide */ public final UndoManager getUndoManager() { - return mEditor == null ? null : mEditor.mUndoManager; + // TODO: Consider supporting a global undo manager. + throw new UnsupportedOperationException("not implemented"); } /** @@ -1590,22 +1637,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @hide */ public final void setUndoManager(UndoManager undoManager, String tag) { - if (undoManager != null) { - createEditorIfNeeded(); - mEditor.mUndoManager = undoManager; - mEditor.mUndoOwner = undoManager.getOwner(tag, this); - mEditor.mUndoInputFilter = new Editor.UndoInputFilter(mEditor); - if (!(mText instanceof Editable)) { - setText(mText, BufferType.EDITABLE); - } - - setFilters((Editable) mText, mFilters); - } else if (mEditor != null) { - // XXX need to destroy all associated state. - mEditor.mUndoManager = null; - mEditor.mUndoOwner = null; - mEditor.mUndoInputFilter = null; - } + // TODO: Consider supporting a global undo manager. An implementation will need to: + // * createEditorIfNeeded() + // * Promote to BufferType.EDITABLE if needed. + // * Update the UndoManager and UndoOwner. + // Likewise it will need to be able to restore the default UndoManager. + throw new UnsupportedOperationException("not implemented"); } /** @@ -1783,7 +1820,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ public int getCompoundPaddingTop() { final Drawables dr = mDrawables; - if (dr == null || dr.mDrawableTop == null) { + if (dr == null || dr.mShowing[Drawables.TOP] == null) { return mPaddingTop; } else { return mPaddingTop + dr.mDrawablePadding + dr.mDrawableSizeTop; @@ -1796,7 +1833,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ public int getCompoundPaddingBottom() { final Drawables dr = mDrawables; - if (dr == null || dr.mDrawableBottom == null) { + if (dr == null || dr.mShowing[Drawables.BOTTOM] == null) { return mPaddingBottom; } else { return mPaddingBottom + dr.mDrawablePadding + dr.mDrawableSizeBottom; @@ -1809,7 +1846,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ public int getCompoundPaddingLeft() { final Drawables dr = mDrawables; - if (dr == null || dr.mDrawableLeft == null) { + if (dr == null || dr.mShowing[Drawables.LEFT] == null) { return mPaddingLeft; } else { return mPaddingLeft + dr.mDrawablePadding + dr.mDrawableSizeLeft; @@ -1822,7 +1859,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ public int getCompoundPaddingRight() { final Drawables dr = mDrawables; - if (dr == null || dr.mDrawableRight == null) { + if (dr == null || dr.mShowing[Drawables.RIGHT] == null) { return mPaddingRight; } else { return mPaddingRight + dr.mDrawablePadding + dr.mDrawableSizeRight; @@ -2020,14 +2057,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } else { // We need to retain the last set padding, so just clear // out all of the fields in the existing structure. - if (dr.mDrawableLeft != null) dr.mDrawableLeft.setCallback(null); - dr.mDrawableLeft = null; - if (dr.mDrawableTop != null) dr.mDrawableTop.setCallback(null); - dr.mDrawableTop = null; - if (dr.mDrawableRight != null) dr.mDrawableRight.setCallback(null); - dr.mDrawableRight = null; - if (dr.mDrawableBottom != null) dr.mDrawableBottom.setCallback(null); - dr.mDrawableBottom = null; + for (int i = dr.mShowing.length - 1; i >= 0; i--) { + if (dr.mShowing[i] != null) { + dr.mShowing[i].setCallback(null); + } + dr.mShowing[i] = null; + } dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0; dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0; dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0; @@ -2041,25 +2076,25 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mDrawables.mOverride = false; - if (dr.mDrawableLeft != left && dr.mDrawableLeft != null) { - dr.mDrawableLeft.setCallback(null); + if (dr.mShowing[Drawables.LEFT] != left && dr.mShowing[Drawables.LEFT] != null) { + dr.mShowing[Drawables.LEFT].setCallback(null); } - dr.mDrawableLeft = left; + dr.mShowing[Drawables.LEFT] = left; - if (dr.mDrawableTop != top && dr.mDrawableTop != null) { - dr.mDrawableTop.setCallback(null); + if (dr.mShowing[Drawables.TOP] != top && dr.mShowing[Drawables.TOP] != null) { + dr.mShowing[Drawables.TOP].setCallback(null); } - dr.mDrawableTop = top; + dr.mShowing[Drawables.TOP] = top; - if (dr.mDrawableRight != right && dr.mDrawableRight != null) { - dr.mDrawableRight.setCallback(null); + if (dr.mShowing[Drawables.RIGHT] != right && dr.mShowing[Drawables.RIGHT] != null) { + dr.mShowing[Drawables.RIGHT].setCallback(null); } - dr.mDrawableRight = right; + dr.mShowing[Drawables.RIGHT] = right; - if (dr.mDrawableBottom != bottom && dr.mDrawableBottom != null) { - dr.mDrawableBottom.setCallback(null); + if (dr.mShowing[Drawables.BOTTOM] != bottom && dr.mShowing[Drawables.BOTTOM] != null) { + dr.mShowing[Drawables.BOTTOM].setCallback(null); } - dr.mDrawableBottom = bottom; + dr.mShowing[Drawables.BOTTOM] = bottom; final Rect compoundRect = dr.mCompoundRect; int[] state; @@ -2115,6 +2150,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener resetResolvedDrawables(); resolveDrawables(); + applyCompoundDrawableTint(); invalidate(); requestLayout(); } @@ -2138,7 +2174,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @attr ref android.R.styleable#TextView_drawableBottom */ @android.view.RemotableViewMethod - public void setCompoundDrawablesWithIntrinsicBounds(int left, int top, int right, int bottom) { + public void setCompoundDrawablesWithIntrinsicBounds(@DrawableRes int left, + @DrawableRes int top, @DrawableRes int right, @DrawableRes int bottom) { final Context context = getContext(); setCompoundDrawablesWithIntrinsicBounds(left != 0 ? context.getDrawable(left) : null, top != 0 ? context.getDrawable(top) : null, @@ -2198,10 +2235,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // We're switching to relative, discard absolute. if (dr != null) { - if (dr.mDrawableLeft != null) dr.mDrawableLeft.setCallback(null); - dr.mDrawableLeft = dr.mDrawableLeftInitial = null; - if (dr.mDrawableRight != null) dr.mDrawableRight.setCallback(null); - dr.mDrawableRight = dr.mDrawableRightInitial = null; + if (dr.mShowing[Drawables.LEFT] != null) { + dr.mShowing[Drawables.LEFT].setCallback(null); + } + dr.mShowing[Drawables.LEFT] = dr.mDrawableLeftInitial = null; + if (dr.mShowing[Drawables.RIGHT] != null) { + dr.mShowing[Drawables.RIGHT].setCallback(null); + } + dr.mShowing[Drawables.RIGHT] = dr.mDrawableRightInitial = null; dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0; dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0; } @@ -2219,12 +2260,18 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // out all of the fields in the existing structure. if (dr.mDrawableStart != null) dr.mDrawableStart.setCallback(null); dr.mDrawableStart = null; - if (dr.mDrawableTop != null) dr.mDrawableTop.setCallback(null); - dr.mDrawableTop = null; - if (dr.mDrawableEnd != null) dr.mDrawableEnd.setCallback(null); + if (dr.mShowing[Drawables.TOP] != null) { + dr.mShowing[Drawables.TOP].setCallback(null); + } + dr.mShowing[Drawables.TOP] = null; + if (dr.mDrawableEnd != null) { + dr.mDrawableEnd.setCallback(null); + } dr.mDrawableEnd = null; - if (dr.mDrawableBottom != null) dr.mDrawableBottom.setCallback(null); - dr.mDrawableBottom = null; + if (dr.mShowing[Drawables.BOTTOM] != null) { + dr.mShowing[Drawables.BOTTOM].setCallback(null); + } + dr.mShowing[Drawables.BOTTOM] = null; dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0; dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0; dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0; @@ -2243,20 +2290,20 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } dr.mDrawableStart = start; - if (dr.mDrawableTop != top && dr.mDrawableTop != null) { - dr.mDrawableTop.setCallback(null); + if (dr.mShowing[Drawables.TOP] != top && dr.mShowing[Drawables.TOP] != null) { + dr.mShowing[Drawables.TOP].setCallback(null); } - dr.mDrawableTop = top; + dr.mShowing[Drawables.TOP] = top; if (dr.mDrawableEnd != end && dr.mDrawableEnd != null) { dr.mDrawableEnd.setCallback(null); } dr.mDrawableEnd = end; - if (dr.mDrawableBottom != bottom && dr.mDrawableBottom != null) { - dr.mDrawableBottom.setCallback(null); + if (dr.mShowing[Drawables.BOTTOM] != bottom && dr.mShowing[Drawables.BOTTOM] != null) { + dr.mShowing[Drawables.BOTTOM].setCallback(null); } - dr.mDrawableBottom = bottom; + dr.mShowing[Drawables.BOTTOM] = bottom; final Rect compoundRect = dr.mCompoundRect; int[] state; @@ -2329,8 +2376,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @attr ref android.R.styleable#TextView_drawableBottom */ @android.view.RemotableViewMethod - public void setCompoundDrawablesRelativeWithIntrinsicBounds(int start, int top, int end, - int bottom) { + public void setCompoundDrawablesRelativeWithIntrinsicBounds(@DrawableRes int start, + @DrawableRes int top, @DrawableRes int end, @DrawableRes int bottom) { final Context context = getContext(); setCompoundDrawablesRelativeWithIntrinsicBounds( start != 0 ? context.getDrawable(start) : null, @@ -2382,9 +2429,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public Drawable[] getCompoundDrawables() { final Drawables dr = mDrawables; if (dr != null) { - return new Drawable[] { - dr.mDrawableLeft, dr.mDrawableTop, dr.mDrawableRight, dr.mDrawableBottom - }; + return dr.mShowing.clone(); } else { return new Drawable[] { null, null, null, null }; } @@ -2403,7 +2448,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final Drawables dr = mDrawables; if (dr != null) { return new Drawable[] { - dr.mDrawableStart, dr.mDrawableTop, dr.mDrawableEnd, dr.mDrawableBottom + dr.mDrawableStart, dr.mShowing[Drawables.TOP], + dr.mDrawableEnd, dr.mShowing[Drawables.BOTTOM] }; } else { return new Drawable[] { null, null, null, null }; @@ -2444,6 +2490,118 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return dr != null ? dr.mDrawablePadding : 0; } + /** + * Applies a tint to the compound drawables. Does not modify the + * current tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default. + * <p> + * Subsequent calls to + * {@link #setCompoundDrawables(Drawable, Drawable, Drawable, Drawable)} + * and related methods will automatically mutate the drawables and apply + * the specified tint and tint mode using + * {@link Drawable#setTintList(ColorStateList)}. + * + * @param tint the tint to apply, may be {@code null} to clear tint + * + * @attr ref android.R.styleable#TextView_drawableTint + * @see #getCompoundDrawableTintList() + * @see Drawable#setTintList(ColorStateList) + */ + public void setCompoundDrawableTintList(@Nullable ColorStateList tint) { + if (mDrawables == null) { + mDrawables = new Drawables(getContext()); + } + mDrawables.mTintList = tint; + mDrawables.mHasTint = true; + + applyCompoundDrawableTint(); + } + + /** + * @return the tint applied to the compound drawables + * @attr ref android.R.styleable#TextView_drawableTint + * @see #setCompoundDrawableTintList(ColorStateList) + */ + public ColorStateList getCompoundDrawableTintList() { + return mDrawables != null ? mDrawables.mTintList : null; + } + + /** + * Specifies the blending mode used to apply the tint specified by + * {@link #setCompoundDrawableTintList(ColorStateList)} to the compound + * drawables. The default mode is {@link PorterDuff.Mode#SRC_IN}. + * + * @param tintMode the blending mode used to apply the tint, may be + * {@code null} to clear tint + * @attr ref android.R.styleable#TextView_drawableTintMode + * @see #setCompoundDrawableTintList(ColorStateList) + * @see Drawable#setTintMode(PorterDuff.Mode) + */ + public void setCompoundDrawableTintMode(@Nullable PorterDuff.Mode tintMode) { + if (mDrawables == null) { + mDrawables = new Drawables(getContext()); + } + mDrawables.mTintMode = tintMode; + mDrawables.mHasTintMode = true; + + applyCompoundDrawableTint(); + } + + /** + * Returns the blending mode used to apply the tint to the compound + * drawables, if specified. + * + * @return the blending mode used to apply the tint to the compound + * drawables + * @attr ref android.R.styleable#TextView_drawableTintMode + * @see #setCompoundDrawableTintMode(PorterDuff.Mode) + */ + public PorterDuff.Mode getCompoundDrawableTintMode() { + return mDrawables != null ? mDrawables.mTintMode : null; + } + + private void applyCompoundDrawableTint() { + if (mDrawables == null) { + return; + } + + if (mDrawables.mHasTint || mDrawables.mHasTintMode) { + final ColorStateList tintList = mDrawables.mTintList; + final PorterDuff.Mode tintMode = mDrawables.mTintMode; + final boolean hasTint = mDrawables.mHasTint; + final boolean hasTintMode = mDrawables.mHasTintMode; + final int[] state = getDrawableState(); + + for (Drawable dr : mDrawables.mShowing) { + if (dr == null) { + continue; + } + + if (dr == mDrawables.mDrawableError) { + // From a developer's perspective, the error drawable isn't + // a compound drawable. Don't apply the generic compound + // drawable tint to it. + continue; + } + + dr.mutate(); + + if (hasTint) { + dr.setTintList(tintList); + } + + if (hasTintMode) { + dr.setTintMode(tintMode); + } + + // The drawable (or one of its children) may not have been + // stateful before applying the tint, so let's try again. + if (dr.isStateful()) { + dr.setState(state); + } + } + } + } + @Override public void setPadding(int left, int top, int right, int bottom) { if (left != mPaddingLeft || @@ -2484,94 +2642,92 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * Sets the text appearance from the specified style resource. + * <p> + * Use a framework-defined {@code TextAppearance} style like + * {@link android.R.style#TextAppearance_Material_Body1 @android:style/TextAppearance.Material.Body1} + * or see {@link android.R.styleable#TextAppearance TextAppearance} for the + * set of attributes that can be used in a custom style. + * + * @param resId the resource identifier of the style to apply + * @attr ref android.R.styleable#TextView_textAppearance + */ + @SuppressWarnings("deprecation") + public void setTextAppearance(@StyleRes int resId) { + setTextAppearance(mContext, resId); + } + + /** * Sets the text color, size, style, hint color, and highlight color * from the specified TextAppearance resource. + * + * @deprecated Use {@link #setTextAppearance(int)} instead. */ - public void setTextAppearance(Context context, int resid) { - TypedArray appearance = - context.obtainStyledAttributes(resid, - com.android.internal.R.styleable.TextAppearance); - - int color; - ColorStateList colors; - int ts; + @Deprecated + public void setTextAppearance(Context context, @StyleRes int resId) { + final TypedArray ta = context.obtainStyledAttributes(resId, R.styleable.TextAppearance); - color = appearance.getColor( - com.android.internal.R.styleable.TextAppearance_textColorHighlight, 0); - if (color != 0) { - setHighlightColor(color); + final int textColorHighlight = ta.getColor( + R.styleable.TextAppearance_textColorHighlight, 0); + if (textColorHighlight != 0) { + setHighlightColor(textColorHighlight); } - colors = appearance.getColorStateList(com.android.internal.R.styleable. - TextAppearance_textColor); - if (colors != null) { - setTextColor(colors); + final ColorStateList textColor = ta.getColorStateList(R.styleable.TextAppearance_textColor); + if (textColor != null) { + setTextColor(textColor); } - ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable. - TextAppearance_textSize, 0); - if (ts != 0) { - setRawTextSize(ts); + final int textSize = ta.getDimensionPixelSize(R.styleable.TextAppearance_textSize, 0); + if (textSize != 0) { + setRawTextSize(textSize); } - colors = appearance.getColorStateList(com.android.internal.R.styleable. - TextAppearance_textColorHint); - if (colors != null) { - setHintTextColor(colors); + final ColorStateList textColorHint = ta.getColorStateList( + R.styleable.TextAppearance_textColorHint); + if (textColorHint != null) { + setHintTextColor(textColorHint); } - colors = appearance.getColorStateList(com.android.internal.R.styleable. - TextAppearance_textColorLink); - if (colors != null) { - setLinkTextColor(colors); + final ColorStateList textColorLink = ta.getColorStateList( + R.styleable.TextAppearance_textColorLink); + if (textColorLink != null) { + setLinkTextColor(textColorLink); } - String familyName; - int typefaceIndex, styleIndex; - - familyName = appearance.getString(com.android.internal.R.styleable. - TextAppearance_fontFamily); - typefaceIndex = appearance.getInt(com.android.internal.R.styleable. - TextAppearance_typeface, -1); - styleIndex = appearance.getInt(com.android.internal.R.styleable. - TextAppearance_textStyle, -1); - - setTypefaceFromAttrs(familyName, typefaceIndex, styleIndex); - - final int shadowcolor = appearance.getInt( - com.android.internal.R.styleable.TextAppearance_shadowColor, 0); - if (shadowcolor != 0) { - final float dx = appearance.getFloat( - com.android.internal.R.styleable.TextAppearance_shadowDx, 0); - final float dy = appearance.getFloat( - com.android.internal.R.styleable.TextAppearance_shadowDy, 0); - final float r = appearance.getFloat( - com.android.internal.R.styleable.TextAppearance_shadowRadius, 0); + final String fontFamily = ta.getString(R.styleable.TextAppearance_fontFamily); + final int typefaceIndex = ta.getInt(R.styleable.TextAppearance_typeface, -1); + final int styleIndex = ta.getInt(R.styleable.TextAppearance_textStyle, -1); + setTypefaceFromAttrs(fontFamily, typefaceIndex, styleIndex); - setShadowLayer(r, dx, dy, shadowcolor); + final int shadowColor = ta.getInt(R.styleable.TextAppearance_shadowColor, 0); + if (shadowColor != 0) { + final float dx = ta.getFloat(R.styleable.TextAppearance_shadowDx, 0); + final float dy = ta.getFloat(R.styleable.TextAppearance_shadowDy, 0); + final float r = ta.getFloat(R.styleable.TextAppearance_shadowRadius, 0); + setShadowLayer(r, dx, dy, shadowColor); } - if (appearance.getBoolean(com.android.internal.R.styleable.TextAppearance_textAllCaps, - false)) { + if (ta.getBoolean(R.styleable.TextAppearance_textAllCaps, false)) { setTransformationMethod(new AllCapsTransformationMethod(getContext())); } - if (appearance.hasValue(com.android.internal.R.styleable.TextAppearance_elegantTextHeight)) { - setElegantTextHeight(appearance.getBoolean( - com.android.internal.R.styleable.TextAppearance_elegantTextHeight, false)); + if (ta.hasValue(R.styleable.TextAppearance_elegantTextHeight)) { + setElegantTextHeight(ta.getBoolean( + R.styleable.TextAppearance_elegantTextHeight, false)); } - if (appearance.hasValue(com.android.internal.R.styleable.TextAppearance_letterSpacing)) { - setLetterSpacing(appearance.getFloat( - com.android.internal.R.styleable.TextAppearance_letterSpacing, 0)); + if (ta.hasValue(R.styleable.TextAppearance_letterSpacing)) { + setLetterSpacing(ta.getFloat( + R.styleable.TextAppearance_letterSpacing, 0)); } - if (appearance.hasValue(com.android.internal.R.styleable.TextAppearance_fontFeatureSettings)) { - setFontFeatureSettings(appearance.getString( - com.android.internal.R.styleable.TextAppearance_fontFeatureSettings)); + if (ta.hasValue(R.styleable.TextAppearance_fontFeatureSettings)) { + setFontFeatureSettings(ta.getString( + R.styleable.TextAppearance_fontFeatureSettings)); } - appearance.recycle(); + ta.recycle(); } /** @@ -2592,9 +2748,18 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @see Paint#setTextLocale */ public void setTextLocale(Locale locale) { + mLocaleChanged = true; mTextPaint.setTextLocale(locale); } + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + if (!mLocaleChanged) { + mTextPaint.setTextLocale(Locale.getDefault()); + } + } + /** * @return the size (in pixels) of the default text size in this TextView. */ @@ -2829,7 +2994,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @attr ref android.R.styleable#TextView_textColor */ @android.view.RemotableViewMethod - public void setTextColor(int color) { + public void setTextColor(@ColorInt int color) { mTextColor = ColorStateList.valueOf(color); updateTextColors(); } @@ -2870,6 +3035,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @return Returns the current text color. */ + @ColorInt public final int getCurrentTextColor() { return mCurTextColor; } @@ -2880,7 +3046,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @attr ref android.R.styleable#TextView_textColorHighlight */ @android.view.RemotableViewMethod - public void setHighlightColor(int color) { + public void setHighlightColor(@ColorInt int color) { if (mHighlightColor != color) { mHighlightColor = color; invalidate(); @@ -2894,6 +3060,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_textColorHighlight */ + @ColorInt public int getHighlightColor() { return mHighlightColor; } @@ -2988,6 +3155,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_shadowColor */ + @ColorInt public int getShadowColor() { return mShadowColor; } @@ -3063,7 +3231,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @attr ref android.R.styleable#TextView_textColorHint */ @android.view.RemotableViewMethod - public final void setHintTextColor(int color) { + public final void setHintTextColor(@ColorInt int color) { mHintTextColor = ColorStateList.valueOf(color); updateTextColors(); } @@ -3102,6 +3270,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @return Returns the current hint text color. */ + @ColorInt public final int getCurrentHintTextColor() { return mHintTextColor != null ? mCurHintTextColor : mCurTextColor; } @@ -3115,7 +3284,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @attr ref android.R.styleable#TextView_textColorLink */ @android.view.RemotableViewMethod - public final void setLinkTextColor(int color) { + public final void setLinkTextColor(@ColorInt int color) { mLinkTextColor = ColorStateList.valueOf(color); updateTextColors(); } @@ -3662,26 +3831,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener updateTextColors(); } - final Drawables dr = mDrawables; - if (dr != null) { - int[] state = getDrawableState(); - if (dr.mDrawableTop != null && dr.mDrawableTop.isStateful()) { - dr.mDrawableTop.setState(state); - } - if (dr.mDrawableBottom != null && dr.mDrawableBottom.isStateful()) { - dr.mDrawableBottom.setState(state); - } - if (dr.mDrawableLeft != null && dr.mDrawableLeft.isStateful()) { - dr.mDrawableLeft.setState(state); - } - if (dr.mDrawableRight != null && dr.mDrawableRight.isStateful()) { - dr.mDrawableRight.setState(state); - } - if (dr.mDrawableStart != null && dr.mDrawableStart.isStateful()) { - dr.mDrawableStart.setState(state); - } - if (dr.mDrawableEnd != null && dr.mDrawableEnd.isStateful()) { - dr.mDrawableEnd.setState(state); + if (mDrawables != null) { + final int[] state = getDrawableState(); + for (Drawable dr : mDrawables.mShowing) { + if (dr != null && dr.isStateful()) { + dr.setState(state); + } } } } @@ -3690,25 +3845,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public void drawableHotspotChanged(float x, float y) { super.drawableHotspotChanged(x, y); - final Drawables dr = mDrawables; - if (dr != null) { - if (dr.mDrawableTop != null) { - dr.mDrawableTop.setHotspot(x, y); - } - if (dr.mDrawableBottom != null) { - dr.mDrawableBottom.setHotspot(x, y); - } - if (dr.mDrawableLeft != null) { - dr.mDrawableLeft.setHotspot(x, y); - } - if (dr.mDrawableRight != null) { - dr.mDrawableRight.setHotspot(x, y); - } - if (dr.mDrawableStart != null) { - dr.mDrawableStart.setHotspot(x, y); - } - if (dr.mDrawableEnd != null) { - dr.mDrawableEnd.setHotspot(x, y); + if (mDrawables != null) { + final int[] state = getDrawableState(); + for (Drawable dr : mDrawables.mShowing) { + if (dr != null && dr.isStateful()) { + dr.setHotspot(x, y); + } } } } @@ -3756,6 +3898,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener ss.error = getError(); + if (mEditor != null) { + ss.editorState = mEditor.saveInstanceState(); + } return ss; } @@ -3825,6 +3970,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } }); } + + if (ss.editorState != null) { + createEditorIfNeeded(); + mEditor.restoreInstanceState(ss.editorState); + } } /** @@ -3971,6 +4121,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (type == BufferType.EDITABLE || getKeyListener() != null || needEditableForNotification) { createEditorIfNeeded(); + mEditor.forgetUndoRedo(); Editable t = mEditableFactory.newEditable(text); text = t; setFilters(t, mFilters); @@ -4129,11 +4280,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } @android.view.RemotableViewMethod - public final void setText(int resid) { + public final void setText(@StringRes int resid) { setText(getContext().getResources().getText(resid)); } - public final void setText(int resid, BufferType type) { + public final void setText(@StringRes int resid, BufferType type) { setText(getContext().getResources().getText(resid), type); } @@ -4169,7 +4320,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @attr ref android.R.styleable#TextView_hint */ @android.view.RemotableViewMethod - public final void setHint(int resid) { + public final void setHint(@StringRes int resid) { setHint(getContext().getResources().getText(resid)); } @@ -4573,7 +4724,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @see EditorInfo#extras * @attr ref android.R.styleable#TextView_editorExtras */ - public void setInputExtras(int xmlResId) throws XmlPullParserException, IOException { + public void setInputExtras(@XmlRes int xmlResId) throws XmlPullParserException, IOException { createEditorIfNeeded(); XmlResourceParser parser = getResources().getXml(xmlResId); mEditor.createInputContentTypeIfNeeded(); @@ -4951,7 +5102,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // - onFocusChanged cannot start it when focus is given to a view with selected text (after // a screen rotation) since layout is not yet initialized at that point. if (mEditor != null && mEditor.mCreatedWithASelection) { - mEditor.startSelectionActionMode(); + mEditor.startSelectionActionModeWithSelection(); mEditor.mCreatedWithASelection = false; } @@ -4959,7 +5110,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // ExtractEditText does not call onFocus when it is displayed, and mHasSelectionOnFocus can // not be set. Do the test here instead. if (this instanceof ExtractEditText && hasSelection() && mEditor != null) { - mEditor.startSelectionActionMode(); + mEditor.startSelectionActionModeWithSelection(); } unregisterForPreDraw(); @@ -5039,9 +5190,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener protected boolean verifyDrawable(Drawable who) { final boolean verified = super.verifyDrawable(who); if (!verified && mDrawables != null) { - return who == mDrawables.mDrawableLeft || who == mDrawables.mDrawableTop || - who == mDrawables.mDrawableRight || who == mDrawables.mDrawableBottom || - who == mDrawables.mDrawableStart || who == mDrawables.mDrawableEnd; + for (Drawable dr : mDrawables.mShowing) { + if (who == dr) { + return true; + } + } } return verified; } @@ -5050,23 +5203,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public void jumpDrawablesToCurrentState() { super.jumpDrawablesToCurrentState(); if (mDrawables != null) { - if (mDrawables.mDrawableLeft != null) { - mDrawables.mDrawableLeft.jumpToCurrentState(); - } - if (mDrawables.mDrawableTop != null) { - mDrawables.mDrawableTop.jumpToCurrentState(); - } - if (mDrawables.mDrawableRight != null) { - mDrawables.mDrawableRight.jumpToCurrentState(); - } - if (mDrawables.mDrawableBottom != null) { - mDrawables.mDrawableBottom.jumpToCurrentState(); - } - if (mDrawables.mDrawableStart != null) { - mDrawables.mDrawableStart.jumpToCurrentState(); - } - if (mDrawables.mDrawableEnd != null) { - mDrawables.mDrawableEnd.jumpToCurrentState(); + for (Drawable dr : mDrawables.mShowing) { + if (dr != null) { + dr.jumpToCurrentState(); + } } } } @@ -5085,7 +5225,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // accordingly. final TextView.Drawables drawables = mDrawables; if (drawables != null) { - if (drawable == drawables.mDrawableLeft) { + if (drawable == drawables.mShowing[Drawables.LEFT]) { final int compoundPaddingTop = getCompoundPaddingTop(); final int compoundPaddingBottom = getCompoundPaddingBottom(); final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop; @@ -5093,7 +5233,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener scrollX += mPaddingLeft; scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightLeft) / 2; handled = true; - } else if (drawable == drawables.mDrawableRight) { + } else if (drawable == drawables.mShowing[Drawables.RIGHT]) { final int compoundPaddingTop = getCompoundPaddingTop(); final int compoundPaddingBottom = getCompoundPaddingBottom(); final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop; @@ -5101,7 +5241,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener scrollX += (mRight - mLeft - mPaddingRight - drawables.mDrawableSizeRight); scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightRight) / 2; handled = true; - } else if (drawable == drawables.mDrawableTop) { + } else if (drawable == drawables.mShowing[Drawables.TOP]) { final int compoundPaddingLeft = getCompoundPaddingLeft(); final int compoundPaddingRight = getCompoundPaddingRight(); final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft; @@ -5109,7 +5249,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthTop) / 2; scrollY += mPaddingTop; handled = true; - } else if (drawable == drawables.mDrawableBottom) { + } else if (drawable == drawables.mShowing[Drawables.BOTTOM]) { final int compoundPaddingLeft = getCompoundPaddingLeft(); final int compoundPaddingRight = getCompoundPaddingRight(); final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft; @@ -5313,44 +5453,44 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // IMPORTANT: The coordinates computed are also used in invalidateDrawable() // Make sure to update invalidateDrawable() when changing this code. - if (dr.mDrawableLeft != null) { + if (dr.mShowing[Drawables.LEFT] != null) { canvas.save(); canvas.translate(scrollX + mPaddingLeft + leftOffset, scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightLeft) / 2); - dr.mDrawableLeft.draw(canvas); + dr.mShowing[Drawables.LEFT].draw(canvas); canvas.restore(); } // IMPORTANT: The coordinates computed are also used in invalidateDrawable() // Make sure to update invalidateDrawable() when changing this code. - if (dr.mDrawableRight != null) { + if (dr.mShowing[Drawables.RIGHT] != null) { canvas.save(); canvas.translate(scrollX + right - left - mPaddingRight - dr.mDrawableSizeRight - rightOffset, scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightRight) / 2); - dr.mDrawableRight.draw(canvas); + dr.mShowing[Drawables.RIGHT].draw(canvas); canvas.restore(); } // IMPORTANT: The coordinates computed are also used in invalidateDrawable() // Make sure to update invalidateDrawable() when changing this code. - if (dr.mDrawableTop != null) { + if (dr.mShowing[Drawables.TOP] != null) { canvas.save(); canvas.translate(scrollX + compoundPaddingLeft + (hspace - dr.mDrawableWidthTop) / 2, scrollY + mPaddingTop); - dr.mDrawableTop.draw(canvas); + dr.mShowing[Drawables.TOP].draw(canvas); canvas.restore(); } // IMPORTANT: The coordinates computed are also used in invalidateDrawable() // Make sure to update invalidateDrawable() when changing this code. - if (dr.mDrawableBottom != null) { + if (dr.mShowing[Drawables.BOTTOM] != null) { canvas.save(); canvas.translate(scrollX + compoundPaddingLeft + (hspace - dr.mDrawableWidthBottom) / 2, scrollY + bottom - top - mPaddingBottom - dr.mDrawableSizeBottom); - dr.mDrawableBottom.draw(canvas); + dr.mShowing[Drawables.BOTTOM].draw(canvas); canvas.restore(); } } @@ -7961,7 +8101,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public boolean onTouchEvent(MotionEvent event) { final int action = event.getActionMasked(); - if (mEditor != null) mEditor.onTouchEvent(event); + if (mEditor != null) { + mEditor.onTouchEvent(event); + + if (mEditor.mSelectionModifierCursorController != null && + mEditor.mSelectionModifierCursorController.isDragAcceleratorActive()) { + return true; + } + } final boolean superResult = super.onTouchEvent(event); @@ -8243,14 +8390,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Override public boolean onKeyShortcut(int keyCode, KeyEvent event) { - final int filteredMetaState = event.getMetaState() & ~KeyEvent.META_CTRL_MASK; - if (KeyEvent.metaStateHasNoModifiers(filteredMetaState)) { + if (event.hasModifiers(KeyEvent.META_CTRL_ON)) { + // Handle Ctrl-only shortcuts. switch (keyCode) { case KeyEvent.KEYCODE_A: if (canSelectText()) { return onTextContextMenuItem(ID_SELECT_ALL); } break; + case KeyEvent.KEYCODE_Z: + if (canUndo()) { + return onTextContextMenuItem(ID_UNDO); + } + break; case KeyEvent.KEYCODE_X: if (canCut()) { return onTextContextMenuItem(ID_CUT); @@ -8267,6 +8419,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } break; } + } else if (event.hasModifiers(KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON)) { + // Handle Ctrl-Shift shortcuts. + switch (keyCode) { + case KeyEvent.KEYCODE_Z: + if (canRedo()) { + return onTextContextMenuItem(ID_REDO); + } + break; + case KeyEvent.KEYCODE_V: + if (canPaste()) { + return onTextContextMenuItem(ID_PASTE_AS_PLAIN_TEXT); + } + } } return super.onKeyShortcut(keyCode, event); } @@ -8381,9 +8546,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } + /** @hide */ @Override - public void onPopulateAccessibilityEvent(AccessibilityEvent event) { - super.onPopulateAccessibilityEvent(event); + public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) { + super.onPopulateAccessibilityEventInternal(event); final boolean isPassword = hasPasswordTransformationMethod(); if (!isPassword || shouldSpeakPasswordsForAccessibility()) { @@ -8405,10 +8571,26 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); + public CharSequence getAccessibilityClassName() { + return TextView.class.getName(); + } + + @Override + public void onProvideAssistStructure(ViewAssistStructure structure, Bundle extras) { + super.onProvideAssistStructure(structure, extras); + final boolean isPassword = hasPasswordTransformationMethod(); + if (!isPassword) { + structure.setText(getText(), getSelectionStart(), getSelectionEnd()); + structure.setTextPaint(mTextPaint); + } + structure.setHint(getHint()); + } + + /** @hide */ + @Override + public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { + super.onInitializeAccessibilityEventInternal(event); - event.setClassName(TextView.class.getName()); final boolean isPassword = hasPasswordTransformationMethod(); event.setPassword(isPassword); @@ -8419,11 +8601,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } + /** @hide */ @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); + public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfoInternal(info); - info.setClassName(TextView.class.getName()); final boolean isPassword = hasPasswordTransformationMethod(); info.setPassword(isPassword); @@ -8561,7 +8743,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener Selection.setSelection((Spannable) text, start, end); // Make sure selection mode is engaged. if (mEditor != null) { - mEditor.startSelectionActionMode(); + mEditor.startSelectionActionModeWithSelection(); } return true; } @@ -8579,15 +8761,16 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } + /** @hide */ @Override - public void sendAccessibilityEvent(int eventType) { + public void sendAccessibilityEventInternal(int eventType) { // Do not send scroll events since first they are not interesting for // accessibility and second such events a generated too frequently. // For details see the implementation of bringTextIntoView(). if (eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED) { return; } - super.sendAccessibilityEvent(eventType); + super.sendAccessibilityEventInternal(eventType); } /** @@ -8626,9 +8809,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } static final int ID_SELECT_ALL = android.R.id.selectAll; + static final int ID_UNDO = android.R.id.undo; + static final int ID_REDO = android.R.id.redo; static final int ID_CUT = android.R.id.cut; static final int ID_COPY = android.R.id.copy; static final int ID_PASTE = android.R.id.paste; + static final int ID_PASTE_AS_PLAIN_TEXT = android.R.id.pasteAsPlainText; + static final int ID_REPLACE = android.R.id.replaceText; /** * Called when a context menu option for the text view is selected. Currently @@ -8656,8 +8843,24 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener selectAllText(); return true; + case ID_UNDO: + if (mEditor != null) { + mEditor.undo(); + } + return true; // Returns true even if nothing was undone. + + case ID_REDO: + if (mEditor != null) { + mEditor.redo(); + } + return true; // Returns true even if nothing was undone. + case ID_PASTE: - paste(min, max); + paste(min, max, true /* withFormatting */); + return true; + + case ID_PASTE_AS_PLAIN_TEXT: + paste(min, max, false /* withFormatting */); return true; case ID_CUT: @@ -8752,9 +8955,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * A custom implementation can add new entries in the default menu in its * {@link android.view.ActionMode.Callback#onPrepareActionMode(ActionMode, Menu)} method. The - * default actions can also be removed from the menu using {@link Menu#removeItem(int)} and - * passing {@link android.R.id#selectAll}, {@link android.R.id#cut}, {@link android.R.id#copy} - * or {@link android.R.id#paste} ids as parameters. + * default actions can also be removed from the menu using + * {@link android.view.Menu#removeItem(int)} and passing {@link android.R.id#selectAll}, + * {@link android.R.id#cut}, {@link android.R.id#copy} or {@link android.R.id#paste} ids as + * parameters. * * Returning false from * {@link android.view.ActionMode.Callback#onCreateActionMode(ActionMode, Menu)} will prevent @@ -8785,7 +8989,17 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @hide */ protected void stopSelectionActionMode() { - mEditor.stopSelectionActionMode(); + if (mEditor != null) { + mEditor.stopSelectionActionMode(); + } + } + + boolean canUndo() { + return mEditor != null && mEditor.canUndo(); + } + + boolean canRedo() { + return mEditor != null && mEditor.canRedo(); } boolean canCut() { @@ -8831,14 +9045,21 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** * Paste clipboard content between min and max positions. */ - private void paste(int min, int max) { + private void paste(int min, int max, boolean withFormatting) { ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); ClipData clip = clipboard.getPrimaryClip(); if (clip != null) { boolean didFirst = false; for (int i=0; i<clip.getItemCount(); i++) { - CharSequence paste = clip.getItemAt(i).coerceToStyledText(getContext()); + final CharSequence paste; + if (withFormatting) { + paste = clip.getItemAt(i).coerceToStyledText(getContext()); + } else { + // Get an item as text and remove all spans by toString(). + final CharSequence text = clip.getItemAt(i).coerceToText(getContext()); + paste = (text instanceof Spanned) ? text.toString() : text; + } if (paste != null) { if (!didFirst) { Selection.setSelection((Spannable) mText, max); @@ -8896,7 +9117,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return getLayout().getLineForVertical((int) y); } - private int getOffsetAtCoordinate(int line, float x) { + int getOffsetAtCoordinate(int line, float x) { x = convertToLocalHorizontalCoordinate(x); return getLayout().getOffsetForHorizontal(line, x); } @@ -9155,6 +9376,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener CharSequence text; boolean frozenWithFocus; CharSequence error; + ParcelableParcel editorState; // Optional state from Editor. SavedState(Parcelable superState) { super(superState); @@ -9174,6 +9396,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener out.writeInt(1); TextUtils.writeToParcel(error, out, flags); } + + if (editorState == null) { + out.writeInt(0); + } else { + out.writeInt(1); + editorState.writeToParcel(out, flags); + } } @Override @@ -9209,6 +9438,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (in.readInt() != 0) { error = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); } + + if (in.readInt() != 0) { + editorState = ParcelableParcel.CREATOR.createFromParcel(in); + } } } @@ -9300,7 +9533,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // TODO: Add an option to configure this private static final float MARQUEE_DELTA_MAX = 0.07f; private static final int MARQUEE_DELAY = 1200; - private static final int MARQUEE_RESTART_DELAY = 1200; private static final int MARQUEE_DP_PER_SECOND = 30; private static final byte MARQUEE_STOPPED = 0x0; diff --git a/core/java/android/widget/TextViewWithCircularIndicator.java b/core/java/android/widget/TextViewWithCircularIndicator.java index 43c0843..d3c786c 100644 --- a/core/java/android/widget/TextViewWithCircularIndicator.java +++ b/core/java/android/widget/TextViewWithCircularIndicator.java @@ -18,7 +18,6 @@ package android.widget; import android.content.Context; import android.content.res.Resources; -import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Typeface; @@ -27,14 +26,8 @@ import android.util.AttributeSet; import com.android.internal.R; class TextViewWithCircularIndicator extends TextView { - - private static final int SELECTED_CIRCLE_ALPHA = 60; - private final Paint mCirclePaint = new Paint(); - private final String mItemIsSelectedText; - private int mCircleColor; - private boolean mDrawIndicator; public TextViewWithCircularIndicator(Context context) { this(context, null); @@ -50,22 +43,11 @@ class TextViewWithCircularIndicator extends TextView { public TextViewWithCircularIndicator(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs); - - - // Use Theme attributes if possible - final TypedArray a = mContext.obtainStyledAttributes(attrs, - R.styleable.DatePicker, defStyleAttr, defStyleRes); - final int resId = a.getResourceId(R.styleable.DatePicker_yearListItemTextAppearance, -1); - if (resId != -1) { - setTextAppearance(context, resId); - } + super(context, attrs, defStyleAttr, defStyleRes); final Resources res = context.getResources(); mItemIsSelectedText = res.getString(R.string.item_is_selected); - a.recycle(); - init(); } @@ -77,33 +59,26 @@ class TextViewWithCircularIndicator extends TextView { } public void setCircleColor(int color) { - if (color != mCircleColor) { - mCircleColor = color; - mCirclePaint.setColor(mCircleColor); - mCirclePaint.setAlpha(SELECTED_CIRCLE_ALPHA); - requestLayout(); - } - } - - public void setDrawIndicator(boolean drawIndicator) { - mDrawIndicator = drawIndicator; + mCirclePaint.setColor(color); + invalidate(); } @Override public void onDraw(Canvas canvas) { - super.onDraw(canvas); - if (mDrawIndicator) { + if (isActivated()) { final int width = getWidth(); final int height = getHeight(); - int radius = Math.min(width, height) / 2; + final int radius = Math.min(width, height) / 2; canvas.drawCircle(width / 2, height / 2, radius, mCirclePaint); } + + super.onDraw(canvas); } @Override public CharSequence getContentDescription() { - CharSequence itemText = getText(); - if (mDrawIndicator) { + final CharSequence itemText = getText(); + if (isActivated()) { return String.format(mItemIsSelectedText, itemText); } else { return itemText; diff --git a/core/java/android/widget/TimePicker.java b/core/java/android/widget/TimePicker.java index 26e02f8..944b491 100644 --- a/core/java/android/widget/TimePicker.java +++ b/core/java/android/widget/TimePicker.java @@ -24,8 +24,6 @@ import android.content.res.TypedArray; import android.os.Parcelable; import android.util.AttributeSet; import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; - import com.android.internal.R; import java.util.Locale; @@ -196,26 +194,14 @@ public class TimePicker extends FrameLayout { } @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - return mDelegate.dispatchPopulateAccessibilityEvent(event); - } - - @Override - public void onPopulateAccessibilityEvent(AccessibilityEvent event) { - super.onPopulateAccessibilityEvent(event); - mDelegate.onPopulateAccessibilityEvent(event); + public CharSequence getAccessibilityClassName() { + return TimePicker.class.getName(); } + /** @hide */ @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - mDelegate.onInitializeAccessibilityEvent(event); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - mDelegate.onInitializeAccessibilityNodeInfo(info); + public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) { + return mDelegate.dispatchPopulateAccessibilityEvent(event); } /** @@ -248,8 +234,6 @@ public class TimePicker extends FrameLayout { boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event); void onPopulateAccessibilityEvent(AccessibilityEvent event); - void onInitializeAccessibilityEvent(AccessibilityEvent event); - void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info); } /** diff --git a/core/java/android/widget/TimePickerClockDelegate.java b/core/java/android/widget/TimePickerClockDelegate.java index 1534429..9fdd718 100644 --- a/core/java/android/widget/TimePickerClockDelegate.java +++ b/core/java/android/widget/TimePickerClockDelegate.java @@ -17,7 +17,6 @@ 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; @@ -34,7 +33,6 @@ import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.View.AccessibilityDelegate; -import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; @@ -91,6 +89,7 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate impl private int mInitialHourOfDay; private int mInitialMinute; private boolean mIs24HourView; + private boolean mIsAmPmAtStart; // For hardware IME input. private char mPlaceholderText; @@ -131,19 +130,19 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate impl mPmText = amPmStrings[1]; final int layoutResourceId = a.getResourceId(R.styleable.TimePicker_internalLayout, - R.layout.time_picker_holo); + R.layout.time_picker_material); 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 = (TextView) mainView.findViewById(R.id.hours); mHourView.setOnClickListener(mClickListener); mHourView.setAccessibilityDelegate( new ClickActionDelegate(context, R.string.select_hours)); - mSeparatorView = (TextView) mHeaderView.findViewById(R.id.separator); - mMinuteView = (TextView) mHeaderView.findViewById(R.id.minutes); + mSeparatorView = (TextView) mainView.findViewById(R.id.separator); + mMinuteView = (TextView) mainView.findViewById(R.id.minutes); mMinuteView.setOnClickListener(mClickListener); mMinuteView.setAccessibilityDelegate( new ClickActionDelegate(context, R.string.select_minutes)); @@ -161,17 +160,8 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate impl mHourView.setMinWidth(computeStableWidth(mHourView, 24)); mMinuteView.setMinWidth(computeStableWidth(mMinuteView, 60)); - // 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); + mAmPmLayout = mainView.findViewById(R.id.ampm_layout); mAmLabel = (CheckedTextView) mAmPmLayout.findViewById(R.id.am_label); mAmLabel.setText(amPmStrings[0]); mAmLabel.setOnClickListener(mClickListener); @@ -284,24 +274,40 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate impl } private void updateHeaderAmPm() { + if (mIs24HourView) { mAmPmLayout.setVisibility(View.GONE); } else { // Ensure that AM/PM layout is in the correct position. final String dateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, "hm"); - final boolean amPmAtStart = dateTimePattern.startsWith("a"); - final ViewGroup parent = (ViewGroup) mAmPmLayout.getParent(); - final int targetIndex = amPmAtStart ? 0 : parent.getChildCount() - 1; - final int currentIndex = parent.indexOfChild(mAmPmLayout); - if (targetIndex != currentIndex) { - parent.removeView(mAmPmLayout); - parent.addView(mAmPmLayout, targetIndex); - } + final boolean isAmPmAtStart = dateTimePattern.startsWith("a"); + setAmPmAtStart(isAmPmAtStart); updateAmPmLabelStates(mInitialHourOfDay < 12 ? AM : PM); } } + private void setAmPmAtStart(boolean isAmPmAtStart) { + if (mIsAmPmAtStart != isAmPmAtStart) { + mIsAmPmAtStart = isAmPmAtStart; + + final RelativeLayout.LayoutParams params = + (RelativeLayout.LayoutParams) mAmPmLayout.getLayoutParams(); + if (params.getRule(RelativeLayout.RIGHT_OF) != 0 || + params.getRule(RelativeLayout.LEFT_OF) != 0) { + if (isAmPmAtStart) { + params.removeRule(RelativeLayout.RIGHT_OF); + params.addRule(RelativeLayout.LEFT_OF, mHourView.getId()); + } else { + params.removeRule(RelativeLayout.LEFT_OF); + params.addRule(RelativeLayout.RIGHT_OF, mMinuteView.getId()); + } + } + + mAmPmLayout.setLayoutParams(params); + } + } + /** * Set the current hour. */ @@ -466,16 +472,6 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate impl event.getText().add(selectedDate); } - @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - event.setClassName(TimePicker.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - info.setClassName(TimePicker.class.getName()); - } - /** * Set whether in keyboard mode or not. * @@ -609,11 +605,11 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate impl private void updateAmPmLabelStates(int amOrPm) { final boolean isAm = amOrPm == AM; mAmLabel.setChecked(isAm); - mAmLabel.setAlpha(isAm ? 1 : mDisabledAlpha); + mAmLabel.setSelected(isAm); final boolean isPm = amOrPm == PM; mPmLabel.setChecked(isPm); - mPmLabel.setAlpha(isPm ? 1 : mDisabledAlpha); + mPmLabel.setSelected(isPm); } /** diff --git a/core/java/android/widget/TimePickerSpinnerDelegate.java b/core/java/android/widget/TimePickerSpinnerDelegate.java index e162f4a..513c55b 100644 --- a/core/java/android/widget/TimePickerSpinnerDelegate.java +++ b/core/java/android/widget/TimePickerSpinnerDelegate.java @@ -28,12 +28,10 @@ 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.Calendar; import java.util.Locale; @@ -427,16 +425,6 @@ class TimePickerSpinnerDelegate extends TimePicker.AbstractTimePickerDelegate { event.getText().add(selectedDateUtterance); } - @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - event.setClassName(TimePicker.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - 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 diff --git a/core/java/android/widget/Toast.java b/core/java/android/widget/Toast.java index be4cdc1..207f675 100644 --- a/core/java/android/widget/Toast.java +++ b/core/java/android/widget/Toast.java @@ -17,6 +17,7 @@ package android.widget; import android.annotation.IntDef; +import android.annotation.StringRes; import android.app.INotificationManager; import android.app.ITransientNotification; import android.content.Context; @@ -280,7 +281,7 @@ public class Toast { * * @throws Resources.NotFoundException if the resource can't be found. */ - public static Toast makeText(Context context, int resId, @Duration int duration) + public static Toast makeText(Context context, @StringRes int resId, @Duration int duration) throws Resources.NotFoundException { return makeText(context, context.getResources().getText(resId), duration); } @@ -289,7 +290,7 @@ public class Toast { * Update the text in a Toast that was previously created using one of the makeText() methods. * @param resId The new text for the Toast. */ - public void setText(int resId) { + public void setText(@StringRes int resId) { setText(mContext.getText(resId)); } diff --git a/core/java/android/widget/ToggleButton.java b/core/java/android/widget/ToggleButton.java index 28519d1..6a8449e 100644 --- a/core/java/android/widget/ToggleButton.java +++ b/core/java/android/widget/ToggleButton.java @@ -21,8 +21,6 @@ import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.util.AttributeSet; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; /** * Displays checked/unchecked states as a button @@ -154,14 +152,7 @@ public class ToggleButton extends CompoundButton { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(ToggleButton.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(ToggleButton.class.getName()); + public CharSequence getAccessibilityClassName() { + return ToggleButton.class.getName(); } } diff --git a/core/java/android/widget/Toolbar.java b/core/java/android/widget/Toolbar.java index c5325c4..087406a 100644 --- a/core/java/android/widget/Toolbar.java +++ b/core/java/android/widget/Toolbar.java @@ -16,12 +16,18 @@ package android.widget; +import android.annotation.ColorInt; +import android.annotation.DrawableRes; +import android.annotation.MenuRes; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.StringRes; +import android.annotation.StyleRes; import android.app.ActionBar; import android.content.Context; +import android.content.res.ColorStateList; import android.content.res.TypedArray; -import android.graphics.RectF; +import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.os.Parcel; import android.os.Parcelable; @@ -104,6 +110,9 @@ public class Toolbar extends ViewGroup { private ImageButton mNavButtonView; private ImageView mLogoView; + private TintInfo mOverflowTintInfo; + private TintInfo mNavTintInfo; + private Drawable mCollapseIcon; private CharSequence mCollapseDescription; private ImageButton mCollapseButtonView; @@ -266,6 +275,21 @@ public class Toolbar extends ViewGroup { if (!TextUtils.isEmpty(navDesc)) { setNavigationContentDescription(navDesc); } + + if (a.hasValue(R.styleable.Toolbar_overflowTint)) { + setOverflowTintList(a.getColorStateList(R.styleable.Toolbar_overflowTint)); + } + if (a.hasValue(R.styleable.Toolbar_overflowTintMode)) { + setOverflowTintMode(Drawable.parseTintMode( + a.getInt(R.styleable.Toolbar_overflowTintMode, -1), null)); + } + if (a.hasValue(R.styleable.Toolbar_navigationTint)) { + setNavigationTintList(a.getColorStateList(R.styleable.Toolbar_navigationTint)); + } + if (a.hasValue(R.styleable.Toolbar_navigationTintMode)) { + setNavigationTintMode(Drawable.parseTintMode( + a.getInt(R.styleable.Toolbar_navigationTintMode, -1), null)); + } a.recycle(); } @@ -276,7 +300,7 @@ public class Toolbar extends ViewGroup { * @param resId theme used to inflate popup menus * @see #getPopupTheme() */ - public void setPopupTheme(int resId) { + public void setPopupTheme(@StyleRes int resId) { if (mPopupTheme != resId) { mPopupTheme = resId; if (resId == 0) { @@ -311,7 +335,7 @@ public class Toolbar extends ViewGroup { * * @param resId ID of a drawable resource */ - public void setLogo(int resId) { + public void setLogo(@DrawableRes int resId) { setLogo(getContext().getDrawable(resId)); } @@ -461,7 +485,7 @@ public class Toolbar extends ViewGroup { * * @param resId String resource id */ - public void setLogoDescription(int resId) { + public void setLogoDescription(@StringRes int resId) { setLogoDescription(getContext().getText(resId)); } @@ -546,7 +570,7 @@ public class Toolbar extends ViewGroup { * * @param resId Resource ID of a string to set as the title */ - public void setTitle(int resId) { + public void setTitle(@StringRes int resId) { setTitle(getContext().getText(resId)); } @@ -601,7 +625,7 @@ public class Toolbar extends ViewGroup { * * @param resId String resource ID */ - public void setSubtitle(int resId) { + public void setSubtitle(@StringRes int resId) { setSubtitle(getContext().getText(resId)); } @@ -643,7 +667,7 @@ public class Toolbar extends ViewGroup { * Sets the text color, size, style, hint color, and highlight color * from the specified TextAppearance resource. */ - public void setTitleTextAppearance(Context context, int resId) { + public void setTitleTextAppearance(Context context, @StyleRes int resId) { mTitleTextAppearance = resId; if (mTitleTextView != null) { mTitleTextView.setTextAppearance(context, resId); @@ -654,7 +678,7 @@ public class Toolbar extends ViewGroup { * Sets the text color, size, style, hint color, and highlight color * from the specified TextAppearance resource. */ - public void setSubtitleTextAppearance(Context context, int resId) { + public void setSubtitleTextAppearance(Context context, @StyleRes int resId) { mSubtitleTextAppearance = resId; if (mSubtitleTextView != null) { mSubtitleTextView.setTextAppearance(context, resId); @@ -666,7 +690,7 @@ public class Toolbar extends ViewGroup { * * @param color The new text color in 0xAARRGGBB format */ - public void setTitleTextColor(int color) { + public void setTitleTextColor(@ColorInt int color) { mTitleTextColor = color; if (mTitleTextView != null) { mTitleTextView.setTextColor(color); @@ -678,7 +702,7 @@ public class Toolbar extends ViewGroup { * * @param color The new text color in 0xAARRGGBB format */ - public void setSubtitleTextColor(int color) { + public void setSubtitleTextColor(@ColorInt int color) { mSubtitleTextColor = color; if (mSubtitleTextView != null) { mSubtitleTextView.setTextColor(color); @@ -709,7 +733,7 @@ public class Toolbar extends ViewGroup { * * @attr ref android.R.styleable#Toolbar_navigationContentDescription */ - public void setNavigationContentDescription(int resId) { + public void setNavigationContentDescription(@StringRes int resId) { setNavigationContentDescription(resId != 0 ? getContext().getText(resId) : null); } @@ -746,7 +770,7 @@ public class Toolbar extends ViewGroup { * * @attr ref android.R.styleable#Toolbar_navigationIcon */ - public void setNavigationIcon(int resId) { + public void setNavigationIcon(@DrawableRes int resId) { setNavigationIcon(getContext().getDrawable(resId)); } @@ -806,6 +830,91 @@ public class Toolbar extends ViewGroup { } /** + * Applies a tint to the icon drawable. Does not modify the current tint + * mode, which is {@link PorterDuff.Mode#SRC_IN} by default. + * <p> + * Subsequent calls to {@link #setNavigationIcon(Drawable)} will automatically mutate + * the drawable and apply the specified tint and tint mode. + * + * @param tint the tint to apply, may be {@code null} to clear tint + * + * @attr ref android.R.styleable#Toolbar_navigationTint + */ + public void setNavigationTintList(ColorStateList tint) { + if (mNavTintInfo == null) { + mNavTintInfo = new TintInfo(); + } + mNavTintInfo.mTintList = tint; + mNavTintInfo.mHasTintList = true; + + applyNavigationTint(); + } + + /** + * Specifies the blending mode used to apply the tint specified by {@link + * #setNavigationTintList(ColorStateList)} to the navigation drawable. + * The default mode is {@link PorterDuff.Mode#SRC_IN}. + * + * @param tintMode the blending mode used to apply the tint, may be {@code null} to clear tint + * + * @attr ref android.R.styleable#Toolbar_navigationTintMode + */ + public void setNavigationTintMode(PorterDuff.Mode tintMode) { + if (mNavTintInfo == null) { + mNavTintInfo = new TintInfo(); + } + mNavTintInfo.mTintMode = tintMode; + mNavTintInfo.mHasTintMode = true; + + applyNavigationTint(); + } + + /** + * Applies a tint to the overflow drawable. Does not modify the current tint + * mode, which is {@link PorterDuff.Mode#SRC_IN} by default. + * + * @param tint the tint to apply, may be {@code null} to clear tint + * + * @attr ref android.R.styleable#Toolbar_overflowTint + */ + public void setOverflowTintList(ColorStateList tint) { + if (mMenuView != null) { + // If the menu view is available, directly set the tint + mMenuView.setOverflowTintList(tint); + } else { + // Otherwise we will record the value + if (mOverflowTintInfo == null) { + mOverflowTintInfo = new TintInfo(); + } + mOverflowTintInfo.mTintList = tint; + mOverflowTintInfo.mHasTintList = true; + } + } + + /** + * Specifies the blending mode used to apply the tint specified by {@link + * #setOverflowTintList(ColorStateList)} to the overflow drawable. + * The default mode is {@link PorterDuff.Mode#SRC_IN}. + * + * @param tintMode the blending mode used to apply the tint, may be {@code null} to clear tint + * + * @attr ref android.R.styleable#Toolbar_overflowTintMode + */ + public void setOverflowTintMode(PorterDuff.Mode tintMode) { + if (mMenuView != null) { + // If the menu view is available, directly set the tint mode + mMenuView.setOverflowTintMode(tintMode); + } else { + // Otherwise we will record the value + if (mOverflowTintInfo == null) { + mOverflowTintInfo = new TintInfo(); + } + mOverflowTintInfo.mTintMode = tintMode; + mOverflowTintInfo.mHasTintMode = true; + } + } + + /** * Return the Menu shown in the toolbar. * * <p>Applications that wish to populate the toolbar's menu can do so from here. To use @@ -841,6 +950,17 @@ public class Toolbar extends ViewGroup { lp.gravity = Gravity.END | (mButtonGravity & Gravity.VERTICAL_GRAVITY_MASK); mMenuView.setLayoutParams(lp); addSystemView(mMenuView); + + if (mOverflowTintInfo != null) { + // If we have tint info for the overflow, set it on the menu view now + if (mOverflowTintInfo.mHasTintList) { + mMenuView.setOverflowTintList(mOverflowTintInfo.mTintList); + } + if (mOverflowTintInfo.mHasTintMode) { + mMenuView.setOverflowTintMode(mOverflowTintInfo.mTintMode); + } + mOverflowTintInfo = null; + } } } @@ -856,7 +976,7 @@ public class Toolbar extends ViewGroup { * * @param resId ID of a menu resource to inflate */ - public void inflateMenu(int resId) { + public void inflateMenu(@MenuRes int resId) { getMenuInflater().inflate(resId, getMenu()); } @@ -994,6 +1114,7 @@ public class Toolbar extends ViewGroup { final LayoutParams lp = generateDefaultLayoutParams(); lp.gravity = Gravity.START | (mButtonGravity & Gravity.VERTICAL_GRAVITY_MASK); mNavButtonView.setLayoutParams(lp); + applyNavigationTint(); } } @@ -1012,6 +1133,7 @@ public class Toolbar extends ViewGroup { collapseActionView(); } }); + applyNavigationTint(); } } @@ -1763,6 +1885,30 @@ public class Toolbar extends ViewGroup { return mPopupContext; } + private void applyNavigationTint() { + final TintInfo tintInfo = mNavTintInfo; + if (tintInfo != null && (tintInfo.mHasTintList || tintInfo.mHasTintMode)) { + if (mNavButtonView != null) { + if (tintInfo.mHasTintList) { + mNavButtonView.setImageTintList(tintInfo.mTintList); + } + if (tintInfo.mHasTintMode) { + mNavButtonView.setImageTintMode(tintInfo.mTintMode); + } + } + + if (mCollapseButtonView != null) { + // We will use the same tint for the collapse button + if (tintInfo.mHasTintList) { + mCollapseButtonView.setImageTintList(tintInfo.mTintList); + } + if (tintInfo.mHasTintMode) { + mCollapseButtonView.setImageTintMode(tintInfo.mTintMode); + } + } + } + } + /** * Interface responsible for receiving menu item click events if the items themselves * do not have individual item click listeners. @@ -1990,4 +2136,11 @@ public class Toolbar extends ViewGroup { public void onRestoreInstanceState(Parcelable state) { } } + + private static class TintInfo { + ColorStateList mTintList; + PorterDuff.Mode mTintMode; + boolean mHasTintMode; + boolean mHasTintList; + } } diff --git a/core/java/android/widget/TwoLineListItem.java b/core/java/android/widget/TwoLineListItem.java index 5606c60..69ff488 100644 --- a/core/java/android/widget/TwoLineListItem.java +++ b/core/java/android/widget/TwoLineListItem.java @@ -20,8 +20,6 @@ import android.annotation.Widget; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RelativeLayout; /** @@ -94,14 +92,7 @@ public class TwoLineListItem extends RelativeLayout { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(TwoLineListItem.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(TwoLineListItem.class.getName()); + public CharSequence getAccessibilityClassName() { + return TwoLineListItem.class.getName(); } } diff --git a/core/java/android/widget/VideoView.java b/core/java/android/widget/VideoView.java index 47644f9..2671739 100644 --- a/core/java/android/widget/VideoView.java +++ b/core/java/android/widget/VideoView.java @@ -43,8 +43,6 @@ import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; import android.widget.MediaController.MediaPlayerControl; import java.io.IOException; @@ -202,15 +200,8 @@ public class VideoView extends SurfaceView } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(VideoView.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(VideoView.class.getName()); + public CharSequence getAccessibilityClassName() { + return VideoView.class.getName(); } public int resolveAdjustedSize(int desiredSize, int measureSpec) { diff --git a/core/java/android/widget/ViewAnimator.java b/core/java/android/widget/ViewAnimator.java index eee914e..1580f51 100644 --- a/core/java/android/widget/ViewAnimator.java +++ b/core/java/android/widget/ViewAnimator.java @@ -17,13 +17,12 @@ package android.widget; +import android.annotation.AnimRes; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.Animation; import android.view.animation.AnimationUtils; @@ -311,7 +310,7 @@ public class ViewAnimator extends FrameLayout { * @see #getInAnimation() * @see #setInAnimation(android.view.animation.Animation) */ - public void setInAnimation(Context context, int resourceID) { + public void setInAnimation(Context context, @AnimRes int resourceID) { setInAnimation(AnimationUtils.loadAnimation(context, resourceID)); } @@ -324,7 +323,7 @@ public class ViewAnimator extends FrameLayout { * @see #getOutAnimation() * @see #setOutAnimation(android.view.animation.Animation) */ - public void setOutAnimation(Context context, int resourceID) { + public void setOutAnimation(Context context, @AnimRes int resourceID) { setOutAnimation(AnimationUtils.loadAnimation(context, resourceID)); } @@ -358,14 +357,7 @@ public class ViewAnimator extends FrameLayout { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(ViewAnimator.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(ViewAnimator.class.getName()); + public CharSequence getAccessibilityClassName() { + return ViewAnimator.class.getName(); } } diff --git a/core/java/android/widget/ViewFlipper.java b/core/java/android/widget/ViewFlipper.java index cf1f554..94e7ba1 100644 --- a/core/java/android/widget/ViewFlipper.java +++ b/core/java/android/widget/ViewFlipper.java @@ -24,8 +24,6 @@ import android.content.res.TypedArray; import android.os.*; import android.util.AttributeSet; import android.util.Log; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; /** @@ -150,15 +148,8 @@ public class ViewFlipper extends ViewAnimator { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(ViewFlipper.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(ViewFlipper.class.getName()); + public CharSequence getAccessibilityClassName() { + return ViewFlipper.class.getName(); } /** diff --git a/core/java/android/widget/ViewSwitcher.java b/core/java/android/widget/ViewSwitcher.java index 0376918..0d5627e 100644 --- a/core/java/android/widget/ViewSwitcher.java +++ b/core/java/android/widget/ViewSwitcher.java @@ -20,8 +20,6 @@ import android.content.Context; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; /** * {@link ViewAnimator} that switches between two views, and has a factory @@ -69,15 +67,8 @@ public class ViewSwitcher extends ViewAnimator { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(ViewSwitcher.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(ViewSwitcher.class.getName()); + public CharSequence getAccessibilityClassName() { + return ViewSwitcher.class.getName(); } /** diff --git a/core/java/android/widget/YearPickerView.java b/core/java/android/widget/YearPickerView.java index 24ed7ce..6f0465f 100644 --- a/core/java/android/widget/YearPickerView.java +++ b/core/java/android/widget/YearPickerView.java @@ -17,8 +17,10 @@ package android.widget; import android.content.Context; +import android.content.res.ColorStateList; import android.content.res.Resources; import android.util.AttributeSet; +import android.util.StateSet; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; @@ -42,7 +44,7 @@ class YearPickerView extends ListView implements AdapterView.OnItemClickListener private DatePickerController mController; private int mSelectedPosition = -1; - private int mYearSelectedCircleColor; + private int mYearActivatedColor; public YearPickerView(Context context) { this(context, null); @@ -97,15 +99,14 @@ class YearPickerView extends ListView implements AdapterView.OnItemClickListener onDateChanged(); } - public void setYearSelectedCircleColor(int color) { - if (color != mYearSelectedCircleColor) { - mYearSelectedCircleColor = color; - } - requestLayout(); + public void setYearBackgroundColor(ColorStateList yearBackgroundColor) { + mYearActivatedColor = yearBackgroundColor.getColorForState( + StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_ACTIVATED), 0); + invalidate(); } - public int getYearSelectedCircleColor() { - return mYearSelectedCircleColor; + public void setYearTextAppearance(int resId) { + mAdapter.setItemTextAppearance(resId); } private void updateAdapterData() { @@ -127,12 +128,8 @@ class YearPickerView extends ListView implements AdapterView.OnItemClickListener mController.onYearSelected(mAdapter.getItem(position)); } - void setItemTextAppearance(int resId) { - mAdapter.setItemTextAppearance(resId); - } - private class YearAdapter extends ArrayAdapter<Integer> { - int mItemTextAppearanceResId; + private int mItemTextAppearanceResId; public YearAdapter(Context context, int resource) { super(context, resource); @@ -140,16 +137,15 @@ class YearPickerView extends ListView implements AdapterView.OnItemClickListener @Override public View getView(int position, View convertView, ViewGroup parent) { - TextViewWithCircularIndicator v = (TextViewWithCircularIndicator) + final TextViewWithCircularIndicator v = (TextViewWithCircularIndicator) super.getView(position, convertView, parent); - v.setTextAppearance(getContext(), mItemTextAppearanceResId); - v.requestLayout(); - int year = getItem(position); - boolean selected = mController.getSelectedDay().get(Calendar.YEAR) == year; - v.setDrawIndicator(selected); - if (selected) { - v.setCircleColor(mYearSelectedCircleColor); - } + v.setTextAppearance(v.getContext(), mItemTextAppearanceResId); + v.setCircleColor(mYearActivatedColor); + + final int year = getItem(position); + final boolean selected = mController.getSelectedDay().get(Calendar.YEAR) == year; + v.setActivated(selected); + return v; } @@ -189,9 +185,10 @@ class YearPickerView extends ListView implements AdapterView.OnItemClickListener mController.getSelectedDay().get(Calendar.YEAR) - mMinDate.get(Calendar.YEAR)); } + /** @hide */ @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); + public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { + super.onInitializeAccessibilityEventInternal(event); if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) { event.setFromIndex(0); diff --git a/core/java/android/widget/ZoomButton.java b/core/java/android/widget/ZoomButton.java index 715e868..0255bdb 100644 --- a/core/java/android/widget/ZoomButton.java +++ b/core/java/android/widget/ZoomButton.java @@ -23,8 +23,6 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.View.OnLongClickListener; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; public class ZoomButton extends ImageButton implements OnLongClickListener { @@ -104,14 +102,7 @@ public class ZoomButton extends ImageButton implements OnLongClickListener { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(ZoomButton.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(ZoomButton.class.getName()); + public CharSequence getAccessibilityClassName() { + return ZoomButton.class.getName(); } } diff --git a/core/java/android/widget/ZoomControls.java b/core/java/android/widget/ZoomControls.java index 8897875..66c052b 100644 --- a/core/java/android/widget/ZoomControls.java +++ b/core/java/android/widget/ZoomControls.java @@ -22,8 +22,6 @@ import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.AlphaAnimation; import com.android.internal.R; @@ -110,14 +108,7 @@ public class ZoomControls extends LinearLayout { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(ZoomControls.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(ZoomControls.class.getName()); + public CharSequence getAccessibilityClassName() { + return ZoomControls.class.getName(); } } diff --git a/core/java/com/android/internal/alsa/AlsaCardsParser.java b/core/java/com/android/internal/alsa/AlsaCardsParser.java new file mode 100644 index 0000000..5c0a888 --- /dev/null +++ b/core/java/com/android/internal/alsa/AlsaCardsParser.java @@ -0,0 +1,225 @@ +/* + * 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 com.android.internal.alsa; + +import android.util.Slog; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; + +/** + * @hide Retrieves information from an ALSA "cards" file. + */ +public class AlsaCardsParser { + private static final String TAG = "AlsaCardsParser"; + protected static final boolean DEBUG = true; + + private static final String kCardsFilePath = "/proc/asound/cards"; + + private static LineTokenizer mTokenizer = new LineTokenizer(" :[]"); + + private ArrayList<AlsaCardRecord> mCardRecords = new ArrayList<AlsaCardRecord>(); + + public class AlsaCardRecord { + private static final String TAG = "AlsaCardRecord"; + private static final String kUsbCardKeyStr = "at usb-"; + + public int mCardNum = -1; + public String mField1 = ""; + public String mCardName = ""; + public String mCardDescription = ""; + public boolean mIsUsb = false; + + public AlsaCardRecord() {} + + public boolean parse(String line, int lineIndex) { + int tokenIndex = 0; + int delimIndex = 0; + + if (lineIndex == 0) { + // line # (skip) + tokenIndex = mTokenizer.nextToken(line, tokenIndex); + delimIndex = mTokenizer.nextDelimiter(line, tokenIndex); + + try { + // mCardNum + mCardNum = Integer.parseInt(line.substring(tokenIndex, delimIndex)); + } catch (NumberFormatException e) { + Slog.e(TAG, "Failed to parse line " + lineIndex + " of " + kCardsFilePath + + ": " + line.substring(tokenIndex, delimIndex)); + return false; + } + + // mField1 + tokenIndex = mTokenizer.nextToken(line, delimIndex); + delimIndex = mTokenizer.nextDelimiter(line, tokenIndex); + mField1 = line.substring(tokenIndex, delimIndex); + + // mCardName + tokenIndex = mTokenizer.nextToken(line, delimIndex); + mCardName = line.substring(tokenIndex); + + // done + } else if (lineIndex == 1) { + tokenIndex = mTokenizer.nextToken(line, 0); + if (tokenIndex != -1) { + int keyIndex = line.indexOf(kUsbCardKeyStr); + mIsUsb = keyIndex != -1; + if (mIsUsb) { + mCardDescription = line.substring(tokenIndex, keyIndex - 1); + } + } + } + + return true; + } + + public String textFormat() { + return mCardName + " : " + mCardDescription; + } + } + + public AlsaCardsParser() {} + + public void scan() { + if (DEBUG) { + Slog.i(TAG, "AlsaCardsParser.scan()"); + } + mCardRecords = new ArrayList<AlsaCardRecord>(); + + File cardsFile = new File(kCardsFilePath); + try { + FileReader reader = new FileReader(cardsFile); + BufferedReader bufferedReader = new BufferedReader(reader); + String line = ""; + while ((line = bufferedReader.readLine()) != null) { + AlsaCardRecord cardRecord = new AlsaCardRecord(); + if (DEBUG) { + Slog.i(TAG, " " + line); + } + cardRecord.parse(line, 0); + + line = bufferedReader.readLine(); + if (line == null) { + break; + } + if (DEBUG) { + Slog.i(TAG, " " + line); + } + cardRecord.parse(line, 1); + + mCardRecords.add(cardRecord); + } + reader.close(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public ArrayList<AlsaCardRecord> getScanRecords() { + return mCardRecords; + } + + public AlsaCardRecord getCardRecordAt(int index) { + return mCardRecords.get(index); + } + + public AlsaCardRecord getCardRecordFor(int cardNum) { + for (AlsaCardRecord rec : mCardRecords) { + if (rec.mCardNum == cardNum) { + return rec; + } + } + + return null; + } + + public int getNumCardRecords() { + return mCardRecords.size(); + } + + public boolean isCardUsb(int cardNum) { + for (AlsaCardRecord rec : mCardRecords) { + if (rec.mCardNum == cardNum) { + return rec.mIsUsb; + } + } + + return false; + } + + // return -1 if none found + public int getDefaultUsbCard() { + // Choose the most-recently added EXTERNAL card + // or return the first added EXTERNAL card? + for (AlsaCardRecord rec : mCardRecords) { + if (rec.mIsUsb) { + return rec.mCardNum; + } + } + + return -1; + } + + public int getDefaultCard() { + // return an external card if possible + int card = getDefaultUsbCard(); + + if (card < 0 && getNumCardRecords() > 0) { + // otherwise return the (internal) card with the highest number + card = getCardRecordAt(getNumCardRecords() - 1).mCardNum; + } + return card; + } + + static public boolean hasCardNumber(ArrayList<AlsaCardRecord> recs, int cardNum) { + for (AlsaCardRecord cardRec : recs) { + if (cardRec.mCardNum == cardNum) { + return true; + } + } + return false; + } + + public ArrayList<AlsaCardRecord> getNewCardRecords(ArrayList<AlsaCardRecord> prevScanRecs) { + ArrayList<AlsaCardRecord> newRecs = new ArrayList<AlsaCardRecord>(); + for (AlsaCardRecord rec : mCardRecords) { + // now scan to see if this card number is in the previous scan list + if (!hasCardNumber(prevScanRecs, rec.mCardNum)) { + newRecs.add(rec); + } + } + return newRecs; + } + + // + // Logging + // + public void Log(String heading) { + if (DEBUG) { + Slog.i(TAG, heading); + for (AlsaCardRecord cardRec : mCardRecords) { + Slog.i(TAG, cardRec.textFormat()); + } + } + } +} diff --git a/core/java/android/alsa/AlsaDevicesParser.java b/core/java/com/android/internal/alsa/AlsaDevicesParser.java index 82cc1ae..81b7943 100644 --- a/core/java/android/alsa/AlsaDevicesParser.java +++ b/core/java/com/android/internal/alsa/AlsaDevicesParser.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package android.alsa; +package com.android.internal.alsa; import android.util.Slog; import java.io.BufferedReader; @@ -21,7 +21,7 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; -import java.util.Vector; +import java.util.ArrayList; /** * @hide @@ -29,6 +29,9 @@ import java.util.Vector; */ public class AlsaDevicesParser { private static final String TAG = "AlsaDevicesParser"; + protected static final boolean DEBUG = false; + + private static final String kDevicesFilePath = "/proc/asound/devices"; private static final int kIndex_CardDeviceField = 5; private static final int kStartIndex_CardNum = 6; @@ -58,8 +61,7 @@ public class AlsaDevicesParser { int mDeviceType = kDeviceType_Unknown; int mDeviceDir = kDeviceDir_Unknown; - public AlsaDeviceRecord() { - } + public AlsaDeviceRecord() {} public boolean parse(String line) { // "0123456789012345678901234567890" @@ -89,51 +91,57 @@ public class AlsaDevicesParser { } String token = line.substring(tokenOffset, delimOffset); - switch (tokenIndex) { - case kToken_LineNum: - // ignore - break; - - case kToken_CardNum: - mCardNum = Integer.parseInt(token); - if (line.charAt(delimOffset) != '-') { - tokenIndex++; // no device # in the token stream - } - break; - - case kToken_DeviceNum: - mDeviceNum = Integer.parseInt(token); - break; - - case kToken_Type0: - if (token.equals("digital")) { - // NOP - } else if (token.equals("control")) { - mDeviceType = kDeviceType_Control; - } else if (token.equals("raw")) { - // NOP - } - break; - - case kToken_Type1: - if (token.equals("audio")) { - mDeviceType = kDeviceType_Audio; - } else if (token.equals("midi")) { - mDeviceType = kDeviceType_MIDI; - mHasMIDIDevices = true; - } - break; - - case kToken_Type2: - if (token.equals("capture")) { - mDeviceDir = kDeviceDir_Capture; - mHasCaptureDevices = true; - } else if (token.equals("playback")) { - mDeviceDir = kDeviceDir_Playback; - mHasPlaybackDevices = true; - } - break; - } // switch (tokenIndex) + try { + switch (tokenIndex) { + case kToken_LineNum: + // ignore + break; + + case kToken_CardNum: + mCardNum = Integer.parseInt(token); + if (line.charAt(delimOffset) != '-') { + tokenIndex++; // no device # in the token stream + } + break; + + case kToken_DeviceNum: + mDeviceNum = Integer.parseInt(token); + break; + + case kToken_Type0: + if (token.equals("digital")) { + // NOP + } else if (token.equals("control")) { + mDeviceType = kDeviceType_Control; + } else if (token.equals("raw")) { + // NOP + } + break; + + case kToken_Type1: + if (token.equals("audio")) { + mDeviceType = kDeviceType_Audio; + } else if (token.equals("midi")) { + mDeviceType = kDeviceType_MIDI; + mHasMIDIDevices = true; + } + break; + + case kToken_Type2: + if (token.equals("capture")) { + mDeviceDir = kDeviceDir_Capture; + mHasCaptureDevices = true; + } else if (token.equals("playback")) { + mDeviceDir = kDeviceDir_Playback; + mHasPlaybackDevices = true; + } + break; + } // switch (tokenIndex) + } catch (NumberFormatException e) { + Slog.e(TAG, "Failed to parse token " + tokenIndex + " of " + kDevicesFilePath + + " token: " + token); + return false; + } tokenIndex++; } // while (true) @@ -176,38 +184,27 @@ public class AlsaDevicesParser { } } - private Vector<AlsaDeviceRecord> - deviceRecords_ = new Vector<AlsaDeviceRecord>(); + private ArrayList<AlsaDeviceRecord> mDeviceRecords = new ArrayList<AlsaDeviceRecord>(); - private boolean isLineDeviceRecord(String line) { - return line.charAt(kIndex_CardDeviceField) == '['; - } + public AlsaDevicesParser() {} - public AlsaDevicesParser() { + // + // Access + // + public int getDefaultDeviceNum(int card) { + // TODO - This (obviously) isn't sufficient. Revisit. + return 0; } - public int getNumDeviceRecords() { - return deviceRecords_.size(); - } - - public AlsaDeviceRecord getDeviceRecordAt(int index) { - return deviceRecords_.get(index); - } - - public void Log() { - int numDevRecs = getNumDeviceRecords(); - for (int index = 0; index < numDevRecs; ++index) { - Slog.w(TAG, "usb:" + getDeviceRecordAt(index).textFormat()); - } - } - - public boolean hasPlaybackDevices() { + // + // Predicates + // + public boolean hasPlaybackDevices() { return mHasPlaybackDevices; } public boolean hasPlaybackDevices(int card) { - for (int index = 0; index < deviceRecords_.size(); index++) { - AlsaDeviceRecord deviceRecord = deviceRecords_.get(index); + for (AlsaDeviceRecord deviceRecord : mDeviceRecords) { if (deviceRecord.mCardNum == card && deviceRecord.mDeviceType == AlsaDeviceRecord.kDeviceType_Audio && deviceRecord.mDeviceDir == AlsaDeviceRecord.kDeviceDir_Playback) { @@ -222,8 +219,7 @@ public class AlsaDevicesParser { } public boolean hasCaptureDevices(int card) { - for (int index = 0; index < deviceRecords_.size(); index++) { - AlsaDeviceRecord deviceRecord = deviceRecords_.get(index); + for (AlsaDeviceRecord deviceRecord : mDeviceRecords) { if (deviceRecord.mCardNum == card && deviceRecord.mDeviceType == AlsaDeviceRecord.kDeviceType_Audio && deviceRecord.mDeviceDir == AlsaDeviceRecord.kDeviceDir_Capture) { @@ -238,8 +234,7 @@ public class AlsaDevicesParser { } public boolean hasMIDIDevices(int card) { - for (int index = 0; index < deviceRecords_.size(); index++) { - AlsaDeviceRecord deviceRecord = deviceRecords_.get(index); + for (AlsaDeviceRecord deviceRecord : mDeviceRecords) { if (deviceRecord.mCardNum == card && deviceRecord.mDeviceType == AlsaDeviceRecord.kDeviceType_MIDI) { return true; @@ -248,11 +243,17 @@ public class AlsaDevicesParser { return false; } + // + // Process + // + private boolean isLineDeviceRecord(String line) { + return line.charAt(kIndex_CardDeviceField) == '['; + } + public void scan() { - deviceRecords_.clear(); + mDeviceRecords.clear(); - final String devicesFilePath = "/proc/asound/devices"; - File devicesFile = new File(devicesFilePath); + File devicesFile = new File(kDevicesFilePath); try { FileReader reader = new FileReader(devicesFile); BufferedReader bufferedReader = new BufferedReader(reader); @@ -261,7 +262,7 @@ public class AlsaDevicesParser { if (isLineDeviceRecord(line)) { AlsaDeviceRecord deviceRecord = new AlsaDeviceRecord(); deviceRecord.parse(line); - deviceRecords_.add(deviceRecord); + mDeviceRecords.add(deviceRecord); } } reader.close(); @@ -271,5 +272,17 @@ public class AlsaDevicesParser { e.printStackTrace(); } } + + // + // Loging + // + public void Log(String heading) { + if (DEBUG) { + Slog.i(TAG, heading); + for (AlsaDeviceRecord deviceRecord : mDeviceRecords) { + Slog.i(TAG, deviceRecord.textFormat()); + } + } + } } // class AlsaDevicesParser diff --git a/core/java/android/alsa/LineTokenizer.java b/core/java/com/android/internal/alsa/LineTokenizer.java index 78c91b5..43047a9 100644 --- a/core/java/android/alsa/LineTokenizer.java +++ b/core/java/com/android/internal/alsa/LineTokenizer.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package android.alsa; +package com.android.internal.alsa; /** * @hide diff --git a/core/java/com/android/internal/app/AlertActivity.java b/core/java/com/android/internal/app/AlertActivity.java index 5566aa6..ed48b0d 100644 --- a/core/java/com/android/internal/app/AlertActivity.java +++ b/core/java/com/android/internal/app/AlertActivity.java @@ -17,11 +17,9 @@ package com.android.internal.app; import android.app.Activity; -import android.app.AlertDialog; import android.app.Dialog; import android.content.DialogInterface; import android.os.Bundle; -import android.text.TextUtils; import android.view.KeyEvent; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; diff --git a/core/java/com/android/internal/app/AlertController.java b/core/java/com/android/internal/app/AlertController.java index 20d209f..9dabb4e 100644 --- a/core/java/com/android/internal/app/AlertController.java +++ b/core/java/com/android/internal/app/AlertController.java @@ -20,6 +20,7 @@ import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import com.android.internal.R; +import android.annotation.Nullable; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; @@ -38,7 +39,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.ViewParent; -import android.view.ViewTreeObserver; +import android.view.ViewStub; import android.view.Window; import android.view.WindowInsets; import android.view.WindowManager; @@ -450,27 +451,107 @@ public class AlertController { } } + /** + * Resolves whether a custom or default panel should be used. Removes the + * default panel if a custom panel should be used. If the resolved panel is + * a view stub, inflates before returning. + * + * @param customPanel the custom panel + * @param defaultPanel the default panel + * @return the panel to use + */ + @Nullable + private ViewGroup resolvePanel(@Nullable View customPanel, @Nullable View defaultPanel) { + if (customPanel == null) { + // Inflate the default panel, if needed. + if (defaultPanel instanceof ViewStub) { + defaultPanel = ((ViewStub) defaultPanel).inflate(); + } + + return (ViewGroup) defaultPanel; + } + + // Remove the default panel entirely. + if (defaultPanel != null) { + final ViewParent parent = defaultPanel.getParent(); + if (parent instanceof ViewGroup) { + ((ViewGroup) parent).removeView(defaultPanel); + } + } + + // Inflate the custom panel, if needed. + if (customPanel instanceof ViewStub) { + customPanel = ((ViewStub) customPanel).inflate(); + } + + return (ViewGroup) customPanel; + } + private void setupView() { - final ViewGroup contentPanel = (ViewGroup) mWindow.findViewById(R.id.contentPanel); - setupContent(contentPanel); - final boolean hasButtons = setupButtons(); + final View parentPanel = mWindow.findViewById(R.id.parentPanel); + final View defaultTopPanel = parentPanel.findViewById(R.id.topPanel); + final View defaultContentPanel = parentPanel.findViewById(R.id.contentPanel); + final View defaultButtonPanel = parentPanel.findViewById(R.id.buttonPanel); - 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); + // Install custom content before setting up the title or buttons so + // that we can handle panel overrides. + final ViewGroup customPanel = (ViewGroup) parentPanel.findViewById(R.id.customPanel); + setupCustomContent(customPanel); - final View buttonPanel = mWindow.findViewById(R.id.buttonPanel); - if (!hasButtons) { - buttonPanel.setVisibility(View.GONE); - final View spacer = mWindow.findViewById(R.id.textSpacerNoButtons); - if (spacer != null) { - spacer.setVisibility(View.VISIBLE); + final View customTopPanel = customPanel.findViewById(R.id.topPanel); + final View customContentPanel = customPanel.findViewById(R.id.contentPanel); + final View customButtonPanel = customPanel.findViewById(R.id.buttonPanel); + + // Resolve the correct panels and remove the defaults, if needed. + final ViewGroup topPanel = resolvePanel(customTopPanel, defaultTopPanel); + final ViewGroup contentPanel = resolvePanel(customContentPanel, defaultContentPanel); + final ViewGroup buttonPanel = resolvePanel(customButtonPanel, defaultButtonPanel); + + setupContent(contentPanel); + setupButtons(buttonPanel); + setupTitle(topPanel); + + final boolean hasCustomPanel = customPanel != null + && customPanel.getVisibility() != View.GONE; + final boolean hasTopPanel = topPanel != null + && topPanel.getVisibility() != View.GONE; + final boolean hasButtonPanel = buttonPanel != null + && buttonPanel.getVisibility() != View.GONE; + + // Only display the text spacer if we don't have buttons. + if (!hasButtonPanel) { + if (contentPanel != null) { + final View spacer = contentPanel.findViewById(R.id.textSpacerNoButtons); + if (spacer != null) { + spacer.setVisibility(View.VISIBLE); + } } mWindow.setCloseOnTouchOutsideIfNotSet(true); } - final FrameLayout customPanel = (FrameLayout) mWindow.findViewById(R.id.customPanel); + // Only display the divider if we have a title and a custom view or a + // message. + if (hasTopPanel) { + final View divider; + if (mMessage != null || hasCustomPanel || mListView != null) { + divider = topPanel.findViewById(R.id.titleDivider); + } else { + divider = topPanel.findViewById(R.id.titleDividerTop); + } + + if (divider != null) { + divider.setVisibility(View.VISIBLE); + } + } + + final TypedArray a = mContext.obtainStyledAttributes( + null, R.styleable.AlertDialog, R.attr.alertDialogStyle, 0); + setBackground(a, topPanel, contentPanel, customPanel, buttonPanel, + hasTopPanel, hasCustomPanel, hasButtonPanel); + a.recycle(); + } + + private void setupCustomContent(ViewGroup customPanel) { final View customView; if (mView != null) { customView = mView; @@ -502,30 +583,9 @@ public class AlertController { } else { customPanel.setVisibility(View.GONE); } - - // Only display the divider if we have a title and a custom view or a - // message. - if (hasTitle) { - final View divider; - if (mMessage != null || customView != null || mListView != null) { - divider = mWindow.findViewById(R.id.titleDivider); - } else { - divider = mWindow.findViewById(R.id.titleDividerTop); - } - - if (divider != null) { - divider.setVisibility(View.VISIBLE); - } - } - - setBackground(a, topPanel, contentPanel, customPanel, buttonPanel, hasTitle, hasCustomView, - hasButtons); - a.recycle(); } - private boolean setupTitle(ViewGroup topPanel) { - boolean hasTitle = true; - + private void setupTitle(ViewGroup topPanel) { if (mCustomTitleView != null) { // Add the custom title view directly to the topPanel layout LayoutParams lp = new LayoutParams( @@ -567,18 +627,16 @@ public class AlertController { titleTemplate.setVisibility(View.GONE); mIconView.setVisibility(View.GONE); topPanel.setVisibility(View.GONE); - hasTitle = false; } } - return hasTitle; } private void setupContent(ViewGroup contentPanel) { - mScrollView = (ScrollView) mWindow.findViewById(R.id.scrollView); + mScrollView = (ScrollView) contentPanel.findViewById(R.id.scrollView); mScrollView.setFocusable(false); // Special case for users that only want to display a String - mMessageView = (TextView) mWindow.findViewById(R.id.message); + mMessageView = (TextView) contentPanel.findViewById(R.id.message); if (mMessageView == null) { return; } @@ -601,8 +659,8 @@ public class AlertController { } // Set up scroll indicators (if present). - final View indicatorUp = mWindow.findViewById(R.id.scrollIndicatorUp); - final View indicatorDown = mWindow.findViewById(R.id.scrollIndicatorDown); + final View indicatorUp = contentPanel.findViewById(R.id.scrollIndicatorUp); + final View indicatorDown = contentPanel.findViewById(R.id.scrollIndicatorDown); if (indicatorUp != null || indicatorDown != null) { if (mMessage != null) { // We're just showing the ScrollView, set up listener. @@ -663,12 +721,12 @@ public class AlertController { } } - private boolean setupButtons() { + private void setupButtons(ViewGroup buttonPanel) { int BIT_BUTTON_POSITIVE = 1; int BIT_BUTTON_NEGATIVE = 2; int BIT_BUTTON_NEUTRAL = 4; int whichButtons = 0; - mButtonPositive = (Button) mWindow.findViewById(R.id.button1); + mButtonPositive = (Button) buttonPanel.findViewById(R.id.button1); mButtonPositive.setOnClickListener(mButtonHandler); if (TextUtils.isEmpty(mButtonPositiveText)) { @@ -679,7 +737,7 @@ public class AlertController { whichButtons = whichButtons | BIT_BUTTON_POSITIVE; } - mButtonNegative = (Button) mWindow.findViewById(R.id.button2); + mButtonNegative = (Button) buttonPanel.findViewById(R.id.button2); mButtonNegative.setOnClickListener(mButtonHandler); if (TextUtils.isEmpty(mButtonNegativeText)) { @@ -691,7 +749,7 @@ public class AlertController { whichButtons = whichButtons | BIT_BUTTON_NEGATIVE; } - mButtonNeutral = (Button) mWindow.findViewById(R.id.button3); + mButtonNeutral = (Button) buttonPanel.findViewById(R.id.button3); mButtonNeutral.setOnClickListener(mButtonHandler); if (TextUtils.isEmpty(mButtonNeutralText)) { @@ -717,7 +775,10 @@ public class AlertController { } } - return whichButtons != 0; + final boolean hasButtons = whichButtons != 0; + if (!hasButtons) { + buttonPanel.setVisibility(View.GONE); + } } private void centerButton(Button button) { diff --git a/core/java/com/android/internal/app/DumpHeapActivity.java b/core/java/com/android/internal/app/DumpHeapActivity.java new file mode 100644 index 0000000..7e70b0c --- /dev/null +++ b/core/java/com/android/internal/app/DumpHeapActivity.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2015 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.app; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.ClipData; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.util.DebugUtils; + +/** + * This activity is displayed when the system has collected a heap dump from + * a large process and the user has selected to share it. + */ +public class DumpHeapActivity extends Activity { + /** The process we are reporting */ + public static final String KEY_PROCESS = "process"; + /** The size limit the process reached */ + public static final String KEY_SIZE = "size"; + + // Broadcast action to determine when to delete the current dump heap data. + public static final String ACTION_DELETE_DUMPHEAP = "com.android.server.am.DELETE_DUMPHEAP"; + + // Extra for above: delay delete of data, since the user is in the process of sharing it. + public static final String EXTRA_DELAY_DELETE = "delay_delete"; + + static final public Uri JAVA_URI = Uri.parse("content://com.android.server.heapdump/java"); + + String mProcess; + long mSize; + AlertDialog mDialog; + boolean mHandled = false; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mProcess = getIntent().getStringExtra(KEY_PROCESS); + mSize = getIntent().getLongExtra(KEY_SIZE, 0); + AlertDialog.Builder b = new AlertDialog.Builder(this, + android.R.style.Theme_Material_Light_Dialog_Alert); + b.setTitle(com.android.internal.R.string.dump_heap_title); + b.setMessage(getString(com.android.internal.R.string.dump_heap_text, + mProcess, DebugUtils.sizeValueToString(mSize, null))); + b.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mHandled = true; + sendBroadcast(new Intent(ACTION_DELETE_DUMPHEAP)); + finish(); + } + }); + b.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mHandled = true; + Intent broadcast = new Intent(ACTION_DELETE_DUMPHEAP); + broadcast.putExtra(EXTRA_DELAY_DELETE, true); + sendBroadcast(broadcast); + Intent intent = new Intent(Intent.ACTION_SEND); + ClipData clip = ClipData.newUri(getContentResolver(), "Heap Dump", JAVA_URI); + intent.setClipData(clip); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.setType(clip.getDescription().getMimeType(0)); + intent.putExtra(Intent.EXTRA_STREAM, JAVA_URI); + startActivity(Intent.createChooser(intent, + getText(com.android.internal.R.string.dump_heap_title))); + finish(); + } + }); + mDialog = b.show(); + } + + @Override + protected void onStop() { + super.onStop(); + if (!isChangingConfigurations()) { + if (!mHandled) { + sendBroadcast(new Intent(ACTION_DELETE_DUMPHEAP)); + } + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + mDialog.dismiss(); + } +} diff --git a/core/java/com/android/internal/app/IAssistScreenshotReceiver.aidl b/core/java/com/android/internal/app/IAssistScreenshotReceiver.aidl new file mode 100644 index 0000000..a987a16 --- /dev/null +++ b/core/java/com/android/internal/app/IAssistScreenshotReceiver.aidl @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2015 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.app; + +import android.graphics.Bitmap; + +/** @hide */ +oneway interface IAssistScreenshotReceiver { + void send(in Bitmap screenshot); +} diff --git a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl index 5a10524..6450d52 100644 --- a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl +++ b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl @@ -26,10 +26,13 @@ import android.service.voice.IVoiceInteractionService; import android.service.voice.IVoiceInteractionSession; interface IVoiceInteractionManagerService { - void startSession(IVoiceInteractionService service, in Bundle sessionArgs); + void showSession(IVoiceInteractionService service, in Bundle sessionArgs, int flags); boolean deliverNewSession(IBinder token, IVoiceInteractionSession session, IVoiceInteractor interactor); + boolean showSessionFromSession(IBinder token, in Bundle sessionArgs, int flags); + boolean hideSessionFromSession(IBinder token); int startVoiceActivity(IBinder token, in Intent intent, String resolvedType); + void setKeepAwake(IBinder token, boolean keepAwake); void finish(IBinder token); /** diff --git a/core/java/com/android/internal/app/IVoiceInteractor.aidl b/core/java/com/android/internal/app/IVoiceInteractor.aidl index 3e0b021..84e9cf0 100644 --- a/core/java/com/android/internal/app/IVoiceInteractor.aidl +++ b/core/java/com/android/internal/app/IVoiceInteractor.aidl @@ -16,6 +16,7 @@ package com.android.internal.app; +import android.app.VoiceInteractor; import android.os.Bundle; import com.android.internal.app.IVoiceInteractorCallback; @@ -27,6 +28,9 @@ import com.android.internal.app.IVoiceInteractorRequest; interface IVoiceInteractor { IVoiceInteractorRequest startConfirmation(String callingPackage, IVoiceInteractorCallback callback, CharSequence prompt, in Bundle extras); + IVoiceInteractorRequest startPickOption(String callingPackage, + IVoiceInteractorCallback callback, CharSequence prompt, + in VoiceInteractor.PickOptionRequest.Option[] options, in Bundle extras); IVoiceInteractorRequest startCompleteVoice(String callingPackage, IVoiceInteractorCallback callback, CharSequence message, in Bundle extras); IVoiceInteractorRequest startAbortVoice(String callingPackage, diff --git a/core/java/com/android/internal/app/IVoiceInteractorCallback.aidl b/core/java/com/android/internal/app/IVoiceInteractorCallback.aidl index dcd5759..1331e74 100644 --- a/core/java/com/android/internal/app/IVoiceInteractorCallback.aidl +++ b/core/java/com/android/internal/app/IVoiceInteractorCallback.aidl @@ -16,6 +16,7 @@ package com.android.internal.app; +import android.app.VoiceInteractor; import android.os.Bundle; import com.android.internal.app.IVoiceInteractorRequest; @@ -26,8 +27,10 @@ import com.android.internal.app.IVoiceInteractorRequest; oneway interface IVoiceInteractorCallback { void deliverConfirmationResult(IVoiceInteractorRequest request, boolean confirmed, in Bundle result); + void deliverPickOptionResult(IVoiceInteractorRequest request, boolean finished, + in VoiceInteractor.PickOptionRequest.Option[] selections, in Bundle result); void deliverCompleteVoiceResult(IVoiceInteractorRequest request, in Bundle result); void deliverAbortVoiceResult(IVoiceInteractorRequest request, in Bundle result); - void deliverCommandResult(IVoiceInteractorRequest request, boolean complete, in Bundle result); + void deliverCommandResult(IVoiceInteractorRequest request, boolean finished, in Bundle result); void deliverCancel(IVoiceInteractorRequest request); } diff --git a/core/java/com/android/internal/app/IntentForwarderActivity.java b/core/java/com/android/internal/app/IntentForwarderActivity.java index 7c1308f..f598828 100644 --- a/core/java/com/android/internal/app/IntentForwarderActivity.java +++ b/core/java/com/android/internal/app/IntentForwarderActivity.java @@ -28,7 +28,6 @@ import android.content.Intent; import android.content.pm.IPackageManager; import android.content.pm.UserInfo; import android.os.Bundle; -import android.os.Process; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; diff --git a/core/java/com/android/internal/app/ProcessStats.java b/core/java/com/android/internal/app/ProcessStats.java index 70fb510..75beee9 100644 --- a/core/java/com/android/internal/app/ProcessStats.java +++ b/core/java/com/android/internal/app/ProcessStats.java @@ -24,6 +24,7 @@ import android.os.UserHandle; import android.text.format.DateFormat; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.DebugUtils; import android.util.Log; import android.util.Slog; import android.util.SparseArray; @@ -897,17 +898,17 @@ public final class ProcessStats implements Parcelable { pw.print(STATE_NAMES[procStates[ip]]); pw.print(": "); pw.print(count); pw.print(" samples "); - printSizeValue(pw, proc.getPssMinimum(bucket) * 1024); + DebugUtils.printSizeValue(pw, proc.getPssMinimum(bucket) * 1024); pw.print(" "); - printSizeValue(pw, proc.getPssAverage(bucket) * 1024); + DebugUtils.printSizeValue(pw, proc.getPssAverage(bucket) * 1024); pw.print(" "); - printSizeValue(pw, proc.getPssMaximum(bucket) * 1024); + DebugUtils.printSizeValue(pw, proc.getPssMaximum(bucket) * 1024); pw.print(" / "); - printSizeValue(pw, proc.getPssUssMinimum(bucket) * 1024); + DebugUtils.printSizeValue(pw, proc.getPssUssMinimum(bucket) * 1024); pw.print(" "); - printSizeValue(pw, proc.getPssUssAverage(bucket) * 1024); + DebugUtils.printSizeValue(pw, proc.getPssUssAverage(bucket) * 1024); pw.print(" "); - printSizeValue(pw, proc.getPssUssMaximum(bucket) * 1024); + DebugUtils.printSizeValue(pw, proc.getPssUssMaximum(bucket) * 1024); pw.println(); } } @@ -924,9 +925,9 @@ public final class ProcessStats implements Parcelable { if (proc.mNumCachedKill != 0) { pw.print(prefix); pw.print("Killed from cached state: "); pw.print(proc.mNumCachedKill); pw.print(" times from pss "); - printSizeValue(pw, proc.mMinCachedKillPss * 1024); pw.print("-"); - printSizeValue(pw, proc.mAvgCachedKillPss * 1024); pw.print("-"); - printSizeValue(pw, proc.mMaxCachedKillPss * 1024); pw.println(); + DebugUtils.printSizeValue(pw, proc.mMinCachedKillPss * 1024); pw.print("-"); + DebugUtils.printSizeValue(pw, proc.mAvgCachedKillPss * 1024); pw.print("-"); + DebugUtils.printSizeValue(pw, proc.mMaxCachedKillPss * 1024); pw.println(); } } @@ -939,11 +940,11 @@ public final class ProcessStats implements Parcelable { int bucket, int index) { pw.print(prefix); pw.print(label); pw.print(": "); - printSizeValue(pw, getSysMemUsageValue(bucket, index) * 1024); + DebugUtils.printSizeValue(pw, getSysMemUsageValue(bucket, index) * 1024); pw.print(" min, "); - printSizeValue(pw, getSysMemUsageValue(bucket, index + 1) * 1024); + DebugUtils.printSizeValue(pw, getSysMemUsageValue(bucket, index + 1) * 1024); pw.print(" avg, "); - printSizeValue(pw, getSysMemUsageValue(bucket, index+2) * 1024); + DebugUtils.printSizeValue(pw, getSysMemUsageValue(bucket, index+2) * 1024); pw.println(" max"); } @@ -1150,43 +1151,6 @@ public final class ProcessStats implements Parcelable { pw.print("%"); } - static void printSizeValue(PrintWriter pw, long number) { - float result = number; - String suffix = ""; - if (result > 900) { - suffix = "KB"; - result = result / 1024; - } - if (result > 900) { - suffix = "MB"; - result = result / 1024; - } - if (result > 900) { - suffix = "GB"; - result = result / 1024; - } - if (result > 900) { - suffix = "TB"; - result = result / 1024; - } - if (result > 900) { - suffix = "PB"; - result = result / 1024; - } - String value; - if (result < 1) { - value = String.format("%.2f", result); - } else if (result < 10) { - value = String.format("%.1f", result); - } else if (result < 100) { - value = String.format("%.0f", result); - } else { - value = String.format("%.0f", result); - } - pw.print(value); - pw.print(suffix); - } - public static void dumpProcessListCsv(PrintWriter pw, ArrayList<ProcessState> procs, boolean sepScreenStates, int[] screenStates, boolean sepMemStates, int[] memStates, boolean sepProcStates, int[] procStates, long now) { @@ -2437,7 +2401,7 @@ public final class ProcessStats implements Parcelable { pw.print(prefix); pw.print(label); pw.print(": "); - printSizeValue(pw, mem); + DebugUtils.printSizeValue(pw, mem); pw.print(" ("); pw.print(samples); pw.print(" samples)"); @@ -2475,7 +2439,7 @@ public final class ProcessStats implements Parcelable { totalPss = printMemoryCategory(pw, " ", "Z-Ram ", totalMem.sysMemZRamWeight, totalMem.totalTime, totalPss, totalMem.sysMemSamples); pw.print(" TOTAL : "); - printSizeValue(pw, totalPss); + DebugUtils.printSizeValue(pw, totalPss); pw.println(); printMemoryCategory(pw, " ", STATE_NAMES[STATE_SERVICE_RESTARTING], totalMem.processStateWeight[STATE_SERVICE_RESTARTING], totalMem.totalTime, totalPss, @@ -3781,17 +3745,17 @@ public final class ProcessStats implements Parcelable { printPercent(pw, (double) totalTime / (double) overallTime); if (numPss > 0) { pw.print(" ("); - printSizeValue(pw, minPss * 1024); + DebugUtils.printSizeValue(pw, minPss * 1024); pw.print("-"); - printSizeValue(pw, avgPss * 1024); + DebugUtils.printSizeValue(pw, avgPss * 1024); pw.print("-"); - printSizeValue(pw, maxPss * 1024); + DebugUtils.printSizeValue(pw, maxPss * 1024); pw.print("/"); - printSizeValue(pw, minUss * 1024); + DebugUtils.printSizeValue(pw, minUss * 1024); pw.print("-"); - printSizeValue(pw, avgUss * 1024); + DebugUtils.printSizeValue(pw, avgUss * 1024); pw.print("-"); - printSizeValue(pw, maxUss * 1024); + DebugUtils.printSizeValue(pw, maxUss * 1024); if (full) { pw.print(" over "); pw.print(numPss); diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index 649a59f..3ceea9d 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -342,6 +342,9 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic return; } + // Do not show the profile switch message anymore. + mProfileSwitchMessageId = -1; + final Intent intent = intentForDisplayResolveInfo(dri); onIntentSelected(dri.ri, intent, false); finish(); @@ -505,15 +508,6 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic // Header views don't count. return; } - ResolveInfo resolveInfo = mAdapter.resolveInfoForPosition(position, true); - if (mResolvingHome && hasManagedProfile() - && !supportsManagedProfiles(resolveInfo)) { - Toast.makeText(this, String.format(getResources().getString( - com.android.internal.R.string.activity_resolver_work_profiles_support), - resolveInfo.activityInfo.loadLabel(getPackageManager()).toString()), - Toast.LENGTH_LONG).show(); - return; - } final int checkedPos = mListView.getCheckedItemPosition(); final boolean hasValidSelection = checkedPos != ListView.INVALID_POSITION; if (mAlwaysUseOption && (!hasValidSelection || mLastSelected != checkedPos)) { @@ -579,7 +573,6 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic mListView.getCheckedItemPosition() : mAdapter.getFilteredPosition(), id == R.id.button_always, mAlwaysUseOption); - dismiss(); } void startSelected(int which, boolean always, boolean filtered) { @@ -587,6 +580,14 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic return; } ResolveInfo ri = mAdapter.resolveInfoForPosition(which, filtered); + if (mResolvingHome && hasManagedProfile() && !supportsManagedProfiles(ri)) { + Toast.makeText(this, String.format(getResources().getString( + com.android.internal.R.string.activity_resolver_work_profiles_support), + ri.activityInfo.loadLabel(getPackageManager()).toString()), + Toast.LENGTH_LONG).show(); + return; + } + Intent intent = mAdapter.intentForPosition(which, filtered); onIntentSelected(ri, intent, always); finish(); @@ -841,6 +842,8 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic Log.d(TAG, "Error calling setLastChosenActivity\n" + re); } + // Clear the value of mOtherProfile from previous call. + mOtherProfile = null; mList.clear(); if (mBaseResolveList != null) { currentResolveList = mOrigResolveList = mBaseResolveList; diff --git a/core/java/com/android/internal/app/WindowDecorActionBar.java b/core/java/com/android/internal/app/WindowDecorActionBar.java index 061b535..7ae7d0f 100644 --- a/core/java/com/android/internal/app/WindowDecorActionBar.java +++ b/core/java/com/android/internal/app/WindowDecorActionBar.java @@ -20,6 +20,7 @@ import android.animation.ValueAnimator; import android.content.res.TypedArray; import android.view.ViewParent; import android.widget.Toolbar; + import com.android.internal.R; import com.android.internal.view.ActionBarPolicy; import com.android.internal.view.menu.MenuBuilder; @@ -46,6 +47,7 @@ import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.util.TypedValue; import android.view.ActionMode; +import android.view.ActionMode.Callback; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.Menu; @@ -88,20 +90,20 @@ public class WindowDecorActionBar extends ActionBar implements private TabImpl mSelectedTab; private int mSavedTabPosition = INVALID_POSITION; - + private boolean mDisplayHomeAsUpSet; - ActionModeImpl mActionMode; + ActionMode mActionMode; ActionMode mDeferredDestroyActionMode; ActionMode.Callback mDeferredModeDestroyCallback; - + private boolean mLastMenuVisibility; private ArrayList<OnMenuVisibilityListener> mMenuVisibilityListeners = new ArrayList<OnMenuVisibilityListener>(); private static final int CONTEXT_DISPLAY_NORMAL = 0; private static final int CONTEXT_DISPLAY_SPLIT = 1; - + private static final int INVALID_POSITION = -1; private int mContextDisplayMode; @@ -942,11 +944,12 @@ public class WindowDecorActionBar extends ActionBar implements private ActionMode.Callback mCallback; private WeakReference<View> mCustomView; - public ActionModeImpl(Context context, ActionMode.Callback callback) { + public ActionModeImpl( + Context context, ActionMode.Callback callback) { mActionModeContext = context; mCallback = callback; mMenu = new MenuBuilder(context) - .setDefaultShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + .setDefaultShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); mMenu.setCallback(this); } diff --git a/core/java/com/android/internal/backup/LocalTransport.java b/core/java/com/android/internal/backup/LocalTransport.java index 044383e..e32a3a2 100644 --- a/core/java/com/android/internal/backup/LocalTransport.java +++ b/core/java/com/android/internal/backup/LocalTransport.java @@ -44,11 +44,8 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashSet; -import java.util.List; - import static android.system.OsConstants.*; /** @@ -87,7 +84,6 @@ public class LocalTransport extends BackupTransport { private File mRestoreSetDir; private File mRestoreSetIncrementalDir; private File mRestoreSetFullDir; - private long mRestoreToken; // Additional bookkeeping for full backup private String mFullTargetPackage; @@ -96,20 +92,22 @@ public class LocalTransport extends BackupTransport { private BufferedOutputStream mFullBackupOutputStream; private byte[] mFullBackupBuffer; - private File mFullRestoreSetDir; - private HashSet<String> mFullRestorePackages; private FileInputStream mCurFullRestoreStream; private FileOutputStream mFullRestoreSocketStream; private byte[] mFullRestoreBuffer; - public LocalTransport(Context context) { - mContext = context; + private void makeDataDirs() { mCurrentSetDir.mkdirs(); - mCurrentSetFullDir.mkdir(); - mCurrentSetIncrementalDir.mkdir(); if (!SELinux.restorecon(mCurrentSetDir)) { Log.e(TAG, "SELinux restorecon failed for " + mCurrentSetDir); } + mCurrentSetFullDir.mkdir(); + mCurrentSetIncrementalDir.mkdir(); + } + + public LocalTransport(Context context) { + mContext = context; + makeDataDirs(); } @Override @@ -154,6 +152,7 @@ public class LocalTransport extends BackupTransport { public int initializeDevice() { if (DEBUG) Log.v(TAG, "wiping all data"); deleteContents(mCurrentSetDir); + makeDataDirs(); return TRANSPORT_OK; } @@ -372,6 +371,9 @@ public class LocalTransport extends BackupTransport { return TRANSPORT_ERROR; } } + if (DEBUG) { + Log.v(TAG, " stored " + numBytes + " of data"); + } return TRANSPORT_OK; } @@ -425,7 +427,6 @@ public class LocalTransport extends BackupTransport { + " matching packages"); mRestorePackages = packages; mRestorePackage = -1; - mRestoreToken = token; mRestoreSetDir = new File(mDataDir, Long.toString(token)); mRestoreSetIncrementalDir = new File(mRestoreSetDir, INCREMENTAL_DIR); mRestoreSetFullDir = new File(mRestoreSetDir, FULL_DATA_DIR); @@ -434,6 +435,10 @@ public class LocalTransport extends BackupTransport { @Override public RestoreDescription nextRestorePackage() { + if (DEBUG) { + Log.v(TAG, "nextRestorePackage() : mRestorePackage=" + mRestorePackage + + " length=" + mRestorePackages.length); + } if (mRestorePackages == null) throw new IllegalStateException("startRestore not called"); boolean found = false; @@ -444,7 +449,10 @@ public class LocalTransport extends BackupTransport { // skip packages where we have a data dir but no actual contents String[] contents = (new File(mRestoreSetIncrementalDir, name)).list(); if (contents != null && contents.length > 0) { - if (DEBUG) Log.v(TAG, " nextRestorePackage(TYPE_KEY_VALUE) = " + name); + if (DEBUG) { + Log.v(TAG, " nextRestorePackage(TYPE_KEY_VALUE) @ " + + mRestorePackage + " = " + name); + } mRestoreType = RestoreDescription.TYPE_KEY_VALUE; found = true; } @@ -453,7 +461,10 @@ public class LocalTransport extends BackupTransport { // No key/value data; check for [non-empty] full data File maybeFullData = new File(mRestoreSetFullDir, name); if (maybeFullData.length() > 0) { - if (DEBUG) Log.v(TAG, " nextRestorePackage(TYPE_FULL_STREAM) = " + name); + if (DEBUG) { + Log.v(TAG, " nextRestorePackage(TYPE_FULL_STREAM) @ " + + mRestorePackage + " = " + name); + } mRestoreType = RestoreDescription.TYPE_FULL_STREAM; mCurFullRestoreStream = null; // ensure starting from the ground state found = true; @@ -463,6 +474,11 @@ public class LocalTransport extends BackupTransport { if (found) { return new RestoreDescription(name, mRestoreType); } + + if (DEBUG) { + Log.v(TAG, " ... package @ " + mRestorePackage + " = " + name + + " has no data; skipping"); + } } if (DEBUG) Log.v(TAG, " no more packages to restore"); diff --git a/core/java/com/android/internal/http/multipart/ByteArrayPartSource.java b/core/java/com/android/internal/http/multipart/ByteArrayPartSource.java deleted file mode 100644 index faaac7f..0000000 --- a/core/java/com/android/internal/http/multipart/ByteArrayPartSource.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/methods/multipart/ByteArrayPartSource.java,v 1.7 2004/04/18 23:51:37 jsdever Exp $ - * $Revision: 480424 $ - * $Date: 2006-11-29 06:56:49 +0100 (Wed, 29 Nov 2006) $ - * - * ==================================================================== - * - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * <http://www.apache.org/>. - * - */ - -package com.android.internal.http.multipart; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; - -/** - * A PartSource that reads from a byte array. This class should be used when - * the data to post is already loaded into memory. - * - * @author <a href="mailto:becke@u.washington.edu">Michael Becke</a> - * - * @since 2.0 - */ -public class ByteArrayPartSource implements PartSource { - - /** Name of the source file. */ - private String fileName; - - /** Byte array of the source file. */ - private byte[] bytes; - - /** - * Constructor for ByteArrayPartSource. - * - * @param fileName the name of the file these bytes represent - * @param bytes the content of this part - */ - public ByteArrayPartSource(String fileName, byte[] bytes) { - - this.fileName = fileName; - this.bytes = bytes; - - } - - /** - * @see PartSource#getLength() - */ - public long getLength() { - return bytes.length; - } - - /** - * @see PartSource#getFileName() - */ - public String getFileName() { - return fileName; - } - - /** - * @see PartSource#createInputStream() - */ - public InputStream createInputStream() { - return new ByteArrayInputStream(bytes); - } - -} diff --git a/core/java/com/android/internal/http/multipart/FilePart.java b/core/java/com/android/internal/http/multipart/FilePart.java deleted file mode 100644 index 45e4be6..0000000 --- a/core/java/com/android/internal/http/multipart/FilePart.java +++ /dev/null @@ -1,259 +0,0 @@ -/* - * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/methods/multipart/FilePart.java,v 1.19 2004/04/18 23:51:37 jsdever Exp $ - * $Revision: 480424 $ - * $Date: 2006-11-29 06:56:49 +0100 (Wed, 29 Nov 2006) $ - * - * ==================================================================== - * - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * <http://www.apache.org/>. - * - */ - -package com.android.internal.http.multipart; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import org.apache.http.util.EncodingUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -/** - * This class implements a part of a Multipart post object that - * consists of a file. - * - * @author <a href="mailto:mattalbright@yahoo.com">Matthew Albright</a> - * @author <a href="mailto:jsdever@apache.org">Jeff Dever</a> - * @author <a href="mailto:adrian@ephox.com">Adrian Sutton</a> - * @author <a href="mailto:becke@u.washington.edu">Michael Becke</a> - * @author <a href="mailto:mdiggory@latte.harvard.edu">Mark Diggory</a> - * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a> - * @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 FilePart extends PartBase { - - /** Default content encoding of file attachments. */ - public static final String DEFAULT_CONTENT_TYPE = "application/octet-stream"; - - /** Default charset of file attachments. */ - public static final String DEFAULT_CHARSET = "ISO-8859-1"; - - /** Default transfer encoding of file attachments. */ - public static final String DEFAULT_TRANSFER_ENCODING = "binary"; - - /** Log object for this class. */ - private static final Log LOG = LogFactory.getLog(FilePart.class); - - /** Attachment's file name */ - protected static final String FILE_NAME = "; filename="; - - /** Attachment's file name as a byte array */ - private static final byte[] FILE_NAME_BYTES = - EncodingUtils.getAsciiBytes(FILE_NAME); - - /** Source of the file part. */ - private PartSource source; - - /** - * FilePart Constructor. - * - * @param name the name for this part - * @param partSource the source for this part - * @param contentType the content type for this part, if <code>null</code> the - * {@link #DEFAULT_CONTENT_TYPE default} is used - * @param charset the charset encoding for this part, if <code>null</code> the - * {@link #DEFAULT_CHARSET default} is used - */ - public FilePart(String name, PartSource partSource, String contentType, String charset) { - - super( - name, - contentType == null ? DEFAULT_CONTENT_TYPE : contentType, - charset == null ? "ISO-8859-1" : charset, - DEFAULT_TRANSFER_ENCODING - ); - - if (partSource == null) { - throw new IllegalArgumentException("Source may not be null"); - } - this.source = partSource; - } - - /** - * FilePart Constructor. - * - * @param name the name for this part - * @param partSource the source for this part - */ - public FilePart(String name, PartSource partSource) { - this(name, partSource, null, null); - } - - /** - * FilePart Constructor. - * - * @param name the name of the file part - * @param file the file to post - * - * @throws FileNotFoundException if the <i>file</i> is not a normal - * file or if it is not readable. - */ - public FilePart(String name, File file) - throws FileNotFoundException { - this(name, new FilePartSource(file), null, null); - } - - /** - * FilePart Constructor. - * - * @param name the name of the file part - * @param file the file to post - * @param contentType the content type for this part, if <code>null</code> the - * {@link #DEFAULT_CONTENT_TYPE default} is used - * @param charset the charset encoding for this part, if <code>null</code> the - * {@link #DEFAULT_CHARSET default} is used - * - * @throws FileNotFoundException if the <i>file</i> is not a normal - * file or if it is not readable. - */ - public FilePart(String name, File file, String contentType, String charset) - throws FileNotFoundException { - this(name, new FilePartSource(file), contentType, charset); - } - - /** - * FilePart Constructor. - * - * @param name the name of the file part - * @param fileName the file name - * @param file the file to post - * - * @throws FileNotFoundException if the <i>file</i> is not a normal - * file or if it is not readable. - */ - public FilePart(String name, String fileName, File file) - throws FileNotFoundException { - this(name, new FilePartSource(fileName, file), null, null); - } - - /** - * FilePart Constructor. - * - * @param name the name of the file part - * @param fileName the file name - * @param file the file to post - * @param contentType the content type for this part, if <code>null</code> the - * {@link #DEFAULT_CONTENT_TYPE default} is used - * @param charset the charset encoding for this part, if <code>null</code> the - * {@link #DEFAULT_CHARSET default} is used - * - * @throws FileNotFoundException if the <i>file</i> is not a normal - * file or if it is not readable. - */ - public FilePart(String name, String fileName, File file, String contentType, String charset) - throws FileNotFoundException { - this(name, new FilePartSource(fileName, file), contentType, charset); - } - - /** - * Write the disposition header to the output stream - * @param out The output stream - * @throws IOException If an IO problem occurs - * @see Part#sendDispositionHeader(OutputStream) - */ - @Override - protected void sendDispositionHeader(OutputStream out) - throws IOException { - LOG.trace("enter sendDispositionHeader(OutputStream out)"); - super.sendDispositionHeader(out); - String filename = this.source.getFileName(); - if (filename != null) { - out.write(FILE_NAME_BYTES); - out.write(QUOTE_BYTES); - out.write(EncodingUtils.getAsciiBytes(filename)); - out.write(QUOTE_BYTES); - } - } - - /** - * Write the data in "source" to the specified stream. - * @param out The output stream. - * @throws IOException if an IO problem occurs. - * @see Part#sendData(OutputStream) - */ - @Override - protected void sendData(OutputStream out) throws IOException { - LOG.trace("enter sendData(OutputStream out)"); - if (lengthOfData() == 0) { - - // this file contains no data, so there is nothing to send. - // we don't want to create a zero length buffer as this will - // cause an infinite loop when reading. - LOG.debug("No data to send."); - return; - } - - byte[] tmp = new byte[4096]; - InputStream instream = source.createInputStream(); - try { - int len; - while ((len = instream.read(tmp)) >= 0) { - out.write(tmp, 0, len); - } - } finally { - // we're done with the stream, close it - instream.close(); - } - } - - /** - * Returns the source of the file part. - * - * @return The source. - */ - protected PartSource getSource() { - LOG.trace("enter getSource()"); - return this.source; - } - - /** - * Return the length of the data. - * @return The length. - * @see Part#lengthOfData() - */ - @Override - protected long lengthOfData() { - LOG.trace("enter lengthOfData()"); - return source.getLength(); - } - -} diff --git a/core/java/com/android/internal/http/multipart/FilePartSource.java b/core/java/com/android/internal/http/multipart/FilePartSource.java deleted file mode 100644 index eb5cc0f..0000000 --- a/core/java/com/android/internal/http/multipart/FilePartSource.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/methods/multipart/FilePartSource.java,v 1.10 2004/04/18 23:51:37 jsdever Exp $ - * $Revision: 480424 $ - * $Date: 2006-11-29 06:56:49 +0100 (Wed, 29 Nov 2006) $ - * - * ==================================================================== - * - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * <http://www.apache.org/>. - * - */ - -package com.android.internal.http.multipart; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; - -/** - * A PartSource that reads from a File. - * - * @author <a href="mailto:becke@u.washington.edu">Michael Becke</a> - * @author <a href="mailto:mdiggory@latte.harvard.edu">Mark Diggory</a> - * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a> - * - * @since 2.0 - */ -public class FilePartSource implements PartSource { - - /** File part file. */ - private File file = null; - - /** File part file name. */ - private String fileName = null; - - /** - * Constructor for FilePartSource. - * - * @param file the FilePart source File. - * - * @throws FileNotFoundException if the file does not exist or - * cannot be read - */ - public FilePartSource(File file) throws FileNotFoundException { - this.file = file; - if (file != null) { - if (!file.isFile()) { - throw new FileNotFoundException("File is not a normal file."); - } - if (!file.canRead()) { - throw new FileNotFoundException("File is not readable."); - } - this.fileName = file.getName(); - } - } - - /** - * Constructor for FilePartSource. - * - * @param fileName the file name of the FilePart - * @param file the source File for the FilePart - * - * @throws FileNotFoundException if the file does not exist or - * cannot be read - */ - public FilePartSource(String fileName, File file) - throws FileNotFoundException { - this(file); - if (fileName != null) { - this.fileName = fileName; - } - } - - /** - * Return the length of the file - * @return the length of the file. - * @see PartSource#getLength() - */ - public long getLength() { - if (this.file != null) { - return this.file.length(); - } else { - return 0; - } - } - - /** - * Return the current filename - * @return the filename. - * @see PartSource#getFileName() - */ - public String getFileName() { - return (fileName == null) ? "noname" : fileName; - } - - /** - * Return a new {@link FileInputStream} for the current filename. - * @return the new input stream. - * @throws IOException If an IO problem occurs. - * @see PartSource#createInputStream() - */ - public InputStream createInputStream() throws IOException { - if (this.file != null) { - return new FileInputStream(this.file); - } else { - return new ByteArrayInputStream(new byte[] {}); - } - } - -} diff --git a/core/java/com/android/internal/http/multipart/MultipartEntity.java b/core/java/com/android/internal/http/multipart/MultipartEntity.java deleted file mode 100644 index 5319251..0000000 --- a/core/java/com/android/internal/http/multipart/MultipartEntity.java +++ /dev/null @@ -1,236 +0,0 @@ -/* - * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/methods/multipart/MultipartRequestEntity.java,v 1.1 2004/10/06 03:39:59 mbecke Exp $ - * $Revision: 502647 $ - * $Date: 2007-02-02 17:22:54 +0100 (Fri, 02 Feb 2007) $ - * - * ==================================================================== - * - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * <http://www.apache.org/>. - * - */ - -package com.android.internal.http.multipart; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.Random; - -import org.apache.http.Header; -import org.apache.http.entity.AbstractHttpEntity; -import org.apache.http.message.BasicHeader; -import org.apache.http.params.HttpParams; -import org.apache.http.protocol.HTTP; -import org.apache.http.util.EncodingUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -/** - * Implements a request entity suitable for an HTTP multipart POST method. - * <p> - * The HTTP multipart POST method is defined in section 3.3 of - * <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC1867</a>: - * <blockquote> - * The media-type multipart/form-data follows the rules of all multipart - * MIME data streams as outlined in RFC 1521. The multipart/form-data contains - * a series of parts. Each part is expected to contain a content-disposition - * header where the value is "form-data" and a name attribute specifies - * the field name within the form, e.g., 'content-disposition: form-data; - * name="xxxxx"', where xxxxx is the field name corresponding to that field. - * Field names originally in non-ASCII character sets may be encoded using - * the method outlined in RFC 1522. - * </blockquote> - * </p> - * <p>This entity is designed to be used in conjunction with the - * {@link org.apache.http.HttpRequest} to provide - * multipart posts. Example usage:</p> - * <pre> - * File f = new File("/path/fileToUpload.txt"); - * HttpRequest request = new HttpRequest("http://host/some_path"); - * Part[] parts = { - * new StringPart("param_name", "value"), - * new FilePart(f.getName(), f) - * }; - * filePost.setEntity( - * new MultipartRequestEntity(parts, filePost.getParams()) - * ); - * HttpClient client = new HttpClient(); - * int status = client.executeMethod(filePost); - * </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); - - /** The Content-Type for multipart/form-data. */ - private static final String MULTIPART_FORM_CONTENT_TYPE = "multipart/form-data"; - - /** - * Sets the value to use as the multipart boundary. - * <p> - * This parameter expects a value if type {@link String}. - * </p> - */ - public static final String MULTIPART_BOUNDARY = "http.method.multipart.boundary"; - - /** - * The pool of ASCII chars to be used for generating a multipart boundary. - */ - private static byte[] MULTIPART_CHARS = EncodingUtils.getAsciiBytes( - "-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"); - - /** - * Generates a random multipart boundary string. - */ - private static byte[] generateMultipartBoundary() { - Random rand = new Random(); - byte[] bytes = new byte[rand.nextInt(11) + 30]; // a random size from 30 to 40 - for (int i = 0; i < bytes.length; i++) { - bytes[i] = MULTIPART_CHARS[rand.nextInt(MULTIPART_CHARS.length)]; - } - return bytes; - } - - /** The MIME parts as set by the constructor */ - protected Part[] parts; - - private byte[] multipartBoundary; - - private HttpParams params; - - private boolean contentConsumed = false; - - /** - * Creates a new multipart entity containing the given parts. - * @param parts The parts to include. - * @param params The params of the HttpMethod using this entity. - */ - public MultipartEntity(Part[] parts, HttpParams params) { - if (parts == null) { - throw new IllegalArgumentException("parts cannot be null"); - } - if (params == null) { - throw new IllegalArgumentException("params cannot be null"); - } - this.parts = parts; - this.params = params; - } - - public MultipartEntity(Part[] parts) { - setContentType(MULTIPART_FORM_CONTENT_TYPE); - if (parts == null) { - throw new IllegalArgumentException("parts cannot be null"); - } - this.parts = parts; - this.params = null; - } - - /** - * Returns the MIME boundary string that is used to demarcate boundaries of - * this part. The first call to this method will implicitly create a new - * boundary string. To create a boundary string first the - * HttpMethodParams.MULTIPART_BOUNDARY parameter is considered. Otherwise - * a random one is generated. - * - * @return The boundary string of this entity in ASCII encoding. - */ - protected byte[] getMultipartBoundary() { - if (multipartBoundary == null) { - String temp = null; - if (params != null) { - temp = (String) params.getParameter(MULTIPART_BOUNDARY); - } - if (temp != null) { - multipartBoundary = EncodingUtils.getAsciiBytes(temp); - } else { - multipartBoundary = generateMultipartBoundary(); - } - } - return multipartBoundary; - } - - /** - * Returns <code>true</code> if all parts are repeatable, <code>false</code> otherwise. - */ - public boolean isRepeatable() { - for (int i = 0; i < parts.length; i++) { - if (!parts[i].isRepeatable()) { - return false; - } - } - return true; - } - - /* (non-Javadoc) - */ - public void writeTo(OutputStream out) throws IOException { - Part.sendParts(out, parts, getMultipartBoundary()); - } - /* (non-Javadoc) - * @see org.apache.commons.http.AbstractHttpEntity.#getContentType() - */ - @Override - public Header getContentType() { - StringBuffer buffer = new StringBuffer(MULTIPART_FORM_CONTENT_TYPE); - buffer.append("; boundary="); - buffer.append(EncodingUtils.getAsciiString(getMultipartBoundary())); - return new BasicHeader(HTTP.CONTENT_TYPE, buffer.toString()); - - } - - /* (non-Javadoc) - */ - public long getContentLength() { - try { - return Part.getLengthOfParts(parts, getMultipartBoundary()); - } catch (Exception e) { - log.error("An exception occurred while getting the length of the parts", e); - return 0; - } - } - - public InputStream getContent() throws IOException, IllegalStateException { - if(!isRepeatable() && this.contentConsumed ) { - throw new IllegalStateException("Content has been consumed"); - } - this.contentConsumed = true; - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - Part.sendParts(baos, this.parts, this.multipartBoundary); - ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); - return bais; - } - - public boolean isStreaming() { - return false; - } -} diff --git a/core/java/com/android/internal/http/multipart/Part.java b/core/java/com/android/internal/http/multipart/Part.java deleted file mode 100644 index 1d66dc6..0000000 --- a/core/java/com/android/internal/http/multipart/Part.java +++ /dev/null @@ -1,445 +0,0 @@ -/* - * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/methods/multipart/Part.java,v 1.16 2005/01/14 21:16:40 olegk Exp $ - * $Revision: 480424 $ - * $Date: 2006-11-29 06:56:49 +0100 (Wed, 29 Nov 2006) $ - * - * ==================================================================== - * - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * <http://www.apache.org/>. - * - */ - -package com.android.internal.http.multipart; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -import org.apache.http.util.EncodingUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -/** - * Abstract class for one Part of a multipart post object. - * - * @author <a href="mailto:mattalbright@yahoo.com">Matthew Albright</a> - * @author <a href="mailto:jsdever@apache.org">Jeff Dever</a> - * @author <a href="mailto:adrian@ephox.com">Adrian Sutton</a> - * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a> - * @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. */ - private static final Log LOG = LogFactory.getLog(Part.class); - - /** - * The boundary - * @deprecated use {@link org.apache.http.client.methods.multipart#MULTIPART_BOUNDARY} - */ - protected static final String BOUNDARY = "----------------314159265358979323846"; - - /** - * The boundary as a byte array. - * @deprecated - */ - protected static final byte[] BOUNDARY_BYTES = EncodingUtils.getAsciiBytes(BOUNDARY); - - /** - * The default boundary to be used if {@link #setPartBoundary(byte[])} has not - * been called. - */ - private static final byte[] DEFAULT_BOUNDARY_BYTES = BOUNDARY_BYTES; - - /** Carriage return/linefeed */ - protected static final String CRLF = "\r\n"; - - /** Carriage return/linefeed as a byte array */ - protected static final byte[] CRLF_BYTES = EncodingUtils.getAsciiBytes(CRLF); - - /** Content dispostion characters */ - protected static final String QUOTE = "\""; - - /** Content dispostion as a byte array */ - protected static final byte[] QUOTE_BYTES = - EncodingUtils.getAsciiBytes(QUOTE); - - /** Extra characters */ - protected static final String EXTRA = "--"; - - /** Extra characters as a byte array */ - protected static final byte[] EXTRA_BYTES = - EncodingUtils.getAsciiBytes(EXTRA); - - /** Content dispostion characters */ - protected static final String CONTENT_DISPOSITION = "Content-Disposition: form-data; name="; - - /** Content dispostion as a byte array */ - protected static final byte[] CONTENT_DISPOSITION_BYTES = - EncodingUtils.getAsciiBytes(CONTENT_DISPOSITION); - - /** Content type header */ - protected static final String CONTENT_TYPE = "Content-Type: "; - - /** Content type header as a byte array */ - protected static final byte[] CONTENT_TYPE_BYTES = - EncodingUtils.getAsciiBytes(CONTENT_TYPE); - - /** Content charset */ - protected static final String CHARSET = "; charset="; - - /** Content charset as a byte array */ - protected static final byte[] CHARSET_BYTES = - EncodingUtils.getAsciiBytes(CHARSET); - - /** Content type header */ - protected static final String CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding: "; - - /** Content type header as a byte array */ - protected static final byte[] CONTENT_TRANSFER_ENCODING_BYTES = - EncodingUtils.getAsciiBytes(CONTENT_TRANSFER_ENCODING); - - /** - * Return the boundary string. - * @return the boundary string - * @deprecated uses a constant string. Rather use {@link #getPartBoundary} - */ - public static String getBoundary() { - return BOUNDARY; - } - - /** - * The ASCII bytes to use as the multipart boundary. - */ - private byte[] boundaryBytes; - - /** - * Return the name of this part. - * @return The name. - */ - public abstract String getName(); - - /** - * Returns the content type of this part. - * @return the content type, or <code>null</code> to exclude the content type header - */ - public abstract String getContentType(); - - /** - * Return the character encoding of this part. - * @return the character encoding, or <code>null</code> to exclude the character - * encoding header - */ - public abstract String getCharSet(); - - /** - * Return the transfer encoding of this part. - * @return the transfer encoding, or <code>null</code> to exclude the transfer encoding header - */ - public abstract String getTransferEncoding(); - - /** - * Gets the part boundary to be used. - * @return the part boundary as an array of bytes. - * - * @since 3.0 - */ - protected byte[] getPartBoundary() { - if (boundaryBytes == null) { - // custom boundary bytes have not been set, use the default. - return DEFAULT_BOUNDARY_BYTES; - } else { - return boundaryBytes; - } - } - - /** - * Sets the part boundary. Only meant to be used by - * {@link Part#sendParts(OutputStream, Part[], byte[])} - * and {@link Part#getLengthOfParts(Part[], byte[])} - * @param boundaryBytes An array of ASCII bytes. - * @since 3.0 - */ - void setPartBoundary(byte[] boundaryBytes) { - this.boundaryBytes = boundaryBytes; - } - - /** - * Tests if this part can be sent more than once. - * @return <code>true</code> if {@link #sendData(OutputStream)} can be successfully called - * more than once. - * @since 3.0 - */ - public boolean isRepeatable() { - return true; - } - - /** - * Write the start to the specified output stream - * @param out The output stream - * @throws IOException If an IO problem occurs. - */ - protected void sendStart(OutputStream out) throws IOException { - LOG.trace("enter sendStart(OutputStream out)"); - out.write(EXTRA_BYTES); - out.write(getPartBoundary()); - out.write(CRLF_BYTES); - } - - /** - * Write the content disposition header to the specified output stream - * - * @param out The output stream - * @throws IOException If an IO problem occurs. - */ - protected void sendDispositionHeader(OutputStream out) throws IOException { - LOG.trace("enter sendDispositionHeader(OutputStream out)"); - out.write(CONTENT_DISPOSITION_BYTES); - out.write(QUOTE_BYTES); - out.write(EncodingUtils.getAsciiBytes(getName())); - out.write(QUOTE_BYTES); - } - - /** - * Write the content type header to the specified output stream - * @param out The output stream - * @throws IOException If an IO problem occurs. - */ - protected void sendContentTypeHeader(OutputStream out) throws IOException { - LOG.trace("enter sendContentTypeHeader(OutputStream out)"); - String contentType = getContentType(); - if (contentType != null) { - out.write(CRLF_BYTES); - out.write(CONTENT_TYPE_BYTES); - out.write(EncodingUtils.getAsciiBytes(contentType)); - String charSet = getCharSet(); - if (charSet != null) { - out.write(CHARSET_BYTES); - out.write(EncodingUtils.getAsciiBytes(charSet)); - } - } - } - - /** - * Write the content transfer encoding header to the specified - * output stream - * - * @param out The output stream - * @throws IOException If an IO problem occurs. - */ - protected void sendTransferEncodingHeader(OutputStream out) throws IOException { - LOG.trace("enter sendTransferEncodingHeader(OutputStream out)"); - String transferEncoding = getTransferEncoding(); - if (transferEncoding != null) { - out.write(CRLF_BYTES); - out.write(CONTENT_TRANSFER_ENCODING_BYTES); - out.write(EncodingUtils.getAsciiBytes(transferEncoding)); - } - } - - /** - * Write the end of the header to the output stream - * @param out The output stream - * @throws IOException If an IO problem occurs. - */ - protected void sendEndOfHeader(OutputStream out) throws IOException { - LOG.trace("enter sendEndOfHeader(OutputStream out)"); - out.write(CRLF_BYTES); - out.write(CRLF_BYTES); - } - - /** - * Write the data to the specified output stream - * @param out The output stream - * @throws IOException If an IO problem occurs. - */ - protected abstract void sendData(OutputStream out) throws IOException; - - /** - * Return the length of the main content - * - * @return long The length. - * @throws IOException If an IO problem occurs - */ - protected abstract long lengthOfData() throws IOException; - - /** - * Write the end data to the output stream. - * @param out The output stream - * @throws IOException If an IO problem occurs. - */ - protected void sendEnd(OutputStream out) throws IOException { - LOG.trace("enter sendEnd(OutputStream out)"); - out.write(CRLF_BYTES); - } - - /** - * Write all the data to the output stream. - * If you override this method make sure to override - * #length() as well - * - * @param out The output stream - * @throws IOException If an IO problem occurs. - */ - public void send(OutputStream out) throws IOException { - LOG.trace("enter send(OutputStream out)"); - sendStart(out); - sendDispositionHeader(out); - sendContentTypeHeader(out); - sendTransferEncodingHeader(out); - sendEndOfHeader(out); - sendData(out); - sendEnd(out); - } - - - /** - * Return the full length of all the data. - * If you override this method make sure to override - * #send(OutputStream) as well - * - * @return long The length. - * @throws IOException If an IO problem occurs - */ - public long length() throws IOException { - LOG.trace("enter length()"); - if (lengthOfData() < 0) { - return -1; - } - ByteArrayOutputStream overhead = new ByteArrayOutputStream(); - sendStart(overhead); - sendDispositionHeader(overhead); - sendContentTypeHeader(overhead); - sendTransferEncodingHeader(overhead); - sendEndOfHeader(overhead); - sendEnd(overhead); - return overhead.size() + lengthOfData(); - } - - /** - * Return a string representation of this object. - * @return A string representation of this object. - * @see java.lang.Object#toString() - */ - @Override - public String toString() { - return this.getName(); - } - - /** - * Write all parts and the last boundary to the specified output stream. - * - * @param out The stream to write to. - * @param parts The parts to write. - * - * @throws IOException If an I/O error occurs while writing the parts. - */ - public static void sendParts(OutputStream out, final Part[] parts) - throws IOException { - sendParts(out, parts, DEFAULT_BOUNDARY_BYTES); - } - - /** - * Write all parts and the last boundary to the specified output stream. - * - * @param out The stream to write to. - * @param parts The parts to write. - * @param partBoundary The ASCII bytes to use as the part boundary. - * - * @throws IOException If an I/O error occurs while writing the parts. - * - * @since 3.0 - */ - public static void sendParts(OutputStream out, Part[] parts, byte[] partBoundary) - throws IOException { - - if (parts == null) { - throw new IllegalArgumentException("Parts may not be null"); - } - if (partBoundary == null || partBoundary.length == 0) { - throw new IllegalArgumentException("partBoundary may not be empty"); - } - for (int i = 0; i < parts.length; i++) { - // set the part boundary before the part is sent - parts[i].setPartBoundary(partBoundary); - parts[i].send(out); - } - out.write(EXTRA_BYTES); - out.write(partBoundary); - out.write(EXTRA_BYTES); - out.write(CRLF_BYTES); - } - - /** - * Return the total sum of all parts and that of the last boundary - * - * @param parts The parts. - * @return The total length - * - * @throws IOException If an I/O error occurs while writing the parts. - */ - public static long getLengthOfParts(Part[] parts) - throws IOException { - return getLengthOfParts(parts, DEFAULT_BOUNDARY_BYTES); - } - - /** - * Gets the length of the multipart message including the given parts. - * - * @param parts The parts. - * @param partBoundary The ASCII bytes to use as the part boundary. - * @return The total length - * - * @throws IOException If an I/O error occurs while writing the parts. - * - * @since 3.0 - */ - public static long getLengthOfParts(Part[] parts, byte[] partBoundary) throws IOException { - LOG.trace("getLengthOfParts(Parts[])"); - if (parts == null) { - throw new IllegalArgumentException("Parts may not be null"); - } - long total = 0; - for (int i = 0; i < parts.length; i++) { - // set the part boundary before we calculate the part's length - parts[i].setPartBoundary(partBoundary); - long l = parts[i].length(); - if (l < 0) { - return -1; - } - total += l; - } - total += EXTRA_BYTES.length; - total += partBoundary.length; - total += EXTRA_BYTES.length; - total += CRLF_BYTES.length; - return total; - } -} diff --git a/core/java/com/android/internal/http/multipart/PartBase.java b/core/java/com/android/internal/http/multipart/PartBase.java deleted file mode 100644 index 876d15d..0000000 --- a/core/java/com/android/internal/http/multipart/PartBase.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/methods/multipart/PartBase.java,v 1.5 2004/04/18 23:51:37 jsdever Exp $ - * $Revision: 480424 $ - * $Date: 2006-11-29 06:56:49 +0100 (Wed, 29 Nov 2006) $ - * - * ==================================================================== - * - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * <http://www.apache.org/>. - * - */ - -package com.android.internal.http.multipart; - - -/** - * Provides setters and getters for the basic Part properties. - * - * @author Michael Becke - */ -public abstract class PartBase extends Part { - - /** Name of the file part. */ - private String name; - - /** Content type of the file part. */ - private String contentType; - - /** Content encoding of the file part. */ - private String charSet; - - /** The transfer encoding. */ - private String transferEncoding; - - /** - * Constructor. - * - * @param name The name of the part - * @param contentType The content type, or <code>null</code> - * @param charSet The character encoding, or <code>null</code> - * @param transferEncoding The transfer encoding, or <code>null</code> - */ - public PartBase(String name, String contentType, String charSet, String transferEncoding) { - - if (name == null) { - throw new IllegalArgumentException("Name must not be null"); - } - this.name = name; - this.contentType = contentType; - this.charSet = charSet; - this.transferEncoding = transferEncoding; - } - - /** - * Returns the name. - * @return The name. - * @see Part#getName() - */ - @Override - public String getName() { - return this.name; - } - - /** - * Returns the content type of this part. - * @return String The name. - */ - @Override - public String getContentType() { - return this.contentType; - } - - /** - * Return the character encoding of this part. - * @return String The name. - */ - @Override - public String getCharSet() { - return this.charSet; - } - - /** - * Returns the transfer encoding of this part. - * @return String The name. - */ - @Override - public String getTransferEncoding() { - return transferEncoding; - } - - /** - * Sets the character encoding. - * - * @param charSet the character encoding, or <code>null</code> to exclude the character - * encoding header - */ - public void setCharSet(String charSet) { - this.charSet = charSet; - } - - /** - * Sets the content type. - * - * @param contentType the content type, or <code>null</code> to exclude the content type header - */ - public void setContentType(String contentType) { - this.contentType = contentType; - } - - /** - * Sets the part name. - * - * @param name - */ - public void setName(String name) { - if (name == null) { - throw new IllegalArgumentException("Name must not be null"); - } - this.name = name; - } - - /** - * Sets the transfer encoding. - * - * @param transferEncoding the transfer encoding, or <code>null</code> to exclude the - * transfer encoding header - */ - public void setTransferEncoding(String transferEncoding) { - this.transferEncoding = transferEncoding; - } - -} diff --git a/core/java/com/android/internal/http/multipart/PartSource.java b/core/java/com/android/internal/http/multipart/PartSource.java deleted file mode 100644 index 3740696..0000000 --- a/core/java/com/android/internal/http/multipart/PartSource.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/methods/multipart/PartSource.java,v 1.6 2004/04/18 23:51:37 jsdever Exp $ - * $Revision: 480424 $ - * $Date: 2006-11-29 06:56:49 +0100 (Wed, 29 Nov 2006) $ - * - * ==================================================================== - * - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * <http://www.apache.org/>. - * - */ - -package com.android.internal.http.multipart; - -import java.io.IOException; -import java.io.InputStream; - -/** - * An interface for providing access to data when posting MultiPart messages. - * - * @see FilePart - * - * @author <a href="mailto:becke@u.washington.edu">Michael Becke</a> - * - * @since 2.0 - */ -public interface PartSource { - - /** - * Gets the number of bytes contained in this source. - * - * @return a value >= 0 - */ - long getLength(); - - /** - * Gets the name of the file this source represents. - * - * @return the fileName used for posting a MultiPart file part - */ - String getFileName(); - - /** - * Gets a new InputStream for reading this source. This method can be - * called more than once and should therefore return a new stream every - * time. - * - * @return a new InputStream - * - * @throws IOException if an error occurs when creating the InputStream - */ - InputStream createInputStream() throws IOException; - -} diff --git a/core/java/com/android/internal/http/multipart/StringPart.java b/core/java/com/android/internal/http/multipart/StringPart.java deleted file mode 100644 index 73d0f90..0000000 --- a/core/java/com/android/internal/http/multipart/StringPart.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/methods/multipart/StringPart.java,v 1.11 2004/04/18 23:51:37 jsdever Exp $ - * $Revision: 480424 $ - * $Date: 2006-11-29 06:56:49 +0100 (Wed, 29 Nov 2006) $ - * - * ==================================================================== - * - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * <http://www.apache.org/>. - * - */ - -package com.android.internal.http.multipart; - -import java.io.OutputStream; -import java.io.IOException; - -import org.apache.http.util.EncodingUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -/** - * Simple string parameter for a multipart post - * - * @author <a href="mailto:mattalbright@yahoo.com">Matthew Albright</a> - * @author <a href="mailto:jsdever@apache.org">Jeff Dever</a> - * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a> - * @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. */ - private static final Log LOG = LogFactory.getLog(StringPart.class); - - /** Default content encoding of string parameters. */ - public static final String DEFAULT_CONTENT_TYPE = "text/plain"; - - /** Default charset of string parameters*/ - public static final String DEFAULT_CHARSET = "US-ASCII"; - - /** Default transfer encoding of string parameters*/ - public static final String DEFAULT_TRANSFER_ENCODING = "8bit"; - - /** Contents of this StringPart. */ - private byte[] content; - - /** The String value of this part. */ - private String value; - - /** - * Constructor. - * - * @param name The name of the part - * @param value the string to post - * @param charset the charset to be used to encode the string, if <code>null</code> - * the {@link #DEFAULT_CHARSET default} is used - */ - public StringPart(String name, String value, String charset) { - - super( - name, - DEFAULT_CONTENT_TYPE, - charset == null ? DEFAULT_CHARSET : charset, - DEFAULT_TRANSFER_ENCODING - ); - if (value == null) { - throw new IllegalArgumentException("Value may not be null"); - } - if (value.indexOf(0) != -1) { - // See RFC 2048, 2.8. "8bit Data" - throw new IllegalArgumentException("NULs may not be present in string parts"); - } - this.value = value; - } - - /** - * Constructor. - * - * @param name The name of the part - * @param value the string to post - */ - public StringPart(String name, String value) { - this(name, value, null); - } - - /** - * Gets the content in bytes. Bytes are lazily created to allow the charset to be changed - * after the part is created. - * - * @return the content in bytes - */ - private byte[] getContent() { - if (content == null) { - content = EncodingUtils.getBytes(value, getCharSet()); - } - return content; - } - - /** - * Writes the data to the given OutputStream. - * @param out the OutputStream to write to - * @throws IOException if there is a write error - */ - @Override - protected void sendData(OutputStream out) throws IOException { - LOG.trace("enter sendData(OutputStream)"); - out.write(getContent()); - } - - /** - * Return the length of the data. - * @return The length of the data. - * @see Part#lengthOfData() - */ - @Override - protected long lengthOfData() { - LOG.trace("enter lengthOfData()"); - return getContent().length; - } - - /* (non-Javadoc) - * @see org.apache.commons.httpclient.methods.multipart.BasePart#setCharSet(java.lang.String) - */ - @Override - public void setCharSet(String charSet) { - super.setCharSet(charSet); - this.content = null; - } - -} diff --git a/core/java/com/android/internal/inputmethod/InputMethodUtils.java b/core/java/com/android/internal/inputmethod/InputMethodUtils.java index 183527c..57fcf57 100644 --- a/core/java/com/android/internal/inputmethod/InputMethodUtils.java +++ b/core/java/com/android/internal/inputmethod/InputMethodUtils.java @@ -35,6 +35,8 @@ import android.view.inputmethod.InputMethodSubtype; import android.view.textservice.SpellCheckerInfo; import android.view.textservice.TextServicesManager; +import com.android.internal.annotations.VisibleForTesting; + import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -118,19 +120,7 @@ public class InputMethodUtils { & ApplicationInfo.FLAG_SYSTEM) != 0; } - /** - * @deprecated Use {@link #isSystemImeThatHasSubtypeOf(InputMethodInfo, Context, boolean, - * Locale, boolean, String)} instead. - */ - @Deprecated - public static boolean isSystemImeThatHasEnglishKeyboardSubtype(InputMethodInfo imi) { - if (!isSystemIme(imi)) { - return false; - } - return containsSubtypeOf(imi, ENGLISH_LOCALE.getLanguage(), SUBTYPE_MODE_KEYBOARD); - } - - private static boolean isSystemImeThatHasSubtypeOf(final InputMethodInfo imi, + public static boolean isSystemImeThatHasSubtypeOf(final InputMethodInfo imi, final Context context, final boolean checkDefaultAttribute, @Nullable final Locale requiredLocale, final boolean checkCountry, final String requiredSubtypeMode) { @@ -380,33 +370,22 @@ public class InputMethodUtils { .build(); } - /** - * @deprecated Use {@link #isSystemImeThatHasSubtypeOf(InputMethodInfo, Context, boolean, - * Locale, boolean, String)} instead. - */ - @Deprecated - public static boolean isValidSystemDefaultIme( - boolean isSystemReady, InputMethodInfo imi, Context context) { - if (!isSystemReady) { - return false; - } - if (!isSystemIme(imi)) { - return false; - } - if (imi.getIsDefaultResourceId() != 0) { - try { - if (imi.isDefault(context) && containsSubtypeOf( - imi, context.getResources().getConfiguration().locale.getLanguage(), - SUBTYPE_MODE_ANY)) { - return true; - } - } catch (Resources.NotFoundException ex) { - } + public static Locale constructLocaleFromString(String localeStr) { + if (TextUtils.isEmpty(localeStr)) { + return null; } - if (imi.getSubtypeCount() == 0) { - Slog.w(TAG, "Found no subtypes in a system IME: " + imi.getPackageName()); + // TODO: Use {@link Locale#toLanguageTag()} and {@link Locale#forLanguageTag(languageTag)}. + String[] localeParams = localeStr.split("_", 3); + // The length of localeStr is guaranteed to always return a 1 <= value <= 3 + // because localeStr is not empty. + if (localeParams.length == 1) { + return new Locale(localeParams[0]); + } else if (localeParams.length == 2) { + return new Locale(localeParams[0], localeParams[1]); + } else if (localeParams.length == 3) { + return new Locale(localeParams[0], localeParams[1], localeParams[2]); } - return false; + return null; } public static boolean containsSubtypeOf(final InputMethodInfo imi, @@ -418,15 +397,16 @@ public class InputMethodUtils { for (int i = 0; i < N; ++i) { final InputMethodSubtype subtype = imi.getSubtypeAt(i); if (checkCountry) { - // TODO: Use {@link Locale#toLanguageTag()} and - // {@link Locale#forLanguageTag(languageTag)} instead. - if (!TextUtils.equals(subtype.getLocale(), locale.toString())) { + final Locale subtypeLocale = constructLocaleFromString(subtype.getLocale()); + if (subtypeLocale == null || + !TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage()) || + !TextUtils.equals(subtypeLocale.getCountry(), locale.getCountry())) { continue; } } else { final Locale subtypeLocale = new Locale(getLanguageFromLocaleString( subtype.getLocale())); - if (!subtypeLocale.getLanguage().equals(locale.getLanguage())) { + if (!TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage())) { continue; } } @@ -438,25 +418,6 @@ public class InputMethodUtils { return false; } - /** - * @deprecated Use {@link #containsSubtypeOf(InputMethodInfo, Locale, boolean, String)} instead. - */ - @Deprecated - public static boolean containsSubtypeOf(InputMethodInfo imi, String language, String mode) { - final int N = imi.getSubtypeCount(); - for (int i = 0; i < N; ++i) { - final InputMethodSubtype subtype = imi.getSubtypeAt(i); - if (!subtype.getLocale().startsWith(language)) { - continue; - } - if (mode == SUBTYPE_MODE_ANY || TextUtils.isEmpty(mode) || - mode.equalsIgnoreCase(subtype.getMode())) { - return true; - } - } - return false; - } - public static ArrayList<InputMethodSubtype> getSubtypes(InputMethodInfo imi) { ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>(); final int subtypeCount = imi.getSubtypeCount(); @@ -489,12 +450,15 @@ public class InputMethodUtils { while (i > 0) { i--; final InputMethodInfo imi = enabledImes.get(i); - if (InputMethodUtils.isSystemImeThatHasEnglishKeyboardSubtype(imi) - && !imi.isAuxiliaryIme()) { + if (imi.isAuxiliaryIme()) { + continue; + } + if (InputMethodUtils.isSystemIme(imi) + && containsSubtypeOf(imi, ENGLISH_LOCALE, false /* checkCountry */, + SUBTYPE_MODE_KEYBOARD)) { return imi; } - if (firstFoundSystemIme < 0 && InputMethodUtils.isSystemIme(imi) - && !imi.isAuxiliaryIme()) { + if (firstFoundSystemIme < 0 && InputMethodUtils.isSystemIme(imi)) { firstFoundSystemIme = i; } } @@ -518,7 +482,8 @@ public class InputMethodUtils { return NOT_A_SUBTYPE_ID; } - private static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked( + @VisibleForTesting + public static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked( Resources res, InputMethodInfo imi) { final List<InputMethodSubtype> subtypes = InputMethodUtils.getSubtypes(imi); final String systemLocale = res.getConfiguration().locale.toString(); diff --git a/core/java/com/android/internal/net/VpnInfo.aidl b/core/java/com/android/internal/net/VpnInfo.aidl new file mode 100644 index 0000000..6fc97be --- /dev/null +++ b/core/java/com/android/internal/net/VpnInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2015 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.net; + +parcelable VpnInfo; diff --git a/core/java/com/android/internal/net/VpnInfo.java b/core/java/com/android/internal/net/VpnInfo.java new file mode 100644 index 0000000..a676dac --- /dev/null +++ b/core/java/com/android/internal/net/VpnInfo.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2015 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.net; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * A lightweight container used to carry information of the ongoing VPN. + * Internal use only.. + * + * @hide + */ +public class VpnInfo implements Parcelable { + public int ownerUid; + public String vpnIface; + public String primaryUnderlyingIface; + + @Override + public String toString() { + return "VpnInfo{" + + "ownerUid=" + ownerUid + + ", vpnIface='" + vpnIface + '\'' + + ", primaryUnderlyingIface='" + primaryUnderlyingIface + '\'' + + '}'; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(ownerUid); + dest.writeString(vpnIface); + dest.writeString(primaryUnderlyingIface); + } + + public static final Parcelable.Creator<VpnInfo> CREATOR = new Parcelable.Creator<VpnInfo>() { + @Override + public VpnInfo createFromParcel(Parcel source) { + VpnInfo info = new VpnInfo(); + info.ownerUid = source.readInt(); + info.vpnIface = source.readString(); + info.primaryUnderlyingIface = source.readString(); + return info; + } + + @Override + public VpnInfo[] newArray(int size) { + return new VpnInfo[size]; + } + }; +} diff --git a/core/java/com/android/internal/os/BatterySipper.java b/core/java/com/android/internal/os/BatterySipper.java index cfeca08..4cd959f 100644 --- a/core/java/com/android/internal/os/BatterySipper.java +++ b/core/java/com/android/internal/os/BatterySipper.java @@ -26,12 +26,15 @@ public class BatterySipper implements Comparable<BatterySipper> { public double value; public double[] values; public DrainType drainType; + + // Measured in milliseconds. public long usageTime; public long cpuTime; public long gpsTime; public long wifiRunningTime; public long cpuFgTime; public long wakeLockTime; + public long mobileRxPackets; public long mobileTxPackets; public long mobileActive; @@ -48,6 +51,14 @@ public class BatterySipper implements Comparable<BatterySipper> { public String[] mPackages; public String packageWithHighestDrain; + // Measured in mAh (milli-ampere per hour). + public double wifiPower; + public double cpuPower; + public double wakeLockPower; + public double mobileRadioPower; + public double gpsPower; + public double sensorPower; + public enum DrainType { IDLE, CELL, @@ -107,4 +118,31 @@ public class BatterySipper implements Comparable<BatterySipper> { } return uidObj.getUid(); } + + /** + * Add stats from other to this BatterySipper. + */ + public void add(BatterySipper other) { + cpuTime += other.cpuTime; + gpsTime += other.gpsTime; + wifiRunningTime += other.wifiRunningTime; + cpuFgTime += other.cpuFgTime; + wakeLockTime += other.wakeLockTime; + mobileRxPackets += other.mobileRxPackets; + mobileTxPackets += other.mobileTxPackets; + mobileActive += other.mobileActive; + mobileActiveCount += other.mobileActiveCount; + wifiRxPackets += other.wifiRxPackets; + wifiTxPackets += other.wifiTxPackets; + mobileRxBytes += other.mobileRxBytes; + mobileTxBytes += other.mobileTxBytes; + wifiRxBytes += other.wifiRxBytes; + wifiTxBytes += other.wifiTxBytes; + wifiPower += other.wifiPower; + gpsPower += other.gpsPower; + cpuPower += other.cpuPower; + sensorPower += other.sensorPower; + mobileRadioPower += other.mobileRadioPower; + wakeLockPower += other.wakeLockPower; + } } diff --git a/core/java/com/android/internal/os/BatteryStatsHelper.java b/core/java/com/android/internal/os/BatteryStatsHelper.java index eae4427..d3611bf 100644 --- a/core/java/com/android/internal/os/BatteryStatsHelper.java +++ b/core/java/com/android/internal/os/BatteryStatsHelper.java @@ -344,6 +344,7 @@ public final class BatteryStatsHelper { mMobilemsppList.add(bs); } } + for (int i=0; i<mUserSippers.size(); i++) { List<BatterySipper> user = mUserSippers.valueAt(i); for (int j=0; j<user.size(); j++) { @@ -389,8 +390,8 @@ public final class BatteryStatsHelper { private void processAppUsage(SparseArray<UserHandle> asUsers) { final boolean forAllUsers = (asUsers.get(UserHandle.USER_ALL) != null); - SensorManager sensorManager = (SensorManager) mContext.getSystemService( - Context.SENSOR_SERVICE); + final SensorManager sensorManager = + (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE); final int which = mStatsType; final int speedSteps = mPowerProfile.getNumSpeedSteps(); final double[] powerCpuNormal = new double[speedSteps]; @@ -401,238 +402,317 @@ public final class BatteryStatsHelper { final double mobilePowerPerPacket = getMobilePowerPerPacket(); final double mobilePowerPerMs = getMobilePowerPerMs(); final double wifiPowerPerPacket = getWifiPowerPerPacket(); - long appWakelockTimeUs = 0; + long totalAppWakelockTimeUs = 0; BatterySipper osApp = null; mStatsPeriod = mTypeBatteryRealtime; - SparseArray<? extends Uid> uidStats = mStats.getUidStats(); + + final ArrayList<BatterySipper> appList = new ArrayList<>(); + + // Max values used to normalize later. + double maxWifiPower = 0; + double maxCpuPower = 0; + double maxWakeLockPower = 0; + double maxMobileRadioPower = 0; + double maxGpsPower = 0; + double maxSensorPower = 0; + + final SparseArray<? extends Uid> uidStats = mStats.getUidStats(); final int NU = uidStats.size(); for (int iu = 0; iu < NU; iu++) { - Uid u = uidStats.valueAt(iu); - double p; // in mAs - double power = 0; // in mAs - double highestDrain = 0; - String packageWithHighestDrain = null; - Map<String, ? extends BatteryStats.Uid.Proc> processStats = u.getProcessStats(); - long cpuTime = 0; - long cpuFgTime = 0; - long wakelockTime = 0; - long gpsTime = 0; + final Uid u = uidStats.valueAt(iu); + final BatterySipper app = new BatterySipper( + BatterySipper.DrainType.APP, u, new double[]{0}); + + final Map<String, ? extends BatteryStats.Uid.Proc> processStats = u.getProcessStats(); if (processStats.size() > 0) { - // Process CPU time + // Process CPU time. + + // Keep track of the package with highest drain. + double highestDrain = 0; + for (Map.Entry<String, ? extends BatteryStats.Uid.Proc> ent : processStats.entrySet()) { Uid.Proc ps = ent.getValue(); - final long userTime = ps.getUserTime(which); - final long systemTime = ps.getSystemTime(which); - final long foregroundTime = ps.getForegroundTime(which); - cpuFgTime += foregroundTime * 10; // convert to millis - final long tmpCpuTime = (userTime + systemTime) * 10; // convert to millis - int totalTimeAtSpeeds = 0; - // Get the total first + app.cpuFgTime += ps.getForegroundTime(which); + final long totalCpuTime = ps.getUserTime(which) + ps.getSystemTime(which); + app.cpuTime += totalCpuTime; + + // Calculate the total CPU time spent at the various speed steps. + long totalTimeAtSpeeds = 0; for (int step = 0; step < speedSteps; step++) { cpuSpeedStepTimes[step] = ps.getTimeAtCpuSpeedStep(step, which); totalTimeAtSpeeds += cpuSpeedStepTimes[step]; } - if (totalTimeAtSpeeds == 0) totalTimeAtSpeeds = 1; - // Then compute the ratio of time spent at each speed - double processPower = 0; + totalTimeAtSpeeds = Math.max(totalTimeAtSpeeds, 1); + + // Then compute the ratio of time spent at each speed and figure out + // the total power consumption. + double cpuPower = 0; for (int step = 0; step < speedSteps; step++) { - double ratio = (double) cpuSpeedStepTimes[step] / totalTimeAtSpeeds; - if (DEBUG && ratio != 0) Log.d(TAG, "UID " + u.getUid() + ": CPU step #" - + step + " ratio=" + makemAh(ratio) + " power=" - + makemAh(ratio*tmpCpuTime*powerCpuNormal[step] / (60*60*1000))); - processPower += ratio * tmpCpuTime * powerCpuNormal[step]; + final double ratio = (double) cpuSpeedStepTimes[step] / totalTimeAtSpeeds; + final double cpuSpeedStepPower = + ratio * totalCpuTime * powerCpuNormal[step]; + if (DEBUG && ratio != 0) { + Log.d(TAG, "UID " + u.getUid() + ": CPU step #" + + step + " ratio=" + makemAh(ratio) + " power=" + + makemAh(cpuSpeedStepPower / (60 * 60 * 1000))); + } + cpuPower += cpuSpeedStepPower; } - cpuTime += tmpCpuTime; - if (DEBUG && processPower != 0) { + + if (DEBUG && cpuPower != 0) { Log.d(TAG, String.format("process %s, cpu power=%s", - ent.getKey(), makemAh(processPower / (60*60*1000)))); + ent.getKey(), makemAh(cpuPower / (60 * 60 * 1000)))); } - power += processPower; - if (packageWithHighestDrain == null - || packageWithHighestDrain.startsWith("*")) { - highestDrain = processPower; - packageWithHighestDrain = ent.getKey(); - } else if (highestDrain < processPower - && !ent.getKey().startsWith("*")) { - highestDrain = processPower; - packageWithHighestDrain = ent.getKey(); + app.cpuPower += cpuPower; + + // Each App can have multiple packages and with multiple running processes. + // Keep track of the package who's process has the highest drain. + if (app.packageWithHighestDrain == null || + app.packageWithHighestDrain.startsWith("*")) { + highestDrain = cpuPower; + app.packageWithHighestDrain = ent.getKey(); + } else if (highestDrain < cpuPower && !ent.getKey().startsWith("*")) { + highestDrain = cpuPower; + app.packageWithHighestDrain = ent.getKey(); } } } - if (cpuFgTime > cpuTime) { - if (DEBUG && cpuFgTime > cpuTime + 10000) { + + // Ensure that the CPU times make sense. + if (app.cpuFgTime > app.cpuTime) { + if (DEBUG && app.cpuFgTime > app.cpuTime + 10000) { Log.d(TAG, "WARNING! Cputime is more than 10 seconds behind Foreground time"); } - cpuTime = cpuFgTime; // Statistics may not have been gathered yet. + + // Statistics may not have been gathered yet. + app.cpuTime = app.cpuFgTime; } - power /= (60*60*1000); + + // Convert the CPU power to mAh + app.cpuPower /= (60 * 60 * 1000); + maxCpuPower = Math.max(maxCpuPower, app.cpuPower); // Process wake lock usage - Map<String, ? extends BatteryStats.Uid.Wakelock> wakelockStats = u.getWakelockStats(); + final Map<String, ? extends BatteryStats.Uid.Wakelock> wakelockStats = + u.getWakelockStats(); + long wakeLockTimeUs = 0; for (Map.Entry<String, ? extends BatteryStats.Uid.Wakelock> wakelockEntry : wakelockStats.entrySet()) { - Uid.Wakelock wakelock = wakelockEntry.getValue(); + final Uid.Wakelock wakelock = wakelockEntry.getValue(); + // Only care about partial wake locks since full wake locks // are canceled when the user turns the screen off. BatteryStats.Timer timer = wakelock.getWakeTime(BatteryStats.WAKE_TYPE_PARTIAL); if (timer != null) { - wakelockTime += timer.getTotalTimeLocked(mRawRealtime, which); + wakeLockTimeUs += timer.getTotalTimeLocked(mRawRealtime, which); } } - appWakelockTimeUs += wakelockTime; - wakelockTime /= 1000; // convert to millis - - // Add cost of holding a wake lock - p = (wakelockTime - * mPowerProfile.getAveragePower(PowerProfile.POWER_CPU_AWAKE)) / (60*60*1000); - if (DEBUG && p != 0) Log.d(TAG, "UID " + u.getUid() + ": wake " - + wakelockTime + " power=" + makemAh(p)); - power += p; + app.wakeLockTime = wakeLockTimeUs / 1000; // convert to millis + totalAppWakelockTimeUs += wakeLockTimeUs; + + // Add cost of holding a wake lock. + app.wakeLockPower = (app.wakeLockTime * + mPowerProfile.getAveragePower(PowerProfile.POWER_CPU_AWAKE)) / (60 * 60 * 1000); + if (DEBUG && app.wakeLockPower != 0) { + Log.d(TAG, "UID " + u.getUid() + ": wake " + + app.wakeLockTime + " power=" + makemAh(app.wakeLockPower)); + } + maxWakeLockPower = Math.max(maxWakeLockPower, app.wakeLockPower); - // Add cost of mobile traffic - final long mobileRx = u.getNetworkActivityPackets(NETWORK_MOBILE_RX_DATA, mStatsType); - final long mobileTx = u.getNetworkActivityPackets(NETWORK_MOBILE_TX_DATA, mStatsType); - final long mobileRxB = u.getNetworkActivityBytes(NETWORK_MOBILE_RX_DATA, mStatsType); - final long mobileTxB = u.getNetworkActivityBytes(NETWORK_MOBILE_TX_DATA, mStatsType); + // Add cost of mobile traffic. final long mobileActive = u.getMobileRadioActiveTime(mStatsType); + app.mobileRxPackets = u.getNetworkActivityPackets(NETWORK_MOBILE_RX_DATA, mStatsType); + app.mobileTxPackets = u.getNetworkActivityPackets(NETWORK_MOBILE_TX_DATA, mStatsType); + app.mobileActive = mobileActive / 1000; + app.mobileActiveCount = u.getMobileRadioActiveCount(mStatsType); + app.mobileRxBytes = u.getNetworkActivityBytes(NETWORK_MOBILE_RX_DATA, mStatsType); + app.mobileTxBytes = u.getNetworkActivityBytes(NETWORK_MOBILE_TX_DATA, mStatsType); + if (mobileActive > 0) { // We are tracking when the radio is up, so can use the active time to // determine power use. mAppMobileActive += mobileActive; - p = (mobilePowerPerMs * mobileActive) / 1000; + app.mobileRadioPower = (mobilePowerPerMs * mobileActive) / 1000; } else { // We are not tracking when the radio is up, so must approximate power use // based on the number of packets. - p = (mobileRx + mobileTx) * mobilePowerPerPacket; + app.mobileRadioPower = (app.mobileRxPackets + app.mobileTxPackets) + * mobilePowerPerPacket; } - if (DEBUG && p != 0) Log.d(TAG, "UID " + u.getUid() + ": mobile packets " - + (mobileRx+mobileTx) + " active time " + mobileActive - + " power=" + makemAh(p)); - power += p; + if (DEBUG && app.mobileRadioPower != 0) { + Log.d(TAG, "UID " + u.getUid() + ": mobile packets " + + (app.mobileRxPackets + app.mobileTxPackets) + + " active time " + mobileActive + + " power=" + makemAh(app.mobileRadioPower)); + } + maxMobileRadioPower = Math.max(maxMobileRadioPower, app.mobileRadioPower); // Add cost of wifi traffic - final long wifiRx = u.getNetworkActivityPackets(NETWORK_WIFI_RX_DATA, mStatsType); - final long wifiTx = u.getNetworkActivityPackets(NETWORK_WIFI_TX_DATA, mStatsType); - final long wifiRxB = u.getNetworkActivityBytes(NETWORK_WIFI_RX_DATA, mStatsType); - final long wifiTxB = u.getNetworkActivityBytes(NETWORK_WIFI_TX_DATA, mStatsType); - p = (wifiRx + wifiTx) * wifiPowerPerPacket; - if (DEBUG && p != 0) Log.d(TAG, "UID " + u.getUid() + ": wifi packets " - + (mobileRx+mobileTx) + " power=" + makemAh(p)); - power += p; + app.wifiRxPackets = u.getNetworkActivityPackets(NETWORK_WIFI_RX_DATA, mStatsType); + app.wifiTxPackets = u.getNetworkActivityPackets(NETWORK_WIFI_TX_DATA, mStatsType); + app.wifiRxBytes = u.getNetworkActivityBytes(NETWORK_WIFI_RX_DATA, mStatsType); + app.wifiTxBytes = u.getNetworkActivityBytes(NETWORK_WIFI_TX_DATA, mStatsType); + + final double wifiPacketPower = (app.wifiRxPackets + app.wifiTxPackets) + * wifiPowerPerPacket; + if (DEBUG && wifiPacketPower != 0) { + Log.d(TAG, "UID " + u.getUid() + ": wifi packets " + + (app.wifiRxPackets + app.wifiTxPackets) + + " power=" + makemAh(wifiPacketPower)); + } // Add cost of keeping WIFI running. - long wifiRunningTimeMs = u.getWifiRunningTime(mRawRealtime, which) / 1000; - mAppWifiRunning += wifiRunningTimeMs; - p = (wifiRunningTimeMs - * mPowerProfile.getAveragePower(PowerProfile.POWER_WIFI_ON)) / (60*60*1000); - if (DEBUG && p != 0) Log.d(TAG, "UID " + u.getUid() + ": wifi running " - + wifiRunningTimeMs + " power=" + makemAh(p)); - power += p; + app.wifiRunningTime = u.getWifiRunningTime(mRawRealtime, which) / 1000; + mAppWifiRunning += app.wifiRunningTime; + + final double wifiLockPower = (app.wifiRunningTime + * mPowerProfile.getAveragePower(PowerProfile.POWER_WIFI_ON)) / (60 * 60 * 1000); + if (DEBUG && wifiLockPower != 0) { + Log.d(TAG, "UID " + u.getUid() + ": wifi running " + + app.wifiRunningTime + " power=" + makemAh(wifiLockPower)); + } // Add cost of WIFI scans - long wifiScanTimeMs = u.getWifiScanTime(mRawRealtime, which) / 1000; - p = (wifiScanTimeMs - * mPowerProfile.getAveragePower(PowerProfile.POWER_WIFI_SCAN)) / (60*60*1000); - if (DEBUG) Log.d(TAG, "UID " + u.getUid() + ": wifi scan " + wifiScanTimeMs - + " power=" + makemAh(p)); - power += p; + final long wifiScanTimeMs = u.getWifiScanTime(mRawRealtime, which) / 1000; + final double wifiScanPower = (wifiScanTimeMs + * mPowerProfile.getAveragePower(PowerProfile.POWER_WIFI_SCAN)) + / (60 * 60 * 1000); + if (DEBUG && wifiScanPower != 0) { + Log.d(TAG, "UID " + u.getUid() + ": wifi scan " + wifiScanTimeMs + + " power=" + makemAh(wifiScanPower)); + } + + // Add cost of WIFI batch scans. + double wifiBatchScanPower = 0; for (int bin = 0; bin < BatteryStats.Uid.NUM_WIFI_BATCHED_SCAN_BINS; bin++) { - long batchScanTimeMs = u.getWifiBatchedScanTime(bin, mRawRealtime, which) / 1000; - p = ((batchScanTimeMs + final long batchScanTimeMs = + u.getWifiBatchedScanTime(bin, mRawRealtime, which) / 1000; + final double batchScanPower = ((batchScanTimeMs * mPowerProfile.getAveragePower(PowerProfile.POWER_WIFI_BATCHED_SCAN, bin)) - ) / (60*60*1000); - if (DEBUG && p != 0) Log.d(TAG, "UID " + u.getUid() + ": wifi batched scan # " + bin - + " time=" + batchScanTimeMs + " power=" + makemAh(p)); - power += p; + ) / (60 * 60 * 1000); + if (DEBUG && batchScanPower != 0) { + Log.d(TAG, "UID " + u.getUid() + ": wifi batched scan # " + bin + + " time=" + batchScanTimeMs + " power=" + makemAh(batchScanPower)); + } + wifiBatchScanPower += batchScanPower; } + // Add up all the WiFi costs. + app.wifiPower = wifiPacketPower + wifiLockPower + wifiScanPower + wifiBatchScanPower; + maxWifiPower = Math.max(maxWifiPower, app.wifiPower); + // Process Sensor usage - SparseArray<? extends BatteryStats.Uid.Sensor> sensorStats = u.getSensorStats(); - int NSE = sensorStats.size(); - for (int ise=0; ise<NSE; ise++) { - Uid.Sensor sensor = sensorStats.valueAt(ise); - int sensorHandle = sensorStats.keyAt(ise); - BatteryStats.Timer timer = sensor.getSensorTime(); - long sensorTime = timer.getTotalTimeLocked(mRawRealtime, which) / 1000; - double multiplier = 0; + final SparseArray<? extends BatteryStats.Uid.Sensor> sensorStats = u.getSensorStats(); + final int NSE = sensorStats.size(); + for (int ise = 0; ise < NSE; ise++) { + final Uid.Sensor sensor = sensorStats.valueAt(ise); + final int sensorHandle = sensorStats.keyAt(ise); + final BatteryStats.Timer timer = sensor.getSensorTime(); + final long sensorTime = timer.getTotalTimeLocked(mRawRealtime, which) / 1000; + double sensorPower = 0; switch (sensorHandle) { case Uid.Sensor.GPS: - multiplier = mPowerProfile.getAveragePower(PowerProfile.POWER_GPS_ON); - gpsTime = sensorTime; + app.gpsTime = sensorTime; + app.gpsPower = (app.gpsTime + * mPowerProfile.getAveragePower(PowerProfile.POWER_GPS_ON)) + / (60 * 60 * 1000); + sensorPower = app.gpsPower; + maxGpsPower = Math.max(maxGpsPower, app.gpsPower); break; default: List<Sensor> sensorList = sensorManager.getSensorList( android.hardware.Sensor.TYPE_ALL); for (android.hardware.Sensor s : sensorList) { if (s.getHandle() == sensorHandle) { - multiplier = s.getPower(); + sensorPower = (sensorTime * s.getPower()) / (60 * 60 * 1000); + app.sensorPower += sensorPower; break; } } } - p = (multiplier * sensorTime) / (60*60*1000); - if (DEBUG && p != 0) Log.d(TAG, "UID " + u.getUid() + ": sensor #" + sensorHandle - + " time=" + sensorTime + " power=" + makemAh(p)); - power += p; + if (DEBUG && sensorPower != 0) { + Log.d(TAG, "UID " + u.getUid() + ": sensor #" + sensorHandle + + " time=" + sensorTime + " power=" + makemAh(sensorPower)); + } } + maxSensorPower = Math.max(maxSensorPower, app.sensorPower); - if (DEBUG && power != 0) Log.d(TAG, String.format("UID %d: total power=%s", - u.getUid(), makemAh(power))); - - // Add the app to the list if it is consuming power - final int userId = UserHandle.getUserId(u.getUid()); - if (power != 0 || u.getUid() == 0) { - BatterySipper app = new BatterySipper(BatterySipper.DrainType.APP, u, - new double[] {power}); - app.cpuTime = cpuTime; - app.gpsTime = gpsTime; - app.wifiRunningTime = wifiRunningTimeMs; - app.cpuFgTime = cpuFgTime; - app.wakeLockTime = wakelockTime; - app.mobileRxPackets = mobileRx; - app.mobileTxPackets = mobileTx; - app.mobileActive = mobileActive / 1000; - app.mobileActiveCount = u.getMobileRadioActiveCount(mStatsType); - app.wifiRxPackets = wifiRx; - app.wifiTxPackets = wifiTx; - app.mobileRxBytes = mobileRxB; - app.mobileTxBytes = mobileTxB; - app.wifiRxBytes = wifiRxB; - app.wifiTxBytes = wifiTxB; - app.packageWithHighestDrain = packageWithHighestDrain; - if (u.getUid() == Process.WIFI_UID) { - mWifiSippers.add(app); - mWifiPower += power; - } else if (u.getUid() == Process.BLUETOOTH_UID) { - mBluetoothSippers.add(app); - mBluetoothPower += power; - } else if (!forAllUsers && asUsers.get(userId) == null - && UserHandle.getAppId(u.getUid()) >= Process.FIRST_APPLICATION_UID) { - List<BatterySipper> list = mUserSippers.get(userId); - if (list == null) { - list = new ArrayList<BatterySipper>(); - mUserSippers.put(userId, list); - } - list.add(app); - if (power != 0) { - Double userPower = mUserPower.get(userId); - if (userPower == null) { - userPower = power; - } else { - userPower += power; - } - mUserPower.put(userId, userPower); - } - } else { - mUsageList.add(app); - if (power > mMaxPower) mMaxPower = power; - if (power > mMaxRealPower) mMaxRealPower = power; - mComputedPower += power; + final double totalUnnormalizedPower = app.cpuPower + app.wifiPower + app.wakeLockPower + + app.mobileRadioPower + app.gpsPower + app.sensorPower; + if (DEBUG && totalUnnormalizedPower != 0) { + Log.d(TAG, String.format("UID %d: total power=%s", + u.getUid(), makemAh(totalUnnormalizedPower))); + } + + // Add the app to the list if it is consuming power. + if (totalUnnormalizedPower != 0 || u.getUid() == 0) { + appList.add(app); + } + } + + // Fetch real power consumption from hardware. + double actualTotalWifiPower = 0.0; + if (mStats.getWifiControllerActivity(BatteryStats.CONTROLLER_ENERGY, mStatsType) != 0) { + final double kDefaultVoltage = 3.36; + final long energy = mStats.getWifiControllerActivity( + BatteryStats.CONTROLLER_ENERGY, mStatsType); + final double voltage = mPowerProfile.getAveragePowerOrDefault( + PowerProfile.OPERATING_VOLTAGE_WIFI, kDefaultVoltage); + actualTotalWifiPower = energy / (voltage * 1000*60*60); + } + + final int appCount = appList.size(); + for (int i = 0; i < appCount; i++) { + // Normalize power where possible. + final BatterySipper app = appList.get(i); + if (actualTotalWifiPower != 0) { + app.wifiPower = (app.wifiPower / maxWifiPower) * actualTotalWifiPower; + } + + // Assign the final power consumption here. + final double power = app.wifiPower + app.cpuPower + app.wakeLockPower + + app.mobileRadioPower + app.gpsPower + app.sensorPower; + app.values[0] = app.value = power; + + // + // Add the app to the app list, WiFi, Bluetooth, etc, or into "Other Users" list. + // + + final int uid = app.getUid(); + final int userId = UserHandle.getUserId(uid); + if (uid == Process.WIFI_UID) { + mWifiSippers.add(app); + mWifiPower += power; + } else if (uid == Process.BLUETOOTH_UID) { + mBluetoothSippers.add(app); + mBluetoothPower += power; + } else if (!forAllUsers && asUsers.get(userId) == null + && UserHandle.getAppId(uid) >= Process.FIRST_APPLICATION_UID) { + // We are told to just report this user's apps as one large entry. + List<BatterySipper> list = mUserSippers.get(userId); + if (list == null) { + list = new ArrayList<>(); + mUserSippers.put(userId, list); } - if (u.getUid() == 0) { - osApp = app; + list.add(app); + + Double userPower = mUserPower.get(userId); + if (userPower == null) { + userPower = power; + } else { + userPower += power; } + mUserPower.put(userId, userPower); + } else { + mUsageList.add(app); + if (power > mMaxPower) mMaxPower = power; + if (power > mMaxRealPower) mMaxRealPower = power; + mComputedPower += power; + } + + if (uid == 0) { + osApp = app; } } @@ -641,7 +721,7 @@ public final class BatteryStatsHelper { // this remainder to the OS, if possible. if (osApp != null) { long wakeTimeMillis = mBatteryUptime / 1000; - wakeTimeMillis -= (appWakelockTimeUs / 1000) + wakeTimeMillis -= (totalAppWakelockTimeUs / 1000) + (mStats.getScreenOnTime(mRawRealtime, which) / 1000); if (wakeTimeMillis > 0) { double power = (wakeTimeMillis @@ -741,46 +821,11 @@ public final class BatteryStatsHelper { for (int i=0; i<from.size(); i++) { BatterySipper wbs = from.get(i); if (DEBUG) Log.d(TAG, tag + " adding sipper " + wbs + ": cpu=" + wbs.cpuTime); - bs.cpuTime += wbs.cpuTime; - bs.gpsTime += wbs.gpsTime; - bs.wifiRunningTime += wbs.wifiRunningTime; - bs.cpuFgTime += wbs.cpuFgTime; - bs.wakeLockTime += wbs.wakeLockTime; - bs.mobileRxPackets += wbs.mobileRxPackets; - bs.mobileTxPackets += wbs.mobileTxPackets; - bs.mobileActive += wbs.mobileActive; - bs.mobileActiveCount += wbs.mobileActiveCount; - bs.wifiRxPackets += wbs.wifiRxPackets; - bs.wifiTxPackets += wbs.wifiTxPackets; - bs.mobileRxBytes += wbs.mobileRxBytes; - bs.mobileTxBytes += wbs.mobileTxBytes; - bs.wifiRxBytes += wbs.wifiRxBytes; - bs.wifiTxBytes += wbs.wifiTxBytes; + bs.add(wbs); } bs.computeMobilemspp(); } - private void addWiFiUsage() { - long onTimeMs = mStats.getWifiOnTime(mRawRealtime, mStatsType) / 1000; - long runningTimeMs = mStats.getGlobalWifiRunningTime(mRawRealtime, mStatsType) / 1000; - if (DEBUG) Log.d(TAG, "WIFI runningTime=" + runningTimeMs - + " app runningTime=" + mAppWifiRunning); - runningTimeMs -= mAppWifiRunning; - if (runningTimeMs < 0) runningTimeMs = 0; - double wifiPower = (onTimeMs * 0 /* TODO */ - * mPowerProfile.getAveragePower(PowerProfile.POWER_WIFI_ON) - + runningTimeMs * mPowerProfile.getAveragePower(PowerProfile.POWER_WIFI_ON)) - / (60*60*1000); - if (DEBUG && wifiPower != 0) { - Log.d(TAG, "Wifi: time=" + runningTimeMs + " power=" + makemAh(wifiPower)); - } - if ((wifiPower+mWifiPower) != 0) { - BatterySipper bs = addEntry(BatterySipper.DrainType.WIFI, runningTimeMs, - wifiPower + mWifiPower); - aggregateSippers(bs, mWifiSippers, "WIFI"); - } - } - private void addIdleUsage() { long idleTimeMs = (mTypeBatteryRealtime - mStats.getScreenOnTime(mRawRealtime, mStatsType)) / 1000; @@ -794,24 +839,81 @@ public final class BatteryStatsHelper { } } - private void addBluetoothUsage() { - long btOnTimeMs = mStats.getBluetoothOnTime(mRawRealtime, mStatsType) / 1000; - double btPower = btOnTimeMs * mPowerProfile.getAveragePower(PowerProfile.POWER_BLUETOOTH_ON) - / (60*60*1000); - if (DEBUG && btPower != 0) { - Log.d(TAG, "Bluetooth: time=" + btOnTimeMs + " power=" + makemAh(btPower)); + /** + * We do per-app blaming of WiFi activity. If energy info is reported from the controller, + * then only the WiFi process gets blamed here since we normalize power calculations and + * assign all the power drain to apps. If energy info is not reported, we attribute the + * difference between total running time of WiFi for all apps and the actual running time + * of WiFi to the WiFi subsystem. + */ + private void addWiFiUsage() { + final long idleTimeMs = mStats.getWifiControllerActivity( + BatteryStats.CONTROLLER_IDLE_TIME, mStatsType); + final long txTimeMs = mStats.getWifiControllerActivity( + BatteryStats.CONTROLLER_TX_TIME, mStatsType); + final long rxTimeMs = mStats.getWifiControllerActivity( + BatteryStats.CONTROLLER_RX_TIME, mStatsType); + final long energy = mStats.getWifiControllerActivity( + BatteryStats.CONTROLLER_ENERGY, mStatsType); + final long totalTimeRunning = idleTimeMs + txTimeMs + rxTimeMs; + + double powerDrain = 0; + if (energy == 0 && totalTimeRunning > 0) { + // Energy is not reported, which means we may have left over power drain not attributed + // to any app. Assign this power to the WiFi app. + // TODO(adamlesinski): This mimics the old behavior. However, mAppWifiRunningTime + // is the accumulation of the time each app kept the WiFi chip on. Multiple apps + // can do this at the same time, so these times do not add up to the total time + // the WiFi chip was on. Consider normalizing the time spent running and calculating + // power from that? Normalizing the times will assign a weight to each app which + // should better represent power usage. + powerDrain = ((totalTimeRunning - mAppWifiRunning) + * mPowerProfile.getAveragePower(PowerProfile.POWER_WIFI_ON)) / (60*60*1000); } - int btPingCount = mStats.getBluetoothPingCount(); - double pingPower = (btPingCount - * mPowerProfile.getAveragePower(PowerProfile.POWER_BLUETOOTH_AT_CMD)) - / (60*60*1000); - if (DEBUG && pingPower != 0) { - Log.d(TAG, "Bluetooth ping: count=" + btPingCount + " power=" + makemAh(pingPower)); + + if (DEBUG && powerDrain != 0) { + Log.d(TAG, "Wifi active: time=" + (txTimeMs + rxTimeMs) + + " power=" + makemAh(powerDrain)); + } + + // TODO(adamlesinski): mWifiPower is already added as a BatterySipper... + // Are we double counting here? + final double power = mWifiPower + powerDrain; + if (power > 0) { + BatterySipper bs = addEntry(BatterySipper.DrainType.WIFI, totalTimeRunning, power); + aggregateSippers(bs, mWifiSippers, "WIFI"); } - btPower += pingPower; - if ((btPower+mBluetoothPower) != 0) { - BatterySipper bs = addEntry(BatterySipper.DrainType.BLUETOOTH, btOnTimeMs, - btPower + mBluetoothPower); + } + + /** + * Bluetooth usage is not attributed to any apps yet, so the entire blame goes to the + * Bluetooth Category. + */ + private void addBluetoothUsage() { + final double kDefaultVoltage = 3.36; + final long idleTimeMs = mStats.getBluetoothControllerActivity( + BatteryStats.CONTROLLER_IDLE_TIME, mStatsType); + final long txTimeMs = mStats.getBluetoothControllerActivity( + BatteryStats.CONTROLLER_TX_TIME, mStatsType); + final long rxTimeMs = mStats.getBluetoothControllerActivity( + BatteryStats.CONTROLLER_RX_TIME, mStatsType); + final long energy = mStats.getBluetoothControllerActivity( + BatteryStats.CONTROLLER_ENERGY, mStatsType); + final double voltage = mPowerProfile.getAveragePowerOrDefault( + PowerProfile.OPERATING_VOLTAGE_BLUETOOTH, kDefaultVoltage); + + // energy is measured in mA * V * ms, and we are interested in mAh + final double powerDrain = energy / (voltage * 60*60*1000); + + if (DEBUG && powerDrain != 0) { + Log.d(TAG, "Bluetooth active: time=" + (txTimeMs + rxTimeMs) + + " power=" + makemAh(powerDrain)); + } + + final long totalTime = idleTimeMs + txTimeMs + rxTimeMs; + final double power = mBluetoothPower + powerDrain; + if (power > 0) { + BatterySipper bs = addEntry(BatterySipper.DrainType.BLUETOOTH, totalTime, power); aggregateSippers(bs, mBluetoothSippers, "Bluetooth"); } } diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java index 20bb95e..f9b1ca1 100644 --- a/core/java/com/android/internal/os/BatteryStatsImpl.java +++ b/core/java/com/android/internal/os/BatteryStatsImpl.java @@ -20,11 +20,15 @@ import static android.net.NetworkStats.UID_ALL; import static com.android.server.NetworkManagementSocketTagger.PROP_QTAGUID_ENABLED; import android.app.ActivityManager; +import android.bluetooth.BluetoothActivityEnergyInfo; +import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadset; import android.content.Context; import android.net.ConnectivityManager; import android.net.NetworkStats; +import android.net.wifi.IWifiManager; +import android.net.wifi.WifiActivityEnergyInfo; import android.net.wifi.WifiManager; import android.os.BadParcelableException; import android.os.BatteryManager; @@ -38,6 +42,8 @@ import android.os.Parcel; import android.os.ParcelFormatException; import android.os.Parcelable; import android.os.Process; +import android.os.RemoteException; +import android.os.ServiceManager; import android.os.SystemClock; import android.os.SystemProperties; import android.os.WorkSource; @@ -56,20 +62,29 @@ import android.util.Slog; import android.util.SparseArray; import android.util.SparseIntArray; import android.util.TimeUtils; +import android.util.Xml; import android.view.Display; import com.android.internal.annotations.GuardedBy; import com.android.internal.net.NetworkStatsFactory; import com.android.internal.util.ArrayUtils; import com.android.internal.util.FastPrintWriter; +import com.android.internal.util.FastXmlSerializer; import com.android.internal.util.JournaledFile; +import com.android.internal.util.XmlUtils; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Calendar; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -94,7 +109,7 @@ public final class BatteryStatsImpl extends BatteryStats { private static final int MAGIC = 0xBA757475; // 'BATSTATS' // Current on-disk Parcel version - private static final int VERSION = 116 + (USE_OLD_HISTORY ? 1000 : 0); + private static final int VERSION = 119 + (USE_OLD_HISTORY ? 1000 : 0); // Maximum number of items we will record in the history. private static final int MAX_HISTORY_ITEMS = 2000; @@ -111,6 +126,7 @@ public final class BatteryStatsImpl extends BatteryStats { private final JournaledFile mFile; public final AtomicFile mCheckinFile; + public final AtomicFile mDailyFile; static final int MSG_UPDATE_WAKELOCKS = 1; static final int MSG_REPORT_POWER_CHANGE = 2; @@ -208,7 +224,7 @@ public final class BatteryStatsImpl extends BatteryStats { final HistoryItem mHistoryLastLastWritten = new HistoryItem(); final HistoryItem mHistoryReadTmp = new HistoryItem(); final HistoryItem mHistoryAddTmp = new HistoryItem(); - final HashMap<HistoryTag, Integer> mHistoryTagPool = new HashMap<HistoryTag, Integer>(); + final HashMap<HistoryTag, Integer> mHistoryTagPool = new HashMap(); String[] mReadHistoryStrings; int[] mReadHistoryUids; int mReadHistoryChars; @@ -227,6 +243,38 @@ public final class BatteryStatsImpl extends BatteryStats { HistoryItem mHistoryLastEnd; HistoryItem mHistoryCache; + // Used by computeHistoryStepDetails + HistoryStepDetails mLastHistoryStepDetails = null; + byte mLastHistoryStepLevel = 0; + final HistoryStepDetails mCurHistoryStepDetails = new HistoryStepDetails(); + final HistoryStepDetails mReadHistoryStepDetails = new HistoryStepDetails(); + final HistoryStepDetails mTmpHistoryStepDetails = new HistoryStepDetails(); + /** + * Total time (in 1/100 sec) spent executing in user code. + */ + long mLastStepCpuUserTime; + long mCurStepCpuUserTime; + /** + * Total time (in 1/100 sec) spent executing in kernel code. + */ + long mLastStepCpuSystemTime; + long mCurStepCpuSystemTime; + /** + * Times from /proc/stat + */ + long mLastStepStatUserTime; + long mLastStepStatSystemTime; + long mLastStepStatIOWaitTime; + long mLastStepStatIrqTime; + long mLastStepStatSoftIrqTime; + long mLastStepStatIdleTime; + long mCurStepStatUserTime; + long mCurStepStatSystemTime; + long mCurStepStatIOWaitTime; + long mCurStepStatIrqTime; + long mCurStepStatSoftIrqTime; + long mCurStepStatIdleTime; + private HistoryItem mHistoryIterator; private boolean mReadOverflow; private boolean mIteratingHistory; @@ -290,6 +338,12 @@ public final class BatteryStatsImpl extends BatteryStats { final LongSamplingCounter[] mNetworkPacketActivityCounters = new LongSamplingCounter[NUM_NETWORK_ACTIVITY_TYPES]; + final LongSamplingCounter[] mBluetoothActivityCounters = + new LongSamplingCounter[NUM_CONTROLLER_ACTIVITY_TYPES]; + + final LongSamplingCounter[] mWifiActivityCounters = + new LongSamplingCounter[NUM_CONTROLLER_ACTIVITY_TYPES]; + boolean mWifiOn; StopwatchTimer mWifiOnTimer; @@ -354,16 +408,22 @@ public final class BatteryStatsImpl extends BatteryStats { int mModStepMode = 0; int mLastDischargeStepLevel; - long mLastDischargeStepTime; int mMinDischargeStepLevel; - int mNumDischargeStepDurations; - final long[] mDischargeStepDurations = new long[MAX_LEVEL_STEPS]; + final LevelStepTracker mDischargeStepTracker = new LevelStepTracker(MAX_LEVEL_STEPS); + final LevelStepTracker mDailyDischargeStepTracker = new LevelStepTracker(MAX_LEVEL_STEPS*2); int mLastChargeStepLevel; - long mLastChargeStepTime; int mMaxChargeStepLevel; - int mNumChargeStepDurations; - final long[] mChargeStepDurations = new long[MAX_LEVEL_STEPS]; + final LevelStepTracker mChargeStepTracker = new LevelStepTracker(MAX_LEVEL_STEPS); + final LevelStepTracker mDailyChargeStepTracker = new LevelStepTracker(MAX_LEVEL_STEPS*2); + + static final int MAX_DAILY_ITEMS = 10; + + long mDailyStartTime = 0; + long mNextMinDailyDeadline = 0; + long mNextMaxDailyDeadline = 0; + + final ArrayList<DailyItem> mDailyItems = new ArrayList<>(); long mLastWriteTime = 0; // Milliseconds @@ -446,6 +506,7 @@ public final class BatteryStatsImpl extends BatteryStats { public BatteryStatsImpl() { mFile = null; mCheckinFile = null; + mDailyFile = null; mHandler = null; clearHistoryLocked(); } @@ -1938,6 +1999,10 @@ public final class BatteryStatsImpl extends BatteryStats { static final int STATE_BATTERY_PLUG_MASK = 0x00000003; static final int STATE_BATTERY_PLUG_SHIFT = 24; + // We use the low bit of the battery state int to indicate that we have full details + // from a battery level change. + static final int BATTERY_DELTA_LEVEL_FLAG = 0x00000001; + public void writeHistoryDelta(Parcel dest, HistoryItem cur, HistoryItem last) { if (last == null || cur.cmd != HistoryItem.CMD_UPDATE) { dest.writeInt(DELTA_TIME_ABS); @@ -1958,7 +2023,11 @@ public final class BatteryStatsImpl extends BatteryStats { deltaTimeToken = (int)deltaTime; } int firstToken = deltaTimeToken | (cur.states&DELTA_STATE_MASK); - final int batteryLevelInt = buildBatteryLevelInt(cur); + final int includeStepDetails = mLastHistoryStepLevel > cur.batteryLevel + ? BATTERY_DELTA_LEVEL_FLAG : 0; + final boolean computeStepDetails = includeStepDetails != 0 + || mLastHistoryStepDetails == null; + final int batteryLevelInt = buildBatteryLevelInt(cur) | includeStepDetails; final boolean batteryLevelIntChanged = batteryLevelInt != lastBatteryLevelInt; if (batteryLevelIntChanged) { firstToken |= DELTA_BATTERY_LEVEL_FLAG; @@ -2040,12 +2109,26 @@ public final class BatteryStatsImpl extends BatteryStats { + cur.eventTag.poolIdx + " " + cur.eventTag.uid + ":" + cur.eventTag.string); } + if (computeStepDetails) { + computeHistoryStepDetails(mCurHistoryStepDetails, mLastHistoryStepDetails); + if (includeStepDetails != 0) { + mCurHistoryStepDetails.writeToParcel(dest); + } + cur.stepDetails = mCurHistoryStepDetails; + mLastHistoryStepDetails = mCurHistoryStepDetails; + } else { + cur.stepDetails = null; + } + if (mLastHistoryStepLevel < cur.batteryLevel) { + mLastHistoryStepDetails = null; + } + mLastHistoryStepLevel = cur.batteryLevel; } private int buildBatteryLevelInt(HistoryItem h) { return ((((int)h.batteryLevel)<<25)&0xfe000000) - | ((((int)h.batteryTemperature)<<14)&0x01ffc000) - | (((int)h.batteryVoltage)&0x00003fff); + | ((((int)h.batteryTemperature)<<14)&0x01ff8000) + | ((((int)h.batteryVoltage)<<1)&0x00007fff); } private int buildStateInt(HistoryItem h) { @@ -2063,6 +2146,98 @@ public final class BatteryStatsImpl extends BatteryStats { | (h.states&(~DELTA_STATE_MASK)); } + private void computeHistoryStepDetails(final HistoryStepDetails out, + final HistoryStepDetails last) { + final HistoryStepDetails tmp = last != null ? mTmpHistoryStepDetails : out; + + // Perform a CPU update right after we do this collection, so we have started + // collecting good data for the next step. + requestImmediateCpuUpdate(); + + if (last == null) { + // We are not generating a delta, so all we need to do is reset the stats + // we will later be doing a delta from. + final int NU = mUidStats.size(); + for (int i=0; i<NU; i++) { + final BatteryStatsImpl.Uid uid = mUidStats.valueAt(i); + uid.mLastStepUserTime = uid.mCurStepUserTime; + uid.mLastStepSystemTime = uid.mCurStepSystemTime; + } + mLastStepCpuUserTime = mCurStepCpuUserTime; + mLastStepCpuSystemTime = mCurStepCpuSystemTime; + mLastStepStatUserTime = mCurStepStatUserTime; + mLastStepStatSystemTime = mCurStepStatSystemTime; + mLastStepStatIOWaitTime = mCurStepStatIOWaitTime; + mLastStepStatIrqTime = mCurStepStatIrqTime; + mLastStepStatSoftIrqTime = mCurStepStatSoftIrqTime; + mLastStepStatIdleTime = mCurStepStatIdleTime; + tmp.clear(); + return; + } + if (DEBUG) { + Slog.d(TAG, "Step stats last: user=" + mLastStepCpuUserTime + " sys=" + + mLastStepStatSystemTime + " io=" + mLastStepStatIOWaitTime + + " irq=" + mLastStepStatIrqTime + " sirq=" + + mLastStepStatSoftIrqTime + " idle=" + mLastStepStatIdleTime); + Slog.d(TAG, "Step stats cur: user=" + mCurStepCpuUserTime + " sys=" + + mCurStepStatSystemTime + " io=" + mCurStepStatIOWaitTime + + " irq=" + mCurStepStatIrqTime + " sirq=" + + mCurStepStatSoftIrqTime + " idle=" + mCurStepStatIdleTime); + } + out.userTime = (int)(mCurStepCpuUserTime - mLastStepCpuUserTime); + out.systemTime = (int)(mCurStepCpuSystemTime - mLastStepCpuSystemTime); + out.statUserTime = (int)(mCurStepStatUserTime - mLastStepStatUserTime); + out.statSystemTime = (int)(mCurStepStatSystemTime - mLastStepStatSystemTime); + out.statIOWaitTime = (int)(mCurStepStatIOWaitTime - mLastStepStatIOWaitTime); + out.statIrqTime = (int)(mCurStepStatIrqTime - mLastStepStatIrqTime); + out.statSoftIrqTime = (int)(mCurStepStatSoftIrqTime - mLastStepStatSoftIrqTime); + out.statIdlTime = (int)(mCurStepStatIdleTime - mLastStepStatIdleTime); + out.appCpuUid1 = out.appCpuUid2 = out.appCpuUid3 = -1; + out.appCpuUTime1 = out.appCpuUTime2 = out.appCpuUTime3 = 0; + out.appCpuSTime1 = out.appCpuSTime2 = out.appCpuSTime3 = 0; + final int NU = mUidStats.size(); + for (int i=0; i<NU; i++) { + final BatteryStatsImpl.Uid uid = mUidStats.valueAt(i); + final int totalUTime = (int)(uid.mCurStepUserTime - uid.mLastStepUserTime); + final int totalSTime = (int)(uid.mCurStepSystemTime - uid.mLastStepSystemTime); + final int totalTime = totalUTime + totalSTime; + uid.mLastStepUserTime = uid.mCurStepUserTime; + uid.mLastStepSystemTime = uid.mCurStepSystemTime; + if (totalTime <= (out.appCpuUTime3+out.appCpuSTime3)) { + continue; + } + if (totalTime <= (out.appCpuUTime2+out.appCpuSTime2)) { + out.appCpuUid3 = uid.mUid; + out.appCpuUTime3 = totalUTime; + out.appCpuSTime3 = totalSTime; + } else { + out.appCpuUid3 = out.appCpuUid2; + out.appCpuUTime3 = out.appCpuUTime2; + out.appCpuSTime3 = out.appCpuSTime2; + if (totalTime <= (out.appCpuUTime1+out.appCpuSTime1)) { + out.appCpuUid2 = uid.mUid; + out.appCpuUTime2 = totalUTime; + out.appCpuSTime2 = totalSTime; + } else { + out.appCpuUid2 = out.appCpuUid1; + out.appCpuUTime2 = out.appCpuUTime1; + out.appCpuSTime2 = out.appCpuSTime1; + out.appCpuUid1 = uid.mUid; + out.appCpuUTime1 = totalUTime; + out.appCpuSTime1 = totalSTime; + } + } + } + mLastStepCpuUserTime = mCurStepCpuUserTime; + mLastStepCpuSystemTime = mCurStepCpuSystemTime; + mLastStepStatUserTime = mCurStepStatUserTime; + mLastStepStatSystemTime = mCurStepStatSystemTime; + mLastStepStatIOWaitTime = mCurStepStatIOWaitTime; + mLastStepStatIrqTime = mCurStepStatIrqTime; + mLastStepStatSoftIrqTime = mCurStepStatSoftIrqTime; + mLastStepStatIdleTime = mCurStepStatIdleTime; + } + public void readHistoryDelta(Parcel src, HistoryItem cur) { int firstToken = src.readInt(); int deltaTimeToken = firstToken&DELTA_TIME_MASK; @@ -2091,8 +2266,9 @@ public final class BatteryStatsImpl extends BatteryStats { cur.numReadInts += 2; } + final int batteryLevelInt; if ((firstToken&DELTA_BATTERY_LEVEL_FLAG) != 0) { - int batteryLevelInt = src.readInt(); + batteryLevelInt = src.readInt(); cur.batteryLevel = (byte)((batteryLevelInt>>25)&0x7f); cur.batteryTemperature = (short)((batteryLevelInt<<7)>>21); cur.batteryVoltage = (char)(batteryLevelInt&0x3fff); @@ -2102,6 +2278,8 @@ public final class BatteryStatsImpl extends BatteryStats { + " batteryLevel=" + cur.batteryLevel + " batteryTemp=" + cur.batteryTemperature + " batteryVolt=" + (int)cur.batteryVoltage); + } else { + batteryLevelInt = 0; } if ((firstToken&DELTA_STATE_FLAG) != 0) { @@ -2180,6 +2358,13 @@ public final class BatteryStatsImpl extends BatteryStats { } else { cur.eventCode = HistoryItem.EVENT_NONE; } + + if ((batteryLevelInt&BATTERY_DELTA_LEVEL_FLAG) != 0) { + cur.stepDetails = mReadHistoryStepDetails; + cur.stepDetails.readFromParcel(src); + } else { + cur.stepDetails = null; + } } @Override @@ -2207,6 +2392,7 @@ public final class BatteryStatsImpl extends BatteryStats { && (diffStates2&lastDiffStates2) == 0 && (mHistoryLastWritten.wakelockTag == null || cur.wakelockTag == null) && (mHistoryLastWritten.wakeReasonTag == null || cur.wakeReasonTag == null) + && mHistoryLastWritten.stepDetails == null && (mHistoryLastWritten.eventCode == HistoryItem.EVENT_NONE || cur.eventCode == HistoryItem.EVENT_NONE) && mHistoryLastWritten.batteryLevel == cur.batteryLevel @@ -2632,6 +2818,11 @@ public final class BatteryStatsImpl extends BatteryStats { } } + private void requestImmediateCpuUpdate() { + mHandler.removeMessages(MSG_UPDATE_WAKELOCKS); + mHandler.sendEmptyMessage(MSG_UPDATE_WAKELOCKS); + } + public void setRecordAllHistoryLocked(boolean enabled) { mRecordAllHistory = enabled; if (!enabled) { @@ -2823,6 +3014,10 @@ public final class BatteryStatsImpl extends BatteryStats { public int startAddingCpuLocked() { mHandler.removeMessages(MSG_UPDATE_WAKELOCKS); + if (!mOnBatteryInternal) { + return -1; + } + final int N = mPartialTimers.size(); if (N == 0) { mLastPartialTimers.clear(); @@ -2853,7 +3048,23 @@ public final class BatteryStatsImpl extends BatteryStats { return 0; } - public void finishAddingCpuLocked(int perc, int utime, int stime, long[] cpuSpeedTimes) { + public void finishAddingCpuLocked(int perc, int remainUTime, int remainSTtime, + int totalUTime, int totalSTime, int statUserTime, int statSystemTime, + int statIOWaitTime, int statIrqTime, int statSoftIrqTime, int statIdleTime, + long[] cpuSpeedTimes) { + if (DEBUG) Slog.d(TAG, "Adding cpu: tuser=" + totalUTime + " tsys=" + totalSTime + + " user=" + statUserTime + " sys=" + statSystemTime + + " io=" + statIOWaitTime + " irq=" + statIrqTime + + " sirq=" + statSoftIrqTime + " idle=" + statIdleTime); + mCurStepCpuUserTime += totalUTime; + mCurStepCpuSystemTime += totalSTime; + mCurStepStatUserTime += statUserTime; + mCurStepStatSystemTime += statSystemTime; + mCurStepStatIOWaitTime += statIOWaitTime; + mCurStepStatIrqTime += statIrqTime; + mCurStepStatSoftIrqTime += statSoftIrqTime; + mCurStepStatIdleTime += statIdleTime; + final int N = mPartialTimers.size(); if (perc != 0) { int num = 0; @@ -2874,26 +3085,24 @@ public final class BatteryStatsImpl extends BatteryStats { if (st.mInList) { Uid uid = st.mUid; if (uid != null && uid.mUid != Process.SYSTEM_UID) { - int myUTime = utime/num; - int mySTime = stime/num; - utime -= myUTime; - stime -= mySTime; + int myUTime = remainUTime/num; + int mySTime = remainSTtime/num; + remainUTime -= myUTime; + remainSTtime -= mySTime; num--; Uid.Proc proc = uid.getProcessStatsLocked("*wakelock*"); - proc.addCpuTimeLocked(myUTime, mySTime); - proc.addSpeedStepTimes(cpuSpeedTimes); + proc.addCpuTimeLocked(myUTime, mySTime, cpuSpeedTimes); } } } } // Just in case, collect any lost CPU time. - if (utime != 0 || stime != 0) { + if (remainUTime != 0 || remainSTtime != 0) { Uid uid = getUidStatsLocked(Process.SYSTEM_UID); if (uid != null) { Uid.Proc proc = uid.getProcessStatsLocked("*lost*"); - proc.addCpuTimeLocked(utime, stime); - proc.addSpeedStepTimes(cpuSpeedTimes); + proc.addCpuTimeLocked(remainUTime, remainSTtime, cpuSpeedTimes); } } } @@ -3019,6 +3228,7 @@ public final class BatteryStatsImpl extends BatteryStats { public void noteScreenStateLocked(int state) { if (mScreenState != state) { + recordDailyStatsIfNeededLocked(true); final int oldState = mScreenState; mScreenState = state; if (DEBUG) Slog.v(TAG, "Screen state: oldState=" + Display.stateToString(oldState) @@ -4109,6 +4319,20 @@ public final class BatteryStatsImpl extends BatteryStats { return mBluetoothStateTimer[bluetoothState].getCountLocked(which); } + @Override public long getBluetoothControllerActivity(int type, int which) { + if (type >= 0 && type < mBluetoothActivityCounters.length) { + return mBluetoothActivityCounters[type].getCountLocked(which); + } + return 0; + } + + @Override public long getWifiControllerActivity(int type, int which) { + if (type >= 0 && type < mWifiActivityCounters.length) { + return mWifiActivityCounters[type].getCountLocked(which); + } + return 0; + } + @Override public long getFlashlightOnTime(long elapsedRealtimeUs, int which) { return mFlashlightOnTimer.getTotalTimeLocked(elapsedRealtimeUs, which); } @@ -4214,6 +4438,14 @@ public final class BatteryStatsImpl extends BatteryStats { LongSamplingCounter mMobileRadioActiveCount; /** + * The CPU times we had at the last history details update. + */ + long mLastStepUserTime; + long mLastStepSystemTime; + long mCurStepUserTime; + long mCurStepSystemTime; + + /** * The statistics we have collected for this uid's wake locks. */ final OverflowArrayMap<Wakelock> mWakelockStats = new OverflowArrayMap<Wakelock>() { @@ -4543,6 +4775,14 @@ public final class BatteryStatsImpl extends BatteryStats { } @Override + public int getWifiScanCount(int which) { + if (mWifiScanTimer == null) { + return 0; + } + return mWifiScanTimer.getCountLocked(which); + } + + @Override public long getWifiBatchedScanTime(int csphBin, long elapsedRealtimeUs, int which) { if (csphBin < 0 || csphBin >= NUM_WIFI_BATCHED_SCAN_BINS) return 0; if (mWifiBatchedScanTimer[csphBin] == null) { @@ -4552,6 +4792,15 @@ public final class BatteryStatsImpl extends BatteryStats { } @Override + public int getWifiBatchedScanCount(int csphBin, int which) { + if (csphBin < 0 || csphBin >= NUM_WIFI_BATCHED_SCAN_BINS) return 0; + if (mWifiBatchedScanTimer[csphBin] == null) { + return 0; + } + return mWifiBatchedScanTimer[csphBin].getCountLocked(which); + } + + @Override public long getWifiMulticastTime(long elapsedRealtimeUs, int which) { if (mWifiMulticastTimer == null) { return 0; @@ -4876,6 +5125,9 @@ public final class BatteryStatsImpl extends BatteryStats { mPackageStats.clear(); } + mLastStepUserTime = mLastStepSystemTime = 0; + mCurStepUserTime = mCurStepSystemTime = 0; + if (!active) { if (mWifiRunningTimer != null) { mWifiRunningTimer.detach(); @@ -5392,17 +5644,17 @@ public final class BatteryStatsImpl extends BatteryStats { boolean mActive = true; /** - * Total time (in 1/100 sec) spent executing in user code. + * Total time (in ms) spent executing in user code. */ long mUserTime; /** - * Total time (in 1/100 sec) spent executing in kernel code. + * Total time (in ms) spent executing in kernel code. */ long mSystemTime; /** - * Amount of time the process was running in the foreground. + * Amount of time (in ms) the process was running in the foreground. */ long mForegroundTime; @@ -5678,9 +5930,22 @@ public final class BatteryStatsImpl extends BatteryStats { return BatteryStatsImpl.this; } - public void addCpuTimeLocked(int utime, int stime) { + public void addCpuTimeLocked(int utime, int stime, long[] speedStepBins) { mUserTime += utime; + mCurStepUserTime += utime; mSystemTime += stime; + mCurStepSystemTime += stime; + + for (int i = 0; i < mSpeedBins.length && i < speedStepBins.length; i++) { + long amt = speedStepBins[i]; + if (amt != 0) { + SamplingCounter c = mSpeedBins[i]; + if (c == null) { + mSpeedBins[i] = c = new SamplingCounter(mOnBatteryTimeBase); + } + c.addCountAtomic(speedStepBins[i]); + } + } } public void addForegroundTimeLocked(long ttime) { @@ -5770,20 +6035,6 @@ public final class BatteryStatsImpl extends BatteryStats { return val; } - /* Called by ActivityManagerService when CPU times are updated. */ - public void addSpeedStepTimes(long[] values) { - for (int i = 0; i < mSpeedBins.length && i < values.length; i++) { - long amt = values[i]; - if (amt != 0) { - SamplingCounter c = mSpeedBins[i]; - if (c == null) { - mSpeedBins[i] = c = new SamplingCounter(mOnBatteryTimeBase); - } - c.addCountAtomic(values[i]); - } - } - } - @Override public long getTimeAtCpuSpeedStep(int speedStep, int which) { if (speedStep < mSpeedBins.length) { @@ -6404,6 +6655,7 @@ public final class BatteryStatsImpl extends BatteryStats { mFile = null; } mCheckinFile = new AtomicFile(new File(systemDir, "batterystats-checkin.bin")); + mDailyFile = new AtomicFile(new File(systemDir, "batterystats-daily.xml")); mHandler = new MyHandler(handler.getLooper()); mStartCount++; mScreenOnTimer = new StopwatchTimer(null, -1, null, mOnBatteryTimeBase); @@ -6426,6 +6678,10 @@ public final class BatteryStatsImpl extends BatteryStats { mNetworkByteActivityCounters[i] = new LongSamplingCounter(mOnBatteryTimeBase); mNetworkPacketActivityCounters[i] = new LongSamplingCounter(mOnBatteryTimeBase); } + for (int i = 0; i < NUM_CONTROLLER_ACTIVITY_TYPES; i++) { + mBluetoothActivityCounters[i] = new LongSamplingCounter(mOnBatteryTimeBase); + mWifiActivityCounters[i] = new LongSamplingCounter(mOnBatteryTimeBase); + } mMobileRadioActiveTimer = new StopwatchTimer(null, -400, null, mOnBatteryTimeBase); mMobileRadioActivePerAppTimer = new StopwatchTimer(null, -401, null, mOnBatteryTimeBase); mMobileRadioActiveAdjustedTime = new LongSamplingCounter(mOnBatteryTimeBase); @@ -6462,11 +6718,13 @@ public final class BatteryStatsImpl extends BatteryStats { mCurrentBatteryLevel = 0; initDischarge(); clearHistoryLocked(); + updateDailyDeadlineLocked(); } public BatteryStatsImpl(Parcel p) { mFile = null; mCheckinFile = null; + mDailyFile = null; mHandler = null; clearHistoryLocked(); readFromParcel(p); @@ -6486,6 +6744,286 @@ public final class BatteryStatsImpl extends BatteryStats { } } + public void updateDailyDeadlineLocked() { + // Get the current time. + long currentTime = mDailyStartTime = System.currentTimeMillis(); + Calendar calDeadline = Calendar.getInstance(); + calDeadline.setTimeInMillis(currentTime); + + // Move time up to the next day, ranging from 1am to 3pm. + calDeadline.set(Calendar.DAY_OF_YEAR, calDeadline.get(Calendar.DAY_OF_YEAR) + 1); + calDeadline.set(Calendar.MILLISECOND, 0); + calDeadline.set(Calendar.SECOND, 0); + calDeadline.set(Calendar.MINUTE, 0); + calDeadline.set(Calendar.HOUR_OF_DAY, 1); + mNextMinDailyDeadline = calDeadline.getTimeInMillis(); + calDeadline.set(Calendar.HOUR_OF_DAY, 3); + mNextMaxDailyDeadline = calDeadline.getTimeInMillis(); + } + + public void recordDailyStatsIfNeededLocked(boolean settled) { + long currentTime = System.currentTimeMillis(); + if (currentTime >= mNextMaxDailyDeadline) { + recordDailyStatsLocked(); + } else if (settled && currentTime >= mNextMinDailyDeadline) { + recordDailyStatsLocked(); + } else if (currentTime < (mDailyStartTime-(1000*60*60*24))) { + recordDailyStatsLocked(); + } + } + + public void recordDailyStatsLocked() { + DailyItem item = new DailyItem(); + item.mStartTime = mDailyStartTime; + item.mEndTime = System.currentTimeMillis(); + boolean hasData = false; + if (mDailyDischargeStepTracker.mNumStepDurations > 0) { + hasData = true; + item.mDischargeSteps = new LevelStepTracker( + mDailyDischargeStepTracker.mNumStepDurations, + mDailyDischargeStepTracker.mStepDurations); + } + if (mDailyChargeStepTracker.mNumStepDurations > 0) { + hasData = true; + item.mChargeSteps = new LevelStepTracker( + mDailyChargeStepTracker.mNumStepDurations, + mDailyChargeStepTracker.mStepDurations); + } + mDailyDischargeStepTracker.init(); + mDailyChargeStepTracker.init(); + updateDailyDeadlineLocked(); + + if (hasData) { + mDailyItems.add(item); + while (mDailyItems.size() > MAX_DAILY_ITEMS) { + mDailyItems.remove(0); + } + final ByteArrayOutputStream memStream = new ByteArrayOutputStream(); + try { + XmlSerializer out = new FastXmlSerializer(); + out.setOutput(memStream, "utf-8"); + writeDailyItemsLocked(out); + BackgroundThread.getHandler().post(new Runnable() { + @Override + public void run() { + synchronized (mCheckinFile) { + FileOutputStream stream = null; + try { + stream = mDailyFile.startWrite(); + memStream.writeTo(stream); + stream.flush(); + FileUtils.sync(stream); + stream.close(); + mDailyFile.finishWrite(stream); + } catch (IOException e) { + Slog.w("BatteryStats", + "Error writing battery daily items", e); + mDailyFile.failWrite(stream); + } + } + } + }); + } catch (IOException e) { + } + } + } + + private void writeDailyItemsLocked(XmlSerializer out) throws IOException { + StringBuilder sb = new StringBuilder(64); + out.startDocument(null, true); + out.startTag(null, "daily-items"); + for (int i=0; i<mDailyItems.size(); i++) { + final DailyItem dit = mDailyItems.get(i); + out.startTag(null, "item"); + out.attribute(null, "start", Long.toString(dit.mStartTime)); + out.attribute(null, "end", Long.toString(dit.mEndTime)); + writeDailyLevelSteps(out, "dis", dit.mDischargeSteps, sb); + writeDailyLevelSteps(out, "chg", dit.mChargeSteps, sb); + out.endTag(null, "item"); + } + out.endTag(null, "daily-items"); + out.endDocument(); + } + + private void writeDailyLevelSteps(XmlSerializer out, String tag, LevelStepTracker steps, + StringBuilder tmpBuilder) throws IOException { + if (steps != null) { + out.startTag(null, tag); + out.attribute(null, "n", Integer.toString(steps.mNumStepDurations)); + for (int i=0; i<steps.mNumStepDurations; i++) { + out.startTag(null, "s"); + tmpBuilder.setLength(0); + steps.encodeEntryAt(i, tmpBuilder); + out.attribute(null, "v", tmpBuilder.toString()); + out.endTag(null, "s"); + } + out.endTag(null, tag); + } + } + + public void readDailyStatsLocked() { + Slog.d(TAG, "Reading daily items from " + mDailyFile.getBaseFile()); + mDailyItems.clear(); + FileInputStream stream; + try { + stream = mDailyFile.openRead(); + } catch (FileNotFoundException e) { + return; + } + try { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(stream, null); + readDailyItemsLocked(parser); + } catch (XmlPullParserException e) { + } finally { + try { + stream.close(); + } catch (IOException e) { + } + } + } + + private void readDailyItemsLocked(XmlPullParser parser) { + try { + int type; + while ((type = parser.next()) != XmlPullParser.START_TAG + && type != XmlPullParser.END_DOCUMENT) { + ; + } + + if (type != XmlPullParser.START_TAG) { + throw new IllegalStateException("no start tag found"); + } + + int outerDepth = parser.getDepth(); + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + String tagName = parser.getName(); + if (tagName.equals("item")) { + readDailyItemTagLocked(parser); + } else { + Slog.w(TAG, "Unknown element under <daily-items>: " + + parser.getName()); + XmlUtils.skipCurrentTag(parser); + } + } + + } catch (IllegalStateException e) { + Slog.w(TAG, "Failed parsing daily " + e); + } catch (NullPointerException e) { + Slog.w(TAG, "Failed parsing daily " + e); + } catch (NumberFormatException e) { + Slog.w(TAG, "Failed parsing daily " + e); + } catch (XmlPullParserException e) { + Slog.w(TAG, "Failed parsing daily " + e); + } catch (IOException e) { + Slog.w(TAG, "Failed parsing daily " + e); + } catch (IndexOutOfBoundsException e) { + Slog.w(TAG, "Failed parsing daily " + e); + } + } + + void readDailyItemTagLocked(XmlPullParser parser) throws NumberFormatException, + XmlPullParserException, IOException { + DailyItem dit = new DailyItem(); + String attr = parser.getAttributeValue(null, "start"); + if (attr != null) { + dit.mStartTime = Long.parseLong(attr); + } + attr = parser.getAttributeValue(null, "end"); + if (attr != null) { + dit.mEndTime = Long.parseLong(attr); + } + int outerDepth = parser.getDepth(); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + String tagName = parser.getName(); + if (tagName.equals("dis")) { + readDailyItemTagDetailsLocked(parser, dit, false, "dis"); + } else if (tagName.equals("chg")) { + readDailyItemTagDetailsLocked(parser, dit, true, "chg"); + } else { + Slog.w(TAG, "Unknown element under <item>: " + + parser.getName()); + XmlUtils.skipCurrentTag(parser); + } + } + mDailyItems.add(dit); + } + + void readDailyItemTagDetailsLocked(XmlPullParser parser, DailyItem dit, boolean isCharge, + String tag) + throws NumberFormatException, XmlPullParserException, IOException { + final String numAttr = parser.getAttributeValue(null, "n"); + if (numAttr == null) { + Slog.w(TAG, "Missing 'n' attribute at " + parser.getPositionDescription()); + XmlUtils.skipCurrentTag(parser); + return; + } + final int num = Integer.parseInt(numAttr); + LevelStepTracker steps = new LevelStepTracker(num); + if (isCharge) { + dit.mChargeSteps = steps; + } else { + dit.mDischargeSteps = steps; + } + int i = 0; + int outerDepth = parser.getDepth(); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + String tagName = parser.getName(); + if ("s".equals(tagName)) { + if (i < num) { + String valueAttr = parser.getAttributeValue(null, "v"); + if (valueAttr != null) { + steps.decodeEntryAt(i, valueAttr); + i++; + } + } + } else { + Slog.w(TAG, "Unknown element under <" + tag + ">: " + + parser.getName()); + XmlUtils.skipCurrentTag(parser); + } + } + steps.mNumStepDurations = i; + } + + @Override + public DailyItem getDailyItemLocked(int daysAgo) { + int index = mDailyItems.size()-1-daysAgo; + return index >= 0 ? mDailyItems.get(index) : null; + } + + @Override + public long getCurrentDailyStartTime() { + return mDailyStartTime; + } + + @Override + public long getNextMinDailyDeadline() { + return mNextMinDailyDeadline; + } + + @Override + public long getNextMaxDailyDeadline() { + return mNextMaxDailyDeadline; + } + @Override public boolean startIteratingOldHistoryLocked() { if (DEBUG_HISTORY) Slog.i(TAG, "ITERATING: buff size=" + mHistoryBuffer.dataSize() @@ -6656,10 +7194,8 @@ public final class BatteryStatsImpl extends BatteryStats { mDischargeAmountScreenOnSinceCharge = 0; mDischargeAmountScreenOff = 0; mDischargeAmountScreenOffSinceCharge = 0; - mLastDischargeStepTime = -1; - mNumDischargeStepDurations = 0; - mLastChargeStepTime = -1; - mNumChargeStepDurations = 0; + mDischargeStepTracker.init(); + mChargeStepTracker.init(); } public void resetAllStatsCmdLocked() { @@ -6733,6 +7269,10 @@ public final class BatteryStatsImpl extends BatteryStats { for (int i=0; i< NUM_BLUETOOTH_STATES; i++) { mBluetoothStateTimer[i].reset(false); } + for (int i=0; i< NUM_CONTROLLER_ACTIVITY_TYPES; i++) { + mBluetoothActivityCounters[i].reset(false); + mWifiActivityCounters[i].reset(false); + } mNumConnectivityChange = mLoadedNumConnectivityChange = mUnpluggedNumConnectivityChange = 0; for (int i=0; i<mUidStats.size(); i++) { @@ -6756,6 +7296,18 @@ public final class BatteryStatsImpl extends BatteryStats { mWakeupReasonStats.clear(); } + mLastHistoryStepDetails = null; + mLastStepCpuUserTime = mLastStepCpuSystemTime = 0; + mCurStepCpuUserTime = mCurStepCpuSystemTime = 0; + mLastStepCpuUserTime = mCurStepCpuUserTime = 0; + mLastStepCpuSystemTime = mCurStepCpuSystemTime = 0; + mLastStepStatUserTime = mCurStepStatUserTime = 0; + mLastStepStatSystemTime = mCurStepStatSystemTime = 0; + mLastStepStatIOWaitTime = mCurStepStatIOWaitTime = 0; + mLastStepStatIrqTime = mCurStepStatIrqTime = 0; + mLastStepStatSoftIrqTime = mCurStepStatSoftIrqTime = 0; + mLastStepStatIdleTime = mCurStepStatIdleTime = 0; + initDischarge(); clearHistoryLocked(); @@ -6807,6 +7359,9 @@ public final class BatteryStatsImpl extends BatteryStats { public void pullPendingStateUpdatesLocked() { updateKernelWakelocksLocked(); updateNetworkActivityLocked(NET_UPDATE_ALL, SystemClock.elapsedRealtime()); + // TODO(adamlesinski): enable when bluedroid stops deadlocking. b/19248786 + // updateBluetoothControllerActivityLocked(); + updateWifiControllerActivityLocked(); if (mOnBatteryInternal) { final boolean screenOn = mScreenState == Display.STATE_ON; updateDischargeScreenLevelsLocked(screenOn, screenOn); @@ -6870,12 +7425,13 @@ public final class BatteryStatsImpl extends BatteryStats { resetAllStatsLocked(); mDischargeStartLevel = level; reset = true; - mNumDischargeStepDurations = 0; + mDischargeStepTracker.init(); } - mOnBattery = mOnBatteryInternal = onBattery; + mOnBattery = mOnBatteryInternal = true; mLastDischargeStepLevel = level; mMinDischargeStepLevel = level; - mLastDischargeStepTime = -1; + mDischargeStepTracker.clearTime(); + mDailyDischargeStepTracker.clearTime(); mInitStepMode = mCurStepMode; mModStepMode = 0; pullPendingStateUpdatesLocked(); @@ -6900,7 +7456,7 @@ public final class BatteryStatsImpl extends BatteryStats { mDischargeAmountScreenOff = 0; updateTimeBasesLocked(true, !screenOn, uptime, realtime); } else { - mOnBattery = mOnBatteryInternal = onBattery; + mOnBattery = mOnBatteryInternal = false; pullPendingStateUpdatesLocked(); mHistoryCur.batteryLevel = (byte)level; mHistoryCur.states |= HistoryItem.STATE_BATTERY_PLUGGED_FLAG; @@ -6914,10 +7470,9 @@ public final class BatteryStatsImpl extends BatteryStats { } updateDischargeScreenLevelsLocked(screenOn, screenOn); updateTimeBasesLocked(false, !screenOn, uptime, realtime); - mNumChargeStepDurations = 0; + mChargeStepTracker.init(); mLastChargeStepLevel = level; mMaxChargeStepLevel = level; - mLastChargeStepTime = -1; mInitStepMode = mCurStepMode; mModStepMode = 0; } @@ -6969,34 +7524,12 @@ public final class BatteryStatsImpl extends BatteryStats { // This should probably be exposed in the API, though it's not critical private static final int BATTERY_PLUGGED_NONE = 0; - private static int addLevelSteps(long[] steps, int stepCount, long lastStepTime, - int numStepLevels, long modeBits, long elapsedRealtime) { - if (lastStepTime >= 0 && numStepLevels > 0) { - long duration = elapsedRealtime - lastStepTime; - for (int i=0; i<numStepLevels; i++) { - System.arraycopy(steps, 0, steps, 1, steps.length-1); - long thisDuration = duration / (numStepLevels-i); - duration -= thisDuration; - if (thisDuration > STEP_LEVEL_TIME_MASK) { - thisDuration = STEP_LEVEL_TIME_MASK; - } - steps[0] = thisDuration | modeBits; - } - stepCount += numStepLevels; - if (stepCount > steps.length) { - stepCount = steps.length; - } - } - return stepCount; - } - public void setBatteryState(int status, int health, int plugType, int level, int temp, int volt) { synchronized(this) { final boolean onBattery = plugType == BATTERY_PLUGGED_NONE; final long uptime = SystemClock.uptimeMillis(); final long elapsedRealtime = SystemClock.elapsedRealtime(); - int oldStatus = mHistoryCur.batteryStatus; if (!mHaveBatteryLevel) { mHaveBatteryLevel = true; // We start out assuming that the device is plugged in (not @@ -7010,8 +7543,14 @@ public final class BatteryStatsImpl extends BatteryStats { mHistoryCur.states |= HistoryItem.STATE_BATTERY_PLUGGED_FLAG; } } - oldStatus = status; + mHistoryCur.batteryStatus = (byte)status; + mHistoryCur.batteryLevel = (byte)level; + mMaxChargeStepLevel = mMinDischargeStepLevel = + mLastChargeStepLevel = mLastDischargeStepLevel = level; + } else if (mCurrentBatteryLevel != level || mOnBattery != onBattery) { + recordDailyStatsIfNeededLocked(level >= 100 && onBattery); } + int oldStatus = mHistoryCur.batteryStatus; if (onBattery) { mDischargeCurrentLevel = level; if (!mRecordingHistory) { @@ -7072,23 +7611,23 @@ public final class BatteryStatsImpl extends BatteryStats { | (((long)(level&0xff)) << STEP_LEVEL_LEVEL_SHIFT); if (onBattery) { if (mLastDischargeStepLevel != level && mMinDischargeStepLevel > level) { - mNumDischargeStepDurations = addLevelSteps(mDischargeStepDurations, - mNumDischargeStepDurations, mLastDischargeStepTime, - mLastDischargeStepLevel - level, modeBits, elapsedRealtime); + mDischargeStepTracker.addLevelSteps(mLastDischargeStepLevel - level, + modeBits, elapsedRealtime); + mDailyDischargeStepTracker.addLevelSteps(mLastDischargeStepLevel - level, + modeBits, elapsedRealtime); mLastDischargeStepLevel = level; mMinDischargeStepLevel = level; - mLastDischargeStepTime = elapsedRealtime; mInitStepMode = mCurStepMode; mModStepMode = 0; } } else { if (mLastChargeStepLevel != level && mMaxChargeStepLevel < level) { - mNumChargeStepDurations = addLevelSteps(mChargeStepDurations, - mNumChargeStepDurations, mLastChargeStepTime, - level - mLastChargeStepLevel, modeBits, elapsedRealtime); + mChargeStepTracker.addLevelSteps(level - mLastChargeStepLevel, + modeBits, elapsedRealtime); + mDailyChargeStepTracker.addLevelSteps(level - mLastChargeStepLevel, + modeBits, elapsedRealtime); mLastChargeStepLevel = level; mMaxChargeStepLevel = level; - mLastChargeStepTime = elapsedRealtime; mInitStepMode = mCurStepMode; mModStepMode = 0; } @@ -7259,6 +7798,65 @@ public final class BatteryStatsImpl extends BatteryStats { } } + private void updateBluetoothControllerActivityLocked() { + BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); + if (adapter == null) { + return; + } + + // We read the data even if we are not on battery. Each read clears + // the previous data, so we must always read to make sure the + // data is for the current interval. + BluetoothActivityEnergyInfo info = adapter.getControllerActivityEnergyInfo( + BluetoothAdapter.ACTIVITY_ENERGY_INFO_REFRESHED); + if (info == null || !info.isValid() || !mOnBatteryInternal) { + // Bad info or we are not on battery. + return; + } + + mBluetoothActivityCounters[CONTROLLER_RX_TIME].addCountLocked( + info.getControllerRxTimeMillis()); + mBluetoothActivityCounters[CONTROLLER_TX_TIME].addCountLocked( + info.getControllerTxTimeMillis()); + mBluetoothActivityCounters[CONTROLLER_IDLE_TIME].addCountLocked( + info.getControllerIdleTimeMillis()); + mBluetoothActivityCounters[CONTROLLER_ENERGY].addCountLocked( + info.getControllerEnergyUsed()); + } + + private void updateWifiControllerActivityLocked() { + IWifiManager wifiManager = IWifiManager.Stub.asInterface( + ServiceManager.getService(Context.WIFI_SERVICE)); + if (wifiManager == null) { + return; + } + + WifiActivityEnergyInfo info; + try { + // We read the data even if we are not on battery. Each read clears + // the previous data, so we must always read to make sure the + // data is for the current interval. + info = wifiManager.reportActivityInfo(); + } catch (RemoteException e) { + // Nothing to report, WiFi is dead. + return; + } + + if (info == null || !info.isValid() || !mOnBatteryInternal) { + // Bad info or we are not on battery. + return; + } + + mWifiActivityCounters[CONTROLLER_RX_TIME].addCountLocked( + info.getControllerRxTimeMillis()); + mWifiActivityCounters[CONTROLLER_TX_TIME].addCountLocked( + info.getControllerTxTimeMillis()); + mWifiActivityCounters[CONTROLLER_IDLE_TIME].addCountLocked( + info.getControllerIdleTimeMillis()); + mWifiActivityCounters[CONTROLLER_ENERGY].addCountLocked( + info.getControllerEnergyUsed()); + } + public long getAwakeTimeBattery() { return computeBatteryUptime(getBatteryUptimeLocked(), STATS_CURRENT); } @@ -7363,22 +7961,24 @@ public final class BatteryStatsImpl extends BatteryStats { long usPerLevel = duration/discharge; return usPerLevel * mCurrentBatteryLevel; */ - if (mNumDischargeStepDurations < 1) { + if (mDischargeStepTracker.mNumStepDurations < 1) { return -1; } - long msPerLevel = computeTimePerLevel(mDischargeStepDurations, mNumDischargeStepDurations); + long msPerLevel = mDischargeStepTracker.computeTimePerLevel(); if (msPerLevel <= 0) { return -1; } return (msPerLevel * mCurrentBatteryLevel) * 1000; } - public int getNumDischargeStepDurations() { - return mNumDischargeStepDurations; + @Override + public LevelStepTracker getDischargeLevelStepTracker() { + return mDischargeStepTracker; } - public long[] getDischargeStepDurationsArray() { - return mDischargeStepDurations; + @Override + public LevelStepTracker getDailyDischargeLevelStepTracker() { + return mDailyDischargeStepTracker; } @Override @@ -7400,22 +8000,24 @@ public final class BatteryStatsImpl extends BatteryStats { long usPerLevel = duration/(curLevel-plugLevel); return usPerLevel * (100-curLevel); */ - if (mNumChargeStepDurations < 1) { + if (mChargeStepTracker.mNumStepDurations < 1) { return -1; } - long msPerLevel = computeTimePerLevel(mChargeStepDurations, mNumChargeStepDurations); + long msPerLevel = mChargeStepTracker.computeTimePerLevel(); if (msPerLevel <= 0) { return -1; } return (msPerLevel * (100-mCurrentBatteryLevel)) * 1000; } - public int getNumChargeStepDurations() { - return mNumChargeStepDurations; + @Override + public LevelStepTracker getChargeLevelStepTracker() { + return mChargeStepTracker; } - public long[] getChargeStepDurationsArray() { - return mChargeStepDurations; + @Override + public LevelStepTracker getDailyChargeLevelStepTracker() { + return mDailyChargeStepTracker; } long getBatteryUptimeLocked() { @@ -7713,6 +8315,10 @@ public final class BatteryStatsImpl extends BatteryStats { } public void readLocked() { + if (mDailyFile != null) { + readDailyStatsLocked(); + } + if (mFile == null) { Slog.w("BatteryStats", "readLocked: no file associated with this instance"); return; @@ -7750,6 +8356,8 @@ public final class BatteryStatsImpl extends BatteryStats { addHistoryBufferLocked(elapsedRealtime, uptime, HistoryItem.CMD_START, mHistoryCur); startRecordingHistory(elapsedRealtime, uptime, false); } + + recordDailyStatsIfNeededLocked(false); } public int describeContents() { @@ -7908,10 +8516,13 @@ public final class BatteryStatsImpl extends BatteryStats { mHighDischargeAmountSinceCharge = in.readInt(); mDischargeAmountScreenOnSinceCharge = in.readInt(); mDischargeAmountScreenOffSinceCharge = in.readInt(); - mNumDischargeStepDurations = in.readInt(); - in.readLongArray(mDischargeStepDurations); - mNumChargeStepDurations = in.readInt(); - in.readLongArray(mChargeStepDurations); + mDischargeStepTracker.readFromParcel(in); + mChargeStepTracker.readFromParcel(in); + mDailyDischargeStepTracker.readFromParcel(in); + mDailyChargeStepTracker.readFromParcel(in); + mDailyStartTime = in.readLong(); + mNextMinDailyDeadline = in.readLong(); + mNextMaxDailyDeadline = in.readLong(); mStartCount++; @@ -7960,6 +8571,15 @@ public final class BatteryStatsImpl extends BatteryStats { for (int i=0; i< NUM_BLUETOOTH_STATES; i++) { mBluetoothStateTimer[i].readSummaryFromParcelLocked(in); } + + for (int i = 0; i < NUM_CONTROLLER_ACTIVITY_TYPES; i++) { + mBluetoothActivityCounters[i].readSummaryFromParcelLocked(in); + } + + for (int i = 0; i < NUM_CONTROLLER_ACTIVITY_TYPES; i++) { + mWifiActivityCounters[i].readSummaryFromParcelLocked(in); + } + mNumConnectivityChange = mLoadedNumConnectivityChange = in.readInt(); mFlashlightOn = false; mFlashlightOnTimer.readSummaryFromParcelLocked(in); @@ -8202,10 +8822,13 @@ public final class BatteryStatsImpl extends BatteryStats { out.writeInt(getHighDischargeAmountSinceCharge()); out.writeInt(getDischargeAmountScreenOnSinceCharge()); out.writeInt(getDischargeAmountScreenOffSinceCharge()); - out.writeInt(mNumDischargeStepDurations); - out.writeLongArray(mDischargeStepDurations); - out.writeInt(mNumChargeStepDurations); - out.writeLongArray(mChargeStepDurations); + mDischargeStepTracker.writeToParcel(out); + mChargeStepTracker.writeToParcel(out); + mDailyDischargeStepTracker.writeToParcel(out); + mDailyChargeStepTracker.writeToParcel(out); + out.writeLong(mDailyStartTime); + out.writeLong(mNextMinDailyDeadline); + out.writeLong(mNextMaxDailyDeadline); mScreenOnTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS); for (int i=0; i<NUM_SCREEN_BRIGHTNESS_BINS; i++) { @@ -8245,6 +8868,12 @@ public final class BatteryStatsImpl extends BatteryStats { for (int i=0; i< NUM_BLUETOOTH_STATES; i++) { mBluetoothStateTimer[i].writeSummaryFromParcelLocked(out, NOWREAL_SYS); } + for (int i=0; i< NUM_CONTROLLER_ACTIVITY_TYPES; i++) { + mBluetoothActivityCounters[i].writeSummaryFromParcelLocked(out); + } + for (int i=0; i< NUM_CONTROLLER_ACTIVITY_TYPES; i++) { + mWifiActivityCounters[i].writeSummaryFromParcelLocked(out); + } out.writeInt(mNumConnectivityChange); mFlashlightOnTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS); @@ -8549,6 +9178,15 @@ public final class BatteryStatsImpl extends BatteryStats { mBluetoothStateTimer[i] = new StopwatchTimer(null, -500-i, null, mOnBatteryTimeBase, in); } + + for (int i = 0; i < NUM_CONTROLLER_ACTIVITY_TYPES; i++) { + mBluetoothActivityCounters[i] = new LongSamplingCounter(mOnBatteryTimeBase, in); + } + + for (int i = 0; i < NUM_CONTROLLER_ACTIVITY_TYPES; i++) { + mWifiActivityCounters[i] = new LongSamplingCounter(mOnBatteryTimeBase, in); + } + mNumConnectivityChange = in.readInt(); mLoadedNumConnectivityChange = in.readInt(); mUnpluggedNumConnectivityChange = in.readInt(); @@ -8568,10 +9206,8 @@ public final class BatteryStatsImpl extends BatteryStats { mDischargeAmountScreenOnSinceCharge = in.readInt(); mDischargeAmountScreenOff = in.readInt(); mDischargeAmountScreenOffSinceCharge = in.readInt(); - mNumDischargeStepDurations = in.readInt(); - in.readLongArray(mDischargeStepDurations); - mNumChargeStepDurations = in.readInt(); - in.readLongArray(mChargeStepDurations); + mDischargeStepTracker.readFromParcel(in); + mChargeStepTracker.readFromParcel(in); mLastWriteTime = in.readLong(); mBluetoothPingCount = in.readInt(); @@ -8696,6 +9332,12 @@ public final class BatteryStatsImpl extends BatteryStats { for (int i=0; i< NUM_BLUETOOTH_STATES; i++) { mBluetoothStateTimer[i].writeToParcel(out, uSecRealtime); } + for (int i=0; i< NUM_CONTROLLER_ACTIVITY_TYPES; i++) { + mBluetoothActivityCounters[i].writeToParcel(out); + } + for (int i=0; i< NUM_CONTROLLER_ACTIVITY_TYPES; i++) { + mWifiActivityCounters[i].writeToParcel(out); + } out.writeInt(mNumConnectivityChange); out.writeInt(mLoadedNumConnectivityChange); out.writeInt(mUnpluggedNumConnectivityChange); @@ -8710,10 +9352,8 @@ public final class BatteryStatsImpl extends BatteryStats { out.writeInt(mDischargeAmountScreenOnSinceCharge); out.writeInt(mDischargeAmountScreenOff); out.writeInt(mDischargeAmountScreenOffSinceCharge); - out.writeInt(mNumDischargeStepDurations); - out.writeLongArray(mDischargeStepDurations); - out.writeInt(mNumChargeStepDurations); - out.writeLongArray(mChargeStepDurations); + mDischargeStepTracker.writeToParcel(out); + mChargeStepTracker.writeToParcel(out); out.writeLong(mLastWriteTime); out.writeInt(getBluetoothPingCount()); diff --git a/core/java/com/android/internal/os/HandlerCaller.java b/core/java/com/android/internal/os/HandlerCaller.java index 99286cb..113768e 100644 --- a/core/java/com/android/internal/os/HandlerCaller.java +++ b/core/java/com/android/internal/os/HandlerCaller.java @@ -164,6 +164,15 @@ public class HandlerCaller { return mH.obtainMessage(what, arg1, 0, args); } + public Message obtainMessageIIOOO(int what, int arg1, int arg2, Object arg3, Object arg4, + Object arg5) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = arg3; + args.arg2 = arg4; + args.arg3 = arg5; + return mH.obtainMessage(what, arg1, arg2, args); + } + public Message obtainMessageOO(int what, Object arg1, Object arg2) { SomeArgs args = SomeArgs.obtain(); args.arg1 = arg1; diff --git a/core/java/com/android/internal/os/PowerProfile.java b/core/java/com/android/internal/os/PowerProfile.java index b3bafa1..944eb5a 100644 --- a/core/java/com/android/internal/os/PowerProfile.java +++ b/core/java/com/android/internal/os/PowerProfile.java @@ -76,6 +76,11 @@ public class PowerProfile { public static final String POWER_WIFI_ACTIVE = "wifi.active"; /** + * Operating voltage of the WiFi controller. + */ + public static final String OPERATING_VOLTAGE_WIFI = "wifi.voltage"; + + /** * Power consumption when GPS is on. */ public static final String POWER_GPS_ON = "gps.on"; @@ -96,6 +101,11 @@ public class PowerProfile { public static final String POWER_BLUETOOTH_AT_CMD = "bluetooth.at"; /** + * Operating voltage of the Bluetooth controller. + */ + public static final String OPERATING_VOLTAGE_BLUETOOTH = "bluetooth.voltage"; + + /** * Power consumption when screen is on, not including the backlight power. */ public static final String POWER_SCREEN_ON = "screen.on"; @@ -224,11 +234,13 @@ public class PowerProfile { } /** - * Returns the average current in mA consumed by the subsystem + * Returns the average current in mA consumed by the subsystem, or the given + * default value if the subsystem has no recorded value. * @param type the subsystem type + * @param defaultValue the value to return if the subsystem has no recorded value. * @return the average current in milliAmps. */ - public double getAveragePower(String type) { + public double getAveragePowerOrDefault(String type, double defaultValue) { if (sPowerMap.containsKey(type)) { Object data = sPowerMap.get(type); if (data instanceof Double[]) { @@ -237,9 +249,18 @@ public class PowerProfile { return (Double) sPowerMap.get(type); } } else { - return 0; + return defaultValue; } } + + /** + * Returns the average current in mA consumed by the subsystem + * @param type the subsystem type + * @return the average current in milliAmps. + */ + public double getAveragePower(String type) { + return getAveragePowerOrDefault(type, 0); + } /** * Returns the average current in mA consumed by the subsystem for the given level. diff --git a/core/java/com/android/internal/os/ProcessCpuTracker.java b/core/java/com/android/internal/os/ProcessCpuTracker.java index b5338df..2983047 100644 --- a/core/java/com/android/internal/os/ProcessCpuTracker.java +++ b/core/java/com/android/internal/os/ProcessCpuTracker.java @@ -22,11 +22,13 @@ import android.os.FileUtils; import android.os.Process; import android.os.StrictMode; import android.os.SystemClock; +import android.system.OsConstants; import android.util.Slog; import com.android.internal.util.FastPrintWriter; import libcore.io.IoUtils; +import libcore.io.Libcore; import java.io.File; import java.io.FileInputStream; @@ -130,6 +132,9 @@ public class ProcessCpuTracker { private final boolean mIncludeThreads; + // How long a CPU jiffy is in milliseconds. + private final long mJiffyMillis; + private float mLoad1 = 0; private float mLoad5 = 0; private float mLoad15 = 0; @@ -152,6 +157,7 @@ public class ProcessCpuTracker { private int mRelIrqTime; private int mRelSoftIrqTime; private int mRelIdleTime; + private boolean mRelStatsAreGood; private int[] mCurPids; private int[] mCurThreadPids; @@ -268,6 +274,8 @@ public class ProcessCpuTracker { public ProcessCpuTracker(boolean includeThreads) { mIncludeThreads = includeThreads; + long jiffyHz = Libcore.os.sysconf(OsConstants._SC_CLK_TCK); + mJiffyMillis = 1000/jiffyHz; } public void onLoadChanged(float load1, float load5, float load15) { @@ -285,49 +293,72 @@ public class ProcessCpuTracker { public void update() { if (DEBUG) Slog.v(TAG, "Update: " + this); - mLastSampleTime = mCurrentSampleTime; - mCurrentSampleTime = SystemClock.uptimeMillis(); - mLastSampleRealTime = mCurrentSampleRealTime; - mCurrentSampleRealTime = SystemClock.elapsedRealtime(); + + final long nowUptime = SystemClock.uptimeMillis(); + final long nowRealtime = SystemClock.elapsedRealtime(); final long[] sysCpu = mSystemCpuData; if (Process.readProcFile("/proc/stat", SYSTEM_CPU_FORMAT, null, sysCpu, null)) { // Total user time is user + nice time. - final long usertime = sysCpu[0]+sysCpu[1]; + final long usertime = (sysCpu[0]+sysCpu[1]) * mJiffyMillis; // Total system time is simply system time. - final long systemtime = sysCpu[2]; + final long systemtime = sysCpu[2] * mJiffyMillis; // Total idle time is simply idle time. - final long idletime = sysCpu[3]; + final long idletime = sysCpu[3] * mJiffyMillis; // Total irq time is iowait + irq + softirq time. - final long iowaittime = sysCpu[4]; - final long irqtime = sysCpu[5]; - final long softirqtime = sysCpu[6]; - - mRelUserTime = (int)(usertime - mBaseUserTime); - mRelSystemTime = (int)(systemtime - mBaseSystemTime); - mRelIoWaitTime = (int)(iowaittime - mBaseIoWaitTime); - mRelIrqTime = (int)(irqtime - mBaseIrqTime); - mRelSoftIrqTime = (int)(softirqtime - mBaseSoftIrqTime); - mRelIdleTime = (int)(idletime - mBaseIdleTime); - - if (DEBUG) { - Slog.i("Load", "Total U:" + sysCpu[0] + " N:" + sysCpu[1] - + " S:" + sysCpu[2] + " I:" + sysCpu[3] - + " W:" + sysCpu[4] + " Q:" + sysCpu[5] - + " O:" + sysCpu[6]); - Slog.i("Load", "Rel U:" + mRelUserTime + " S:" + mRelSystemTime - + " I:" + mRelIdleTime + " Q:" + mRelIrqTime); - } + final long iowaittime = sysCpu[4] * mJiffyMillis; + final long irqtime = sysCpu[5] * mJiffyMillis; + final long softirqtime = sysCpu[6] * mJiffyMillis; + + // This code is trying to avoid issues with idle time going backwards, + // but currently it gets into situations where it triggers most of the time. :( + if (true || (usertime >= mBaseUserTime && systemtime >= mBaseSystemTime + && iowaittime >= mBaseIoWaitTime && irqtime >= mBaseIrqTime + && softirqtime >= mBaseSoftIrqTime && idletime >= mBaseIdleTime)) { + mRelUserTime = (int)(usertime - mBaseUserTime); + mRelSystemTime = (int)(systemtime - mBaseSystemTime); + mRelIoWaitTime = (int)(iowaittime - mBaseIoWaitTime); + mRelIrqTime = (int)(irqtime - mBaseIrqTime); + mRelSoftIrqTime = (int)(softirqtime - mBaseSoftIrqTime); + mRelIdleTime = (int)(idletime - mBaseIdleTime); + mRelStatsAreGood = true; + + if (DEBUG) { + Slog.i("Load", "Total U:" + (sysCpu[0]*mJiffyMillis) + + " N:" + (sysCpu[1]*mJiffyMillis) + + " S:" + (sysCpu[2]*mJiffyMillis) + " I:" + (sysCpu[3]*mJiffyMillis) + + " W:" + (sysCpu[4]*mJiffyMillis) + " Q:" + (sysCpu[5]*mJiffyMillis) + + " O:" + (sysCpu[6]*mJiffyMillis)); + Slog.i("Load", "Rel U:" + mRelUserTime + " S:" + mRelSystemTime + + " I:" + mRelIdleTime + " Q:" + mRelIrqTime); + } + + mBaseUserTime = usertime; + mBaseSystemTime = systemtime; + mBaseIoWaitTime = iowaittime; + mBaseIrqTime = irqtime; + mBaseSoftIrqTime = softirqtime; + mBaseIdleTime = idletime; - mBaseUserTime = usertime; - mBaseSystemTime = systemtime; - mBaseIoWaitTime = iowaittime; - mBaseIrqTime = irqtime; - mBaseSoftIrqTime = softirqtime; - mBaseIdleTime = idletime; + } else { + mRelUserTime = 0; + mRelSystemTime = 0; + mRelIoWaitTime = 0; + mRelIrqTime = 0; + mRelSoftIrqTime = 0; + mRelIdleTime = 0; + mRelStatsAreGood = false; + Slog.w(TAG, "/proc/stats has gone backwards; skipping CPU update"); + return; + } } + mLastSampleTime = mCurrentSampleTime; + mCurrentSampleTime = nowUptime; + mLastSampleRealTime = mCurrentSampleRealTime; + mCurrentSampleRealTime = nowRealtime; + final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads(); try { mCurPids = collectStats("/proc", -1, mFirst, mCurPids, mProcStats); @@ -391,8 +422,8 @@ public class ProcessCpuTracker { final long minfaults = procStats[PROCESS_STAT_MINOR_FAULTS]; final long majfaults = procStats[PROCESS_STAT_MAJOR_FAULTS]; - final long utime = procStats[PROCESS_STAT_UTIME]; - final long stime = procStats[PROCESS_STAT_STIME]; + final long utime = procStats[PROCESS_STAT_UTIME] * mJiffyMillis; + final long stime = procStats[PROCESS_STAT_STIME] * mJiffyMillis; if (utime == st.base_utime && stime == st.base_stime) { st.rel_utime = 0; @@ -466,8 +497,8 @@ public class ProcessCpuTracker { st.baseName = procStatsString[0]; st.base_minfaults = procStats[PROCESS_FULL_STAT_MINOR_FAULTS]; st.base_majfaults = procStats[PROCESS_FULL_STAT_MAJOR_FAULTS]; - st.base_utime = procStats[PROCESS_FULL_STAT_UTIME]; - st.base_stime = procStats[PROCESS_FULL_STAT_STIME]; + st.base_utime = procStats[PROCESS_FULL_STAT_UTIME] * mJiffyMillis; + st.base_stime = procStats[PROCESS_FULL_STAT_STIME] * mJiffyMillis; } else { Slog.i(TAG, "Skipping kernel process pid " + pid + " name " + procStatsString[0]); @@ -553,7 +584,7 @@ public class ProcessCpuTracker { null, statsData, null)) { long time = statsData[PROCESS_STAT_UTIME] + statsData[PROCESS_STAT_STIME]; - return time; + return time * mJiffyMillis; } return 0; } @@ -647,6 +678,10 @@ public class ProcessCpuTracker { return mRelIdleTime; } + final public boolean hasGoodLastStats() { + return mRelStatsAreGood; + } + final public float getTotalCpuPercent() { int denom = mRelUserTime+mRelSystemTime+mRelIrqTime+mRelIdleTime; if (denom <= 0) { @@ -750,7 +785,7 @@ public class ProcessCpuTracker { for (int i=0; i<N; i++) { Stats st = mWorkingProcs.get(i); printProcessCPU(pw, st.added ? " +" : (st.removed ? " -": " "), - st.pid, st.name, (int)(st.rel_uptime+5)/10, + st.pid, st.name, (int)st.rel_uptime, st.rel_utime, st.rel_stime, 0, 0, 0, st.rel_minfaults, st.rel_majfaults); if (!st.removed && st.workingThreads != null) { int M = st.workingThreads.size(); @@ -758,7 +793,7 @@ public class ProcessCpuTracker { Stats tst = st.workingThreads.get(j); printProcessCPU(pw, tst.added ? " +" : (tst.removed ? " -": " "), - tst.pid, tst.name, (int)(st.rel_uptime+5)/10, + tst.pid, tst.name, (int)st.rel_uptime, tst.rel_utime, tst.rel_stime, 0, 0, 0, 0, 0); } } diff --git a/core/java/com/android/internal/os/ZygoteConnection.java b/core/java/com/android/internal/os/ZygoteConnection.java index 0dc242d..4d405b2 100644 --- a/core/java/com/android/internal/os/ZygoteConnection.java +++ b/core/java/com/android/internal/os/ZygoteConnection.java @@ -30,7 +30,6 @@ import android.os.SystemProperties; import android.system.ErrnoException; import android.system.Os; import android.util.Log; -import dalvik.system.PathClassLoader; import dalvik.system.VMRuntime; import java.io.BufferedReader; import java.io.DataInputStream; diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java index 98638ed..49d565d 100644 --- a/core/java/com/android/internal/os/ZygoteInit.java +++ b/core/java/com/android/internal/os/ZygoteInit.java @@ -24,8 +24,6 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.net.LocalServerSocket; import android.opengl.EGL14; -import android.os.Build; -import android.os.Debug; import android.os.Process; import android.os.SystemClock; import android.os.SystemProperties; @@ -36,7 +34,6 @@ import android.system.OsConstants; import android.system.StructPollfd; import android.util.EventLog; import android.util.Log; -import android.util.Slog; import android.webkit.WebViewFactory; import dalvik.system.DexFile; @@ -54,7 +51,6 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; import java.util.ArrayList; /** @@ -355,7 +351,7 @@ public class ZygoteInit { Log.v(TAG, "Preloading resource #" + Integer.toHexString(id)); } if (id != 0) { - if (mResources.getColorStateList(id) == null) { + if (mResources.getColorStateList(id, null) == null) { throw new IllegalArgumentException( "Unable to find preloaded color resource #0x" + Integer.toHexString(id) diff --git a/core/java/com/android/internal/policy/IPolicy.java b/core/java/com/android/internal/policy/IPolicy.java deleted file mode 100644 index d08b3b4..0000000 --- a/core/java/com/android/internal/policy/IPolicy.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2008 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.policy; - -import android.content.Context; -import android.view.FallbackEventHandler; -import android.view.LayoutInflater; -import android.view.Window; -import android.view.WindowManagerPolicy; - -/** - * {@hide} - */ - -/* The implementation of this interface must be called Policy and contained - * within the com.android.internal.policy.impl package */ -public interface IPolicy { - public Window makeNewWindow(Context context); - - public LayoutInflater makeNewLayoutInflater(Context context); - - public WindowManagerPolicy makeNewWindowManager(); - - public FallbackEventHandler makeNewFallbackEventHandler(Context context); -} diff --git a/core/java/com/android/internal/policy/PolicyManager.java b/core/java/com/android/internal/policy/PolicyManager.java deleted file mode 100644 index 462b3a9..0000000 --- a/core/java/com/android/internal/policy/PolicyManager.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2008 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.policy; - -import android.content.Context; -import android.view.FallbackEventHandler; -import android.view.LayoutInflater; -import android.view.Window; -import android.view.WindowManagerPolicy; - -/** - * {@hide} - */ - -public final class PolicyManager { - private static final String POLICY_IMPL_CLASS_NAME = - "com.android.internal.policy.impl.Policy"; - - private static final IPolicy sPolicy; - - static { - // Pull in the actual implementation of the policy at run-time - try { - Class policyClass = Class.forName(POLICY_IMPL_CLASS_NAME); - sPolicy = (IPolicy)policyClass.newInstance(); - } catch (ClassNotFoundException ex) { - throw new RuntimeException( - POLICY_IMPL_CLASS_NAME + " could not be loaded", ex); - } catch (InstantiationException ex) { - throw new RuntimeException( - POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex); - } catch (IllegalAccessException ex) { - throw new RuntimeException( - POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex); - } - } - - // Cannot instantiate this class - private PolicyManager() {} - - // The static methods to spawn new policy-specific objects - public static Window makeNewWindow(Context context) { - return sPolicy.makeNewWindow(context); - } - - public static LayoutInflater makeNewLayoutInflater(Context context) { - return sPolicy.makeNewLayoutInflater(context); - } - - public static WindowManagerPolicy makeNewWindowManager() { - return sPolicy.makeNewWindowManager(); - } - - public static FallbackEventHandler makeNewFallbackEventHandler(Context context) { - return sPolicy.makeNewFallbackEventHandler(context); - } -} diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index a3c0db4..2b0d244 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -1,19 +1,19 @@ /** * Copyright (c) 2007, 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 + * 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 + * 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 + * 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.statusbar; import com.android.internal.statusbar.StatusBarIcon; @@ -43,5 +43,26 @@ oneway interface IStatusBar void preloadRecentApps(); void cancelPreloadRecentApps(); void showScreenPinningRequest(); + + /** + * Notifies the status bar that an app transition is pending to delay applying some flags with + * visual impact until {@link #appTransitionReady} is called. + */ + void appTransitionPending(); + + /** + * Notifies the status bar that a pending app transition has been cancelled. + */ + void appTransitionCancelled(); + + /** + * Notifies the status bar that an app transition is now being executed. + * + * @param statusBarAnimationsStartTime the desired start time for all visual animations in the + * status bar caused by this app transition in uptime millis + * @param statusBarAnimationsDuration the duration for all visual animations in the status + * bar caused by this app transition in millis + */ + void appTransitionStarting(long statusBarAnimationsStartTime, long statusBarAnimationsDuration); } diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl index 40c009f..6cb839e 100644 --- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl @@ -1,16 +1,16 @@ /** * Copyright (c) 2007, 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 + * 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 + * 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 + * 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. */ @@ -61,4 +61,25 @@ interface IStatusBarService void toggleRecentApps(); void preloadRecentApps(); void cancelPreloadRecentApps(); + + /** + * Notifies the status bar that an app transition is pending to delay applying some flags with + * visual impact until {@link #appTransitionReady} is called. + */ + void appTransitionPending(); + + /** + * Notifies the status bar that a pending app transition has been cancelled. + */ + void appTransitionCancelled(); + + /** + * Notifies the status bar that an app transition is now being executed. + * + * @param statusBarAnimationsStartTime the desired start time for all visual animations in the + * status bar caused by this app transition in uptime millis + * @param statusBarAnimationsDuration the duration for all visual animations in the status + * bar caused by this app transition in millis + */ + void appTransitionStarting(long statusBarAnimationsStartTime, long statusBarAnimationsDuration); } diff --git a/core/java/com/android/internal/transition/EpicenterClipReveal.java b/core/java/com/android/internal/transition/EpicenterClipReveal.java new file mode 100644 index 0000000..abb50c1 --- /dev/null +++ b/core/java/com/android/internal/transition/EpicenterClipReveal.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2015 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.transition; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.RectEvaluator; +import android.content.Context; +import android.graphics.Rect; +import android.transition.TransitionValues; +import android.transition.Visibility; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; + +/** + * EpicenterClipReveal captures the {@link View#getClipBounds()} before and + * after the scene change and animates between those and the epicenter bounds + * during a visibility transition. + */ +public class EpicenterClipReveal extends Visibility { + private static final String PROPNAME_CLIP = "android:epicenterReveal:clip"; + private static final String PROPNAME_BOUNDS = "android:epicenterReveal:bounds"; + + public EpicenterClipReveal() {} + + public EpicenterClipReveal(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void captureStartValues(TransitionValues transitionValues) { + super.captureStartValues(transitionValues); + captureValues(transitionValues); + } + + @Override + public void captureEndValues(TransitionValues transitionValues) { + super.captureEndValues(transitionValues); + captureValues(transitionValues); + } + + private void captureValues(TransitionValues values) { + final View view = values.view; + if (view.getVisibility() == View.GONE) { + return; + } + + final Rect clip = view.getClipBounds(); + values.values.put(PROPNAME_CLIP, clip); + + if (clip == null) { + final Rect bounds = new Rect(0, 0, view.getWidth(), view.getHeight()); + values.values.put(PROPNAME_BOUNDS, bounds); + } + } + + @Override + public Animator onAppear(ViewGroup sceneRoot, View view, + TransitionValues startValues, TransitionValues endValues) { + if (endValues == null) { + return null; + } + + final Rect end = getBestRect(endValues); + final Rect start = getEpicenterOrCenter(end); + + // Prepare the view. + view.setClipBounds(start); + + return createRectAnimator(view, start, end, endValues); + } + + @Override + public Animator onDisappear(ViewGroup sceneRoot, View view, + TransitionValues startValues, TransitionValues endValues) { + if (startValues == null) { + return null; + } + + final Rect start = getBestRect(startValues); + final Rect end = getEpicenterOrCenter(start); + + // Prepare the view. + view.setClipBounds(start); + + return createRectAnimator(view, start, end, endValues); + } + + private Rect getEpicenterOrCenter(Rect bestRect) { + final Rect epicenter = getEpicenter(); + if (epicenter != null) { + return epicenter; + } + + int centerX = bestRect.centerX(); + int centerY = bestRect.centerY(); + return new Rect(centerX, centerY, centerX, centerY); + } + + private Rect getBestRect(TransitionValues values) { + final Rect clipRect = (Rect) values.values.get(PROPNAME_CLIP); + if (clipRect == null) { + return (Rect) values.values.get(PROPNAME_BOUNDS); + } + return clipRect; + } + + private Animator createRectAnimator(final View view, Rect start, Rect end, + TransitionValues endValues) { + final Rect terminalClip = (Rect) endValues.values.get(PROPNAME_CLIP); + final RectEvaluator evaluator = new RectEvaluator(new Rect()); + ObjectAnimator anim = ObjectAnimator.ofObject(view, "clipBounds", evaluator, start, end); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + view.setClipBounds(terminalClip); + } + }); + return anim; + } +} diff --git a/core/java/com/android/internal/util/DumpUtils.java b/core/java/com/android/internal/util/DumpUtils.java index 65b56ec..64e1d10 100644 --- a/core/java/com/android/internal/util/DumpUtils.java +++ b/core/java/com/android/internal/util/DumpUtils.java @@ -35,13 +35,14 @@ public final class DumpUtils { * trying to acquire, we use a short timeout to avoid deadlocks. The process * is inelegant but this function is only used for debugging purposes. */ - public static void dumpAsync(Handler handler, final Dump dump, PrintWriter pw, long timeout) { + public static void dumpAsync(Handler handler, final Dump dump, PrintWriter pw, + final String prefix, long timeout) { final StringWriter sw = new StringWriter(); if (handler.runWithScissors(new Runnable() { @Override public void run() { PrintWriter lpw = new FastPrintWriter(sw); - dump.dump(lpw); + dump.dump(lpw, prefix); lpw.close(); } }, timeout)) { @@ -52,6 +53,6 @@ public final class DumpUtils { } public interface Dump { - void dump(PrintWriter pw); + void dump(PrintWriter pw, String prefix); } } diff --git a/core/java/com/android/internal/util/UserIcons.java b/core/java/com/android/internal/util/UserIcons.java index e1e9d5e..daf745f 100644 --- a/core/java/com/android/internal/util/UserIcons.java +++ b/core/java/com/android/internal/util/UserIcons.java @@ -48,9 +48,12 @@ public class UserIcons { if (icon == null) { return null; } - Bitmap bitmap = Bitmap.createBitmap(icon.getIntrinsicWidth(), icon.getIntrinsicHeight(), - Bitmap.Config.ARGB_8888); - icon.draw(new Canvas(bitmap)); + final int width = icon.getIntrinsicWidth(); + final int height = icon.getIntrinsicHeight(); + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + icon.setBounds(0, 0, width, height); + icon.draw(canvas); return bitmap; } @@ -67,8 +70,8 @@ public class UserIcons { // Return colored icon instead colorResId = USER_ICON_COLORS[userId % USER_ICON_COLORS.length]; } - Drawable icon = Resources.getSystem().getDrawable(R.drawable.ic_account_circle).mutate(); - icon.setColorFilter(Resources.getSystem().getColor(colorResId), Mode.SRC_IN); + Drawable icon = Resources.getSystem().getDrawable(R.drawable.ic_account_circle, null).mutate(); + icon.setColorFilter(Resources.getSystem().getColor(colorResId, null), Mode.SRC_IN); icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); return icon; } diff --git a/core/java/com/android/internal/util/XmlUtils.java b/core/java/com/android/internal/util/XmlUtils.java index 2bd607c..0350d61 100644 --- a/core/java/com/android/internal/util/XmlUtils.java +++ b/core/java/com/android/internal/util/XmlUtils.java @@ -20,6 +20,7 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Bitmap.CompressFormat; import android.net.Uri; +import android.util.ArrayMap; import android.util.Base64; import android.util.Xml; @@ -822,7 +823,36 @@ public class XmlUtils { int eventType = parser.getEventType(); do { if (eventType == parser.START_TAG) { - Object val = readThisValueXml(parser, name, callback); + Object val = readThisValueXml(parser, name, callback, false); + map.put(name[0], val); + } else if (eventType == parser.END_TAG) { + if (parser.getName().equals(endTag)) { + return map; + } + throw new XmlPullParserException( + "Expected " + endTag + " end tag at: " + parser.getName()); + } + eventType = parser.next(); + } while (eventType != parser.END_DOCUMENT); + + throw new XmlPullParserException( + "Document ended before " + endTag + " end tag"); + } + + /** + * Like {@link #readThisMapXml}, but returns an ArrayMap instead of HashMap. + * @hide + */ + public static final ArrayMap<String, ?> readThisArrayMapXml(XmlPullParser parser, String endTag, + String[] name, ReadMapCallback callback) + throws XmlPullParserException, java.io.IOException + { + ArrayMap<String, Object> map = new ArrayMap<>(); + + int eventType = parser.getEventType(); + do { + if (eventType == parser.START_TAG) { + Object val = readThisValueXml(parser, name, callback, true); map.put(name[0], val); } else if (eventType == parser.END_TAG) { if (parser.getName().equals(endTag)) { @@ -854,7 +884,7 @@ public class XmlUtils { */ public static final ArrayList readThisListXml(XmlPullParser parser, String endTag, String[] name) throws XmlPullParserException, java.io.IOException { - return readThisListXml(parser, endTag, name, null); + return readThisListXml(parser, endTag, name, null, false); } /** @@ -872,14 +902,14 @@ public class XmlUtils { * @see #readListXml */ private static final ArrayList readThisListXml(XmlPullParser parser, String endTag, - String[] name, ReadMapCallback callback) + String[] name, ReadMapCallback callback, boolean arrayMap) throws XmlPullParserException, java.io.IOException { ArrayList list = new ArrayList(); int eventType = parser.getEventType(); do { if (eventType == parser.START_TAG) { - Object val = readThisValueXml(parser, name, callback); + Object val = readThisValueXml(parser, name, callback, arrayMap); list.add(val); //System.out.println("Adding to list: " + val); } else if (eventType == parser.END_TAG) { @@ -915,7 +945,7 @@ public class XmlUtils { */ public static final HashSet readThisSetXml(XmlPullParser parser, String endTag, String[] name) throws XmlPullParserException, java.io.IOException { - return readThisSetXml(parser, endTag, name, null); + return readThisSetXml(parser, endTag, name, null, false); } /** @@ -937,13 +967,14 @@ public class XmlUtils { * @hide */ private static final HashSet readThisSetXml(XmlPullParser parser, String endTag, String[] name, - ReadMapCallback callback) throws XmlPullParserException, java.io.IOException { + ReadMapCallback callback, boolean arrayMap) + throws XmlPullParserException, java.io.IOException { HashSet set = new HashSet(); int eventType = parser.getEventType(); do { if (eventType == parser.START_TAG) { - Object val = readThisValueXml(parser, name, callback); + Object val = readThisValueXml(parser, name, callback, arrayMap); set.add(val); //System.out.println("Adding to set: " + val); } else if (eventType == parser.END_TAG) { @@ -1292,7 +1323,7 @@ public class XmlUtils { int eventType = parser.getEventType(); do { if (eventType == parser.START_TAG) { - return readThisValueXml(parser, name, null); + return readThisValueXml(parser, name, null, false); } else if (eventType == parser.END_TAG) { throw new XmlPullParserException( "Unexpected end tag at: " + parser.getName()); @@ -1308,7 +1339,8 @@ public class XmlUtils { } private static final Object readThisValueXml(XmlPullParser parser, String[] name, - ReadMapCallback callback) throws XmlPullParserException, java.io.IOException { + ReadMapCallback callback, boolean arrayMap) + throws XmlPullParserException, java.io.IOException { final String valueName = parser.getAttributeValue(null, "name"); final String tagName = parser.getName(); @@ -1368,19 +1400,21 @@ public class XmlUtils { return res; } else if (tagName.equals("map")) { parser.next(); - res = readThisMapXml(parser, "map", name); + res = arrayMap + ? readThisArrayMapXml(parser, "map", name, callback) + : readThisMapXml(parser, "map", name, callback); name[0] = valueName; //System.out.println("Returning value for " + valueName + ": " + res); return res; } else if (tagName.equals("list")) { parser.next(); - res = readThisListXml(parser, "list", name); + res = readThisListXml(parser, "list", name, callback, arrayMap); name[0] = valueName; //System.out.println("Returning value for " + valueName + ": " + res); return res; } else if (tagName.equals("set")) { parser.next(); - res = readThisSetXml(parser, "set", name); + res = readThisSetXml(parser, "set", name, callback, arrayMap); name[0] = valueName; //System.out.println("Returning value for " + valueName + ": " + res); return res; diff --git a/core/java/com/android/internal/view/StandaloneActionMode.java b/core/java/com/android/internal/view/StandaloneActionMode.java index d5d3602..e6d911c 100644 --- a/core/java/com/android/internal/view/StandaloneActionMode.java +++ b/core/java/com/android/internal/view/StandaloneActionMode.java @@ -47,7 +47,7 @@ public class StandaloneActionMode extends ActionMode implements MenuBuilder.Call mCallback = callback; mMenu = new MenuBuilder(view.getContext()).setDefaultShowAsAction( - MenuItem.SHOW_AS_ACTION_IF_ROOM); + MenuItem.SHOW_AS_ACTION_IF_ROOM); mMenu.setCallback(this); mFocusable = isFocusable; } @@ -64,12 +64,12 @@ public class StandaloneActionMode extends ActionMode implements MenuBuilder.Call @Override public void setTitle(int resId) { - setTitle(mContext.getString(resId)); + setTitle(resId != 0 ? mContext.getString(resId) : null); } @Override public void setSubtitle(int resId) { - setSubtitle(mContext.getString(resId)); + setSubtitle(resId != 0 ? mContext.getString(resId) : null); } @Override diff --git a/core/java/com/android/internal/view/menu/ActionMenuItem.java b/core/java/com/android/internal/view/menu/ActionMenuItem.java index ed676bb..00af401 100644 --- a/core/java/com/android/internal/view/menu/ActionMenuItem.java +++ b/core/java/com/android/internal/view/menu/ActionMenuItem.java @@ -18,6 +18,8 @@ package com.android.internal.view.menu; import android.content.Context; import android.content.Intent; +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.view.ActionProvider; import android.view.ContextMenu.ContextMenuInfo; @@ -42,6 +44,7 @@ public class ActionMenuItem implements MenuItem { private Drawable mIconDrawable; private int mIconResId = NO_ICON; + private TintInfo mIconTintInfo; private Context mContext; @@ -158,12 +161,14 @@ public class ActionMenuItem implements MenuItem { public MenuItem setIcon(Drawable icon) { mIconDrawable = icon; mIconResId = NO_ICON; + applyIconTint(); return this; } public MenuItem setIcon(int iconRes) { mIconResId = iconRes; mIconDrawable = mContext.getDrawable(iconRes); + applyIconTint(); return this; } @@ -274,4 +279,48 @@ public class ActionMenuItem implements MenuItem { // No need to save the listener; ActionMenuItem does not support collapsing items. return this; } + + @Override + public MenuItem setIconTintList(ColorStateList tintList) { + if (mIconTintInfo == null) { + mIconTintInfo = new TintInfo(); + } + mIconTintInfo.mTintList = tintList; + mIconTintInfo.mHasTintList = true; + applyIconTint(); + return this; + } + + @Override + public MenuItem setIconTintMode(PorterDuff.Mode tintMode) { + if (mIconTintInfo == null) { + mIconTintInfo = new TintInfo(); + } + mIconTintInfo.mTintMode = tintMode; + mIconTintInfo.mHasTintMode = true; + applyIconTint(); + return this; + } + + private void applyIconTint() { + final TintInfo tintInfo = mIconTintInfo; + if (mIconDrawable != null && tintInfo != null) { + if (tintInfo.mHasTintList || tintInfo.mHasTintMode) { + mIconDrawable = mIconDrawable.mutate(); + if (tintInfo.mHasTintList) { + mIconDrawable.setTintList(tintInfo.mTintList); + } + if (tintInfo.mHasTintMode) { + mIconDrawable.setTintMode(tintInfo.mTintMode); + } + } + } + } + + private static class TintInfo { + ColorStateList mTintList; + PorterDuff.Mode mTintMode; + boolean mHasTintMode; + boolean mHasTintList; + } } diff --git a/core/java/com/android/internal/view/menu/ActionMenuItemView.java b/core/java/com/android/internal/view/menu/ActionMenuItemView.java index 7eec392..f75b139 100644 --- a/core/java/com/android/internal/view/menu/ActionMenuItemView.java +++ b/core/java/com/android/internal/view/menu/ActionMenuItemView.java @@ -217,14 +217,14 @@ public class ActionMenuItemView extends TextView } @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) { onPopulateAccessibilityEvent(event); return true; } @Override - public void onPopulateAccessibilityEvent(AccessibilityEvent event) { - super.onPopulateAccessibilityEvent(event); + public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) { + super.onPopulateAccessibilityEventInternal(event); final CharSequence cdesc = getContentDescription(); if (!TextUtils.isEmpty(cdesc)) { event.getText().add(cdesc); diff --git a/core/java/com/android/internal/view/menu/ListMenuItemView.java b/core/java/com/android/internal/view/menu/ListMenuItemView.java index 692bdac..29ac3f3 100644 --- a/core/java/com/android/internal/view/menu/ListMenuItemView.java +++ b/core/java/com/android/internal/view/menu/ListMenuItemView.java @@ -276,8 +276,8 @@ public class ListMenuItemView extends LinearLayout implements MenuView.ItemView } @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); + public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfoInternal(info); if (mItemData != null && mItemData.hasSubMenu()) { info.setCanOpenPopup(true); diff --git a/core/java/com/android/internal/view/menu/MenuItemImpl.java b/core/java/com/android/internal/view/menu/MenuItemImpl.java index 3b1f20d..ef4e546 100644 --- a/core/java/com/android/internal/view/menu/MenuItemImpl.java +++ b/core/java/com/android/internal/view/menu/MenuItemImpl.java @@ -21,6 +21,8 @@ import com.android.internal.view.menu.MenuView.ItemView; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.util.Log; import android.view.ActionProvider; @@ -60,6 +62,11 @@ public final class MenuItemImpl implements MenuItem { * needed). */ private int mIconResId = NO_ICON; + + /** + * Tint info for the icon + */ + private TintInfo mIconTintInfo; /** The menu to which this item belongs */ private MenuBuilder mMenu; @@ -385,10 +392,10 @@ public final class MenuItemImpl implements MenuItem { } if (mIconResId != NO_ICON) { - Drawable icon = mMenu.getContext().getDrawable(mIconResId); + mIconDrawable = mMenu.getContext().getDrawable(mIconResId); mIconResId = NO_ICON; - mIconDrawable = icon; - return icon; + applyIconTint(); + return mIconDrawable; } return null; @@ -397,6 +404,7 @@ public final class MenuItemImpl implements MenuItem { public MenuItem setIcon(Drawable icon) { mIconResId = NO_ICON; mIconDrawable = icon; + applyIconTint(); mMenu.onItemsChanged(false); return this; @@ -670,4 +678,48 @@ public final class MenuItemImpl implements MenuItem { public boolean isActionViewExpanded() { return mIsActionViewExpanded; } + + @Override + public MenuItem setIconTintList(ColorStateList tintList) { + if (mIconTintInfo == null) { + mIconTintInfo = new TintInfo(); + } + mIconTintInfo.mTintList = tintList; + mIconTintInfo.mHasTintList = true; + applyIconTint(); + return this; + } + + @Override + public MenuItem setIconTintMode(PorterDuff.Mode tintMode) { + if (mIconTintInfo == null) { + mIconTintInfo = new TintInfo(); + } + mIconTintInfo.mTintMode = tintMode; + mIconTintInfo.mHasTintMode = true; + applyIconTint(); + return this; + } + + private void applyIconTint() { + final TintInfo tintInfo = mIconTintInfo; + if (mIconDrawable != null && tintInfo != null) { + if (tintInfo.mHasTintList || tintInfo.mHasTintMode) { + mIconDrawable = mIconDrawable.mutate(); + if (tintInfo.mHasTintList) { + mIconDrawable.setTintList(tintInfo.mTintList); + } + if (tintInfo.mHasTintMode) { + mIconDrawable.setTintMode(tintInfo.mTintMode); + } + } + } + } + + private static class TintInfo { + ColorStateList mTintList; + PorterDuff.Mode mTintMode; + boolean mHasTintMode; + boolean mHasTintList; + } } diff --git a/core/java/com/android/internal/view/menu/MenuPopupHelper.java b/core/java/com/android/internal/view/menu/MenuPopupHelper.java index 99bb1ac..7d45071 100644 --- a/core/java/com/android/internal/view/menu/MenuPopupHelper.java +++ b/core/java/com/android/internal/view/menu/MenuPopupHelper.java @@ -43,8 +43,6 @@ import java.util.ArrayList; public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.OnKeyListener, ViewTreeObserver.OnGlobalLayoutListener, PopupWindow.OnDismissListener, View.OnAttachStateChangeListener, MenuPresenter { - private static final String TAG = "MenuPopupHelper"; - static final int ITEM_LAYOUT = com.android.internal.R.layout.popup_menu_item_layout; private final Context mContext; @@ -118,6 +116,10 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On mDropDownGravity = gravity; } + public int getGravity() { + return mDropDownGravity; + } + public void show() { if (!tryShow()) { throw new IllegalStateException("MenuPopupHelper cannot be used without an anchor"); @@ -128,14 +130,25 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On return mPopup; } + /** + * Attempts to show the popup anchored to the view specified by + * {@link #setAnchorView(View)}. + * + * @return {@code true} if the popup was shown or was already showing prior + * to calling this method, {@code false} otherwise + */ public boolean tryShow() { + if (isShowing()) { + return true; + } + mPopup = new ListPopupWindow(mContext, null, mPopupStyleAttr, mPopupStyleRes); mPopup.setOnDismissListener(this); mPopup.setOnItemClickListener(this); mPopup.setAdapter(mAdapter); mPopup.setModal(true); - View anchor = mAnchorView; + final View anchor = mAnchorView; if (anchor != null) { final boolean addGlobalListener = mTreeObserver == null; mTreeObserver = anchor.getViewTreeObserver(); // Refresh to latest @@ -165,6 +178,7 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On } } + @Override public void onDismiss() { mPopup = null; mMenu.close(); @@ -186,6 +200,7 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On adapter.mAdapterMenu.performItemAction(adapter.getItem(position), 0); } + @Override public boolean onKey(View v, int keyCode, KeyEvent event) { if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_MENU) { dismiss(); diff --git a/core/java/com/android/internal/widget/AccessibleDateAnimator.java b/core/java/com/android/internal/widget/AccessibleDateAnimator.java index e91a55c..f97a5d1 100644 --- a/core/java/com/android/internal/widget/AccessibleDateAnimator.java +++ b/core/java/com/android/internal/widget/AccessibleDateAnimator.java @@ -40,7 +40,7 @@ public class AccessibleDateAnimator extends ViewAnimator { * Announce the currently-selected date when launched. */ @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) { if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { // Clear the event's current text so that only the current date will be spoken. event.getText().clear(); @@ -51,6 +51,6 @@ public class AccessibleDateAnimator extends ViewAnimator { event.getText().add(dateString); return true; } - return super.dispatchPopulateAccessibilityEvent(event); + return super.dispatchPopulateAccessibilityEventInternal(event); } } diff --git a/core/java/com/android/internal/widget/ActionBarContainer.java b/core/java/com/android/internal/widget/ActionBarContainer.java index 7937a95..a5efa82 100644 --- a/core/java/com/android/internal/widget/ActionBarContainer.java +++ b/core/java/com/android/internal/widget/ActionBarContainer.java @@ -23,7 +23,6 @@ import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Outline; import android.graphics.PixelFormat; -import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.ActionMode; @@ -32,8 +31,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; -import java.util.List; - /** * This class acts as a container for the action bar view and action mode context views. * It applies special styles as needed to help handle animated transitions between them. @@ -259,6 +256,15 @@ public class ActionBarContainer extends FrameLayout { return null; } + @Override + public ActionMode startActionModeForChild( + View child, ActionMode.Callback callback, int type) { + if (type != ActionMode.TYPE_PRIMARY) { + return super.startActionModeForChild(child, callback, type); + } + return null; + } + private static boolean isCollapsed(View view) { return view == null || view.getVisibility() == GONE || view.getMeasuredHeight() == 0; } @@ -387,7 +393,7 @@ public class ActionBarContainer extends FrameLayout { } @Override - public void setColorFilter(ColorFilter cf) { + public void setColorFilter(ColorFilter colorFilter) { } @Override diff --git a/core/java/com/android/internal/widget/ActionBarContextView.java b/core/java/com/android/internal/widget/ActionBarContextView.java index 7c671e8..42d875d 100644 --- a/core/java/com/android/internal/widget/ActionBarContextView.java +++ b/core/java/com/android/internal/widget/ActionBarContextView.java @@ -17,8 +17,6 @@ package com.android.internal.widget; import com.android.internal.R; -import android.util.TypedValue; -import android.view.ContextThemeWrapper; import android.widget.ActionMenuPresenter; import android.widget.ActionMenuView; import com.android.internal.view.menu.MenuBuilder; @@ -158,7 +156,7 @@ public class ActionBarContextView extends AbsActionBarView implements AnimatorLi removeView(mCustomView); } mCustomView = view; - if (mTitleLayout != null) { + if (view != null && mTitleLayout != null) { removeView(mTitleLayout); mTitleLayout = null; } @@ -529,7 +527,7 @@ public class ActionBarContextView extends AbsActionBarView implements AnimatorLi } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { // Action mode started event.setSource(this); @@ -537,7 +535,7 @@ public class ActionBarContextView extends AbsActionBarView implements AnimatorLi event.setPackageName(getContext().getPackageName()); event.setContentDescription(mTitle); } else { - super.onInitializeAccessibilityEvent(event); + super.onInitializeAccessibilityEventInternal(event); } } diff --git a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java index cca48d3..c3a7460 100644 --- a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java +++ b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java @@ -32,7 +32,6 @@ import android.util.IntProperty; import android.util.Log; import android.util.Property; import android.util.SparseArray; -import android.view.KeyEvent; import android.view.Menu; import android.view.View; import android.view.ViewGroup; diff --git a/core/java/com/android/internal/widget/ActionBarView.java b/core/java/com/android/internal/widget/ActionBarView.java index 654d08b..88436f8 100644 --- a/core/java/com/android/internal/widget/ActionBarView.java +++ b/core/java/com/android/internal/widget/ActionBarView.java @@ -1463,14 +1463,14 @@ public class ActionBarView extends AbsActionBarView implements DecorToolbar { } @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) { onPopulateAccessibilityEvent(event); return true; } @Override - public void onPopulateAccessibilityEvent(AccessibilityEvent event) { - super.onPopulateAccessibilityEvent(event); + public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) { + super.onPopulateAccessibilityEventInternal(event); final CharSequence cdesc = getContentDescription(); if (!TextUtils.isEmpty(cdesc)) { event.getText().add(cdesc); diff --git a/core/java/com/android/internal/widget/ButtonBarLayout.java b/core/java/com/android/internal/widget/ButtonBarLayout.java new file mode 100644 index 0000000..64e6c69 --- /dev/null +++ b/core/java/com/android/internal/widget/ButtonBarLayout.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.widget.LinearLayout; + +import com.android.internal.R; + +/** + * An extension of LinearLayout that automatically switches to vertical + * orientation when it can't fit its child views horizontally. + */ +public class ButtonBarLayout extends LinearLayout { + /** Spacer used in horizontal orientation. */ + private final View mSpacer; + + /** Whether the current configuration allows stacking. */ + private final boolean mAllowStacked; + + /** Whether the layout is currently stacked. */ + private boolean mStacked; + + public ButtonBarLayout(Context context, AttributeSet attrs) { + super(context, attrs); + + mAllowStacked = context.getResources().getBoolean(R.bool.allow_stacked_button_bar); + mSpacer = findViewById(R.id.spacer); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + // Maybe we can fit the content now? + if (w > oldw && mStacked) { + setStacked(false); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + if (mAllowStacked && getOrientation() == LinearLayout.HORIZONTAL) { + final int measuredWidth = getMeasuredWidthAndState(); + final int measuredWidthState = measuredWidth & MEASURED_STATE_MASK; + if (measuredWidthState == MEASURED_STATE_TOO_SMALL) { + setStacked(true); + + // Measure again in the new orientation. + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + } + } + + private void setStacked(boolean stacked) { + setOrientation(stacked ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL); + setGravity(stacked ? Gravity.RIGHT : Gravity.BOTTOM); + + if (mSpacer != null) { + mSpacer.setVisibility(stacked ? View.GONE : View.INVISIBLE); + } + + // Reverse the child order. This is specific to the Material button + // bar's layout XML and will probably not generalize. + final int childCount = getChildCount(); + for (int i = childCount - 2; i >= 0; i--) { + bringChildToFront(getChildAt(i)); + } + + mStacked = stacked; + } +} diff --git a/core/java/com/android/internal/widget/ExploreByTouchHelper.java b/core/java/com/android/internal/widget/ExploreByTouchHelper.java index 0e046cb..bdf17fc 100644 --- a/core/java/com/android/internal/widget/ExploreByTouchHelper.java +++ b/core/java/com/android/internal/widget/ExploreByTouchHelper.java @@ -567,7 +567,15 @@ public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate { } // TODO: Check virtual view visibility. if (!isAccessibilityFocused(virtualViewId)) { + // Clear focus from the previously focused view, if applicable. + if (mFocusedVirtualViewId != INVALID_ID) { + sendEventForVirtualView(mFocusedVirtualViewId, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); + } + + // Set focus on the new view. mFocusedVirtualViewId = virtualViewId; + // TODO: Only invalidate virtual view bounds. mView.invalidate(); sendEventForVirtualView(virtualViewId, diff --git a/core/java/com/android/internal/widget/FaceUnlockView.java b/core/java/com/android/internal/widget/FaceUnlockView.java deleted file mode 100644 index 121e601..0000000 --- a/core/java/com/android/internal/widget/FaceUnlockView.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.internal.widget; - -import android.content.Context; -import android.util.AttributeSet; -import android.widget.RelativeLayout; - -public class FaceUnlockView extends RelativeLayout { - private static final String TAG = "FaceUnlockView"; - - public FaceUnlockView(Context context) { - this(context, null); - } - - public FaceUnlockView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - private int resolveMeasured(int measureSpec, int desired) - { - int result = 0; - int specSize = MeasureSpec.getSize(measureSpec); - switch (MeasureSpec.getMode(measureSpec)) { - case MeasureSpec.UNSPECIFIED: - result = desired; - break; - case MeasureSpec.AT_MOST: - result = Math.max(specSize, desired); - break; - case MeasureSpec.EXACTLY: - default: - result = specSize; - } - return result; - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - final int minimumWidth = getSuggestedMinimumWidth(); - final int minimumHeight = getSuggestedMinimumHeight(); - int viewWidth = resolveMeasured(widthMeasureSpec, minimumWidth); - int viewHeight = resolveMeasured(heightMeasureSpec, minimumHeight); - - final int chosenSize = Math.min(viewWidth, viewHeight); - final int newWidthMeasureSpec = - MeasureSpec.makeMeasureSpec(chosenSize, MeasureSpec.AT_MOST); - final int newHeightMeasureSpec = - MeasureSpec.makeMeasureSpec(chosenSize, MeasureSpec.AT_MOST); - - super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec); - } -} diff --git a/core/java/com/android/internal/widget/ILockSettings.aidl b/core/java/com/android/internal/widget/ILockSettings.aidl index 9501f92..0cb1f38 100644 --- a/core/java/com/android/internal/widget/ILockSettings.aidl +++ b/core/java/com/android/internal/widget/ILockSettings.aidl @@ -31,5 +31,4 @@ interface ILockSettings { boolean checkVoldPassword(int userId); boolean havePattern(int userId); boolean havePassword(int userId); - void removeUser(int userId); } diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index 0afc651..90821dc 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -18,14 +18,11 @@ package com.android.internal.widget; import android.Manifest; import android.app.ActivityManagerNative; -import android.app.AlarmManager; import android.app.admin.DevicePolicyManager; import android.app.trust.TrustManager; -import android.appwidget.AppWidgetManager; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; -import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.UserInfo; import android.os.AsyncTask; @@ -39,19 +36,12 @@ import android.os.UserManager; import android.os.storage.IMountService; import android.os.storage.StorageManager; import android.provider.Settings; -import android.telecom.TelecomManager; import android.text.TextUtils; import android.util.Log; -import android.view.IWindowManager; -import android.view.View; -import android.widget.Button; -import com.android.internal.R; import com.google.android.collect.Lists; -import java.io.ByteArrayOutputStream; -import java.nio.charset.StandardCharsets; -import libcore.util.HexEncoding; +import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; @@ -59,6 +49,8 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import libcore.util.HexEncoding; + /** * Utilities for the lock pattern and its settings. */ @@ -103,52 +95,36 @@ public class LockPatternUtils { public static final int MIN_LOCK_PATTERN_SIZE = 4; /** + * The minimum size of a valid password. + */ + public static final int MIN_LOCK_PASSWORD_SIZE = 4; + + /** * The minimum number of dots the user must include in a wrong pattern * attempt for it to be counted against the counts that affect * {@link #FAILED_ATTEMPTS_BEFORE_TIMEOUT} and {@link #FAILED_ATTEMPTS_BEFORE_RESET} */ public static final int MIN_PATTERN_REGISTER_FAIL = MIN_LOCK_PATTERN_SIZE; - /** - * Tells the keyguard to show the user switcher when the keyguard is created. - */ - public static final String KEYGUARD_SHOW_USER_SWITCHER = "showuserswitcher"; - - /** - * Tells the keyguard to show the security challenge when the keyguard is created. - */ - public static final String KEYGUARD_SHOW_SECURITY_CHALLENGE = "showsecuritychallenge"; - - /** - * Tells the keyguard to show the widget with the specified id when the keyguard is created. - */ - public static final String KEYGUARD_SHOW_APPWIDGET = "showappwidget"; - - /** - * The bit in LOCK_BIOMETRIC_WEAK_FLAGS to be used to indicate whether liveliness should - * be used - */ - public static final int FLAG_BIOMETRIC_WEAK_LIVELINESS = 0x1; - - /** - * Pseudo-appwidget id we use to represent the default clock status widget - */ - public static final int ID_DEFAULT_STATUS_WIDGET = -2; - + @Deprecated public final static String LOCKOUT_PERMANENT_KEY = "lockscreen.lockedoutpermanently"; public final static String LOCKOUT_ATTEMPT_DEADLINE = "lockscreen.lockoutattemptdeadline"; public final static String PATTERN_EVER_CHOSEN_KEY = "lockscreen.patterneverchosen"; public final static String PASSWORD_TYPE_KEY = "lockscreen.password_type"; - public static final String PASSWORD_TYPE_ALTERNATE_KEY = "lockscreen.password_type_alternate"; + @Deprecated + public final static String PASSWORD_TYPE_ALTERNATE_KEY = "lockscreen.password_type_alternate"; public final static String LOCK_PASSWORD_SALT_KEY = "lockscreen.password_salt"; public final static String DISABLE_LOCKSCREEN_KEY = "lockscreen.disabled"; public final static String LOCKSCREEN_OPTIONS = "lockscreen.options"; + @Deprecated public final static String LOCKSCREEN_BIOMETRIC_WEAK_FALLBACK = "lockscreen.biometric_weak_fallback"; + @Deprecated public final static String BIOMETRIC_WEAK_EVER_CHOSEN_KEY = "lockscreen.biometricweakeverchosen"; public final static String LOCKSCREEN_POWER_BUTTON_INSTANTLY_LOCKS = "lockscreen.power_button_instantly_locks"; + @Deprecated public final static String LOCKSCREEN_WIDGETS_ENABLED = "lockscreen.widgets_enabled"; public final static String PASSWORD_HISTORY_KEY = "lockscreen.passwordhistory"; @@ -227,7 +203,11 @@ public class LockPatternUtils { } public int getRequestedPasswordHistoryLength() { - return getDevicePolicyManager().getPasswordHistoryLength(null, getCurrentOrCallingUserId()); + return getRequestedPasswordHistoryLength(getCurrentOrCallingUserId()); + } + + private int getRequestedPasswordHistoryLength(int userId) { + return getDevicePolicyManager().getPasswordHistoryLength(null, userId); } public int getRequestedPasswordMinimumLetters() { @@ -289,14 +269,6 @@ public class LockPatternUtils { } } - public void removeUser(int userId) { - try { - getLockSettings().removeUser(userId); - } catch (RemoteException re) { - Log.e(TAG, "Couldn't remove lock settings for user " + userId); - } - } - private int getCurrentOrCallingUserId() { if (mMultiUserMode) { // TODO: This is a little inefficient. See if all users of this are able to @@ -359,9 +331,10 @@ public class LockPatternUtils { * @return Whether the password matches any in the history. */ public boolean checkPasswordHistory(String password) { + int userId = getCurrentOrCallingUserId(); String passwordHashString = new String( - passwordToHash(password, getCurrentOrCallingUserId()), StandardCharsets.UTF_8); - String passwordHistory = getString(PASSWORD_HISTORY_KEY); + passwordToHash(password, userId), StandardCharsets.UTF_8); + String passwordHistory = getString(PASSWORD_HISTORY_KEY, userId); if (passwordHistory == null) { return false; } @@ -383,15 +356,7 @@ public class LockPatternUtils { * Check to see if the user has stored a lock pattern. * @return Whether a saved pattern exists. */ - public boolean savedPatternExists() { - return savedPatternExists(getCurrentOrCallingUserId()); - } - - /** - * Check to see if the user has stored a lock pattern. - * @return Whether a saved pattern exists. - */ - public boolean savedPatternExists(int userId) { + private boolean savedPatternExists(int userId) { try { return getLockSettings().havePattern(userId); } catch (RemoteException re) { @@ -403,15 +368,7 @@ public class LockPatternUtils { * Check to see if the user has stored a lock pattern. * @return Whether a saved pattern exists. */ - public boolean savedPasswordExists() { - return savedPasswordExists(getCurrentOrCallingUserId()); - } - - /** - * Check to see if the user has stored a lock pattern. - * @return Whether a saved pattern exists. - */ - public boolean savedPasswordExists(int userId) { + private boolean savedPasswordExists(int userId) { try { return getLockSettings().havePassword(userId); } catch (RemoteException re) { @@ -426,86 +383,62 @@ public class LockPatternUtils { * @return True if the user has ever chosen a pattern. */ public boolean isPatternEverChosen() { - return getBoolean(PATTERN_EVER_CHOSEN_KEY, false); + return getBoolean(PATTERN_EVER_CHOSEN_KEY, false, getCurrentOrCallingUserId()); } /** - * Return true if the user has ever chosen biometric weak. This is true even if biometric - * weak is not current set. - * - * @return True if the user has ever chosen biometric weak. + * Used by device policy manager to validate the current password + * information it has. */ - public boolean isBiometricWeakEverChosen() { - return getBoolean(BIOMETRIC_WEAK_EVER_CHOSEN_KEY, false); + public int getActivePasswordQuality() { + return getActivePasswordQuality(getCurrentOrCallingUserId()); } /** * Used by device policy manager to validate the current password * information it has. */ - public int getActivePasswordQuality() { - int activePasswordQuality = DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; - // Note we don't want to use getKeyguardStoredPasswordQuality() because we want this to - // return biometric_weak if that is being used instead of the backup - int quality = - (int) getLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_SOMETHING); - switch (quality) { - case DevicePolicyManager.PASSWORD_QUALITY_SOMETHING: - if (isLockPatternEnabled()) { - activePasswordQuality = DevicePolicyManager.PASSWORD_QUALITY_SOMETHING; - } - break; - case DevicePolicyManager.PASSWORD_QUALITY_BIOMETRIC_WEAK: - if (isBiometricWeakInstalled()) { - activePasswordQuality = DevicePolicyManager.PASSWORD_QUALITY_BIOMETRIC_WEAK; - } - break; - case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC: - if (isLockPasswordEnabled()) { - activePasswordQuality = DevicePolicyManager.PASSWORD_QUALITY_NUMERIC; - } - break; - case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX: - if (isLockPasswordEnabled()) { - activePasswordQuality = DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX; - } - break; - case DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC: - if (isLockPasswordEnabled()) { - activePasswordQuality = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC; - } - break; - case DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC: - if (isLockPasswordEnabled()) { - activePasswordQuality = DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC; - } - break; - case DevicePolicyManager.PASSWORD_QUALITY_COMPLEX: - if (isLockPasswordEnabled()) { - activePasswordQuality = DevicePolicyManager.PASSWORD_QUALITY_COMPLEX; - } - break; + public int getActivePasswordQuality(int userId) { + int quality = getKeyguardStoredPasswordQuality(userId); + + if (isLockPasswordEnabled(quality, userId)) { + // Quality is a password and a password exists. Return the quality. + return quality; } - return activePasswordQuality; + if (isLockPatternEnabled(quality, userId)) { + // Quality is a pattern and a pattern exists. Return the quality. + return quality; + } + + return DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; } - public void clearLock(boolean isFallback) { - clearLock(isFallback, getCurrentOrCallingUserId()); + public void clearLock() { + clearLock(getCurrentOrCallingUserId()); } /** * Clear any lock pattern or password. */ - public void clearLock(boolean isFallback, int userHandle) { - if(!isFallback) deleteGallery(userHandle); - saveLockPassword(null, DevicePolicyManager.PASSWORD_QUALITY_SOMETHING, isFallback, - userHandle); - setLockPatternEnabled(false, userHandle); - saveLockPattern(null, isFallback, userHandle); + public void clearLock(int userHandle) { setLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, userHandle); - setLong(PASSWORD_TYPE_ALTERNATE_KEY, DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, - userHandle); + + try { + getLockSettings().setLockPassword(null, userHandle); + getLockSettings().setLockPattern(null, userHandle); + } catch (RemoteException e) { + // well, we tried... + } + + if (userHandle == UserHandle.USER_OWNER) { + // Set the encryption password to default. + updateEncryptionPassword(StorageManager.CRYPT_TYPE_DEFAULT, null); + } + + getDevicePolicyManager().setActivePasswordState( + DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, 0, 0, 0, 0, 0, 0, 0, userHandle); + onAfterChangingPassword(userHandle); } @@ -516,7 +449,7 @@ public class LockPatternUtils { * @param disable Disables lock screen when true */ public void setLockScreenDisabled(boolean disable) { - setLong(DISABLE_LOCKSCREEN_KEY, disable ? 1 : 0); + setBoolean(DISABLE_LOCKSCREEN_KEY, disable, getCurrentOrCallingUserId()); } /** @@ -526,7 +459,7 @@ public class LockPatternUtils { * @return true if lock screen is can be disabled */ public boolean isLockScreenDisabled() { - if (!isSecure() && getLong(DISABLE_LOCKSCREEN_KEY, 0) != 0) { + if (!isSecure() && getBoolean(DISABLE_LOCKSCREEN_KEY, false, getCurrentOrCallingUserId())) { // Check if the number of switchable users forces the lockscreen. final List<UserInfo> users = UserManager.get(mContext).getUsers(true); final int userCount = users.size(); @@ -542,96 +475,57 @@ public class LockPatternUtils { } /** - * Calls back SetupFaceLock to delete the temporary gallery file - */ - public void deleteTempGallery() { - Intent intent = new Intent().setAction("com.android.facelock.DELETE_GALLERY"); - intent.putExtra("deleteTempGallery", true); - mContext.sendBroadcast(intent); - } - - /** - * Calls back SetupFaceLock to delete the gallery file when the lock type is changed - */ - void deleteGallery(int userId) { - if(usingBiometricWeak(userId)) { - Intent intent = new Intent().setAction("com.android.facelock.DELETE_GALLERY"); - intent.putExtra("deleteGallery", true); - mContext.sendBroadcastAsUser(intent, new UserHandle(userId)); - } - } - - /** * Save a lock pattern. * @param pattern The new pattern to save. */ public void saveLockPattern(List<LockPatternView.Cell> pattern) { - this.saveLockPattern(pattern, false); - } - - /** - * Save a lock pattern. - * @param pattern The new pattern to save. - */ - public void saveLockPattern(List<LockPatternView.Cell> pattern, boolean isFallback) { - this.saveLockPattern(pattern, isFallback, getCurrentOrCallingUserId()); + this.saveLockPattern(pattern, getCurrentOrCallingUserId()); } /** * Save a lock pattern. * @param pattern The new pattern to save. - * @param isFallback Specifies if this is a fallback to biometric weak * @param userId the user whose pattern is to be saved. */ - public void saveLockPattern(List<LockPatternView.Cell> pattern, boolean isFallback, - int userId) { + public void saveLockPattern(List<LockPatternView.Cell> pattern, int userId) { try { + if (pattern == null || pattern.size() < MIN_LOCK_PATTERN_SIZE) { + throw new IllegalArgumentException("pattern must not be null and at least " + + MIN_LOCK_PATTERN_SIZE + " dots long."); + } + getLockSettings().setLockPattern(patternToString(pattern), userId); DevicePolicyManager dpm = getDevicePolicyManager(); - if (pattern != null) { - // Update the device encryption password. - if (userId == UserHandle.USER_OWNER - && LockPatternUtils.isDeviceEncryptionEnabled()) { - final boolean required = isCredentialRequiredToDecrypt(true); - if (!required) { - clearEncryptionPassword(); - } else { - String stringPattern = patternToString(pattern); - updateEncryptionPassword(StorageManager.CRYPT_TYPE_PATTERN, stringPattern); - } - } - setBoolean(PATTERN_EVER_CHOSEN_KEY, true, userId); - if (!isFallback) { - deleteGallery(userId); - setLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_SOMETHING, userId); - dpm.setActivePasswordState(DevicePolicyManager.PASSWORD_QUALITY_SOMETHING, - pattern.size(), 0, 0, 0, 0, 0, 0, userId); + // Update the device encryption password. + if (userId == UserHandle.USER_OWNER + && LockPatternUtils.isDeviceEncryptionEnabled()) { + final boolean required = isCredentialRequiredToDecrypt(true); + if (!required) { + clearEncryptionPassword(); } else { - setLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_BIOMETRIC_WEAK, userId); - setLong(PASSWORD_TYPE_ALTERNATE_KEY, - DevicePolicyManager.PASSWORD_QUALITY_SOMETHING, userId); - finishBiometricWeak(userId); - dpm.setActivePasswordState(DevicePolicyManager.PASSWORD_QUALITY_BIOMETRIC_WEAK, - 0, 0, 0, 0, 0, 0, 0, userId); + String stringPattern = patternToString(pattern); + updateEncryptionPassword(StorageManager.CRYPT_TYPE_PATTERN, stringPattern); } - } else { - dpm.setActivePasswordState(DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, 0, 0, - 0, 0, 0, 0, 0, userId); } + + setBoolean(PATTERN_EVER_CHOSEN_KEY, true, userId); + + setLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_SOMETHING, userId); + dpm.setActivePasswordState(DevicePolicyManager.PASSWORD_QUALITY_SOMETHING, + pattern.size(), 0, 0, 0, 0, 0, 0, userId); onAfterChangingPassword(userId); } catch (RemoteException re) { Log.e(TAG, "Couldn't save lock pattern " + re); } } - private void updateCryptoUserInfo() { - int userId = getCurrentOrCallingUserId(); + private void updateCryptoUserInfo(int userId) { if (userId != UserHandle.USER_OWNER) { return; } - final String ownerInfo = isOwnerInfoEnabled() ? getOwnerInfo(userId) : ""; + final String ownerInfo = isOwnerInfoEnabled(userId) ? getOwnerInfo(userId) : ""; IBinder service = ServiceManager.getService("mount"); if (service == null) { @@ -650,20 +544,25 @@ public class LockPatternUtils { public void setOwnerInfo(String info, int userId) { setString(LOCK_SCREEN_OWNER_INFO, info, userId); - updateCryptoUserInfo(); + updateCryptoUserInfo(userId); } public void setOwnerInfoEnabled(boolean enabled) { - setBoolean(LOCK_SCREEN_OWNER_INFO_ENABLED, enabled); - updateCryptoUserInfo(); + int userId = getCurrentOrCallingUserId(); + setBoolean(LOCK_SCREEN_OWNER_INFO_ENABLED, enabled, userId); + updateCryptoUserInfo(userId); } public String getOwnerInfo(int userId) { - return getString(LOCK_SCREEN_OWNER_INFO); + return getString(LOCK_SCREEN_OWNER_INFO, userId); } public boolean isOwnerInfoEnabled() { - return getBoolean(LOCK_SCREEN_OWNER_INFO_ENABLED, false); + return isOwnerInfoEnabled(getCurrentOrCallingUserId()); + } + + private boolean isOwnerInfoEnabled(int userId) { + return getBoolean(LOCK_SCREEN_OWNER_INFO_ENABLED, false, userId); } /** @@ -789,19 +688,7 @@ public class LockPatternUtils { * @param quality {@see DevicePolicyManager#getPasswordQuality(android.content.ComponentName)} */ public void saveLockPassword(String password, int quality) { - this.saveLockPassword(password, quality, false, getCurrentOrCallingUserId()); - } - - /** - * Save a lock password. Does not ensure that the password is as good - * as the requested mode, but will adjust the mode to be as good as the - * pattern. - * @param password The password to save - * @param quality {@see DevicePolicyManager#getPasswordQuality(android.content.ComponentName)} - * @param isFallback Specifies if this is a fallback to biometric weak - */ - public void saveLockPassword(String password, int quality, boolean isFallback) { - saveLockPassword(password, quality, isFallback, getCurrentOrCallingUserId()); + saveLockPassword(password, quality, getCurrentOrCallingUserId()); } /** @@ -810,108 +697,88 @@ public class LockPatternUtils { * pattern. * @param password The password to save * @param quality {@see DevicePolicyManager#getPasswordQuality(android.content.ComponentName)} - * @param isFallback Specifies if this is a fallback to biometric weak * @param userHandle The userId of the user to change the password for */ - public void saveLockPassword(String password, int quality, boolean isFallback, int userHandle) { + public void saveLockPassword(String password, int quality, int userHandle) { try { DevicePolicyManager dpm = getDevicePolicyManager(); - if (!TextUtils.isEmpty(password)) { - getLockSettings().setLockPassword(password, userHandle); - int computedQuality = computePasswordQuality(password); - - // Update the device encryption password. - if (userHandle == UserHandle.USER_OWNER - && LockPatternUtils.isDeviceEncryptionEnabled()) { - if (!isCredentialRequiredToDecrypt(true)) { - clearEncryptionPassword(); - } else { - boolean numeric = computedQuality - == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC; - boolean numericComplex = computedQuality - == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX; - int type = numeric || numericComplex ? StorageManager.CRYPT_TYPE_PIN - : StorageManager.CRYPT_TYPE_PASSWORD; - updateEncryptionPassword(type, password); - } + if (password == null || password.length() < MIN_LOCK_PASSWORD_SIZE) { + throw new IllegalArgumentException("password must not be null and at least " + + "of length " + MIN_LOCK_PASSWORD_SIZE); + } + + getLockSettings().setLockPassword(password, userHandle); + int computedQuality = computePasswordQuality(password); + + // Update the device encryption password. + if (userHandle == UserHandle.USER_OWNER + && LockPatternUtils.isDeviceEncryptionEnabled()) { + if (!isCredentialRequiredToDecrypt(true)) { + clearEncryptionPassword(); + } else { + boolean numeric = computedQuality + == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC; + boolean numericComplex = computedQuality + == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX; + int type = numeric || numericComplex ? StorageManager.CRYPT_TYPE_PIN + : StorageManager.CRYPT_TYPE_PASSWORD; + updateEncryptionPassword(type, password); } + } - if (!isFallback) { - deleteGallery(userHandle); - setLong(PASSWORD_TYPE_KEY, Math.max(quality, computedQuality), userHandle); - if (computedQuality != DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED) { - int letters = 0; - int uppercase = 0; - int lowercase = 0; - int numbers = 0; - int symbols = 0; - int nonletter = 0; - for (int i = 0; i < password.length(); i++) { - char c = password.charAt(i); - if (c >= 'A' && c <= 'Z') { - letters++; - uppercase++; - } else if (c >= 'a' && c <= 'z') { - letters++; - lowercase++; - } else if (c >= '0' && c <= '9') { - numbers++; - nonletter++; - } else { - symbols++; - nonletter++; - } - } - dpm.setActivePasswordState(Math.max(quality, computedQuality), - password.length(), letters, uppercase, lowercase, - numbers, symbols, nonletter, userHandle); + setLong(PASSWORD_TYPE_KEY, Math.max(quality, computedQuality), userHandle); + if (computedQuality != DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED) { + int letters = 0; + int uppercase = 0; + int lowercase = 0; + int numbers = 0; + int symbols = 0; + int nonletter = 0; + for (int i = 0; i < password.length(); i++) { + char c = password.charAt(i); + if (c >= 'A' && c <= 'Z') { + letters++; + uppercase++; + } else if (c >= 'a' && c <= 'z') { + letters++; + lowercase++; + } else if (c >= '0' && c <= '9') { + numbers++; + nonletter++; } else { - // The password is not anything. - dpm.setActivePasswordState( - DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, - 0, 0, 0, 0, 0, 0, 0, userHandle); + symbols++; + nonletter++; } - } else { - // Case where it's a fallback for biometric weak - setLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_BIOMETRIC_WEAK, - userHandle); - setLong(PASSWORD_TYPE_ALTERNATE_KEY, Math.max(quality, computedQuality), - userHandle); - finishBiometricWeak(userHandle); - dpm.setActivePasswordState(DevicePolicyManager.PASSWORD_QUALITY_BIOMETRIC_WEAK, - 0, 0, 0, 0, 0, 0, 0, userHandle); } - // Add the password to the password history. We assume all - // password hashes have the same length for simplicity of implementation. - String passwordHistory = getString(PASSWORD_HISTORY_KEY, userHandle); - if (passwordHistory == null) { - passwordHistory = ""; - } - int passwordHistoryLength = getRequestedPasswordHistoryLength(); - if (passwordHistoryLength == 0) { - passwordHistory = ""; - } else { - byte[] hash = passwordToHash(password, userHandle); - passwordHistory = new String(hash, StandardCharsets.UTF_8) + "," + passwordHistory; - // Cut it to contain passwordHistoryLength hashes - // and passwordHistoryLength -1 commas. - passwordHistory = passwordHistory.substring(0, Math.min(hash.length - * passwordHistoryLength + passwordHistoryLength - 1, passwordHistory - .length())); - } - setString(PASSWORD_HISTORY_KEY, passwordHistory, userHandle); + dpm.setActivePasswordState(Math.max(quality, computedQuality), + password.length(), letters, uppercase, lowercase, + numbers, symbols, nonletter, userHandle); } else { - // Empty password - getLockSettings().setLockPassword(null, userHandle); - if (userHandle == UserHandle.USER_OWNER) { - // Set the encryption password to default. - updateEncryptionPassword(StorageManager.CRYPT_TYPE_DEFAULT, null); - } - + // The password is not anything. dpm.setActivePasswordState( - DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, 0, 0, 0, 0, 0, 0, 0, - userHandle); + DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, + 0, 0, 0, 0, 0, 0, 0, userHandle); } + + // Add the password to the password history. We assume all + // password hashes have the same length for simplicity of implementation. + String passwordHistory = getString(PASSWORD_HISTORY_KEY, userHandle); + if (passwordHistory == null) { + passwordHistory = ""; + } + int passwordHistoryLength = getRequestedPasswordHistoryLength(userHandle); + if (passwordHistoryLength == 0) { + passwordHistory = ""; + } else { + byte[] hash = passwordToHash(password, userHandle); + passwordHistory = new String(hash, StandardCharsets.UTF_8) + "," + passwordHistory; + // Cut it to contain passwordHistoryLength hashes + // and passwordHistoryLength -1 commas. + passwordHistory = passwordHistory.substring(0, Math.min(hash.length + * passwordHistoryLength + passwordHistoryLength - 1, passwordHistory + .length())); + } + setString(PASSWORD_HISTORY_KEY, passwordHistory, userHandle); onAfterChangingPassword(userHandle); } catch (RemoteException re) { // Cant do much @@ -971,31 +838,8 @@ public class LockPatternUtils { * @return stored password quality */ public int getKeyguardStoredPasswordQuality(int userHandle) { - int quality = (int) getLong(PASSWORD_TYPE_KEY, + return (int) getLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, userHandle); - // If the user has chosen to use weak biometric sensor, then return the backup locking - // method and treat biometric as a special case. - if (quality == DevicePolicyManager.PASSWORD_QUALITY_BIOMETRIC_WEAK) { - quality = (int) getLong(PASSWORD_TYPE_ALTERNATE_KEY, - DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, userHandle); - } - return quality; - } - - /** - * @return true if the lockscreen method is set to biometric weak - */ - public boolean usingBiometricWeak() { - return usingBiometricWeak(getCurrentOrCallingUserId()); - } - - /** - * @return true if the lockscreen method is set to biometric weak - */ - public boolean usingBiometricWeak(int userId) { - int quality = (int) getLong( - PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, userId); - return quality == DevicePolicyManager.PASSWORD_QUALITY_BIOMETRIC_WEAK; } /** @@ -1004,6 +848,10 @@ public class LockPatternUtils { * @return The pattern. */ public static List<LockPatternView.Cell> stringToPattern(String string) { + if (string == null) { + return null; + } + List<LockPatternView.Cell> result = Lists.newArrayList(); final byte[] bytes = string.getBytes(); @@ -1106,126 +954,73 @@ public class LockPatternUtils { } /** - * @return Whether the lock password is enabled, or if it is set as a backup for biometric weak + * @return Whether the lock screen is secured. */ - public boolean isLockPasswordEnabled() { - long mode = getLong(PASSWORD_TYPE_KEY, 0); - long backupMode = getLong(PASSWORD_TYPE_ALTERNATE_KEY, 0); - final boolean passwordEnabled = mode == DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC - || mode == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC - || mode == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX - || mode == DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC - || mode == DevicePolicyManager.PASSWORD_QUALITY_COMPLEX; - final boolean backupEnabled = backupMode == DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC - || backupMode == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC - || backupMode == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX - || backupMode == DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC - || backupMode == DevicePolicyManager.PASSWORD_QUALITY_COMPLEX; - - return savedPasswordExists() && (passwordEnabled || - (usingBiometricWeak() && backupEnabled)); + public boolean isSecure() { + return isSecure(getCurrentOrCallingUserId()); } /** - * @return Whether the lock pattern is enabled, or if it is set as a backup for biometric weak + * @param userId the user for which to report the value + * @return Whether the lock screen is secured. */ - public boolean isLockPatternEnabled() { - return isLockPatternEnabled(getCurrentOrCallingUserId()); + public boolean isSecure(int userId) { + int mode = getKeyguardStoredPasswordQuality(userId); + return isLockPatternEnabled(mode, userId) || isLockPasswordEnabled(mode, userId); } /** - * @return Whether the lock pattern is enabled, or if it is set as a backup for biometric weak + * @return Whether the lock password is enabled */ - public boolean isLockPatternEnabled(int userId) { - final boolean backupEnabled = - getLong(PASSWORD_TYPE_ALTERNATE_KEY, - DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, userId) - == DevicePolicyManager.PASSWORD_QUALITY_SOMETHING; - - return getBoolean(Settings.Secure.LOCK_PATTERN_ENABLED, false, userId) - && (getLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, - userId) == DevicePolicyManager.PASSWORD_QUALITY_SOMETHING - || (usingBiometricWeak(userId) && backupEnabled)); + public boolean isLockPasswordEnabled() { + return isLockPasswordEnabled(getCurrentOrCallingUserId()); } - /** - * @return Whether biometric weak lock is installed and that the front facing camera exists - */ - public boolean isBiometricWeakInstalled() { - // Check that it's installed - PackageManager pm = mContext.getPackageManager(); - try { - pm.getPackageInfo("com.android.facelock", PackageManager.GET_ACTIVITIES); - } catch (PackageManager.NameNotFoundException e) { - return false; - } - - // Check that the camera is enabled - if (!pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT)) { - return false; - } - if (getDevicePolicyManager().getCameraDisabled(null, getCurrentOrCallingUserId())) { - return false; - } - - // TODO: If we decide not to proceed with Face Unlock as a trustlet, this must be changed - // back to returning true. If we become certain that Face Unlock will be a trustlet, this - // entire function and a lot of other code can be removed. - if (DEBUG) Log.d(TAG, "Forcing isBiometricWeakInstalled() to return false to disable it"); - return false; + public boolean isLockPasswordEnabled(int userId) { + return isLockPasswordEnabled(getKeyguardStoredPasswordQuality(userId), userId); } - /** - * Set whether biometric weak liveliness is enabled. - */ - public void setBiometricWeakLivelinessEnabled(boolean enabled) { - long currentFlag = getLong(Settings.Secure.LOCK_BIOMETRIC_WEAK_FLAGS, 0L); - long newFlag; - if (enabled) { - newFlag = currentFlag | FLAG_BIOMETRIC_WEAK_LIVELINESS; - } else { - newFlag = currentFlag & ~FLAG_BIOMETRIC_WEAK_LIVELINESS; - } - setLong(Settings.Secure.LOCK_BIOMETRIC_WEAK_FLAGS, newFlag); + private boolean isLockPasswordEnabled(int mode, int userId) { + final boolean passwordEnabled = mode == DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC + || mode == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC + || mode == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX + || mode == DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC + || mode == DevicePolicyManager.PASSWORD_QUALITY_COMPLEX; + return passwordEnabled && savedPasswordExists(userId); } /** - * @return Whether the biometric weak liveliness is enabled. + * @return Whether the lock pattern is enabled */ - public boolean isBiometricWeakLivelinessEnabled() { - long currentFlag = getLong(Settings.Secure.LOCK_BIOMETRIC_WEAK_FLAGS, 0L); - return ((currentFlag & FLAG_BIOMETRIC_WEAK_LIVELINESS) != 0); + public boolean isLockPatternEnabled() { + return isLockPatternEnabled(getCurrentOrCallingUserId()); } - /** - * Set whether the lock pattern is enabled. - */ - public void setLockPatternEnabled(boolean enabled) { - setLockPatternEnabled(enabled, getCurrentOrCallingUserId()); + public boolean isLockPatternEnabled(int userId) { + return isLockPatternEnabled(getKeyguardStoredPasswordQuality(userId), userId); } - /** - * Set whether the lock pattern is enabled. - */ - public void setLockPatternEnabled(boolean enabled, int userHandle) { - setBoolean(Settings.Secure.LOCK_PATTERN_ENABLED, enabled, userHandle); + private boolean isLockPatternEnabled(int mode, int userId) { + return mode == DevicePolicyManager.PASSWORD_QUALITY_SOMETHING + && savedPatternExists(userId); } /** * @return Whether the visible pattern is enabled. */ public boolean isVisiblePatternEnabled() { - return getBoolean(Settings.Secure.LOCK_PATTERN_VISIBLE, false); + return getBoolean(Settings.Secure.LOCK_PATTERN_VISIBLE, false, getCurrentOrCallingUserId()); } /** * Set whether the visible pattern is enabled. */ public void setVisiblePatternEnabled(boolean enabled) { - setBoolean(Settings.Secure.LOCK_PATTERN_VISIBLE, enabled); + int userId = getCurrentOrCallingUserId(); + + setBoolean(Settings.Secure.LOCK_PATTERN_VISIBLE, enabled, userId); // Update for crypto if owner - int userId = getCurrentOrCallingUserId(); if (userId != UserHandle.USER_OWNER) { return; } @@ -1259,7 +1054,7 @@ public class LockPatternUtils { */ public long setLockoutAttemptDeadline() { final long deadline = SystemClock.elapsedRealtime() + FAILED_ATTEMPT_TIMEOUT_MS; - setLong(LOCKOUT_ATTEMPT_DEADLINE, deadline); + setLong(LOCKOUT_ATTEMPT_DEADLINE, deadline, getCurrentOrCallingUserId()); return deadline; } @@ -1269,7 +1064,7 @@ public class LockPatternUtils { * enter a pattern. */ public long getLockoutAttemptDeadline() { - final long deadline = getLong(LOCKOUT_ATTEMPT_DEADLINE, 0L); + final long deadline = getLong(LOCKOUT_ATTEMPT_DEADLINE, 0L, getCurrentOrCallingUserId()); final long now = SystemClock.elapsedRealtime(); if (deadline < now || deadline > (now + FAILED_ATTEMPT_TIMEOUT_MS)) { return 0L; @@ -1277,51 +1072,6 @@ public class LockPatternUtils { return deadline; } - /** - * @return Whether the user is permanently locked out until they verify their - * credentials. Occurs after {@link #FAILED_ATTEMPTS_BEFORE_RESET} failed - * attempts. - */ - public boolean isPermanentlyLocked() { - return getBoolean(LOCKOUT_PERMANENT_KEY, false); - } - - /** - * Set the state of whether the device is permanently locked, meaning the user - * must authenticate via other means. - * - * @param locked Whether the user is permanently locked out until they verify their - * credentials. Occurs after {@link #FAILED_ATTEMPTS_BEFORE_RESET} failed - * attempts. - */ - public void setPermanentlyLocked(boolean locked) { - setBoolean(LOCKOUT_PERMANENT_KEY, locked); - } - - public boolean isEmergencyCallCapable() { - return mContext.getResources().getBoolean( - com.android.internal.R.bool.config_voice_capable); - } - - public boolean isPukUnlockScreenEnable() { - return mContext.getResources().getBoolean( - com.android.internal.R.bool.config_enable_puk_unlock_screen); - } - - public boolean isEmergencyCallEnabledWhileSimLocked() { - return mContext.getResources().getBoolean( - com.android.internal.R.bool.config_enable_emergency_call_while_sim_locked); - } - - /** - * @return A formatted string of the next alarm (for showing on the lock screen), - * or null if there is no next alarm. - */ - public AlarmManager.AlarmClockInfo getNextAlarm() { - AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); - return alarmManager.getNextAlarmClock(UserHandle.USER_CURRENT); - } - private boolean getBoolean(String secureSettingKey, boolean defaultValue, int userId) { try { return getLockSettings().getBoolean(secureSettingKey, defaultValue, userId); @@ -1330,10 +1080,6 @@ public class LockPatternUtils { } } - private boolean getBoolean(String secureSettingKey, boolean defaultValue) { - return getBoolean(secureSettingKey, defaultValue, getCurrentOrCallingUserId()); - } - private void setBoolean(String secureSettingKey, boolean enabled, int userId) { try { getLockSettings().setBoolean(secureSettingKey, enabled, userId); @@ -1343,144 +1089,6 @@ public class LockPatternUtils { } } - private void setBoolean(String secureSettingKey, boolean enabled) { - setBoolean(secureSettingKey, enabled, getCurrentOrCallingUserId()); - } - - public int[] getAppWidgets() { - return getAppWidgets(UserHandle.USER_CURRENT); - } - - private int[] getAppWidgets(int userId) { - String appWidgetIdString = Settings.Secure.getStringForUser( - mContentResolver, Settings.Secure.LOCK_SCREEN_APPWIDGET_IDS, userId); - String delims = ","; - if (appWidgetIdString != null && appWidgetIdString.length() > 0) { - String[] appWidgetStringIds = appWidgetIdString.split(delims); - int[] appWidgetIds = new int[appWidgetStringIds.length]; - for (int i = 0; i < appWidgetStringIds.length; i++) { - String appWidget = appWidgetStringIds[i]; - try { - appWidgetIds[i] = Integer.decode(appWidget); - } catch (NumberFormatException e) { - Log.d(TAG, "Error when parsing widget id " + appWidget); - return null; - } - } - return appWidgetIds; - } - return new int[0]; - } - - private static String combineStrings(int[] list, String separator) { - int listLength = list.length; - - switch (listLength) { - case 0: { - return ""; - } - case 1: { - return Integer.toString(list[0]); - } - } - - int strLength = 0; - int separatorLength = separator.length(); - - String[] stringList = new String[list.length]; - for (int i = 0; i < listLength; i++) { - stringList[i] = Integer.toString(list[i]); - strLength += stringList[i].length(); - if (i < listLength - 1) { - strLength += separatorLength; - } - } - - StringBuilder sb = new StringBuilder(strLength); - - for (int i = 0; i < listLength; i++) { - sb.append(list[i]); - if (i < listLength - 1) { - sb.append(separator); - } - } - - return sb.toString(); - } - - // appwidget used when appwidgets are disabled (we make an exception for - // default clock widget) - public void writeFallbackAppWidgetId(int appWidgetId) { - Settings.Secure.putIntForUser(mContentResolver, - Settings.Secure.LOCK_SCREEN_FALLBACK_APPWIDGET_ID, - appWidgetId, - UserHandle.USER_CURRENT); - } - - // appwidget used when appwidgets are disabled (we make an exception for - // default clock widget) - public int getFallbackAppWidgetId() { - return Settings.Secure.getIntForUser( - mContentResolver, - Settings.Secure.LOCK_SCREEN_FALLBACK_APPWIDGET_ID, - AppWidgetManager.INVALID_APPWIDGET_ID, - UserHandle.USER_CURRENT); - } - - private void writeAppWidgets(int[] appWidgetIds) { - Settings.Secure.putStringForUser(mContentResolver, - Settings.Secure.LOCK_SCREEN_APPWIDGET_IDS, - combineStrings(appWidgetIds, ","), - UserHandle.USER_CURRENT); - } - - // TODO: log an error if this returns false - public boolean addAppWidget(int widgetId, int index) { - int[] widgets = getAppWidgets(); - if (widgets == null) { - return false; - } - if (index < 0 || index > widgets.length) { - return false; - } - int[] newWidgets = new int[widgets.length + 1]; - for (int i = 0, j = 0; i < newWidgets.length; i++) { - if (index == i) { - newWidgets[i] = widgetId; - i++; - } - if (i < newWidgets.length) { - newWidgets[i] = widgets[j]; - j++; - } - } - writeAppWidgets(newWidgets); - return true; - } - - public boolean removeAppWidget(int widgetId) { - int[] widgets = getAppWidgets(); - - if (widgets.length == 0) { - return false; - } - - int[] newWidgets = new int[widgets.length - 1]; - for (int i = 0, j = 0; i < widgets.length; i++) { - if (widgets[i] == widgetId) { - // continue... - } else if (j >= newWidgets.length) { - // we couldn't find the widget - return false; - } else { - newWidgets[j] = widgets[i]; - j++; - } - } - writeAppWidgets(newWidgets); - return true; - } - private long getLong(String secureSettingKey, long defaultValue, int userHandle) { try { return getLockSettings().getLong(secureSettingKey, defaultValue, userHandle); @@ -1489,19 +1097,6 @@ public class LockPatternUtils { } } - private long getLong(String secureSettingKey, long defaultValue) { - try { - return getLockSettings().getLong(secureSettingKey, defaultValue, - getCurrentOrCallingUserId()); - } catch (RemoteException re) { - return defaultValue; - } - } - - private void setLong(String secureSettingKey, long value) { - setLong(secureSettingKey, value, getCurrentOrCallingUserId()); - } - private void setLong(String secureSettingKey, long value, int userHandle) { try { getLockSettings().setLong(secureSettingKey, value, userHandle); @@ -1511,10 +1106,6 @@ public class LockPatternUtils { } } - private String getString(String secureSettingKey) { - return getString(secureSettingKey, getCurrentOrCallingUserId()); - } - private String getString(String secureSettingKey, int userHandle) { try { return getLockSettings().getString(secureSettingKey, null, userHandle); @@ -1532,134 +1123,13 @@ public class LockPatternUtils { } } - public boolean isSecure() { - return isSecure(getCurrentOrCallingUserId()); - } - - public boolean isSecure(int userId) { - long mode = getKeyguardStoredPasswordQuality(userId); - final boolean isPattern = mode == DevicePolicyManager.PASSWORD_QUALITY_SOMETHING; - final boolean isPassword = mode == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC - || mode == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX - || mode == DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC - || mode == DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC - || mode == DevicePolicyManager.PASSWORD_QUALITY_COMPLEX; - final boolean secure = - isPattern && isLockPatternEnabled(userId) && savedPatternExists(userId) - || isPassword && savedPasswordExists(userId); - return secure; - } - - /** - * Sets the emergency button visibility based on isEmergencyCallCapable(). - * - * If the emergency button is visible, sets the text on the emergency button - * to indicate what action will be taken. - * - * If there's currently a call in progress, the button will take them to the call - * @param button The button to update - * @param shown Indicates whether the given screen wants the emergency button to show at all - * @param showIcon Indicates whether to show a phone icon for the button. - */ - public void updateEmergencyCallButtonState(Button button, boolean shown, boolean showIcon) { - if (isEmergencyCallCapable() && shown) { - button.setVisibility(View.VISIBLE); - } else { - button.setVisibility(View.GONE); - return; - } - - int textId; - if (isInCall()) { - // show "return to call" text and show phone icon - textId = R.string.lockscreen_return_to_call; - int phoneCallIcon = showIcon ? R.drawable.stat_sys_phone_call : 0; - button.setCompoundDrawablesWithIntrinsicBounds(phoneCallIcon, 0, 0, 0); - } else { - textId = R.string.lockscreen_emergency_call; - int emergencyIcon = showIcon ? R.drawable.ic_emergency : 0; - button.setCompoundDrawablesWithIntrinsicBounds(emergencyIcon, 0, 0, 0); - } - button.setText(textId); - } - - /** - * Resumes a call in progress. Typically launched from the EmergencyCall button - * on various lockscreens. - */ - public void resumeCall() { - getTelecommManager().showInCallScreen(false); - } - - /** - * @return {@code true} if there is a call currently in progress, {@code false} otherwise. - */ - public boolean isInCall() { - return getTelecommManager().isInCall(); - } - - private TelecomManager getTelecommManager() { - return (TelecomManager) mContext.getSystemService(Context.TELECOM_SERVICE); - } - - private void finishBiometricWeak(int userId) { - setBoolean(BIOMETRIC_WEAK_EVER_CHOSEN_KEY, true, userId); - - // Launch intent to show final screen, this also - // moves the temporary gallery to the actual gallery - Intent intent = new Intent(); - intent.setClassName("com.android.facelock", - "com.android.facelock.SetupEndScreen"); - mContext.startActivityAsUser(intent, new UserHandle(userId)); - } - public void setPowerButtonInstantlyLocks(boolean enabled) { - setBoolean(LOCKSCREEN_POWER_BUTTON_INSTANTLY_LOCKS, enabled); + setBoolean(LOCKSCREEN_POWER_BUTTON_INSTANTLY_LOCKS, enabled, getCurrentOrCallingUserId()); } public boolean getPowerButtonInstantlyLocks() { - return getBoolean(LOCKSCREEN_POWER_BUTTON_INSTANTLY_LOCKS, true); - } - - public static boolean isSafeModeEnabled() { - try { - return IWindowManager.Stub.asInterface( - ServiceManager.getService("window")).isSafeModeEnabled(); - } catch (RemoteException e) { - // Shouldn't happen! - } - return false; - } - - /** - * Determine whether the user has selected any non-system widgets in keyguard - * - * @return true if widgets have been selected - */ - public boolean hasWidgetsEnabledInKeyguard(int userid) { - int widgets[] = getAppWidgets(userid); - for (int i = 0; i < widgets.length; i++) { - if (widgets[i] > 0) { - return true; - } - } - return false; - } - - public boolean getWidgetsEnabled() { - return getWidgetsEnabled(getCurrentOrCallingUserId()); - } - - public boolean getWidgetsEnabled(int userId) { - return getBoolean(LOCKSCREEN_WIDGETS_ENABLED, false, userId); - } - - public void setWidgetsEnabled(boolean enabled) { - setWidgetsEnabled(enabled, getCurrentOrCallingUserId()); - } - - public void setWidgetsEnabled(boolean enabled, int userId) { - setBoolean(LOCKSCREEN_WIDGETS_ENABLED, enabled, userId); + return getBoolean(LOCKSCREEN_POWER_BUTTON_INSTANTLY_LOCKS, true, + getCurrentOrCallingUserId()); } public void setEnabledTrustAgents(Collection<ComponentName> activeTrustAgents) { diff --git a/core/java/com/android/internal/widget/LockPatternView.java b/core/java/com/android/internal/widget/LockPatternView.java index 9fa6882..8be34e7 100644 --- a/core/java/com/android/internal/widget/LockPatternView.java +++ b/core/java/com/android/internal/widget/LockPatternView.java @@ -65,8 +65,8 @@ public class LockPatternView extends View { private boolean mDrawingProfilingStarted = false; - private Paint mPaint = new Paint(); - private Paint mPathPaint = new Paint(); + private final Paint mPaint = new Paint(); + private final Paint mPathPaint = new Paint(); /** * How many milliseconds we spend animating each circle of a lock pattern @@ -82,7 +82,7 @@ public class LockPatternView extends View { private static final float DRAG_THRESHHOLD = 0.0f; private OnPatternListener mOnPatternListener; - private ArrayList<Cell> mPattern = new ArrayList<Cell>(9); + private final ArrayList<Cell> mPattern = new ArrayList<Cell>(9); /** * Lookup table for the circles of the pattern we are currently drawing. @@ -90,7 +90,7 @@ public class LockPatternView extends View { * in which case we use this to hold the cells we are drawing for the in * progress animation. */ - private boolean[][] mPatternDrawLookup = new boolean[3][3]; + private final boolean[][] mPatternDrawLookup = new boolean[3][3]; /** * the in progress point: @@ -122,24 +122,27 @@ public class LockPatternView extends View { private int mErrorColor; private int mSuccessColor; - private Interpolator mFastOutSlowInInterpolator; - private Interpolator mLinearOutSlowInInterpolator; + private final Interpolator mFastOutSlowInInterpolator; + private final Interpolator mLinearOutSlowInInterpolator; /** * Represents a cell in the 3 X 3 matrix of the unlock pattern view. */ - public static class Cell { - int row; - int column; + public static final class Cell { + final int row; + final int column; // keep # objects limited to 9 - static Cell[][] sCells = new Cell[3][3]; - static { + private static final Cell[][] sCells = createCells(); + + private static Cell[][] createCells() { + Cell[][] res = new Cell[3][3]; for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { - sCells[i][j] = new Cell(i, j); + res[i][j] = new Cell(i, j); } } + return res; } /** @@ -160,11 +163,7 @@ public class LockPatternView extends View { return column; } - /** - * @param row The row of the cell. - * @param column The column of the cell. - */ - public static synchronized Cell of(int row, int column) { + public static Cell of(int row, int column) { checkRange(row, column); return sCells[row][column]; } @@ -178,6 +177,7 @@ public class LockPatternView extends View { } } + @Override public String toString() { return "(row=" + row + ",clmn=" + column + ")"; } @@ -269,9 +269,9 @@ public class LockPatternView extends View { mPathPaint.setAntiAlias(true); mPathPaint.setDither(true); - mRegularColor = getResources().getColor(R.color.lock_pattern_view_regular_color); - mErrorColor = getResources().getColor(R.color.lock_pattern_view_error_color); - mSuccessColor = getResources().getColor(R.color.lock_pattern_view_success_color); + mRegularColor = context.getColor(R.color.lock_pattern_view_regular_color); + mErrorColor = context.getColor(R.color.lock_pattern_view_error_color); + mSuccessColor = context.getColor(R.color.lock_pattern_view_success_color); mRegularColor = a.getColor(R.styleable.LockPatternView_regularColor, mRegularColor); mErrorColor = a.getColor(R.styleable.LockPatternView_errorColor, mErrorColor); mSuccessColor = a.getColor(R.styleable.LockPatternView_successColor, mSuccessColor); @@ -722,7 +722,7 @@ public class LockPatternView extends View { handleActionDown(event); return true; case MotionEvent.ACTION_UP: - handleActionUp(event); + handleActionUp(); return true; case MotionEvent.ACTION_MOVE: handleActionMove(event); @@ -812,7 +812,7 @@ public class LockPatternView extends View { announceForAccessibility(mContext.getString(resId)); } - private void handleActionUp(MotionEvent event) { + private void handleActionUp() { // report pattern detected if (!mPattern.isEmpty()) { mPatternInProgress = false; @@ -1119,12 +1119,15 @@ public class LockPatternView extends View { dest.writeValue(mTactileFeedbackEnabled); } + @SuppressWarnings({ "unused", "hiding" }) // Found using reflection public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() { + @Override public SavedState createFromParcel(Parcel in) { return new SavedState(in); } + @Override public SavedState[] newArray(int size) { return new SavedState[size]; } diff --git a/core/java/com/android/internal/widget/PagerAdapter.java b/core/java/com/android/internal/widget/PagerAdapter.java new file mode 100644 index 0000000..910a720 --- /dev/null +++ b/core/java/com/android/internal/widget/PagerAdapter.java @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget; + +import android.database.DataSetObservable; +import android.database.DataSetObserver; +import android.os.Parcelable; +import android.view.View; +import android.view.ViewGroup; + +/** + * Base class providing the adapter to populate pages inside of + * a {@link android.support.v4.view.ViewPager}. You will most likely want to use a more + * specific implementation of this, such as + * {@link android.support.v4.app.FragmentPagerAdapter} or + * {@link android.support.v4.app.FragmentStatePagerAdapter}. + * + * <p>When you implement a PagerAdapter, you must override the following methods + * at minimum:</p> + * <ul> + * <li>{@link #instantiateItem(android.view.ViewGroup, int)}</li> + * <li>{@link #destroyItem(android.view.ViewGroup, int, Object)}</li> + * <li>{@link #getCount()}</li> + * <li>{@link #isViewFromObject(android.view.View, Object)}</li> + * </ul> + * + * <p>PagerAdapter is more general than the adapters used for + * {@link android.widget.AdapterView AdapterViews}. Instead of providing a + * View recycling mechanism directly ViewPager uses callbacks to indicate the + * steps taken during an update. A PagerAdapter may implement a form of View + * recycling if desired or use a more sophisticated method of managing page + * Views such as Fragment transactions where each page is represented by its + * own Fragment.</p> + * + * <p>ViewPager associates each page with a key Object instead of working with + * Views directly. This key is used to track and uniquely identify a given page + * independent of its position in the adapter. A call to the PagerAdapter method + * {@link #startUpdate(android.view.ViewGroup)} indicates that the contents of the ViewPager + * are about to change. One or more calls to {@link #instantiateItem(android.view.ViewGroup, int)} + * and/or {@link #destroyItem(android.view.ViewGroup, int, Object)} will follow, and the end + * of an update will be signaled by a call to {@link #finishUpdate(android.view.ViewGroup)}. + * By the time {@link #finishUpdate(android.view.ViewGroup) finishUpdate} returns the views + * associated with the key objects returned by + * {@link #instantiateItem(android.view.ViewGroup, int) instantiateItem} should be added to + * the parent ViewGroup passed to these methods and the views associated with + * the keys passed to {@link #destroyItem(android.view.ViewGroup, int, Object) destroyItem} + * should be removed. The method {@link #isViewFromObject(android.view.View, Object)} identifies + * whether a page View is associated with a given key object.</p> + * + * <p>A very simple PagerAdapter may choose to use the page Views themselves + * as key objects, returning them from {@link #instantiateItem(android.view.ViewGroup, int)} + * after creation and adding them to the parent ViewGroup. A matching + * {@link #destroyItem(android.view.ViewGroup, int, Object)} implementation would remove the + * View from the parent ViewGroup and {@link #isViewFromObject(android.view.View, Object)} + * could be implemented as <code>return view == object;</code>.</p> + * + * <p>PagerAdapter supports data set changes. Data set changes must occur on the + * main thread and must end with a call to {@link #notifyDataSetChanged()} similar + * to AdapterView adapters derived from {@link android.widget.BaseAdapter}. A data + * set change may involve pages being added, removed, or changing position. The + * ViewPager will keep the current page active provided the adapter implements + * the method {@link #getItemPosition(Object)}.</p> + */ +public abstract class PagerAdapter { + private DataSetObservable mObservable = new DataSetObservable(); + + public static final int POSITION_UNCHANGED = -1; + public static final int POSITION_NONE = -2; + + /** + * Return the number of views available. + */ + public abstract int getCount(); + + /** + * Called when a change in the shown pages is going to start being made. + * @param container The containing View which is displaying this adapter's + * page views. + */ + public void startUpdate(ViewGroup container) { + startUpdate((View) container); + } + + /** + * Create the page for the given position. The adapter is responsible + * for adding the view to the container given here, although it only + * must ensure this is done by the time it returns from + * {@link #finishUpdate(android.view.ViewGroup)}. + * + * @param container The containing View in which the page will be shown. + * @param position The page position to be instantiated. + * @return Returns an Object representing the new page. This does not + * need to be a View, but can be some other container of the page. + */ + public Object instantiateItem(ViewGroup container, int position) { + return instantiateItem((View) container, position); + } + + /** + * Remove a page for the given position. The adapter is responsible + * for removing the view from its container, although it only must ensure + * this is done by the time it returns from {@link #finishUpdate(android.view.ViewGroup)}. + * + * @param container The containing View from which the page will be removed. + * @param position The page position to be removed. + * @param object The same object that was returned by + * {@link #instantiateItem(android.view.View, int)}. + */ + public void destroyItem(ViewGroup container, int position, Object object) { + destroyItem((View) container, position, object); + } + + /** + * Called to inform the adapter of which item is currently considered to + * be the "primary", that is the one show to the user as the current page. + * + * @param container The containing View from which the page will be removed. + * @param position The page position that is now the primary. + * @param object The same object that was returned by + * {@link #instantiateItem(android.view.View, int)}. + */ + public void setPrimaryItem(ViewGroup container, int position, Object object) { + setPrimaryItem((View) container, position, object); + } + + /** + * Called when the a change in the shown pages has been completed. At this + * point you must ensure that all of the pages have actually been added or + * removed from the container as appropriate. + * @param container The containing View which is displaying this adapter's + * page views. + */ + public void finishUpdate(ViewGroup container) { + finishUpdate((View) container); + } + + /** + * Called when a change in the shown pages is going to start being made. + * @param container The containing View which is displaying this adapter's + * page views. + * + * @deprecated Use {@link #startUpdate(android.view.ViewGroup)} + */ + public void startUpdate(View container) { + } + + /** + * Create the page for the given position. The adapter is responsible + * for adding the view to the container given here, although it only + * must ensure this is done by the time it returns from + * {@link #finishUpdate(android.view.ViewGroup)}. + * + * @param container The containing View in which the page will be shown. + * @param position The page position to be instantiated. + * @return Returns an Object representing the new page. This does not + * need to be a View, but can be some other container of the page. + * + * @deprecated Use {@link #instantiateItem(android.view.ViewGroup, int)} + */ + public Object instantiateItem(View container, int position) { + throw new UnsupportedOperationException( + "Required method instantiateItem was not overridden"); + } + + /** + * Remove a page for the given position. The adapter is responsible + * for removing the view from its container, although it only must ensure + * this is done by the time it returns from {@link #finishUpdate(android.view.View)}. + * + * @param container The containing View from which the page will be removed. + * @param position The page position to be removed. + * @param object The same object that was returned by + * {@link #instantiateItem(android.view.View, int)}. + * + * @deprecated Use {@link #destroyItem(android.view.ViewGroup, int, Object)} + */ + public void destroyItem(View container, int position, Object object) { + throw new UnsupportedOperationException("Required method destroyItem was not overridden"); + } + + /** + * Called to inform the adapter of which item is currently considered to + * be the "primary", that is the one show to the user as the current page. + * + * @param container The containing View from which the page will be removed. + * @param position The page position that is now the primary. + * @param object The same object that was returned by + * {@link #instantiateItem(android.view.View, int)}. + * + * @deprecated Use {@link #setPrimaryItem(android.view.ViewGroup, int, Object)} + */ + public void setPrimaryItem(View container, int position, Object object) { + } + + /** + * Called when the a change in the shown pages has been completed. At this + * point you must ensure that all of the pages have actually been added or + * removed from the container as appropriate. + * @param container The containing View which is displaying this adapter's + * page views. + * + * @deprecated Use {@link #finishUpdate(android.view.ViewGroup)} + */ + public void finishUpdate(View container) { + } + + /** + * Determines whether a page View is associated with a specific key object + * as returned by {@link #instantiateItem(android.view.ViewGroup, int)}. This method is + * required for a PagerAdapter to function properly. + * + * @param view Page View to check for association with <code>object</code> + * @param object Object to check for association with <code>view</code> + * @return true if <code>view</code> is associated with the key object <code>object</code> + */ + public abstract boolean isViewFromObject(View view, Object object); + + /** + * Save any instance state associated with this adapter and its pages that should be + * restored if the current UI state needs to be reconstructed. + * + * @return Saved state for this adapter + */ + public Parcelable saveState() { + return null; + } + + /** + * Restore any instance state associated with this adapter and its pages + * that was previously saved by {@link #saveState()}. + * + * @param state State previously saved by a call to {@link #saveState()} + * @param loader A ClassLoader that should be used to instantiate any restored objects + */ + public void restoreState(Parcelable state, ClassLoader loader) { + } + + /** + * Called when the host view is attempting to determine if an item's position + * has changed. Returns {@link #POSITION_UNCHANGED} if the position of the given + * item has not changed or {@link #POSITION_NONE} if the item is no longer present + * in the adapter. + * + * <p>The default implementation assumes that items will never + * change position and always returns {@link #POSITION_UNCHANGED}. + * + * @param object Object representing an item, previously returned by a call to + * {@link #instantiateItem(android.view.View, int)}. + * @return object's new position index from [0, {@link #getCount()}), + * {@link #POSITION_UNCHANGED} if the object's position has not changed, + * or {@link #POSITION_NONE} if the item is no longer present. + */ + public int getItemPosition(Object object) { + return POSITION_UNCHANGED; + } + + /** + * This method should be called by the application if the data backing this adapter has changed + * and associated views should update. + */ + public void notifyDataSetChanged() { + mObservable.notifyChanged(); + } + + /** + * Register an observer to receive callbacks related to the adapter's data changing. + * + * @param observer The {@link android.database.DataSetObserver} which will receive callbacks. + */ + public void registerDataSetObserver(DataSetObserver observer) { + mObservable.registerObserver(observer); + } + + /** + * Unregister an observer from callbacks related to the adapter's data changing. + * + * @param observer The {@link android.database.DataSetObserver} which will be unregistered. + */ + public void unregisterDataSetObserver(DataSetObserver observer) { + mObservable.unregisterObserver(observer); + } + + /** + * This method may be called by the ViewPager to obtain a title string + * to describe the specified page. This method may return null + * indicating no title for this page. The default implementation returns + * null. + * + * @param position The position of the title requested + * @return A title for the requested page + */ + public CharSequence getPageTitle(int position) { + return null; + } + + /** + * Returns the proportional width of a given page as a percentage of the + * ViewPager's measured width from (0.f-1.f] + * + * @param position The position of the page requested + * @return Proportional width for the given page position + */ + public float getPageWidth(int position) { + return 1.f; + } +} diff --git a/core/java/com/android/internal/widget/ResolverDrawerLayout.java b/core/java/com/android/internal/widget/ResolverDrawerLayout.java index 4e48454..01e835b 100644 --- a/core/java/com/android/internal/widget/ResolverDrawerLayout.java +++ b/core/java/com/android/internal/widget/ResolverDrawerLayout.java @@ -593,15 +593,13 @@ public class ResolverDrawerLayout extends ViewGroup { } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(ResolverDrawerLayout.class.getName()); + public CharSequence getAccessibilityClassName() { + return ResolverDrawerLayout.class.getName(); } @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(ResolverDrawerLayout.class.getName()); if (isEnabled()) { if (mCollapseOffset != 0) { info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); diff --git a/core/java/com/android/internal/widget/ScrollingTabContainerView.java b/core/java/com/android/internal/widget/ScrollingTabContainerView.java index d6bd1d6..ffd9b24 100644 --- a/core/java/com/android/internal/widget/ScrollingTabContainerView.java +++ b/core/java/com/android/internal/widget/ScrollingTabContainerView.java @@ -31,7 +31,6 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.DecelerateInterpolator; import android.widget.AdapterView; import android.widget.BaseAdapter; @@ -150,7 +149,9 @@ public class ScrollingTabContainerView extends HorizontalScrollView addView(mTabSpinner, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)); if (mTabSpinner.getAdapter() == null) { - mTabSpinner.setAdapter(new TabAdapter()); + final TabAdapter adapter = new TabAdapter(mContext); + adapter.setDropDownViewContext(mTabSpinner.getPopupContext()); + mTabSpinner.setAdapter(adapter); } if (mTabSelector != null) { removeCallbacks(mTabSelector); @@ -276,8 +277,8 @@ public class ScrollingTabContainerView extends HorizontalScrollView } } - private TabView createTabView(ActionBar.Tab tab, boolean forAdapter) { - final TabView tabView = new TabView(getContext(), tab, forAdapter); + private TabView createTabView(Context context, ActionBar.Tab tab, boolean forAdapter) { + final TabView tabView = new TabView(context, tab, forAdapter); if (forAdapter) { tabView.setBackgroundDrawable(null); tabView.setLayoutParams(new ListView.LayoutParams(ListView.LayoutParams.MATCH_PARENT, @@ -294,7 +295,7 @@ public class ScrollingTabContainerView extends HorizontalScrollView } public void addTab(ActionBar.Tab tab, boolean setSelected) { - TabView tabView = createTabView(tab, false); + TabView tabView = createTabView(mContext, tab, false); mTabLayout.addView(tabView, new LinearLayout.LayoutParams(0, LayoutParams.MATCH_PARENT, 1)); if (mTabSpinner != null) { @@ -309,7 +310,7 @@ public class ScrollingTabContainerView extends HorizontalScrollView } public void addTab(ActionBar.Tab tab, int position, boolean setSelected) { - final TabView tabView = createTabView(tab, false); + final TabView tabView = createTabView(mContext, tab, false); mTabLayout.addView(tabView, position, new LinearLayout.LayoutParams( 0, LayoutParams.MATCH_PARENT, 1)); if (mTabSpinner != null) { @@ -391,17 +392,9 @@ public class ScrollingTabContainerView extends HorizontalScrollView } @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); + public CharSequence getAccessibilityClassName() { // This view masquerades as an action bar tab. - event.setClassName(ActionBar.Tab.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - // This view masquerades as an action bar tab. - info.setClassName(ActionBar.Tab.class.getName()); + return ActionBar.Tab.class.getName(); } @Override @@ -514,6 +507,16 @@ public class ScrollingTabContainerView extends HorizontalScrollView } private class TabAdapter extends BaseAdapter { + private Context mDropDownContext; + + public TabAdapter(Context context) { + setDropDownViewContext(context); + } + + public void setDropDownViewContext(Context context) { + mDropDownContext = context; + } + @Override public int getCount() { return mTabLayout.getChildCount(); @@ -532,7 +535,18 @@ public class ScrollingTabContainerView extends HorizontalScrollView @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { - convertView = createTabView((ActionBar.Tab) getItem(position), true); + convertView = createTabView(mContext, (ActionBar.Tab) getItem(position), true); + } else { + ((TabView) convertView).bindTab((ActionBar.Tab) getItem(position)); + } + return convertView; + } + + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = createTabView(mDropDownContext, + (ActionBar.Tab) getItem(position), true); } else { ((TabView) convertView).bindTab((ActionBar.Tab) getItem(position)); } diff --git a/core/java/com/android/internal/widget/SizeAdaptiveLayout.java b/core/java/com/android/internal/widget/SizeAdaptiveLayout.java deleted file mode 100644 index 5f3c5f9..0000000 --- a/core/java/com/android/internal/widget/SizeAdaptiveLayout.java +++ /dev/null @@ -1,442 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.internal.widget; - -import java.lang.Math; - -import com.android.internal.R; - -import android.animation.Animator; -import android.animation.Animator.AnimatorListener; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.StateListDrawable; -import android.util.AttributeSet; -import android.util.Log; -import android.util.StateSet; -import android.view.View; -import android.view.ViewDebug; -import android.view.ViewGroup; -import android.widget.RemoteViews.RemoteView; - -/** - * A layout that switches between its children based on the requested layout height. - * Each child specifies its minimum and maximum valid height. Results are undefined - * if children specify overlapping ranges. A child may specify the maximum height - * as 'unbounded' to indicate that it is willing to be displayed arbitrarily tall. - * - * <p> - * See {@link SizeAdaptiveLayout.LayoutParams} for a full description of the - * layout parameters used by SizeAdaptiveLayout. - */ -@RemoteView -public class SizeAdaptiveLayout extends ViewGroup { - - private static final String TAG = "SizeAdaptiveLayout"; - private static final boolean DEBUG = false; - private static final boolean REPORT_BAD_BOUNDS = true; - private static final long CROSSFADE_TIME = 250; - - // TypedArray indices - private static final int MIN_VALID_HEIGHT = - R.styleable.SizeAdaptiveLayout_Layout_layout_minHeight; - private static final int MAX_VALID_HEIGHT = - R.styleable.SizeAdaptiveLayout_Layout_layout_maxHeight; - - // view state - private View mActiveChild; - private View mLastActive; - - // animation state - private AnimatorSet mTransitionAnimation; - private AnimatorListener mAnimatorListener; - private ObjectAnimator mFadePanel; - private ObjectAnimator mFadeView; - private int mCanceledAnimationCount; - private View mEnteringView; - private View mLeavingView; - // View used to hide larger views under smaller ones to create a uniform crossfade - private View mModestyPanel; - private int mModestyPanelTop; - - public SizeAdaptiveLayout(Context context) { - this(context, null); - } - - public SizeAdaptiveLayout(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public SizeAdaptiveLayout(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); - } - - public SizeAdaptiveLayout( - Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - initialize(); - } - - private void initialize() { - mModestyPanel = new View(getContext()); - // If the SizeAdaptiveLayout has a solid background, use it as a transition hint. - Drawable background = getBackground(); - if (background instanceof StateListDrawable) { - StateListDrawable sld = (StateListDrawable) background; - sld.setState(StateSet.WILD_CARD); - background = sld.getCurrent(); - } - if (background instanceof ColorDrawable) { - mModestyPanel.setBackgroundDrawable(background); - } - SizeAdaptiveLayout.LayoutParams layout = - new SizeAdaptiveLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT); - mModestyPanel.setLayoutParams(layout); - addView(mModestyPanel); - mFadePanel = ObjectAnimator.ofFloat(mModestyPanel, "alpha", 0f); - mFadeView = ObjectAnimator.ofFloat(null, "alpha", 0f); - mAnimatorListener = new BringToFrontOnEnd(); - mTransitionAnimation = new AnimatorSet(); - mTransitionAnimation.play(mFadeView).with(mFadePanel); - mTransitionAnimation.setDuration(CROSSFADE_TIME); - mTransitionAnimation.addListener(mAnimatorListener); - } - - /** - * Visible for testing - * @hide - */ - public Animator getTransitionAnimation() { - return mTransitionAnimation; - } - - /** - * Visible for testing - * @hide - */ - public View getModestyPanel() { - return mModestyPanel; - } - - @Override - public void onAttachedToWindow() { - mLastActive = null; - // make sure all views start off invisible. - for (int i = 0; i < getChildCount(); i++) { - getChildAt(i).setVisibility(View.GONE); - } - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - if (DEBUG) Log.d(TAG, this + " measure spec: " + - MeasureSpec.toString(heightMeasureSpec)); - View model = selectActiveChild(heightMeasureSpec); - if (model == null) { - setMeasuredDimension(0, 0); - return; - } - SizeAdaptiveLayout.LayoutParams lp = - (SizeAdaptiveLayout.LayoutParams) model.getLayoutParams(); - if (DEBUG) Log.d(TAG, "active min: " + lp.minHeight + " max: " + lp.maxHeight); - measureChild(model, widthMeasureSpec, heightMeasureSpec); - int childHeight = model.getMeasuredHeight(); - int childWidth = model.getMeasuredHeight(); - int childState = combineMeasuredStates(0, model.getMeasuredState()); - if (DEBUG) Log.d(TAG, "measured child at: " + childHeight); - int resolvedWidth = resolveSizeAndState(childWidth, widthMeasureSpec, childState); - int resolvedHeight = resolveSizeAndState(childHeight, heightMeasureSpec, childState); - if (DEBUG) Log.d(TAG, "resolved to: " + resolvedHeight); - int boundedHeight = clampSizeToBounds(resolvedHeight, model); - if (DEBUG) Log.d(TAG, "bounded to: " + boundedHeight); - setMeasuredDimension(resolvedWidth, boundedHeight); - } - - private int clampSizeToBounds(int measuredHeight, View child) { - SizeAdaptiveLayout.LayoutParams lp = - (SizeAdaptiveLayout.LayoutParams) child.getLayoutParams(); - int heightIn = View.MEASURED_SIZE_MASK & measuredHeight; - int height = Math.max(heightIn, lp.minHeight); - if (lp.maxHeight != SizeAdaptiveLayout.LayoutParams.UNBOUNDED) { - height = Math.min(height, lp.maxHeight); - } - - if (REPORT_BAD_BOUNDS && heightIn != height) { - Log.d(TAG, this + "child view " + child + " " + - "measured out of bounds at " + heightIn +"px " + - "clamped to " + height + "px"); - } - - return height; - } - - //TODO extend to width and height - private View selectActiveChild(int heightMeasureSpec) { - final int heightMode = MeasureSpec.getMode(heightMeasureSpec); - final int heightSize = MeasureSpec.getSize(heightMeasureSpec); - - View unboundedView = null; - View tallestView = null; - int tallestViewSize = 0; - View smallestView = null; - int smallestViewSize = Integer.MAX_VALUE; - for (int i = 0; i < getChildCount(); i++) { - View child = getChildAt(i); - if (child != mModestyPanel) { - SizeAdaptiveLayout.LayoutParams lp = - (SizeAdaptiveLayout.LayoutParams) child.getLayoutParams(); - if (DEBUG) Log.d(TAG, "looking at " + i + - " with min: " + lp.minHeight + - " max: " + lp.maxHeight); - if (lp.maxHeight == SizeAdaptiveLayout.LayoutParams.UNBOUNDED && - unboundedView == null) { - unboundedView = child; - } - if (lp.maxHeight > tallestViewSize) { - tallestViewSize = lp.maxHeight; - tallestView = child; - } - if (lp.minHeight < smallestViewSize) { - smallestViewSize = lp.minHeight; - smallestView = child; - } - if (heightMode != MeasureSpec.UNSPECIFIED && - heightSize >= lp.minHeight && heightSize <= lp.maxHeight) { - if (DEBUG) Log.d(TAG, " found exact match, finishing early"); - return child; - } - } - } - if (unboundedView != null) { - tallestView = unboundedView; - } - if (heightMode == MeasureSpec.UNSPECIFIED || heightSize > tallestViewSize) { - return tallestView; - } else { - return smallestView; - } - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - if (DEBUG) Log.d(TAG, this + " onlayout height: " + (bottom - top)); - mLastActive = mActiveChild; - int measureSpec = View.MeasureSpec.makeMeasureSpec(bottom - top, - View.MeasureSpec.EXACTLY); - mActiveChild = selectActiveChild(measureSpec); - if (mActiveChild == null) return; - - mActiveChild.setVisibility(View.VISIBLE); - - if (mLastActive != mActiveChild && mLastActive != null) { - if (DEBUG) Log.d(TAG, this + " changed children from: " + mLastActive + - " to: " + mActiveChild); - - mEnteringView = mActiveChild; - mLeavingView = mLastActive; - - mEnteringView.setAlpha(1f); - - mModestyPanel.setAlpha(1f); - mModestyPanel.bringToFront(); - mModestyPanelTop = mLeavingView.getHeight(); - mModestyPanel.setVisibility(View.VISIBLE); - // TODO: mModestyPanel background should be compatible with mLeavingView - - mLeavingView.bringToFront(); - - if (mTransitionAnimation.isRunning()) { - mTransitionAnimation.cancel(); - } - mFadeView.setTarget(mLeavingView); - mFadeView.setFloatValues(0f); - mFadePanel.setFloatValues(0f); - mTransitionAnimation.setupStartValues(); - mTransitionAnimation.start(); - } - final int childWidth = mActiveChild.getMeasuredWidth(); - final int childHeight = mActiveChild.getMeasuredHeight(); - // TODO investigate setting LAYER_TYPE_HARDWARE on mLastActive - mActiveChild.layout(0, 0, childWidth, childHeight); - - if (DEBUG) Log.d(TAG, "got modesty offset of " + mModestyPanelTop); - mModestyPanel.layout(0, mModestyPanelTop, childWidth, mModestyPanelTop + childHeight); - } - - @Override - public LayoutParams generateLayoutParams(AttributeSet attrs) { - if (DEBUG) Log.d(TAG, "generate layout from attrs"); - return new SizeAdaptiveLayout.LayoutParams(getContext(), attrs); - } - - @Override - protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { - if (DEBUG) Log.d(TAG, "generate default layout from viewgroup"); - return new SizeAdaptiveLayout.LayoutParams(p); - } - - @Override - protected LayoutParams generateDefaultLayoutParams() { - if (DEBUG) Log.d(TAG, "generate default layout from null"); - return new SizeAdaptiveLayout.LayoutParams(); - } - - @Override - protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { - return p instanceof SizeAdaptiveLayout.LayoutParams; - } - - /** - * Per-child layout information associated with ViewSizeAdaptiveLayout. - * - * TODO extend to width and height - * - * @attr ref android.R.styleable#SizeAdaptiveLayout_Layout_layout_minHeight - * @attr ref android.R.styleable#SizeAdaptiveLayout_Layout_layout_maxHeight - */ - public static class LayoutParams extends ViewGroup.LayoutParams { - - /** - * Indicates the minimum valid height for the child. - */ - @ViewDebug.ExportedProperty(category = "layout") - public int minHeight; - - /** - * Indicates the maximum valid height for the child. - */ - @ViewDebug.ExportedProperty(category = "layout") - public int maxHeight; - - /** - * Constant value for maxHeight that indicates there is not maximum height. - */ - public static final int UNBOUNDED = -1; - - /** - * {@inheritDoc} - */ - public LayoutParams(Context c, AttributeSet attrs) { - super(c, attrs); - if (DEBUG) { - Log.d(TAG, "construct layout from attrs"); - for (int i = 0; i < attrs.getAttributeCount(); i++) { - Log.d(TAG, " " + attrs.getAttributeName(i) + " = " + - attrs.getAttributeValue(i)); - } - } - TypedArray a = - c.obtainStyledAttributes(attrs, - R.styleable.SizeAdaptiveLayout_Layout); - - minHeight = a.getDimensionPixelSize(MIN_VALID_HEIGHT, 0); - if (DEBUG) Log.d(TAG, "got minHeight of: " + minHeight); - - try { - maxHeight = a.getLayoutDimension(MAX_VALID_HEIGHT, UNBOUNDED); - if (DEBUG) Log.d(TAG, "got maxHeight of: " + maxHeight); - } catch (Exception e) { - if (DEBUG) Log.d(TAG, "caught exception looking for maxValidHeight " + e); - } - - a.recycle(); - } - - /** - * Creates a new set of layout parameters with the specified width, height - * and valid height bounds. - * - * @param width the width, either {@link #MATCH_PARENT}, - * {@link #WRAP_CONTENT} or a fixed size in pixels - * @param height the height, either {@link #MATCH_PARENT}, - * {@link #WRAP_CONTENT} or a fixed size in pixels - * @param minHeight the minimum height of this child - * @param maxHeight the maximum height of this child - * or {@link #UNBOUNDED} if the child can grow forever - */ - public LayoutParams(int width, int height, int minHeight, int maxHeight) { - super(width, height); - this.minHeight = minHeight; - this.maxHeight = maxHeight; - } - - /** - * {@inheritDoc} - */ - public LayoutParams(int width, int height) { - this(width, height, UNBOUNDED, UNBOUNDED); - } - - /** - * Constructs a new LayoutParams with default values as defined in {@link LayoutParams}. - */ - public LayoutParams() { - this(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); - } - - /** - * {@inheritDoc} - */ - public LayoutParams(ViewGroup.LayoutParams p) { - super(p); - minHeight = UNBOUNDED; - maxHeight = UNBOUNDED; - } - - public String debug(String output) { - return output + "SizeAdaptiveLayout.LayoutParams={" + - ", max=" + maxHeight + - ", max=" + minHeight + "}"; - } - } - - class BringToFrontOnEnd implements AnimatorListener { - @Override - public void onAnimationEnd(Animator animation) { - if (mCanceledAnimationCount == 0) { - mLeavingView.setVisibility(View.GONE); - mModestyPanel.setVisibility(View.GONE); - mEnteringView.bringToFront(); - mEnteringView = null; - mLeavingView = null; - } else { - mCanceledAnimationCount--; - } - } - - @Override - public void onAnimationCancel(Animator animation) { - mCanceledAnimationCount++; - } - - @Override - public void onAnimationRepeat(Animator animation) { - if (DEBUG) Log.d(TAG, "fade animation repeated: should never happen."); - assert(false); - } - - @Override - public void onAnimationStart(Animator animation) { - } - } -} diff --git a/core/java/com/android/internal/widget/SubtitleView.java b/core/java/com/android/internal/widget/SubtitleView.java index a206e7f..8c395ec 100644 --- a/core/java/com/android/internal/widget/SubtitleView.java +++ b/core/java/com/android/internal/widget/SubtitleView.java @@ -31,7 +31,6 @@ import android.text.Layout.Alignment; import android.text.StaticLayout; import android.text.TextPaint; import android.util.AttributeSet; -import android.util.DisplayMetrics; import android.view.View; import android.view.accessibility.CaptioningManager.CaptionStyle; diff --git a/core/java/com/android/internal/widget/ToolbarWidgetWrapper.java b/core/java/com/android/internal/widget/ToolbarWidgetWrapper.java index 8d1f73a..54df87b 100644 --- a/core/java/com/android/internal/widget/ToolbarWidgetWrapper.java +++ b/core/java/com/android/internal/widget/ToolbarWidgetWrapper.java @@ -27,8 +27,6 @@ import android.os.Parcelable; import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; -import android.util.TypedValue; -import android.view.ContextThemeWrapper; import android.view.Gravity; import android.view.LayoutInflater; import android.view.Menu; diff --git a/core/java/com/android/internal/widget/ViewPager.java b/core/java/com/android/internal/widget/ViewPager.java new file mode 100644 index 0000000..f916e6f --- /dev/null +++ b/core/java/com/android/internal/widget/ViewPager.java @@ -0,0 +1,2866 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget; + +import android.annotation.DrawableRes; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.util.Log; +import android.view.FocusFinder; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.SoundEffectConstants; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityRecord; +import android.view.animation.Interpolator; +import android.widget.EdgeEffect; +import android.widget.Scroller; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; + +/** + * Layout manager that allows the user to flip left and right + * through pages of data. You supply an implementation of a + * {@link android.support.v4.view.PagerAdapter} to generate the pages that the view shows. + * + * <p>Note this class is currently under early design and + * development. The API will likely change in later updates of + * the compatibility library, requiring changes to the source code + * of apps when they are compiled against the newer version.</p> + * + * <p>ViewPager is most often used in conjunction with {@link android.app.Fragment}, + * which is a convenient way to supply and manage the lifecycle of each page. + * There are standard adapters implemented for using fragments with the ViewPager, + * which cover the most common use cases. These are + * {@link android.support.v4.app.FragmentPagerAdapter} and + * {@link android.support.v4.app.FragmentStatePagerAdapter}; each of these + * classes have simple code showing how to build a full user interface + * with them. + * + * <p>For more information about how to use ViewPager, read <a + * href="{@docRoot}training/implementing-navigation/lateral.html">Creating Swipe Views with + * Tabs</a>.</p> + * + * <p>Below is a more complicated example of ViewPager, using it in conjunction + * with {@link android.app.ActionBar} tabs. You can find other examples of using + * ViewPager in the API 4+ Support Demos and API 13+ Support Demos sample code. + * + * {@sample development/samples/Support13Demos/src/com/example/android/supportv13/app/ActionBarTabsPager.java + * complete} + */ +public class ViewPager extends ViewGroup { + private static final String TAG = "ViewPager"; + private static final boolean DEBUG = false; + + private static final boolean USE_CACHE = false; + + private static final int DEFAULT_OFFSCREEN_PAGES = 1; + private static final int MAX_SETTLE_DURATION = 600; // ms + private static final int MIN_DISTANCE_FOR_FLING = 25; // dips + + private static final int DEFAULT_GUTTER_SIZE = 16; // dips + + private static final int MIN_FLING_VELOCITY = 400; // dips + + private static final int[] LAYOUT_ATTRS = new int[] { + com.android.internal.R.attr.layout_gravity + }; + + /** + * Used to track what the expected number of items in the adapter should be. + * If the app changes this when we don't expect it, we'll throw a big obnoxious exception. + */ + private int mExpectedAdapterCount; + + static class ItemInfo { + Object object; + int position; + boolean scrolling; + float widthFactor; + float offset; + } + + private static final Comparator<ItemInfo> COMPARATOR = new Comparator<ItemInfo>(){ + @Override + public int compare(ItemInfo lhs, ItemInfo rhs) { + return lhs.position - rhs.position; + } + }; + + private static final Interpolator sInterpolator = new Interpolator() { + public float getInterpolation(float t) { + t -= 1.0f; + return t * t * t * t * t + 1.0f; + } + }; + + private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>(); + private final ItemInfo mTempItem = new ItemInfo(); + + private final Rect mTempRect = new Rect(); + + private PagerAdapter mAdapter; + private int mCurItem; // Index of currently displayed page. + private int mRestoredCurItem = -1; + private Parcelable mRestoredAdapterState = null; + private ClassLoader mRestoredClassLoader = null; + private Scroller mScroller; + private PagerObserver mObserver; + + private int mPageMargin; + private Drawable mMarginDrawable; + private int mTopPageBounds; + private int mBottomPageBounds; + + // Offsets of the first and last items, if known. + // Set during population, used to determine if we are at the beginning + // or end of the pager data set during touch scrolling. + private float mFirstOffset = -Float.MAX_VALUE; + private float mLastOffset = Float.MAX_VALUE; + + private int mChildWidthMeasureSpec; + private int mChildHeightMeasureSpec; + private boolean mInLayout; + + private boolean mScrollingCacheEnabled; + + private boolean mPopulatePending; + private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES; + + private boolean mIsBeingDragged; + private boolean mIsUnableToDrag; + private int mDefaultGutterSize; + private int mGutterSize; + private int mTouchSlop; + /** + * Position of the last motion event. + */ + private float mLastMotionX; + private float mLastMotionY; + private float mInitialMotionX; + private float mInitialMotionY; + /** + * ID of the active pointer. This is used to retain consistency during + * drags/flings if multiple pointers are used. + */ + private int mActivePointerId = INVALID_POINTER; + /** + * Sentinel value for no current active pointer. + * Used by {@link #mActivePointerId}. + */ + private static final int INVALID_POINTER = -1; + + /** + * Determines speed during touch scrolling + */ + private VelocityTracker mVelocityTracker; + private int mMinimumVelocity; + private int mMaximumVelocity; + private int mFlingDistance; + private int mCloseEnough; + + // If the pager is at least this close to its final position, complete the scroll + // on touch down and let the user interact with the content inside instead of + // "catching" the flinging pager. + private static final int CLOSE_ENOUGH = 2; // dp + + private boolean mFakeDragging; + private long mFakeDragBeginTime; + + private EdgeEffect mLeftEdge; + private EdgeEffect mRightEdge; + + private boolean mFirstLayout = true; + private boolean mNeedCalculatePageOffsets = false; + private boolean mCalledSuper; + private int mDecorChildCount; + + private OnPageChangeListener mOnPageChangeListener; + private OnPageChangeListener mInternalPageChangeListener; + private OnAdapterChangeListener mAdapterChangeListener; + private PageTransformer mPageTransformer; + + private static final int DRAW_ORDER_DEFAULT = 0; + private static final int DRAW_ORDER_FORWARD = 1; + private static final int DRAW_ORDER_REVERSE = 2; + private int mDrawingOrder; + private ArrayList<View> mDrawingOrderedChildren; + private static final ViewPositionComparator sPositionComparator = new ViewPositionComparator(); + + /** + * Indicates that the pager is in an idle, settled state. The current page + * is fully in view and no animation is in progress. + */ + public static final int SCROLL_STATE_IDLE = 0; + + /** + * Indicates that the pager is currently being dragged by the user. + */ + public static final int SCROLL_STATE_DRAGGING = 1; + + /** + * Indicates that the pager is in the process of settling to a final position. + */ + public static final int SCROLL_STATE_SETTLING = 2; + + private final Runnable mEndScrollRunnable = new Runnable() { + public void run() { + setScrollState(SCROLL_STATE_IDLE); + populate(); + } + }; + + private int mScrollState = SCROLL_STATE_IDLE; + + /** + * Callback interface for responding to changing state of the selected page. + */ + public interface OnPageChangeListener { + + /** + * This method will be invoked when the current page is scrolled, either as part + * of a programmatically initiated smooth scroll or a user initiated touch scroll. + * + * @param position Position index of the first page currently being displayed. + * Page position+1 will be visible if positionOffset is nonzero. + * @param positionOffset Value from [0, 1) indicating the offset from the page at position. + * @param positionOffsetPixels Value in pixels indicating the offset from position. + */ + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels); + + /** + * This method will be invoked when a new page becomes selected. Animation is not + * necessarily complete. + * + * @param position Position index of the new selected page. + */ + public void onPageSelected(int position); + + /** + * Called when the scroll state changes. Useful for discovering when the user + * begins dragging, when the pager is automatically settling to the current page, + * or when it is fully stopped/idle. + * + * @param state The new scroll state. + * @see com.android.internal.widget.ViewPager#SCROLL_STATE_IDLE + * @see com.android.internal.widget.ViewPager#SCROLL_STATE_DRAGGING + * @see com.android.internal.widget.ViewPager#SCROLL_STATE_SETTLING + */ + public void onPageScrollStateChanged(int state); + } + + /** + * Simple implementation of the {@link OnPageChangeListener} interface with stub + * implementations of each method. Extend this if you do not intend to override + * every method of {@link OnPageChangeListener}. + */ + public static class SimpleOnPageChangeListener implements OnPageChangeListener { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + // This space for rent + } + + @Override + public void onPageSelected(int position) { + // This space for rent + } + + @Override + public void onPageScrollStateChanged(int state) { + // This space for rent + } + } + + /** + * A PageTransformer is invoked whenever a visible/attached page is scrolled. + * This offers an opportunity for the application to apply a custom transformation + * to the page views using animation properties. + * + * <p>As property animation is only supported as of Android 3.0 and forward, + * setting a PageTransformer on a ViewPager on earlier platform versions will + * be ignored.</p> + */ + public interface PageTransformer { + /** + * Apply a property transformation to the given page. + * + * @param page Apply the transformation to this page + * @param position Position of page relative to the current front-and-center + * position of the pager. 0 is front and center. 1 is one full + * page position to the right, and -1 is one page position to the left. + */ + public void transformPage(View page, float position); + } + + /** + * Used internally to monitor when adapters are switched. + */ + interface OnAdapterChangeListener { + public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter); + } + + /** + * Used internally to tag special types of child views that should be added as + * pager decorations by default. + */ + interface Decor {} + + public ViewPager(Context context) { + super(context); + initViewPager(); + } + + public ViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + initViewPager(); + } + + void initViewPager() { + setWillNotDraw(false); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + setFocusable(true); + final Context context = getContext(); + mScroller = new Scroller(context, sInterpolator); + final ViewConfiguration configuration = ViewConfiguration.get(context); + final float density = context.getResources().getDisplayMetrics().density; + + mTouchSlop = configuration.getScaledPagingTouchSlop(); + mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + mLeftEdge = new EdgeEffect(context); + mRightEdge = new EdgeEffect(context); + + mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density); + mCloseEnough = (int) (CLOSE_ENOUGH * density); + mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density); + + setAccessibilityDelegate(new MyAccessibilityDelegate()); + + if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } + } + + @Override + protected void onDetachedFromWindow() { + removeCallbacks(mEndScrollRunnable); + super.onDetachedFromWindow(); + } + + private void setScrollState(int newState) { + if (mScrollState == newState) { + return; + } + + mScrollState = newState; + if (mPageTransformer != null) { + // PageTransformers can do complex things that benefit from hardware layers. + enableLayers(newState != SCROLL_STATE_IDLE); + } + if (mOnPageChangeListener != null) { + mOnPageChangeListener.onPageScrollStateChanged(newState); + } + } + + /** + * Set a PagerAdapter that will supply views for this pager as needed. + * + * @param adapter Adapter to use + */ + public void setAdapter(PagerAdapter adapter) { + if (mAdapter != null) { + mAdapter.unregisterDataSetObserver(mObserver); + mAdapter.startUpdate(this); + for (int i = 0; i < mItems.size(); i++) { + final ItemInfo ii = mItems.get(i); + mAdapter.destroyItem(this, ii.position, ii.object); + } + mAdapter.finishUpdate(this); + mItems.clear(); + removeNonDecorViews(); + mCurItem = 0; + scrollTo(0, 0); + } + + final PagerAdapter oldAdapter = mAdapter; + mAdapter = adapter; + mExpectedAdapterCount = 0; + + if (mAdapter != null) { + if (mObserver == null) { + mObserver = new PagerObserver(); + } + mAdapter.registerDataSetObserver(mObserver); + mPopulatePending = false; + final boolean wasFirstLayout = mFirstLayout; + mFirstLayout = true; + mExpectedAdapterCount = mAdapter.getCount(); + if (mRestoredCurItem >= 0) { + mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader); + setCurrentItemInternal(mRestoredCurItem, false, true); + mRestoredCurItem = -1; + mRestoredAdapterState = null; + mRestoredClassLoader = null; + } else if (!wasFirstLayout) { + populate(); + } else { + requestLayout(); + } + } + + if (mAdapterChangeListener != null && oldAdapter != adapter) { + mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter); + } + } + + private void removeNonDecorViews() { + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.isDecor) { + removeViewAt(i); + i--; + } + } + } + + /** + * Retrieve the current adapter supplying pages. + * + * @return The currently registered PagerAdapter + */ + public PagerAdapter getAdapter() { + return mAdapter; + } + + void setOnAdapterChangeListener(OnAdapterChangeListener listener) { + mAdapterChangeListener = listener; + } + + private int getClientWidth() { + return getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); + } + + /** + * Set the currently selected page. If the ViewPager has already been through its first + * layout with its current adapter there will be a smooth animated transition between + * the current item and the specified item. + * + * @param item Item index to select + */ + public void setCurrentItem(int item) { + mPopulatePending = false; + setCurrentItemInternal(item, !mFirstLayout, false); + } + + /** + * Set the currently selected page. + * + * @param item Item index to select + * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately + */ + public void setCurrentItem(int item, boolean smoothScroll) { + mPopulatePending = false; + setCurrentItemInternal(item, smoothScroll, false); + } + + public int getCurrentItem() { + return mCurItem; + } + + void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) { + setCurrentItemInternal(item, smoothScroll, always, 0); + } + + void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) { + if (mAdapter == null || mAdapter.getCount() <= 0) { + setScrollingCacheEnabled(false); + return; + } + if (!always && mCurItem == item && mItems.size() != 0) { + setScrollingCacheEnabled(false); + return; + } + + if (item < 0) { + item = 0; + } else if (item >= mAdapter.getCount()) { + item = mAdapter.getCount() - 1; + } + final int pageLimit = mOffscreenPageLimit; + if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) { + // We are doing a jump by more than one page. To avoid + // glitches, we want to keep all current pages in the view + // until the scroll ends. + for (int i=0; i<mItems.size(); i++) { + mItems.get(i).scrolling = true; + } + } + final boolean dispatchSelected = mCurItem != item; + + if (mFirstLayout) { + // We don't have any idea how big we are yet and shouldn't have any pages either. + // Just set things up and let the pending layout handle things. + mCurItem = item; + if (dispatchSelected && mOnPageChangeListener != null) { + mOnPageChangeListener.onPageSelected(item); + } + if (dispatchSelected && mInternalPageChangeListener != null) { + mInternalPageChangeListener.onPageSelected(item); + } + requestLayout(); + } else { + populate(item); + scrollToItem(item, smoothScroll, velocity, dispatchSelected); + } + } + + private void scrollToItem(int item, boolean smoothScroll, int velocity, + boolean dispatchSelected) { + final ItemInfo curInfo = infoForPosition(item); + int destX = 0; + if (curInfo != null) { + final int width = getClientWidth(); + destX = (int) (width * Math.max(mFirstOffset, + Math.min(curInfo.offset, mLastOffset))); + } + if (smoothScroll) { + smoothScrollTo(destX, 0, velocity); + if (dispatchSelected && mOnPageChangeListener != null) { + mOnPageChangeListener.onPageSelected(item); + } + if (dispatchSelected && mInternalPageChangeListener != null) { + mInternalPageChangeListener.onPageSelected(item); + } + } else { + if (dispatchSelected && mOnPageChangeListener != null) { + mOnPageChangeListener.onPageSelected(item); + } + if (dispatchSelected && mInternalPageChangeListener != null) { + mInternalPageChangeListener.onPageSelected(item); + } + completeScroll(false); + scrollTo(destX, 0); + pageScrolled(destX); + } + } + + /** + * Set a listener that will be invoked whenever the page changes or is incrementally + * scrolled. See {@link OnPageChangeListener}. + * + * @param listener Listener to set + */ + public void setOnPageChangeListener(OnPageChangeListener listener) { + mOnPageChangeListener = listener; + } + + /** + * Set a {@link PageTransformer} that will be called for each attached page whenever + * the scroll position is changed. This allows the application to apply custom property + * transformations to each page, overriding the default sliding look and feel. + * + * <p><em>Note:</em> Prior to Android 3.0 the property animation APIs did not exist. + * As a result, setting a PageTransformer prior to Android 3.0 (API 11) will have no effect.</p> + * + * @param reverseDrawingOrder true if the supplied PageTransformer requires page views + * to be drawn from last to first instead of first to last. + * @param transformer PageTransformer that will modify each page's animation properties + */ + public void setPageTransformer(boolean reverseDrawingOrder, PageTransformer transformer) { + final boolean hasTransformer = transformer != null; + final boolean needsPopulate = hasTransformer != (mPageTransformer != null); + mPageTransformer = transformer; + setChildrenDrawingOrderEnabled(hasTransformer); + if (hasTransformer) { + mDrawingOrder = reverseDrawingOrder ? DRAW_ORDER_REVERSE : DRAW_ORDER_FORWARD; + } else { + mDrawingOrder = DRAW_ORDER_DEFAULT; + } + if (needsPopulate) populate(); + } + + @Override + protected int getChildDrawingOrder(int childCount, int i) { + final int index = mDrawingOrder == DRAW_ORDER_REVERSE ? childCount - 1 - i : i; + final int result = ((LayoutParams) mDrawingOrderedChildren.get(index).getLayoutParams()).childIndex; + return result; + } + + /** + * Set a separate OnPageChangeListener for internal use by the support library. + * + * @param listener Listener to set + * @return The old listener that was set, if any. + */ + OnPageChangeListener setInternalPageChangeListener(OnPageChangeListener listener) { + OnPageChangeListener oldListener = mInternalPageChangeListener; + mInternalPageChangeListener = listener; + return oldListener; + } + + /** + * Returns the number of pages that will be retained to either side of the + * current page in the view hierarchy in an idle state. Defaults to 1. + * + * @return How many pages will be kept offscreen on either side + * @see #setOffscreenPageLimit(int) + */ + public int getOffscreenPageLimit() { + return mOffscreenPageLimit; + } + + /** + * Set the number of pages that should be retained to either side of the + * current page in the view hierarchy in an idle state. Pages beyond this + * limit will be recreated from the adapter when needed. + * + * <p>This is offered as an optimization. If you know in advance the number + * of pages you will need to support or have lazy-loading mechanisms in place + * on your pages, tweaking this setting can have benefits in perceived smoothness + * of paging animations and interaction. If you have a small number of pages (3-4) + * that you can keep active all at once, less time will be spent in layout for + * newly created view subtrees as the user pages back and forth.</p> + * + * <p>You should keep this limit low, especially if your pages have complex layouts. + * This setting defaults to 1.</p> + * + * @param limit How many pages will be kept offscreen in an idle state. + */ + public void setOffscreenPageLimit(int limit) { + if (limit < DEFAULT_OFFSCREEN_PAGES) { + Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " + + DEFAULT_OFFSCREEN_PAGES); + limit = DEFAULT_OFFSCREEN_PAGES; + } + if (limit != mOffscreenPageLimit) { + mOffscreenPageLimit = limit; + populate(); + } + } + + /** + * Set the margin between pages. + * + * @param marginPixels Distance between adjacent pages in pixels + * @see #getPageMargin() + * @see #setPageMarginDrawable(android.graphics.drawable.Drawable) + * @see #setPageMarginDrawable(int) + */ + public void setPageMargin(int marginPixels) { + final int oldMargin = mPageMargin; + mPageMargin = marginPixels; + + final int width = getWidth(); + recomputeScrollPosition(width, width, marginPixels, oldMargin); + + requestLayout(); + } + + /** + * Return the margin between pages. + * + * @return The size of the margin in pixels + */ + public int getPageMargin() { + return mPageMargin; + } + + /** + * Set a drawable that will be used to fill the margin between pages. + * + * @param d Drawable to display between pages + */ + public void setPageMarginDrawable(Drawable d) { + mMarginDrawable = d; + if (d != null) refreshDrawableState(); + setWillNotDraw(d == null); + invalidate(); + } + + /** + * Set a drawable that will be used to fill the margin between pages. + * + * @param resId Resource ID of a drawable to display between pages + */ + public void setPageMarginDrawable(@DrawableRes int resId) { + setPageMarginDrawable(getContext().getDrawable(resId)); + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return super.verifyDrawable(who) || who == mMarginDrawable; + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + final Drawable d = mMarginDrawable; + if (d != null && d.isStateful()) { + d.setState(getDrawableState()); + } + } + + // We want the duration of the page snap animation to be influenced by the distance that + // the screen has to travel, however, we don't want this duration to be effected in a + // purely linear fashion. Instead, we use this method to moderate the effect that the distance + // of travel has on the overall snap duration. + float distanceInfluenceForSnapDuration(float f) { + f -= 0.5f; // center the values about 0. + f *= 0.3f * Math.PI / 2.0f; + return (float) Math.sin(f); + } + + /** + * Like {@link android.view.View#scrollBy}, but scroll smoothly instead of immediately. + * + * @param x the number of pixels to scroll by on the X axis + * @param y the number of pixels to scroll by on the Y axis + */ + void smoothScrollTo(int x, int y) { + smoothScrollTo(x, y, 0); + } + + /** + * Like {@link android.view.View#scrollBy}, but scroll smoothly instead of immediately. + * + * @param x the number of pixels to scroll by on the X axis + * @param y the number of pixels to scroll by on the Y axis + * @param velocity the velocity associated with a fling, if applicable. (0 otherwise) + */ + void smoothScrollTo(int x, int y, int velocity) { + if (getChildCount() == 0) { + // Nothing to do. + setScrollingCacheEnabled(false); + return; + } + int sx = getScrollX(); + int sy = getScrollY(); + int dx = x - sx; + int dy = y - sy; + if (dx == 0 && dy == 0) { + completeScroll(false); + populate(); + setScrollState(SCROLL_STATE_IDLE); + return; + } + + setScrollingCacheEnabled(true); + setScrollState(SCROLL_STATE_SETTLING); + + final int width = getClientWidth(); + final int halfWidth = width / 2; + final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width); + final float distance = halfWidth + halfWidth * + distanceInfluenceForSnapDuration(distanceRatio); + + int duration = 0; + velocity = Math.abs(velocity); + if (velocity > 0) { + duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); + } else { + final float pageWidth = width * mAdapter.getPageWidth(mCurItem); + final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin); + duration = (int) ((pageDelta + 1) * 100); + } + duration = Math.min(duration, MAX_SETTLE_DURATION); + + mScroller.startScroll(sx, sy, dx, dy, duration); + postInvalidateOnAnimation(); + } + + ItemInfo addNewItem(int position, int index) { + ItemInfo ii = new ItemInfo(); + ii.position = position; + ii.object = mAdapter.instantiateItem(this, position); + ii.widthFactor = mAdapter.getPageWidth(position); + if (index < 0 || index >= mItems.size()) { + mItems.add(ii); + } else { + mItems.add(index, ii); + } + return ii; + } + + void dataSetChanged() { + // This method only gets called if our observer is attached, so mAdapter is non-null. + + final int adapterCount = mAdapter.getCount(); + mExpectedAdapterCount = adapterCount; + boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 && + mItems.size() < adapterCount; + int newCurrItem = mCurItem; + + boolean isUpdating = false; + for (int i = 0; i < mItems.size(); i++) { + final ItemInfo ii = mItems.get(i); + final int newPos = mAdapter.getItemPosition(ii.object); + + if (newPos == PagerAdapter.POSITION_UNCHANGED) { + continue; + } + + if (newPos == PagerAdapter.POSITION_NONE) { + mItems.remove(i); + i--; + + if (!isUpdating) { + mAdapter.startUpdate(this); + isUpdating = true; + } + + mAdapter.destroyItem(this, ii.position, ii.object); + needPopulate = true; + + if (mCurItem == ii.position) { + // Keep the current item in the valid range + newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1)); + needPopulate = true; + } + continue; + } + + if (ii.position != newPos) { + if (ii.position == mCurItem) { + // Our current item changed position. Follow it. + newCurrItem = newPos; + } + + ii.position = newPos; + needPopulate = true; + } + } + + if (isUpdating) { + mAdapter.finishUpdate(this); + } + + Collections.sort(mItems, COMPARATOR); + + if (needPopulate) { + // Reset our known page widths; populate will recompute them. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.isDecor) { + lp.widthFactor = 0.f; + } + } + + setCurrentItemInternal(newCurrItem, false, true); + requestLayout(); + } + } + + void populate() { + populate(mCurItem); + } + + void populate(int newCurrentItem) { + ItemInfo oldCurInfo = null; + int focusDirection = View.FOCUS_FORWARD; + if (mCurItem != newCurrentItem) { + focusDirection = mCurItem < newCurrentItem ? View.FOCUS_RIGHT : View.FOCUS_LEFT; + oldCurInfo = infoForPosition(mCurItem); + mCurItem = newCurrentItem; + } + + if (mAdapter == null) { + sortChildDrawingOrder(); + return; + } + + // Bail now if we are waiting to populate. This is to hold off + // on creating views from the time the user releases their finger to + // fling to a new position until we have finished the scroll to + // that position, avoiding glitches from happening at that point. + if (mPopulatePending) { + if (DEBUG) Log.i(TAG, "populate is pending, skipping for now..."); + sortChildDrawingOrder(); + return; + } + + // Also, don't populate until we are attached to a window. This is to + // avoid trying to populate before we have restored our view hierarchy + // state and conflicting with what is restored. + if (getWindowToken() == null) { + return; + } + + mAdapter.startUpdate(this); + + final int pageLimit = mOffscreenPageLimit; + final int startPos = Math.max(0, mCurItem - pageLimit); + final int N = mAdapter.getCount(); + final int endPos = Math.min(N-1, mCurItem + pageLimit); + + if (N != mExpectedAdapterCount) { + String resName; + try { + resName = getResources().getResourceName(getId()); + } catch (Resources.NotFoundException e) { + resName = Integer.toHexString(getId()); + } + throw new IllegalStateException("The application's PagerAdapter changed the adapter's" + + " contents without calling PagerAdapter#notifyDataSetChanged!" + + " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N + + " Pager id: " + resName + + " Pager class: " + getClass() + + " Problematic adapter: " + mAdapter.getClass()); + } + + // Locate the currently focused item or add it if needed. + int curIndex = -1; + ItemInfo curItem = null; + for (curIndex = 0; curIndex < mItems.size(); curIndex++) { + final ItemInfo ii = mItems.get(curIndex); + if (ii.position >= mCurItem) { + if (ii.position == mCurItem) curItem = ii; + break; + } + } + + if (curItem == null && N > 0) { + curItem = addNewItem(mCurItem, curIndex); + } + + // Fill 3x the available width or up to the number of offscreen + // pages requested to either side, whichever is larger. + // If we have no current item we have no work to do. + if (curItem != null) { + float extraWidthLeft = 0.f; + int itemIndex = curIndex - 1; + ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + final int clientWidth = getClientWidth(); + final float leftWidthNeeded = clientWidth <= 0 ? 0 : + 2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth; + for (int pos = mCurItem - 1; pos >= 0; pos--) { + if (extraWidthLeft >= leftWidthNeeded && pos < startPos) { + if (ii == null) { + break; + } + if (pos == ii.position && !ii.scrolling) { + mItems.remove(itemIndex); + mAdapter.destroyItem(this, pos, ii.object); + if (DEBUG) { + Log.i(TAG, "populate() - destroyItem() with pos: " + pos + + " view: " + ((View) ii.object)); + } + itemIndex--; + curIndex--; + ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + } + } else if (ii != null && pos == ii.position) { + extraWidthLeft += ii.widthFactor; + itemIndex--; + ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + } else { + ii = addNewItem(pos, itemIndex + 1); + extraWidthLeft += ii.widthFactor; + curIndex++; + ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + } + } + + float extraWidthRight = curItem.widthFactor; + itemIndex = curIndex + 1; + if (extraWidthRight < 2.f) { + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + final float rightWidthNeeded = clientWidth <= 0 ? 0 : + (float) getPaddingRight() / (float) clientWidth + 2.f; + for (int pos = mCurItem + 1; pos < N; pos++) { + if (extraWidthRight >= rightWidthNeeded && pos > endPos) { + if (ii == null) { + break; + } + if (pos == ii.position && !ii.scrolling) { + mItems.remove(itemIndex); + mAdapter.destroyItem(this, pos, ii.object); + if (DEBUG) { + Log.i(TAG, "populate() - destroyItem() with pos: " + pos + + " view: " + ((View) ii.object)); + } + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + } + } else if (ii != null && pos == ii.position) { + extraWidthRight += ii.widthFactor; + itemIndex++; + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + } else { + ii = addNewItem(pos, itemIndex); + itemIndex++; + extraWidthRight += ii.widthFactor; + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + } + } + } + + calculatePageOffsets(curItem, curIndex, oldCurInfo); + } + + if (DEBUG) { + Log.i(TAG, "Current page list:"); + for (int i=0; i<mItems.size(); i++) { + Log.i(TAG, "#" + i + ": page " + mItems.get(i).position); + } + } + + mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null); + + mAdapter.finishUpdate(this); + + // Check width measurement of current pages and drawing sort order. + // Update LayoutParams as needed. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + lp.childIndex = i; + if (!lp.isDecor && lp.widthFactor == 0.f) { + // 0 means requery the adapter for this, it doesn't have a valid width. + final ItemInfo ii = infoForChild(child); + if (ii != null) { + lp.widthFactor = ii.widthFactor; + lp.position = ii.position; + } + } + } + sortChildDrawingOrder(); + + if (hasFocus()) { + View currentFocused = findFocus(); + ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null; + if (ii == null || ii.position != mCurItem) { + for (int i=0; i<getChildCount(); i++) { + View child = getChildAt(i); + ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + if (child.requestFocus(focusDirection)) { + break; + } + } + } + } + } + } + + private void sortChildDrawingOrder() { + if (mDrawingOrder != DRAW_ORDER_DEFAULT) { + if (mDrawingOrderedChildren == null) { + mDrawingOrderedChildren = new ArrayList<View>(); + } else { + mDrawingOrderedChildren.clear(); + } + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + mDrawingOrderedChildren.add(child); + } + Collections.sort(mDrawingOrderedChildren, sPositionComparator); + } + } + + private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) { + final int N = mAdapter.getCount(); + final int width = getClientWidth(); + final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; + // Fix up offsets for later layout. + if (oldCurInfo != null) { + final int oldCurPosition = oldCurInfo.position; + // Base offsets off of oldCurInfo. + if (oldCurPosition < curItem.position) { + int itemIndex = 0; + ItemInfo ii = null; + float offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset; + for (int pos = oldCurPosition + 1; + pos <= curItem.position && itemIndex < mItems.size(); pos++) { + ii = mItems.get(itemIndex); + while (pos > ii.position && itemIndex < mItems.size() - 1) { + itemIndex++; + ii = mItems.get(itemIndex); + } + while (pos < ii.position) { + // We don't have an item populated for this, + // ask the adapter for an offset. + offset += mAdapter.getPageWidth(pos) + marginOffset; + pos++; + } + ii.offset = offset; + offset += ii.widthFactor + marginOffset; + } + } else if (oldCurPosition > curItem.position) { + int itemIndex = mItems.size() - 1; + ItemInfo ii = null; + float offset = oldCurInfo.offset; + for (int pos = oldCurPosition - 1; + pos >= curItem.position && itemIndex >= 0; pos--) { + ii = mItems.get(itemIndex); + while (pos < ii.position && itemIndex > 0) { + itemIndex--; + ii = mItems.get(itemIndex); + } + while (pos > ii.position) { + // We don't have an item populated for this, + // ask the adapter for an offset. + offset -= mAdapter.getPageWidth(pos) + marginOffset; + pos--; + } + offset -= ii.widthFactor + marginOffset; + ii.offset = offset; + } + } + } + + // Base all offsets off of curItem. + final int itemCount = mItems.size(); + float offset = curItem.offset; + int pos = curItem.position - 1; + mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE; + mLastOffset = curItem.position == N - 1 ? + curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE; + // Previous pages + for (int i = curIndex - 1; i >= 0; i--, pos--) { + final ItemInfo ii = mItems.get(i); + while (pos > ii.position) { + offset -= mAdapter.getPageWidth(pos--) + marginOffset; + } + offset -= ii.widthFactor + marginOffset; + ii.offset = offset; + if (ii.position == 0) mFirstOffset = offset; + } + offset = curItem.offset + curItem.widthFactor + marginOffset; + pos = curItem.position + 1; + // Next pages + for (int i = curIndex + 1; i < itemCount; i++, pos++) { + final ItemInfo ii = mItems.get(i); + while (pos < ii.position) { + offset += mAdapter.getPageWidth(pos++) + marginOffset; + } + if (ii.position == N - 1) { + mLastOffset = offset + ii.widthFactor - 1; + } + ii.offset = offset; + offset += ii.widthFactor + marginOffset; + } + + mNeedCalculatePageOffsets = false; + } + + /** + * This is the persistent state that is saved by ViewPager. Only needed + * if you are creating a sublass of ViewPager that must save its own + * state, in which case it should implement a subclass of this which + * contains that state. + */ + public static class SavedState extends BaseSavedState { + int position; + Parcelable adapterState; + ClassLoader loader; + + public SavedState(Parcel source) { + super(source); + } + + public SavedState(Parcelable superState) { + super(superState); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(position); + out.writeParcelable(adapterState, flags); + } + + @Override + public String toString() { + return "FragmentPager.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " position=" + position + "}"; + } + + public static final Creator<SavedState> CREATOR = new Creator<SavedState>() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + + SavedState(Parcel in, ClassLoader loader) { + super(in); + if (loader == null) { + loader = getClass().getClassLoader(); + } + position = in.readInt(); + adapterState = in.readParcelable(loader); + this.loader = loader; + } + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.position = mCurItem; + if (mAdapter != null) { + ss.adapterState = mAdapter.saveState(); + } + return ss; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (!(state instanceof SavedState)) { + super.onRestoreInstanceState(state); + return; + } + + SavedState ss = (SavedState)state; + super.onRestoreInstanceState(ss.getSuperState()); + + if (mAdapter != null) { + mAdapter.restoreState(ss.adapterState, ss.loader); + setCurrentItemInternal(ss.position, false, true); + } else { + mRestoredCurItem = ss.position; + mRestoredAdapterState = ss.adapterState; + mRestoredClassLoader = ss.loader; + } + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + if (!checkLayoutParams(params)) { + params = generateLayoutParams(params); + } + final LayoutParams lp = (LayoutParams) params; + lp.isDecor |= child instanceof Decor; + if (mInLayout) { + if (lp != null && lp.isDecor) { + throw new IllegalStateException("Cannot add pager decor view during layout"); + } + lp.needsMeasure = true; + addViewInLayout(child, index, params); + } else { + super.addView(child, index, params); + } + + if (USE_CACHE) { + if (child.getVisibility() != GONE) { + child.setDrawingCacheEnabled(mScrollingCacheEnabled); + } else { + child.setDrawingCacheEnabled(false); + } + } + } + + @Override + public void removeView(View view) { + if (mInLayout) { + removeViewInLayout(view); + } else { + super.removeView(view); + } + } + + ItemInfo infoForChild(View child) { + for (int i=0; i<mItems.size(); i++) { + ItemInfo ii = mItems.get(i); + if (mAdapter.isViewFromObject(child, ii.object)) { + return ii; + } + } + return null; + } + + ItemInfo infoForAnyChild(View child) { + ViewParent parent; + while ((parent=child.getParent()) != this) { + if (parent == null || !(parent instanceof View)) { + return null; + } + child = (View)parent; + } + return infoForChild(child); + } + + ItemInfo infoForPosition(int position) { + for (int i = 0; i < mItems.size(); i++) { + ItemInfo ii = mItems.get(i); + if (ii.position == position) { + return ii; + } + } + return null; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mFirstLayout = true; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // For simple implementation, our internal size is always 0. + // We depend on the container to specify the layout size of + // our view. We can't really know what it is since we will be + // adding and removing different arbitrary views and do not + // want the layout to change as this happens. + setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), + getDefaultSize(0, heightMeasureSpec)); + + final int measuredWidth = getMeasuredWidth(); + final int maxGutterSize = measuredWidth / 10; + mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize); + + // Children are just made to fill our space. + int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight(); + int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); + + /* + * Make sure all children have been properly measured. Decor views first. + * Right now we cheat and make this less complicated by assuming decor + * views won't intersect. We will pin to edges based on gravity. + */ + int size = getChildCount(); + for (int i = 0; i < size; ++i) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp != null && lp.isDecor) { + final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; + final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; + int widthMode = MeasureSpec.AT_MOST; + int heightMode = MeasureSpec.AT_MOST; + boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM; + boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT; + + if (consumeVertical) { + widthMode = MeasureSpec.EXACTLY; + } else if (consumeHorizontal) { + heightMode = MeasureSpec.EXACTLY; + } + + int widthSize = childWidthSize; + int heightSize = childHeightSize; + if (lp.width != LayoutParams.WRAP_CONTENT) { + widthMode = MeasureSpec.EXACTLY; + if (lp.width != LayoutParams.FILL_PARENT) { + widthSize = lp.width; + } + } + if (lp.height != LayoutParams.WRAP_CONTENT) { + heightMode = MeasureSpec.EXACTLY; + if (lp.height != LayoutParams.FILL_PARENT) { + heightSize = lp.height; + } + } + final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode); + final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode); + child.measure(widthSpec, heightSpec); + + if (consumeVertical) { + childHeightSize -= child.getMeasuredHeight(); + } else if (consumeHorizontal) { + childWidthSize -= child.getMeasuredWidth(); + } + } + } + } + + mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY); + mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY); + + // Make sure we have created all fragments that we need to have shown. + mInLayout = true; + populate(); + mInLayout = false; + + // Page views next. + size = getChildCount(); + for (int i = 0; i < size; ++i) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child + + ": " + mChildWidthMeasureSpec); + + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp == null || !lp.isDecor) { + final int widthSpec = MeasureSpec.makeMeasureSpec( + (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY); + child.measure(widthSpec, mChildHeightMeasureSpec); + } + } + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + // Make sure scroll position is set correctly. + if (w != oldw) { + recomputeScrollPosition(w, oldw, mPageMargin, mPageMargin); + } + } + + private void recomputeScrollPosition(int width, int oldWidth, int margin, int oldMargin) { + if (oldWidth > 0 && !mItems.isEmpty()) { + final int widthWithMargin = width - getPaddingLeft() - getPaddingRight() + margin; + final int oldWidthWithMargin = oldWidth - getPaddingLeft() - getPaddingRight() + + oldMargin; + final int xpos = getScrollX(); + final float pageOffset = (float) xpos / oldWidthWithMargin; + final int newOffsetPixels = (int) (pageOffset * widthWithMargin); + + scrollTo(newOffsetPixels, getScrollY()); + if (!mScroller.isFinished()) { + // We now return to your regularly scheduled scroll, already in progress. + final int newDuration = mScroller.getDuration() - mScroller.timePassed(); + ItemInfo targetInfo = infoForPosition(mCurItem); + mScroller.startScroll(newOffsetPixels, 0, + (int) (targetInfo.offset * width), 0, newDuration); + } + } else { + final ItemInfo ii = infoForPosition(mCurItem); + final float scrollOffset = ii != null ? Math.min(ii.offset, mLastOffset) : 0; + final int scrollPos = (int) (scrollOffset * + (width - getPaddingLeft() - getPaddingRight())); + if (scrollPos != getScrollX()) { + completeScroll(false); + scrollTo(scrollPos, getScrollY()); + } + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + final int count = getChildCount(); + int width = r - l; + int height = b - t; + int paddingLeft = getPaddingLeft(); + int paddingTop = getPaddingTop(); + int paddingRight = getPaddingRight(); + int paddingBottom = getPaddingBottom(); + final int scrollX = getScrollX(); + + int decorCount = 0; + + // First pass - decor views. We need to do this in two passes so that + // we have the proper offsets for non-decor views later. + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + int childLeft = 0; + int childTop = 0; + if (lp.isDecor) { + final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; + final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; + switch (hgrav) { + default: + childLeft = paddingLeft; + break; + case Gravity.LEFT: + childLeft = paddingLeft; + paddingLeft += child.getMeasuredWidth(); + break; + case Gravity.CENTER_HORIZONTAL: + childLeft = Math.max((width - child.getMeasuredWidth()) / 2, + paddingLeft); + break; + case Gravity.RIGHT: + childLeft = width - paddingRight - child.getMeasuredWidth(); + paddingRight += child.getMeasuredWidth(); + break; + } + switch (vgrav) { + default: + childTop = paddingTop; + break; + case Gravity.TOP: + childTop = paddingTop; + paddingTop += child.getMeasuredHeight(); + break; + case Gravity.CENTER_VERTICAL: + childTop = Math.max((height - child.getMeasuredHeight()) / 2, + paddingTop); + break; + case Gravity.BOTTOM: + childTop = height - paddingBottom - child.getMeasuredHeight(); + paddingBottom += child.getMeasuredHeight(); + break; + } + childLeft += scrollX; + child.layout(childLeft, childTop, + childLeft + child.getMeasuredWidth(), + childTop + child.getMeasuredHeight()); + decorCount++; + } + } + } + + final int childWidth = width - paddingLeft - paddingRight; + // Page views. Do this once we have the right padding offsets from above. + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + ItemInfo ii; + if (!lp.isDecor && (ii = infoForChild(child)) != null) { + int loff = (int) (childWidth * ii.offset); + int childLeft = paddingLeft + loff; + int childTop = paddingTop; + if (lp.needsMeasure) { + // This was added during layout and needs measurement. + // Do it now that we know what we're working with. + lp.needsMeasure = false; + final int widthSpec = MeasureSpec.makeMeasureSpec( + (int) (childWidth * lp.widthFactor), + MeasureSpec.EXACTLY); + final int heightSpec = MeasureSpec.makeMeasureSpec( + (int) (height - paddingTop - paddingBottom), + MeasureSpec.EXACTLY); + child.measure(widthSpec, heightSpec); + } + if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object + + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth() + + "x" + child.getMeasuredHeight()); + child.layout(childLeft, childTop, + childLeft + child.getMeasuredWidth(), + childTop + child.getMeasuredHeight()); + } + } + } + mTopPageBounds = paddingTop; + mBottomPageBounds = height - paddingBottom; + mDecorChildCount = decorCount; + + if (mFirstLayout) { + scrollToItem(mCurItem, false, 0, false); + } + mFirstLayout = false; + } + + @Override + public void computeScroll() { + if (!mScroller.isFinished() && mScroller.computeScrollOffset()) { + int oldX = getScrollX(); + int oldY = getScrollY(); + int x = mScroller.getCurrX(); + int y = mScroller.getCurrY(); + + if (oldX != x || oldY != y) { + scrollTo(x, y); + if (!pageScrolled(x)) { + mScroller.abortAnimation(); + scrollTo(0, y); + } + } + + // Keep on drawing until the animation has finished. + postInvalidateOnAnimation(); + return; + } + + // Done with scroll, clean up state. + completeScroll(true); + } + + private boolean pageScrolled(int xpos) { + if (mItems.size() == 0) { + mCalledSuper = false; + onPageScrolled(0, 0, 0); + if (!mCalledSuper) { + throw new IllegalStateException( + "onPageScrolled did not call superclass implementation"); + } + return false; + } + final ItemInfo ii = infoForCurrentScrollPosition(); + final int width = getClientWidth(); + final int widthWithMargin = width + mPageMargin; + final float marginOffset = (float) mPageMargin / width; + final int currentPage = ii.position; + final float pageOffset = (((float) xpos / width) - ii.offset) / + (ii.widthFactor + marginOffset); + final int offsetPixels = (int) (pageOffset * widthWithMargin); + + mCalledSuper = false; + onPageScrolled(currentPage, pageOffset, offsetPixels); + if (!mCalledSuper) { + throw new IllegalStateException( + "onPageScrolled did not call superclass implementation"); + } + return true; + } + + /** + * This method will be invoked when the current page is scrolled, either as part + * of a programmatically initiated smooth scroll or a user initiated touch scroll. + * If you override this method you must call through to the superclass implementation + * (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled + * returns. + * + * @param position Position index of the first page currently being displayed. + * Page position+1 will be visible if positionOffset is nonzero. + * @param offset Value from [0, 1) indicating the offset from the page at position. + * @param offsetPixels Value in pixels indicating the offset from position. + */ + protected void onPageScrolled(int position, float offset, int offsetPixels) { + // Offset any decor views if needed - keep them on-screen at all times. + if (mDecorChildCount > 0) { + final int scrollX = getScrollX(); + int paddingLeft = getPaddingLeft(); + int paddingRight = getPaddingRight(); + final int width = getWidth(); + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.isDecor) continue; + + final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; + int childLeft = 0; + switch (hgrav) { + default: + childLeft = paddingLeft; + break; + case Gravity.LEFT: + childLeft = paddingLeft; + paddingLeft += child.getWidth(); + break; + case Gravity.CENTER_HORIZONTAL: + childLeft = Math.max((width - child.getMeasuredWidth()) / 2, + paddingLeft); + break; + case Gravity.RIGHT: + childLeft = width - paddingRight - child.getMeasuredWidth(); + paddingRight += child.getMeasuredWidth(); + break; + } + childLeft += scrollX; + + final int childOffset = childLeft - child.getLeft(); + if (childOffset != 0) { + child.offsetLeftAndRight(childOffset); + } + } + } + + if (mOnPageChangeListener != null) { + mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels); + } + if (mInternalPageChangeListener != null) { + mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels); + } + + if (mPageTransformer != null) { + final int scrollX = getScrollX(); + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (lp.isDecor) continue; + + final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth(); + mPageTransformer.transformPage(child, transformPos); + } + } + + mCalledSuper = true; + } + + private void completeScroll(boolean postEvents) { + boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING; + if (needPopulate) { + // Done with scroll, no longer want to cache view drawing. + setScrollingCacheEnabled(false); + mScroller.abortAnimation(); + int oldX = getScrollX(); + int oldY = getScrollY(); + int x = mScroller.getCurrX(); + int y = mScroller.getCurrY(); + if (oldX != x || oldY != y) { + scrollTo(x, y); + } + } + mPopulatePending = false; + for (int i=0; i<mItems.size(); i++) { + ItemInfo ii = mItems.get(i); + if (ii.scrolling) { + needPopulate = true; + ii.scrolling = false; + } + } + if (needPopulate) { + if (postEvents) { + postOnAnimation(mEndScrollRunnable); + } else { + mEndScrollRunnable.run(); + } + } + } + + private boolean isGutterDrag(float x, float dx) { + return (x < mGutterSize && dx > 0) || (x > getWidth() - mGutterSize && dx < 0); + } + + private void enableLayers(boolean enable) { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final int layerType = enable ? LAYER_TYPE_HARDWARE : LAYER_TYPE_NONE; + getChildAt(i).setLayerType(layerType, null); + } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + /* + * This method JUST determines whether we want to intercept the motion. + * If we return true, onMotionEvent will be called and we do the actual + * scrolling there. + */ + + final int action = ev.getAction() & MotionEvent.ACTION_MASK; + + // Always take care of the touch gesture being complete. + if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { + // Release the drag. + if (DEBUG) Log.v(TAG, "Intercept done!"); + mIsBeingDragged = false; + mIsUnableToDrag = false; + mActivePointerId = INVALID_POINTER; + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + return false; + } + + // Nothing more to do here if we have decided whether or not we + // are dragging. + if (action != MotionEvent.ACTION_DOWN) { + if (mIsBeingDragged) { + if (DEBUG) Log.v(TAG, "Intercept returning true!"); + return true; + } + if (mIsUnableToDrag) { + if (DEBUG) Log.v(TAG, "Intercept returning false!"); + return false; + } + } + + switch (action) { + case MotionEvent.ACTION_MOVE: { + /* + * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check + * whether the user has moved far enough from his original down touch. + */ + + /* + * Locally do absolute value. mLastMotionY is set to the y value + * of the down event. + */ + final int activePointerId = mActivePointerId; + if (activePointerId == INVALID_POINTER) { + // If we don't have a valid id, the touch down wasn't on content. + break; + } + + final int pointerIndex = ev.findPointerIndex(activePointerId); + final float x = ev.getX(pointerIndex); + final float dx = x - mLastMotionX; + final float xDiff = Math.abs(dx); + final float y = ev.getY(pointerIndex); + final float yDiff = Math.abs(y - mInitialMotionY); + if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); + + if (dx != 0 && !isGutterDrag(mLastMotionX, dx) && + canScroll(this, false, (int) dx, (int) x, (int) y)) { + // Nested view has scrollable area under this point. Let it be handled there. + mLastMotionX = x; + mLastMotionY = y; + mIsUnableToDrag = true; + return false; + } + if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) { + if (DEBUG) Log.v(TAG, "Starting drag!"); + mIsBeingDragged = true; + requestParentDisallowInterceptTouchEvent(true); + setScrollState(SCROLL_STATE_DRAGGING); + mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop : + mInitialMotionX - mTouchSlop; + mLastMotionY = y; + setScrollingCacheEnabled(true); + } else if (yDiff > mTouchSlop) { + // The finger has moved enough in the vertical + // direction to be counted as a drag... abort + // any attempt to drag horizontally, to work correctly + // with children that have scrolling containers. + if (DEBUG) Log.v(TAG, "Starting unable to drag!"); + mIsUnableToDrag = true; + } + if (mIsBeingDragged) { + // Scroll to follow the motion event + if (performDrag(x)) { + postInvalidateOnAnimation(); + } + } + break; + } + + case MotionEvent.ACTION_DOWN: { + /* + * Remember location of down touch. + * ACTION_DOWN always refers to pointer index 0. + */ + mLastMotionX = mInitialMotionX = ev.getX(); + mLastMotionY = mInitialMotionY = ev.getY(); + mActivePointerId = ev.getPointerId(0); + mIsUnableToDrag = false; + + mScroller.computeScrollOffset(); + if (mScrollState == SCROLL_STATE_SETTLING && + Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) { + // Let the user 'catch' the pager as it animates. + mScroller.abortAnimation(); + mPopulatePending = false; + populate(); + mIsBeingDragged = true; + requestParentDisallowInterceptTouchEvent(true); + setScrollState(SCROLL_STATE_DRAGGING); + } else { + completeScroll(false); + mIsBeingDragged = false; + } + + if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY + + " mIsBeingDragged=" + mIsBeingDragged + + "mIsUnableToDrag=" + mIsUnableToDrag); + break; + } + + case MotionEvent.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + break; + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + /* + * The only time we want to intercept motion events is if we are in the + * drag mode. + */ + return mIsBeingDragged; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mFakeDragging) { + // A fake drag is in progress already, ignore this real one + // but still eat the touch events. + // (It is likely that the user is multi-touching the screen.) + return true; + } + + if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { + // Don't handle edge touches immediately -- they may actually belong to one of our + // descendants. + return false; + } + + if (mAdapter == null || mAdapter.getCount() == 0) { + // Nothing to present or scroll; nothing to touch. + return false; + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + final int action = ev.getAction(); + boolean needsInvalidate = false; + + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: { + mScroller.abortAnimation(); + mPopulatePending = false; + populate(); + + // Remember where the motion event started + mLastMotionX = mInitialMotionX = ev.getX(); + mLastMotionY = mInitialMotionY = ev.getY(); + mActivePointerId = ev.getPointerId(0); + break; + } + case MotionEvent.ACTION_MOVE: + if (!mIsBeingDragged) { + final int pointerIndex = ev.findPointerIndex(mActivePointerId); + final float x = ev.getX(pointerIndex); + final float xDiff = Math.abs(x - mLastMotionX); + final float y = ev.getY(pointerIndex); + final float yDiff = Math.abs(y - mLastMotionY); + if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); + if (xDiff > mTouchSlop && xDiff > yDiff) { + if (DEBUG) Log.v(TAG, "Starting drag!"); + mIsBeingDragged = true; + requestParentDisallowInterceptTouchEvent(true); + mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop : + mInitialMotionX - mTouchSlop; + mLastMotionY = y; + setScrollState(SCROLL_STATE_DRAGGING); + setScrollingCacheEnabled(true); + + // Disallow Parent Intercept, just in case + ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + } + } + // Not else! Note that mIsBeingDragged can be set above. + if (mIsBeingDragged) { + // Scroll to follow the motion event + final int activePointerIndex = ev.findPointerIndex(mActivePointerId); + final float x = ev.getX(activePointerIndex); + needsInvalidate |= performDrag(x); + } + break; + case MotionEvent.ACTION_UP: + if (mIsBeingDragged) { + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId); + mPopulatePending = true; + final int width = getClientWidth(); + final int scrollX = getScrollX(); + final ItemInfo ii = infoForCurrentScrollPosition(); + final int currentPage = ii.position; + final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor; + final int activePointerIndex = + ev.findPointerIndex(mActivePointerId); + final float x = ev.getX(activePointerIndex); + final int totalDelta = (int) (x - mInitialMotionX); + int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, + totalDelta); + setCurrentItemInternal(nextPage, true, true, initialVelocity); + + mActivePointerId = INVALID_POINTER; + endDrag(); + mLeftEdge.onRelease(); + mRightEdge.onRelease(); + needsInvalidate = true; + } + break; + case MotionEvent.ACTION_CANCEL: + if (mIsBeingDragged) { + scrollToItem(mCurItem, true, 0, false); + mActivePointerId = INVALID_POINTER; + endDrag(); + mLeftEdge.onRelease(); + mRightEdge.onRelease(); + needsInvalidate = true; + } + break; + case MotionEvent.ACTION_POINTER_DOWN: { + final int index = ev.getActionIndex(); + final float x = ev.getX(index); + mLastMotionX = x; + mActivePointerId = ev.getPointerId(index); + break; + } + case MotionEvent.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId)); + break; + } + if (needsInvalidate) { + postInvalidateOnAnimation(); + } + return true; + } + + private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) { + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(disallowIntercept); + } + } + + private boolean performDrag(float x) { + boolean needsInvalidate = false; + + final float deltaX = mLastMotionX - x; + mLastMotionX = x; + + float oldScrollX = getScrollX(); + float scrollX = oldScrollX + deltaX; + final int width = getClientWidth(); + + float leftBound = width * mFirstOffset; + float rightBound = width * mLastOffset; + boolean leftAbsolute = true; + boolean rightAbsolute = true; + + final ItemInfo firstItem = mItems.get(0); + final ItemInfo lastItem = mItems.get(mItems.size() - 1); + if (firstItem.position != 0) { + leftAbsolute = false; + leftBound = firstItem.offset * width; + } + if (lastItem.position != mAdapter.getCount() - 1) { + rightAbsolute = false; + rightBound = lastItem.offset * width; + } + + if (scrollX < leftBound) { + if (leftAbsolute) { + float over = leftBound - scrollX; + mLeftEdge.onPull(Math.abs(over) / width); + needsInvalidate = true; + } + scrollX = leftBound; + } else if (scrollX > rightBound) { + if (rightAbsolute) { + float over = scrollX - rightBound; + mRightEdge.onPull(Math.abs(over) / width); + needsInvalidate = true; + } + scrollX = rightBound; + } + // Don't lose the rounded component + mLastMotionX += scrollX - (int) scrollX; + scrollTo((int) scrollX, getScrollY()); + pageScrolled((int) scrollX); + + return needsInvalidate; + } + + /** + * @return Info about the page at the current scroll position. + * This can be synthetic for a missing middle page; the 'object' field can be null. + */ + private ItemInfo infoForCurrentScrollPosition() { + final int width = getClientWidth(); + final float scrollOffset = width > 0 ? (float) getScrollX() / width : 0; + final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; + int lastPos = -1; + float lastOffset = 0.f; + float lastWidth = 0.f; + boolean first = true; + + ItemInfo lastItem = null; + for (int i = 0; i < mItems.size(); i++) { + ItemInfo ii = mItems.get(i); + float offset; + if (!first && ii.position != lastPos + 1) { + // Create a synthetic item for a missing page. + ii = mTempItem; + ii.offset = lastOffset + lastWidth + marginOffset; + ii.position = lastPos + 1; + ii.widthFactor = mAdapter.getPageWidth(ii.position); + i--; + } + offset = ii.offset; + + final float leftBound = offset; + final float rightBound = offset + ii.widthFactor + marginOffset; + if (first || scrollOffset >= leftBound) { + if (scrollOffset < rightBound || i == mItems.size() - 1) { + return ii; + } + } else { + return lastItem; + } + first = false; + lastPos = ii.position; + lastOffset = offset; + lastWidth = ii.widthFactor; + lastItem = ii; + } + + return lastItem; + } + + private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) { + int targetPage; + if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) { + targetPage = velocity > 0 ? currentPage : currentPage + 1; + } else { + final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f; + targetPage = (int) (currentPage + pageOffset + truncator); + } + + if (mItems.size() > 0) { + final ItemInfo firstItem = mItems.get(0); + final ItemInfo lastItem = mItems.get(mItems.size() - 1); + + // Only let the user target pages we have items for + targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position)); + } + + return targetPage; + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + boolean needsInvalidate = false; + + final int overScrollMode = getOverScrollMode(); + if (overScrollMode == View.OVER_SCROLL_ALWAYS || + (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && + mAdapter != null && mAdapter.getCount() > 1)) { + if (!mLeftEdge.isFinished()) { + final int restoreCount = canvas.save(); + final int height = getHeight() - getPaddingTop() - getPaddingBottom(); + final int width = getWidth(); + + canvas.rotate(270); + canvas.translate(-height + getPaddingTop(), mFirstOffset * width); + mLeftEdge.setSize(height, width); + needsInvalidate |= mLeftEdge.draw(canvas); + canvas.restoreToCount(restoreCount); + } + if (!mRightEdge.isFinished()) { + final int restoreCount = canvas.save(); + final int width = getWidth(); + final int height = getHeight() - getPaddingTop() - getPaddingBottom(); + + canvas.rotate(90); + canvas.translate(-getPaddingTop(), -(mLastOffset + 1) * width); + mRightEdge.setSize(height, width); + needsInvalidate |= mRightEdge.draw(canvas); + canvas.restoreToCount(restoreCount); + } + } else { + mLeftEdge.finish(); + mRightEdge.finish(); + } + + if (needsInvalidate) { + // Keep animating + postInvalidateOnAnimation(); + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // Draw the margin drawable between pages if needed. + if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) { + final int scrollX = getScrollX(); + final int width = getWidth(); + + final float marginOffset = (float) mPageMargin / width; + int itemIndex = 0; + ItemInfo ii = mItems.get(0); + float offset = ii.offset; + final int itemCount = mItems.size(); + final int firstPos = ii.position; + final int lastPos = mItems.get(itemCount - 1).position; + for (int pos = firstPos; pos < lastPos; pos++) { + while (pos > ii.position && itemIndex < itemCount) { + ii = mItems.get(++itemIndex); + } + + float drawAt; + if (pos == ii.position) { + drawAt = (ii.offset + ii.widthFactor) * width; + offset = ii.offset + ii.widthFactor + marginOffset; + } else { + float widthFactor = mAdapter.getPageWidth(pos); + drawAt = (offset + widthFactor) * width; + offset += widthFactor + marginOffset; + } + + if (drawAt + mPageMargin > scrollX) { + mMarginDrawable.setBounds((int) drawAt, mTopPageBounds, + (int) (drawAt + mPageMargin + 0.5f), mBottomPageBounds); + mMarginDrawable.draw(canvas); + } + + if (drawAt > scrollX + width) { + break; // No more visible, no sense in continuing + } + } + } + } + + /** + * Start a fake drag of the pager. + * + * <p>A fake drag can be useful if you want to synchronize the motion of the ViewPager + * with the touch scrolling of another view, while still letting the ViewPager + * control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.) + * Call {@link #fakeDragBy(float)} to simulate the actual drag motion. Call + * {@link #endFakeDrag()} to complete the fake drag and fling as necessary. + * + * <p>During a fake drag the ViewPager will ignore all touch events. If a real drag + * is already in progress, this method will return false. + * + * @return true if the fake drag began successfully, false if it could not be started. + * + * @see #fakeDragBy(float) + * @see #endFakeDrag() + */ + public boolean beginFakeDrag() { + if (mIsBeingDragged) { + return false; + } + mFakeDragging = true; + setScrollState(SCROLL_STATE_DRAGGING); + mInitialMotionX = mLastMotionX = 0; + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } else { + mVelocityTracker.clear(); + } + final long time = SystemClock.uptimeMillis(); + final MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0); + mVelocityTracker.addMovement(ev); + ev.recycle(); + mFakeDragBeginTime = time; + return true; + } + + /** + * End a fake drag of the pager. + * + * @see #beginFakeDrag() + * @see #fakeDragBy(float) + */ + public void endFakeDrag() { + if (!mFakeDragging) { + throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); + } + + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId); + mPopulatePending = true; + final int width = getClientWidth(); + final int scrollX = getScrollX(); + final ItemInfo ii = infoForCurrentScrollPosition(); + final int currentPage = ii.position; + final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor; + final int totalDelta = (int) (mLastMotionX - mInitialMotionX); + int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, + totalDelta); + setCurrentItemInternal(nextPage, true, true, initialVelocity); + endDrag(); + + mFakeDragging = false; + } + + /** + * Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first. + * + * @param xOffset Offset in pixels to drag by. + * @see #beginFakeDrag() + * @see #endFakeDrag() + */ + public void fakeDragBy(float xOffset) { + if (!mFakeDragging) { + throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); + } + + mLastMotionX += xOffset; + + float oldScrollX = getScrollX(); + float scrollX = oldScrollX - xOffset; + final int width = getClientWidth(); + + float leftBound = width * mFirstOffset; + float rightBound = width * mLastOffset; + + final ItemInfo firstItem = mItems.get(0); + final ItemInfo lastItem = mItems.get(mItems.size() - 1); + if (firstItem.position != 0) { + leftBound = firstItem.offset * width; + } + if (lastItem.position != mAdapter.getCount() - 1) { + rightBound = lastItem.offset * width; + } + + if (scrollX < leftBound) { + scrollX = leftBound; + } else if (scrollX > rightBound) { + scrollX = rightBound; + } + // Don't lose the rounded component + mLastMotionX += scrollX - (int) scrollX; + scrollTo((int) scrollX, getScrollY()); + pageScrolled((int) scrollX); + + // Synthesize an event for the VelocityTracker. + final long time = SystemClock.uptimeMillis(); + final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE, + mLastMotionX, 0, 0); + mVelocityTracker.addMovement(ev); + ev.recycle(); + } + + /** + * Returns true if a fake drag is in progress. + * + * @return true if currently in a fake drag, false otherwise. + * + * @see #beginFakeDrag() + * @see #fakeDragBy(float) + * @see #endFakeDrag() + */ + public boolean isFakeDragging() { + return mFakeDragging; + } + + private void onSecondaryPointerUp(MotionEvent ev) { + final int pointerIndex = ev.getActionIndex(); + final int pointerId = ev.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mLastMotionX = ev.getX(newPointerIndex); + mActivePointerId = ev.getPointerId(newPointerIndex); + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + } + } + + private void endDrag() { + mIsBeingDragged = false; + mIsUnableToDrag = false; + + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + private void setScrollingCacheEnabled(boolean enabled) { + if (mScrollingCacheEnabled != enabled) { + mScrollingCacheEnabled = enabled; + if (USE_CACHE) { + final int size = getChildCount(); + for (int i = 0; i < size; ++i) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + child.setDrawingCacheEnabled(enabled); + } + } + } + } + } + + public boolean canScrollHorizontally(int direction) { + if (mAdapter == null) { + return false; + } + + final int width = getClientWidth(); + final int scrollX = getScrollX(); + if (direction < 0) { + return (scrollX > (int) (width * mFirstOffset)); + } else if (direction > 0) { + return (scrollX < (int) (width * mLastOffset)); + } else { + return false; + } + } + + /** + * Tests scrollability within child views of v given a delta of dx. + * + * @param v View to test for horizontal scrollability + * @param checkV Whether the view v passed should itself be checked for scrollability (true), + * or just its children (false). + * @param dx Delta scrolled in pixels + * @param x X coordinate of the active touch point + * @param y Y coordinate of the active touch point + * @return true if child views of v can be scrolled by delta of dx. + */ + protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { + if (v instanceof ViewGroup) { + final ViewGroup group = (ViewGroup) v; + final int scrollX = v.getScrollX(); + final int scrollY = v.getScrollY(); + final int count = group.getChildCount(); + // Count backwards - let topmost views consume scroll distance first. + for (int i = count - 1; i >= 0; i--) { + // TODO: Add versioned support here for transformed views. + // This will not work for transformed views in Honeycomb+ + final View child = group.getChildAt(i); + if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && + y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && + canScroll(child, true, dx, x + scrollX - child.getLeft(), + y + scrollY - child.getTop())) { + return true; + } + } + } + + return checkV && v.canScrollHorizontally(-dx); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // Let the focused view and/or our descendants get the key first + return super.dispatchKeyEvent(event) || executeKeyEvent(event); + } + + /** + * You can call this function yourself to have the scroll view perform + * scrolling from a key event, just as if the event had been dispatched to + * it by the view hierarchy. + * + * @param event The key event to execute. + * @return Return true if the event was handled, else false. + */ + public boolean executeKeyEvent(KeyEvent event) { + boolean handled = false; + if (event.getAction() == KeyEvent.ACTION_DOWN) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_DPAD_LEFT: + handled = arrowScroll(FOCUS_LEFT); + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + handled = arrowScroll(FOCUS_RIGHT); + break; + case KeyEvent.KEYCODE_TAB: + if (event.hasNoModifiers()) { + handled = arrowScroll(FOCUS_FORWARD); + } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) { + handled = arrowScroll(FOCUS_BACKWARD); + } + break; + } + } + return handled; + } + + public boolean arrowScroll(int direction) { + View currentFocused = findFocus(); + if (currentFocused == this) { + currentFocused = null; + } else if (currentFocused != null) { + boolean isChild = false; + for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup; + parent = parent.getParent()) { + if (parent == this) { + isChild = true; + break; + } + } + if (!isChild) { + // This would cause the focus search down below to fail in fun ways. + final StringBuilder sb = new StringBuilder(); + sb.append(currentFocused.getClass().getSimpleName()); + for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup; + parent = parent.getParent()) { + sb.append(" => ").append(parent.getClass().getSimpleName()); + } + Log.e(TAG, "arrowScroll tried to find focus based on non-child " + + "current focused view " + sb.toString()); + currentFocused = null; + } + } + + boolean handled = false; + + View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, + direction); + if (nextFocused != null && nextFocused != currentFocused) { + if (direction == View.FOCUS_LEFT) { + // If there is nothing to the left, or this is causing us to + // jump to the right, then what we really want to do is page left. + final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left; + final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left; + if (currentFocused != null && nextLeft >= currLeft) { + handled = pageLeft(); + } else { + handled = nextFocused.requestFocus(); + } + } else if (direction == View.FOCUS_RIGHT) { + // If there is nothing to the right, or this is causing us to + // jump to the left, then what we really want to do is page right. + final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left; + final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left; + if (currentFocused != null && nextLeft <= currLeft) { + handled = pageRight(); + } else { + handled = nextFocused.requestFocus(); + } + } + } else if (direction == FOCUS_LEFT || direction == FOCUS_BACKWARD) { + // Trying to move left and nothing there; try to page. + handled = pageLeft(); + } else if (direction == FOCUS_RIGHT || direction == FOCUS_FORWARD) { + // Trying to move right and nothing there; try to page. + handled = pageRight(); + } + if (handled) { + playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); + } + return handled; + } + + private Rect getChildRectInPagerCoordinates(Rect outRect, View child) { + if (outRect == null) { + outRect = new Rect(); + } + if (child == null) { + outRect.set(0, 0, 0, 0); + return outRect; + } + outRect.left = child.getLeft(); + outRect.right = child.getRight(); + outRect.top = child.getTop(); + outRect.bottom = child.getBottom(); + + ViewParent parent = child.getParent(); + while (parent instanceof ViewGroup && parent != this) { + final ViewGroup group = (ViewGroup) parent; + outRect.left += group.getLeft(); + outRect.right += group.getRight(); + outRect.top += group.getTop(); + outRect.bottom += group.getBottom(); + + parent = group.getParent(); + } + return outRect; + } + + boolean pageLeft() { + if (mCurItem > 0) { + setCurrentItem(mCurItem-1, true); + return true; + } + return false; + } + + boolean pageRight() { + if (mAdapter != null && mCurItem < (mAdapter.getCount()-1)) { + setCurrentItem(mCurItem+1, true); + return true; + } + return false; + } + + /** + * We only want the current page that is being shown to be focusable. + */ + @Override + public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { + final int focusableCount = views.size(); + + final int descendantFocusability = getDescendantFocusability(); + + if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) { + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + child.addFocusables(views, direction, focusableMode); + } + } + } + } + + // we add ourselves (if focusable) in all cases except for when we are + // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable. this is + // to avoid the focus search finding layouts when a more precise search + // among the focusable children would be more interesting. + if ( + descendantFocusability != FOCUS_AFTER_DESCENDANTS || + // No focusable descendants + (focusableCount == views.size())) { + // Note that we can't call the superclass here, because it will + // add all views in. So we need to do the same thing View does. + if (!isFocusable()) { + return; + } + if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE && + isInTouchMode() && !isFocusableInTouchMode()) { + return; + } + if (views != null) { + views.add(this); + } + } + } + + /** + * We only want the current page that is being shown to be touchable. + */ + @Override + public void addTouchables(ArrayList<View> views) { + // Note that we don't call super.addTouchables(), which means that + // we don't call View.addTouchables(). This is okay because a ViewPager + // is itself not touchable. + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + child.addTouchables(views); + } + } + } + } + + /** + * We only want the current page that is being shown to be focusable. + */ + @Override + protected boolean onRequestFocusInDescendants(int direction, + Rect previouslyFocusedRect) { + int index; + int increment; + int end; + int count = getChildCount(); + if ((direction & FOCUS_FORWARD) != 0) { + index = 0; + increment = 1; + end = count; + } else { + index = count - 1; + increment = -1; + end = -1; + } + for (int i = index; i != end; i += increment) { + View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + if (child.requestFocus(direction, previouslyFocusedRect)) { + return true; + } + } + } + } + return false; + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + // Dispatch scroll events from this ViewPager. + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) { + return super.dispatchPopulateAccessibilityEvent(event); + } + + // Dispatch all other accessibility events from the current page. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + final ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem && + child.dispatchPopulateAccessibilityEvent(event)) { + return true; + } + } + } + + return false; + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return generateDefaultLayoutParams(); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams && super.checkLayoutParams(p); + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + class MyAccessibilityDelegate extends AccessibilityDelegate { + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(host, event); + event.setClassName(ViewPager.class.getName()); + final AccessibilityRecord record = AccessibilityRecord.obtain(); + record.setScrollable(canScroll()); + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED + && mAdapter != null) { + record.setItemCount(mAdapter.getCount()); + record.setFromIndex(mCurItem); + record.setToIndex(mCurItem); + } + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.setClassName(ViewPager.class.getName()); + info.setScrollable(canScroll()); + if (canScrollHorizontally(1)) { + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); + } + if (canScrollHorizontally(-1)) { + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); + } + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if (super.performAccessibilityAction(host, action, args)) { + return true; + } + switch (action) { + case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { + if (canScrollHorizontally(1)) { + setCurrentItem(mCurItem + 1); + return true; + } + } return false; + case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { + if (canScrollHorizontally(-1)) { + setCurrentItem(mCurItem - 1); + return true; + } + } return false; + } + return false; + } + + private boolean canScroll() { + return (mAdapter != null) && (mAdapter.getCount() > 1); + } + } + + private class PagerObserver extends DataSetObserver { + @Override + public void onChanged() { + dataSetChanged(); + } + @Override + public void onInvalidated() { + dataSetChanged(); + } + } + + /** + * Layout parameters that should be supplied for views added to a + * ViewPager. + */ + public static class LayoutParams extends ViewGroup.LayoutParams { + /** + * true if this view is a decoration on the pager itself and not + * a view supplied by the adapter. + */ + public boolean isDecor; + + /** + * Gravity setting for use on decor views only: + * Where to position the view page within the overall ViewPager + * container; constants are defined in {@link android.view.Gravity}. + */ + public int gravity; + + /** + * Width as a 0-1 multiplier of the measured pager width + */ + float widthFactor = 0.f; + + /** + * true if this view was added during layout and needs to be measured + * before being positioned. + */ + boolean needsMeasure; + + /** + * Adapter position this view is for if !isDecor + */ + int position; + + /** + * Current child index within the ViewPager that this view occupies + */ + int childIndex; + + public LayoutParams() { + super(FILL_PARENT, FILL_PARENT); + } + + public LayoutParams(Context context, AttributeSet attrs) { + super(context, attrs); + + final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); + gravity = a.getInteger(0, Gravity.TOP); + a.recycle(); + } + } + + static class ViewPositionComparator implements Comparator<View> { + @Override + public int compare(View lhs, View rhs) { + final LayoutParams llp = (LayoutParams) lhs.getLayoutParams(); + final LayoutParams rlp = (LayoutParams) rhs.getLayoutParams(); + if (llp.isDecor != rlp.isDecor) { + return llp.isDecor ? 1 : -1; + } + return llp.position - rlp.position; + } + } +} diff --git a/core/java/com/android/internal/widget/WaveView.java b/core/java/com/android/internal/widget/WaveView.java deleted file mode 100644 index 9e7a649..0000000 --- a/core/java/com/android/internal/widget/WaveView.java +++ /dev/null @@ -1,663 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.internal.widget; - -import java.util.ArrayList; - -import android.animation.ValueAnimator; -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.drawable.BitmapDrawable; -import android.media.AudioAttributes; -import android.os.UserHandle; -import android.os.Vibrator; -import android.provider.Settings; -import android.util.AttributeSet; -import android.util.Log; -import android.view.MotionEvent; -import android.view.View; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityManager; - -import com.android.internal.R; - -/** - * A special widget containing a center and outer ring. Moving the center ring to the outer ring - * causes an event that can be caught by implementing OnTriggerListener. - */ -public class WaveView extends View implements ValueAnimator.AnimatorUpdateListener { - private static final String TAG = "WaveView"; - private static final boolean DBG = false; - private static final int WAVE_COUNT = 20; // default wave count - private static final long VIBRATE_SHORT = 20; // msec - private static final long VIBRATE_LONG = 20; // msec - - // Lock state machine states - private static final int STATE_RESET_LOCK = 0; - private static final int STATE_READY = 1; - private static final int STATE_START_ATTEMPT = 2; - private static final int STATE_ATTEMPTING = 3; - private static final int STATE_UNLOCK_ATTEMPT = 4; - private static final int STATE_UNLOCK_SUCCESS = 5; - - // Animation properties. - private static final long DURATION = 300; // duration of transitional animations - private static final long FINAL_DURATION = 200; // duration of final animations when unlocking - private static final long RING_DELAY = 1300; // when to start fading animated rings - private static final long FINAL_DELAY = 200; // delay for unlock success animation - private static final long SHORT_DELAY = 100; // for starting one animation after another. - private static final long WAVE_DURATION = 2000; // amount of time for way to expand/decay - private static final long RESET_TIMEOUT = 3000; // elapsed time of inactivity before we reset - private static final long DELAY_INCREMENT = 15; // increment per wave while tracking motion - private static final long DELAY_INCREMENT2 = 12; // increment per wave while not tracking - private static final long WAVE_DELAY = WAVE_DURATION / WAVE_COUNT; // initial propagation delay - - /** - * The scale by which to multiply the unlock handle width to compute the radius - * in which it can be grabbed when accessibility is disabled. - */ - private static final float GRAB_HANDLE_RADIUS_SCALE_ACCESSIBILITY_DISABLED = 0.5f; - - /** - * The scale by which to multiply the unlock handle width to compute the radius - * in which it can be grabbed when accessibility is enabled (more generous). - */ - private static final float GRAB_HANDLE_RADIUS_SCALE_ACCESSIBILITY_ENABLED = 1.0f; - - private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) - .build(); - - private Vibrator mVibrator; - private OnTriggerListener mOnTriggerListener; - private ArrayList<DrawableHolder> mDrawables = new ArrayList<DrawableHolder>(3); - private ArrayList<DrawableHolder> mLightWaves = new ArrayList<DrawableHolder>(WAVE_COUNT); - private boolean mFingerDown = false; - private float mRingRadius = 182.0f; // Radius of bitmap ring. Used to snap halo to it - private int mSnapRadius = 136; // minimum threshold for drag unlock - private int mWaveCount = WAVE_COUNT; // number of waves - private long mWaveTimerDelay = WAVE_DELAY; - private int mCurrentWave = 0; - private float mLockCenterX; // center of widget as dictated by widget size - private float mLockCenterY; - private float mMouseX; // current mouse position as of last touch event - private float mMouseY; - private DrawableHolder mUnlockRing; - private DrawableHolder mUnlockDefault; - private DrawableHolder mUnlockHalo; - private int mLockState = STATE_RESET_LOCK; - private int mGrabbedState = OnTriggerListener.NO_HANDLE; - private boolean mWavesRunning; - private boolean mFinishWaves; - - public WaveView(Context context) { - this(context, null); - } - - public WaveView(Context context, AttributeSet attrs) { - super(context, attrs); - - // TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.WaveView); - // mOrientation = a.getInt(R.styleable.WaveView_orientation, HORIZONTAL); - // a.recycle(); - - initDrawables(); - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - mLockCenterX = 0.5f * w; - mLockCenterY = 0.5f * h; - super.onSizeChanged(w, h, oldw, oldh); - } - - @Override - protected int getSuggestedMinimumWidth() { - // View should be large enough to contain the unlock ring + halo - return mUnlockRing.getWidth() + mUnlockHalo.getWidth(); - } - - @Override - protected int getSuggestedMinimumHeight() { - // View should be large enough to contain the unlock ring + halo - return mUnlockRing.getHeight() + mUnlockHalo.getHeight(); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); - int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); - int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); - int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); - int width; - int height; - - if (widthSpecMode == MeasureSpec.AT_MOST) { - width = Math.min(widthSpecSize, getSuggestedMinimumWidth()); - } else if (widthSpecMode == MeasureSpec.EXACTLY) { - width = widthSpecSize; - } else { - width = getSuggestedMinimumWidth(); - } - - if (heightSpecMode == MeasureSpec.AT_MOST) { - height = Math.min(heightSpecSize, getSuggestedMinimumWidth()); - } else if (heightSpecMode == MeasureSpec.EXACTLY) { - height = heightSpecSize; - } else { - height = getSuggestedMinimumHeight(); - } - - setMeasuredDimension(width, height); - } - - private void initDrawables() { - mUnlockRing = new DrawableHolder(createDrawable(R.drawable.unlock_ring)); - mUnlockRing.setX(mLockCenterX); - mUnlockRing.setY(mLockCenterY); - mUnlockRing.setScaleX(0.1f); - mUnlockRing.setScaleY(0.1f); - mUnlockRing.setAlpha(0.0f); - mDrawables.add(mUnlockRing); - - mUnlockDefault = new DrawableHolder(createDrawable(R.drawable.unlock_default)); - mUnlockDefault.setX(mLockCenterX); - mUnlockDefault.setY(mLockCenterY); - mUnlockDefault.setScaleX(0.1f); - mUnlockDefault.setScaleY(0.1f); - mUnlockDefault.setAlpha(0.0f); - mDrawables.add(mUnlockDefault); - - mUnlockHalo = new DrawableHolder(createDrawable(R.drawable.unlock_halo)); - mUnlockHalo.setX(mLockCenterX); - mUnlockHalo.setY(mLockCenterY); - mUnlockHalo.setScaleX(0.1f); - mUnlockHalo.setScaleY(0.1f); - mUnlockHalo.setAlpha(0.0f); - mDrawables.add(mUnlockHalo); - - BitmapDrawable wave = createDrawable(R.drawable.unlock_wave); - for (int i = 0; i < mWaveCount; i++) { - DrawableHolder holder = new DrawableHolder(wave); - mLightWaves.add(holder); - holder.setAlpha(0.0f); - } - } - - private void waveUpdateFrame(float mouseX, float mouseY, boolean fingerDown) { - double distX = mouseX - mLockCenterX; - double distY = mouseY - mLockCenterY; - int dragDistance = (int) Math.ceil(Math.hypot(distX, distY)); - double touchA = Math.atan2(distX, distY); - float ringX = (float) (mLockCenterX + mRingRadius * Math.sin(touchA)); - float ringY = (float) (mLockCenterY + mRingRadius * Math.cos(touchA)); - - switch (mLockState) { - case STATE_RESET_LOCK: - if (DBG) Log.v(TAG, "State RESET_LOCK"); - mWaveTimerDelay = WAVE_DELAY; - for (int i = 0; i < mLightWaves.size(); i++) { - DrawableHolder holder = mLightWaves.get(i); - holder.addAnimTo(300, 0, "alpha", 0.0f, false); - } - for (int i = 0; i < mLightWaves.size(); i++) { - mLightWaves.get(i).startAnimations(this); - } - - mUnlockRing.addAnimTo(DURATION, 0, "x", mLockCenterX, true); - mUnlockRing.addAnimTo(DURATION, 0, "y", mLockCenterY, true); - mUnlockRing.addAnimTo(DURATION, 0, "scaleX", 0.1f, true); - mUnlockRing.addAnimTo(DURATION, 0, "scaleY", 0.1f, true); - mUnlockRing.addAnimTo(DURATION, 0, "alpha", 0.0f, true); - - mUnlockDefault.removeAnimationFor("x"); - mUnlockDefault.removeAnimationFor("y"); - mUnlockDefault.removeAnimationFor("scaleX"); - mUnlockDefault.removeAnimationFor("scaleY"); - mUnlockDefault.removeAnimationFor("alpha"); - mUnlockDefault.setX(mLockCenterX); - mUnlockDefault.setY(mLockCenterY); - mUnlockDefault.setScaleX(0.1f); - mUnlockDefault.setScaleY(0.1f); - mUnlockDefault.setAlpha(0.0f); - mUnlockDefault.addAnimTo(DURATION, SHORT_DELAY, "scaleX", 1.0f, true); - mUnlockDefault.addAnimTo(DURATION, SHORT_DELAY, "scaleY", 1.0f, true); - mUnlockDefault.addAnimTo(DURATION, SHORT_DELAY, "alpha", 1.0f, true); - - mUnlockHalo.removeAnimationFor("x"); - mUnlockHalo.removeAnimationFor("y"); - mUnlockHalo.removeAnimationFor("scaleX"); - mUnlockHalo.removeAnimationFor("scaleY"); - mUnlockHalo.removeAnimationFor("alpha"); - mUnlockHalo.setX(mLockCenterX); - mUnlockHalo.setY(mLockCenterY); - mUnlockHalo.setScaleX(0.1f); - mUnlockHalo.setScaleY(0.1f); - mUnlockHalo.setAlpha(0.0f); - mUnlockHalo.addAnimTo(DURATION, SHORT_DELAY, "x", mLockCenterX, true); - mUnlockHalo.addAnimTo(DURATION, SHORT_DELAY, "y", mLockCenterY, true); - mUnlockHalo.addAnimTo(DURATION, SHORT_DELAY, "scaleX", 1.0f, true); - mUnlockHalo.addAnimTo(DURATION, SHORT_DELAY, "scaleY", 1.0f, true); - mUnlockHalo.addAnimTo(DURATION, SHORT_DELAY, "alpha", 1.0f, true); - - removeCallbacks(mLockTimerActions); - - mLockState = STATE_READY; - break; - - case STATE_READY: - if (DBG) Log.v(TAG, "State READY"); - mWaveTimerDelay = WAVE_DELAY; - break; - - case STATE_START_ATTEMPT: - if (DBG) Log.v(TAG, "State START_ATTEMPT"); - mUnlockDefault.removeAnimationFor("x"); - mUnlockDefault.removeAnimationFor("y"); - mUnlockDefault.removeAnimationFor("scaleX"); - mUnlockDefault.removeAnimationFor("scaleY"); - mUnlockDefault.removeAnimationFor("alpha"); - mUnlockDefault.setX(mLockCenterX + 182); - mUnlockDefault.setY(mLockCenterY); - mUnlockDefault.setScaleX(0.1f); - mUnlockDefault.setScaleY(0.1f); - mUnlockDefault.setAlpha(0.0f); - - mUnlockDefault.addAnimTo(DURATION, SHORT_DELAY, "scaleX", 1.0f, false); - mUnlockDefault.addAnimTo(DURATION, SHORT_DELAY, "scaleY", 1.0f, false); - mUnlockDefault.addAnimTo(DURATION, SHORT_DELAY, "alpha", 1.0f, false); - - mUnlockRing.addAnimTo(DURATION, 0, "scaleX", 1.0f, true); - mUnlockRing.addAnimTo(DURATION, 0, "scaleY", 1.0f, true); - mUnlockRing.addAnimTo(DURATION, 0, "alpha", 1.0f, true); - - mLockState = STATE_ATTEMPTING; - break; - - case STATE_ATTEMPTING: - if (DBG) Log.v(TAG, "State ATTEMPTING (fingerDown = " + fingerDown + ")"); - if (dragDistance > mSnapRadius) { - mFinishWaves = true; // don't start any more waves. - if (fingerDown) { - mUnlockHalo.addAnimTo(0, 0, "x", ringX, true); - mUnlockHalo.addAnimTo(0, 0, "y", ringY, true); - mUnlockHalo.addAnimTo(0, 0, "scaleX", 1.0f, true); - mUnlockHalo.addAnimTo(0, 0, "scaleY", 1.0f, true); - mUnlockHalo.addAnimTo(0, 0, "alpha", 1.0f, true); - } else { - if (DBG) Log.v(TAG, "up detected, moving to STATE_UNLOCK_ATTEMPT"); - mLockState = STATE_UNLOCK_ATTEMPT; - } - } else { - // If waves have stopped, we need to kick them off again... - if (!mWavesRunning) { - mWavesRunning = true; - mFinishWaves = false; - // mWaveTimerDelay = WAVE_DELAY; - postDelayed(mAddWaveAction, mWaveTimerDelay); - } - mUnlockHalo.addAnimTo(0, 0, "x", mouseX, true); - mUnlockHalo.addAnimTo(0, 0, "y", mouseY, true); - mUnlockHalo.addAnimTo(0, 0, "scaleX", 1.0f, true); - mUnlockHalo.addAnimTo(0, 0, "scaleY", 1.0f, true); - mUnlockHalo.addAnimTo(0, 0, "alpha", 1.0f, true); - } - break; - - case STATE_UNLOCK_ATTEMPT: - if (DBG) Log.v(TAG, "State UNLOCK_ATTEMPT"); - if (dragDistance > mSnapRadius) { - for (int n = 0; n < mLightWaves.size(); n++) { - DrawableHolder wave = mLightWaves.get(n); - long delay = 1000L*(6 + n - mCurrentWave)/10L; - wave.addAnimTo(FINAL_DURATION, delay, "x", ringX, true); - wave.addAnimTo(FINAL_DURATION, delay, "y", ringY, true); - wave.addAnimTo(FINAL_DURATION, delay, "scaleX", 0.1f, true); - wave.addAnimTo(FINAL_DURATION, delay, "scaleY", 0.1f, true); - wave.addAnimTo(FINAL_DURATION, delay, "alpha", 0.0f, true); - } - for (int i = 0; i < mLightWaves.size(); i++) { - mLightWaves.get(i).startAnimations(this); - } - - mUnlockRing.addAnimTo(FINAL_DURATION, 0, "x", ringX, false); - mUnlockRing.addAnimTo(FINAL_DURATION, 0, "y", ringY, false); - mUnlockRing.addAnimTo(FINAL_DURATION, 0, "scaleX", 0.1f, false); - mUnlockRing.addAnimTo(FINAL_DURATION, 0, "scaleY", 0.1f, false); - mUnlockRing.addAnimTo(FINAL_DURATION, 0, "alpha", 0.0f, false); - - mUnlockRing.addAnimTo(FINAL_DURATION, FINAL_DELAY, "alpha", 0.0f, false); - - mUnlockDefault.removeAnimationFor("x"); - mUnlockDefault.removeAnimationFor("y"); - mUnlockDefault.removeAnimationFor("scaleX"); - mUnlockDefault.removeAnimationFor("scaleY"); - mUnlockDefault.removeAnimationFor("alpha"); - mUnlockDefault.setX(ringX); - mUnlockDefault.setY(ringY); - mUnlockDefault.setScaleX(0.1f); - mUnlockDefault.setScaleY(0.1f); - mUnlockDefault.setAlpha(0.0f); - - mUnlockDefault.addAnimTo(FINAL_DURATION, 0, "x", ringX, true); - mUnlockDefault.addAnimTo(FINAL_DURATION, 0, "y", ringY, true); - mUnlockDefault.addAnimTo(FINAL_DURATION, 0, "scaleX", 1.0f, true); - mUnlockDefault.addAnimTo(FINAL_DURATION, 0, "scaleY", 1.0f, true); - mUnlockDefault.addAnimTo(FINAL_DURATION, 0, "alpha", 1.0f, true); - - mUnlockDefault.addAnimTo(FINAL_DURATION, FINAL_DELAY, "scaleX", 3.0f, false); - mUnlockDefault.addAnimTo(FINAL_DURATION, FINAL_DELAY, "scaleY", 3.0f, false); - mUnlockDefault.addAnimTo(FINAL_DURATION, FINAL_DELAY, "alpha", 0.0f, false); - - mUnlockHalo.addAnimTo(FINAL_DURATION, 0, "x", ringX, false); - mUnlockHalo.addAnimTo(FINAL_DURATION, 0, "y", ringY, false); - - mUnlockHalo.addAnimTo(FINAL_DURATION, FINAL_DELAY, "scaleX", 3.0f, false); - mUnlockHalo.addAnimTo(FINAL_DURATION, FINAL_DELAY, "scaleY", 3.0f, false); - mUnlockHalo.addAnimTo(FINAL_DURATION, FINAL_DELAY, "alpha", 0.0f, false); - - removeCallbacks(mLockTimerActions); - - postDelayed(mLockTimerActions, RESET_TIMEOUT); - - dispatchTriggerEvent(OnTriggerListener.CENTER_HANDLE); - mLockState = STATE_UNLOCK_SUCCESS; - } else { - mLockState = STATE_RESET_LOCK; - } - break; - - case STATE_UNLOCK_SUCCESS: - if (DBG) Log.v(TAG, "State UNLOCK_SUCCESS"); - removeCallbacks(mAddWaveAction); - break; - - default: - if (DBG) Log.v(TAG, "Unknown state " + mLockState); - break; - } - mUnlockDefault.startAnimations(this); - mUnlockHalo.startAnimations(this); - mUnlockRing.startAnimations(this); - } - - BitmapDrawable createDrawable(int resId) { - Resources res = getResources(); - Bitmap bitmap = BitmapFactory.decodeResource(res, resId); - return new BitmapDrawable(res, bitmap); - } - - @Override - protected void onDraw(Canvas canvas) { - waveUpdateFrame(mMouseX, mMouseY, mFingerDown); - for (int i = 0; i < mDrawables.size(); ++i) { - mDrawables.get(i).draw(canvas); - } - for (int i = 0; i < mLightWaves.size(); ++i) { - mLightWaves.get(i).draw(canvas); - } - } - - private final Runnable mLockTimerActions = new Runnable() { - public void run() { - if (DBG) Log.v(TAG, "LockTimerActions"); - // reset lock after inactivity - if (mLockState == STATE_ATTEMPTING) { - if (DBG) Log.v(TAG, "Timer resets to STATE_RESET_LOCK"); - mLockState = STATE_RESET_LOCK; - } - // for prototype, reset after successful unlock - if (mLockState == STATE_UNLOCK_SUCCESS) { - if (DBG) Log.v(TAG, "Timer resets to STATE_RESET_LOCK after success"); - mLockState = STATE_RESET_LOCK; - } - invalidate(); - } - }; - - private final Runnable mAddWaveAction = new Runnable() { - public void run() { - double distX = mMouseX - mLockCenterX; - double distY = mMouseY - mLockCenterY; - int dragDistance = (int) Math.ceil(Math.hypot(distX, distY)); - if (mLockState == STATE_ATTEMPTING && dragDistance < mSnapRadius - && mWaveTimerDelay >= WAVE_DELAY) { - mWaveTimerDelay = Math.min(WAVE_DURATION, mWaveTimerDelay + DELAY_INCREMENT); - - DrawableHolder wave = mLightWaves.get(mCurrentWave); - wave.setAlpha(0.0f); - wave.setScaleX(0.2f); - wave.setScaleY(0.2f); - wave.setX(mMouseX); - wave.setY(mMouseY); - - wave.addAnimTo(WAVE_DURATION, 0, "x", mLockCenterX, true); - wave.addAnimTo(WAVE_DURATION, 0, "y", mLockCenterY, true); - wave.addAnimTo(WAVE_DURATION*2/3, 0, "alpha", 1.0f, true); - wave.addAnimTo(WAVE_DURATION, 0, "scaleX", 1.0f, true); - wave.addAnimTo(WAVE_DURATION, 0, "scaleY", 1.0f, true); - - wave.addAnimTo(1000, RING_DELAY, "alpha", 0.0f, false); - wave.startAnimations(WaveView.this); - - mCurrentWave = (mCurrentWave+1) % mWaveCount; - if (DBG) Log.v(TAG, "WaveTimerDelay: start new wave in " + mWaveTimerDelay); - } else { - mWaveTimerDelay += DELAY_INCREMENT2; - } - if (mFinishWaves) { - // sentinel used to restart the waves after they've stopped - mWavesRunning = false; - } else { - postDelayed(mAddWaveAction, mWaveTimerDelay); - } - } - }; - - @Override - public boolean onHoverEvent(MotionEvent event) { - if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) { - final int action = event.getAction(); - switch (action) { - case MotionEvent.ACTION_HOVER_ENTER: - event.setAction(MotionEvent.ACTION_DOWN); - break; - case MotionEvent.ACTION_HOVER_MOVE: - event.setAction(MotionEvent.ACTION_MOVE); - break; - case MotionEvent.ACTION_HOVER_EXIT: - event.setAction(MotionEvent.ACTION_UP); - break; - } - onTouchEvent(event); - event.setAction(action); - } - return super.onHoverEvent(event); - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - final int action = event.getAction(); - mMouseX = event.getX(); - mMouseY = event.getY(); - boolean handled = false; - switch (action) { - case MotionEvent.ACTION_DOWN: - removeCallbacks(mLockTimerActions); - mFingerDown = true; - tryTransitionToStartAttemptState(event); - handled = true; - break; - - case MotionEvent.ACTION_MOVE: - tryTransitionToStartAttemptState(event); - handled = true; - break; - - case MotionEvent.ACTION_UP: - if (DBG) Log.v(TAG, "ACTION_UP"); - mFingerDown = false; - postDelayed(mLockTimerActions, RESET_TIMEOUT); - setGrabbedState(OnTriggerListener.NO_HANDLE); - // Normally the state machine is driven by user interaction causing redraws. - // However, when there's no more user interaction and no running animations, - // the state machine stops advancing because onDraw() never gets called. - // The following ensures we advance to the next state in this case, - // either STATE_UNLOCK_ATTEMPT or STATE_RESET_LOCK. - waveUpdateFrame(mMouseX, mMouseY, mFingerDown); - handled = true; - break; - - case MotionEvent.ACTION_CANCEL: - mFingerDown = false; - handled = true; - break; - } - invalidate(); - return handled ? true : super.onTouchEvent(event); - } - - /** - * Tries to transition to start attempt state. - * - * @param event A motion event. - */ - private void tryTransitionToStartAttemptState(MotionEvent event) { - final float dx = event.getX() - mUnlockHalo.getX(); - final float dy = event.getY() - mUnlockHalo.getY(); - float dist = (float) Math.hypot(dx, dy); - if (dist <= getScaledGrabHandleRadius()) { - setGrabbedState(OnTriggerListener.CENTER_HANDLE); - if (mLockState == STATE_READY) { - mLockState = STATE_START_ATTEMPT; - if (AccessibilityManager.getInstance(mContext).isEnabled()) { - announceUnlockHandle(); - } - } - } - } - - /** - * @return The radius in which the handle is grabbed scaled based on - * whether accessibility is enabled. - */ - private float getScaledGrabHandleRadius() { - if (AccessibilityManager.getInstance(mContext).isEnabled()) { - return GRAB_HANDLE_RADIUS_SCALE_ACCESSIBILITY_ENABLED * mUnlockHalo.getWidth(); - } else { - return GRAB_HANDLE_RADIUS_SCALE_ACCESSIBILITY_DISABLED * mUnlockHalo.getWidth(); - } - } - - /** - * Announces the unlock handle if accessibility is enabled. - */ - private void announceUnlockHandle() { - setContentDescription(mContext.getString(R.string.description_target_unlock_tablet)); - sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); - setContentDescription(null); - } - - /** - * Triggers haptic feedback. - */ - private synchronized void vibrate(long duration) { - final boolean hapticEnabled = Settings.System.getIntForUser( - mContext.getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 1, - UserHandle.USER_CURRENT) != 0; - if (hapticEnabled) { - if (mVibrator == null) { - mVibrator = (android.os.Vibrator) getContext() - .getSystemService(Context.VIBRATOR_SERVICE); - } - mVibrator.vibrate(duration, VIBRATION_ATTRIBUTES); - } - } - - /** - * Registers a callback to be invoked when the user triggers an event. - * - * @param listener the OnDialTriggerListener to attach to this view - */ - public void setOnTriggerListener(OnTriggerListener listener) { - mOnTriggerListener = listener; - } - - /** - * Dispatches a trigger event to listener. Ignored if a listener is not set. - * @param whichHandle the handle that triggered the event. - */ - private void dispatchTriggerEvent(int whichHandle) { - vibrate(VIBRATE_LONG); - if (mOnTriggerListener != null) { - mOnTriggerListener.onTrigger(this, whichHandle); - } - } - - /** - * Sets the current grabbed state, and dispatches a grabbed state change - * event to our listener. - */ - private void setGrabbedState(int newState) { - if (newState != mGrabbedState) { - mGrabbedState = newState; - if (mOnTriggerListener != null) { - mOnTriggerListener.onGrabbedStateChange(this, mGrabbedState); - } - } - } - - public interface OnTriggerListener { - /** - * Sent when the user releases the handle. - */ - public static final int NO_HANDLE = 0; - - /** - * Sent when the user grabs the center handle - */ - public static final int CENTER_HANDLE = 10; - - /** - * Called when the user drags the center ring beyond a threshold. - */ - void onTrigger(View v, int whichHandle); - - /** - * Called when the "grabbed state" changes (i.e. when the user either grabs or releases - * one of the handles.) - * - * @param v the view that was triggered - * @param grabbedState the new state: {@link #NO_HANDLE}, {@link #CENTER_HANDLE}, - */ - void onGrabbedStateChange(View v, int grabbedState); - } - - public void onAnimationUpdate(ValueAnimator animation) { - invalidate(); - } - - public void reset() { - if (DBG) Log.v(TAG, "reset() : resets state to STATE_RESET_LOCK"); - mLockState = STATE_RESET_LOCK; - invalidate(); - } -} diff --git a/core/java/com/android/internal/widget/multiwaveview/Ease.java b/core/java/com/android/internal/widget/multiwaveview/Ease.java deleted file mode 100644 index 7f90c44..0000000 --- a/core/java/com/android/internal/widget/multiwaveview/Ease.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.internal.widget.multiwaveview; - -import android.animation.TimeInterpolator; - -class Ease { - private static final float DOMAIN = 1.0f; - private static final float DURATION = 1.0f; - private static final float START = 0.0f; - - static class Linear { - public static final TimeInterpolator easeNone = new TimeInterpolator() { - public float getInterpolation(float input) { - return input; - } - }; - } - - static class Cubic { - public static final TimeInterpolator easeIn = new TimeInterpolator() { - public float getInterpolation(float input) { - return DOMAIN*(input/=DURATION)*input*input + START; - } - }; - public static final TimeInterpolator easeOut = new TimeInterpolator() { - public float getInterpolation(float input) { - return DOMAIN*((input=input/DURATION-1)*input*input + 1) + START; - } - }; - public static final TimeInterpolator easeInOut = new TimeInterpolator() { - public float getInterpolation(float input) { - return ((input/=DURATION/2) < 1.0f) ? - (DOMAIN/2*input*input*input + START) - : (DOMAIN/2*((input-=2)*input*input + 2) + START); - } - }; - } - - static class Quad { - public static final TimeInterpolator easeIn = new TimeInterpolator() { - public float getInterpolation (float input) { - return DOMAIN*(input/=DURATION)*input + START; - } - }; - public static final TimeInterpolator easeOut = new TimeInterpolator() { - public float getInterpolation(float input) { - return -DOMAIN *(input/=DURATION)*(input-2) + START; - } - }; - public static final TimeInterpolator easeInOut = new TimeInterpolator() { - public float getInterpolation(float input) { - return ((input/=DURATION/2) < 1) ? - (DOMAIN/2*input*input + START) - : (-DOMAIN/2 * ((--input)*(input-2) - 1) + START); - } - }; - } - - static class Quart { - public static final TimeInterpolator easeIn = new TimeInterpolator() { - public float getInterpolation(float input) { - return DOMAIN*(input/=DURATION)*input*input*input + START; - } - }; - public static final TimeInterpolator easeOut = new TimeInterpolator() { - public float getInterpolation(float input) { - return -DOMAIN * ((input=input/DURATION-1)*input*input*input - 1) + START; - } - }; - public static final TimeInterpolator easeInOut = new TimeInterpolator() { - public float getInterpolation(float input) { - return ((input/=DURATION/2) < 1) ? - (DOMAIN/2*input*input*input*input + START) - : (-DOMAIN/2 * ((input-=2)*input*input*input - 2) + START); - } - }; - } - - static class Quint { - public static final TimeInterpolator easeIn = new TimeInterpolator() { - public float getInterpolation(float input) { - return DOMAIN*(input/=DURATION)*input*input*input*input + START; - } - }; - public static final TimeInterpolator easeOut = new TimeInterpolator() { - public float getInterpolation(float input) { - return DOMAIN*((input=input/DURATION-1)*input*input*input*input + 1) + START; - } - }; - public static final TimeInterpolator easeInOut = new TimeInterpolator() { - public float getInterpolation(float input) { - return ((input/=DURATION/2) < 1) ? - (DOMAIN/2*input*input*input*input*input + START) - : (DOMAIN/2*((input-=2)*input*input*input*input + 2) + START); - } - }; - } - - static class Sine { - public static final TimeInterpolator easeIn = new TimeInterpolator() { - public float getInterpolation(float input) { - return -DOMAIN * (float) Math.cos(input/DURATION * (Math.PI/2)) + DOMAIN + START; - } - }; - public static final TimeInterpolator easeOut = new TimeInterpolator() { - public float getInterpolation(float input) { - return DOMAIN * (float) Math.sin(input/DURATION * (Math.PI/2)) + START; - } - }; - public static final TimeInterpolator easeInOut = new TimeInterpolator() { - public float getInterpolation(float input) { - return -DOMAIN/2 * ((float)Math.cos(Math.PI*input/DURATION) - 1.0f) + START; - } - }; - } - -} diff --git a/core/java/com/android/internal/widget/multiwaveview/GlowPadView.java b/core/java/com/android/internal/widget/multiwaveview/GlowPadView.java deleted file mode 100644 index 11ac19e..0000000 --- a/core/java/com/android/internal/widget/multiwaveview/GlowPadView.java +++ /dev/null @@ -1,1383 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.internal.widget.multiwaveview; - -import android.animation.Animator; -import android.animation.Animator.AnimatorListener; -import android.animation.AnimatorListenerAdapter; -import android.animation.TimeInterpolator; -import android.animation.ValueAnimator; -import android.animation.ValueAnimator.AnimatorUpdateListener; -import android.content.ComponentName; -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.drawable.Drawable; -import android.media.AudioAttributes; -import android.os.Bundle; -import android.os.UserHandle; -import android.os.Vibrator; -import android.provider.Settings; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.util.Log; -import android.util.TypedValue; -import android.view.Gravity; -import android.view.MotionEvent; -import android.view.View; -import android.view.accessibility.AccessibilityManager; - -import com.android.internal.R; - -import java.util.ArrayList; - -/** - * A re-usable widget containing a center, outer ring and wave animation. - */ -public class GlowPadView extends View { - private static final String TAG = "GlowPadView"; - private static final boolean DEBUG = false; - - // Wave state machine - private static final int STATE_IDLE = 0; - private static final int STATE_START = 1; - private static final int STATE_FIRST_TOUCH = 2; - private static final int STATE_TRACKING = 3; - private static final int STATE_SNAP = 4; - private static final int STATE_FINISH = 5; - - // Animation properties. - private static final float SNAP_MARGIN_DEFAULT = 20.0f; // distance to ring before we snap to it - - public interface OnTriggerListener { - int NO_HANDLE = 0; - int CENTER_HANDLE = 1; - public void onGrabbed(View v, int handle); - public void onReleased(View v, int handle); - public void onTrigger(View v, int target); - public void onGrabbedStateChange(View v, int handle); - public void onFinishFinalAnimation(); - } - - // Tuneable parameters for animation - private static final int WAVE_ANIMATION_DURATION = 1000; - private static final int RETURN_TO_HOME_DELAY = 1200; - private static final int RETURN_TO_HOME_DURATION = 200; - private static final int HIDE_ANIMATION_DELAY = 200; - private static final int HIDE_ANIMATION_DURATION = 200; - private static final int SHOW_ANIMATION_DURATION = 200; - private static final int SHOW_ANIMATION_DELAY = 50; - private static final int INITIAL_SHOW_HANDLE_DURATION = 200; - private static final int REVEAL_GLOW_DELAY = 0; - private static final int REVEAL_GLOW_DURATION = 0; - - private static final float TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED = 1.3f; - private static final float TARGET_SCALE_EXPANDED = 1.0f; - private static final float TARGET_SCALE_COLLAPSED = 0.8f; - private static final float RING_SCALE_EXPANDED = 1.0f; - private static final float RING_SCALE_COLLAPSED = 0.5f; - - private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) - .build(); - - private ArrayList<TargetDrawable> mTargetDrawables = new ArrayList<TargetDrawable>(); - private AnimationBundle mWaveAnimations = new AnimationBundle(); - private AnimationBundle mTargetAnimations = new AnimationBundle(); - private AnimationBundle mGlowAnimations = new AnimationBundle(); - private ArrayList<String> mTargetDescriptions; - private ArrayList<String> mDirectionDescriptions; - private OnTriggerListener mOnTriggerListener; - private TargetDrawable mHandleDrawable; - private TargetDrawable mOuterRing; - private Vibrator mVibrator; - - private int mFeedbackCount = 3; - private int mVibrationDuration = 0; - private int mGrabbedState; - private int mActiveTarget = -1; - private float mGlowRadius; - private float mWaveCenterX; - private float mWaveCenterY; - private int mMaxTargetHeight; - private int mMaxTargetWidth; - private float mRingScaleFactor = 1f; - private boolean mAllowScaling; - - private float mOuterRadius = 0.0f; - private float mSnapMargin = 0.0f; - private float mFirstItemOffset = 0.0f; - private boolean mMagneticTargets = false; - private boolean mDragging; - private int mNewTargetResources; - - private class AnimationBundle extends ArrayList<Tweener> { - private static final long serialVersionUID = 0xA84D78726F127468L; - private boolean mSuspended; - - public void start() { - if (mSuspended) return; // ignore attempts to start animations - final int count = size(); - for (int i = 0; i < count; i++) { - Tweener anim = get(i); - anim.animator.start(); - } - } - - public void cancel() { - final int count = size(); - for (int i = 0; i < count; i++) { - Tweener anim = get(i); - anim.animator.cancel(); - } - clear(); - } - - public void stop() { - final int count = size(); - for (int i = 0; i < count; i++) { - Tweener anim = get(i); - anim.animator.end(); - } - clear(); - } - - public void setSuspended(boolean suspend) { - mSuspended = suspend; - } - }; - - private AnimatorListener mResetListener = new AnimatorListenerAdapter() { - public void onAnimationEnd(Animator animator) { - switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY); - dispatchOnFinishFinalAnimation(); - } - }; - - private AnimatorListener mResetListenerWithPing = new AnimatorListenerAdapter() { - public void onAnimationEnd(Animator animator) { - ping(); - switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY); - dispatchOnFinishFinalAnimation(); - } - }; - - private AnimatorUpdateListener mUpdateListener = new AnimatorUpdateListener() { - public void onAnimationUpdate(ValueAnimator animation) { - invalidate(); - } - }; - - private boolean mAnimatingTargets; - private AnimatorListener mTargetUpdateListener = new AnimatorListenerAdapter() { - public void onAnimationEnd(Animator animator) { - if (mNewTargetResources != 0) { - internalSetTargetResources(mNewTargetResources); - mNewTargetResources = 0; - hideTargets(false, false); - } - mAnimatingTargets = false; - } - }; - private int mTargetResourceId; - private int mTargetDescriptionsResourceId; - private int mDirectionDescriptionsResourceId; - private boolean mAlwaysTrackFinger; - private int mHorizontalInset; - private int mVerticalInset; - private int mGravity = Gravity.TOP; - private boolean mInitialLayout = true; - private Tweener mBackgroundAnimator; - private PointCloud mPointCloud; - private float mInnerRadius; - private int mPointerId; - - public GlowPadView(Context context) { - this(context, null); - } - - public GlowPadView(Context context, AttributeSet attrs) { - super(context, attrs); - Resources res = context.getResources(); - - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.GlowPadView); - mInnerRadius = a.getDimension(R.styleable.GlowPadView_innerRadius, mInnerRadius); - mOuterRadius = a.getDimension(R.styleable.GlowPadView_outerRadius, mOuterRadius); - mSnapMargin = a.getDimension(R.styleable.GlowPadView_snapMargin, mSnapMargin); - mFirstItemOffset = (float) Math.toRadians( - a.getFloat(R.styleable.GlowPadView_firstItemOffset, - (float) Math.toDegrees(mFirstItemOffset))); - mVibrationDuration = a.getInt(R.styleable.GlowPadView_vibrationDuration, - mVibrationDuration); - mFeedbackCount = a.getInt(R.styleable.GlowPadView_feedbackCount, - mFeedbackCount); - mAllowScaling = a.getBoolean(R.styleable.GlowPadView_allowScaling, false); - TypedValue handle = a.peekValue(R.styleable.GlowPadView_handleDrawable); - mHandleDrawable = new TargetDrawable(res, handle != null ? handle.resourceId : 0); - mHandleDrawable.setState(TargetDrawable.STATE_INACTIVE); - mOuterRing = new TargetDrawable(res, - getResourceId(a, R.styleable.GlowPadView_outerRingDrawable)); - - mAlwaysTrackFinger = a.getBoolean(R.styleable.GlowPadView_alwaysTrackFinger, false); - mMagneticTargets = a.getBoolean(R.styleable.GlowPadView_magneticTargets, mMagneticTargets); - - int pointId = getResourceId(a, R.styleable.GlowPadView_pointDrawable); - Drawable pointDrawable = pointId != 0 ? context.getDrawable(pointId) : null; - mGlowRadius = a.getDimension(R.styleable.GlowPadView_glowRadius, 0.0f); - - mPointCloud = new PointCloud(pointDrawable); - mPointCloud.makePointCloud(mInnerRadius, mOuterRadius); - mPointCloud.glowManager.setRadius(mGlowRadius); - - TypedValue outValue = new TypedValue(); - - // Read array of target drawables - if (a.getValue(R.styleable.GlowPadView_targetDrawables, outValue)) { - internalSetTargetResources(outValue.resourceId); - } - if (mTargetDrawables == null || mTargetDrawables.size() == 0) { - throw new IllegalStateException("Must specify at least one target drawable"); - } - - // Read array of target descriptions - if (a.getValue(R.styleable.GlowPadView_targetDescriptions, outValue)) { - final int resourceId = outValue.resourceId; - if (resourceId == 0) { - throw new IllegalStateException("Must specify target descriptions"); - } - setTargetDescriptionsResourceId(resourceId); - } - - // Read array of direction descriptions - if (a.getValue(R.styleable.GlowPadView_directionDescriptions, outValue)) { - final int resourceId = outValue.resourceId; - if (resourceId == 0) { - throw new IllegalStateException("Must specify direction descriptions"); - } - setDirectionDescriptionsResourceId(resourceId); - } - - mGravity = a.getInt(R.styleable.GlowPadView_gravity, Gravity.TOP); - - a.recycle(); - - setVibrateEnabled(mVibrationDuration > 0); - - assignDefaultsIfNeeded(); - } - - private int getResourceId(TypedArray a, int id) { - TypedValue tv = a.peekValue(id); - return tv == null ? 0 : tv.resourceId; - } - - private void dump() { - Log.v(TAG, "Outer Radius = " + mOuterRadius); - Log.v(TAG, "SnapMargin = " + mSnapMargin); - Log.v(TAG, "FeedbackCount = " + mFeedbackCount); - Log.v(TAG, "VibrationDuration = " + mVibrationDuration); - Log.v(TAG, "GlowRadius = " + mGlowRadius); - Log.v(TAG, "WaveCenterX = " + mWaveCenterX); - Log.v(TAG, "WaveCenterY = " + mWaveCenterY); - } - - public void suspendAnimations() { - mWaveAnimations.setSuspended(true); - mTargetAnimations.setSuspended(true); - mGlowAnimations.setSuspended(true); - } - - public void resumeAnimations() { - mWaveAnimations.setSuspended(false); - mTargetAnimations.setSuspended(false); - mGlowAnimations.setSuspended(false); - mWaveAnimations.start(); - mTargetAnimations.start(); - mGlowAnimations.start(); - } - - @Override - protected int getSuggestedMinimumWidth() { - // View should be large enough to contain the background + handle and - // target drawable on either edge. - return (int) (Math.max(mOuterRing.getWidth(), 2 * mOuterRadius) + mMaxTargetWidth); - } - - @Override - protected int getSuggestedMinimumHeight() { - // View should be large enough to contain the unlock ring + target and - // target drawable on either edge - return (int) (Math.max(mOuterRing.getHeight(), 2 * mOuterRadius) + mMaxTargetHeight); - } - - /** - * This gets the suggested width accounting for the ring's scale factor. - */ - protected int getScaledSuggestedMinimumWidth() { - return (int) (mRingScaleFactor * Math.max(mOuterRing.getWidth(), 2 * mOuterRadius) - + mMaxTargetWidth); - } - - /** - * This gets the suggested height accounting for the ring's scale factor. - */ - protected int getScaledSuggestedMinimumHeight() { - return (int) (mRingScaleFactor * Math.max(mOuterRing.getHeight(), 2 * mOuterRadius) - + mMaxTargetHeight); - } - - private int resolveMeasured(int measureSpec, int desired) - { - int result = 0; - int specSize = MeasureSpec.getSize(measureSpec); - switch (MeasureSpec.getMode(measureSpec)) { - case MeasureSpec.UNSPECIFIED: - result = desired; - break; - case MeasureSpec.AT_MOST: - result = Math.min(specSize, desired); - break; - case MeasureSpec.EXACTLY: - default: - result = specSize; - } - return result; - } - - private void switchToState(int state, float x, float y) { - switch (state) { - case STATE_IDLE: - deactivateTargets(); - hideGlow(0, 0, 0.0f, null); - startBackgroundAnimation(0, 0.0f); - mHandleDrawable.setState(TargetDrawable.STATE_INACTIVE); - mHandleDrawable.setAlpha(1.0f); - break; - - case STATE_START: - startBackgroundAnimation(0, 0.0f); - break; - - case STATE_FIRST_TOUCH: - mHandleDrawable.setAlpha(0.0f); - deactivateTargets(); - showTargets(true); - startBackgroundAnimation(INITIAL_SHOW_HANDLE_DURATION, 1.0f); - setGrabbedState(OnTriggerListener.CENTER_HANDLE); - if (AccessibilityManager.getInstance(mContext).isEnabled()) { - announceTargets(); - } - break; - - case STATE_TRACKING: - mHandleDrawable.setAlpha(0.0f); - showGlow(REVEAL_GLOW_DURATION , REVEAL_GLOW_DELAY, 1.0f, null); - break; - - case STATE_SNAP: - // TODO: Add transition states (see list_selector_background_transition.xml) - mHandleDrawable.setAlpha(0.0f); - showGlow(REVEAL_GLOW_DURATION , REVEAL_GLOW_DELAY, 0.0f, null); - break; - - case STATE_FINISH: - doFinish(); - break; - } - } - - private void showGlow(int duration, int delay, float finalAlpha, - AnimatorListener finishListener) { - mGlowAnimations.cancel(); - mGlowAnimations.add(Tweener.to(mPointCloud.glowManager, duration, - "ease", Ease.Cubic.easeIn, - "delay", delay, - "alpha", finalAlpha, - "onUpdate", mUpdateListener, - "onComplete", finishListener)); - mGlowAnimations.start(); - } - - private void hideGlow(int duration, int delay, float finalAlpha, - AnimatorListener finishListener) { - mGlowAnimations.cancel(); - mGlowAnimations.add(Tweener.to(mPointCloud.glowManager, duration, - "ease", Ease.Quart.easeOut, - "delay", delay, - "alpha", finalAlpha, - "x", 0.0f, - "y", 0.0f, - "onUpdate", mUpdateListener, - "onComplete", finishListener)); - mGlowAnimations.start(); - } - - private void deactivateTargets() { - final int count = mTargetDrawables.size(); - for (int i = 0; i < count; i++) { - TargetDrawable target = mTargetDrawables.get(i); - target.setState(TargetDrawable.STATE_INACTIVE); - } - mActiveTarget = -1; - } - - /** - * Dispatches a trigger event to listener. Ignored if a listener is not set. - * @param whichTarget the target that was triggered. - */ - private void dispatchTriggerEvent(int whichTarget) { - vibrate(); - if (mOnTriggerListener != null) { - mOnTriggerListener.onTrigger(this, whichTarget); - } - } - - private void dispatchOnFinishFinalAnimation() { - if (mOnTriggerListener != null) { - mOnTriggerListener.onFinishFinalAnimation(); - } - } - - private void doFinish() { - final int activeTarget = mActiveTarget; - final boolean targetHit = activeTarget != -1; - - if (targetHit) { - if (DEBUG) Log.v(TAG, "Finish with target hit = " + targetHit); - - highlightSelected(activeTarget); - - // Inform listener of any active targets. Typically only one will be active. - hideGlow(RETURN_TO_HOME_DURATION, RETURN_TO_HOME_DELAY, 0.0f, mResetListener); - dispatchTriggerEvent(activeTarget); - if (!mAlwaysTrackFinger) { - // Force ring and targets to finish animation to final expanded state - mTargetAnimations.stop(); - } - } else { - // Animate handle back to the center based on current state. - hideGlow(HIDE_ANIMATION_DURATION, 0, 0.0f, mResetListenerWithPing); - hideTargets(true, false); - } - - setGrabbedState(OnTriggerListener.NO_HANDLE); - } - - private void highlightSelected(int activeTarget) { - // Highlight the given target and fade others - mTargetDrawables.get(activeTarget).setState(TargetDrawable.STATE_ACTIVE); - hideUnselected(activeTarget); - } - - private void hideUnselected(int active) { - for (int i = 0; i < mTargetDrawables.size(); i++) { - if (i != active) { - mTargetDrawables.get(i).setAlpha(0.0f); - } - } - } - - private void hideTargets(boolean animate, boolean expanded) { - mTargetAnimations.cancel(); - // Note: these animations should complete at the same time so that we can swap out - // the target assets asynchronously from the setTargetResources() call. - mAnimatingTargets = animate; - final int duration = animate ? HIDE_ANIMATION_DURATION : 0; - final int delay = animate ? HIDE_ANIMATION_DELAY : 0; - - final float targetScale = expanded ? - TARGET_SCALE_EXPANDED : TARGET_SCALE_COLLAPSED; - final int length = mTargetDrawables.size(); - final TimeInterpolator interpolator = Ease.Cubic.easeOut; - for (int i = 0; i < length; i++) { - TargetDrawable target = mTargetDrawables.get(i); - target.setState(TargetDrawable.STATE_INACTIVE); - mTargetAnimations.add(Tweener.to(target, duration, - "ease", interpolator, - "alpha", 0.0f, - "scaleX", targetScale, - "scaleY", targetScale, - "delay", delay, - "onUpdate", mUpdateListener)); - } - - float ringScaleTarget = expanded ? - RING_SCALE_EXPANDED : RING_SCALE_COLLAPSED; - ringScaleTarget *= mRingScaleFactor; - mTargetAnimations.add(Tweener.to(mOuterRing, duration, - "ease", interpolator, - "alpha", 0.0f, - "scaleX", ringScaleTarget, - "scaleY", ringScaleTarget, - "delay", delay, - "onUpdate", mUpdateListener, - "onComplete", mTargetUpdateListener)); - - mTargetAnimations.start(); - } - - private void showTargets(boolean animate) { - mTargetAnimations.stop(); - mAnimatingTargets = animate; - final int delay = animate ? SHOW_ANIMATION_DELAY : 0; - final int duration = animate ? SHOW_ANIMATION_DURATION : 0; - final int length = mTargetDrawables.size(); - for (int i = 0; i < length; i++) { - TargetDrawable target = mTargetDrawables.get(i); - target.setState(TargetDrawable.STATE_INACTIVE); - mTargetAnimations.add(Tweener.to(target, duration, - "ease", Ease.Cubic.easeOut, - "alpha", 1.0f, - "scaleX", 1.0f, - "scaleY", 1.0f, - "delay", delay, - "onUpdate", mUpdateListener)); - } - - float ringScale = mRingScaleFactor * RING_SCALE_EXPANDED; - mTargetAnimations.add(Tweener.to(mOuterRing, duration, - "ease", Ease.Cubic.easeOut, - "alpha", 1.0f, - "scaleX", ringScale, - "scaleY", ringScale, - "delay", delay, - "onUpdate", mUpdateListener, - "onComplete", mTargetUpdateListener)); - - mTargetAnimations.start(); - } - - private void vibrate() { - final boolean hapticEnabled = Settings.System.getIntForUser( - mContext.getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 1, - UserHandle.USER_CURRENT) != 0; - if (mVibrator != null && hapticEnabled) { - mVibrator.vibrate(mVibrationDuration, VIBRATION_ATTRIBUTES); - } - } - - private ArrayList<TargetDrawable> loadDrawableArray(int resourceId) { - Resources res = getContext().getResources(); - TypedArray array = res.obtainTypedArray(resourceId); - final int count = array.length(); - ArrayList<TargetDrawable> drawables = new ArrayList<TargetDrawable>(count); - for (int i = 0; i < count; i++) { - TypedValue value = array.peekValue(i); - TargetDrawable target = new TargetDrawable(res, value != null ? value.resourceId : 0); - drawables.add(target); - } - array.recycle(); - return drawables; - } - - private void internalSetTargetResources(int resourceId) { - final ArrayList<TargetDrawable> targets = loadDrawableArray(resourceId); - mTargetDrawables = targets; - mTargetResourceId = resourceId; - - int maxWidth = mHandleDrawable.getWidth(); - int maxHeight = mHandleDrawable.getHeight(); - final int count = targets.size(); - for (int i = 0; i < count; i++) { - TargetDrawable target = targets.get(i); - maxWidth = Math.max(maxWidth, target.getWidth()); - maxHeight = Math.max(maxHeight, target.getHeight()); - } - if (mMaxTargetWidth != maxWidth || mMaxTargetHeight != maxHeight) { - mMaxTargetWidth = maxWidth; - mMaxTargetHeight = maxHeight; - requestLayout(); // required to resize layout and call updateTargetPositions() - } else { - updateTargetPositions(mWaveCenterX, mWaveCenterY); - updatePointCloudPosition(mWaveCenterX, mWaveCenterY); - } - } - - /** - * Loads an array of drawables from the given resourceId. - * - * @param resourceId - */ - public void setTargetResources(int resourceId) { - if (mAnimatingTargets) { - // postpone this change until we return to the initial state - mNewTargetResources = resourceId; - } else { - internalSetTargetResources(resourceId); - } - } - - public int getTargetResourceId() { - return mTargetResourceId; - } - - /** - * Sets the resource id specifying the target descriptions for accessibility. - * - * @param resourceId The resource id. - */ - public void setTargetDescriptionsResourceId(int resourceId) { - mTargetDescriptionsResourceId = resourceId; - if (mTargetDescriptions != null) { - mTargetDescriptions.clear(); - } - } - - /** - * Gets the resource id specifying the target descriptions for accessibility. - * - * @return The resource id. - */ - public int getTargetDescriptionsResourceId() { - return mTargetDescriptionsResourceId; - } - - /** - * Sets the resource id specifying the target direction descriptions for accessibility. - * - * @param resourceId The resource id. - */ - public void setDirectionDescriptionsResourceId(int resourceId) { - mDirectionDescriptionsResourceId = resourceId; - if (mDirectionDescriptions != null) { - mDirectionDescriptions.clear(); - } - } - - /** - * Gets the resource id specifying the target direction descriptions. - * - * @return The resource id. - */ - public int getDirectionDescriptionsResourceId() { - return mDirectionDescriptionsResourceId; - } - - /** - * Enable or disable vibrate on touch. - * - * @param enabled - */ - public void setVibrateEnabled(boolean enabled) { - if (enabled && mVibrator == null) { - mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE); - } else { - mVibrator = null; - } - } - - /** - * Starts wave animation. - * - */ - public void ping() { - if (mFeedbackCount > 0) { - boolean doWaveAnimation = true; - final AnimationBundle waveAnimations = mWaveAnimations; - - // Don't do a wave if there's already one in progress - if (waveAnimations.size() > 0 && waveAnimations.get(0).animator.isRunning()) { - long t = waveAnimations.get(0).animator.getCurrentPlayTime(); - if (t < WAVE_ANIMATION_DURATION/2) { - doWaveAnimation = false; - } - } - - if (doWaveAnimation) { - startWaveAnimation(); - } - } - } - - private void stopAndHideWaveAnimation() { - mWaveAnimations.cancel(); - mPointCloud.waveManager.setAlpha(0.0f); - } - - private void startWaveAnimation() { - mWaveAnimations.cancel(); - mPointCloud.waveManager.setAlpha(1.0f); - mPointCloud.waveManager.setRadius(mHandleDrawable.getWidth()/2.0f); - mWaveAnimations.add(Tweener.to(mPointCloud.waveManager, WAVE_ANIMATION_DURATION, - "ease", Ease.Quad.easeOut, - "delay", 0, - "radius", 2.0f * mOuterRadius, - "onUpdate", mUpdateListener, - "onComplete", - new AnimatorListenerAdapter() { - public void onAnimationEnd(Animator animator) { - mPointCloud.waveManager.setRadius(0.0f); - mPointCloud.waveManager.setAlpha(0.0f); - } - })); - mWaveAnimations.start(); - } - - /** - * Resets the widget to default state and cancels all animation. If animate is 'true', will - * animate objects into place. Otherwise, objects will snap back to place. - * - * @param animate - */ - public void reset(boolean animate) { - mGlowAnimations.stop(); - mTargetAnimations.stop(); - startBackgroundAnimation(0, 0.0f); - stopAndHideWaveAnimation(); - hideTargets(animate, false); - hideGlow(0, 0, 0.0f, null); - Tweener.reset(); - } - - private void startBackgroundAnimation(int duration, float alpha) { - final Drawable background = getBackground(); - if (mAlwaysTrackFinger && background != null) { - if (mBackgroundAnimator != null) { - mBackgroundAnimator.animator.cancel(); - } - mBackgroundAnimator = Tweener.to(background, duration, - "ease", Ease.Cubic.easeIn, - "alpha", (int)(255.0f * alpha), - "delay", SHOW_ANIMATION_DELAY); - mBackgroundAnimator.animator.start(); - } - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - final int action = event.getActionMasked(); - boolean handled = false; - switch (action) { - case MotionEvent.ACTION_POINTER_DOWN: - case MotionEvent.ACTION_DOWN: - if (DEBUG) Log.v(TAG, "*** DOWN ***"); - handleDown(event); - handleMove(event); - handled = true; - break; - - case MotionEvent.ACTION_MOVE: - if (DEBUG) Log.v(TAG, "*** MOVE ***"); - handleMove(event); - handled = true; - break; - - case MotionEvent.ACTION_POINTER_UP: - case MotionEvent.ACTION_UP: - if (DEBUG) Log.v(TAG, "*** UP ***"); - handleMove(event); - handleUp(event); - handled = true; - break; - - case MotionEvent.ACTION_CANCEL: - if (DEBUG) Log.v(TAG, "*** CANCEL ***"); - handleMove(event); - handleCancel(event); - handled = true; - break; - - } - invalidate(); - return handled ? true : super.onTouchEvent(event); - } - - private void updateGlowPosition(float x, float y) { - float dx = x - mOuterRing.getX(); - float dy = y - mOuterRing.getY(); - dx *= 1f / mRingScaleFactor; - dy *= 1f / mRingScaleFactor; - mPointCloud.glowManager.setX(mOuterRing.getX() + dx); - mPointCloud.glowManager.setY(mOuterRing.getY() + dy); - } - - private void handleDown(MotionEvent event) { - int actionIndex = event.getActionIndex(); - float eventX = event.getX(actionIndex); - float eventY = event.getY(actionIndex); - switchToState(STATE_START, eventX, eventY); - if (!trySwitchToFirstTouchState(eventX, eventY)) { - mDragging = false; - } else { - mPointerId = event.getPointerId(actionIndex); - updateGlowPosition(eventX, eventY); - } - } - - private void handleUp(MotionEvent event) { - if (DEBUG && mDragging) Log.v(TAG, "** Handle RELEASE"); - int actionIndex = event.getActionIndex(); - if (event.getPointerId(actionIndex) == mPointerId) { - switchToState(STATE_FINISH, event.getX(actionIndex), event.getY(actionIndex)); - } - } - - private void handleCancel(MotionEvent event) { - if (DEBUG && mDragging) Log.v(TAG, "** Handle CANCEL"); - - // Drop the active target if canceled. - mActiveTarget = -1; - - int actionIndex = event.findPointerIndex(mPointerId); - actionIndex = actionIndex == -1 ? 0 : actionIndex; - switchToState(STATE_FINISH, event.getX(actionIndex), event.getY(actionIndex)); - } - - private void handleMove(MotionEvent event) { - int activeTarget = -1; - final int historySize = event.getHistorySize(); - ArrayList<TargetDrawable> targets = mTargetDrawables; - int ntargets = targets.size(); - float x = 0.0f; - float y = 0.0f; - float activeAngle = 0.0f; - int actionIndex = event.findPointerIndex(mPointerId); - - if (actionIndex == -1) { - return; // no data for this pointer - } - - for (int k = 0; k < historySize + 1; k++) { - float eventX = k < historySize ? event.getHistoricalX(actionIndex, k) - : event.getX(actionIndex); - float eventY = k < historySize ? event.getHistoricalY(actionIndex, k) - : event.getY(actionIndex); - // tx and ty are relative to wave center - float tx = eventX - mWaveCenterX; - float ty = eventY - mWaveCenterY; - float touchRadius = (float) Math.hypot(tx, ty); - final float scale = touchRadius > mOuterRadius ? mOuterRadius / touchRadius : 1.0f; - float limitX = tx * scale; - float limitY = ty * scale; - double angleRad = Math.atan2(-ty, tx); - - if (!mDragging) { - trySwitchToFirstTouchState(eventX, eventY); - } - - if (mDragging) { - // For multiple targets, snap to the one that matches - final float snapRadius = mRingScaleFactor * mOuterRadius - mSnapMargin; - final float snapDistance2 = snapRadius * snapRadius; - // Find first target in range - for (int i = 0; i < ntargets; i++) { - TargetDrawable target = targets.get(i); - - double targetMinRad = mFirstItemOffset + (i - 0.5) * 2 * Math.PI / ntargets; - double targetMaxRad = mFirstItemOffset + (i + 0.5) * 2 * Math.PI / ntargets; - if (target.isEnabled()) { - boolean angleMatches = - (angleRad > targetMinRad && angleRad <= targetMaxRad) || - (angleRad + 2 * Math.PI > targetMinRad && - angleRad + 2 * Math.PI <= targetMaxRad) || - (angleRad - 2 * Math.PI > targetMinRad && - angleRad - 2 * Math.PI <= targetMaxRad); - if (angleMatches && (dist2(tx, ty) > snapDistance2)) { - activeTarget = i; - activeAngle = (float) -angleRad; - } - } - } - } - x = limitX; - y = limitY; - } - - if (!mDragging) { - return; - } - - if (activeTarget != -1) { - switchToState(STATE_SNAP, x,y); - updateGlowPosition(x, y); - } else { - switchToState(STATE_TRACKING, x, y); - updateGlowPosition(x, y); - } - - if (mActiveTarget != activeTarget) { - // Defocus the old target - if (mActiveTarget != -1) { - TargetDrawable target = targets.get(mActiveTarget); - if (target.hasState(TargetDrawable.STATE_FOCUSED)) { - target.setState(TargetDrawable.STATE_INACTIVE); - } - if (mMagneticTargets) { - updateTargetPosition(mActiveTarget, mWaveCenterX, mWaveCenterY); - } - } - // Focus the new target - if (activeTarget != -1) { - TargetDrawable target = targets.get(activeTarget); - if (target.hasState(TargetDrawable.STATE_FOCUSED)) { - target.setState(TargetDrawable.STATE_FOCUSED); - } - if (mMagneticTargets) { - updateTargetPosition(activeTarget, mWaveCenterX, mWaveCenterY, activeAngle); - } - if (AccessibilityManager.getInstance(mContext).isEnabled()) { - String targetContentDescription = getTargetDescription(activeTarget); - announceForAccessibility(targetContentDescription); - } - } - } - mActiveTarget = activeTarget; - } - - @Override - public boolean onHoverEvent(MotionEvent event) { - if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) { - final int action = event.getAction(); - switch (action) { - case MotionEvent.ACTION_HOVER_ENTER: - event.setAction(MotionEvent.ACTION_DOWN); - break; - case MotionEvent.ACTION_HOVER_MOVE: - event.setAction(MotionEvent.ACTION_MOVE); - break; - case MotionEvent.ACTION_HOVER_EXIT: - event.setAction(MotionEvent.ACTION_UP); - break; - } - onTouchEvent(event); - event.setAction(action); - } - super.onHoverEvent(event); - return true; - } - - /** - * Sets the current grabbed state, and dispatches a grabbed state change - * event to our listener. - */ - private void setGrabbedState(int newState) { - if (newState != mGrabbedState) { - if (newState != OnTriggerListener.NO_HANDLE) { - vibrate(); - } - mGrabbedState = newState; - if (mOnTriggerListener != null) { - if (newState == OnTriggerListener.NO_HANDLE) { - mOnTriggerListener.onReleased(this, OnTriggerListener.CENTER_HANDLE); - } else { - mOnTriggerListener.onGrabbed(this, OnTriggerListener.CENTER_HANDLE); - } - mOnTriggerListener.onGrabbedStateChange(this, newState); - } - } - } - - private boolean trySwitchToFirstTouchState(float x, float y) { - final float tx = x - mWaveCenterX; - final float ty = y - mWaveCenterY; - if (mAlwaysTrackFinger || dist2(tx,ty) <= getScaledGlowRadiusSquared()) { - if (DEBUG) Log.v(TAG, "** Handle HIT"); - switchToState(STATE_FIRST_TOUCH, x, y); - updateGlowPosition(tx, ty); - mDragging = true; - return true; - } - return false; - } - - private void assignDefaultsIfNeeded() { - if (mOuterRadius == 0.0f) { - mOuterRadius = Math.max(mOuterRing.getWidth(), mOuterRing.getHeight())/2.0f; - } - if (mSnapMargin == 0.0f) { - mSnapMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, - SNAP_MARGIN_DEFAULT, getContext().getResources().getDisplayMetrics()); - } - if (mInnerRadius == 0.0f) { - mInnerRadius = mHandleDrawable.getWidth() / 10.0f; - } - } - - private void computeInsets(int dx, int dy) { - final int layoutDirection = getLayoutDirection(); - final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection); - - switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { - case Gravity.LEFT: - mHorizontalInset = 0; - break; - case Gravity.RIGHT: - mHorizontalInset = dx; - break; - case Gravity.CENTER_HORIZONTAL: - default: - mHorizontalInset = dx / 2; - break; - } - switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) { - case Gravity.TOP: - mVerticalInset = 0; - break; - case Gravity.BOTTOM: - mVerticalInset = dy; - break; - case Gravity.CENTER_VERTICAL: - default: - mVerticalInset = dy / 2; - break; - } - } - - /** - * Given the desired width and height of the ring and the allocated width and height, compute - * how much we need to scale the ring. - */ - private float computeScaleFactor(int desiredWidth, int desiredHeight, - int actualWidth, int actualHeight) { - - // Return unity if scaling is not allowed. - if (!mAllowScaling) return 1f; - - final int layoutDirection = getLayoutDirection(); - final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection); - - float scaleX = 1f; - float scaleY = 1f; - - // We use the gravity as a cue for whether we want to scale on a particular axis. - // We only scale to fit horizontally if we're not pinned to the left or right. Likewise, - // we only scale to fit vertically if we're not pinned to the top or bottom. In these - // cases, we want the ring to hang off the side or top/bottom, respectively. - switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { - case Gravity.LEFT: - case Gravity.RIGHT: - break; - case Gravity.CENTER_HORIZONTAL: - default: - if (desiredWidth > actualWidth) { - scaleX = (1f * actualWidth - mMaxTargetWidth) / - (desiredWidth - mMaxTargetWidth); - } - break; - } - switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) { - case Gravity.TOP: - case Gravity.BOTTOM: - break; - case Gravity.CENTER_VERTICAL: - default: - if (desiredHeight > actualHeight) { - scaleY = (1f * actualHeight - mMaxTargetHeight) / - (desiredHeight - mMaxTargetHeight); - } - break; - } - return Math.min(scaleX, scaleY); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - final int minimumWidth = getSuggestedMinimumWidth(); - final int minimumHeight = getSuggestedMinimumHeight(); - int computedWidth = resolveMeasured(widthMeasureSpec, minimumWidth); - int computedHeight = resolveMeasured(heightMeasureSpec, minimumHeight); - - mRingScaleFactor = computeScaleFactor(minimumWidth, minimumHeight, - computedWidth, computedHeight); - - int scaledWidth = getScaledSuggestedMinimumWidth(); - int scaledHeight = getScaledSuggestedMinimumHeight(); - - computeInsets(computedWidth - scaledWidth, computedHeight - scaledHeight); - setMeasuredDimension(computedWidth, computedHeight); - } - - private float getRingWidth() { - return mRingScaleFactor * Math.max(mOuterRing.getWidth(), 2 * mOuterRadius); - } - - private float getRingHeight() { - return mRingScaleFactor * Math.max(mOuterRing.getHeight(), 2 * mOuterRadius); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - final int width = right - left; - final int height = bottom - top; - - // Target placement width/height. This puts the targets on the greater of the ring - // width or the specified outer radius. - final float placementWidth = getRingWidth(); - final float placementHeight = getRingHeight(); - float newWaveCenterX = mHorizontalInset - + Math.max(width, mMaxTargetWidth + placementWidth) / 2; - float newWaveCenterY = mVerticalInset - + Math.max(height, + mMaxTargetHeight + placementHeight) / 2; - - if (mInitialLayout) { - stopAndHideWaveAnimation(); - hideTargets(false, false); - mInitialLayout = false; - } - - mOuterRing.setPositionX(newWaveCenterX); - mOuterRing.setPositionY(newWaveCenterY); - - mPointCloud.setScale(mRingScaleFactor); - - mHandleDrawable.setPositionX(newWaveCenterX); - mHandleDrawable.setPositionY(newWaveCenterY); - - updateTargetPositions(newWaveCenterX, newWaveCenterY); - updatePointCloudPosition(newWaveCenterX, newWaveCenterY); - updateGlowPosition(newWaveCenterX, newWaveCenterY); - - mWaveCenterX = newWaveCenterX; - mWaveCenterY = newWaveCenterY; - - if (DEBUG) dump(); - } - - private void updateTargetPosition(int i, float centerX, float centerY) { - final float angle = getAngle(getSliceAngle(), i); - updateTargetPosition(i, centerX, centerY, angle); - } - - private void updateTargetPosition(int i, float centerX, float centerY, float angle) { - final float placementRadiusX = getRingWidth() / 2; - final float placementRadiusY = getRingHeight() / 2; - if (i >= 0) { - ArrayList<TargetDrawable> targets = mTargetDrawables; - final TargetDrawable targetIcon = targets.get(i); - targetIcon.setPositionX(centerX); - targetIcon.setPositionY(centerY); - targetIcon.setX(placementRadiusX * (float) Math.cos(angle)); - targetIcon.setY(placementRadiusY * (float) Math.sin(angle)); - } - } - - private void updateTargetPositions(float centerX, float centerY) { - updateTargetPositions(centerX, centerY, false); - } - - private void updateTargetPositions(float centerX, float centerY, boolean skipActive) { - final int size = mTargetDrawables.size(); - final float alpha = getSliceAngle(); - // Reposition the target drawables if the view changed. - for (int i = 0; i < size; i++) { - if (!skipActive || i != mActiveTarget) { - updateTargetPosition(i, centerX, centerY, getAngle(alpha, i)); - } - } - } - - private float getAngle(float alpha, int i) { - return mFirstItemOffset + alpha * i; - } - - private float getSliceAngle() { - return (float) (-2.0f * Math.PI / mTargetDrawables.size()); - } - - private void updatePointCloudPosition(float centerX, float centerY) { - mPointCloud.setCenter(centerX, centerY); - } - - @Override - protected void onDraw(Canvas canvas) { - mPointCloud.draw(canvas); - mOuterRing.draw(canvas); - final int ntargets = mTargetDrawables.size(); - for (int i = 0; i < ntargets; i++) { - TargetDrawable target = mTargetDrawables.get(i); - if (target != null) { - target.draw(canvas); - } - } - mHandleDrawable.draw(canvas); - } - - public void setOnTriggerListener(OnTriggerListener listener) { - mOnTriggerListener = listener; - } - - private float square(float d) { - return d * d; - } - - private float dist2(float dx, float dy) { - return dx*dx + dy*dy; - } - - private float getScaledGlowRadiusSquared() { - final float scaledTapRadius; - if (AccessibilityManager.getInstance(mContext).isEnabled()) { - scaledTapRadius = TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED * mGlowRadius; - } else { - scaledTapRadius = mGlowRadius; - } - return square(scaledTapRadius); - } - - private void announceTargets() { - StringBuilder utterance = new StringBuilder(); - final int targetCount = mTargetDrawables.size(); - for (int i = 0; i < targetCount; i++) { - String targetDescription = getTargetDescription(i); - String directionDescription = getDirectionDescription(i); - if (!TextUtils.isEmpty(targetDescription) - && !TextUtils.isEmpty(directionDescription)) { - String text = String.format(directionDescription, targetDescription); - utterance.append(text); - } - } - if (utterance.length() > 0) { - announceForAccessibility(utterance.toString()); - } - } - - private String getTargetDescription(int index) { - if (mTargetDescriptions == null || mTargetDescriptions.isEmpty()) { - mTargetDescriptions = loadDescriptions(mTargetDescriptionsResourceId); - if (mTargetDrawables.size() != mTargetDescriptions.size()) { - Log.w(TAG, "The number of target drawables must be" - + " equal to the number of target descriptions."); - return null; - } - } - return mTargetDescriptions.get(index); - } - - private String getDirectionDescription(int index) { - if (mDirectionDescriptions == null || mDirectionDescriptions.isEmpty()) { - mDirectionDescriptions = loadDescriptions(mDirectionDescriptionsResourceId); - if (mTargetDrawables.size() != mDirectionDescriptions.size()) { - Log.w(TAG, "The number of target drawables must be" - + " equal to the number of direction descriptions."); - return null; - } - } - return mDirectionDescriptions.get(index); - } - - private ArrayList<String> loadDescriptions(int resourceId) { - TypedArray array = getContext().getResources().obtainTypedArray(resourceId); - final int count = array.length(); - ArrayList<String> targetContentDescriptions = new ArrayList<String>(count); - for (int i = 0; i < count; i++) { - String contentDescription = array.getString(i); - targetContentDescriptions.add(contentDescription); - } - array.recycle(); - return targetContentDescriptions; - } - - public int getResourceIdForTarget(int index) { - final TargetDrawable drawable = mTargetDrawables.get(index); - return drawable == null ? 0 : drawable.getResourceId(); - } - - public void setEnableTarget(int resourceId, boolean enabled) { - for (int i = 0; i < mTargetDrawables.size(); i++) { - final TargetDrawable target = mTargetDrawables.get(i); - if (target.getResourceId() == resourceId) { - target.setEnabled(enabled); - break; // should never be more than one match - } - } - } - - /** - * Gets the position of a target in the array that matches the given resource. - * @param resourceId - * @return the index or -1 if not found - */ - public int getTargetPosition(int resourceId) { - for (int i = 0; i < mTargetDrawables.size(); i++) { - final TargetDrawable target = mTargetDrawables.get(i); - if (target.getResourceId() == resourceId) { - return i; // should never be more than one match - } - } - return -1; - } - - private boolean replaceTargetDrawables(Resources res, int existingResourceId, - int newResourceId) { - if (existingResourceId == 0 || newResourceId == 0) { - return false; - } - - boolean result = false; - final ArrayList<TargetDrawable> drawables = mTargetDrawables; - final int size = drawables.size(); - for (int i = 0; i < size; i++) { - final TargetDrawable target = drawables.get(i); - if (target != null && target.getResourceId() == existingResourceId) { - target.setDrawable(res, newResourceId); - result = true; - } - } - - if (result) { - requestLayout(); // in case any given drawable's size changes - } - - return result; - } - - /** - * Searches the given package for a resource to use to replace the Drawable on the - * target with the given resource id - * @param component of the .apk that contains the resource - * @param name of the metadata in the .apk - * @param existingResId the resource id of the target to search for - * @return true if found in the given package and replaced at least one target Drawables - */ - public boolean replaceTargetDrawablesIfPresent(ComponentName component, String name, - int existingResId) { - if (existingResId == 0) return false; - - boolean replaced = false; - if (component != null) { - try { - PackageManager packageManager = mContext.getPackageManager(); - // Look for the search icon specified in the activity meta-data - Bundle metaData = packageManager.getActivityInfo( - component, PackageManager.GET_META_DATA).metaData; - if (metaData != null) { - int iconResId = metaData.getInt(name); - if (iconResId != 0) { - Resources res = packageManager.getResourcesForActivity(component); - replaced = replaceTargetDrawables(res, existingResId, iconResId); - } - } - } catch (NameNotFoundException e) { - Log.w(TAG, "Failed to swap drawable; " - + component.flattenToShortString() + " not found", e); - } catch (Resources.NotFoundException nfe) { - Log.w(TAG, "Failed to swap drawable from " - + component.flattenToShortString(), nfe); - } - } - if (!replaced) { - // Restore the original drawable - replaceTargetDrawables(mContext.getResources(), existingResId, existingResId); - } - return replaced; - } -} diff --git a/core/java/com/android/internal/widget/multiwaveview/PointCloud.java b/core/java/com/android/internal/widget/multiwaveview/PointCloud.java deleted file mode 100644 index 6f26b99..0000000 --- a/core/java/com/android/internal/widget/multiwaveview/PointCloud.java +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.internal.widget.multiwaveview; - -import java.util.ArrayList; - -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.drawable.Drawable; -import android.util.Log; - -public class PointCloud { - private static final float MIN_POINT_SIZE = 2.0f; - private static final float MAX_POINT_SIZE = 4.0f; - private static final int INNER_POINTS = 8; - private static final String TAG = "PointCloud"; - private ArrayList<Point> mPointCloud = new ArrayList<Point>(); - private Drawable mDrawable; - private float mCenterX; - private float mCenterY; - private Paint mPaint; - private float mScale = 1.0f; - private static final float PI = (float) Math.PI; - - // These allow us to have multiple concurrent animations. - WaveManager waveManager = new WaveManager(); - GlowManager glowManager = new GlowManager(); - private float mOuterRadius; - - public class WaveManager { - private float radius = 50; - private float alpha = 0.0f; - - public void setRadius(float r) { - radius = r; - } - - public float getRadius() { - return radius; - } - - public void setAlpha(float a) { - alpha = a; - } - - public float getAlpha() { - return alpha; - } - }; - - public class GlowManager { - private float x; - private float y; - private float radius = 0.0f; - private float alpha = 0.0f; - - public void setX(float x1) { - x = x1; - } - - public float getX() { - return x; - } - - public void setY(float y1) { - y = y1; - } - - public float getY() { - return y; - } - - public void setAlpha(float a) { - alpha = a; - } - - public float getAlpha() { - return alpha; - } - - public void setRadius(float r) { - radius = r; - } - - public float getRadius() { - return radius; - } - } - - class Point { - float x; - float y; - float radius; - - public Point(float x2, float y2, float r) { - x = (float) x2; - y = (float) y2; - radius = r; - } - } - - public PointCloud(Drawable drawable) { - mPaint = new Paint(); - mPaint.setFilterBitmap(true); - mPaint.setColor(Color.rgb(255, 255, 255)); // TODO: make configurable - mPaint.setAntiAlias(true); - mPaint.setDither(true); - - mDrawable = drawable; - if (mDrawable != null) { - drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); - } - } - - public void setCenter(float x, float y) { - mCenterX = x; - mCenterY = y; - } - - public void makePointCloud(float innerRadius, float outerRadius) { - if (innerRadius == 0) { - Log.w(TAG, "Must specify an inner radius"); - return; - } - mOuterRadius = outerRadius; - mPointCloud.clear(); - final float pointAreaRadius = (outerRadius - innerRadius); - final float ds = (2.0f * PI * innerRadius / INNER_POINTS); - final int bands = (int) Math.round(pointAreaRadius / ds); - final float dr = pointAreaRadius / bands; - float r = innerRadius; - for (int b = 0; b <= bands; b++, r += dr) { - float circumference = 2.0f * PI * r; - final int pointsInBand = (int) (circumference / ds); - float eta = PI/2.0f; - float dEta = 2.0f * PI / pointsInBand; - for (int i = 0; i < pointsInBand; i++) { - float x = r * (float) Math.cos(eta); - float y = r * (float) Math.sin(eta); - eta += dEta; - mPointCloud.add(new Point(x, y, r)); - } - } - } - - public void setScale(float scale) { - mScale = scale; - } - - public float getScale() { - return mScale; - } - - public int getAlphaForPoint(Point point) { - // Contribution from positional glow - float glowDistance = (float) Math.hypot(glowManager.x - point.x, glowManager.y - point.y); - float glowAlpha = 0.0f; - if (glowDistance < glowManager.radius) { - float cosf = (float) Math.cos(PI * 0.25f * glowDistance / glowManager.radius); - glowAlpha = glowManager.alpha * Math.max(0.0f, (float) Math.pow(cosf, 10.0f)); - } - - // Compute contribution from Wave - float radius = (float) Math.hypot(point.x, point.y); - float waveAlpha = 0.0f; - if (radius < waveManager.radius * 2) { - float distanceToWaveRing = (radius - waveManager.radius); - float cosf = (float) Math.cos(PI * 0.5f * distanceToWaveRing / waveManager.radius); - waveAlpha = waveManager.alpha * Math.max(0.0f, (float) Math.pow(cosf, 6.0f)); - } - return (int) (Math.max(glowAlpha, waveAlpha) * 255); - } - - private float interp(float min, float max, float f) { - return min + (max - min) * f; - } - - public void draw(Canvas canvas) { - ArrayList<Point> points = mPointCloud; - canvas.save(Canvas.MATRIX_SAVE_FLAG); - canvas.scale(mScale, mScale, mCenterX, mCenterY); - for (int i = 0; i < points.size(); i++) { - Point point = points.get(i); - final float pointSize = interp(MAX_POINT_SIZE, MIN_POINT_SIZE, - point.radius / mOuterRadius); - final float px = point.x + mCenterX; - final float py = point.y + mCenterY; - int alpha = getAlphaForPoint(point); - - if (alpha == 0) continue; - - if (mDrawable != null) { - canvas.save(Canvas.MATRIX_SAVE_FLAG); - final float cx = mDrawable.getIntrinsicWidth() * 0.5f; - final float cy = mDrawable.getIntrinsicHeight() * 0.5f; - final float s = pointSize / MAX_POINT_SIZE; - canvas.scale(s, s, px, py); - canvas.translate(px - cx, py - cy); - mDrawable.setAlpha(alpha); - mDrawable.draw(canvas); - canvas.restore(); - } else { - mPaint.setAlpha(alpha); - canvas.drawCircle(px, py, pointSize, mPaint); - } - } - canvas.restore(); - } - -} diff --git a/core/java/com/android/internal/widget/multiwaveview/TargetDrawable.java b/core/java/com/android/internal/widget/multiwaveview/TargetDrawable.java deleted file mode 100644 index 5a4c441..0000000 --- a/core/java/com/android/internal/widget/multiwaveview/TargetDrawable.java +++ /dev/null @@ -1,229 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.internal.widget.multiwaveview; - -import android.content.res.Resources; -import android.graphics.Canvas; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.StateListDrawable; -import android.util.Log; - -public class TargetDrawable { - private static final String TAG = "TargetDrawable"; - private static final boolean DEBUG = false; - - public static final int[] STATE_ACTIVE = - { android.R.attr.state_enabled, android.R.attr.state_active }; - public static final int[] STATE_INACTIVE = - { android.R.attr.state_enabled, -android.R.attr.state_active }; - public static final int[] STATE_FOCUSED = - { android.R.attr.state_enabled, -android.R.attr.state_active, - android.R.attr.state_focused }; - - private float mTranslationX = 0.0f; - private float mTranslationY = 0.0f; - private float mPositionX = 0.0f; - private float mPositionY = 0.0f; - private float mScaleX = 1.0f; - private float mScaleY = 1.0f; - private float mAlpha = 1.0f; - private Drawable mDrawable; - private boolean mEnabled = true; - private final int mResourceId; - - public TargetDrawable(Resources res, int resId) { - mResourceId = resId; - setDrawable(res, resId); - } - - public void setDrawable(Resources res, int resId) { - // Note we explicitly don't set mResourceId to resId since we allow the drawable to be - // swapped at runtime and want to re-use the existing resource id for identification. - Drawable drawable = resId == 0 ? null : res.getDrawable(resId); - // Mutate the drawable so we can animate shared drawable properties. - mDrawable = drawable != null ? drawable.mutate() : null; - resizeDrawables(); - setState(STATE_INACTIVE); - } - - public TargetDrawable(TargetDrawable other) { - mResourceId = other.mResourceId; - // Mutate the drawable so we can animate shared drawable properties. - mDrawable = other.mDrawable != null ? other.mDrawable.mutate() : null; - resizeDrawables(); - setState(STATE_INACTIVE); - } - - public void setState(int [] state) { - if (mDrawable instanceof StateListDrawable) { - StateListDrawable d = (StateListDrawable) mDrawable; - d.setState(state); - } - } - - public boolean hasState(int [] state) { - if (mDrawable instanceof StateListDrawable) { - StateListDrawable d = (StateListDrawable) mDrawable; - // TODO: this doesn't seem to work - return d.getStateDrawableIndex(state) != -1; - } - return false; - } - - /** - * Returns true if the drawable is a StateListDrawable and is in the focused state. - * - * @return - */ - public boolean isActive() { - if (mDrawable instanceof StateListDrawable) { - StateListDrawable d = (StateListDrawable) mDrawable; - int[] states = d.getState(); - for (int i = 0; i < states.length; i++) { - if (states[i] == android.R.attr.state_focused) { - return true; - } - } - } - return false; - } - - /** - * Returns true if this target is enabled. Typically an enabled target contains a valid - * drawable in a valid state. Currently all targets with valid drawables are valid. - * - * @return - */ - public boolean isEnabled() { - return mDrawable != null && mEnabled; - } - - /** - * Makes drawables in a StateListDrawable all the same dimensions. - * If not a StateListDrawable, then justs sets the bounds to the intrinsic size of the - * drawable. - */ - private void resizeDrawables() { - if (mDrawable instanceof StateListDrawable) { - StateListDrawable d = (StateListDrawable) mDrawable; - int maxWidth = 0; - int maxHeight = 0; - for (int i = 0; i < d.getStateCount(); i++) { - Drawable childDrawable = d.getStateDrawable(i); - maxWidth = Math.max(maxWidth, childDrawable.getIntrinsicWidth()); - maxHeight = Math.max(maxHeight, childDrawable.getIntrinsicHeight()); - } - if (DEBUG) Log.v(TAG, "union of childDrawable rects " + d + " to: " - + maxWidth + "x" + maxHeight); - d.setBounds(0, 0, maxWidth, maxHeight); - for (int i = 0; i < d.getStateCount(); i++) { - Drawable childDrawable = d.getStateDrawable(i); - if (DEBUG) Log.v(TAG, "sizing drawable " + childDrawable + " to: " - + maxWidth + "x" + maxHeight); - childDrawable.setBounds(0, 0, maxWidth, maxHeight); - } - } else if (mDrawable != null) { - mDrawable.setBounds(0, 0, - mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight()); - } - } - - public void setX(float x) { - mTranslationX = x; - } - - public void setY(float y) { - mTranslationY = y; - } - - public void setScaleX(float x) { - mScaleX = x; - } - - public void setScaleY(float y) { - mScaleY = y; - } - - public void setAlpha(float alpha) { - mAlpha = alpha; - } - - public float getX() { - return mTranslationX; - } - - public float getY() { - return mTranslationY; - } - - public float getScaleX() { - return mScaleX; - } - - public float getScaleY() { - return mScaleY; - } - - public float getAlpha() { - return mAlpha; - } - - public void setPositionX(float x) { - mPositionX = x; - } - - public void setPositionY(float y) { - mPositionY = y; - } - - public float getPositionX() { - return mPositionX; - } - - public float getPositionY() { - return mPositionY; - } - - public int getWidth() { - return mDrawable != null ? mDrawable.getIntrinsicWidth() : 0; - } - - public int getHeight() { - return mDrawable != null ? mDrawable.getIntrinsicHeight() : 0; - } - - public void draw(Canvas canvas) { - if (mDrawable == null || !mEnabled) { - return; - } - canvas.save(Canvas.MATRIX_SAVE_FLAG); - canvas.scale(mScaleX, mScaleY, mPositionX, mPositionY); - canvas.translate(mTranslationX + mPositionX, mTranslationY + mPositionY); - canvas.translate(-0.5f * getWidth(), -0.5f * getHeight()); - mDrawable.setAlpha((int) Math.round(mAlpha * 255f)); - mDrawable.draw(canvas); - canvas.restore(); - } - - public void setEnabled(boolean enabled) { - mEnabled = enabled; - } - - public int getResourceId() { - return mResourceId; - } -} diff --git a/core/java/com/android/internal/widget/multiwaveview/Tweener.java b/core/java/com/android/internal/widget/multiwaveview/Tweener.java deleted file mode 100644 index d559d9d..0000000 --- a/core/java/com/android/internal/widget/multiwaveview/Tweener.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.internal.widget.multiwaveview; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map.Entry; - -import android.animation.Animator.AnimatorListener; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; -import android.animation.PropertyValuesHolder; -import android.animation.TimeInterpolator; -import android.animation.ValueAnimator.AnimatorUpdateListener; -import android.util.Log; - -class Tweener { - private static final String TAG = "Tweener"; - private static final boolean DEBUG = false; - - ObjectAnimator animator; - private static HashMap<Object, Tweener> sTweens = new HashMap<Object, Tweener>(); - - public Tweener(ObjectAnimator anim) { - animator = anim; - } - - private static void remove(Animator animator) { - Iterator<Entry<Object, Tweener>> iter = sTweens.entrySet().iterator(); - while (iter.hasNext()) { - Entry<Object, Tweener> entry = iter.next(); - if (entry.getValue().animator == animator) { - if (DEBUG) Log.v(TAG, "Removing tweener " + sTweens.get(entry.getKey()) - + " sTweens.size() = " + sTweens.size()); - iter.remove(); - break; // an animator can only be attached to one object - } - } - } - - public static Tweener to(Object object, long duration, Object... vars) { - long delay = 0; - AnimatorUpdateListener updateListener = null; - AnimatorListener listener = null; - TimeInterpolator interpolator = null; - - // Iterate through arguments and discover properties to animate - ArrayList<PropertyValuesHolder> props = new ArrayList<PropertyValuesHolder>(vars.length/2); - for (int i = 0; i < vars.length; i+=2) { - if (!(vars[i] instanceof String)) { - throw new IllegalArgumentException("Key must be a string: " + vars[i]); - } - String key = (String) vars[i]; - Object value = vars[i+1]; - if ("simultaneousTween".equals(key)) { - // TODO - } else if ("ease".equals(key)) { - interpolator = (TimeInterpolator) value; // TODO: multiple interpolators? - } else if ("onUpdate".equals(key) || "onUpdateListener".equals(key)) { - updateListener = (AnimatorUpdateListener) value; - } else if ("onComplete".equals(key) || "onCompleteListener".equals(key)) { - listener = (AnimatorListener) value; - } else if ("delay".equals(key)) { - delay = ((Number) value).longValue(); - } else if ("syncWith".equals(key)) { - // TODO - } else if (value instanceof float[]) { - props.add(PropertyValuesHolder.ofFloat(key, - ((float[])value)[0], ((float[])value)[1])); - } else if (value instanceof int[]) { - props.add(PropertyValuesHolder.ofInt(key, - ((int[])value)[0], ((int[])value)[1])); - } else if (value instanceof Number) { - float floatValue = ((Number)value).floatValue(); - props.add(PropertyValuesHolder.ofFloat(key, floatValue)); - } else { - throw new IllegalArgumentException( - "Bad argument for key \"" + key + "\" with value " + value.getClass()); - } - } - - // Re-use existing tween, if present - Tweener tween = sTweens.get(object); - ObjectAnimator anim = null; - if (tween == null) { - anim = ObjectAnimator.ofPropertyValuesHolder(object, - props.toArray(new PropertyValuesHolder[props.size()])); - tween = new Tweener(anim); - sTweens.put(object, tween); - if (DEBUG) Log.v(TAG, "Added new Tweener " + tween); - } else { - anim = sTweens.get(object).animator; - replace(props, object); // Cancel all animators for given object - } - - if (interpolator != null) { - anim.setInterpolator(interpolator); - } - - // Update animation with properties discovered in loop above - anim.setStartDelay(delay); - anim.setDuration(duration); - if (updateListener != null) { - anim.removeAllUpdateListeners(); // There should be only one - anim.addUpdateListener(updateListener); - } - if (listener != null) { - anim.removeAllListeners(); // There should be only one. - anim.addListener(listener); - } - anim.addListener(mCleanupListener); - - return tween; - } - - Tweener from(Object object, long duration, Object... vars) { - // TODO: for v of vars - // toVars[v] = object[v] - // object[v] = vars[v] - return Tweener.to(object, duration, vars); - } - - // Listener to watch for completed animations and remove them. - private static AnimatorListener mCleanupListener = new AnimatorListenerAdapter() { - - @Override - public void onAnimationEnd(Animator animation) { - remove(animation); - } - - @Override - public void onAnimationCancel(Animator animation) { - remove(animation); - } - }; - - public static void reset() { - if (DEBUG) { - Log.v(TAG, "Reset()"); - if (sTweens.size() > 0) { - Log.v(TAG, "Cleaning up " + sTweens.size() + " animations"); - } - } - sTweens.clear(); - } - - private static void replace(ArrayList<PropertyValuesHolder> props, Object... args) { - for (final Object killobject : args) { - Tweener tween = sTweens.get(killobject); - if (tween != null) { - tween.animator.cancel(); - if (props != null) { - tween.animator.setValues( - props.toArray(new PropertyValuesHolder[props.size()])); - } else { - sTweens.remove(tween); - } - } - } - } -} diff --git a/core/java/com/android/server/backup/AccountSyncSettingsBackupHelper.java b/core/java/com/android/server/backup/AccountSyncSettingsBackupHelper.java new file mode 100644 index 0000000..3f4b980 --- /dev/null +++ b/core/java/com/android/server/backup/AccountSyncSettingsBackupHelper.java @@ -0,0 +1,380 @@ +/* + * 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 com.android.server.backup; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.app.backup.BackupDataInputStream; +import android.app.backup.BackupDataOutput; +import android.app.backup.BackupHelper; +import android.content.ContentResolver; +import android.content.Context; +import android.content.SyncAdapterType; +import android.content.SyncStatusObserver; +import android.os.Bundle; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Helper for backing up account sync settings (whether or not a service should be synced). The + * sync settings are backed up as a JSON object containing all the necessary information for + * restoring the sync settings later. + */ +public class AccountSyncSettingsBackupHelper implements BackupHelper { + + private static final String TAG = "AccountSyncSettingsBackupHelper"; + private static final boolean DEBUG = false; + + private static final int STATE_VERSION = 1; + private static final int MD5_BYTE_SIZE = 16; + private static final int SYNC_REQUEST_LATCH_TIMEOUT_SECONDS = 1; + + private static final String JSON_FORMAT_HEADER_KEY = "account_data"; + private static final String JSON_FORMAT_ENCODING = "UTF-8"; + private static final int JSON_FORMAT_VERSION = 1; + + private static final String KEY_VERSION = "version"; + private static final String KEY_MASTER_SYNC_ENABLED = "masterSyncEnabled"; + private static final String KEY_ACCOUNTS = "accounts"; + private static final String KEY_ACCOUNT_NAME = "name"; + private static final String KEY_ACCOUNT_TYPE = "type"; + private static final String KEY_ACCOUNT_AUTHORITIES = "authorities"; + private static final String KEY_AUTHORITY_NAME = "name"; + private static final String KEY_AUTHORITY_SYNC_STATE = "syncState"; + private static final String KEY_AUTHORITY_SYNC_ENABLED = "syncEnabled"; + + private Context mContext; + private AccountManager mAccountManager; + + public AccountSyncSettingsBackupHelper(Context context) { + mContext = context; + mAccountManager = AccountManager.get(mContext); + } + + /** + * Take a snapshot of the current account sync settings and write them to the given output. + */ + @Override + public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput output, + ParcelFileDescriptor newState) { + try { + JSONObject dataJSON = serializeAccountSyncSettingsToJSON(); + + if (DEBUG) { + Log.d(TAG, "Account sync settings JSON: " + dataJSON); + } + + // Encode JSON data to bytes. + byte[] dataBytes = dataJSON.toString().getBytes(JSON_FORMAT_ENCODING); + byte[] oldMd5Checksum = readOldMd5Checksum(oldState); + byte[] newMd5Checksum = generateMd5Checksum(dataBytes); + if (!Arrays.equals(oldMd5Checksum, newMd5Checksum)) { + int dataSize = dataBytes.length; + output.writeEntityHeader(JSON_FORMAT_HEADER_KEY, dataSize); + output.writeEntityData(dataBytes, dataSize); + + Log.i(TAG, "Backup successful."); + } else { + Log.i(TAG, "Old and new MD5 checksums match. Skipping backup."); + } + + writeNewMd5Checksum(newState, newMd5Checksum); + } catch (JSONException | IOException | NoSuchAlgorithmException e) { + Log.e(TAG, "Couldn't backup account sync settings\n" + e); + } + } + + /** + * Fetch and serialize Account and authority information as a JSON Array. + */ + private JSONObject serializeAccountSyncSettingsToJSON() throws JSONException { + Account[] accounts = mAccountManager.getAccounts(); + SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypesAsUser( + mContext.getUserId()); + + // Create a map of Account types to authorities. Later this will make it easier for us to + // generate our JSON. + HashMap<String, List<String>> accountTypeToAuthorities = new HashMap<String, + List<String>>(); + for (SyncAdapterType syncAdapter : syncAdapters) { + // Skip adapters that aren’t visible to the user. + if (!syncAdapter.isUserVisible()) { + continue; + } + if (!accountTypeToAuthorities.containsKey(syncAdapter.accountType)) { + accountTypeToAuthorities.put(syncAdapter.accountType, new ArrayList<String>()); + } + accountTypeToAuthorities.get(syncAdapter.accountType).add(syncAdapter.authority); + } + + // Generate JSON. + JSONObject backupJSON = new JSONObject(); + backupJSON.put(KEY_VERSION, JSON_FORMAT_VERSION); + backupJSON.put(KEY_MASTER_SYNC_ENABLED, ContentResolver.getMasterSyncAutomatically()); + + JSONArray accountJSONArray = new JSONArray(); + for (Account account : accounts) { + List<String> authorities = accountTypeToAuthorities.get(account.type); + + // We ignore Accounts that don't have any authorities because there would be no sync + // settings for us to restore. + if (authorities == null || authorities.isEmpty()) { + continue; + } + + JSONObject accountJSON = new JSONObject(); + accountJSON.put(KEY_ACCOUNT_NAME, account.name); + accountJSON.put(KEY_ACCOUNT_TYPE, account.type); + + // Add authorities for this Account type and check whether or not sync is enabled. + JSONArray authoritiesJSONArray = new JSONArray(); + for (String authority : authorities) { + int syncState = ContentResolver.getIsSyncable(account, authority); + boolean syncEnabled = ContentResolver.getSyncAutomatically(account, authority); + + JSONObject authorityJSON = new JSONObject(); + authorityJSON.put(KEY_AUTHORITY_NAME, authority); + authorityJSON.put(KEY_AUTHORITY_SYNC_STATE, syncState); + authorityJSON.put(KEY_AUTHORITY_SYNC_ENABLED, syncEnabled); + authoritiesJSONArray.put(authorityJSON); + } + accountJSON.put(KEY_ACCOUNT_AUTHORITIES, authoritiesJSONArray); + + accountJSONArray.put(accountJSON); + } + backupJSON.put(KEY_ACCOUNTS, accountJSONArray); + + return backupJSON; + } + + /** + * Read the MD5 checksum from the old state. + * + * @return the old MD5 checksum + */ + private byte[] readOldMd5Checksum(ParcelFileDescriptor oldState) throws IOException { + DataInputStream dataInput = new DataInputStream( + new FileInputStream(oldState.getFileDescriptor())); + + byte[] oldMd5Checksum = new byte[MD5_BYTE_SIZE]; + try { + int stateVersion = dataInput.readInt(); + if (stateVersion <= STATE_VERSION) { + // If the state version is a version we can understand then read the MD5 sum, + // otherwise we return an empty byte array for the MD5 sum which will force a + // backup. + for (int i = 0; i < MD5_BYTE_SIZE; i++) { + oldMd5Checksum[i] = dataInput.readByte(); + } + } else { + Log.i(TAG, "Backup state version is: " + stateVersion + + " (support only up to version " + STATE_VERSION + ")"); + } + } catch (EOFException eof) { + // Initial state may be empty. + } finally { + dataInput.close(); + } + return oldMd5Checksum; + } + + /** + * Write the given checksum to the file descriptor. + */ + private void writeNewMd5Checksum(ParcelFileDescriptor newState, byte[] md5Checksum) + throws IOException { + DataOutputStream dataOutput = new DataOutputStream( + new BufferedOutputStream(new FileOutputStream(newState.getFileDescriptor()))); + + dataOutput.writeInt(STATE_VERSION); + dataOutput.write(md5Checksum); + dataOutput.close(); + } + + private byte[] generateMd5Checksum(byte[] data) throws NoSuchAlgorithmException { + if (data == null) { + return null; + } + + MessageDigest md5 = MessageDigest.getInstance("MD5"); + return md5.digest(data); + } + + /** + * Restore account sync settings from the given data input stream. + */ + @Override + public void restoreEntity(BackupDataInputStream data) { + byte[] dataBytes = new byte[data.size()]; + try { + // Read the data and convert it to a String. + data.read(dataBytes); + String dataString = new String(dataBytes, JSON_FORMAT_ENCODING); + + // Convert data to a JSON object. + JSONObject dataJSON = new JSONObject(dataString); + boolean masterSyncEnabled = dataJSON.getBoolean(KEY_MASTER_SYNC_ENABLED); + JSONArray accountJSONArray = dataJSON.getJSONArray(KEY_ACCOUNTS); + + boolean currentMasterSyncEnabled = ContentResolver.getMasterSyncAutomatically(); + if (currentMasterSyncEnabled) { + // Disable master sync to prevent any syncs from running. + ContentResolver.setMasterSyncAutomatically(false); + } + + try { + HashSet<Account> currentAccounts = getAccountsHashSet(); + for (int i = 0; i < accountJSONArray.length(); i++) { + JSONObject accountJSON = (JSONObject) accountJSONArray.get(i); + String accountName = accountJSON.getString(KEY_ACCOUNT_NAME); + String accountType = accountJSON.getString(KEY_ACCOUNT_TYPE); + + Account account = new Account(accountName, accountType); + + // Check if the account already exists. Accounts that don't exist on the device + // yet won't be restored. + if (currentAccounts.contains(account)) { + restoreExistingAccountSyncSettingsFromJSON(accountJSON); + } + } + } finally { + // Set the master sync preference to the value from the backup set. + ContentResolver.setMasterSyncAutomatically(masterSyncEnabled); + } + + Log.i(TAG, "Restore successful."); + } catch (IOException | JSONException e) { + Log.e(TAG, "Couldn't restore account sync settings\n" + e); + } + } + + /** + * Helper method - fetch accounts and return them as a HashSet. + * + * @return Accounts in a HashSet. + */ + private HashSet<Account> getAccountsHashSet() { + Account[] accounts = mAccountManager.getAccounts(); + HashSet<Account> accountHashSet = new HashSet<Account>(); + for (Account account : accounts) { + accountHashSet.add(account); + } + return accountHashSet; + } + + /** + * Restore account sync settings using the given JSON. This function won't work if the account + * doesn't exist yet. + */ + private void restoreExistingAccountSyncSettingsFromJSON(JSONObject accountJSON) + throws JSONException { + // Restore authorities. + JSONArray authorities = accountJSON.getJSONArray(KEY_ACCOUNT_AUTHORITIES); + String accountName = accountJSON.getString(KEY_ACCOUNT_NAME); + String accountType = accountJSON.getString(KEY_ACCOUNT_TYPE); + final Account account = new Account(accountName, accountType); + for (int i = 0; i < authorities.length(); i++) { + JSONObject authority = (JSONObject) authorities.get(i); + final String authorityName = authority.getString(KEY_AUTHORITY_NAME); + boolean syncEnabled = authority.getBoolean(KEY_AUTHORITY_SYNC_ENABLED); + + // Cancel any active syncs. + if (ContentResolver.isSyncActive(account, authorityName)) { + ContentResolver.cancelSync(account, authorityName); + } + + boolean overwriteSync = true; + Bundle initializationExtras = createSyncInitializationBundle(); + int currentSyncState = ContentResolver.getIsSyncable(account, authorityName); + if (currentSyncState < 0) { + // Requesting a sync is an asynchronous operation, so we setup a countdown latch to + // wait for it to finish. Initialization syncs are generally very brief and + // shouldn't take too much time to finish. + final CountDownLatch latch = new CountDownLatch(1); + Object syncStatusObserverHandle = ContentResolver.addStatusChangeListener( + ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE, new SyncStatusObserver() { + @Override + public void onStatusChanged(int which) { + if (!ContentResolver.isSyncActive(account, authorityName)) { + latch.countDown(); + } + } + }); + + // If we set sync settings for a sync that hasn't been initialized yet, we run the + // risk of having our changes overwritten later on when the sync gets initialized. + // To prevent this from happening we will manually initiate the sync adapter. We + // also explicitly pass in a Bundle with SYNC_EXTRAS_INITIALIZE to prevent a data + // sync from running after the initialization sync. Two syncs will be scheduled, but + // the second one (data sync) will override the first one (initialization sync) and + // still behave as an initialization sync because of the Bundle. + ContentResolver.requestSync(account, authorityName, initializationExtras); + + boolean done = false; + try { + done = latch.await(SYNC_REQUEST_LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Log.e(TAG, "CountDownLatch interrupted\n" + e); + done = false; + } + if (!done) { + overwriteSync = false; + Log.i(TAG, "CountDownLatch timed out, skipping '" + authorityName + + "' authority."); + } + ContentResolver.removeStatusChangeListener(syncStatusObserverHandle); + } + + if (overwriteSync) { + ContentResolver.setSyncAutomatically(account, authorityName, syncEnabled); + Log.i(TAG, "Set sync automatically for '" + authorityName + "': " + syncEnabled); + } + } + } + + private Bundle createSyncInitializationBundle() { + Bundle extras = new Bundle(); + extras.putBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, true); + return extras; + } + + @Override + public void writeNewStateDescription(ParcelFileDescriptor newState) { + + } +} diff --git a/core/java/com/android/server/backup/SystemBackupAgent.java b/core/java/com/android/server/backup/SystemBackupAgent.java index 35a1a5a..b5f2f37 100644 --- a/core/java/com/android/server/backup/SystemBackupAgent.java +++ b/core/java/com/android/server/backup/SystemBackupAgent.java @@ -86,6 +86,8 @@ public class SystemBackupAgent extends BackupAgentHelper { } addHelper("wallpaper", new WallpaperBackupHelper(SystemBackupAgent.this, files, keys)); addHelper("recents", new RecentsBackupHelper(SystemBackupAgent.this)); + addHelper("account_sync_settings", + new AccountSyncSettingsBackupHelper(SystemBackupAgent.this)); super.onBackup(oldState, data, newState); } @@ -118,6 +120,8 @@ public class SystemBackupAgent extends BackupAgentHelper { new String[] { WALLPAPER_IMAGE }, new String[] { WALLPAPER_IMAGE_KEY} )); addHelper("recents", new RecentsBackupHelper(SystemBackupAgent.this)); + addHelper("account_sync_settings", + new AccountSyncSettingsBackupHelper(SystemBackupAgent.this)); try { super.onRestore(data, appVersionCode, newState); diff --git a/core/java/com/android/server/net/NetlinkTracker.java b/core/java/com/android/server/net/NetlinkTracker.java index 0dde465..d45982e 100644 --- a/core/java/com/android/server/net/NetlinkTracker.java +++ b/core/java/com/android/server/net/NetlinkTracker.java @@ -24,11 +24,9 @@ import android.util.Log; import java.net.InetAddress; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.Objects; import java.util.Set; /** diff --git a/core/java/org/apache/http/conn/ConnectTimeoutException.java b/core/java/org/apache/http/conn/ConnectTimeoutException.java new file mode 100644 index 0000000..6cc6922 --- /dev/null +++ b/core/java/org/apache/http/conn/ConnectTimeoutException.java @@ -0,0 +1,69 @@ +/* + * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/conn/ConnectTimeoutException.java $ + * $Revision: 617645 $ + * $Date: 2008-02-01 13:05:31 -0800 (Fri, 01 Feb 2008) $ + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ + +package org.apache.http.conn; + +import java.io.InterruptedIOException; + +/** + * A timeout while connecting to an HTTP server or waiting for an + * available connection from an HttpConnectionManager. + * + * @author <a href="mailto:laura@lwerner.org">Laura Werner</a> + * + * @since 4.0 + * + * @deprecated Please use {@link java.net.URL#openConnection} instead. + * Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> + * for further details. + */ +@Deprecated +public class ConnectTimeoutException extends InterruptedIOException { + + private static final long serialVersionUID = -4816682903149535989L; + + /** + * Creates a ConnectTimeoutException with a <tt>null</tt> detail message. + */ + public ConnectTimeoutException() { + super(); + } + + /** + * Creates a ConnectTimeoutException with the specified detail message. + * + * @param message The exception detail message + */ + public ConnectTimeoutException(final String message) { + super(message); + } + +} diff --git a/core/java/org/apache/http/conn/scheme/HostNameResolver.java b/core/java/org/apache/http/conn/scheme/HostNameResolver.java new file mode 100644 index 0000000..30ef298 --- /dev/null +++ b/core/java/org/apache/http/conn/scheme/HostNameResolver.java @@ -0,0 +1,47 @@ +/* + * $HeadURL:$ + * $Revision:$ + * $Date:$ + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ + +package org.apache.http.conn.scheme; + +import java.io.IOException; +import java.net.InetAddress; + +/** + * @deprecated Please use {@link java.net.URL#openConnection} instead. + * Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> + * for further details. + */ +@Deprecated +public interface HostNameResolver { + + InetAddress resolve (String hostname) throws IOException; + +} diff --git a/core/java/org/apache/http/conn/scheme/LayeredSocketFactory.java b/core/java/org/apache/http/conn/scheme/LayeredSocketFactory.java new file mode 100644 index 0000000..b9f5348 --- /dev/null +++ b/core/java/org/apache/http/conn/scheme/LayeredSocketFactory.java @@ -0,0 +1,77 @@ +/* + * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/conn/scheme/LayeredSocketFactory.java $ + * $Revision: 645850 $ + * $Date: 2008-04-08 04:08:52 -0700 (Tue, 08 Apr 2008) $ + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ + +package org.apache.http.conn.scheme; + +import java.io.IOException; +import java.net.Socket; +import java.net.UnknownHostException; + +/** + * A {@link SocketFactory SocketFactory} for layered sockets (SSL/TLS). + * See there for things to consider when implementing a socket factory. + * + * @author Michael Becke + * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a> + * @since 4.0 + * + * @deprecated Please use {@link java.net.URL#openConnection} instead. + * Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> + * for further details. + */ +@Deprecated +public interface LayeredSocketFactory extends SocketFactory { + + /** + * Returns a socket connected to the given host that is layered over an + * existing socket. Used primarily for creating secure sockets through + * proxies. + * + * @param socket the existing socket + * @param host the host name/IP + * @param port the port on the host + * @param autoClose a flag for closing the underling socket when the created + * socket is closed + * + * @return Socket a new socket + * + * @throws IOException if an I/O error occurs while creating the socket + * @throws UnknownHostException if the IP address of the host cannot be + * determined + */ + Socket createSocket( + Socket socket, + String host, + int port, + boolean autoClose + ) throws IOException, UnknownHostException; + +} diff --git a/core/java/org/apache/http/conn/scheme/SocketFactory.java b/core/java/org/apache/http/conn/scheme/SocketFactory.java new file mode 100644 index 0000000..c6bc03c --- /dev/null +++ b/core/java/org/apache/http/conn/scheme/SocketFactory.java @@ -0,0 +1,143 @@ +/* + * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/conn/scheme/SocketFactory.java $ + * $Revision: 645850 $ + * $Date: 2008-04-08 04:08:52 -0700 (Tue, 08 Apr 2008) $ + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ + +package org.apache.http.conn.scheme; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; + +import org.apache.http.conn.ConnectTimeoutException; +import org.apache.http.params.HttpParams; + +/** + * A factory for creating and connecting sockets. + * The factory encapsulates the logic for establishing a socket connection. + * <br/> + * Both {@link java.lang.Object#equals(java.lang.Object) Object.equals()} + * and {@link java.lang.Object#hashCode() Object.hashCode()} + * must be overridden for the correct operation of some connection managers. + * + * @author <a href="mailto:rolandw at apache.org">Roland Weber</a> + * @author Michael Becke + * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a> + * + * @deprecated Please use {@link java.net.URL#openConnection} instead. + * Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> + * for further details. + */ +@Deprecated +public interface SocketFactory { + + /** + * Creates a new, unconnected socket. + * The socket should subsequently be passed to + * {@link #connectSocket connectSocket}. + * + * @return a new socket + * + * @throws IOException if an I/O error occurs while creating the socket + */ + Socket createSocket() + throws IOException + ; + + + /** + * Connects a socket to the given host. + * + * @param sock the socket to connect, as obtained from + * {@link #createSocket createSocket}. + * <code>null</code> indicates that a new socket + * should be created and connected. + * @param host the host to connect to + * @param port the port to connect to on the host + * @param localAddress the local address to bind the socket to, or + * <code>null</code> for any + * @param localPort the port on the local machine, + * 0 or a negative number for any + * @param params additional {@link HttpParams parameters} for connecting + * + * @return the connected socket. The returned object may be different + * from the <code>sock</code> argument if this factory supports + * a layered protocol. + * + * @throws IOException if an I/O error occurs + * @throws UnknownHostException if the IP address of the target host + * can not be determined + * @throws ConnectTimeoutException if the socket cannot be connected + * within the time limit defined in the <code>params</code> + */ + Socket connectSocket( + Socket sock, + String host, + int port, + InetAddress localAddress, + int localPort, + HttpParams params + ) throws IOException, UnknownHostException, ConnectTimeoutException; + + + /** + * Checks whether a socket provides a secure connection. + * The socket must be {@link #connectSocket connected} + * by this factory. + * The factory will <i>not</i> perform I/O operations + * in this method. + * <br/> + * As a rule of thumb, plain sockets are not secure and + * TLS/SSL sockets are secure. However, there may be + * application specific deviations. For example, a plain + * socket to a host in the same intranet ("trusted zone") + * could be considered secure. On the other hand, a + * TLS/SSL socket could be considered insecure based on + * the cypher suite chosen for the connection. + * + * @param sock the connected socket to check + * + * @return <code>true</code> if the connection of the socket + * should be considered secure, or + * <code>false</code> if it should not + * + * @throws IllegalArgumentException + * if the argument is invalid, for example because it is + * not a connected socket or was created by a different + * socket factory. + * Note that socket factories are <i>not</i> required to + * check these conditions, they may simply return a default + * value when called with an invalid socket argument. + */ + boolean isSecure(Socket sock) + throws IllegalArgumentException + ; + +} diff --git a/core/java/org/apache/http/conn/ssl/AbstractVerifier.java b/core/java/org/apache/http/conn/ssl/AbstractVerifier.java new file mode 100644 index 0000000..e264f1c --- /dev/null +++ b/core/java/org/apache/http/conn/ssl/AbstractVerifier.java @@ -0,0 +1,288 @@ +/* + * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/conn/ssl/AbstractVerifier.java $ + * $Revision: 653041 $ + * $Date: 2008-05-03 03:39:28 -0700 (Sat, 03 May 2008) $ + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ + +package org.apache.http.conn.ssl; + +import java.util.regex.Pattern; + +import java.io.IOException; +import java.security.cert.Certificate; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.logging.Logger; +import java.util.logging.Level; + +import javax.net.ssl.DistinguishedNameParser; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; + +/** + * Abstract base class for all standard {@link X509HostnameVerifier} + * implementations. + * + * @author Julius Davies + * + * @deprecated Please use {@link java.net.URL#openConnection} instead. + * 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 AbstractVerifier implements X509HostnameVerifier { + + private static final Pattern IPV4_PATTERN = Pattern.compile( + "^(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}$"); + + /** + * This contains a list of 2nd-level domains that aren't allowed to + * have wildcards when combined with country-codes. + * For example: [*.co.uk]. + * <p/> + * The [*.co.uk] problem is an interesting one. Should we just hope + * that CA's would never foolishly allow such a certificate to happen? + * Looks like we're the only implementation guarding against this. + * Firefox, Curl, Sun Java 1.4, 5, 6 don't bother with this check. + */ + private final static String[] BAD_COUNTRY_2LDS = + { "ac", "co", "com", "ed", "edu", "go", "gouv", "gov", "info", + "lg", "ne", "net", "or", "org" }; + + static { + // Just in case developer forgot to manually sort the array. :-) + Arrays.sort(BAD_COUNTRY_2LDS); + } + + public AbstractVerifier() { + super(); + } + + public final void verify(String host, SSLSocket ssl) + throws IOException { + if(host == null) { + throw new NullPointerException("host to verify is null"); + } + + SSLSession session = ssl.getSession(); + Certificate[] certs = session.getPeerCertificates(); + X509Certificate x509 = (X509Certificate) certs[0]; + verify(host, x509); + } + + public final boolean verify(String host, SSLSession session) { + try { + Certificate[] certs = session.getPeerCertificates(); + X509Certificate x509 = (X509Certificate) certs[0]; + verify(host, x509); + return true; + } + catch(SSLException e) { + return false; + } + } + + public final void verify(String host, X509Certificate cert) + throws SSLException { + String[] cns = getCNs(cert); + String[] subjectAlts = getDNSSubjectAlts(cert); + verify(host, cns, subjectAlts); + } + + public final void verify(final String host, final String[] cns, + final String[] subjectAlts, + final boolean strictWithSubDomains) + throws SSLException { + + // Build the list of names we're going to check. Our DEFAULT and + // STRICT implementations of the HostnameVerifier only use the + // first CN provided. All other CNs are ignored. + // (Firefox, wget, curl, Sun Java 1.4, 5, 6 all work this way). + LinkedList<String> names = new LinkedList<String>(); + if(cns != null && cns.length > 0 && cns[0] != null) { + names.add(cns[0]); + } + if(subjectAlts != null) { + for (String subjectAlt : subjectAlts) { + if (subjectAlt != null) { + names.add(subjectAlt); + } + } + } + + if(names.isEmpty()) { + String msg = "Certificate for <" + host + "> doesn't contain CN or DNS subjectAlt"; + throw new SSLException(msg); + } + + // StringBuffer for building the error message. + StringBuffer buf = new StringBuffer(); + + // We're can be case-insensitive when comparing the host we used to + // establish the socket to the hostname in the certificate. + String hostName = host.trim().toLowerCase(Locale.ENGLISH); + boolean match = false; + for(Iterator<String> it = names.iterator(); it.hasNext();) { + // Don't trim the CN, though! + String cn = it.next(); + cn = cn.toLowerCase(Locale.ENGLISH); + // Store CN in StringBuffer in case we need to report an error. + buf.append(" <"); + buf.append(cn); + buf.append('>'); + if(it.hasNext()) { + buf.append(" OR"); + } + + // The CN better have at least two dots if it wants wildcard + // action. It also can't be [*.co.uk] or [*.co.jp] or + // [*.org.uk], etc... + boolean doWildcard = cn.startsWith("*.") && + cn.indexOf('.', 2) != -1 && + acceptableCountryWildcard(cn) && + !isIPv4Address(host); + + if(doWildcard) { + match = hostName.endsWith(cn.substring(1)); + if(match && strictWithSubDomains) { + // If we're in strict mode, then [*.foo.com] is not + // allowed to match [a.b.foo.com] + match = countDots(hostName) == countDots(cn); + } + } else { + match = hostName.equals(cn); + } + if(match) { + break; + } + } + if(!match) { + throw new SSLException("hostname in certificate didn't match: <" + host + "> !=" + buf); + } + } + + public static boolean acceptableCountryWildcard(String cn) { + int cnLen = cn.length(); + if(cnLen >= 7 && cnLen <= 9) { + // Look for the '.' in the 3rd-last position: + if(cn.charAt(cnLen - 3) == '.') { + // Trim off the [*.] and the [.XX]. + String s = cn.substring(2, cnLen - 3); + // And test against the sorted array of bad 2lds: + int x = Arrays.binarySearch(BAD_COUNTRY_2LDS, s); + return x < 0; + } + } + return true; + } + + public static String[] getCNs(X509Certificate cert) { + DistinguishedNameParser dnParser = + new DistinguishedNameParser(cert.getSubjectX500Principal()); + List<String> cnList = dnParser.getAllMostSpecificFirst("cn"); + + if(!cnList.isEmpty()) { + String[] cns = new String[cnList.size()]; + cnList.toArray(cns); + return cns; + } else { + return null; + } + } + + + /** + * Extracts the array of SubjectAlt DNS names from an X509Certificate. + * Returns null if there aren't any. + * <p/> + * Note: Java doesn't appear able to extract international characters + * from the SubjectAlts. It can only extract international characters + * from the CN field. + * <p/> + * (Or maybe the version of OpenSSL I'm using to test isn't storing the + * international characters correctly in the SubjectAlts?). + * + * @param cert X509Certificate + * @return Array of SubjectALT DNS names stored in the certificate. + */ + public static String[] getDNSSubjectAlts(X509Certificate cert) { + LinkedList<String> subjectAltList = new LinkedList<String>(); + Collection<List<?>> c = null; + try { + c = cert.getSubjectAlternativeNames(); + } + catch(CertificateParsingException cpe) { + Logger.getLogger(AbstractVerifier.class.getName()) + .log(Level.FINE, "Error parsing certificate.", cpe); + } + if(c != null) { + for (List<?> aC : c) { + List<?> list = aC; + int type = ((Integer) list.get(0)).intValue(); + // If type is 2, then we've got a dNSName + if (type == 2) { + String s = (String) list.get(1); + subjectAltList.add(s); + } + } + } + if(!subjectAltList.isEmpty()) { + String[] subjectAlts = new String[subjectAltList.size()]; + subjectAltList.toArray(subjectAlts); + return subjectAlts; + } else { + return null; + } + } + + /** + * Counts the number of dots "." in a string. + * @param s string to count dots from + * @return number of dots + */ + public static int countDots(final String s) { + int count = 0; + for(int i = 0; i < s.length(); i++) { + if(s.charAt(i) == '.') { + count++; + } + } + return count; + } + + private static boolean isIPv4Address(final String input) { + return IPV4_PATTERN.matcher(input).matches(); + } +} diff --git a/core/java/org/apache/http/conn/ssl/AllowAllHostnameVerifier.java b/core/java/org/apache/http/conn/ssl/AllowAllHostnameVerifier.java new file mode 100644 index 0000000..c2bf4c4 --- /dev/null +++ b/core/java/org/apache/http/conn/ssl/AllowAllHostnameVerifier.java @@ -0,0 +1,59 @@ +/* + * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/conn/ssl/AllowAllHostnameVerifier.java $ + * $Revision: 617642 $ + * $Date: 2008-02-01 12:54:07 -0800 (Fri, 01 Feb 2008) $ + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ + +package org.apache.http.conn.ssl; + +/** + * The ALLOW_ALL HostnameVerifier essentially turns hostname verification + * off. This implementation is a no-op, and never throws the SSLException. + * + * @author Julius Davies + * + * @deprecated Please use {@link java.net.URL#openConnection} instead. + * Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> + * for further details. + */ +@Deprecated +public class AllowAllHostnameVerifier extends AbstractVerifier { + + public final void verify( + final String host, + final String[] cns, + final String[] subjectAlts) { + // Allow everything - so never blowup. + } + + @Override + public final String toString() { + return "ALLOW_ALL"; + } + +} diff --git a/core/java/org/apache/http/conn/ssl/BrowserCompatHostnameVerifier.java b/core/java/org/apache/http/conn/ssl/BrowserCompatHostnameVerifier.java new file mode 100644 index 0000000..48a7bf9 --- /dev/null +++ b/core/java/org/apache/http/conn/ssl/BrowserCompatHostnameVerifier.java @@ -0,0 +1,67 @@ +/* + * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/conn/ssl/BrowserCompatHostnameVerifier.java $ + * $Revision: 617642 $ + * $Date: 2008-02-01 12:54:07 -0800 (Fri, 01 Feb 2008) $ + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ + +package org.apache.http.conn.ssl; + +import javax.net.ssl.SSLException; + +/** + * The HostnameVerifier that works the same way as Curl and Firefox. + * <p/> + * The hostname must match either the first CN, or any of the subject-alts. + * A wildcard can occur in the CN, and in any of the subject-alts. + * <p/> + * The only difference between BROWSER_COMPATIBLE and STRICT is that a wildcard + * (such as "*.foo.com") with BROWSER_COMPATIBLE matches all subdomains, + * including "a.b.foo.com". + * + * @author Julius Davies + * + * @deprecated Please use {@link java.net.URL#openConnection} instead. + * Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> + * for further details. + */ +@Deprecated +public class BrowserCompatHostnameVerifier extends AbstractVerifier { + + public final void verify( + final String host, + final String[] cns, + final String[] subjectAlts) throws SSLException { + verify(host, cns, subjectAlts, false); + } + + @Override + public final String toString() { + return "BROWSER_COMPATIBLE"; + } + +} diff --git a/core/java/org/apache/http/conn/ssl/SSLSocketFactory.java b/core/java/org/apache/http/conn/ssl/SSLSocketFactory.java new file mode 100644 index 0000000..4d53d40 --- /dev/null +++ b/core/java/org/apache/http/conn/ssl/SSLSocketFactory.java @@ -0,0 +1,408 @@ +/* + * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/conn/ssl/SSLSocketFactory.java $ + * $Revision: 659194 $ + * $Date: 2008-05-22 11:33:47 -0700 (Thu, 22 May 2008) $ + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ + +package org.apache.http.conn.ssl; + +import org.apache.http.conn.scheme.HostNameResolver; +import org.apache.http.conn.scheme.LayeredSocketFactory; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.UnrecoverableKeyException; + +/** + * Layered socket factory for TLS/SSL connections, based on JSSE. + *. + * <p> + * SSLSocketFactory can be used to validate the identity of the HTTPS + * server against a list of trusted certificates and to authenticate to + * the HTTPS server using a private key. + * </p> + * + * <p> + * SSLSocketFactory will enable server authentication when supplied with + * a {@link KeyStore truststore} file containg one or several trusted + * certificates. The client secure socket will reject the connection during + * the SSL session handshake if the target HTTPS server attempts to + * authenticate itself with a non-trusted certificate. + * </p> + * + * <p> + * Use JDK keytool utility to import a trusted certificate and generate a truststore file: + * <pre> + * keytool -import -alias "my server cert" -file server.crt -keystore my.truststore + * </pre> + * </p> + * + * <p> + * SSLSocketFactory will enable client authentication when supplied with + * a {@link KeyStore keystore} file containg a private key/public certificate + * pair. The client secure socket will use the private key to authenticate + * itself to the target HTTPS server during the SSL session handshake if + * requested to do so by the server. + * The target HTTPS server will in its turn verify the certificate presented + * by the client in order to establish client's authenticity + * </p> + * + * <p> + * Use the following sequence of actions to generate a keystore file + * </p> + * <ul> + * <li> + * <p> + * Use JDK keytool utility to generate a new key + * <pre>keytool -genkey -v -alias "my client key" -validity 365 -keystore my.keystore</pre> + * For simplicity use the same password for the key as that of the keystore + * </p> + * </li> + * <li> + * <p> + * Issue a certificate signing request (CSR) + * <pre>keytool -certreq -alias "my client key" -file mycertreq.csr -keystore my.keystore</pre> + * </p> + * </li> + * <li> + * <p> + * Send the certificate request to the trusted Certificate Authority for signature. + * One may choose to act as her own CA and sign the certificate request using a PKI + * tool, such as OpenSSL. + * </p> + * </li> + * <li> + * <p> + * Import the trusted CA root certificate + * <pre>keytool -import -alias "my trusted ca" -file caroot.crt -keystore my.keystore</pre> + * </p> + * </li> + * <li> + * <p> + * Import the PKCS#7 file containg the complete certificate chain + * <pre>keytool -import -alias "my client key" -file mycert.p7 -keystore my.keystore</pre> + * </p> + * </li> + * <li> + * <p> + * Verify the content the resultant keystore file + * <pre>keytool -list -v -keystore my.keystore</pre> + * </p> + * </li> + * </ul> + * @author <a href="mailto:oleg at ural.ru">Oleg Kalnichevski</a> + * @author Julius Davies + * + * @deprecated Please use {@link java.net.URL#openConnection} instead. + * Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> + * for further details. + */ +@Deprecated +public class SSLSocketFactory implements LayeredSocketFactory { + + public static final String TLS = "TLS"; + public static final String SSL = "SSL"; + public static final String SSLV2 = "SSLv2"; + + public static final X509HostnameVerifier ALLOW_ALL_HOSTNAME_VERIFIER + = new AllowAllHostnameVerifier(); + + public static final X509HostnameVerifier BROWSER_COMPATIBLE_HOSTNAME_VERIFIER + = new BrowserCompatHostnameVerifier(); + + public static final X509HostnameVerifier STRICT_HOSTNAME_VERIFIER + = new StrictHostnameVerifier(); + + /* + * Put defaults into holder class to avoid class preloading creating an + * instance of the classes referenced. + */ + private static class NoPreloadHolder { + /** + * The factory using the default JVM settings for secure connections. + */ + private static final SSLSocketFactory DEFAULT_FACTORY = new SSLSocketFactory(); + } + + /** + * Gets an singleton instance of the SSLProtocolSocketFactory. + * @return a SSLProtocolSocketFactory + */ + public static SSLSocketFactory getSocketFactory() { + return NoPreloadHolder.DEFAULT_FACTORY; + } + + private final SSLContext sslcontext; + private final javax.net.ssl.SSLSocketFactory socketfactory; + private final HostNameResolver nameResolver; + private X509HostnameVerifier hostnameVerifier = BROWSER_COMPATIBLE_HOSTNAME_VERIFIER; + + public SSLSocketFactory( + String algorithm, + final KeyStore keystore, + final String keystorePassword, + final KeyStore truststore, + final SecureRandom random, + final HostNameResolver nameResolver) + throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException + { + super(); + if (algorithm == null) { + algorithm = TLS; + } + KeyManager[] keymanagers = null; + if (keystore != null) { + keymanagers = createKeyManagers(keystore, keystorePassword); + } + TrustManager[] trustmanagers = null; + if (truststore != null) { + trustmanagers = createTrustManagers(truststore); + } + this.sslcontext = SSLContext.getInstance(algorithm); + this.sslcontext.init(keymanagers, trustmanagers, random); + this.socketfactory = this.sslcontext.getSocketFactory(); + this.nameResolver = nameResolver; + } + + public SSLSocketFactory( + final KeyStore keystore, + final String keystorePassword, + final KeyStore truststore) + throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException + { + this(TLS, keystore, keystorePassword, truststore, null, null); + } + + public SSLSocketFactory(final KeyStore keystore, final String keystorePassword) + throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException + { + this(TLS, keystore, keystorePassword, null, null, null); + } + + public SSLSocketFactory(final KeyStore truststore) + throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException + { + this(TLS, null, null, truststore, null, null); + } + + /** + * Constructs an HttpClient SSLSocketFactory backed by the given JSSE + * SSLSocketFactory. + * + * @hide + */ + public SSLSocketFactory(javax.net.ssl.SSLSocketFactory socketfactory) { + super(); + this.sslcontext = null; + this.socketfactory = socketfactory; + this.nameResolver = null; + } + + /** + * Creates the default SSL socket factory. + * This constructor is used exclusively to instantiate the factory for + * {@link #getSocketFactory getSocketFactory}. + */ + private SSLSocketFactory() { + super(); + this.sslcontext = null; + this.socketfactory = HttpsURLConnection.getDefaultSSLSocketFactory(); + this.nameResolver = null; + } + + private static KeyManager[] createKeyManagers(final KeyStore keystore, final String password) + throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { + if (keystore == null) { + throw new IllegalArgumentException("Keystore may not be null"); + } + KeyManagerFactory kmfactory = KeyManagerFactory.getInstance( + KeyManagerFactory.getDefaultAlgorithm()); + kmfactory.init(keystore, password != null ? password.toCharArray(): null); + return kmfactory.getKeyManagers(); + } + + private static TrustManager[] createTrustManagers(final KeyStore keystore) + throws KeyStoreException, NoSuchAlgorithmException { + if (keystore == null) { + throw new IllegalArgumentException("Keystore may not be null"); + } + TrustManagerFactory tmfactory = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm()); + tmfactory.init(keystore); + return tmfactory.getTrustManagers(); + } + + + // non-javadoc, see interface org.apache.http.conn.SocketFactory + public Socket createSocket() + throws IOException { + + // the cast makes sure that the factory is working as expected + return (SSLSocket) this.socketfactory.createSocket(); + } + + + // non-javadoc, see interface org.apache.http.conn.SocketFactory + public Socket connectSocket( + final Socket sock, + final String host, + final int port, + final InetAddress localAddress, + int localPort, + final HttpParams params + ) throws IOException { + + if (host == null) { + throw new IllegalArgumentException("Target host may not be null."); + } + if (params == null) { + throw new IllegalArgumentException("Parameters may not be null."); + } + + SSLSocket sslsock = (SSLSocket) + ((sock != null) ? sock : createSocket()); + + if ((localAddress != null) || (localPort > 0)) { + + // we need to bind explicitly + if (localPort < 0) + localPort = 0; // indicates "any" + + InetSocketAddress isa = + new InetSocketAddress(localAddress, localPort); + sslsock.bind(isa); + } + + int connTimeout = HttpConnectionParams.getConnectionTimeout(params); + int soTimeout = HttpConnectionParams.getSoTimeout(params); + + InetSocketAddress remoteAddress; + if (this.nameResolver != null) { + remoteAddress = new InetSocketAddress(this.nameResolver.resolve(host), port); + } else { + remoteAddress = new InetSocketAddress(host, port); + } + + sslsock.connect(remoteAddress, connTimeout); + + sslsock.setSoTimeout(soTimeout); + try { + hostnameVerifier.verify(host, sslsock); + // verifyHostName() didn't blowup - good! + } catch (IOException iox) { + // close the socket before re-throwing the exception + try { sslsock.close(); } catch (Exception x) { /*ignore*/ } + throw iox; + } + + return sslsock; + } + + + /** + * Checks whether a socket connection is secure. + * This factory creates TLS/SSL socket connections + * which, by default, are considered secure. + * <br/> + * Derived classes may override this method to perform + * runtime checks, for example based on the cypher suite. + * + * @param sock the connected socket + * + * @return <code>true</code> + * + * @throws IllegalArgumentException if the argument is invalid + */ + public boolean isSecure(Socket sock) + throws IllegalArgumentException { + + if (sock == null) { + throw new IllegalArgumentException("Socket may not be null."); + } + // This instanceof check is in line with createSocket() above. + if (!(sock instanceof SSLSocket)) { + throw new IllegalArgumentException + ("Socket not created by this factory."); + } + // This check is performed last since it calls the argument object. + if (sock.isClosed()) { + throw new IllegalArgumentException("Socket is closed."); + } + + return true; + + } // isSecure + + + // non-javadoc, see interface LayeredSocketFactory + public Socket createSocket( + final Socket socket, + final String host, + final int port, + final boolean autoClose + ) throws IOException, UnknownHostException { + SSLSocket sslSocket = (SSLSocket) this.socketfactory.createSocket( + socket, + host, + port, + autoClose + ); + hostnameVerifier.verify(host, sslSocket); + // verifyHostName() didn't blowup - good! + return sslSocket; + } + + public void setHostnameVerifier(X509HostnameVerifier hostnameVerifier) { + if ( hostnameVerifier == null ) { + throw new IllegalArgumentException("Hostname verifier may not be null"); + } + this.hostnameVerifier = hostnameVerifier; + } + + public X509HostnameVerifier getHostnameVerifier() { + return hostnameVerifier; + } + +} diff --git a/core/java/org/apache/http/conn/ssl/StrictHostnameVerifier.java b/core/java/org/apache/http/conn/ssl/StrictHostnameVerifier.java new file mode 100644 index 0000000..bd9e70d --- /dev/null +++ b/core/java/org/apache/http/conn/ssl/StrictHostnameVerifier.java @@ -0,0 +1,74 @@ +/* + * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/conn/ssl/StrictHostnameVerifier.java $ + * $Revision: 617642 $ + * $Date: 2008-02-01 12:54:07 -0800 (Fri, 01 Feb 2008) $ + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ + +package org.apache.http.conn.ssl; + +import javax.net.ssl.SSLException; + +/** + * The Strict HostnameVerifier works the same way as Sun Java 1.4, Sun + * Java 5, Sun Java 6-rc. It's also pretty close to IE6. This + * implementation appears to be compliant with RFC 2818 for dealing with + * wildcards. + * <p/> + * The hostname must match either the first CN, or any of the subject-alts. + * A wildcard can occur in the CN, and in any of the subject-alts. The + * one divergence from IE6 is how we only check the first CN. IE6 allows + * a match against any of the CNs present. We decided to follow in + * Sun Java 1.4's footsteps and only check the first CN. (If you need + * to check all the CN's, feel free to write your own implementation!). + * <p/> + * A wildcard such as "*.foo.com" matches only subdomains in the same + * level, for example "a.foo.com". It does not match deeper subdomains + * such as "a.b.foo.com". + * + * @author Julius Davies + * + * @deprecated Please use {@link java.net.URL#openConnection} instead. + * Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> + * for further details. + */ +@Deprecated +public class StrictHostnameVerifier extends AbstractVerifier { + + public final void verify( + final String host, + final String[] cns, + final String[] subjectAlts) throws SSLException { + verify(host, cns, subjectAlts, true); + } + + @Override + public final String toString() { + return "STRICT"; + } + +} diff --git a/core/java/org/apache/http/conn/ssl/X509HostnameVerifier.java b/core/java/org/apache/http/conn/ssl/X509HostnameVerifier.java new file mode 100644 index 0000000..e38db5f --- /dev/null +++ b/core/java/org/apache/http/conn/ssl/X509HostnameVerifier.java @@ -0,0 +1,91 @@ +/* + * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/conn/ssl/X509HostnameVerifier.java $ + * $Revision: 618365 $ + * $Date: 2008-02-04 10:20:08 -0800 (Mon, 04 Feb 2008) $ + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ + +package org.apache.http.conn.ssl; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import java.io.IOException; +import java.security.cert.X509Certificate; + +/** + * Interface for checking if a hostname matches the names stored inside the + * server's X.509 certificate. Implements javax.net.ssl.HostnameVerifier, but + * we don't actually use that interface. Instead we added some methods that + * take String parameters (instead of javax.net.ssl.HostnameVerifier's + * SSLSession). JUnit is a lot easier this way! :-) + * <p/> + * We provide the HostnameVerifier.DEFAULT, HostnameVerifier.STRICT, and + * HostnameVerifier.ALLOW_ALL implementations. But feel free to define + * your own implementation! + * <p/> + * Inspired by Sebastian Hauer's original StrictSSLProtocolSocketFactory in the + * HttpClient "contrib" repository. + * + * @author Julius Davies + * @author <a href="mailto:hauer@psicode.com">Sebastian Hauer</a> + * + * @since 4.0 (8-Dec-2006) + * + * @deprecated Please use {@link java.net.URL#openConnection} instead. + * Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> + * for further details. + */ +@Deprecated +public interface X509HostnameVerifier extends HostnameVerifier { + + boolean verify(String host, SSLSession session); + + void verify(String host, SSLSocket ssl) throws IOException; + + void verify(String host, X509Certificate cert) throws SSLException; + + /** + * Checks to see if the supplied hostname matches any of the supplied CNs + * or "DNS" Subject-Alts. Most implementations only look at the first CN, + * and ignore any additional CNs. Most implementations do look at all of + * the "DNS" Subject-Alts. The CNs or Subject-Alts may contain wildcards + * according to RFC 2818. + * + * @param cns CN fields, in order, as extracted from the X.509 + * certificate. + * @param subjectAlts Subject-Alt fields of type 2 ("DNS"), as extracted + * from the X.509 certificate. + * @param host The hostname to verify. + * @throws SSLException If verification failed. + */ + void verify(String host, String[] cns, String[] subjectAlts) + throws SSLException; + + +} diff --git a/core/java/org/apache/http/conn/ssl/package.html b/core/java/org/apache/http/conn/ssl/package.html new file mode 100644 index 0000000..a5c737f --- /dev/null +++ b/core/java/org/apache/http/conn/ssl/package.html @@ -0,0 +1,40 @@ +<html> +<head> +<!-- +/* + * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/conn/ssl/package.html $ + * $Revision: 555193 $ + * $Date: 2007-07-11 00:36:47 -0700 (Wed, 11 Jul 2007) $ + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ +--> +</head> +<body> +TLS/SSL specific parts of the <i>HttpConn</i> API. + +</body> +</html> diff --git a/core/java/org/apache/http/params/CoreConnectionPNames.java b/core/java/org/apache/http/params/CoreConnectionPNames.java new file mode 100644 index 0000000..9479db1 --- /dev/null +++ b/core/java/org/apache/http/params/CoreConnectionPNames.java @@ -0,0 +1,136 @@ +/* + * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpcore/trunk/module-main/src/main/java/org/apache/http/params/CoreConnectionPNames.java $ + * $Revision: 576077 $ + * $Date: 2007-09-16 04:50:22 -0700 (Sun, 16 Sep 2007) $ + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ + +package org.apache.http.params; + + +/** + * Defines parameter names for connections in HttpCore. + * + * @version $Revision: 576077 $ + * + * @since 4.0 + * + * @deprecated Please use {@link java.net.URL#openConnection} instead. + * Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> + * for further details. + */ +@Deprecated +public interface CoreConnectionPNames { + + /** + * Defines the default socket timeout (<tt>SO_TIMEOUT</tt>) in milliseconds which is the + * timeout for waiting for data. A timeout value of zero is interpreted as an infinite + * timeout. This value is used when no socket timeout is set in the + * method parameters. + * <p> + * This parameter expects a value of type {@link Integer}. + * </p> + * @see java.net.SocketOptions#SO_TIMEOUT + */ + public static final String SO_TIMEOUT = "http.socket.timeout"; + + /** + * Determines whether Nagle's algorithm is to be used. The Nagle's algorithm + * tries to conserve bandwidth by minimizing the number of segments that are + * sent. When applications wish to decrease network latency and increase + * performance, they can disable Nagle's algorithm (that is enable TCP_NODELAY). + * Data will be sent earlier, at the cost of an increase in bandwidth consumption. + * <p> + * This parameter expects a value of type {@link Boolean}. + * </p> + * @see java.net.SocketOptions#TCP_NODELAY + */ + public static final String TCP_NODELAY = "http.tcp.nodelay"; + + /** + * Determines the size of the internal socket buffer used to buffer data + * while receiving / transmitting HTTP messages. + * <p> + * This parameter expects a value of type {@link Integer}. + * </p> + */ + public static final String SOCKET_BUFFER_SIZE = "http.socket.buffer-size"; + + /** + * Sets SO_LINGER with the specified linger time in seconds. The maximum timeout + * value is platform specific. Value <tt>0</tt> implies that the option is disabled. + * Value <tt>-1</tt> implies that the JRE default is used. The setting only affects + * socket close. + * <p> + * This parameter expects a value of type {@link Integer}. + * </p> + * @see java.net.SocketOptions#SO_LINGER + */ + public static final String SO_LINGER = "http.socket.linger"; + + /** + * Determines the timeout until a connection is etablished. A value of zero + * means the timeout is not used. The default value is zero. + * <p> + * This parameter expects a value of type {@link Integer}. + * </p> + */ + public static final String CONNECTION_TIMEOUT = "http.connection.timeout"; + + /** + * Determines whether stale connection check is to be used. Disabling + * stale connection check may result in slight performance improvement + * at the risk of getting an I/O error when executing a request over a + * connection that has been closed at the server side. + * <p> + * This parameter expects a value of type {@link Boolean}. + * </p> + */ + public static final String STALE_CONNECTION_CHECK = "http.connection.stalecheck"; + + /** + * Determines the maximum line length limit. If set to a positive value, any HTTP + * line exceeding this limit will cause an IOException. A negative or zero value + * will effectively disable the check. + * <p> + * This parameter expects a value of type {@link Integer}. + * </p> + */ + public static final String MAX_LINE_LENGTH = "http.connection.max-line-length"; + + /** + * Determines the maximum HTTP header count allowed. If set to a positive value, + * the number of HTTP headers received from the data stream exceeding this limit + * will cause an IOException. A negative or zero value will effectively disable + * the check. + * <p> + * This parameter expects a value of type {@link Integer}. + * </p> + */ + public static final String MAX_HEADER_COUNT = "http.connection.max-header-count"; + +} diff --git a/core/java/org/apache/http/params/HttpConnectionParams.java b/core/java/org/apache/http/params/HttpConnectionParams.java new file mode 100644 index 0000000..a7b31fc --- /dev/null +++ b/core/java/org/apache/http/params/HttpConnectionParams.java @@ -0,0 +1,229 @@ +/* + * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpcore/trunk/module-main/src/main/java/org/apache/http/params/HttpConnectionParams.java $ + * $Revision: 576089 $ + * $Date: 2007-09-16 05:39:56 -0700 (Sun, 16 Sep 2007) $ + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ + +package org.apache.http.params; + +/** + * An adaptor for accessing connection parameters in {@link HttpParams}. + * <br/> + * Note that the <i>implements</i> relation to {@link CoreConnectionPNames} + * is for compatibility with existing application code only. References to + * the parameter names should use the interface, not this class. + * + * @author <a href="mailto:oleg at ural.ru">Oleg Kalnichevski</a> + * + * @version $Revision: 576089 $ + * + * @since 4.0 + * + * @deprecated Please use {@link java.net.URL#openConnection} instead. + * 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 HttpConnectionParams implements CoreConnectionPNames { + + /** + */ + private HttpConnectionParams() { + super(); + } + + /** + * Returns the default socket timeout (<tt>SO_TIMEOUT</tt>) in milliseconds which is the + * timeout for waiting for data. A timeout value of zero is interpreted as an infinite + * timeout. This value is used when no socket timeout is set in the + * method parameters. + * + * @return timeout in milliseconds + */ + public static int getSoTimeout(final HttpParams params) { + if (params == null) { + throw new IllegalArgumentException("HTTP parameters may not be null"); + } + return params.getIntParameter(CoreConnectionPNames.SO_TIMEOUT, 0); + } + + /** + * Sets the default socket timeout (<tt>SO_TIMEOUT</tt>) in milliseconds which is the + * timeout for waiting for data. A timeout value of zero is interpreted as an infinite + * timeout. This value is used when no socket timeout is set in the + * method parameters. + * + * @param timeout Timeout in milliseconds + */ + public static void setSoTimeout(final HttpParams params, int timeout) { + if (params == null) { + throw new IllegalArgumentException("HTTP parameters may not be null"); + } + params.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, timeout); + + } + + /** + * Tests if Nagle's algorithm is to be used. + * + * @return <tt>true</tt> if the Nagle's algorithm is to NOT be used + * (that is enable TCP_NODELAY), <tt>false</tt> otherwise. + */ + public static boolean getTcpNoDelay(final HttpParams params) { + if (params == null) { + throw new IllegalArgumentException("HTTP parameters may not be null"); + } + return params.getBooleanParameter + (CoreConnectionPNames.TCP_NODELAY, true); + } + + /** + * Determines whether Nagle's algorithm is to be used. The Nagle's algorithm + * tries to conserve bandwidth by minimizing the number of segments that are + * sent. When applications wish to decrease network latency and increase + * performance, they can disable Nagle's algorithm (that is enable TCP_NODELAY). + * Data will be sent earlier, at the cost of an increase in bandwidth consumption. + * + * @param value <tt>true</tt> if the Nagle's algorithm is to NOT be used + * (that is enable TCP_NODELAY), <tt>false</tt> otherwise. + */ + public static void setTcpNoDelay(final HttpParams params, boolean value) { + if (params == null) { + throw new IllegalArgumentException("HTTP parameters may not be null"); + } + params.setBooleanParameter(CoreConnectionPNames.TCP_NODELAY, value); + } + + public static int getSocketBufferSize(final HttpParams params) { + if (params == null) { + throw new IllegalArgumentException("HTTP parameters may not be null"); + } + return params.getIntParameter + (CoreConnectionPNames.SOCKET_BUFFER_SIZE, -1); + } + + public static void setSocketBufferSize(final HttpParams params, int size) { + if (params == null) { + throw new IllegalArgumentException("HTTP parameters may not be null"); + } + params.setIntParameter(CoreConnectionPNames.SOCKET_BUFFER_SIZE, size); + } + + /** + * Returns linger-on-close timeout. Value <tt>0</tt> implies that the option is + * disabled. Value <tt>-1</tt> implies that the JRE default is used. + * + * @return the linger-on-close timeout + */ + public static int getLinger(final HttpParams params) { + if (params == null) { + throw new IllegalArgumentException("HTTP parameters may not be null"); + } + return params.getIntParameter(CoreConnectionPNames.SO_LINGER, -1); + } + + /** + * Returns linger-on-close timeout. This option disables/enables immediate return + * from a close() of a TCP Socket. Enabling this option with a non-zero Integer + * timeout means that a close() will block pending the transmission and + * acknowledgement of all data written to the peer, at which point the socket is + * closed gracefully. Value <tt>0</tt> implies that the option is + * disabled. Value <tt>-1</tt> implies that the JRE default is used. + * + * @param value the linger-on-close timeout + */ + public static void setLinger(final HttpParams params, int value) { + if (params == null) { + throw new IllegalArgumentException("HTTP parameters may not be null"); + } + params.setIntParameter(CoreConnectionPNames.SO_LINGER, value); + } + + /** + * Returns the timeout until a connection is etablished. A value of zero + * means the timeout is not used. The default value is zero. + * + * @return timeout in milliseconds. + */ + public static int getConnectionTimeout(final HttpParams params) { + if (params == null) { + throw new IllegalArgumentException("HTTP parameters may not be null"); + } + return params.getIntParameter + (CoreConnectionPNames.CONNECTION_TIMEOUT, 0); + } + + /** + * Sets the timeout until a connection is etablished. A value of zero + * means the timeout is not used. The default value is zero. + * + * @param timeout Timeout in milliseconds. + */ + public static void setConnectionTimeout(final HttpParams params, int timeout) { + if (params == null) { + throw new IllegalArgumentException("HTTP parameters may not be null"); + } + params.setIntParameter + (CoreConnectionPNames.CONNECTION_TIMEOUT, timeout); + } + + /** + * Tests whether stale connection check is to be used. Disabling + * stale connection check may result in slight performance improvement + * at the risk of getting an I/O error when executing a request over a + * connection that has been closed at the server side. + * + * @return <tt>true</tt> if stale connection check is to be used, + * <tt>false</tt> otherwise. + */ + public static boolean isStaleCheckingEnabled(final HttpParams params) { + if (params == null) { + throw new IllegalArgumentException("HTTP parameters may not be null"); + } + return params.getBooleanParameter + (CoreConnectionPNames.STALE_CONNECTION_CHECK, true); + } + + /** + * Defines whether stale connection check is to be used. Disabling + * stale connection check may result in slight performance improvement + * at the risk of getting an I/O error when executing a request over a + * connection that has been closed at the server side. + * + * @param value <tt>true</tt> if stale connection check is to be used, + * <tt>false</tt> otherwise. + */ + public static void setStaleCheckingEnabled(final HttpParams params, boolean value) { + if (params == null) { + throw new IllegalArgumentException("HTTP parameters may not be null"); + } + params.setBooleanParameter + (CoreConnectionPNames.STALE_CONNECTION_CHECK, value); + } + +} diff --git a/core/java/org/apache/http/params/HttpParams.java b/core/java/org/apache/http/params/HttpParams.java new file mode 100644 index 0000000..9562e54 --- /dev/null +++ b/core/java/org/apache/http/params/HttpParams.java @@ -0,0 +1,192 @@ +/* + * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpcore/trunk/module-main/src/main/java/org/apache/http/params/HttpParams.java $ + * $Revision: 610763 $ + * $Date: 2008-01-10 04:01:13 -0800 (Thu, 10 Jan 2008) $ + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ + +package org.apache.http.params; + +/** + * Represents a collection of HTTP protocol and framework parameters. + * + * @author <a href="mailto:oleg at ural.ru">Oleg Kalnichevski</a> + * + * @version $Revision: 610763 $ + * + * @since 4.0 + * + * @deprecated Please use {@link java.net.URL#openConnection} instead. + * Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> + * for further details. + */ +@Deprecated +public interface HttpParams { + + /** + * Obtains the value of the given parameter. + * + * @param name the parent name. + * + * @return an object that represents the value of the parameter, + * <code>null</code> if the parameter is not set or if it + * is explicitly set to <code>null</code> + * + * @see #setParameter(String, Object) + */ + Object getParameter(String name); + + /** + * Assigns the value to the parameter with the given name. + * + * @param name parameter name + * @param value parameter value + */ + HttpParams setParameter(String name, Object value); + + /** + * Creates a copy of these parameters. + * + * @return a new set of parameters holding the same values as this one + */ + HttpParams copy(); + + /** + * Removes the parameter with the specified name. + * + * @param name parameter name + * + * @return true if the parameter existed and has been removed, false else. + */ + boolean removeParameter(String name); + + /** + * Returns a {@link Long} parameter value with the given name. + * If the parameter is not explicitly set, the default value is returned. + * + * @param name the parent name. + * @param defaultValue the default value. + * + * @return a {@link Long} that represents the value of the parameter. + * + * @see #setLongParameter(String, long) + */ + long getLongParameter(String name, long defaultValue); + + /** + * Assigns a {@link Long} to the parameter with the given name + * + * @param name parameter name + * @param value parameter value + */ + HttpParams setLongParameter(String name, long value); + + /** + * Returns an {@link Integer} parameter value with the given name. + * If the parameter is not explicitly set, the default value is returned. + * + * @param name the parent name. + * @param defaultValue the default value. + * + * @return a {@link Integer} that represents the value of the parameter. + * + * @see #setIntParameter(String, int) + */ + int getIntParameter(String name, int defaultValue); + + /** + * Assigns an {@link Integer} to the parameter with the given name + * + * @param name parameter name + * @param value parameter value + */ + HttpParams setIntParameter(String name, int value); + + /** + * Returns a {@link Double} parameter value with the given name. + * If the parameter is not explicitly set, the default value is returned. + * + * @param name the parent name. + * @param defaultValue the default value. + * + * @return a {@link Double} that represents the value of the parameter. + * + * @see #setDoubleParameter(String, double) + */ + double getDoubleParameter(String name, double defaultValue); + + /** + * Assigns a {@link Double} to the parameter with the given name + * + * @param name parameter name + * @param value parameter value + */ + HttpParams setDoubleParameter(String name, double value); + + /** + * Returns a {@link Boolean} parameter value with the given name. + * If the parameter is not explicitly set, the default value is returned. + * + * @param name the parent name. + * @param defaultValue the default value. + * + * @return a {@link Boolean} that represents the value of the parameter. + * + * @see #setBooleanParameter(String, boolean) + */ + boolean getBooleanParameter(String name, boolean defaultValue); + + /** + * Assigns a {@link Boolean} to the parameter with the given name + * + * @param name parameter name + * @param value parameter value + */ + HttpParams setBooleanParameter(String name, boolean value); + + /** + * Checks if a boolean parameter is set to <code>true</code>. + * + * @param name parameter name + * + * @return <tt>true</tt> if the parameter is set to value <tt>true</tt>, + * <tt>false</tt> if it is not set or set to <code>false</code> + */ + boolean isParameterTrue(String name); + + /** + * Checks if a boolean parameter is not set or <code>false</code>. + * + * @param name parameter name + * + * @return <tt>true</tt> if the parameter is either not set or + * set to value <tt>false</tt>, + * <tt>false</tt> if it is set to <code>true</code> + */ + boolean isParameterFalse(String name); + +} |
