diff options
Diffstat (limited to 'core/java/android')
337 files changed, 16122 insertions, 6124 deletions
diff --git a/core/java/android/accounts/AccountAuthenticatorActivity.java b/core/java/android/accounts/AccountAuthenticatorActivity.java index 6a55ddf..f9284e6 100644 --- a/core/java/android/accounts/AccountAuthenticatorActivity.java +++ b/core/java/android/accounts/AccountAuthenticatorActivity.java @@ -17,7 +17,6 @@ package android.accounts; import android.app.Activity; -import android.content.Intent; import android.os.Bundle; /** diff --git a/core/java/android/accounts/AccountManagerFuture.java b/core/java/android/accounts/AccountManagerFuture.java index a1ab00c..af00a08 100644 --- a/core/java/android/accounts/AccountManagerFuture.java +++ b/core/java/android/accounts/AccountManagerFuture.java @@ -15,10 +15,7 @@ */ package android.accounts; -import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeoutException; import java.io.IOException; /** diff --git a/core/java/android/accounts/GrantCredentialsPermissionActivity.java b/core/java/android/accounts/GrantCredentialsPermissionActivity.java index 8b01c6a..12b2b9c 100644 --- a/core/java/android/accounts/GrantCredentialsPermissionActivity.java +++ b/core/java/android/accounts/GrantCredentialsPermissionActivity.java @@ -16,7 +16,6 @@ package android.accounts; import android.app.Activity; -import android.content.pm.RegisteredServicesCache; import android.content.res.Resources; import android.os.Bundle; import android.widget.TextView; @@ -30,7 +29,6 @@ import android.text.TextUtils; import com.android.internal.R; import java.io.IOException; -import java.net.Authenticator; /** * @hide diff --git a/core/java/android/animation/AnimatorInflater.java b/core/java/android/animation/AnimatorInflater.java index d753e32..20236aa 100644 --- a/core/java/android/animation/AnimatorInflater.java +++ b/core/java/android/animation/AnimatorInflater.java @@ -215,7 +215,7 @@ public class AnimatorInflater { (toType <= TypedValue.TYPE_LAST_COLOR_INT))) { // special case for colors: ignore valueType and get ints getFloats = false; - anim.setEvaluator(new ArgbEvaluator()); + evaluator = ArgbEvaluator.getInstance(); } if (getFloats) { diff --git a/core/java/android/animation/ArgbEvaluator.java b/core/java/android/animation/ArgbEvaluator.java index 717a3d9..ed07195 100644 --- a/core/java/android/animation/ArgbEvaluator.java +++ b/core/java/android/animation/ArgbEvaluator.java @@ -21,6 +21,19 @@ package android.animation; * values that represent ARGB colors. */ public class ArgbEvaluator implements TypeEvaluator { + private static final ArgbEvaluator sInstance = new ArgbEvaluator(); + + /** + * Returns an instance of <code>ArgbEvaluator</code> that may be used in + * {@link ValueAnimator#setEvaluator(TypeEvaluator)}. The same instance may + * be used in multiple <code>Animator</code>s because it holds no state. + * @return An instance of <code>ArgbEvalutor</code>. + * + * @hide + */ + public static ArgbEvaluator getInstance() { + return sInstance; + } /** * This function returns the calculated in-between value for a color diff --git a/core/java/android/animation/FloatArrayEvaluator.java b/core/java/android/animation/FloatArrayEvaluator.java new file mode 100644 index 0000000..9ae1197 --- /dev/null +++ b/core/java/android/animation/FloatArrayEvaluator.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2013 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.animation; + +/** + * This evaluator can be used to perform type interpolation between <code>float[]</code> values. + * Each index into the array is treated as a separate value to interpolate. For example, + * evaluating <code>{100, 200}</code> and <code>{300, 400}</code> will interpolate the value at + * the first index between 100 and 300 and the value at the second index value between 200 and 400. + */ +public class FloatArrayEvaluator implements TypeEvaluator<float[]> { + + private float[] mArray; + + /** + * Create a FloatArrayEvaluator that does not reuse the animated value. Care must be taken + * when using this option because on every evaluation a new <code>float[]</code> will be + * allocated. + * + * @see #FloatArrayEvaluator(float[]) + */ + public FloatArrayEvaluator() { + } + + /** + * Create a FloatArrayEvaluator that reuses <code>reuseArray</code> for every evaluate() call. + * Caution must be taken to ensure that the value returned from + * {@link android.animation.ValueAnimator#getAnimatedValue()} is not cached, modified, or + * used across threads. The value will be modified on each <code>evaluate()</code> call. + * + * @param reuseArray The array to modify and return from <code>evaluate</code>. + */ + public FloatArrayEvaluator(float[] reuseArray) { + mArray = reuseArray; + } + + /** + * Interpolates the value at each index by the fraction. If + * {@link #FloatArrayEvaluator(float[])} was used to construct this object, + * <code>reuseArray</code> will be returned, otherwise a new <code>float[]</code> + * will be returned. + * + * @param fraction The fraction from the starting to the ending values + * @param startValue The start value. + * @param endValue The end value. + * @return A <code>float[]</code> where each element is an interpolation between + * the same index in startValue and endValue. + */ + @Override + public float[] evaluate(float fraction, float[] startValue, float[] endValue) { + float[] array = mArray; + if (array == null) { + array = new float[startValue.length]; + } + + for (int i = 0; i < array.length; i++) { + float start = startValue[i]; + float end = endValue[i]; + array[i] = start + (fraction * (end - start)); + } + return array; + } +} diff --git a/core/java/android/animation/IntArrayEvaluator.java b/core/java/android/animation/IntArrayEvaluator.java new file mode 100644 index 0000000..d7f10f3 --- /dev/null +++ b/core/java/android/animation/IntArrayEvaluator.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2013 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.animation; + +/** + * This evaluator can be used to perform type interpolation between <code>int[]</code> values. + * Each index into the array is treated as a separate value to interpolate. For example, + * evaluating <code>{100, 200}</code> and <code>{300, 400}</code> will interpolate the value at + * the first index between 100 and 300 and the value at the second index value between 200 and 400. + */ +public class IntArrayEvaluator implements TypeEvaluator<int[]> { + + private int[] mArray; + + /** + * Create an IntArrayEvaluator that does not reuse the animated value. Care must be taken + * when using this option because on every evaluation a new <code>int[]</code> will be + * allocated. + * + * @see #IntArrayEvaluator(int[]) + */ + public IntArrayEvaluator() { + } + + /** + * Create an IntArrayEvaluator that reuses <code>reuseArray</code> for every evaluate() call. + * Caution must be taken to ensure that the value returned from + * {@link android.animation.ValueAnimator#getAnimatedValue()} is not cached, modified, or + * used across threads. The value will be modified on each <code>evaluate()</code> call. + * + * @param reuseArray The array to modify and return from <code>evaluate</code>. + */ + public IntArrayEvaluator(int[] reuseArray) { + mArray = reuseArray; + } + + /** + * Interpolates the value at each index by the fraction. If {@link #IntArrayEvaluator(int[])} + * was used to construct this object, <code>reuseArray</code> will be returned, otherwise + * a new <code>int[]</code> will be returned. + * + * @param fraction The fraction from the starting to the ending values + * @param startValue The start value. + * @param endValue The end value. + * @return An <code>int[]</code> where each element is an interpolation between + * the same index in startValue and endValue. + */ + @Override + public int[] evaluate(float fraction, int[] startValue, int[] endValue) { + int[] array = mArray; + if (array == null) { + array = new int[startValue.length]; + } + for (int i = 0; i < array.length; i++) { + int start = startValue[i]; + int end = endValue[i]; + array[i] = (int) (start + (fraction * (end - start))); + } + return array; + } +} diff --git a/core/java/android/animation/ObjectAnimator.java b/core/java/android/animation/ObjectAnimator.java index 9c88ccf..c0ce795 100644 --- a/core/java/android/animation/ObjectAnimator.java +++ b/core/java/android/animation/ObjectAnimator.java @@ -16,6 +16,8 @@ package android.animation; +import android.graphics.Path; +import android.graphics.PointF; import android.util.Log; import android.util.Property; @@ -191,7 +193,7 @@ public final class ObjectAnimator extends ValueAnimator { /** * Constructs and returns an ObjectAnimator that animates between int values. A single - * value implies that that value is the one being animated to. Two values imply a starting + * value implies that that value is the one being animated to. Two values imply starting * and ending values. More than two values imply a starting value, values to animate through * along the way, and an ending value (these values will be distributed evenly across * the duration of the animation). @@ -210,8 +212,33 @@ public final class ObjectAnimator extends ValueAnimator { } /** + * Constructs and returns an ObjectAnimator that animates coordinates along a <code>Path</code> + * using two properties. A <code>Path</code></> animation moves in two dimensions, animating + * coordinates <code>(x, y)</code> together to follow the line. In this variation, the + * coordinates are integers that are set to separate properties designated by + * <code>xPropertyName</code> and <code>yPropertyName</code>. + * + * @param target The object whose properties are to be animated. This object should + * have public methods on it called <code>setNameX()</code> and + * <code>setNameY</code>, where <code>nameX</code> and <code>nameY</code> + * are the value of <code>xPropertyName</code> and <code>yPropertyName</code> + * parameters, respectively. + * @param xPropertyName The name of the property for the x coordinate being animated. + * @param yPropertyName The name of the property for the y coordinate being animated. + * @param path The <code>Path</code> to animate values along. + * @return An ObjectAnimator object that is set up to animate along <code>path</code>. + */ + public static ObjectAnimator ofInt(Object target, String xPropertyName, String yPropertyName, + Path path) { + Keyframe[][] keyframes = PropertyValuesHolder.createKeyframes(path, true); + PropertyValuesHolder x = PropertyValuesHolder.ofKeyframe(xPropertyName, keyframes[0]); + PropertyValuesHolder y = PropertyValuesHolder.ofKeyframe(yPropertyName, keyframes[1]); + return ofPropertyValuesHolder(target, x, y); + } + + /** * Constructs and returns an ObjectAnimator that animates between int values. A single - * value implies that that value is the one being animated to. Two values imply a starting + * value implies that that value is the one being animated to. Two values imply starting * and ending values. More than two values imply a starting value, values to animate through * along the way, and an ending value (these values will be distributed evenly across * the duration of the animation). @@ -228,8 +255,135 @@ public final class ObjectAnimator extends ValueAnimator { } /** + * Constructs and returns an ObjectAnimator that animates coordinates along a <code>Path</code> + * using two properties. A <code>Path</code></> animation moves in two dimensions, animating + * coordinates <code>(x, y)</code> together to follow the line. In this variation, the + * coordinates are integers that are set to separate properties, <code>xProperty</code> and + * <code>yProperty</code>. + * + * @param target The object whose properties are to be animated. + * @param xProperty The property for the x coordinate being animated. + * @param yProperty The property for the y coordinate being animated. + * @param path The <code>Path</code> to animate values along. + * @return An ObjectAnimator object that is set up to animate along <code>path</code>. + */ + public static <T> ObjectAnimator ofInt(T target, Property<T, Integer> xProperty, + Property<T, Integer> yProperty, Path path) { + Keyframe[][] keyframes = PropertyValuesHolder.createKeyframes(path, true); + PropertyValuesHolder x = PropertyValuesHolder.ofKeyframe(xProperty, keyframes[0]); + PropertyValuesHolder y = PropertyValuesHolder.ofKeyframe(yProperty, keyframes[1]); + return ofPropertyValuesHolder(target, x, y); + } + + /** + * Constructs and returns an ObjectAnimator that animates over int values for a multiple + * parameters setter. Only public methods that take only int parameters are supported. + * Each <code>int[]</code> contains a complete set of parameters to the setter method. + * At least two <code>int[]</code> values must be provided, a start and end. More than two + * values imply a starting value, values to animate through along the way, and an ending + * value (these values will be distributed evenly across the duration of the animation). + * + * @param target The object whose property is to be animated. This object may + * have a public method on it called <code>setName()</code>, where <code>name</code> is + * the value of the <code>propertyName</code> parameter. <code>propertyName</code> may also + * be the case-sensitive complete name of the public setter method. + * @param propertyName The name of the property being animated or the name of the setter method. + * @param values A set of values that the animation will animate between over time. + * @return An ObjectAnimator object that is set up to animate between the given values. + */ + public static ObjectAnimator ofMultiInt(Object target, String propertyName, int[][] values) { + PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiInt(propertyName, values); + return ofPropertyValuesHolder(target, pvh); + } + + /** + * Constructs and returns an ObjectAnimator that animates the target using a multi-int setter + * along the given <code>Path</code>. A <code>Path</code></> animation moves in two dimensions, + * animating coordinates <code>(x, y)</code> together to follow the line. In this variation, the + * coordinates are integer x and y coordinates used in the first and second parameter of the + * setter, respectively. + * + * @param target The object whose property is to be animated. This object may + * have a public method on it called <code>setName()</code>, where <code>name</code> is + * the value of the <code>propertyName</code> parameter. <code>propertyName</code> may also + * be the case-sensitive complete name of the public setter method. + * @param propertyName The name of the property being animated or the name of the setter method. + * @param path The <code>Path</code> to animate values along. + * @return An ObjectAnimator object that is set up to animate along <code>path</code>. + */ + public static ObjectAnimator ofMultiInt(Object target, String propertyName, Path path) { + PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiInt(propertyName, path); + return ofPropertyValuesHolder(target, pvh); + } + + /** + * Constructs and returns an ObjectAnimator that animates over values for a multiple int + * parameters setter. Only public methods that take only int parameters are supported. + * <p>At least two values must be provided, a start and end. More than two + * values imply a starting value, values to animate through along the way, and an ending + * value (these values will be distributed evenly across the duration of the animation).</p> + * + * @param target The object whose property is to be animated. This object may + * have a public method on it called <code>setName()</code>, where <code>name</code> is + * the value of the <code>propertyName</code> parameter. <code>propertyName</code> may also + * be the case-sensitive complete name of the public setter method. + * @param propertyName The name of the property being animated or the name of the setter method. + * @param converter Converts T objects into int parameters for the multi-value setter. + * @param evaluator A TypeEvaluator that will be called on each animation frame to + * provide the necessary interpolation between the Object values to derive the animated + * value. + * @param values A set of values that the animation will animate between over time. + * @return An ObjectAnimator object that is set up to animate between the given values. + */ + public static <T> ObjectAnimator ofMultiInt(Object target, String propertyName, + TypeConverter<T, int[]> converter, TypeEvaluator<T> evaluator, T... values) { + PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiInt(propertyName, converter, + evaluator, values); + return ObjectAnimator.ofPropertyValuesHolder(target, pvh); + } + + /** + * Constructs and returns an ObjectAnimator that animates between color values. A single + * value implies that that value is the one being animated to. Two values imply starting + * and ending values. More than two values imply a starting value, values to animate through + * along the way, and an ending value (these values will be distributed evenly across + * the duration of the animation). + * + * @param target The object whose property is to be animated. This object should + * have a public method on it called <code>setName()</code>, where <code>name</code> is + * the value of the <code>propertyName</code> parameter. + * @param propertyName The name of the property being animated. + * @param values A set of values that the animation will animate between over time. + * @return An ObjectAnimator object that is set up to animate between the given values. + */ + public static ObjectAnimator ofArgb(Object target, String propertyName, int... values) { + ObjectAnimator animator = ofInt(target, propertyName, values); + animator.setEvaluator(ArgbEvaluator.getInstance()); + return animator; + } + + /** + * Constructs and returns an ObjectAnimator that animates between color values. A single + * value implies that that value is the one being animated to. Two values imply starting + * and ending values. More than two values imply a starting value, values to animate through + * along the way, and an ending value (these values will be distributed evenly across + * the duration of the animation). + * + * @param target The object whose property is to be animated. + * @param property The property being animated. + * @param values A set of values that the animation will animate between over time. + * @return An ObjectAnimator object that is set up to animate between the given values. + */ + public static <T> ObjectAnimator ofArgb(T target, Property<T, Integer> property, + int... values) { + ObjectAnimator animator = ofInt(target, property, values); + animator.setEvaluator(ArgbEvaluator.getInstance()); + return animator; + } + + /** * Constructs and returns an ObjectAnimator that animates between float values. A single - * value implies that that value is the one being animated to. Two values imply a starting + * value implies that that value is the one being animated to. Two values imply starting * and ending values. More than two values imply a starting value, values to animate through * along the way, and an ending value (these values will be distributed evenly across * the duration of the animation). @@ -248,8 +402,33 @@ public final class ObjectAnimator extends ValueAnimator { } /** + * Constructs and returns an ObjectAnimator that animates coordinates along a <code>Path</code> + * using two properties. A <code>Path</code></> animation moves in two dimensions, animating + * coordinates <code>(x, y)</code> together to follow the line. In this variation, the + * coordinates are floats that are set to separate properties designated by + * <code>xPropertyName</code> and <code>yPropertyName</code>. + * + * @param target The object whose properties are to be animated. This object should + * have public methods on it called <code>setNameX()</code> and + * <code>setNameY</code>, where <code>nameX</code> and <code>nameY</code> + * are the value of the <code>xPropertyName</code> and <code>yPropertyName</code> + * parameters, respectively. + * @param xPropertyName The name of the property for the x coordinate being animated. + * @param yPropertyName The name of the property for the y coordinate being animated. + * @param path The <code>Path</code> to animate values along. + * @return An ObjectAnimator object that is set up to animate along <code>path</code>. + */ + public static ObjectAnimator ofFloat(Object target, String xPropertyName, String yPropertyName, + Path path) { + Keyframe[][] keyframes = PropertyValuesHolder.createKeyframes(path, false); + PropertyValuesHolder x = PropertyValuesHolder.ofKeyframe(xPropertyName, keyframes[0]); + PropertyValuesHolder y = PropertyValuesHolder.ofKeyframe(yPropertyName, keyframes[1]); + return ofPropertyValuesHolder(target, x, y); + } + + /** * Constructs and returns an ObjectAnimator that animates between float values. A single - * value implies that that value is the one being animated to. Two values imply a starting + * value implies that that value is the one being animated to. Two values imply starting * and ending values. More than two values imply a starting value, values to animate through * along the way, and an ending value (these values will be distributed evenly across * the duration of the animation). @@ -267,8 +446,94 @@ public final class ObjectAnimator extends ValueAnimator { } /** + * Constructs and returns an ObjectAnimator that animates coordinates along a <code>Path</code> + * using two properties. A <code>Path</code></> animation moves in two dimensions, animating + * coordinates <code>(x, y)</code> together to follow the line. In this variation, the + * coordinates are floats that are set to separate properties, <code>xProperty</code> and + * <code>yProperty</code>. + * + * @param target The object whose properties are to be animated. + * @param xProperty The property for the x coordinate being animated. + * @param yProperty The property for the y coordinate being animated. + * @param path The <code>Path</code> to animate values along. + * @return An ObjectAnimator object that is set up to animate along <code>path</code>. + */ + public static <T> ObjectAnimator ofFloat(T target, Property<T, Float> xProperty, + Property<T, Float> yProperty, Path path) { + return ofFloat(target, xProperty.getName(), yProperty.getName(), path); + } + + /** + * Constructs and returns an ObjectAnimator that animates over float values for a multiple + * parameters setter. Only public methods that take only float parameters are supported. + * Each <code>float[]</code> contains a complete set of parameters to the setter method. + * At least two <code>float[]</code> values must be provided, a start and end. More than two + * values imply a starting value, values to animate through along the way, and an ending + * value (these values will be distributed evenly across the duration of the animation). + * + * @param target The object whose property is to be animated. This object may + * have a public method on it called <code>setName()</code>, where <code>name</code> is + * the value of the <code>propertyName</code> parameter. <code>propertyName</code> may also + * be the case-sensitive complete name of the public setter method. + * @param propertyName The name of the property being animated or the name of the setter method. + * @param values A set of values that the animation will animate between over time. + * @return An ObjectAnimator object that is set up to animate between the given values. + */ + public static ObjectAnimator ofMultiFloat(Object target, String propertyName, + float[][] values) { + PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiFloat(propertyName, values); + return ofPropertyValuesHolder(target, pvh); + } + + /** + * Constructs and returns an ObjectAnimator that animates the target using a multi-float setter + * along the given <code>Path</code>. A <code>Path</code></> animation moves in two dimensions, + * animating coordinates <code>(x, y)</code> together to follow the line. In this variation, the + * coordinates are float x and y coordinates used in the first and second parameter of the + * setter, respectively. + * + * @param target The object whose property is to be animated. This object may + * have a public method on it called <code>setName()</code>, where <code>name</code> is + * the value of the <code>propertyName</code> parameter. <code>propertyName</code> may also + * be the case-sensitive complete name of the public setter method. + * @param propertyName The name of the property being animated or the name of the setter method. + * @param path The <code>Path</code> to animate values along. + * @return An ObjectAnimator object that is set up to animate along <code>path</code>. + */ + public static ObjectAnimator ofMultiFloat(Object target, String propertyName, Path path) { + PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiFloat(propertyName, path); + return ofPropertyValuesHolder(target, pvh); + } + + /** + * Constructs and returns an ObjectAnimator that animates over values for a multiple float + * parameters setter. Only public methods that take only float parameters are supported. + * <p>At least two values must be provided, a start and end. More than two + * values imply a starting value, values to animate through along the way, and an ending + * value (these values will be distributed evenly across the duration of the animation).</p> + * + * @param target The object whose property is to be animated. This object may + * have a public method on it called <code>setName()</code>, where <code>name</code> is + * the value of the <code>propertyName</code> parameter. <code>propertyName</code> may also + * be the case-sensitive complete name of the public setter method. + * @param propertyName The name of the property being animated or the name of the setter method. + * @param converter Converts T objects into float parameters for the multi-value setter. + * @param evaluator A TypeEvaluator that will be called on each animation frame to + * provide the necessary interpolation between the Object values to derive the animated + * value. + * @param values A set of values that the animation will animate between over time. + * @return An ObjectAnimator object that is set up to animate between the given values. + */ + public static <T> ObjectAnimator ofMultiFloat(Object target, String propertyName, + TypeConverter<T, float[]> converter, TypeEvaluator<T> evaluator, T... values) { + PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiFloat(propertyName, converter, + evaluator, values); + return ObjectAnimator.ofPropertyValuesHolder(target, pvh); + } + + /** * Constructs and returns an ObjectAnimator that animates between Object values. A single - * value implies that that value is the one being animated to. Two values imply a starting + * value implies that that value is the one being animated to. Two values imply starting * and ending values. More than two values imply a starting value, values to animate through * along the way, and an ending value (these values will be distributed evenly across * the duration of the animation). @@ -292,8 +557,32 @@ public final class ObjectAnimator extends ValueAnimator { } /** + * Constructs and returns an ObjectAnimator that animates a property along a <code>Path</code>. + * A <code>Path</code></> animation moves in two dimensions, animating coordinates + * <code>(x, y)</code> together to follow the line. This variant animates the coordinates + * in a <code>PointF</code> to follow the <code>Path</code>. If the <code>Property</code> + * associated with <code>propertyName</code> uses a type other than <code>PointF</code>, + * <code>converter</code> can be used to change from <code>PointF</code> to the type + * associated with the <code>Property</code>. + * + * @param target The object whose property is to be animated. This object should + * have a public method on it called <code>setName()</code>, where <code>name</code> is + * the value of the <code>propertyName</code> parameter. + * @param propertyName The name of the property being animated. + * @param converter Converts a PointF to the type associated with the setter. May be + * null if conversion is unnecessary. + * @param path The <code>Path</code> to animate values along. + * @return An ObjectAnimator object that is set up to animate along <code>path</code>. + */ + public static ObjectAnimator ofObject(Object target, String propertyName, + TypeConverter<PointF, ?> converter, Path path) { + PropertyValuesHolder pvh = PropertyValuesHolder.ofObject(propertyName, converter, path); + return ofPropertyValuesHolder(target, pvh); + } + + /** * Constructs and returns an ObjectAnimator that animates between Object values. A single - * value implies that that value is the one being animated to. Two values imply a starting + * value implies that that value is the one being animated to. Two values imply starting * and ending values. More than two values imply a starting value, values to animate through * along the way, and an ending value (these values will be distributed evenly across * the duration of the animation). @@ -315,6 +604,53 @@ public final class ObjectAnimator extends ValueAnimator { } /** + * Constructs and returns an ObjectAnimator that animates between Object values. A single + * value implies that that value is the one being animated to. Two values imply starting + * and ending values. More than two values imply a starting value, values to animate through + * along the way, and an ending value (these values will be distributed evenly across + * the duration of the animation). This variant supplies a <code>TypeConverter</code> to + * convert from the animated values to the type of the property. If only one value is + * supplied, the <code>TypeConverter</code> must implement + * {@link TypeConverter#convertBack(Object)} to retrieve the current value. + * + * @param target The object whose property is to be animated. + * @param property The property being animated. + * @param converter Converts the animated object to the Property type. + * @param evaluator A TypeEvaluator that will be called on each animation frame to + * provide the necessary interpolation between the Object values to derive the animated + * value. + * @param values A set of values that the animation will animate between over time. + * @return An ObjectAnimator object that is set up to animate between the given values. + */ + public static <T, V, P> ObjectAnimator ofObject(T target, Property<T, P> property, + TypeConverter<V, P> converter, TypeEvaluator<V> evaluator, V... values) { + PropertyValuesHolder pvh = PropertyValuesHolder.ofObject(property, converter, evaluator, + values); + return ofPropertyValuesHolder(target, pvh); + } + + /** + * Constructs and returns an ObjectAnimator that animates a property along a <code>Path</code>. + * A <code>Path</code></> animation moves in two dimensions, animating coordinates + * <code>(x, y)</code> together to follow the line. This variant animates the coordinates + * in a <code>PointF</code> to follow the <code>Path</code>. If <code>property</code> + * uses a type other than <code>PointF</code>, <code>converter</code> can be used to change + * from <code>PointF</code> to the type associated with the <code>Property</code>. + * + * @param target The object whose property is to be animated. + * @param property The property being animated. Should not be null. + * @param converter Converts a PointF to the type associated with the setter. May be + * null if conversion is unnecessary. + * @param path The <code>Path</code> to animate values along. + * @return An ObjectAnimator object that is set up to animate along <code>path</code>. + */ + public static <T, V> ObjectAnimator ofObject(T target, Property<T, V> property, + TypeConverter<PointF, V> converter, Path path) { + PropertyValuesHolder pvh = PropertyValuesHolder.ofObject(property, converter, path); + return ofPropertyValuesHolder(target, pvh); + } + + /** * Constructs and returns an ObjectAnimator that animates between the sets of values specified * in <code>PropertyValueHolder</code> objects. This variant should be used when animating * several properties at once with the same ObjectAnimator, since PropertyValuesHolder allows diff --git a/core/java/android/animation/PointFEvaluator.java b/core/java/android/animation/PointFEvaluator.java new file mode 100644 index 0000000..91d501f --- /dev/null +++ b/core/java/android/animation/PointFEvaluator.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2013 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.animation; + +import android.graphics.PointF; + +/** + * This evaluator can be used to perform type interpolation between <code>PointF</code> values. + */ +public class PointFEvaluator implements TypeEvaluator<PointF> { + + /** + * When null, a new PointF is returned on every evaluate call. When non-null, + * mPoint will be modified and returned on every evaluate. + */ + private PointF mPoint; + + /** + * Construct a PointFEvaluator that returns a new PointF on every evaluate call. + * To avoid creating an object for each evaluate call, + * {@link PointFEvaluator#PointFEvaluator(android.graphics.PointF)} should be used + * whenever possible. + */ + public PointFEvaluator() { + } + + /** + * Constructs a PointFEvaluator that modifies and returns <code>reuse</code> + * in {@link #evaluate(float, android.graphics.PointF, android.graphics.PointF)} calls. + * The value returned from + * {@link #evaluate(float, android.graphics.PointF, android.graphics.PointF)} should + * not be cached because it will change over time as the object is reused on each + * call. + * + * @param reuse A PointF to be modified and returned by evaluate. + */ + public PointFEvaluator(PointF reuse) { + mPoint = reuse; + } + + /** + * This function returns the result of linearly interpolating the start and + * end PointF values, with <code>fraction</code> representing the proportion + * between the start and end values. The calculation is a simple parametric + * calculation on each of the separate components in the PointF objects + * (x, y). + * + * <p>If {@link #PointFEvaluator(android.graphics.PointF)} was used to construct + * this PointFEvaluator, the object returned will be the <code>reuse</code> + * passed into the constructor.</p> + * + * @param fraction The fraction from the starting to the ending values + * @param startValue The start PointF + * @param endValue The end PointF + * @return A linear interpolation between the start and end values, given the + * <code>fraction</code> parameter. + */ + @Override + public PointF evaluate(float fraction, PointF startValue, PointF endValue) { + float x = startValue.x + (fraction * (endValue.x - startValue.x)); + float y = startValue.y + (fraction * (endValue.y - startValue.y)); + + if (mPoint != null) { + mPoint.set(x, y); + return mPoint; + } else { + return new PointF(x, y); + } + } +} diff --git a/core/java/android/animation/PropertyValuesHolder.java b/core/java/android/animation/PropertyValuesHolder.java index 43014ad..1b028e0 100644 --- a/core/java/android/animation/PropertyValuesHolder.java +++ b/core/java/android/animation/PropertyValuesHolder.java @@ -16,6 +16,9 @@ package android.animation; +import android.graphics.Path; +import android.graphics.PointF; +import android.util.FloatMath; import android.util.FloatProperty; import android.util.IntProperty; import android.util.Log; @@ -124,6 +127,11 @@ public class PropertyValuesHolder implements Cloneable { private Object mAnimatedValue; /** + * Converts from the source Object type to the setter Object type. + */ + private TypeConverter mConverter; + + /** * Internal utility constructor, used by the factory methods to set the property name. * @param propertyName The name of the property for this holder. */ @@ -166,6 +174,104 @@ public class PropertyValuesHolder implements Cloneable { /** * Constructs and returns a PropertyValuesHolder with a given property name and + * set of <code>int[]</code> values. At least two <code>int[]</code> values must be supplied, + * a start and end value. If more values are supplied, the values will be animated from the + * start, through all intermediate values to the end value. When used with ObjectAnimator, + * the elements of the array represent the parameters of the setter function. + * + * @param propertyName The name of the property being animated. Can also be the + * case-sensitive name of the entire setter method. Should not be null. + * @param values The values that the property will animate between. + * @return PropertyValuesHolder The constructed PropertyValuesHolder object. + * @see IntArrayEvaluator#IntArrayEvaluator(int[]) + * @see ObjectAnimator#ofMultiInt(Object, String, TypeConverter, TypeEvaluator, Object[]) + */ + public static PropertyValuesHolder ofMultiInt(String propertyName, int[][] values) { + if (values.length < 2) { + throw new IllegalArgumentException("At least 2 values must be supplied"); + } + int numParameters = 0; + for (int i = 0; i < values.length; i++) { + if (values[i] == null) { + throw new IllegalArgumentException("values must not be null"); + } + int length = values[i].length; + if (i == 0) { + numParameters = length; + } else if (length != numParameters) { + throw new IllegalArgumentException("Values must all have the same length"); + } + } + IntArrayEvaluator evaluator = new IntArrayEvaluator(new int[numParameters]); + return new MultiIntValuesHolder(propertyName, null, evaluator, (Object[]) values); + } + + /** + * Constructs and returns a PropertyValuesHolder with a given property name to use + * as a multi-int setter. The values are animated along the path, with the first + * parameter of the setter set to the x coordinate and the second set to the y coordinate. + * + * @param propertyName The name of the property being animated. Can also be the + * case-sensitive name of the entire setter method. Should not be null. + * The setter must take exactly two <code>int</code> parameters. + * @param path The Path along which the values should be animated. + * @return PropertyValuesHolder The constructed PropertyValuesHolder object. + * @see ObjectAnimator#ofPropertyValuesHolder(Object, PropertyValuesHolder...) + */ + public static PropertyValuesHolder ofMultiInt(String propertyName, Path path) { + Keyframe[] keyframes = createKeyframes(path); + KeyframeSet keyframeSet = KeyframeSet.ofKeyframe(keyframes); + TypeEvaluator<PointF> evaluator = new PointFEvaluator(new PointF()); + PointFToIntArray converter = new PointFToIntArray(); + return new MultiIntValuesHolder(propertyName, converter, evaluator, keyframeSet); + } + + /** + * Constructs and returns a PropertyValuesHolder with a given property and + * set of Object values for use with ObjectAnimator multi-value setters. The Object + * values are converted to <code>int[]</code> using the converter. + * + * @param propertyName The property being animated or complete name of the setter. + * Should not be null. + * @param converter Used to convert the animated value to setter parameters. + * @param evaluator A TypeEvaluator that will be called on each animation frame to + * provide the necessary interpolation between the Object values to derive the animated + * value. + * @param values The values that the property will animate between. + * @return PropertyValuesHolder The constructed PropertyValuesHolder object. + * @see ObjectAnimator#ofMultiInt(Object, String, TypeConverter, TypeEvaluator, Object[]) + * @see ObjectAnimator#ofPropertyValuesHolder(Object, PropertyValuesHolder...) + */ + public static <V> PropertyValuesHolder ofMultiInt(String propertyName, + TypeConverter<V, int[]> converter, TypeEvaluator<V> evaluator, V... values) { + return new MultiIntValuesHolder(propertyName, converter, evaluator, values); + } + + /** + * Constructs and returns a PropertyValuesHolder object with the specified property name or + * setter name for use in a multi-int setter function using ObjectAnimator. The values can be + * of any type, but the type should be consistent so that the supplied + * {@link android.animation.TypeEvaluator} can be used to to evaluate the animated value. The + * <code>converter</code> converts the values to parameters in the setter function. + * + * <p>At least two values must be supplied, a start and an end value.</p> + * + * @param propertyName The name of the property to associate with the set of values. This + * may also be the complete name of a setter function. + * @param converter Converts <code>values</code> into int parameters for the setter. + * Can be null if the Keyframes have int[] values. + * @param evaluator Used to interpolate between values. + * @param values The values at specific fractional times to evaluate between + * @return A PropertyValuesHolder for a multi-int parameter setter. + */ + public static <T> PropertyValuesHolder ofMultiInt(String propertyName, + TypeConverter<T, int[]> converter, TypeEvaluator<T> evaluator, Keyframe... values) { + KeyframeSet keyframeSet = KeyframeSet.ofKeyframe(values); + return new MultiIntValuesHolder(propertyName, converter, evaluator, keyframeSet); + } + + /** + * Constructs and returns a PropertyValuesHolder with a given property name and * set of float values. * @param propertyName The name of the property being animated. * @param values The values that the named property will animate between. @@ -188,6 +294,103 @@ public class PropertyValuesHolder implements Cloneable { /** * Constructs and returns a PropertyValuesHolder with a given property name and + * set of <code>float[]</code> values. At least two <code>float[]</code> values must be supplied, + * a start and end value. If more values are supplied, the values will be animated from the + * start, through all intermediate values to the end value. When used with ObjectAnimator, + * the elements of the array represent the parameters of the setter function. + * + * @param propertyName The name of the property being animated. Can also be the + * case-sensitive name of the entire setter method. Should not be null. + * @param values The values that the property will animate between. + * @return PropertyValuesHolder The constructed PropertyValuesHolder object. + * @see FloatArrayEvaluator#FloatArrayEvaluator(float[]) + * @see ObjectAnimator#ofMultiFloat(Object, String, TypeConverter, TypeEvaluator, Object[]) + */ + public static PropertyValuesHolder ofMultiFloat(String propertyName, float[][] values) { + if (values.length < 2) { + throw new IllegalArgumentException("At least 2 values must be supplied"); + } + int numParameters = 0; + for (int i = 0; i < values.length; i++) { + if (values[i] == null) { + throw new IllegalArgumentException("values must not be null"); + } + int length = values[i].length; + if (i == 0) { + numParameters = length; + } else if (length != numParameters) { + throw new IllegalArgumentException("Values must all have the same length"); + } + } + FloatArrayEvaluator evaluator = new FloatArrayEvaluator(new float[numParameters]); + return new MultiFloatValuesHolder(propertyName, null, evaluator, (Object[]) values); + } + + /** + * Constructs and returns a PropertyValuesHolder with a given property name to use + * as a multi-float setter. The values are animated along the path, with the first + * parameter of the setter set to the x coordinate and the second set to the y coordinate. + * + * @param propertyName The name of the property being animated. Can also be the + * case-sensitive name of the entire setter method. Should not be null. + * The setter must take exactly two <code>float</code> parameters. + * @param path The Path along which the values should be animated. + * @return PropertyValuesHolder The constructed PropertyValuesHolder object. + * @see ObjectAnimator#ofPropertyValuesHolder(Object, PropertyValuesHolder...) + */ + public static PropertyValuesHolder ofMultiFloat(String propertyName, Path path) { + Keyframe[] keyframes = createKeyframes(path); + KeyframeSet keyframeSet = KeyframeSet.ofKeyframe(keyframes); + TypeEvaluator<PointF> evaluator = new PointFEvaluator(new PointF()); + PointFToFloatArray converter = new PointFToFloatArray(); + return new MultiFloatValuesHolder(propertyName, converter, evaluator, keyframeSet); + } + + /** + * Constructs and returns a PropertyValuesHolder with a given property and + * set of Object values for use with ObjectAnimator multi-value setters. The Object + * values are converted to <code>float[]</code> using the converter. + * + * @param propertyName The property being animated or complete name of the setter. + * Should not be null. + * @param converter Used to convert the animated value to setter parameters. + * @param evaluator A TypeEvaluator that will be called on each animation frame to + * provide the necessary interpolation between the Object values to derive the animated + * value. + * @param values The values that the property will animate between. + * @return PropertyValuesHolder The constructed PropertyValuesHolder object. + * @see ObjectAnimator#ofMultiFloat(Object, String, TypeConverter, TypeEvaluator, Object[]) + */ + public static <V> PropertyValuesHolder ofMultiFloat(String propertyName, + TypeConverter<V, float[]> converter, TypeEvaluator<V> evaluator, V... values) { + return new MultiFloatValuesHolder(propertyName, converter, evaluator, values); + } + + /** + * Constructs and returns a PropertyValuesHolder object with the specified property name or + * setter name for use in a multi-float setter function using ObjectAnimator. The values can be + * of any type, but the type should be consistent so that the supplied + * {@link android.animation.TypeEvaluator} can be used to to evaluate the animated value. The + * <code>converter</code> converts the values to parameters in the setter function. + * + * <p>At least two values must be supplied, a start and an end value.</p> + * + * @param propertyName The name of the property to associate with the set of values. This + * may also be the complete name of a setter function. + * @param converter Converts <code>values</code> into float parameters for the setter. + * Can be null if the Keyframes have float[] values. + * @param evaluator Used to interpolate between values. + * @param values The values at specific fractional times to evaluate between + * @return A PropertyValuesHolder for a multi-float parameter setter. + */ + public static <T> PropertyValuesHolder ofMultiFloat(String propertyName, + TypeConverter<T, float[]> converter, TypeEvaluator<T> evaluator, Keyframe... values) { + KeyframeSet keyframeSet = KeyframeSet.ofKeyframe(values); + return new MultiFloatValuesHolder(propertyName, converter, evaluator, keyframeSet); + } + + /** + * Constructs and returns a PropertyValuesHolder with a given property name and * set of Object values. This variant also takes a TypeEvaluator because the system * cannot automatically interpolate between objects of unknown type. * @@ -207,6 +410,27 @@ public class PropertyValuesHolder implements Cloneable { } /** + * Constructs and returns a PropertyValuesHolder with a given property name and + * a Path along which the values should be animated. This variant supports a + * <code>TypeConverter</code> to convert from <code>PointF</code> to the target + * type. + * + * @param propertyName The name of the property being animated. + * @param converter Converts a PointF to the type associated with the setter. May be + * null if conversion is unnecessary. + * @param path The Path along which the values should be animated. + * @return PropertyValuesHolder The constructed PropertyValuesHolder object. + */ + public static PropertyValuesHolder ofObject(String propertyName, + TypeConverter<PointF, ?> converter, Path path) { + Keyframe[] keyframes = createKeyframes(path); + PropertyValuesHolder pvh = ofKeyframe(propertyName, keyframes); + pvh.setEvaluator(new PointFEvaluator(new PointF())); + pvh.setConverter(converter); + return pvh; + } + + /** * Constructs and returns a PropertyValuesHolder with a given property and * set of Object values. This variant also takes a TypeEvaluator because the system * cannot automatically interpolate between objects of unknown type. @@ -227,6 +451,55 @@ public class PropertyValuesHolder implements Cloneable { } /** + * Constructs and returns a PropertyValuesHolder with a given property and + * set of Object values. This variant also takes a TypeEvaluator because the system + * cannot automatically interpolate between objects of unknown type. This variant also + * takes a <code>TypeConverter</code> to convert from animated values to the type + * of the property. If only one value is supplied, the <code>TypeConverter</code> + * must implement {@link TypeConverter#convertBack(Object)} to retrieve the current + * value. + * + * @param property The property being animated. Should not be null. + * @param converter Converts the animated object to the Property type. + * @param evaluator A TypeEvaluator that will be called on each animation frame to + * provide the necessary interpolation between the Object values to derive the animated + * value. + * @param values The values that the property will animate between. + * @return PropertyValuesHolder The constructed PropertyValuesHolder object. + * @see #setConverter(TypeConverter) + * @see TypeConverter + */ + public static <T, V> PropertyValuesHolder ofObject(Property<?, V> property, + TypeConverter<T, V> converter, TypeEvaluator<T> evaluator, T... values) { + PropertyValuesHolder pvh = new PropertyValuesHolder(property); + pvh.setConverter(converter); + pvh.setObjectValues(values); + pvh.setEvaluator(evaluator); + return pvh; + } + + /** + * Constructs and returns a PropertyValuesHolder with a given property and + * a Path along which the values should be animated. This variant supports a + * <code>TypeConverter</code> to convert from <code>PointF</code> to the target + * type. + * + * @param property The property being animated. Should not be null. + * @param converter Converts a PointF to the type associated with the setter. May be + * null if conversion is unnecessary. + * @param path The Path along which the values should be animated. + * @return PropertyValuesHolder The constructed PropertyValuesHolder object. + */ + public static <V> PropertyValuesHolder ofObject(Property<?, V> property, + TypeConverter<PointF, V> converter, Path path) { + Keyframe[] keyframes = createKeyframes(path); + PropertyValuesHolder pvh = ofKeyframe(property, keyframes); + pvh.setEvaluator(new PointFEvaluator(new PointF())); + pvh.setConverter(converter); + return pvh; + } + + /** * Constructs and returns a PropertyValuesHolder object with the specified property name and set * of values. These values can be of any type, but the type should be consistent so that * an appropriate {@link android.animation.TypeEvaluator} can be found that matches @@ -361,6 +634,14 @@ public class PropertyValuesHolder implements Cloneable { } /** + * Sets the converter to convert from the values type to the setter's parameter type. + * @param converter The converter to use to convert values. + */ + public void setConverter(TypeConverter converter) { + mConverter = converter; + } + + /** * Determine the setter or getter function using the JavaBeans convention of setFoo or * getFoo for a property named 'foo'. This function figures out what the name of the * function should be and uses reflection to find the Method with that name on the @@ -389,22 +670,24 @@ public class PropertyValuesHolder implements Cloneable { } else { args = new Class[1]; Class typeVariants[]; - if (mValueType.equals(Float.class)) { + if (valueType.equals(Float.class)) { typeVariants = FLOAT_VARIANTS; - } else if (mValueType.equals(Integer.class)) { + } else if (valueType.equals(Integer.class)) { typeVariants = INTEGER_VARIANTS; - } else if (mValueType.equals(Double.class)) { + } else if (valueType.equals(Double.class)) { typeVariants = DOUBLE_VARIANTS; } else { typeVariants = new Class[1]; - typeVariants[0] = mValueType; + typeVariants[0] = valueType; } for (Class typeVariant : typeVariants) { args[0] = typeVariant; try { returnVal = targetClass.getMethod(methodName, args); - // change the value type to suit - mValueType = typeVariant; + if (mConverter == null) { + // change the value type to suit + mValueType = typeVariant; + } return returnVal; } catch (NoSuchMethodException e) { // Swallow the error and keep trying other variants @@ -415,7 +698,7 @@ public class PropertyValuesHolder implements Cloneable { if (returnVal == null) { Log.w("PropertyValuesHolder", "Method " + - getMethodName(prefix, mPropertyName) + "() with type " + mValueType + + getMethodName(prefix, mPropertyName) + "() with type " + valueType + " not found on target class " + targetClass); } @@ -465,7 +748,8 @@ public class PropertyValuesHolder implements Cloneable { * @param targetClass The Class on which the requested method should exist. */ void setupSetter(Class targetClass) { - mSetter = setupSetterOrGetter(targetClass, sSetterPropertyMap, "set", mValueType); + Class<?> propertyType = mConverter == null ? mValueType : mConverter.getTargetType(); + mSetter = setupSetterOrGetter(targetClass, sSetterPropertyMap, "set", propertyType); } /** @@ -489,10 +773,13 @@ public class PropertyValuesHolder implements Cloneable { if (mProperty != null) { // check to make sure that mProperty is on the class of target try { - Object testValue = mProperty.get(target); + Object testValue = null; for (Keyframe kf : mKeyframeSet.mKeyframes) { if (!kf.hasValue()) { - kf.setValue(mProperty.get(target)); + if (testValue == null) { + testValue = convertBack(mProperty.get(target)); + } + kf.setValue(testValue); } } return; @@ -516,7 +803,8 @@ public class PropertyValuesHolder implements Cloneable { } } try { - kf.setValue(mGetter.invoke(target)); + Object value = convertBack(mGetter.invoke(target)); + kf.setValue(value); } catch (InvocationTargetException e) { Log.e("PropertyValuesHolder", e.toString()); } catch (IllegalAccessException e) { @@ -526,6 +814,18 @@ public class PropertyValuesHolder implements Cloneable { } } + private Object convertBack(Object value) { + if (mConverter != null) { + value = mConverter.convertBack(value); + if (value == null) { + throw new IllegalArgumentException("Converter " + + mConverter.getClass().getName() + + " must implement convertBack and not return null."); + } + } + return value; + } + /** * Utility function to set the value stored in a particular Keyframe. The value used is * whatever the value is for the property name specified in the keyframe on the target object. @@ -535,7 +835,8 @@ public class PropertyValuesHolder implements Cloneable { */ private void setupValue(Object target, Keyframe kf) { if (mProperty != null) { - kf.setValue(mProperty.get(target)); + Object value = convertBack(mProperty.get(target)); + kf.setValue(value); } try { if (mGetter == null) { @@ -546,7 +847,8 @@ public class PropertyValuesHolder implements Cloneable { return; } } - kf.setValue(mGetter.invoke(target)); + Object value = convertBack(mGetter.invoke(target)); + kf.setValue(value); } catch (InvocationTargetException e) { Log.e("PropertyValuesHolder", e.toString()); } catch (IllegalAccessException e) { @@ -657,7 +959,8 @@ public class PropertyValuesHolder implements Cloneable { * @param fraction The elapsed, interpolated fraction of the animation. */ void calculateValue(float fraction) { - mAnimatedValue = mKeyframeSet.getValue(fraction); + Object value = mKeyframeSet.getValue(fraction); + mAnimatedValue = mConverter == null ? value : mConverter.convert(value); } /** @@ -1015,8 +1318,334 @@ public class PropertyValuesHolder implements Cloneable { } + static class MultiFloatValuesHolder extends PropertyValuesHolder { + private int mJniSetter; + private static final HashMap<Class, HashMap<String, Integer>> sJNISetterPropertyMap = + new HashMap<Class, HashMap<String, Integer>>(); + + public MultiFloatValuesHolder(String propertyName, TypeConverter converter, + TypeEvaluator evaluator, Object... values) { + super(propertyName); + setConverter(converter); + setObjectValues(values); + setEvaluator(evaluator); + } + + public MultiFloatValuesHolder(String propertyName, TypeConverter converter, + TypeEvaluator evaluator, KeyframeSet keyframeSet) { + super(propertyName); + setConverter(converter); + mKeyframeSet = keyframeSet; + setEvaluator(evaluator); + } + + /** + * Internal function to set the value on the target object, using the setter set up + * earlier on this PropertyValuesHolder object. This function is called by ObjectAnimator + * to handle turning the value calculated by ValueAnimator into a value set on the object + * according to the name of the property. + * + * @param target The target object on which the value is set + */ + @Override + void setAnimatedValue(Object target) { + float[] values = (float[]) getAnimatedValue(); + int numParameters = values.length; + if (mJniSetter != 0) { + switch (numParameters) { + case 1: + nCallFloatMethod(target, mJniSetter, values[0]); + break; + case 2: + nCallTwoFloatMethod(target, mJniSetter, values[0], values[1]); + break; + case 4: + nCallFourFloatMethod(target, mJniSetter, values[0], values[1], + values[2], values[3]); + break; + default: { + nCallMultipleFloatMethod(target, mJniSetter, values); + break; + } + } + } + } + + /** + * Internal function (called from ObjectAnimator) to set up the setter and getter + * prior to running the animation. No getter can be used for multiple parameters. + * + * @param target The object on which the setter exists. + */ + @Override + void setupSetterAndGetter(Object target) { + setupSetter(target.getClass()); + } + + @Override + void setupSetter(Class targetClass) { + if (mJniSetter != 0) { + return; + } + try { + mPropertyMapLock.writeLock().lock(); + HashMap<String, Integer> propertyMap = sJNISetterPropertyMap.get(targetClass); + if (propertyMap != null) { + Integer jniSetterInteger = propertyMap.get(mPropertyName); + if (jniSetterInteger != null) { + mJniSetter = jniSetterInteger; + } + } + if (mJniSetter == 0) { + String methodName = getMethodName("set", mPropertyName); + calculateValue(0f); + float[] values = (float[]) getAnimatedValue(); + int numParams = values.length; + try { + mJniSetter = nGetMultipleFloatMethod(targetClass, methodName, numParams); + } catch (NoSuchMethodError e) { + // try without the 'set' prefix + mJniSetter = nGetMultipleFloatMethod(targetClass, mPropertyName, numParams); + } + if (mJniSetter != 0) { + if (propertyMap == null) { + propertyMap = new HashMap<String, Integer>(); + sJNISetterPropertyMap.put(targetClass, propertyMap); + } + propertyMap.put(mPropertyName, mJniSetter); + } + } + } finally { + mPropertyMapLock.writeLock().unlock(); + } + } + } + + static class MultiIntValuesHolder extends PropertyValuesHolder { + private int mJniSetter; + private static final HashMap<Class, HashMap<String, Integer>> sJNISetterPropertyMap = + new HashMap<Class, HashMap<String, Integer>>(); + + public MultiIntValuesHolder(String propertyName, TypeConverter converter, + TypeEvaluator evaluator, Object... values) { + super(propertyName); + setConverter(converter); + setObjectValues(values); + setEvaluator(evaluator); + } + + public MultiIntValuesHolder(String propertyName, TypeConverter converter, + TypeEvaluator evaluator, KeyframeSet keyframeSet) { + super(propertyName); + setConverter(converter); + mKeyframeSet = keyframeSet; + setEvaluator(evaluator); + } + + /** + * Internal function to set the value on the target object, using the setter set up + * earlier on this PropertyValuesHolder object. This function is called by ObjectAnimator + * to handle turning the value calculated by ValueAnimator into a value set on the object + * according to the name of the property. + * + * @param target The target object on which the value is set + */ + @Override + void setAnimatedValue(Object target) { + int[] values = (int[]) getAnimatedValue(); + int numParameters = values.length; + if (mJniSetter != 0) { + switch (numParameters) { + case 1: + nCallIntMethod(target, mJniSetter, values[0]); + break; + case 2: + nCallTwoIntMethod(target, mJniSetter, values[0], values[1]); + break; + case 4: + nCallFourIntMethod(target, mJniSetter, values[0], values[1], + values[2], values[3]); + break; + default: { + nCallMultipleIntMethod(target, mJniSetter, values); + break; + } + } + } + } + + /** + * Internal function (called from ObjectAnimator) to set up the setter and getter + * prior to running the animation. No getter can be used for multiple parameters. + * + * @param target The object on which the setter exists. + */ + @Override + void setupSetterAndGetter(Object target) { + setupSetter(target.getClass()); + } + + @Override + void setupSetter(Class targetClass) { + if (mJniSetter != 0) { + return; + } + try { + mPropertyMapLock.writeLock().lock(); + HashMap<String, Integer> propertyMap = sJNISetterPropertyMap.get(targetClass); + if (propertyMap != null) { + Integer jniSetterInteger = propertyMap.get(mPropertyName); + if (jniSetterInteger != null) { + mJniSetter = jniSetterInteger; + } + } + if (mJniSetter == 0) { + String methodName = getMethodName("set", mPropertyName); + calculateValue(0f); + int[] values = (int[]) getAnimatedValue(); + int numParams = values.length; + try { + mJniSetter = nGetMultipleIntMethod(targetClass, methodName, numParams); + } catch (NoSuchMethodError e) { + // try without the 'set' prefix + mJniSetter = nGetMultipleIntMethod(targetClass, mPropertyName, numParams); + } + if (mJniSetter != 0) { + if (propertyMap == null) { + propertyMap = new HashMap<String, Integer>(); + sJNISetterPropertyMap.put(targetClass, propertyMap); + } + propertyMap.put(mPropertyName, mJniSetter); + } + } + } finally { + mPropertyMapLock.writeLock().unlock(); + } + } + } + + /* Path interpolation relies on approximating the Path as a series of line segments. + The line segments are recursively divided until there is less than 1/2 pixel error + between the lines and the curve. Each point of the line segment is converted + to a Keyframe and a linear interpolation between Keyframes creates a good approximation + of the curve. + + The fraction for each Keyframe is the length along the Path to the point, divided by + the total Path length. Two points may have the same fraction in the case of a move + command causing a disjoint Path. + + The value for each Keyframe is either the point as a PointF or one of the x or y + coordinates as an int or float. In the latter case, two Keyframes are generated for + each point that have the same fraction. */ + + /** + * Returns separate Keyframes arrays for the x and y coordinates along a Path. If + * isInt is true, the Keyframes will be IntKeyframes, otherwise they will be FloatKeyframes. + * The element at index 0 are the x coordinate Keyframes and element at index 1 are the + * y coordinate Keyframes. The returned values can be linearly interpolated and get less + * than 1/2 pixel error. + */ + static Keyframe[][] createKeyframes(Path path, boolean isInt) { + if (path == null || path.isEmpty()) { + throw new IllegalArgumentException("The path must not be null or empty"); + } + float[] pointComponents = path.approximate(0.5f); + + int numPoints = pointComponents.length / 3; + + Keyframe[][] keyframes = new Keyframe[2][]; + keyframes[0] = new Keyframe[numPoints]; + keyframes[1] = new Keyframe[numPoints]; + int componentIndex = 0; + for (int i = 0; i < numPoints; i++) { + float fraction = pointComponents[componentIndex++]; + float x = pointComponents[componentIndex++]; + float y = pointComponents[componentIndex++]; + if (isInt) { + keyframes[0][i] = Keyframe.ofInt(fraction, Math.round(x)); + keyframes[1][i] = Keyframe.ofInt(fraction, Math.round(y)); + } else { + keyframes[0][i] = Keyframe.ofFloat(fraction, x); + keyframes[1][i] = Keyframe.ofFloat(fraction, y); + } + } + return keyframes; + } + + /** + * Returns PointF Keyframes for a Path. The resulting points can be linearly interpolated + * with less than 1/2 pixel in error. + */ + private static Keyframe[] createKeyframes(Path path) { + if (path == null || path.isEmpty()) { + throw new IllegalArgumentException("The path must not be null or empty"); + } + float[] pointComponents = path.approximate(0.5f); + + int numPoints = pointComponents.length / 3; + + Keyframe[] keyframes = new Keyframe[numPoints]; + int componentIndex = 0; + for (int i = 0; i < numPoints; i++) { + float fraction = pointComponents[componentIndex++]; + float x = pointComponents[componentIndex++]; + float y = pointComponents[componentIndex++]; + keyframes[i] = Keyframe.ofObject(fraction, new PointF(x, y)); + } + return keyframes; + } + + /** + * Convert from PointF to float[] for multi-float setters along a Path. + */ + private static class PointFToFloatArray extends TypeConverter<PointF, float[]> { + private float[] mCoordinates = new float[2]; + + public PointFToFloatArray() { + super(PointF.class, float[].class); + } + + @Override + public float[] convert(PointF value) { + mCoordinates[0] = value.x; + mCoordinates[1] = value.y; + return mCoordinates; + } + }; + + /** + * Convert from PointF to int[] for multi-int setters along a Path. + */ + private static class PointFToIntArray extends TypeConverter<PointF, int[]> { + private int[] mCoordinates = new int[2]; + + public PointFToIntArray() { + super(PointF.class, int[].class); + } + + @Override + public int[] convert(PointF value) { + mCoordinates[0] = Math.round(value.x); + mCoordinates[1] = Math.round(value.y); + return mCoordinates; + } + }; + native static private int nGetIntMethod(Class targetClass, String methodName); native static private int nGetFloatMethod(Class targetClass, String methodName); + native static private int nGetMultipleIntMethod(Class targetClass, String methodName, + int numParams); + native static private int nGetMultipleFloatMethod(Class targetClass, String methodName, + int numParams); native static private void nCallIntMethod(Object target, int methodID, int arg); native static private void nCallFloatMethod(Object target, int methodID, float arg); -}
\ No newline at end of file + native static private void nCallTwoIntMethod(Object target, int methodID, int arg1, int arg2); + native static private void nCallFourIntMethod(Object target, int methodID, int arg1, int arg2, + int arg3, int arg4); + native static private void nCallMultipleIntMethod(Object target, int methodID, int[] args); + native static private void nCallTwoFloatMethod(Object target, int methodID, float arg1, + float arg2); + native static private void nCallFourFloatMethod(Object target, int methodID, float arg1, + float arg2, float arg3, float arg4); + native static private void nCallMultipleFloatMethod(Object target, int methodID, float[] args); +} diff --git a/core/java/android/animation/RectEvaluator.java b/core/java/android/animation/RectEvaluator.java index 28d496b..23eb766 100644 --- a/core/java/android/animation/RectEvaluator.java +++ b/core/java/android/animation/RectEvaluator.java @@ -23,12 +23,45 @@ import android.graphics.Rect; public class RectEvaluator implements TypeEvaluator<Rect> { /** + * When null, a new Rect is returned on every evaluate call. When non-null, + * mRect will be modified and returned on every evaluate. + */ + private Rect mRect; + + /** + * Construct a RectEvaluator that returns a new Rect on every evaluate call. + * To avoid creating an object for each evaluate call, + * {@link RectEvaluator#RectEvaluator(android.graphics.Rect)} should be used + * whenever possible. + */ + public RectEvaluator() { + } + + /** + * Constructs a RectEvaluator that modifies and returns <code>reuseRect</code> + * in {@link #evaluate(float, android.graphics.Rect, android.graphics.Rect)} calls. + * The value returned from + * {@link #evaluate(float, android.graphics.Rect, android.graphics.Rect)} should + * not be cached because it will change over time as the object is reused on each + * call. + * + * @param reuseRect A Rect to be modified and returned by evaluate. + */ + public RectEvaluator(Rect reuseRect) { + mRect = reuseRect; + } + + /** * This function returns the result of linearly interpolating the start and * end Rect values, with <code>fraction</code> representing the proportion * between the start and end values. The calculation is a simple parametric * calculation on each of the separate components in the Rect objects * (left, top, right, and bottom). * + * <p>If {@link #RectEvaluator(android.graphics.Rect)} was used to construct + * this RectEvaluator, the object returned will be the <code>reuseRect</code> + * passed into the constructor.</p> + * * @param fraction The fraction from the starting to the ending values * @param startValue The start Rect * @param endValue The end Rect @@ -37,9 +70,15 @@ public class RectEvaluator implements TypeEvaluator<Rect> { */ @Override public Rect evaluate(float fraction, Rect startValue, Rect endValue) { - return new Rect(startValue.left + (int)((endValue.left - startValue.left) * fraction), - startValue.top + (int)((endValue.top - startValue.top) * fraction), - startValue.right + (int)((endValue.right - startValue.right) * fraction), - startValue.bottom + (int)((endValue.bottom - startValue.bottom) * fraction)); + int left = startValue.left + (int) ((endValue.left - startValue.left) * fraction); + int top = startValue.top + (int) ((endValue.top - startValue.top) * fraction); + int right = startValue.right + (int) ((endValue.right - startValue.right) * fraction); + int bottom = startValue.bottom + (int) ((endValue.bottom - startValue.bottom) * fraction); + if (mRect == null) { + return new Rect(left, top, right, bottom); + } else { + mRect.set(left, top, right, bottom); + return mRect; + } } } diff --git a/core/java/android/animation/TypeConverter.java b/core/java/android/animation/TypeConverter.java new file mode 100644 index 0000000..03b3eb5 --- /dev/null +++ b/core/java/android/animation/TypeConverter.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2013 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.animation; + +/** + * Abstract base class used convert type T to another type V. This + * is necessary when the value types of in animation are different + * from the property type. + * @see PropertyValuesHolder#setConverter(TypeConverter) + */ +public abstract class TypeConverter<T, V> { + private Class<T> mFromClass; + private Class<V> mToClass; + + public TypeConverter(Class<T> fromClass, Class<V> toClass) { + mFromClass = fromClass; + mToClass = toClass; + } + + /** + * Returns the target converted type. Used by the animation system to determine + * the proper setter function to call. + * @return The Class to convert the input to. + */ + Class<V> getTargetType() { + return mToClass; + } + + /** + * Returns the source conversion type. + */ + Class<T> getSourceType() { + return mFromClass; + } + + /** + * Converts a value from one type to another. + * @param value The Object to convert. + * @return A value of type V, converted from <code>value</code>. + */ + public abstract V convert(T value); + + /** + * Does a conversion from the target type back to the source type. The subclass + * must implement this when a TypeConverter is used in animations and current + * values will need to be read for an animation. By default, this will return null, + * indicating that back-conversion is not supported. + * @param value The Object to convert. + * @return A value of type T, converted from <code>value</code>. + */ + public T convertBack(V value) { + return null; + } +} diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java index 86da673..7880f39 100644 --- a/core/java/android/animation/ValueAnimator.java +++ b/core/java/android/animation/ValueAnimator.java @@ -280,6 +280,24 @@ public class ValueAnimator extends Animator { } /** + * Constructs and returns a ValueAnimator that animates between color values. A single + * value implies that that value is the one being animated to. However, this is not typically + * useful in a ValueAnimator object because there is no way for the object to determine the + * starting value for the animation (unlike ObjectAnimator, which can derive that value + * from the target object and property being animated). Therefore, there should typically + * be two or more values. + * + * @param values A set of values that the animation will animate between over time. + * @return A ValueAnimator object that is set up to animate between the given values. + */ + public static ValueAnimator ofArgb(int... values) { + ValueAnimator anim = new ValueAnimator(); + anim.setIntValues(values); + anim.setEvaluator(ArgbEvaluator.getInstance()); + return anim; + } + + /** * Constructs and returns a ValueAnimator that animates between float values. A single * value implies that that value is the one being animated to. However, this is not typically * useful in a ValueAnimator object because there is no way for the object to determine the diff --git a/core/java/android/annotation/IntDef.java b/core/java/android/annotation/IntDef.java new file mode 100644 index 0000000..3cae9c5 --- /dev/null +++ b/core/java/android/annotation/IntDef.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2013 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.ANNOTATION_TYPE; +import static java.lang.annotation.RetentionPolicy.CLASS; + +/** + * Denotes that the annotated element of integer type, represents + * a logical type and that its value should be one of the explicitly + * named constants. If the {@link #flag()} attribute is set to true, + * multiple constants can be combined. + * <p> + * Example: + * <pre>{@code + * @Retention(CLASS) + * @IntDef({NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS}) + * public @interface NavigationMode {} + * public static final int NAVIGATION_MODE_STANDARD = 0; + * public static final int NAVIGATION_MODE_LIST = 1; + * public static final int NAVIGATION_MODE_TABS = 2; + * ... + * public abstract void setNavigationMode(@NavigationMode int mode); + * @NavigationMode + * public abstract int getNavigationMode(); + * }</pre> + * For a flag, set the flag attribute: + * <pre>{@code + * @IntDef( + * flag = true + * value = {NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS}) + * }</pre> + * + * @hide + */ +@Retention(CLASS) +@Target({ANNOTATION_TYPE}) +public @interface IntDef { + /** Defines the allowed constants for this element */ + long[] value() default {}; + + /** Defines whether the constants can be used as a flag, or just as an enum (the default) */ + boolean flag() default false; +} diff --git a/core/java/android/annotation/NonNull.java b/core/java/android/annotation/NonNull.java new file mode 100644 index 0000000..8e604f0 --- /dev/null +++ b/core/java/android/annotation/NonNull.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2013 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.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that a parameter, field or method return value can never be null. + * <p> + * This is a marker annotation and it has no specific attributes. + */ +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +public @interface NonNull { +} diff --git a/core/java/android/annotation/Nullable.java b/core/java/android/annotation/Nullable.java new file mode 100644 index 0000000..cdba2f6 --- /dev/null +++ b/core/java/android/annotation/Nullable.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2013 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.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that a parameter, field or method return value can be null. + * <p> + * When decorating a method call parameter, this denotes that the parameter can + * legitimately be null and the method will gracefully deal with it. Typically + * used on optional parameters. + * <p> + * When decorating a method, this denotes the method might legitimately return + * null. + * <p> + * This is a marker annotation and it has no specific attributes. + */ +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +public @interface Nullable { +} diff --git a/core/java/android/annotation/StringDef.java b/core/java/android/annotation/StringDef.java new file mode 100644 index 0000000..5f7f380 --- /dev/null +++ b/core/java/android/annotation/StringDef.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2013 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.ANNOTATION_TYPE; +import static java.lang.annotation.RetentionPolicy.CLASS; + +/** + * Denotes that the annotated String element, represents a logical + * type and that its value should be one of the explicitly named constants. + * <p> + * Example: + * <pre>{@code + * @Retention(SOURCE) + * @StringDef({ + * POWER_SERVICE, + * WINDOW_SERVICE, + * LAYOUT_INFLATER_SERVICE + * }) + * public @interface ServiceName {} + * public static final String POWER_SERVICE = "power"; + * public static final String WINDOW_SERVICE = "window"; + * public static final String LAYOUT_INFLATER_SERVICE = "layout_inflater"; + * ... + * public abstract Object getSystemService(@ServiceName String name); + * }</pre> + * + * @hide + */ +@Retention(CLASS) +@Target({ANNOTATION_TYPE}) +public @interface StringDef { + /** Defines the allowed constants for this element */ + String[] value() default {}; +} diff --git a/core/java/android/app/ActionBar.java b/core/java/android/app/ActionBar.java index c4ddf1f..fbe8987 100644 --- a/core/java/android/app/ActionBar.java +++ b/core/java/android/app/ActionBar.java @@ -16,6 +16,9 @@ package android.app; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; @@ -28,6 +31,9 @@ import android.view.ViewGroup.MarginLayoutParams; import android.view.Window; import android.widget.SpinnerAdapter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * A window feature at the top of the activity that may display the activity title, navigation * modes, and other interactive items. @@ -57,6 +63,11 @@ import android.widget.SpinnerAdapter; * </div> */ public abstract class ActionBar { + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS}) + public @interface NavigationMode {} + /** * Standard navigation mode. Consists of either a logo or icon * and title text with an optional subtitle. Clicking any of these elements @@ -78,6 +89,19 @@ public abstract class ActionBar { */ public static final int NAVIGATION_MODE_TABS = 2; + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, + value = { + DISPLAY_USE_LOGO, + DISPLAY_SHOW_HOME, + DISPLAY_HOME_AS_UP, + DISPLAY_SHOW_TITLE, + DISPLAY_SHOW_CUSTOM, + DISPLAY_TITLE_MULTIPLE_LINES + }) + public @interface DisplayOptions {} + /** * Use logo instead of icon if available. This flag will cause appropriate * navigation modes to use a wider logo in place of the standard icon. @@ -341,7 +365,7 @@ public abstract class ActionBar { * @param options A combination of the bits defined by the DISPLAY_ constants * defined in ActionBar. */ - public abstract void setDisplayOptions(int options); + public abstract void setDisplayOptions(@DisplayOptions int options); /** * Set selected display options. Only the options specified by mask will be changed. @@ -356,7 +380,7 @@ public abstract class ActionBar { * defined in ActionBar. * @param mask A bit mask declaring which display options should be changed. */ - public abstract void setDisplayOptions(int options, int mask); + public abstract void setDisplayOptions(@DisplayOptions int options, @DisplayOptions int mask); /** * Set whether to display the activity logo rather than the activity icon. @@ -431,7 +455,7 @@ public abstract class ActionBar { * @see #setStackedBackgroundDrawable(Drawable) * @see #setSplitBackgroundDrawable(Drawable) */ - public abstract void setBackgroundDrawable(Drawable d); + public abstract void setBackgroundDrawable(@Nullable Drawable d); /** * Set the ActionBar's stacked background. This will appear @@ -484,6 +508,7 @@ public abstract class ActionBar { * * @return The current navigation mode. */ + @NavigationMode public abstract int getNavigationMode(); /** @@ -494,7 +519,7 @@ public abstract class ActionBar { * @see #NAVIGATION_MODE_LIST * @see #NAVIGATION_MODE_TABS */ - public abstract void setNavigationMode(int mode); + public abstract void setNavigationMode(@NavigationMode int mode); /** * @return The current set of display options. @@ -1024,7 +1049,7 @@ public abstract class ActionBar { }) public int gravity = Gravity.NO_GRAVITY; - public LayoutParams(Context c, AttributeSet attrs) { + public LayoutParams(@NonNull Context c, AttributeSet attrs) { super(c, attrs); TypedArray a = c.obtainStyledAttributes(attrs, diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index d6db8c2..d34b05d 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -16,11 +16,14 @@ package android.app; +import android.annotation.NonNull; import android.util.ArrayMap; import android.util.SuperNotCalledException; import com.android.internal.app.ActionBarImpl; import com.android.internal.policy.PolicyManager; +import android.annotation.IntDef; +import android.annotation.Nullable; import android.content.ComponentCallbacks2; import android.content.ComponentName; import android.content.ContentResolver; @@ -84,6 +87,8 @@ import android.widget.AdapterView; 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.HashMap; @@ -852,6 +857,7 @@ public class Activity extends ContextThemeWrapper * @see #getWindow * @see android.view.Window#getCurrentFocus */ + @Nullable public View getCurrentFocus() { return mWindow != null ? mWindow.getCurrentFocus() : null; } @@ -882,7 +888,7 @@ public class Activity extends ContextThemeWrapper * @see #onRestoreInstanceState * @see #onPostCreate */ - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(@Nullable Bundle savedInstanceState) { if (DEBUG_LIFECYCLE) Slog.v(TAG, "onCreate " + this + ": " + savedInstanceState); if (mLastNonConfigurationInstances != null) { mAllLoaderManagers = mLastNonConfigurationInstances.loaders; @@ -1010,7 +1016,7 @@ public class Activity extends ContextThemeWrapper * recently supplied in {@link #onSaveInstanceState}. <b><i>Note: Otherwise it is null.</i></b> * @see #onCreate */ - protected void onPostCreate(Bundle savedInstanceState) { + protected void onPostCreate(@Nullable Bundle savedInstanceState) { if (!isChild()) { mTitleReady = true; onTitleChanged(getTitle(), getTitleColor()); @@ -1347,6 +1353,7 @@ public class Activity extends ContextThemeWrapper * @see #onSaveInstanceState * @see #onPause */ + @Nullable public CharSequence onCreateDescription() { return null; } @@ -1551,6 +1558,7 @@ public class Activity extends ContextThemeWrapper * {@link Fragment#setRetainInstance(boolean)} instead; this is also * available on older platforms through the Android compatibility package. */ + @Nullable @Deprecated public Object getLastNonConfigurationInstance() { return mLastNonConfigurationInstances != null @@ -1630,6 +1638,7 @@ public class Activity extends ContextThemeWrapper * @return Returns the object previously returned by * {@link #onRetainNonConfigurationChildInstances()} */ + @Nullable HashMap<String, Object> getLastNonConfigurationChildInstances() { return mLastNonConfigurationInstances != null ? mLastNonConfigurationInstances.children : null; @@ -1642,6 +1651,7 @@ public class Activity extends ContextThemeWrapper * set of child activities, such as ActivityGroup. The same guarantees and restrictions apply * as for {@link #onRetainNonConfigurationInstance()}. The default implementation returns null. */ + @Nullable HashMap<String,Object> onRetainNonConfigurationChildInstances() { return null; } @@ -1889,6 +1899,7 @@ public class Activity extends ContextThemeWrapper * * @return The Activity's ActionBar, or null if it does not have one. */ + @Nullable public ActionBar getActionBar() { initActionBar(); return mActionBar; @@ -1985,7 +1996,17 @@ public class Activity extends ContextThemeWrapper public void setFinishOnTouchOutside(boolean finish) { mWindow.setCloseOnTouchOutside(finish); } - + + /** @hide */ + @IntDef({ + DEFAULT_KEYS_DISABLE, + DEFAULT_KEYS_DIALER, + DEFAULT_KEYS_SHORTCUT, + DEFAULT_KEYS_SEARCH_LOCAL, + DEFAULT_KEYS_SEARCH_GLOBAL}) + @Retention(RetentionPolicy.SOURCE) + @interface DefaultKeyMode {} + /** * Use with {@link #setDefaultKeyMode} to turn off default handling of * keys. @@ -2055,7 +2076,7 @@ public class Activity extends ContextThemeWrapper * @see #DEFAULT_KEYS_SEARCH_GLOBAL * @see #onKeyDown */ - public final void setDefaultKeyMode(int mode) { + public final void setDefaultKeyMode(@DefaultKeyMode int mode) { mDefaultKeyMode = mode; // Some modes use a SpannableStringBuilder to track & dispatch input events @@ -2521,6 +2542,7 @@ public class Activity extends ContextThemeWrapper * simply returns null so that all panel sub-windows will have the default * menu behavior. */ + @Nullable public View onCreatePanelView(int featureId) { return null; } @@ -3018,6 +3040,7 @@ public class Activity extends ContextThemeWrapper * {@link FragmentManager} instead; this is also * available on older platforms through the Android compatibility package. */ + @Nullable @Deprecated protected Dialog onCreateDialog(int id, Bundle args) { return onCreateDialog(id); @@ -3105,6 +3128,7 @@ public class Activity extends ContextThemeWrapper * {@link FragmentManager} instead; this is also * available on older platforms through the Android compatibility package. */ + @Nullable @Deprecated public final boolean showDialog(int id, Bundle args) { if (mManagedDialogs == null) { @@ -3226,13 +3250,13 @@ public class Activity extends ContextThemeWrapper * <p>It is typically called from onSearchRequested(), either directly from * Activity.onSearchRequested() or from an overridden version in any given * Activity. If your goal is simply to activate search, it is preferred to call - * onSearchRequested(), which may have been overriden elsewhere in your Activity. If your goal + * onSearchRequested(), which may have been overridden elsewhere in your Activity. If your goal * is to inject specific data such as context data, it is preferred to <i>override</i> * onSearchRequested(), so that any callers to it will benefit from the override. * * @param initialQuery Any non-null non-empty string will be inserted as * pre-entered text in the search query box. - * @param selectInitialQuery If true, the intial query will be preselected, which means that + * @param selectInitialQuery If true, the initial query will be preselected, which means that * any further typing will replace it. This is useful for cases where an entire pre-formed * query is being inserted. If false, the selection point will be placed at the end of the * inserted query. This is useful when the inserted query is text that the user entered, @@ -3250,8 +3274,8 @@ public class Activity extends ContextThemeWrapper * @see android.app.SearchManager * @see #onSearchRequested */ - public void startSearch(String initialQuery, boolean selectInitialQuery, - Bundle appSearchData, boolean globalSearch) { + public void startSearch(@Nullable String initialQuery, boolean selectInitialQuery, + @Nullable Bundle appSearchData, boolean globalSearch) { ensureSearchManager(); mSearchManager.startSearch(initialQuery, selectInitialQuery, getComponentName(), appSearchData, globalSearch); @@ -3267,7 +3291,7 @@ public class Activity extends ContextThemeWrapper * searches. This data will be returned with SEARCH intent(s). Null if * no extra data is required. */ - public void triggerSearch(String query, Bundle appSearchData) { + public void triggerSearch(String query, @Nullable Bundle appSearchData) { ensureSearchManager(); mSearchManager.triggerSearch(query, getComponentName(), appSearchData); } @@ -3334,6 +3358,7 @@ public class Activity extends ContextThemeWrapper * Convenience for calling * {@link android.view.Window#getLayoutInflater}. */ + @NonNull public LayoutInflater getLayoutInflater() { return getWindow().getLayoutInflater(); } @@ -3341,6 +3366,7 @@ public class Activity extends ContextThemeWrapper /** * Returns a {@link MenuInflater} with this context. */ + @NonNull public MenuInflater getMenuInflater() { // Make sure that action views can get an appropriate theme. if (mMenuInflater == null) { @@ -3419,7 +3445,7 @@ public class Activity extends ContextThemeWrapper * * @see #startActivity */ - public void startActivityForResult(Intent intent, int requestCode, Bundle options) { + public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options) { if (mParent == null) { Instrumentation.ActivityResult ar = mInstrumentation.execStartActivity( @@ -3498,7 +3524,7 @@ public class Activity extends ContextThemeWrapper * @param extraFlags Always set to 0. */ public void startIntentSenderForResult(IntentSender intent, int requestCode, - Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags) + @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags) throws IntentSender.SendIntentException { startIntentSenderForResult(intent, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, null); @@ -3530,7 +3556,7 @@ public class Activity extends ContextThemeWrapper * override any that conflict with those given by the IntentSender. */ public void startIntentSenderForResult(IntentSender intent, int requestCode, - Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, + @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, Bundle options) throws IntentSender.SendIntentException { if (mParent == null) { startIntentSenderForResultInner(intent, requestCode, fillInIntent, @@ -3618,7 +3644,7 @@ public class Activity extends ContextThemeWrapper * @see #startActivityForResult */ @Override - public void startActivity(Intent intent, Bundle options) { + public void startActivity(Intent intent, @Nullable Bundle options) { if (options != null) { startActivityForResult(intent, -1, options); } else { @@ -3667,7 +3693,7 @@ public class Activity extends ContextThemeWrapper * @see #startActivityForResult */ @Override - public void startActivities(Intent[] intents, Bundle options) { + public void startActivities(Intent[] intents, @Nullable Bundle options) { mInstrumentation.execStartActivities(this, mMainThread.getApplicationThread(), mToken, this, intents, options); } @@ -3686,7 +3712,7 @@ public class Activity extends ContextThemeWrapper * @param extraFlags Always set to 0. */ public void startIntentSender(IntentSender intent, - Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags) + @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags) throws IntentSender.SendIntentException { startIntentSender(intent, fillInIntent, flagsMask, flagsValues, extraFlags, null); @@ -3713,7 +3739,7 @@ public class Activity extends ContextThemeWrapper * override any that conflict with those given by the IntentSender. */ public void startIntentSender(IntentSender intent, - Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, + @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, Bundle options) throws IntentSender.SendIntentException { if (options != null) { startIntentSenderForResult(intent, -1, fillInIntent, flagsMask, @@ -3741,7 +3767,7 @@ public class Activity extends ContextThemeWrapper * @see #startActivity * @see #startActivityForResult */ - public boolean startActivityIfNeeded(Intent intent, int requestCode) { + public boolean startActivityIfNeeded(@NonNull Intent intent, int requestCode) { return startActivityIfNeeded(intent, requestCode, null); } @@ -3775,7 +3801,8 @@ public class Activity extends ContextThemeWrapper * @see #startActivity * @see #startActivityForResult */ - public boolean startActivityIfNeeded(Intent intent, int requestCode, Bundle options) { + public boolean startActivityIfNeeded(@NonNull Intent intent, int requestCode, + @Nullable Bundle options) { if (mParent == null) { int result = ActivityManager.START_RETURN_INTENT_TO_CALLER; try { @@ -3824,7 +3851,7 @@ public class Activity extends ContextThemeWrapper * wasn't. In general, if true is returned you will then want to call * finish() on yourself. */ - public boolean startNextMatchingActivity(Intent intent) { + public boolean startNextMatchingActivity(@NonNull Intent intent) { return startNextMatchingActivity(intent, null); } @@ -3847,7 +3874,7 @@ public class Activity extends ContextThemeWrapper * wasn't. In general, if true is returned you will then want to call * finish() on yourself. */ - public boolean startNextMatchingActivity(Intent intent, Bundle options) { + public boolean startNextMatchingActivity(@NonNull Intent intent, @Nullable Bundle options) { if (mParent == null) { try { intent.migrateExtraStreamToClipData(); @@ -3877,7 +3904,7 @@ public class Activity extends ContextThemeWrapper * @see #startActivity * @see #startActivityForResult */ - public void startActivityFromChild(Activity child, Intent intent, + public void startActivityFromChild(@NonNull Activity child, Intent intent, int requestCode) { startActivityFromChild(child, intent, requestCode, null); } @@ -3901,8 +3928,8 @@ public class Activity extends ContextThemeWrapper * @see #startActivity * @see #startActivityForResult */ - public void startActivityFromChild(Activity child, Intent intent, - int requestCode, Bundle options) { + public void startActivityFromChild(@NonNull Activity child, Intent intent, + int requestCode, @Nullable Bundle options) { Instrumentation.ActivityResult ar = mInstrumentation.execStartActivity( this, mMainThread.getApplicationThread(), mToken, child, @@ -3927,7 +3954,7 @@ public class Activity extends ContextThemeWrapper * @see Fragment#startActivity * @see Fragment#startActivityForResult */ - public void startActivityFromFragment(Fragment fragment, Intent intent, + public void startActivityFromFragment(@NonNull Fragment fragment, Intent intent, int requestCode) { startActivityFromFragment(fragment, intent, requestCode, null); } @@ -3952,8 +3979,8 @@ public class Activity extends ContextThemeWrapper * @see Fragment#startActivity * @see Fragment#startActivityForResult */ - public void startActivityFromFragment(Fragment fragment, Intent intent, - int requestCode, Bundle options) { + public void startActivityFromFragment(@NonNull Fragment fragment, Intent intent, + int requestCode, @Nullable Bundle options) { Instrumentation.ActivityResult ar = mInstrumentation.execStartActivity( this, mMainThread.getApplicationThread(), mToken, fragment, @@ -3985,7 +4012,7 @@ public class Activity extends ContextThemeWrapper */ public void startIntentSenderFromChild(Activity child, IntentSender intent, int requestCode, Intent fillInIntent, int flagsMask, int flagsValues, - int extraFlags, Bundle options) + int extraFlags, @Nullable Bundle options) throws IntentSender.SendIntentException { startIntentSenderForResultInner(intent, requestCode, fillInIntent, flagsMask, flagsValues, child, options); @@ -4084,6 +4111,7 @@ public class Activity extends ContextThemeWrapper * @return The package of the activity that will receive your * reply, or null if none. */ + @Nullable public String getCallingPackage() { try { return ActivityManagerNative.getDefault().getCallingPackage(mToken); @@ -4106,6 +4134,7 @@ public class Activity extends ContextThemeWrapper * @return The ComponentName of the activity that will receive your * reply, or null if none. */ + @Nullable public ComponentName getCallingActivity() { try { return ActivityManagerNative.getDefault().getCallingActivity(mToken); @@ -4298,7 +4327,7 @@ public class Activity extends ContextThemeWrapper * @param requestCode Request code that had been used to start the * activity. */ - public void finishActivityFromChild(Activity child, int requestCode) { + public void finishActivityFromChild(@NonNull Activity child, int requestCode) { try { ActivityManagerNative.getDefault() .finishSubActivity(mToken, child.mEmbeddedID, requestCode); @@ -4359,8 +4388,8 @@ public class Activity extends ContextThemeWrapper * * @see PendingIntent */ - public PendingIntent createPendingResult(int requestCode, Intent data, - int flags) { + public PendingIntent createPendingResult(int requestCode, @NonNull Intent data, + @PendingIntent.Flags int flags) { String packageName = getPackageName(); try { data.prepareToLeaveProcess(); @@ -4387,7 +4416,7 @@ public class Activity extends ContextThemeWrapper * @param requestedOrientation An orientation constant as used in * {@link ActivityInfo#screenOrientation ActivityInfo.screenOrientation}. */ - public void setRequestedOrientation(int requestedOrientation) { + public void setRequestedOrientation(@ActivityInfo.ScreenOrientation int requestedOrientation) { if (mParent == null) { try { ActivityManagerNative.getDefault().setRequestedOrientation( @@ -4409,6 +4438,7 @@ public class Activity extends ContextThemeWrapper * @return Returns an orientation constant as used in * {@link ActivityInfo#screenOrientation ActivityInfo.screenOrientation}. */ + @ActivityInfo.ScreenOrientation public int getRequestedOrientation() { if (mParent == null) { try { @@ -4480,6 +4510,7 @@ public class Activity extends ContextThemeWrapper * * @return The local class name. */ + @NonNull public String getLocalClassName() { final String pkg = getPackageName(); final String cls = mComponent.getClassName(); @@ -4525,9 +4556,9 @@ public class Activity extends ContextThemeWrapper mSearchManager = new SearchManager(this, null); } - + @Override - public Object getSystemService(String name) { + public Object getSystemService(@ServiceName @NonNull String name) { if (getBaseContext() == null) { throw new IllegalStateException( "System services not available to Activities before onCreate()"); @@ -4567,6 +4598,17 @@ public class Activity extends ContextThemeWrapper setTitle(getText(titleId)); } + /** + * Change the color of the title associated with this activity. + * <p> + * This method is deprecated starting in API Level 11 and replaced by action + * bar styles. For information on styling the Action Bar, read the <a + * href="{@docRoot} guide/topics/ui/actionbar.html">Action Bar</a> developer + * guide. + * + * @deprecated Use action bar styles instead. + */ + @Deprecated public void setTitleColor(int textColor) { mTitleColor = textColor; onTitleChanged(mTitle, textColor); @@ -4689,7 +4731,7 @@ public class Activity extends ContextThemeWrapper /** * Gets the suggested audio stream whose volume should be changed by the - * harwdare volume controls. + * hardware volume controls. * * @return The suggested audio stream type whose volume should be changed by * the hardware volume controls. @@ -4725,6 +4767,7 @@ public class Activity extends ContextThemeWrapper * @see android.view.LayoutInflater#createView * @see android.view.Window#getLayoutInflater */ + @Nullable public View onCreateView(String name, Context context, AttributeSet attrs) { return null; } @@ -4983,6 +5026,7 @@ public class Activity extends ContextThemeWrapper * * @see ActionMode */ + @Nullable public ActionMode startActionMode(ActionMode.Callback callback) { return mWindow.getDecorView().startActionMode(callback); } @@ -4998,6 +5042,7 @@ public class Activity extends ContextThemeWrapper * @return The new action mode, or <code>null</code> if the activity does not want to * provide special handling for this action mode. (It will be handled by the system.) */ + @Nullable @Override public ActionMode onWindowStartingActionMode(ActionMode.Callback callback) { initActionBar(); @@ -5141,6 +5186,7 @@ public class Activity extends ContextThemeWrapper * @return a new Intent targeting the defined parent of this activity or null if * there is no valid parent. */ + @Nullable public Intent getParentActivityIntent() { final String parentName = mActivityInfo.parentActivityName; if (TextUtils.isEmpty(parentName)) { diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index 87b1e24..44f6859 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -90,6 +90,35 @@ public class ActivityOptions { */ public static final String KEY_ANIM_START_LISTENER = "android:animStartListener"; + /** + * A string array of names for the destination scene. This defines an API in the same + * way that intent action or extra names do and should follow a similar convention: + * "com.example.scene.FOO" + * + * @hide + */ + public static final String KEY_DEST_SCENE_NAMES = "android:destSceneNames"; + + /** + * A string indicating the destination scene name that was chosen by the target. + * Used by {@link OnSceneTransitionStartedListener}. + * @hide + */ + public static final String KEY_DEST_SCENE_NAME_CHOSEN = "android:destSceneNameChosen"; + + /** + * Callback for when scene transition is started. + * @hide + */ + public static final String KEY_SCENE_TRANSITION_START_LISTENER = + "android:sceneTransitionStartListener"; + + /** + * Arguments for the scene transition about to begin. + * @hide + */ + public static final String KEY_SCENE_TRANSITION_ARGS = "android:sceneTransitionArgs"; + /** @hide */ public static final int ANIM_NONE = 0; /** @hide */ @@ -100,6 +129,8 @@ public class ActivityOptions { public static final int ANIM_THUMBNAIL_SCALE_UP = 3; /** @hide */ public static final int ANIM_THUMBNAIL_SCALE_DOWN = 4; + /** @hide */ + public static final int ANIM_SCENE_TRANSITION = 5; private String mPackageName; private int mAnimationType = ANIM_NONE; @@ -110,7 +141,10 @@ public class ActivityOptions { private int mStartY; private int mStartWidth; private int mStartHeight; + private String[] mDestSceneNames; + private Bundle mTransitionArgs; private IRemoteCallback mAnimationStartedListener; + private IRemoteCallback mSceneTransitionStartedListener; /** * Create an ActivityOptions specifying a custom animation to run when @@ -156,11 +190,12 @@ public class ActivityOptions { opts.mAnimationType = ANIM_CUSTOM; opts.mCustomEnterResId = enterResId; opts.mCustomExitResId = exitResId; - opts.setListener(handler, listener); + opts.setOnAnimationStartedListener(handler, listener); return opts; } - private void setListener(Handler handler, OnAnimationStartedListener listener) { + private void setOnAnimationStartedListener(Handler handler, + OnAnimationStartedListener listener) { if (listener != null) { final Handler h = handler; final OnAnimationStartedListener finalListener = listener; @@ -176,6 +211,24 @@ public class ActivityOptions { } } + private void setOnSceneTransitionStartedListener(Handler handler, + OnSceneTransitionStartedListener listener) { + if (listener != null) { + final Handler h = handler; + final OnSceneTransitionStartedListener l = listener; + mSceneTransitionStartedListener = new IRemoteCallback.Stub() { + @Override public void sendResult(final Bundle data) throws RemoteException { + h.post(new Runnable() { + public void run() { + l.onSceneTransitionStarted(data != null ? + data.getString(KEY_DEST_SCENE_NAME_CHOSEN) : null); + } + }); + } + }; + } + } + /** * Callback for use with {@link ActivityOptions#makeThumbnailScaleUpAnimation} * to find out when the given animation has started running. @@ -186,6 +239,15 @@ public class ActivityOptions { } /** + * Callback for use with {@link ActivityOptions#makeSceneTransitionAnimation} + * to find out when a transition is about to begin. + * @hide + */ + public interface OnSceneTransitionStartedListener { + void onSceneTransitionStarted(String destSceneName); + } + + /** * Create an ActivityOptions specifying an animation where the new * activity is scaled from a small originating area of the screen to * its final full representation. @@ -298,7 +360,23 @@ public class ActivityOptions { source.getLocationOnScreen(pts); opts.mStartX = pts[0] + startX; opts.mStartY = pts[1] + startY; - opts.setListener(source.getHandler(), listener); + opts.setOnAnimationStartedListener(source.getHandler(), listener); + return opts; + } + + /** + * Create an ActivityOptions specifying an animation where an activity window is asked + * to perform animations within the window content. + * + * @hide + */ + public static ActivityOptions makeSceneTransitionAnimation(String[] destSceneNames, + Bundle args, OnSceneTransitionStartedListener listener, Handler handler) { + ActivityOptions opts = new ActivityOptions(); + opts.mAnimationType = ANIM_SCENE_TRANSITION; + opts.mDestSceneNames = destSceneNames; + opts.mTransitionArgs = args; + opts.setOnSceneTransitionStartedListener(handler, listener); return opts; } @@ -309,23 +387,36 @@ public class ActivityOptions { public ActivityOptions(Bundle opts) { mPackageName = opts.getString(KEY_PACKAGE_NAME); mAnimationType = opts.getInt(KEY_ANIM_TYPE); - if (mAnimationType == ANIM_CUSTOM) { - mCustomEnterResId = opts.getInt(KEY_ANIM_ENTER_RES_ID, 0); - mCustomExitResId = opts.getInt(KEY_ANIM_EXIT_RES_ID, 0); - mAnimationStartedListener = IRemoteCallback.Stub.asInterface( - opts.getIBinder(KEY_ANIM_START_LISTENER)); - } else if (mAnimationType == ANIM_SCALE_UP) { - mStartX = opts.getInt(KEY_ANIM_START_X, 0); - mStartY = opts.getInt(KEY_ANIM_START_Y, 0); - mStartWidth = opts.getInt(KEY_ANIM_START_WIDTH, 0); - mStartHeight = opts.getInt(KEY_ANIM_START_HEIGHT, 0); - } else if (mAnimationType == ANIM_THUMBNAIL_SCALE_UP || - mAnimationType == ANIM_THUMBNAIL_SCALE_DOWN) { - mThumbnail = (Bitmap)opts.getParcelable(KEY_ANIM_THUMBNAIL); - mStartX = opts.getInt(KEY_ANIM_START_X, 0); - mStartY = opts.getInt(KEY_ANIM_START_Y, 0); - mAnimationStartedListener = IRemoteCallback.Stub.asInterface( - opts.getIBinder(KEY_ANIM_START_LISTENER)); + switch (mAnimationType) { + case ANIM_CUSTOM: + mCustomEnterResId = opts.getInt(KEY_ANIM_ENTER_RES_ID, 0); + mCustomExitResId = opts.getInt(KEY_ANIM_EXIT_RES_ID, 0); + mAnimationStartedListener = IRemoteCallback.Stub.asInterface( + opts.getBinder(KEY_ANIM_START_LISTENER)); + break; + + case ANIM_SCALE_UP: + mStartX = opts.getInt(KEY_ANIM_START_X, 0); + mStartY = opts.getInt(KEY_ANIM_START_Y, 0); + mStartWidth = opts.getInt(KEY_ANIM_START_WIDTH, 0); + mStartHeight = opts.getInt(KEY_ANIM_START_HEIGHT, 0); + break; + + case ANIM_THUMBNAIL_SCALE_UP: + case ANIM_THUMBNAIL_SCALE_DOWN: + mThumbnail = (Bitmap)opts.getParcelable(KEY_ANIM_THUMBNAIL); + mStartX = opts.getInt(KEY_ANIM_START_X, 0); + mStartY = opts.getInt(KEY_ANIM_START_Y, 0); + mAnimationStartedListener = IRemoteCallback.Stub.asInterface( + opts.getBinder(KEY_ANIM_START_LISTENER)); + break; + + case ANIM_SCENE_TRANSITION: + mDestSceneNames = opts.getStringArray(KEY_DEST_SCENE_NAMES); + mTransitionArgs = opts.getBundle(KEY_SCENE_TRANSITION_ARGS); + mSceneTransitionStartedListener = IRemoteCallback.Stub.asInterface( + opts.getBinder(KEY_SCENE_TRANSITION_START_LISTENER)); + break; } } @@ -375,11 +466,26 @@ public class ActivityOptions { } /** @hide */ + public String[] getDestSceneNames() { + return mDestSceneNames; + } + + /** @hide */ + public Bundle getSceneTransitionArgs() { + return mTransitionArgs; + } + + /** @hide */ public IRemoteCallback getOnAnimationStartListener() { return mAnimationStartedListener; } /** @hide */ + public IRemoteCallback getOnSceneTransitionStartedListener() { + return mSceneTransitionStartedListener; + } + + /** @hide */ public void abort() { if (mAnimationStartedListener != null) { try { @@ -411,13 +517,16 @@ public class ActivityOptions { mCustomEnterResId = otherOptions.mCustomEnterResId; mCustomExitResId = otherOptions.mCustomExitResId; mThumbnail = null; - if (otherOptions.mAnimationStartedListener != null) { + if (mAnimationStartedListener != null) { try { - otherOptions.mAnimationStartedListener.sendResult(null); + mAnimationStartedListener.sendResult(null); } catch (RemoteException e) { } } mAnimationStartedListener = otherOptions.mAnimationStartedListener; + mSceneTransitionStartedListener = null; + mTransitionArgs = null; + mDestSceneNames = null; break; case ANIM_SCALE_UP: mAnimationType = otherOptions.mAnimationType; @@ -425,13 +534,16 @@ public class ActivityOptions { mStartY = otherOptions.mStartY; mStartWidth = otherOptions.mStartWidth; mStartHeight = otherOptions.mStartHeight; - if (otherOptions.mAnimationStartedListener != null) { + if (mAnimationStartedListener != null) { try { - otherOptions.mAnimationStartedListener.sendResult(null); + mAnimationStartedListener.sendResult(null); } catch (RemoteException e) { } } mAnimationStartedListener = null; + mSceneTransitionStartedListener = null; + mTransitionArgs = null; + mDestSceneNames = null; break; case ANIM_THUMBNAIL_SCALE_UP: case ANIM_THUMBNAIL_SCALE_DOWN: @@ -439,13 +551,28 @@ public class ActivityOptions { mThumbnail = otherOptions.mThumbnail; mStartX = otherOptions.mStartX; mStartY = otherOptions.mStartY; - if (otherOptions.mAnimationStartedListener != null) { + if (mAnimationStartedListener != null) { try { - otherOptions.mAnimationStartedListener.sendResult(null); + mAnimationStartedListener.sendResult(null); } catch (RemoteException e) { } } mAnimationStartedListener = otherOptions.mAnimationStartedListener; + mSceneTransitionStartedListener = null; + mTransitionArgs = null; + mDestSceneNames = null; + break; + case ANIM_SCENE_TRANSITION: + mAnimationType = otherOptions.mAnimationType; + if (mSceneTransitionStartedListener != null) { + try { + mSceneTransitionStartedListener.sendResult(null); + } catch (RemoteException e) { + } + } + mSceneTransitionStartedListener = otherOptions.mSceneTransitionStartedListener; + mDestSceneNames = otherOptions.mDestSceneNames; + mAnimationStartedListener = null; break; } } diff --git a/core/java/android/app/AlertDialog.java b/core/java/android/app/AlertDialog.java index 10d5e25..77526c3 100644 --- a/core/java/android/app/AlertDialog.java +++ b/core/java/android/app/AlertDialog.java @@ -27,7 +27,6 @@ import android.os.Message; import android.util.TypedValue; import android.view.ContextThemeWrapper; import android.view.KeyEvent; -import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; import android.widget.AdapterView; diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index aece462..e71d47d 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -36,7 +36,7 @@ import android.os.RemoteException; * API for interacting with "application operation" tracking. * * <p>This API is not generally intended for third party application developers; most - * features are only available to system applicatins. Obtain an instance of it through + * features are only available to system applications. Obtain an instance of it through * {@link Context#getSystemService(String) Context.getSystemService} with * {@link Context#APP_OPS_SERVICE Context.APP_OPS_SERVICE}.</p> */ diff --git a/core/java/android/app/ApplicationErrorReport.java b/core/java/android/app/ApplicationErrorReport.java index c117486..8b132e0 100644 --- a/core/java/android/app/ApplicationErrorReport.java +++ b/core/java/android/app/ApplicationErrorReport.java @@ -24,7 +24,6 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Parcel; import android.os.Parcelable; -import android.os.SystemClock; import android.os.SystemProperties; import android.provider.Settings; import android.util.Printer; diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index b505d4f..b910ba5 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -57,8 +57,6 @@ import android.view.Display; import java.lang.ref.WeakReference; import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; import java.util.List; /*package*/ diff --git a/core/java/android/app/Dialog.java b/core/java/android/app/Dialog.java index cda2c5f..92f3ffc 100644 --- a/core/java/android/app/Dialog.java +++ b/core/java/android/app/Dialog.java @@ -17,7 +17,6 @@ package android.app; import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; import com.android.internal.app.ActionBarImpl; import com.android.internal.policy.PolicyManager; diff --git a/core/java/android/app/ExpandableListActivity.java b/core/java/android/app/ExpandableListActivity.java index 9651078..e08f25a 100644 --- a/core/java/android/app/ExpandableListActivity.java +++ b/core/java/android/app/ExpandableListActivity.java @@ -27,7 +27,6 @@ import android.widget.ExpandableListAdapter; import android.widget.ExpandableListView; import android.widget.SimpleCursorTreeAdapter; import android.widget.SimpleExpandableListAdapter; -import android.widget.AdapterView.AdapterContextMenuInfo; import java.util.Map; diff --git a/core/java/android/app/Fragment.java b/core/java/android/app/Fragment.java index d626e5f..c09da87 100644 --- a/core/java/android/app/Fragment.java +++ b/core/java/android/app/Fragment.java @@ -17,6 +17,7 @@ package android.app; import android.animation.Animator; +import android.annotation.Nullable; import android.content.ComponentCallbacks2; import android.content.Context; import android.content.Intent; @@ -575,7 +576,7 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene * the given fragment class. This is a runtime exception; it is not * normally expected to happen. */ - public static Fragment instantiate(Context context, String fname, Bundle args) { + public static Fragment instantiate(Context context, String fname, @Nullable Bundle args) { try { Class<?> clazz = sClassMap.get(fname); if (clazz == null) { @@ -1213,7 +1214,8 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene * * @return Return the View for the fragment's UI, or null. */ - public View onCreateView(LayoutInflater inflater, ViewGroup container, + @Nullable + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { return null; } @@ -1228,7 +1230,7 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene * @param savedInstanceState If non-null, this fragment is being re-constructed * from a previous saved state as given here. */ - public void onViewCreated(View view, Bundle savedInstanceState) { + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { } /** @@ -1237,6 +1239,7 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene * * @return The fragment's root view, or null if it has no layout. */ + @Nullable public View getView() { return mView; } diff --git a/core/java/android/app/FragmentBreadCrumbs.java b/core/java/android/app/FragmentBreadCrumbs.java index b810b89..e4de7af 100644 --- a/core/java/android/app/FragmentBreadCrumbs.java +++ b/core/java/android/app/FragmentBreadCrumbs.java @@ -81,14 +81,19 @@ public class FragmentBreadCrumbs extends ViewGroup } public FragmentBreadCrumbs(Context context, AttributeSet attrs) { - this(context, attrs, android.R.style.Widget_FragmentBreadCrumbs); + this(context, attrs, com.android.internal.R.attr.fragmentBreadCrumbsStyle); } - public FragmentBreadCrumbs(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public FragmentBreadCrumbs(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public FragmentBreadCrumbs( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.FragmentBreadCrumbs, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.FragmentBreadCrumbs, defStyleAttr, defStyleRes); mGravity = a.getInt(com.android.internal.R.styleable.FragmentBreadCrumbs_gravity, DEFAULT_GRAVITY); diff --git a/core/java/android/app/IWallpaperManager.aidl b/core/java/android/app/IWallpaperManager.aidl index 3efd3c0..181eb63 100644 --- a/core/java/android/app/IWallpaperManager.aidl +++ b/core/java/android/app/IWallpaperManager.aidl @@ -71,4 +71,14 @@ interface IWallpaperManager { * Returns the desired minimum height for the wallpaper. */ int getHeightHint(); + + /** + * Returns the name of the wallpaper. Private API. + */ + String getName(); + + /** + * Informs the service that wallpaper settings have been restored. Private API. + */ + void settingsRestored(); } diff --git a/core/java/android/app/MediaRouteButton.java b/core/java/android/app/MediaRouteButton.java index a7982f4..fa2813e 100644 --- a/core/java/android/app/MediaRouteButton.java +++ b/core/java/android/app/MediaRouteButton.java @@ -73,13 +73,18 @@ public class MediaRouteButton extends View { } public MediaRouteButton(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); + this(context, attrs, defStyleAttr, 0); + } + + public MediaRouteButton( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); mRouter = (MediaRouter)context.getSystemService(Context.MEDIA_ROUTER_SERVICE); mCallback = new MediaRouterCallback(); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.MediaRouteButton, defStyleAttr, 0); + final TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.MediaRouteButton, defStyleAttr, defStyleRes); setRemoteIndicatorDrawable(a.getDrawable( com.android.internal.R.styleable.MediaRouteButton_externalRouteEnabledDrawable)); mMinWidth = a.getDimensionPixelSize( diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index c63e586..ed3bb92 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -18,6 +18,7 @@ package android.app; import com.android.internal.R; +import android.annotation.IntDef; import android.content.Context; import android.content.Intent; import android.content.res.Resources; @@ -37,6 +38,8 @@ import android.view.View; import android.widget.ProgressBar; import android.widget.RemoteViews; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.text.NumberFormat; import java.util.ArrayList; @@ -350,6 +353,11 @@ public class Notification implements Parcelable public int flags; + /** @hide */ + @IntDef({PRIORITY_DEFAULT,PRIORITY_LOW,PRIORITY_MIN,PRIORITY_HIGH,PRIORITY_MAX}) + @Retention(RetentionPolicy.SOURCE) + public @interface Priority {} + /** * Default notification {@link #priority}. If your application does not prioritize its own * notifications, use this value for all notifications. @@ -391,6 +399,7 @@ public class Notification implements Parcelable * system will make a determination about how to interpret this priority when presenting * the notification. */ + @Priority public int priority; /** @@ -1550,7 +1559,7 @@ public class Notification implements Parcelable * * @see Notification#priority */ - public Builder setPriority(int pri) { + public Builder setPriority(@Priority int pri) { mPriority = pri; return this; } diff --git a/core/java/android/app/PendingIntent.java b/core/java/android/app/PendingIntent.java index bdd0adb..a03e5b6 100644 --- a/core/java/android/app/PendingIntent.java +++ b/core/java/android/app/PendingIntent.java @@ -16,6 +16,9 @@ package android.app; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; import android.content.Intent; import android.content.IIntentReceiver; @@ -30,6 +33,9 @@ import android.os.Parcelable; import android.os.UserHandle; import android.util.AndroidException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * A description of an Intent and target action to perform with it. Instances * of this class are created with {@link #getActivity}, {@link #getActivities}, @@ -86,6 +92,26 @@ import android.util.AndroidException; public final class PendingIntent implements Parcelable { private final IIntentSender mTarget; + /** @hide */ + @IntDef(flag = true, + value = { + FLAG_ONE_SHOT, + FLAG_NO_CREATE, + FLAG_CANCEL_CURRENT, + FLAG_UPDATE_CURRENT, + + Intent.FILL_IN_ACTION, + Intent.FILL_IN_DATA, + Intent.FILL_IN_CATEGORIES, + Intent.FILL_IN_COMPONENT, + Intent.FILL_IN_PACKAGE, + Intent.FILL_IN_SOURCE_BOUNDS, + Intent.FILL_IN_SELECTOR, + Intent.FILL_IN_CLIP_DATA + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Flags {} + /** * Flag for use with {@link #getActivity}, {@link #getBroadcast}, and * {@link #getService}: this @@ -220,7 +246,7 @@ public final class PendingIntent implements Parcelable { * supplied. */ public static PendingIntent getActivity(Context context, int requestCode, - Intent intent, int flags) { + Intent intent, @Flags int flags) { return getActivity(context, requestCode, intent, flags, null); } @@ -253,7 +279,7 @@ public final class PendingIntent implements Parcelable { * supplied. */ public static PendingIntent getActivity(Context context, int requestCode, - Intent intent, int flags, Bundle options) { + @NonNull Intent intent, @Flags int flags, @Nullable Bundle options) { String packageName = context.getPackageName(); String resolvedType = intent != null ? intent.resolveTypeIfNeeded( context.getContentResolver()) : null; @@ -278,7 +304,7 @@ public final class PendingIntent implements Parcelable { * activity is started, not when the pending intent is created. */ public static PendingIntent getActivityAsUser(Context context, int requestCode, - Intent intent, int flags, Bundle options, UserHandle user) { + @NonNull Intent intent, int flags, Bundle options, UserHandle user) { String packageName = context.getPackageName(); String resolvedType = intent != null ? intent.resolveTypeIfNeeded( context.getContentResolver()) : null; @@ -343,7 +369,7 @@ public final class PendingIntent implements Parcelable { * supplied. */ public static PendingIntent getActivities(Context context, int requestCode, - Intent[] intents, int flags) { + @NonNull Intent[] intents, @Flags int flags) { return getActivities(context, requestCode, intents, flags, null); } @@ -393,7 +419,7 @@ public final class PendingIntent implements Parcelable { * supplied. */ public static PendingIntent getActivities(Context context, int requestCode, - Intent[] intents, int flags, Bundle options) { + @NonNull Intent[] intents, @Flags int flags, @Nullable Bundle options) { String packageName = context.getPackageName(); String[] resolvedTypes = new String[intents.length]; for (int i=0; i<intents.length; i++) { @@ -419,7 +445,7 @@ public final class PendingIntent implements Parcelable { * activity is started, not when the pending intent is created. */ public static PendingIntent getActivitiesAsUser(Context context, int requestCode, - Intent[] intents, int flags, Bundle options, UserHandle user) { + @NonNull Intent[] intents, int flags, Bundle options, UserHandle user) { String packageName = context.getPackageName(); String[] resolvedTypes = new String[intents.length]; for (int i=0; i<intents.length; i++) { @@ -463,7 +489,7 @@ public final class PendingIntent implements Parcelable { * supplied. */ public static PendingIntent getBroadcast(Context context, int requestCode, - Intent intent, int flags) { + Intent intent, @Flags int flags) { return getBroadcastAsUser(context, requestCode, intent, flags, new UserHandle(UserHandle.myUserId())); } @@ -517,7 +543,7 @@ public final class PendingIntent implements Parcelable { * supplied. */ public static PendingIntent getService(Context context, int requestCode, - Intent intent, int flags) { + @NonNull Intent intent, @Flags int flags) { String packageName = context.getPackageName(); String resolvedType = intent != null ? intent.resolveTypeIfNeeded( context.getContentResolver()) : null; @@ -747,6 +773,7 @@ public final class PendingIntent implements Parcelable { * @return The package name of the PendingIntent, or null if there is * none associated with it. */ + @Nullable public String getCreatorPackage() { try { return ActivityManagerNative.getDefault() @@ -805,6 +832,7 @@ public final class PendingIntent implements Parcelable { * @return The user handle of the PendingIntent, or null if there is * none associated with it. */ + @Nullable public UserHandle getCreatorUserHandle() { try { int uid = ActivityManagerNative.getDefault() @@ -920,8 +948,8 @@ public final class PendingIntent implements Parcelable { * @param sender The PendingIntent to write, or null. * @param out Where to write the PendingIntent. */ - public static void writePendingIntentOrNullToParcel(PendingIntent sender, - Parcel out) { + public static void writePendingIntentOrNullToParcel(@Nullable PendingIntent sender, + @NonNull Parcel out) { out.writeStrongBinder(sender != null ? sender.mTarget.asBinder() : null); } @@ -936,7 +964,8 @@ public final class PendingIntent implements Parcelable { * @return Returns the Messenger read from the Parcel, or null if null had * been written. */ - public static PendingIntent readPendingIntentOrNullFromParcel(Parcel in) { + @Nullable + public static PendingIntent readPendingIntentOrNullFromParcel(@NonNull Parcel in) { IBinder b = in.readStrongBinder(); return b != null ? new PendingIntent(b) : null; } diff --git a/core/java/android/app/ResultInfo.java b/core/java/android/app/ResultInfo.java index 48a0fc2..5e0867c 100644 --- a/core/java/android/app/ResultInfo.java +++ b/core/java/android/app/ResultInfo.java @@ -17,12 +17,8 @@ package android.app; import android.content.Intent; -import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; -import android.os.Bundle; - -import java.util.Map; /** * {@hide} diff --git a/core/java/android/app/SearchManager.java b/core/java/android/app/SearchManager.java index f9c245e..33c3409 100644 --- a/core/java/android/app/SearchManager.java +++ b/core/java/android/app/SearchManager.java @@ -22,7 +22,6 @@ import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.database.Cursor; import android.graphics.Rect; @@ -34,7 +33,6 @@ import android.os.ServiceManager; import android.os.UserHandle; import android.text.TextUtils; import android.util.Log; -import android.util.Slog; import android.view.KeyEvent; import java.util.List; diff --git a/core/java/android/app/SharedPreferencesImpl.java b/core/java/android/app/SharedPreferencesImpl.java index 86fd7b9..dd882ce 100644 --- a/core/java/android/app/SharedPreferencesImpl.java +++ b/core/java/android/app/SharedPreferencesImpl.java @@ -42,7 +42,6 @@ import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; import libcore.io.ErrnoException; import libcore.io.IoUtils; diff --git a/core/java/android/app/TaskStackBuilder.java b/core/java/android/app/TaskStackBuilder.java index 3e0ac7e..0077db1 100644 --- a/core/java/android/app/TaskStackBuilder.java +++ b/core/java/android/app/TaskStackBuilder.java @@ -16,6 +16,7 @@ package android.app; +import android.annotation.NonNull; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -244,7 +245,7 @@ public class TaskStackBuilder { * * @return The obtained PendingIntent */ - public PendingIntent getPendingIntent(int requestCode, int flags) { + public PendingIntent getPendingIntent(int requestCode, @PendingIntent.Flags int flags) { return getPendingIntent(requestCode, flags, null); } @@ -263,7 +264,8 @@ public class TaskStackBuilder { * * @return The obtained PendingIntent */ - public PendingIntent getPendingIntent(int requestCode, int flags, Bundle options) { + public PendingIntent getPendingIntent(int requestCode, @PendingIntent.Flags int flags, + Bundle options) { if (mIntents.isEmpty()) { throw new IllegalStateException( "No intents added to TaskStackBuilder; cannot getPendingIntent"); @@ -294,6 +296,7 @@ public class TaskStackBuilder { * * @return An array containing the intents added to this builder. */ + @NonNull public Intent[] getIntents() { Intent[] intents = new Intent[mIntents.size()]; if (intents.length == 0) return intents; diff --git a/core/java/android/app/TimePickerDialog.java b/core/java/android/app/TimePickerDialog.java index 952227f..a85c61f 100644 --- a/core/java/android/app/TimePickerDialog.java +++ b/core/java/android/app/TimePickerDialog.java @@ -16,17 +16,19 @@ package android.app; -import com.android.internal.R; - import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.os.Bundle; +import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.widget.TimePicker; import android.widget.TimePicker.OnTimeChangedListener; +import com.android.internal.R; + + /** * A dialog that prompts the user for the time of day using a {@link TimePicker}. * @@ -38,7 +40,7 @@ public class TimePickerDialog extends AlertDialog /** * The callback interface used to indicate the user is done filling in - * the time (they clicked on the 'Set' button). + * the time (they clicked on the 'Done' button). */ public interface OnTimeSetListener { @@ -55,7 +57,7 @@ public class TimePickerDialog extends AlertDialog private static final String IS_24_HOUR = "is24hour"; private final TimePicker mTimePicker; - private final OnTimeSetListener mCallback; + private final OnTimeSetListener mTimeSetCallback; int mInitialHourOfDay; int mInitialMinute; @@ -74,6 +76,16 @@ public class TimePickerDialog extends AlertDialog this(context, 0, callBack, hourOfDay, minute, is24HourView); } + static int resolveDialogTheme(Context context, int resid) { + if (resid == 0) { + TypedValue outValue = new TypedValue(); + context.getTheme().resolveAttribute(R.attr.timePickerDialogTheme, outValue, true); + return outValue.resourceId; + } else { + return resid; + } + } + /** * @param context Parent. * @param theme the theme to apply to this dialog @@ -86,17 +98,13 @@ public class TimePickerDialog extends AlertDialog int theme, OnTimeSetListener callBack, int hourOfDay, int minute, boolean is24HourView) { - super(context, theme); - mCallback = callBack; + super(context, resolveDialogTheme(context, theme)); + mTimeSetCallback = callBack; mInitialHourOfDay = hourOfDay; mInitialMinute = minute; mIs24HourView = is24HourView; - setIcon(0); - setTitle(R.string.time_picker_dialog_title); - Context themeContext = getContext(); - setButton(BUTTON_POSITIVE, themeContext.getText(R.string.date_time_done), this); LayoutInflater inflater = (LayoutInflater) themeContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); @@ -104,7 +112,18 @@ public class TimePickerDialog extends AlertDialog setView(view); mTimePicker = (TimePicker) view.findViewById(R.id.timePicker); - // initialize state + // Initialize state + mTimePicker.setLegacyMode(false /* will show new UI */); + mTimePicker.setShowDoneButton(true); + mTimePicker.setDismissCallback(new TimePicker.TimePickerDismissCallback() { + @Override + public void dismiss(TimePicker view, boolean isCancel, int hourOfDay, int minute) { + if (!isCancel) { + mTimeSetCallback.onTimeSet(view, hourOfDay, minute); + } + TimePickerDialog.this.dismiss(); + } + }); mTimePicker.setIs24HourView(mIs24HourView); mTimePicker.setCurrentHour(mInitialHourOfDay); mTimePicker.setCurrentMinute(mInitialMinute); @@ -125,9 +144,9 @@ public class TimePickerDialog extends AlertDialog } private void tryNotifyTimeSet() { - if (mCallback != null) { + if (mTimeSetCallback != null) { mTimePicker.clearFocus(); - mCallback.onTimeSet(mTimePicker, mTimePicker.getCurrentHour(), + mTimeSetCallback.onTimeSet(mTimePicker, mTimePicker.getCurrentHour(), mTimePicker.getCurrentMinute()); } } diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index ced72f8..4435032 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -41,7 +41,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; -import android.os.Message; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.ServiceManager; @@ -221,24 +220,9 @@ public class WallpaperManager { private static final int MSG_CLEAR_WALLPAPER = 1; - private final Handler mHandler; - Globals(Looper looper) { IBinder b = ServiceManager.getService(Context.WALLPAPER_SERVICE); mService = IWallpaperManager.Stub.asInterface(b); - mHandler = new Handler(looper) { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_CLEAR_WALLPAPER: - synchronized (this) { - mWallpaper = null; - mDefaultWallpaper = null; - } - break; - } - } - }; } public void onWallpaperChanged() { @@ -247,7 +231,10 @@ public class WallpaperManager { * to null so if the user requests the wallpaper again then we'll * fetch it. */ - mHandler.sendEmptyMessage(MSG_CLEAR_WALLPAPER); + synchronized (this) { + mWallpaper = null; + mDefaultWallpaper = null; + } } public Bitmap peekWallpaperBitmap(Context context, boolean returnDefault) { @@ -280,7 +267,6 @@ public class WallpaperManager { synchronized (this) { mWallpaper = null; mDefaultWallpaper = null; - mHandler.removeMessages(MSG_CLEAR_WALLPAPER); } } diff --git a/core/java/android/app/backup/FullBackup.java b/core/java/android/app/backup/FullBackup.java index cb0737e..cfd0a65 100644 --- a/core/java/android/app/backup/FullBackup.java +++ b/core/java/android/app/backup/FullBackup.java @@ -16,9 +16,6 @@ package android.app.backup; -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; import android.os.ParcelFileDescriptor; import android.util.Log; diff --git a/core/java/android/app/backup/SharedPreferencesBackupHelper.java b/core/java/android/app/backup/SharedPreferencesBackupHelper.java index 213bd31..939616b 100644 --- a/core/java/android/app/backup/SharedPreferencesBackupHelper.java +++ b/core/java/android/app/backup/SharedPreferencesBackupHelper.java @@ -18,7 +18,6 @@ package android.app.backup; import android.app.QueuedWork; import android.content.Context; -import android.content.SharedPreferences; import android.os.ParcelFileDescriptor; import android.util.Log; diff --git a/core/java/android/appwidget/AppWidgetHost.java b/core/java/android/appwidget/AppWidgetHost.java index f104d71..84d3835 100644 --- a/core/java/android/appwidget/AppWidgetHost.java +++ b/core/java/android/appwidget/AppWidgetHost.java @@ -19,7 +19,6 @@ package android.appwidget; import java.util.ArrayList; import java.util.HashMap; -import android.app.ActivityThread; import android.content.Context; import android.os.Binder; import android.os.Handler; @@ -31,7 +30,6 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; import android.util.DisplayMetrics; -import android.util.Log; import android.util.TypedValue; import android.widget.RemoteViews; import android.widget.RemoteViews.OnClickHandler; diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java index d1c7bec..8a89cbc 100644 --- a/core/java/android/appwidget/AppWidgetManager.java +++ b/core/java/android/appwidget/AppWidgetManager.java @@ -16,7 +16,6 @@ package android.appwidget; -import android.app.ActivityManagerNative; import android.content.ComponentName; import android.content.Context; import android.content.Intent; diff --git a/core/java/android/bluetooth/BluetoothAdapter.java b/core/java/android/bluetooth/BluetoothAdapter.java index e2bc80a..7ee2313 100644 --- a/core/java/android/bluetooth/BluetoothAdapter.java +++ b/core/java/android/bluetooth/BluetoothAdapter.java @@ -19,9 +19,7 @@ package android.bluetooth; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.content.Context; -import android.os.Binder; import android.os.IBinder; -import android.os.Message; import android.os.ParcelUuid; import android.os.RemoteException; import android.os.ServiceManager; @@ -34,10 +32,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.HashMap; -import java.util.LinkedList; import java.util.Locale; import java.util.Map; -import java.util.Random; import java.util.Set; import java.util.UUID; diff --git a/core/java/android/bluetooth/BluetoothDevice.java b/core/java/android/bluetooth/BluetoothDevice.java index d789a94..1bd698d 100644 --- a/core/java/android/bluetooth/BluetoothDevice.java +++ b/core/java/android/bluetooth/BluetoothDevice.java @@ -19,12 +19,10 @@ package android.bluetooth; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.content.Context; -import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; import android.os.ParcelUuid; import android.os.RemoteException; -import android.os.ServiceManager; import android.util.Log; import java.io.IOException; diff --git a/core/java/android/bluetooth/BluetoothGatt.java b/core/java/android/bluetooth/BluetoothGatt.java index a2bb78c..b6e1bb3 100644 --- a/core/java/android/bluetooth/BluetoothGatt.java +++ b/core/java/android/bluetooth/BluetoothGatt.java @@ -18,18 +18,9 @@ package android.bluetooth; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; -import android.bluetooth.BluetoothProfile.ServiceListener; -import android.bluetooth.IBluetoothManager; -import android.bluetooth.IBluetoothStateChangeCallback; - -import android.content.ComponentName; import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.os.IBinder; import android.os.ParcelUuid; import android.os.RemoteException; -import android.os.ServiceManager; import android.util.Log; import java.util.ArrayList; diff --git a/core/java/android/bluetooth/BluetoothGattCharacteristic.java b/core/java/android/bluetooth/BluetoothGattCharacteristic.java index f0ecbb4..a86677c 100644 --- a/core/java/android/bluetooth/BluetoothGattCharacteristic.java +++ b/core/java/android/bluetooth/BluetoothGattCharacteristic.java @@ -16,7 +16,6 @@ package android.bluetooth; import java.util.ArrayList; -import java.util.IllegalFormatConversionException; import java.util.List; import java.util.UUID; diff --git a/core/java/android/bluetooth/BluetoothGattServer.java b/core/java/android/bluetooth/BluetoothGattServer.java index 58ee54f..09072f9 100644 --- a/core/java/android/bluetooth/BluetoothGattServer.java +++ b/core/java/android/bluetooth/BluetoothGattServer.java @@ -19,18 +19,9 @@ package android.bluetooth; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; -import android.bluetooth.BluetoothProfile.ServiceListener; -import android.bluetooth.IBluetoothManager; -import android.bluetooth.IBluetoothStateChangeCallback; - -import android.content.ComponentName; import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.os.IBinder; import android.os.ParcelUuid; import android.os.RemoteException; -import android.os.ServiceManager; import android.util.Log; import java.util.ArrayList; diff --git a/core/java/android/bluetooth/BluetoothGattServerCallback.java b/core/java/android/bluetooth/BluetoothGattServerCallback.java index f9f1d97..fc3ffe8 100644 --- a/core/java/android/bluetooth/BluetoothGattServerCallback.java +++ b/core/java/android/bluetooth/BluetoothGattServerCallback.java @@ -18,8 +18,6 @@ package android.bluetooth; import android.bluetooth.BluetoothDevice; -import android.util.Log; - /** * This abstract class is used to implement {@link BluetoothGattServer} callbacks. */ diff --git a/core/java/android/bluetooth/BluetoothHealth.java b/core/java/android/bluetooth/BluetoothHealth.java index 2e950fa..daf3bad 100644 --- a/core/java/android/bluetooth/BluetoothHealth.java +++ b/core/java/android/bluetooth/BluetoothHealth.java @@ -23,7 +23,6 @@ import android.content.ServiceConnection; import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.os.RemoteException; -import android.os.ServiceManager; import android.util.Log; import java.util.ArrayList; diff --git a/core/java/android/bluetooth/BluetoothInputDevice.java b/core/java/android/bluetooth/BluetoothInputDevice.java index 844f432..c4ba5b1 100644 --- a/core/java/android/bluetooth/BluetoothInputDevice.java +++ b/core/java/android/bluetooth/BluetoothInputDevice.java @@ -24,7 +24,6 @@ import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; import android.os.RemoteException; -import android.os.ServiceManager; import android.util.Log; import java.util.ArrayList; diff --git a/core/java/android/bluetooth/BluetoothMap.java b/core/java/android/bluetooth/BluetoothMap.java index 92a2f1e..5a1b7aa 100644 --- a/core/java/android/bluetooth/BluetoothMap.java +++ b/core/java/android/bluetooth/BluetoothMap.java @@ -24,7 +24,6 @@ import android.content.Intent; import android.content.ServiceConnection; import android.os.RemoteException; import android.os.IBinder; -import android.os.ServiceManager; import android.util.Log; /** diff --git a/core/java/android/bluetooth/BluetoothPan.java b/core/java/android/bluetooth/BluetoothPan.java index b7a37f4..e72832c 100644 --- a/core/java/android/bluetooth/BluetoothPan.java +++ b/core/java/android/bluetooth/BluetoothPan.java @@ -24,7 +24,6 @@ import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; import android.os.RemoteException; -import android.os.ServiceManager; import android.util.Log; import java.util.ArrayList; diff --git a/core/java/android/bluetooth/BluetoothPbap.java b/core/java/android/bluetooth/BluetoothPbap.java index 7f45652..8522ee0 100644 --- a/core/java/android/bluetooth/BluetoothPbap.java +++ b/core/java/android/bluetooth/BluetoothPbap.java @@ -22,7 +22,6 @@ import android.content.Intent; import android.content.ServiceConnection; import android.os.RemoteException; import android.os.IBinder; -import android.os.ServiceManager; import android.util.Log; /** diff --git a/core/java/android/bluetooth/BluetoothServerSocket.java b/core/java/android/bluetooth/BluetoothServerSocket.java index 96be8a2..bc56e55 100644 --- a/core/java/android/bluetooth/BluetoothServerSocket.java +++ b/core/java/android/bluetooth/BluetoothServerSocket.java @@ -17,7 +17,6 @@ package android.bluetooth; import android.os.Handler; -import android.os.Message; import android.os.ParcelUuid; import java.io.Closeable; diff --git a/core/java/android/bluetooth/BluetoothSocket.java b/core/java/android/bluetooth/BluetoothSocket.java index d10eaea..ddefc70 100644 --- a/core/java/android/bluetooth/BluetoothSocket.java +++ b/core/java/android/bluetooth/BluetoothSocket.java @@ -16,21 +16,16 @@ package android.bluetooth; -import android.os.IBinder; import android.os.ParcelUuid; import android.os.ParcelFileDescriptor; import android.os.RemoteException; -import android.os.ServiceManager; import android.util.Log; import java.io.Closeable; import java.io.FileDescriptor; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.List; import java.util.Locale; import java.util.UUID; import android.net.LocalSocket; diff --git a/core/java/android/bluetooth/BluetoothTetheringDataTracker.java b/core/java/android/bluetooth/BluetoothTetheringDataTracker.java index a9b7176..6dd551e 100644 --- a/core/java/android/bluetooth/BluetoothTetheringDataTracker.java +++ b/core/java/android/bluetooth/BluetoothTetheringDataTracker.java @@ -17,9 +17,6 @@ package android.bluetooth; import android.net.BaseNetworkStateTracker; -import android.os.IBinder; -import android.os.ServiceManager; -import android.os.INetworkManagementService; import android.content.Context; import android.net.ConnectivityManager; import android.net.DhcpResults; @@ -35,11 +32,6 @@ import android.os.Message; import android.os.Messenger; import android.text.TextUtils; import android.util.Log; -import java.net.InterfaceAddress; -import android.net.LinkAddress; -import android.net.RouteInfo; -import java.net.Inet4Address; -import android.os.SystemProperties; import com.android.internal.util.AsyncChannel; @@ -143,11 +135,6 @@ public class BluetoothTetheringDataTracker extends BaseNetworkStateTracker { } @Override - public void captivePortalCheckComplete() { - // not implemented - } - - @Override public void captivePortalCheckCompleted(boolean isCaptivePortal) { // not implemented } diff --git a/core/java/android/content/AsyncTaskLoader.java b/core/java/android/content/AsyncTaskLoader.java index eb7426e..7241e0d 100644 --- a/core/java/android/content/AsyncTaskLoader.java +++ b/core/java/android/content/AsyncTaskLoader.java @@ -20,7 +20,7 @@ import android.os.AsyncTask; import android.os.Handler; import android.os.OperationCanceledException; import android.os.SystemClock; -import android.util.Slog; +import android.util.Log; import android.util.TimeUtils; import java.io.FileDescriptor; @@ -64,10 +64,10 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { /* Runs on a worker thread */ @Override protected D doInBackground(Void... params) { - if (DEBUG) Slog.v(TAG, this + " >>> doInBackground"); + if (DEBUG) Log.v(TAG, this + " >>> doInBackground"); try { D data = AsyncTaskLoader.this.onLoadInBackground(); - if (DEBUG) Slog.v(TAG, this + " <<< doInBackground"); + if (DEBUG) Log.v(TAG, this + " <<< doInBackground"); return data; } catch (OperationCanceledException ex) { if (!isCancelled()) { @@ -79,7 +79,7 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { // So we treat this case as an unhandled exception. throw ex; } - if (DEBUG) Slog.v(TAG, this + " <<< doInBackground (was canceled)", ex); + if (DEBUG) Log.v(TAG, this + " <<< doInBackground (was canceled)", ex); return null; } } @@ -87,7 +87,7 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { /* Runs on the UI thread */ @Override protected void onPostExecute(D data) { - if (DEBUG) Slog.v(TAG, this + " onPostExecute"); + if (DEBUG) Log.v(TAG, this + " onPostExecute"); try { AsyncTaskLoader.this.dispatchOnLoadComplete(this, data); } finally { @@ -98,7 +98,7 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { /* Runs on the UI thread */ @Override protected void onCancelled(D data) { - if (DEBUG) Slog.v(TAG, this + " onCancelled"); + if (DEBUG) Log.v(TAG, this + " onCancelled"); try { AsyncTaskLoader.this.dispatchOnCancelled(this, data); } finally { @@ -162,18 +162,18 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { super.onForceLoad(); cancelLoad(); mTask = new LoadTask(); - if (DEBUG) Slog.v(TAG, "Preparing load: mTask=" + mTask); + if (DEBUG) Log.v(TAG, "Preparing load: mTask=" + mTask); executePendingTask(); } @Override protected boolean onCancelLoad() { - if (DEBUG) Slog.v(TAG, "onCancelLoad: mTask=" + mTask); + if (DEBUG) Log.v(TAG, "onCancelLoad: mTask=" + mTask); if (mTask != null) { if (mCancellingTask != null) { // There was a pending task already waiting for a previous // one being canceled; just drop it. - if (DEBUG) Slog.v(TAG, + if (DEBUG) Log.v(TAG, "cancelLoad: still waiting for cancelled task; dropping next"); if (mTask.waiting) { mTask.waiting = false; @@ -184,14 +184,14 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { } else if (mTask.waiting) { // There is a task, but it is waiting for the time it should // execute. We can just toss it. - if (DEBUG) Slog.v(TAG, "cancelLoad: task is waiting, dropping it"); + if (DEBUG) Log.v(TAG, "cancelLoad: task is waiting, dropping it"); mTask.waiting = false; mHandler.removeCallbacks(mTask); mTask = null; return false; } else { boolean cancelled = mTask.cancel(false); - if (DEBUG) Slog.v(TAG, "cancelLoad: cancelled=" + cancelled); + if (DEBUG) Log.v(TAG, "cancelLoad: cancelled=" + cancelled); if (cancelled) { mCancellingTask = mTask; cancelLoadInBackground(); @@ -223,7 +223,7 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { long now = SystemClock.uptimeMillis(); if (now < (mLastLoadCompleteTime+mUpdateThrottle)) { // Not yet time to do another load. - if (DEBUG) Slog.v(TAG, "Waiting until " + if (DEBUG) Log.v(TAG, "Waiting until " + (mLastLoadCompleteTime+mUpdateThrottle) + " to execute: " + mTask); mTask.waiting = true; @@ -231,7 +231,7 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { return; } } - if (DEBUG) Slog.v(TAG, "Executing: " + mTask); + if (DEBUG) Log.v(TAG, "Executing: " + mTask); mTask.executeOnExecutor(mExecutor, (Void[]) null); } } @@ -239,11 +239,11 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { void dispatchOnCancelled(LoadTask task, D data) { onCanceled(data); if (mCancellingTask == task) { - if (DEBUG) Slog.v(TAG, "Cancelled task is now canceled!"); + if (DEBUG) Log.v(TAG, "Cancelled task is now canceled!"); rollbackContentChanged(); mLastLoadCompleteTime = SystemClock.uptimeMillis(); mCancellingTask = null; - if (DEBUG) Slog.v(TAG, "Delivering cancellation"); + if (DEBUG) Log.v(TAG, "Delivering cancellation"); deliverCancellation(); executePendingTask(); } @@ -251,7 +251,7 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { void dispatchOnLoadComplete(LoadTask task, D data) { if (mTask != task) { - if (DEBUG) Slog.v(TAG, "Load complete of old task, trying to cancel"); + if (DEBUG) Log.v(TAG, "Load complete of old task, trying to cancel"); dispatchOnCancelled(task, data); } else { if (isAbandoned()) { @@ -261,7 +261,7 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { commitContentChanged(); mLastLoadCompleteTime = SystemClock.uptimeMillis(); mTask = null; - if (DEBUG) Slog.v(TAG, "Delivering result"); + if (DEBUG) Log.v(TAG, "Delivering result"); deliverResult(data); } } diff --git a/core/java/android/content/ClipboardManager.java b/core/java/android/content/ClipboardManager.java index 73e6fd0..5653cad 100644 --- a/core/java/android/content/ClipboardManager.java +++ b/core/java/android/content/ClipboardManager.java @@ -22,8 +22,6 @@ import android.os.RemoteException; import android.os.Handler; import android.os.IBinder; import android.os.ServiceManager; -import android.os.StrictMode; -import android.util.Log; import java.util.ArrayList; diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index 4e6cc92..018e4c5 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -141,7 +141,7 @@ public abstract class ContentResolver { public static final String SYNC_EXTRAS_PRIORITY = "sync_priority"; /** {@hide} Flag to allow sync to occur on metered network. */ - public static final String SYNC_EXTRAS_DISALLOW_METERED = "disallow_metered"; + public static final String SYNC_EXTRAS_DISALLOW_METERED = "allow_metered"; /** * Set by the SyncManager to request that the SyncAdapter initialize itself for @@ -368,9 +368,7 @@ public abstract class ContentResolver { } /** - * <p> * Query the given URI, returning a {@link Cursor} over the result set. - * </p> * <p> * For best performance, the caller should follow these guidelines: * <ul> @@ -405,9 +403,8 @@ public abstract class ContentResolver { } /** - * <p> - * Query the given URI, returning a {@link Cursor} over the result set. - * </p> + * Query the given URI, returning a {@link Cursor} over the result set + * with optional support for cancellation. * <p> * For best performance, the caller should follow these guidelines: * <ul> @@ -1751,7 +1748,7 @@ public abstract class ContentResolver { new SyncRequest.Builder() .setSyncAdapter(account, authority) .setExtras(extras) - .syncOnce() + .syncOnce() // Immediate sync. .build(); requestSync(request); } @@ -1759,9 +1756,6 @@ public abstract class ContentResolver { /** * Register a sync with the SyncManager. These requests are built using the * {@link SyncRequest.Builder}. - * - * @param request The immutable SyncRequest object containing the sync parameters. Use - * {@link SyncRequest.Builder} to construct these. */ public static void requestSync(SyncRequest request) { try { @@ -1829,12 +1823,25 @@ public abstract class ContentResolver { */ public static void cancelSync(Account account, String authority) { try { - getContentService().cancelSync(account, authority); + getContentService().cancelSync(account, authority, null); } catch (RemoteException e) { } } /** + * Cancel any active or pending syncs that are running on this service. + * + * @param cname the service for which to cancel all active/pending operations. + */ + public static void cancelSync(ComponentName cname) { + try { + getContentService().cancelSync(null, null, cname); + } catch (RemoteException e) { + + } + } + + /** * Get information about the SyncAdapters that are known to the system. * @return an array of SyncAdapters that have registered with the system */ @@ -1897,12 +1904,13 @@ public abstract class ContentResolver { * {@link #SYNC_EXTRAS_INITIALIZE}, {@link #SYNC_EXTRAS_FORCE}, * {@link #SYNC_EXTRAS_EXPEDITED}, {@link #SYNC_EXTRAS_MANUAL} set to true. * If any are supplied then an {@link IllegalArgumentException} will be thrown. - * <p>As of API level 19 this function introduces a default flexibility of ~4% (up to a maximum - * of one hour in the day) into the requested period. Use - * {@link SyncRequest.Builder#syncPeriodic(long, long)} to set this flexibility manually. * * <p>This method requires the caller to hold the permission * {@link android.Manifest.permission#WRITE_SYNC_SETTINGS}. + * <p>The bundle for a periodic sync can be queried by applications with the correct + * permissions using + * {@link ContentResolver#getPeriodicSyncs(Account account, String provider)}, so no + * sensitive data should be transferred here. * * @param account the account to specify in the sync * @param authority the provider to specify in the sync request @@ -1932,6 +1940,26 @@ public abstract class ContentResolver { } /** + * {@hide} + * Helper function to throw an <code>IllegalArgumentException</code> if any illegal + * extras were set for a periodic sync. + * + * @param extras bundle to validate. + */ + public static boolean invalidPeriodicExtras(Bundle extras) { + if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false) + || extras.getBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, false) + || extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false) + || extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, false) + || extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false) + || extras.getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false) + || extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false)) { + return true; + } + return false; + } + + /** * Remove a periodic sync. Has no affect if account, authority and extras don't match * an existing periodic sync. * <p>This method requires the caller to hold the permission @@ -1951,6 +1979,31 @@ public abstract class ContentResolver { } /** + * Remove the specified sync. This will cancel any pending or active syncs. If the request is + * for a periodic sync, this call will remove any future occurrences. + * <p>If a periodic sync is specified, the caller must hold the permission + * {@link android.Manifest.permission#WRITE_SYNC_SETTINGS}. If this SyncRequest targets a + * SyncService adapter,the calling application must be signed with the same certificate as the + * adapter. + *</p>It is possible to cancel a sync using a SyncRequest object that is not the same object + * with which you requested the sync. Do so by building a SyncRequest with the same + * service/adapter, frequency, <b>and</b> extras bundle. + * + * @param request SyncRequest object containing information about sync to cancel. + */ + public static void cancelSync(SyncRequest request) { + if (request == null) { + throw new IllegalArgumentException("request cannot be null"); + } + try { + getContentService().cancelRequest(request); + } catch (RemoteException e) { + // exception ignored; if this is thrown then it means the runtime is in the midst of + // being restarted + } + } + + /** * Get the list of information about the periodic syncs for the given account and authority. * <p>This method requires the caller to hold the permission * {@link android.Manifest.permission#READ_SYNC_SETTINGS}. @@ -1961,7 +2014,23 @@ public abstract class ContentResolver { */ public static List<PeriodicSync> getPeriodicSyncs(Account account, String authority) { try { - return getContentService().getPeriodicSyncs(account, authority); + return getContentService().getPeriodicSyncs(account, authority, null); + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + /** + * Return periodic syncs associated with the provided component. + * <p>The calling application must be signed with the same certificate as the target component, + * otherwise this call will fail. + */ + public static List<PeriodicSync> getPeriodicSyncs(ComponentName cname) { + if (cname == null) { + throw new IllegalArgumentException("Component must not be null"); + } + try { + return getContentService().getPeriodicSyncs(null, null, cname); } catch (RemoteException e) { throw new RuntimeException("the ContentService should always be reachable", e); } @@ -1997,6 +2066,38 @@ public abstract class ContentResolver { } /** + * Set whether the provided {@link SyncService} is available to process work. + * <p>This method requires the caller to hold the permission + * {@link android.Manifest.permission#WRITE_SYNC_SETTINGS}. + * <p>The calling application must be signed with the same certificate as the target component, + * otherwise this call will fail. + */ + public static void setServiceActive(ComponentName cname, boolean active) { + try { + getContentService().setServiceActive(cname, active); + } catch (RemoteException e) { + // exception ignored; if this is thrown then it means the runtime is in the midst of + // being restarted + } + } + + /** + * Query the state of this sync service. + * <p>Set with {@link #setServiceActive(ComponentName cname, boolean active)}. + * <p>The calling application must be signed with the same certificate as the target component, + * otherwise this call will fail. + * @param cname ComponentName referring to a {@link SyncService} + * @return true if jobs will be run on this service, false otherwise. + */ + public static boolean isServiceActive(ComponentName cname) { + try { + return getContentService().isServiceActive(cname); + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + /** * Gets the master auto-sync setting that applies to all the providers and accounts. * If this is false then the per-provider auto-sync setting is ignored. * <p>This method requires the caller to hold the permission @@ -2030,8 +2131,8 @@ public abstract class ContentResolver { } /** - * Returns true if there is currently a sync operation for the given - * account or authority in the pending list, or actively being processed. + * Returns true if there is currently a sync operation for the given account or authority + * actively being processed. * <p>This method requires the caller to hold the permission * {@link android.Manifest.permission#READ_SYNC_STATS}. * @param account the account whose setting we are querying @@ -2039,8 +2140,26 @@ public abstract class ContentResolver { * @return true if a sync is active for the given account or authority. */ public static boolean isSyncActive(Account account, String authority) { + if (account == null) { + throw new IllegalArgumentException("account must not be null"); + } + if (authority == null) { + throw new IllegalArgumentException("authority must not be null"); + } + + try { + return getContentService().isSyncActive(account, authority, null); + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + public static boolean isSyncActive(ComponentName cname) { + if (cname == null) { + throw new IllegalArgumentException("component name must not be null"); + } try { - return getContentService().isSyncActive(account, authority); + return getContentService().isSyncActive(null, null, cname); } catch (RemoteException e) { throw new RuntimeException("the ContentService should always be reachable", e); } @@ -2098,7 +2217,7 @@ public abstract class ContentResolver { */ public static SyncStatusInfo getSyncStatus(Account account, String authority) { try { - return getContentService().getSyncStatus(account, authority); + return getContentService().getSyncStatus(account, authority, null); } catch (RemoteException e) { throw new RuntimeException("the ContentService should always be reachable", e); } @@ -2114,7 +2233,15 @@ public abstract class ContentResolver { */ public static boolean isSyncPending(Account account, String authority) { try { - return getContentService().isSyncPending(account, authority); + return getContentService().isSyncPending(account, authority, null); + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + public static boolean isSyncPending(ComponentName cname) { + try { + return getContentService().isSyncPending(null, null, cname); } catch (RemoteException e) { throw new RuntimeException("the ContentService should always be reachable", e); } diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 2e4e209..32b95c2 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -16,6 +16,10 @@ package android.content; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.StringDef; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.AssetManager; @@ -47,6 +51,8 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Interface to global information about an application environment. This is @@ -132,6 +138,20 @@ public abstract class Context { */ public static final int MODE_ENABLE_WRITE_AHEAD_LOGGING = 0x0008; + /** @hide */ + @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 + }) + @Retention(RetentionPolicy.SOURCE) + public @interface BindServiceFlags {} + /** * Flag for {@link #bindService}: automatically create the service as long * as the binding exists. Note that while this will create the service, @@ -495,7 +515,7 @@ public abstract class Context { * and {@link #MODE_WORLD_WRITEABLE} to control permissions. The bit * {@link #MODE_MULTI_PROCESS} can also be used if multiple processes * are mutating the same SharedPreferences file. {@link #MODE_MULTI_PROCESS} - * is always on in apps targetting Gingerbread (Android 2.3) and below, and + * is always on in apps targeting Gingerbread (Android 2.3) and below, and * off by default in later versions. * * @return The single {@link SharedPreferences} instance that can be used @@ -674,7 +694,8 @@ public abstract class Context { * @see #getFilesDir * @see android.os.Environment#getExternalStoragePublicDirectory */ - public abstract File getExternalFilesDir(String type); + @Nullable + public abstract File getExternalFilesDir(@Nullable String type); /** * Returns absolute paths to application-specific directories on all @@ -840,6 +861,7 @@ public abstract class Context { * * @see #getCacheDir */ + @Nullable public abstract File getExternalCacheDir(); /** @@ -960,7 +982,8 @@ public abstract class Context { * @see #deleteDatabase */ public abstract SQLiteDatabase openOrCreateDatabase(String name, - int mode, CursorFactory factory, DatabaseErrorHandler errorHandler); + int mode, CursorFactory factory, + @Nullable DatabaseErrorHandler errorHandler); /** * Delete an existing private SQLiteDatabase associated with this Context's @@ -1106,7 +1129,7 @@ public abstract class Context { * @see #startActivity(Intent) * @see PackageManager#resolveActivity */ - public abstract void startActivity(Intent intent, Bundle options); + public abstract void startActivity(Intent intent, @Nullable Bundle options); /** * Version of {@link #startActivity(Intent, Bundle)} that allows you to specify the @@ -1122,7 +1145,7 @@ public abstract class Context { * @throws ActivityNotFoundException * @hide */ - public void startActivityAsUser(Intent intent, Bundle options, UserHandle userId) { + public void startActivityAsUser(Intent intent, @Nullable Bundle options, UserHandle userId) { throw new RuntimeException("Not implemented. Must override in a subclass."); } @@ -1241,7 +1264,7 @@ public abstract class Context { * @see #startIntentSender(IntentSender, Intent, int, int, int) */ public abstract void startIntentSender(IntentSender intent, - Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, + @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, Bundle options) throws IntentSender.SendIntentException; /** @@ -1291,11 +1314,11 @@ public abstract class Context { * @see #sendOrderedBroadcast(Intent, String, BroadcastReceiver, Handler, int, String, Bundle) */ public abstract void sendBroadcast(Intent intent, - String receiverPermission); + @Nullable String receiverPermission); /** * Like {@link #sendBroadcast(Intent, String)}, but also allows specification - * of an assocated app op as per {@link android.app.AppOpsManager}. + * of an associated app op as per {@link android.app.AppOpsManager}. * @hide */ public abstract void sendBroadcast(Intent intent, @@ -1322,7 +1345,7 @@ public abstract class Context { * @see #sendOrderedBroadcast(Intent, String, BroadcastReceiver, Handler, int, String, Bundle) */ public abstract void sendOrderedBroadcast(Intent intent, - String receiverPermission); + @Nullable String receiverPermission); /** * Version of {@link #sendBroadcast(Intent)} that allows you to @@ -1366,15 +1389,15 @@ public abstract class Context { * @see #registerReceiver * @see android.app.Activity#RESULT_OK */ - public abstract void sendOrderedBroadcast(Intent intent, - String receiverPermission, BroadcastReceiver resultReceiver, - Handler scheduler, int initialCode, String initialData, - Bundle initialExtras); + public abstract void sendOrderedBroadcast(@NonNull Intent intent, + @Nullable String receiverPermission, BroadcastReceiver resultReceiver, + @Nullable Handler scheduler, int initialCode, @Nullable String initialData, + @Nullable Bundle initialExtras); /** * Like {@link #sendOrderedBroadcast(Intent, String, BroadcastReceiver, android.os.Handler, * int, String, android.os.Bundle)}, but also allows specification - * of an assocated app op as per {@link android.app.AppOpsManager}. + * of an associated app op as per {@link android.app.AppOpsManager}. * @hide */ public abstract void sendOrderedBroadcast(Intent intent, @@ -1409,7 +1432,7 @@ public abstract class Context { * @see #sendBroadcast(Intent, String) */ public abstract void sendBroadcastAsUser(Intent intent, UserHandle user, - String receiverPermission); + @Nullable String receiverPermission); /** * Version of @@ -1442,8 +1465,9 @@ public abstract class Context { * @see #sendOrderedBroadcast(Intent, String, BroadcastReceiver, Handler, int, String, Bundle) */ public abstract void sendOrderedBroadcastAsUser(Intent intent, UserHandle user, - String receiverPermission, BroadcastReceiver resultReceiver, Handler scheduler, - int initialCode, String initialData, Bundle initialExtras); + @Nullable String receiverPermission, BroadcastReceiver resultReceiver, + @Nullable Handler scheduler, int initialCode, @Nullable String initialData, + @Nullable Bundle initialExtras); /** * Perform a {@link #sendBroadcast(Intent)} that is "sticky," meaning the @@ -1508,8 +1532,8 @@ public abstract class Context { */ public abstract void sendStickyOrderedBroadcast(Intent intent, BroadcastReceiver resultReceiver, - Handler scheduler, int initialCode, String initialData, - Bundle initialExtras); + @Nullable Handler scheduler, int initialCode, @Nullable String initialData, + @Nullable Bundle initialExtras); /** * Remove the data previously sent with {@link #sendStickyBroadcast}, @@ -1569,8 +1593,8 @@ public abstract class Context { */ public abstract void sendStickyOrderedBroadcastAsUser(Intent intent, UserHandle user, BroadcastReceiver resultReceiver, - Handler scheduler, int initialCode, String initialData, - Bundle initialExtras); + @Nullable Handler scheduler, int initialCode, @Nullable String initialData, + @Nullable Bundle initialExtras); /** * Version of {@link #removeStickyBroadcast(Intent)} that allows you to specify the @@ -1637,7 +1661,8 @@ public abstract class Context { * @see #sendBroadcast * @see #unregisterReceiver */ - public abstract Intent registerReceiver(BroadcastReceiver receiver, + @Nullable + public abstract Intent registerReceiver(@Nullable BroadcastReceiver receiver, IntentFilter filter); /** @@ -1671,8 +1696,10 @@ public abstract class Context { * @see #sendBroadcast * @see #unregisterReceiver */ + @Nullable public abstract Intent registerReceiver(BroadcastReceiver receiver, - IntentFilter filter, String broadcastPermission, Handler scheduler); + IntentFilter filter, @Nullable String broadcastPermission, + @Nullable Handler scheduler); /** * @hide @@ -1698,9 +1725,10 @@ public abstract class Context { * @see #sendBroadcast * @see #unregisterReceiver */ + @Nullable public abstract Intent registerReceiverAsUser(BroadcastReceiver receiver, - UserHandle user, IntentFilter filter, String broadcastPermission, - Handler scheduler); + UserHandle user, IntentFilter filter, @Nullable String broadcastPermission, + @Nullable Handler scheduler); /** * Unregister a previously registered BroadcastReceiver. <em>All</em> @@ -1759,6 +1787,7 @@ public abstract class Context { * @see #stopService * @see #bindService */ + @Nullable public abstract ComponentName startService(Intent service); /** @@ -1846,8 +1875,8 @@ public abstract class Context { * @see #BIND_DEBUG_UNBIND * @see #BIND_NOT_FOREGROUND */ - public abstract boolean bindService(Intent service, ServiceConnection conn, - int flags); + public abstract boolean bindService(Intent service, @NonNull ServiceConnection conn, + @BindServiceFlags int flags); /** * Same as {@link #bindService(Intent, ServiceConnection, int)}, but with an explicit userHandle @@ -1868,7 +1897,7 @@ public abstract class Context { * * @see #bindService */ - public abstract void unbindService(ServiceConnection conn); + public abstract void unbindService(@NonNull ServiceConnection conn); /** * Start executing an {@link android.app.Instrumentation} class. The given @@ -1893,8 +1922,64 @@ public abstract class Context { * @return {@code true} if the instrumentation was successfully started, * else {@code false} if it could not be found. */ - public abstract boolean startInstrumentation(ComponentName className, - String profileFile, Bundle arguments); + public abstract boolean startInstrumentation(@NonNull ComponentName className, + @Nullable String profileFile, @Nullable Bundle arguments); + + /** @hide */ + @StringDef({ + POWER_SERVICE, + WINDOW_SERVICE, + LAYOUT_INFLATER_SERVICE, + ACCOUNT_SERVICE, + ACTIVITY_SERVICE, + ALARM_SERVICE, + NOTIFICATION_SERVICE, + ACCESSIBILITY_SERVICE, + CAPTIONING_SERVICE, + KEYGUARD_SERVICE, + LOCATION_SERVICE, + //@hide: COUNTRY_DETECTOR, + SEARCH_SERVICE, + SENSOR_SERVICE, + STORAGE_SERVICE, + WALLPAPER_SERVICE, + VIBRATOR_SERVICE, + //@hide: STATUS_BAR_SERVICE, + CONNECTIVITY_SERVICE, + //@hide: UPDATE_LOCK_SERVICE, + //@hide: NETWORKMANAGEMENT_SERVICE, + //@hide: NETWORK_STATS_SERVICE, + //@hide: NETWORK_POLICY_SERVICE, + WIFI_SERVICE, + WIFI_P2P_SERVICE, + NSD_SERVICE, + AUDIO_SERVICE, + MEDIA_ROUTER_SERVICE, + TELEPHONY_SERVICE, + CLIPBOARD_SERVICE, + INPUT_METHOD_SERVICE, + TEXT_SERVICES_MANAGER_SERVICE, + //@hide: APPWIDGET_SERVICE, + //@hide: BACKUP_SERVICE, + DROPBOX_SERVICE, + DEVICE_POLICY_SERVICE, + UI_MODE_SERVICE, + DOWNLOAD_SERVICE, + NFC_SERVICE, + BLUETOOTH_SERVICE, + //@hide: SIP_SERVICE, + USB_SERVICE, + //@hide: SERIAL_SERVICE, + INPUT_SERVICE, + DISPLAY_SERVICE, + //@hide: SCHEDULING_POLICY_SERVICE, + USER_SERVICE, + //@hide: APP_OPS_SERVICE + CAMERA_SERVICE, + PRINT_SERVICE + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ServiceName {} /** * Return the handle to a system-level service by name. The class of the @@ -1995,7 +2080,7 @@ public abstract class Context { * @see #DOWNLOAD_SERVICE * @see android.app.DownloadManager */ - public abstract Object getSystemService(String name); + public abstract Object getSystemService(@ServiceName @NonNull String name); /** * Use with {@link #getSystemService} to retrieve a @@ -2431,7 +2516,6 @@ public abstract class Context { * * @see #getSystemService * @see android.hardware.camera2.CameraManager - * @hide */ public static final String CAMERA_SERVICE = "camera"; @@ -2470,7 +2554,8 @@ public abstract class Context { * @see PackageManager#checkPermission(String, String) * @see #checkCallingPermission */ - public abstract int checkPermission(String permission, int pid, int uid); + @PackageManager.PermissionResult + public abstract int checkPermission(@NonNull String permission, int pid, int uid); /** * Determine whether the calling process of an IPC you are handling has been @@ -2493,7 +2578,8 @@ public abstract class Context { * @see #checkPermission * @see #checkCallingOrSelfPermission */ - public abstract int checkCallingPermission(String permission); + @PackageManager.PermissionResult + public abstract int checkCallingPermission(@NonNull String permission); /** * Determine whether the calling process of an IPC <em>or you</em> have been @@ -2511,7 +2597,8 @@ public abstract class Context { * @see #checkPermission * @see #checkCallingPermission */ - public abstract int checkCallingOrSelfPermission(String permission); + @PackageManager.PermissionResult + public abstract int checkCallingOrSelfPermission(@NonNull String permission); /** * If the given permission is not allowed for a particular process @@ -2526,7 +2613,7 @@ public abstract class Context { * @see #checkPermission(String, int, int) */ public abstract void enforcePermission( - String permission, int pid, int uid, String message); + @NonNull String permission, int pid, int uid, @Nullable String message); /** * If the calling process of an IPC you are handling has not been @@ -2547,7 +2634,7 @@ public abstract class Context { * @see #checkCallingPermission(String) */ public abstract void enforceCallingPermission( - String permission, String message); + @NonNull String permission, @Nullable String message); /** * If neither you nor the calling process of an IPC you are @@ -2563,7 +2650,7 @@ public abstract class Context { * @see #checkCallingOrSelfPermission(String) */ public abstract void enforceCallingOrSelfPermission( - String permission, String message); + @NonNull String permission, @Nullable String message); /** * Grant permission to access a specific Uri to another package, regardless @@ -2599,7 +2686,7 @@ public abstract class Context { * @see #revokeUriPermission */ public abstract void grantUriPermission(String toPackage, Uri uri, - int modeFlags); + @Intent.GrantUriMode int modeFlags); /** * Remove all permissions to access a particular content provider Uri @@ -2618,7 +2705,7 @@ public abstract class Context { * * @see #grantUriPermission */ - public abstract void revokeUriPermission(Uri uri, int modeFlags); + public abstract void revokeUriPermission(Uri uri, @Intent.GrantUriMode int modeFlags); /** * Determine whether a particular process and user ID has been granted @@ -2641,7 +2728,8 @@ public abstract class Context { * * @see #checkCallingUriPermission */ - public abstract int checkUriPermission(Uri uri, int pid, int uid, int modeFlags); + public abstract int checkUriPermission(Uri uri, int pid, int uid, + @Intent.GrantUriMode int modeFlags); /** * Determine whether the calling process and user ID has been @@ -2664,7 +2752,7 @@ public abstract class Context { * * @see #checkUriPermission(Uri, int, int, int) */ - public abstract int checkCallingUriPermission(Uri uri, int modeFlags); + public abstract int checkCallingUriPermission(Uri uri, @Intent.GrantUriMode int modeFlags); /** * Determine whether the calling process of an IPC <em>or you</em> has been granted @@ -2683,7 +2771,8 @@ public abstract class Context { * * @see #checkCallingUriPermission */ - public abstract int checkCallingOrSelfUriPermission(Uri uri, int modeFlags); + public abstract int checkCallingOrSelfUriPermission(Uri uri, + @Intent.GrantUriMode int modeFlags); /** * Check both a Uri and normal permission. This allows you to perform @@ -2695,7 +2784,7 @@ public abstract class Context { * @param readPermission The permission that provides overall read access, * or null to not do this check. * @param writePermission The permission that provides overall write - * acess, or null to not do this check. + * access, or null to not do this check. * @param pid The process ID being checked against. Must be > 0. * @param uid The user ID being checked against. A uid of 0 is the root * user, which will pass every permission check. @@ -2707,8 +2796,9 @@ 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. */ - public abstract int checkUriPermission(Uri uri, String readPermission, - String writePermission, int pid, int uid, int modeFlags); + public abstract int checkUriPermission(@Nullable Uri uri, @Nullable String readPermission, + @Nullable String writePermission, int pid, int uid, + @Intent.GrantUriMode int modeFlags); /** * If a particular process and user ID has not been granted @@ -2730,7 +2820,7 @@ public abstract class Context { * @see #checkUriPermission(Uri, int, int, int) */ public abstract void enforceUriPermission( - Uri uri, int pid, int uid, int modeFlags, String message); + Uri uri, int pid, int uid, @Intent.GrantUriMode int modeFlags, String message); /** * If the calling process and user ID has not been granted @@ -2752,7 +2842,7 @@ public abstract class Context { * @see #checkCallingUriPermission(Uri, int) */ public abstract void enforceCallingUriPermission( - Uri uri, int modeFlags, String message); + Uri uri, @Intent.GrantUriMode int modeFlags, String message); /** * If the calling process of an IPC <em>or you</em> has not been @@ -2771,7 +2861,7 @@ public abstract class Context { * @see #checkCallingOrSelfUriPermission(Uri, int) */ public abstract void enforceCallingOrSelfUriPermission( - Uri uri, int modeFlags, String message); + Uri uri, @Intent.GrantUriMode int modeFlags, String message); /** * Enforce both a Uri and normal permission. This allows you to perform @@ -2783,7 +2873,7 @@ public abstract class Context { * @param readPermission The permission that provides overall read access, * or null to not do this check. * @param writePermission The permission that provides overall write - * acess, or null to not do this check. + * access, or null to not do this check. * @param pid The process ID being checked against. Must be > 0. * @param uid The user ID being checked against. A uid of 0 is the root * user, which will pass every permission check. @@ -2795,8 +2885,15 @@ public abstract class Context { * @see #checkUriPermission(Uri, String, String, int, int, int) */ public abstract void enforceUriPermission( - Uri uri, String readPermission, String writePermission, - int pid, int uid, int modeFlags, String message); + @Nullable Uri uri, @Nullable String readPermission, + @Nullable String writePermission, int pid, int uid, @Intent.GrantUriMode int modeFlags, + @Nullable String message); + + /** @hide */ + @IntDef(flag = true, + value = {CONTEXT_INCLUDE_CODE, CONTEXT_IGNORE_SECURITY, CONTEXT_RESTRICTED}) + @Retention(RetentionPolicy.SOURCE) + public @interface CreatePackageOptions {} /** * Flag for use with {@link #createPackageContext}: include the application @@ -2854,7 +2951,7 @@ public abstract class Context { * the given package name. */ public abstract Context createPackageContext(String packageName, - int flags) throws PackageManager.NameNotFoundException; + @CreatePackageOptions int flags) throws PackageManager.NameNotFoundException; /** * Similar to {@link #createPackageContext(String, int)}, but with a @@ -2890,7 +2987,8 @@ public abstract class Context { * * @return A {@link Context} with the given configuration override. */ - public abstract Context createConfigurationContext(Configuration overrideConfiguration); + public abstract Context createConfigurationContext( + @NonNull Configuration overrideConfiguration); /** * Return a new Context object for the current Context but whose resources @@ -2910,7 +3008,7 @@ public abstract class Context { * * @return A {@link Context} for the display. */ - public abstract Context createDisplayContext(Display display); + public abstract Context createDisplayContext(@NonNull Display display); /** * Gets the display adjustments holder for this context. This information diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java index a708dad..93f6cdf 100644 --- a/core/java/android/content/ContextWrapper.java +++ b/core/java/android/content/ContextWrapper.java @@ -16,9 +16,6 @@ package android.content; -import android.app.Activity; -import android.app.ActivityManagerNative; -import android.app.LoadedApk; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.AssetManager; @@ -33,7 +30,6 @@ import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.os.RemoteException; import android.os.UserHandle; import android.view.DisplayAdjustments; import android.view.Display; diff --git a/core/java/android/content/CursorLoader.java b/core/java/android/content/CursorLoader.java index 5d7d677..c78871c 100644 --- a/core/java/android/content/CursorLoader.java +++ b/core/java/android/content/CursorLoader.java @@ -16,7 +16,6 @@ package android.content; -import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; import android.os.CancellationSignal; diff --git a/core/java/android/content/Entity.java b/core/java/android/content/Entity.java index 7842de0..607cb3f 100644 --- a/core/java/android/content/Entity.java +++ b/core/java/android/content/Entity.java @@ -16,10 +16,7 @@ package android.content; -import android.os.Parcelable; -import android.os.Parcel; import android.net.Uri; -import android.util.Log; import java.util.ArrayList; diff --git a/core/java/android/content/IContentService.aidl b/core/java/android/content/IContentService.aidl index 9ad5a19..73a76e8 100644 --- a/core/java/android/content/IContentService.aidl +++ b/core/java/android/content/IContentService.aidl @@ -17,6 +17,7 @@ package android.content; import android.accounts.Account; +import android.content.ComponentName; import android.content.SyncInfo; import android.content.ISyncStatusObserver; import android.content.SyncAdapterType; @@ -55,8 +56,14 @@ interface IContentService { int userHandle); void requestSync(in Account account, String authority, in Bundle extras); + /** + * Start a sync given a request. + */ void sync(in SyncRequest request); - void cancelSync(in Account account, String authority); + void cancelSync(in Account account, String authority, in ComponentName cname); + + /** Cancel a sync, providing information about the sync to be cancelled. */ + void cancelRequest(in SyncRequest request); /** * Check if the provider should be synced when a network tickle is received @@ -74,12 +81,14 @@ interface IContentService { void setSyncAutomatically(in Account account, String providerName, boolean sync); /** - * Get the frequency of the periodic poll, if any. - * @param providerName the provider whose setting we are querying - * @return the frequency of the periodic sync in seconds. If 0 then no periodic syncs - * will take place. + * Get a list of periodic operations for a specified authority, or service. + * @param account account for authority, must be null if cname is non-null. + * @param providerName name of provider, must be null if cname is non-null. + * @param cname component to identify sync service, must be null if account/providerName are + * non-null. */ - List<PeriodicSync> getPeriodicSyncs(in Account account, String providerName); + List<PeriodicSync> getPeriodicSyncs(in Account account, String providerName, + in ComponentName cname); /** * Set whether or not the provider is to be synced on a periodic basis. @@ -112,15 +121,22 @@ interface IContentService { */ void setIsSyncable(in Account account, String providerName, int syncable); - void setMasterSyncAutomatically(boolean flag); - - boolean getMasterSyncAutomatically(); + /** + * Corresponds roughly to setIsSyncable(String account, String provider) for syncs that bind + * to a SyncService. + */ + void setServiceActive(in ComponentName cname, boolean active); /** - * Returns true if there is currently a sync operation for the given - * account or authority in the pending list, or actively being processed. + * Corresponds roughly to getIsSyncable(String account, String provider) for syncs that bind + * to a SyncService. + * @return 0 if this SyncService is not enabled, 1 if enabled, <0 if unknown. */ - boolean isSyncActive(in Account account, String authority); + boolean isServiceActive(in ComponentName cname); + + void setMasterSyncAutomatically(boolean flag); + + boolean getMasterSyncAutomatically(); List<SyncInfo> getCurrentSyncs(); @@ -131,17 +147,33 @@ interface IContentService { SyncAdapterType[] getSyncAdapterTypes(); /** + * Returns true if there is currently a operation for the given account/authority or service + * actively being processed. + * @param account account for authority, must be null if cname is non-null. + * @param providerName name of provider, must be null if cname is non-null. + * @param cname component to identify sync service, must be null if account/providerName are + * non-null. + */ + boolean isSyncActive(in Account account, String authority, in ComponentName cname); + + /** * Returns the status that matches the authority. If there are multiples accounts for * the authority, the one with the latest "lastSuccessTime" status is returned. - * @param authority the authority whose row should be selected - * @return the SyncStatusInfo for the authority, or null if none exists + * @param account account for authority, must be null if cname is non-null. + * @param providerName name of provider, must be null if cname is non-null. + * @param cname component to identify sync service, must be null if account/providerName are + * non-null. */ - SyncStatusInfo getSyncStatus(in Account account, String authority); + SyncStatusInfo getSyncStatus(in Account account, String authority, in ComponentName cname); /** * Return true if the pending status is true of any matching authorities. + * @param account account for authority, must be null if cname is non-null. + * @param providerName name of provider, must be null if cname is non-null. + * @param cname component to identify sync service, must be null if account/providerName are + * non-null. */ - boolean isSyncPending(in Account account, String authority); + boolean isSyncPending(in Account account, String authority, in ComponentName cname); void addStatusChangeListener(int mask, ISyncStatusObserver callback); diff --git a/core/java/android/content/IAnonymousSyncAdapter.aidl b/core/java/android/content/ISyncServiceAdapter.aidl index a80cea3..d419307 100644 --- a/core/java/android/content/IAnonymousSyncAdapter.aidl +++ b/core/java/android/content/ISyncServiceAdapter.aidl @@ -24,7 +24,7 @@ import android.content.ISyncContext; * Provider specified). See {@link android.content.AbstractThreadedSyncAdapter}. * {@hide} */ -oneway interface IAnonymousSyncAdapter { +oneway interface ISyncServiceAdapter { /** * Initiate a sync. SyncAdapter-specific parameters may be specified in diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index a289649..85b3141 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -21,6 +21,7 @@ import android.util.ArraySet; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import android.annotation.IntDef; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.content.pm.ActivityInfo; @@ -45,6 +46,8 @@ import com.android.internal.util.XmlUtils; import java.io.IOException; import java.io.Serializable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; @@ -3335,6 +3338,12 @@ public class Intent implements Parcelable, Cloneable { // --------------------------------------------------------------------- // Intent flags (see mFlags variable). + /** @hide */ + @IntDef(flag = true, + value = {FLAG_GRANT_READ_URI_PERMISSION, FLAG_GRANT_WRITE_URI_PERMISSION}) + @Retention(RetentionPolicy.SOURCE) + public @interface GrantUriMode {} + /** * If set, the recipient of this Intent will be granted permission to * perform read operations on the URI in the Intent's data and any URIs @@ -6374,6 +6383,21 @@ public class Intent implements Parcelable, Cloneable { } } + /** @hide */ + @IntDef(flag = true, + value = { + FILL_IN_ACTION, + FILL_IN_DATA, + FILL_IN_CATEGORIES, + FILL_IN_COMPONENT, + FILL_IN_PACKAGE, + FILL_IN_SOURCE_BOUNDS, + FILL_IN_SELECTOR, + FILL_IN_CLIP_DATA + }) + @Retention(RetentionPolicy.SOURCE) + public @interface FillInFlags {} + /** * Use with {@link #fillIn} to allow the current action value to be * overwritten, even if it is already set. @@ -6467,10 +6491,12 @@ public class Intent implements Parcelable, Cloneable { * * @return Returns a bit mask of {@link #FILL_IN_ACTION}, * {@link #FILL_IN_DATA}, {@link #FILL_IN_CATEGORIES}, {@link #FILL_IN_PACKAGE}, - * {@link #FILL_IN_COMPONENT}, {@link #FILL_IN_SOURCE_BOUNDS}, and - * {@link #FILL_IN_SELECTOR} indicating which fields were changed. + * {@link #FILL_IN_COMPONENT}, {@link #FILL_IN_SOURCE_BOUNDS}, + * {@link #FILL_IN_SELECTOR} and {@link #FILL_IN_CLIP_DATA indicating which fields were + * changed. */ - public int fillIn(Intent other, int flags) { + @FillInFlags + public int fillIn(Intent other, @FillInFlags int flags) { int changes = 0; if (other.mAction != null && (mAction == null || (flags&FILL_IN_ACTION) != 0)) { diff --git a/core/java/android/content/Loader.java b/core/java/android/content/Loader.java index 911e49c..f3828b0 100644 --- a/core/java/android/content/Loader.java +++ b/core/java/android/content/Loader.java @@ -413,7 +413,7 @@ public class Loader<D> { * {@link #onReset()} happens. You can retrieve the current abandoned * state with {@link #isAbandoned}. */ - protected void onAbandon() { + protected void onAbandon() { } /** diff --git a/core/java/android/content/PeriodicSync.java b/core/java/android/content/PeriodicSync.java index b586eec..836c6f8 100644 --- a/core/java/android/content/PeriodicSync.java +++ b/core/java/android/content/PeriodicSync.java @@ -29,13 +29,17 @@ public class PeriodicSync implements Parcelable { public final Account account; /** The authority of the sync. Can be null. */ public final String authority; + /** The service for syncing, if this is an anonymous sync. Can be null.*/ + public final ComponentName service; /** Any extras that parameters that are to be passed to the sync adapter. */ public final Bundle extras; /** How frequently the sync should be scheduled, in seconds. Kept around for API purposes. */ public final long period; + /** Whether this periodic sync runs on a {@link SyncService}. */ + public final boolean isService; /** - * {@hide} * How much flexibility can be taken in scheduling the sync, in seconds. + * {@hide} */ public final long flexTime; @@ -48,44 +52,74 @@ public class PeriodicSync implements Parcelable { public PeriodicSync(Account account, String authority, Bundle extras, long periodInSeconds) { this.account = account; this.authority = authority; + this.service = null; + this.isService = false; if (extras == null) { this.extras = new Bundle(); } else { this.extras = new Bundle(extras); } this.period = periodInSeconds; - // Initialise to a sane value. + // Old API uses default flex time. No-one should be using this ctor anyway. this.flexTime = 0L; } /** - * {@hide} * Create a copy of a periodic sync. + * {@hide} */ public PeriodicSync(PeriodicSync other) { this.account = other.account; this.authority = other.authority; + this.service = other.service; + this.isService = other.isService; this.extras = new Bundle(other.extras); this.period = other.period; this.flexTime = other.flexTime; } /** - * {@hide} * A PeriodicSync for a sync with a specified provider. + * {@hide} */ public PeriodicSync(Account account, String authority, Bundle extras, long period, long flexTime) { this.account = account; this.authority = authority; + this.service = null; + this.isService = false; + this.extras = new Bundle(extras); + this.period = period; + this.flexTime = flexTime; + } + + /** + * A PeriodicSync for a sync with a specified SyncService. + * {@hide} + */ + public PeriodicSync(ComponentName service, Bundle extras, + long period, + long flexTime) { + this.account = null; + this.authority = null; + this.service = service; + this.isService = true; this.extras = new Bundle(extras); this.period = period; this.flexTime = flexTime; } private PeriodicSync(Parcel in) { - this.account = in.readParcelable(null); - this.authority = in.readString(); + this.isService = (in.readInt() != 0); + if (this.isService) { + this.service = in.readParcelable(null); + this.account = null; + this.authority = null; + } else { + this.account = in.readParcelable(null); + this.authority = in.readString(); + this.service = null; + } this.extras = in.readBundle(); this.period = in.readLong(); this.flexTime = in.readLong(); @@ -98,8 +132,13 @@ public class PeriodicSync implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeParcelable(account, flags); - dest.writeString(authority); + dest.writeInt(isService ? 1 : 0); + if (account == null && authority == null) { + dest.writeParcelable(service, flags); + } else { + dest.writeParcelable(account, flags); + dest.writeString(authority); + } dest.writeBundle(extras); dest.writeLong(period); dest.writeLong(flexTime); @@ -126,14 +165,24 @@ public class PeriodicSync implements Parcelable { return false; } final PeriodicSync other = (PeriodicSync) o; - return account.equals(other.account) - && authority.equals(other.authority) + if (this.isService != other.isService) { + return false; + } + boolean equal = false; + if (this.isService) { + equal = service.equals(other.service); + } else { + equal = account.equals(other.account) + && authority.equals(other.authority); + } + return equal && period == other.period && syncExtrasEquals(extras, other.extras); } /** - * Periodic sync extra comparison function. + * Periodic sync extra comparison function. Duplicated from + * {@link com.android.server.content.SyncManager#syncExtrasEquals(Bundle b1, Bundle b2)} * {@hide} */ public static boolean syncExtrasEquals(Bundle b1, Bundle b2) { @@ -158,6 +207,7 @@ public class PeriodicSync implements Parcelable { public String toString() { return "account: " + account + ", authority: " + authority + + ", service: " + service + ". period: " + period + "s " + ", flex: " + flexTime; } diff --git a/core/java/android/content/RestrictionEntry.java b/core/java/android/content/RestrictionEntry.java index 283a097..3ff53bf 100644 --- a/core/java/android/content/RestrictionEntry.java +++ b/core/java/android/content/RestrictionEntry.java @@ -19,8 +19,6 @@ package android.content; import android.os.Parcel; import android.os.Parcelable; -import java.lang.annotation.Inherited; - /** * Applications can expose restrictions for a restricted user on a * multiuser device. The administrator can configure these restrictions that will then be diff --git a/core/java/android/content/SyncInfo.java b/core/java/android/content/SyncInfo.java index cffc653..146dd99 100644 --- a/core/java/android/content/SyncInfo.java +++ b/core/java/android/content/SyncInfo.java @@ -19,7 +19,6 @@ package android.content; import android.accounts.Account; import android.os.Parcel; import android.os.Parcelable; -import android.os.Parcelable.Creator; /** * Information about the sync operation that is currently underway. @@ -29,16 +28,24 @@ public class SyncInfo implements Parcelable { public final int authorityId; /** - * The {@link Account} that is currently being synced. + * The {@link Account} that is currently being synced. Will be null if this sync is running via + * a {@link SyncService}. */ public final Account account; /** - * The authority of the provider that is currently being synced. + * The authority of the provider that is currently being synced. Will be null if this sync + * is running via a {@link SyncService}. */ public final String authority; /** + * The {@link SyncService} that is targeted by this operation. Null if this sync is running via + * a {@link AbstractThreadedSyncAdapter}. + */ + public final ComponentName service; + + /** * The start time of the current sync operation in milliseconds since boot. * This is represented in elapsed real time. * See {@link android.os.SystemClock#elapsedRealtime()}. @@ -46,12 +53,13 @@ public class SyncInfo implements Parcelable { public final long startTime; /** @hide */ - public SyncInfo(int authorityId, Account account, String authority, + public SyncInfo(int authorityId, Account account, String authority, ComponentName service, long startTime) { this.authorityId = authorityId; this.account = account; this.authority = authority; this.startTime = startTime; + this.service = service; } /** @hide */ @@ -60,6 +68,7 @@ public class SyncInfo implements Parcelable { this.account = new Account(other.account.name, other.account.type); this.authority = other.authority; this.startTime = other.startTime; + this.service = other.service; } /** @hide */ @@ -70,17 +79,20 @@ public class SyncInfo implements Parcelable { /** @hide */ public void writeToParcel(Parcel parcel, int flags) { parcel.writeInt(authorityId); - account.writeToParcel(parcel, 0); + parcel.writeParcelable(account, flags); parcel.writeString(authority); parcel.writeLong(startTime); + parcel.writeParcelable(service, flags); + } /** @hide */ SyncInfo(Parcel parcel) { authorityId = parcel.readInt(); - account = new Account(parcel); + account = parcel.readParcelable(Account.class.getClassLoader()); authority = parcel.readString(); startTime = parcel.readLong(); + service = parcel.readParcelable(ComponentName.class.getClassLoader()); } /** @hide */ diff --git a/core/java/android/content/SyncRequest.java b/core/java/android/content/SyncRequest.java index 6ca283d..a9a62a7 100644 --- a/core/java/android/content/SyncRequest.java +++ b/core/java/android/content/SyncRequest.java @@ -23,15 +23,15 @@ import android.os.Parcelable; public class SyncRequest implements Parcelable { private static final String TAG = "SyncRequest"; - /** Account to pass to the sync adapter. May be null. */ + /** Account to pass to the sync adapter. Can be null. */ private final Account mAccountToSync; /** Authority string that corresponds to a ContentProvider. */ private final String mAuthority; - /** Sync service identifier. May be null.*/ + /** {@link SyncService} identifier. */ private final ComponentName mComponentInfo; /** Bundle containing user info as well as sync settings. */ private final Bundle mExtras; - /** Disallow this sync request on metered networks. */ + /** Don't allow this sync request on metered networks. */ private final boolean mDisallowMetered; /** * Anticipated upload size in bytes. @@ -69,18 +69,14 @@ public class SyncRequest implements Parcelable { return mIsPeriodic; } - /** - * {@hide} - * @return whether this is an expedited sync. - */ public boolean isExpedited() { return mIsExpedited; } /** * {@hide} - * @return true if this sync uses an account/authority pair, or false if this sync is bound to - * a Sync Service. + * @return true if this sync uses an account/authority pair, or false if + * this is an anonymous sync bound to an @link AnonymousSyncService. */ public boolean hasAuthority() { return mIsAuthority; @@ -88,34 +84,51 @@ public class SyncRequest implements Parcelable { /** * {@hide} + * * @return account object for this sync. - * @throws IllegalArgumentException if this function is called for a request that does not - * specify an account/provider authority. + * @throws IllegalArgumentException if this function is called for a request that targets a + * sync service. */ public Account getAccount() { if (!hasAuthority()) { - throw new IllegalArgumentException("Cannot getAccount() for a sync that does not" - + "specify an authority."); + throw new IllegalArgumentException("Cannot getAccount() for a sync that targets a sync" + + "service."); } return mAccountToSync; } /** * {@hide} + * * @return provider for this sync. - * @throws IllegalArgumentException if this function is called for a request that does not - * specify an account/provider authority. + * @throws IllegalArgumentException if this function is called for a request that targets a + * sync service. */ public String getProvider() { if (!hasAuthority()) { - throw new IllegalArgumentException("Cannot getProvider() for a sync that does not" - + "specify a provider."); + throw new IllegalArgumentException("Cannot getProvider() for a sync that targets a" + + "sync service."); } return mAuthority; } /** * {@hide} + * Throws a runtime IllegalArgumentException if this function is called for a + * SyncRequest that is bound to an account/provider. + * + * @return ComponentName for the service that this sync will bind to. + */ + public ComponentName getService() { + if (hasAuthority()) { + throw new IllegalArgumentException( + "Cannot getAnonymousService() for a sync that has specified a provider."); + } + return mComponentInfo; + } + + /** + * {@hide} * Retrieve bundle for this SyncRequest. Will not be null. */ public Bundle getBundle() { @@ -129,7 +142,6 @@ public class SyncRequest implements Parcelable { public long getSyncFlexTime() { return mSyncFlexTimeSecs; } - /** * {@hide} * @return the last point in time at which this sync must scheduled. @@ -216,7 +228,7 @@ public class SyncRequest implements Parcelable { } /** - * Builder class for a {@link SyncRequest}. As you build your SyncRequest this class will also + * Builder class for a @link SyncRequest. As you build your SyncRequest this class will also * perform validation. */ public static class Builder { @@ -232,9 +244,12 @@ public class SyncRequest implements Parcelable { private static final int SYNC_TARGET_SERVICE = 1; /** Specify that this is a sync with a provider. */ private static final int SYNC_TARGET_ADAPTER = 2; - /** Earliest point of displacement into the future at which this sync can occur. */ + /** + * Earliest point of displacement into the future at which this sync can + * occur. + */ private long mSyncFlexTimeSecs; - /** Latest point of displacement into the future at which this sync must occur. */ + /** Displacement into the future at which this sync must occur. */ private long mSyncRunTimeSecs; /** * Sync configuration information - custom user data explicitly provided by the developer. @@ -283,8 +298,9 @@ public class SyncRequest implements Parcelable { private boolean mExpedited; /** - * The sync component that contains the sync logic if this is a provider-less sync, - * otherwise null. + * The {@link SyncService} component that + * contains the sync logic if this is a provider-less sync, otherwise + * null. */ private ComponentName mComponentName; /** @@ -320,11 +336,15 @@ public class SyncRequest implements Parcelable { /** * Build a periodic sync. Either this or syncOnce() <b>must</b> be called for this builder. - * Syncs are identified by target {@link android.provider}/{@link android.accounts.Account} - * and by the contents of the extras bundle. - * You cannot reuse the same builder for one-time syncs (by calling this function) after - * having specified a periodic sync. If you do, an <code>IllegalArgumentException</code> + * Syncs are identified by target {@link SyncService}/{@link android.provider} and by the + * contents of the extras bundle. + * You cannot reuse the same builder for one-time syncs after having specified a periodic + * sync (by calling this function). If you do, an <code>IllegalArgumentException</code> * will be thrown. + * <p>The bundle for a periodic sync can be queried by applications with the correct + * permissions using + * {@link ContentResolver#getPeriodicSyncs(Account account, String provider)}, so no + * sensitive data should be transferred here. * * Example usage. * @@ -375,7 +395,6 @@ public class SyncRequest implements Parcelable { } /** - * {@hide} * Developer can provide insight into their payload size; optional. -1 specifies unknown, * so that you are not restricted to defining both fields. * @@ -389,20 +408,28 @@ public class SyncRequest implements Parcelable { } /** - * @see android.net.ConnectivityManager#isActiveNetworkMetered() - * @param disallow true to enforce that this transfer not occur on metered networks. - * Default false. + * Will throw an <code>IllegalArgumentException</code> if called and + * {@link #setIgnoreSettings(boolean ignoreSettings)} has already been called. + * @param disallow true to allow this transfer on metered networks. Default false. + * */ public Builder setDisallowMetered(boolean disallow) { + if (mIgnoreSettings && disallow) { + throw new IllegalArgumentException("setDisallowMetered(true) after having" + + "specified that settings are ignored."); + } mDisallowMetered = disallow; return this; } /** - * Specify an authority and account for this transfer. + * Specify an authority and account for this transfer. Cannot be used with + * {@link #setSyncAdapter(ComponentName cname)}. * - * @param authority String identifying which content provider to sync. - * @param account Account to sync. Can be null unless this is a periodic sync. + * @param authority + * @param account Account to sync. Can be null unless this is a periodic + * sync, for which verification by the ContentResolver will + * fail. If a sync is performed without an account, the */ public Builder setSyncAdapter(Account account, String authority) { if (mSyncTarget != SYNC_TARGET_UNKNOWN) { @@ -419,10 +446,26 @@ public class SyncRequest implements Parcelable { } /** - * Optional developer-provided extras handed back in - * {@link AbstractThreadedSyncAdapter#onPerformSync(Account, Bundle, String, - * ContentProviderClient, SyncResult)} occurs. This bundle is copied into the SyncRequest - * returned by {@link #build()}. + * Specify the {@link SyncService} component for this sync. This is not validated until + * sync time so providing an incorrect component name here will not fail. Cannot be used + * with {@link #setSyncAdapter(Account account, String authority)}. + * + * @param cname ComponentName to identify your Anonymous service + */ + public Builder setSyncAdapter(ComponentName cname) { + if (mSyncTarget != SYNC_TARGET_UNKNOWN) { + throw new IllegalArgumentException("Sync target has already been defined."); + } + mSyncTarget = SYNC_TARGET_SERVICE; + mComponentName = cname; + mAccount = null; + mAuthority = null; + return this; + } + + /** + * Developer-provided extras handed back when sync actually occurs. This bundle is copied + * into the SyncRequest returned by {@link #build()}. * * Example: * <pre> @@ -436,7 +479,7 @@ public class SyncRequest implements Parcelable { * Bundle extras = new Bundle(); * extras.setString("data", syncData); * builder.setExtras(extras); - * ContentResolver.sync(builder.build()); // Each sync() request is for a unique sync. + * ContentResolver.sync(builder.build()); // Each sync() request creates a unique sync. * } * </pre> * Only values of the following types may be used in the extras bundle: @@ -477,13 +520,19 @@ public class SyncRequest implements Parcelable { /** * Convenience function for setting {@link ContentResolver#SYNC_EXTRAS_IGNORE_SETTINGS}. * - * A sync can specify that system sync settings be ignored (user has turned sync off). Not - * valid for periodic sync and will throw an <code>IllegalArgumentException</code> in + * Not valid for periodic sync and will throw an <code>IllegalArgumentException</code> in * {@link #build()}. + * <p>Throws <code>IllegalArgumentException</code> if called and + * {@link #setDisallowMetered(boolean)} has been set. + * * * @param ignoreSettings true to ignore the sync automatically settings. Default false. */ public Builder setIgnoreSettings(boolean ignoreSettings) { + if (mDisallowMetered && ignoreSettings) { + throw new IllegalArgumentException("setIgnoreSettings(true) after having specified" + + " sync settings with this builder."); + } mIgnoreSettings = ignoreSettings; return this; } @@ -491,13 +540,13 @@ public class SyncRequest implements Parcelable { /** * Convenience function for setting {@link ContentResolver#SYNC_EXTRAS_IGNORE_BACKOFF}. * - * Force the sync scheduling process to ignore any back-off that was the result of a failed - * sync, as well as to invalidate any {@link SyncResult#delayUntil} value that may have - * been set by the adapter. Successive failures will not honor this flag. Not valid for - * periodic sync and will throw an <code>IllegalArgumentException</code> in - * {@link #build()}. + * Ignoring back-off will force the sync scheduling process to ignore any back-off that was + * the result of a failed sync, as well as to invalidate any {@link SyncResult#delayUntil} + * value that may have been set by the adapter. Successive failures will not honor this + * flag. Not valid for periodic sync and will throw an <code>IllegalArgumentException</code> + * in {@link #build()}. * - * @param ignoreBackoff ignore back-off settings. Default false. + * @param ignoreBackoff ignore back off settings. Default false. */ public Builder setIgnoreBackoff(boolean ignoreBackoff) { mIgnoreBackoff = ignoreBackoff; @@ -507,9 +556,8 @@ public class SyncRequest implements Parcelable { /** * Convenience function for setting {@link ContentResolver#SYNC_EXTRAS_MANUAL}. * - * A manual sync is functionally equivalent to calling {@link #setIgnoreBackoff(boolean)} - * and {@link #setIgnoreSettings(boolean)}. Not valid for periodic sync and will throw an - * <code>IllegalArgumentException</code> in {@link #build()}. + * Not valid for periodic sync and will throw an <code>IllegalArgumentException</code> in + * {@link #build()}. * * @param isManual User-initiated sync or not. Default false. */ @@ -519,7 +567,7 @@ public class SyncRequest implements Parcelable { } /** - * An expedited sync runs immediately and will preempt another non-expedited running sync. + * An expedited sync runs immediately and can preempt other non-expedited running syncs. * * Not valid for periodic sync and will throw an <code>IllegalArgumentException</code> in * {@link #build()}. @@ -532,7 +580,6 @@ public class SyncRequest implements Parcelable { } /** - * {@hide} * @param priority the priority of this request among all requests from the calling app. * Range of [-2,2] similar to how this is done with notifications. */ @@ -552,11 +599,11 @@ public class SyncRequest implements Parcelable { * builder. */ public SyncRequest build() { + // Validate the extras bundle + ContentResolver.validateSyncExtrasBundle(mCustomExtras); if (mCustomExtras == null) { mCustomExtras = new Bundle(); } - // Validate the extras bundle - ContentResolver.validateSyncExtrasBundle(mCustomExtras); // Combine builder extra flags into the config bundle. mSyncConfigExtras = new Bundle(); if (mIgnoreBackoff) { @@ -575,51 +622,33 @@ public class SyncRequest implements Parcelable { mSyncConfigExtras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); } if (mIsManual) { - mSyncConfigExtras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); + mSyncConfigExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true); + mSyncConfigExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true); } mSyncConfigExtras.putLong(ContentResolver.SYNC_EXTRAS_EXPECTED_UPLOAD, mTxBytes); mSyncConfigExtras.putLong(ContentResolver.SYNC_EXTRAS_EXPECTED_DOWNLOAD, mRxBytes); mSyncConfigExtras.putInt(ContentResolver.SYNC_EXTRAS_PRIORITY, mPriority); if (mSyncType == SYNC_TYPE_PERIODIC) { // If this is a periodic sync ensure than invalid extras were not set. - validatePeriodicExtras(mCustomExtras); - validatePeriodicExtras(mSyncConfigExtras); - // Verify that account and provider are not null. - if (mAccount == null) { - throw new IllegalArgumentException("Account must not be null for periodic" - + " sync."); - } - if (mAuthority == null) { - throw new IllegalArgumentException("Authority must not be null for periodic" - + " sync."); + if (ContentResolver.invalidPeriodicExtras(mCustomExtras) || + ContentResolver.invalidPeriodicExtras(mSyncConfigExtras)) { + throw new IllegalArgumentException("Illegal extras were set"); } } else if (mSyncType == SYNC_TYPE_UNKNOWN) { throw new IllegalArgumentException("Must call either syncOnce() or syncPeriodic()"); } + if (mSyncTarget == SYNC_TARGET_SERVICE) { + if (mSyncConfigExtras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false)) { + throw new IllegalArgumentException("Cannot specify an initialisation sync" + + " that targets a service."); + } + } // Ensure that a target for the sync has been set. if (mSyncTarget == SYNC_TARGET_UNKNOWN) { - throw new IllegalArgumentException("Must specify an adapter with " - + "setSyncAdapter(Account, String"); + throw new IllegalArgumentException("Must specify an adapter with one of" + + "setSyncAdapter(ComponentName) or setSyncAdapter(Account, String"); } return new SyncRequest(this); } - - /** - * Helper function to throw an <code>IllegalArgumentException</code> if any illegal - * extras were set for a periodic sync. - * - * @param extras bundle to validate. - */ - private void validatePeriodicExtras(Bundle extras) { - if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false) - || extras.getBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, false) - || extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false) - || extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, false) - || extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false) - || extras.getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false) - || extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false)) { - throw new IllegalArgumentException("Illegal extras were set"); - } - } - } + } } diff --git a/core/java/android/content/SyncService.java b/core/java/android/content/SyncService.java new file mode 100644 index 0000000..4df998c --- /dev/null +++ b/core/java/android/content/SyncService.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import android.app.Service; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Process; +import android.os.Trace; +import android.util.SparseArray; +import android.util.Log; + +import com.android.internal.annotations.GuardedBy; + +/** + * Simplified @link android.content.AbstractThreadedSyncAdapter. Folds that + * behaviour into a service to which the system can bind when requesting an + * anonymous (providerless/accountless) sync. + * <p> + * In order to perform an anonymous sync operation you must extend this service, implementing the + * abstract methods. This service must be declared in the application's manifest as usual. You + * can use this service for other work, however you <b> must not </b> override the onBind() method + * unless you know what you're doing, which limits the usefulness of this service for other work. + * <p>A {@link SyncService} can either be active or inactive. Different to an + * {@link AbstractThreadedSyncAdapter}, there is no + * {@link ContentResolver#setSyncAutomatically(android.accounts.Account account, String provider, boolean sync)}, + * as well as no concept of initialisation (you can handle your own if needed). + * + * <pre> + * <service android:name=".MySyncService"/> + * </pre> + * Like @link android.content.AbstractThreadedSyncAdapter this service supports + * multiple syncs at the same time. Each incoming startSync() with a unique tag + * will spawn a thread to do the work of that sync. If startSync() is called + * with a tag that already exists, a SyncResult.ALREADY_IN_PROGRESS is returned. + * Remember that your service will spawn multiple threads if you schedule multiple syncs + * at once, so if you mutate local objects you must ensure synchronization. + */ +public abstract class SyncService extends Service { + private static final String TAG = "SyncService"; + + private final SyncAdapterImpl mSyncAdapter = new SyncAdapterImpl(); + + /** Keep track of on-going syncs, keyed by bundle. */ + @GuardedBy("mSyncThreadLock") + private final SparseArray<SyncThread> + mSyncThreads = new SparseArray<SyncThread>(); + /** Lock object for accessing the SyncThreads HashMap. */ + private final Object mSyncThreadLock = new Object(); + /** + * Default key for if this sync service does not support parallel operations. Currently not + * sure if null keys will make it into the ArrayMap for KLP, so keeping our default for now. + */ + private static final int KEY_DEFAULT = 0; + /** Identifier for this sync service. */ + private ComponentName mServiceComponent; + + /** {@hide} */ + public IBinder onBind(Intent intent) { + mServiceComponent = new ComponentName(this, getClass()); + return mSyncAdapter.asBinder(); + } + + /** {@hide} */ + private class SyncAdapterImpl extends ISyncServiceAdapter.Stub { + @Override + public void startSync(ISyncContext syncContext, Bundle extras) { + // Wrap the provided Sync Context because it may go away by the time + // we call it. + final SyncContext syncContextClient = new SyncContext(syncContext); + boolean alreadyInProgress = false; + final int extrasAsKey = extrasToKey(extras); + synchronized (mSyncThreadLock) { + if (mSyncThreads.get(extrasAsKey) == null) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "starting sync for : " + mServiceComponent); + } + // Start sync. + SyncThread syncThread = new SyncThread(syncContextClient, extras); + mSyncThreads.put(extrasAsKey, syncThread); + syncThread.start(); + } else { + // Don't want to call back to SyncManager while still + // holding lock. + alreadyInProgress = true; + } + } + if (alreadyInProgress) { + syncContextClient.onFinished(SyncResult.ALREADY_IN_PROGRESS); + } + } + + /** + * Used by the SM to cancel a specific sync using the + * com.android.server.content.SyncManager.ActiveSyncContext as a handle. + */ + @Override + public void cancelSync(ISyncContext syncContext) { + SyncThread runningSync = null; + synchronized (mSyncThreadLock) { + for (int i = 0; i < mSyncThreads.size(); i++) { + SyncThread thread = mSyncThreads.valueAt(i); + if (thread.mSyncContext.getSyncContextBinder() == syncContext.asBinder()) { + runningSync = thread; + break; + } + } + } + if (runningSync != null) { + runningSync.interrupt(); + } + } + } + + /** + * + * @param extras Bundle for which to compute hash + * @return an integer hash that is equal to that of another bundle if they both contain the + * same key -> value mappings, however, not necessarily in order. + * Based on the toString() representation of the value mapped. + */ + private int extrasToKey(Bundle extras) { + int hash = KEY_DEFAULT; // Empty bundle, or no parallel operations enabled. + if (parallelSyncsEnabled()) { + for (String key : extras.keySet()) { + String mapping = key + " " + extras.get(key).toString(); + hash += mapping.hashCode(); + } + } + return hash; + } + + /** + * {@hide} + * Similar to {@link android.content.AbstractThreadedSyncAdapter.SyncThread}. However while + * the ATSA considers an already in-progress sync to be if the account provided is currently + * syncing, this anonymous sync has no notion of account and considers a sync unique if the + * provided bundle is different. + */ + private class SyncThread extends Thread { + private final SyncContext mSyncContext; + private final Bundle mExtras; + private final int mThreadsKey; + + public SyncThread(SyncContext syncContext, Bundle extras) { + mSyncContext = syncContext; + mExtras = extras; + mThreadsKey = extrasToKey(extras); + } + + @Override + public void run() { + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + + Trace.traceBegin(Trace.TRACE_TAG_SYNC_MANAGER, getApplication().getPackageName()); + + SyncResult syncResult = new SyncResult(); + try { + if (isCancelled()) return; + // Run the sync. + SyncService.this.onPerformSync(mExtras, syncResult); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_SYNC_MANAGER); + if (!isCancelled()) { + mSyncContext.onFinished(syncResult); + } + // Synchronize so that the assignment will be seen by other + // threads that also synchronize accesses to mSyncThreads. + synchronized (mSyncThreadLock) { + mSyncThreads.remove(mThreadsKey); + } + } + } + + private boolean isCancelled() { + return Thread.currentThread().isInterrupted(); + } + } + + /** + * Initiate an anonymous sync using this service. SyncAdapter-specific + * parameters may be specified in extras, which is guaranteed to not be + * null. + */ + public abstract void onPerformSync(Bundle extras, SyncResult syncResult); + + /** + * Override this function to indicated whether you want to support parallel syncs. + * <p>If you override and return true multiple threads will be spawned within your Service to + * handle each concurrent sync request. + * + * @return false to indicate that this service does not support parallel operations by default. + */ + protected boolean parallelSyncsEnabled() { + return false; + } +} diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index b8ac3bf..40275d8 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -16,11 +16,15 @@ package android.content.pm; +import android.annotation.IntDef; import android.content.res.Configuration; import android.os.Parcel; import android.os.Parcelable; import android.util.Printer; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * Information you can retrieve about a particular application * activity or receiver. This corresponds to information collected @@ -212,6 +216,28 @@ public class ActivityInfo extends ComponentInfo */ public int flags; + /** @hide */ + @IntDef({ + SCREEN_ORIENTATION_UNSPECIFIED, + SCREEN_ORIENTATION_LANDSCAPE, + SCREEN_ORIENTATION_PORTRAIT, + SCREEN_ORIENTATION_USER, + SCREEN_ORIENTATION_BEHIND, + SCREEN_ORIENTATION_SENSOR, + SCREEN_ORIENTATION_NOSENSOR, + SCREEN_ORIENTATION_SENSOR_LANDSCAPE, + SCREEN_ORIENTATION_SENSOR_PORTRAIT, + SCREEN_ORIENTATION_REVERSE_LANDSCAPE, + SCREEN_ORIENTATION_REVERSE_PORTRAIT, + SCREEN_ORIENTATION_FULL_SENSOR, + SCREEN_ORIENTATION_USER_LANDSCAPE, + SCREEN_ORIENTATION_USER_PORTRAIT, + SCREEN_ORIENTATION_FULL_USER, + SCREEN_ORIENTATION_LOCKED + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ScreenOrientation {} + /** * Constant corresponding to <code>unspecified</code> in * the {@link android.R.attr#screenOrientation} attribute. @@ -323,6 +349,7 @@ public class ActivityInfo extends ComponentInfo * {@link #SCREEN_ORIENTATION_FULL_USER}, * {@link #SCREEN_ORIENTATION_LOCKED}, */ + @ScreenOrientation public int screenOrientation = SCREEN_ORIENTATION_UNSPECIFIED; /** diff --git a/core/java/android/content/pm/FeatureInfo.java b/core/java/android/content/pm/FeatureInfo.java index 89394f9..d919fc3 100644 --- a/core/java/android/content/pm/FeatureInfo.java +++ b/core/java/android/content/pm/FeatureInfo.java @@ -18,7 +18,6 @@ package android.content.pm; import android.os.Parcel; import android.os.Parcelable; -import android.os.Parcelable.Creator; /** * A single feature that can be requested by an application. This corresponds diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index c97c2b8..0192a30 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -16,6 +16,7 @@ package android.content.pm; +import android.annotation.IntDef; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.content.ComponentName; @@ -33,6 +34,8 @@ import android.util.AndroidException; import android.util.DisplayMetrics; import java.io.File; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.List; /** @@ -190,6 +193,11 @@ public abstract class PackageManager { */ public static final int MATCH_DEFAULT_ONLY = 0x00010000; + /** @hide */ + @IntDef({PERMISSION_GRANTED, PERMISSION_DENIED}) + @Retention(RetentionPolicy.SOURCE) + public @interface PermissionResult {} + /** * Permission check result: this is returned by {@link #checkPermission} * if the permission has been granted to the given package. diff --git a/core/java/android/content/pm/XmlSerializerAndParser.java b/core/java/android/content/pm/XmlSerializerAndParser.java index 935fc02..20cb61c 100644 --- a/core/java/android/content/pm/XmlSerializerAndParser.java +++ b/core/java/android/content/pm/XmlSerializerAndParser.java @@ -19,7 +19,6 @@ package android.content.pm; import org.xmlpull.v1.XmlSerializer; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; -import android.os.Parcel; import java.io.IOException; diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java index fc9e486..e53486d 100644 --- a/core/java/android/content/res/AssetManager.java +++ b/core/java/android/content/res/AssetManager.java @@ -17,7 +17,6 @@ package android.content.res; import android.os.ParcelFileDescriptor; -import android.os.Trace; import android.util.Log; import android.util.TypedValue; diff --git a/core/java/android/content/res/ColorStateList.java b/core/java/android/content/res/ColorStateList.java index bd23db4..431226a 100644 --- a/core/java/android/content/res/ColorStateList.java +++ b/core/java/android/content/res/ColorStateList.java @@ -16,6 +16,7 @@ package android.content.res; +import android.graphics.Color; import com.android.internal.util.ArrayUtils; import org.xmlpull.v1.XmlPullParser; @@ -259,7 +260,17 @@ public class ColorStateList implements Parcelable { public boolean isStateful() { return mStateSpecs.length > 1; } - + + 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 the color associated with the given set of {@link android.view.View} states. * diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index cd5b5d2..eb41ee9 100644 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -36,7 +36,6 @@ import android.util.Log; import android.util.Slog; import android.util.TypedValue; import android.util.LongSparseArray; -import android.view.DisplayAdjustments; import java.io.IOException; import java.io.InputStream; diff --git a/core/java/android/content/res/TypedArray.java b/core/java/android/content/res/TypedArray.java index 83d48aa..4b96800 100644 --- a/core/java/android/content/res/TypedArray.java +++ b/core/java/android/content/res/TypedArray.java @@ -16,7 +16,6 @@ package android.content.res; -import android.content.pm.ActivityInfo; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.DisplayMetrics; diff --git a/core/java/android/database/CursorToBulkCursorAdaptor.java b/core/java/android/database/CursorToBulkCursorAdaptor.java index 82a61d4..7dcfae2 100644 --- a/core/java/android/database/CursorToBulkCursorAdaptor.java +++ b/core/java/android/database/CursorToBulkCursorAdaptor.java @@ -20,7 +20,6 @@ import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; -import android.util.Log; /** diff --git a/core/java/android/database/sqlite/SQLiteOpenHelper.java b/core/java/android/database/sqlite/SQLiteOpenHelper.java index 431eca2..2dd4800 100644 --- a/core/java/android/database/sqlite/SQLiteOpenHelper.java +++ b/core/java/android/database/sqlite/SQLiteOpenHelper.java @@ -18,7 +18,6 @@ package android.database.sqlite; import android.content.Context; import android.database.DatabaseErrorHandler; -import android.database.DefaultDatabaseErrorHandler; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.util.Log; diff --git a/core/java/android/ddm/DdmHandleNativeHeap.java b/core/java/android/ddm/DdmHandleNativeHeap.java index 6bd65aa..775c570 100644 --- a/core/java/android/ddm/DdmHandleNativeHeap.java +++ b/core/java/android/ddm/DdmHandleNativeHeap.java @@ -20,7 +20,6 @@ import org.apache.harmony.dalvik.ddmc.Chunk; import org.apache.harmony.dalvik.ddmc.ChunkHandler; import org.apache.harmony.dalvik.ddmc.DdmServer; import android.util.Log; -import java.nio.ByteBuffer; /** * Handle thread-related traffic. diff --git a/core/java/android/ddm/DdmHandleProfiling.java b/core/java/android/ddm/DdmHandleProfiling.java index 537763d..cce4dd2 100644 --- a/core/java/android/ddm/DdmHandleProfiling.java +++ b/core/java/android/ddm/DdmHandleProfiling.java @@ -21,7 +21,6 @@ import org.apache.harmony.dalvik.ddmc.ChunkHandler; import org.apache.harmony.dalvik.ddmc.DdmServer; import android.os.Debug; import android.util.Log; -import java.io.IOException; import java.nio.ByteBuffer; /** diff --git a/core/java/android/gesture/GestureOverlayView.java b/core/java/android/gesture/GestureOverlayView.java index 2d47f28..6e3a00f 100644 --- a/core/java/android/gesture/GestureOverlayView.java +++ b/core/java/android/gesture/GestureOverlayView.java @@ -134,11 +134,16 @@ public class GestureOverlayView extends FrameLayout { this(context, attrs, com.android.internal.R.attr.gestureOverlayViewStyle); } - public GestureOverlayView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public GestureOverlayView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public GestureOverlayView( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes(attrs, - R.styleable.GestureOverlayView, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.GestureOverlayView, defStyleAttr, defStyleRes); mGestureStrokeWidth = a.getFloat(R.styleable.GestureOverlayView_gestureStrokeWidth, mGestureStrokeWidth); diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java index feb47aa..9913e33 100644 --- a/core/java/android/hardware/Camera.java +++ b/core/java/android/hardware/Camera.java @@ -46,7 +46,6 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashMap; import java.util.List; -import java.util.concurrent.locks.ReentrantLock; /** * The Camera class is used to set image capture settings, start/stop preview, diff --git a/core/java/android/hardware/SerialManager.java b/core/java/android/hardware/SerialManager.java index c5e1c2b..e0680bf 100644 --- a/core/java/android/hardware/SerialManager.java +++ b/core/java/android/hardware/SerialManager.java @@ -17,16 +17,12 @@ package android.hardware; -import android.app.PendingIntent; import android.content.Context; -import android.os.Bundle; import android.os.ParcelFileDescriptor; import android.os.RemoteException; -import android.os.SystemProperties; import android.util.Log; import java.io.IOException; -import java.util.HashMap; /** * @hide diff --git a/core/java/android/hardware/SerialPort.java b/core/java/android/hardware/SerialPort.java index f50cdef..5d83d9c 100644 --- a/core/java/android/hardware/SerialPort.java +++ b/core/java/android/hardware/SerialPort.java @@ -17,14 +17,9 @@ package android.hardware; import android.os.ParcelFileDescriptor; -import android.util.Log; import java.io.FileDescriptor; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.InputStream; import java.io.IOException; -import java.io.OutputStream; import java.nio.ByteBuffer; diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index 65b6c7a..2ac50e4 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -19,7 +19,6 @@ package android.hardware.camera2; import android.content.Context; import android.hardware.ICameraService; import android.hardware.ICameraServiceListener; -import android.hardware.IProCameraUser; import android.hardware.camera2.impl.CameraMetadataNative; import android.hardware.camera2.utils.CameraBinderDecorator; import android.hardware.camera2.utils.CameraRuntimeException; diff --git a/core/java/android/hardware/camera2/CameraMetadata.java b/core/java/android/hardware/camera2/CameraMetadata.java index 1d6ff7d..5d0bb33 100644 --- a/core/java/android/hardware/camera2/CameraMetadata.java +++ b/core/java/android/hardware/camera2/CameraMetadata.java @@ -168,7 +168,7 @@ public abstract class CameraMetadata { Key lhs = (Key) o; - return mName.equals(lhs.mName); + return mName.equals(lhs.mName) && mType.equals(lhs.mType); } /** diff --git a/core/java/android/hardware/camera2/CaptureFailure.java b/core/java/android/hardware/camera2/CaptureFailure.java index 3b408cf..35f9af1 100644 --- a/core/java/android/hardware/camera2/CaptureFailure.java +++ b/core/java/android/hardware/camera2/CaptureFailure.java @@ -15,8 +15,6 @@ */ package android.hardware.camera2; -import android.hardware.camera2.CameraDevice.CaptureListener; - /** * A report of failed capture for a single image capture from the image sensor. * diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java index 898f123..00b02fa 100644 --- a/core/java/android/hardware/camera2/CaptureRequest.java +++ b/core/java/android/hardware/camera2/CaptureRequest.java @@ -17,7 +17,6 @@ package android.hardware.camera2; import android.hardware.camera2.impl.CameraMetadataNative; -import android.hardware.camera2.CameraDevice.CaptureListener; import android.os.Parcel; import android.os.Parcelable; import android.view.Surface; diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java index 535b963..7224577 100644 --- a/core/java/android/hardware/camera2/CaptureResult.java +++ b/core/java/android/hardware/camera2/CaptureResult.java @@ -16,8 +16,6 @@ package android.hardware.camera2; -import android.graphics.Point; -import android.graphics.Rect; import android.hardware.camera2.impl.CameraMetadataNative; /** @@ -784,6 +782,8 @@ public final class CaptureResult extends CameraMetadata { * <p> * Only available if faceDetectMode == FULL * </p> + * + * @hide */ public static final Key<int[]> STATISTICS_FACE_IDS = new Key<int[]>("android.statistics.faceIds", int[].class); @@ -796,6 +796,8 @@ public final class CaptureResult extends CameraMetadata { * <p> * Only available if faceDetectMode == FULL * </p> + * + * @hide */ public static final Key<int[]> STATISTICS_FACE_LANDMARKS = new Key<int[]>("android.statistics.faceLandmarks", int[].class); @@ -808,6 +810,8 @@ public final class CaptureResult extends CameraMetadata { * <p> * Only available if faceDetectMode != OFF * </p> + * + * @hide */ public static final Key<android.graphics.Rect[]> STATISTICS_FACE_RECTANGLES = new Key<android.graphics.Rect[]>("android.statistics.faceRectangles", android.graphics.Rect[].class); @@ -821,6 +825,8 @@ public final class CaptureResult extends CameraMetadata { * Only available if faceDetectMode != OFF. The value should be * meaningful (for example, setting 100 at all times is illegal). * </p> + * + * @hide */ public static final Key<byte[]> STATISTICS_FACE_SCORES = new Key<byte[]>("android.statistics.faceScores", byte[].class); diff --git a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java index 072c5bb..2ddcb14 100644 --- a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java +++ b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java @@ -448,7 +448,7 @@ public class CameraMetadataNative extends CameraMetadata implements Parcelable { } else if (key.equals(CaptureResult.STATISTICS_FACES)) { return (T) getFaces(); } else if (key.equals(CaptureResult.STATISTICS_FACE_RECTANGLES)) { - return (T) fixFaceRectangles(); + return (T) getFaceRectangles(); } // For other keys, get() falls back to getBase() @@ -457,12 +457,15 @@ public class CameraMetadataNative extends CameraMetadata implements Parcelable { private int[] getAvailableFormats() { int[] availableFormats = getBase(CameraCharacteristics.SCALER_AVAILABLE_FORMATS); - for (int i = 0; i < availableFormats.length; i++) { - // JPEG has different value between native and managed side, need override. - if (availableFormats[i] == NATIVE_JPEG_FORMAT) { - availableFormats[i] = ImageFormat.JPEG; + if (availableFormats != null) { + for (int i = 0; i < availableFormats.length; i++) { + // JPEG has different value between native and managed side, need override. + if (availableFormats[i] == NATIVE_JPEG_FORMAT) { + availableFormats[i] = ImageFormat.JPEG; + } } } + return availableFormats; } @@ -550,7 +553,7 @@ public class CameraMetadataNative extends CameraMetadata implements Parcelable { // (left, top, width, height) at the native level, so the normal Rect // conversion that does (l, t, w, h) -> (l, t, r, b) is unnecessary. Undo // that conversion here for just the faces. - private Rect[] fixFaceRectangles() { + private Rect[] getFaceRectangles() { Rect[] faceRectangles = getBase(CaptureResult.STATISTICS_FACE_RECTANGLES); if (faceRectangles == null) return null; @@ -590,6 +593,8 @@ public class CameraMetadataNative extends CameraMetadata implements Parcelable { private <T> boolean setOverride(Key<T> key, T value) { if (key.equals(CameraCharacteristics.SCALER_AVAILABLE_FORMATS)) { return setAvailableFormats((int[]) value); + } else if (key.equals(CaptureResult.STATISTICS_FACE_RECTANGLES)) { + return setFaceRectangles((Rect[]) value); } // For other keys, set() falls back to setBase(). @@ -615,6 +620,36 @@ public class CameraMetadataNative extends CameraMetadata implements Parcelable { return true; } + /** + * Convert Face Rectangles from managed side to native side as they have different definitions. + * <p> + * Managed side face rectangles are defined as: left, top, width, height. + * Native side face rectangles are defined as: left, top, right, bottom. + * The input face rectangle need to be converted to native side definition when set is called. + * </p> + * + * @param faceRects Input face rectangles. + * @return true if face rectangles can be set successfully. Otherwise, Let the caller + * (setBase) to handle it appropriately. + */ + private boolean setFaceRectangles(Rect[] faceRects) { + if (faceRects == null) { + return false; + } + + Rect[] newFaceRects = new Rect[faceRects.length]; + for (int i = 0; i < newFaceRects.length; i++) { + newFaceRects[i] = new Rect( + faceRects[i].left, + faceRects[i].top, + faceRects[i].right + faceRects[i].left, + faceRects[i].bottom + faceRects[i].top); + } + + setBase(CaptureResult.STATISTICS_FACE_RECTANGLES, newFaceRects); + return true; + } + private long mMetadataPtr; // native CameraMetadata* private native long nativeAllocate(); diff --git a/core/java/android/hardware/camera2/package.html b/core/java/android/hardware/camera2/package.html index c619984..9f6c2a9 100644 --- a/core/java/android/hardware/camera2/package.html +++ b/core/java/android/hardware/camera2/package.html @@ -80,7 +80,5 @@ output streams included in the request. These are produced asynchronously relative to the output CaptureResult, sometimes substantially later.</p> -{@hide} - </BODY> </HTML> diff --git a/core/java/android/hardware/display/WifiDisplayStatus.java b/core/java/android/hardware/display/WifiDisplayStatus.java index 5216727..b645662 100644 --- a/core/java/android/hardware/display/WifiDisplayStatus.java +++ b/core/java/android/hardware/display/WifiDisplayStatus.java @@ -20,8 +20,6 @@ import android.os.Parcel; import android.os.Parcelable; import java.util.Arrays; -import java.util.ArrayList; -import java.util.List; /** * Describes the current global state of Wifi display connectivity, including the diff --git a/core/java/android/hardware/location/GeofenceHardwareRequest.java b/core/java/android/hardware/location/GeofenceHardwareRequest.java index 6e7b592..796d7f8 100644 --- a/core/java/android/hardware/location/GeofenceHardwareRequest.java +++ b/core/java/android/hardware/location/GeofenceHardwareRequest.java @@ -16,8 +16,6 @@ package android.hardware.location; -import android.location.Location; - /** * This class represents the characteristics of the geofence. * diff --git a/core/java/android/hardware/usb/UsbAccessory.java b/core/java/android/hardware/usb/UsbAccessory.java index 5719452..2f9178c 100644 --- a/core/java/android/hardware/usb/UsbAccessory.java +++ b/core/java/android/hardware/usb/UsbAccessory.java @@ -16,10 +16,8 @@ package android.hardware.usb; -import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; -import android.util.Log; /** * A class representing a USB accessory, which is an external hardware component diff --git a/core/java/android/hardware/usb/UsbDevice.java b/core/java/android/hardware/usb/UsbDevice.java index 9bd38f9..ae6118c 100644 --- a/core/java/android/hardware/usb/UsbDevice.java +++ b/core/java/android/hardware/usb/UsbDevice.java @@ -16,12 +16,8 @@ package android.hardware.usb; -import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; -import android.util.Log; - -import java.io.FileDescriptor; /** * This class represents a USB device attached to the android device with the android device diff --git a/core/java/android/hardware/usb/UsbEndpoint.java b/core/java/android/hardware/usb/UsbEndpoint.java index 753a447..708d651 100644 --- a/core/java/android/hardware/usb/UsbEndpoint.java +++ b/core/java/android/hardware/usb/UsbEndpoint.java @@ -16,7 +16,6 @@ package android.hardware.usb; -import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; diff --git a/core/java/android/hardware/usb/UsbInterface.java b/core/java/android/hardware/usb/UsbInterface.java index d6c54a8..e94baa1 100644 --- a/core/java/android/hardware/usb/UsbInterface.java +++ b/core/java/android/hardware/usb/UsbInterface.java @@ -16,7 +16,6 @@ package android.hardware.usb; -import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; diff --git a/core/java/android/inputmethodservice/ExtractButton.java b/core/java/android/inputmethodservice/ExtractButton.java index b6b7595..fe63c1e 100644 --- a/core/java/android/inputmethodservice/ExtractButton.java +++ b/core/java/android/inputmethodservice/ExtractButton.java @@ -32,8 +32,12 @@ class ExtractButton extends Button { super(context, attrs, com.android.internal.R.attr.buttonStyle); } - public ExtractButton(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public ExtractButton(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ExtractButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); } /** diff --git a/core/java/android/inputmethodservice/ExtractEditText.java b/core/java/android/inputmethodservice/ExtractEditText.java index 23ae21b..48b604c 100644 --- a/core/java/android/inputmethodservice/ExtractEditText.java +++ b/core/java/android/inputmethodservice/ExtractEditText.java @@ -38,8 +38,12 @@ public class ExtractEditText extends EditText { super(context, attrs, com.android.internal.R.attr.editTextStyle); } - public ExtractEditText(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public ExtractEditText(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ExtractEditText(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); } void setIME(InputMethodService ime) { diff --git a/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java b/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java index bbea8ff..8437228 100644 --- a/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java +++ b/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java @@ -25,7 +25,6 @@ import android.graphics.Rect; import android.os.Bundle; import android.os.Looper; import android.os.Message; -import android.os.RemoteException; import android.util.Log; import android.util.SparseArray; import android.view.InputChannel; diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 1b7d9ea..81ad28b 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -2322,6 +2322,21 @@ public class InputMethodService extends AbstractInputMethodService { } /** + * @return The recommended height of the input method window. + * An IME author can get the last input method's height as the recommended height + * by calling this in + * {@link android.inputmethodservice.InputMethodService#onStartInputView(EditorInfo, boolean)}. + * If you don't need to use a predefined fixed height, you can avoid the window-resizing of IME + * switching by using this value as a visible inset height. It's efficient for the smooth + * transition between different IMEs. However, note that this may return 0 (or possibly + * unexpectedly low height). You should thus avoid relying on the return value of this method + * all the time. Please make sure to use a reasonable height for the IME. + */ + public int getInputMethodWindowRecommendedHeight() { + return mImm.getInputMethodWindowVisibleHeight(); + } + + /** * Performs a dump of the InputMethodService's internal state. Override * to add your own information to the dump. */ diff --git a/core/java/android/inputmethodservice/KeyboardView.java b/core/java/android/inputmethodservice/KeyboardView.java index 4916244..af75a0a 100644 --- a/core/java/android/inputmethodservice/KeyboardView.java +++ b/core/java/android/inputmethodservice/KeyboardView.java @@ -279,12 +279,15 @@ public class KeyboardView extends View implements View.OnClickListener { this(context, attrs, com.android.internal.R.attr.keyboardViewStyle); } - public KeyboardView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public KeyboardView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public KeyboardView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = - context.obtainStyledAttributes( - attrs, android.R.styleable.KeyboardView, defStyle, 0); + TypedArray a = context.obtainStyledAttributes( + attrs, android.R.styleable.KeyboardView, defStyleAttr, defStyleRes); LayoutInflater inflate = (LayoutInflater) context diff --git a/core/java/android/net/BaseNetworkStateTracker.java b/core/java/android/net/BaseNetworkStateTracker.java index 476fefe..804f8ee 100644 --- a/core/java/android/net/BaseNetworkStateTracker.java +++ b/core/java/android/net/BaseNetworkStateTracker.java @@ -20,10 +20,10 @@ import android.content.Context; import android.os.Handler; import android.os.Messenger; -import com.android.internal.util.Preconditions; - import java.util.concurrent.atomic.AtomicBoolean; +import com.android.internal.util.Preconditions; + /** * Interface to control and observe state of a specific network, hiding * network-specific details from {@link ConnectivityManager}. Surfaces events @@ -108,11 +108,6 @@ public abstract class BaseNetworkStateTracker implements NetworkStateTracker { } @Override - public void captivePortalCheckComplete() { - // not implemented - } - - @Override public void captivePortalCheckCompleted(boolean isCaptivePortal) { // not implemented } diff --git a/core/java/android/net/CaptivePortalTracker.java b/core/java/android/net/CaptivePortalTracker.java index d678f1e..5b6f154 100644 --- a/core/java/android/net/CaptivePortalTracker.java +++ b/core/java/android/net/CaptivePortalTracker.java @@ -48,7 +48,6 @@ import java.io.IOException; import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.Inet4Address; -import java.net.SocketTimeoutException; import java.net.URL; import java.net.UnknownHostException; import java.util.List; @@ -84,13 +83,12 @@ public class CaptivePortalTracker extends StateMachine { private String mServer; private String mUrl; private boolean mIsCaptivePortalCheckEnabled = false; - private IConnectivityManager mConnService; - private TelephonyManager mTelephonyManager; - private WifiManager mWifiManager; - private Context mContext; + private final IConnectivityManager mConnService; + private final TelephonyManager mTelephonyManager; + private final WifiManager mWifiManager; + private final Context mContext; private NetworkInfo mNetworkInfo; - private static final int CMD_DETECT_PORTAL = 0; private static final int CMD_CONNECTIVITY_CHANGE = 1; private static final int CMD_DELAYED_CAPTIVE_CHECK = 2; @@ -98,14 +96,15 @@ public class CaptivePortalTracker extends StateMachine { private static final int DELAYED_CHECK_INTERVAL_MS = 10000; private int mDelayedCheckToken = 0; - private State mDefaultState = new DefaultState(); - private State mNoActiveNetworkState = new NoActiveNetworkState(); - private State mActiveNetworkState = new ActiveNetworkState(); - private State mDelayedCaptiveCheckState = new DelayedCaptiveCheckState(); + private final State mDefaultState = new DefaultState(); + private final State mNoActiveNetworkState = new NoActiveNetworkState(); + private final State mActiveNetworkState = new ActiveNetworkState(); + private final State mDelayedCaptiveCheckState = new DelayedCaptiveCheckState(); private static final String SETUP_WIZARD_PACKAGE = "com.google.android.setupwizard"; private boolean mDeviceProvisioned = false; - private ProvisioningObserver mProvisioningObserver; + @SuppressWarnings("unused") + private final ProvisioningObserver mProvisioningObserver; private CaptivePortalTracker(Context context, IConnectivityManager cs) { super(TAG); @@ -174,29 +173,11 @@ public class CaptivePortalTracker extends StateMachine { return captivePortal; } - public void detectCaptivePortal(NetworkInfo info) { - sendMessage(obtainMessage(CMD_DETECT_PORTAL, info)); - } - private class DefaultState extends State { @Override public boolean processMessage(Message message) { - if (DBG) log(getName() + message.toString()); - switch (message.what) { - case CMD_DETECT_PORTAL: - NetworkInfo info = (NetworkInfo) message.obj; - // Checking on a secondary connection is not supported - // yet - notifyPortalCheckComplete(info); - break; - case CMD_CONNECTIVITY_CHANGE: - case CMD_DELAYED_CAPTIVE_CHECK: - break; - default: - loge("Ignoring " + message); - break; - } + loge("Ignoring " + message); return HANDLED; } } @@ -316,19 +297,6 @@ public class CaptivePortalTracker extends StateMachine { } } - private void notifyPortalCheckComplete(NetworkInfo info) { - if (info == null) { - loge("notifyPortalCheckComplete on null"); - return; - } - try { - if (DBG) log("notifyPortalCheckComplete: ni=" + info); - mConnService.captivePortalCheckComplete(info); - } catch(RemoteException e) { - e.printStackTrace(); - } - } - private void notifyPortalCheckCompleted(NetworkInfo info, boolean isCaptivePortal) { if (info == null) { loge("notifyPortalCheckComplete on null"); @@ -464,7 +432,6 @@ public class CaptivePortalTracker extends StateMachine { latencyBroadcast.putExtra(EXTRA_NETWORK_TYPE, mTelephonyManager.getNetworkType()); List<CellInfo> info = mTelephonyManager.getAllCellInfo(); if (info == null) return; - StringBuffer uniqueCellId = new StringBuffer(); int numRegisteredCellInfo = 0; for (CellInfo cellInfo : info) { if (cellInfo.isRegistered()) { diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index c78a973..a9b2533 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -25,7 +25,6 @@ import android.os.Binder; import android.os.Build.VERSION_CODES; import android.os.Messenger; import android.os.RemoteException; -import android.os.ResultReceiver; import android.provider.Settings; import java.net.InetAddress; @@ -1330,24 +1329,6 @@ public class ConnectivityManager { /** * Signal that the captive portal check on the indicated network - * is complete and we can turn the network on for general use. - * - * @param info the {@link NetworkInfo} object for the networkType - * in question. - * - * <p>This method requires the call to hold the permission - * {@link android.Manifest.permission#CONNECTIVITY_INTERNAL}. - * {@hide} - */ - public void captivePortalCheckComplete(NetworkInfo info) { - try { - mService.captivePortalCheckComplete(info); - } catch (RemoteException e) { - } - } - - /** - * Signal that the captive portal check on the indicated network * is complete and whether its a captive portal or not. * * @param info the {@link NetworkInfo} object for the networkType diff --git a/core/java/android/net/DhcpInfo.java b/core/java/android/net/DhcpInfo.java index 3bede5d..788d7d9 100644 --- a/core/java/android/net/DhcpInfo.java +++ b/core/java/android/net/DhcpInfo.java @@ -18,7 +18,6 @@ package android.net; import android.os.Parcelable; import android.os.Parcel; -import java.net.InetAddress; /** * A simple object for retrieving the results of a DHCP request. diff --git a/core/java/android/net/DhcpResults.java b/core/java/android/net/DhcpResults.java index a3f70da..22b26b1 100644 --- a/core/java/android/net/DhcpResults.java +++ b/core/java/android/net/DhcpResults.java @@ -23,9 +23,6 @@ import android.util.Log; import java.net.InetAddress; import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; /** * A simple object for retrieving the results of a DHCP request. diff --git a/core/java/android/net/DummyDataStateTracker.java b/core/java/android/net/DummyDataStateTracker.java index 51a1191..a5d059e 100644 --- a/core/java/android/net/DummyDataStateTracker.java +++ b/core/java/android/net/DummyDataStateTracker.java @@ -117,11 +117,6 @@ public class DummyDataStateTracker extends BaseNetworkStateTracker { } @Override - public void captivePortalCheckComplete() { - // not implemented - } - - @Override public void captivePortalCheckCompleted(boolean isCaptivePortal) { // not implemented } diff --git a/core/java/android/net/EthernetDataTracker.java b/core/java/android/net/EthernetDataTracker.java index cc8c771..95faa77 100644 --- a/core/java/android/net/EthernetDataTracker.java +++ b/core/java/android/net/EthernetDataTracker.java @@ -270,11 +270,6 @@ public class EthernetDataTracker extends BaseNetworkStateTracker { } @Override - public void captivePortalCheckComplete() { - // not implemented - } - - @Override public void captivePortalCheckCompleted(boolean isCaptivePortal) { // not implemented } diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl index c1da2e3..b3217eb 100644 --- a/core/java/android/net/IConnectivityManager.aidl +++ b/core/java/android/net/IConnectivityManager.aidl @@ -129,8 +129,6 @@ interface IConnectivityManager boolean updateLockdownVpn(); - void captivePortalCheckComplete(in NetworkInfo info); - void captivePortalCheckCompleted(in NetworkInfo info, boolean isCaptivePortal); void supplyMessenger(int networkType, in Messenger messenger); diff --git a/core/java/android/net/INetworkManagementEventObserver.aidl b/core/java/android/net/INetworkManagementEventObserver.aidl index b76e4c2..c720c7b 100644 --- a/core/java/android/net/INetworkManagementEventObserver.aidl +++ b/core/java/android/net/INetworkManagementEventObserver.aidl @@ -90,4 +90,13 @@ interface INetworkManagementEventObserver { * @param active True if the interface is actively transmitting data, false if it is idle. */ void interfaceClassDataActivityChanged(String label, boolean active); + + /** + * Information about available DNS servers has been received. + * + * @param iface The interface on which the information was received. + * @param lifetime The time in seconds for which the DNS servers may be used. + * @param servers The IP addresses of the DNS servers. + */ + void interfaceDnsServerInfo(String iface, long lifetime, in String[] servers); } diff --git a/core/java/android/net/LinkSocketNotifier.java b/core/java/android/net/LinkSocketNotifier.java index 28e2834..e2429d8 100644 --- a/core/java/android/net/LinkSocketNotifier.java +++ b/core/java/android/net/LinkSocketNotifier.java @@ -16,8 +16,6 @@ package android.net; -import java.util.Map; - /** * Interface used to get feedback about a {@link android.net.LinkSocket}. Instance is optionally * passed when a LinkSocket is constructed. Multiple LinkSockets may use the same notifier. diff --git a/core/java/android/net/MailTo.java b/core/java/android/net/MailTo.java index b90dcb1..dadb6d9 100644 --- a/core/java/android/net/MailTo.java +++ b/core/java/android/net/MailTo.java @@ -19,7 +19,6 @@ package android.net; import java.util.HashMap; import java.util.Locale; import java.util.Map; -import java.util.Set; /** * diff --git a/core/java/android/net/MobileDataStateTracker.java b/core/java/android/net/MobileDataStateTracker.java index c106514..d9c35c0 100644 --- a/core/java/android/net/MobileDataStateTracker.java +++ b/core/java/android/net/MobileDataStateTracker.java @@ -460,11 +460,6 @@ public class MobileDataStateTracker extends BaseNetworkStateTracker { } @Override - public void captivePortalCheckComplete() { - // not implemented - } - - @Override public void captivePortalCheckCompleted(boolean isCaptivePortal) { if (mIsCaptivePortal.getAndSet(isCaptivePortal) != isCaptivePortal) { // Captive portal change enable/disable failing fast diff --git a/core/java/android/net/NetworkConfig.java b/core/java/android/net/NetworkConfig.java index 5d95f41..32a2cda 100644 --- a/core/java/android/net/NetworkConfig.java +++ b/core/java/android/net/NetworkConfig.java @@ -16,7 +16,6 @@ package android.net; -import android.util.Log; import java.util.Locale; /** diff --git a/core/java/android/net/NetworkInfo.java b/core/java/android/net/NetworkInfo.java index 4d2a70d..53b1308 100644 --- a/core/java/android/net/NetworkInfo.java +++ b/core/java/android/net/NetworkInfo.java @@ -156,18 +156,20 @@ public class NetworkInfo implements Parcelable { /** {@hide} */ public NetworkInfo(NetworkInfo source) { if (source != null) { - mNetworkType = source.mNetworkType; - mSubtype = source.mSubtype; - mTypeName = source.mTypeName; - mSubtypeName = source.mSubtypeName; - mState = source.mState; - mDetailedState = source.mDetailedState; - mReason = source.mReason; - mExtraInfo = source.mExtraInfo; - mIsFailover = source.mIsFailover; - mIsRoaming = source.mIsRoaming; - mIsAvailable = source.mIsAvailable; - mIsConnectedToProvisioningNetwork = source.mIsConnectedToProvisioningNetwork; + synchronized (source) { + mNetworkType = source.mNetworkType; + mSubtype = source.mSubtype; + mTypeName = source.mTypeName; + mSubtypeName = source.mSubtypeName; + mState = source.mState; + mDetailedState = source.mDetailedState; + mReason = source.mReason; + mExtraInfo = source.mExtraInfo; + mIsFailover = source.mIsFailover; + mIsRoaming = source.mIsRoaming; + mIsAvailable = source.mIsAvailable; + mIsConnectedToProvisioningNetwork = source.mIsConnectedToProvisioningNetwork; + } } } diff --git a/core/java/android/net/NetworkStateTracker.java b/core/java/android/net/NetworkStateTracker.java index 1ca9255..c49b1d1 100644 --- a/core/java/android/net/NetworkStateTracker.java +++ b/core/java/android/net/NetworkStateTracker.java @@ -144,11 +144,6 @@ public interface NetworkStateTracker { public boolean reconnect(); /** - * Ready to switch on to the network after captive portal check - */ - public void captivePortalCheckComplete(); - - /** * Captive portal check has completed */ public void captivePortalCheckCompleted(boolean isCaptive); diff --git a/core/java/android/net/ProxyProperties.java b/core/java/android/net/ProxyProperties.java index 010e527..54fc01d 100644 --- a/core/java/android/net/ProxyProperties.java +++ b/core/java/android/net/ProxyProperties.java @@ -22,7 +22,6 @@ import android.os.Parcelable; import android.text.TextUtils; import java.net.InetSocketAddress; -import java.net.UnknownHostException; import java.util.Locale; /** diff --git a/core/java/android/net/SSLCertificateSocketFactory.java b/core/java/android/net/SSLCertificateSocketFactory.java index b0278d3..12e8791 100644 --- a/core/java/android/net/SSLCertificateSocketFactory.java +++ b/core/java/android/net/SSLCertificateSocketFactory.java @@ -135,7 +135,8 @@ public class SSLCertificateSocketFactory extends SSLSocketFactory { * disabled, using an optional handshake timeout and SSL session cache. * * <p class="caution"><b>Warning:</b> Sockets created using this factory - * are vulnerable to man-in-the-middle attacks!</p> + * are vulnerable to man-in-the-middle attacks!</p>. The caller must implement + * its own verification. * * @param handshakeTimeoutMillis to use for SSL connection handshake, or 0 * for none. The socket timeout is reset to 0 after the handshake. @@ -223,8 +224,6 @@ public class SSLCertificateSocketFactory extends SSLSocketFactory { if (mInsecureFactory == null) { if (mSecure) { Log.w(TAG, "*** BYPASSING SSL SECURITY CHECKS (socket.relaxsslcheck=yes) ***"); - } else { - Log.w(TAG, "Bypassing SSL security checks at caller's request"); } mInsecureFactory = makeSocketFactory(mKeyManagers, INSECURE_TRUST_MANAGER); } @@ -431,6 +430,7 @@ public class SSLCertificateSocketFactory extends SSLSocketFactory { s.setAlpnProtocols(mAlpnProtocols); s.setHandshakeTimeout(mHandshakeTimeoutMillis); s.setChannelIdPrivateKey(mChannelIdPrivateKey); + s.setHostname(host); if (mSecure) { verifyHostname(s, host); } diff --git a/core/java/android/net/SSLSessionCache.java b/core/java/android/net/SSLSessionCache.java index 15421de..65db2c3 100644 --- a/core/java/android/net/SSLSessionCache.java +++ b/core/java/android/net/SSLSessionCache.java @@ -19,12 +19,16 @@ package android.net; import android.content.Context; import android.util.Log; +import com.android.org.conscrypt.ClientSessionContext; import com.android.org.conscrypt.FileClientSessionCache; import com.android.org.conscrypt.SSLClientSessionCache; import java.io.File; import java.io.IOException; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSessionContext; + /** * File-based cache of established SSL sessions. When re-establishing a * connection to the same server, using an SSL session cache can save some time, @@ -38,6 +42,40 @@ public final class SSLSessionCache { /* package */ final SSLClientSessionCache mSessionCache; /** + * Installs a {@link SSLSessionCache} on a {@link SSLContext}. The cache will + * be used on all socket factories created by this context (including factories + * created before this call). + * + * @param cache the cache instance to install, or {@code null} to uninstall any + * existing cache. + * @param context the context to install it on. + * @throws IllegalArgumentException if the context does not support a session + * cache. + * + * @hide candidate for public API + */ + public static void install(SSLSessionCache cache, SSLContext context) { + SSLSessionContext clientContext = context.getClientSessionContext(); + if (clientContext instanceof ClientSessionContext) { + ((ClientSessionContext) clientContext).setPersistentCache( + cache == null ? null : cache.mSessionCache); + } else { + throw new IllegalArgumentException("Incompatible SSLContext: " + context); + } + } + + /** + * NOTE: This needs to be Object (and not SSLClientSessionCache) because apps + * that build directly against the framework (and not the SDK) might not declare + * a dependency on conscrypt. Javac will then has fail while resolving constructors. + * + * @hide For unit test use only + */ + public SSLSessionCache(Object cache) { + mSessionCache = (SSLClientSessionCache) cache; + } + + /** * Create a session cache using the specified directory. * Individual session entries will be files within the directory. * Multiple instances for the same directory share data internally. diff --git a/core/java/android/net/SntpClient.java b/core/java/android/net/SntpClient.java index 316440f..7673011 100644 --- a/core/java/android/net/SntpClient.java +++ b/core/java/android/net/SntpClient.java @@ -19,7 +19,6 @@ package android.net; import android.os.SystemClock; import android.util.Log; -import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; diff --git a/core/java/android/net/dhcp/DhcpAckPacket.java b/core/java/android/net/dhcp/DhcpAckPacket.java index 4eca531..7b8be9c 100644 --- a/core/java/android/net/dhcp/DhcpAckPacket.java +++ b/core/java/android/net/dhcp/DhcpAckPacket.java @@ -19,7 +19,6 @@ package android.net.dhcp; import java.net.InetAddress; import java.net.Inet4Address; import java.nio.ByteBuffer; -import java.util.List; /** * This class implements the DHCP-ACK packet. diff --git a/core/java/android/net/dhcp/DhcpOfferPacket.java b/core/java/android/net/dhcp/DhcpOfferPacket.java index 3d79f4d..f1c30e1 100644 --- a/core/java/android/net/dhcp/DhcpOfferPacket.java +++ b/core/java/android/net/dhcp/DhcpOfferPacket.java @@ -19,7 +19,6 @@ package android.net.dhcp; import java.net.InetAddress; import java.net.Inet4Address; import java.nio.ByteBuffer; -import java.util.List; /** * This class implements the DHCP-OFFER packet. diff --git a/core/java/android/net/dhcp/DhcpPacket.java b/core/java/android/net/dhcp/DhcpPacket.java index 317a9b4..c7c25f0 100644 --- a/core/java/android/net/dhcp/DhcpPacket.java +++ b/core/java/android/net/dhcp/DhcpPacket.java @@ -1,8 +1,5 @@ package android.net.dhcp; -import android.util.Log; - -import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.UnknownHostException; import java.nio.ByteBuffer; diff --git a/core/java/android/net/dhcp/DhcpStateMachine.java b/core/java/android/net/dhcp/DhcpStateMachine.java index b6c384d..bc9a798 100644 --- a/core/java/android/net/dhcp/DhcpStateMachine.java +++ b/core/java/android/net/dhcp/DhcpStateMachine.java @@ -17,7 +17,6 @@ package android.net.dhcp; import java.net.InetAddress; -import java.nio.ByteBuffer; import java.util.List; /** diff --git a/core/java/android/net/http/AndroidHttpClientConnection.java b/core/java/android/net/http/AndroidHttpClientConnection.java index eb96679..6d48fce 100644 --- a/core/java/android/net/http/AndroidHttpClientConnection.java +++ b/core/java/android/net/http/AndroidHttpClientConnection.java @@ -16,8 +16,6 @@ package android.net.http; -import org.apache.http.Header; - import org.apache.http.HttpConnection; import org.apache.http.HttpClientConnection; import org.apache.http.HttpConnectionMetrics; @@ -27,12 +25,10 @@ import org.apache.http.HttpException; import org.apache.http.HttpInetConnection; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; -import org.apache.http.HttpResponseFactory; 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.DefaultHttpResponseFactory; import org.apache.http.impl.HttpConnectionMetricsImpl; import org.apache.http.impl.entity.EntitySerializer; import org.apache.http.impl.entity.StrictContentLengthStrategy; diff --git a/core/java/android/net/http/Connection.java b/core/java/android/net/http/Connection.java index 95cecd2..834ad69 100644 --- a/core/java/android/net/http/Connection.java +++ b/core/java/android/net/http/Connection.java @@ -21,7 +21,6 @@ import android.os.SystemClock; import java.io.IOException; import java.net.UnknownHostException; -import java.util.ListIterator; import java.util.LinkedList; import javax.net.ssl.SSLHandshakeException; diff --git a/core/java/android/net/http/ConnectionThread.java b/core/java/android/net/http/ConnectionThread.java index 32191d2..d825530 100644 --- a/core/java/android/net/http/ConnectionThread.java +++ b/core/java/android/net/http/ConnectionThread.java @@ -19,8 +19,6 @@ package android.net.http; import android.content.Context; import android.os.SystemClock; -import org.apache.http.HttpHost; - import java.lang.Thread; /** diff --git a/core/java/android/net/http/HttpConnection.java b/core/java/android/net/http/HttpConnection.java index 6df86bf..edf8fed3 100644 --- a/core/java/android/net/http/HttpConnection.java +++ b/core/java/android/net/http/HttpConnection.java @@ -21,9 +21,7 @@ import android.content.Context; import java.net.Socket; import java.io.IOException; -import org.apache.http.HttpClientConnection; import org.apache.http.HttpHost; -import org.apache.http.impl.DefaultHttpClientConnection; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpConnectionParams; diff --git a/core/java/android/net/http/HttpResponseCache.java b/core/java/android/net/http/HttpResponseCache.java index 269dfb8..2785a15 100644 --- a/core/java/android/net/http/HttpResponseCache.java +++ b/core/java/android/net/http/HttpResponseCache.java @@ -17,9 +17,6 @@ package android.net.http; import android.content.Context; -import com.android.okhttp.OkResponseCache; -import com.android.okhttp.ResponseSource; -import com.android.okhttp.internal.DiskLruCache; import java.io.Closeable; import java.io.File; import java.io.IOException; @@ -32,7 +29,6 @@ import java.net.URLConnection; import java.util.List; import java.util.Map; import javax.net.ssl.HttpsURLConnection; -import libcore.io.IoUtils; import org.apache.http.impl.client.DefaultHttpClient; /** diff --git a/core/java/android/net/http/HttpsConnection.java b/core/java/android/net/http/HttpsConnection.java index 7a12e53..6bf01e2 100644 --- a/core/java/android/net/http/HttpsConnection.java +++ b/core/java/android/net/http/HttpsConnection.java @@ -40,7 +40,6 @@ import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import java.io.File; import java.io.IOException; -import java.net.InetSocketAddress; import java.net.Socket; import java.security.KeyManagementException; import java.security.cert.X509Certificate; diff --git a/core/java/android/net/http/Request.java b/core/java/android/net/http/Request.java index 8c0d503..76d7bb9 100644 --- a/core/java/android/net/http/Request.java +++ b/core/java/android/net/http/Request.java @@ -26,15 +26,12 @@ import java.util.zip.GZIPInputStream; import org.apache.http.entity.InputStreamEntity; import org.apache.http.Header; -import org.apache.http.HttpClientConnection; 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.HttpResponse; import org.apache.http.HttpStatus; -import org.apache.http.HttpVersion; import org.apache.http.ParseException; import org.apache.http.ProtocolVersion; diff --git a/core/java/android/net/http/RequestQueue.java b/core/java/android/net/http/RequestQueue.java index ce6b1ad..7d2da1b 100644 --- a/core/java/android/net/http/RequestQueue.java +++ b/core/java/android/net/http/RequestQueue.java @@ -29,10 +29,6 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Proxy; import android.net.WebAddress; -import android.os.Handler; -import android.os.Message; -import android.os.SystemProperties; -import android.text.TextUtils; import android.util.Log; import java.io.InputStream; diff --git a/core/java/android/net/nsd/NsdManager.java b/core/java/android/net/nsd/NsdManager.java index 9c3e405..6840207 100644 --- a/core/java/android/net/nsd/NsdManager.java +++ b/core/java/android/net/nsd/NsdManager.java @@ -19,8 +19,6 @@ package android.net.nsd; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.content.Context; -import android.os.Binder; -import android.os.IBinder; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; diff --git a/core/java/android/nfc/NdefRecord.java b/core/java/android/nfc/NdefRecord.java index 2b58818..de481cf 100644 --- a/core/java/android/nfc/NdefRecord.java +++ b/core/java/android/nfc/NdefRecord.java @@ -269,6 +269,7 @@ public final class NdefRecord implements Parcelable { "urn:epc:pat:", // 0x20 "urn:epc:raw:", // 0x21 "urn:epc:", // 0x22 + "urn:nfc:", // 0x23 }; private static final int MAX_PAYLOAD_SIZE = 10 * (1 << 20); // 10 MB payload limit diff --git a/core/java/android/nfc/tech/Ndef.java b/core/java/android/nfc/tech/Ndef.java index 64aa299..8240ea6 100644 --- a/core/java/android/nfc/tech/Ndef.java +++ b/core/java/android/nfc/tech/Ndef.java @@ -20,7 +20,6 @@ import android.nfc.ErrorCodes; import android.nfc.FormatException; import android.nfc.INfcTag; import android.nfc.NdefMessage; -import android.nfc.NfcAdapter; import android.nfc.Tag; import android.nfc.TagLostException; import android.os.Bundle; diff --git a/core/java/android/nfc/tech/NdefFormatable.java b/core/java/android/nfc/tech/NdefFormatable.java index ffa6a2b..4175cd0 100644 --- a/core/java/android/nfc/tech/NdefFormatable.java +++ b/core/java/android/nfc/tech/NdefFormatable.java @@ -20,7 +20,6 @@ import android.nfc.ErrorCodes; import android.nfc.FormatException; import android.nfc.INfcTag; import android.nfc.NdefMessage; -import android.nfc.NfcAdapter; import android.nfc.Tag; import android.nfc.TagLostException; import android.os.RemoteException; diff --git a/core/java/android/os/BatteryProperties.java b/core/java/android/os/BatteryProperties.java index 5df5214..2d67264 100644 --- a/core/java/android/os/BatteryProperties.java +++ b/core/java/android/os/BatteryProperties.java @@ -30,8 +30,6 @@ public class BatteryProperties implements Parcelable { public boolean batteryPresent; public int batteryLevel; public int batteryVoltage; - public int batteryCurrentNow; - public int batteryChargeCounter; public int batteryTemperature; public String batteryTechnology; @@ -49,8 +47,6 @@ public class BatteryProperties implements Parcelable { batteryPresent = p.readInt() == 1 ? true : false; batteryLevel = p.readInt(); batteryVoltage = p.readInt(); - batteryCurrentNow = p.readInt(); - batteryChargeCounter = p.readInt(); batteryTemperature = p.readInt(); batteryTechnology = p.readString(); } @@ -64,8 +60,6 @@ public class BatteryProperties implements Parcelable { p.writeInt(batteryPresent ? 1 : 0); p.writeInt(batteryLevel); p.writeInt(batteryVoltage); - p.writeInt(batteryCurrentNow); - p.writeInt(batteryChargeCounter); p.writeInt(batteryTemperature); p.writeString(batteryTechnology); } diff --git a/core/java/android/os/BatteryProperty.aidl b/core/java/android/os/BatteryProperty.aidl new file mode 100644 index 0000000..b3f2ec3 --- /dev/null +++ b/core/java/android/os/BatteryProperty.aidl @@ -0,0 +1,19 @@ +/* +** Copyright 2013, 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; + +parcelable BatteryProperty; diff --git a/core/java/android/os/BatteryProperty.java b/core/java/android/os/BatteryProperty.java new file mode 100644 index 0000000..346f5de --- /dev/null +++ b/core/java/android/os/BatteryProperty.java @@ -0,0 +1,70 @@ +/* Copyright 2013, 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 android.os.Parcel; +import android.os.Parcelable; + +/** + * {@hide} + */ +public class BatteryProperty implements Parcelable { + /* + * Battery property identifiers. These must match the values in + * frameworks/native/include/batteryservice/BatteryService.h + */ + public static final int BATTERY_PROP_CHARGE_COUNTER = 1; + public static final int BATTERY_PROP_CURRENT_NOW = 2; + public static final int BATTERY_PROP_CURRENT_AVG = 3; + + public int valueInt; + + public BatteryProperty() { + valueInt = Integer.MIN_VALUE; + } + + /* + * Parcel read/write code must be kept in sync with + * frameworks/native/services/batteryservice/BatteryProperty.cpp + */ + + private BatteryProperty(Parcel p) { + readFromParcel(p); + } + + public void readFromParcel(Parcel p) { + valueInt = p.readInt(); + } + + public void writeToParcel(Parcel p, int flags) { + p.writeInt(valueInt); + } + + public static final Parcelable.Creator<BatteryProperty> CREATOR + = new Parcelable.Creator<BatteryProperty>() { + public BatteryProperty createFromParcel(Parcel p) { + return new BatteryProperty(p); + } + + public BatteryProperty[] newArray(int size) { + return new BatteryProperty[size]; + } + }; + + public int describeContents() { + return 0; + } +} diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java index b1a9ea3..06fd8fb 100644 --- a/core/java/android/os/BatteryStats.java +++ b/core/java/android/os/BatteryStats.java @@ -26,7 +26,6 @@ import java.util.Map; import android.content.pm.ApplicationInfo; import android.telephony.SignalStrength; -import android.util.Log; import android.util.Printer; import android.util.Slog; import android.util.SparseArray; diff --git a/core/java/android/os/CommonClock.java b/core/java/android/os/CommonClock.java index 3a1da97..2ecf317 100644 --- a/core/java/android/os/CommonClock.java +++ b/core/java/android/os/CommonClock.java @@ -15,17 +15,8 @@ */ package android.os; -import java.net.InetAddress; -import java.net.Inet4Address; -import java.net.Inet6Address; import java.net.InetSocketAddress; import java.util.NoSuchElementException; -import static libcore.io.OsConstants.*; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; import android.os.Binder; import android.os.CommonTimeUtils; import android.os.IBinder; diff --git a/core/java/android/os/CommonTimeConfig.java b/core/java/android/os/CommonTimeConfig.java index 3355ee3..1f9fab5 100644 --- a/core/java/android/os/CommonTimeConfig.java +++ b/core/java/android/os/CommonTimeConfig.java @@ -15,7 +15,6 @@ */ package android.os; -import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.NoSuchElementException; diff --git a/core/java/android/os/CountDownTimer.java b/core/java/android/os/CountDownTimer.java index 15e6405..c5b1146 100644 --- a/core/java/android/os/CountDownTimer.java +++ b/core/java/android/os/CountDownTimer.java @@ -16,8 +16,6 @@ package android.os; -import android.util.Log; - /** * Schedule a countdown until a time in the future, with * regular notifications on intervals along the way. diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java index 974bf8d..7f167d5 100644 --- a/core/java/android/os/Debug.java +++ b/core/java/android/os/Debug.java @@ -26,7 +26,6 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; -import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.Reader; import java.lang.reflect.Field; @@ -41,7 +40,6 @@ import org.apache.harmony.dalvik.ddmc.ChunkHandler; import org.apache.harmony.dalvik.ddmc.DdmServer; import dalvik.bytecode.OpcodeInfo; -import dalvik.bytecode.Opcodes; import dalvik.system.VMDebug; diff --git a/core/java/android/os/DropBoxManager.java b/core/java/android/os/DropBoxManager.java index e1c1678..27001dc 100644 --- a/core/java/android/os/DropBoxManager.java +++ b/core/java/android/os/DropBoxManager.java @@ -16,14 +16,11 @@ package android.os; -import android.util.Log; - import com.android.internal.os.IDropBoxManagerService; import java.io.ByteArrayInputStream; import java.io.Closeable; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.zip.GZIPInputStream; diff --git a/core/java/android/os/FileObserver.java b/core/java/android/os/FileObserver.java index d633486..4e705e0 100644 --- a/core/java/android/os/FileObserver.java +++ b/core/java/android/os/FileObserver.java @@ -18,10 +18,7 @@ package android.os; import android.util.Log; -import com.android.internal.os.RuntimeInit; - import java.lang.ref.WeakReference; -import java.util.ArrayList; import java.util.HashMap; /** diff --git a/core/java/android/os/FileUtils.java b/core/java/android/os/FileUtils.java index ff3e277..2d60df0 100644 --- a/core/java/android/os/FileUtils.java +++ b/core/java/android/os/FileUtils.java @@ -20,9 +20,7 @@ import android.util.Log; import android.util.Slog; import libcore.io.ErrnoException; -import libcore.io.IoUtils; import libcore.io.Libcore; -import libcore.io.OsConstants; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; diff --git a/core/java/android/os/IBatteryPropertiesRegistrar.aidl b/core/java/android/os/IBatteryPropertiesRegistrar.aidl index 376f6c9..fd01802 100644 --- a/core/java/android/os/IBatteryPropertiesRegistrar.aidl +++ b/core/java/android/os/IBatteryPropertiesRegistrar.aidl @@ -17,6 +17,7 @@ package android.os; import android.os.IBatteryPropertiesListener; +import android.os.BatteryProperty; /** * {@hide} @@ -25,4 +26,5 @@ import android.os.IBatteryPropertiesListener; interface IBatteryPropertiesRegistrar { void registerListener(IBatteryPropertiesListener listener); void unregisterListener(IBatteryPropertiesListener listener); + int getProperty(in int id, out BatteryProperty prop); } diff --git a/core/java/android/os/IBinder.java b/core/java/android/os/IBinder.java index a2432d6..73a0f65 100644 --- a/core/java/android/os/IBinder.java +++ b/core/java/android/os/IBinder.java @@ -17,7 +17,6 @@ package android.os; import java.io.FileDescriptor; -import java.io.PrintWriter; /** * Base interface for a remotable object, the core part of a lightweight diff --git a/core/java/android/os/Looper.java b/core/java/android/os/Looper.java index 21e9f6b..ff31130 100644 --- a/core/java/android/os/Looper.java +++ b/core/java/android/os/Looper.java @@ -18,7 +18,6 @@ package android.os; import android.util.Log; import android.util.Printer; -import android.util.PrefixPrinter; /** * Class used to run a message loop for a thread. Threads by default do diff --git a/core/java/android/os/MessageQueue.java b/core/java/android/os/MessageQueue.java index 799de5c..e90a457 100644 --- a/core/java/android/os/MessageQueue.java +++ b/core/java/android/os/MessageQueue.java @@ -126,6 +126,14 @@ public final class MessageQueue { } 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 + // which is not supported. + final int ptr = mPtr; + if (ptr == 0) { + return null; + } + int pendingIdleHandlerCount = -1; // -1 only during first iteration int nextPollTimeoutMillis = 0; for (;;) { @@ -133,9 +141,7 @@ public final class MessageQueue { Binder.flushPendingCommands(); } - // We can assume mPtr != 0 because the loop is obviously still running. - // The looper will not call this method after the loop quits. - nativePollOnce(mPtr, nextPollTimeoutMillis); + nativePollOnce(ptr, nextPollTimeoutMillis); synchronized (this) { // Try to retrieve the next message. Return if found. diff --git a/core/java/android/os/NullVibrator.java b/core/java/android/os/NullVibrator.java index ac6027f..af90bdb 100644 --- a/core/java/android/os/NullVibrator.java +++ b/core/java/android/os/NullVibrator.java @@ -16,8 +16,6 @@ package android.os; -import android.util.Log; - /** * Vibrator implementation that does nothing. * diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java index 5273c20..86dc8b4 100644 --- a/core/java/android/os/ParcelFileDescriptor.java +++ b/core/java/android/os/ParcelFileDescriptor.java @@ -872,6 +872,8 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { */ @Override public void writeToParcel(Parcel out, int flags) { + // WARNING: This must stay in sync with Parcel::readParcelFileDescriptor() + // in frameworks/native/libs/binder/Parcel.cpp if (mWrapped != null) { try { mWrapped.writeToParcel(out, flags); @@ -897,6 +899,8 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { = new Parcelable.Creator<ParcelFileDescriptor>() { @Override public ParcelFileDescriptor createFromParcel(Parcel in) { + // WARNING: This must stay in sync with Parcel::writeParcelFileDescriptor() + // in frameworks/native/libs/binder/Parcel.cpp final FileDescriptor fd = in.readRawFileDescriptor(); FileDescriptor commChannel = null; if (in.readInt() != 0) { diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java index 631edd6..057f516 100644 --- a/core/java/android/os/Process.java +++ b/core/java/android/os/Process.java @@ -892,19 +892,6 @@ public class Process { } /** - * Set the out-of-memory badness adjustment for a process. - * - * @param pid The process identifier to set. - * @param amt Adjustment value -- linux allows -16 to +15. - * - * @return Returns true if the underlying system supports this - * feature, else false. - * - * {@hide} - */ - public static final native boolean setOomAdj(int pid, int amt); - - /** * Adjust the swappiness level for a process. * * @param pid The process identifier to set. diff --git a/core/java/android/os/Registrant.java b/core/java/android/os/Registrant.java index c1780b9..705cc5d 100644 --- a/core/java/android/os/Registrant.java +++ b/core/java/android/os/Registrant.java @@ -20,7 +20,6 @@ import android.os.Handler; import android.os.Message; import java.lang.ref.WeakReference; -import java.util.HashMap; /** @hide */ public class Registrant diff --git a/core/java/android/os/RegistrantList.java b/core/java/android/os/RegistrantList.java index 56b9e2b..9ab61f5 100644 --- a/core/java/android/os/RegistrantList.java +++ b/core/java/android/os/RegistrantList.java @@ -17,10 +17,8 @@ package android.os; import android.os.Handler; -import android.os.Message; import java.util.ArrayList; -import java.util.HashMap; /** @hide */ public class RegistrantList diff --git a/core/java/android/os/SystemProperties.java b/core/java/android/os/SystemProperties.java index 156600e..1479035 100644 --- a/core/java/android/os/SystemProperties.java +++ b/core/java/android/os/SystemProperties.java @@ -18,8 +18,6 @@ package android.os; import java.util.ArrayList; -import android.util.Log; - /** * Gives access to the system properties store. The system properties diff --git a/core/java/android/os/SystemService.java b/core/java/android/os/SystemService.java index f345271..41e7546 100644 --- a/core/java/android/os/SystemService.java +++ b/core/java/android/os/SystemService.java @@ -16,8 +16,6 @@ package android.os; -import android.util.Slog; - import com.google.android.collect.Maps; import java.util.HashMap; diff --git a/core/java/android/os/Trace.java b/core/java/android/os/Trace.java index 3249bcb..57ed979 100644 --- a/core/java/android/os/Trace.java +++ b/core/java/android/os/Trace.java @@ -16,8 +16,6 @@ package android.os; -import android.util.Log; - /** * Writes trace events to the system trace buffer. These trace events can be * collected and visualized using the Systrace tool. diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index a3752a1..5d087ea 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -17,7 +17,6 @@ package android.os; import android.app.ActivityManagerNative; import android.content.Context; -import android.content.RestrictionEntry; import android.content.pm.UserInfo; import android.content.res.Resources; import android.graphics.Bitmap; diff --git a/core/java/android/preference/CheckBoxPreference.java b/core/java/android/preference/CheckBoxPreference.java index 1536760..1ce98b8 100644 --- a/core/java/android/preference/CheckBoxPreference.java +++ b/core/java/android/preference/CheckBoxPreference.java @@ -34,11 +34,16 @@ import android.widget.Checkable; */ public class CheckBoxPreference extends TwoStatePreference { - public CheckBoxPreference(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.CheckBoxPreference, defStyle, 0); + public CheckBoxPreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public CheckBoxPreference( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + final TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.CheckBoxPreference, defStyleAttr, defStyleRes); setSummaryOn(a.getString(com.android.internal.R.styleable.CheckBoxPreference_summaryOn)); setSummaryOff(a.getString(com.android.internal.R.styleable.CheckBoxPreference_summaryOff)); setDisableDependentsState(a.getBoolean( diff --git a/core/java/android/preference/DialogPreference.java b/core/java/android/preference/DialogPreference.java index a643c8a..5275bc0 100644 --- a/core/java/android/preference/DialogPreference.java +++ b/core/java/android/preference/DialogPreference.java @@ -64,12 +64,13 @@ public abstract class DialogPreference extends Preference implements /** Which button was clicked. */ private int mWhichButtonClicked; - - public DialogPreference(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.DialogPreference, defStyle, 0); + + public DialogPreference( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + final TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.DialogPreference, defStyleAttr, defStyleRes); mDialogTitle = a.getString(com.android.internal.R.styleable.DialogPreference_dialogTitle); if (mDialogTitle == null) { // Fallback on the regular title of the preference @@ -83,13 +84,20 @@ public abstract class DialogPreference extends Preference implements mDialogLayoutResId = a.getResourceId(com.android.internal.R.styleable.DialogPreference_dialogLayout, mDialogLayoutResId); a.recycle(); - + } + + public DialogPreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); } public DialogPreference(Context context, AttributeSet attrs) { this(context, attrs, com.android.internal.R.attr.dialogPreferenceStyle); } - + + public DialogPreference(Context context) { + this(context, null); + } + /** * Sets the title of the dialog. This will be shown on subsequent dialogs. * diff --git a/core/java/android/preference/EditTextPreference.java b/core/java/android/preference/EditTextPreference.java index aa27627..ff37042 100644 --- a/core/java/android/preference/EditTextPreference.java +++ b/core/java/android/preference/EditTextPreference.java @@ -49,9 +49,9 @@ public class EditTextPreference extends DialogPreference { private EditText mEditText; private String mText; - - public EditTextPreference(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + + public EditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); mEditText = new EditText(context, attrs); @@ -67,6 +67,10 @@ public class EditTextPreference extends DialogPreference { mEditText.setEnabled(true); } + public EditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + public EditTextPreference(Context context, AttributeSet attrs) { this(context, attrs, com.android.internal.R.attr.editTextPreferenceStyle); } diff --git a/core/java/android/preference/ListPreference.java b/core/java/android/preference/ListPreference.java index 9edf112..8081a54 100644 --- a/core/java/android/preference/ListPreference.java +++ b/core/java/android/preference/ListPreference.java @@ -42,12 +42,12 @@ public class ListPreference extends DialogPreference { private String mSummary; private int mClickedDialogEntryIndex; private boolean mValueSet; - - public ListPreference(Context context, AttributeSet attrs) { - super(context, attrs); - - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.ListPreference, 0, 0); + + public ListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.ListPreference, defStyleAttr, defStyleRes); mEntries = a.getTextArray(com.android.internal.R.styleable.ListPreference_entries); mEntryValues = a.getTextArray(com.android.internal.R.styleable.ListPreference_entryValues); a.recycle(); @@ -56,11 +56,19 @@ public class ListPreference extends DialogPreference { * in the Preference class. */ a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.Preference, 0, 0); + com.android.internal.R.styleable.Preference, defStyleAttr, defStyleRes); mSummary = a.getString(com.android.internal.R.styleable.Preference_summary); a.recycle(); } + public ListPreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ListPreference(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.dialogPreferenceStyle); + } + public ListPreference(Context context) { this(context, null); } diff --git a/core/java/android/preference/MultiCheckPreference.java b/core/java/android/preference/MultiCheckPreference.java index 6953075..57c906d 100644 --- a/core/java/android/preference/MultiCheckPreference.java +++ b/core/java/android/preference/MultiCheckPreference.java @@ -40,12 +40,13 @@ public class MultiCheckPreference extends DialogPreference { private boolean[] mSetValues; private boolean[] mOrigValues; private String mSummary; - - public MultiCheckPreference(Context context, AttributeSet attrs) { - super(context, attrs); - - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.ListPreference, 0, 0); + + public MultiCheckPreference( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.ListPreference, defStyleAttr, defStyleRes); mEntries = a.getTextArray(com.android.internal.R.styleable.ListPreference_entries); if (mEntries != null) { setEntries(mEntries); @@ -63,6 +64,14 @@ public class MultiCheckPreference extends DialogPreference { a.recycle(); } + public MultiCheckPreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public MultiCheckPreference(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.dialogPreferenceStyle); + } + public MultiCheckPreference(Context context) { this(context, null); } diff --git a/core/java/android/preference/MultiSelectListPreference.java b/core/java/android/preference/MultiSelectListPreference.java index 553ce80..6c4c20f 100644 --- a/core/java/android/preference/MultiSelectListPreference.java +++ b/core/java/android/preference/MultiSelectListPreference.java @@ -44,16 +44,26 @@ public class MultiSelectListPreference extends DialogPreference { private Set<String> mValues = new HashSet<String>(); private Set<String> mNewValues = new HashSet<String>(); private boolean mPreferenceChanged; - - public MultiSelectListPreference(Context context, AttributeSet attrs) { - super(context, attrs); - - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.MultiSelectListPreference, 0, 0); + + public MultiSelectListPreference( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + final TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.MultiSelectListPreference, defStyleAttr, + defStyleRes); mEntries = a.getTextArray(com.android.internal.R.styleable.MultiSelectListPreference_entries); mEntryValues = a.getTextArray(com.android.internal.R.styleable.MultiSelectListPreference_entryValues); a.recycle(); } + + public MultiSelectListPreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public MultiSelectListPreference(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.dialogPreferenceStyle); + } public MultiSelectListPreference(Context context) { this(context, null); diff --git a/core/java/android/preference/Preference.java b/core/java/android/preference/Preference.java index f7d1eb7..76fccc7 100644 --- a/core/java/android/preference/Preference.java +++ b/core/java/android/preference/Preference.java @@ -188,30 +188,33 @@ public class Preference implements Comparable<Preference> { /** * Perform inflation from XML and apply a class-specific base style. This - * constructor of Preference allows subclasses to use their own base - * style when they are inflating. For example, a {@link CheckBoxPreference} + * constructor of Preference allows subclasses to use their own base style + * when they are inflating. For example, a {@link CheckBoxPreference} * constructor calls this version of the super class constructor and - * supplies {@code android.R.attr.checkBoxPreferenceStyle} for <var>defStyle</var>. - * This allows the theme's checkbox preference style to modify all of the base - * preference attributes as well as the {@link CheckBoxPreference} class's - * attributes. - * + * supplies {@code android.R.attr.checkBoxPreferenceStyle} for + * <var>defStyleAttr</var>. This allows the theme's checkbox preference + * style to modify all of the base preference attributes as well as the + * {@link CheckBoxPreference} class's attributes. + * * @param context The Context this is associated with, through which it can - * access the current theme, resources, {@link SharedPreferences}, - * etc. - * @param attrs The attributes of the XML tag that is inflating the preference. - * @param defStyle The default style to apply to this preference. If 0, no style - * will be applied (beyond what is included in the theme). This - * may either be an attribute resource, whose value will be - * retrieved from the current theme, or an explicit style - * resource. + * access the current theme, resources, + * {@link SharedPreferences}, etc. + * @param attrs The attributes of the XML tag that is inflating the + * preference. + * @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. * @see #Preference(Context, AttributeSet) */ - public Preference(Context context, AttributeSet attrs, int defStyle) { + public Preference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { mContext = context; - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.Preference, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.Preference, defStyleAttr, defStyleRes); for (int i = a.getIndexCount(); i >= 0; i--) { int attr = a.getIndex(i); switch (attr) { @@ -281,6 +284,30 @@ public class Preference implements Comparable<Preference> { mCanRecycleLayout = false; } } + + /** + * Perform inflation from XML and apply a class-specific base style. This + * constructor of Preference allows subclasses to use their own base style + * when they are inflating. For example, a {@link CheckBoxPreference} + * constructor calls this version of the super class constructor and + * supplies {@code android.R.attr.checkBoxPreferenceStyle} for + * <var>defStyleAttr</var>. This allows the theme's checkbox preference + * style to modify all of the base preference attributes as well as the + * {@link CheckBoxPreference} class's attributes. + * + * @param context The Context this is associated with, through which it can + * access the current theme, resources, + * {@link SharedPreferences}, etc. + * @param attrs The attributes of the XML tag that is inflating the + * preference. + * @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. + * @see #Preference(Context, AttributeSet) + */ + public Preference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } /** * Constructor that is called when inflating a Preference from XML. This is diff --git a/core/java/android/preference/PreferenceCategory.java b/core/java/android/preference/PreferenceCategory.java index 229a96a..253481b 100644 --- a/core/java/android/preference/PreferenceCategory.java +++ b/core/java/android/preference/PreferenceCategory.java @@ -16,8 +16,6 @@ package android.preference; -import java.util.Map; - import android.content.Context; import android.util.AttributeSet; @@ -34,9 +32,14 @@ import android.util.AttributeSet; */ public class PreferenceCategory extends PreferenceGroup { private static final String TAG = "PreferenceCategory"; - - public PreferenceCategory(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + + public PreferenceCategory( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public PreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); } public PreferenceCategory(Context context, AttributeSet attrs) { diff --git a/core/java/android/preference/PreferenceFrameLayout.java b/core/java/android/preference/PreferenceFrameLayout.java index 75372aa..886338f 100644 --- a/core/java/android/preference/PreferenceFrameLayout.java +++ b/core/java/android/preference/PreferenceFrameLayout.java @@ -16,7 +16,6 @@ package android.preference; -import android.app.FragmentBreadCrumbs; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; @@ -45,10 +44,15 @@ public class PreferenceFrameLayout extends FrameLayout { this(context, attrs, com.android.internal.R.attr.preferenceFrameLayoutStyle); } - public PreferenceFrameLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.PreferenceFrameLayout, defStyle, 0); + public PreferenceFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public PreferenceFrameLayout( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + final TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.PreferenceFrameLayout, defStyleAttr, defStyleRes); float density = context.getResources().getDisplayMetrics().density; int defaultBorderTop = (int) (density * DEFAULT_BORDER_TOP + 0.5f); diff --git a/core/java/android/preference/PreferenceGroup.java b/core/java/android/preference/PreferenceGroup.java index 5f8c78d..2d35b1b 100644 --- a/core/java/android/preference/PreferenceGroup.java +++ b/core/java/android/preference/PreferenceGroup.java @@ -55,19 +55,23 @@ public abstract class PreferenceGroup extends Preference implements GenericInfla private int mCurrentPreferenceOrder = 0; private boolean mAttachedToActivity = false; - - public PreferenceGroup(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + + public PreferenceGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); mPreferenceList = new ArrayList<Preference>(); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.PreferenceGroup, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.PreferenceGroup, defStyleAttr, defStyleRes); mOrderingAsAdded = a.getBoolean(com.android.internal.R.styleable.PreferenceGroup_orderingFromXml, mOrderingAsAdded); a.recycle(); } + public PreferenceGroup(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + public PreferenceGroup(Context context, AttributeSet attrs) { this(context, attrs, 0); } diff --git a/core/java/android/preference/PreferenceInflater.java b/core/java/android/preference/PreferenceInflater.java index c21aa18..727fbca 100644 --- a/core/java/android/preference/PreferenceInflater.java +++ b/core/java/android/preference/PreferenceInflater.java @@ -19,16 +19,13 @@ package android.preference; import com.android.internal.util.XmlUtils; import java.io.IOException; -import java.util.Map; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; -import android.app.AliasActivity; import android.content.Context; import android.content.Intent; import android.util.AttributeSet; -import android.util.Log; /** * The {@link PreferenceInflater} is used to inflate preference hierarchies from diff --git a/core/java/android/preference/PreferenceScreen.java b/core/java/android/preference/PreferenceScreen.java index db80676..b1317e6 100644 --- a/core/java/android/preference/PreferenceScreen.java +++ b/core/java/android/preference/PreferenceScreen.java @@ -27,7 +27,6 @@ import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.Window; -import android.widget.AbsListView; import android.widget.Adapter; import android.widget.AdapterView; import android.widget.ListAdapter; diff --git a/core/java/android/preference/RingtonePreference.java b/core/java/android/preference/RingtonePreference.java index 2ebf294..488a0c4 100644 --- a/core/java/android/preference/RingtonePreference.java +++ b/core/java/android/preference/RingtonePreference.java @@ -50,11 +50,11 @@ public class RingtonePreference extends Preference implements private int mRequestCode; - public RingtonePreference(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.RingtonePreference, defStyle, 0); + public RingtonePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + final TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.RingtonePreference, defStyleAttr, defStyleRes); mRingtoneType = a.getInt(com.android.internal.R.styleable.RingtonePreference_ringtoneType, RingtoneManager.TYPE_RINGTONE); mShowDefault = a.getBoolean(com.android.internal.R.styleable.RingtonePreference_showDefault, @@ -64,6 +64,10 @@ public class RingtonePreference extends Preference implements a.recycle(); } + public RingtonePreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + public RingtonePreference(Context context, AttributeSet attrs) { this(context, attrs, com.android.internal.R.attr.ringtonePreferenceStyle); } diff --git a/core/java/android/preference/SeekBarDialogPreference.java b/core/java/android/preference/SeekBarDialogPreference.java index 0e89b16..9a08827 100644 --- a/core/java/android/preference/SeekBarDialogPreference.java +++ b/core/java/android/preference/SeekBarDialogPreference.java @@ -32,8 +32,9 @@ public class SeekBarDialogPreference extends DialogPreference { private Drawable mMyIcon; - public SeekBarDialogPreference(Context context, AttributeSet attrs) { - super(context, attrs); + public SeekBarDialogPreference( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); setDialogLayoutResource(com.android.internal.R.layout.seekbar_dialog); createActionButtons(); @@ -43,6 +44,18 @@ public class SeekBarDialogPreference extends DialogPreference { setDialogIcon(null); } + public SeekBarDialogPreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public SeekBarDialogPreference(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.dialogPreferenceStyle); + } + + public SeekBarDialogPreference(Context context) { + this(context, null); + } + // Allow subclasses to override the action buttons public void createActionButtons() { setPositiveButtonText(android.R.string.ok); diff --git a/core/java/android/preference/SeekBarPreference.java b/core/java/android/preference/SeekBarPreference.java index 7133d3a..e32890d 100644 --- a/core/java/android/preference/SeekBarPreference.java +++ b/core/java/android/preference/SeekBarPreference.java @@ -37,15 +37,20 @@ public class SeekBarPreference extends Preference private boolean mTrackingTouch; public SeekBarPreference( - Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.ProgressBar, defStyle, 0); + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.ProgressBar, defStyleAttr, defStyleRes); setMax(a.getInt(com.android.internal.R.styleable.ProgressBar_max, mMax)); a.recycle(); setLayoutResource(com.android.internal.R.layout.preference_widget_seekbar); } + public SeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + public SeekBarPreference(Context context, AttributeSet attrs) { this(context, attrs, 0); } diff --git a/core/java/android/preference/SwitchPreference.java b/core/java/android/preference/SwitchPreference.java index 8bac6bd..76ef544 100644 --- a/core/java/android/preference/SwitchPreference.java +++ b/core/java/android/preference/SwitchPreference.java @@ -60,13 +60,19 @@ public class SwitchPreference extends TwoStatePreference { * * @param context The Context that will style this preference * @param attrs Style attributes that differ from the default - * @param defStyle Theme attribute defining the default style options + * @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. */ - public SwitchPreference(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public SwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.SwitchPreference, defStyle, 0); + com.android.internal.R.styleable.SwitchPreference, defStyleAttr, defStyleRes); setSummaryOn(a.getString(com.android.internal.R.styleable.SwitchPreference_summaryOn)); setSummaryOff(a.getString(com.android.internal.R.styleable.SwitchPreference_summaryOff)); setSwitchTextOn(a.getString( @@ -83,6 +89,19 @@ public class SwitchPreference extends TwoStatePreference { * * @param context The Context that will style this preference * @param attrs Style attributes that differ from the default + * @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. + */ + public SwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + /** + * Construct a new SwitchPreference with the given style options. + * + * @param context The Context that will style this preference + * @param attrs Style attributes that differ from the default */ public SwitchPreference(Context context, AttributeSet attrs) { this(context, attrs, com.android.internal.R.attr.switchPreferenceStyle); diff --git a/core/java/android/preference/TwoStatePreference.java b/core/java/android/preference/TwoStatePreference.java index af83953..6f8be1f 100644 --- a/core/java/android/preference/TwoStatePreference.java +++ b/core/java/android/preference/TwoStatePreference.java @@ -42,9 +42,13 @@ public abstract class TwoStatePreference extends Preference { private boolean mSendClickAccessibilityEvent; private boolean mDisableDependentsState; + public TwoStatePreference( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } - public TwoStatePreference(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public TwoStatePreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); } public TwoStatePreference(Context context, AttributeSet attrs) { diff --git a/core/java/android/preference/VolumePreference.java b/core/java/android/preference/VolumePreference.java index dc683a6..29f2545 100644 --- a/core/java/android/preference/VolumePreference.java +++ b/core/java/android/preference/VolumePreference.java @@ -51,15 +51,24 @@ public class VolumePreference extends SeekBarDialogPreference implements /** May be null if the dialog isn't visible. */ private SeekBarVolumizer mSeekBarVolumizer; - public VolumePreference(Context context, AttributeSet attrs) { - super(context, attrs); + public VolumePreference( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.VolumePreference, 0, 0); + final TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.VolumePreference, defStyleAttr, defStyleRes); mStreamType = a.getInt(android.R.styleable.VolumePreference_streamType, 0); a.recycle(); } + public VolumePreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public VolumePreference(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + public void setStreamType(int streamType) { mStreamType = streamType; } diff --git a/core/java/android/provider/Contacts.java b/core/java/android/provider/Contacts.java index c7e3c08..9e2aacd 100644 --- a/core/java/android/provider/Contacts.java +++ b/core/java/android/provider/Contacts.java @@ -26,7 +26,6 @@ import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; -import android.os.Build; import android.text.TextUtils; import android.util.Log; import android.widget.ImageView; diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java index b16df28..daa9881 100644 --- a/core/java/android/provider/ContactsContract.java +++ b/core/java/android/provider/ContactsContract.java @@ -45,9 +45,6 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; /** * <p> diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 04f3f0a..0dffc17 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -3764,6 +3764,97 @@ public final class Settings { "accessibility_captioning_font_scale"; /** + * Setting that specifies whether the quick setting tile for display + * color inversion is enabled. + * + * @hide + */ + public static final String ACCESSIBILITY_DISPLAY_INVERSION_QUICK_SETTING_ENABLED = + "accessibility_display_inversion_quick_setting_enabled"; + + /** + * Setting that specifies whether display color inversion is enabled. + * + * @hide + */ + public static final String ACCESSIBILITY_DISPLAY_INVERSION_ENABLED = + "accessibility_display_inversion_enabled"; + + /** + * Integer property that specifies the type of color inversion to + * perform. Valid values are defined in AccessibilityManager. + * + * @hide + */ + public static final String ACCESSIBILITY_DISPLAY_INVERSION = + "accessibility_display_inversion"; + + /** + * Setting that specifies whether the quick setting tile for display + * color space adjustment is enabled. + * + * @hide + */ + public static final String ACCESSIBILITY_DISPLAY_DALTONIZER_QUICK_SETTING_ENABLED = + "accessibility_display_daltonizer_quick_setting_enabled"; + + /** + * Setting that specifies whether display color space adjustment is + * enabled. + * + * @hide + */ + public static final String ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED = + "accessibility_display_daltonizer_enabled"; + + /** + * Integer property that specifies the type of color space adjustment to + * perform. Valid values are defined in AccessibilityManager. + * + * @hide + */ + public static final String ACCESSIBILITY_DISPLAY_DALTONIZER = + "accessibility_display_daltonizer"; + + /** + * Setting that specifies whether the quick setting tile for display + * contrast enhancement is enabled. + * + * @hide + */ + public static final String ACCESSIBILITY_DISPLAY_CONTRAST_QUICK_SETTING_ENABLED = + "accessibility_display_contrast_quick_setting_enabled"; + + /** + * Setting that specifies whether display contrast enhancement is + * enabled. + * + * @hide + */ + public static final String ACCESSIBILITY_DISPLAY_CONTRAST_ENABLED = + "accessibility_display_contrast_enabled"; + + /** + * Floating point property that specifies display contrast adjustment. + * Valid range is [0, ...] where 0 is gray, 1 is normal, and higher + * values indicate enhanced contrast. + * + * @hide + */ + public static final String ACCESSIBILITY_DISPLAY_CONTRAST = + "accessibility_display_contrast"; + + /** + * Floating point property that specifies display brightness adjustment. + * Valid range is [-1, 1] where -1 is black, 0 is default, and 1 is + * white. + * + * @hide + */ + public static final String ACCESSIBILITY_DISPLAY_BRIGHTNESS = + "accessibility_display_brightness"; + + /** * The timout for considering a press to be a long press in milliseconds. * @hide */ diff --git a/core/java/android/service/textservice/SpellCheckerService.java b/core/java/android/service/textservice/SpellCheckerService.java index 77b22ed..acfef82 100644 --- a/core/java/android/service/textservice/SpellCheckerService.java +++ b/core/java/android/service/textservice/SpellCheckerService.java @@ -32,7 +32,6 @@ import android.util.Log; import android.view.textservice.SentenceSuggestionsInfo; import android.view.textservice.SuggestionsInfo; import android.view.textservice.TextInfo; -import android.widget.SpellChecker; import java.lang.ref.WeakReference; import java.text.BreakIterator; diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 5db8168..03ce4e0 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -38,7 +38,6 @@ import android.os.Message; import android.os.PowerManager; import android.os.RemoteException; import android.util.Log; -import android.util.LogPrinter; import android.view.Display; import android.view.Gravity; import android.view.IWindowSession; diff --git a/core/java/android/speech/srec/Recognizer.java b/core/java/android/speech/srec/Recognizer.java index db5d8fd..4bdaf5b 100644 --- a/core/java/android/speech/srec/Recognizer.java +++ b/core/java/android/speech/srec/Recognizer.java @@ -22,8 +22,6 @@ package android.speech.srec; -import android.util.Log; - import java.io.File; import java.io.InputStream; import java.io.IOException; diff --git a/core/java/android/speech/tts/AbstractEventLogger.java b/core/java/android/speech/tts/AbstractEventLogger.java new file mode 100644 index 0000000..37f8656 --- /dev/null +++ b/core/java/android/speech/tts/AbstractEventLogger.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package android.speech.tts; + +import android.os.SystemClock; + +/** + * Base class for storing data about a given speech synthesis request to the + * event logs. The data that is logged depends on actual implementation. Note + * that {@link AbstractEventLogger#onAudioDataWritten()} and + * {@link AbstractEventLogger#onEngineComplete()} must be called from a single + * thread (usually the audio playback thread}. + */ +abstract class AbstractEventLogger { + protected final String mServiceApp; + protected final int mCallerUid; + protected final int mCallerPid; + protected final long mReceivedTime; + protected long mPlaybackStartTime = -1; + + private volatile long mRequestProcessingStartTime = -1; + private volatile long mEngineStartTime = -1; + private volatile long mEngineCompleteTime = -1; + + private boolean mLogWritten = false; + + AbstractEventLogger(int callerUid, int callerPid, String serviceApp) { + mCallerUid = callerUid; + mCallerPid = callerPid; + mServiceApp = serviceApp; + mReceivedTime = SystemClock.elapsedRealtime(); + } + + /** + * Notifies the logger that this request has been selected from + * the processing queue for processing. Engine latency / total time + * is measured from this baseline. + */ + public void onRequestProcessingStart() { + mRequestProcessingStartTime = SystemClock.elapsedRealtime(); + } + + /** + * Notifies the logger that a chunk of data has been received from + * the engine. Might be called multiple times. + */ + public void onEngineDataReceived() { + if (mEngineStartTime == -1) { + mEngineStartTime = SystemClock.elapsedRealtime(); + } + } + + /** + * Notifies the logger that the engine has finished processing data. + * Will be called exactly once. + */ + public void onEngineComplete() { + mEngineCompleteTime = SystemClock.elapsedRealtime(); + } + + /** + * Notifies the logger that audio playback has started for some section + * of the synthesis. This is normally some amount of time after the engine + * has synthesized data and varies depending on utterances and + * other audio currently in the queue. + */ + public void onAudioDataWritten() { + // For now, keep track of only the first chunk of audio + // that was played. + if (mPlaybackStartTime == -1) { + mPlaybackStartTime = SystemClock.elapsedRealtime(); + } + } + + /** + * Notifies the logger that the current synthesis has completed. + * All available data is not logged. + */ + public void onCompleted(int statusCode) { + if (mLogWritten) { + return; + } else { + mLogWritten = true; + } + + long completionTime = SystemClock.elapsedRealtime(); + + // We don't report latency for stopped syntheses because their overall + // total time spent will be inaccurate (will not correlate with + // the length of the utterance). + + // onAudioDataWritten() should normally always be called, and hence mPlaybackStartTime + // should be set, if an error does not occur. + if (statusCode != TextToSpeechClient.Status.SUCCESS + || mPlaybackStartTime == -1 || mEngineCompleteTime == -1) { + logFailure(statusCode); + return; + } + + final long audioLatency = mPlaybackStartTime - mReceivedTime; + final long engineLatency = mEngineStartTime - mRequestProcessingStartTime; + final long engineTotal = mEngineCompleteTime - mRequestProcessingStartTime; + logSuccess(audioLatency, engineLatency, engineTotal); + } + + protected abstract void logFailure(int statusCode); + protected abstract void logSuccess(long audioLatency, long engineLatency, + long engineTotal); + + +} diff --git a/core/java/android/speech/tts/AbstractSynthesisCallback.java b/core/java/android/speech/tts/AbstractSynthesisCallback.java index c7a4af0..91e119b 100644 --- a/core/java/android/speech/tts/AbstractSynthesisCallback.java +++ b/core/java/android/speech/tts/AbstractSynthesisCallback.java @@ -15,15 +15,28 @@ */ package android.speech.tts; + /** * Defines additional methods the synthesis callback must implement that * are private to the TTS service implementation. + * + * All of these class methods (with the exception of {@link #stop()}) can be only called on the + * synthesis thread, while inside + * {@link TextToSpeechService#onSynthesizeText} or {@link TextToSpeechService#onSynthesizeTextV2}. + * {@link #stop()} is the exception, it may be called from multiple threads. */ abstract class AbstractSynthesisCallback implements SynthesisCallback { + /** If true, request comes from V2 TTS interface */ + protected final boolean mClientIsUsingV2; + /** - * Checks whether the synthesis request completed successfully. + * Constructor. + * @param clientIsUsingV2 If true, this callback will be used inside + * {@link TextToSpeechService#onSynthesizeTextV2} method. */ - abstract boolean isDone(); + AbstractSynthesisCallback(boolean clientIsUsingV2) { + mClientIsUsingV2 = clientIsUsingV2; + } /** * Aborts the speech request. @@ -31,4 +44,16 @@ abstract class AbstractSynthesisCallback implements SynthesisCallback { * Can be called from multiple threads. */ abstract void stop(); + + /** + * Get status code for a "stop". + * + * V2 Clients will receive special status, V1 clients will receive standard error. + * + * This method should only be called on the synthesis thread, + * while in {@link TextToSpeechService#onSynthesizeText}. + */ + int errorCodeOnStop() { + return mClientIsUsingV2 ? TextToSpeechClient.Status.STOPPED : TextToSpeech.ERROR; + } } diff --git a/core/java/android/speech/tts/AudioPlaybackHandler.java b/core/java/android/speech/tts/AudioPlaybackHandler.java index d63f605..dcf49b0 100644 --- a/core/java/android/speech/tts/AudioPlaybackHandler.java +++ b/core/java/android/speech/tts/AudioPlaybackHandler.java @@ -43,7 +43,7 @@ class AudioPlaybackHandler { return; } - item.stop(false); + item.stop(TextToSpeechClient.Status.STOPPED); } public void enqueue(PlaybackQueueItem item) { diff --git a/core/java/android/speech/tts/AudioPlaybackQueueItem.java b/core/java/android/speech/tts/AudioPlaybackQueueItem.java index 1a1fda8..c514639 100644 --- a/core/java/android/speech/tts/AudioPlaybackQueueItem.java +++ b/core/java/android/speech/tts/AudioPlaybackQueueItem.java @@ -53,7 +53,7 @@ class AudioPlaybackQueueItem extends PlaybackQueueItem { dispatcher.dispatchOnStart(); mPlayer = MediaPlayer.create(mContext, mUri); if (mPlayer == null) { - dispatcher.dispatchOnError(); + dispatcher.dispatchOnError(TextToSpeechClient.Status.ERROR_OUTPUT); return; } @@ -83,9 +83,9 @@ class AudioPlaybackQueueItem extends PlaybackQueueItem { } if (mFinished) { - dispatcher.dispatchOnDone(); + dispatcher.dispatchOnSuccess(); } else { - dispatcher.dispatchOnError(); + dispatcher.dispatchOnStop(); } } @@ -99,7 +99,7 @@ class AudioPlaybackQueueItem extends PlaybackQueueItem { } @Override - void stop(boolean isError) { + void stop(int errorCode) { mDone.open(); } } diff --git a/core/java/android/speech/tts/EventLogTags.logtags b/core/java/android/speech/tts/EventLogTags.logtags index f8654ad..e209a28 100644 --- a/core/java/android/speech/tts/EventLogTags.logtags +++ b/core/java/android/speech/tts/EventLogTags.logtags @@ -4,3 +4,6 @@ option java_package android.speech.tts; 76001 tts_speak_success (engine|3),(caller_uid|1),(caller_pid|1),(length|1),(locale|3),(rate|1),(pitch|1),(engine_latency|2|3),(engine_total|2|3),(audio_latency|2|3) 76002 tts_speak_failure (engine|3),(caller_uid|1),(caller_pid|1),(length|1),(locale|3),(rate|1),(pitch|1) + +76003 tts_v2_speak_success (engine|3),(caller_uid|1),(caller_pid|1),(length|1),(request_config|3),(engine_latency|2|3),(engine_total|2|3),(audio_latency|2|3) +76004 tts_v2_speak_failure (engine|3),(caller_uid|1),(caller_pid|1),(length|1),(request_config|3), (statusCode|1) diff --git a/core/java/android/speech/tts/EventLogger.java b/core/java/android/speech/tts/EventLogger.java deleted file mode 100644 index 82ed4dd..0000000 --- a/core/java/android/speech/tts/EventLogger.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package android.speech.tts; - -import android.os.SystemClock; -import android.text.TextUtils; -import android.util.Log; - -/** - * Writes data about a given speech synthesis request to the event logs. - * The data that is logged includes the calling app, length of the utterance, - * speech rate / pitch and the latency and overall time taken. - * - * Note that {@link EventLogger#onStopped()} and {@link EventLogger#onError()} - * might be called from any thread, but on {@link EventLogger#onAudioDataWritten()} and - * {@link EventLogger#onComplete()} must be called from a single thread - * (usually the audio playback thread} - */ -class EventLogger { - private final SynthesisRequest mRequest; - private final String mServiceApp; - private final int mCallerUid; - private final int mCallerPid; - private final long mReceivedTime; - private long mPlaybackStartTime = -1; - private volatile long mRequestProcessingStartTime = -1; - private volatile long mEngineStartTime = -1; - private volatile long mEngineCompleteTime = -1; - - private volatile boolean mError = false; - private volatile boolean mStopped = false; - private boolean mLogWritten = false; - - EventLogger(SynthesisRequest request, int callerUid, int callerPid, String serviceApp) { - mRequest = request; - mCallerUid = callerUid; - mCallerPid = callerPid; - mServiceApp = serviceApp; - mReceivedTime = SystemClock.elapsedRealtime(); - } - - /** - * Notifies the logger that this request has been selected from - * the processing queue for processing. Engine latency / total time - * is measured from this baseline. - */ - public void onRequestProcessingStart() { - mRequestProcessingStartTime = SystemClock.elapsedRealtime(); - } - - /** - * Notifies the logger that a chunk of data has been received from - * the engine. Might be called multiple times. - */ - public void onEngineDataReceived() { - if (mEngineStartTime == -1) { - mEngineStartTime = SystemClock.elapsedRealtime(); - } - } - - /** - * Notifies the logger that the engine has finished processing data. - * Will be called exactly once. - */ - public void onEngineComplete() { - mEngineCompleteTime = SystemClock.elapsedRealtime(); - } - - /** - * Notifies the logger that audio playback has started for some section - * of the synthesis. This is normally some amount of time after the engine - * has synthesized data and varies depending on utterances and - * other audio currently in the queue. - */ - public void onAudioDataWritten() { - // For now, keep track of only the first chunk of audio - // that was played. - if (mPlaybackStartTime == -1) { - mPlaybackStartTime = SystemClock.elapsedRealtime(); - } - } - - /** - * Notifies the logger that the current synthesis was stopped. - * Latency numbers are not reported for stopped syntheses. - */ - public void onStopped() { - mStopped = false; - } - - /** - * Notifies the logger that the current synthesis resulted in - * an error. This is logged using {@link EventLogTags#writeTtsSpeakFailure}. - */ - public void onError() { - mError = true; - } - - /** - * Notifies the logger that the current synthesis has completed. - * All available data is not logged. - */ - public void onWriteData() { - if (mLogWritten) { - return; - } else { - mLogWritten = true; - } - - long completionTime = SystemClock.elapsedRealtime(); - // onAudioDataWritten() should normally always be called if an - // error does not occur. - if (mError || mPlaybackStartTime == -1 || mEngineCompleteTime == -1) { - EventLogTags.writeTtsSpeakFailure(mServiceApp, mCallerUid, mCallerPid, - getUtteranceLength(), getLocaleString(), - mRequest.getSpeechRate(), mRequest.getPitch()); - return; - } - - // We don't report stopped syntheses because their overall - // total time spent will be innacurate (will not correlate with - // the length of the utterance). - if (mStopped) { - return; - } - - final long audioLatency = mPlaybackStartTime - mReceivedTime; - final long engineLatency = mEngineStartTime - mRequestProcessingStartTime; - final long engineTotal = mEngineCompleteTime - mRequestProcessingStartTime; - - EventLogTags.writeTtsSpeakSuccess(mServiceApp, mCallerUid, mCallerPid, - getUtteranceLength(), getLocaleString(), - mRequest.getSpeechRate(), mRequest.getPitch(), - engineLatency, engineTotal, audioLatency); - } - - /** - * @return the length of the utterance for the given synthesis, 0 - * if the utterance was {@code null}. - */ - private int getUtteranceLength() { - final String utterance = mRequest.getText(); - return utterance == null ? 0 : utterance.length(); - } - - /** - * Returns a formatted locale string from the synthesis params of the - * form lang-country-variant. - */ - private String getLocaleString() { - StringBuilder sb = new StringBuilder(mRequest.getLanguage()); - if (!TextUtils.isEmpty(mRequest.getCountry())) { - sb.append('-'); - sb.append(mRequest.getCountry()); - - if (!TextUtils.isEmpty(mRequest.getVariant())) { - sb.append('-'); - sb.append(mRequest.getVariant()); - } - } - - return sb.toString(); - } - -} diff --git a/core/java/android/speech/tts/EventLoggerV1.java b/core/java/android/speech/tts/EventLoggerV1.java new file mode 100644 index 0000000..f484347 --- /dev/null +++ b/core/java/android/speech/tts/EventLoggerV1.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package android.speech.tts; + +import android.text.TextUtils; + +/** + * Writes data about a given speech synthesis request for V1 API to the event + * logs. The data that is logged includes the calling app, length of the + * utterance, speech rate / pitch, the latency, and overall time taken. + */ +class EventLoggerV1 extends AbstractEventLogger { + private final SynthesisRequest mRequest; + + EventLoggerV1(SynthesisRequest request, int callerUid, int callerPid, String serviceApp) { + super(callerUid, callerPid, serviceApp); + mRequest = request; + } + + @Override + protected void logFailure(int statusCode) { + // We don't report stopped syntheses because their overall + // total time spent will be inaccurate (will not correlate with + // the length of the utterance). + if (statusCode != TextToSpeechClient.Status.STOPPED) { + EventLogTags.writeTtsSpeakFailure(mServiceApp, mCallerUid, mCallerPid, + getUtteranceLength(), getLocaleString(), + mRequest.getSpeechRate(), mRequest.getPitch()); + } + } + + @Override + protected void logSuccess(long audioLatency, long engineLatency, long engineTotal) { + EventLogTags.writeTtsSpeakSuccess(mServiceApp, mCallerUid, mCallerPid, + getUtteranceLength(), getLocaleString(), + mRequest.getSpeechRate(), mRequest.getPitch(), + engineLatency, engineTotal, audioLatency); + } + + /** + * @return the length of the utterance for the given synthesis, 0 + * if the utterance was {@code null}. + */ + private int getUtteranceLength() { + final String utterance = mRequest.getText(); + return utterance == null ? 0 : utterance.length(); + } + + /** + * Returns a formatted locale string from the synthesis params of the + * form lang-country-variant. + */ + private String getLocaleString() { + StringBuilder sb = new StringBuilder(mRequest.getLanguage()); + if (!TextUtils.isEmpty(mRequest.getCountry())) { + sb.append('-'); + sb.append(mRequest.getCountry()); + + if (!TextUtils.isEmpty(mRequest.getVariant())) { + sb.append('-'); + sb.append(mRequest.getVariant()); + } + } + + return sb.toString(); + } +} diff --git a/core/java/android/speech/tts/EventLoggerV2.java b/core/java/android/speech/tts/EventLoggerV2.java new file mode 100644 index 0000000..b8e4dae --- /dev/null +++ b/core/java/android/speech/tts/EventLoggerV2.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package android.speech.tts; + + + +/** + * Writes data about a given speech synthesis request for V2 API to the event logs. + * The data that is logged includes the calling app, length of the utterance, + * synthesis request configuration and the latency and overall time taken. + */ +class EventLoggerV2 extends AbstractEventLogger { + private final SynthesisRequestV2 mRequest; + + EventLoggerV2(SynthesisRequestV2 request, int callerUid, int callerPid, String serviceApp) { + super(callerUid, callerPid, serviceApp); + mRequest = request; + } + + @Override + protected void logFailure(int statusCode) { + // We don't report stopped syntheses because their overall + // total time spent will be inaccurate (will not correlate with + // the length of the utterance). + if (statusCode != TextToSpeechClient.Status.STOPPED) { + EventLogTags.writeTtsV2SpeakFailure(mServiceApp, + mCallerUid, mCallerPid, getUtteranceLength(), getRequestConfigString(), statusCode); + } + } + + @Override + protected void logSuccess(long audioLatency, long engineLatency, long engineTotal) { + EventLogTags.writeTtsV2SpeakSuccess(mServiceApp, + mCallerUid, mCallerPid, getUtteranceLength(), getRequestConfigString(), + engineLatency, engineTotal, audioLatency); + } + + /** + * @return the length of the utterance for the given synthesis, 0 + * if the utterance was {@code null}. + */ + private int getUtteranceLength() { + final String utterance = mRequest.getText(); + return utterance == null ? 0 : utterance.length(); + } + + /** + * Returns a string representation of the synthesis request configuration. + */ + private String getRequestConfigString() { + // Ensure the bundles are unparceled. + mRequest.getVoiceParams().size(); + mRequest.getAudioParams().size(); + + return new StringBuilder(64).append("VoiceName: ").append(mRequest.getVoiceName()) + .append(" ,VoiceParams: ").append(mRequest.getVoiceParams()) + .append(" ,SystemParams: ").append(mRequest.getAudioParams()) + .append("]").toString(); + } +} diff --git a/core/java/android/speech/tts/FileSynthesisCallback.java b/core/java/android/speech/tts/FileSynthesisCallback.java index ab8f82f..717aeb6 100644 --- a/core/java/android/speech/tts/FileSynthesisCallback.java +++ b/core/java/android/speech/tts/FileSynthesisCallback.java @@ -16,13 +16,10 @@ package android.speech.tts; import android.media.AudioFormat; -import android.os.FileUtils; +import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher; import android.util.Log; -import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.channels.FileChannel; @@ -48,19 +45,39 @@ class FileSynthesisCallback extends AbstractSynthesisCallback { private FileChannel mFileChannel; + private final UtteranceProgressDispatcher mDispatcher; + private final Object mCallerIdentity; + private boolean mStarted = false; - private boolean mStopped = false; private boolean mDone = false; - FileSynthesisCallback(FileChannel fileChannel) { + /** Status code of synthesis */ + protected int mStatusCode; + + FileSynthesisCallback(FileChannel fileChannel, UtteranceProgressDispatcher dispatcher, + Object callerIdentity, boolean clientIsUsingV2) { + super(clientIsUsingV2); mFileChannel = fileChannel; + mDispatcher = dispatcher; + mCallerIdentity = callerIdentity; + mStatusCode = TextToSpeechClient.Status.SUCCESS; } @Override void stop() { synchronized (mStateLock) { - mStopped = true; + if (mDone) { + return; + } + if (mStatusCode == TextToSpeechClient.Status.STOPPED) { + return; + } + + mStatusCode = TextToSpeechClient.Status.STOPPED; cleanUp(); + if (mDispatcher != null) { + mDispatcher.dispatchOnStop(); + } } } @@ -75,14 +92,8 @@ class FileSynthesisCallback extends AbstractSynthesisCallback { * Must be called while holding the monitor on {@link #mStateLock}. */ private void closeFile() { - try { - if (mFileChannel != null) { - mFileChannel.close(); - mFileChannel = null; - } - } catch (IOException ex) { - Log.e(TAG, "Failed to close output file descriptor", ex); - } + // File will be closed by the SpeechItem in the speech service. + mFileChannel = null; } @Override @@ -91,38 +102,46 @@ class FileSynthesisCallback extends AbstractSynthesisCallback { } @Override - boolean isDone() { - return mDone; - } - - @Override public int start(int sampleRateInHz, int audioFormat, int channelCount) { if (DBG) { Log.d(TAG, "FileSynthesisRequest.start(" + sampleRateInHz + "," + audioFormat + "," + channelCount + ")"); } + FileChannel fileChannel = null; synchronized (mStateLock) { - if (mStopped) { + if (mStatusCode == TextToSpeechClient.Status.STOPPED) { if (DBG) Log.d(TAG, "Request has been aborted."); + return errorCodeOnStop(); + } + if (mStatusCode != TextToSpeechClient.Status.SUCCESS) { + if (DBG) Log.d(TAG, "Error was raised"); return TextToSpeech.ERROR; } if (mStarted) { - cleanUp(); - throw new IllegalArgumentException("FileSynthesisRequest.start() called twice"); + Log.e(TAG, "Start called twice"); + return TextToSpeech.ERROR; } mStarted = true; mSampleRateInHz = sampleRateInHz; mAudioFormat = audioFormat; mChannelCount = channelCount; - try { - mFileChannel.write(ByteBuffer.allocate(WAV_HEADER_LENGTH)); + if (mDispatcher != null) { + mDispatcher.dispatchOnStart(); + } + fileChannel = mFileChannel; + } + + try { + fileChannel.write(ByteBuffer.allocate(WAV_HEADER_LENGTH)); return TextToSpeech.SUCCESS; - } catch (IOException ex) { - Log.e(TAG, "Failed to write wav header to output file descriptor" + ex); + } catch (IOException ex) { + Log.e(TAG, "Failed to write wav header to output file descriptor", ex); + synchronized (mStateLock) { cleanUp(); - return TextToSpeech.ERROR; + mStatusCode = TextToSpeechClient.Status.ERROR_OUTPUT; } + return TextToSpeech.ERROR; } } @@ -132,66 +151,128 @@ class FileSynthesisCallback extends AbstractSynthesisCallback { Log.d(TAG, "FileSynthesisRequest.audioAvailable(" + buffer + "," + offset + "," + length + ")"); } + FileChannel fileChannel = null; synchronized (mStateLock) { - if (mStopped) { + if (mStatusCode == TextToSpeechClient.Status.STOPPED) { if (DBG) Log.d(TAG, "Request has been aborted."); + return errorCodeOnStop(); + } + if (mStatusCode != TextToSpeechClient.Status.SUCCESS) { + if (DBG) Log.d(TAG, "Error was raised"); return TextToSpeech.ERROR; } if (mFileChannel == null) { Log.e(TAG, "File not open"); + mStatusCode = TextToSpeechClient.Status.ERROR_OUTPUT; return TextToSpeech.ERROR; } - try { - mFileChannel.write(ByteBuffer.wrap(buffer, offset, length)); - return TextToSpeech.SUCCESS; - } catch (IOException ex) { - Log.e(TAG, "Failed to write to output file descriptor", ex); - cleanUp(); + if (!mStarted) { + Log.e(TAG, "Start method was not called"); return TextToSpeech.ERROR; } + fileChannel = mFileChannel; + } + + try { + fileChannel.write(ByteBuffer.wrap(buffer, offset, length)); + return TextToSpeech.SUCCESS; + } catch (IOException ex) { + Log.e(TAG, "Failed to write to output file descriptor", ex); + synchronized (mStateLock) { + cleanUp(); + mStatusCode = TextToSpeechClient.Status.ERROR_OUTPUT; + } + return TextToSpeech.ERROR; } } @Override public int done() { if (DBG) Log.d(TAG, "FileSynthesisRequest.done()"); + FileChannel fileChannel = null; + + int sampleRateInHz = 0; + int audioFormat = 0; + int channelCount = 0; + synchronized (mStateLock) { if (mDone) { - if (DBG) Log.d(TAG, "Duplicate call to done()"); - // This preserves existing behaviour. Earlier, if done was called twice - // we'd return ERROR because mFile == null and we'd add to logspam. + Log.w(TAG, "Duplicate call to done()"); + // This is not an error that would prevent synthesis. Hence no + // setStatusCode is set. return TextToSpeech.ERROR; } - if (mStopped) { + if (mStatusCode == TextToSpeechClient.Status.STOPPED) { if (DBG) Log.d(TAG, "Request has been aborted."); + return errorCodeOnStop(); + } + if (mDispatcher != null && mStatusCode != TextToSpeechClient.Status.SUCCESS && + mStatusCode != TextToSpeechClient.Status.STOPPED) { + mDispatcher.dispatchOnError(mStatusCode); return TextToSpeech.ERROR; } if (mFileChannel == null) { Log.e(TAG, "File not open"); return TextToSpeech.ERROR; } - try { - // Write WAV header at start of file - mFileChannel.position(0); - int dataLength = (int) (mFileChannel.size() - WAV_HEADER_LENGTH); - mFileChannel.write( - makeWavHeader(mSampleRateInHz, mAudioFormat, mChannelCount, dataLength)); + mDone = true; + fileChannel = mFileChannel; + sampleRateInHz = mSampleRateInHz; + audioFormat = mAudioFormat; + channelCount = mChannelCount; + } + + try { + // Write WAV header at start of file + fileChannel.position(0); + int dataLength = (int) (fileChannel.size() - WAV_HEADER_LENGTH); + fileChannel.write( + makeWavHeader(sampleRateInHz, audioFormat, channelCount, dataLength)); + + synchronized (mStateLock) { closeFile(); - mDone = true; + if (mDispatcher != null) { + mDispatcher.dispatchOnSuccess(); + } return TextToSpeech.SUCCESS; - } catch (IOException ex) { - Log.e(TAG, "Failed to write to output file descriptor", ex); + } + } catch (IOException ex) { + Log.e(TAG, "Failed to write to output file descriptor", ex); + synchronized (mStateLock) { cleanUp(); - return TextToSpeech.ERROR; } + return TextToSpeech.ERROR; } } @Override public void error() { + error(TextToSpeechClient.Status.ERROR_SYNTHESIS); + } + + @Override + public void error(int errorCode) { if (DBG) Log.d(TAG, "FileSynthesisRequest.error()"); synchronized (mStateLock) { + if (mDone) { + return; + } cleanUp(); + mStatusCode = errorCode; + } + } + + @Override + public boolean hasStarted() { + synchronized (mStateLock) { + return mStarted; + } + } + + @Override + public boolean hasFinished() { + synchronized (mStateLock) { + return mDone; } } @@ -225,4 +306,16 @@ class FileSynthesisCallback extends AbstractSynthesisCallback { return header; } + @Override + public int fallback() { + synchronized (mStateLock) { + if (hasStarted() || hasFinished()) { + return TextToSpeech.ERROR; + } + + mDispatcher.dispatchOnFallback(); + mStatusCode = TextToSpeechClient.Status.SUCCESS; + return TextToSpeechClient.Status.SUCCESS; + } + } } diff --git a/core/java/android/speech/tts/ITextToSpeechCallback.aidl b/core/java/android/speech/tts/ITextToSpeechCallback.aidl index f0287d4..3c808ff 100644 --- a/core/java/android/speech/tts/ITextToSpeechCallback.aidl +++ b/core/java/android/speech/tts/ITextToSpeechCallback.aidl @@ -15,13 +15,53 @@ */ package android.speech.tts; +import android.speech.tts.VoiceInfo; + /** * Interface for callbacks from TextToSpeechService * * {@hide} */ oneway interface ITextToSpeechCallback { + /** + * Tells the client that the synthesis has started. + * + * @param utteranceId Unique id identifying synthesis request. + */ void onStart(String utteranceId); - void onDone(String utteranceId); - void onError(String utteranceId); + + /** + * Tells the client that the synthesis has finished. + * + * @param utteranceId Unique id identifying synthesis request. + */ + void onSuccess(String utteranceId); + + /** + * Tells the client that the synthesis was stopped. + * + * @param utteranceId Unique id identifying synthesis request. + */ + void onStop(String utteranceId); + + /** + * Tells the client that the synthesis failed, and fallback synthesis will be attempted. + * + * @param utteranceId Unique id identifying synthesis request. + */ + void onFallback(String utteranceId); + + /** + * Tells the client that the synthesis has failed. + * + * @param utteranceId Unique id identifying synthesis request. + * @param errorCode One of the values from + * {@link android.speech.tts.v2.TextToSpeechClient.Status}. + */ + void onError(String utteranceId, int errorCode); + + /** + * Inform the client that set of available voices changed. + */ + void onVoicesInfoChange(in List<VoiceInfo> voices); } diff --git a/core/java/android/speech/tts/ITextToSpeechService.aidl b/core/java/android/speech/tts/ITextToSpeechService.aidl index b7bc70c..9cf49ff 100644 --- a/core/java/android/speech/tts/ITextToSpeechService.aidl +++ b/core/java/android/speech/tts/ITextToSpeechService.aidl @@ -20,6 +20,8 @@ import android.net.Uri; import android.os.Bundle; import android.os.ParcelFileDescriptor; import android.speech.tts.ITextToSpeechCallback; +import android.speech.tts.VoiceInfo; +import android.speech.tts.SynthesisRequestV2; /** * Interface for TextToSpeech to talk to TextToSpeechService. @@ -70,9 +72,10 @@ interface ITextToSpeechService { * TextToSpeech object. * @param duration Number of milliseconds of silence to play. * @param queueMode Determines what to do to requests already in the queue. - * @param param Request parameters. + * @param utteranceId Unique id used to identify this request in callbacks. */ - int playSilence(in IBinder callingInstance, in long duration, in int queueMode, in Bundle params); + int playSilence(in IBinder callingInstance, in long duration, in int queueMode, + in String utteranceId); /** * Checks whether the service is currently playing some audio. @@ -90,7 +93,6 @@ interface ITextToSpeechService { /** * Returns the language, country and variant currently being used by the TTS engine. - * * Can be called from multiple threads. * * @return A 3-element array, containing language (ISO 3-letter code), @@ -99,7 +101,7 @@ interface ITextToSpeechService { * be empty too. */ String[] getLanguage(); - + /** * Returns a default TTS language, country and variant as set by the user. * @@ -111,7 +113,7 @@ interface ITextToSpeechService { * be empty too. */ String[] getClientDefaultLanguage(); - + /** * Checks whether the engine supports a given language. * @@ -137,7 +139,7 @@ interface ITextToSpeechService { * @param country ISO-3 country code. May be empty or null. * @param variant Language variant. May be empty or null. * @return An array of strings containing the set of features supported for - * the supplied locale. The array of strings must not contain + * the supplied locale. The array of strings must not contain * duplicates. */ String[] getFeaturesForLanguage(in String lang, in String country, in String variant); @@ -169,4 +171,44 @@ interface ITextToSpeechService { */ void setCallback(in IBinder caller, ITextToSpeechCallback cb); + /** + * Tells the engine to synthesize some speech and play it back. + * + * @param callingInstance a binder representing the identity of the calling + * TextToSpeech object. + * @param text The text to synthesize. + * @param queueMode Determines what to do to requests already in the queue. + * @param request Request parameters. + */ + int speakV2(in IBinder callingInstance, in SynthesisRequestV2 request); + + /** + * Tells the engine to synthesize some speech and write it to a file. + * + * @param callingInstance a binder representing the identity of the calling + * TextToSpeech object. + * @param text The text to synthesize. + * @param fileDescriptor The file descriptor to write the synthesized audio to. Has to be + writable. + * @param request Request parameters. + */ + int synthesizeToFileDescriptorV2(in IBinder callingInstance, + in ParcelFileDescriptor fileDescriptor, in SynthesisRequestV2 request); + + /** + * Plays an existing audio resource. V2 version + * + * @param callingInstance a binder representing the identity of the calling + * TextToSpeech object. + * @param audioUri URI for the audio resource (a file or android.resource URI) + * @param utteranceId Unique identifier. + * @param audioParameters Parameters for audio playback (from {@link SynthesisRequestV2}). + */ + int playAudioV2(in IBinder callingInstance, in Uri audioUri, in String utteranceId, + in Bundle audioParameters); + + /** + * Request the list of available voices from the service. + */ + List<VoiceInfo> getVoicesInfo(); } diff --git a/core/java/android/speech/tts/PlaybackQueueItem.java b/core/java/android/speech/tts/PlaybackQueueItem.java index d0957ff..b2e323e 100644 --- a/core/java/android/speech/tts/PlaybackQueueItem.java +++ b/core/java/android/speech/tts/PlaybackQueueItem.java @@ -22,6 +22,16 @@ abstract class PlaybackQueueItem implements Runnable { return mDispatcher; } + @Override public abstract void run(); - abstract void stop(boolean isError); + + /** + * Stop the playback. + * + * @param errorCode Cause of the stop. Can be either one of the error codes from + * {@link android.speech.tts.TextToSpeechClient.Status} or + * {@link android.speech.tts.TextToSpeechClient.Status#STOPPED} + * if stopped on a client request. + */ + abstract void stop(int errorCode); } diff --git a/core/java/android/speech/tts/PlaybackSynthesisCallback.java b/core/java/android/speech/tts/PlaybackSynthesisCallback.java index c99f201..e345e89 100644 --- a/core/java/android/speech/tts/PlaybackSynthesisCallback.java +++ b/core/java/android/speech/tts/PlaybackSynthesisCallback.java @@ -55,20 +55,20 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { private final AudioPlaybackHandler mAudioTrackHandler; // A request "token", which will be non null after start() has been called. private SynthesisPlaybackQueueItem mItem = null; - // Whether this request has been stopped. This is useful for keeping - // track whether stop() has been called before start(). In all other cases, - // a non-null value of mItem will provide the same information. - private boolean mStopped = false; private volatile boolean mDone = false; + /** Status code of synthesis */ + protected int mStatusCode; + private final UtteranceProgressDispatcher mDispatcher; private final Object mCallerIdentity; - private final EventLogger mLogger; + private final AbstractEventLogger mLogger; PlaybackSynthesisCallback(int streamType, float volume, float pan, AudioPlaybackHandler audioTrackHandler, UtteranceProgressDispatcher dispatcher, - Object callerIdentity, EventLogger logger) { + Object callerIdentity, AbstractEventLogger logger, boolean clientIsUsingV2) { + super(clientIsUsingV2); mStreamType = streamType; mVolume = volume; mPan = pan; @@ -76,28 +76,25 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { mDispatcher = dispatcher; mCallerIdentity = callerIdentity; mLogger = logger; + mStatusCode = TextToSpeechClient.Status.SUCCESS; } @Override void stop() { - stopImpl(false); - } - - void stopImpl(boolean wasError) { if (DBG) Log.d(TAG, "stop()"); - // Note that mLogger.mError might be true too at this point. - mLogger.onStopped(); - SynthesisPlaybackQueueItem item; synchronized (mStateLock) { - if (mStopped) { + if (mDone) { + return; + } + if (mStatusCode == TextToSpeechClient.Status.STOPPED) { Log.w(TAG, "stop() called twice"); return; } item = mItem; - mStopped = true; + mStatusCode = TextToSpeechClient.Status.STOPPED; } if (item != null) { @@ -105,19 +102,15 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { // point it will write an additional buffer to the item - but we // won't worry about that because the audio playback queue will be cleared // soon after (see SynthHandler#stop(String). - item.stop(wasError); + item.stop(TextToSpeechClient.Status.STOPPED); } else { // This happens when stop() or error() were called before start() was. // In all other cases, mAudioTrackHandler.stop() will // result in onSynthesisDone being called, and we will // write data there. - mLogger.onWriteData(); - - if (wasError) { - // We have to dispatch the error ourselves. - mDispatcher.dispatchOnError(); - } + mLogger.onCompleted(TextToSpeechClient.Status.STOPPED); + mDispatcher.dispatchOnStop(); } } @@ -129,26 +122,42 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { } @Override - boolean isDone() { - return mDone; + public boolean hasStarted() { + synchronized (mStateLock) { + return mItem != null; + } } @Override - public int start(int sampleRateInHz, int audioFormat, int channelCount) { - if (DBG) { - Log.d(TAG, "start(" + sampleRateInHz + "," + audioFormat - + "," + channelCount + ")"); + public boolean hasFinished() { + synchronized (mStateLock) { + return mDone; } + } + + @Override + public int start(int sampleRateInHz, int audioFormat, int channelCount) { + if (DBG) Log.d(TAG, "start(" + sampleRateInHz + "," + audioFormat + "," + channelCount + + ")"); int channelConfig = BlockingAudioTrack.getChannelConfig(channelCount); - if (channelConfig == 0) { - Log.e(TAG, "Unsupported number of channels :" + channelCount); - return TextToSpeech.ERROR; - } synchronized (mStateLock) { - if (mStopped) { + if (channelConfig == 0) { + Log.e(TAG, "Unsupported number of channels :" + channelCount); + mStatusCode = TextToSpeechClient.Status.ERROR_OUTPUT; + return TextToSpeech.ERROR; + } + if (mStatusCode == TextToSpeechClient.Status.STOPPED) { if (DBG) Log.d(TAG, "stop() called before start(), returning."); + return errorCodeOnStop(); + } + if (mStatusCode != TextToSpeechClient.Status.SUCCESS) { + if (DBG) Log.d(TAG, "Error was raised"); + return TextToSpeech.ERROR; + } + if (mItem != null) { + Log.e(TAG, "Start called twice"); return TextToSpeech.ERROR; } SynthesisPlaybackQueueItem item = new SynthesisPlaybackQueueItem( @@ -161,13 +170,11 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { return TextToSpeech.SUCCESS; } - @Override public int audioAvailable(byte[] buffer, int offset, int length) { - if (DBG) { - Log.d(TAG, "audioAvailable(byte[" + buffer.length + "]," - + offset + "," + length + ")"); - } + if (DBG) Log.d(TAG, "audioAvailable(byte[" + buffer.length + "]," + offset + "," + length + + ")"); + if (length > getMaxBufferSize() || length <= 0) { throw new IllegalArgumentException("buffer is too large or of zero length (" + + length + " bytes)"); @@ -175,9 +182,17 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { SynthesisPlaybackQueueItem item = null; synchronized (mStateLock) { - if (mItem == null || mStopped) { + if (mItem == null) { + mStatusCode = TextToSpeechClient.Status.ERROR_OUTPUT; return TextToSpeech.ERROR; } + if (mStatusCode != TextToSpeechClient.Status.SUCCESS) { + if (DBG) Log.d(TAG, "Error was raised"); + return TextToSpeech.ERROR; + } + if (mStatusCode == TextToSpeechClient.Status.STOPPED) { + return errorCodeOnStop(); + } item = mItem; } @@ -190,11 +205,13 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { try { item.put(bufferCopy); } catch (InterruptedException ie) { - return TextToSpeech.ERROR; + synchronized (mStateLock) { + mStatusCode = TextToSpeechClient.Status.ERROR_OUTPUT; + return TextToSpeech.ERROR; + } } mLogger.onEngineDataReceived(); - return TextToSpeech.SUCCESS; } @@ -202,35 +219,74 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { public int done() { if (DBG) Log.d(TAG, "done()"); + int statusCode = 0; SynthesisPlaybackQueueItem item = null; synchronized (mStateLock) { if (mDone) { Log.w(TAG, "Duplicate call to done()"); + // Not an error that would prevent synthesis. Hence no + // setStatusCode return TextToSpeech.ERROR; } - + if (mStatusCode == TextToSpeechClient.Status.STOPPED) { + if (DBG) Log.d(TAG, "Request has been aborted."); + return errorCodeOnStop(); + } mDone = true; if (mItem == null) { + // .done() was called before .start. Treat it as successful synthesis + // for a client, despite service bad implementation. + Log.w(TAG, "done() was called before start() call"); + if (mStatusCode == TextToSpeechClient.Status.SUCCESS) { + mDispatcher.dispatchOnSuccess(); + } else { + mDispatcher.dispatchOnError(mStatusCode); + } + mLogger.onEngineComplete(); return TextToSpeech.ERROR; } item = mItem; + statusCode = mStatusCode; } - item.done(); + // Signal done or error to item + if (statusCode == TextToSpeechClient.Status.SUCCESS) { + item.done(); + } else { + item.stop(statusCode); + } mLogger.onEngineComplete(); - return TextToSpeech.SUCCESS; } @Override public void error() { + error(TextToSpeechClient.Status.ERROR_SYNTHESIS); + } + + @Override + public void error(int errorCode) { if (DBG) Log.d(TAG, "error() [will call stop]"); - // Currently, this call will not be logged if error( ) is called - // before start. - mLogger.onError(); - stopImpl(true); + synchronized (mStateLock) { + if (mDone) { + return; + } + mStatusCode = errorCode; + } } + @Override + public int fallback() { + synchronized (mStateLock) { + if (hasStarted() || hasFinished()) { + return TextToSpeech.ERROR; + } + + mDispatcher.dispatchOnFallback(); + mStatusCode = TextToSpeechClient.Status.SUCCESS; + return TextToSpeechClient.Status.SUCCESS; + } + } } diff --git a/core/java/android/speech/tts/RequestConfig.java b/core/java/android/speech/tts/RequestConfig.java new file mode 100644 index 0000000..4b5385f --- /dev/null +++ b/core/java/android/speech/tts/RequestConfig.java @@ -0,0 +1,213 @@ +package android.speech.tts; + +import android.media.AudioManager; +import android.os.Bundle; + +/** + * Synthesis request configuration. + * + * This class is immutable, and can only be constructed using + * {@link RequestConfig.Builder}. + */ +public final class RequestConfig { + + /** Builder for constructing RequestConfig objects. */ + public static final class Builder { + private VoiceInfo mCurrentVoiceInfo; + private Bundle mVoiceParams; + private Bundle mAudioParams; + + Builder(VoiceInfo currentVoiceInfo, Bundle voiceParams, Bundle audioParams) { + mCurrentVoiceInfo = currentVoiceInfo; + mVoiceParams = voiceParams; + mAudioParams = audioParams; + } + + /** + * Create new RequestConfig builder. + */ + public static Builder newBuilder() { + return new Builder(null, new Bundle(), new Bundle()); + } + + /** + * Create new RequestConfig builder. + * @param prototype + * Prototype of new RequestConfig. Copies all fields of the + * prototype to the constructed object. + */ + public static Builder newBuilder(RequestConfig prototype) { + return new Builder(prototype.mCurrentVoiceInfo, + (Bundle)prototype.mVoiceParams.clone(), + (Bundle)prototype.mAudioParams.clone()); + } + + /** Set voice for request. Will reset voice parameters to the defaults. */ + public Builder setVoice(VoiceInfo voice) { + mCurrentVoiceInfo = voice; + mVoiceParams = (Bundle)voice.getParamsWithDefaults().clone(); + return this; + } + + /** + * Set request voice parameter. + * + * @param paramName + * The name of the parameter. It has to be one of the keys + * from {@link VoiceInfo#getParamsWithDefaults()} + * @param value + * Value of the parameter. Its type can be one of: Integer, Float, + * Boolean, String, VoiceInfo (will be set as a String, result of a call to + * the {@link VoiceInfo#getName()}) or byte[]. It has to be of the same type + * as the default value from {@link VoiceInfo#getParamsWithDefaults()} + * for that parameter. + * @throws IllegalArgumentException + * If paramName is not a valid parameter name or its value is of a wrong + * type. + * @throws IllegalStateException + * If no voice is set. + */ + public Builder setVoiceParam(String paramName, Object value){ + if (mCurrentVoiceInfo == null) { + throw new IllegalStateException( + "Couldn't set voice parameter, no voice is set"); + } + Object defaultValue = mCurrentVoiceInfo.getParamsWithDefaults().get(paramName); + if (defaultValue == null) { + throw new IllegalArgumentException( + "Parameter \"" + paramName + "\" is not available in set voice with " + + "name: " + mCurrentVoiceInfo.getName()); + } + + // If it's VoiceInfo, get its name + if (value instanceof VoiceInfo) { + value = ((VoiceInfo)value).getName(); + } + + // Check type information + if (!defaultValue.getClass().equals(value.getClass())) { + throw new IllegalArgumentException( + "Parameter \"" + paramName +"\" is of different type. Value passed has " + + "type " + value.getClass().getSimpleName() + " but should have " + + "type " + defaultValue.getClass().getSimpleName()); + } + + setParam(mVoiceParams, paramName, value); + return this; + } + + /** + * Set request audio parameter. + * + * Doesn't requires a set voice. + * + * @param paramName + * Name of parameter. + * @param value + * Value of parameter. Its type can be one of: Integer, Float, Boolean, String + * or byte[]. + */ + public Builder setAudioParam(String paramName, Object value) { + setParam(mAudioParams, paramName, value); + return this; + } + + /** + * Set the {@link TextToSpeechClient.Params#AUDIO_PARAM_STREAM} audio parameter. + * + * @param streamId One of the STREAM_ constants defined in {@link AudioManager}. + */ + public void setAudioParamStream(int streamId) { + setAudioParam(TextToSpeechClient.Params.AUDIO_PARAM_STREAM, streamId); + } + + /** + * Set the {@link TextToSpeechClient.Params#AUDIO_PARAM_VOLUME} audio parameter. + * + * @param volume Float in range of 0.0 to 1.0. + */ + public void setAudioParamVolume(float volume) { + setAudioParam(TextToSpeechClient.Params.AUDIO_PARAM_VOLUME, volume); + } + + /** + * Set the {@link TextToSpeechClient.Params#AUDIO_PARAM_PAN} audio parameter. + * + * @param pan Float in range of -1.0 to +1.0. + */ + public void setAudioParamPan(float pan) { + setAudioParam(TextToSpeechClient.Params.AUDIO_PARAM_PAN, pan); + } + + private void setParam(Bundle bundle, String featureName, Object value) { + if (value instanceof String) { + bundle.putString(featureName, (String)value); + } else if(value instanceof byte[]) { + bundle.putByteArray(featureName, (byte[])value); + } else if(value instanceof Integer) { + bundle.putInt(featureName, (Integer)value); + } else if(value instanceof Float) { + bundle.putFloat(featureName, (Float)value); + } else if(value instanceof Double) { + bundle.putFloat(featureName, (Float)value); + } else if(value instanceof Boolean) { + bundle.putBoolean(featureName, (Boolean)value); + } else { + throw new IllegalArgumentException("Illegal type of object"); + } + return; + } + + /** + * Build new RequestConfig instance. + */ + public RequestConfig build() { + RequestConfig config = + new RequestConfig(mCurrentVoiceInfo, mVoiceParams, mAudioParams); + return config; + } + } + + private RequestConfig(VoiceInfo voiceInfo, Bundle voiceParams, Bundle audioParams) { + mCurrentVoiceInfo = voiceInfo; + mVoiceParams = voiceParams; + mAudioParams = audioParams; + } + + /** + * Currently set voice. + */ + private final VoiceInfo mCurrentVoiceInfo; + + /** + * Voice parameters bundle. + */ + private final Bundle mVoiceParams; + + /** + * Audio parameters bundle. + */ + private final Bundle mAudioParams; + + /** + * @return Currently set request voice. + */ + public VoiceInfo getVoice() { + return mCurrentVoiceInfo; + } + + /** + * @return Request audio parameters. + */ + public Bundle getAudioParams() { + return mAudioParams; + } + + /** + * @return Request voice parameters. + */ + public Bundle getVoiceParams() { + return mVoiceParams; + } + +} diff --git a/core/java/android/speech/tts/RequestConfigHelper.java b/core/java/android/speech/tts/RequestConfigHelper.java new file mode 100644 index 0000000..b25c985 --- /dev/null +++ b/core/java/android/speech/tts/RequestConfigHelper.java @@ -0,0 +1,170 @@ +package android.speech.tts; + +import android.speech.tts.TextToSpeechClient.EngineStatus; + +import java.util.Locale; + +/** + * Set of common heuristics for selecting {@link VoiceInfo} from + * {@link TextToSpeechClient#getEngineStatus()} output. + */ +public final class RequestConfigHelper { + private RequestConfigHelper() {} + + /** + * Interface for scoring VoiceInfo object. + */ + public static interface VoiceScorer { + /** + * Score VoiceInfo. If the score is less than or equal to zero, that voice is discarded. + * If two voices have same desired primary characteristics (highest quality, lowest + * latency or others), the one with the higher score is selected. + */ + public int scoreVoice(VoiceInfo voiceInfo); + } + + /** + * Score positively voices that exactly match the locale supplied to the constructor. + */ + public static final class ExactLocaleMatcher implements VoiceScorer { + private final Locale mLocale; + + /** + * Score positively voices that exactly match the given locale + * @param locale Reference locale. If null, the default locale will be used. + */ + public ExactLocaleMatcher(Locale locale) { + if (locale == null) { + mLocale = Locale.getDefault(); + } else { + mLocale = locale; + } + } + @Override + public int scoreVoice(VoiceInfo voiceInfo) { + return mLocale.equals(voiceInfo.getLocale()) ? 1 : 0; + } + } + + /** + * Score positively voices that match exactly the given locale (score 3) + * or that share same language and country (score 2), or that share just a language (score 1). + */ + public static final class LanguageMatcher implements VoiceScorer { + private final Locale mLocale; + + /** + * Score positively voices with similar locale. + * @param locale Reference locale. If null, default will be used. + */ + public LanguageMatcher(Locale locale) { + if (locale == null) { + mLocale = Locale.getDefault(); + } else { + mLocale = locale; + } + } + + @Override + public int scoreVoice(VoiceInfo voiceInfo) { + final Locale voiceLocale = voiceInfo.getLocale(); + if (mLocale.equals(voiceLocale)) { + return 3; + } else { + if (mLocale.getLanguage().equals(voiceLocale.getLanguage())) { + if (mLocale.getCountry().equals(voiceLocale.getCountry())) { + return 2; + } + return 1; + } + return 0; + } + } + } + + /** + * Get the highest quality voice from voices that score more than zero from the passed scorer. + * If there is more than one voice with the same highest quality, then this method returns one + * with the highest score. If they share same score as well, one with the lower index in the + * voices list is returned. + * + * @param engineStatus + * Voices status received from a {@link TextToSpeechClient#getEngineStatus()} call. + * @param voiceScorer + * Used to discard unsuitable voices and help settle cases where more than + * one voice has the desired characteristic. + * @param hasToBeEmbedded + * If true, require the voice to be an embedded voice (no network + * access will be required for synthesis). + */ + private static VoiceInfo getHighestQualityVoice(EngineStatus engineStatus, + VoiceScorer voiceScorer, boolean hasToBeEmbedded) { + VoiceInfo bestVoice = null; + int bestScoreMatch = 1; + int bestVoiceQuality = 0; + + for (VoiceInfo voice : engineStatus.getVoices()) { + int score = voiceScorer.scoreVoice(voice); + if (score <= 0 || hasToBeEmbedded && voice.getRequiresNetworkConnection() + || voice.getQuality() < bestVoiceQuality) { + continue; + } + + if (bestVoice == null || + voice.getQuality() > bestVoiceQuality || + score > bestScoreMatch) { + bestVoice = voice; + bestScoreMatch = score; + bestVoiceQuality = voice.getQuality(); + } + } + return bestVoice; + } + + /** + * Get highest quality voice. + * + * Highest quality voice is selected from voices that score more than zero from the passed + * scorer. If there is more than one voice with the same highest quality, then this method + * will return one with the highest score. If they share same score as well, one with the lower + * index in the voices list is returned. + + * @param engineStatus + * Voices status received from a {@link TextToSpeechClient#getEngineStatus()} call. + * @param hasToBeEmbedded + * If true, require the voice to be an embedded voice (no network + * access will be required for synthesis). + * @param voiceScorer + * Scorer is used to discard unsuitable voices and help settle cases where more than + * one voice has highest quality. + * @return RequestConfig with selected voice or null if suitable voice was not found. + */ + public static RequestConfig highestQuality(EngineStatus engineStatus, + boolean hasToBeEmbedded, VoiceScorer voiceScorer) { + VoiceInfo voice = getHighestQualityVoice(engineStatus, voiceScorer, hasToBeEmbedded); + if (voice == null) { + return null; + } + return RequestConfig.Builder.newBuilder().setVoice(voice).build(); + } + + /** + * Get highest quality voice for the default locale. + * + * Call {@link #highestQuality(EngineStatus, boolean, VoiceScorer)} with + * {@link LanguageMatcher} set to device default locale. + * + * @param engineStatus + * Voices status received from a {@link TextToSpeechClient#getEngineStatus()} call. + * @param hasToBeEmbedded + * If true, require the voice to be an embedded voice (no network + * access will be required for synthesis). + * @return RequestConfig with selected voice or null if suitable voice was not found. + */ + public static RequestConfig highestQuality(EngineStatus engineStatus, + boolean hasToBeEmbedded) { + return highestQuality(engineStatus, hasToBeEmbedded, + new LanguageMatcher(Locale.getDefault())); + } + +} diff --git a/core/java/android/speech/tts/SilencePlaybackQueueItem.java b/core/java/android/speech/tts/SilencePlaybackQueueItem.java index a5e47ae..88b7c70 100644 --- a/core/java/android/speech/tts/SilencePlaybackQueueItem.java +++ b/core/java/android/speech/tts/SilencePlaybackQueueItem.java @@ -17,7 +17,6 @@ package android.speech.tts; import android.os.ConditionVariable; import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher; -import android.util.Log; class SilencePlaybackQueueItem extends PlaybackQueueItem { private final ConditionVariable mCondVar = new ConditionVariable(); @@ -32,14 +31,20 @@ class SilencePlaybackQueueItem extends PlaybackQueueItem { @Override public void run() { getDispatcher().dispatchOnStart(); + boolean wasStopped = false; if (mSilenceDurationMs > 0) { - mCondVar.block(mSilenceDurationMs); + wasStopped = mCondVar.block(mSilenceDurationMs); } - getDispatcher().dispatchOnDone(); + if (wasStopped) { + getDispatcher().dispatchOnStop(); + } else { + getDispatcher().dispatchOnSuccess(); + } + } @Override - void stop(boolean isError) { + void stop(int errorCode) { mCondVar.open(); } } diff --git a/core/java/android/speech/tts/SynthesisCallback.java b/core/java/android/speech/tts/SynthesisCallback.java index f98bb09..bc2f239 100644 --- a/core/java/android/speech/tts/SynthesisCallback.java +++ b/core/java/android/speech/tts/SynthesisCallback.java @@ -26,7 +26,9 @@ package android.speech.tts; * indicate that an error has occurred, but if the call is made after a call * to {@link #done}, it might be discarded. * - * After {@link #start} been called, {@link #done} must be called regardless of errors. + * {@link #done} must be called at the end of synthesis, regardless of errors. + * + * All methods can be only called on the synthesis thread. */ public interface SynthesisCallback { /** @@ -41,13 +43,16 @@ public interface SynthesisCallback { * request. * * This method should only be called on the synthesis thread, - * while in {@link TextToSpeechService#onSynthesizeText}. + * while in {@link TextToSpeechService#onSynthesizeText} or + * {@link TextToSpeechService#onSynthesizeTextV2}. * * @param sampleRateInHz Sample rate in HZ of the generated audio. * @param audioFormat Audio format of the generated audio. Must be one of * the ENCODING_ constants defined in {@link android.media.AudioFormat}. * @param channelCount The number of channels. Must be {@code 1} or {@code 2}. - * @return {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}. + * @return {@link TextToSpeech#SUCCESS}, {@link TextToSpeech#ERROR}. + * {@link TextToSpeechClient.Status#STOPPED} is also possible if called in context of + * {@link TextToSpeechService#onSynthesizeTextV2}. */ public int start(int sampleRateInHz, int audioFormat, int channelCount); @@ -55,7 +60,8 @@ public interface SynthesisCallback { * The service should call this method when synthesized audio is ready for consumption. * * This method should only be called on the synthesis thread, - * while in {@link TextToSpeechService#onSynthesizeText}. + * while in {@link TextToSpeechService#onSynthesizeText} or + * {@link TextToSpeechService#onSynthesizeTextV2}. * * @param buffer The generated audio data. This method will not hold on to {@code buffer}, * so the caller is free to modify it after this method returns. @@ -63,6 +69,8 @@ public interface SynthesisCallback { * @param length The number of bytes of audio data in {@code buffer}. This must be * less than or equal to the return value of {@link #getMaxBufferSize}. * @return {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}. + * {@link TextToSpeechClient.Status#STOPPED} is also possible if called in context of + * {@link TextToSpeechService#onSynthesizeTextV2}. */ public int audioAvailable(byte[] buffer, int offset, int length); @@ -71,11 +79,14 @@ public interface SynthesisCallback { * been passed to {@link #audioAvailable}. * * This method should only be called on the synthesis thread, - * while in {@link TextToSpeechService#onSynthesizeText}. + * while in {@link TextToSpeechService#onSynthesizeText} or + * {@link TextToSpeechService#onSynthesizeTextV2}. * - * This method has to be called if {@link #start} was called. + * This method has to be called if {@link #start} and/or {@link #error} was called. * * @return {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}. + * {@link TextToSpeechClient.Status#STOPPED} is also possible if called in context of + * {@link TextToSpeechService#onSynthesizeTextV2}. */ public int done(); @@ -87,4 +98,58 @@ public interface SynthesisCallback { */ public void error(); -}
\ No newline at end of file + + /** + * The service should call this method if the speech synthesis fails. + * + * This method should only be called on the synthesis thread, + * while in {@link TextToSpeechService#onSynthesizeText} or + * {@link TextToSpeechService#onSynthesizeTextV2}. + * + * @param errorCode Error code to pass to the client. One of the ERROR_ values from + * {@link TextToSpeechClient.Status} + */ + public void error(int errorCode); + + /** + * Communicate to client that the original request can't be done and client-requested + * fallback is happening. + * + * Fallback can be requested by the client by setting + * {@link TextToSpeechClient.Params#FALLBACK_VOICE_NAME} voice parameter with a id of + * the voice that is expected to be used for the fallback. + * + * This method will fail if user called {@link #start(int, int, int)} and/or + * {@link #done()}. + * + * This method should only be called on the synthesis thread, + * while in {@link TextToSpeechService#onSynthesizeTextV2}. + * + * @return {@link TextToSpeech#SUCCESS}, {@link TextToSpeech#ERROR} if client already + * called {@link #start(int, int, int)}, {@link TextToSpeechClient.Status#STOPPED} + * if stop was requested. + */ + public int fallback(); + + /** + * Check if {@link #start} was called or not. + * + * This method should only be called on the synthesis thread, + * while in {@link TextToSpeechService#onSynthesizeText} or + * {@link TextToSpeechService#onSynthesizeTextV2}. + * + * Useful for checking if a fallback from network request is possible. + */ + public boolean hasStarted(); + + /** + * Check if {@link #done} was called or not. + * + * This method should only be called on the synthesis thread, + * while in {@link TextToSpeechService#onSynthesizeText} or + * {@link TextToSpeechService#onSynthesizeTextV2}. + * + * Useful for checking if a fallback from network request is possible. + */ + public boolean hasFinished(); +} diff --git a/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java b/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java index e853c9e..b424356 100644 --- a/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java +++ b/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java @@ -57,23 +57,22 @@ final class SynthesisPlaybackQueueItem extends PlaybackQueueItem { */ private volatile boolean mStopped; private volatile boolean mDone; - private volatile boolean mIsError; + private volatile int mStatusCode; private final BlockingAudioTrack mAudioTrack; - private final EventLogger mLogger; - + private final AbstractEventLogger mLogger; SynthesisPlaybackQueueItem(int streamType, int sampleRate, int audioFormat, int channelCount, float volume, float pan, UtteranceProgressDispatcher dispatcher, - Object callerIdentity, EventLogger logger) { + Object callerIdentity, AbstractEventLogger logger) { super(dispatcher, callerIdentity); mUnconsumedBytes = 0; mStopped = false; mDone = false; - mIsError = false; + mStatusCode = TextToSpeechClient.Status.SUCCESS; mAudioTrack = new BlockingAudioTrack(streamType, sampleRate, audioFormat, channelCount, volume, pan); @@ -86,9 +85,8 @@ final class SynthesisPlaybackQueueItem extends PlaybackQueueItem { final UtteranceProgressDispatcher dispatcher = getDispatcher(); dispatcher.dispatchOnStart(); - if (!mAudioTrack.init()) { - dispatcher.dispatchOnError(); + dispatcher.dispatchOnError(TextToSpeechClient.Status.ERROR_OUTPUT); return; } @@ -112,23 +110,25 @@ final class SynthesisPlaybackQueueItem extends PlaybackQueueItem { mAudioTrack.waitAndRelease(); - if (mIsError) { - dispatcher.dispatchOnError(); + if (mStatusCode == TextToSpeechClient.Status.SUCCESS) { + dispatcher.dispatchOnSuccess(); + } else if(mStatusCode == TextToSpeechClient.Status.STOPPED) { + dispatcher.dispatchOnStop(); } else { - dispatcher.dispatchOnDone(); + dispatcher.dispatchOnError(mStatusCode); } - mLogger.onWriteData(); + mLogger.onCompleted(mStatusCode); } @Override - void stop(boolean isError) { + void stop(int statusCode) { try { mListLock.lock(); // Update our internal state. mStopped = true; - mIsError = isError; + mStatusCode = statusCode; // Wake up the audio playback thread if it was waiting on take(). // take() will return null since mStopped was true, and will then diff --git a/core/java/android/speech/tts/SynthesisRequestV2.aidl b/core/java/android/speech/tts/SynthesisRequestV2.aidl new file mode 100644 index 0000000..2ac7da6 --- /dev/null +++ b/core/java/android/speech/tts/SynthesisRequestV2.aidl @@ -0,0 +1,20 @@ +/* +** +** Copyright 2013, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.speech.tts; + +parcelable SynthesisRequestV2;
\ No newline at end of file diff --git a/core/java/android/speech/tts/SynthesisRequestV2.java b/core/java/android/speech/tts/SynthesisRequestV2.java new file mode 100644 index 0000000..ed268b7 --- /dev/null +++ b/core/java/android/speech/tts/SynthesisRequestV2.java @@ -0,0 +1,132 @@ +package android.speech.tts; + +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.speech.tts.TextToSpeechClient.UtteranceId; + +/** + * Service-side representation of a synthesis request from a V2 API client. Contains: + * <ul> + * <li>The utterance to synthesize</li> + * <li>The id of the utterance (String, result of {@link UtteranceId#toUniqueString()}</li> + * <li>The synthesis voice name (String, result of {@link VoiceInfo#getName()})</li> + * <li>Voice parameters (Bundle of parameters)</li> + * <li>Audio parameters (Bundle of parameters)</li> + * </ul> + */ +public final class SynthesisRequestV2 implements Parcelable { + /** Synthesis utterance. */ + private final String mText; + + /** Synthesis id. */ + private final String mUtteranceId; + + /** Voice ID. */ + private final String mVoiceName; + + /** Voice Parameters. */ + private final Bundle mVoiceParams; + + /** Audio Parameters. */ + private final Bundle mAudioParams; + + /** + * Parcel based constructor. + * + * @hide + */ + public SynthesisRequestV2(Parcel in) { + this.mText = in.readString(); + this.mUtteranceId = in.readString(); + this.mVoiceName = in.readString(); + this.mVoiceParams = in.readBundle(); + this.mAudioParams = in.readBundle(); + } + + SynthesisRequestV2(String text, String utteranceId, RequestConfig rconfig) { + this.mText = text; + this.mUtteranceId = utteranceId; + this.mVoiceName = rconfig.getVoice().getName(); + this.mVoiceParams = rconfig.getVoiceParams(); + this.mAudioParams = rconfig.getAudioParams(); + } + + /** + * Write to parcel. + * + * @hide + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mText); + dest.writeString(mUtteranceId); + dest.writeString(mVoiceName); + dest.writeBundle(mVoiceParams); + dest.writeBundle(mAudioParams); + } + + /** + * @return the text which should be synthesized. + */ + public String getText() { + return mText; + } + + /** + * @return the id of the synthesis request. It's an output of a call to the + * {@link UtteranceId#toUniqueString()} method of the {@link UtteranceId} associated with + * this request. + */ + public String getUtteranceId() { + return mUtteranceId; + } + + /** + * @return the name of the voice to use for this synthesis request. Result of a call to + * the {@link VoiceInfo#getName()} method. + */ + public String getVoiceName() { + return mVoiceName; + } + + /** + * @return bundle of voice parameters. + */ + public Bundle getVoiceParams() { + return mVoiceParams; + } + + /** + * @return bundle of audio parameters. + */ + public Bundle getAudioParams() { + return mAudioParams; + } + + /** + * Parcel creators. + * + * @hide + */ + public static final Parcelable.Creator<SynthesisRequestV2> CREATOR = + new Parcelable.Creator<SynthesisRequestV2>() { + @Override + public SynthesisRequestV2 createFromParcel(Parcel source) { + return new SynthesisRequestV2(source); + } + + @Override + public SynthesisRequestV2[] newArray(int size) { + return new SynthesisRequestV2[size]; + } + }; + + /** + * @hide + */ + @Override + public int describeContents() { + return 0; + } +} diff --git a/core/java/android/speech/tts/TextToSpeech.java b/core/java/android/speech/tts/TextToSpeech.java index 2752085..02152fb 100644 --- a/core/java/android/speech/tts/TextToSpeech.java +++ b/core/java/android/speech/tts/TextToSpeech.java @@ -54,7 +54,9 @@ import java.util.Set; * When you are done using the TextToSpeech instance, call the {@link #shutdown()} method * to release the native resources used by the TextToSpeech engine. * + * @deprecated Use {@link TextToSpeechClient} instead */ +@Deprecated public class TextToSpeech { private static final String TAG = "TextToSpeech"; @@ -970,7 +972,7 @@ public class TextToSpeech { @Override public Integer run(ITextToSpeechService service) throws RemoteException { return service.playSilence(getCallerIdentity(), durationInMs, queueMode, - getParams(params)); + params == null ? null : params.get(Engine.KEY_PARAM_UTTERANCE_ID)); } }, ERROR, "playSilence"); } @@ -1443,8 +1445,17 @@ public class TextToSpeech { private boolean mEstablished; private final ITextToSpeechCallback.Stub mCallback = new ITextToSpeechCallback.Stub() { + public void onStop(String utteranceId) throws RemoteException { + // do nothing + }; + + @Override + public void onFallback(String utteranceId) throws RemoteException { + // do nothing + } + @Override - public void onDone(String utteranceId) { + public void onSuccess(String utteranceId) { UtteranceProgressListener listener = mUtteranceProgressListener; if (listener != null) { listener.onDone(utteranceId); @@ -1452,7 +1463,7 @@ public class TextToSpeech { } @Override - public void onError(String utteranceId) { + public void onError(String utteranceId, int errorCode) { UtteranceProgressListener listener = mUtteranceProgressListener; if (listener != null) { listener.onError(utteranceId); @@ -1466,6 +1477,11 @@ public class TextToSpeech { listener.onStart(utteranceId); } } + + @Override + public void onVoicesInfoChange(List<VoiceInfo> voicesInfo) throws RemoteException { + // Ignore it + } }; private class SetupConnectionAsyncTask extends AsyncTask<Void, Void, Integer> { diff --git a/core/java/android/speech/tts/TextToSpeechClient.java b/core/java/android/speech/tts/TextToSpeechClient.java new file mode 100644 index 0000000..0d8d42c --- /dev/null +++ b/core/java/android/speech/tts/TextToSpeechClient.java @@ -0,0 +1,1054 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package android.speech.tts; + +import android.app.Activity; +import android.app.Application; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.media.AudioManager; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.speech.tts.ITextToSpeechCallback; +import android.speech.tts.ITextToSpeechService; +import android.util.Log; +import android.util.Pair; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +/** + * Synthesizes speech from text for immediate playback or to create a sound + * file. + * <p> + * This is an updated version of the speech synthesis client that supersedes + * {@link android.speech.tts.TextToSpeech}. + * <p> + * A TextToSpeechClient instance can only be used to synthesize text once it has + * connected to the service. The TextToSpeechClient instance will start establishing + * the connection after a call to the {@link #connect()} method. This is usually done in + * {@link Application#onCreate()} or {@link Activity#onCreate}. When the connection + * is established, the instance will call back using the + * {@link TextToSpeechClient.ConnectionCallbacks} interface. Only after a + * successful callback is the client usable. + * <p> + * After successful connection, the list of all available voices can be obtained + * by calling the {@link TextToSpeechClient#getEngineStatus() method. The client can + * choose a voice using some custom heuristic and build a {@link RequestConfig} object + * using {@link RequestConfig.Builder}, or can use one of the common heuristics found + * in ({@link RequestConfigHelper}. + * <p> + * When you are done using the TextToSpeechClient instance, call the + * {@link #disconnect()} method to release the connection. + * <p> + * In the rare case of a change to the set of available voices, the service will call to the + * {@link ConnectionCallbacks#onEngineStatusChange} with new set of available voices as argument. + * In response, the client HAVE to recreate all {@link RequestConfig} instances in use. + */ +public final class TextToSpeechClient { + private static final String TAG = TextToSpeechClient.class.getSimpleName(); + + private final Object mLock = new Object(); + private final TtsEngines mEnginesHelper; + private final Context mContext; + + // Guarded by mLock + private Connection mServiceConnection; + private final RequestCallbacks mDefaultRequestCallbacks; + private final ConnectionCallbacks mConnectionCallbacks; + private EngineStatus mEngineStatus; + private String mRequestedEngine; + private boolean mFallbackToDefault; + private HashMap<String, Pair<UtteranceId, RequestCallbacks>> mCallbacks; + // Guarded by mLock + + /** Common voices parameters */ + public static final class Params { + private Params() {} + + /** + * Maximum allowed time for a single request attempt, in milliseconds, before synthesis + * fails (or fallback request starts, if requested using + * {@link #FALLBACK_VOICE_NAME}). + */ + public static final String NETWORK_TIMEOUT_MS = "networkTimeoutMs"; + + /** + * Number of network request retries that are attempted in case of failure + */ + public static final String NETWORK_RETRIES_COUNT = "networkRetriesCount"; + + /** + * Should synthesizer report sub-utterance progress on synthesis. Only applicable + * for the {@link TextToSpeechClient#queueSpeak} method. + */ + public static final String TRACK_SUBUTTERANCE_PROGRESS = "trackSubutteranceProgress"; + + /** + * If a voice exposes this parameter then it supports the fallback request feature. + * + * If it is set to a valid name of some other voice ({@link VoiceInfo#getName()}) then + * in case of request failure (due to network problems or missing data), fallback request + * will be attempted. Request will be done using the voice referenced by this parameter. + * If it is the case, the client will be informed by a callback to the {@link + * RequestCallbacks#onSynthesisFallback(UtteranceId)}. + */ + public static final String FALLBACK_VOICE_NAME = "fallbackVoiceName"; + + /** + * Audio parameter for specifying a linear multiplier to the speaking speed of the voice. + * The value is a float. Values below zero decrease speed of the synthesized speech + * values above one increase it. If the value of this parameter is equal to zero, + * then it will be replaced by a settings-configurable default before it reaches + * TTS service. + */ + public static final String SPEECH_SPEED = "speechSpeed"; + + /** + * Audio parameter for controlling the pitch of the output. The Value is a positive float, + * with default of {@code 1.0}. The value is used to scale the primary frequency linearly. + * Lower values lower the tone of the synthesized voice, greater values increase it. + */ + public static final String SPEECH_PITCH = "speechPitch"; + + /** + * Audio parameter for controlling output volume. Value is a float with scale of 0 to 1 + */ + public static final String AUDIO_PARAM_VOLUME = TextToSpeech.Engine.KEY_PARAM_VOLUME; + + /** + * Audio parameter for controlling output pan. + * Value is a float ranging from -1 to +1 where -1 maps to a hard-left pan, + * 0 to center (the default behavior), and +1 to hard-right. + */ + public static final String AUDIO_PARAM_PAN = TextToSpeech.Engine.KEY_PARAM_PAN; + + /** + * Audio parameter for specifying the audio stream type to be used when speaking text + * or playing back a file. The value should be one of the STREAM_ constants + * defined in {@link AudioManager}. + */ + public static final String AUDIO_PARAM_STREAM = TextToSpeech.Engine.KEY_PARAM_STREAM; + } + + /** + * Result codes for TTS operations. + */ + public static final class Status { + private Status() {} + + /** + * Denotes a successful operation. + */ + public static final int SUCCESS = 0; + + /** + * Denotes a stop requested by a client. It's used only on the service side of the API, + * client should never expect to see this result code. + */ + public static final int STOPPED = 100; + + /** + * Denotes a generic failure. + */ + public static final int ERROR_UNKNOWN = -1; + + /** + * Denotes a failure of a TTS engine to synthesize the given input. + */ + public static final int ERROR_SYNTHESIS = 10; + + /** + * Denotes a failure of a TTS service. + */ + public static final int ERROR_SERVICE = 11; + + /** + * Denotes a failure related to the output (audio device or a file). + */ + public static final int ERROR_OUTPUT = 12; + + /** + * Denotes a failure caused by a network connectivity problems. + */ + public static final int ERROR_NETWORK = 13; + + /** + * Denotes a failure caused by network timeout. + */ + public static final int ERROR_NETWORK_TIMEOUT = 14; + + /** + * Denotes a failure caused by an invalid request. + */ + public static final int ERROR_INVALID_REQUEST = 15; + + /** + * Denotes a failure related to passing a non-unique utterance id. + */ + public static final int ERROR_NON_UNIQUE_UTTERANCE_ID = 16; + + /** + * Denotes a failure related to missing data. The TTS implementation may download + * the missing data, and if so, request will succeed in future. This error can only happen + * for voices with {@link VoiceInfo#FEATURE_MAY_AUTOINSTALL} feature. + * Note: the recommended way to avoid this error is to create a request with the fallback + * voice. + */ + public static final int ERROR_DOWNLOADING_ADDITIONAL_DATA = 17; + } + + /** + * Set of callbacks for the events related to the progress of a synthesis request + * through the synthesis queue. Each synthesis request is associated with a call to + * {@link #queueSpeak} or {@link #queueSynthesizeToFile}. + * + * The callbacks specified in this method will NOT be called on UI thread. + */ + public static abstract class RequestCallbacks { + /** + * Called after synthesis of utterance successfully starts. + */ + public void onSynthesisStart(UtteranceId utteranceId) {} + + /** + * Called after synthesis successfully finishes. + * @param utteranceId + * Unique identifier of synthesized utterance. + */ + public void onSynthesisSuccess(UtteranceId utteranceId) {} + + /** + * Called after synthesis was stopped in middle of synthesis process. + * @param utteranceId + * Unique identifier of synthesized utterance. + */ + public void onSynthesisStop(UtteranceId utteranceId) {} + + /** + * Called when requested synthesis failed and fallback synthesis is about to be attempted. + * + * Requires voice with available {@link TextToSpeechClient.Params#FALLBACK_VOICE_NAME} + * parameter, and request with this parameter enabled. + * + * This callback will be followed by callback to the {@link #onSynthesisStart}, + * {@link #onSynthesisFailure} or {@link #onSynthesisSuccess} that depends on the + * fallback outcome. + * + * For more fallback feature reference, look at the + * {@link TextToSpeechClient.Params#FALLBACK_VOICE_NAME}. + * + * @param utteranceId + * Unique identifier of synthesized utterance. + */ + public void onSynthesisFallback(UtteranceId utteranceId) {} + + /** + * Called after synthesis of utterance fails. + * + * It may be called instead or after a {@link #onSynthesisStart} callback. + * + * @param utteranceId + * Unique identifier of synthesized utterance. + * @param errorCode + * One of the values from {@link Status}. + */ + public void onSynthesisFailure(UtteranceId utteranceId, int errorCode) {} + + /** + * Called during synthesis to mark synthesis progress. + * + * Requires voice with available + * {@link TextToSpeechClient.Params#TRACK_SUBUTTERANCE_PROGRESS} parameter, and + * request with this parameter enabled. + * + * @param utteranceId + * Unique identifier of synthesized utterance. + * @param charIndex + * String index (java char offset) of recently synthesized character. + * @param msFromStart + * Miliseconds from the start of the synthesis. + */ + public void onSynthesisProgress(UtteranceId utteranceId, int charIndex, int msFromStart) {} + } + + /** + * Interface definition of callbacks that are called when the client is + * connected or disconnected from the TTS service. + */ + public static interface ConnectionCallbacks { + /** + * After calling {@link TextToSpeechClient#connect()}, this method will be invoked + * asynchronously when the connect request has successfully completed. + * + * Clients are strongly encouraged to call {@link TextToSpeechClient#getEngineStatus()} + * and create {@link RequestConfig} objects used in subsequent synthesis requests. + */ + public void onConnectionSuccess(); + + /** + * After calling {@link TextToSpeechClient#connect()}, this method may be invoked + * asynchronously when the connect request has failed to complete. + * + * It may be also invoked synchronously, from the body of + * {@link TextToSpeechClient#connect()} method. + */ + public void onConnectionFailure(); + + /** + * Called when the connection to the service is lost. This can happen if there is a problem + * with the speech service (e.g. a crash or resource problem causes it to be killed by the + * system). When called, all requests have been canceled and no outstanding listeners will + * be executed. Applications should disable UI components that require the service. + */ + public void onServiceDisconnected(); + + /** + * After receiving {@link #onConnectionSuccess()} callback, this method may be invoked + * if engine status obtained from {@link TextToSpeechClient#getEngineStatus()}) changes. + * It usually means that some voices were removed, changed or added. + * + * Clients are required to recreate {@link RequestConfig} objects used in subsequent + * synthesis requests. + */ + public void onEngineStatusChange(EngineStatus newEngineStatus); + } + + /** State of voices as provided by engine and user. */ + public static final class EngineStatus { + /** All available voices. */ + private final List<VoiceInfo> mVoices; + + /** Name of the TTS engine package */ + private final String mPackageName; + + private EngineStatus(String packageName, List<VoiceInfo> voices) { + this.mVoices = Collections.unmodifiableList(voices); + this.mPackageName = packageName; + } + + /** + * Get an immutable list of all Voices exposed by the TTS engine. + */ + public List<VoiceInfo> getVoices() { + return mVoices; + } + + /** + * Get name of the TTS engine package currently in use. + */ + public String getEnginePackage() { + return mPackageName; + } + } + + /** Unique synthesis request identifier. */ + public static final class UtteranceId { + private final String mDescription; + /** + * Create new, unique UtteranceId instance. + */ + public UtteranceId() { + mDescription = null; + } + + /** + * Create new, unique UtteranceId instance. + * + * @param description Additional string, that will be appended to + * {@link #toUniqueString()} output, allowing easier identification of the utterance in + * callbacks. + */ + public UtteranceId(String description) { + mDescription = description; + } + + /** + * Returns a unique string associated with an instance of this object. + * + * If you subclass {@link UtteranceId} make sure that output of this method is + * consistent across multiple calls and unique for the instance. + * + * This string will be used to identify the synthesis request/utterance inside the + * TTS service. + */ + public String toUniqueString() { + return mDescription == null ? "UtteranceId" + System.identityHashCode(this) : + "UtteranceId" + System.identityHashCode(this) + ": " + mDescription; + } + } + + /** + * Create TextToSpeech service client. + * + * Will connect to the default TTS service. In order to be usable, {@link #connect()} need + * to be called first and successful connection callback need to be received. + * + * @param context + * The context this instance is running in. + * @param engine + * Package name of requested TTS engine. If it's null, then default engine will + * be selected regardless of {@code fallbackToDefaultEngine} parameter value. + * @param fallbackToDefaultEngine + * If requested engine is not available, should we fallback to the default engine? + * @param defaultRequestCallbacks + * Default request callbacks, it will be used for all synthesis requests without + * supplied RequestCallbacks instance. Can't be null. + * @param connectionCallbacks + * Callbacks for connecting and disconnecting from the service. Can't be null. + */ + public TextToSpeechClient(Context context, + String engine, boolean fallbackToDefaultEngine, + RequestCallbacks defaultRequestCallbacks, + ConnectionCallbacks connectionCallbacks) { + if (context == null) + throw new IllegalArgumentException("context can't be null"); + if (defaultRequestCallbacks == null) + throw new IllegalArgumentException("defaultRequestCallbacks can't be null"); + if (connectionCallbacks == null) + throw new IllegalArgumentException("connectionCallbacks can't be null"); + mContext = context; + mEnginesHelper = new TtsEngines(mContext); + mCallbacks = new HashMap<String, Pair<UtteranceId, RequestCallbacks>>(); + mDefaultRequestCallbacks = defaultRequestCallbacks; + mConnectionCallbacks = connectionCallbacks; + + mRequestedEngine = engine; + mFallbackToDefault = fallbackToDefaultEngine; + } + + /** + * Create TextToSpeech service client. Will connect to the default TTS + * service. In order to be usable, {@link #connect()} need to be called + * first and successful connection callback need to be received. + * + * @param context Context this instance is running in. + * @param defaultRequestCallbacks Default request callbacks, it + * will be used for all synthesis requests without supplied + * RequestCallbacks instance. Can't be null. + * @param connectionCallbacks Callbacks for connecting and disconnecting + * from the service. Can't be null. + */ + public TextToSpeechClient(Context context, RequestCallbacks defaultRequestCallbacks, + ConnectionCallbacks connectionCallbacks) { + this(context, null, true, defaultRequestCallbacks, connectionCallbacks); + } + + + private boolean initTts(String requestedEngine, boolean fallbackToDefaultEngine) { + // Step 1: Try connecting to the engine that was requested. + if (requestedEngine != null) { + if (mEnginesHelper.isEngineInstalled(requestedEngine)) { + if ((mServiceConnection = connectToEngine(requestedEngine)) != null) { + return true; + } else if (!fallbackToDefaultEngine) { + Log.w(TAG, "Couldn't connect to requested engine: " + requestedEngine); + return false; + } + } else if (!fallbackToDefaultEngine) { + Log.w(TAG, "Requested engine not installed: " + requestedEngine); + return false; + } + } + + // Step 2: Try connecting to the user's default engine. + final String defaultEngine = mEnginesHelper.getDefaultEngine(); + if (defaultEngine != null && !defaultEngine.equals(requestedEngine)) { + if ((mServiceConnection = connectToEngine(defaultEngine)) != null) { + return true; + } + } + + // Step 3: Try connecting to the highest ranked engine in the + // system. + final String highestRanked = mEnginesHelper.getHighestRankedEngineName(); + if (highestRanked != null && !highestRanked.equals(requestedEngine) && + !highestRanked.equals(defaultEngine)) { + if ((mServiceConnection = connectToEngine(highestRanked)) != null) { + return true; + } + } + + Log.w(TAG, "Couldn't find working TTS engine"); + return false; + } + + private Connection connectToEngine(String engine) { + Connection connection = new Connection(engine); + Intent intent = new Intent(TextToSpeech.Engine.INTENT_ACTION_TTS_SERVICE); + intent.setPackage(engine); + boolean bound = mContext.bindService(intent, connection, Context.BIND_AUTO_CREATE); + if (!bound) { + Log.e(TAG, "Failed to bind to " + engine); + return null; + } else { + Log.i(TAG, "Successfully bound to " + engine); + return connection; + } + } + + + /** + * Connects the client to TTS service. This method returns immediately, and connects to the + * service in the background. + * + * After connection initializes successfully, {@link ConnectionCallbacks#onConnectionSuccess()} + * is called. On a failure {@link ConnectionCallbacks#onConnectionFailure} is called. + * + * Both of those callback may be called asynchronously on the main thread, + * {@link ConnectionCallbacks#onConnectionFailure} may be called synchronously, before + * this method returns. + */ + public void connect() { + synchronized (mLock) { + if (mServiceConnection != null) { + return; + } + if(!initTts(mRequestedEngine, mFallbackToDefault)) { + mConnectionCallbacks.onConnectionFailure(); + } + } + } + + /** + * Checks if the client is currently connected to the service, so that + * requests to other methods will succeed. + */ + public boolean isConnected() { + synchronized (mLock) { + return mServiceConnection != null && mServiceConnection.isEstablished(); + } + } + + /** + * Closes the connection to TextToSpeech service. No calls can be made on this object after + * calling this method. + * It is good practice to call this method in the onDestroy() method of an Activity + * so the TextToSpeech engine can be cleanly stopped. + */ + public void disconnect() { + synchronized (mLock) { + if (mServiceConnection != null) { + mServiceConnection.disconnect(); + mServiceConnection = null; + mCallbacks.clear(); + } + } + } + + /** + * Register callback. + * + * @param utteranceId Non-null UtteranceId instance. + * @param callback Non-null callbacks for the request + * @return Status.SUCCESS or error code in case of invalid arguments. + */ + private int addCallback(UtteranceId utteranceId, RequestCallbacks callback) { + synchronized (mLock) { + if (utteranceId == null || callback == null) { + return Status.ERROR_INVALID_REQUEST; + } + if (mCallbacks.put(utteranceId.toUniqueString(), + new Pair<UtteranceId, RequestCallbacks>(utteranceId, callback)) != null) { + return Status.ERROR_NON_UNIQUE_UTTERANCE_ID; + } + return Status.SUCCESS; + } + } + + /** + * Remove and return callback. + * + * @param utteranceIdStr Unique string obtained from {@link UtteranceId#toUniqueString}. + */ + private Pair<UtteranceId, RequestCallbacks> removeCallback(String utteranceIdStr) { + synchronized (mLock) { + return mCallbacks.remove(utteranceIdStr); + } + } + + /** + * Get callback and utterance id. + * + * @param utteranceIdStr Unique string obtained from {@link UtteranceId#toUniqueString}. + */ + private Pair<UtteranceId, RequestCallbacks> getCallback(String utteranceIdStr) { + synchronized (mLock) { + return mCallbacks.get(utteranceIdStr); + } + } + + /** + * Remove callback and call {@link RequestCallbacks#onSynthesisFailure} with passed + * error code. + * + * @param utteranceIdStr Unique string obtained from {@link UtteranceId#toUniqueString}. + * @param errorCode argument to {@link RequestCallbacks#onSynthesisFailure} call. + */ + private void removeCallbackAndErr(String utteranceIdStr, int errorCode) { + synchronized (mLock) { + Pair<UtteranceId, RequestCallbacks> c = mCallbacks.remove(utteranceIdStr); + c.second.onSynthesisFailure(c.first, errorCode); + } + } + + /** + * Retrieve TTS engine status {@link EngineStatus}. Requires connected client. + */ + public EngineStatus getEngineStatus() { + synchronized (mLock) { + return mEngineStatus; + } + } + + /** + * Query TTS engine about available voices and defaults. + * + * @return EngineStatus is connected or null if client is disconnected. + */ + private EngineStatus requestEngineStatus(ITextToSpeechService service) + throws RemoteException { + List<VoiceInfo> voices = service.getVoicesInfo(); + if (voices == null) { + Log.e(TAG, "Requested engine doesn't support TTS V2 API"); + return null; + } + + return new EngineStatus(mServiceConnection.getEngineName(), voices); + } + + private class Connection implements ServiceConnection { + private final String mEngineName; + + private ITextToSpeechService mService; + + private boolean mEstablished; + + private PrepareConnectionAsyncTask mSetupConnectionAsyncTask; + + public Connection(String engineName) { + this.mEngineName = engineName; + } + + private final ITextToSpeechCallback.Stub mCallback = new ITextToSpeechCallback.Stub() { + + @Override + public void onStart(String utteranceIdStr) { + synchronized (mLock) { + Pair<UtteranceId, RequestCallbacks> callbacks = getCallback(utteranceIdStr); + callbacks.second.onSynthesisStart(callbacks.first); + } + } + + public void onStop(String utteranceIdStr) { + synchronized (mLock) { + Pair<UtteranceId, RequestCallbacks> callbacks = removeCallback(utteranceIdStr); + callbacks.second.onSynthesisStop(callbacks.first); + } + } + + @Override + public void onSuccess(String utteranceIdStr) { + synchronized (mLock) { + Pair<UtteranceId, RequestCallbacks> callbacks = removeCallback(utteranceIdStr); + callbacks.second.onSynthesisSuccess(callbacks.first); + } + } + + public void onFallback(String utteranceIdStr) { + synchronized (mLock) { + Pair<UtteranceId, RequestCallbacks> callbacks = getCallback(utteranceIdStr); + callbacks.second.onSynthesisFallback(callbacks.first); + } + }; + + @Override + public void onError(String utteranceIdStr, int errorCode) { + removeCallbackAndErr(utteranceIdStr, errorCode); + } + + @Override + public void onVoicesInfoChange(List<VoiceInfo> voicesInfo) { + synchronized (mLock) { + mEngineStatus = new EngineStatus(mServiceConnection.getEngineName(), + voicesInfo); + mConnectionCallbacks.onEngineStatusChange(mEngineStatus); + } + } + }; + + private class PrepareConnectionAsyncTask extends AsyncTask<Void, Void, EngineStatus> { + + private final ComponentName mName; + + public PrepareConnectionAsyncTask(ComponentName name) { + mName = name; + } + + @Override + protected EngineStatus doInBackground(Void... params) { + synchronized(mLock) { + if (isCancelled()) { + return null; + } + try { + mService.setCallback(getCallerIdentity(), mCallback); + return requestEngineStatus(mService); + } catch (RemoteException re) { + Log.e(TAG, "Error setting up the TTS service"); + return null; + } + } + } + + @Override + protected void onPostExecute(EngineStatus result) { + synchronized(mLock) { + if (mSetupConnectionAsyncTask == this) { + mSetupConnectionAsyncTask = null; + } + if (result == null) { + Log.e(TAG, "Setup task failed"); + disconnect(); + mConnectionCallbacks.onConnectionFailure(); + return; + } + + mEngineStatus = result; + mEstablished = true; + } + mConnectionCallbacks.onConnectionSuccess(); + } + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + Log.i(TAG, "Connected to " + name); + + synchronized(mLock) { + mEstablished = false; + mService = ITextToSpeechService.Stub.asInterface(service); + startSetupConnectionTask(name); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + Log.i(TAG, "Asked to disconnect from " + name); + + synchronized(mLock) { + stopSetupConnectionTask(); + } + mConnectionCallbacks.onServiceDisconnected(); + } + + private void startSetupConnectionTask(ComponentName name) { + stopSetupConnectionTask(); + mSetupConnectionAsyncTask = new PrepareConnectionAsyncTask(name); + mSetupConnectionAsyncTask.execute(); + } + + private boolean stopSetupConnectionTask() { + boolean result = false; + if (mSetupConnectionAsyncTask != null) { + result = mSetupConnectionAsyncTask.cancel(false); + mSetupConnectionAsyncTask = null; + } + return result; + } + + IBinder getCallerIdentity() { + return mCallback; + } + + boolean isEstablished() { + return mService != null && mEstablished; + } + + boolean runAction(Action action) { + synchronized (mLock) { + try { + action.run(mService); + return true; + } catch (Exception ex) { + Log.e(TAG, action.getName() + " failed", ex); + disconnect(); + return false; + } + } + } + + void disconnect() { + mContext.unbindService(this); + stopSetupConnectionTask(); + mService = null; + mEstablished = false; + if (mServiceConnection == this) { + mServiceConnection = null; + } + } + + String getEngineName() { + return mEngineName; + } + } + + private abstract class Action { + private final String mName; + + public Action(String name) { + mName = name; + } + + public String getName() {return mName;} + abstract void run(ITextToSpeechService service) throws RemoteException; + } + + private IBinder getCallerIdentity() { + if (mServiceConnection != null) { + return mServiceConnection.getCallerIdentity(); + } + return null; + } + + private boolean runAction(Action action) { + synchronized (mLock) { + if (mServiceConnection == null) { + return false; + } + if (!mServiceConnection.isEstablished()) { + return false; + } + mServiceConnection.runAction(action); + return true; + } + } + + private static final String ACTION_STOP_NAME = "stop"; + + /** + * Interrupts the current utterance spoken (whether played or rendered to file) and discards + * other utterances in the queue. + */ + public void stop() { + runAction(new Action(ACTION_STOP_NAME) { + @Override + public void run(ITextToSpeechService service) throws RemoteException { + if (service.stop(getCallerIdentity()) != Status.SUCCESS) { + Log.e(TAG, "Stop failed"); + } + mCallbacks.clear(); + } + }); + } + + private static final String ACTION_QUEUE_SPEAK_NAME = "queueSpeak"; + + /** + * Speaks the string using the specified queuing strategy using current + * voice. This method is asynchronous, i.e. the method just adds the request + * to the queue of TTS requests and then returns. The synthesis might not + * have finished (or even started!) at the time when this method returns. + * + * @param utterance The string of text to be spoken. No longer than + * 1000 characters. + * @param utteranceId Unique identificator used to track the synthesis progress + * in {@link RequestCallbacks}. + * @param config Synthesis request configuration. Can't be null. Has to contain a + * voice. + * @param callbacks Synthesis request callbacks. If null, default request + * callbacks object will be used. + */ + public void queueSpeak(final String utterance, final UtteranceId utteranceId, + final RequestConfig config, + final RequestCallbacks callbacks) { + runAction(new Action(ACTION_QUEUE_SPEAK_NAME) { + @Override + public void run(ITextToSpeechService service) throws RemoteException { + RequestCallbacks c = mDefaultRequestCallbacks; + if (callbacks != null) { + c = callbacks; + } + int addCallbackStatus = addCallback(utteranceId, c); + if (addCallbackStatus != Status.SUCCESS) { + c.onSynthesisFailure(utteranceId, Status.ERROR_INVALID_REQUEST); + return; + } + + int queueResult = service.speakV2( + getCallerIdentity(), + new SynthesisRequestV2(utterance, utteranceId.toUniqueString(), config)); + if (queueResult != Status.SUCCESS) { + removeCallbackAndErr(utteranceId.toUniqueString(), queueResult); + } + } + }); + } + + private static final String ACTION_QUEUE_SYNTHESIZE_TO_FILE = "queueSynthesizeToFile"; + + /** + * Synthesizes the given text to a file using the specified parameters. This + * method is asynchronous, i.e. the method just adds the request to the + * queue of TTS requests and then returns. The synthesis might not have + * finished (or even started!) at the time when this method returns. + * + * @param utterance The text that should be synthesized. No longer than + * 1000 characters. + * @param utteranceId Unique identificator used to track the synthesis progress + * in {@link RequestCallbacks}. + * @param outputFile File to write the generated audio data to. + * @param config Synthesis request configuration. Can't be null. Have to contain a + * voice. + * @param callbacks Synthesis request callbacks. If null, default request + * callbacks object will be used. + */ + public void queueSynthesizeToFile(final String utterance, final UtteranceId utteranceId, + final File outputFile, final RequestConfig config, + final RequestCallbacks callbacks) { + runAction(new Action(ACTION_QUEUE_SYNTHESIZE_TO_FILE) { + @Override + public void run(ITextToSpeechService service) throws RemoteException { + RequestCallbacks c = mDefaultRequestCallbacks; + if (callbacks != null) { + c = callbacks; + } + int addCallbackStatus = addCallback(utteranceId, c); + if (addCallbackStatus != Status.SUCCESS) { + c.onSynthesisFailure(utteranceId, Status.ERROR_INVALID_REQUEST); + return; + } + + ParcelFileDescriptor fileDescriptor = null; + try { + if (outputFile.exists() && !outputFile.canWrite()) { + Log.e(TAG, "No permissions to write to " + outputFile); + removeCallbackAndErr(utteranceId.toUniqueString(), Status.ERROR_OUTPUT); + return; + } + fileDescriptor = ParcelFileDescriptor.open(outputFile, + ParcelFileDescriptor.MODE_WRITE_ONLY | + ParcelFileDescriptor.MODE_CREATE | + ParcelFileDescriptor.MODE_TRUNCATE); + + int queueResult = service.synthesizeToFileDescriptorV2(getCallerIdentity(), + fileDescriptor, + new SynthesisRequestV2(utterance, utteranceId.toUniqueString(), + config)); + fileDescriptor.close(); + if (queueResult != Status.SUCCESS) { + removeCallbackAndErr(utteranceId.toUniqueString(), queueResult); + } + } catch (FileNotFoundException e) { + Log.e(TAG, "Opening file " + outputFile + " failed", e); + removeCallbackAndErr(utteranceId.toUniqueString(), Status.ERROR_OUTPUT); + } catch (IOException e) { + Log.e(TAG, "Closing file " + outputFile + " failed", e); + removeCallbackAndErr(utteranceId.toUniqueString(), Status.ERROR_OUTPUT); + } + } + }); + } + + private static final String ACTION_QUEUE_SILENCE_NAME = "queueSilence"; + + /** + * Plays silence for the specified amount of time. This method is asynchronous, + * i.e. the method just adds the request to the queue of TTS requests and then + * returns. The synthesis might not have finished (or even started!) at the time + * when this method returns. + * + * @param durationInMs The duration of the silence in milliseconds. + * @param utteranceId Unique identificator used to track the synthesis progress + * in {@link RequestCallbacks}. + * @param callbacks Synthesis request callbacks. If null, default request + * callbacks object will be used. + */ + public void queueSilence(final long durationInMs, final UtteranceId utteranceId, + final RequestCallbacks callbacks) { + runAction(new Action(ACTION_QUEUE_SILENCE_NAME) { + @Override + public void run(ITextToSpeechService service) throws RemoteException { + RequestCallbacks c = mDefaultRequestCallbacks; + if (callbacks != null) { + c = callbacks; + } + int addCallbackStatus = addCallback(utteranceId, c); + if (addCallbackStatus != Status.SUCCESS) { + c.onSynthesisFailure(utteranceId, Status.ERROR_INVALID_REQUEST); + } + + int queueResult = service.playSilence(getCallerIdentity(), durationInMs, + TextToSpeech.QUEUE_ADD, utteranceId.toUniqueString()); + + if (queueResult != Status.SUCCESS) { + removeCallbackAndErr(utteranceId.toUniqueString(), queueResult); + } + } + }); + } + + + private static final String ACTION_QUEUE_AUDIO_NAME = "queueAudio"; + + /** + * Plays the audio resource using the specified parameters. + * This method is asynchronous, i.e. the method just adds the request to the queue of TTS + * requests and then returns. The synthesis might not have finished (or even started!) at the + * time when this method returns. + * + * @param audioUrl The audio resource that should be played + * @param utteranceId Unique identificator used to track synthesis progress + * in {@link RequestCallbacks}. + * @param config Synthesis request configuration. Can't be null. Doesn't have to contain a + * voice (only system parameters are used). + * @param callbacks Synthesis request callbacks. If null, default request + * callbacks object will be used. + */ + public void queueAudio(final Uri audioUrl, final UtteranceId utteranceId, + final RequestConfig config, final RequestCallbacks callbacks) { + runAction(new Action(ACTION_QUEUE_AUDIO_NAME) { + @Override + public void run(ITextToSpeechService service) throws RemoteException { + RequestCallbacks c = mDefaultRequestCallbacks; + if (callbacks != null) { + c = callbacks; + } + int addCallbackStatus = addCallback(utteranceId, c); + if (addCallbackStatus != Status.SUCCESS) { + c.onSynthesisFailure(utteranceId, Status.ERROR_INVALID_REQUEST); + } + + int queueResult = service.playAudioV2(getCallerIdentity(), audioUrl, + utteranceId.toUniqueString(), config.getVoiceParams()); + + if (queueResult != Status.SUCCESS) { + removeCallbackAndErr(utteranceId.toUniqueString(), queueResult); + } + } + }); + } +} diff --git a/core/java/android/speech/tts/TextToSpeechService.java b/core/java/android/speech/tts/TextToSpeechService.java index 575855c..d7c51fc 100644 --- a/core/java/android/speech/tts/TextToSpeechService.java +++ b/core/java/android/speech/tts/TextToSpeechService.java @@ -34,26 +34,27 @@ import android.speech.tts.TextToSpeech.Engine; import android.text.TextUtils; import android.util.Log; -import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Locale; +import java.util.Map; +import java.util.MissingResourceException; import java.util.Set; /** * Abstract base class for TTS engine implementations. The following methods - * need to be implemented. - * + * need to be implemented for V1 API ({@link TextToSpeech}) implementation. * <ul> - * <li>{@link #onIsLanguageAvailable}</li> - * <li>{@link #onLoadLanguage}</li> - * <li>{@link #onGetLanguage}</li> - * <li>{@link #onSynthesizeText}</li> - * <li>{@link #onStop}</li> + * <li>{@link #onIsLanguageAvailable}</li> + * <li>{@link #onLoadLanguage}</li> + * <li>{@link #onGetLanguage}</li> + * <li>{@link #onSynthesizeText}</li> + * <li>{@link #onStop}</li> * </ul> - * * The first three deal primarily with language management, and are used to * query the engine for it's support for a given language and indicate to it * that requests in a given language are imminent. @@ -61,22 +62,44 @@ import java.util.Set; * {@link #onSynthesizeText} is central to the engine implementation. The * implementation should synthesize text as per the request parameters and * return synthesized data via the supplied callback. This class and its helpers - * will then consume that data, which might mean queueing it for playback or writing - * it to a file or similar. All calls to this method will be on a single - * thread, which will be different from the main thread of the service. Synthesis - * must be synchronous which means the engine must NOT hold on the callback or call - * any methods on it after the method returns + * will then consume that data, which might mean queuing it for playback or writing + * it to a file or similar. All calls to this method will be on a single thread, + * which will be different from the main thread of the service. Synthesis must be + * synchronous which means the engine must NOT hold on to the callback or call any + * methods on it after the method returns. * - * {@link #onStop} tells the engine that it should stop all ongoing synthesis, if - * any. Any pending data from the current synthesis will be discarded. + * {@link #onStop} tells the engine that it should stop + * all ongoing synthesis, if any. Any pending data from the current synthesis + * will be discarded. * + * {@link #onGetLanguage} is not required as of JELLYBEAN_MR2 (API 18) and later, it is only + * called on earlier versions of Android. + * <p> + * In order to fully support the V2 API ({@link TextToSpeechClient}), + * these methods must be implemented: + * <ul> + * <li>{@link #onSynthesizeTextV2}</li> + * <li>{@link #checkVoicesInfo}</li> + * <li>{@link #onVoicesInfoChange}</li> + * <li>{@link #implementsV2API}</li> + * </ul> + * In addition {@link #implementsV2API} has to return true. + * <p> + * If the service does not implement these methods and {@link #implementsV2API} returns false, + * then the V2 API will be provided by converting V2 requests ({@link #onSynthesizeTextV2}) + * to V1 requests ({@link #onSynthesizeText}). On service setup, all of the available device + * locales will be fed to {@link #onIsLanguageAvailable} to check if they are supported. + * If they are, embedded and/or network voices will be created depending on the result of + * {@link #onGetFeaturesForLanguage}. + * <p> + * Note that a V2 service will still receive requests from V1 clients and has to implement all + * of the V1 API methods. */ public abstract class TextToSpeechService extends Service { private static final boolean DBG = false; private static final String TAG = "TextToSpeechService"; - private static final String SYNTH_THREAD_NAME = "SynthThread"; private SynthHandler mSynthHandler; @@ -89,6 +112,11 @@ public abstract class TextToSpeechService extends Service { private CallbackMap mCallbacks; private String mPackageName; + private final Object mVoicesInfoLock = new Object(); + + private List<VoiceInfo> mVoicesInfoList; + private Map<String, VoiceInfo> mVoicesInfoLookup; + @Override public void onCreate() { if (DBG) Log.d(TAG, "onCreate()"); @@ -108,6 +136,7 @@ public abstract class TextToSpeechService extends Service { mPackageName = getApplicationInfo().packageName; String[] defaultLocale = getSettingsLocale(); + // Load default language onLoadLanguage(defaultLocale[0], defaultLocale[1], defaultLocale[2]); } @@ -148,6 +177,9 @@ public abstract class TextToSpeechService extends Service { /** * Returns the language, country and variant currently being used by the TTS engine. * + * This method will be called only on Android 4.2 and before (API <= 17). In later versions + * this method is not called by the Android TTS framework. + * * Can be called on multiple threads. * * @return A 3-element array, containing language (ISO 3-letter code), @@ -191,21 +223,159 @@ public abstract class TextToSpeechService extends Service { protected abstract void onStop(); /** - * Tells the service to synthesize speech from the given text. This method should - * block until the synthesis is finished. - * - * Called on the synthesis thread. + * Tells the service to synthesize speech from the given text. This method + * should block until the synthesis is finished. Used for requests from V1 + * clients ({@link android.speech.tts.TextToSpeech}). Called on the synthesis + * thread. * * @param request The synthesis request. - * @param callback The callback the the engine must use to make data available for - * playback or for writing to a file. + * @param callback The callback that the engine must use to make data + * available for playback or for writing to a file. */ protected abstract void onSynthesizeText(SynthesisRequest request, SynthesisCallback callback); /** + * Check the available voices data and return an immutable list of the available voices. + * The output of this method will be passed to clients to allow them to configure synthesis + * requests. + * + * Can be called on multiple threads. + * + * The result of this method will be saved and served to all TTS clients. If a TTS service wants + * to update the set of available voices, it should call the {@link #forceVoicesInfoCheck()} + * method. + */ + protected List<VoiceInfo> checkVoicesInfo() { + if (implementsV2API()) { + throw new IllegalStateException("For proper V2 API implementation this method has to" + + " be implemented"); + } + + // V2 to V1 interface adapter. This allows using V2 client interface on V1-only services. + Bundle defaultParams = new Bundle(); + defaultParams.putFloat(TextToSpeechClient.Params.SPEECH_PITCH, 1.0f); + defaultParams.putFloat(TextToSpeechClient.Params.SPEECH_SPEED, -1.0f); + + // Enumerate all locales and check if they are available + ArrayList<VoiceInfo> voicesInfo = new ArrayList<VoiceInfo>(); + int id = 0; + for (Locale locale : Locale.getAvailableLocales()) { + int expectedStatus = TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE; + if (locale.getVariant().isEmpty()) { + if (locale.getCountry().isEmpty()) { + expectedStatus = TextToSpeech.LANG_AVAILABLE; + } else { + expectedStatus = TextToSpeech.LANG_COUNTRY_AVAILABLE; + } + } + try { + int localeStatus = onIsLanguageAvailable(locale.getISO3Language(), + locale.getISO3Country(), locale.getVariant()); + if (localeStatus != expectedStatus) { + continue; + } + } catch (MissingResourceException e) { + // Ignore locale without iso 3 codes + continue; + } + + Set<String> features = onGetFeaturesForLanguage(locale.getISO3Language(), + locale.getISO3Country(), locale.getVariant()); + + VoiceInfo.Builder builder = new VoiceInfo.Builder(); + builder.setLatency(VoiceInfo.LATENCY_NORMAL); + builder.setQuality(VoiceInfo.QUALITY_NORMAL); + builder.setLocale(locale); + builder.setParamsWithDefaults(defaultParams); + + if (features == null || features.contains( + TextToSpeech.Engine.KEY_FEATURE_EMBEDDED_SYNTHESIS)) { + builder.setName(locale.toString() + "-embedded"); + builder.setRequiresNetworkConnection(false); + voicesInfo.add(builder.build()); + } + + if (features != null && features.contains( + TextToSpeech.Engine.KEY_FEATURE_NETWORK_SYNTHESIS)) { + builder.setName(locale.toString() + "-network"); + builder.setRequiresNetworkConnection(true); + voicesInfo.add(builder.build()); + } + } + + return voicesInfo; + } + + /** + * Tells the synthesis thread that it should reload voice data. + * There's a high probability that the underlying set of available voice data has changed. + * Called only on the synthesis thread. + */ + protected void onVoicesInfoChange() { + + } + + /** + * Tells the service to synthesize speech from the given text. This method + * should block until the synthesis is finished. Used for requests from V2 + * client {@link android.speech.tts.TextToSpeechClient}. Called on the + * synthesis thread. + * + * @param request The synthesis request. + * @param callback The callback the the engine must use to make data + * available for playback or for writing to a file. + */ + protected void onSynthesizeTextV2(SynthesisRequestV2 request, + VoiceInfo selectedVoice, + SynthesisCallback callback) { + if (implementsV2API()) { + throw new IllegalStateException("For proper V2 API implementation this method has to" + + " be implemented"); + } + + // Convert to V1 params + int speechRate = (int) (request.getVoiceParams().getFloat( + TextToSpeechClient.Params.SPEECH_SPEED, 1.0f) * 100); + int speechPitch = (int) (request.getVoiceParams().getFloat( + TextToSpeechClient.Params.SPEECH_PITCH, 1.0f) * 100); + + // Provide adapter to V1 API + Bundle params = new Bundle(); + params.putString(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, request.getUtteranceId()); + params.putInt(TextToSpeech.Engine.KEY_PARAM_PITCH, speechPitch); + params.putInt(TextToSpeech.Engine.KEY_PARAM_RATE, speechRate); + if (selectedVoice.getRequiresNetworkConnection()) { + params.putString(TextToSpeech.Engine.KEY_FEATURE_NETWORK_SYNTHESIS, "true"); + } else { + params.putString(TextToSpeech.Engine.KEY_FEATURE_EMBEDDED_SYNTHESIS, "true"); + } + + // Build V1 request + SynthesisRequest requestV1 = new SynthesisRequest(request.getText(), params); + Locale locale = selectedVoice.getLocale(); + requestV1.setLanguage(locale.getISO3Language(), locale.getISO3Country(), + locale.getVariant()); + requestV1.setSpeechRate(speechRate); + requestV1.setPitch(speechPitch); + + // Synthesize using V1 interface + onSynthesizeText(requestV1, callback); + } + + /** + * If true, this service implements proper V2 TTS API service. If it's false, + * V2 API will be provided through adapter. + */ + protected boolean implementsV2API() { + return false; + } + + /** * Queries the service for a set of features supported for a given language. * + * Can be called on multiple threads. + * * @param lang ISO-3 language code. * @param country ISO-3 country code. May be empty or null. * @param variant Language variant. May be empty or null. @@ -215,6 +385,69 @@ public abstract class TextToSpeechService extends Service { return null; } + private List<VoiceInfo> getVoicesInfo() { + synchronized (mVoicesInfoLock) { + if (mVoicesInfoList == null) { + // Get voices. Defensive copy to make sure TTS engine won't alter the list. + mVoicesInfoList = new ArrayList<VoiceInfo>(checkVoicesInfo()); + // Build lookup map + mVoicesInfoLookup = new HashMap<String, VoiceInfo>((int) ( + mVoicesInfoList.size()*1.5f)); + for (VoiceInfo voiceInfo : mVoicesInfoList) { + VoiceInfo prev = mVoicesInfoLookup.put(voiceInfo.getName(), voiceInfo); + if (prev != null) { + Log.e(TAG, "Duplicate name (" + voiceInfo.getName() + ") of the voice "); + } + } + } + return mVoicesInfoList; + } + } + + public VoiceInfo getVoicesInfoWithName(String name) { + synchronized (mVoicesInfoLock) { + if (mVoicesInfoLookup != null) { + return mVoicesInfoLookup.get(name); + } + } + return null; + } + + /** + * Force TTS service to reevaluate the set of available languages. Will result in + * a call to {@link #checkVoicesInfo()} on the same thread, {@link #onVoicesInfoChange} + * on the synthesizer thread and callback to + * {@link TextToSpeechClient.ConnectionCallbacks#onEngineStatusChange} of all connected + * TTS clients. + * + * Use this method only if you know that set of available languages changed. + * + * Can be called on multiple threads. + */ + public void forceVoicesInfoCheck() { + synchronized (mVoicesInfoLock) { + List<VoiceInfo> old = mVoicesInfoList; + + mVoicesInfoList = null; // Force recreation of voices info list + getVoicesInfo(); + + if (mVoicesInfoList == null) { + throw new IllegalStateException("This method applies only to services " + + "supporting V2 TTS API. This services doesn't support V2 TTS API."); + } + + if (old != null) { + // Flush all existing items, and inform synthesis thread about the change. + mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_FLUSH, + new VoicesInfoChangeItem()); + // TODO: Handle items that may be added to queue after SynthesizerRestartItem + // but before client reconnection + // Disconnect all of them + mCallbacks.dispatchVoicesInfoChange(mVoicesInfoList); + } + } + } + private int getDefaultSpeechRate() { return getSecureSettingInt(Settings.Secure.TTS_DEFAULT_RATE, Engine.DEFAULT_RATE); } @@ -317,7 +550,8 @@ public abstract class TextToSpeechService extends Service { if (!speechItem.isValid()) { if (utterenceProgress != null) { - utterenceProgress.dispatchOnError(); + utterenceProgress.dispatchOnError( + TextToSpeechClient.Status.ERROR_INVALID_REQUEST); } return TextToSpeech.ERROR; } @@ -342,12 +576,13 @@ public abstract class TextToSpeechService extends Service { // // Note that this string is interned, so the == comparison works. msg.obj = speechItem.getCallerIdentity(); + if (sendMessage(msg)) { return TextToSpeech.SUCCESS; } else { Log.w(TAG, "SynthThread has quit"); if (utterenceProgress != null) { - utterenceProgress.dispatchOnError(); + utterenceProgress.dispatchOnError(TextToSpeechClient.Status.ERROR_SERVICE); } return TextToSpeech.ERROR; } @@ -399,9 +634,11 @@ public abstract class TextToSpeechService extends Service { } interface UtteranceProgressDispatcher { - public void dispatchOnDone(); + public void dispatchOnFallback(); + public void dispatchOnStop(); + public void dispatchOnSuccess(); public void dispatchOnStart(); - public void dispatchOnError(); + public void dispatchOnError(int errorCode); } /** @@ -409,15 +646,13 @@ public abstract class TextToSpeechService extends Service { */ private abstract class SpeechItem { private final Object mCallerIdentity; - protected final Bundle mParams; private final int mCallerUid; private final int mCallerPid; private boolean mStarted = false; private boolean mStopped = false; - public SpeechItem(Object caller, int callerUid, int callerPid, Bundle params) { + public SpeechItem(Object caller, int callerUid, int callerPid) { mCallerIdentity = caller; - mParams = params; mCallerUid = callerUid; mCallerPid = callerPid; } @@ -446,20 +681,18 @@ public abstract class TextToSpeechService extends Service { * Must not be called more than once. * * Only called on the synthesis thread. - * - * @return {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}. */ - public int play() { + public void play() { synchronized (this) { if (mStarted) { throw new IllegalStateException("play() called twice"); } mStarted = true; } - return playImpl(); + playImpl(); } - protected abstract int playImpl(); + protected abstract void playImpl(); /** * Stops the speech item. @@ -485,20 +718,37 @@ public abstract class TextToSpeechService extends Service { } /** - * An item in the synth thread queue that process utterance. + * An item in the synth thread queue that process utterance (and call back to client about + * progress). */ private abstract class UtteranceSpeechItem extends SpeechItem implements UtteranceProgressDispatcher { - public UtteranceSpeechItem(Object caller, int callerUid, int callerPid, Bundle params) { - super(caller, callerUid, callerPid, params); + public UtteranceSpeechItem(Object caller, int callerUid, int callerPid) { + super(caller, callerUid, callerPid); + } + + @Override + public void dispatchOnSuccess() { + final String utteranceId = getUtteranceId(); + if (utteranceId != null) { + mCallbacks.dispatchOnSuccess(getCallerIdentity(), utteranceId); + } } @Override - public void dispatchOnDone() { + public void dispatchOnStop() { final String utteranceId = getUtteranceId(); if (utteranceId != null) { - mCallbacks.dispatchOnDone(getCallerIdentity(), utteranceId); + mCallbacks.dispatchOnStop(getCallerIdentity(), utteranceId); + } + } + + @Override + public void dispatchOnFallback() { + final String utteranceId = getUtteranceId(); + if (utteranceId != null) { + mCallbacks.dispatchOnFallback(getCallerIdentity(), utteranceId); } } @@ -511,44 +761,260 @@ public abstract class TextToSpeechService extends Service { } @Override - public void dispatchOnError() { + public void dispatchOnError(int errorCode) { final String utteranceId = getUtteranceId(); if (utteranceId != null) { - mCallbacks.dispatchOnError(getCallerIdentity(), utteranceId); + mCallbacks.dispatchOnError(getCallerIdentity(), utteranceId, errorCode); } } - public int getStreamType() { - return getIntParam(Engine.KEY_PARAM_STREAM, Engine.DEFAULT_STREAM); + abstract public String getUtteranceId(); + + String getStringParam(Bundle params, String key, String defaultValue) { + return params == null ? defaultValue : params.getString(key, defaultValue); + } + + int getIntParam(Bundle params, String key, int defaultValue) { + return params == null ? defaultValue : params.getInt(key, defaultValue); + } + + float getFloatParam(Bundle params, String key, float defaultValue) { + return params == null ? defaultValue : params.getFloat(key, defaultValue); + } + } + + /** + * UtteranceSpeechItem for V1 API speech items. V1 API speech items keep + * synthesis parameters in a single Bundle passed as parameter. This class + * allow subclasses to access them conveniently. + */ + private abstract class SpeechItemV1 extends UtteranceSpeechItem { + protected final Bundle mParams; + + SpeechItemV1(Object callerIdentity, int callerUid, int callerPid, + Bundle params) { + super(callerIdentity, callerUid, callerPid); + mParams = params; + } + + boolean hasLanguage() { + return !TextUtils.isEmpty(getStringParam(mParams, Engine.KEY_PARAM_LANGUAGE, null)); } - public float getVolume() { - return getFloatParam(Engine.KEY_PARAM_VOLUME, Engine.DEFAULT_VOLUME); + int getSpeechRate() { + return getIntParam(mParams, Engine.KEY_PARAM_RATE, getDefaultSpeechRate()); } - public float getPan() { - return getFloatParam(Engine.KEY_PARAM_PAN, Engine.DEFAULT_PAN); + int getPitch() { + return getIntParam(mParams, Engine.KEY_PARAM_PITCH, Engine.DEFAULT_PITCH); } + @Override public String getUtteranceId() { - return getStringParam(Engine.KEY_PARAM_UTTERANCE_ID, null); + return getStringParam(mParams, Engine.KEY_PARAM_UTTERANCE_ID, null); + } + + int getStreamType() { + return getIntParam(mParams, Engine.KEY_PARAM_STREAM, Engine.DEFAULT_STREAM); + } + + float getVolume() { + return getFloatParam(mParams, Engine.KEY_PARAM_VOLUME, Engine.DEFAULT_VOLUME); + } + + float getPan() { + return getFloatParam(mParams, Engine.KEY_PARAM_PAN, Engine.DEFAULT_PAN); + } + } + + class SynthesisSpeechItemV2 extends UtteranceSpeechItem { + private final SynthesisRequestV2 mSynthesisRequest; + private AbstractSynthesisCallback mSynthesisCallback; + private final EventLoggerV2 mEventLogger; + + public SynthesisSpeechItemV2(Object callerIdentity, int callerUid, int callerPid, + SynthesisRequestV2 synthesisRequest) { + super(callerIdentity, callerUid, callerPid); + + mSynthesisRequest = synthesisRequest; + mEventLogger = new EventLoggerV2(synthesisRequest, callerUid, callerPid, + mPackageName); + + updateSpeechSpeedParam(synthesisRequest); + } + + private void updateSpeechSpeedParam(SynthesisRequestV2 synthesisRequest) { + Bundle voiceParams = mSynthesisRequest.getVoiceParams(); + + // Inject default speech speed if needed + if (voiceParams.containsKey(TextToSpeechClient.Params.SPEECH_SPEED)) { + if (voiceParams.getFloat(TextToSpeechClient.Params.SPEECH_SPEED) <= 0) { + voiceParams.putFloat(TextToSpeechClient.Params.SPEECH_SPEED, + getDefaultSpeechRate() / 100.0f); + } + } } - protected String getStringParam(String key, String defaultValue) { - return mParams == null ? defaultValue : mParams.getString(key, defaultValue); + @Override + public boolean isValid() { + if (mSynthesisRequest.getText() == null) { + Log.e(TAG, "null synthesis text"); + return false; + } + if (mSynthesisRequest.getText().length() >= TextToSpeech.getMaxSpeechInputLength()) { + Log.w(TAG, "Text too long: " + mSynthesisRequest.getText().length() + " chars"); + return false; + } + + return true; } - protected int getIntParam(String key, int defaultValue) { - return mParams == null ? defaultValue : mParams.getInt(key, defaultValue); + @Override + protected void playImpl() { + AbstractSynthesisCallback synthesisCallback; + if (mEventLogger != null) { + mEventLogger.onRequestProcessingStart(); + } + synchronized (this) { + // stop() might have been called before we enter this + // synchronized block. + if (isStopped()) { + return; + } + mSynthesisCallback = createSynthesisCallback(); + synthesisCallback = mSynthesisCallback; + } + + // Get voice info + VoiceInfo voiceInfo = getVoicesInfoWithName(mSynthesisRequest.getVoiceName()); + if (voiceInfo != null) { + // Primary voice + TextToSpeechService.this.onSynthesizeTextV2(mSynthesisRequest, voiceInfo, + synthesisCallback); + } else { + Log.e(TAG, "Unknown voice name:" + mSynthesisRequest.getVoiceName()); + synthesisCallback.error(TextToSpeechClient.Status.ERROR_INVALID_REQUEST); + } + + // Fix for case where client called .start() & .error(), but did not called .done() + if (!synthesisCallback.hasFinished()) { + synthesisCallback.done(); + } } - protected float getFloatParam(String key, float defaultValue) { - return mParams == null ? defaultValue : mParams.getFloat(key, defaultValue); + @Override + protected void stopImpl() { + AbstractSynthesisCallback synthesisCallback; + synchronized (this) { + synthesisCallback = mSynthesisCallback; + } + if (synthesisCallback != null) { + // If the synthesis callback is null, it implies that we haven't + // entered the synchronized(this) block in playImpl which in + // turn implies that synthesis would not have started. + synthesisCallback.stop(); + TextToSpeechService.this.onStop(); + } } + protected AbstractSynthesisCallback createSynthesisCallback() { + return new PlaybackSynthesisCallback(getStreamType(), getVolume(), getPan(), + mAudioPlaybackHandler, this, getCallerIdentity(), mEventLogger, + implementsV2API()); + } + + private int getStreamType() { + return getIntParam(mSynthesisRequest.getAudioParams(), + TextToSpeechClient.Params.AUDIO_PARAM_STREAM, + Engine.DEFAULT_STREAM); + } + + private float getVolume() { + return getFloatParam(mSynthesisRequest.getAudioParams(), + TextToSpeechClient.Params.AUDIO_PARAM_VOLUME, + Engine.DEFAULT_VOLUME); + } + + private float getPan() { + return getFloatParam(mSynthesisRequest.getAudioParams(), + TextToSpeechClient.Params.AUDIO_PARAM_PAN, + Engine.DEFAULT_PAN); + } + + @Override + public String getUtteranceId() { + return mSynthesisRequest.getUtteranceId(); + } + } + + private class SynthesisToFileOutputStreamSpeechItemV2 extends SynthesisSpeechItemV2 { + private final FileOutputStream mFileOutputStream; + + public SynthesisToFileOutputStreamSpeechItemV2(Object callerIdentity, int callerUid, + int callerPid, + SynthesisRequestV2 synthesisRequest, + FileOutputStream fileOutputStream) { + super(callerIdentity, callerUid, callerPid, synthesisRequest); + mFileOutputStream = fileOutputStream; + } + + @Override + protected AbstractSynthesisCallback createSynthesisCallback() { + return new FileSynthesisCallback(mFileOutputStream.getChannel(), + this, getCallerIdentity(), implementsV2API()); + } + + @Override + protected void playImpl() { + super.playImpl(); + try { + mFileOutputStream.close(); + } catch(IOException e) { + Log.w(TAG, "Failed to close output file", e); + } + } + } + + private class AudioSpeechItemV2 extends UtteranceSpeechItem { + private final AudioPlaybackQueueItem mItem; + private final Bundle mAudioParams; + private final String mUtteranceId; + + public AudioSpeechItemV2(Object callerIdentity, int callerUid, int callerPid, + String utteranceId, Bundle audioParams, Uri uri) { + super(callerIdentity, callerUid, callerPid); + mUtteranceId = utteranceId; + mAudioParams = audioParams; + mItem = new AudioPlaybackQueueItem(this, getCallerIdentity(), + TextToSpeechService.this, uri, getStreamType()); + } + + @Override + public boolean isValid() { + return true; + } + + @Override + protected void playImpl() { + mAudioPlaybackHandler.enqueue(mItem); + } + + @Override + protected void stopImpl() { + // Do nothing. + } + + protected int getStreamType() { + return mAudioParams.getInt(TextToSpeechClient.Params.AUDIO_PARAM_STREAM); + } + + public String getUtteranceId() { + return mUtteranceId; + } } - class SynthesisSpeechItem extends UtteranceSpeechItem { + + class SynthesisSpeechItemV1 extends SpeechItemV1 { // Never null. private final String mText; private final SynthesisRequest mSynthesisRequest; @@ -556,10 +1022,10 @@ public abstract class TextToSpeechService extends Service { // Non null after synthesis has started, and all accesses // guarded by 'this'. private AbstractSynthesisCallback mSynthesisCallback; - private final EventLogger mEventLogger; + private final EventLoggerV1 mEventLogger; private final int mCallerUid; - public SynthesisSpeechItem(Object callerIdentity, int callerUid, int callerPid, + public SynthesisSpeechItemV1(Object callerIdentity, int callerUid, int callerPid, Bundle params, String text) { super(callerIdentity, callerUid, callerPid, params); mText = text; @@ -567,7 +1033,7 @@ public abstract class TextToSpeechService extends Service { mSynthesisRequest = new SynthesisRequest(mText, mParams); mDefaultLocale = getSettingsLocale(); setRequestParams(mSynthesisRequest); - mEventLogger = new EventLogger(mSynthesisRequest, callerUid, callerPid, + mEventLogger = new EventLoggerV1(mSynthesisRequest, callerUid, callerPid, mPackageName); } @@ -589,25 +1055,30 @@ public abstract class TextToSpeechService extends Service { } @Override - protected int playImpl() { + protected void playImpl() { AbstractSynthesisCallback synthesisCallback; mEventLogger.onRequestProcessingStart(); synchronized (this) { // stop() might have been called before we enter this // synchronized block. if (isStopped()) { - return TextToSpeech.ERROR; + return; } mSynthesisCallback = createSynthesisCallback(); synthesisCallback = mSynthesisCallback; } + TextToSpeechService.this.onSynthesizeText(mSynthesisRequest, synthesisCallback); - return synthesisCallback.isDone() ? TextToSpeech.SUCCESS : TextToSpeech.ERROR; + + // Fix for case where client called .start() & .error(), but did not called .done() + if (synthesisCallback.hasStarted() && !synthesisCallback.hasFinished()) { + synthesisCallback.done(); + } } protected AbstractSynthesisCallback createSynthesisCallback() { return new PlaybackSynthesisCallback(getStreamType(), getVolume(), getPan(), - mAudioPlaybackHandler, this, getCallerIdentity(), mEventLogger); + mAudioPlaybackHandler, this, getCallerIdentity(), mEventLogger, false); } private void setRequestParams(SynthesisRequest request) { @@ -632,37 +1103,25 @@ public abstract class TextToSpeechService extends Service { } } - public String getLanguage() { - return getStringParam(Engine.KEY_PARAM_LANGUAGE, mDefaultLocale[0]); - } - - private boolean hasLanguage() { - return !TextUtils.isEmpty(getStringParam(Engine.KEY_PARAM_LANGUAGE, null)); - } - private String getCountry() { if (!hasLanguage()) return mDefaultLocale[1]; - return getStringParam(Engine.KEY_PARAM_COUNTRY, ""); + return getStringParam(mParams, Engine.KEY_PARAM_COUNTRY, ""); } private String getVariant() { if (!hasLanguage()) return mDefaultLocale[2]; - return getStringParam(Engine.KEY_PARAM_VARIANT, ""); + return getStringParam(mParams, Engine.KEY_PARAM_VARIANT, ""); } - private int getSpeechRate() { - return getIntParam(Engine.KEY_PARAM_RATE, getDefaultSpeechRate()); - } - - private int getPitch() { - return getIntParam(Engine.KEY_PARAM_PITCH, Engine.DEFAULT_PITCH); + public String getLanguage() { + return getStringParam(mParams, Engine.KEY_PARAM_LANGUAGE, mDefaultLocale[0]); } } - private class SynthesisToFileOutputStreamSpeechItem extends SynthesisSpeechItem { + private class SynthesisToFileOutputStreamSpeechItemV1 extends SynthesisSpeechItemV1 { private final FileOutputStream mFileOutputStream; - public SynthesisToFileOutputStreamSpeechItem(Object callerIdentity, int callerUid, + public SynthesisToFileOutputStreamSpeechItemV1(Object callerIdentity, int callerUid, int callerPid, Bundle params, String text, FileOutputStream fileOutputStream) { super(callerIdentity, callerUid, callerPid, params, text); mFileOutputStream = fileOutputStream; @@ -670,30 +1129,26 @@ public abstract class TextToSpeechService extends Service { @Override protected AbstractSynthesisCallback createSynthesisCallback() { - return new FileSynthesisCallback(mFileOutputStream.getChannel()); + return new FileSynthesisCallback(mFileOutputStream.getChannel(), + this, getCallerIdentity(), false); } @Override - protected int playImpl() { + protected void playImpl() { dispatchOnStart(); - int status = super.playImpl(); - if (status == TextToSpeech.SUCCESS) { - dispatchOnDone(); - } else { - dispatchOnError(); - } + super.playImpl(); try { mFileOutputStream.close(); } catch(IOException e) { Log.w(TAG, "Failed to close output file", e); } - return status; } } - private class AudioSpeechItem extends UtteranceSpeechItem { + private class AudioSpeechItemV1 extends SpeechItemV1 { private final AudioPlaybackQueueItem mItem; - public AudioSpeechItem(Object callerIdentity, int callerUid, int callerPid, + + public AudioSpeechItemV1(Object callerIdentity, int callerUid, int callerPid, Bundle params, Uri uri) { super(callerIdentity, callerUid, callerPid, params); mItem = new AudioPlaybackQueueItem(this, getCallerIdentity(), @@ -706,23 +1161,29 @@ public abstract class TextToSpeechService extends Service { } @Override - protected int playImpl() { + protected void playImpl() { mAudioPlaybackHandler.enqueue(mItem); - return TextToSpeech.SUCCESS; } @Override protected void stopImpl() { // Do nothing. } + + @Override + public String getUtteranceId() { + return getStringParam(mParams, Engine.KEY_PARAM_UTTERANCE_ID, null); + } } private class SilenceSpeechItem extends UtteranceSpeechItem { private final long mDuration; + private final String mUtteranceId; public SilenceSpeechItem(Object callerIdentity, int callerUid, int callerPid, - Bundle params, long duration) { - super(callerIdentity, callerUid, callerPid, params); + String utteranceId, long duration) { + super(callerIdentity, callerUid, callerPid); + mUtteranceId = utteranceId; mDuration = duration; } @@ -732,26 +1193,57 @@ public abstract class TextToSpeechService extends Service { } @Override - protected int playImpl() { + protected void playImpl() { mAudioPlaybackHandler.enqueue(new SilencePlaybackQueueItem( this, getCallerIdentity(), mDuration)); - return TextToSpeech.SUCCESS; } @Override protected void stopImpl() { - // Do nothing, handled by AudioPlaybackHandler#stopForApp + + } + + @Override + public String getUtteranceId() { + return mUtteranceId; + } + } + + /** + * Call {@link TextToSpeechService#onVoicesInfoChange} on synthesis thread. + */ + private class VoicesInfoChangeItem extends SpeechItem { + public VoicesInfoChangeItem() { + super(null, 0, 0); // It's never initiated by an user + } + + @Override + public boolean isValid() { + return true; + } + + @Override + protected void playImpl() { + TextToSpeechService.this.onVoicesInfoChange(); + } + + @Override + protected void stopImpl() { + // No-op } } + /** + * Call {@link TextToSpeechService#onLoadLanguage} on synth thread. + */ private class LoadLanguageItem extends SpeechItem { private final String mLanguage; private final String mCountry; private final String mVariant; public LoadLanguageItem(Object callerIdentity, int callerUid, int callerPid, - Bundle params, String language, String country, String variant) { - super(callerIdentity, callerUid, callerPid, params); + String language, String country, String variant) { + super(callerIdentity, callerUid, callerPid); mLanguage = language; mCountry = country; mVariant = variant; @@ -763,14 +1255,8 @@ public abstract class TextToSpeechService extends Service { } @Override - protected int playImpl() { - int result = TextToSpeechService.this.onLoadLanguage(mLanguage, mCountry, mVariant); - if (result == TextToSpeech.LANG_AVAILABLE || - result == TextToSpeech.LANG_COUNTRY_AVAILABLE || - result == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) { - return TextToSpeech.SUCCESS; - } - return TextToSpeech.ERROR; + protected void playImpl() { + TextToSpeechService.this.onLoadLanguage(mLanguage, mCountry, mVariant); } @Override @@ -800,7 +1286,7 @@ public abstract class TextToSpeechService extends Service { return TextToSpeech.ERROR; } - SpeechItem item = new SynthesisSpeechItem(caller, + SpeechItem item = new SynthesisSpeechItemV1(caller, Binder.getCallingUid(), Binder.getCallingPid(), params, text); return mSynthHandler.enqueueSpeechItem(queueMode, item); } @@ -818,7 +1304,7 @@ public abstract class TextToSpeechService extends Service { final ParcelFileDescriptor sameFileDescriptor = ParcelFileDescriptor.adoptFd( fileDescriptor.detachFd()); - SpeechItem item = new SynthesisToFileOutputStreamSpeechItem(caller, + SpeechItem item = new SynthesisToFileOutputStreamSpeechItemV1(caller, Binder.getCallingUid(), Binder.getCallingPid(), params, text, new ParcelFileDescriptor.AutoCloseOutputStream(sameFileDescriptor)); return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item); @@ -830,19 +1316,19 @@ public abstract class TextToSpeechService extends Service { return TextToSpeech.ERROR; } - SpeechItem item = new AudioSpeechItem(caller, + SpeechItem item = new AudioSpeechItemV1(caller, Binder.getCallingUid(), Binder.getCallingPid(), params, audioUri); return mSynthHandler.enqueueSpeechItem(queueMode, item); } @Override - public int playSilence(IBinder caller, long duration, int queueMode, Bundle params) { - if (!checkNonNull(caller, params)) { + public int playSilence(IBinder caller, long duration, int queueMode, String utteranceId) { + if (!checkNonNull(caller)) { return TextToSpeech.ERROR; } SpeechItem item = new SilenceSpeechItem(caller, - Binder.getCallingUid(), Binder.getCallingPid(), params, duration); + Binder.getCallingUid(), Binder.getCallingPid(), utteranceId, duration); return mSynthHandler.enqueueSpeechItem(queueMode, item); } @@ -912,7 +1398,7 @@ public abstract class TextToSpeechService extends Service { retVal == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) { SpeechItem item = new LoadLanguageItem(caller, Binder.getCallingUid(), - Binder.getCallingPid(), null, lang, country, variant); + Binder.getCallingPid(), lang, country, variant); if (mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item) != TextToSpeech.SUCCESS) { @@ -943,6 +1429,58 @@ public abstract class TextToSpeechService extends Service { } return true; } + + @Override + public List<VoiceInfo> getVoicesInfo() { + return TextToSpeechService.this.getVoicesInfo(); + } + + @Override + public int speakV2(IBinder callingInstance, + SynthesisRequestV2 request) { + if (!checkNonNull(callingInstance, request)) { + return TextToSpeech.ERROR; + } + + SpeechItem item = new SynthesisSpeechItemV2(callingInstance, + Binder.getCallingUid(), Binder.getCallingPid(), request); + return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item); + } + + @Override + public int synthesizeToFileDescriptorV2(IBinder callingInstance, + ParcelFileDescriptor fileDescriptor, + SynthesisRequestV2 request) { + if (!checkNonNull(callingInstance, request, fileDescriptor)) { + return TextToSpeech.ERROR; + } + + // In test env, ParcelFileDescriptor instance may be EXACTLY the same + // one that is used by client. And it will be closed by a client, thus + // preventing us from writing anything to it. + final ParcelFileDescriptor sameFileDescriptor = ParcelFileDescriptor.adoptFd( + fileDescriptor.detachFd()); + + SpeechItem item = new SynthesisToFileOutputStreamSpeechItemV2(callingInstance, + Binder.getCallingUid(), Binder.getCallingPid(), request, + new ParcelFileDescriptor.AutoCloseOutputStream(sameFileDescriptor)); + return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item); + + } + + @Override + public int playAudioV2( + IBinder callingInstance, Uri audioUri, String utteranceId, + Bundle systemParameters) { + if (!checkNonNull(callingInstance, audioUri, systemParameters)) { + return TextToSpeech.ERROR; + } + + SpeechItem item = new AudioSpeechItemV2(callingInstance, + Binder.getCallingUid(), Binder.getCallingPid(), utteranceId, systemParameters, + audioUri); + return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item); + } }; private class CallbackMap extends RemoteCallbackList<ITextToSpeechCallback> { @@ -964,11 +1502,31 @@ public abstract class TextToSpeechService extends Service { } } - public void dispatchOnDone(Object callerIdentity, String utteranceId) { + public void dispatchOnFallback(Object callerIdentity, String utteranceId) { + ITextToSpeechCallback cb = getCallbackFor(callerIdentity); + if (cb == null) return; + try { + cb.onFallback(utteranceId); + } catch (RemoteException e) { + Log.e(TAG, "Callback onFallback failed: " + e); + } + } + + public void dispatchOnStop(Object callerIdentity, String utteranceId) { + ITextToSpeechCallback cb = getCallbackFor(callerIdentity); + if (cb == null) return; + try { + cb.onStop(utteranceId); + } catch (RemoteException e) { + Log.e(TAG, "Callback onStop failed: " + e); + } + } + + public void dispatchOnSuccess(Object callerIdentity, String utteranceId) { ITextToSpeechCallback cb = getCallbackFor(callerIdentity); if (cb == null) return; try { - cb.onDone(utteranceId); + cb.onSuccess(utteranceId); } catch (RemoteException e) { Log.e(TAG, "Callback onDone failed: " + e); } @@ -985,11 +1543,12 @@ public abstract class TextToSpeechService extends Service { } - public void dispatchOnError(Object callerIdentity, String utteranceId) { + public void dispatchOnError(Object callerIdentity, String utteranceId, + int errorCode) { ITextToSpeechCallback cb = getCallbackFor(callerIdentity); if (cb == null) return; try { - cb.onError(utteranceId); + cb.onError(utteranceId, errorCode); } catch (RemoteException e) { Log.e(TAG, "Callback onError failed: " + e); } @@ -1001,7 +1560,7 @@ public abstract class TextToSpeechService extends Service { synchronized (mCallerToCallback) { mCallerToCallback.remove(caller); } - mSynthHandler.stopForApp(caller); + //mSynthHandler.stopForApp(caller); } @Override @@ -1012,6 +1571,18 @@ public abstract class TextToSpeechService extends Service { } } + public void dispatchVoicesInfoChange(List<VoiceInfo> voicesInfo) { + synchronized (mCallerToCallback) { + for (ITextToSpeechCallback callback : mCallerToCallback.values()) { + try { + callback.onVoicesInfoChange(voicesInfo); + } catch (RemoteException e) { + Log.e(TAG, "Failed to request reconnect", e); + } + } + } + } + private ITextToSpeechCallback getCallbackFor(Object caller) { ITextToSpeechCallback cb; IBinder asBinder = (IBinder) caller; @@ -1021,7 +1592,5 @@ public abstract class TextToSpeechService extends Service { return cb; } - } - } diff --git a/core/java/android/speech/tts/VoiceInfo.aidl b/core/java/android/speech/tts/VoiceInfo.aidl new file mode 100644 index 0000000..4005f8b --- /dev/null +++ b/core/java/android/speech/tts/VoiceInfo.aidl @@ -0,0 +1,20 @@ +/* +** +** Copyright 2013, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.speech.tts; + +parcelable VoiceInfo;
\ No newline at end of file diff --git a/core/java/android/speech/tts/VoiceInfo.java b/core/java/android/speech/tts/VoiceInfo.java new file mode 100644 index 0000000..16b9a97 --- /dev/null +++ b/core/java/android/speech/tts/VoiceInfo.java @@ -0,0 +1,325 @@ +package android.speech.tts; + +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Locale; + +/** + * Characteristics and features of a Text-To-Speech Voice. Each TTS Engine can expose + * multiple voices for multiple locales, with different set of features. + * + * Each VoiceInfo has an unique name. This name can be obtained using the {@link #getName()} method + * and will persist until the client is asked to re-evaluate the list of available voices in the + * {@link TextToSpeechClient.ConnectionCallbacks#onEngineStatusChange(android.speech.tts.TextToSpeechClient.EngineStatus)} + * callback. The name can be used to reference a VoiceInfo in an instance of {@link RequestConfig}; + * the {@link TextToSpeechClient.Params#FALLBACK_VOICE_NAME} voice parameter is an example of this. + * It is recommended that the voice name never change during the TTS service lifetime. + */ +public final class VoiceInfo implements Parcelable { + /** Very low, but still intelligible quality of speech synthesis */ + public static final int QUALITY_VERY_LOW = 100; + + /** Low, not human-like quality of speech synthesis */ + public static final int QUALITY_LOW = 200; + + /** Normal quality of speech synthesis */ + public static final int QUALITY_NORMAL = 300; + + /** High, human-like quality of speech synthesis */ + public static final int QUALITY_HIGH = 400; + + /** Very high, almost human-indistinguishable quality of speech synthesis */ + public static final int QUALITY_VERY_HIGH = 500; + + /** Very low expected synthesizer latency (< 20ms) */ + public static final int LATENCY_VERY_LOW = 100; + + /** Low expected synthesizer latency (~20ms) */ + public static final int LATENCY_LOW = 200; + + /** Normal expected synthesizer latency (~50ms) */ + public static final int LATENCY_NORMAL = 300; + + /** Network based expected synthesizer latency (~200ms) */ + public static final int LATENCY_HIGH = 400; + + /** Very slow network based expected synthesizer latency (> 200ms) */ + public static final int LATENCY_VERY_HIGH = 500; + + /** Additional feature key, with string value, gender of the speaker */ + public static final String FEATURE_SPEAKER_GENDER = "speakerGender"; + + /** Additional feature key, with integer value, speaking speed in words per minute + * when {@link TextToSpeechClient.Params#SPEECH_SPEED} parameter is set to {@code 1.0} */ + public static final String FEATURE_WORDS_PER_MINUTE = "wordsPerMinute"; + + /** + * Additional feature key, with boolean value, that indicates that voice may need to + * download additional data if used for synthesis. + * + * Making a request with a voice that has this feature may result in a + * {@link TextToSpeechClient.Status#ERROR_DOWNLOADING_ADDITIONAL_DATA} error. It's recommended + * to set the {@link TextToSpeechClient.Params#FALLBACK_VOICE_NAME} voice parameter to reference + * a fully installed voice (or network voice) that can serve as replacement. + * + * Note: It's a good practice for a TTS engine to provide a sensible fallback voice as the + * default value for {@link TextToSpeechClient.Params#FALLBACK_VOICE_NAME} parameter if this + * feature is present. + */ + public static final String FEATURE_MAY_AUTOINSTALL = "mayAutoInstall"; + + private final String mName; + private final Locale mLocale; + private final int mQuality; + private final int mLatency; + private final boolean mRequiresNetworkConnection; + private final Bundle mParams; + private final Bundle mAdditionalFeatures; + + private VoiceInfo(Parcel in) { + this.mName = in.readString(); + String[] localesData = new String[3]; + in.readStringArray(localesData); + this.mLocale = new Locale(localesData[0], localesData[1], localesData[2]); + + this.mQuality = in.readInt(); + this.mLatency = in.readInt(); + this.mRequiresNetworkConnection = (in.readByte() == 1); + + this.mParams = in.readBundle(); + this.mAdditionalFeatures = in.readBundle(); + } + + private VoiceInfo(String name, + Locale locale, + int quality, + int latency, + boolean requiresNetworkConnection, + Bundle params, + Bundle additionalFeatures) { + this.mName = name; + this.mLocale = locale; + this.mQuality = quality; + this.mLatency = latency; + this.mRequiresNetworkConnection = requiresNetworkConnection; + this.mParams = params; + this.mAdditionalFeatures = additionalFeatures; + } + + /** Builder, allows TTS engines to create VoiceInfo instances. */ + public static final class Builder { + private String name; + private Locale locale; + private int quality = VoiceInfo.QUALITY_NORMAL; + private int latency = VoiceInfo.LATENCY_NORMAL; + private boolean requiresNetworkConnection; + private Bundle params; + private Bundle additionalFeatures; + + public Builder() { + + } + + /** + * Copy fields from given VoiceInfo instance. + */ + public Builder(VoiceInfo voiceInfo) { + this.name = voiceInfo.mName; + this.locale = voiceInfo.mLocale; + this.quality = voiceInfo.mQuality; + this.latency = voiceInfo.mLatency; + this.requiresNetworkConnection = voiceInfo.mRequiresNetworkConnection; + this.params = (Bundle)voiceInfo.mParams.clone(); + this.additionalFeatures = (Bundle) voiceInfo.mAdditionalFeatures.clone(); + } + + /** + * Sets the voice's unique name. It will be used by clients to reference the voice used by a + * request. + * + * It's recommended that each voice use the same consistent name during the TTS service + * lifetime. + */ + public Builder setName(String name) { + this.name = name; + return this; + } + + /** + * Sets voice locale. This has to be a valid locale, built from ISO 639-1 and ISO 3166-1 + * two letter codes. + */ + public Builder setLocale(Locale locale) { + this.locale = locale; + return this; + } + + /** + * Sets map of all available request parameters with their default values. + * Some common parameter names can be found in {@link TextToSpeechClient.Params} static + * members. + */ + public Builder setParamsWithDefaults(Bundle params) { + this.params = params; + return this; + } + + /** + * Sets map of additional voice features. Some common feature names can be found in + * {@link VoiceInfo} static members. + */ + public Builder setAdditionalFeatures(Bundle additionalFeatures) { + this.additionalFeatures = additionalFeatures; + return this; + } + + /** + * Sets the voice quality (higher is better). + */ + public Builder setQuality(int quality) { + this.quality = quality; + return this; + } + + /** + * Sets the voice latency (lower is better). + */ + public Builder setLatency(int latency) { + this.latency = latency; + return this; + } + + /** + * Sets whether the voice requires network connection to work properly. + */ + public Builder setRequiresNetworkConnection(boolean requiresNetworkConnection) { + this.requiresNetworkConnection = requiresNetworkConnection; + return this; + } + + /** + * @return The built VoiceInfo instance. + */ + public VoiceInfo build() { + if (name == null || name.isEmpty()) { + throw new IllegalStateException("Name can't be null or empty"); + } + if (locale == null) { + throw new IllegalStateException("Locale can't be null"); + } + + return new VoiceInfo(name, locale, quality, latency, + requiresNetworkConnection, + ((params == null) ? new Bundle() : + (Bundle)params.clone()), + ((additionalFeatures == null) ? new Bundle() : + (Bundle)additionalFeatures.clone())); + } + } + + /** + * @hide + */ + @Override + public int describeContents() { + return 0; + } + + /** + * @hide + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mName); + String[] localesData = new String[]{mLocale.getLanguage(), mLocale.getCountry(), mLocale.getVariant()}; + dest.writeStringArray(localesData); + dest.writeInt(mQuality); + dest.writeInt(mLatency); + dest.writeByte((byte) (mRequiresNetworkConnection ? 1 : 0)); + dest.writeBundle(mParams); + dest.writeBundle(mAdditionalFeatures); + } + + /** + * @hide + */ + public static final Parcelable.Creator<VoiceInfo> CREATOR = new Parcelable.Creator<VoiceInfo>() { + @Override + public VoiceInfo createFromParcel(Parcel in) { + return new VoiceInfo(in); + } + + @Override + public VoiceInfo[] newArray(int size) { + return new VoiceInfo[size]; + } + }; + + /** + * @return The voice's locale + */ + public Locale getLocale() { + return mLocale; + } + + /** + * @return The voice's quality (higher is better) + */ + public int getQuality() { + return mQuality; + } + + /** + * @return The voice's latency (lower is better) + */ + public int getLatency() { + return mLatency; + } + + /** + * @return Does the Voice require a network connection to work. + */ + public boolean getRequiresNetworkConnection() { + return mRequiresNetworkConnection; + } + + /** + * @return Bundle of all available parameters with their default values. + */ + public Bundle getParamsWithDefaults() { + return mParams; + } + + /** + * @return Unique voice name. + * + * Each VoiceInfo has an unique name, that persists until client is asked to re-evaluate the + * set of the available languages in the {@link TextToSpeechClient.ConnectionCallbacks#onEngineStatusChange(android.speech.tts.TextToSpeechClient.EngineStatus)} + * callback (Voice may disappear from the set if voice was removed by the user). + */ + public String getName() { + return mName; + } + + /** + * @return Additional features of the voice. + */ + public Bundle getAdditionalFeatures() { + return mAdditionalFeatures; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(64); + return builder.append("VoiceInfo[Name: ").append(mName) + .append(" ,locale: ").append(mLocale) + .append(" ,quality: ").append(mQuality) + .append(" ,latency: ").append(mLatency) + .append(" ,requiresNetwork: ").append(mRequiresNetworkConnection) + .append(" ,paramsWithDefaults: ").append(mParams.toString()) + .append(" ,additionalFeatures: ").append(mAdditionalFeatures.toString()) + .append("]").toString(); + } +} diff --git a/core/java/android/text/Html.java b/core/java/android/text/Html.java index f839d52..c80321c 100644 --- a/core/java/android/text/Html.java +++ b/core/java/android/text/Html.java @@ -48,11 +48,8 @@ import android.text.style.TypefaceSpan; import android.text.style.URLSpan; import android.text.style.UnderlineSpan; -import com.android.internal.util.XmlUtils; - import java.io.IOException; import java.io.StringReader; -import java.util.HashMap; /** * This class processes HTML strings into displayable styled text. diff --git a/core/java/android/text/format/DateUtils.java b/core/java/android/text/format/DateUtils.java index 22675b4..d0ed871 100644 --- a/core/java/android/text/format/DateUtils.java +++ b/core/java/android/text/format/DateUtils.java @@ -28,7 +28,6 @@ import java.util.Date; import java.util.Formatter; import java.util.GregorianCalendar; import java.util.Locale; -import java.util.TimeZone; import libcore.icu.DateIntervalFormat; import libcore.icu.LocaleData; diff --git a/core/java/android/text/method/HideReturnsTransformationMethod.java b/core/java/android/text/method/HideReturnsTransformationMethod.java index ce18692..c6a90ca 100644 --- a/core/java/android/text/method/HideReturnsTransformationMethod.java +++ b/core/java/android/text/method/HideReturnsTransformationMethod.java @@ -16,13 +16,6 @@ package android.text.method; -import android.graphics.Rect; -import android.text.GetChars; -import android.text.Spanned; -import android.text.SpannedString; -import android.text.TextUtils; -import android.view.View; - /** * This transformation method causes any carriage return characters (\r) * to be hidden by displaying them as zero-width non-breaking space diff --git a/core/java/android/text/method/PasswordTransformationMethod.java b/core/java/android/text/method/PasswordTransformationMethod.java index b769b76..88a69b9 100644 --- a/core/java/android/text/method/PasswordTransformationMethod.java +++ b/core/java/android/text/method/PasswordTransformationMethod.java @@ -25,7 +25,6 @@ import android.text.GetChars; import android.text.NoCopySpan; import android.text.TextUtils; import android.text.TextWatcher; -import android.text.Selection; import android.text.Spanned; import android.text.Spannable; import android.text.style.UpdateLayout; diff --git a/core/java/android/text/method/SingleLineTransformationMethod.java b/core/java/android/text/method/SingleLineTransformationMethod.java index 6a05fe4..818526a 100644 --- a/core/java/android/text/method/SingleLineTransformationMethod.java +++ b/core/java/android/text/method/SingleLineTransformationMethod.java @@ -16,15 +16,6 @@ package android.text.method; -import android.graphics.Rect; -import android.text.Editable; -import android.text.GetChars; -import android.text.Spannable; -import android.text.Spanned; -import android.text.SpannedString; -import android.text.TextUtils; -import android.view.View; - /** * This transformation method causes any newline characters (\n) to be * displayed as spaces instead of causing line breaks, and causes diff --git a/core/java/android/text/style/DrawableMarginSpan.java b/core/java/android/text/style/DrawableMarginSpan.java index c2564d5..20b6886 100644 --- a/core/java/android/text/style/DrawableMarginSpan.java +++ b/core/java/android/text/style/DrawableMarginSpan.java @@ -19,7 +19,6 @@ package android.text.style; import android.graphics.drawable.Drawable; import android.graphics.Paint; import android.graphics.Canvas; -import android.graphics.RectF; import android.text.Spanned; import android.text.Layout; diff --git a/core/java/android/text/style/DynamicDrawableSpan.java b/core/java/android/text/style/DynamicDrawableSpan.java index 89dc45b..5b8a6dd 100644 --- a/core/java/android/text/style/DynamicDrawableSpan.java +++ b/core/java/android/text/style/DynamicDrawableSpan.java @@ -17,12 +17,9 @@ package android.text.style; import android.graphics.Canvas; -import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; -import android.graphics.Paint.Style; import android.graphics.drawable.Drawable; -import android.util.Log; import java.lang.ref.WeakReference; diff --git a/core/java/android/text/style/IconMarginSpan.java b/core/java/android/text/style/IconMarginSpan.java index c786a17..cf9a705 100644 --- a/core/java/android/text/style/IconMarginSpan.java +++ b/core/java/android/text/style/IconMarginSpan.java @@ -19,7 +19,6 @@ package android.text.style; import android.graphics.Paint; import android.graphics.Bitmap; import android.graphics.Canvas; -import android.graphics.RectF; import android.text.Spanned; import android.text.Layout; diff --git a/core/java/android/text/style/LineHeightSpan.java b/core/java/android/text/style/LineHeightSpan.java index 44a1706..1ebee82 100644 --- a/core/java/android/text/style/LineHeightSpan.java +++ b/core/java/android/text/style/LineHeightSpan.java @@ -17,8 +17,6 @@ package android.text.style; import android.graphics.Paint; -import android.graphics.Canvas; -import android.text.Layout; import android.text.TextPaint; public interface LineHeightSpan diff --git a/core/java/android/text/style/MetricAffectingSpan.java b/core/java/android/text/style/MetricAffectingSpan.java index 92558eb..a02b276 100644 --- a/core/java/android/text/style/MetricAffectingSpan.java +++ b/core/java/android/text/style/MetricAffectingSpan.java @@ -16,7 +16,6 @@ package android.text.style; -import android.graphics.Paint; import android.text.TextPaint; /** diff --git a/core/java/android/text/style/SuggestionSpan.java b/core/java/android/text/style/SuggestionSpan.java index 0ec7e84..8b40953 100644 --- a/core/java/android/text/style/SuggestionSpan.java +++ b/core/java/android/text/style/SuggestionSpan.java @@ -166,25 +166,25 @@ public class SuggestionSpan extends CharacterStyle implements ParcelableSpan { return; } - int defStyle = com.android.internal.R.attr.textAppearanceMisspelledSuggestion; + int defStyleAttr = com.android.internal.R.attr.textAppearanceMisspelledSuggestion; TypedArray typedArray = context.obtainStyledAttributes( - null, com.android.internal.R.styleable.SuggestionSpan, defStyle, 0); + null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0); mMisspelledUnderlineThickness = typedArray.getDimension( com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0); mMisspelledUnderlineColor = typedArray.getColor( com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK); - defStyle = com.android.internal.R.attr.textAppearanceEasyCorrectSuggestion; + defStyleAttr = com.android.internal.R.attr.textAppearanceEasyCorrectSuggestion; typedArray = context.obtainStyledAttributes( - null, com.android.internal.R.styleable.SuggestionSpan, defStyle, 0); + null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0); mEasyCorrectUnderlineThickness = typedArray.getDimension( com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0); mEasyCorrectUnderlineColor = typedArray.getColor( com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK); - defStyle = com.android.internal.R.attr.textAppearanceAutoCorrectionSuggestion; + defStyleAttr = com.android.internal.R.attr.textAppearanceAutoCorrectionSuggestion; typedArray = context.obtainStyledAttributes( - null, com.android.internal.R.styleable.SuggestionSpan, defStyle, 0); + null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0); mAutoCorrectionUnderlineThickness = typedArray.getDimension( com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0); mAutoCorrectionUnderlineColor = typedArray.getColor( diff --git a/core/java/android/transition/Recolor.java b/core/java/android/transition/Recolor.java index 70111d1..1638f67 100644 --- a/core/java/android/transition/Recolor.java +++ b/core/java/android/transition/Recolor.java @@ -17,7 +17,6 @@ package android.transition; import android.animation.Animator; -import android.animation.ArgbEvaluator; import android.animation.ObjectAnimator; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; @@ -75,8 +74,8 @@ public class Recolor extends Transition { if (startColor.getColor() != endColor.getColor()) { endColor.setColor(startColor.getColor()); changed = true; - return ObjectAnimator.ofObject(endBackground, "color", - new ArgbEvaluator(), startColor.getColor(), endColor.getColor()); + return ObjectAnimator.ofArgb(endBackground, "color", startColor.getColor(), + endColor.getColor()); } } if (view instanceof TextView) { @@ -86,8 +85,7 @@ public class Recolor extends Transition { if (start != end) { textView.setTextColor(end); changed = true; - return ObjectAnimator.ofObject(textView, "textColor", - new ArgbEvaluator(), start, end); + return ObjectAnimator.ofArgb(textView, "textColor", start, end); } } return null; diff --git a/core/java/android/transition/Scene.java b/core/java/android/transition/Scene.java index e1f1896..4267a65 100644 --- a/core/java/android/transition/Scene.java +++ b/core/java/android/transition/Scene.java @@ -34,7 +34,7 @@ public final class Scene { private Context mContext; private int mLayoutId = -1; private ViewGroup mSceneRoot; - private ViewGroup mLayout; // alternative to layoutId + private View mLayout; // alternative to layoutId Runnable mEnterAction, mExitAction; /** @@ -114,6 +114,15 @@ public final class Scene { * @param layout The view hierarchy of this scene, added as a child * of sceneRoot when this scene is entered. */ + public Scene(ViewGroup sceneRoot, View layout) { + mSceneRoot = sceneRoot; + mLayout = layout; + } + + /** + * @deprecated use {@link #Scene(ViewGroup, View)}. + */ + @Deprecated public Scene(ViewGroup sceneRoot, ViewGroup layout) { mSceneRoot = sceneRoot; mLayout = layout; diff --git a/core/java/android/util/EventLogTags.java b/core/java/android/util/EventLogTags.java index 8c18417..f4ce4fd 100644 --- a/core/java/android/util/EventLogTags.java +++ b/core/java/android/util/EventLogTags.java @@ -16,14 +16,8 @@ package android.util; -import android.util.Log; - import java.io.BufferedReader; -import java.io.FileReader; import java.io.IOException; -import java.util.HashMap; -import java.util.regex.Matcher; -import java.util.regex.Pattern; /** * @deprecated This class is no longer functional. diff --git a/core/java/android/util/LocalLog.java b/core/java/android/util/LocalLog.java index 641d1b4..eeb6d58 100644 --- a/core/java/android/util/LocalLog.java +++ b/core/java/android/util/LocalLog.java @@ -20,7 +20,6 @@ import android.text.format.Time; import java.io.FileDescriptor; import java.io.PrintWriter; -import java.io.StringWriter; import java.util.Iterator; import java.util.LinkedList; diff --git a/core/java/android/util/LongArray.java b/core/java/android/util/LongArray.java new file mode 100644 index 0000000..7d42063 --- /dev/null +++ b/core/java/android/util/LongArray.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +import com.android.internal.util.ArrayUtils; + +/** + * Implements a growing array of long primitives. + * + * @hide + */ +public class LongArray implements Cloneable { + private static final int MIN_CAPACITY_INCREMENT = 12; + + private long[] mValues; + private int mSize; + + /** + * Creates an empty LongArray with the default initial capacity. + */ + public LongArray() { + this(10); + } + + /** + * Creates an empty LongArray with the specified initial capacity. + */ + public LongArray(int initialCapacity) { + if (initialCapacity == 0) { + mValues = ContainerHelpers.EMPTY_LONGS; + } else { + initialCapacity = ArrayUtils.idealLongArraySize(initialCapacity); + mValues = new long[initialCapacity]; + } + mSize = 0; + } + + /** + * Appends the specified value to the end of this array. + */ + public void add(long value) { + add(mSize, value); + } + + /** + * Inserts a value at the specified position in this array. + * + * @throws IndexOutOfBoundsException when index < 0 || index > size() + */ + public void add(int index, long value) { + if (index < 0 || index > mSize) { + throw new IndexOutOfBoundsException(); + } + + ensureCapacity(1); + + if (mSize - index != 0) { + System.arraycopy(mValues, index, mValues, index + 1, mSize - index); + } + + mValues[index] = value; + mSize++; + } + + /** + * Adds the values in the specified array to this array. + */ + public void addAll(LongArray values) { + final int count = values.mSize; + ensureCapacity(count); + + System.arraycopy(mValues, mSize, values.mValues, 0, count); + mSize += count; + } + + /** + * Ensures capacity to append at least <code>count</code> values. + */ + private void ensureCapacity(int count) { + final int currentSize = mSize; + final int minCapacity = currentSize + count; + if (minCapacity >= mValues.length) { + final int targetCap = currentSize + (currentSize < (MIN_CAPACITY_INCREMENT / 2) ? + MIN_CAPACITY_INCREMENT : currentSize >> 1); + final int newCapacity = targetCap > minCapacity ? targetCap : minCapacity; + final long[] newValues = new long[ArrayUtils.idealLongArraySize(newCapacity)]; + System.arraycopy(mValues, 0, newValues, 0, currentSize); + mValues = newValues; + } + } + + /** + * Removes all values from this array. + */ + public void clear() { + mSize = 0; + } + + @Override + @SuppressWarnings("unchecked") + public LongArray clone() { + LongArray clone = null; + try { + clone = (LongArray) super.clone(); + clone.mValues = mValues.clone(); + } catch (CloneNotSupportedException cnse) { + /* ignore */ + } + return clone; + } + + /** + * Returns the value at the specified position in this array. + */ + public long get(int index) { + return mValues[index]; + } + + /** + * Returns the index of the first occurrence of the specified value in this + * array, or -1 if this array does not contain the value. + */ + public int indexOf(long value) { + final int n = mSize; + for (int i = 0; i < n; i++) { + if (mValues[i] == value) { + return i; + } + } + return -1; + } + + /** + * Removes the value at the specified index from this array. + */ + public void remove(int index) { + System.arraycopy(mValues, index, mValues, index + 1, mSize - index); + } + + /** + * Returns the number of values in this array. + */ + public int size() { + return mSize; + } +} diff --git a/core/java/android/util/LongSparseLongArray.java b/core/java/android/util/LongSparseLongArray.java index 6654899..b8073dd 100644 --- a/core/java/android/util/LongSparseLongArray.java +++ b/core/java/android/util/LongSparseLongArray.java @@ -18,8 +18,6 @@ package android.util; import com.android.internal.util.ArrayUtils; -import java.util.Arrays; - /** * Map of {@code long} to {@code long}. Unlike a normal array of longs, there * can be gaps in the indices. It is intended to be more memory efficient than using a diff --git a/core/java/android/util/Slog.java b/core/java/android/util/Slog.java index 70795bb..b25d80f 100644 --- a/core/java/android/util/Slog.java +++ b/core/java/android/util/Slog.java @@ -16,11 +16,6 @@ package android.util; -import com.android.internal.os.RuntimeInit; - -import java.io.PrintWriter; -import java.io.StringWriter; - /** * @hide */ diff --git a/core/java/android/view/AccessibilityInteractionController.java b/core/java/android/view/AccessibilityInteractionController.java index 41d3700..3859ad4 100644 --- a/core/java/android/view/AccessibilityInteractionController.java +++ b/core/java/android/view/AccessibilityInteractionController.java @@ -24,7 +24,6 @@ import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.RemoteException; -import android.util.SparseLongArray; import android.view.View.AttachInfo; import android.view.accessibility.AccessibilityInteractionClient; import android.view.accessibility.AccessibilityNodeInfo; @@ -881,13 +880,12 @@ final class AccessibilityInteractionController { AccessibilityNodeInfo parent = provider.createAccessibilityNodeInfo(parentVirtualDescendantId); if (parent != null) { - SparseLongArray childNodeIds = parent.getChildNodeIds(); - final int childCount = childNodeIds.size(); + final int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { if (outInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { return; } - final long childNodeId = childNodeIds.get(i); + final long childNodeId = parent.getChildId(i); if (childNodeId != current.getSourceNodeId()) { final int childVirtualDescendantId = AccessibilityNodeInfo.getVirtualDescendantId(childNodeId); @@ -906,14 +904,13 @@ final class AccessibilityInteractionController { private void prefetchDescendantsOfVirtualNode(AccessibilityNodeInfo root, AccessibilityNodeProvider provider, List<AccessibilityNodeInfo> outInfos) { - SparseLongArray childNodeIds = root.getChildNodeIds(); final int initialOutInfosSize = outInfos.size(); - final int childCount = childNodeIds.size(); + final int childCount = root.getChildCount(); for (int i = 0; i < childCount; i++) { if (outInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { return; } - final long childNodeId = childNodeIds.get(i); + final long childNodeId = root.getChildId(i); AccessibilityNodeInfo child = provider.createAccessibilityNodeInfo( AccessibilityNodeInfo.getVirtualDescendantId(childNodeId)); if (child != null) { diff --git a/core/java/android/view/AccessibilityIterators.java b/core/java/android/view/AccessibilityIterators.java index 17ce4f6..e59937d 100644 --- a/core/java/android/view/AccessibilityIterators.java +++ b/core/java/android/view/AccessibilityIterators.java @@ -17,8 +17,6 @@ package android.view; import android.content.ComponentCallbacks; -import android.content.Context; -import android.content.pm.ActivityInfo; import android.content.res.Configuration; import java.text.BreakIterator; diff --git a/core/java/android/view/ContextThemeWrapper.java b/core/java/android/view/ContextThemeWrapper.java index 6c733f9..1de9c35 100644 --- a/core/java/android/view/ContextThemeWrapper.java +++ b/core/java/android/view/ContextThemeWrapper.java @@ -20,7 +20,6 @@ import android.content.Context; import android.content.ContextWrapper; import android.content.res.Configuration; import android.content.res.Resources; -import android.os.Build; /** * A ContextWrapper that allows you to modify the theme from what is in the diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java index 7d310a2..d3f63b4 100644 --- a/core/java/android/view/Display.java +++ b/core/java/android/view/Display.java @@ -528,6 +528,7 @@ public final class Display { * 90 degrees clockwise and thus the returned value here will be * {@link Surface#ROTATION_90 Surface.ROTATION_90}. */ + @Surface.Rotation public int getRotation() { synchronized (this) { updateDisplayInfoLocked(); @@ -540,6 +541,7 @@ public final class Display { * @return orientation of this display. */ @Deprecated + @Surface.Rotation public int getOrientation() { return getRotation(); } diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java index 8944207..7fd7b83 100644 --- a/core/java/android/view/DisplayInfo.java +++ b/core/java/android/view/DisplayInfo.java @@ -20,7 +20,6 @@ import android.content.res.CompatibilityInfo; import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; -import android.os.Process; import android.util.DisplayMetrics; import libcore.util.Objects; @@ -144,6 +143,7 @@ public final class DisplayInfo implements Parcelable { * more than one physical display. * </p> */ + @Surface.Rotation public int rotation; /** diff --git a/core/java/android/view/DisplayList.java b/core/java/android/view/DisplayList.java index 43fd628..623472a 100644 --- a/core/java/android/view/DisplayList.java +++ b/core/java/android/view/DisplayList.java @@ -86,18 +86,15 @@ import android.graphics.Matrix; * * <pre class="prettyprint"> * private void createDisplayList() { - * HardwareRenderer renderer = getHardwareRenderer(); - * if (renderer != null) { - * mDisplayList = renderer.createDisplayList(); - * HardwareCanvas canvas = mDisplayList.start(width, height); - * try { - * for (Bitmap b : mBitmaps) { - * canvas.drawBitmap(b, 0.0f, 0.0f, null); - * canvas.translate(0.0f, b.getHeight()); - * } - * } finally { - * displayList.end(); + * mDisplayList = DisplayList.create("MyDisplayList"); + * HardwareCanvas canvas = mDisplayList.start(width, height); + * try { + * for (Bitmap b : mBitmaps) { + * canvas.drawBitmap(b, 0.0f, 0.0f, null); + * canvas.translate(0.0f, b.getHeight()); * } + * } finally { + * displayList.end(); * } * } * @@ -177,6 +174,20 @@ public abstract class DisplayList { public static final int STATUS_DREW = 0x4; /** + * Creates a new display list that can be used to record batches of + * drawing operations. + * + * @param name The name of the display list, used for debugging purpose. May be null. + * + * @return A new display list. + * + * @hide + */ + public static DisplayList create(String name) { + return new GLES20DisplayList(name); + } + + /** * Starts recording the display list. All operations performed on the * returned canvas are recorded and stored in this display list. * diff --git a/core/java/android/view/GLRenderer.java b/core/java/android/view/GLRenderer.java new file mode 100644 index 0000000..a195231 --- /dev/null +++ b/core/java/android/view/GLRenderer.java @@ -0,0 +1,1610 @@ +/* + * Copyright (C) 2013 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 javax.microedition.khronos.egl.EGL10.EGL_ALPHA_SIZE; +import static javax.microedition.khronos.egl.EGL10.EGL_BAD_NATIVE_WINDOW; +import static javax.microedition.khronos.egl.EGL10.EGL_BLUE_SIZE; +import static javax.microedition.khronos.egl.EGL10.EGL_CONFIG_CAVEAT; +import static javax.microedition.khronos.egl.EGL10.EGL_DEFAULT_DISPLAY; +import static javax.microedition.khronos.egl.EGL10.EGL_DEPTH_SIZE; +import static javax.microedition.khronos.egl.EGL10.EGL_DRAW; +import static javax.microedition.khronos.egl.EGL10.EGL_GREEN_SIZE; +import static javax.microedition.khronos.egl.EGL10.EGL_HEIGHT; +import static javax.microedition.khronos.egl.EGL10.EGL_NONE; +import static javax.microedition.khronos.egl.EGL10.EGL_NO_CONTEXT; +import static javax.microedition.khronos.egl.EGL10.EGL_NO_DISPLAY; +import static javax.microedition.khronos.egl.EGL10.EGL_NO_SURFACE; +import static javax.microedition.khronos.egl.EGL10.EGL_RED_SIZE; +import static javax.microedition.khronos.egl.EGL10.EGL_RENDERABLE_TYPE; +import static javax.microedition.khronos.egl.EGL10.EGL_SAMPLES; +import static javax.microedition.khronos.egl.EGL10.EGL_SAMPLE_BUFFERS; +import static javax.microedition.khronos.egl.EGL10.EGL_STENCIL_SIZE; +import static javax.microedition.khronos.egl.EGL10.EGL_SUCCESS; +import static javax.microedition.khronos.egl.EGL10.EGL_SURFACE_TYPE; +import static javax.microedition.khronos.egl.EGL10.EGL_WIDTH; +import static javax.microedition.khronos.egl.EGL10.EGL_WINDOW_BIT; + +import android.content.ComponentCallbacks2; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.SurfaceTexture; +import android.opengl.EGL14; +import android.opengl.GLUtils; +import android.opengl.ManagedEGLContext; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.os.Trace; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.Surface.OutOfResourcesException; + +import com.google.android.gles_jni.EGLImpl; + +import java.io.PrintWriter; +import java.util.concurrent.locks.ReentrantLock; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGL11; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; +import javax.microedition.khronos.opengles.GL; + +/** + * Hardware renderer using OpenGL + * + * @hide + */ +public class GLRenderer extends HardwareRenderer { + static final int SURFACE_STATE_ERROR = 0; + static final int SURFACE_STATE_SUCCESS = 1; + static final int SURFACE_STATE_UPDATED = 2; + + static final int FUNCTOR_PROCESS_DELAY = 4; + + /** + * Number of frames to profile. + */ + private static final int PROFILE_MAX_FRAMES = 128; + + /** + * Number of floats per profiled frame. + */ + private static final int PROFILE_FRAME_DATA_COUNT = 3; + + private static final int PROFILE_DRAW_MARGIN = 0; + private static final int PROFILE_DRAW_WIDTH = 3; + private static final int[] PROFILE_DRAW_COLORS = { 0xcf3e66cc, 0xcfdc3912, 0xcfe69800 }; + private static final int PROFILE_DRAW_CURRENT_FRAME_COLOR = 0xcf5faa4d; + private static final int PROFILE_DRAW_THRESHOLD_COLOR = 0xff5faa4d; + private static final int PROFILE_DRAW_THRESHOLD_STROKE_WIDTH = 2; + private static final int PROFILE_DRAW_DP_PER_MS = 7; + + private static final String[] VISUALIZERS = { + PROFILE_PROPERTY_VISUALIZE_BARS, + PROFILE_PROPERTY_VISUALIZE_LINES + }; + + private static final String[] OVERDRAW = { + OVERDRAW_PROPERTY_SHOW, + OVERDRAW_PROPERTY_COUNT + }; + private static final int OVERDRAW_TYPE_COUNT = 1; + private static final int GL_VERSION = 2; + + static EGL10 sEgl; + static EGLDisplay sEglDisplay; + static EGLConfig sEglConfig; + static final Object[] sEglLock = new Object[0]; + int mWidth = -1, mHeight = -1; + + static final ThreadLocal<ManagedEGLContext> sEglContextStorage + = new ThreadLocal<ManagedEGLContext>(); + + EGLContext mEglContext; + Thread mEglThread; + + EGLSurface mEglSurface; + + GL mGl; + HardwareCanvas mCanvas; + + String mName; + + long mFrameCount; + Paint mDebugPaint; + + static boolean sDirtyRegions; + static final boolean sDirtyRegionsRequested; + static { + String dirtyProperty = SystemProperties.get(RENDER_DIRTY_REGIONS_PROPERTY, "true"); + //noinspection PointlessBooleanExpression,ConstantConditions + sDirtyRegions = "true".equalsIgnoreCase(dirtyProperty); + sDirtyRegionsRequested = sDirtyRegions; + } + + boolean mDirtyRegionsEnabled; + boolean mUpdateDirtyRegions; + + boolean mProfileEnabled; + int mProfileVisualizerType = -1; + float[] mProfileData; + ReentrantLock mProfileLock; + int mProfileCurrentFrame = -PROFILE_FRAME_DATA_COUNT; + + GraphDataProvider mDebugDataProvider; + float[][] mProfileShapes; + Paint mProfilePaint; + + boolean mDebugDirtyRegions; + int mDebugOverdraw = -1; + HardwareLayer mDebugOverdrawLayer; + Paint mDebugOverdrawPaint; + + final boolean mTranslucent; + + private boolean mDestroyed; + + private final Rect mRedrawClip = new Rect(); + + private final int[] mSurfaceSize = new int[2]; + private final FunctorsRunnable mFunctorsRunnable = new FunctorsRunnable(); + + private long mDrawDelta = Long.MAX_VALUE; + + private GLES20Canvas mGlCanvas; + + private DisplayMetrics mDisplayMetrics; + + private static EGLSurface sPbuffer; + private static final Object[] sPbufferLock = new Object[0]; + + private static class GLRendererEglContext extends ManagedEGLContext { + final Handler mHandler = new Handler(); + + public GLRendererEglContext(EGLContext context) { + super(context); + } + + @Override + public void onTerminate(final EGLContext eglContext) { + // Make sure we do this on the correct thread. + if (mHandler.getLooper() != Looper.myLooper()) { + mHandler.post(new Runnable() { + @Override + public void run() { + onTerminate(eglContext); + } + }); + return; + } + + synchronized (sEglLock) { + if (sEgl == null) return; + + if (EGLImpl.getInitCount(sEglDisplay) == 1) { + usePbufferSurface(eglContext); + GLES20Canvas.terminateCaches(); + + sEgl.eglDestroyContext(sEglDisplay, eglContext); + sEglContextStorage.set(null); + sEglContextStorage.remove(); + + sEgl.eglDestroySurface(sEglDisplay, sPbuffer); + sEgl.eglMakeCurrent(sEglDisplay, EGL_NO_SURFACE, + EGL_NO_SURFACE, EGL_NO_CONTEXT); + + sEgl.eglReleaseThread(); + sEgl.eglTerminate(sEglDisplay); + + sEgl = null; + sEglDisplay = null; + sEglConfig = null; + sPbuffer = null; + } + } + } + } + + HardwareCanvas createCanvas() { + return mGlCanvas = new GLES20Canvas(mTranslucent); + } + + ManagedEGLContext createManagedContext(EGLContext eglContext) { + return new GLRendererEglContext(mEglContext); + } + + int[] getConfig(boolean dirtyRegions) { + //noinspection PointlessBooleanExpression,ConstantConditions + final int stencilSize = GLES20Canvas.getStencilSize(); + final int swapBehavior = dirtyRegions ? EGL14.EGL_SWAP_BEHAVIOR_PRESERVED_BIT : 0; + + return new int[] { + EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_DEPTH_SIZE, 0, + EGL_CONFIG_CAVEAT, EGL_NONE, + EGL_STENCIL_SIZE, stencilSize, + EGL_SURFACE_TYPE, EGL_WINDOW_BIT | swapBehavior, + EGL_NONE + }; + } + + void initCaches() { + if (GLES20Canvas.initCaches()) { + // Caches were (re)initialized, rebind atlas + initAtlas(); + } + } + + void initAtlas() { + IBinder binder = ServiceManager.getService("assetatlas"); + if (binder == null) return; + + IAssetAtlas atlas = IAssetAtlas.Stub.asInterface(binder); + try { + if (atlas.isCompatible(android.os.Process.myPpid())) { + GraphicBuffer buffer = atlas.getBuffer(); + if (buffer != null) { + int[] map = atlas.getMap(); + if (map != null) { + GLES20Canvas.initAtlas(buffer, map); + } + // If IAssetAtlas is not the same class as the IBinder + // we are using a remote service and we can safely + // destroy the graphic buffer + if (atlas.getClass() != binder.getClass()) { + buffer.destroy(); + } + } + } + } catch (RemoteException e) { + Log.w(LOG_TAG, "Could not acquire atlas", e); + } + } + + boolean canDraw() { + return mGl != null && mCanvas != null && mGlCanvas != null; + } + + int onPreDraw(Rect dirty) { + return mGlCanvas.onPreDraw(dirty); + } + + void onPostDraw() { + mGlCanvas.onPostDraw(); + } + + void drawProfileData(View.AttachInfo attachInfo) { + if (mDebugDataProvider != null) { + final GraphDataProvider provider = mDebugDataProvider; + initProfileDrawData(attachInfo, provider); + + final int height = provider.getVerticalUnitSize(); + final int margin = provider.getHorizontaUnitMargin(); + final int width = provider.getHorizontalUnitSize(); + + int x = 0; + int count = 0; + int current = 0; + + final float[] data = provider.getData(); + final int elementCount = provider.getElementCount(); + final int graphType = provider.getGraphType(); + + int totalCount = provider.getFrameCount() * elementCount; + if (graphType == GraphDataProvider.GRAPH_TYPE_LINES) { + totalCount -= elementCount; + } + + for (int i = 0; i < totalCount; i += elementCount) { + if (data[i] < 0.0f) break; + + int index = count * 4; + if (i == provider.getCurrentFrame() * elementCount) current = index; + + x += margin; + int x2 = x + width; + + int y2 = mHeight; + int y1 = (int) (y2 - data[i] * height); + + switch (graphType) { + case GraphDataProvider.GRAPH_TYPE_BARS: { + for (int j = 0; j < elementCount; j++) { + //noinspection MismatchedReadAndWriteOfArray + final float[] r = mProfileShapes[j]; + r[index] = x; + r[index + 1] = y1; + r[index + 2] = x2; + r[index + 3] = y2; + + y2 = y1; + if (j < elementCount - 1) { + y1 = (int) (y2 - data[i + j + 1] * height); + } + } + } break; + case GraphDataProvider.GRAPH_TYPE_LINES: { + for (int j = 0; j < elementCount; j++) { + //noinspection MismatchedReadAndWriteOfArray + final float[] r = mProfileShapes[j]; + r[index] = (x + x2) * 0.5f; + r[index + 1] = index == 0 ? y1 : r[index - 1]; + r[index + 2] = r[index] + width; + r[index + 3] = y1; + + y2 = y1; + if (j < elementCount - 1) { + y1 = (int) (y2 - data[i + j + 1] * height); + } + } + } break; + } + + + x += width; + count++; + } + + x += margin; + + drawGraph(graphType, count); + drawCurrentFrame(graphType, current); + drawThreshold(x, height); + } + } + + private void drawGraph(int graphType, int count) { + for (int i = 0; i < mProfileShapes.length; i++) { + mDebugDataProvider.setupGraphPaint(mProfilePaint, i); + switch (graphType) { + case GraphDataProvider.GRAPH_TYPE_BARS: + mGlCanvas.drawRects(mProfileShapes[i], count * 4, mProfilePaint); + break; + case GraphDataProvider.GRAPH_TYPE_LINES: + mGlCanvas.drawLines(mProfileShapes[i], 0, count * 4, mProfilePaint); + break; + } + } + } + + private void drawCurrentFrame(int graphType, int index) { + if (index >= 0) { + mDebugDataProvider.setupCurrentFramePaint(mProfilePaint); + switch (graphType) { + case GraphDataProvider.GRAPH_TYPE_BARS: + mGlCanvas.drawRect(mProfileShapes[2][index], mProfileShapes[2][index + 1], + mProfileShapes[2][index + 2], mProfileShapes[0][index + 3], + mProfilePaint); + break; + case GraphDataProvider.GRAPH_TYPE_LINES: + mGlCanvas.drawLine(mProfileShapes[2][index], mProfileShapes[2][index + 1], + mProfileShapes[2][index], mHeight, mProfilePaint); + break; + } + } + } + + private void drawThreshold(int x, int height) { + float threshold = mDebugDataProvider.getThreshold(); + if (threshold > 0.0f) { + mDebugDataProvider.setupThresholdPaint(mProfilePaint); + int y = (int) (mHeight - threshold * height); + mGlCanvas.drawLine(0.0f, y, x, y, mProfilePaint); + } + } + + private void initProfileDrawData(View.AttachInfo attachInfo, GraphDataProvider provider) { + if (mProfileShapes == null) { + final int elementCount = provider.getElementCount(); + final int frameCount = provider.getFrameCount(); + + mProfileShapes = new float[elementCount][]; + for (int i = 0; i < elementCount; i++) { + mProfileShapes[i] = new float[frameCount * 4]; + } + + mProfilePaint = new Paint(); + } + + mProfilePaint.reset(); + if (provider.getGraphType() == GraphDataProvider.GRAPH_TYPE_LINES) { + mProfilePaint.setAntiAlias(true); + } + + if (mDisplayMetrics == null) { + mDisplayMetrics = new DisplayMetrics(); + } + + attachInfo.mDisplay.getMetrics(mDisplayMetrics); + provider.prepare(mDisplayMetrics); + } + + @Override + void destroy(boolean full) { + try { + if (full && mCanvas != null) { + mCanvas = null; + } + + if (!isEnabled() || mDestroyed) { + setEnabled(false); + return; + } + + destroySurface(); + setEnabled(false); + + mDestroyed = true; + mGl = null; + } finally { + if (full && mGlCanvas != null) { + mGlCanvas = null; + } + } + } + + @Override + void pushLayerUpdate(HardwareLayer layer) { + mGlCanvas.pushLayerUpdate(layer); + } + + @Override + void cancelLayerUpdate(HardwareLayer layer) { + mGlCanvas.cancelLayerUpdate(layer); + } + + @Override + void flushLayerUpdates() { + mGlCanvas.flushLayerUpdates(); + } + + @Override + HardwareLayer createHardwareLayer(boolean isOpaque) { + return new GLES20TextureLayer(isOpaque); + } + + @Override + public HardwareLayer createHardwareLayer(int width, int height, boolean isOpaque) { + return new GLES20RenderLayer(width, height, isOpaque); + } + + void countOverdraw(HardwareCanvas canvas) { + ((GLES20Canvas) canvas).setCountOverdrawEnabled(true); + } + + float getOverdraw(HardwareCanvas canvas) { + return ((GLES20Canvas) canvas).getOverdraw(); + } + + @Override + public SurfaceTexture createSurfaceTexture(HardwareLayer layer) { + return ((GLES20TextureLayer) layer).getSurfaceTexture(); + } + + @Override + void setSurfaceTexture(HardwareLayer layer, SurfaceTexture surfaceTexture) { + ((GLES20TextureLayer) layer).setSurfaceTexture(surfaceTexture); + } + + @Override + boolean safelyRun(Runnable action) { + boolean needsContext = !isEnabled() || checkRenderContext() == SURFACE_STATE_ERROR; + + if (needsContext) { + GLRendererEglContext managedContext = + (GLRendererEglContext) sEglContextStorage.get(); + if (managedContext == null) return false; + usePbufferSurface(managedContext.getContext()); + } + + try { + action.run(); + } finally { + if (needsContext) { + sEgl.eglMakeCurrent(sEglDisplay, EGL_NO_SURFACE, + EGL_NO_SURFACE, EGL_NO_CONTEXT); + } + } + + return true; + } + + @Override + void destroyLayers(final View view) { + if (view != null) { + safelyRun(new Runnable() { + @Override + public void run() { + if (mCanvas != null) { + mCanvas.clearLayerUpdates(); + } + destroyHardwareLayer(view); + GLES20Canvas.flushCaches(GLES20Canvas.FLUSH_CACHES_LAYERS); + } + }); + } + } + + private static void destroyHardwareLayer(View view) { + view.destroyLayer(true); + + if (view instanceof ViewGroup) { + ViewGroup group = (ViewGroup) view; + + int count = group.getChildCount(); + for (int i = 0; i < count; i++) { + destroyHardwareLayer(group.getChildAt(i)); + } + } + } + + @Override + void destroyHardwareResources(final View view) { + if (view != null) { + safelyRun(new Runnable() { + @Override + public void run() { + if (mCanvas != null) { + mCanvas.clearLayerUpdates(); + } + destroyResources(view); + GLES20Canvas.flushCaches(GLES20Canvas.FLUSH_CACHES_LAYERS); + } + }); + } + } + + private static void destroyResources(View view) { + view.destroyHardwareResources(); + + if (view instanceof ViewGroup) { + ViewGroup group = (ViewGroup) view; + + int count = group.getChildCount(); + for (int i = 0; i < count; i++) { + destroyResources(group.getChildAt(i)); + } + } + } + + static void startTrimMemory(int level) { + if (sEgl == null || sEglConfig == null) return; + + GLRendererEglContext managedContext = + (GLRendererEglContext) sEglContextStorage.get(); + // We do not have OpenGL objects + if (managedContext == null) { + return; + } else { + usePbufferSurface(managedContext.getContext()); + } + + if (level >= ComponentCallbacks2.TRIM_MEMORY_COMPLETE) { + GLES20Canvas.flushCaches(GLES20Canvas.FLUSH_CACHES_FULL); + } else if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) { + GLES20Canvas.flushCaches(GLES20Canvas.FLUSH_CACHES_MODERATE); + } + } + + static void endTrimMemory() { + if (sEgl != null && sEglDisplay != null) { + sEgl.eglMakeCurrent(sEglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + } + } + + private static void usePbufferSurface(EGLContext eglContext) { + synchronized (sPbufferLock) { + // Create a temporary 1x1 pbuffer so we have a context + // to clear our OpenGL objects + if (sPbuffer == null) { + sPbuffer = sEgl.eglCreatePbufferSurface(sEglDisplay, sEglConfig, new int[] { + EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE + }); + } + } + sEgl.eglMakeCurrent(sEglDisplay, sPbuffer, sPbuffer, eglContext); + } + + GLRenderer(boolean translucent) { + mTranslucent = translucent; + + loadSystemProperties(null); + } + + @Override + boolean loadSystemProperties(Surface surface) { + boolean value; + boolean changed = false; + + String profiling = SystemProperties.get(PROFILE_PROPERTY); + int graphType = search(VISUALIZERS, profiling); + value = graphType >= 0; + + if (graphType != mProfileVisualizerType) { + changed = true; + mProfileVisualizerType = graphType; + + mProfileShapes = null; + mProfilePaint = null; + + if (value) { + mDebugDataProvider = new DrawPerformanceDataProvider(graphType); + } else { + mDebugDataProvider = null; + } + } + + // If on-screen profiling is not enabled, we need to check whether + // console profiling only is enabled + if (!value) { + value = Boolean.parseBoolean(profiling); + } + + if (value != mProfileEnabled) { + changed = true; + mProfileEnabled = value; + + if (mProfileEnabled) { + Log.d(LOG_TAG, "Profiling hardware renderer"); + + int maxProfileFrames = SystemProperties.getInt(PROFILE_MAXFRAMES_PROPERTY, + PROFILE_MAX_FRAMES); + mProfileData = new float[maxProfileFrames * PROFILE_FRAME_DATA_COUNT]; + for (int i = 0; i < mProfileData.length; i += PROFILE_FRAME_DATA_COUNT) { + mProfileData[i] = mProfileData[i + 1] = mProfileData[i + 2] = -1; + } + + mProfileLock = new ReentrantLock(); + } else { + mProfileData = null; + mProfileLock = null; + mProfileVisualizerType = -1; + } + + mProfileCurrentFrame = -PROFILE_FRAME_DATA_COUNT; + } + + value = SystemProperties.getBoolean(DEBUG_DIRTY_REGIONS_PROPERTY, false); + if (value != mDebugDirtyRegions) { + changed = true; + mDebugDirtyRegions = value; + + if (mDebugDirtyRegions) { + Log.d(LOG_TAG, "Debugging dirty regions"); + } + } + + String overdraw = SystemProperties.get(HardwareRenderer.DEBUG_OVERDRAW_PROPERTY); + int debugOverdraw = search(OVERDRAW, overdraw); + if (debugOverdraw != mDebugOverdraw) { + changed = true; + mDebugOverdraw = debugOverdraw; + + if (mDebugOverdraw != OVERDRAW_TYPE_COUNT) { + if (mDebugOverdrawLayer != null) { + mDebugOverdrawLayer.destroy(); + mDebugOverdrawLayer = null; + mDebugOverdrawPaint = null; + } + } + } + + if (loadProperties()) { + changed = true; + } + + return changed; + } + + private static int search(String[] values, String value) { + for (int i = 0; i < values.length; i++) { + if (values[i].equals(value)) return i; + } + return -1; + } + + @Override + void dumpGfxInfo(PrintWriter pw) { + if (mProfileEnabled) { + pw.printf("\n\tDraw\tProcess\tExecute\n"); + + mProfileLock.lock(); + try { + for (int i = 0; i < mProfileData.length; i += PROFILE_FRAME_DATA_COUNT) { + if (mProfileData[i] < 0) { + break; + } + pw.printf("\t%3.2f\t%3.2f\t%3.2f\n", mProfileData[i], mProfileData[i + 1], + mProfileData[i + 2]); + mProfileData[i] = mProfileData[i + 1] = mProfileData[i + 2] = -1; + } + mProfileCurrentFrame = mProfileData.length; + } finally { + mProfileLock.unlock(); + } + } + } + + @Override + long getFrameCount() { + return mFrameCount; + } + + /** + * Indicates whether this renderer instance can track and update dirty regions. + */ + boolean hasDirtyRegions() { + return mDirtyRegionsEnabled; + } + + /** + * Checks for OpenGL errors. If an error has occured, {@link #destroy(boolean)} + * is invoked and the requested flag is turned off. The error code is + * also logged as a warning. + */ + void checkEglErrors() { + if (isEnabled()) { + checkEglErrorsForced(); + } + } + + private void checkEglErrorsForced() { + int error = sEgl.eglGetError(); + if (error != EGL_SUCCESS) { + // something bad has happened revert to + // normal rendering. + Log.w(LOG_TAG, "EGL error: " + GLUtils.getEGLErrorString(error)); + fallback(error != EGL11.EGL_CONTEXT_LOST); + } + } + + private void fallback(boolean fallback) { + destroy(true); + if (fallback) { + // we'll try again if it was context lost + setRequested(false); + Log.w(LOG_TAG, "Mountain View, we've had a problem here. " + + "Switching back to software rendering."); + } + } + + @Override + boolean initialize(Surface surface) throws OutOfResourcesException { + if (isRequested() && !isEnabled()) { + boolean contextCreated = initializeEgl(); + mGl = createEglSurface(surface); + mDestroyed = false; + + if (mGl != null) { + int err = sEgl.eglGetError(); + if (err != EGL_SUCCESS) { + destroy(true); + setRequested(false); + } else { + if (mCanvas == null) { + mCanvas = createCanvas(); + mCanvas.setName(mName); + } + setEnabled(true); + + if (contextCreated) { + initAtlas(); + } + } + + return mCanvas != null; + } + } + return false; + } + + @Override + void updateSurface(Surface surface) throws OutOfResourcesException { + if (isRequested() && isEnabled()) { + createEglSurface(surface); + } + } + + boolean initializeEgl() { + synchronized (sEglLock) { + if (sEgl == null && sEglConfig == null) { + sEgl = (EGL10) EGLContext.getEGL(); + + // Get to the default display. + sEglDisplay = sEgl.eglGetDisplay(EGL_DEFAULT_DISPLAY); + + if (sEglDisplay == EGL_NO_DISPLAY) { + throw new RuntimeException("eglGetDisplay failed " + + GLUtils.getEGLErrorString(sEgl.eglGetError())); + } + + // We can now initialize EGL for that display + int[] version = new int[2]; + if (!sEgl.eglInitialize(sEglDisplay, version)) { + throw new RuntimeException("eglInitialize failed " + + GLUtils.getEGLErrorString(sEgl.eglGetError())); + } + + checkEglErrorsForced(); + + sEglConfig = loadEglConfig(); + } + } + + ManagedEGLContext managedContext = sEglContextStorage.get(); + mEglContext = managedContext != null ? managedContext.getContext() : null; + mEglThread = Thread.currentThread(); + + if (mEglContext == null) { + mEglContext = createContext(sEgl, sEglDisplay, sEglConfig); + sEglContextStorage.set(createManagedContext(mEglContext)); + return true; + } + + return false; + } + + private EGLConfig loadEglConfig() { + EGLConfig eglConfig = chooseEglConfig(); + if (eglConfig == null) { + // We tried to use EGL_SWAP_BEHAVIOR_PRESERVED_BIT, try again without + if (sDirtyRegions) { + sDirtyRegions = false; + eglConfig = chooseEglConfig(); + if (eglConfig == null) { + throw new RuntimeException("eglConfig not initialized"); + } + } else { + throw new RuntimeException("eglConfig not initialized"); + } + } + return eglConfig; + } + + private EGLConfig chooseEglConfig() { + EGLConfig[] configs = new EGLConfig[1]; + int[] configsCount = new int[1]; + int[] configSpec = getConfig(sDirtyRegions); + + // Debug + final String debug = SystemProperties.get(PRINT_CONFIG_PROPERTY, ""); + if ("all".equalsIgnoreCase(debug)) { + sEgl.eglChooseConfig(sEglDisplay, configSpec, null, 0, configsCount); + + EGLConfig[] debugConfigs = new EGLConfig[configsCount[0]]; + sEgl.eglChooseConfig(sEglDisplay, configSpec, debugConfigs, + configsCount[0], configsCount); + + for (EGLConfig config : debugConfigs) { + printConfig(config); + } + } + + if (!sEgl.eglChooseConfig(sEglDisplay, configSpec, configs, 1, configsCount)) { + throw new IllegalArgumentException("eglChooseConfig failed " + + GLUtils.getEGLErrorString(sEgl.eglGetError())); + } else if (configsCount[0] > 0) { + if ("choice".equalsIgnoreCase(debug)) { + printConfig(configs[0]); + } + return configs[0]; + } + + return null; + } + + private static void printConfig(EGLConfig config) { + int[] value = new int[1]; + + Log.d(LOG_TAG, "EGL configuration " + config + ":"); + + sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_RED_SIZE, value); + Log.d(LOG_TAG, " RED_SIZE = " + value[0]); + + sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_GREEN_SIZE, value); + Log.d(LOG_TAG, " GREEN_SIZE = " + value[0]); + + sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_BLUE_SIZE, value); + Log.d(LOG_TAG, " BLUE_SIZE = " + value[0]); + + sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_ALPHA_SIZE, value); + Log.d(LOG_TAG, " ALPHA_SIZE = " + value[0]); + + sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_DEPTH_SIZE, value); + Log.d(LOG_TAG, " DEPTH_SIZE = " + value[0]); + + sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_STENCIL_SIZE, value); + Log.d(LOG_TAG, " STENCIL_SIZE = " + value[0]); + + sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_SAMPLE_BUFFERS, value); + Log.d(LOG_TAG, " SAMPLE_BUFFERS = " + value[0]); + + sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_SAMPLES, value); + Log.d(LOG_TAG, " SAMPLES = " + value[0]); + + sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_SURFACE_TYPE, value); + Log.d(LOG_TAG, " SURFACE_TYPE = 0x" + Integer.toHexString(value[0])); + + sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_CONFIG_CAVEAT, value); + Log.d(LOG_TAG, " CONFIG_CAVEAT = 0x" + Integer.toHexString(value[0])); + } + + GL createEglSurface(Surface surface) throws OutOfResourcesException { + // Check preconditions. + if (sEgl == null) { + throw new RuntimeException("egl not initialized"); + } + if (sEglDisplay == null) { + throw new RuntimeException("eglDisplay not initialized"); + } + if (sEglConfig == null) { + throw new RuntimeException("eglConfig not initialized"); + } + if (Thread.currentThread() != mEglThread) { + throw new IllegalStateException("HardwareRenderer cannot be used " + + "from multiple threads"); + } + + // In case we need to destroy an existing surface + destroySurface(); + + // Create an EGL surface we can render into. + if (!createSurface(surface)) { + return null; + } + + initCaches(); + + return mEglContext.getGL(); + } + + private void enableDirtyRegions() { + // If mDirtyRegions is set, this means we have an EGL configuration + // with EGL_SWAP_BEHAVIOR_PRESERVED_BIT set + if (sDirtyRegions) { + if (!(mDirtyRegionsEnabled = preserveBackBuffer())) { + Log.w(LOG_TAG, "Backbuffer cannot be preserved"); + } + } else if (sDirtyRegionsRequested) { + // If mDirtyRegions is not set, our EGL configuration does not + // have EGL_SWAP_BEHAVIOR_PRESERVED_BIT; however, the default + // swap behavior might be EGL_BUFFER_PRESERVED, which means we + // want to set mDirtyRegions. We try to do this only if dirty + // regions were initially requested as part of the device + // configuration (see RENDER_DIRTY_REGIONS) + mDirtyRegionsEnabled = isBackBufferPreserved(); + } + } + + EGLContext createContext(EGL10 egl, EGLDisplay eglDisplay, EGLConfig eglConfig) { + final int[] attribs = { EGL14.EGL_CONTEXT_CLIENT_VERSION, GL_VERSION, EGL_NONE }; + + EGLContext context = egl.eglCreateContext(eglDisplay, eglConfig, EGL_NO_CONTEXT, + attribs); + if (context == null || context == EGL_NO_CONTEXT) { + //noinspection ConstantConditions + throw new IllegalStateException( + "Could not create an EGL context. eglCreateContext failed with error: " + + GLUtils.getEGLErrorString(sEgl.eglGetError())); + } + + return context; + } + + void destroySurface() { + if (mEglSurface != null && mEglSurface != EGL_NO_SURFACE) { + if (mEglSurface.equals(sEgl.eglGetCurrentSurface(EGL_DRAW))) { + sEgl.eglMakeCurrent(sEglDisplay, + EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + } + sEgl.eglDestroySurface(sEglDisplay, mEglSurface); + mEglSurface = null; + } + } + + @Override + void invalidate(Surface surface) { + // Cancels any existing buffer to ensure we'll get a buffer + // of the right size before we call eglSwapBuffers + sEgl.eglMakeCurrent(sEglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + + if (mEglSurface != null && mEglSurface != EGL_NO_SURFACE) { + sEgl.eglDestroySurface(sEglDisplay, mEglSurface); + mEglSurface = null; + setEnabled(false); + } + + if (surface.isValid()) { + if (!createSurface(surface)) { + return; + } + + mUpdateDirtyRegions = true; + + if (mCanvas != null) { + setEnabled(true); + } + } + } + + private boolean createSurface(Surface surface) { + mEglSurface = sEgl.eglCreateWindowSurface(sEglDisplay, sEglConfig, surface, null); + + if (mEglSurface == null || mEglSurface == EGL_NO_SURFACE) { + int error = sEgl.eglGetError(); + if (error == EGL_BAD_NATIVE_WINDOW) { + Log.e(LOG_TAG, "createWindowSurface returned EGL_BAD_NATIVE_WINDOW."); + return false; + } + throw new RuntimeException("createWindowSurface failed " + + GLUtils.getEGLErrorString(error)); + } + + if (!sEgl.eglMakeCurrent(sEglDisplay, mEglSurface, mEglSurface, mEglContext)) { + throw new IllegalStateException("eglMakeCurrent failed " + + GLUtils.getEGLErrorString(sEgl.eglGetError())); + } + + enableDirtyRegions(); + + return true; + } + + @Override + boolean validate() { + return checkRenderContext() != SURFACE_STATE_ERROR; + } + + @Override + void setup(int width, int height) { + if (validate()) { + mCanvas.setViewport(width, height); + mWidth = width; + mHeight = height; + } + } + + @Override + int getWidth() { + return mWidth; + } + + @Override + int getHeight() { + return mHeight; + } + + @Override + HardwareCanvas getCanvas() { + return mCanvas; + } + + @Override + void setName(String name) { + mName = name; + } + + class FunctorsRunnable implements Runnable { + View.AttachInfo attachInfo; + + @Override + public void run() { + final HardwareRenderer renderer = attachInfo.mHardwareRenderer; + if (renderer == null || !renderer.isEnabled() || renderer != GLRenderer.this) { + return; + } + + if (checkRenderContext() != SURFACE_STATE_ERROR) { + int status = mCanvas.invokeFunctors(mRedrawClip); + handleFunctorStatus(attachInfo, status); + } + } + } + + @Override + void draw(View view, View.AttachInfo attachInfo, HardwareDrawCallbacks callbacks, + Rect dirty) { + if (canDraw()) { + if (!hasDirtyRegions()) { + dirty = null; + } + attachInfo.mIgnoreDirtyState = true; + attachInfo.mDrawingTime = SystemClock.uptimeMillis(); + + view.mPrivateFlags |= View.PFLAG_DRAWN; + + // We are already on the correct thread + final int surfaceState = checkRenderContextUnsafe(); + if (surfaceState != SURFACE_STATE_ERROR) { + HardwareCanvas canvas = mCanvas; + attachInfo.mHardwareCanvas = canvas; + + if (mProfileEnabled) { + mProfileLock.lock(); + } + + dirty = beginFrame(canvas, dirty, surfaceState); + + DisplayList displayList = buildDisplayList(view, canvas); + + // buildDisplayList() calls into user code which can cause + // an eglMakeCurrent to happen with a different surface/context. + // We must therefore check again here. + if (checkRenderContextUnsafe() == SURFACE_STATE_ERROR) { + return; + } + + int saveCount = 0; + int status = DisplayList.STATUS_DONE; + + long start = getSystemTime(); + try { + status = prepareFrame(dirty); + + saveCount = canvas.save(); + callbacks.onHardwarePreDraw(canvas); + + if (displayList != null) { + status |= drawDisplayList(attachInfo, canvas, displayList, status); + } else { + // Shouldn't reach here + view.draw(canvas); + } + } catch (Exception e) { + Log.e(LOG_TAG, "An error has occurred while drawing:", e); + } finally { + callbacks.onHardwarePostDraw(canvas); + canvas.restoreToCount(saveCount); + view.mRecreateDisplayList = false; + + mDrawDelta = getSystemTime() - start; + + if (mDrawDelta > 0) { + mFrameCount++; + + debugOverdraw(attachInfo, dirty, canvas, displayList); + debugDirtyRegions(dirty, canvas); + drawProfileData(attachInfo); + } + } + + onPostDraw(); + + swapBuffers(status); + + if (mProfileEnabled) { + mProfileLock.unlock(); + } + + attachInfo.mIgnoreDirtyState = false; + } + } + } + + private void debugOverdraw(View.AttachInfo attachInfo, Rect dirty, + HardwareCanvas canvas, DisplayList displayList) { + + if (mDebugOverdraw == OVERDRAW_TYPE_COUNT) { + if (mDebugOverdrawLayer == null) { + mDebugOverdrawLayer = createHardwareLayer(mWidth, mHeight, true); + } else if (mDebugOverdrawLayer.getWidth() != mWidth || + mDebugOverdrawLayer.getHeight() != mHeight) { + mDebugOverdrawLayer.resize(mWidth, mHeight); + } + + if (!mDebugOverdrawLayer.isValid()) { + mDebugOverdraw = -1; + return; + } + + HardwareCanvas layerCanvas = mDebugOverdrawLayer.start(canvas, dirty); + countOverdraw(layerCanvas); + final int restoreCount = layerCanvas.save(); + layerCanvas.drawDisplayList(displayList, null, DisplayList.FLAG_CLIP_CHILDREN); + layerCanvas.restoreToCount(restoreCount); + mDebugOverdrawLayer.end(canvas); + + float overdraw = getOverdraw(layerCanvas); + DisplayMetrics metrics = attachInfo.mRootView.getResources().getDisplayMetrics(); + + drawOverdrawCounter(canvas, overdraw, metrics.density); + } + } + + private void drawOverdrawCounter(HardwareCanvas canvas, float overdraw, float density) { + final String text = String.format("%.2fx", overdraw); + final Paint paint = setupPaint(density); + // HSBtoColor will clamp the values in the 0..1 range + paint.setColor(Color.HSBtoColor(0.28f - 0.28f * overdraw / 3.5f, 0.8f, 1.0f)); + + canvas.drawText(text, density * 4.0f, mHeight - paint.getFontMetrics().bottom, paint); + } + + private Paint setupPaint(float density) { + if (mDebugOverdrawPaint == null) { + mDebugOverdrawPaint = new Paint(); + mDebugOverdrawPaint.setAntiAlias(true); + mDebugOverdrawPaint.setShadowLayer(density * 3.0f, 0.0f, 0.0f, 0xff000000); + mDebugOverdrawPaint.setTextSize(density * 20.0f); + } + return mDebugOverdrawPaint; + } + + private DisplayList buildDisplayList(View view, HardwareCanvas canvas) { + if (mDrawDelta <= 0) { + return view.mDisplayList; + } + + view.mRecreateDisplayList = (view.mPrivateFlags & View.PFLAG_INVALIDATED) + == View.PFLAG_INVALIDATED; + view.mPrivateFlags &= ~View.PFLAG_INVALIDATED; + + long buildDisplayListStartTime = startBuildDisplayListProfiling(); + canvas.clearLayerUpdates(); + + Trace.traceBegin(Trace.TRACE_TAG_VIEW, "getDisplayList"); + DisplayList displayList = view.getDisplayList(); + Trace.traceEnd(Trace.TRACE_TAG_VIEW); + + endBuildDisplayListProfiling(buildDisplayListStartTime); + + return displayList; + } + + private Rect beginFrame(HardwareCanvas canvas, Rect dirty, int surfaceState) { + // We had to change the current surface and/or context, redraw everything + if (surfaceState == SURFACE_STATE_UPDATED) { + dirty = null; + beginFrame(null); + } else { + int[] size = mSurfaceSize; + beginFrame(size); + + if (size[1] != mHeight || size[0] != mWidth) { + mWidth = size[0]; + mHeight = size[1]; + + canvas.setViewport(mWidth, mHeight); + + dirty = null; + } + } + + if (mDebugDataProvider != null) dirty = null; + + return dirty; + } + + private long startBuildDisplayListProfiling() { + if (mProfileEnabled) { + mProfileCurrentFrame += PROFILE_FRAME_DATA_COUNT; + if (mProfileCurrentFrame >= mProfileData.length) { + mProfileCurrentFrame = 0; + } + + return System.nanoTime(); + } + return 0; + } + + private void endBuildDisplayListProfiling(long getDisplayListStartTime) { + if (mProfileEnabled) { + long now = System.nanoTime(); + float total = (now - getDisplayListStartTime) * 0.000001f; + //noinspection PointlessArithmeticExpression + mProfileData[mProfileCurrentFrame] = total; + } + } + + private int prepareFrame(Rect dirty) { + int status; + Trace.traceBegin(Trace.TRACE_TAG_VIEW, "prepareFrame"); + try { + status = onPreDraw(dirty); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIEW); + } + return status; + } + + private int drawDisplayList(View.AttachInfo attachInfo, HardwareCanvas canvas, + DisplayList displayList, int status) { + + long drawDisplayListStartTime = 0; + if (mProfileEnabled) { + drawDisplayListStartTime = System.nanoTime(); + } + + Trace.traceBegin(Trace.TRACE_TAG_VIEW, "drawDisplayList"); + try { + status |= canvas.drawDisplayList(displayList, mRedrawClip, + DisplayList.FLAG_CLIP_CHILDREN); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIEW); + } + + if (mProfileEnabled) { + long now = System.nanoTime(); + float total = (now - drawDisplayListStartTime) * 0.000001f; + mProfileData[mProfileCurrentFrame + 1] = total; + } + + handleFunctorStatus(attachInfo, status); + return status; + } + + private void swapBuffers(int status) { + if ((status & DisplayList.STATUS_DREW) == DisplayList.STATUS_DREW) { + long eglSwapBuffersStartTime = 0; + if (mProfileEnabled) { + eglSwapBuffersStartTime = System.nanoTime(); + } + + sEgl.eglSwapBuffers(sEglDisplay, mEglSurface); + + if (mProfileEnabled) { + long now = System.nanoTime(); + float total = (now - eglSwapBuffersStartTime) * 0.000001f; + mProfileData[mProfileCurrentFrame + 2] = total; + } + + checkEglErrors(); + } + } + + private void debugDirtyRegions(Rect dirty, HardwareCanvas canvas) { + if (mDebugDirtyRegions) { + if (mDebugPaint == null) { + mDebugPaint = new Paint(); + mDebugPaint.setColor(0x7fff0000); + } + + if (dirty != null && (mFrameCount & 1) == 0) { + canvas.drawRect(dirty, mDebugPaint); + } + } + } + + private void handleFunctorStatus(View.AttachInfo attachInfo, int status) { + // If the draw flag is set, functors will be invoked while executing + // the tree of display lists + if ((status & DisplayList.STATUS_DRAW) != 0) { + if (mRedrawClip.isEmpty()) { + attachInfo.mViewRootImpl.invalidate(); + } else { + attachInfo.mViewRootImpl.invalidateChildInParent(null, mRedrawClip); + mRedrawClip.setEmpty(); + } + } + + if ((status & DisplayList.STATUS_INVOKE) != 0 || + attachInfo.mHandler.hasCallbacks(mFunctorsRunnable)) { + attachInfo.mHandler.removeCallbacks(mFunctorsRunnable); + mFunctorsRunnable.attachInfo = attachInfo; + attachInfo.mHandler.postDelayed(mFunctorsRunnable, FUNCTOR_PROCESS_DELAY); + } + } + + @Override + void detachFunctor(int functor) { + if (mCanvas != null) { + mCanvas.detachFunctor(functor); + } + } + + @Override + boolean attachFunctor(View.AttachInfo attachInfo, int functor) { + if (mCanvas != null) { + mCanvas.attachFunctor(functor); + mFunctorsRunnable.attachInfo = attachInfo; + attachInfo.mHandler.removeCallbacks(mFunctorsRunnable); + attachInfo.mHandler.postDelayed(mFunctorsRunnable, 0); + return true; + } + return false; + } + + /** + * Ensures the current EGL context and surface are the ones we expect. + * This method throws an IllegalStateException if invoked from a thread + * that did not initialize EGL. + * + * @return {@link #SURFACE_STATE_ERROR} if the correct EGL context cannot be made current, + * {@link #SURFACE_STATE_UPDATED} if the EGL context was changed or + * {@link #SURFACE_STATE_SUCCESS} if the EGL context was the correct one + * + * @see #checkRenderContextUnsafe() + */ + int checkRenderContext() { + if (mEglThread != Thread.currentThread()) { + throw new IllegalStateException("Hardware acceleration can only be used with a " + + "single UI thread.\nOriginal thread: " + mEglThread + "\n" + + "Current thread: " + Thread.currentThread()); + } + + return checkRenderContextUnsafe(); + } + + /** + * Ensures the current EGL context and surface are the ones we expect. + * This method does not check the current thread. + * + * @return {@link #SURFACE_STATE_ERROR} if the correct EGL context cannot be made current, + * {@link #SURFACE_STATE_UPDATED} if the EGL context was changed or + * {@link #SURFACE_STATE_SUCCESS} if the EGL context was the correct one + * + * @see #checkRenderContext() + */ + private int checkRenderContextUnsafe() { + if (!mEglSurface.equals(sEgl.eglGetCurrentSurface(EGL_DRAW)) || + !mEglContext.equals(sEgl.eglGetCurrentContext())) { + if (!sEgl.eglMakeCurrent(sEglDisplay, mEglSurface, mEglSurface, mEglContext)) { + Log.e(LOG_TAG, "eglMakeCurrent failed " + + GLUtils.getEGLErrorString(sEgl.eglGetError())); + fallback(true); + return SURFACE_STATE_ERROR; + } else { + if (mUpdateDirtyRegions) { + enableDirtyRegions(); + mUpdateDirtyRegions = false; + } + return SURFACE_STATE_UPDATED; + } + } + return SURFACE_STATE_SUCCESS; + } + + private static int dpToPx(int dp, float density) { + return (int) (dp * density + 0.5f); + } + + private static native boolean loadProperties(); + + static native void setupShadersDiskCache(String cacheFile); + + /** + * Notifies EGL that the frame is about to be rendered. + * @param size + */ + private static native void beginFrame(int[] size); + + /** + * Returns the current system time according to the renderer. + * This method is used for debugging only and should not be used + * as a clock. + */ + private static native long getSystemTime(); + + /** + * Preserves the back buffer of the current surface after a buffer swap. + * Calling this method sets the EGL_SWAP_BEHAVIOR attribute of the current + * surface to EGL_BUFFER_PRESERVED. Calling this method requires an EGL + * config that supports EGL_SWAP_BEHAVIOR_PRESERVED_BIT. + * + * @return True if the swap behavior was successfully changed, + * false otherwise. + */ + private static native boolean preserveBackBuffer(); + + /** + * Indicates whether the current surface preserves its back buffer + * after a buffer swap. + * + * @return True, if the surface's EGL_SWAP_BEHAVIOR is EGL_BUFFER_PRESERVED, + * false otherwise + */ + private static native boolean isBackBufferPreserved(); + + class DrawPerformanceDataProvider extends GraphDataProvider { + private final int mGraphType; + + private int mVerticalUnit; + private int mHorizontalUnit; + private int mHorizontalMargin; + private int mThresholdStroke; + + DrawPerformanceDataProvider(int graphType) { + mGraphType = graphType; + } + + @Override + void prepare(DisplayMetrics metrics) { + final float density = metrics.density; + + mVerticalUnit = dpToPx(PROFILE_DRAW_DP_PER_MS, density); + mHorizontalUnit = dpToPx(PROFILE_DRAW_WIDTH, density); + mHorizontalMargin = dpToPx(PROFILE_DRAW_MARGIN, density); + mThresholdStroke = dpToPx(PROFILE_DRAW_THRESHOLD_STROKE_WIDTH, density); + } + + @Override + int getGraphType() { + return mGraphType; + } + + @Override + int getVerticalUnitSize() { + return mVerticalUnit; + } + + @Override + int getHorizontalUnitSize() { + return mHorizontalUnit; + } + + @Override + int getHorizontaUnitMargin() { + return mHorizontalMargin; + } + + @Override + float[] getData() { + return mProfileData; + } + + @Override + float getThreshold() { + return 16; + } + + @Override + int getFrameCount() { + return mProfileData.length / PROFILE_FRAME_DATA_COUNT; + } + + @Override + int getElementCount() { + return PROFILE_FRAME_DATA_COUNT; + } + + @Override + int getCurrentFrame() { + return mProfileCurrentFrame / PROFILE_FRAME_DATA_COUNT; + } + + @Override + void setupGraphPaint(Paint paint, int elementIndex) { + paint.setColor(PROFILE_DRAW_COLORS[elementIndex]); + if (mGraphType == GRAPH_TYPE_LINES) paint.setStrokeWidth(mThresholdStroke); + } + + @Override + void setupThresholdPaint(Paint paint) { + paint.setColor(PROFILE_DRAW_THRESHOLD_COLOR); + paint.setStrokeWidth(mThresholdStroke); + } + + @Override + void setupCurrentFramePaint(Paint paint) { + paint.setColor(PROFILE_DRAW_CURRENT_FRAME_COLOR); + if (mGraphType == GRAPH_TYPE_LINES) paint.setStrokeWidth(mThresholdStroke); + } + } +} diff --git a/core/java/android/view/HapticFeedbackConstants.java b/core/java/android/view/HapticFeedbackConstants.java index 8f40260..26f47f9 100644 --- a/core/java/android/view/HapticFeedbackConstants.java +++ b/core/java/android/view/HapticFeedbackConstants.java @@ -41,6 +41,11 @@ public class HapticFeedbackConstants { public static final int KEYBOARD_TAP = 3; /** + * The user has pressed either an hour or minute tick of a Clock. + */ + public static final int CLOCK_TICK = 4; + + /** * This is a private constant. Feel free to renumber as desired. * @hide */ diff --git a/core/java/android/view/HardwareRenderer.java b/core/java/android/view/HardwareRenderer.java index f215189..5c0be4a 100644 --- a/core/java/android/view/HardwareRenderer.java +++ b/core/java/android/view/HardwareRenderer.java @@ -16,41 +16,14 @@ package android.view; -import android.content.ComponentCallbacks2; -import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.SurfaceTexture; -import android.opengl.EGL14; -import android.opengl.GLUtils; -import android.opengl.ManagedEGLContext; -import android.os.Handler; -import android.os.IBinder; -import android.os.Looper; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.SystemClock; -import android.os.SystemProperties; -import android.os.Trace; import android.util.DisplayMetrics; -import android.util.Log; import android.view.Surface.OutOfResourcesException; -import com.google.android.gles_jni.EGLImpl; - -import javax.microedition.khronos.egl.EGL10; -import javax.microedition.khronos.egl.EGL11; -import javax.microedition.khronos.egl.EGLConfig; -import javax.microedition.khronos.egl.EGLContext; -import javax.microedition.khronos.egl.EGLDisplay; -import javax.microedition.khronos.egl.EGLSurface; -import javax.microedition.khronos.opengles.GL; - import java.io.File; import java.io.PrintWriter; -import java.util.concurrent.locks.ReentrantLock; - -import static javax.microedition.khronos.egl.EGL10.*; /** * Interface for rendering a view hierarchy using hardware acceleration. @@ -66,13 +39,6 @@ public abstract class HardwareRenderer { private static final String CACHE_PATH_SHADERS = "com.android.opengl.shaders_cache"; /** - * Turn on to only refresh the parts of the screen that need updating. - * When turned on the property defined by {@link #RENDER_DIRTY_REGIONS_PROPERTY} - * must also have the value "true". - */ - static final boolean RENDER_DIRTY_REGIONS = true; - - /** * System property used to enable or disable dirty regions invalidation. * This property is only queried if {@link #RENDER_DIRTY_REGIONS} is true. * The default value of this property is assumed to be true. @@ -222,16 +188,6 @@ public abstract class HardwareRenderer { */ public static boolean sSystemRendererDisabled = false; - /** - * Number of frames to profile. - */ - private static final int PROFILE_MAX_FRAMES = 128; - - /** - * Number of floats per profiled frame. - */ - private static final int PROFILE_FRAME_DATA_COUNT = 3; - private boolean mEnabled; private boolean mRequested = true; @@ -381,8 +337,6 @@ public abstract class HardwareRenderer { */ abstract boolean loadSystemProperties(Surface surface); - private static native boolean nLoadProperties(); - /** * Sets the directory to use as a persistent storage for hardware rendering * resources. @@ -392,60 +346,9 @@ public abstract class HardwareRenderer { * @hide */ public static void setupDiskCache(File cacheDir) { - nSetupShadersDiskCache(new File(cacheDir, CACHE_PATH_SHADERS).getAbsolutePath()); - } - - private static native void nSetupShadersDiskCache(String cacheFile); - - /** - * Notifies EGL that the frame is about to be rendered. - * @param size - */ - static void beginFrame(int[] size) { - nBeginFrame(size); + GLRenderer.setupShadersDiskCache(new File(cacheDir, CACHE_PATH_SHADERS).getAbsolutePath()); } - private static native void nBeginFrame(int[] size); - - /** - * Returns the current system time according to the renderer. - * This method is used for debugging only and should not be used - * as a clock. - */ - static long getSystemTime() { - return nGetSystemTime(); - } - - private static native long nGetSystemTime(); - - /** - * Preserves the back buffer of the current surface after a buffer swap. - * Calling this method sets the EGL_SWAP_BEHAVIOR attribute of the current - * surface to EGL_BUFFER_PRESERVED. Calling this method requires an EGL - * config that supports EGL_SWAP_BEHAVIOR_PRESERVED_BIT. - * - * @return True if the swap behavior was successfully changed, - * false otherwise. - */ - static boolean preserveBackBuffer() { - return nPreserveBackBuffer(); - } - - private static native boolean nPreserveBackBuffer(); - - /** - * Indicates whether the current surface preserves its back buffer - * after a buffer swap. - * - * @return True, if the surface's EGL_SWAP_BEHAVIOR is EGL_BUFFER_PRESERVED, - * false otherwise - */ - static boolean isBackBufferPreserved() { - return nIsBackBufferPreserved(); - } - - private static native boolean nIsBackBufferPreserved(); - /** * Indicates that the specified hardware layer needs to be updated * as soon as possible. @@ -509,18 +412,6 @@ public abstract class HardwareRenderer { Rect dirty); /** - * Creates a new display list that can be used to record batches of - * drawing operations. - * - * @param name The name of the display list, used for debugging purpose. May be null. - * - * @return A new display list. - * - * @hide - */ - public abstract DisplayList createDisplayList(String name); - - /** * Creates a new hardware layer. A hardware layer built by calling this * method will be treated as a texture layer, instead of as a render target. * @@ -621,17 +512,15 @@ public abstract class HardwareRenderer { /** * Creates a hardware renderer using OpenGL. * - * @param glVersion The version of OpenGL to use (1 for OpenGL 1, 11 for OpenGL 1.1, etc.) * @param translucent True if the surface is translucent, false otherwise * * @return A hardware renderer backed by OpenGL. */ - static HardwareRenderer createGlRenderer(int glVersion, boolean translucent) { - switch (glVersion) { - case 2: - return Gl20Renderer.create(translucent); + static HardwareRenderer create(boolean translucent) { + if (GLES20Canvas.isAvailable()) { + return new GLRenderer(translucent); } - throw new IllegalArgumentException("Unknown GL version: " + glVersion); + return null; } /** @@ -656,7 +545,7 @@ public abstract class HardwareRenderer { * see {@link android.content.ComponentCallbacks} */ static void startTrimMemory(int level) { - Gl20Renderer.startTrimMemory(level); + GLRenderer.startTrimMemory(level); } /** @@ -664,7 +553,7 @@ public abstract class HardwareRenderer { * cleanup special resources used by the memory trimming process. */ static void endTrimMemory() { - Gl20Renderer.endTrimMemory(); + GLRenderer.endTrimMemory(); } /** @@ -798,1553 +687,4 @@ public abstract class HardwareRenderer { */ abstract void setupCurrentFramePaint(Paint paint); } - - @SuppressWarnings({"deprecation"}) - static abstract class GlRenderer extends HardwareRenderer { - static final int SURFACE_STATE_ERROR = 0; - static final int SURFACE_STATE_SUCCESS = 1; - static final int SURFACE_STATE_UPDATED = 2; - - static final int FUNCTOR_PROCESS_DELAY = 4; - - private static final int PROFILE_DRAW_MARGIN = 0; - private static final int PROFILE_DRAW_WIDTH = 3; - private static final int[] PROFILE_DRAW_COLORS = { 0xcf3e66cc, 0xcfdc3912, 0xcfe69800 }; - private static final int PROFILE_DRAW_CURRENT_FRAME_COLOR = 0xcf5faa4d; - private static final int PROFILE_DRAW_THRESHOLD_COLOR = 0xff5faa4d; - private static final int PROFILE_DRAW_THRESHOLD_STROKE_WIDTH = 2; - private static final int PROFILE_DRAW_DP_PER_MS = 7; - - private static final String[] VISUALIZERS = { - PROFILE_PROPERTY_VISUALIZE_BARS, - PROFILE_PROPERTY_VISUALIZE_LINES - }; - - private static final String[] OVERDRAW = { - OVERDRAW_PROPERTY_SHOW, - OVERDRAW_PROPERTY_COUNT - }; - private static final int OVERDRAW_TYPE_COUNT = 1; - - static EGL10 sEgl; - static EGLDisplay sEglDisplay; - static EGLConfig sEglConfig; - static final Object[] sEglLock = new Object[0]; - int mWidth = -1, mHeight = -1; - - static final ThreadLocal<ManagedEGLContext> sEglContextStorage - = new ThreadLocal<ManagedEGLContext>(); - - EGLContext mEglContext; - Thread mEglThread; - - EGLSurface mEglSurface; - - GL mGl; - HardwareCanvas mCanvas; - - String mName; - - long mFrameCount; - Paint mDebugPaint; - - static boolean sDirtyRegions; - static final boolean sDirtyRegionsRequested; - static { - String dirtyProperty = SystemProperties.get(RENDER_DIRTY_REGIONS_PROPERTY, "true"); - //noinspection PointlessBooleanExpression,ConstantConditions - sDirtyRegions = RENDER_DIRTY_REGIONS && "true".equalsIgnoreCase(dirtyProperty); - sDirtyRegionsRequested = sDirtyRegions; - } - - boolean mDirtyRegionsEnabled; - boolean mUpdateDirtyRegions; - - boolean mProfileEnabled; - int mProfileVisualizerType = -1; - float[] mProfileData; - ReentrantLock mProfileLock; - int mProfileCurrentFrame = -PROFILE_FRAME_DATA_COUNT; - - GraphDataProvider mDebugDataProvider; - float[][] mProfileShapes; - Paint mProfilePaint; - - boolean mDebugDirtyRegions; - int mDebugOverdraw = -1; - HardwareLayer mDebugOverdrawLayer; - Paint mDebugOverdrawPaint; - - final int mGlVersion; - final boolean mTranslucent; - - private boolean mDestroyed; - - private final Rect mRedrawClip = new Rect(); - - private final int[] mSurfaceSize = new int[2]; - private final FunctorsRunnable mFunctorsRunnable = new FunctorsRunnable(); - - private long mDrawDelta = Long.MAX_VALUE; - - GlRenderer(int glVersion, boolean translucent) { - mGlVersion = glVersion; - mTranslucent = translucent; - - loadSystemProperties(null); - } - - @Override - boolean loadSystemProperties(Surface surface) { - boolean value; - boolean changed = false; - - String profiling = SystemProperties.get(PROFILE_PROPERTY); - int graphType = search(VISUALIZERS, profiling); - value = graphType >= 0; - - if (graphType != mProfileVisualizerType) { - changed = true; - mProfileVisualizerType = graphType; - - mProfileShapes = null; - mProfilePaint = null; - - if (value) { - mDebugDataProvider = new DrawPerformanceDataProvider(graphType); - } else { - mDebugDataProvider = null; - } - } - - // If on-screen profiling is not enabled, we need to check whether - // console profiling only is enabled - if (!value) { - value = Boolean.parseBoolean(profiling); - } - - if (value != mProfileEnabled) { - changed = true; - mProfileEnabled = value; - - if (mProfileEnabled) { - Log.d(LOG_TAG, "Profiling hardware renderer"); - - int maxProfileFrames = SystemProperties.getInt(PROFILE_MAXFRAMES_PROPERTY, - PROFILE_MAX_FRAMES); - mProfileData = new float[maxProfileFrames * PROFILE_FRAME_DATA_COUNT]; - for (int i = 0; i < mProfileData.length; i += PROFILE_FRAME_DATA_COUNT) { - mProfileData[i] = mProfileData[i + 1] = mProfileData[i + 2] = -1; - } - - mProfileLock = new ReentrantLock(); - } else { - mProfileData = null; - mProfileLock = null; - mProfileVisualizerType = -1; - } - - mProfileCurrentFrame = -PROFILE_FRAME_DATA_COUNT; - } - - value = SystemProperties.getBoolean(DEBUG_DIRTY_REGIONS_PROPERTY, false); - if (value != mDebugDirtyRegions) { - changed = true; - mDebugDirtyRegions = value; - - if (mDebugDirtyRegions) { - Log.d(LOG_TAG, "Debugging dirty regions"); - } - } - - String overdraw = SystemProperties.get(HardwareRenderer.DEBUG_OVERDRAW_PROPERTY); - int debugOverdraw = search(OVERDRAW, overdraw); - if (debugOverdraw != mDebugOverdraw) { - changed = true; - mDebugOverdraw = debugOverdraw; - - if (mDebugOverdraw != OVERDRAW_TYPE_COUNT) { - if (mDebugOverdrawLayer != null) { - mDebugOverdrawLayer.destroy(); - mDebugOverdrawLayer = null; - mDebugOverdrawPaint = null; - } - } - } - - if (nLoadProperties()) { - changed = true; - } - - return changed; - } - - private static int search(String[] values, String value) { - for (int i = 0; i < values.length; i++) { - if (values[i].equals(value)) return i; - } - return -1; - } - - @Override - void dumpGfxInfo(PrintWriter pw) { - if (mProfileEnabled) { - pw.printf("\n\tDraw\tProcess\tExecute\n"); - - mProfileLock.lock(); - try { - for (int i = 0; i < mProfileData.length; i += PROFILE_FRAME_DATA_COUNT) { - if (mProfileData[i] < 0) { - break; - } - pw.printf("\t%3.2f\t%3.2f\t%3.2f\n", mProfileData[i], mProfileData[i + 1], - mProfileData[i + 2]); - mProfileData[i] = mProfileData[i + 1] = mProfileData[i + 2] = -1; - } - mProfileCurrentFrame = mProfileData.length; - } finally { - mProfileLock.unlock(); - } - } - } - - @Override - long getFrameCount() { - return mFrameCount; - } - - /** - * Indicates whether this renderer instance can track and update dirty regions. - */ - boolean hasDirtyRegions() { - return mDirtyRegionsEnabled; - } - - /** - * Checks for OpenGL errors. If an error has occured, {@link #destroy(boolean)} - * is invoked and the requested flag is turned off. The error code is - * also logged as a warning. - */ - void checkEglErrors() { - if (isEnabled()) { - checkEglErrorsForced(); - } - } - - private void checkEglErrorsForced() { - int error = sEgl.eglGetError(); - if (error != EGL_SUCCESS) { - // something bad has happened revert to - // normal rendering. - Log.w(LOG_TAG, "EGL error: " + GLUtils.getEGLErrorString(error)); - fallback(error != EGL11.EGL_CONTEXT_LOST); - } - } - - private void fallback(boolean fallback) { - destroy(true); - if (fallback) { - // we'll try again if it was context lost - setRequested(false); - Log.w(LOG_TAG, "Mountain View, we've had a problem here. " - + "Switching back to software rendering."); - } - } - - @Override - boolean initialize(Surface surface) throws OutOfResourcesException { - if (isRequested() && !isEnabled()) { - boolean contextCreated = initializeEgl(); - mGl = createEglSurface(surface); - mDestroyed = false; - - if (mGl != null) { - int err = sEgl.eglGetError(); - if (err != EGL_SUCCESS) { - destroy(true); - setRequested(false); - } else { - if (mCanvas == null) { - mCanvas = createCanvas(); - mCanvas.setName(mName); - } - setEnabled(true); - - if (contextCreated) { - initAtlas(); - } - } - - return mCanvas != null; - } - } - return false; - } - - @Override - void updateSurface(Surface surface) throws OutOfResourcesException { - if (isRequested() && isEnabled()) { - createEglSurface(surface); - } - } - - abstract HardwareCanvas createCanvas(); - - abstract int[] getConfig(boolean dirtyRegions); - - boolean initializeEgl() { - synchronized (sEglLock) { - if (sEgl == null && sEglConfig == null) { - sEgl = (EGL10) EGLContext.getEGL(); - - // Get to the default display. - sEglDisplay = sEgl.eglGetDisplay(EGL_DEFAULT_DISPLAY); - - if (sEglDisplay == EGL_NO_DISPLAY) { - throw new RuntimeException("eglGetDisplay failed " - + GLUtils.getEGLErrorString(sEgl.eglGetError())); - } - - // We can now initialize EGL for that display - int[] version = new int[2]; - if (!sEgl.eglInitialize(sEglDisplay, version)) { - throw new RuntimeException("eglInitialize failed " + - GLUtils.getEGLErrorString(sEgl.eglGetError())); - } - - checkEglErrorsForced(); - - sEglConfig = loadEglConfig(); - } - } - - ManagedEGLContext managedContext = sEglContextStorage.get(); - mEglContext = managedContext != null ? managedContext.getContext() : null; - mEglThread = Thread.currentThread(); - - if (mEglContext == null) { - mEglContext = createContext(sEgl, sEglDisplay, sEglConfig); - sEglContextStorage.set(createManagedContext(mEglContext)); - return true; - } - - return false; - } - - private EGLConfig loadEglConfig() { - EGLConfig eglConfig = chooseEglConfig(); - if (eglConfig == null) { - // We tried to use EGL_SWAP_BEHAVIOR_PRESERVED_BIT, try again without - if (sDirtyRegions) { - sDirtyRegions = false; - eglConfig = chooseEglConfig(); - if (eglConfig == null) { - throw new RuntimeException("eglConfig not initialized"); - } - } else { - throw new RuntimeException("eglConfig not initialized"); - } - } - return eglConfig; - } - - abstract ManagedEGLContext createManagedContext(EGLContext eglContext); - - private EGLConfig chooseEglConfig() { - EGLConfig[] configs = new EGLConfig[1]; - int[] configsCount = new int[1]; - int[] configSpec = getConfig(sDirtyRegions); - - // Debug - final String debug = SystemProperties.get(PRINT_CONFIG_PROPERTY, ""); - if ("all".equalsIgnoreCase(debug)) { - sEgl.eglChooseConfig(sEglDisplay, configSpec, null, 0, configsCount); - - EGLConfig[] debugConfigs = new EGLConfig[configsCount[0]]; - sEgl.eglChooseConfig(sEglDisplay, configSpec, debugConfigs, - configsCount[0], configsCount); - - for (EGLConfig config : debugConfigs) { - printConfig(config); - } - } - - if (!sEgl.eglChooseConfig(sEglDisplay, configSpec, configs, 1, configsCount)) { - throw new IllegalArgumentException("eglChooseConfig failed " + - GLUtils.getEGLErrorString(sEgl.eglGetError())); - } else if (configsCount[0] > 0) { - if ("choice".equalsIgnoreCase(debug)) { - printConfig(configs[0]); - } - return configs[0]; - } - - return null; - } - - private static void printConfig(EGLConfig config) { - int[] value = new int[1]; - - Log.d(LOG_TAG, "EGL configuration " + config + ":"); - - sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_RED_SIZE, value); - Log.d(LOG_TAG, " RED_SIZE = " + value[0]); - - sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_GREEN_SIZE, value); - Log.d(LOG_TAG, " GREEN_SIZE = " + value[0]); - - sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_BLUE_SIZE, value); - Log.d(LOG_TAG, " BLUE_SIZE = " + value[0]); - - sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_ALPHA_SIZE, value); - Log.d(LOG_TAG, " ALPHA_SIZE = " + value[0]); - - sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_DEPTH_SIZE, value); - Log.d(LOG_TAG, " DEPTH_SIZE = " + value[0]); - - sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_STENCIL_SIZE, value); - Log.d(LOG_TAG, " STENCIL_SIZE = " + value[0]); - - sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_SAMPLE_BUFFERS, value); - Log.d(LOG_TAG, " SAMPLE_BUFFERS = " + value[0]); - - sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_SAMPLES, value); - Log.d(LOG_TAG, " SAMPLES = " + value[0]); - - sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_SURFACE_TYPE, value); - Log.d(LOG_TAG, " SURFACE_TYPE = 0x" + Integer.toHexString(value[0])); - - sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_CONFIG_CAVEAT, value); - Log.d(LOG_TAG, " CONFIG_CAVEAT = 0x" + Integer.toHexString(value[0])); - } - - GL createEglSurface(Surface surface) throws OutOfResourcesException { - // Check preconditions. - if (sEgl == null) { - throw new RuntimeException("egl not initialized"); - } - if (sEglDisplay == null) { - throw new RuntimeException("eglDisplay not initialized"); - } - if (sEglConfig == null) { - throw new RuntimeException("eglConfig not initialized"); - } - if (Thread.currentThread() != mEglThread) { - throw new IllegalStateException("HardwareRenderer cannot be used " - + "from multiple threads"); - } - - // In case we need to destroy an existing surface - destroySurface(); - - // Create an EGL surface we can render into. - if (!createSurface(surface)) { - return null; - } - - initCaches(); - - return mEglContext.getGL(); - } - - private void enableDirtyRegions() { - // If mDirtyRegions is set, this means we have an EGL configuration - // with EGL_SWAP_BEHAVIOR_PRESERVED_BIT set - if (sDirtyRegions) { - if (!(mDirtyRegionsEnabled = preserveBackBuffer())) { - Log.w(LOG_TAG, "Backbuffer cannot be preserved"); - } - } else if (sDirtyRegionsRequested) { - // If mDirtyRegions is not set, our EGL configuration does not - // have EGL_SWAP_BEHAVIOR_PRESERVED_BIT; however, the default - // swap behavior might be EGL_BUFFER_PRESERVED, which means we - // want to set mDirtyRegions. We try to do this only if dirty - // regions were initially requested as part of the device - // configuration (see RENDER_DIRTY_REGIONS) - mDirtyRegionsEnabled = isBackBufferPreserved(); - } - } - - abstract void initCaches(); - abstract void initAtlas(); - - EGLContext createContext(EGL10 egl, EGLDisplay eglDisplay, EGLConfig eglConfig) { - int[] attribs = { EGL14.EGL_CONTEXT_CLIENT_VERSION, mGlVersion, EGL_NONE }; - - EGLContext context = egl.eglCreateContext(eglDisplay, eglConfig, EGL_NO_CONTEXT, - mGlVersion != 0 ? attribs : null); - if (context == null || context == EGL_NO_CONTEXT) { - //noinspection ConstantConditions - throw new IllegalStateException( - "Could not create an EGL context. eglCreateContext failed with error: " + - GLUtils.getEGLErrorString(sEgl.eglGetError())); - } - - return context; - } - - @Override - void destroy(boolean full) { - if (full && mCanvas != null) { - mCanvas = null; - } - - if (!isEnabled() || mDestroyed) { - setEnabled(false); - return; - } - - destroySurface(); - setEnabled(false); - - mDestroyed = true; - mGl = null; - } - - void destroySurface() { - if (mEglSurface != null && mEglSurface != EGL_NO_SURFACE) { - if (mEglSurface.equals(sEgl.eglGetCurrentSurface(EGL_DRAW))) { - sEgl.eglMakeCurrent(sEglDisplay, - EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); - } - sEgl.eglDestroySurface(sEglDisplay, mEglSurface); - mEglSurface = null; - } - } - - @Override - void invalidate(Surface surface) { - // Cancels any existing buffer to ensure we'll get a buffer - // of the right size before we call eglSwapBuffers - sEgl.eglMakeCurrent(sEglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); - - if (mEglSurface != null && mEglSurface != EGL_NO_SURFACE) { - sEgl.eglDestroySurface(sEglDisplay, mEglSurface); - mEglSurface = null; - setEnabled(false); - } - - if (surface.isValid()) { - if (!createSurface(surface)) { - return; - } - - mUpdateDirtyRegions = true; - - if (mCanvas != null) { - setEnabled(true); - } - } - } - - private boolean createSurface(Surface surface) { - mEglSurface = sEgl.eglCreateWindowSurface(sEglDisplay, sEglConfig, surface, null); - - if (mEglSurface == null || mEglSurface == EGL_NO_SURFACE) { - int error = sEgl.eglGetError(); - if (error == EGL_BAD_NATIVE_WINDOW) { - Log.e(LOG_TAG, "createWindowSurface returned EGL_BAD_NATIVE_WINDOW."); - return false; - } - throw new RuntimeException("createWindowSurface failed " - + GLUtils.getEGLErrorString(error)); - } - - if (!sEgl.eglMakeCurrent(sEglDisplay, mEglSurface, mEglSurface, mEglContext)) { - throw new IllegalStateException("eglMakeCurrent failed " + - GLUtils.getEGLErrorString(sEgl.eglGetError())); - } - - enableDirtyRegions(); - - return true; - } - - @Override - boolean validate() { - return checkRenderContext() != SURFACE_STATE_ERROR; - } - - @Override - void setup(int width, int height) { - if (validate()) { - mCanvas.setViewport(width, height); - mWidth = width; - mHeight = height; - } - } - - @Override - int getWidth() { - return mWidth; - } - - @Override - int getHeight() { - return mHeight; - } - - @Override - HardwareCanvas getCanvas() { - return mCanvas; - } - - @Override - void setName(String name) { - mName = name; - } - - boolean canDraw() { - return mGl != null && mCanvas != null; - } - - int onPreDraw(Rect dirty) { - return DisplayList.STATUS_DONE; - } - - void onPostDraw() { - } - - class FunctorsRunnable implements Runnable { - View.AttachInfo attachInfo; - - @Override - public void run() { - final HardwareRenderer renderer = attachInfo.mHardwareRenderer; - if (renderer == null || !renderer.isEnabled() || renderer != GlRenderer.this) { - return; - } - - if (checkRenderContext() != SURFACE_STATE_ERROR) { - int status = mCanvas.invokeFunctors(mRedrawClip); - handleFunctorStatus(attachInfo, status); - } - } - } - - @Override - void draw(View view, View.AttachInfo attachInfo, HardwareDrawCallbacks callbacks, - Rect dirty) { - if (canDraw()) { - if (!hasDirtyRegions()) { - dirty = null; - } - attachInfo.mIgnoreDirtyState = true; - attachInfo.mDrawingTime = SystemClock.uptimeMillis(); - - view.mPrivateFlags |= View.PFLAG_DRAWN; - - // We are already on the correct thread - final int surfaceState = checkRenderContextUnsafe(); - if (surfaceState != SURFACE_STATE_ERROR) { - HardwareCanvas canvas = mCanvas; - attachInfo.mHardwareCanvas = canvas; - - if (mProfileEnabled) { - mProfileLock.lock(); - } - - dirty = beginFrame(canvas, dirty, surfaceState); - - DisplayList displayList = buildDisplayList(view, canvas); - - // buildDisplayList() calls into user code which can cause - // an eglMakeCurrent to happen with a different surface/context. - // We must therefore check again here. - if (checkRenderContextUnsafe() == SURFACE_STATE_ERROR) { - return; - } - - int saveCount = 0; - int status = DisplayList.STATUS_DONE; - - long start = getSystemTime(); - try { - status = prepareFrame(dirty); - - saveCount = canvas.save(); - callbacks.onHardwarePreDraw(canvas); - - if (displayList != null) { - status |= drawDisplayList(attachInfo, canvas, displayList, status); - } else { - // Shouldn't reach here - view.draw(canvas); - } - } catch (Exception e) { - Log.e(LOG_TAG, "An error has occurred while drawing:", e); - } finally { - callbacks.onHardwarePostDraw(canvas); - canvas.restoreToCount(saveCount); - view.mRecreateDisplayList = false; - - mDrawDelta = getSystemTime() - start; - - if (mDrawDelta > 0) { - mFrameCount++; - - debugOverdraw(attachInfo, dirty, canvas, displayList); - debugDirtyRegions(dirty, canvas); - drawProfileData(attachInfo); - } - } - - onPostDraw(); - - swapBuffers(status); - - if (mProfileEnabled) { - mProfileLock.unlock(); - } - - attachInfo.mIgnoreDirtyState = false; - } - } - } - - abstract void countOverdraw(HardwareCanvas canvas); - abstract float getOverdraw(HardwareCanvas canvas); - - private void debugOverdraw(View.AttachInfo attachInfo, Rect dirty, - HardwareCanvas canvas, DisplayList displayList) { - - if (mDebugOverdraw == OVERDRAW_TYPE_COUNT) { - if (mDebugOverdrawLayer == null) { - mDebugOverdrawLayer = createHardwareLayer(mWidth, mHeight, true); - } else if (mDebugOverdrawLayer.getWidth() != mWidth || - mDebugOverdrawLayer.getHeight() != mHeight) { - mDebugOverdrawLayer.resize(mWidth, mHeight); - } - - if (!mDebugOverdrawLayer.isValid()) { - mDebugOverdraw = -1; - return; - } - - HardwareCanvas layerCanvas = mDebugOverdrawLayer.start(canvas, dirty); - countOverdraw(layerCanvas); - final int restoreCount = layerCanvas.save(); - layerCanvas.drawDisplayList(displayList, null, DisplayList.FLAG_CLIP_CHILDREN); - layerCanvas.restoreToCount(restoreCount); - mDebugOverdrawLayer.end(canvas); - - float overdraw = getOverdraw(layerCanvas); - DisplayMetrics metrics = attachInfo.mRootView.getResources().getDisplayMetrics(); - - drawOverdrawCounter(canvas, overdraw, metrics.density); - } - } - - private void drawOverdrawCounter(HardwareCanvas canvas, float overdraw, float density) { - final String text = String.format("%.2fx", overdraw); - final Paint paint = setupPaint(density); - // HSBtoColor will clamp the values in the 0..1 range - paint.setColor(Color.HSBtoColor(0.28f - 0.28f * overdraw / 3.5f, 0.8f, 1.0f)); - - canvas.drawText(text, density * 4.0f, mHeight - paint.getFontMetrics().bottom, paint); - } - - private Paint setupPaint(float density) { - if (mDebugOverdrawPaint == null) { - mDebugOverdrawPaint = new Paint(); - mDebugOverdrawPaint.setAntiAlias(true); - mDebugOverdrawPaint.setShadowLayer(density * 3.0f, 0.0f, 0.0f, 0xff000000); - mDebugOverdrawPaint.setTextSize(density * 20.0f); - } - return mDebugOverdrawPaint; - } - - private DisplayList buildDisplayList(View view, HardwareCanvas canvas) { - if (mDrawDelta <= 0) { - return view.mDisplayList; - } - - view.mRecreateDisplayList = (view.mPrivateFlags & View.PFLAG_INVALIDATED) - == View.PFLAG_INVALIDATED; - view.mPrivateFlags &= ~View.PFLAG_INVALIDATED; - - long buildDisplayListStartTime = startBuildDisplayListProfiling(); - canvas.clearLayerUpdates(); - - Trace.traceBegin(Trace.TRACE_TAG_VIEW, "getDisplayList"); - DisplayList displayList = view.getDisplayList(); - Trace.traceEnd(Trace.TRACE_TAG_VIEW); - - endBuildDisplayListProfiling(buildDisplayListStartTime); - - return displayList; - } - - abstract void drawProfileData(View.AttachInfo attachInfo); - - private Rect beginFrame(HardwareCanvas canvas, Rect dirty, int surfaceState) { - // We had to change the current surface and/or context, redraw everything - if (surfaceState == SURFACE_STATE_UPDATED) { - dirty = null; - beginFrame(null); - } else { - int[] size = mSurfaceSize; - beginFrame(size); - - if (size[1] != mHeight || size[0] != mWidth) { - mWidth = size[0]; - mHeight = size[1]; - - canvas.setViewport(mWidth, mHeight); - - dirty = null; - } - } - - if (mDebugDataProvider != null) dirty = null; - - return dirty; - } - - private long startBuildDisplayListProfiling() { - if (mProfileEnabled) { - mProfileCurrentFrame += PROFILE_FRAME_DATA_COUNT; - if (mProfileCurrentFrame >= mProfileData.length) { - mProfileCurrentFrame = 0; - } - - return System.nanoTime(); - } - return 0; - } - - private void endBuildDisplayListProfiling(long getDisplayListStartTime) { - if (mProfileEnabled) { - long now = System.nanoTime(); - float total = (now - getDisplayListStartTime) * 0.000001f; - //noinspection PointlessArithmeticExpression - mProfileData[mProfileCurrentFrame] = total; - } - } - - private int prepareFrame(Rect dirty) { - int status; - Trace.traceBegin(Trace.TRACE_TAG_VIEW, "prepareFrame"); - try { - status = onPreDraw(dirty); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIEW); - } - return status; - } - - private int drawDisplayList(View.AttachInfo attachInfo, HardwareCanvas canvas, - DisplayList displayList, int status) { - - long drawDisplayListStartTime = 0; - if (mProfileEnabled) { - drawDisplayListStartTime = System.nanoTime(); - } - - Trace.traceBegin(Trace.TRACE_TAG_VIEW, "drawDisplayList"); - try { - status |= canvas.drawDisplayList(displayList, mRedrawClip, - DisplayList.FLAG_CLIP_CHILDREN); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIEW); - } - - if (mProfileEnabled) { - long now = System.nanoTime(); - float total = (now - drawDisplayListStartTime) * 0.000001f; - mProfileData[mProfileCurrentFrame + 1] = total; - } - - handleFunctorStatus(attachInfo, status); - return status; - } - - private void swapBuffers(int status) { - if ((status & DisplayList.STATUS_DREW) == DisplayList.STATUS_DREW) { - long eglSwapBuffersStartTime = 0; - if (mProfileEnabled) { - eglSwapBuffersStartTime = System.nanoTime(); - } - - sEgl.eglSwapBuffers(sEglDisplay, mEglSurface); - - if (mProfileEnabled) { - long now = System.nanoTime(); - float total = (now - eglSwapBuffersStartTime) * 0.000001f; - mProfileData[mProfileCurrentFrame + 2] = total; - } - - checkEglErrors(); - } - } - - private void debugDirtyRegions(Rect dirty, HardwareCanvas canvas) { - if (mDebugDirtyRegions) { - if (mDebugPaint == null) { - mDebugPaint = new Paint(); - mDebugPaint.setColor(0x7fff0000); - } - - if (dirty != null && (mFrameCount & 1) == 0) { - canvas.drawRect(dirty, mDebugPaint); - } - } - } - - private void handleFunctorStatus(View.AttachInfo attachInfo, int status) { - // If the draw flag is set, functors will be invoked while executing - // the tree of display lists - if ((status & DisplayList.STATUS_DRAW) != 0) { - if (mRedrawClip.isEmpty()) { - attachInfo.mViewRootImpl.invalidate(); - } else { - attachInfo.mViewRootImpl.invalidateChildInParent(null, mRedrawClip); - mRedrawClip.setEmpty(); - } - } - - if ((status & DisplayList.STATUS_INVOKE) != 0 || - attachInfo.mHandler.hasCallbacks(mFunctorsRunnable)) { - attachInfo.mHandler.removeCallbacks(mFunctorsRunnable); - mFunctorsRunnable.attachInfo = attachInfo; - attachInfo.mHandler.postDelayed(mFunctorsRunnable, FUNCTOR_PROCESS_DELAY); - } - } - - @Override - void detachFunctor(int functor) { - if (mCanvas != null) { - mCanvas.detachFunctor(functor); - } - } - - @Override - boolean attachFunctor(View.AttachInfo attachInfo, int functor) { - if (mCanvas != null) { - mCanvas.attachFunctor(functor); - mFunctorsRunnable.attachInfo = attachInfo; - attachInfo.mHandler.removeCallbacks(mFunctorsRunnable); - attachInfo.mHandler.postDelayed(mFunctorsRunnable, 0); - return true; - } - return false; - } - - /** - * Ensures the current EGL context and surface are the ones we expect. - * This method throws an IllegalStateException if invoked from a thread - * that did not initialize EGL. - * - * @return {@link #SURFACE_STATE_ERROR} if the correct EGL context cannot be made current, - * {@link #SURFACE_STATE_UPDATED} if the EGL context was changed or - * {@link #SURFACE_STATE_SUCCESS} if the EGL context was the correct one - * - * @see #checkRenderContextUnsafe() - */ - int checkRenderContext() { - if (mEglThread != Thread.currentThread()) { - throw new IllegalStateException("Hardware acceleration can only be used with a " + - "single UI thread.\nOriginal thread: " + mEglThread + "\n" + - "Current thread: " + Thread.currentThread()); - } - - return checkRenderContextUnsafe(); - } - - /** - * Ensures the current EGL context and surface are the ones we expect. - * This method does not check the current thread. - * - * @return {@link #SURFACE_STATE_ERROR} if the correct EGL context cannot be made current, - * {@link #SURFACE_STATE_UPDATED} if the EGL context was changed or - * {@link #SURFACE_STATE_SUCCESS} if the EGL context was the correct one - * - * @see #checkRenderContext() - */ - private int checkRenderContextUnsafe() { - if (!mEglSurface.equals(sEgl.eglGetCurrentSurface(EGL_DRAW)) || - !mEglContext.equals(sEgl.eglGetCurrentContext())) { - if (!sEgl.eglMakeCurrent(sEglDisplay, mEglSurface, mEglSurface, mEglContext)) { - Log.e(LOG_TAG, "eglMakeCurrent failed " + - GLUtils.getEGLErrorString(sEgl.eglGetError())); - fallback(true); - return SURFACE_STATE_ERROR; - } else { - if (mUpdateDirtyRegions) { - enableDirtyRegions(); - mUpdateDirtyRegions = false; - } - return SURFACE_STATE_UPDATED; - } - } - return SURFACE_STATE_SUCCESS; - } - - private static int dpToPx(int dp, float density) { - return (int) (dp * density + 0.5f); - } - - class DrawPerformanceDataProvider extends GraphDataProvider { - private final int mGraphType; - - private int mVerticalUnit; - private int mHorizontalUnit; - private int mHorizontalMargin; - private int mThresholdStroke; - - DrawPerformanceDataProvider(int graphType) { - mGraphType = graphType; - } - - @Override - void prepare(DisplayMetrics metrics) { - final float density = metrics.density; - - mVerticalUnit = dpToPx(PROFILE_DRAW_DP_PER_MS, density); - mHorizontalUnit = dpToPx(PROFILE_DRAW_WIDTH, density); - mHorizontalMargin = dpToPx(PROFILE_DRAW_MARGIN, density); - mThresholdStroke = dpToPx(PROFILE_DRAW_THRESHOLD_STROKE_WIDTH, density); - } - - @Override - int getGraphType() { - return mGraphType; - } - - @Override - int getVerticalUnitSize() { - return mVerticalUnit; - } - - @Override - int getHorizontalUnitSize() { - return mHorizontalUnit; - } - - @Override - int getHorizontaUnitMargin() { - return mHorizontalMargin; - } - - @Override - float[] getData() { - return mProfileData; - } - - @Override - float getThreshold() { - return 16; - } - - @Override - int getFrameCount() { - return mProfileData.length / PROFILE_FRAME_DATA_COUNT; - } - - @Override - int getElementCount() { - return PROFILE_FRAME_DATA_COUNT; - } - - @Override - int getCurrentFrame() { - return mProfileCurrentFrame / PROFILE_FRAME_DATA_COUNT; - } - - @Override - void setupGraphPaint(Paint paint, int elementIndex) { - paint.setColor(PROFILE_DRAW_COLORS[elementIndex]); - if (mGraphType == GRAPH_TYPE_LINES) paint.setStrokeWidth(mThresholdStroke); - } - - @Override - void setupThresholdPaint(Paint paint) { - paint.setColor(PROFILE_DRAW_THRESHOLD_COLOR); - paint.setStrokeWidth(mThresholdStroke); - } - - @Override - void setupCurrentFramePaint(Paint paint) { - paint.setColor(PROFILE_DRAW_CURRENT_FRAME_COLOR); - if (mGraphType == GRAPH_TYPE_LINES) paint.setStrokeWidth(mThresholdStroke); - } - } - } - - /** - * Hardware renderer using OpenGL ES 2.0. - */ - static class Gl20Renderer extends GlRenderer { - private GLES20Canvas mGlCanvas; - - private DisplayMetrics mDisplayMetrics; - - private static EGLSurface sPbuffer; - private static final Object[] sPbufferLock = new Object[0]; - - static class Gl20RendererEglContext extends ManagedEGLContext { - final Handler mHandler = new Handler(); - - public Gl20RendererEglContext(EGLContext context) { - super(context); - } - - @Override - public void onTerminate(final EGLContext eglContext) { - // Make sure we do this on the correct thread. - if (mHandler.getLooper() != Looper.myLooper()) { - mHandler.post(new Runnable() { - @Override - public void run() { - onTerminate(eglContext); - } - }); - return; - } - - synchronized (sEglLock) { - if (sEgl == null) return; - - if (EGLImpl.getInitCount(sEglDisplay) == 1) { - usePbufferSurface(eglContext); - GLES20Canvas.terminateCaches(); - - sEgl.eglDestroyContext(sEglDisplay, eglContext); - sEglContextStorage.set(null); - sEglContextStorage.remove(); - - sEgl.eglDestroySurface(sEglDisplay, sPbuffer); - sEgl.eglMakeCurrent(sEglDisplay, EGL_NO_SURFACE, - EGL_NO_SURFACE, EGL_NO_CONTEXT); - - sEgl.eglReleaseThread(); - sEgl.eglTerminate(sEglDisplay); - - sEgl = null; - sEglDisplay = null; - sEglConfig = null; - sPbuffer = null; - } - } - } - } - - Gl20Renderer(boolean translucent) { - super(2, translucent); - } - - @Override - HardwareCanvas createCanvas() { - return mGlCanvas = new GLES20Canvas(mTranslucent); - } - - @Override - ManagedEGLContext createManagedContext(EGLContext eglContext) { - return new Gl20Renderer.Gl20RendererEglContext(mEglContext); - } - - @Override - int[] getConfig(boolean dirtyRegions) { - //noinspection PointlessBooleanExpression,ConstantConditions - final int stencilSize = GLES20Canvas.getStencilSize(); - final int swapBehavior = dirtyRegions ? EGL14.EGL_SWAP_BEHAVIOR_PRESERVED_BIT : 0; - - return new int[] { - EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, - EGL_RED_SIZE, 8, - EGL_GREEN_SIZE, 8, - EGL_BLUE_SIZE, 8, - EGL_ALPHA_SIZE, 8, - EGL_DEPTH_SIZE, 0, - EGL_CONFIG_CAVEAT, EGL_NONE, - EGL_STENCIL_SIZE, stencilSize, - EGL_SURFACE_TYPE, EGL_WINDOW_BIT | swapBehavior, - EGL_NONE - }; - } - - @Override - void initCaches() { - if (GLES20Canvas.initCaches()) { - // Caches were (re)initialized, rebind atlas - initAtlas(); - } - } - - @Override - void initAtlas() { - IBinder binder = ServiceManager.getService("assetatlas"); - if (binder == null) return; - - IAssetAtlas atlas = IAssetAtlas.Stub.asInterface(binder); - try { - if (atlas.isCompatible(android.os.Process.myPpid())) { - GraphicBuffer buffer = atlas.getBuffer(); - if (buffer != null) { - int[] map = atlas.getMap(); - if (map != null) { - GLES20Canvas.initAtlas(buffer, map); - } - // If IAssetAtlas is not the same class as the IBinder - // we are using a remote service and we can safely - // destroy the graphic buffer - if (atlas.getClass() != binder.getClass()) { - buffer.destroy(); - } - } - } - } catch (RemoteException e) { - Log.w(LOG_TAG, "Could not acquire atlas", e); - } - } - - @Override - boolean canDraw() { - return super.canDraw() && mGlCanvas != null; - } - - @Override - int onPreDraw(Rect dirty) { - return mGlCanvas.onPreDraw(dirty); - } - - @Override - void onPostDraw() { - mGlCanvas.onPostDraw(); - } - - @Override - void drawProfileData(View.AttachInfo attachInfo) { - if (mDebugDataProvider != null) { - final GraphDataProvider provider = mDebugDataProvider; - initProfileDrawData(attachInfo, provider); - - final int height = provider.getVerticalUnitSize(); - final int margin = provider.getHorizontaUnitMargin(); - final int width = provider.getHorizontalUnitSize(); - - int x = 0; - int count = 0; - int current = 0; - - final float[] data = provider.getData(); - final int elementCount = provider.getElementCount(); - final int graphType = provider.getGraphType(); - - int totalCount = provider.getFrameCount() * elementCount; - if (graphType == GraphDataProvider.GRAPH_TYPE_LINES) { - totalCount -= elementCount; - } - - for (int i = 0; i < totalCount; i += elementCount) { - if (data[i] < 0.0f) break; - - int index = count * 4; - if (i == provider.getCurrentFrame() * elementCount) current = index; - - x += margin; - int x2 = x + width; - - int y2 = mHeight; - int y1 = (int) (y2 - data[i] * height); - - switch (graphType) { - case GraphDataProvider.GRAPH_TYPE_BARS: { - for (int j = 0; j < elementCount; j++) { - //noinspection MismatchedReadAndWriteOfArray - final float[] r = mProfileShapes[j]; - r[index] = x; - r[index + 1] = y1; - r[index + 2] = x2; - r[index + 3] = y2; - - y2 = y1; - if (j < elementCount - 1) { - y1 = (int) (y2 - data[i + j + 1] * height); - } - } - } break; - case GraphDataProvider.GRAPH_TYPE_LINES: { - for (int j = 0; j < elementCount; j++) { - //noinspection MismatchedReadAndWriteOfArray - final float[] r = mProfileShapes[j]; - r[index] = (x + x2) * 0.5f; - r[index + 1] = index == 0 ? y1 : r[index - 1]; - r[index + 2] = r[index] + width; - r[index + 3] = y1; - - y2 = y1; - if (j < elementCount - 1) { - y1 = (int) (y2 - data[i + j + 1] * height); - } - } - } break; - } - - - x += width; - count++; - } - - x += margin; - - drawGraph(graphType, count); - drawCurrentFrame(graphType, current); - drawThreshold(x, height); - } - } - - private void drawGraph(int graphType, int count) { - for (int i = 0; i < mProfileShapes.length; i++) { - mDebugDataProvider.setupGraphPaint(mProfilePaint, i); - switch (graphType) { - case GraphDataProvider.GRAPH_TYPE_BARS: - mGlCanvas.drawRects(mProfileShapes[i], count * 4, mProfilePaint); - break; - case GraphDataProvider.GRAPH_TYPE_LINES: - mGlCanvas.drawLines(mProfileShapes[i], 0, count * 4, mProfilePaint); - break; - } - } - } - - private void drawCurrentFrame(int graphType, int index) { - if (index >= 0) { - mDebugDataProvider.setupCurrentFramePaint(mProfilePaint); - switch (graphType) { - case GraphDataProvider.GRAPH_TYPE_BARS: - mGlCanvas.drawRect(mProfileShapes[2][index], mProfileShapes[2][index + 1], - mProfileShapes[2][index + 2], mProfileShapes[0][index + 3], - mProfilePaint); - break; - case GraphDataProvider.GRAPH_TYPE_LINES: - mGlCanvas.drawLine(mProfileShapes[2][index], mProfileShapes[2][index + 1], - mProfileShapes[2][index], mHeight, mProfilePaint); - break; - } - } - } - - private void drawThreshold(int x, int height) { - float threshold = mDebugDataProvider.getThreshold(); - if (threshold > 0.0f) { - mDebugDataProvider.setupThresholdPaint(mProfilePaint); - int y = (int) (mHeight - threshold * height); - mGlCanvas.drawLine(0.0f, y, x, y, mProfilePaint); - } - } - - private void initProfileDrawData(View.AttachInfo attachInfo, GraphDataProvider provider) { - if (mProfileShapes == null) { - final int elementCount = provider.getElementCount(); - final int frameCount = provider.getFrameCount(); - - mProfileShapes = new float[elementCount][]; - for (int i = 0; i < elementCount; i++) { - mProfileShapes[i] = new float[frameCount * 4]; - } - - mProfilePaint = new Paint(); - } - - mProfilePaint.reset(); - if (provider.getGraphType() == GraphDataProvider.GRAPH_TYPE_LINES) { - mProfilePaint.setAntiAlias(true); - } - - if (mDisplayMetrics == null) { - mDisplayMetrics = new DisplayMetrics(); - } - - attachInfo.mDisplay.getMetrics(mDisplayMetrics); - provider.prepare(mDisplayMetrics); - } - - @Override - void destroy(boolean full) { - try { - super.destroy(full); - } finally { - if (full && mGlCanvas != null) { - mGlCanvas = null; - } - } - } - - @Override - void pushLayerUpdate(HardwareLayer layer) { - mGlCanvas.pushLayerUpdate(layer); - } - - @Override - void cancelLayerUpdate(HardwareLayer layer) { - mGlCanvas.cancelLayerUpdate(layer); - } - - @Override - void flushLayerUpdates() { - mGlCanvas.flushLayerUpdates(); - } - - @Override - public DisplayList createDisplayList(String name) { - return new GLES20DisplayList(name); - } - - @Override - HardwareLayer createHardwareLayer(boolean isOpaque) { - return new GLES20TextureLayer(isOpaque); - } - - @Override - public HardwareLayer createHardwareLayer(int width, int height, boolean isOpaque) { - return new GLES20RenderLayer(width, height, isOpaque); - } - - @Override - void countOverdraw(HardwareCanvas canvas) { - ((GLES20Canvas) canvas).setCountOverdrawEnabled(true); - } - - @Override - float getOverdraw(HardwareCanvas canvas) { - return ((GLES20Canvas) canvas).getOverdraw(); - } - - @Override - public SurfaceTexture createSurfaceTexture(HardwareLayer layer) { - return ((GLES20TextureLayer) layer).getSurfaceTexture(); - } - - @Override - void setSurfaceTexture(HardwareLayer layer, SurfaceTexture surfaceTexture) { - ((GLES20TextureLayer) layer).setSurfaceTexture(surfaceTexture); - } - - @Override - boolean safelyRun(Runnable action) { - boolean needsContext = !isEnabled() || checkRenderContext() == SURFACE_STATE_ERROR; - - if (needsContext) { - Gl20RendererEglContext managedContext = - (Gl20RendererEglContext) sEglContextStorage.get(); - if (managedContext == null) return false; - usePbufferSurface(managedContext.getContext()); - } - - try { - action.run(); - } finally { - if (needsContext) { - sEgl.eglMakeCurrent(sEglDisplay, EGL_NO_SURFACE, - EGL_NO_SURFACE, EGL_NO_CONTEXT); - } - } - - return true; - } - - @Override - void destroyLayers(final View view) { - if (view != null) { - safelyRun(new Runnable() { - @Override - public void run() { - if (mCanvas != null) { - mCanvas.clearLayerUpdates(); - } - destroyHardwareLayer(view); - GLES20Canvas.flushCaches(GLES20Canvas.FLUSH_CACHES_LAYERS); - } - }); - } - } - - private static void destroyHardwareLayer(View view) { - view.destroyLayer(true); - - if (view instanceof ViewGroup) { - ViewGroup group = (ViewGroup) view; - - int count = group.getChildCount(); - for (int i = 0; i < count; i++) { - destroyHardwareLayer(group.getChildAt(i)); - } - } - } - - @Override - void destroyHardwareResources(final View view) { - if (view != null) { - safelyRun(new Runnable() { - @Override - public void run() { - if (mCanvas != null) { - mCanvas.clearLayerUpdates(); - } - destroyResources(view); - GLES20Canvas.flushCaches(GLES20Canvas.FLUSH_CACHES_LAYERS); - } - }); - } - } - - private static void destroyResources(View view) { - view.destroyHardwareResources(); - - if (view instanceof ViewGroup) { - ViewGroup group = (ViewGroup) view; - - int count = group.getChildCount(); - for (int i = 0; i < count; i++) { - destroyResources(group.getChildAt(i)); - } - } - } - - static HardwareRenderer create(boolean translucent) { - if (GLES20Canvas.isAvailable()) { - return new Gl20Renderer(translucent); - } - return null; - } - - static void startTrimMemory(int level) { - if (sEgl == null || sEglConfig == null) return; - - Gl20RendererEglContext managedContext = - (Gl20RendererEglContext) sEglContextStorage.get(); - // We do not have OpenGL objects - if (managedContext == null) { - return; - } else { - usePbufferSurface(managedContext.getContext()); - } - - if (level >= ComponentCallbacks2.TRIM_MEMORY_COMPLETE) { - GLES20Canvas.flushCaches(GLES20Canvas.FLUSH_CACHES_FULL); - } else if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) { - GLES20Canvas.flushCaches(GLES20Canvas.FLUSH_CACHES_MODERATE); - } - } - - static void endTrimMemory() { - if (sEgl != null && sEglDisplay != null) { - sEgl.eglMakeCurrent(sEglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); - } - } - - private static void usePbufferSurface(EGLContext eglContext) { - synchronized (sPbufferLock) { - // Create a temporary 1x1 pbuffer so we have a context - // to clear our OpenGL objects - if (sPbuffer == null) { - sPbuffer = sEgl.eglCreatePbufferSurface(sEglDisplay, sEglConfig, new int[] { - EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE - }); - } - } - sEgl.eglMakeCurrent(sEglDisplay, sPbuffer, sPbuffer, eglContext); - } - } } diff --git a/core/java/android/view/InputQueue.java b/core/java/android/view/InputQueue.java index e3de89d..b552c20 100644 --- a/core/java/android/view/InputQueue.java +++ b/core/java/android/view/InputQueue.java @@ -18,7 +18,6 @@ package android.view; import dalvik.system.CloseGuard; -import android.os.Handler; import android.os.Looper; import android.os.MessageQueue; import android.util.Pools.Pool; diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java index 5a5fc10..30b1e52 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.Slog; import android.util.SparseArray; import android.util.SparseIntArray; import android.view.KeyCharacterMap; diff --git a/core/java/android/view/Surface.java b/core/java/android/view/Surface.java index 1bfda2d..a2775d4 100644 --- a/core/java/android/view/Surface.java +++ b/core/java/android/view/Surface.java @@ -16,6 +16,7 @@ package android.view; +import android.annotation.IntDef; import android.content.res.CompatibilityInfo.Translator; import android.graphics.Canvas; import android.graphics.Matrix; @@ -24,6 +25,10 @@ import android.graphics.SurfaceTexture; import android.os.Parcel; import android.os.Parcelable; import android.util.Log; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + import dalvik.system.CloseGuard; /** @@ -80,6 +85,11 @@ public class Surface implements Parcelable { // non compatibility mode. private Matrix mCompatibleMatrix; + /** @hide */ + @IntDef({ROTATION_0, ROTATION_90, ROTATION_180, ROTATION_270}) + @Retention(RetentionPolicy.SOURCE) + public @interface Rotation {} + /** * Rotation constant: 0 degree rotation (natural orientation) */ diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java index b22d5cf..914a5ca 100644 --- a/core/java/android/view/SurfaceControl.java +++ b/core/java/android/view/SurfaceControl.java @@ -22,7 +22,6 @@ import android.graphics.Rect; import android.graphics.Region; import android.view.Surface; import android.os.IBinder; -import android.os.SystemProperties; import android.util.Log; import android.view.Surface.OutOfResourcesException; @@ -79,9 +78,6 @@ public class SurfaceControl { private final String mName; int mNativeObject; // package visibility only for Surface.java access - private static final boolean HEADLESS = "1".equals( - SystemProperties.get("ro.config.headless", "0")); - /* flags used in constructor (keep in sync with ISurfaceComposerClient.h) */ /** @@ -232,8 +228,6 @@ public class SurfaceControl { new Throwable()); } - checkHeadless(); - mName = name; mNativeObject = nativeCreate(session, name, w, h, format, flags); if (mNativeObject == 0) { @@ -619,10 +613,4 @@ public class SurfaceControl { } nativeScreenshot(display, consumer, width, height, minLayer, maxLayer, allLayers); } - - private static void checkHeadless() { - if (HEADLESS) { - throw new UnsupportedOperationException("Device is headless"); - } - } } diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java index 22d4c9b..65d3f6d 100644 --- a/core/java/android/view/SurfaceView.java +++ b/core/java/android/view/SurfaceView.java @@ -188,8 +188,13 @@ public class SurfaceView extends View { init(); } - public SurfaceView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public SurfaceView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + public SurfaceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); init(); } diff --git a/core/java/android/view/TextureView.java b/core/java/android/view/TextureView.java index 47f7628..bf81811 100644 --- a/core/java/android/view/TextureView.java +++ b/core/java/android/view/TextureView.java @@ -156,14 +156,32 @@ public class TextureView extends View { * * @param context The context to associate this view with. * @param attrs The attributes of the XML tag that is inflating the view. - * @param defStyle The default style to apply to this view. If 0, no style - * will be applied (beyond what is included in the theme). This may - * either be an attribute resource, whose value will be retrieved - * from the current theme, or an explicit style resource. + * @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. */ @SuppressWarnings({"UnusedDeclaration"}) - public TextureView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public TextureView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + /** + * Creates a new TextureView. + * + * @param context The context to associate this view with. + * @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. + */ + @SuppressWarnings({"UnusedDeclaration"}) + public TextureView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); init(); } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index b0bae46..2734abc 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -16,6 +16,9 @@ package android.view; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.ClipData; import android.content.Context; import android.content.res.Configuration; @@ -86,6 +89,8 @@ import com.android.internal.view.menu.MenuBuilder; import com.google.android.collect.Lists; import com.google.android.collect.Maps; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; @@ -729,6 +734,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ private static final int FITS_SYSTEM_WINDOWS = 0x00000002; + /** @hide */ + @IntDef({VISIBLE, INVISIBLE, GONE}) + @Retention(RetentionPolicy.SOURCE) + public @interface Visibility {} + /** * This view is visible. * Use with {@link #setVisibility} and <a href="#attr_android:visibility">{@code @@ -896,6 +906,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ static final int FOCUSABLE_IN_TOUCH_MODE = 0x00040000; + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({DRAWING_CACHE_QUALITY_LOW, DRAWING_CACHE_QUALITY_HIGH, DRAWING_CACHE_QUALITY_AUTO}) + public @interface DrawingCacheQuality {} + /** * <p>Enables low quality mode for the drawing cache.</p> */ @@ -940,6 +955,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ static final int DUPLICATE_PARENT_STATE = 0x00400000; + /** @hide */ + @IntDef({ + SCROLLBARS_INSIDE_OVERLAY, + SCROLLBARS_INSIDE_INSET, + SCROLLBARS_OUTSIDE_OVERLAY, + SCROLLBARS_OUTSIDE_INSET + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ScrollBarStyle {} + /** * The scrollbar style to display the scrollbars inside the content area, * without increasing the padding. The scrollbars will be overlaid with @@ -1020,6 +1045,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ static final int PARENT_SAVE_DISABLED_MASK = 0x20000000; + /** @hide */ + @IntDef(flag = true, + value = { + FOCUSABLES_ALL, + FOCUSABLES_TOUCH_MODE + }) + @Retention(RetentionPolicy.SOURCE) + public @interface FocusableMode {} + /** * View flag indicating whether {@link #addFocusables(ArrayList, int, int)} * should add all focusable Views regardless if they are focusable in touch mode. @@ -1032,6 +1066,28 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public static final int FOCUSABLES_TOUCH_MODE = 0x00000001; + /** @hide */ + @IntDef({ + FOCUS_BACKWARD, + FOCUS_FORWARD, + FOCUS_LEFT, + FOCUS_UP, + FOCUS_RIGHT, + FOCUS_DOWN + }) + @Retention(RetentionPolicy.SOURCE) + public @interface FocusDirection {} + + /** @hide */ + @IntDef({ + FOCUS_LEFT, + FOCUS_UP, + FOCUS_RIGHT, + FOCUS_DOWN + }) + @Retention(RetentionPolicy.SOURCE) + public @interface FocusRealDirection {} // Like @FocusDirection, but without forward/backward + /** * Use with {@link #focusSearch(int)}. Move focus to the previous selectable * item. @@ -1804,6 +1860,25 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ static final int PFLAG2_DRAG_HOVERED = 0x00000002; + /** @hide */ + @IntDef({ + LAYOUT_DIRECTION_LTR, + LAYOUT_DIRECTION_RTL, + LAYOUT_DIRECTION_INHERIT, + LAYOUT_DIRECTION_LOCALE + }) + @Retention(RetentionPolicy.SOURCE) + // Not called LayoutDirection to avoid conflict with android.util.LayoutDirection + public @interface LayoutDir {} + + /** @hide */ + @IntDef({ + LAYOUT_DIRECTION_LTR, + LAYOUT_DIRECTION_RTL + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ResolvedLayoutDir {} + /** * Horizontal layout direction of this view is from Left to Right. * Use with {@link #setLayoutDirection}. @@ -1982,7 +2057,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback, static final int PFLAG2_TEXT_DIRECTION_RESOLVED_DEFAULT = TEXT_DIRECTION_RESOLVED_DEFAULT << PFLAG2_TEXT_DIRECTION_RESOLVED_MASK_SHIFT; - /* + /** @hide */ + @IntDef({ + TEXT_ALIGNMENT_INHERIT, + TEXT_ALIGNMENT_GRAVITY, + TEXT_ALIGNMENT_CENTER, + TEXT_ALIGNMENT_TEXT_START, + TEXT_ALIGNMENT_TEXT_END, + TEXT_ALIGNMENT_VIEW_START, + TEXT_ALIGNMENT_VIEW_END + }) + @Retention(RetentionPolicy.SOURCE) + public @interface TextAlignment {} + + /** * Default text alignment. The text alignment of this View is inherited from its parent. * Use with {@link #setTextAlignment(int)} */ @@ -2664,6 +2752,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; + /** @hide */ + @IntDef(flag = true, + value = { FIND_VIEWS_WITH_TEXT, FIND_VIEWS_WITH_CONTENT_DESCRIPTION }) + @Retention(RetentionPolicy.SOURCE) + public @interface FindViewFlags {} + /** * Find views that render the specified text. * @@ -2685,7 +2779,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * added and it is a responsibility of the client to call the APIs of * the provider to determine whether the virtual tree rooted at this View * contains the text, i.e. getting the list of {@link AccessibilityNodeInfo}s - * represeting the virtual views with this text. + * representing the virtual views with this text. * * @see #findViewsWithText(ArrayList, CharSequence, int) * @@ -3485,27 +3579,64 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * Perform inflation from XML and apply a class-specific base style. This - * constructor of View allows subclasses to use their own base style when - * they are inflating. For example, a Button class's constructor would call - * this version of the super class constructor and supply - * <code>R.attr.buttonStyle</code> for <var>defStyle</var>; this allows - * the theme's button style to modify all of the base view attributes (in - * particular its background) as well as the Button class's attributes. + * Perform inflation from XML and apply a class-specific base style from a + * theme attribute. This constructor of View allows subclasses to use their + * own base style when they are inflating. For example, a Button class's + * constructor would call this version of the super class constructor and + * supply <code>R.attr.buttonStyle</code> for <var>defStyleAttr</var>; this + * allows the theme's button style to modify all of the base view attributes + * (in particular its background) as well as the Button class's attributes. * * @param context The Context the view is running in, through which it can * access 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 to apply to this view. If 0, no - * default style will be applied. + * reference to a style resource that supplies default values for + * the view. Can be 0 to not look for defaults. * @see #View(Context, AttributeSet) */ public View(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + /** + * Perform inflation from XML and apply a class-specific base style from a + * theme attribute or style resource. This constructor of View allows + * subclasses to use their own base style when they are inflating. + * <p> + * When determining the final value of a particular attribute, there are + * four inputs that come into play: + * <ol> + * <li>Any attribute values in the given AttributeSet. + * <li>The style resource specified in the AttributeSet (named "style"). + * <li>The default style specified by <var>defStyleAttr</var>. + * <li>The default style specified by <var>defStyleRes</var>. + * <li>The base values in this theme. + * </ol> + * <p> + * Each of these inputs is considered in-order, with the first listed taking + * precedence over the following ones. In other words, if in the + * AttributeSet you have supplied <code><Button * textColor="#ff000000"></code> + * , then the button's text will <em>always</em> be black, regardless of + * what is specified in any of the styles. + * + * @param context The Context the view is running in, through which it can + * access 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. + * @see #View(Context, AttributeSet, int) + */ + public View(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { this(context); - TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.View, - defStyleAttr, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes); Drawable background = null; @@ -4596,7 +4727,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param previouslyFocusedRect The rectangle of the view that had focus * prior in this View's coordinate system. */ - void handleFocusGainInternal(int direction, Rect previouslyFocusedRect) { + void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) { if (DBG) { System.out.println(this + " requestFocus()"); } @@ -4807,7 +4938,8 @@ 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. */ - protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { + protected void onFocusChanged(boolean gainFocus, @FocusDirection int direction, + @Nullable Rect previouslyFocusedRect) { if (gainFocus) { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); } else { @@ -5667,6 +5799,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @attr ref android.R.styleable#View_drawingCacheQuality */ + @DrawingCacheQuality public int getDrawingCacheQuality() { return mViewFlags & DRAWING_CACHE_QUALITY_MASK; } @@ -5684,7 +5817,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @attr ref android.R.styleable#View_drawingCacheQuality */ - public void setDrawingCacheQuality(int quality) { + public void setDrawingCacheQuality(@DrawingCacheQuality int quality) { setFlags(quality, DRAWING_CACHE_QUALITY_MASK); } @@ -6021,6 +6154,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @ViewDebug.IntToString(from = INVISIBLE, to = "INVISIBLE"), @ViewDebug.IntToString(from = GONE, to = "GONE") }) + @Visibility public int getVisibility() { return mViewFlags & VISIBILITY_MASK; } @@ -6032,7 +6166,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @attr ref android.R.styleable#View_visibility */ @RemotableViewMethod - public void setVisibility(int visibility) { + public void setVisibility(@Visibility int visibility) { setFlags(visibility, VISIBILITY_MASK); if (mBackground != null) mBackground.setVisible(visibility == VISIBLE, false); } @@ -6191,6 +6325,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @ViewDebug.IntToString(from = LAYOUT_DIRECTION_INHERIT, to = "INHERIT"), @ViewDebug.IntToString(from = LAYOUT_DIRECTION_LOCALE, to = "LOCALE") }) + @LayoutDir public int getRawLayoutDirection() { return (mPrivateFlags2 & PFLAG2_LAYOUT_DIRECTION_MASK) >> PFLAG2_LAYOUT_DIRECTION_MASK_SHIFT; } @@ -6213,7 +6348,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @attr ref android.R.styleable#View_layoutDirection */ @RemotableViewMethod - public void setLayoutDirection(int layoutDirection) { + public void setLayoutDirection(@LayoutDir int layoutDirection) { if (getRawLayoutDirection() != layoutDirection) { // Reset the current layout direction and the resolved one mPrivateFlags2 &= ~PFLAG2_LAYOUT_DIRECTION_MASK; @@ -6243,6 +6378,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @ViewDebug.IntToString(from = LAYOUT_DIRECTION_LTR, to = "RESOLVED_DIRECTION_LTR"), @ViewDebug.IntToString(from = LAYOUT_DIRECTION_RTL, to = "RESOLVED_DIRECTION_RTL") }) + @ResolvedLayoutDir public int getLayoutDirection() { final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion; if (targetSdkVersion < JELLY_BEAN_MR1) { @@ -6612,7 +6748,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @return The nearest focusable in the specified direction, or null if none * can be found. */ - public View focusSearch(int direction) { + public View focusSearch(@FocusRealDirection int direction) { if (mParent != null) { return mParent.focusSearch(this, direction); } else { @@ -6631,7 +6767,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT. * @return True if the this view consumed this unhandled move. */ - public boolean dispatchUnhandledMove(View focused, int direction) { + public boolean dispatchUnhandledMove(View focused, @FocusRealDirection int direction) { return false; } @@ -6643,7 +6779,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * or FOCUS_BACKWARD. * @return The user specified next view, or null if there is none. */ - View findUserSetNextFocus(View root, int direction) { + View findUserSetNextFocus(View root, @FocusDirection int direction) { switch (direction) { case FOCUS_LEFT: if (mNextFocusLeftId == View.NO_ID) return null; @@ -6693,7 +6829,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param direction The direction of the focus * @return A list of focusable views */ - public ArrayList<View> getFocusables(int direction) { + public ArrayList<View> getFocusables(@FocusDirection int direction) { ArrayList<View> result = new ArrayList<View>(24); addFocusables(result, direction); return result; @@ -6707,7 +6843,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param views Focusable views found so far * @param direction The direction of the focus */ - public void addFocusables(ArrayList<View> views, int direction) { + public void addFocusables(ArrayList<View> views, @FocusDirection int direction) { addFocusables(views, direction, FOCUSABLES_TOUCH_MODE); } @@ -6727,7 +6863,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #FOCUSABLES_ALL * @see #FOCUSABLES_TOUCH_MODE */ - public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { + public void addFocusables(ArrayList<View> views, @FocusDirection int direction, + @FocusableMode int focusableMode) { if (views == null) { return; } @@ -6756,7 +6893,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #FIND_VIEWS_WITH_CONTENT_DESCRIPTION * @see #setContentDescription(CharSequence) */ - public void findViewsWithText(ArrayList<View> outViews, CharSequence searched, int flags) { + public void findViewsWithText(ArrayList<View> outViews, CharSequence searched, + @FindViewFlags int flags) { if (getAccessibilityNodeProvider() != null) { if ((flags & FIND_VIEWS_WITH_ACCESSIBILITY_NODE_PROVIDERS) != 0) { outViews.add(this); @@ -7247,7 +7385,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * Returns whether the View has registered callbacks wich makes it + * Returns whether the View has registered callbacks which makes it * important for accessibility. * * @return True if the view is actionable for accessibility. @@ -7266,7 +7404,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * notification is at at most once every * {@link ViewConfiguration#getSendRecurringAccessibilityEventsInterval()} * to avoid unnecessary load to the system. Also once a view has a pending - * notifucation this method is a NOP until the notification has been sent. + * notification this method is a NOP until the notification has been sent. * * @hide */ @@ -7946,7 +8084,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param visibility The new visibility of changedView: {@link #VISIBLE}, * {@link #INVISIBLE} or {@link #GONE}. */ - protected void dispatchVisibilityChanged(View changedView, int visibility) { + protected void dispatchVisibilityChanged(@NonNull View changedView, + @Visibility int visibility) { onVisibilityChanged(changedView, visibility); } @@ -7957,7 +8096,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param visibility The new visibility of changedView: {@link #VISIBLE}, * {@link #INVISIBLE} or {@link #GONE}. */ - protected void onVisibilityChanged(View changedView, int visibility) { + protected void onVisibilityChanged(@NonNull View changedView, @Visibility int visibility) { if (visibility == VISIBLE) { if (mAttachInfo != null) { initialAwakenScrollBars(); @@ -7976,7 +8115,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param hint A hint about whether or not this view is displayed: * {@link #VISIBLE} or {@link #INVISIBLE}. */ - public void dispatchDisplayHint(int hint) { + public void dispatchDisplayHint(@Visibility int hint) { onDisplayHint(hint); } @@ -7989,7 +8128,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param hint A hint about whether or not this view is displayed: * {@link #VISIBLE} or {@link #INVISIBLE}. */ - protected void onDisplayHint(int hint) { + protected void onDisplayHint(@Visibility int hint) { } /** @@ -8000,7 +8139,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @see #onWindowVisibilityChanged(int) */ - public void dispatchWindowVisibilityChanged(int visibility) { + public void dispatchWindowVisibilityChanged(@Visibility int visibility) { onWindowVisibilityChanged(visibility); } @@ -8014,7 +8153,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @param visibility The new visibility of the window. */ - protected void onWindowVisibilityChanged(int visibility) { + protected void onWindowVisibilityChanged(@Visibility int visibility) { if (visibility == VISIBLE) { initialAwakenScrollBars(); } @@ -8026,6 +8165,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @return Returns the current visibility of the view's window. */ + @Visibility public int getWindowVisibility() { return mAttachInfo != null ? mAttachInfo.mWindowVisibility : GONE; } @@ -10818,15 +10958,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mPrivateFlags |= PFLAG_DIRTY; final ViewParent p = mParent; final AttachInfo ai = mAttachInfo; - //noinspection PointlessBooleanExpression,ConstantConditions - if (!HardwareRenderer.RENDER_DIRTY_REGIONS) { - if (p != null && ai != null && ai.mHardwareAccelerated) { - // fast-track for GL-enabled applications; just invalidate the whole hierarchy - // with a null dirty rect, which tells the ViewAncestor to redraw everything - p.invalidateChild(this, null); - return; - } - } if (p != null && ai != null) { final int scrollX = mScrollX; final int scrollY = mScrollY; @@ -10861,15 +10992,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mPrivateFlags |= PFLAG_DIRTY; final ViewParent p = mParent; final AttachInfo ai = mAttachInfo; - //noinspection PointlessBooleanExpression,ConstantConditions - if (!HardwareRenderer.RENDER_DIRTY_REGIONS) { - if (p != null && ai != null && ai.mHardwareAccelerated) { - // fast-track for GL-enabled applications; just invalidate the whole hierarchy - // with a null dirty rect, which tells the ViewAncestor to redraw everything - p.invalidateChild(this, null); - return; - } - } if (p != null && ai != null && l < r && t < b) { final int scrollX = mScrollX; final int scrollY = mScrollY; @@ -10917,15 +11039,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } final AttachInfo ai = mAttachInfo; final ViewParent p = mParent; - //noinspection PointlessBooleanExpression,ConstantConditions - if (!HardwareRenderer.RENDER_DIRTY_REGIONS) { - if (p != null && ai != null && ai.mHardwareAccelerated) { - // fast-track for GL-enabled applications; just invalidate the whole hierarchy - // with a null dirty rect, which tells the ViewAncestor to redraw everything - p.invalidateChild(this, null); - return; - } - } if (p != null && ai != null) { final Rect r = ai.mTmpInvalRect; @@ -11212,10 +11325,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, attachInfo.mHandler.removeCallbacks(action); attachInfo.mViewRootImpl.mChoreographer.removeCallbacks( Choreographer.CALLBACK_ANIMATION, action, null); - } else { - // Assume that post will succeed later - ViewRootImpl.getRunQueue().removeCallbacks(action); } + // Assume that post will succeed later + ViewRootImpl.getRunQueue().removeCallbacks(action); } return true; } @@ -11706,7 +11818,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @attr ref android.R.styleable#View_scrollbarStyle */ - public void setScrollBarStyle(int style) { + public void setScrollBarStyle(@ScrollBarStyle int style) { if (style != (mViewFlags & SCROLLBARS_STYLE_MASK)) { mViewFlags = (mViewFlags & ~SCROLLBARS_STYLE_MASK) | (style & SCROLLBARS_STYLE_MASK); computeOpaqueFlags(); @@ -11730,6 +11842,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @ViewDebug.IntToString(from = SCROLLBARS_OUTSIDE_OVERLAY, to = "OUTSIDE_OVERLAY"), @ViewDebug.IntToString(from = SCROLLBARS_OUTSIDE_INSET, to = "OUTSIDE_INSET") }) + @ScrollBarStyle public int getScrollBarStyle() { return mViewFlags & SCROLLBARS_STYLE_MASK; } @@ -12231,7 +12344,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #LAYOUT_DIRECTION_LTR * @see #LAYOUT_DIRECTION_RTL */ - public void onRtlPropertiesChanged(int layoutDirection) { + public void onRtlPropertiesChanged(@ResolvedLayoutDir int layoutDirection) { } /** @@ -13260,20 +13373,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * @return The {@link HardwareRenderer} associated with that view or null if - * hardware rendering is not supported or this view is not attached - * to a window. - * - * @hide - */ - public HardwareRenderer getHardwareRenderer() { - if (mAttachInfo != null) { - return mAttachInfo.mHardwareRenderer; - } - return null; - } - - /** * Returns a DisplayList. If the incoming displayList is null, one will be created. * Otherwise, the same display list will be returned (after having been rendered into * along the way, depending on the invalidation state of the view). @@ -13308,7 +13407,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mRecreateDisplayList = true; } if (displayList == null) { - displayList = mAttachInfo.mHardwareRenderer.createDisplayList(getClass().getName()); + displayList = DisplayList.create(getClass().getName()); // If we're creating a new display list, make sure our parent gets invalidated // since they will need to recreate their display list to account for this // new child display list. @@ -15003,9 +15102,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (mAttachInfo != null) { mAttachInfo.mViewRootImpl.mChoreographer.removeCallbacks( Choreographer.CALLBACK_ANIMATION, what, who); - } else { - ViewRootImpl.getRunQueue().removeCallbacks(what); } + ViewRootImpl.getRunQueue().removeCallbacks(what); } } @@ -15068,7 +15166,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @hide */ - public void onResolveDrawables(int layoutDirection) { + public void onResolveDrawables(@ResolvedLayoutDir int layoutDirection) { } /** @@ -17870,6 +17968,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @ViewDebug.IntToString(from = TEXT_ALIGNMENT_VIEW_START, to = "VIEW_START"), @ViewDebug.IntToString(from = TEXT_ALIGNMENT_VIEW_END, to = "VIEW_END") }) + @TextAlignment public int getRawTextAlignment() { return (mPrivateFlags2 & PFLAG2_TEXT_ALIGNMENT_MASK) >> PFLAG2_TEXT_ALIGNMENT_MASK_SHIFT; } @@ -17893,7 +17992,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @attr ref android.R.styleable#View_textAlignment */ - public void setTextAlignment(int textAlignment) { + public void setTextAlignment(@TextAlignment int textAlignment) { if (textAlignment != getRawTextAlignment()) { // Reset the current and resolved text alignment mPrivateFlags2 &= ~PFLAG2_TEXT_ALIGNMENT_MASK; @@ -17934,6 +18033,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @ViewDebug.IntToString(from = TEXT_ALIGNMENT_VIEW_START, to = "VIEW_START"), @ViewDebug.IntToString(from = TEXT_ALIGNMENT_VIEW_END, to = "VIEW_END") }) + @TextAlignment public int getTextAlignment() { return (mPrivateFlags2 & PFLAG2_TEXT_ALIGNMENT_RESOLVED_MASK) >> PFLAG2_TEXT_ALIGNMENT_RESOLVED_MASK_SHIFT; @@ -18746,7 +18846,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, View mRootView; IBinder mPanelParentWindowToken; - Surface mSurface; boolean mHardwareAccelerated; boolean mHardwareAccelerationRequested; diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 9414237..a1b7ef6 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -38,7 +38,6 @@ import android.util.Log; import android.util.Pools.SynchronizedPool; import android.util.SparseArray; import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.Animation; import android.view.animation.AnimationUtils; @@ -456,20 +455,21 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager private int mChildCountWithTransientState = 0; public ViewGroup(Context context) { - super(context); - initViewGroup(); + this(context, null); } public ViewGroup(Context context, AttributeSet attrs) { - super(context, attrs); - initViewGroup(); - initFromAttributes(context, attrs); + this(context, attrs, 0); + } + + public ViewGroup(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); } - public ViewGroup(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public ViewGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); initViewGroup(); - initFromAttributes(context, attrs); + initFromAttributes(context, attrs, defStyleAttr, defStyleRes); } private boolean debugDraw() { @@ -499,9 +499,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager mPersistentDrawingCache = PERSISTENT_SCROLLING_CACHE; } - private void initFromAttributes(Context context, AttributeSet attrs) { - TypedArray a = context.obtainStyledAttributes(attrs, - R.styleable.ViewGroup); + private void initFromAttributes( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ViewGroup); final int N = a.getIndexCount(); for (int i = 0; i < N; i++) { @@ -2510,13 +2510,13 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfoInternal(info); if (mAttachInfo != null) { - ArrayList<View> childrenForAccessibility = mAttachInfo.mTempArrayList; + final ArrayList<View> childrenForAccessibility = mAttachInfo.mTempArrayList; childrenForAccessibility.clear(); addChildrenForAccessibility(childrenForAccessibility); final int childrenForAccessibilityCount = childrenForAccessibility.size(); for (int i = 0; i < childrenForAccessibilityCount; i++) { - View child = childrenForAccessibility.get(i); - info.addChild(child); + final View child = childrenForAccessibility.get(i); + info.addChildUnchecked(child); } childrenForAccessibility.clear(); } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index bc0d7e3..a5f797e 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -108,7 +108,6 @@ public final class ViewRootImpl implements ViewParent, private static final boolean DEBUG_IMF = false || LOCAL_LOGV; private static final boolean DEBUG_CONFIGURATION = false || LOCAL_LOGV; private static final boolean DEBUG_FPS = false; - private static final boolean DEBUG_INPUT_PROCESSING = false || LOCAL_LOGV; /** * Set this system property to true to force the view hierarchy to render @@ -719,7 +718,7 @@ public final class ViewRootImpl implements ViewParent, } final boolean translucent = attrs.format != PixelFormat.OPAQUE; - mAttachInfo.mHardwareRenderer = HardwareRenderer.createGlRenderer(2, translucent); + mAttachInfo.mHardwareRenderer = HardwareRenderer.create(translucent); if (mAttachInfo.mHardwareRenderer != null) { mAttachInfo.mHardwareRenderer.setName(attrs.getTitle().toString()); mAttachInfo.mHardwareAccelerated = @@ -1195,11 +1194,6 @@ public final class ViewRootImpl implements ViewParent, desiredWindowHeight = packageMetrics.heightPixels; } - // For the very first time, tell the view hierarchy that it - // is attached to the window. Note that at this point the surface - // object is not initialized to its backing store, but soon it - // will be (assuming the window is visible). - attachInfo.mSurface = mSurface; // We used to use the following condition to choose 32 bits drawing caches: // PixelFormat.hasAlpha(lp.format) || lp.format == PixelFormat.RGBX_8888 // However, windows are now always 32 bits by default, so choose 32 bits @@ -1548,7 +1542,7 @@ public final class ViewRootImpl implements ViewParent, if (mAttachInfo.mHardwareRenderer != null) { try { hwInitialized = mAttachInfo.mHardwareRenderer.initialize( - mHolder.getSurface()); + mSurface); } catch (OutOfResourcesException e) { handleOutOfResourcesException(e); return; @@ -1575,7 +1569,7 @@ public final class ViewRootImpl implements ViewParent, mSurfaceHolder == null && mAttachInfo.mHardwareRenderer != null) { mFullRedrawNeeded = true; try { - mAttachInfo.mHardwareRenderer.updateSurface(mHolder.getSurface()); + mAttachInfo.mHardwareRenderer.updateSurface(mSurface); } catch (OutOfResourcesException e) { handleOutOfResourcesException(e); return; @@ -1658,7 +1652,7 @@ public final class ViewRootImpl implements ViewParent, mHeight != mAttachInfo.mHardwareRenderer.getHeight()) { mAttachInfo.mHardwareRenderer.setup(mWidth, mHeight); if (!hwInitialized) { - mAttachInfo.mHardwareRenderer.invalidate(mHolder.getSurface()); + mAttachInfo.mHardwareRenderer.invalidate(mSurface); mFullRedrawNeeded = true; } } @@ -2395,7 +2389,7 @@ public final class ViewRootImpl implements ViewParent, try { attachInfo.mHardwareRenderer.initializeIfNeeded(mWidth, mHeight, - mHolder.getSurface()); + mSurface); } catch (OutOfResourcesException e) { handleOutOfResourcesException(e); return; @@ -2530,28 +2524,35 @@ public final class ViewRootImpl implements ViewParent, * @param canvas The canvas on which to draw. */ private void drawAccessibilityFocusedDrawableIfNeeded(Canvas canvas) { - AccessibilityManager manager = AccessibilityManager.getInstance(mView.mContext); + if (!mAttachInfo.mHasWindowFocus) { + return; + } + + final AccessibilityManager manager = AccessibilityManager.getInstance(mView.mContext); if (!manager.isEnabled() || !manager.isTouchExplorationEnabled()) { return; } - if (mAccessibilityFocusedHost == null || mAccessibilityFocusedHost.mAttachInfo == null) { + + final View host = mAccessibilityFocusedHost; + if (host == null || host.mAttachInfo == null) { return; } - Drawable drawable = getAccessibilityFocusedDrawable(); + + final Drawable drawable = getAccessibilityFocusedDrawable(); if (drawable == null) { return; } - AccessibilityNodeProvider provider = - mAccessibilityFocusedHost.getAccessibilityNodeProvider(); - Rect bounds = mView.mAttachInfo.mTmpInvalRect; + + final AccessibilityNodeProvider provider = host.getAccessibilityNodeProvider(); + final Rect bounds = mView.mAttachInfo.mTmpInvalRect; if (provider == null) { - mAccessibilityFocusedHost.getBoundsOnScreen(bounds); - } else { - if (mAccessibilityFocusedVirtualView == null) { - return; - } + host.getBoundsOnScreen(bounds); + } else if (mAccessibilityFocusedVirtualView != null) { mAccessibilityFocusedVirtualView.getBoundsInScreen(bounds); + } else { + return; } + bounds.offset(-mAttachInfo.mWindowLeft, -mAttachInfo.mWindowTop); bounds.intersect(0, 0, mAttachInfo.mViewRootImpl.mWidth, mAttachInfo.mViewRootImpl.mHeight); drawable.setBounds(bounds); @@ -2850,7 +2851,6 @@ public final class ViewRootImpl implements ViewParent, mView.assignParent(null); mView = null; mAttachInfo.mRootView = null; - mAttachInfo.mSurface = null; mSurface.release(); @@ -3110,7 +3110,7 @@ public final class ViewRootImpl implements ViewParent, mFullRedrawNeeded = true; try { mAttachInfo.mHardwareRenderer.initializeIfNeeded( - mWidth, mHeight, mHolder.getSurface()); + mWidth, mHeight, mSurface); } catch (OutOfResourcesException e) { Log.e(TAG, "OutOfResourcesException locking surface", e); try { @@ -3159,8 +3159,6 @@ public final class ViewRootImpl implements ViewParent, mHasHadWindowFocus = true; } - setAccessibilityFocus(null, null); - if (mView != null && mAccessibilityManager.isEnabled()) { if (hasWindowFocus) { mView.sendAccessibilityEvent( @@ -4506,8 +4504,7 @@ public final class ViewRootImpl implements ViewParent, // The active pointer id, or -1 if none. private int mActivePointerId = -1; - // Time and location where tracking started. - private long mStartTime; + // Location where tracking started. private float mStartX; private float mStartY; @@ -4535,9 +4532,6 @@ public final class ViewRootImpl implements ViewParent, private boolean mFlinging; private float mFlingVelocity; - // The last time a confirm key was pressed on the touch nav device - private long mLastConfirmKeyTime = Long.MAX_VALUE; - public SyntheticTouchNavigationHandler() { super(true); } @@ -4604,7 +4598,6 @@ public final class ViewRootImpl implements ViewParent, mActivePointerId = event.getPointerId(0); mVelocityTracker = VelocityTracker.obtain(); mVelocityTracker.addMovement(event); - mStartTime = time; mStartX = event.getX(); mStartY = event.getY(); mLastX = mStartX; @@ -5388,7 +5381,7 @@ public final class ViewRootImpl implements ViewParent, // Hardware rendering if (mAttachInfo.mHardwareRenderer != null) { - if (mAttachInfo.mHardwareRenderer.loadSystemProperties(mHolder.getSurface())) { + if (mAttachInfo.mHardwareRenderer.loadSystemProperties(mSurface)) { invalidate(); } } @@ -6335,68 +6328,6 @@ public final class ViewRootImpl implements ViewParent, } } - private final SurfaceHolder mHolder = new SurfaceHolder() { - // we only need a SurfaceHolder for opengl. it would be nice - // to implement everything else though, especially the callback - // support (opengl doesn't make use of it right now, but eventually - // will). - @Override - public Surface getSurface() { - return mSurface; - } - - @Override - public boolean isCreating() { - return false; - } - - @Override - public void addCallback(Callback callback) { - } - - @Override - public void removeCallback(Callback callback) { - } - - @Override - public void setFixedSize(int width, int height) { - } - - @Override - public void setSizeFromLayout() { - } - - @Override - public void setFormat(int format) { - } - - @Override - public void setType(int type) { - } - - @Override - public void setKeepScreenOn(boolean screenOn) { - } - - @Override - public Canvas lockCanvas() { - return null; - } - - @Override - public Canvas lockCanvas(Rect dirty) { - return null; - } - - @Override - public void unlockCanvasAndPost(Canvas canvas) { - } - @Override - public Rect getSurfaceFrame() { - return null; - } - }; - static RunQueue getRunQueue() { RunQueue rq = sRunQueues.get(); if (rq != null) { diff --git a/core/java/android/view/ViewStub.java b/core/java/android/view/ViewStub.java index a5dc3ae..d68a860 100644 --- a/core/java/android/view/ViewStub.java +++ b/core/java/android/view/ViewStub.java @@ -97,16 +97,21 @@ public final class ViewStub extends View { } @SuppressWarnings({"UnusedDeclaration"}) - public ViewStub(Context context, AttributeSet attrs, int defStyle) { - TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ViewStub, - defStyle, 0); + 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); 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, defStyle, 0); + a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes); mID = a.getResourceId(R.styleable.View_id, NO_ID); a.recycle(); diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java index b3a0699..11d8d36 100644 --- a/core/java/android/view/Window.java +++ b/core/java/android/view/Window.java @@ -16,6 +16,8 @@ package android.view; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; import android.content.res.Configuration; import android.content.res.TypedArray; @@ -89,12 +91,21 @@ public abstract class Window { * If overlay is enabled, the action mode UI will be allowed to cover existing window content. */ public static final int FEATURE_ACTION_MODE_OVERLAY = 10; + /** + * Flag for requesting that window content changes should be represented + * with scenes and transitions. + * + * TODO Add docs + * + * @see #setContentView + */ + public static final int FEATURE_CONTENT_TRANSITIONS = 11; /** * Max value used as a feature ID * @hide */ - public static final int FEATURE_MAX = FEATURE_ACTION_MODE_OVERLAY; + public static final int FEATURE_MAX = FEATURE_CONTENT_TRANSITIONS; /** Flag for setting the progress bar's visibility to VISIBLE */ public static final int PROGRESS_VISIBILITY_ON = -1; @@ -240,6 +251,7 @@ public abstract class Window { * * @see #onPreparePanel */ + @Nullable public View onCreatePanelView(int featureId); /** @@ -368,6 +380,7 @@ public abstract class Window { * @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 */ + @Nullable public ActionMode onWindowStartingActionMode(ActionMode.Callback callback); /** @@ -969,6 +982,7 @@ public abstract class Window { * * @return View The current View with focus or null. */ + @Nullable public abstract View getCurrentFocus(); /** @@ -977,10 +991,12 @@ public abstract class Window { * * @return LayoutInflater The shared LayoutInflater. */ + @NonNull public abstract LayoutInflater getLayoutInflater(); public abstract void setTitle(CharSequence title); + @Deprecated public abstract void setTitleColor(int textColor); public abstract void openPanel(int featureId, KeyEvent event); diff --git a/core/java/android/view/WindowManagerPolicy.java b/core/java/android/view/WindowManagerPolicy.java index c5a1b86..d9e140e 100644 --- a/core/java/android/view/WindowManagerPolicy.java +++ b/core/java/android/view/WindowManagerPolicy.java @@ -16,7 +16,9 @@ package android.view; +import android.annotation.IntDef; import android.content.Context; +import android.content.pm.ActivityInfo; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; import android.graphics.Rect; @@ -27,6 +29,8 @@ import android.os.Looper; import android.view.animation.Animation; import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * This interface supplies all UI-specific behavior of the window manager. An @@ -464,6 +468,11 @@ public interface WindowManagerPolicy { /** Screen turned off because of proximity sensor */ public final int OFF_BECAUSE_OF_PROX_SENSOR = 4; + /** @hide */ + @IntDef({USER_ROTATION_FREE, USER_ROTATION_LOCKED}) + @Retention(RetentionPolicy.SOURCE) + public @interface UserRotationMode {} + /** When not otherwise specified by the activity's screenOrientation, rotation should be * determined by the system (that is, using sensors). */ public final int USER_ROTATION_FREE = 0; @@ -1022,7 +1031,8 @@ public interface WindowManagerPolicy { * @param lastRotation The most recently used rotation. * @return The surface rotation to use. */ - public int rotationForOrientationLw(int orientation, int lastRotation); + public int rotationForOrientationLw(@ActivityInfo.ScreenOrientation int orientation, + int lastRotation); /** * Given an orientation constant and a rotation, returns true if the rotation @@ -1037,7 +1047,8 @@ public interface WindowManagerPolicy { * @param rotation The rotation to check. * @return True if the rotation is compatible with the requested orientation. */ - public boolean rotationHasCompatibleMetricsLw(int orientation, int rotation); + public boolean rotationHasCompatibleMetricsLw(@ActivityInfo.ScreenOrientation int orientation, + int rotation); /** * Called by the window manager when the rotation changes. @@ -1086,7 +1097,7 @@ public interface WindowManagerPolicy { */ public void enableScreenAfterBoot(); - public void setCurrentOrientationLw(int newOrientation); + public void setCurrentOrientationLw(@ActivityInfo.ScreenOrientation int newOrientation); /** * Call from application to perform haptic feedback on its window. @@ -1113,6 +1124,7 @@ public interface WindowManagerPolicy { * @see WindowManagerPolicy#USER_ROTATION_LOCKED * @see WindowManagerPolicy#USER_ROTATION_FREE */ + @UserRotationMode public int getUserRotationMode(); /** @@ -1123,12 +1135,12 @@ public interface WindowManagerPolicy { * @param rotation One of {@link Surface#ROTATION_0}, {@link Surface#ROTATION_90}, * {@link Surface#ROTATION_180}, {@link Surface#ROTATION_270}. */ - public void setUserRotationMode(int mode, int rotation); + public void setUserRotationMode(@UserRotationMode int mode, @Surface.Rotation int rotation); /** * Called when a new system UI visibility is being reported, allowing * the policy to adjust what is actually reported. - * @param visibility The raw visiblity reported by the status bar. + * @param visibility The raw visibility reported by the status bar. * @return The new desired visibility. */ public int adjustSystemUiVisibilityLw(int visibility); @@ -1151,6 +1163,11 @@ public interface WindowManagerPolicy { public void setLastInputMethodWindowLw(WindowState ime, WindowState target); /** + * @return The current height of the input method window. + */ + public int getInputMethodWindowVisibleHeightLw(); + + /** * Called when the current user changes. Guaranteed to be called before the broadcast * of the new user id is made to all listeners. * diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java index f635eee..8b91155 100644 --- a/core/java/android/view/accessibility/AccessibilityEvent.java +++ b/core/java/android/view/accessibility/AccessibilityEvent.java @@ -722,7 +722,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par int mAction; int mContentChangeTypes; - private final ArrayList<AccessibilityRecord> mRecords = new ArrayList<AccessibilityRecord>(); + private ArrayList<AccessibilityRecord> mRecords; /* * Hide constructor from clients. @@ -755,11 +755,13 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par @Override public void setSealed(boolean sealed) { super.setSealed(sealed); - List<AccessibilityRecord> records = mRecords; - final int recordCount = records.size(); - for (int i = 0; i < recordCount; i++) { - AccessibilityRecord record = records.get(i); - record.setSealed(sealed); + final List<AccessibilityRecord> records = mRecords; + if (records != null) { + final int recordCount = records.size(); + for (int i = 0; i < recordCount; i++) { + AccessibilityRecord record = records.get(i); + record.setSealed(sealed); + } } } @@ -769,7 +771,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par * @return The number of records. */ public int getRecordCount() { - return mRecords.size(); + return mRecords == null ? 0 : mRecords.size(); } /** @@ -781,6 +783,9 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par */ public void appendRecord(AccessibilityRecord record) { enforceNotSealed(); + if (mRecords == null) { + mRecords = new ArrayList<AccessibilityRecord>(); + } mRecords.add(record); } @@ -791,6 +796,9 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par * @return The record at the specified index. */ public AccessibilityRecord getRecord(int index) { + if (mRecords == null) { + throw new IndexOutOfBoundsException("Invalid index " + index + ", size is 0"); + } return mRecords.get(index); } @@ -964,11 +972,14 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par AccessibilityEvent eventClone = AccessibilityEvent.obtain(); eventClone.init(event); - final int recordCount = event.mRecords.size(); - for (int i = 0; i < recordCount; i++) { - AccessibilityRecord record = event.mRecords.get(i); - AccessibilityRecord recordClone = AccessibilityRecord.obtain(record); - eventClone.mRecords.add(recordClone); + if (event.mRecords != null) { + final int recordCount = event.mRecords.size(); + eventClone.mRecords = new ArrayList<AccessibilityRecord>(recordCount); + for (int i = 0; i < recordCount; i++) { + final AccessibilityRecord record = event.mRecords.get(i); + final AccessibilityRecord recordClone = AccessibilityRecord.obtain(record); + eventClone.mRecords.add(recordClone); + } } return eventClone; @@ -1013,9 +1024,11 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par mContentChangeTypes = 0; mPackageName = null; mEventTime = 0; - while (!mRecords.isEmpty()) { - AccessibilityRecord record = mRecords.remove(0); - record.recycle(); + if (mRecords != null) { + while (!mRecords.isEmpty()) { + AccessibilityRecord record = mRecords.remove(0); + record.recycle(); + } } } @@ -1037,11 +1050,14 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par // Read the records. final int recordCount = parcel.readInt(); - for (int i = 0; i < recordCount; i++) { - AccessibilityRecord record = AccessibilityRecord.obtain(); - readAccessibilityRecordFromParcel(record, parcel); - record.mConnectionId = mConnectionId; - mRecords.add(record); + if (recordCount > 0) { + mRecords = new ArrayList<AccessibilityRecord>(recordCount); + for (int i = 0; i < recordCount; i++) { + AccessibilityRecord record = AccessibilityRecord.obtain(); + readAccessibilityRecordFromParcel(record, parcel); + record.mConnectionId = mConnectionId; + mRecords.add(record); + } } } @@ -1147,8 +1163,8 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par builder.append("; ContentChangeTypes: ").append(mContentChangeTypes); builder.append("; sourceWindowId: ").append(mSourceWindowId); builder.append("; mSourceNodeId: ").append(mSourceNodeId); - for (int i = 0; i < mRecords.size(); i++) { - AccessibilityRecord record = mRecords.get(i); + for (int i = 0; i < getRecordCount(); i++) { + final AccessibilityRecord record = getRecord(i); builder.append(" Record "); builder.append(i); builder.append(":"); diff --git a/core/java/android/view/accessibility/AccessibilityInteractionClient.java b/core/java/android/view/accessibility/AccessibilityInteractionClient.java index 139df3e..5a55e34 100644 --- a/core/java/android/view/accessibility/AccessibilityInteractionClient.java +++ b/core/java/android/view/accessibility/AccessibilityInteractionClient.java @@ -27,7 +27,6 @@ import android.os.SystemClock; import android.util.Log; import android.util.LongSparseArray; import android.util.SparseArray; -import android.util.SparseLongArray; import java.util.ArrayList; import java.util.Collections; @@ -718,10 +717,9 @@ public final class AccessibilityInteractionClient Log.e(LOG_TAG, "Duplicate node."); return; } - SparseLongArray childIds = current.getChildNodeIds(); - final int childCount = childIds.size(); + final int childCount = current.getChildCount(); for (int i = 0; i < childCount; i++) { - final long childId = childIds.valueAt(i); + final long childId = current.getChildId(i); for (int j = 0; j < infoCount; j++) { AccessibilityNodeInfo child = infos.get(j); if (child.getSourceNodeId() == childId) { diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index 00f4adb..324ba77 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -75,6 +75,42 @@ public final class AccessibilityManager { /** @hide */ public static final int STATE_FLAG_TOUCH_EXPLORATION_ENABLED = 0x00000002; + /** @hide */ + public static final int INVERSION_DISABLED = -1; + + /** @hide */ + public static final int INVERSION_STANDARD = 0; + + /** @hide */ + public static final int INVERSION_HUE_ONLY = 1; + + /** @hide */ + public static final int INVERSION_VALUE_ONLY = 2; + + /** @hide */ + public static final int DALTONIZER_DISABLED = -1; + + /** @hide */ + public static final int DALTONIZER_SIMULATE_MONOCHROMACY = 0; + + /** @hide */ + public static final int DALTONIZER_SIMULATE_PROTANOMALY = 1; + + /** @hide */ + public static final int DALTONIZER_SIMULATE_DEUTERANOMALY = 2; + + /** @hide */ + public static final int DALTONIZER_SIMULATE_TRITANOMALY = 3; + + /** @hide */ + public static final int DALTONIZER_CORRECT_PROTANOMALY = 11; + + /** @hide */ + public static final int DALTONIZER_CORRECT_DEUTERANOMALY = 12; + + /** @hide */ + public static final int DALTONIZER_CORRECT_TRITANOMALY = 13; + static final Object sInstanceSync = new Object(); private static AccessibilityManager sInstance; diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java index 4f53c1e..61aabea 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -22,8 +22,8 @@ import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.text.InputType; +import android.util.LongArray; import android.util.Pools.SynchronizedPool; -import android.util.SparseLongArray; import android.view.View; import java.util.Collections; @@ -503,7 +503,7 @@ public class AccessibilityNodeInfo implements Parcelable { private CharSequence mContentDescription; private String mViewIdResourceName; - private final SparseLongArray mChildNodeIds = new SparseLongArray(); + private LongArray mChildNodeIds; private int mActions; private int mMovementGranularities; @@ -666,21 +666,35 @@ public class AccessibilityNodeInfo implements Parcelable { } /** - * @return The ids of the children. + * Returns the array containing the IDs of this node's children. * * @hide */ - public SparseLongArray getChildNodeIds() { + public LongArray getChildNodeIds() { return mChildNodeIds; } /** + * Returns the id of the child at the specified index. + * + * @throws IndexOutOfBoundsException when index < 0 || index >= + * getChildCount() + * @hide + */ + public long getChildId(int index) { + if (mChildNodeIds == null) { + throw new IndexOutOfBoundsException(); + } + return mChildNodeIds.get(index); + } + + /** * Gets the number of children. * * @return The child count. */ public int getChildCount() { - return mChildNodeIds.size(); + return mChildNodeIds == null ? 0 : mChildNodeIds.size(); } /** @@ -699,6 +713,9 @@ public class AccessibilityNodeInfo implements Parcelable { */ public AccessibilityNodeInfo getChild(int index) { enforceSealed(); + if (mChildNodeIds == null) { + return null; + } if (!canPerformRequestOverConnection(mSourceNodeId)) { return null; } @@ -721,7 +738,35 @@ public class AccessibilityNodeInfo implements Parcelable { * @throws IllegalStateException If called from an AccessibilityService. */ public void addChild(View child) { - addChild(child, UNDEFINED); + addChildInternal(child, UNDEFINED, true); + } + + /** + * Unchecked version of {@link #addChild(View)} that does not verify + * uniqueness. For framework use only. + * + * @hide + */ + public void addChildUnchecked(View child) { + addChildInternal(child, UNDEFINED, false); + } + + /** + * Removes a child. If the child was not previously added to the node, + * calling this method has no effect. + * <p> + * <strong>Note:</strong> Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * + * @param child The child. + * @return true if the child was present + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public boolean removeChild(View child) { + return removeChild(child, UNDEFINED); } /** @@ -739,12 +784,49 @@ public class AccessibilityNodeInfo implements Parcelable { * @param virtualDescendantId The id of the virtual child. */ public void addChild(View root, int virtualDescendantId) { + addChildInternal(root, virtualDescendantId, true); + } + + private void addChildInternal(View root, int virtualDescendantId, boolean checked) { enforceNotSealed(); - final int index = mChildNodeIds.size(); + if (mChildNodeIds == null) { + mChildNodeIds = new LongArray(); + } final int rootAccessibilityViewId = (root != null) ? root.getAccessibilityViewId() : UNDEFINED; final long childNodeId = makeNodeId(rootAccessibilityViewId, virtualDescendantId); - mChildNodeIds.put(index, childNodeId); + // If we're checking uniqueness and the ID already exists, abort. + if (checked && mChildNodeIds.indexOf(childNodeId) >= 0) { + return; + } + mChildNodeIds.add(childNodeId); + } + + /** + * Removes a virtual child which is a descendant of the given + * <code>root</code>. If the child was not previously added to the node, + * calling this method has no effect. + * + * @param root The root of the virtual subtree. + * @param virtualDescendantId The id of the virtual child. + * @return true if the child was present + * @see #addChild(View, int) + */ + public boolean removeChild(View root, int virtualDescendantId) { + enforceNotSealed(); + final LongArray childIds = mChildNodeIds; + if (childIds == null) { + return false; + } + final int rootAccessibilityViewId = + (root != null) ? root.getAccessibilityViewId() : UNDEFINED; + final long childNodeId = makeNodeId(rootAccessibilityViewId, virtualDescendantId); + final int index = childIds.indexOf(childNodeId); + if (index < 0) { + return false; + } + childIds.remove(index); + return true; } /** @@ -789,6 +871,24 @@ public class AccessibilityNodeInfo implements Parcelable { } /** + * Removes an action that can be performed on the node. If the action was + * not already added to the node, calling this method has no effect. + * <p> + * <strong>Note:</strong> Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * + * @param action The action. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void removeAction(int action) { + enforceNotSealed(); + mActions &= ~action; + } + + /** * Sets the movement granularities for traversing the text of this node. * <p> * <strong>Note:</strong> Cannot be called from an @@ -1408,8 +1508,6 @@ public class AccessibilityNodeInfo implements Parcelable { * {@link android.accessibilityservice.AccessibilityService}. * This class is made immutable before being delivered to an AccessibilityService. * </p> - * - * @return collectionItem True if the node is an item. */ public void setCollectionItemInfo(CollectionItemInfo collectionItemInfo) { enforceNotSealed(); @@ -1951,6 +2049,7 @@ public class AccessibilityNodeInfo implements Parcelable { /** * {@inheritDoc} */ + @Override public int describeContents() { return 0; } @@ -2114,6 +2213,7 @@ public class AccessibilityNodeInfo implements Parcelable { * is recycled. You must not touch the object after calling this function. * </p> */ + @Override public void writeToParcel(Parcel parcel, int flags) { parcel.writeInt(isSealed() ? 1 : 0); parcel.writeLong(mSourceNodeId); @@ -2123,11 +2223,15 @@ public class AccessibilityNodeInfo implements Parcelable { parcel.writeLong(mLabeledById); parcel.writeInt(mConnectionId); - SparseLongArray childIds = mChildNodeIds; - final int childIdsSize = childIds.size(); - parcel.writeInt(childIdsSize); - for (int i = 0; i < childIdsSize; i++) { - parcel.writeLong(childIds.valueAt(i)); + final LongArray childIds = mChildNodeIds; + if (childIds == null) { + parcel.writeInt(0); + } else { + final int childIdsSize = childIds.size(); + parcel.writeInt(childIdsSize); + for (int i = 0; i < childIdsSize; i++) { + parcel.writeLong(childIds.get(i)); + } } parcel.writeInt(mBoundsInParent.top); @@ -2222,10 +2326,16 @@ public class AccessibilityNodeInfo implements Parcelable { mActions= other.mActions; mBooleanProperties = other.mBooleanProperties; mMovementGranularities = other.mMovementGranularities; - final int otherChildIdCount = other.mChildNodeIds.size(); - for (int i = 0; i < otherChildIdCount; i++) { - mChildNodeIds.put(i, other.mChildNodeIds.valueAt(i)); + + final LongArray otherChildNodeIds = other.mChildNodeIds; + if (otherChildNodeIds != null && otherChildNodeIds.size() > 0) { + if (mChildNodeIds == null) { + mChildNodeIds = otherChildNodeIds.clone(); + } else { + mChildNodeIds.addAll(otherChildNodeIds); + } } + mTextSelectionStart = other.mTextSelectionStart; mTextSelectionEnd = other.mTextSelectionEnd; mInputType = other.mInputType; @@ -2255,11 +2365,15 @@ public class AccessibilityNodeInfo implements Parcelable { mLabeledById = parcel.readLong(); mConnectionId = parcel.readInt(); - SparseLongArray childIds = mChildNodeIds; final int childrenSize = parcel.readInt(); - for (int i = 0; i < childrenSize; i++) { - final long childId = parcel.readLong(); - childIds.put(i, childId); + if (childrenSize <= 0) { + mChildNodeIds = null; + } else { + mChildNodeIds = new LongArray(childrenSize); + for (int i = 0; i < childrenSize; i++) { + final long childId = parcel.readLong(); + mChildNodeIds.add(childId); + } } mBoundsInParent.top = parcel.readInt(); @@ -2331,7 +2445,9 @@ public class AccessibilityNodeInfo implements Parcelable { mWindowId = UNDEFINED; mConnectionId = UNDEFINED; mMovementGranularities = 0; - mChildNodeIds.clear(); + if (mChildNodeIds != null) { + mChildNodeIds.clear(); + } mBoundsInParent.set(0, 0, 0, 0); mBoundsInScreen.set(0, 0, 0, 0); mBooleanProperties = 0; @@ -2493,12 +2609,14 @@ public class AccessibilityNodeInfo implements Parcelable { } builder.append("]"); - SparseLongArray childIds = mChildNodeIds; builder.append("; childAccessibilityIds: ["); - for (int i = 0, count = childIds.size(); i < count; i++) { - builder.append(childIds.valueAt(i)); - if (i < count - 1) { - builder.append(", "); + final LongArray childIds = mChildNodeIds; + if (childIds != null) { + for (int i = 0, count = childIds.size(); i < count; i++) { + builder.append(childIds.get(i)); + if (i < count - 1) { + builder.append(", "); + } } } builder.append("]"); @@ -2893,16 +3011,18 @@ public class AccessibilityNodeInfo implements Parcelable { } /** - * @see Parcelable.Creator + * @see android.os.Parcelable.Creator */ public static final Parcelable.Creator<AccessibilityNodeInfo> CREATOR = new Parcelable.Creator<AccessibilityNodeInfo>() { + @Override public AccessibilityNodeInfo createFromParcel(Parcel parcel) { AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(); info.initFromParcel(parcel); return info; } + @Override public AccessibilityNodeInfo[] newArray(int size) { return new AccessibilityNodeInfo[size]; } diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfoCache.java b/core/java/android/view/accessibility/AccessibilityNodeInfoCache.java index a9473a8..3f79b47 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfoCache.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfoCache.java @@ -18,8 +18,8 @@ package android.view.accessibility; import android.os.Build; import android.util.Log; +import android.util.LongArray; import android.util.LongSparseArray; -import android.util.SparseLongArray; import java.util.HashSet; import java.util.LinkedList; @@ -172,12 +172,12 @@ public class AccessibilityNodeInfoCache { // the new one represents a source state where some of the // children have been removed to avoid having disconnected // subtrees in the cache. - SparseLongArray oldChildrenIds = oldInfo.getChildNodeIds(); - SparseLongArray newChildrenIds = info.getChildNodeIds(); - final int oldChildCount = oldChildrenIds.size(); + // TODO: Runs in O(n^2), could optimize to O(n + n log n) + final LongArray newChildrenIds = info.getChildNodeIds(); + final int oldChildCount = oldInfo.getChildCount(); for (int i = 0; i < oldChildCount; i++) { - final long oldChildId = oldChildrenIds.valueAt(i); - if (newChildrenIds.indexOfValue(oldChildId) < 0) { + final long oldChildId = oldInfo.getChildId(i); + if (newChildrenIds.indexOf(oldChildId) < 0) { clearSubTreeLocked(oldChildId); } } @@ -237,10 +237,9 @@ public class AccessibilityNodeInfoCache { return; } mCacheImpl.remove(rootNodeId); - SparseLongArray childNodeIds = current.getChildNodeIds(); - final int childCount = childNodeIds.size(); + final int childCount = current.getChildCount(); for (int i = 0; i < childCount; i++) { - final long childNodeId = childNodeIds.valueAt(i); + final long childNodeId = current.getChildId(i); clearSubTreeRecursiveLocked(childNodeId); } } @@ -301,11 +300,10 @@ public class AccessibilityNodeInfoCache { } } - SparseLongArray childIds = current.getChildNodeIds(); - final int childCount = childIds.size(); + final int childCount = current.getChildCount(); for (int i = 0; i < childCount; i++) { - final long childId = childIds.valueAt(i); - AccessibilityNodeInfo child = mCacheImpl.get(childId); + final long childId = current.getChildId(i); + final AccessibilityNodeInfo child = mCacheImpl.get(childId); if (child != null) { fringe.add(child); } diff --git a/core/java/android/view/animation/BounceInterpolator.java b/core/java/android/view/animation/BounceInterpolator.java index f79e730..ecf99a7 100644 --- a/core/java/android/view/animation/BounceInterpolator.java +++ b/core/java/android/view/animation/BounceInterpolator.java @@ -17,7 +17,6 @@ package android.view.animation; import android.content.Context; -import android.content.res.TypedArray; import android.util.AttributeSet; /** diff --git a/core/java/android/view/inputmethod/BaseInputConnection.java b/core/java/android/view/inputmethod/BaseInputConnection.java index f730cf7..5552952 100644 --- a/core/java/android/view/inputmethod/BaseInputConnection.java +++ b/core/java/android/view/inputmethod/BaseInputConnection.java @@ -601,7 +601,11 @@ public class BaseInputConnection implements InputConnection { } beginBatchEdit(); - + if (!composing && !TextUtils.isEmpty(text)) { + // Notify the text is committed by the user to InputMethodManagerService + mIMM.notifyTextCommitted(); + } + // delete composing text set previously. int a = getComposingSpanStart(content); int b = getComposingSpanEnd(content); diff --git a/core/java/android/view/inputmethod/ExtractedTextRequest.java b/core/java/android/view/inputmethod/ExtractedTextRequest.java index f658b87..bf0bef3 100644 --- a/core/java/android/view/inputmethod/ExtractedTextRequest.java +++ b/core/java/android/view/inputmethod/ExtractedTextRequest.java @@ -18,7 +18,6 @@ package android.view.inputmethod; import android.os.Parcel; import android.os.Parcelable; -import android.text.TextUtils; /** * Description of what an input method would like from an application when diff --git a/core/java/android/view/inputmethod/InputBinding.java b/core/java/android/view/inputmethod/InputBinding.java index f4209ef..bcd459e 100644 --- a/core/java/android/view/inputmethod/InputBinding.java +++ b/core/java/android/view/inputmethod/InputBinding.java @@ -19,7 +19,6 @@ package android.view.inputmethod; import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; -import android.text.TextUtils; /** * Information given to an {@link InputMethod} about a client connecting diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 53f7c79..70c53d2 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -1805,6 +1805,20 @@ public final class InputMethodManager { } /** + * Notify the current IME commits text + * @hide + */ + public void notifyTextCommitted() { + synchronized (mH) { + try { + mService.notifyTextCommitted(); + } catch (RemoteException e) { + Log.w(TAG, "IME died: " + mCurId, e); + } + } + } + + /** * Returns a map of all shortcut input method info and their subtypes. */ public Map<InputMethodInfo, List<InputMethodSubtype>> getShortcutInputMethodsAndSubtypes() { @@ -1840,6 +1854,21 @@ public final class InputMethodManager { } /** + * @return The current height of the input method window. + * @hide + */ + public int getInputMethodWindowVisibleHeight() { + synchronized (mH) { + try { + return mService.getInputMethodWindowVisibleHeight(); + } catch (RemoteException e) { + Log.w(TAG, "IME died: " + mCurId, e); + return 0; + } + } + } + + /** * Force switch to the last used input method and subtype. If the last input method didn't have * any subtypes, the framework will simply switch to the last input method with no subtype * specified. diff --git a/core/java/android/view/inputmethod/InputMethodSubtype.java b/core/java/android/view/inputmethod/InputMethodSubtype.java index 2ab3024..e7ada27 100644 --- a/core/java/android/view/inputmethod/InputMethodSubtype.java +++ b/core/java/android/view/inputmethod/InputMethodSubtype.java @@ -472,12 +472,12 @@ public final class InputMethodSubtype implements Parcelable { return (subtype.hashCode() == hashCode()); } return (subtype.hashCode() == hashCode()) - && (subtype.getNameResId() == getNameResId()) - && (subtype.getMode().equals(getMode())) - && (subtype.getIconResId() == getIconResId()) && (subtype.getLocale().equals(getLocale())) + && (subtype.getMode().equals(getMode())) && (subtype.getExtraValue().equals(getExtraValue())) && (subtype.isAuxiliary() == isAuxiliary()) + && (subtype.overridesImplicitlyEnabledSubtype() + == overridesImplicitlyEnabledSubtype()) && (subtype.isAsciiCapable() == isAsciiCapable()); } return false; diff --git a/core/java/android/webkit/CacheManager.java b/core/java/android/webkit/CacheManager.java index bbd3f2b..45e6eb3 100644 --- a/core/java/android/webkit/CacheManager.java +++ b/core/java/android/webkit/CacheManager.java @@ -16,13 +16,7 @@ package android.webkit; -import android.content.Context; -import android.net.http.Headers; -import android.util.Log; - import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; diff --git a/core/java/android/webkit/DateSorter.java b/core/java/android/webkit/DateSorter.java index 82c13ae..fede244 100644 --- a/core/java/android/webkit/DateSorter.java +++ b/core/java/android/webkit/DateSorter.java @@ -20,7 +20,6 @@ import android.content.Context; import android.content.res.Resources; import java.util.Calendar; -import java.util.Date; import java.util.Locale; import libcore.icu.LocaleData; diff --git a/core/java/android/webkit/Plugin.java b/core/java/android/webkit/Plugin.java index 529820b..072e02a 100644 --- a/core/java/android/webkit/Plugin.java +++ b/core/java/android/webkit/Plugin.java @@ -21,7 +21,6 @@ import com.android.internal.R; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; -import android.webkit.WebView; /** * Represents a plugin (Java equivalent of the PluginPackageAndroid diff --git a/core/java/android/webkit/WebResourceResponse.java b/core/java/android/webkit/WebResourceResponse.java index b7171ee..f21e2b4 100644 --- a/core/java/android/webkit/WebResourceResponse.java +++ b/core/java/android/webkit/WebResourceResponse.java @@ -16,8 +16,6 @@ package android.webkit; -import android.net.http.Headers; - import java.io.InputStream; /** diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index 5bc39f1..7ee33c1 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -28,7 +28,6 @@ import android.graphics.drawable.Drawable; import android.net.http.SslCertificate; import android.os.Build; import android.os.Bundle; -import android.os.CancellationSignal; import android.os.Looper; import android.os.Message; import android.os.StrictMode; @@ -255,7 +254,7 @@ public class WebView extends AbsoluteLayout // Throwing an exception for incorrect thread usage if the // build target is JB MR2 or newer. Defaults to false, and is // set in the WebView constructor. - private static Boolean sEnforceThreadChecking = false; + private static volatile boolean sEnforceThreadChecking = false; /** * Transportation object for returning WebView across thread boundaries. @@ -449,10 +448,12 @@ public class WebView extends AbsoluteLayout * * @param context a Context object used to access application assets * @param attrs an AttributeSet passed to our parent - * @param defStyle the default style resource ID + * @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. */ - public WebView(Context context, AttributeSet attrs, int defStyle) { - this(context, attrs, defStyle, false); + public WebView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); } /** @@ -460,7 +461,26 @@ public class WebView extends AbsoluteLayout * * @param context a Context object used to access application assets * @param attrs an AttributeSet passed to our parent - * @param defStyle the default style resource ID + * @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. + */ + public WebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + this(context, attrs, defStyleAttr, defStyleRes, null, false); + } + + /** + * Constructs a new WebView with layout parameters and a default style. + * + * @param context a Context object used to access application assets + * @param attrs an AttributeSet passed to our parent + * @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 privateBrowsing whether this WebView will be initialized in * private mode * @@ -470,9 +490,9 @@ public class WebView extends AbsoluteLayout * and {@link WebStorage} for fine-grained control of privacy data. */ @Deprecated - public WebView(Context context, AttributeSet attrs, int defStyle, + public WebView(Context context, AttributeSet attrs, int defStyleAttr, boolean privateBrowsing) { - this(context, attrs, defStyle, null, privateBrowsing); + this(context, attrs, defStyleAttr, 0, null, privateBrowsing); } /** @@ -483,7 +503,9 @@ public class WebView extends AbsoluteLayout * * @param context a Context object used to access application assets * @param attrs an AttributeSet passed to our parent - * @param defStyle the default style resource ID + * @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 javaScriptInterfaces a Map of interface names, as keys, and * object implementing those interfaces, as * values @@ -492,10 +514,18 @@ public class WebView extends AbsoluteLayout * @hide This is used internally by dumprendertree, as it requires the javaScript interfaces to * be added synchronously, before a subsequent loadUrl call takes effect. */ + protected WebView(Context context, AttributeSet attrs, int defStyleAttr, + Map<String, Object> javaScriptInterfaces, boolean privateBrowsing) { + this(context, attrs, defStyleAttr, 0, javaScriptInterfaces, privateBrowsing); + } + + /** + * @hide + */ @SuppressWarnings("deprecation") // for super() call into deprecated base class constructor. - protected WebView(Context context, AttributeSet attrs, int defStyle, + protected WebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes, Map<String, Object> javaScriptInterfaces, boolean privateBrowsing) { - super(context, attrs, defStyle); + super(context, attrs, defStyleAttr, defStyleRes); if (context == null) { throw new IllegalArgumentException("Invalid context argument"); } @@ -686,6 +716,15 @@ public class WebView extends AbsoluteLayout } /** + * Used only by internal tests to free up memory. + * + * @hide + */ + public static void freeMemoryForTests() { + getFactory().getStatics().freeMemoryForTests(); + } + + /** * Informs WebView of the network state. This is used to set * the JavaScript property window.navigator.isOnline and * generates the online/offline event as specified in HTML5, sec. 5.7.7 @@ -781,7 +820,15 @@ public class WebView extends AbsoluteLayout */ public void loadUrl(String url, Map<String, String> additionalHttpHeaders) { checkThread(); - if (DebugFlags.TRACE_API) Log.d(LOGTAG, "loadUrl(extra headers)=" + url); + if (DebugFlags.TRACE_API) { + StringBuilder headers = new StringBuilder(); + if (additionalHttpHeaders != null) { + for (Map.Entry<String, String> entry : additionalHttpHeaders.entrySet()) { + headers.append(entry.getKey() + ":" + entry.getValue() + "\n"); + } + } + Log.d(LOGTAG, "loadUrl(extra headers)=" + url + "\n" + headers); + } mProvider.loadUrl(url, additionalHttpHeaders); } diff --git a/core/java/android/webkit/WebViewFactory.java b/core/java/android/webkit/WebViewFactory.java index b9131bf..25bcd44 100644 --- a/core/java/android/webkit/WebViewFactory.java +++ b/core/java/android/webkit/WebViewFactory.java @@ -16,9 +16,7 @@ package android.webkit; -import android.os.Build; import android.os.StrictMode; -import android.os.SystemProperties; import android.util.AndroidRuntimeException; import android.util.Log; diff --git a/core/java/android/webkit/WebViewFactoryProvider.java b/core/java/android/webkit/WebViewFactoryProvider.java index 9d9d882..e391aaf 100644 --- a/core/java/android/webkit/WebViewFactoryProvider.java +++ b/core/java/android/webkit/WebViewFactoryProvider.java @@ -50,6 +50,11 @@ public interface WebViewFactoryProvider { String getDefaultUserAgent(Context context); /** + * Used for tests only. + */ + void freeMemoryForTests(); + + /** * Implements the API method: * {@link android.webkit.WebView#setWebContentsDebuggingEnabled(boolean) } */ diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 092f474..413d6cf 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -55,6 +55,7 @@ import android.view.ViewConfiguration; import android.view.ViewDebug; import android.view.ViewGroup; import android.view.ViewParent; +import android.view.ViewRootImpl; import android.view.ViewTreeObserver; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; @@ -581,7 +582,13 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te /** * Helper object that renders and controls the fast scroll thumb. */ - private FastScroller mFastScroller; + private FastScroller mFastScroll; + + /** + * Temporary holder for fast scroller style until a FastScroller object + * is created. + */ + private int mFastScrollStyle; private boolean mGlobalLayoutListenerAddedFilter; @@ -773,14 +780,18 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te this(context, attrs, com.android.internal.R.attr.absListViewStyle); } - public AbsListView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public AbsListView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public AbsListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); initAbsListView(); mOwnerThread = Thread.currentThread(); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.AbsListView, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.AbsListView, defStyleAttr, defStyleRes); Drawable d = a.getDrawable(com.android.internal.R.styleable.AbsListView_listSelector); if (d != null) { @@ -809,6 +820,9 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te boolean enableFastScroll = a.getBoolean(R.styleable.AbsListView_fastScrollEnabled, false); setFastScrollEnabled(enableFastScroll); + int fastScrollStyle = a.getResourceId(R.styleable.AbsListView_fastScrollStyle, 0); + setFastScrollStyle(fastScrollStyle); + boolean smoothScrollbar = a.getBoolean(R.styleable.AbsListView_smoothScrollbar, true); setSmoothScrollbarEnabled(smoothScrollbar); @@ -1238,17 +1252,31 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } private void setFastScrollerEnabledUiThread(boolean enabled) { - if (mFastScroller != null) { - mFastScroller.setEnabled(enabled); + if (mFastScroll != null) { + mFastScroll.setEnabled(enabled); } else if (enabled) { - mFastScroller = new FastScroller(this); - mFastScroller.setEnabled(true); + mFastScroll = new FastScroller(this, mFastScrollStyle); + mFastScroll.setEnabled(true); } resolvePadding(); - if (mFastScroller != null) { - mFastScroller.updateLayout(); + if (mFastScroll != null) { + mFastScroll.updateLayout(); + } + } + + /** + * Specifies the style of the fast scroller decorations. + * + * @param styleResId style resource containing fast scroller properties + * @see android.R.styleable#FastScroll + */ + public void setFastScrollStyle(int styleResId) { + if (mFastScroll == null) { + mFastScrollStyle = styleResId; + } else { + mFastScroll.setStyle(styleResId); } } @@ -1288,8 +1316,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } private void setFastScrollerAlwaysVisibleUiThread(boolean alwaysShow) { - if (mFastScroller != null) { - mFastScroller.setAlwaysShow(alwaysShow); + if (mFastScroll != null) { + mFastScroll.setAlwaysShow(alwaysShow); } } @@ -1307,17 +1335,17 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * @see #setFastScrollAlwaysVisible(boolean) */ public boolean isFastScrollAlwaysVisible() { - if (mFastScroller == null) { + if (mFastScroll == null) { return mFastScrollEnabled && mFastScrollAlwaysVisible; } else { - return mFastScroller.isEnabled() && mFastScroller.isAlwaysShowEnabled(); + return mFastScroll.isEnabled() && mFastScroll.isAlwaysShowEnabled(); } } @Override public int getVerticalScrollbarWidth() { - if (mFastScroller != null && mFastScroller.isEnabled()) { - return Math.max(super.getVerticalScrollbarWidth(), mFastScroller.getWidth()); + if (mFastScroll != null && mFastScroll.isEnabled()) { + return Math.max(super.getVerticalScrollbarWidth(), mFastScroll.getWidth()); } return super.getVerticalScrollbarWidth(); } @@ -1330,26 +1358,26 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te */ @ViewDebug.ExportedProperty public boolean isFastScrollEnabled() { - if (mFastScroller == null) { + if (mFastScroll == null) { return mFastScrollEnabled; } else { - return mFastScroller.isEnabled(); + return mFastScroll.isEnabled(); } } @Override public void setVerticalScrollbarPosition(int position) { super.setVerticalScrollbarPosition(position); - if (mFastScroller != null) { - mFastScroller.setScrollbarPosition(position); + if (mFastScroll != null) { + mFastScroll.setScrollbarPosition(position); } } @Override public void setScrollBarStyle(int style) { super.setScrollBarStyle(style); - if (mFastScroller != null) { - mFastScroller.setScrollBarStyle(style); + if (mFastScroll != null) { + mFastScroll.setScrollBarStyle(style); } } @@ -1410,8 +1438,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * Notify our scroll listener (if there is one) of a change in scroll state */ void invokeOnItemScrollListener() { - if (mFastScroller != null) { - mFastScroller.onScroll(mFirstPosition, getChildCount(), mItemCount); + if (mFastScroll != null) { + mFastScroll.onScroll(mFirstPosition, getChildCount(), mItemCount); } if (mOnScrollListener != null) { mOnScrollListener.onScroll(this, mFirstPosition, getChildCount(), mItemCount); @@ -2084,8 +2112,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mRecycler.markChildrenDirty(); } - if (mFastScroller != null && (mItemCount != mOldItemCount || mDataChanged)) { - mFastScroller.onItemCountChanged(mItemCount); + if (mFastScroll != null && (mItemCount != mOldItemCount || mDataChanged)) { + mFastScroll.onItemCountChanged(mItemCount); } layoutChildren(); @@ -2120,6 +2148,34 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te protected void layoutChildren() { } + /** + * @return the direct child that contains accessibility focus, or null if no + * child contains accessibility focus + */ + View getAccessibilityFocusedChild() { + final ViewRootImpl viewRootImpl = getViewRootImpl(); + if (viewRootImpl == null) { + return null; + } + + View focusedView = viewRootImpl.getAccessibilityFocusedHost(); + if (focusedView == null) { + return null; + } + + ViewParent viewParent = focusedView.getParent(); + while ((viewParent instanceof View) && (viewParent != this)) { + focusedView = (View) viewParent; + viewParent = viewParent.getParent(); + } + + if (!(viewParent instanceof View)) { + return null; + } + + return focusedView; + } + void updateScrollIndicators() { if (mScrollUp != null) { boolean canScrollUp; @@ -2499,8 +2555,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te rememberSyncState(); } - if (mFastScroller != null) { - mFastScroller.onSizeChanged(w, h, oldw, oldh); + if (mFastScroll != null) { + mFastScroll.onSizeChanged(w, h, oldw, oldh); } } @@ -2829,8 +2885,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te @Override public void onRtlPropertiesChanged(int layoutDirection) { super.onRtlPropertiesChanged(layoutDirection); - if (mFastScroller != null) { - mFastScroller.setScrollbarPosition(getVerticalScrollbarPosition()); + if (mFastScroll != null) { + mFastScroll.setScrollbarPosition(getVerticalScrollbarPosition()); } } @@ -3403,8 +3459,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te return false; } - if (mFastScroller != null) { - boolean intercepted = mFastScroller.onTouchEvent(ev); + if (mFastScroll != null) { + boolean intercepted = mFastScroll.onTouchEvent(ev); if (intercepted) { return true; } @@ -3893,7 +3949,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te @Override public boolean onInterceptHoverEvent(MotionEvent event) { - if (mFastScroller != null && mFastScroller.onInterceptHoverEvent(event)) { + if (mFastScroll != null && mFastScroll.onInterceptHoverEvent(event)) { return true; } @@ -3917,7 +3973,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te return false; } - if (mFastScroller != null && mFastScroller.onInterceptTouchEvent(ev)) { + if (mFastScroll != null && mFastScroll.onInterceptTouchEvent(ev)) { return true; } @@ -6278,16 +6334,16 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te @Override public void onChanged() { super.onChanged(); - if (mFastScroller != null) { - mFastScroller.onSectionsChanged(); + if (mFastScroll != null) { + mFastScroll.onSectionsChanged(); } } @Override public void onInvalidated() { super.onInvalidated(); - if (mFastScroller != null) { - mFastScroller.onSectionsChanged(); + if (mFastScroll != null) { + mFastScroll.onSectionsChanged(); } } } diff --git a/core/java/android/widget/AbsSeekBar.java b/core/java/android/widget/AbsSeekBar.java index 7674837..941cdd0 100644 --- a/core/java/android/widget/AbsSeekBar.java +++ b/core/java/android/widget/AbsSeekBar.java @@ -65,11 +65,15 @@ public abstract class AbsSeekBar extends ProgressBar { super(context, attrs); } - public AbsSeekBar(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public AbsSeekBar(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public AbsSeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.SeekBar, defStyle, 0); + TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.SeekBar, defStyleAttr, defStyleRes); Drawable thumb = a.getDrawable(com.android.internal.R.styleable.SeekBar_thumb); setThumb(thumb); // will guess mThumbOffset if thumb != null... // ...but allow layout to override this diff --git a/core/java/android/widget/AbsSpinner.java b/core/java/android/widget/AbsSpinner.java index f26527f..6a4ad75 100644 --- a/core/java/android/widget/AbsSpinner.java +++ b/core/java/android/widget/AbsSpinner.java @@ -64,12 +64,16 @@ public abstract class AbsSpinner extends AdapterView<SpinnerAdapter> { this(context, attrs, 0); } - public AbsSpinner(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public AbsSpinner(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public AbsSpinner(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); initAbsSpinner(); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.AbsSpinner, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.AbsSpinner, defStyleAttr, defStyleRes); CharSequence[] entries = a.getTextArray(R.styleable.AbsSpinner_entries); if (entries != null) { diff --git a/core/java/android/widget/AbsoluteLayout.java b/core/java/android/widget/AbsoluteLayout.java index 7df6aab..4ce0d5d 100644 --- a/core/java/android/widget/AbsoluteLayout.java +++ b/core/java/android/widget/AbsoluteLayout.java @@ -40,16 +40,19 @@ import android.widget.RemoteViews.RemoteView; @RemoteView public class AbsoluteLayout extends ViewGroup { public AbsoluteLayout(Context context) { - super(context); + this(context, null); } public AbsoluteLayout(Context context, AttributeSet attrs) { - super(context, attrs); + this(context, attrs, 0); } - public AbsoluteLayout(Context context, AttributeSet attrs, - int defStyle) { - super(context, attrs, defStyle); + public AbsoluteLayout(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public AbsoluteLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); } @Override diff --git a/core/java/android/widget/ActivityChooserView.java b/core/java/android/widget/ActivityChooserView.java index 8612964..f9af2f9 100644 --- a/core/java/android/widget/ActivityChooserView.java +++ b/core/java/android/widget/ActivityChooserView.java @@ -18,7 +18,6 @@ package android.widget; import com.android.internal.R; -import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -31,7 +30,6 @@ import android.util.AttributeSet; import android.util.Log; import android.view.ActionProvider; import android.view.LayoutInflater; -import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; @@ -204,13 +202,32 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod * * @param context The application environment. * @param attrs A collection of attributes. - * @param defStyle The default style to apply to this 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. */ - public ActivityChooserView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public ActivityChooserView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + /** + * Create a new instance. + * + * @param context The application environment. + * @param attrs A collection of attributes. + * @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. + */ + public ActivityChooserView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); TypedArray attributesArray = context.obtainStyledAttributes(attrs, - R.styleable.ActivityChooserView, defStyle, 0); + R.styleable.ActivityChooserView, defStyleAttr, defStyleRes); mInitialActivityCount = attributesArray.getInt( R.styleable.ActivityChooserView_initialActivityCount, diff --git a/core/java/android/widget/AdapterView.java b/core/java/android/widget/AdapterView.java index a5fad60..962ffba 100644 --- a/core/java/android/widget/AdapterView.java +++ b/core/java/android/widget/AdapterView.java @@ -223,15 +223,19 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { boolean mBlockLayoutRequests = false; public AdapterView(Context context) { - super(context); + this(context, null); } public AdapterView(Context context, AttributeSet attrs) { - super(context, attrs); + this(context, attrs, 0); } - public AdapterView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public AdapterView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public AdapterView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); // If not explicitly specified this view is important for accessibility. if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { diff --git a/core/java/android/widget/AdapterViewAnimator.java b/core/java/android/widget/AdapterViewAnimator.java index 90e949a..1bc2f4b 100644 --- a/core/java/android/widget/AdapterViewAnimator.java +++ b/core/java/android/widget/AdapterViewAnimator.java @@ -173,10 +173,15 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> } public AdapterViewAnimator(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); + this(context, attrs, defStyleAttr, 0); + } + + public AdapterViewAnimator( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.AdapterViewAnimator, defStyleAttr, 0); + final TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.AdapterViewAnimator, defStyleAttr, defStyleRes); int resource = a.getResourceId( com.android.internal.R.styleable.AdapterViewAnimator_inAnimation, 0); if (resource > 0) { diff --git a/core/java/android/widget/AdapterViewFlipper.java b/core/java/android/widget/AdapterViewFlipper.java index aea029b..3b026bd 100644 --- a/core/java/android/widget/AdapterViewFlipper.java +++ b/core/java/android/widget/AdapterViewFlipper.java @@ -59,10 +59,19 @@ public class AdapterViewFlipper extends AdapterViewAnimator { } public AdapterViewFlipper(Context context, AttributeSet attrs) { - super(context, attrs); + this(context, attrs, 0); + } + + public AdapterViewFlipper(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public AdapterViewFlipper( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.AdapterViewFlipper); + final TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.AdapterViewFlipper, defStyleAttr, defStyleRes); mFlipInterval = a.getInt( com.android.internal.R.styleable.AdapterViewFlipper_flipInterval, DEFAULT_INTERVAL); mAutoStart = a.getBoolean( diff --git a/core/java/android/widget/AnalogClock.java b/core/java/android/widget/AnalogClock.java index c7da818..3c88e94 100644 --- a/core/java/android/widget/AnalogClock.java +++ b/core/java/android/widget/AnalogClock.java @@ -67,13 +67,16 @@ public class AnalogClock extends View { this(context, attrs, 0); } - public AnalogClock(Context context, AttributeSet attrs, - int defStyle) { - super(context, attrs, defStyle); - Resources r = mContext.getResources(); - TypedArray a = - context.obtainStyledAttributes( - attrs, com.android.internal.R.styleable.AnalogClock, defStyle, 0); + public AnalogClock(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public AnalogClock(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + final Resources r = context.getResources(); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.AnalogClock, defStyleAttr, defStyleRes); mDial = a.getDrawable(com.android.internal.R.styleable.AnalogClock_dial); if (mDial == null) { diff --git a/core/java/android/widget/AutoCompleteTextView.java b/core/java/android/widget/AutoCompleteTextView.java index f0eb94f..259c66b 100644 --- a/core/java/android/widget/AutoCompleteTextView.java +++ b/core/java/android/widget/AutoCompleteTextView.java @@ -133,17 +133,21 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe this(context, attrs, com.android.internal.R.attr.autoCompleteTextViewStyle); } - public AutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public AutoCompleteTextView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public AutoCompleteTextView( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); mPopup = new ListPopupWindow(context, attrs, com.android.internal.R.attr.autoCompleteTextViewStyle); mPopup.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); mPopup.setPromptPosition(ListPopupWindow.POSITION_PROMPT_BELOW); - TypedArray a = - context.obtainStyledAttributes( - attrs, com.android.internal.R.styleable.AutoCompleteTextView, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.AutoCompleteTextView, defStyleAttr, defStyleRes); mThreshold = a.getInt( R.styleable.AutoCompleteTextView_completionThreshold, 2); diff --git a/core/java/android/widget/Button.java b/core/java/android/widget/Button.java index 2ac56ac..1663620 100644 --- a/core/java/android/widget/Button.java +++ b/core/java/android/widget/Button.java @@ -103,8 +103,12 @@ public class Button extends TextView { this(context, attrs, com.android.internal.R.attr.buttonStyle); } - public Button(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public Button(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public Button(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); } @Override diff --git a/core/java/android/widget/CalendarView.java b/core/java/android/widget/CalendarView.java index 0957ab4..a87c7d2 100644 --- a/core/java/android/widget/CalendarView.java +++ b/core/java/android/widget/CalendarView.java @@ -80,234 +80,7 @@ public class CalendarView extends FrameLayout { */ private static final String LOG_TAG = CalendarView.class.getSimpleName(); - /** - * Default value whether to show week number. - */ - private static final boolean DEFAULT_SHOW_WEEK_NUMBER = true; - - /** - * The number of milliseconds in a day.e - */ - private static final long MILLIS_IN_DAY = 86400000L; - - /** - * The number of day in a week. - */ - private static final int DAYS_PER_WEEK = 7; - - /** - * The number of milliseconds in a week. - */ - private static final long MILLIS_IN_WEEK = DAYS_PER_WEEK * MILLIS_IN_DAY; - - /** - * Affects when the month selection will change while scrolling upe - */ - private static final int SCROLL_HYST_WEEKS = 2; - - /** - * How long the GoTo fling animation should last. - */ - private static final int GOTO_SCROLL_DURATION = 1000; - - /** - * The duration of the adjustment upon a user scroll in milliseconds. - */ - private static final int ADJUSTMENT_SCROLL_DURATION = 500; - - /** - * How long to wait after receiving an onScrollStateChanged notification - * before acting on it. - */ - private static final int SCROLL_CHANGE_DELAY = 40; - - /** - * String for parsing dates. - */ - private static final String DATE_FORMAT = "MM/dd/yyyy"; - - /** - * The default minimal date. - */ - private static final String DEFAULT_MIN_DATE = "01/01/1900"; - - /** - * The default maximal date. - */ - private static final String DEFAULT_MAX_DATE = "01/01/2100"; - - private static final int DEFAULT_SHOWN_WEEK_COUNT = 6; - - private static final int DEFAULT_DATE_TEXT_SIZE = 14; - - private static final int UNSCALED_SELECTED_DATE_VERTICAL_BAR_WIDTH = 6; - - private static final int UNSCALED_WEEK_MIN_VISIBLE_HEIGHT = 12; - - private static final int UNSCALED_LIST_SCROLL_TOP_OFFSET = 2; - - private static final int UNSCALED_BOTTOM_BUFFER = 20; - - private static final int UNSCALED_WEEK_SEPARATOR_LINE_WIDTH = 1; - - private static final int DEFAULT_WEEK_DAY_TEXT_APPEARANCE_RES_ID = -1; - - private final int mWeekSeperatorLineWidth; - - private int mDateTextSize; - - private Drawable mSelectedDateVerticalBar; - - private final int mSelectedDateVerticalBarWidth; - - private int mSelectedWeekBackgroundColor; - - private int mFocusedMonthDateColor; - - private int mUnfocusedMonthDateColor; - - private int mWeekSeparatorLineColor; - - private int mWeekNumberColor; - - private int mWeekDayTextAppearanceResId; - - private int mDateTextAppearanceResId; - - /** - * The top offset of the weeks list. - */ - private int mListScrollTopOffset = 2; - - /** - * The visible height of a week view. - */ - private int mWeekMinVisibleHeight = 12; - - /** - * The visible height of a week view. - */ - private int mBottomBuffer = 20; - - /** - * The number of shown weeks. - */ - private int mShownWeekCount; - - /** - * Flag whether to show the week number. - */ - private boolean mShowWeekNumber; - - /** - * The number of day per week to be shown. - */ - private int mDaysPerWeek = 7; - - /** - * The friction of the week list while flinging. - */ - private float mFriction = .05f; - - /** - * Scale for adjusting velocity of the week list while flinging. - */ - private float mVelocityScale = 0.333f; - - /** - * The adapter for the weeks list. - */ - private WeeksAdapter mAdapter; - - /** - * The weeks list. - */ - private ListView mListView; - - /** - * The name of the month to display. - */ - private TextView mMonthName; - - /** - * The header with week day names. - */ - private ViewGroup mDayNamesHeader; - - /** - * Cached labels for the week names header. - */ - private String[] mDayLabels; - - /** - * The first day of the week. - */ - private int mFirstDayOfWeek; - - /** - * Which month should be displayed/highlighted [0-11]. - */ - private int mCurrentMonthDisplayed = -1; - - /** - * Used for tracking during a scroll. - */ - private long mPreviousScrollPosition; - - /** - * Used for tracking which direction the view is scrolling. - */ - private boolean mIsScrollingUp = false; - - /** - * The previous scroll state of the weeks ListView. - */ - private int mPreviousScrollState = OnScrollListener.SCROLL_STATE_IDLE; - - /** - * The current scroll state of the weeks ListView. - */ - private int mCurrentScrollState = OnScrollListener.SCROLL_STATE_IDLE; - - /** - * Listener for changes in the selected day. - */ - private OnDateChangeListener mOnDateChangeListener; - - /** - * Command for adjusting the position after a scroll/fling. - */ - private ScrollStateRunnable mScrollStateChangedRunnable = new ScrollStateRunnable(); - - /** - * Temporary instance to avoid multiple instantiations. - */ - private Calendar mTempDate; - - /** - * The first day of the focused month. - */ - private Calendar mFirstDayOfMonth; - - /** - * The start date of the range supported by this picker. - */ - private Calendar mMinDate; - - /** - * The end date of the range supported by this picker. - */ - private Calendar mMaxDate; - - /** - * Date format for parsing dates. - */ - private final java.text.DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT); - - /** - * The current locale. - */ - private Locale mCurrentLocale; + private CalendarViewDelegate mDelegate; /** * The callback used to indicate the user changes the date. @@ -330,91 +103,17 @@ public class CalendarView extends FrameLayout { } public CalendarView(Context context, AttributeSet attrs) { - this(context, attrs, 0); + this(context, attrs, R.attr.calendarViewStyle); } - public CalendarView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, 0); - - // initialization based on locale - setCurrentLocale(Locale.getDefault()); - - TypedArray attributesArray = context.obtainStyledAttributes(attrs, R.styleable.CalendarView, - R.attr.calendarViewStyle, 0); - mShowWeekNumber = attributesArray.getBoolean(R.styleable.CalendarView_showWeekNumber, - DEFAULT_SHOW_WEEK_NUMBER); - mFirstDayOfWeek = attributesArray.getInt(R.styleable.CalendarView_firstDayOfWeek, - LocaleData.get(Locale.getDefault()).firstDayOfWeek); - String minDate = attributesArray.getString(R.styleable.CalendarView_minDate); - if (TextUtils.isEmpty(minDate) || !parseDate(minDate, mMinDate)) { - parseDate(DEFAULT_MIN_DATE, mMinDate); - } - String maxDate = attributesArray.getString(R.styleable.CalendarView_maxDate); - if (TextUtils.isEmpty(maxDate) || !parseDate(maxDate, mMaxDate)) { - parseDate(DEFAULT_MAX_DATE, mMaxDate); - } - if (mMaxDate.before(mMinDate)) { - throw new IllegalArgumentException("Max date cannot be before min date."); - } - mShownWeekCount = attributesArray.getInt(R.styleable.CalendarView_shownWeekCount, - DEFAULT_SHOWN_WEEK_COUNT); - mSelectedWeekBackgroundColor = attributesArray.getColor( - R.styleable.CalendarView_selectedWeekBackgroundColor, 0); - mFocusedMonthDateColor = attributesArray.getColor( - R.styleable.CalendarView_focusedMonthDateColor, 0); - mUnfocusedMonthDateColor = attributesArray.getColor( - R.styleable.CalendarView_unfocusedMonthDateColor, 0); - mWeekSeparatorLineColor = attributesArray.getColor( - R.styleable.CalendarView_weekSeparatorLineColor, 0); - mWeekNumberColor = attributesArray.getColor(R.styleable.CalendarView_weekNumberColor, 0); - mSelectedDateVerticalBar = attributesArray.getDrawable( - R.styleable.CalendarView_selectedDateVerticalBar); - - mDateTextAppearanceResId = attributesArray.getResourceId( - R.styleable.CalendarView_dateTextAppearance, R.style.TextAppearance_Small); - updateDateTextSize(); - - mWeekDayTextAppearanceResId = attributesArray.getResourceId( - R.styleable.CalendarView_weekDayTextAppearance, - DEFAULT_WEEK_DAY_TEXT_APPEARANCE_RES_ID); - attributesArray.recycle(); - - DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); - mWeekMinVisibleHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, - UNSCALED_WEEK_MIN_VISIBLE_HEIGHT, displayMetrics); - mListScrollTopOffset = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, - UNSCALED_LIST_SCROLL_TOP_OFFSET, displayMetrics); - mBottomBuffer = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, - UNSCALED_BOTTOM_BUFFER, displayMetrics); - mSelectedDateVerticalBarWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, - UNSCALED_SELECTED_DATE_VERTICAL_BAR_WIDTH, displayMetrics); - mWeekSeperatorLineWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, - UNSCALED_WEEK_SEPARATOR_LINE_WIDTH, displayMetrics); - - LayoutInflater layoutInflater = (LayoutInflater) context - .getSystemService(Service.LAYOUT_INFLATER_SERVICE); - View content = layoutInflater.inflate(R.layout.calendar_view, null, false); - addView(content); - - mListView = (ListView) findViewById(R.id.list); - mDayNamesHeader = (ViewGroup) content.findViewById(com.android.internal.R.id.day_names); - mMonthName = (TextView) content.findViewById(com.android.internal.R.id.month_name); - - setUpHeader(); - setUpListView(); - setUpAdapter(); - - // go to today or whichever is close to today min or max date - mTempDate.setTimeInMillis(System.currentTimeMillis()); - if (mTempDate.before(mMinDate)) { - goTo(mMinDate, false, true, true); - } else if (mMaxDate.before(mTempDate)) { - goTo(mMaxDate, false, true, true); - } else { - goTo(mTempDate, false, true, true); - } + public CalendarView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public CalendarView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - invalidate(); + mDelegate = new LegacyCalendarViewDelegate(this, context, attrs, defStyleAttr, defStyleRes); } /** @@ -425,10 +124,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_shownWeekCount */ public void setShownWeekCount(int count) { - if (mShownWeekCount != count) { - mShownWeekCount = count; - invalidate(); - } + mDelegate.setShownWeekCount(count); } /** @@ -439,7 +135,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_shownWeekCount */ public int getShownWeekCount() { - return mShownWeekCount; + return mDelegate.getShownWeekCount(); } /** @@ -450,16 +146,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_selectedWeekBackgroundColor */ public void setSelectedWeekBackgroundColor(int color) { - if (mSelectedWeekBackgroundColor != color) { - mSelectedWeekBackgroundColor = color; - final int childCount = mListView.getChildCount(); - for (int i = 0; i < childCount; i++) { - WeekView weekView = (WeekView) mListView.getChildAt(i); - if (weekView.mHasSelectedDay) { - weekView.invalidate(); - } - } - } + mDelegate.setSelectedWeekBackgroundColor(color); } /** @@ -470,7 +157,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_selectedWeekBackgroundColor */ public int getSelectedWeekBackgroundColor() { - return mSelectedWeekBackgroundColor; + return mDelegate.getSelectedWeekBackgroundColor(); } /** @@ -481,16 +168,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_focusedMonthDateColor */ public void setFocusedMonthDateColor(int color) { - if (mFocusedMonthDateColor != color) { - mFocusedMonthDateColor = color; - final int childCount = mListView.getChildCount(); - for (int i = 0; i < childCount; i++) { - WeekView weekView = (WeekView) mListView.getChildAt(i); - if (weekView.mHasFocusedDay) { - weekView.invalidate(); - } - } - } + mDelegate.setFocusedMonthDateColor(color); } /** @@ -501,7 +179,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_focusedMonthDateColor */ public int getFocusedMonthDateColor() { - return mFocusedMonthDateColor; + return mDelegate.getFocusedMonthDateColor(); } /** @@ -512,16 +190,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_unfocusedMonthDateColor */ public void setUnfocusedMonthDateColor(int color) { - if (mUnfocusedMonthDateColor != color) { - mUnfocusedMonthDateColor = color; - final int childCount = mListView.getChildCount(); - for (int i = 0; i < childCount; i++) { - WeekView weekView = (WeekView) mListView.getChildAt(i); - if (weekView.mHasUnfocusedDay) { - weekView.invalidate(); - } - } - } + mDelegate.setUnfocusedMonthDateColor(color); } /** @@ -532,7 +201,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_unfocusedMonthDateColor */ public int getUnfocusedMonthDateColor() { - return mFocusedMonthDateColor; + return mDelegate.getUnfocusedMonthDateColor(); } /** @@ -543,12 +212,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_weekNumberColor */ public void setWeekNumberColor(int color) { - if (mWeekNumberColor != color) { - mWeekNumberColor = color; - if (mShowWeekNumber) { - invalidateAllWeekViews(); - } - } + mDelegate.setWeekNumberColor(color); } /** @@ -559,7 +223,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_weekNumberColor */ public int getWeekNumberColor() { - return mWeekNumberColor; + return mDelegate.getWeekNumberColor(); } /** @@ -570,10 +234,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_weekSeparatorLineColor */ public void setWeekSeparatorLineColor(int color) { - if (mWeekSeparatorLineColor != color) { - mWeekSeparatorLineColor = color; - invalidateAllWeekViews(); - } + mDelegate.setWeekSeparatorLineColor(color); } /** @@ -584,7 +245,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_weekSeparatorLineColor */ public int getWeekSeparatorLineColor() { - return mWeekSeparatorLineColor; + return mDelegate.getWeekSeparatorLineColor(); } /** @@ -596,8 +257,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_selectedDateVerticalBar */ public void setSelectedDateVerticalBar(int resourceId) { - Drawable drawable = getResources().getDrawable(resourceId); - setSelectedDateVerticalBar(drawable); + mDelegate.setSelectedDateVerticalBar(resourceId); } /** @@ -609,16 +269,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_selectedDateVerticalBar */ public void setSelectedDateVerticalBar(Drawable drawable) { - if (mSelectedDateVerticalBar != drawable) { - mSelectedDateVerticalBar = drawable; - final int childCount = mListView.getChildCount(); - for (int i = 0; i < childCount; i++) { - WeekView weekView = (WeekView) mListView.getChildAt(i); - if (weekView.mHasSelectedDay) { - weekView.invalidate(); - } - } - } + mDelegate.setSelectedDateVerticalBar(drawable); } /** @@ -628,7 +279,7 @@ public class CalendarView extends FrameLayout { * @return The vertical bar drawable. */ public Drawable getSelectedDateVerticalBar() { - return mSelectedDateVerticalBar; + return mDelegate.getSelectedDateVerticalBar(); } /** @@ -639,10 +290,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_weekDayTextAppearance */ public void setWeekDayTextAppearance(int resourceId) { - if (mWeekDayTextAppearanceResId != resourceId) { - mWeekDayTextAppearanceResId = resourceId; - setUpHeader(); - } + mDelegate.setWeekDayTextAppearance(resourceId); } /** @@ -653,7 +301,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_weekDayTextAppearance */ public int getWeekDayTextAppearance() { - return mWeekDayTextAppearanceResId; + return mDelegate.getWeekDayTextAppearance(); } /** @@ -664,11 +312,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_dateTextAppearance */ public void setDateTextAppearance(int resourceId) { - if (mDateTextAppearanceResId != resourceId) { - mDateTextAppearanceResId = resourceId; - updateDateTextSize(); - invalidateAllWeekViews(); - } + mDelegate.setDateTextAppearance(resourceId); } /** @@ -679,35 +323,17 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_dateTextAppearance */ public int getDateTextAppearance() { - return mDateTextAppearanceResId; + return mDelegate.getDateTextAppearance(); } @Override public void setEnabled(boolean enabled) { - mListView.setEnabled(enabled); + mDelegate.setEnabled(enabled); } @Override public boolean isEnabled() { - return mListView.isEnabled(); - } - - @Override - protected void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - setCurrentLocale(newConfig.locale); - } - - @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(CalendarView.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(CalendarView.class.getName()); + return mDelegate.isEnabled(); } /** @@ -723,7 +349,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_minDate */ public long getMinDate() { - return mMinDate.getTimeInMillis(); + return mDelegate.getMinDate(); } /** @@ -736,30 +362,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_minDate */ public void setMinDate(long minDate) { - mTempDate.setTimeInMillis(minDate); - if (isSameDate(mTempDate, mMinDate)) { - return; - } - mMinDate.setTimeInMillis(minDate); - // make sure the current date is not earlier than - // the new min date since the latter is used for - // calculating the indices in the adapter thus - // avoiding out of bounds error - Calendar date = mAdapter.mSelectedDate; - if (date.before(mMinDate)) { - mAdapter.setSelectedDay(mMinDate); - } - // reinitialize the adapter since its range depends on min date - mAdapter.init(); - if (date.before(mMinDate)) { - setDate(mTempDate.getTimeInMillis()); - } else { - // we go to the current date to force the ListView to query its - // adapter for the shown views since we have changed the adapter - // range and the base from which the later calculates item indices - // note that calling setDate will not work since the date is the same - goTo(date, false, true, false); - } + mDelegate.setMinDate(minDate); } /** @@ -775,7 +378,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_maxDate */ public long getMaxDate() { - return mMaxDate.getTimeInMillis(); + return mDelegate.getMaxDate(); } /** @@ -788,23 +391,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_maxDate */ public void setMaxDate(long maxDate) { - mTempDate.setTimeInMillis(maxDate); - if (isSameDate(mTempDate, mMaxDate)) { - return; - } - mMaxDate.setTimeInMillis(maxDate); - // reinitialize the adapter since its range depends on max date - mAdapter.init(); - Calendar date = mAdapter.mSelectedDate; - if (date.after(mMaxDate)) { - setDate(mMaxDate.getTimeInMillis()); - } else { - // we go to the current date to force the ListView to query its - // adapter for the shown views since we have changed the adapter - // range and the base from which the later calculates item indices - // note that calling setDate will not work since the date is the same - goTo(date, false, true, false); - } + mDelegate.setMaxDate(maxDate); } /** @@ -815,12 +402,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_showWeekNumber */ public void setShowWeekNumber(boolean showWeekNumber) { - if (mShowWeekNumber == showWeekNumber) { - return; - } - mShowWeekNumber = showWeekNumber; - mAdapter.notifyDataSetChanged(); - setUpHeader(); + mDelegate.setShowWeekNumber(showWeekNumber); } /** @@ -831,7 +413,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_showWeekNumber */ public boolean getShowWeekNumber() { - return mShowWeekNumber; + return mDelegate.getShowWeekNumber(); } /** @@ -850,7 +432,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_firstDayOfWeek */ public int getFirstDayOfWeek() { - return mFirstDayOfWeek; + return mDelegate.getFirstDayOfWeek(); } /** @@ -869,12 +451,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_firstDayOfWeek */ public void setFirstDayOfWeek(int firstDayOfWeek) { - if (mFirstDayOfWeek == firstDayOfWeek) { - return; - } - mFirstDayOfWeek = firstDayOfWeek; - mAdapter.init(); - setUpHeader(); + mDelegate.setFirstDayOfWeek(firstDayOfWeek); } /** @@ -883,7 +460,7 @@ public class CalendarView extends FrameLayout { * @param listener The listener to be notified. */ public void setOnDateChangeListener(OnDateChangeListener listener) { - mOnDateChangeListener = listener; + mDelegate.setOnDateChangeListener(listener); } /** @@ -893,7 +470,7 @@ public class CalendarView extends FrameLayout { * @return The selected date. */ public long getDate() { - return mAdapter.mSelectedDate.getTimeInMillis(); + return mDelegate.getDate(); } /** @@ -910,7 +487,7 @@ public class CalendarView extends FrameLayout { * @see #setMaxDate(long) */ public void setDate(long date) { - setDate(date, false, false); + mDelegate.setDate(date); } /** @@ -928,934 +505,1645 @@ public class CalendarView extends FrameLayout { * @see #setMaxDate(long) */ public void setDate(long date, boolean animate, boolean center) { - mTempDate.setTimeInMillis(date); - if (isSameDate(mTempDate, mAdapter.mSelectedDate)) { - return; - } - goTo(mTempDate, animate, true, center); + mDelegate.setDate(date, animate, center); } - private void updateDateTextSize() { - TypedArray dateTextAppearance = mContext.obtainStyledAttributes( - mDateTextAppearanceResId, R.styleable.TextAppearance); - mDateTextSize = dateTextAppearance.getDimensionPixelSize( - R.styleable.TextAppearance_textSize, DEFAULT_DATE_TEXT_SIZE); - dateTextAppearance.recycle(); + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mDelegate.onConfigurationChanged(newConfig); } - /** - * Invalidates all week views. - */ - private void invalidateAllWeekViews() { - final int childCount = mListView.getChildCount(); - for (int i = 0; i < childCount; i++) { - View view = mListView.getChildAt(i); - view.invalidate(); - } + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + mDelegate.onInitializeAccessibilityEvent(event); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + mDelegate.onInitializeAccessibilityNodeInfo(info); } /** - * Sets the current locale. - * - * @param locale The current locale. + * A delegate interface that defined the public API of the CalendarView. Allows different + * CalendarView implementations. This would need to be implemented by the CalendarView delegates + * for the real behavior. */ - private void setCurrentLocale(Locale locale) { - if (locale.equals(mCurrentLocale)) { - return; - } + private interface CalendarViewDelegate { + void setShownWeekCount(int count); + int getShownWeekCount(); - mCurrentLocale = locale; + void setSelectedWeekBackgroundColor(int color); + int getSelectedWeekBackgroundColor(); - mTempDate = getCalendarForLocale(mTempDate, locale); - mFirstDayOfMonth = getCalendarForLocale(mFirstDayOfMonth, locale); - mMinDate = getCalendarForLocale(mMinDate, locale); - mMaxDate = getCalendarForLocale(mMaxDate, locale); - } + void setFocusedMonthDateColor(int color); + int getFocusedMonthDateColor(); - /** - * Gets a calendar for locale bootstrapped with the value of a given calendar. - * - * @param oldCalendar The old calendar. - * @param locale The locale. - */ - private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) { - if (oldCalendar == null) { - return Calendar.getInstance(locale); - } else { - final long currentTimeMillis = oldCalendar.getTimeInMillis(); - Calendar newCalendar = Calendar.getInstance(locale); - newCalendar.setTimeInMillis(currentTimeMillis); - return newCalendar; - } - } + void setUnfocusedMonthDateColor(int color); + int getUnfocusedMonthDateColor(); - /** - * @return True if the <code>firstDate</code> is the same as the <code> - * secondDate</code>. - */ - private boolean isSameDate(Calendar firstDate, Calendar secondDate) { - return (firstDate.get(Calendar.DAY_OF_YEAR) == secondDate.get(Calendar.DAY_OF_YEAR) - && firstDate.get(Calendar.YEAR) == secondDate.get(Calendar.YEAR)); - } + void setWeekNumberColor(int color); + int getWeekNumberColor(); - /** - * Creates a new adapter if necessary and sets up its parameters. - */ - private void setUpAdapter() { - if (mAdapter == null) { - mAdapter = new WeeksAdapter(); - mAdapter.registerDataSetObserver(new DataSetObserver() { - @Override - public void onChanged() { - if (mOnDateChangeListener != null) { - Calendar selectedDay = mAdapter.getSelectedDay(); - mOnDateChangeListener.onSelectedDayChange(CalendarView.this, - selectedDay.get(Calendar.YEAR), - selectedDay.get(Calendar.MONTH), - selectedDay.get(Calendar.DAY_OF_MONTH)); - } - } - }); - mListView.setAdapter(mAdapter); - } + void setWeekSeparatorLineColor(int color); + int getWeekSeparatorLineColor(); + + void setSelectedDateVerticalBar(int resourceId); + void setSelectedDateVerticalBar(Drawable drawable); + Drawable getSelectedDateVerticalBar(); + + void setWeekDayTextAppearance(int resourceId); + int getWeekDayTextAppearance(); + + void setDateTextAppearance(int resourceId); + int getDateTextAppearance(); + + void setEnabled(boolean enabled); + boolean isEnabled(); + + void setMinDate(long minDate); + long getMinDate(); + + void setMaxDate(long maxDate); + long getMaxDate(); + + void setShowWeekNumber(boolean showWeekNumber); + boolean getShowWeekNumber(); + + void setFirstDayOfWeek(int firstDayOfWeek); + int getFirstDayOfWeek(); + + void setDate(long date); + void setDate(long date, boolean animate, boolean center); + long getDate(); + + void setOnDateChangeListener(OnDateChangeListener listener); - // refresh the view with the new parameters - mAdapter.notifyDataSetChanged(); + void onConfigurationChanged(Configuration newConfig); + void onInitializeAccessibilityEvent(AccessibilityEvent event); + void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info); } /** - * Sets up the strings to be used by the header. + * An abstract class which can be used as a start for CalendarView implementations */ - private void setUpHeader() { - final String[] tinyWeekdayNames = LocaleData.get(Locale.getDefault()).tinyWeekdayNames; - mDayLabels = new String[mDaysPerWeek]; - for (int i = 0; i < mDaysPerWeek; i++) { - final int j = i + mFirstDayOfWeek; - final int calendarDay = (j > Calendar.SATURDAY) ? j - Calendar.SATURDAY : j; - mDayLabels[i] = tinyWeekdayNames[calendarDay]; - } - // Deal with week number - TextView label = (TextView) mDayNamesHeader.getChildAt(0); - if (mShowWeekNumber) { - label.setVisibility(View.VISIBLE); - } else { - label.setVisibility(View.GONE); + abstract static class AbstractCalendarViewDelegate implements CalendarViewDelegate { + // The delegator + protected CalendarView mDelegator; + + // The context + protected Context mContext; + + // The current locale + protected Locale mCurrentLocale; + + AbstractCalendarViewDelegate(CalendarView delegator, Context context) { + mDelegator = delegator; + mContext = context; + + // Initialization based on locale + setCurrentLocale(Locale.getDefault()); } - // Deal with day labels - final int count = mDayNamesHeader.getChildCount(); - for (int i = 0; i < count - 1; i++) { - label = (TextView) mDayNamesHeader.getChildAt(i + 1); - if (mWeekDayTextAppearanceResId > -1) { - label.setTextAppearance(mContext, mWeekDayTextAppearanceResId); - } - if (i < mDaysPerWeek) { - label.setText(mDayLabels[i]); - label.setVisibility(View.VISIBLE); - } else { - label.setVisibility(View.GONE); + + protected void setCurrentLocale(Locale locale) { + if (locale.equals(mCurrentLocale)) { + return; } + mCurrentLocale = locale; } - mDayNamesHeader.invalidate(); } /** - * Sets all the required fields for the list view. + * A delegate implementing the legacy CalendarView */ - private void setUpListView() { - // Configure the listview - mListView.setDivider(null); - mListView.setItemsCanFocus(true); - mListView.setVerticalScrollBarEnabled(false); - mListView.setOnScrollListener(new OnScrollListener() { - public void onScrollStateChanged(AbsListView view, int scrollState) { - CalendarView.this.onScrollStateChanged(view, scrollState); - } - - public void onScroll( - AbsListView view, int firstVisibleItem, int visibleItemCount, - int totalItemCount) { - CalendarView.this.onScroll(view, firstVisibleItem, visibleItemCount, - totalItemCount); - } - }); - // Make the scrolling behavior nicer - mListView.setFriction(mFriction); - mListView.setVelocityScale(mVelocityScale); - } + private static class LegacyCalendarViewDelegate extends AbstractCalendarViewDelegate { - /** - * This moves to the specified time in the view. If the time is not already - * in range it will move the list so that the first of the month containing - * the time is at the top of the view. If the new time is already in view - * the list will not be scrolled unless forceScroll is true. This time may - * optionally be highlighted as selected as well. - * - * @param date The time to move to. - * @param animate Whether to scroll to the given time or just redraw at the - * new location. - * @param setSelected Whether to set the given time as selected. - * @param forceScroll Whether to recenter even if the time is already - * visible. - * - * @throws IllegalArgumentException of the provided date is before the - * range start of after the range end. - */ - private void goTo(Calendar date, boolean animate, boolean setSelected, boolean forceScroll) { - if (date.before(mMinDate) || date.after(mMaxDate)) { - throw new IllegalArgumentException("Time not between " + mMinDate.getTime() - + " and " + mMaxDate.getTime()); - } - // Find the first and last entirely visible weeks - int firstFullyVisiblePosition = mListView.getFirstVisiblePosition(); - View firstChild = mListView.getChildAt(0); - if (firstChild != null && firstChild.getTop() < 0) { - firstFullyVisiblePosition++; - } - int lastFullyVisiblePosition = firstFullyVisiblePosition + mShownWeekCount - 1; - if (firstChild != null && firstChild.getTop() > mBottomBuffer) { - lastFullyVisiblePosition--; - } - if (setSelected) { - mAdapter.setSelectedDay(date); - } - // Get the week we're going to - int position = getWeeksSinceMinDate(date); + /** + * Default value whether to show week number. + */ + private static final boolean DEFAULT_SHOW_WEEK_NUMBER = true; - // Check if the selected day is now outside of our visible range - // and if so scroll to the month that contains it - if (position < firstFullyVisiblePosition || position > lastFullyVisiblePosition - || forceScroll) { - mFirstDayOfMonth.setTimeInMillis(date.getTimeInMillis()); - mFirstDayOfMonth.set(Calendar.DAY_OF_MONTH, 1); + /** + * The number of milliseconds in a day.e + */ + private static final long MILLIS_IN_DAY = 86400000L; - setMonthDisplayed(mFirstDayOfMonth); + /** + * The number of day in a week. + */ + private static final int DAYS_PER_WEEK = 7; - // the earliest time we can scroll to is the min date - if (mFirstDayOfMonth.before(mMinDate)) { - position = 0; - } else { - position = getWeeksSinceMinDate(mFirstDayOfMonth); + /** + * The number of milliseconds in a week. + */ + private static final long MILLIS_IN_WEEK = DAYS_PER_WEEK * MILLIS_IN_DAY; + + /** + * Affects when the month selection will change while scrolling upe + */ + private static final int SCROLL_HYST_WEEKS = 2; + + /** + * How long the GoTo fling animation should last. + */ + private static final int GOTO_SCROLL_DURATION = 1000; + + /** + * The duration of the adjustment upon a user scroll in milliseconds. + */ + private static final int ADJUSTMENT_SCROLL_DURATION = 500; + + /** + * How long to wait after receiving an onScrollStateChanged notification + * before acting on it. + */ + private static final int SCROLL_CHANGE_DELAY = 40; + + /** + * String for parsing dates. + */ + private static final String DATE_FORMAT = "MM/dd/yyyy"; + + /** + * The default minimal date. + */ + private static final String DEFAULT_MIN_DATE = "01/01/1900"; + + /** + * The default maximal date. + */ + private static final String DEFAULT_MAX_DATE = "01/01/2100"; + + private static final int DEFAULT_SHOWN_WEEK_COUNT = 6; + + private static final int DEFAULT_DATE_TEXT_SIZE = 14; + + private static final int UNSCALED_SELECTED_DATE_VERTICAL_BAR_WIDTH = 6; + + private static final int UNSCALED_WEEK_MIN_VISIBLE_HEIGHT = 12; + + private static final int UNSCALED_LIST_SCROLL_TOP_OFFSET = 2; + + private static final int UNSCALED_BOTTOM_BUFFER = 20; + + private static final int UNSCALED_WEEK_SEPARATOR_LINE_WIDTH = 1; + + private static final int DEFAULT_WEEK_DAY_TEXT_APPEARANCE_RES_ID = -1; + + private final int mWeekSeperatorLineWidth; + + private int mDateTextSize; + + private Drawable mSelectedDateVerticalBar; + + private final int mSelectedDateVerticalBarWidth; + + private int mSelectedWeekBackgroundColor; + + private int mFocusedMonthDateColor; + + private int mUnfocusedMonthDateColor; + + private int mWeekSeparatorLineColor; + + private int mWeekNumberColor; + + private int mWeekDayTextAppearanceResId; + + private int mDateTextAppearanceResId; + + /** + * The top offset of the weeks list. + */ + private int mListScrollTopOffset = 2; + + /** + * The visible height of a week view. + */ + private int mWeekMinVisibleHeight = 12; + + /** + * The visible height of a week view. + */ + private int mBottomBuffer = 20; + + /** + * The number of shown weeks. + */ + private int mShownWeekCount; + + /** + * Flag whether to show the week number. + */ + private boolean mShowWeekNumber; + + /** + * The number of day per week to be shown. + */ + private int mDaysPerWeek = 7; + + /** + * The friction of the week list while flinging. + */ + private float mFriction = .05f; + + /** + * Scale for adjusting velocity of the week list while flinging. + */ + private float mVelocityScale = 0.333f; + + /** + * The adapter for the weeks list. + */ + private WeeksAdapter mAdapter; + + /** + * The weeks list. + */ + private ListView mListView; + + /** + * The name of the month to display. + */ + private TextView mMonthName; + + /** + * The header with week day names. + */ + private ViewGroup mDayNamesHeader; + + /** + * Cached labels for the week names header. + */ + private String[] mDayLabels; + + /** + * The first day of the week. + */ + private int mFirstDayOfWeek; + + /** + * Which month should be displayed/highlighted [0-11]. + */ + private int mCurrentMonthDisplayed = -1; + + /** + * Used for tracking during a scroll. + */ + private long mPreviousScrollPosition; + + /** + * Used for tracking which direction the view is scrolling. + */ + private boolean mIsScrollingUp = false; + + /** + * The previous scroll state of the weeks ListView. + */ + private int mPreviousScrollState = OnScrollListener.SCROLL_STATE_IDLE; + + /** + * The current scroll state of the weeks ListView. + */ + private int mCurrentScrollState = OnScrollListener.SCROLL_STATE_IDLE; + + /** + * Listener for changes in the selected day. + */ + private OnDateChangeListener mOnDateChangeListener; + + /** + * Command for adjusting the position after a scroll/fling. + */ + private ScrollStateRunnable mScrollStateChangedRunnable = new ScrollStateRunnable(); + + /** + * Temporary instance to avoid multiple instantiations. + */ + private Calendar mTempDate; + + /** + * The first day of the focused month. + */ + private Calendar mFirstDayOfMonth; + + /** + * The start date of the range supported by this picker. + */ + private Calendar mMinDate; + + /** + * The end date of the range supported by this picker. + */ + private Calendar mMaxDate; + + /** + * Date format for parsing dates. + */ + private final java.text.DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT); + + LegacyCalendarViewDelegate(CalendarView delegator, Context context, AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + super(delegator, context); + + // initialization based on locale + setCurrentLocale(Locale.getDefault()); + + TypedArray attributesArray = context.obtainStyledAttributes(attrs, + R.styleable.CalendarView, defStyleAttr, defStyleRes); + mShowWeekNumber = attributesArray.getBoolean(R.styleable.CalendarView_showWeekNumber, + DEFAULT_SHOW_WEEK_NUMBER); + mFirstDayOfWeek = attributesArray.getInt(R.styleable.CalendarView_firstDayOfWeek, + LocaleData.get(Locale.getDefault()).firstDayOfWeek); + String minDate = attributesArray.getString(R.styleable.CalendarView_minDate); + if (TextUtils.isEmpty(minDate) || !parseDate(minDate, mMinDate)) { + parseDate(DEFAULT_MIN_DATE, mMinDate); + } + String maxDate = attributesArray.getString(R.styleable.CalendarView_maxDate); + if (TextUtils.isEmpty(maxDate) || !parseDate(maxDate, mMaxDate)) { + parseDate(DEFAULT_MAX_DATE, mMaxDate); + } + if (mMaxDate.before(mMinDate)) { + throw new IllegalArgumentException("Max date cannot be before min date."); } + mShownWeekCount = attributesArray.getInt(R.styleable.CalendarView_shownWeekCount, + DEFAULT_SHOWN_WEEK_COUNT); + mSelectedWeekBackgroundColor = attributesArray.getColor( + R.styleable.CalendarView_selectedWeekBackgroundColor, 0); + mFocusedMonthDateColor = attributesArray.getColor( + R.styleable.CalendarView_focusedMonthDateColor, 0); + mUnfocusedMonthDateColor = attributesArray.getColor( + R.styleable.CalendarView_unfocusedMonthDateColor, 0); + mWeekSeparatorLineColor = attributesArray.getColor( + R.styleable.CalendarView_weekSeparatorLineColor, 0); + mWeekNumberColor = attributesArray.getColor(R.styleable.CalendarView_weekNumberColor, 0); + mSelectedDateVerticalBar = attributesArray.getDrawable( + R.styleable.CalendarView_selectedDateVerticalBar); + + mDateTextAppearanceResId = attributesArray.getResourceId( + R.styleable.CalendarView_dateTextAppearance, R.style.TextAppearance_Small); + updateDateTextSize(); + + mWeekDayTextAppearanceResId = attributesArray.getResourceId( + R.styleable.CalendarView_weekDayTextAppearance, + DEFAULT_WEEK_DAY_TEXT_APPEARANCE_RES_ID); + attributesArray.recycle(); + + DisplayMetrics displayMetrics = mDelegator.getResources().getDisplayMetrics(); + mWeekMinVisibleHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + UNSCALED_WEEK_MIN_VISIBLE_HEIGHT, displayMetrics); + mListScrollTopOffset = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + UNSCALED_LIST_SCROLL_TOP_OFFSET, displayMetrics); + mBottomBuffer = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + UNSCALED_BOTTOM_BUFFER, displayMetrics); + mSelectedDateVerticalBarWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + UNSCALED_SELECTED_DATE_VERTICAL_BAR_WIDTH, displayMetrics); + mWeekSeperatorLineWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + UNSCALED_WEEK_SEPARATOR_LINE_WIDTH, displayMetrics); + + LayoutInflater layoutInflater = (LayoutInflater) mContext + .getSystemService(Service.LAYOUT_INFLATER_SERVICE); + View content = layoutInflater.inflate(R.layout.calendar_view, null, false); + mDelegator.addView(content); + + mListView = (ListView) mDelegator.findViewById(R.id.list); + mDayNamesHeader = (ViewGroup) content.findViewById(com.android.internal.R.id.day_names); + mMonthName = (TextView) content.findViewById(com.android.internal.R.id.month_name); - mPreviousScrollState = OnScrollListener.SCROLL_STATE_FLING; - if (animate) { - mListView.smoothScrollToPositionFromTop(position, mListScrollTopOffset, - GOTO_SCROLL_DURATION); + setUpHeader(); + setUpListView(); + setUpAdapter(); + + // go to today or whichever is close to today min or max date + mTempDate.setTimeInMillis(System.currentTimeMillis()); + if (mTempDate.before(mMinDate)) { + goTo(mMinDate, false, true, true); + } else if (mMaxDate.before(mTempDate)) { + goTo(mMaxDate, false, true, true); } else { - mListView.setSelectionFromTop(position, mListScrollTopOffset); - // Perform any after scroll operations that are needed - onScrollStateChanged(mListView, OnScrollListener.SCROLL_STATE_IDLE); + goTo(mTempDate, false, true, true); } - } else if (setSelected) { - // Otherwise just set the selection - setMonthDisplayed(date); + + mDelegator.invalidate(); } - } - /** - * Parses the given <code>date</code> and in case of success sets - * the result to the <code>outDate</code>. - * - * @return True if the date was parsed. - */ - private boolean parseDate(String date, Calendar outDate) { - try { - outDate.setTime(mDateFormat.parse(date)); - return true; - } catch (ParseException e) { - Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT); - return false; + @Override + public void setShownWeekCount(int count) { + if (mShownWeekCount != count) { + mShownWeekCount = count; + mDelegator.invalidate(); + } } - } - /** - * Called when a <code>view</code> transitions to a new <code>scrollState - * </code>. - */ - private void onScrollStateChanged(AbsListView view, int scrollState) { - mScrollStateChangedRunnable.doScrollStateChange(view, scrollState); - } + @Override + public int getShownWeekCount() { + return mShownWeekCount; + } - /** - * Updates the title and selected month if the <code>view</code> has moved to a new - * month. - */ - private void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, - int totalItemCount) { - WeekView child = (WeekView) view.getChildAt(0); - if (child == null) { - return; + @Override + public void setSelectedWeekBackgroundColor(int color) { + if (mSelectedWeekBackgroundColor != color) { + mSelectedWeekBackgroundColor = color; + final int childCount = mListView.getChildCount(); + for (int i = 0; i < childCount; i++) { + WeekView weekView = (WeekView) mListView.getChildAt(i); + if (weekView.mHasSelectedDay) { + weekView.invalidate(); + } + } + } } - // Figure out where we are - long currScroll = view.getFirstVisiblePosition() * child.getHeight() - child.getBottom(); + @Override + public int getSelectedWeekBackgroundColor() { + return mSelectedWeekBackgroundColor; + } - // If we have moved since our last call update the direction - if (currScroll < mPreviousScrollPosition) { - mIsScrollingUp = true; - } else if (currScroll > mPreviousScrollPosition) { - mIsScrollingUp = false; - } else { - return; + @Override + public void setFocusedMonthDateColor(int color) { + if (mFocusedMonthDateColor != color) { + mFocusedMonthDateColor = color; + final int childCount = mListView.getChildCount(); + for (int i = 0; i < childCount; i++) { + WeekView weekView = (WeekView) mListView.getChildAt(i); + if (weekView.mHasFocusedDay) { + weekView.invalidate(); + } + } + } } - // Use some hysteresis for checking which month to highlight. This - // causes the month to transition when two full weeks of a month are - // visible when scrolling up, and when the first day in a month reaches - // the top of the screen when scrolling down. - int offset = child.getBottom() < mWeekMinVisibleHeight ? 1 : 0; - if (mIsScrollingUp) { - child = (WeekView) view.getChildAt(SCROLL_HYST_WEEKS + offset); - } else if (offset != 0) { - child = (WeekView) view.getChildAt(offset); + @Override + public int getFocusedMonthDateColor() { + return mFocusedMonthDateColor; } - // Find out which month we're moving into - int month; - if (mIsScrollingUp) { - month = child.getMonthOfFirstWeekDay(); - } else { - month = child.getMonthOfLastWeekDay(); + @Override + public void setUnfocusedMonthDateColor(int color) { + if (mUnfocusedMonthDateColor != color) { + mUnfocusedMonthDateColor = color; + final int childCount = mListView.getChildCount(); + for (int i = 0; i < childCount; i++) { + WeekView weekView = (WeekView) mListView.getChildAt(i); + if (weekView.mHasUnfocusedDay) { + weekView.invalidate(); + } + } + } } - // And how it relates to our current highlighted month - int monthDiff; - if (mCurrentMonthDisplayed == 11 && month == 0) { - monthDiff = 1; - } else if (mCurrentMonthDisplayed == 0 && month == 11) { - monthDiff = -1; - } else { - monthDiff = month - mCurrentMonthDisplayed; + @Override + public int getUnfocusedMonthDateColor() { + return mFocusedMonthDateColor; } - // Only switch months if we're scrolling away from the currently - // selected month - if ((!mIsScrollingUp && monthDiff > 0) || (mIsScrollingUp && monthDiff < 0)) { - Calendar firstDay = child.getFirstDay(); - if (mIsScrollingUp) { - firstDay.add(Calendar.DAY_OF_MONTH, -DAYS_PER_WEEK); - } else { - firstDay.add(Calendar.DAY_OF_MONTH, DAYS_PER_WEEK); + @Override + public void setWeekNumberColor(int color) { + if (mWeekNumberColor != color) { + mWeekNumberColor = color; + if (mShowWeekNumber) { + invalidateAllWeekViews(); + } } - setMonthDisplayed(firstDay); } - mPreviousScrollPosition = currScroll; - mPreviousScrollState = mCurrentScrollState; - } - /** - * Sets the month displayed at the top of this view based on time. Override - * to add custom events when the title is changed. - * - * @param calendar A day in the new focus month. - */ - private void setMonthDisplayed(Calendar calendar) { - mCurrentMonthDisplayed = calendar.get(Calendar.MONTH); - mAdapter.setFocusMonth(mCurrentMonthDisplayed); - final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_MONTH_DAY - | DateUtils.FORMAT_SHOW_YEAR; - final long millis = calendar.getTimeInMillis(); - String newMonthName = DateUtils.formatDateRange(mContext, millis, millis, flags); - mMonthName.setText(newMonthName); - mMonthName.invalidate(); - } - - /** - * @return Returns the number of weeks between the current <code>date</code> - * and the <code>mMinDate</code>. - */ - private int getWeeksSinceMinDate(Calendar date) { - if (date.before(mMinDate)) { - throw new IllegalArgumentException("fromDate: " + mMinDate.getTime() - + " does not precede toDate: " + date.getTime()); + @Override + public int getWeekNumberColor() { + return mWeekNumberColor; } - long endTimeMillis = date.getTimeInMillis() - + date.getTimeZone().getOffset(date.getTimeInMillis()); - long startTimeMillis = mMinDate.getTimeInMillis() - + mMinDate.getTimeZone().getOffset(mMinDate.getTimeInMillis()); - long dayOffsetMillis = (mMinDate.get(Calendar.DAY_OF_WEEK) - mFirstDayOfWeek) - * MILLIS_IN_DAY; - return (int) ((endTimeMillis - startTimeMillis + dayOffsetMillis) / MILLIS_IN_WEEK); - } - /** - * Command responsible for acting upon scroll state changes. - */ - private class ScrollStateRunnable implements Runnable { - private AbsListView mView; + @Override + public void setWeekSeparatorLineColor(int color) { + if (mWeekSeparatorLineColor != color) { + mWeekSeparatorLineColor = color; + invalidateAllWeekViews(); + } + } - private int mNewState; + @Override + public int getWeekSeparatorLineColor() { + return mWeekSeparatorLineColor; + } - /** - * Sets up the runnable with a short delay in case the scroll state - * immediately changes again. - * - * @param view The list view that changed state - * @param scrollState The new state it changed to - */ - public void doScrollStateChange(AbsListView view, int scrollState) { - mView = view; - mNewState = scrollState; - removeCallbacks(this); - postDelayed(this, SCROLL_CHANGE_DELAY); + @Override + public void setSelectedDateVerticalBar(int resourceId) { + Drawable drawable = mDelegator.getResources().getDrawable(resourceId); + setSelectedDateVerticalBar(drawable); } - public void run() { - mCurrentScrollState = mNewState; - // Fix the position after a scroll or a fling ends - if (mNewState == OnScrollListener.SCROLL_STATE_IDLE - && mPreviousScrollState != OnScrollListener.SCROLL_STATE_IDLE) { - View child = mView.getChildAt(0); - if (child == null) { - // The view is no longer visible, just return - return; - } - int dist = child.getBottom() - mListScrollTopOffset; - if (dist > mListScrollTopOffset) { - if (mIsScrollingUp) { - mView.smoothScrollBy(dist - child.getHeight(), ADJUSTMENT_SCROLL_DURATION); - } else { - mView.smoothScrollBy(dist, ADJUSTMENT_SCROLL_DURATION); + @Override + public void setSelectedDateVerticalBar(Drawable drawable) { + if (mSelectedDateVerticalBar != drawable) { + mSelectedDateVerticalBar = drawable; + final int childCount = mListView.getChildCount(); + for (int i = 0; i < childCount; i++) { + WeekView weekView = (WeekView) mListView.getChildAt(i); + if (weekView.mHasSelectedDay) { + weekView.invalidate(); } } } - mPreviousScrollState = mNewState; } - } - /** - * <p> - * This is a specialized adapter for creating a list of weeks with - * selectable days. It can be configured to display the week number, start - * the week on a given day, show a reduced number of days, or display an - * arbitrary number of weeks at a time. - * </p> - */ - private class WeeksAdapter extends BaseAdapter implements OnTouchListener { - private final Calendar mSelectedDate = Calendar.getInstance(); - private final GestureDetector mGestureDetector; + @Override + public Drawable getSelectedDateVerticalBar() { + return mSelectedDateVerticalBar; + } - private int mSelectedWeek; + @Override + public void setWeekDayTextAppearance(int resourceId) { + if (mWeekDayTextAppearanceResId != resourceId) { + mWeekDayTextAppearanceResId = resourceId; + setUpHeader(); + } + } - private int mFocusedMonth; + @Override + public int getWeekDayTextAppearance() { + return mWeekDayTextAppearanceResId; + } - private int mTotalWeekCount; + @Override + public void setDateTextAppearance(int resourceId) { + if (mDateTextAppearanceResId != resourceId) { + mDateTextAppearanceResId = resourceId; + updateDateTextSize(); + invalidateAllWeekViews(); + } + } - public WeeksAdapter() { - mGestureDetector = new GestureDetector(mContext, new CalendarGestureListener()); - init(); + @Override + public int getDateTextAppearance() { + return mDateTextAppearanceResId; } - /** - * Set up the gesture detector and selected time - */ - private void init() { - mSelectedWeek = getWeeksSinceMinDate(mSelectedDate); - mTotalWeekCount = getWeeksSinceMinDate(mMaxDate); - if (mMinDate.get(Calendar.DAY_OF_WEEK) != mFirstDayOfWeek - || mMaxDate.get(Calendar.DAY_OF_WEEK) != mFirstDayOfWeek) { - mTotalWeekCount++; - } - notifyDataSetChanged(); + @Override + public void setEnabled(boolean enabled) { + mListView.setEnabled(enabled); } - /** - * Updates the selected day and related parameters. - * - * @param selectedDay The time to highlight - */ - public void setSelectedDay(Calendar selectedDay) { - if (selectedDay.get(Calendar.DAY_OF_YEAR) == mSelectedDate.get(Calendar.DAY_OF_YEAR) - && selectedDay.get(Calendar.YEAR) == mSelectedDate.get(Calendar.YEAR)) { + @Override + public boolean isEnabled() { + return mListView.isEnabled(); + } + + @Override + public void setMinDate(long minDate) { + mTempDate.setTimeInMillis(minDate); + if (isSameDate(mTempDate, mMinDate)) { return; } - mSelectedDate.setTimeInMillis(selectedDay.getTimeInMillis()); - mSelectedWeek = getWeeksSinceMinDate(mSelectedDate); - mFocusedMonth = mSelectedDate.get(Calendar.MONTH); - notifyDataSetChanged(); + mMinDate.setTimeInMillis(minDate); + // make sure the current date is not earlier than + // the new min date since the latter is used for + // calculating the indices in the adapter thus + // avoiding out of bounds error + Calendar date = mAdapter.mSelectedDate; + if (date.before(mMinDate)) { + mAdapter.setSelectedDay(mMinDate); + } + // reinitialize the adapter since its range depends on min date + mAdapter.init(); + if (date.before(mMinDate)) { + setDate(mTempDate.getTimeInMillis()); + } else { + // we go to the current date to force the ListView to query its + // adapter for the shown views since we have changed the adapter + // range and the base from which the later calculates item indices + // note that calling setDate will not work since the date is the same + goTo(date, false, true, false); + } } - /** - * @return The selected day of month. - */ - public Calendar getSelectedDay() { - return mSelectedDate; + @Override + public long getMinDate() { + return mMinDate.getTimeInMillis(); } @Override - public int getCount() { - return mTotalWeekCount; + public void setMaxDate(long maxDate) { + mTempDate.setTimeInMillis(maxDate); + if (isSameDate(mTempDate, mMaxDate)) { + return; + } + mMaxDate.setTimeInMillis(maxDate); + // reinitialize the adapter since its range depends on max date + mAdapter.init(); + Calendar date = mAdapter.mSelectedDate; + if (date.after(mMaxDate)) { + setDate(mMaxDate.getTimeInMillis()); + } else { + // we go to the current date to force the ListView to query its + // adapter for the shown views since we have changed the adapter + // range and the base from which the later calculates item indices + // note that calling setDate will not work since the date is the same + goTo(date, false, true, false); + } } @Override - public Object getItem(int position) { - return null; + public long getMaxDate() { + return mMaxDate.getTimeInMillis(); } @Override - public long getItemId(int position) { - return position; + public void setShowWeekNumber(boolean showWeekNumber) { + if (mShowWeekNumber == showWeekNumber) { + return; + } + mShowWeekNumber = showWeekNumber; + mAdapter.notifyDataSetChanged(); + setUpHeader(); } @Override - public View getView(int position, View convertView, ViewGroup parent) { - WeekView weekView = null; - if (convertView != null) { - weekView = (WeekView) convertView; - } else { - weekView = new WeekView(mContext); - android.widget.AbsListView.LayoutParams params = - new android.widget.AbsListView.LayoutParams(LayoutParams.WRAP_CONTENT, - LayoutParams.WRAP_CONTENT); - weekView.setLayoutParams(params); - weekView.setClickable(true); - weekView.setOnTouchListener(this); + public boolean getShowWeekNumber() { + return mShowWeekNumber; + } + + @Override + public void setFirstDayOfWeek(int firstDayOfWeek) { + if (mFirstDayOfWeek == firstDayOfWeek) { + return; } + mFirstDayOfWeek = firstDayOfWeek; + mAdapter.init(); + mAdapter.notifyDataSetChanged(); + setUpHeader(); + } - int selectedWeekDay = (mSelectedWeek == position) ? mSelectedDate.get( - Calendar.DAY_OF_WEEK) : -1; - weekView.init(position, selectedWeekDay, mFocusedMonth); + @Override + public int getFirstDayOfWeek() { + return mFirstDayOfWeek; + } - return weekView; + @Override + public void setDate(long date) { + setDate(date, false, false); } - /** - * Changes which month is in focus and updates the view. - * - * @param month The month to show as in focus [0-11] - */ - public void setFocusMonth(int month) { - if (mFocusedMonth == month) { + @Override + public void setDate(long date, boolean animate, boolean center) { + mTempDate.setTimeInMillis(date); + if (isSameDate(mTempDate, mAdapter.mSelectedDate)) { return; } - mFocusedMonth = month; - notifyDataSetChanged(); + goTo(mTempDate, animate, true, center); } @Override - public boolean onTouch(View v, MotionEvent event) { - if (mListView.isEnabled() && mGestureDetector.onTouchEvent(event)) { - WeekView weekView = (WeekView) v; - // if we cannot find a day for the given location we are done - if (!weekView.getDayFromLocation(event.getX(), mTempDate)) { - return true; - } - // it is possible that the touched day is outside the valid range - // we draw whole weeks but range end can fall not on the week end - if (mTempDate.before(mMinDate) || mTempDate.after(mMaxDate)) { - return true; - } - onDateTapped(mTempDate); - return true; - } - return false; + public long getDate() { + return mAdapter.mSelectedDate.getTimeInMillis(); + } + + @Override + public void setOnDateChangeListener(OnDateChangeListener listener) { + mOnDateChangeListener = listener; + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + setCurrentLocale(newConfig.locale); + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + event.setClassName(CalendarView.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + info.setClassName(CalendarView.class.getName()); } /** - * Maintains the same hour/min/sec but moves the day to the tapped day. + * Sets the current locale. * - * @param day The day that was tapped + * @param locale The current locale. */ - private void onDateTapped(Calendar day) { - setSelectedDay(day); - setMonthDisplayed(day); + @Override + protected void setCurrentLocale(Locale locale) { + super.setCurrentLocale(locale); + + mTempDate = getCalendarForLocale(mTempDate, locale); + mFirstDayOfMonth = getCalendarForLocale(mFirstDayOfMonth, locale); + mMinDate = getCalendarForLocale(mMinDate, locale); + mMaxDate = getCalendarForLocale(mMaxDate, locale); + } + private void updateDateTextSize() { + TypedArray dateTextAppearance = mDelegator.getContext().obtainStyledAttributes( + mDateTextAppearanceResId, R.styleable.TextAppearance); + mDateTextSize = dateTextAppearance.getDimensionPixelSize( + R.styleable.TextAppearance_textSize, DEFAULT_DATE_TEXT_SIZE); + dateTextAppearance.recycle(); } /** - * This is here so we can identify single tap events and set the - * selected day correctly + * Invalidates all week views. */ - class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener { - @Override - public boolean onSingleTapUp(MotionEvent e) { - return true; + private void invalidateAllWeekViews() { + final int childCount = mListView.getChildCount(); + for (int i = 0; i < childCount; i++) { + View view = mListView.getChildAt(i); + view.invalidate(); } } - } - /** - * <p> - * This is a dynamic view for drawing a single week. It can be configured to - * display the week number, start the week on a given day, or show a reduced - * number of days. It is intended for use as a single view within a - * ListView. See {@link WeeksAdapter} for usage. - * </p> - */ - private class WeekView extends View { + /** + * Gets a calendar for locale bootstrapped with the value of a given calendar. + * + * @param oldCalendar The old calendar. + * @param locale The locale. + */ + private static Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) { + if (oldCalendar == null) { + return Calendar.getInstance(locale); + } else { + final long currentTimeMillis = oldCalendar.getTimeInMillis(); + Calendar newCalendar = Calendar.getInstance(locale); + newCalendar.setTimeInMillis(currentTimeMillis); + return newCalendar; + } + } - private final Rect mTempRect = new Rect(); + /** + * @return True if the <code>firstDate</code> is the same as the <code> + * secondDate</code>. + */ + private static boolean isSameDate(Calendar firstDate, Calendar secondDate) { + return (firstDate.get(Calendar.DAY_OF_YEAR) == secondDate.get(Calendar.DAY_OF_YEAR) + && firstDate.get(Calendar.YEAR) == secondDate.get(Calendar.YEAR)); + } - private final Paint mDrawPaint = new Paint(); + /** + * Creates a new adapter if necessary and sets up its parameters. + */ + private void setUpAdapter() { + if (mAdapter == null) { + mAdapter = new WeeksAdapter(mContext); + mAdapter.registerDataSetObserver(new DataSetObserver() { + @Override + public void onChanged() { + if (mOnDateChangeListener != null) { + Calendar selectedDay = mAdapter.getSelectedDay(); + mOnDateChangeListener.onSelectedDayChange(mDelegator, + selectedDay.get(Calendar.YEAR), + selectedDay.get(Calendar.MONTH), + selectedDay.get(Calendar.DAY_OF_MONTH)); + } + } + }); + mListView.setAdapter(mAdapter); + } - private final Paint mMonthNumDrawPaint = new Paint(); + // refresh the view with the new parameters + mAdapter.notifyDataSetChanged(); + } - // Cache the number strings so we don't have to recompute them each time - private String[] mDayNumbers; + /** + * Sets up the strings to be used by the header. + */ + private void setUpHeader() { + mDayLabels = new String[mDaysPerWeek]; + for (int i = mFirstDayOfWeek, count = mFirstDayOfWeek + mDaysPerWeek; i < count; i++) { + int calendarDay = (i > Calendar.SATURDAY) ? i - Calendar.SATURDAY : i; + mDayLabels[i - mFirstDayOfWeek] = DateUtils.getDayOfWeekString(calendarDay, + DateUtils.LENGTH_SHORTEST); + } - // Quick lookup for checking which days are in the focus month - private boolean[] mFocusDay; + TextView label = (TextView) mDayNamesHeader.getChildAt(0); + if (mShowWeekNumber) { + label.setVisibility(View.VISIBLE); + } else { + label.setVisibility(View.GONE); + } + for (int i = 1, count = mDayNamesHeader.getChildCount(); i < count; i++) { + label = (TextView) mDayNamesHeader.getChildAt(i); + if (mWeekDayTextAppearanceResId > -1) { + label.setTextAppearance(mContext, mWeekDayTextAppearanceResId); + } + if (i < mDaysPerWeek + 1) { + label.setText(mDayLabels[i - 1]); + label.setVisibility(View.VISIBLE); + } else { + label.setVisibility(View.GONE); + } + } + mDayNamesHeader.invalidate(); + } - // Whether this view has a focused day. - private boolean mHasFocusedDay; + /** + * Sets all the required fields for the list view. + */ + private void setUpListView() { + // Configure the listview + mListView.setDivider(null); + mListView.setItemsCanFocus(true); + mListView.setVerticalScrollBarEnabled(false); + mListView.setOnScrollListener(new OnScrollListener() { + public void onScrollStateChanged(AbsListView view, int scrollState) { + LegacyCalendarViewDelegate.this.onScrollStateChanged(view, scrollState); + } - // Whether this view has only focused days. - private boolean mHasUnfocusedDay; + public void onScroll( + AbsListView view, int firstVisibleItem, int visibleItemCount, + int totalItemCount) { + LegacyCalendarViewDelegate.this.onScroll(view, firstVisibleItem, + visibleItemCount, totalItemCount); + } + }); + // Make the scrolling behavior nicer + mListView.setFriction(mFriction); + mListView.setVelocityScale(mVelocityScale); + } - // The first day displayed by this item - private Calendar mFirstDay; + /** + * This moves to the specified time in the view. If the time is not already + * in range it will move the list so that the first of the month containing + * the time is at the top of the view. If the new time is already in view + * the list will not be scrolled unless forceScroll is true. This time may + * optionally be highlighted as selected as well. + * + * @param date The time to move to. + * @param animate Whether to scroll to the given time or just redraw at the + * new location. + * @param setSelected Whether to set the given time as selected. + * @param forceScroll Whether to recenter even if the time is already + * visible. + * + * @throws IllegalArgumentException of the provided date is before the + * range start of after the range end. + */ + private void goTo(Calendar date, boolean animate, boolean setSelected, + boolean forceScroll) { + if (date.before(mMinDate) || date.after(mMaxDate)) { + throw new IllegalArgumentException("Time not between " + mMinDate.getTime() + + " and " + mMaxDate.getTime()); + } + // Find the first and last entirely visible weeks + int firstFullyVisiblePosition = mListView.getFirstVisiblePosition(); + View firstChild = mListView.getChildAt(0); + if (firstChild != null && firstChild.getTop() < 0) { + firstFullyVisiblePosition++; + } + int lastFullyVisiblePosition = firstFullyVisiblePosition + mShownWeekCount - 1; + if (firstChild != null && firstChild.getTop() > mBottomBuffer) { + lastFullyVisiblePosition--; + } + if (setSelected) { + mAdapter.setSelectedDay(date); + } + // Get the week we're going to + int position = getWeeksSinceMinDate(date); - // The month of the first day in this week - private int mMonthOfFirstWeekDay = -1; + // Check if the selected day is now outside of our visible range + // and if so scroll to the month that contains it + if (position < firstFullyVisiblePosition || position > lastFullyVisiblePosition + || forceScroll) { + mFirstDayOfMonth.setTimeInMillis(date.getTimeInMillis()); + mFirstDayOfMonth.set(Calendar.DAY_OF_MONTH, 1); - // The month of the last day in this week - private int mLastWeekDayMonth = -1; + setMonthDisplayed(mFirstDayOfMonth); - // The position of this week, equivalent to weeks since the week of Jan - // 1st, 1900 - private int mWeek = -1; + // the earliest time we can scroll to is the min date + if (mFirstDayOfMonth.before(mMinDate)) { + position = 0; + } else { + position = getWeeksSinceMinDate(mFirstDayOfMonth); + } - // Quick reference to the width of this view, matches parent - private int mWidth; + mPreviousScrollState = OnScrollListener.SCROLL_STATE_FLING; + if (animate) { + mListView.smoothScrollToPositionFromTop(position, mListScrollTopOffset, + GOTO_SCROLL_DURATION); + } else { + mListView.setSelectionFromTop(position, mListScrollTopOffset); + // Perform any after scroll operations that are needed + onScrollStateChanged(mListView, OnScrollListener.SCROLL_STATE_IDLE); + } + } else if (setSelected) { + // Otherwise just set the selection + setMonthDisplayed(date); + } + } - // The height this view should draw at in pixels, set by height param - private int mHeight; + /** + * Parses the given <code>date</code> and in case of success sets + * the result to the <code>outDate</code>. + * + * @return True if the date was parsed. + */ + private boolean parseDate(String date, Calendar outDate) { + try { + outDate.setTime(mDateFormat.parse(date)); + return true; + } catch (ParseException e) { + Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT); + return false; + } + } - // If this view contains the selected day - private boolean mHasSelectedDay = false; + /** + * Called when a <code>view</code> transitions to a new <code>scrollState + * </code>. + */ + private void onScrollStateChanged(AbsListView view, int scrollState) { + mScrollStateChangedRunnable.doScrollStateChange(view, scrollState); + } - // Which day is selected [0-6] or -1 if no day is selected - private int mSelectedDay = -1; + /** + * Updates the title and selected month if the <code>view</code> has moved to a new + * month. + */ + private void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, + int totalItemCount) { + WeekView child = (WeekView) view.getChildAt(0); + if (child == null) { + return; + } - // The number of days + a spot for week number if it is displayed - private int mNumCells; + // Figure out where we are + long currScroll = + view.getFirstVisiblePosition() * child.getHeight() - child.getBottom(); - // The left edge of the selected day - private int mSelectedLeft = -1; + // If we have moved since our last call update the direction + if (currScroll < mPreviousScrollPosition) { + mIsScrollingUp = true; + } else if (currScroll > mPreviousScrollPosition) { + mIsScrollingUp = false; + } else { + return; + } - // The right edge of the selected day - private int mSelectedRight = -1; + // Use some hysteresis for checking which month to highlight. This + // causes the month to transition when two full weeks of a month are + // visible when scrolling up, and when the first day in a month reaches + // the top of the screen when scrolling down. + int offset = child.getBottom() < mWeekMinVisibleHeight ? 1 : 0; + if (mIsScrollingUp) { + child = (WeekView) view.getChildAt(SCROLL_HYST_WEEKS + offset); + } else if (offset != 0) { + child = (WeekView) view.getChildAt(offset); + } - public WeekView(Context context) { - super(context); + // Find out which month we're moving into + int month; + if (mIsScrollingUp) { + month = child.getMonthOfFirstWeekDay(); + } else { + month = child.getMonthOfLastWeekDay(); + } - // Sets up any standard paints that will be used - initilaizePaints(); - } + // And how it relates to our current highlighted month + int monthDiff; + if (mCurrentMonthDisplayed == 11 && month == 0) { + monthDiff = 1; + } else if (mCurrentMonthDisplayed == 0 && month == 11) { + monthDiff = -1; + } else { + monthDiff = month - mCurrentMonthDisplayed; + } - /** - * Initializes this week view. - * - * @param weekNumber The number of the week this view represents. The - * week number is a zero based index of the weeks since - * {@link CalendarView#getMinDate()}. - * @param selectedWeekDay The selected day of the week from 0 to 6, -1 if no - * selected day. - * @param focusedMonth The month that is currently in focus i.e. - * highlighted. - */ - public void init(int weekNumber, int selectedWeekDay, int focusedMonth) { - mSelectedDay = selectedWeekDay; - mHasSelectedDay = mSelectedDay != -1; - mNumCells = mShowWeekNumber ? mDaysPerWeek + 1 : mDaysPerWeek; - mWeek = weekNumber; - mTempDate.setTimeInMillis(mMinDate.getTimeInMillis()); - - mTempDate.add(Calendar.WEEK_OF_YEAR, mWeek); - mTempDate.setFirstDayOfWeek(mFirstDayOfWeek); - - // Allocate space for caching the day numbers and focus values - mDayNumbers = new String[mNumCells]; - mFocusDay = new boolean[mNumCells]; - - // If we're showing the week number calculate it based on Monday - int i = 0; - if (mShowWeekNumber) { - mDayNumbers[0] = String.format(Locale.getDefault(), "%d", - mTempDate.get(Calendar.WEEK_OF_YEAR)); - i++; - } - - // Now adjust our starting day based on the start day of the week - int diff = mFirstDayOfWeek - mTempDate.get(Calendar.DAY_OF_WEEK); - mTempDate.add(Calendar.DAY_OF_MONTH, diff); - - mFirstDay = (Calendar) mTempDate.clone(); - mMonthOfFirstWeekDay = mTempDate.get(Calendar.MONTH); - - mHasUnfocusedDay = true; - for (; i < mNumCells; i++) { - final boolean isFocusedDay = (mTempDate.get(Calendar.MONTH) == focusedMonth); - mFocusDay[i] = isFocusedDay; - mHasFocusedDay |= isFocusedDay; - mHasUnfocusedDay &= !isFocusedDay; - // do not draw dates outside the valid range to avoid user confusion - if (mTempDate.before(mMinDate) || mTempDate.after(mMaxDate)) { - mDayNumbers[i] = ""; + // Only switch months if we're scrolling away from the currently + // selected month + if ((!mIsScrollingUp && monthDiff > 0) || (mIsScrollingUp && monthDiff < 0)) { + Calendar firstDay = child.getFirstDay(); + if (mIsScrollingUp) { + firstDay.add(Calendar.DAY_OF_MONTH, -DAYS_PER_WEEK); } else { - mDayNumbers[i] = String.format(Locale.getDefault(), "%d", - mTempDate.get(Calendar.DAY_OF_MONTH)); + firstDay.add(Calendar.DAY_OF_MONTH, DAYS_PER_WEEK); } - mTempDate.add(Calendar.DAY_OF_MONTH, 1); - } - // We do one extra add at the end of the loop, if that pushed us to - // new month undo it - if (mTempDate.get(Calendar.DAY_OF_MONTH) == 1) { - mTempDate.add(Calendar.DAY_OF_MONTH, -1); + setMonthDisplayed(firstDay); } - mLastWeekDayMonth = mTempDate.get(Calendar.MONTH); - - updateSelectionPositions(); + mPreviousScrollPosition = currScroll; + mPreviousScrollState = mCurrentScrollState; } /** - * Initialize the paint instances. - */ - private void initilaizePaints() { - mDrawPaint.setFakeBoldText(false); - mDrawPaint.setAntiAlias(true); - mDrawPaint.setStyle(Style.FILL); - - mMonthNumDrawPaint.setFakeBoldText(true); - mMonthNumDrawPaint.setAntiAlias(true); - mMonthNumDrawPaint.setStyle(Style.FILL); - mMonthNumDrawPaint.setTextAlign(Align.CENTER); - mMonthNumDrawPaint.setTextSize(mDateTextSize); - } - - /** - * Returns the month of the first day in this week. + * Sets the month displayed at the top of this view based on time. Override + * to add custom events when the title is changed. * - * @return The month the first day of this view is in. + * @param calendar A day in the new focus month. */ - public int getMonthOfFirstWeekDay() { - return mMonthOfFirstWeekDay; + private void setMonthDisplayed(Calendar calendar) { + mCurrentMonthDisplayed = calendar.get(Calendar.MONTH); + mAdapter.setFocusMonth(mCurrentMonthDisplayed); + final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_MONTH_DAY + | DateUtils.FORMAT_SHOW_YEAR; + final long millis = calendar.getTimeInMillis(); + String newMonthName = DateUtils.formatDateRange(mContext, millis, millis, flags); + mMonthName.setText(newMonthName); + mMonthName.invalidate(); } /** - * Returns the month of the last day in this week - * - * @return The month the last day of this view is in + * @return Returns the number of weeks between the current <code>date</code> + * and the <code>mMinDate</code>. */ - public int getMonthOfLastWeekDay() { - return mLastWeekDayMonth; + private int getWeeksSinceMinDate(Calendar date) { + if (date.before(mMinDate)) { + throw new IllegalArgumentException("fromDate: " + mMinDate.getTime() + + " does not precede toDate: " + date.getTime()); + } + long endTimeMillis = date.getTimeInMillis() + + date.getTimeZone().getOffset(date.getTimeInMillis()); + long startTimeMillis = mMinDate.getTimeInMillis() + + mMinDate.getTimeZone().getOffset(mMinDate.getTimeInMillis()); + long dayOffsetMillis = (mMinDate.get(Calendar.DAY_OF_WEEK) - mFirstDayOfWeek) + * MILLIS_IN_DAY; + return (int) ((endTimeMillis - startTimeMillis + dayOffsetMillis) / MILLIS_IN_WEEK); } /** - * Returns the first day in this view. - * - * @return The first day in the view. + * Command responsible for acting upon scroll state changes. */ - public Calendar getFirstDay() { - return mFirstDay; + private class ScrollStateRunnable implements Runnable { + private AbsListView mView; + + private int mNewState; + + /** + * Sets up the runnable with a short delay in case the scroll state + * immediately changes again. + * + * @param view The list view that changed state + * @param scrollState The new state it changed to + */ + public void doScrollStateChange(AbsListView view, int scrollState) { + mView = view; + mNewState = scrollState; + mDelegator.removeCallbacks(this); + mDelegator.postDelayed(this, SCROLL_CHANGE_DELAY); + } + + public void run() { + mCurrentScrollState = mNewState; + // Fix the position after a scroll or a fling ends + if (mNewState == OnScrollListener.SCROLL_STATE_IDLE + && mPreviousScrollState != OnScrollListener.SCROLL_STATE_IDLE) { + View child = mView.getChildAt(0); + if (child == null) { + // The view is no longer visible, just return + return; + } + int dist = child.getBottom() - mListScrollTopOffset; + if (dist > mListScrollTopOffset) { + if (mIsScrollingUp) { + mView.smoothScrollBy(dist - child.getHeight(), + ADJUSTMENT_SCROLL_DURATION); + } else { + mView.smoothScrollBy(dist, ADJUSTMENT_SCROLL_DURATION); + } + } + } + mPreviousScrollState = mNewState; + } } /** - * Calculates the day that the given x position is in, accounting for - * week number. - * - * @param x The x position of the touch event. - * @return True if a day was found for the given location. + * <p> + * This is a specialized adapter for creating a list of weeks with + * selectable days. It can be configured to display the week number, start + * the week on a given day, show a reduced number of days, or display an + * arbitrary number of weeks at a time. + * </p> */ - public boolean getDayFromLocation(float x, Calendar outCalendar) { - final boolean isLayoutRtl = isLayoutRtl(); + private class WeeksAdapter extends BaseAdapter implements OnTouchListener { - int start; - int end; + private int mSelectedWeek; - if (isLayoutRtl) { - start = 0; - end = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth; - } else { - start = mShowWeekNumber ? mWidth / mNumCells : 0; - end = mWidth; + private GestureDetector mGestureDetector; + + private int mFocusedMonth; + + private final Calendar mSelectedDate = Calendar.getInstance(); + + private int mTotalWeekCount; + + public WeeksAdapter(Context context) { + mContext = context; + mGestureDetector = new GestureDetector(mContext, new CalendarGestureListener()); + init(); } - if (x < start || x > end) { - outCalendar.clear(); - return false; + /** + * Set up the gesture detector and selected time + */ + private void init() { + mSelectedWeek = getWeeksSinceMinDate(mSelectedDate); + mTotalWeekCount = getWeeksSinceMinDate(mMaxDate); + if (mMinDate.get(Calendar.DAY_OF_WEEK) != mFirstDayOfWeek + || mMaxDate.get(Calendar.DAY_OF_WEEK) != mFirstDayOfWeek) { + mTotalWeekCount++; + } + } + + /** + * Updates the selected day and related parameters. + * + * @param selectedDay The time to highlight + */ + public void setSelectedDay(Calendar selectedDay) { + if (selectedDay.get(Calendar.DAY_OF_YEAR) == mSelectedDate.get(Calendar.DAY_OF_YEAR) + && selectedDay.get(Calendar.YEAR) == mSelectedDate.get(Calendar.YEAR)) { + return; + } + mSelectedDate.setTimeInMillis(selectedDay.getTimeInMillis()); + mSelectedWeek = getWeeksSinceMinDate(mSelectedDate); + mFocusedMonth = mSelectedDate.get(Calendar.MONTH); + notifyDataSetChanged(); + } + + /** + * @return The selected day of month. + */ + public Calendar getSelectedDay() { + return mSelectedDate; } - // Selection is (x - start) / (pixels/day) which is (x - start) * day / pixels - int dayPosition = (int) ((x - start) * mDaysPerWeek / (end - start)); + @Override + public int getCount() { + return mTotalWeekCount; + } - if (isLayoutRtl) { - dayPosition = mDaysPerWeek - 1 - dayPosition; + @Override + public Object getItem(int position) { + return null; } - outCalendar.setTimeInMillis(mFirstDay.getTimeInMillis()); - outCalendar.add(Calendar.DAY_OF_MONTH, dayPosition); + @Override + public long getItemId(int position) { + return position; + } - return true; - } + @Override + public View getView(int position, View convertView, ViewGroup parent) { + WeekView weekView = null; + if (convertView != null) { + weekView = (WeekView) convertView; + } else { + weekView = new WeekView(mContext); + android.widget.AbsListView.LayoutParams params = + new android.widget.AbsListView.LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + weekView.setLayoutParams(params); + weekView.setClickable(true); + weekView.setOnTouchListener(this); + } - @Override - protected void onDraw(Canvas canvas) { - drawBackground(canvas); - drawWeekNumbersAndDates(canvas); - drawWeekSeparators(canvas); - drawSelectedDateVerticalBars(canvas); - } + int selectedWeekDay = (mSelectedWeek == position) ? mSelectedDate.get( + Calendar.DAY_OF_WEEK) : -1; + weekView.init(position, selectedWeekDay, mFocusedMonth); - /** - * This draws the selection highlight if a day is selected in this week. - * - * @param canvas The canvas to draw on - */ - private void drawBackground(Canvas canvas) { - if (!mHasSelectedDay) { - return; + return weekView; } - mDrawPaint.setColor(mSelectedWeekBackgroundColor); - mTempRect.top = mWeekSeperatorLineWidth; - mTempRect.bottom = mHeight; + /** + * Changes which month is in focus and updates the view. + * + * @param month The month to show as in focus [0-11] + */ + public void setFocusMonth(int month) { + if (mFocusedMonth == month) { + return; + } + mFocusedMonth = month; + notifyDataSetChanged(); + } - final boolean isLayoutRtl = isLayoutRtl(); + @Override + public boolean onTouch(View v, MotionEvent event) { + if (mListView.isEnabled() && mGestureDetector.onTouchEvent(event)) { + WeekView weekView = (WeekView) v; + // if we cannot find a day for the given location we are done + if (!weekView.getDayFromLocation(event.getX(), mTempDate)) { + return true; + } + // it is possible that the touched day is outside the valid range + // we draw whole weeks but range end can fall not on the week end + if (mTempDate.before(mMinDate) || mTempDate.after(mMaxDate)) { + return true; + } + onDateTapped(mTempDate); + return true; + } + return false; + } - if (isLayoutRtl) { - mTempRect.left = 0; - mTempRect.right = mSelectedLeft - 2; - } else { - mTempRect.left = mShowWeekNumber ? mWidth / mNumCells : 0; - mTempRect.right = mSelectedLeft - 2; + /** + * Maintains the same hour/min/sec but moves the day to the tapped day. + * + * @param day The day that was tapped + */ + private void onDateTapped(Calendar day) { + setSelectedDay(day); + setMonthDisplayed(day); } - canvas.drawRect(mTempRect, mDrawPaint); - if (isLayoutRtl) { - mTempRect.left = mSelectedRight + 3; - mTempRect.right = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth; - } else { - mTempRect.left = mSelectedRight + 3; - mTempRect.right = mWidth; + /** + * This is here so we can identify single tap events and set the + * selected day correctly + */ + class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onSingleTapUp(MotionEvent e) { + return true; + } } - canvas.drawRect(mTempRect, mDrawPaint); } /** - * Draws the week and month day numbers for this week. - * - * @param canvas The canvas to draw on + * <p> + * This is a dynamic view for drawing a single week. It can be configured to + * display the week number, start the week on a given day, or show a reduced + * number of days. It is intended for use as a single view within a + * ListView. See {@link WeeksAdapter} for usage. + * </p> */ - private void drawWeekNumbersAndDates(Canvas canvas) { - final float textHeight = mDrawPaint.getTextSize(); - final int y = (int) ((mHeight + textHeight) / 2) - mWeekSeperatorLineWidth; - final int nDays = mNumCells; - final int divisor = 2 * nDays; - - mDrawPaint.setTextAlign(Align.CENTER); - mDrawPaint.setTextSize(mDateTextSize); - - int i = 0; - - if (isLayoutRtl()) { - for (; i < nDays - 1; i++) { - mMonthNumDrawPaint.setColor(mFocusDay[i] ? mFocusedMonthDateColor - : mUnfocusedMonthDateColor); - int x = (2 * i + 1) * mWidth / divisor; - canvas.drawText(mDayNumbers[nDays - 1 - i], x, y, mMonthNumDrawPaint); - } - if (mShowWeekNumber) { - mDrawPaint.setColor(mWeekNumberColor); - int x = mWidth - mWidth / divisor; - canvas.drawText(mDayNumbers[0], x, y, mDrawPaint); - } - } else { + private class WeekView extends View { + + private final Rect mTempRect = new Rect(); + + private final Paint mDrawPaint = new Paint(); + + private final Paint mMonthNumDrawPaint = new Paint(); + + // Cache the number strings so we don't have to recompute them each time + private String[] mDayNumbers; + + // Quick lookup for checking which days are in the focus month + private boolean[] mFocusDay; + + // Whether this view has a focused day. + private boolean mHasFocusedDay; + + // Whether this view has only focused days. + private boolean mHasUnfocusedDay; + + // The first day displayed by this item + private Calendar mFirstDay; + + // The month of the first day in this week + private int mMonthOfFirstWeekDay = -1; + + // The month of the last day in this week + private int mLastWeekDayMonth = -1; + + // The position of this week, equivalent to weeks since the week of Jan + // 1st, 1900 + private int mWeek = -1; + + // Quick reference to the width of this view, matches parent + private int mWidth; + + // The height this view should draw at in pixels, set by height param + private int mHeight; + + // If this view contains the selected day + private boolean mHasSelectedDay = false; + + // Which day is selected [0-6] or -1 if no day is selected + private int mSelectedDay = -1; + + // The number of days + a spot for week number if it is displayed + private int mNumCells; + + // The left edge of the selected day + private int mSelectedLeft = -1; + + // The right edge of the selected day + private int mSelectedRight = -1; + + public WeekView(Context context) { + super(context); + + // Sets up any standard paints that will be used + initilaizePaints(); + } + + /** + * Initializes this week view. + * + * @param weekNumber The number of the week this view represents. The + * week number is a zero based index of the weeks since + * {@link CalendarView#getMinDate()}. + * @param selectedWeekDay The selected day of the week from 0 to 6, -1 if no + * selected day. + * @param focusedMonth The month that is currently in focus i.e. + * highlighted. + */ + public void init(int weekNumber, int selectedWeekDay, int focusedMonth) { + mSelectedDay = selectedWeekDay; + mHasSelectedDay = mSelectedDay != -1; + mNumCells = mShowWeekNumber ? mDaysPerWeek + 1 : mDaysPerWeek; + mWeek = weekNumber; + mTempDate.setTimeInMillis(mMinDate.getTimeInMillis()); + + mTempDate.add(Calendar.WEEK_OF_YEAR, mWeek); + mTempDate.setFirstDayOfWeek(mFirstDayOfWeek); + + // Allocate space for caching the day numbers and focus values + mDayNumbers = new String[mNumCells]; + mFocusDay = new boolean[mNumCells]; + + // If we're showing the week number calculate it based on Monday + int i = 0; if (mShowWeekNumber) { - mDrawPaint.setColor(mWeekNumberColor); - int x = mWidth / divisor; - canvas.drawText(mDayNumbers[0], x, y, mDrawPaint); + mDayNumbers[0] = String.format(Locale.getDefault(), "%d", + mTempDate.get(Calendar.WEEK_OF_YEAR)); i++; } - for (; i < nDays; i++) { - mMonthNumDrawPaint.setColor(mFocusDay[i] ? mFocusedMonthDateColor - : mUnfocusedMonthDateColor); - int x = (2 * i + 1) * mWidth / divisor; - canvas.drawText(mDayNumbers[i], x, y, mMonthNumDrawPaint); + + // Now adjust our starting day based on the start day of the week + int diff = mFirstDayOfWeek - mTempDate.get(Calendar.DAY_OF_WEEK); + mTempDate.add(Calendar.DAY_OF_MONTH, diff); + + mFirstDay = (Calendar) mTempDate.clone(); + mMonthOfFirstWeekDay = mTempDate.get(Calendar.MONTH); + + mHasUnfocusedDay = true; + for (; i < mNumCells; i++) { + final boolean isFocusedDay = (mTempDate.get(Calendar.MONTH) == focusedMonth); + mFocusDay[i] = isFocusedDay; + mHasFocusedDay |= isFocusedDay; + mHasUnfocusedDay &= !isFocusedDay; + // do not draw dates outside the valid range to avoid user confusion + if (mTempDate.before(mMinDate) || mTempDate.after(mMaxDate)) { + mDayNumbers[i] = ""; + } else { + mDayNumbers[i] = String.format(Locale.getDefault(), "%d", + mTempDate.get(Calendar.DAY_OF_MONTH)); + } + mTempDate.add(Calendar.DAY_OF_MONTH, 1); } - } - } + // We do one extra add at the end of the loop, if that pushed us to + // new month undo it + if (mTempDate.get(Calendar.DAY_OF_MONTH) == 1) { + mTempDate.add(Calendar.DAY_OF_MONTH, -1); + } + mLastWeekDayMonth = mTempDate.get(Calendar.MONTH); - /** - * Draws a horizontal line for separating the weeks. - * - * @param canvas The canvas to draw on. - */ - private void drawWeekSeparators(Canvas canvas) { - // If it is the topmost fully visible child do not draw separator line - int firstFullyVisiblePosition = mListView.getFirstVisiblePosition(); - if (mListView.getChildAt(0).getTop() < 0) { - firstFullyVisiblePosition++; + updateSelectionPositions(); } - if (firstFullyVisiblePosition == mWeek) { - return; + + /** + * Initialize the paint instances. + */ + private void initilaizePaints() { + mDrawPaint.setFakeBoldText(false); + mDrawPaint.setAntiAlias(true); + mDrawPaint.setStyle(Style.FILL); + + mMonthNumDrawPaint.setFakeBoldText(true); + mMonthNumDrawPaint.setAntiAlias(true); + mMonthNumDrawPaint.setStyle(Style.FILL); + mMonthNumDrawPaint.setTextAlign(Align.CENTER); + mMonthNumDrawPaint.setTextSize(mDateTextSize); } - mDrawPaint.setColor(mWeekSeparatorLineColor); - mDrawPaint.setStrokeWidth(mWeekSeperatorLineWidth); - float startX; - float stopX; - if (isLayoutRtl()) { - startX = 0; - stopX = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth; - } else { - startX = mShowWeekNumber ? mWidth / mNumCells : 0; - stopX = mWidth; + + /** + * Returns the month of the first day in this week. + * + * @return The month the first day of this view is in. + */ + public int getMonthOfFirstWeekDay() { + return mMonthOfFirstWeekDay; } - canvas.drawLine(startX, 0, stopX, 0, mDrawPaint); - } - /** - * Draws the selected date bars if this week has a selected day. - * - * @param canvas The canvas to draw on - */ - private void drawSelectedDateVerticalBars(Canvas canvas) { - if (!mHasSelectedDay) { - return; + /** + * Returns the month of the last day in this week + * + * @return The month the last day of this view is in + */ + public int getMonthOfLastWeekDay() { + return mLastWeekDayMonth; } - mSelectedDateVerticalBar.setBounds(mSelectedLeft - mSelectedDateVerticalBarWidth / 2, - mWeekSeperatorLineWidth, - mSelectedLeft + mSelectedDateVerticalBarWidth / 2, mHeight); - mSelectedDateVerticalBar.draw(canvas); - mSelectedDateVerticalBar.setBounds(mSelectedRight - mSelectedDateVerticalBarWidth / 2, - mWeekSeperatorLineWidth, - mSelectedRight + mSelectedDateVerticalBarWidth / 2, mHeight); - mSelectedDateVerticalBar.draw(canvas); - } - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - mWidth = w; - updateSelectionPositions(); - } + /** + * Returns the first day in this view. + * + * @return The first day in the view. + */ + public Calendar getFirstDay() { + return mFirstDay; + } - /** - * This calculates the positions for the selected day lines. - */ - private void updateSelectionPositions() { - if (mHasSelectedDay) { + /** + * Calculates the day that the given x position is in, accounting for + * week number. + * + * @param x The x position of the touch event. + * @return True if a day was found for the given location. + */ + public boolean getDayFromLocation(float x, Calendar outCalendar) { final boolean isLayoutRtl = isLayoutRtl(); - int selectedPosition = mSelectedDay - mFirstDayOfWeek; - if (selectedPosition < 0) { - selectedPosition += 7; + + int start; + int end; + + if (isLayoutRtl) { + start = 0; + end = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth; + } else { + start = mShowWeekNumber ? mWidth / mNumCells : 0; + end = mWidth; } - if (mShowWeekNumber && !isLayoutRtl) { - selectedPosition++; + + if (x < start || x > end) { + outCalendar.clear(); + return false; } + + // Selection is (x - start) / (pixels/day) which is (x - start) * day / pixels + int dayPosition = (int) ((x - start) * mDaysPerWeek / (end - start)); + if (isLayoutRtl) { - mSelectedLeft = (mDaysPerWeek - 1 - selectedPosition) * mWidth / mNumCells; + dayPosition = mDaysPerWeek - 1 - dayPosition; + } + outCalendar.setTimeInMillis(mFirstDay.getTimeInMillis()); + outCalendar.add(Calendar.DAY_OF_MONTH, dayPosition); + + return true; + } + + @Override + protected void onDraw(Canvas canvas) { + drawBackground(canvas); + drawWeekNumbersAndDates(canvas); + drawWeekSeparators(canvas); + drawSelectedDateVerticalBars(canvas); + } + + /** + * This draws the selection highlight if a day is selected in this week. + * + * @param canvas The canvas to draw on + */ + private void drawBackground(Canvas canvas) { + if (!mHasSelectedDay) { + return; + } + mDrawPaint.setColor(mSelectedWeekBackgroundColor); + + mTempRect.top = mWeekSeperatorLineWidth; + mTempRect.bottom = mHeight; + + final boolean isLayoutRtl = isLayoutRtl(); + + if (isLayoutRtl) { + mTempRect.left = 0; + mTempRect.right = mSelectedLeft - 2; + } else { + mTempRect.left = mShowWeekNumber ? mWidth / mNumCells : 0; + mTempRect.right = mSelectedLeft - 2; + } + canvas.drawRect(mTempRect, mDrawPaint); + + if (isLayoutRtl) { + mTempRect.left = mSelectedRight + 3; + mTempRect.right = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth; } else { - mSelectedLeft = selectedPosition * mWidth / mNumCells; + mTempRect.left = mSelectedRight + 3; + mTempRect.right = mWidth; } - mSelectedRight = mSelectedLeft + mWidth / mNumCells; + canvas.drawRect(mTempRect, mDrawPaint); } - } - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - mHeight = (mListView.getHeight() - mListView.getPaddingTop() - mListView - .getPaddingBottom()) / mShownWeekCount; - setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mHeight); + /** + * Draws the week and month day numbers for this week. + * + * @param canvas The canvas to draw on + */ + private void drawWeekNumbersAndDates(Canvas canvas) { + final float textHeight = mDrawPaint.getTextSize(); + final int y = (int) ((mHeight + textHeight) / 2) - mWeekSeperatorLineWidth; + final int nDays = mNumCells; + final int divisor = 2 * nDays; + + mDrawPaint.setTextAlign(Align.CENTER); + mDrawPaint.setTextSize(mDateTextSize); + + int i = 0; + + if (isLayoutRtl()) { + for (; i < nDays - 1; i++) { + mMonthNumDrawPaint.setColor(mFocusDay[i] ? mFocusedMonthDateColor + : mUnfocusedMonthDateColor); + int x = (2 * i + 1) * mWidth / divisor; + canvas.drawText(mDayNumbers[nDays - 1 - i], x, y, mMonthNumDrawPaint); + } + if (mShowWeekNumber) { + mDrawPaint.setColor(mWeekNumberColor); + int x = mWidth - mWidth / divisor; + canvas.drawText(mDayNumbers[0], x, y, mDrawPaint); + } + } else { + if (mShowWeekNumber) { + mDrawPaint.setColor(mWeekNumberColor); + int x = mWidth / divisor; + canvas.drawText(mDayNumbers[0], x, y, mDrawPaint); + i++; + } + for (; i < nDays; i++) { + mMonthNumDrawPaint.setColor(mFocusDay[i] ? mFocusedMonthDateColor + : mUnfocusedMonthDateColor); + int x = (2 * i + 1) * mWidth / divisor; + canvas.drawText(mDayNumbers[i], x, y, mMonthNumDrawPaint); + } + } + } + + /** + * Draws a horizontal line for separating the weeks. + * + * @param canvas The canvas to draw on. + */ + private void drawWeekSeparators(Canvas canvas) { + // If it is the topmost fully visible child do not draw separator line + int firstFullyVisiblePosition = mListView.getFirstVisiblePosition(); + if (mListView.getChildAt(0).getTop() < 0) { + firstFullyVisiblePosition++; + } + if (firstFullyVisiblePosition == mWeek) { + return; + } + mDrawPaint.setColor(mWeekSeparatorLineColor); + mDrawPaint.setStrokeWidth(mWeekSeperatorLineWidth); + float startX; + float stopX; + if (isLayoutRtl()) { + startX = 0; + stopX = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth; + } else { + startX = mShowWeekNumber ? mWidth / mNumCells : 0; + stopX = mWidth; + } + canvas.drawLine(startX, 0, stopX, 0, mDrawPaint); + } + + /** + * Draws the selected date bars if this week has a selected day. + * + * @param canvas The canvas to draw on + */ + private void drawSelectedDateVerticalBars(Canvas canvas) { + if (!mHasSelectedDay) { + return; + } + mSelectedDateVerticalBar.setBounds( + mSelectedLeft - mSelectedDateVerticalBarWidth / 2, + mWeekSeperatorLineWidth, + mSelectedLeft + mSelectedDateVerticalBarWidth / 2, + mHeight); + mSelectedDateVerticalBar.draw(canvas); + mSelectedDateVerticalBar.setBounds( + mSelectedRight - mSelectedDateVerticalBarWidth / 2, + mWeekSeperatorLineWidth, + mSelectedRight + mSelectedDateVerticalBarWidth / 2, + mHeight); + mSelectedDateVerticalBar.draw(canvas); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + mWidth = w; + updateSelectionPositions(); + } + + /** + * This calculates the positions for the selected day lines. + */ + private void updateSelectionPositions() { + if (mHasSelectedDay) { + final boolean isLayoutRtl = isLayoutRtl(); + int selectedPosition = mSelectedDay - mFirstDayOfWeek; + if (selectedPosition < 0) { + selectedPosition += 7; + } + if (mShowWeekNumber && !isLayoutRtl) { + selectedPosition++; + } + if (isLayoutRtl) { + mSelectedLeft = (mDaysPerWeek - 1 - selectedPosition) * mWidth / mNumCells; + + } else { + mSelectedLeft = selectedPosition * mWidth / mNumCells; + } + mSelectedRight = mSelectedLeft + mWidth / mNumCells; + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + mHeight = (mListView.getHeight() - mListView.getPaddingTop() - mListView + .getPaddingBottom()) / mShownWeekCount; + setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mHeight); + } } + } + } diff --git a/core/java/android/widget/CheckBox.java b/core/java/android/widget/CheckBox.java index f1804f8..71438c9 100644 --- a/core/java/android/widget/CheckBox.java +++ b/core/java/android/widget/CheckBox.java @@ -64,8 +64,12 @@ public class CheckBox extends CompoundButton { this(context, attrs, com.android.internal.R.attr.checkboxStyle); } - public CheckBox(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public CheckBox(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public CheckBox(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); } @Override diff --git a/core/java/android/widget/CheckedTextView.java b/core/java/android/widget/CheckedTextView.java index 5c10a77..78b1b75 100644 --- a/core/java/android/widget/CheckedTextView.java +++ b/core/java/android/widget/CheckedTextView.java @@ -58,11 +58,15 @@ public class CheckedTextView extends TextView implements Checkable { this(context, attrs, R.attr.checkedTextViewStyle); } - public CheckedTextView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public CheckedTextView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public CheckedTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes(attrs, - R.styleable.CheckedTextView, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.CheckedTextView, defStyleAttr, defStyleRes); Drawable d = a.getDrawable(R.styleable.CheckedTextView_checkMark); if (d != null) { diff --git a/core/java/android/widget/Chronometer.java b/core/java/android/widget/Chronometer.java index b7a126e..f94789d 100644 --- a/core/java/android/widget/Chronometer.java +++ b/core/java/android/widget/Chronometer.java @@ -18,14 +18,12 @@ package android.widget; import android.content.Context; import android.content.res.TypedArray; -import android.graphics.Canvas; import android.os.Handler; import android.os.Message; import android.os.SystemClock; import android.text.format.DateUtils; import android.util.AttributeSet; import android.util.Log; -import android.util.Slog; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; @@ -96,12 +94,15 @@ public class Chronometer extends TextView { * Initialize with standard view layout information and style. * Sets the base to the current time. */ - public Chronometer(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public Chronometer(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public Chronometer(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes( - attrs, - com.android.internal.R.styleable.Chronometer, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.Chronometer, defStyleAttr, defStyleRes); setFormat(a.getString(com.android.internal.R.styleable.Chronometer_format)); a.recycle(); diff --git a/core/java/android/widget/CompoundButton.java b/core/java/android/widget/CompoundButton.java index 082ff3d..bdb5046 100644 --- a/core/java/android/widget/CompoundButton.java +++ b/core/java/android/widget/CompoundButton.java @@ -64,12 +64,15 @@ public abstract class CompoundButton extends Button implements Checkable { this(context, attrs, 0); } - public CompoundButton(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public CompoundButton(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public CompoundButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = - context.obtainStyledAttributes( - attrs, com.android.internal.R.styleable.CompoundButton, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.CompoundButton, defStyleAttr, defStyleRes); Drawable d = a.getDrawable(com.android.internal.R.styleable.CompoundButton_button); if (d != null) { diff --git a/core/java/android/widget/DatePicker.java b/core/java/android/widget/DatePicker.java index d03161e..265dbcd 100644 --- a/core/java/android/widget/DatePicker.java +++ b/core/java/android/widget/DatePicker.java @@ -24,7 +24,6 @@ import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import android.text.InputType; -import android.text.format.DateFormat; import android.text.format.DateUtils; import android.util.AttributeSet; import android.util.Log; @@ -76,53 +75,7 @@ public class DatePicker extends FrameLayout { private static final String LOG_TAG = DatePicker.class.getSimpleName(); - private static final String DATE_FORMAT = "MM/dd/yyyy"; - - private static final int DEFAULT_START_YEAR = 1900; - - private static final int DEFAULT_END_YEAR = 2100; - - private static final boolean DEFAULT_CALENDAR_VIEW_SHOWN = true; - - private static final boolean DEFAULT_SPINNERS_SHOWN = true; - - private static final boolean DEFAULT_ENABLED_STATE = true; - - private final LinearLayout mSpinners; - - private final NumberPicker mDaySpinner; - - private final NumberPicker mMonthSpinner; - - private final NumberPicker mYearSpinner; - - private final EditText mDaySpinnerInput; - - private final EditText mMonthSpinnerInput; - - private final EditText mYearSpinnerInput; - - private final CalendarView mCalendarView; - - private Locale mCurrentLocale; - - private OnDateChangedListener mOnDateChangedListener; - - private String[] mShortMonths; - - private final java.text.DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT); - - private int mNumberOfMonths; - - private Calendar mTempDate; - - private Calendar mMinDate; - - private Calendar mMaxDate; - - private Calendar mCurrentDate; - - private boolean mIsEnabled = DEFAULT_ENABLED_STATE; + private DatePickerDelegate mDelegate; /** * The callback used to indicate the user changes\d the date. @@ -149,147 +102,61 @@ public class DatePicker extends FrameLayout { this(context, attrs, R.attr.datePickerStyle); } - public DatePicker(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - // initialization based on locale - setCurrentLocale(Locale.getDefault()); - - TypedArray attributesArray = context.obtainStyledAttributes(attrs, R.styleable.DatePicker, - defStyle, 0); - boolean spinnersShown = attributesArray.getBoolean(R.styleable.DatePicker_spinnersShown, - DEFAULT_SPINNERS_SHOWN); - boolean calendarViewShown = attributesArray.getBoolean( - R.styleable.DatePicker_calendarViewShown, DEFAULT_CALENDAR_VIEW_SHOWN); - int startYear = attributesArray.getInt(R.styleable.DatePicker_startYear, - DEFAULT_START_YEAR); - int endYear = attributesArray.getInt(R.styleable.DatePicker_endYear, DEFAULT_END_YEAR); - String minDate = attributesArray.getString(R.styleable.DatePicker_minDate); - String maxDate = attributesArray.getString(R.styleable.DatePicker_maxDate); - int layoutResourceId = attributesArray.getResourceId(R.styleable.DatePicker_internalLayout, - R.layout.date_picker); - attributesArray.recycle(); - - LayoutInflater inflater = (LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - inflater.inflate(layoutResourceId, this, true); - - OnValueChangeListener onChangeListener = new OnValueChangeListener() { - public void onValueChange(NumberPicker picker, int oldVal, int newVal) { - updateInputState(); - mTempDate.setTimeInMillis(mCurrentDate.getTimeInMillis()); - // take care of wrapping of days and months to update greater fields - if (picker == mDaySpinner) { - int maxDayOfMonth = mTempDate.getActualMaximum(Calendar.DAY_OF_MONTH); - if (oldVal == maxDayOfMonth && newVal == 1) { - mTempDate.add(Calendar.DAY_OF_MONTH, 1); - } else if (oldVal == 1 && newVal == maxDayOfMonth) { - mTempDate.add(Calendar.DAY_OF_MONTH, -1); - } else { - mTempDate.add(Calendar.DAY_OF_MONTH, newVal - oldVal); - } - } else if (picker == mMonthSpinner) { - if (oldVal == 11 && newVal == 0) { - mTempDate.add(Calendar.MONTH, 1); - } else if (oldVal == 0 && newVal == 11) { - mTempDate.add(Calendar.MONTH, -1); - } else { - mTempDate.add(Calendar.MONTH, newVal - oldVal); - } - } else if (picker == mYearSpinner) { - mTempDate.set(Calendar.YEAR, newVal); - } else { - throw new IllegalArgumentException(); - } - // now set the date to the adjusted one - setDate(mTempDate.get(Calendar.YEAR), mTempDate.get(Calendar.MONTH), - mTempDate.get(Calendar.DAY_OF_MONTH)); - updateSpinners(); - updateCalendarView(); - notifyDateChanged(); - } - }; + public DatePicker(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } - mSpinners = (LinearLayout) findViewById(R.id.pickers); + public DatePicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - // calendar view day-picker - mCalendarView = (CalendarView) findViewById(R.id.calendar_view); - mCalendarView.setOnDateChangeListener(new CalendarView.OnDateChangeListener() { - public void onSelectedDayChange(CalendarView view, int year, int month, int monthDay) { - setDate(year, month, monthDay); - updateSpinners(); - notifyDateChanged(); - } - }); - - // day - mDaySpinner = (NumberPicker) findViewById(R.id.day); - mDaySpinner.setFormatter(NumberPicker.getTwoDigitFormatter()); - mDaySpinner.setOnLongPressUpdateInterval(100); - mDaySpinner.setOnValueChangedListener(onChangeListener); - mDaySpinnerInput = (EditText) mDaySpinner.findViewById(R.id.numberpicker_input); - - // month - mMonthSpinner = (NumberPicker) findViewById(R.id.month); - mMonthSpinner.setMinValue(0); - mMonthSpinner.setMaxValue(mNumberOfMonths - 1); - mMonthSpinner.setDisplayedValues(mShortMonths); - mMonthSpinner.setOnLongPressUpdateInterval(200); - mMonthSpinner.setOnValueChangedListener(onChangeListener); - mMonthSpinnerInput = (EditText) mMonthSpinner.findViewById(R.id.numberpicker_input); - - // year - mYearSpinner = (NumberPicker) findViewById(R.id.year); - mYearSpinner.setOnLongPressUpdateInterval(100); - mYearSpinner.setOnValueChangedListener(onChangeListener); - mYearSpinnerInput = (EditText) mYearSpinner.findViewById(R.id.numberpicker_input); - - // show only what the user required but make sure we - // show something and the spinners have higher priority - if (!spinnersShown && !calendarViewShown) { - setSpinnersShown(true); - } else { - setSpinnersShown(spinnersShown); - setCalendarViewShown(calendarViewShown); - } - - // set the min date giving priority of the minDate over startYear - mTempDate.clear(); - if (!TextUtils.isEmpty(minDate)) { - if (!parseDate(minDate, mTempDate)) { - mTempDate.set(startYear, 0, 1); - } - } else { - mTempDate.set(startYear, 0, 1); - } - setMinDate(mTempDate.getTimeInMillis()); + mDelegate = new LegacyDatePickerDelegate(this, context, attrs, defStyleAttr, defStyleRes); + } - // set the max date giving priority of the maxDate over endYear - mTempDate.clear(); - if (!TextUtils.isEmpty(maxDate)) { - if (!parseDate(maxDate, mTempDate)) { - mTempDate.set(endYear, 11, 31); - } - } else { - mTempDate.set(endYear, 11, 31); - } - setMaxDate(mTempDate.getTimeInMillis()); + /** + * Initialize the state. If the provided values designate an inconsistent + * date the values are normalized before updating the spinners. + * + * @param year The initial year. + * @param monthOfYear The initial month <strong>starting from zero</strong>. + * @param dayOfMonth The initial day of the month. + * @param onDateChangedListener How user is notified date is changed by + * user, can be null. + */ + public void init(int year, int monthOfYear, int dayOfMonth, + OnDateChangedListener onDateChangedListener) { + mDelegate.init(year, monthOfYear, dayOfMonth, onDateChangedListener); + } - // initialize to current date - mCurrentDate.setTimeInMillis(System.currentTimeMillis()); - init(mCurrentDate.get(Calendar.YEAR), mCurrentDate.get(Calendar.MONTH), mCurrentDate - .get(Calendar.DAY_OF_MONTH), null); + /** + * Updates the current date. + * + * @param year The year. + * @param month The month which is <strong>starting from zero</strong>. + * @param dayOfMonth The day of the month. + */ + public void updateDate(int year, int month, int dayOfMonth) { + mDelegate.updateDate(year, month, dayOfMonth); + } - // re-order the number spinners to match the current date format - reorderSpinners(); + /** + * @return The selected year. + */ + public int getYear() { + return mDelegate.getYear(); + } - // accessibility - setContentDescriptions(); + /** + * @return The selected month. + */ + public int getMonth() { + return mDelegate.getMonth(); + } - // If not explicitly specified this view is important for accessibility. - if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { - setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); - } + /** + * @return The selected day of month. + */ + public int getDayOfMonth() { + return mDelegate.getDayOfMonth(); } /** @@ -303,7 +170,7 @@ public class DatePicker extends FrameLayout { * @return The minimal supported date. */ public long getMinDate() { - return mCalendarView.getMinDate(); + return mDelegate.getMinDate(); } /** @@ -314,18 +181,7 @@ public class DatePicker extends FrameLayout { * @param minDate The minimal supported date. */ public void setMinDate(long minDate) { - mTempDate.setTimeInMillis(minDate); - if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR) - && mTempDate.get(Calendar.DAY_OF_YEAR) != mMinDate.get(Calendar.DAY_OF_YEAR)) { - return; - } - mMinDate.setTimeInMillis(minDate); - mCalendarView.setMinDate(minDate); - if (mCurrentDate.before(mMinDate)) { - mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis()); - updateCalendarView(); - } - updateSpinners(); + mDelegate.setMinDate(minDate); } /** @@ -339,7 +195,7 @@ public class DatePicker extends FrameLayout { * @return The maximal supported date. */ public long getMaxDate() { - return mCalendarView.getMaxDate(); + return mDelegate.getMaxDate(); } /** @@ -350,70 +206,50 @@ public class DatePicker extends FrameLayout { * @param maxDate The maximal supported date. */ public void setMaxDate(long maxDate) { - mTempDate.setTimeInMillis(maxDate); - if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR) - && mTempDate.get(Calendar.DAY_OF_YEAR) != mMaxDate.get(Calendar.DAY_OF_YEAR)) { - return; - } - mMaxDate.setTimeInMillis(maxDate); - mCalendarView.setMaxDate(maxDate); - if (mCurrentDate.after(mMaxDate)) { - mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis()); - updateCalendarView(); - } - updateSpinners(); + mDelegate.setMaxDate(maxDate); } @Override public void setEnabled(boolean enabled) { - if (mIsEnabled == enabled) { + if (mDelegate.isEnabled() == enabled) { return; } super.setEnabled(enabled); - mDaySpinner.setEnabled(enabled); - mMonthSpinner.setEnabled(enabled); - mYearSpinner.setEnabled(enabled); - mCalendarView.setEnabled(enabled); - mIsEnabled = enabled; + mDelegate.setEnabled(enabled); } @Override public boolean isEnabled() { - return mIsEnabled; + return mDelegate.isEnabled(); } @Override public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - onPopulateAccessibilityEvent(event); - return true; + return mDelegate.dispatchPopulateAccessibilityEvent(event); } @Override public void onPopulateAccessibilityEvent(AccessibilityEvent event) { super.onPopulateAccessibilityEvent(event); - - final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR; - String selectedDateUtterance = DateUtils.formatDateTime(mContext, - mCurrentDate.getTimeInMillis(), flags); - event.getText().add(selectedDateUtterance); + mDelegate.onPopulateAccessibilityEvent(event); } @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); - event.setClassName(DatePicker.class.getName()); + mDelegate.onInitializeAccessibilityEvent(event); } @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(DatePicker.class.getName()); + mDelegate.onInitializeAccessibilityNodeInfo(info); } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - setCurrentLocale(newConfig.locale); + mDelegate.onConfigurationChanged(newConfig); } /** @@ -423,7 +259,7 @@ public class DatePicker extends FrameLayout { * @see #getCalendarView() */ public boolean getCalendarViewShown() { - return (mCalendarView.getVisibility() == View.VISIBLE); + return mDelegate.getCalendarViewShown(); } /** @@ -433,7 +269,7 @@ public class DatePicker extends FrameLayout { * @see #getCalendarViewShown() */ public CalendarView getCalendarView () { - return mCalendarView; + return mDelegate.getCalendarView(); } /** @@ -442,7 +278,7 @@ public class DatePicker extends FrameLayout { * @param shown True if the calendar view is to be shown. */ public void setCalendarViewShown(boolean shown) { - mCalendarView.setVisibility(shown ? VISIBLE : GONE); + mDelegate.setCalendarViewShown(shown); } /** @@ -451,7 +287,7 @@ public class DatePicker extends FrameLayout { * @return True if the spinners are shown. */ public boolean getSpinnersShown() { - return mSpinners.isShown(); + return mDelegate.getSpinnersShown(); } /** @@ -460,330 +296,708 @@ public class DatePicker extends FrameLayout { * @param shown True if the spinners are to be shown. */ public void setSpinnersShown(boolean shown) { - mSpinners.setVisibility(shown ? VISIBLE : GONE); + mDelegate.setSpinnersShown(shown); + } + + // Override so we are in complete control of save / restore for this widget. + @Override + protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { + mDelegate.dispatchRestoreInstanceState(container); + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + return mDelegate.onSaveInstanceState(superState); + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + mDelegate.onRestoreInstanceState(ss); } /** - * Sets the current locale. - * - * @param locale The current locale. + * A delegate interface that defined the public API of the DatePicker. Allows different + * DatePicker implementations. This would need to be implemented by the DatePicker delegates + * for the real behavior. */ - private void setCurrentLocale(Locale locale) { - if (locale.equals(mCurrentLocale)) { - return; - } + interface DatePickerDelegate { + void init(int year, int monthOfYear, int dayOfMonth, + OnDateChangedListener onDateChangedListener); - mCurrentLocale = locale; + void updateDate(int year, int month, int dayOfMonth); - mTempDate = getCalendarForLocale(mTempDate, locale); - mMinDate = getCalendarForLocale(mMinDate, locale); - mMaxDate = getCalendarForLocale(mMaxDate, locale); - mCurrentDate = getCalendarForLocale(mCurrentDate, locale); + int getYear(); + int getMonth(); + int getDayOfMonth(); - mNumberOfMonths = mTempDate.getActualMaximum(Calendar.MONTH) + 1; - mShortMonths = new DateFormatSymbols().getShortMonths(); + void setMinDate(long minDate); + long getMinDate(); - if (usingNumericMonths()) { - // We're in a locale where a date should either be all-numeric, or all-text. - // All-text would require custom NumberPicker formatters for day and year. - mShortMonths = new String[mNumberOfMonths]; - for (int i = 0; i < mNumberOfMonths; ++i) { - mShortMonths[i] = String.format("%d", i + 1); - } - } - } + void setMaxDate(long maxDate); + long getMaxDate(); - /** - * Tests whether the current locale is one where there are no real month names, - * such as Chinese, Japanese, or Korean locales. - */ - private boolean usingNumericMonths() { - return Character.isDigit(mShortMonths[Calendar.JANUARY].charAt(0)); + void setEnabled(boolean enabled); + boolean isEnabled(); + + CalendarView getCalendarView (); + + void setCalendarViewShown(boolean shown); + boolean getCalendarViewShown(); + + void setSpinnersShown(boolean shown); + boolean getSpinnersShown(); + + void onConfigurationChanged(Configuration newConfig); + + void dispatchRestoreInstanceState(SparseArray<Parcelable> container); + Parcelable onSaveInstanceState(Parcelable superState); + void onRestoreInstanceState(Parcelable state); + + boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event); + void onPopulateAccessibilityEvent(AccessibilityEvent event); + void onInitializeAccessibilityEvent(AccessibilityEvent event); + void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info); } /** - * Gets a calendar for locale bootstrapped with the value of a given calendar. - * - * @param oldCalendar The old calendar. - * @param locale The locale. + * An abstract class which can be used as a start for DatePicker implementations */ - private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) { - if (oldCalendar == null) { - return Calendar.getInstance(locale); - } else { - final long currentTimeMillis = oldCalendar.getTimeInMillis(); - Calendar newCalendar = Calendar.getInstance(locale); - newCalendar.setTimeInMillis(currentTimeMillis); - return newCalendar; + abstract static class AbstractTimePickerDelegate implements DatePickerDelegate { + // The delegator + protected DatePicker mDelegator; + + // The context + protected Context mContext; + + // The current locale + protected Locale mCurrentLocale; + + // Callbacks + protected OnDateChangedListener mOnDateChangedListener; + + public AbstractTimePickerDelegate(DatePicker delegator, Context context) { + mDelegator = delegator; + mContext = context; + + // initialization based on locale + setCurrentLocale(Locale.getDefault()); } - } - /** - * Reorders the spinners according to the date format that is - * explicitly set by the user and if no such is set fall back - * to the current locale's default format. - */ - private void reorderSpinners() { - mSpinners.removeAllViews(); - // We use numeric spinners for year and day, but textual months. Ask icu4c what - // order the user's locale uses for that combination. http://b/7207103. - String pattern = ICU.getBestDateTimePattern("yyyyMMMdd", Locale.getDefault().toString()); - char[] order = ICU.getDateFormatOrder(pattern); - final int spinnerCount = order.length; - for (int i = 0; i < spinnerCount; i++) { - switch (order[i]) { - case 'd': - mSpinners.addView(mDaySpinner); - setImeOptions(mDaySpinner, spinnerCount, i); - break; - case 'M': - mSpinners.addView(mMonthSpinner); - setImeOptions(mMonthSpinner, spinnerCount, i); - break; - case 'y': - mSpinners.addView(mYearSpinner); - setImeOptions(mYearSpinner, spinnerCount, i); - break; - default: - throw new IllegalArgumentException(Arrays.toString(order)); + protected void setCurrentLocale(Locale locale) { + if (locale.equals(mCurrentLocale)) { + return; } + mCurrentLocale = locale; } } /** - * Updates the current date. - * - * @param year The year. - * @param month The month which is <strong>starting from zero</strong>. - * @param dayOfMonth The day of the month. + * A delegate implementing the basic DatePicker */ - public void updateDate(int year, int month, int dayOfMonth) { - if (!isNewDate(year, month, dayOfMonth)) { - return; + private static class LegacyDatePickerDelegate extends AbstractTimePickerDelegate { + + private static final String DATE_FORMAT = "MM/dd/yyyy"; + + private static final int DEFAULT_START_YEAR = 1900; + + private static final int DEFAULT_END_YEAR = 2100; + + private static final boolean DEFAULT_CALENDAR_VIEW_SHOWN = true; + + private static final boolean DEFAULT_SPINNERS_SHOWN = true; + + private static final boolean DEFAULT_ENABLED_STATE = true; + + private final LinearLayout mSpinners; + + private final NumberPicker mDaySpinner; + + private final NumberPicker mMonthSpinner; + + private final NumberPicker mYearSpinner; + + private final EditText mDaySpinnerInput; + + private final EditText mMonthSpinnerInput; + + private final EditText mYearSpinnerInput; + + private final CalendarView mCalendarView; + + private String[] mShortMonths; + + private final java.text.DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT); + + private int mNumberOfMonths; + + private Calendar mTempDate; + + private Calendar mMinDate; + + private Calendar mMaxDate; + + private Calendar mCurrentDate; + + private boolean mIsEnabled = DEFAULT_ENABLED_STATE; + + LegacyDatePickerDelegate(DatePicker delegator, Context context, AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + super(delegator, context); + + mDelegator = delegator; + mContext = context; + + // initialization based on locale + setCurrentLocale(Locale.getDefault()); + + final TypedArray attributesArray = context.obtainStyledAttributes(attrs, + R.styleable.DatePicker, defStyleAttr, defStyleRes); + boolean spinnersShown = attributesArray.getBoolean(R.styleable.DatePicker_spinnersShown, + DEFAULT_SPINNERS_SHOWN); + boolean calendarViewShown = attributesArray.getBoolean( + R.styleable.DatePicker_calendarViewShown, DEFAULT_CALENDAR_VIEW_SHOWN); + int startYear = attributesArray.getInt(R.styleable.DatePicker_startYear, + DEFAULT_START_YEAR); + int endYear = attributesArray.getInt(R.styleable.DatePicker_endYear, DEFAULT_END_YEAR); + String minDate = attributesArray.getString(R.styleable.DatePicker_minDate); + String maxDate = attributesArray.getString(R.styleable.DatePicker_maxDate); + int layoutResourceId = attributesArray.getResourceId( + R.styleable.DatePicker_internalLayout, R.layout.date_picker); + attributesArray.recycle(); + + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(layoutResourceId, mDelegator, true); + + OnValueChangeListener onChangeListener = new OnValueChangeListener() { + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + updateInputState(); + mTempDate.setTimeInMillis(mCurrentDate.getTimeInMillis()); + // take care of wrapping of days and months to update greater fields + if (picker == mDaySpinner) { + int maxDayOfMonth = mTempDate.getActualMaximum(Calendar.DAY_OF_MONTH); + if (oldVal == maxDayOfMonth && newVal == 1) { + mTempDate.add(Calendar.DAY_OF_MONTH, 1); + } else if (oldVal == 1 && newVal == maxDayOfMonth) { + mTempDate.add(Calendar.DAY_OF_MONTH, -1); + } else { + mTempDate.add(Calendar.DAY_OF_MONTH, newVal - oldVal); + } + } else if (picker == mMonthSpinner) { + if (oldVal == 11 && newVal == 0) { + mTempDate.add(Calendar.MONTH, 1); + } else if (oldVal == 0 && newVal == 11) { + mTempDate.add(Calendar.MONTH, -1); + } else { + mTempDate.add(Calendar.MONTH, newVal - oldVal); + } + } else if (picker == mYearSpinner) { + mTempDate.set(Calendar.YEAR, newVal); + } else { + throw new IllegalArgumentException(); + } + // now set the date to the adjusted one + setDate(mTempDate.get(Calendar.YEAR), mTempDate.get(Calendar.MONTH), + mTempDate.get(Calendar.DAY_OF_MONTH)); + updateSpinners(); + updateCalendarView(); + notifyDateChanged(); + } + }; + + mSpinners = (LinearLayout) mDelegator.findViewById(R.id.pickers); + + // calendar view day-picker + mCalendarView = (CalendarView) mDelegator.findViewById(R.id.calendar_view); + mCalendarView.setOnDateChangeListener(new CalendarView.OnDateChangeListener() { + public void onSelectedDayChange(CalendarView view, int year, int month, int monthDay) { + setDate(year, month, monthDay); + updateSpinners(); + notifyDateChanged(); + } + }); + + // day + mDaySpinner = (NumberPicker) mDelegator.findViewById(R.id.day); + mDaySpinner.setFormatter(NumberPicker.getTwoDigitFormatter()); + mDaySpinner.setOnLongPressUpdateInterval(100); + mDaySpinner.setOnValueChangedListener(onChangeListener); + mDaySpinnerInput = (EditText) mDaySpinner.findViewById(R.id.numberpicker_input); + + // month + mMonthSpinner = (NumberPicker) mDelegator.findViewById(R.id.month); + mMonthSpinner.setMinValue(0); + mMonthSpinner.setMaxValue(mNumberOfMonths - 1); + mMonthSpinner.setDisplayedValues(mShortMonths); + mMonthSpinner.setOnLongPressUpdateInterval(200); + mMonthSpinner.setOnValueChangedListener(onChangeListener); + mMonthSpinnerInput = (EditText) mMonthSpinner.findViewById(R.id.numberpicker_input); + + // year + mYearSpinner = (NumberPicker) mDelegator.findViewById(R.id.year); + mYearSpinner.setOnLongPressUpdateInterval(100); + mYearSpinner.setOnValueChangedListener(onChangeListener); + mYearSpinnerInput = (EditText) mYearSpinner.findViewById(R.id.numberpicker_input); + + // show only what the user required but make sure we + // show something and the spinners have higher priority + if (!spinnersShown && !calendarViewShown) { + setSpinnersShown(true); + } else { + setSpinnersShown(spinnersShown); + setCalendarViewShown(calendarViewShown); + } + + // set the min date giving priority of the minDate over startYear + mTempDate.clear(); + if (!TextUtils.isEmpty(minDate)) { + if (!parseDate(minDate, mTempDate)) { + mTempDate.set(startYear, 0, 1); + } + } else { + mTempDate.set(startYear, 0, 1); + } + setMinDate(mTempDate.getTimeInMillis()); + + // set the max date giving priority of the maxDate over endYear + mTempDate.clear(); + if (!TextUtils.isEmpty(maxDate)) { + if (!parseDate(maxDate, mTempDate)) { + mTempDate.set(endYear, 11, 31); + } + } else { + mTempDate.set(endYear, 11, 31); + } + setMaxDate(mTempDate.getTimeInMillis()); + + // initialize to current date + mCurrentDate.setTimeInMillis(System.currentTimeMillis()); + init(mCurrentDate.get(Calendar.YEAR), mCurrentDate.get(Calendar.MONTH), mCurrentDate + .get(Calendar.DAY_OF_MONTH), null); + + // re-order the number spinners to match the current date format + reorderSpinners(); + + // accessibility + setContentDescriptions(); + + // If not explicitly specified this view is important for accessibility. + if (mDelegator.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + mDelegator.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } } - setDate(year, month, dayOfMonth); - updateSpinners(); - updateCalendarView(); - notifyDateChanged(); - } - // Override so we are in complete control of save / restore for this widget. - @Override - protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { - dispatchThawSelfOnly(container); - } + @Override + public void init(int year, int monthOfYear, int dayOfMonth, + OnDateChangedListener onDateChangedListener) { + setDate(year, monthOfYear, dayOfMonth); + updateSpinners(); + updateCalendarView(); + mOnDateChangedListener = onDateChangedListener; + } - @Override - protected Parcelable onSaveInstanceState() { - Parcelable superState = super.onSaveInstanceState(); - return new SavedState(superState, getYear(), getMonth(), getDayOfMonth()); - } + @Override + public void updateDate(int year, int month, int dayOfMonth) { + if (!isNewDate(year, month, dayOfMonth)) { + return; + } + setDate(year, month, dayOfMonth); + updateSpinners(); + updateCalendarView(); + notifyDateChanged(); + } - @Override - protected void onRestoreInstanceState(Parcelable state) { - SavedState ss = (SavedState) state; - super.onRestoreInstanceState(ss.getSuperState()); - setDate(ss.mYear, ss.mMonth, ss.mDay); - updateSpinners(); - updateCalendarView(); - } + @Override + public int getYear() { + return mCurrentDate.get(Calendar.YEAR); + } - /** - * Initialize the state. If the provided values designate an inconsistent - * date the values are normalized before updating the spinners. - * - * @param year The initial year. - * @param monthOfYear The initial month <strong>starting from zero</strong>. - * @param dayOfMonth The initial day of the month. - * @param onDateChangedListener How user is notified date is changed by - * user, can be null. - */ - public void init(int year, int monthOfYear, int dayOfMonth, - OnDateChangedListener onDateChangedListener) { - setDate(year, monthOfYear, dayOfMonth); - updateSpinners(); - updateCalendarView(); - mOnDateChangedListener = onDateChangedListener; - } + @Override + public int getMonth() { + return mCurrentDate.get(Calendar.MONTH); + } - /** - * Parses the given <code>date</code> and in case of success sets the result - * to the <code>outDate</code>. - * - * @return True if the date was parsed. - */ - private boolean parseDate(String date, Calendar outDate) { - try { - outDate.setTime(mDateFormat.parse(date)); + @Override + public int getDayOfMonth() { + return mCurrentDate.get(Calendar.DAY_OF_MONTH); + } + + @Override + public void setMinDate(long minDate) { + mTempDate.setTimeInMillis(minDate); + if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR) + && mTempDate.get(Calendar.DAY_OF_YEAR) != mMinDate.get(Calendar.DAY_OF_YEAR)) { + return; + } + mMinDate.setTimeInMillis(minDate); + mCalendarView.setMinDate(minDate); + if (mCurrentDate.before(mMinDate)) { + mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis()); + updateCalendarView(); + } + updateSpinners(); + } + + @Override + public long getMinDate() { + return mCalendarView.getMinDate(); + } + + @Override + public void setMaxDate(long maxDate) { + mTempDate.setTimeInMillis(maxDate); + if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR) + && mTempDate.get(Calendar.DAY_OF_YEAR) != mMaxDate.get(Calendar.DAY_OF_YEAR)) { + return; + } + mMaxDate.setTimeInMillis(maxDate); + mCalendarView.setMaxDate(maxDate); + if (mCurrentDate.after(mMaxDate)) { + mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis()); + updateCalendarView(); + } + updateSpinners(); + } + + @Override + public long getMaxDate() { + return mCalendarView.getMaxDate(); + } + + @Override + public void setEnabled(boolean enabled) { + mDaySpinner.setEnabled(enabled); + mMonthSpinner.setEnabled(enabled); + mYearSpinner.setEnabled(enabled); + mCalendarView.setEnabled(enabled); + mIsEnabled = enabled; + } + + @Override + public boolean isEnabled() { + return mIsEnabled; + } + + @Override + public CalendarView getCalendarView() { + return mCalendarView; + } + + @Override + public void setCalendarViewShown(boolean shown) { + mCalendarView.setVisibility(shown ? VISIBLE : GONE); + } + + @Override + public boolean getCalendarViewShown() { + return (mCalendarView.getVisibility() == View.VISIBLE); + } + + @Override + public void setSpinnersShown(boolean shown) { + mSpinners.setVisibility(shown ? VISIBLE : GONE); + } + + @Override + public boolean getSpinnersShown() { + return mSpinners.isShown(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + setCurrentLocale(newConfig.locale); + } + + @Override + public void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { + mDelegator.dispatchThawSelfOnly(container); + } + + @Override + public Parcelable onSaveInstanceState(Parcelable superState) { + return new SavedState(superState, getYear(), getMonth(), getDayOfMonth()); + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + setDate(ss.mYear, ss.mMonth, ss.mDay); + updateSpinners(); + updateCalendarView(); + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + onPopulateAccessibilityEvent(event); return true; - } catch (ParseException e) { - Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT); - return false; - } - } - - private boolean isNewDate(int year, int month, int dayOfMonth) { - return (mCurrentDate.get(Calendar.YEAR) != year - || mCurrentDate.get(Calendar.MONTH) != dayOfMonth - || mCurrentDate.get(Calendar.DAY_OF_MONTH) != month); - } - - private void setDate(int year, int month, int dayOfMonth) { - mCurrentDate.set(year, month, dayOfMonth); - if (mCurrentDate.before(mMinDate)) { - mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis()); - } else if (mCurrentDate.after(mMaxDate)) { - mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis()); - } - } - - private void updateSpinners() { - // set the spinner ranges respecting the min and max dates - if (mCurrentDate.equals(mMinDate)) { - mDaySpinner.setMinValue(mCurrentDate.get(Calendar.DAY_OF_MONTH)); - mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH)); - mDaySpinner.setWrapSelectorWheel(false); - mMonthSpinner.setDisplayedValues(null); - mMonthSpinner.setMinValue(mCurrentDate.get(Calendar.MONTH)); - mMonthSpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.MONTH)); - mMonthSpinner.setWrapSelectorWheel(false); - } else if (mCurrentDate.equals(mMaxDate)) { - mDaySpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.DAY_OF_MONTH)); - mDaySpinner.setMaxValue(mCurrentDate.get(Calendar.DAY_OF_MONTH)); - mDaySpinner.setWrapSelectorWheel(false); - mMonthSpinner.setDisplayedValues(null); - mMonthSpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.MONTH)); - mMonthSpinner.setMaxValue(mCurrentDate.get(Calendar.MONTH)); - mMonthSpinner.setWrapSelectorWheel(false); - } else { - mDaySpinner.setMinValue(1); - mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH)); - mDaySpinner.setWrapSelectorWheel(true); - mMonthSpinner.setDisplayedValues(null); - mMonthSpinner.setMinValue(0); - mMonthSpinner.setMaxValue(11); - mMonthSpinner.setWrapSelectorWheel(true); } - // make sure the month names are a zero based array - // with the months in the month spinner - String[] displayedValues = Arrays.copyOfRange(mShortMonths, - mMonthSpinner.getMinValue(), mMonthSpinner.getMaxValue() + 1); - mMonthSpinner.setDisplayedValues(displayedValues); + @Override + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR; + String selectedDateUtterance = DateUtils.formatDateTime(mContext, + mCurrentDate.getTimeInMillis(), flags); + event.getText().add(selectedDateUtterance); + } - // year spinner range does not change based on the current date - mYearSpinner.setMinValue(mMinDate.get(Calendar.YEAR)); - mYearSpinner.setMaxValue(mMaxDate.get(Calendar.YEAR)); - mYearSpinner.setWrapSelectorWheel(false); + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + event.setClassName(DatePicker.class.getName()); + } - // set the spinner values - mYearSpinner.setValue(mCurrentDate.get(Calendar.YEAR)); - mMonthSpinner.setValue(mCurrentDate.get(Calendar.MONTH)); - mDaySpinner.setValue(mCurrentDate.get(Calendar.DAY_OF_MONTH)); + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + info.setClassName(DatePicker.class.getName()); + } - if (usingNumericMonths()) { - mMonthSpinnerInput.setRawInputType(InputType.TYPE_CLASS_NUMBER); + /** + * Sets the current locale. + * + * @param locale The current locale. + */ + @Override + protected void setCurrentLocale(Locale locale) { + super.setCurrentLocale(locale); + + mTempDate = getCalendarForLocale(mTempDate, locale); + mMinDate = getCalendarForLocale(mMinDate, locale); + mMaxDate = getCalendarForLocale(mMaxDate, locale); + mCurrentDate = getCalendarForLocale(mCurrentDate, locale); + + mNumberOfMonths = mTempDate.getActualMaximum(Calendar.MONTH) + 1; + mShortMonths = new DateFormatSymbols().getShortMonths(); + + if (usingNumericMonths()) { + // We're in a locale where a date should either be all-numeric, or all-text. + // All-text would require custom NumberPicker formatters for day and year. + mShortMonths = new String[mNumberOfMonths]; + for (int i = 0; i < mNumberOfMonths; ++i) { + mShortMonths[i] = String.format("%d", i + 1); + } + } } - } - /** - * Updates the calendar view with the current date. - */ - private void updateCalendarView() { - mCalendarView.setDate(mCurrentDate.getTimeInMillis(), false, false); - } + /** + * Tests whether the current locale is one where there are no real month names, + * such as Chinese, Japanese, or Korean locales. + */ + private boolean usingNumericMonths() { + return Character.isDigit(mShortMonths[Calendar.JANUARY].charAt(0)); + } - /** - * @return The selected year. - */ - public int getYear() { - return mCurrentDate.get(Calendar.YEAR); - } + /** + * Gets a calendar for locale bootstrapped with the value of a given calendar. + * + * @param oldCalendar The old calendar. + * @param locale The locale. + */ + private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) { + if (oldCalendar == null) { + return Calendar.getInstance(locale); + } else { + final long currentTimeMillis = oldCalendar.getTimeInMillis(); + Calendar newCalendar = Calendar.getInstance(locale); + newCalendar.setTimeInMillis(currentTimeMillis); + return newCalendar; + } + } - /** - * @return The selected month. - */ - public int getMonth() { - return mCurrentDate.get(Calendar.MONTH); - } + /** + * Reorders the spinners according to the date format that is + * explicitly set by the user and if no such is set fall back + * to the current locale's default format. + */ + private void reorderSpinners() { + mSpinners.removeAllViews(); + // We use numeric spinners for year and day, but textual months. Ask icu4c what + // order the user's locale uses for that combination. http://b/7207103. + String pattern = ICU.getBestDateTimePattern("yyyyMMMdd", + Locale.getDefault().toString()); + char[] order = ICU.getDateFormatOrder(pattern); + final int spinnerCount = order.length; + for (int i = 0; i < spinnerCount; i++) { + switch (order[i]) { + case 'd': + mSpinners.addView(mDaySpinner); + setImeOptions(mDaySpinner, spinnerCount, i); + break; + case 'M': + mSpinners.addView(mMonthSpinner); + setImeOptions(mMonthSpinner, spinnerCount, i); + break; + case 'y': + mSpinners.addView(mYearSpinner); + setImeOptions(mYearSpinner, spinnerCount, i); + break; + default: + throw new IllegalArgumentException(Arrays.toString(order)); + } + } + } - /** - * @return The selected day of month. - */ - public int getDayOfMonth() { - return mCurrentDate.get(Calendar.DAY_OF_MONTH); - } + /** + * Parses the given <code>date</code> and in case of success sets the result + * to the <code>outDate</code>. + * + * @return True if the date was parsed. + */ + private boolean parseDate(String date, Calendar outDate) { + try { + outDate.setTime(mDateFormat.parse(date)); + return true; + } catch (ParseException e) { + Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT); + return false; + } + } - /** - * Notifies the listener, if such, for a change in the selected date. - */ - private void notifyDateChanged() { - sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); - if (mOnDateChangedListener != null) { - mOnDateChangedListener.onDateChanged(this, getYear(), getMonth(), getDayOfMonth()); + private boolean isNewDate(int year, int month, int dayOfMonth) { + return (mCurrentDate.get(Calendar.YEAR) != year + || mCurrentDate.get(Calendar.MONTH) != dayOfMonth + || mCurrentDate.get(Calendar.DAY_OF_MONTH) != month); } - } - /** - * Sets the IME options for a spinner based on its ordering. - * - * @param spinner The spinner. - * @param spinnerCount The total spinner count. - * @param spinnerIndex The index of the given spinner. - */ - private void setImeOptions(NumberPicker spinner, int spinnerCount, int spinnerIndex) { - final int imeOptions; - if (spinnerIndex < spinnerCount - 1) { - imeOptions = EditorInfo.IME_ACTION_NEXT; - } else { - imeOptions = EditorInfo.IME_ACTION_DONE; - } - TextView input = (TextView) spinner.findViewById(R.id.numberpicker_input); - input.setImeOptions(imeOptions); - } - - private void setContentDescriptions() { - // Day - trySetContentDescription(mDaySpinner, R.id.increment, - R.string.date_picker_increment_day_button); - trySetContentDescription(mDaySpinner, R.id.decrement, - R.string.date_picker_decrement_day_button); - // Month - trySetContentDescription(mMonthSpinner, R.id.increment, - R.string.date_picker_increment_month_button); - trySetContentDescription(mMonthSpinner, R.id.decrement, - R.string.date_picker_decrement_month_button); - // Year - trySetContentDescription(mYearSpinner, R.id.increment, - R.string.date_picker_increment_year_button); - trySetContentDescription(mYearSpinner, R.id.decrement, - R.string.date_picker_decrement_year_button); - } - - private void trySetContentDescription(View root, int viewId, int contDescResId) { - View target = root.findViewById(viewId); - if (target != null) { - target.setContentDescription(mContext.getString(contDescResId)); - } - } - - private void updateInputState() { - // Make sure that if the user changes the value and the IME is active - // for one of the inputs if this widget, the IME is closed. If the user - // changed the value via the IME and there is a next input the IME will - // be shown, otherwise the user chose another means of changing the - // value and having the IME up makes no sense. - InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); - if (inputMethodManager != null) { - if (inputMethodManager.isActive(mYearSpinnerInput)) { - mYearSpinnerInput.clearFocus(); - inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); - } else if (inputMethodManager.isActive(mMonthSpinnerInput)) { - mMonthSpinnerInput.clearFocus(); - inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); - } else if (inputMethodManager.isActive(mDaySpinnerInput)) { - mDaySpinnerInput.clearFocus(); - inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); + private void setDate(int year, int month, int dayOfMonth) { + mCurrentDate.set(year, month, dayOfMonth); + if (mCurrentDate.before(mMinDate)) { + mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis()); + } else if (mCurrentDate.after(mMaxDate)) { + mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis()); + } + } + + private void updateSpinners() { + // set the spinner ranges respecting the min and max dates + if (mCurrentDate.equals(mMinDate)) { + mDaySpinner.setMinValue(mCurrentDate.get(Calendar.DAY_OF_MONTH)); + mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH)); + mDaySpinner.setWrapSelectorWheel(false); + mMonthSpinner.setDisplayedValues(null); + mMonthSpinner.setMinValue(mCurrentDate.get(Calendar.MONTH)); + mMonthSpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.MONTH)); + mMonthSpinner.setWrapSelectorWheel(false); + } else if (mCurrentDate.equals(mMaxDate)) { + mDaySpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.DAY_OF_MONTH)); + mDaySpinner.setMaxValue(mCurrentDate.get(Calendar.DAY_OF_MONTH)); + mDaySpinner.setWrapSelectorWheel(false); + mMonthSpinner.setDisplayedValues(null); + mMonthSpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.MONTH)); + mMonthSpinner.setMaxValue(mCurrentDate.get(Calendar.MONTH)); + mMonthSpinner.setWrapSelectorWheel(false); + } else { + mDaySpinner.setMinValue(1); + mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH)); + mDaySpinner.setWrapSelectorWheel(true); + mMonthSpinner.setDisplayedValues(null); + mMonthSpinner.setMinValue(0); + mMonthSpinner.setMaxValue(11); + mMonthSpinner.setWrapSelectorWheel(true); + } + + // make sure the month names are a zero based array + // with the months in the month spinner + String[] displayedValues = Arrays.copyOfRange(mShortMonths, + mMonthSpinner.getMinValue(), mMonthSpinner.getMaxValue() + 1); + mMonthSpinner.setDisplayedValues(displayedValues); + + // year spinner range does not change based on the current date + mYearSpinner.setMinValue(mMinDate.get(Calendar.YEAR)); + mYearSpinner.setMaxValue(mMaxDate.get(Calendar.YEAR)); + mYearSpinner.setWrapSelectorWheel(false); + + // set the spinner values + mYearSpinner.setValue(mCurrentDate.get(Calendar.YEAR)); + mMonthSpinner.setValue(mCurrentDate.get(Calendar.MONTH)); + mDaySpinner.setValue(mCurrentDate.get(Calendar.DAY_OF_MONTH)); + + if (usingNumericMonths()) { + mMonthSpinnerInput.setRawInputType(InputType.TYPE_CLASS_NUMBER); + } + } + + /** + * Updates the calendar view with the current date. + */ + private void updateCalendarView() { + mCalendarView.setDate(mCurrentDate.getTimeInMillis(), false, false); + } + + + /** + * Notifies the listener, if such, for a change in the selected date. + */ + private void notifyDateChanged() { + mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + if (mOnDateChangedListener != null) { + mOnDateChangedListener.onDateChanged(mDelegator, getYear(), getMonth(), + getDayOfMonth()); + } + } + + /** + * Sets the IME options for a spinner based on its ordering. + * + * @param spinner The spinner. + * @param spinnerCount The total spinner count. + * @param spinnerIndex The index of the given spinner. + */ + private void setImeOptions(NumberPicker spinner, int spinnerCount, int spinnerIndex) { + final int imeOptions; + if (spinnerIndex < spinnerCount - 1) { + imeOptions = EditorInfo.IME_ACTION_NEXT; + } else { + imeOptions = EditorInfo.IME_ACTION_DONE; + } + TextView input = (TextView) spinner.findViewById(R.id.numberpicker_input); + input.setImeOptions(imeOptions); + } + + private void setContentDescriptions() { + // Day + trySetContentDescription(mDaySpinner, R.id.increment, + R.string.date_picker_increment_day_button); + trySetContentDescription(mDaySpinner, R.id.decrement, + R.string.date_picker_decrement_day_button); + // Month + trySetContentDescription(mMonthSpinner, R.id.increment, + R.string.date_picker_increment_month_button); + trySetContentDescription(mMonthSpinner, R.id.decrement, + R.string.date_picker_decrement_month_button); + // Year + trySetContentDescription(mYearSpinner, R.id.increment, + R.string.date_picker_increment_year_button); + trySetContentDescription(mYearSpinner, R.id.decrement, + R.string.date_picker_decrement_year_button); + } + + private void trySetContentDescription(View root, int viewId, int contDescResId) { + View target = root.findViewById(viewId); + if (target != null) { + target.setContentDescription(mContext.getString(contDescResId)); + } + } + + private void updateInputState() { + // Make sure that if the user changes the value and the IME is active + // for one of the inputs if this widget, the IME is closed. If the user + // changed the value via the IME and there is a next input the IME will + // be shown, otherwise the user chose another means of changing the + // value and having the IME up makes no sense. + InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); + if (inputMethodManager != null) { + if (inputMethodManager.isActive(mYearSpinnerInput)) { + mYearSpinnerInput.clearFocus(); + inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); + } else if (inputMethodManager.isActive(mMonthSpinnerInput)) { + mMonthSpinnerInput.clearFocus(); + inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); + } else if (inputMethodManager.isActive(mDaySpinnerInput)) { + mDaySpinnerInput.clearFocus(); + inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); + } } } } diff --git a/core/java/android/widget/DateTimeView.java b/core/java/android/widget/DateTimeView.java index af6bbcb..45d1403 100644 --- a/core/java/android/widget/DateTimeView.java +++ b/core/java/android/widget/DateTimeView.java @@ -27,12 +27,9 @@ import android.text.format.Time; import android.util.AttributeSet; import android.util.Log; import android.provider.Settings; -import android.provider.Settings.SettingNotFoundException; import android.widget.TextView; import android.widget.RemoteViews.RemoteView; -import com.android.internal.R; - import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; diff --git a/core/java/android/widget/DialerFilter.java b/core/java/android/widget/DialerFilter.java index 20bc114..78786e1 100644 --- a/core/java/android/widget/DialerFilter.java +++ b/core/java/android/widget/DialerFilter.java @@ -28,8 +28,6 @@ import android.text.method.DialerKeyListener; import android.text.method.KeyListener; import android.text.method.TextKeyListener; import android.util.AttributeSet; -import android.util.Log; -import android.view.KeyCharacterMap; import android.view.View; import android.graphics.Rect; diff --git a/core/java/android/widget/EditText.java b/core/java/android/widget/EditText.java index 57e51c2..3a7cc87 100644 --- a/core/java/android/widget/EditText.java +++ b/core/java/android/widget/EditText.java @@ -56,8 +56,12 @@ public class EditText extends TextView { this(context, attrs, com.android.internal.R.attr.editTextStyle); } - public EditText(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public EditText(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public EditText(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); } @Override diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index 9dab7b4..8ea35ff 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -45,8 +45,6 @@ import android.graphics.drawable.Drawable; import android.inputmethodservice.ExtractEditText; import android.os.Bundle; import android.os.Handler; -import android.os.Message; -import android.os.Messenger; import android.os.SystemClock; import android.provider.Settings; import android.text.DynamicLayout; @@ -1346,7 +1344,7 @@ public class Editor { DisplayList blockDisplayList = mTextDisplayLists[blockIndex]; if (blockDisplayList == null) { blockDisplayList = mTextDisplayLists[blockIndex] = - mTextView.getHardwareRenderer().createDisplayList("Text " + blockIndex); + DisplayList.create("Text " + blockIndex); } else { if (blockIsInvalid) blockDisplayList.clear(); } @@ -2321,8 +2319,8 @@ public class Editor { private final HashMap<SuggestionSpan, Integer> mSpansLengths; private class CustomPopupWindow extends PopupWindow { - public CustomPopupWindow(Context context, int defStyle) { - super(context, null, defStyle); + public CustomPopupWindow(Context context, int defStyleAttr) { + super(context, null, defStyleAttr); } @Override diff --git a/core/java/android/widget/ExpandableListView.java b/core/java/android/widget/ExpandableListView.java index 7b81aa8..70089e0 100644 --- a/core/java/android/widget/ExpandableListView.java +++ b/core/java/android/widget/ExpandableListView.java @@ -227,12 +227,16 @@ public class ExpandableListView extends ListView { this(context, attrs, com.android.internal.R.attr.expandableListViewStyle); } - public ExpandableListView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public ExpandableListView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ExpandableListView( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = - context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.ExpandableListView, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.ExpandableListView, defStyleAttr, defStyleRes); mGroupIndicator = a.getDrawable( com.android.internal.R.styleable.ExpandableListView_groupIndicator); diff --git a/core/java/android/widget/FastScroller.java b/core/java/android/widget/FastScroller.java index 01ac8fd..6d38c8f 100644 --- a/core/java/android/widget/FastScroller.java +++ b/core/java/android/widget/FastScroller.java @@ -24,7 +24,6 @@ import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.content.Context; import android.content.res.ColorStateList; -import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Rect; import android.graphics.drawable.Drawable; @@ -43,7 +42,7 @@ import android.view.ViewConfiguration; import android.view.ViewGroup.LayoutParams; import android.view.ViewGroupOverlay; import android.widget.AbsListView.OnScrollListener; -import com.android.internal.R; +import android.widget.ImageView.ScaleType; /** * Helper class for AbsListView to draw and control the Fast Scroll thumb @@ -76,24 +75,6 @@ class FastScroller { /** Scroll thumb and preview being dragged by user. */ private static final int STATE_DRAGGING = 2; - /** Styleable attributes. */ - private static final int[] ATTRS = new int[] { - android.R.attr.fastScrollTextColor, - android.R.attr.fastScrollThumbDrawable, - android.R.attr.fastScrollTrackDrawable, - android.R.attr.fastScrollPreviewBackgroundLeft, - android.R.attr.fastScrollPreviewBackgroundRight, - android.R.attr.fastScrollOverlayPosition - }; - - // Styleable attribute indices. - private static final int TEXT_COLOR = 0; - private static final int THUMB_DRAWABLE = 1; - private static final int TRACK_DRAWABLE = 2; - private static final int PREVIEW_BACKGROUND_LEFT = 3; - private static final int PREVIEW_BACKGROUND_RIGHT = 4; - private static final int OVERLAY_POSITION = 5; - // Positions for preview image and text. private static final int OVERLAY_FLOATING = 0; private static final int OVERLAY_AT_THUMB = 1; @@ -115,7 +96,7 @@ class FastScroller { private final TextView mSecondaryText; private final ImageView mThumbImage; private final ImageView mTrackImage; - private final ImageView mPreviewImage; + private final View mPreviewImage; /** * Preview image resource IDs for left- and right-aligned layouts. See @@ -127,13 +108,25 @@ class FastScroller { * Padding in pixels around the preview text. Applied as layout margins to * the preview text and padding to the preview image. */ - private final int mPreviewPadding; + private int mPreviewPadding; + + private int mPreviewMinWidth; + private int mPreviewMinHeight; + private int mThumbMinWidth; + private int mThumbMinHeight; + + /** Theme-specified text size. Used only if text appearance is not set. */ + private float mTextSize; - /** Whether there is a track image to display. */ - private final boolean mHasTrackImage; + /** Theme-specified text color. Used only if text appearance is not set. */ + private ColorStateList mTextColor; + + private Drawable mThumbDrawable; + private Drawable mTrackDrawable; + private int mTextAppearance; /** Total width of decorations. */ - private final int mWidth; + private int mWidth; /** Set containing decoration transition animations. */ private AnimatorSet mDecorAnimation; @@ -245,89 +238,143 @@ class FastScroller { } }; - public FastScroller(AbsListView listView) { + public FastScroller(AbsListView listView, int styleResId) { mList = listView; - mOverlay = listView.getOverlay(); final Context context = listView.getContext(); mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + mScrollBarStyle = listView.getScrollBarStyle(); + + mScrollCompleted = true; + mState = STATE_VISIBLE; + mMatchDragPosition = + context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB; + + mTrackImage = new ImageView(context); + mTrackImage.setScaleType(ScaleType.FIT_XY); + mThumbImage = new ImageView(context); + mThumbImage.setScaleType(ScaleType.FIT_XY); + mPreviewImage = new View(context); + mPreviewImage.setAlpha(0f); + + mPrimaryText = createPreviewTextView(context); + mSecondaryText = createPreviewTextView(context); + + setStyle(styleResId); - final Resources res = context.getResources(); - final TypedArray ta = context.getTheme().obtainStyledAttributes(ATTRS); + final ViewGroupOverlay overlay = listView.getOverlay(); + mOverlay = overlay; + overlay.add(mTrackImage); + overlay.add(mThumbImage); + overlay.add(mPreviewImage); + overlay.add(mPrimaryText); + overlay.add(mSecondaryText); - final ImageView trackImage = new ImageView(context); - mTrackImage = trackImage; + getSectionsFromIndexer(); + updateLongList(listView.getChildCount(), listView.getCount()); + setScrollbarPosition(listView.getVerticalScrollbarPosition()); + postAutoHide(); + } + private void updateAppearance() { + final Context context = mList.getContext(); int width = 0; // Add track to overlay if it has an image. - final Drawable trackDrawable = ta.getDrawable(TRACK_DRAWABLE); - if (trackDrawable != null) { - mHasTrackImage = true; - trackImage.setBackground(trackDrawable); - mOverlay.add(trackImage); - width = Math.max(width, trackDrawable.getIntrinsicWidth()); - } else { - mHasTrackImage = false; + mTrackImage.setImageDrawable(mTrackDrawable); + if (mTrackDrawable != null) { + width = Math.max(width, mTrackDrawable.getIntrinsicWidth()); } - final ImageView thumbImage = new ImageView(context); - mThumbImage = thumbImage; - // Add thumb to overlay if it has an image. - final Drawable thumbDrawable = ta.getDrawable(THUMB_DRAWABLE); - if (thumbDrawable != null) { - thumbImage.setImageDrawable(thumbDrawable); - mOverlay.add(thumbImage); - width = Math.max(width, thumbDrawable.getIntrinsicWidth()); + mThumbImage.setImageDrawable(mThumbDrawable); + mThumbImage.setMinimumWidth(mThumbMinWidth); + mThumbImage.setMinimumHeight(mThumbMinHeight); + if (mThumbDrawable != null) { + width = Math.max(width, mThumbDrawable.getIntrinsicWidth()); } - // If necessary, apply minimum thumb width and height. - if (thumbDrawable.getIntrinsicWidth() <= 0 || thumbDrawable.getIntrinsicHeight() <= 0) { - final int minWidth = res.getDimensionPixelSize(R.dimen.fastscroll_thumb_width); - thumbImage.setMinimumWidth(minWidth); - thumbImage.setMinimumHeight( - res.getDimensionPixelSize(R.dimen.fastscroll_thumb_height)); - width = Math.max(width, minWidth); - } + // Account for minimum thumb width. + mWidth = Math.max(width, mThumbMinWidth); - mWidth = width; + mPreviewImage.setMinimumWidth(mPreviewMinWidth); + mPreviewImage.setMinimumHeight(mPreviewMinHeight); - final int previewSize = res.getDimensionPixelSize(R.dimen.fastscroll_overlay_size); - mPreviewImage = new ImageView(context); - mPreviewImage.setMinimumWidth(previewSize); - mPreviewImage.setMinimumHeight(previewSize); - mPreviewImage.setAlpha(0f); - mOverlay.add(mPreviewImage); + if (mTextAppearance != 0) { + mPrimaryText.setTextAppearance(context, mTextAppearance); + mSecondaryText.setTextAppearance(context, mTextAppearance); + } - mPreviewPadding = res.getDimensionPixelSize(R.dimen.fastscroll_overlay_padding); + if (mTextColor != null) { + mPrimaryText.setTextColor(mTextColor); + mSecondaryText.setTextColor(mTextColor); + } + + if (mTextSize > 0) { + mPrimaryText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize); + mSecondaryText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize); + } - final int textMinSize = Math.max(0, previewSize - mPreviewPadding); - mPrimaryText = createPreviewTextView(context, ta); + final int textMinSize = Math.max(0, mPreviewMinHeight); mPrimaryText.setMinimumWidth(textMinSize); mPrimaryText.setMinimumHeight(textMinSize); - mOverlay.add(mPrimaryText); - mSecondaryText = createPreviewTextView(context, ta); mSecondaryText.setMinimumWidth(textMinSize); mSecondaryText.setMinimumHeight(textMinSize); - mOverlay.add(mSecondaryText); - mPreviewResId[PREVIEW_LEFT] = ta.getResourceId(PREVIEW_BACKGROUND_LEFT, 0); - mPreviewResId[PREVIEW_RIGHT] = ta.getResourceId(PREVIEW_BACKGROUND_RIGHT, 0); - mOverlayPosition = ta.getInt(OVERLAY_POSITION, OVERLAY_FLOATING); - ta.recycle(); + refreshDrawablePressedState(); + } - mScrollBarStyle = listView.getScrollBarStyle(); - mScrollCompleted = true; - mState = STATE_VISIBLE; - mMatchDragPosition = context.getApplicationInfo().targetSdkVersion - >= Build.VERSION_CODES.HONEYCOMB; + public void setStyle(int resId) { + final Context context = mList.getContext(); + final TypedArray ta = context.obtainStyledAttributes(null, + com.android.internal.R.styleable.FastScroll, android.R.attr.fastScrollStyle, resId); + final int N = ta.getIndexCount(); + for (int i = 0; i < N; i++) { + final int index = ta.getIndex(i); + switch (index) { + case com.android.internal.R.styleable.FastScroll_position: + mOverlayPosition = ta.getInt(index, OVERLAY_FLOATING); + break; + case com.android.internal.R.styleable.FastScroll_backgroundLeft: + mPreviewResId[PREVIEW_LEFT] = ta.getResourceId(index, 0); + break; + case com.android.internal.R.styleable.FastScroll_backgroundRight: + mPreviewResId[PREVIEW_RIGHT] = ta.getResourceId(index, 0); + break; + case com.android.internal.R.styleable.FastScroll_thumbDrawable: + mThumbDrawable = ta.getDrawable(index); + break; + case com.android.internal.R.styleable.FastScroll_trackDrawable: + mTrackDrawable = ta.getDrawable(index); + break; + case com.android.internal.R.styleable.FastScroll_textAppearance: + mTextAppearance = ta.getResourceId(index, 0); + break; + case com.android.internal.R.styleable.FastScroll_textColor: + mTextColor = ta.getColorStateList(index); + break; + case com.android.internal.R.styleable.FastScroll_textSize: + mTextSize = ta.getDimensionPixelSize(index, 0); + break; + case com.android.internal.R.styleable.FastScroll_minWidth: + mPreviewMinWidth = ta.getDimensionPixelSize(index, 0); + break; + case com.android.internal.R.styleable.FastScroll_minHeight: + mPreviewMinHeight = ta.getDimensionPixelSize(index, 0); + break; + case com.android.internal.R.styleable.FastScroll_thumbMinWidth: + mThumbMinWidth = ta.getDimensionPixelSize(index, 0); + break; + case com.android.internal.R.styleable.FastScroll_thumbMinHeight: + mThumbMinHeight = ta.getDimensionPixelSize(index, 0); + break; + case com.android.internal.R.styleable.FastScroll_padding: + mPreviewPadding = ta.getDimensionPixelSize(index, 0); + break; + } + } - getSectionsFromIndexer(); - refreshDrawablePressedState(); - updateLongList(listView.getChildCount(), listView.getCount()); - setScrollbarPosition(mList.getVerticalScrollbarPosition()); - postAutoHide(); + updateAppearance(); } /** @@ -469,17 +516,11 @@ class FastScroller { /** * Creates a view into which preview text can be placed. */ - private TextView createPreviewTextView(Context context, TypedArray ta) { + private TextView createPreviewTextView(Context context) { final LayoutParams params = new LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); - final Resources res = context.getResources(); - final int minSize = res.getDimensionPixelSize(R.dimen.fastscroll_overlay_size); - final ColorStateList textColor = ta.getColorStateList(TEXT_COLOR); - final float textSize = res.getDimensionPixelSize(R.dimen.fastscroll_overlay_text_size); final TextView textView = new TextView(context); textView.setLayoutParams(params); - textView.setTextColor(textColor); - textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); textView.setSingleLine(true); textView.setEllipsize(TruncateAt.MIDDLE); textView.setGravity(Gravity.CENTER); @@ -603,7 +644,7 @@ class FastScroller { view.measure(widthMeasureSpec, heightMeasureSpec); // Align to the left or right. - final int width = view.getMeasuredWidth(); + final int width = Math.min(adjMaxWidth, view.getMeasuredWidth()); final int left; final int right; if (mLayoutFromRight) { @@ -1020,7 +1061,7 @@ class FastScroller { } final Rect bounds = mTempBounds; - final ImageView preview = mPreviewImage; + final View preview = mPreviewImage; final TextView showing; final TextView target; if (mShowingPrimary) { @@ -1046,10 +1087,10 @@ class FastScroller { hideShowing.addListener(mSwitchPrimaryListener); // Apply preview image padding and animate bounds, if necessary. - bounds.left -= mPreviewImage.getPaddingLeft(); - bounds.top -= mPreviewImage.getPaddingTop(); - bounds.right += mPreviewImage.getPaddingRight(); - bounds.bottom += mPreviewImage.getPaddingBottom(); + bounds.left -= preview.getPaddingLeft(); + bounds.top -= preview.getPaddingTop(); + bounds.right += preview.getPaddingRight(); + bounds.bottom += preview.getPaddingBottom(); final Animator resizePreview = animateBounds(preview, bounds); resizePreview.setDuration(DURATION_RESIZE); @@ -1097,8 +1138,8 @@ class FastScroller { final int top = container.top; final int bottom = container.bottom; - final ImageView trackImage = mTrackImage; - final ImageView thumbImage = mThumbImage; + final View trackImage = mTrackImage; + final View thumbImage = mThumbImage; final float min = trackImage.getTop(); final float max = trackImage.getBottom(); final float offset = min; @@ -1109,7 +1150,7 @@ class FastScroller { final float previewPos = mOverlayPosition == OVERLAY_AT_THUMB ? thumbMiddle : 0; // Center the preview on the thumb, constrained to the list bounds. - final ImageView previewImage = mPreviewImage; + final View previewImage = mPreviewImage; final float previewHalfHeight = previewImage.getHeight() / 2f; final float minP = top + previewHalfHeight; final float maxP = bottom - previewHalfHeight; @@ -1122,11 +1163,7 @@ class FastScroller { } private float getPosFromMotionEvent(float y) { - final Rect container = mContainerRect; - final int top = container.top; - final int bottom = container.bottom; - - final ImageView trackImage = mTrackImage; + final View trackImage = mTrackImage; final float min = trackImage.getTop(); final float max = trackImage.getBottom(); final float offset = min; @@ -1393,7 +1430,7 @@ class FastScroller { * @return Whether the coordinate is inside the scroller's activation area. */ private boolean isPointInside(float x, float y) { - return isPointInsideX(x) && (mHasTrackImage || isPointInsideY(y)); + return isPointInsideX(x) && (mTrackDrawable != null || isPointInsideY(y)); } private boolean isPointInsideX(float x) { diff --git a/core/java/android/widget/FrameLayout.java b/core/java/android/widget/FrameLayout.java index d9d4ad7..b029328 100644 --- a/core/java/android/widget/FrameLayout.java +++ b/core/java/android/widget/FrameLayout.java @@ -97,11 +97,15 @@ public class FrameLayout extends ViewGroup { this(context, attrs, 0); } - public FrameLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public FrameLayout(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public FrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.FrameLayout, - defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.FrameLayout, defStyleAttr, defStyleRes); mForegroundGravity = a.getInt( com.android.internal.R.styleable.FrameLayout_foregroundGravity, mForegroundGravity); diff --git a/core/java/android/widget/Gallery.java b/core/java/android/widget/Gallery.java index 78ba6e0..f7c839f 100644 --- a/core/java/android/widget/Gallery.java +++ b/core/java/android/widget/Gallery.java @@ -196,14 +196,18 @@ public class Gallery extends AbsSpinner implements GestureDetector.OnGestureList this(context, attrs, R.attr.galleryStyle); } - public Gallery(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public Gallery(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public Gallery(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); mGestureDetector = new GestureDetector(context, this); mGestureDetector.setIsLongpressEnabled(true); - - TypedArray a = context.obtainStyledAttributes( - attrs, com.android.internal.R.styleable.Gallery, defStyle, 0); + + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.Gallery, defStyleAttr, defStyleRes); int index = a.getInt(com.android.internal.R.styleable.Gallery_gravity, -1); if (index >= 0) { diff --git a/core/java/android/widget/GridLayout.java b/core/java/android/widget/GridLayout.java index 54cc3f4..8511601 100644 --- a/core/java/android/widget/GridLayout.java +++ b/core/java/android/widget/GridLayout.java @@ -16,6 +16,7 @@ package android.widget; +import android.annotation.IntDef; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; @@ -35,6 +36,8 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; import com.android.internal.R; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Arrays; @@ -165,6 +168,11 @@ public class GridLayout extends ViewGroup { // Public constants + /** @hide */ + @IntDef({HORIZONTAL, VERTICAL}) + @Retention(RetentionPolicy.SOURCE) + public @interface Orientation {} + /** * The horizontal orientation. */ @@ -186,6 +194,11 @@ public class GridLayout extends ViewGroup { */ public static final int UNDEFINED = Integer.MIN_VALUE; + /** @hide */ + @IntDef({ALIGN_BOUNDS, ALIGN_MARGINS}) + @Retention(RetentionPolicy.SOURCE) + public @interface AlignmentMode {} + /** * This constant is an {@link #setAlignmentMode(int) alignmentMode}. * When the {@code alignmentMode} is set to {@link #ALIGN_BOUNDS}, alignment @@ -262,13 +275,23 @@ public class GridLayout extends ViewGroup { // Constructors - /** - * {@inheritDoc} - */ - public GridLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public GridLayout(Context context) { + this(context, null); + } + + public GridLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public GridLayout(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public GridLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); mDefaultGap = context.getResources().getDimensionPixelOffset(R.dimen.default_gap); - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.GridLayout); + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.GridLayout, defStyleAttr, defStyleRes); try { setRowCount(a.getInt(ROW_COUNT, DEFAULT_COUNT)); setColumnCount(a.getInt(COLUMN_COUNT, DEFAULT_COUNT)); @@ -282,21 +305,6 @@ public class GridLayout extends ViewGroup { } } - /** - * {@inheritDoc} - */ - public GridLayout(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - /** - * {@inheritDoc} - */ - public GridLayout(Context context) { - //noinspection NullableProblems - this(context, null); - } - // Implementation /** @@ -308,6 +316,7 @@ public class GridLayout extends ViewGroup { * * @attr ref android.R.styleable#GridLayout_orientation */ + @Orientation public int getOrientation() { return mOrientation; } @@ -348,7 +357,7 @@ public class GridLayout extends ViewGroup { * * @attr ref android.R.styleable#GridLayout_orientation */ - public void setOrientation(int orientation) { + public void setOrientation(@Orientation int orientation) { if (this.mOrientation != orientation) { this.mOrientation = orientation; invalidateStructure(); @@ -479,6 +488,7 @@ public class GridLayout extends ViewGroup { * * @attr ref android.R.styleable#GridLayout_alignmentMode */ + @AlignmentMode public int getAlignmentMode() { return mAlignmentMode; } @@ -498,7 +508,7 @@ public class GridLayout extends ViewGroup { * * @attr ref android.R.styleable#GridLayout_alignmentMode */ - public void setAlignmentMode(int alignmentMode) { + public void setAlignmentMode(@AlignmentMode int alignmentMode) { this.mAlignmentMode = alignmentMode; requestLayout(); } diff --git a/core/java/android/widget/GridView.java b/core/java/android/widget/GridView.java index 15daf83..acd711d 100644 --- a/core/java/android/widget/GridView.java +++ b/core/java/android/widget/GridView.java @@ -16,12 +16,14 @@ package android.widget; +import android.annotation.IntDef; import android.content.Context; import android.content.Intent; import android.content.res.TypedArray; import android.graphics.Rect; import android.os.Trace; import android.util.AttributeSet; +import android.util.MathUtils; import android.view.Gravity; import android.view.KeyEvent; import android.view.SoundEffectConstants; @@ -33,9 +35,11 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo; import android.view.accessibility.AccessibilityNodeInfo.CollectionItemInfo; import android.view.animation.GridLayoutAnimationController; -import android.widget.AbsListView.LayoutParams; import android.widget.RemoteViews.RemoteView; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * A view that shows items in two-dimensional scrolling grid. The items in the @@ -53,6 +57,11 @@ import android.widget.RemoteViews.RemoteView; */ @RemoteView public class GridView extends AbsListView { + /** @hide */ + @IntDef({NO_STRETCH, STRETCH_SPACING, STRETCH_COLUMN_WIDTH, STRETCH_SPACING_UNIFORM}) + @Retention(RetentionPolicy.SOURCE) + public @interface StretchMode {} + /** * Disables stretching. * @@ -110,11 +119,15 @@ public class GridView extends AbsListView { this(context, attrs, com.android.internal.R.attr.gridViewStyle); } - public GridView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public GridView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public GridView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.GridView, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.GridView, defStyleAttr, defStyleRes); int hSpacing = a.getDimensionPixelOffset( com.android.internal.R.styleable.GridView_horizontalSpacing, 0); @@ -1202,6 +1215,24 @@ public class GridView extends AbsListView { setSelectedPositionInt(mNextSelectedPosition); + // Remember which child, if any, had accessibility focus. + final int accessibilityFocusPosition; + final View accessFocusedChild = getAccessibilityFocusedChild(); + if (accessFocusedChild != null) { + accessibilityFocusPosition = getPositionForView(accessFocusedChild); + accessFocusedChild.setHasTransientState(true); + } else { + accessibilityFocusPosition = INVALID_POSITION; + } + + // Ensure the child containing focus, if any, has transient state. + // If the list data hasn't changed, or if the adapter has stable + // IDs, this will maintain focus. + final View focusedChild = getFocusedChild(); + if (focusedChild != null) { + focusedChild.setHasTransientState(true); + } + // Pull all children into the RecycleBin. // These views will be reused if possible final int firstPosition = mFirstPosition; @@ -1216,7 +1247,6 @@ public class GridView extends AbsListView { } // Clear out old views - //removeAllViewsInLayout(); detachAllViewsFromParent(); recycleBin.removeSkippedScrap(); @@ -1287,6 +1317,27 @@ public class GridView extends AbsListView { mSelectorRect.setEmpty(); } + if (accessFocusedChild != null) { + accessFocusedChild.setHasTransientState(false); + + // If we failed to maintain accessibility focus on the previous + // view, attempt to restore it to the previous position. + if (!accessFocusedChild.isAccessibilityFocused() + && accessibilityFocusPosition != INVALID_POSITION) { + // Bound the position within the visible children. + final int position = MathUtils.constrain( + accessibilityFocusPosition - mFirstPosition, 0, getChildCount() - 1); + final View restoreView = getChildAt(position); + if (restoreView != null) { + restoreView.requestAccessibilityFocus(); + } + } + } + + if (focusedChild != null) { + focusedChild.setHasTransientState(false); + } + mLayoutMode = LAYOUT_NORMAL; mDataChanged = false; if (mPositionScrollAfterLayout != null) { @@ -2056,13 +2107,14 @@ public class GridView extends AbsListView { * * @attr ref android.R.styleable#GridView_stretchMode */ - public void setStretchMode(int stretchMode) { + public void setStretchMode(@StretchMode int stretchMode) { if (stretchMode != mStretchMode) { mStretchMode = stretchMode; requestLayoutIfNecessary(); } } + @StretchMode public int getStretchMode() { return mStretchMode; } diff --git a/core/java/android/widget/HorizontalScrollView.java b/core/java/android/widget/HorizontalScrollView.java index dab0962..25d4f42 100644 --- a/core/java/android/widget/HorizontalScrollView.java +++ b/core/java/android/widget/HorizontalScrollView.java @@ -146,12 +146,17 @@ public class HorizontalScrollView extends FrameLayout { this(context, attrs, com.android.internal.R.attr.horizontalScrollViewStyle); } - public HorizontalScrollView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public HorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public HorizontalScrollView( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); initScrollView(); - TypedArray a = context.obtainStyledAttributes(attrs, - android.R.styleable.HorizontalScrollView, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, android.R.styleable.HorizontalScrollView, defStyleAttr, defStyleRes); setFillViewport(a.getBoolean(android.R.styleable.HorizontalScrollView_fillViewport, false)); diff --git a/core/java/android/widget/ImageButton.java b/core/java/android/widget/ImageButton.java index 379354c..3a20628 100644 --- a/core/java/android/widget/ImageButton.java +++ b/core/java/android/widget/ImageButton.java @@ -17,16 +17,11 @@ package android.widget; import android.content.Context; -import android.os.Handler; -import android.os.Message; import android.util.AttributeSet; -import android.view.MotionEvent; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; -import java.util.Map; - /** * <p> * Displays a button with an image (instead of text) that can be pressed @@ -83,8 +78,12 @@ public class ImageButton extends ImageView { this(context, attrs, com.android.internal.R.attr.imageButtonStyle); } - public ImageButton(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public ImageButton(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ImageButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); setFocusable(true); } diff --git a/core/java/android/widget/ImageView.java b/core/java/android/widget/ImageView.java index 7daf798..79d5e5d 100644 --- a/core/java/android/widget/ImageView.java +++ b/core/java/android/widget/ImageView.java @@ -119,12 +119,17 @@ public class ImageView extends View { this(context, attrs, 0); } - public ImageView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public ImageView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ImageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + initImageView(); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.ImageView, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.ImageView, defStyleAttr, defStyleRes); Drawable d = a.getDrawable(com.android.internal.R.styleable.ImageView_src); if (d != null) { diff --git a/core/java/android/widget/LegacyTimePickerDelegate.java b/core/java/android/widget/LegacyTimePickerDelegate.java new file mode 100644 index 0000000..1634d5f --- /dev/null +++ b/core/java/android/widget/LegacyTimePickerDelegate.java @@ -0,0 +1,638 @@ +/* + * Copyright (C) 2013 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.widget; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.TypedArray; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.format.DateFormat; +import android.text.format.DateUtils; +import android.util.AttributeSet; +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; + +import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_AUTO; +import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES; + +/** + * A delegate implementing the basic TimePicker + */ +class LegacyTimePickerDelegate extends TimePicker.AbstractTimePickerDelegate { + + private static final boolean DEFAULT_ENABLED_STATE = true; + + private static final int HOURS_IN_HALF_DAY = 12; + + // state + private boolean mIs24HourView; + + private boolean mIsAm; + + // ui components + private final NumberPicker mHourSpinner; + + private final NumberPicker mMinuteSpinner; + + private final NumberPicker mAmPmSpinner; + + private final EditText mHourSpinnerInput; + + private final EditText mMinuteSpinnerInput; + + private final EditText mAmPmSpinnerInput; + + private final TextView mDivider; + + // Note that the legacy implementation of the TimePicker is + // using a button for toggling between AM/PM while the new + // version uses a NumberPicker spinner. Therefore the code + // accommodates these two cases to be backwards compatible. + private final Button mAmPmButton; + + private final String[] mAmPmStrings; + + private boolean mIsEnabled = DEFAULT_ENABLED_STATE; + + private Calendar mTempCalendar; + + private boolean mHourWithTwoDigit; + private char mHourFormat; + + /** + * A no-op callback used in the constructor to avoid null checks later in + * the code. + */ + private static final TimePicker.OnTimeChangedListener NO_OP_CHANGE_LISTENER = + new TimePicker.OnTimeChangedListener() { + public void onTimeChanged(TimePicker view, int hourOfDay, int minute) { + } + }; + + public LegacyTimePickerDelegate(TimePicker delegator, Context context, AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + super(delegator, context); + + // process style attributes + final TypedArray attributesArray = mContext.obtainStyledAttributes( + attrs, R.styleable.TimePicker, defStyleAttr, defStyleRes); + final int layoutResourceId = attributesArray.getResourceId( + R.styleable.TimePicker_legacyLayout, R.layout.time_picker_legacy); + attributesArray.recycle(); + + final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(layoutResourceId, mDelegator, true); + + // hour + mHourSpinner = (NumberPicker) delegator.findViewById(R.id.hour); + mHourSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { + public void onValueChange(NumberPicker spinner, int oldVal, int newVal) { + updateInputState(); + if (!is24HourView()) { + if ((oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) || + (oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1)) { + mIsAm = !mIsAm; + updateAmPmControl(); + } + } + onTimeChanged(); + } + }); + mHourSpinnerInput = (EditText) mHourSpinner.findViewById(R.id.numberpicker_input); + mHourSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); + + // divider (only for the new widget style) + mDivider = (TextView) mDelegator.findViewById(R.id.divider); + if (mDivider != null) { + setDividerText(); + } + + // minute + mMinuteSpinner = (NumberPicker) mDelegator.findViewById(R.id.minute); + mMinuteSpinner.setMinValue(0); + mMinuteSpinner.setMaxValue(59); + mMinuteSpinner.setOnLongPressUpdateInterval(100); + mMinuteSpinner.setFormatter(NumberPicker.getTwoDigitFormatter()); + mMinuteSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { + public void onValueChange(NumberPicker spinner, int oldVal, int newVal) { + updateInputState(); + int minValue = mMinuteSpinner.getMinValue(); + int maxValue = mMinuteSpinner.getMaxValue(); + if (oldVal == maxValue && newVal == minValue) { + int newHour = mHourSpinner.getValue() + 1; + if (!is24HourView() && newHour == HOURS_IN_HALF_DAY) { + mIsAm = !mIsAm; + updateAmPmControl(); + } + mHourSpinner.setValue(newHour); + } else if (oldVal == minValue && newVal == maxValue) { + int newHour = mHourSpinner.getValue() - 1; + if (!is24HourView() && newHour == HOURS_IN_HALF_DAY - 1) { + mIsAm = !mIsAm; + updateAmPmControl(); + } + mHourSpinner.setValue(newHour); + } + onTimeChanged(); + } + }); + mMinuteSpinnerInput = (EditText) mMinuteSpinner.findViewById(R.id.numberpicker_input); + mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); + + /* Get the localized am/pm strings and use them in the spinner */ + mAmPmStrings = new DateFormatSymbols().getAmPmStrings(); + + // am/pm + View amPmView = mDelegator.findViewById(R.id.amPm); + if (amPmView instanceof Button) { + mAmPmSpinner = null; + mAmPmSpinnerInput = null; + mAmPmButton = (Button) amPmView; + mAmPmButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View button) { + button.requestFocus(); + mIsAm = !mIsAm; + updateAmPmControl(); + onTimeChanged(); + } + }); + } else { + mAmPmButton = null; + mAmPmSpinner = (NumberPicker) amPmView; + mAmPmSpinner.setMinValue(0); + mAmPmSpinner.setMaxValue(1); + mAmPmSpinner.setDisplayedValues(mAmPmStrings); + mAmPmSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + updateInputState(); + picker.requestFocus(); + mIsAm = !mIsAm; + updateAmPmControl(); + onTimeChanged(); + } + }); + mAmPmSpinnerInput = (EditText) mAmPmSpinner.findViewById(R.id.numberpicker_input); + mAmPmSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE); + } + + if (isAmPmAtStart()) { + // Move the am/pm view to the beginning + ViewGroup amPmParent = (ViewGroup) delegator.findViewById(R.id.timePickerLayout); + amPmParent.removeView(amPmView); + amPmParent.addView(amPmView, 0); + // Swap layout margins if needed. They may be not symmetrical (Old Standard Theme + // for example and not for Holo Theme) + ViewGroup.MarginLayoutParams lp = + (ViewGroup.MarginLayoutParams) amPmView.getLayoutParams(); + final int startMargin = lp.getMarginStart(); + final int endMargin = lp.getMarginEnd(); + if (startMargin != endMargin) { + lp.setMarginStart(endMargin); + lp.setMarginEnd(startMargin); + } + } + + getHourFormatData(); + + // update controls to initial state + updateHourControl(); + updateMinuteControl(); + updateAmPmControl(); + + setOnTimeChangedListener(NO_OP_CHANGE_LISTENER); + + // set to current time + setCurrentHour(mTempCalendar.get(Calendar.HOUR_OF_DAY)); + setCurrentMinute(mTempCalendar.get(Calendar.MINUTE)); + + if (!isEnabled()) { + setEnabled(false); + } + + // set the content descriptions + setContentDescriptions(); + + // If not explicitly specified this view is important for accessibility. + if (mDelegator.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + mDelegator.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } + } + + private void getHourFormatData() { + final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, + (mIs24HourView) ? "Hm" : "hm"); + final int lengthPattern = bestDateTimePattern.length(); + mHourWithTwoDigit = false; + char hourFormat = '\0'; + // Check if the returned pattern is single or double 'H', 'h', 'K', 'k'. We also save + // the hour format that we found. + for (int i = 0; i < lengthPattern; i++) { + final char c = bestDateTimePattern.charAt(i); + if (c == 'H' || c == 'h' || c == 'K' || c == 'k') { + mHourFormat = c; + if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) { + mHourWithTwoDigit = true; + } + break; + } + } + } + + private boolean isAmPmAtStart() { + final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, + "hm" /* skeleton */); + + return bestDateTimePattern.startsWith("a"); + } + + /** + * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":". + * + * See http://unicode.org/cldr/trac/browser/trunk/common/main + * + * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the + * separator as the character which is just after the hour marker in the returned pattern. + */ + private void setDividerText() { + final String skeleton = (mIs24HourView) ? "Hm" : "hm"; + final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, + skeleton); + final String separatorText; + int hourIndex = bestDateTimePattern.lastIndexOf('H'); + if (hourIndex == -1) { + hourIndex = bestDateTimePattern.lastIndexOf('h'); + } + if (hourIndex == -1) { + // Default case + separatorText = ":"; + } else { + int minuteIndex = bestDateTimePattern.indexOf('m', hourIndex + 1); + if (minuteIndex == -1) { + separatorText = Character.toString(bestDateTimePattern.charAt(hourIndex + 1)); + } else { + separatorText = bestDateTimePattern.substring(hourIndex + 1, minuteIndex); + } + } + mDivider.setText(separatorText); + } + + @Override + public void setCurrentHour(Integer currentHour) { + setCurrentHour(currentHour, true); + } + + private void setCurrentHour(Integer currentHour, boolean notifyTimeChanged) { + // why was Integer used in the first place? + if (currentHour == null || currentHour == getCurrentHour()) { + return; + } + if (!is24HourView()) { + // convert [0,23] ordinal to wall clock display + if (currentHour >= HOURS_IN_HALF_DAY) { + mIsAm = false; + if (currentHour > HOURS_IN_HALF_DAY) { + currentHour = currentHour - HOURS_IN_HALF_DAY; + } + } else { + mIsAm = true; + if (currentHour == 0) { + currentHour = HOURS_IN_HALF_DAY; + } + } + updateAmPmControl(); + } + mHourSpinner.setValue(currentHour); + if (notifyTimeChanged) { + onTimeChanged(); + } + } + + @Override + public Integer getCurrentHour() { + int currentHour = mHourSpinner.getValue(); + if (is24HourView()) { + return currentHour; + } else if (mIsAm) { + return currentHour % HOURS_IN_HALF_DAY; + } else { + return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY; + } + } + + @Override + public void setCurrentMinute(Integer currentMinute) { + if (currentMinute == getCurrentMinute()) { + return; + } + mMinuteSpinner.setValue(currentMinute); + onTimeChanged(); + } + + @Override + public Integer getCurrentMinute() { + return mMinuteSpinner.getValue(); + } + + @Override + public void setIs24HourView(Boolean is24HourView) { + if (mIs24HourView == is24HourView) { + return; + } + // cache the current hour since spinner range changes and BEFORE changing mIs24HourView!! + int currentHour = getCurrentHour(); + // Order is important here. + mIs24HourView = is24HourView; + getHourFormatData(); + updateHourControl(); + // set value after spinner range is updated + setCurrentHour(currentHour, false); + updateMinuteControl(); + updateAmPmControl(); + } + + @Override + public boolean is24HourView() { + return mIs24HourView; + } + + @Override + public void setOnTimeChangedListener(TimePicker.OnTimeChangedListener onTimeChangedListener) { + mOnTimeChangedListener = onTimeChangedListener; + } + + @Override + public void setEnabled(boolean enabled) { + mMinuteSpinner.setEnabled(enabled); + if (mDivider != null) { + mDivider.setEnabled(enabled); + } + mHourSpinner.setEnabled(enabled); + if (mAmPmSpinner != null) { + mAmPmSpinner.setEnabled(enabled); + } else { + mAmPmButton.setEnabled(enabled); + } + mIsEnabled = enabled; + } + + @Override + public boolean isEnabled() { + return mIsEnabled; + } + + @Override + public void setShowDoneButton(boolean showDoneButton) { + // Nothing to do + } + + @Override + public void setDismissCallback(TimePicker.TimePickerDismissCallback callback) { + // Nothing to do + } + + @Override + public int getBaseline() { + return mHourSpinner.getBaseline(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + setCurrentLocale(newConfig.locale); + } + + @Override + public Parcelable onSaveInstanceState(Parcelable superState) { + return new SavedState(superState, getCurrentHour(), getCurrentMinute()); + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + setCurrentHour(ss.getHour()); + setCurrentMinute(ss.getMinute()); + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + onPopulateAccessibilityEvent(event); + return true; + } + + @Override + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + int flags = DateUtils.FORMAT_SHOW_TIME; + if (mIs24HourView) { + flags |= DateUtils.FORMAT_24HOUR; + } else { + flags |= DateUtils.FORMAT_12HOUR; + } + mTempCalendar.set(Calendar.HOUR_OF_DAY, getCurrentHour()); + mTempCalendar.set(Calendar.MINUTE, getCurrentMinute()); + String selectedDateUtterance = DateUtils.formatDateTime(mContext, + mTempCalendar.getTimeInMillis(), flags); + 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 + // changed the value via the IME and there is a next input the IME will + // be shown, otherwise the user chose another means of changing the + // value and having the IME up makes no sense. + InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); + if (inputMethodManager != null) { + if (inputMethodManager.isActive(mHourSpinnerInput)) { + mHourSpinnerInput.clearFocus(); + inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); + } else if (inputMethodManager.isActive(mMinuteSpinnerInput)) { + mMinuteSpinnerInput.clearFocus(); + inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); + } else if (inputMethodManager.isActive(mAmPmSpinnerInput)) { + mAmPmSpinnerInput.clearFocus(); + inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); + } + } + } + + private void updateAmPmControl() { + if (is24HourView()) { + if (mAmPmSpinner != null) { + mAmPmSpinner.setVisibility(View.GONE); + } else { + mAmPmButton.setVisibility(View.GONE); + } + } else { + int index = mIsAm ? Calendar.AM : Calendar.PM; + if (mAmPmSpinner != null) { + mAmPmSpinner.setValue(index); + mAmPmSpinner.setVisibility(View.VISIBLE); + } else { + mAmPmButton.setText(mAmPmStrings[index]); + mAmPmButton.setVisibility(View.VISIBLE); + } + } + mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + } + + /** + * Sets the current locale. + * + * @param locale The current locale. + */ + @Override + public void setCurrentLocale(Locale locale) { + super.setCurrentLocale(locale); + mTempCalendar = Calendar.getInstance(locale); + } + + private void onTimeChanged() { + mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + if (mOnTimeChangedListener != null) { + mOnTimeChangedListener.onTimeChanged(mDelegator, getCurrentHour(), + getCurrentMinute()); + } + } + + private void updateHourControl() { + if (is24HourView()) { + // 'k' means 1-24 hour + if (mHourFormat == 'k') { + mHourSpinner.setMinValue(1); + mHourSpinner.setMaxValue(24); + } else { + mHourSpinner.setMinValue(0); + mHourSpinner.setMaxValue(23); + } + } else { + // 'K' means 0-11 hour + if (mHourFormat == 'K') { + mHourSpinner.setMinValue(0); + mHourSpinner.setMaxValue(11); + } else { + mHourSpinner.setMinValue(1); + mHourSpinner.setMaxValue(12); + } + } + mHourSpinner.setFormatter(mHourWithTwoDigit ? NumberPicker.getTwoDigitFormatter() : null); + } + + private void updateMinuteControl() { + if (is24HourView()) { + mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE); + } else { + mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); + } + } + + private void setContentDescriptions() { + // Minute + trySetContentDescription(mMinuteSpinner, R.id.increment, + R.string.time_picker_increment_minute_button); + trySetContentDescription(mMinuteSpinner, R.id.decrement, + R.string.time_picker_decrement_minute_button); + // Hour + trySetContentDescription(mHourSpinner, R.id.increment, + R.string.time_picker_increment_hour_button); + trySetContentDescription(mHourSpinner, R.id.decrement, + R.string.time_picker_decrement_hour_button); + // AM/PM + if (mAmPmSpinner != null) { + trySetContentDescription(mAmPmSpinner, R.id.increment, + R.string.time_picker_increment_set_pm_button); + trySetContentDescription(mAmPmSpinner, R.id.decrement, + R.string.time_picker_decrement_set_am_button); + } + } + + private void trySetContentDescription(View root, int viewId, int contDescResId) { + View target = root.findViewById(viewId); + if (target != null) { + target.setContentDescription(mContext.getString(contDescResId)); + } + } + + /** + * Used to save / restore state of time picker + */ + private static class SavedState extends View.BaseSavedState { + + private final int mHour; + + private final int mMinute; + + private SavedState(Parcelable superState, int hour, int minute) { + super(superState); + mHour = hour; + mMinute = minute; + } + + private SavedState(Parcel in) { + super(in); + mHour = in.readInt(); + mMinute = in.readInt(); + } + + public int getHour() { + return mHour; + } + + public int getMinute() { + return mMinute; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(mHour); + dest.writeInt(mMinute); + } + + @SuppressWarnings({"unused", "hiding"}) + public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } +} + diff --git a/core/java/android/widget/LinearLayout.java b/core/java/android/widget/LinearLayout.java index ad60a95..65f1ab7 100644 --- a/core/java/android/widget/LinearLayout.java +++ b/core/java/android/widget/LinearLayout.java @@ -18,6 +18,7 @@ package android.widget; import com.android.internal.R; +import android.annotation.IntDef; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; @@ -31,6 +32,9 @@ import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * A Layout that arranges its children in a single column or a single row. The direction of @@ -57,9 +61,25 @@ import android.widget.RemoteViews.RemoteView; */ @RemoteView public class LinearLayout extends ViewGroup { + /** @hide */ + @IntDef({HORIZONTAL, VERTICAL}) + @Retention(RetentionPolicy.SOURCE) + public @interface OrientationMode {} + public static final int HORIZONTAL = 0; public static final int VERTICAL = 1; + /** @hide */ + @IntDef(flag = true, + value = { + SHOW_DIVIDER_NONE, + SHOW_DIVIDER_BEGINNING, + SHOW_DIVIDER_MIDDLE, + SHOW_DIVIDER_END + }) + @Retention(RetentionPolicy.SOURCE) + public @interface DividerMode {} + /** * Don't show any dividers. */ @@ -165,18 +185,22 @@ public class LinearLayout extends ViewGroup { private int mDividerPadding; public LinearLayout(Context context) { - super(context); + this(context, null); } public LinearLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } - public LinearLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public LinearLayout(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public LinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.LinearLayout, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.LinearLayout, defStyleAttr, defStyleRes); int index = a.getInt(com.android.internal.R.styleable.LinearLayout_orientation, -1); if (index >= 0) { @@ -214,7 +238,7 @@ public class LinearLayout extends ViewGroup { * {@link #SHOW_DIVIDER_MIDDLE}, or {@link #SHOW_DIVIDER_END}, * or {@link #SHOW_DIVIDER_NONE} to show no dividers. */ - public void setShowDividers(int showDividers) { + public void setShowDividers(@DividerMode int showDividers) { if (showDividers != mShowDividers) { requestLayout(); } @@ -230,6 +254,7 @@ public class LinearLayout extends ViewGroup { * @return A flag set indicating how dividers should be shown around items. * @see #setShowDividers(int) */ + @DividerMode public int getShowDividers() { return mShowDividers; } @@ -1673,12 +1698,12 @@ public class LinearLayout extends ViewGroup { /** * Should the layout be a column or a row. - * @param orientation Pass HORIZONTAL or VERTICAL. Default - * value is HORIZONTAL. + * @param orientation Pass {@link #HORIZONTAL} or {@link #VERTICAL}. Default + * value is {@link #HORIZONTAL}. * * @attr ref android.R.styleable#LinearLayout_orientation */ - public void setOrientation(int orientation) { + public void setOrientation(@OrientationMode int orientation) { if (mOrientation != orientation) { mOrientation = orientation; requestLayout(); @@ -1690,6 +1715,7 @@ public class LinearLayout extends ViewGroup { * * @return either {@link #HORIZONTAL} or {@link #VERTICAL} */ + @OrientationMode public int getOrientation() { return mOrientation; } diff --git a/core/java/android/widget/ListPopupWindow.java b/core/java/android/widget/ListPopupWindow.java index 66fe46f..64953f8 100644 --- a/core/java/android/widget/ListPopupWindow.java +++ b/core/java/android/widget/ListPopupWindow.java @@ -33,7 +33,6 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.View.MeasureSpec; -import android.view.View.OnAttachStateChangeListener; import android.view.View.OnTouchListener; import android.view.ViewConfiguration; import android.view.ViewGroup; diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java index 78237c3..0cd35e8 100644 --- a/core/java/android/widget/ListView.java +++ b/core/java/android/widget/ListView.java @@ -39,7 +39,6 @@ import android.view.View; 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; @@ -142,11 +141,15 @@ public class ListView extends AbsListView { this(context, attrs, com.android.internal.R.attr.listViewStyle); } - public ListView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public ListView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.ListView, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.ListView, defStyleAttr, defStyleRes); CharSequence[] entries = a.getTextArray( com.android.internal.R.styleable.ListView_entries); @@ -1729,34 +1732,6 @@ public class ListView extends AbsListView { } /** - * @return the direct child that contains accessibility focus, or null if no - * child contains accessibility focus - */ - private View getAccessibilityFocusedChild() { - final ViewRootImpl viewRootImpl = getViewRootImpl(); - if (viewRootImpl == null) { - return null; - } - - View focusedView = viewRootImpl.getAccessibilityFocusedHost(); - if (focusedView == null) { - return null; - } - - ViewParent viewParent = focusedView.getParent(); - while ((viewParent instanceof View) && (viewParent != this)) { - focusedView = (View) viewParent; - viewParent = viewParent.getParent(); - } - - if (!(viewParent instanceof View)) { - return null; - } - - return focusedView; - } - - /** * Obtain the view and add it to our list of children. The view can be made * fresh, converted from an unused view, or used as is if it was in the * recycle bin. diff --git a/core/java/android/widget/MultiAutoCompleteTextView.java b/core/java/android/widget/MultiAutoCompleteTextView.java index 0b30c84..cbd01b0 100644 --- a/core/java/android/widget/MultiAutoCompleteTextView.java +++ b/core/java/android/widget/MultiAutoCompleteTextView.java @@ -67,8 +67,13 @@ public class MultiAutoCompleteTextView extends AutoCompleteTextView { this(context, attrs, com.android.internal.R.attr.autoCompleteTextViewStyle); } - public MultiAutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public MultiAutoCompleteTextView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public MultiAutoCompleteTextView( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); } /* package */ void finishInit() { } diff --git a/core/java/android/widget/NumberPicker.java b/core/java/android/widget/NumberPicker.java index c0fde2e..44c4987 100644 --- a/core/java/android/widget/NumberPicker.java +++ b/core/java/android/widget/NumberPicker.java @@ -16,6 +16,7 @@ package android.widget; +import android.annotation.IntDef; import android.annotation.Widget; import android.content.Context; import android.content.res.ColorStateList; @@ -53,6 +54,8 @@ import android.view.inputmethod.InputMethodManager; import com.android.internal.R; import libcore.icu.LocaleData; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -493,6 +496,10 @@ public class NumberPicker extends LinearLayout { * Interface to listen for the picker scroll state. */ public interface OnScrollListener { + /** @hide */ + @IntDef({SCROLL_STATE_IDLE, SCROLL_STATE_TOUCH_SCROLL, SCROLL_STATE_FLING}) + @Retention(RetentionPolicy.SOURCE) + public @interface ScrollState {} /** * The view is not scrolling. @@ -518,7 +525,7 @@ public class NumberPicker extends LinearLayout { * {@link #SCROLL_STATE_TOUCH_SCROLL} or * {@link #SCROLL_STATE_IDLE}. */ - public void onScrollStateChange(NumberPicker view, int scrollState); + public void onScrollStateChange(NumberPicker view, @ScrollState int scrollState); } /** @@ -559,14 +566,33 @@ public class NumberPicker extends LinearLayout { * * @param context the application environment. * @param attrs a collection of attributes. - * @param defStyle The default style to apply to this 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. */ - public NumberPicker(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public NumberPicker(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + /** + * Create a new number picker + * + * @param context the application environment. + * @param attrs a collection of attributes. + * @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. + */ + public NumberPicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); // process style attributes - TypedArray attributesArray = context.obtainStyledAttributes( - attrs, R.styleable.NumberPicker, defStyle, 0); + final TypedArray attributesArray = context.obtainStyledAttributes( + attrs, R.styleable.NumberPicker, defStyleAttr, defStyleRes); final int layoutResId = attributesArray.getResourceId( R.styleable.NumberPicker_internalLayout, DEFAULT_LAYOUT_RESOURCE_ID); diff --git a/core/java/android/widget/OverScroller.java b/core/java/android/widget/OverScroller.java index f218199..7b3dd31 100644 --- a/core/java/android/widget/OverScroller.java +++ b/core/java/android/widget/OverScroller.java @@ -70,7 +70,11 @@ public class OverScroller { * @hide */ public OverScroller(Context context, Interpolator interpolator, boolean flywheel) { - mInterpolator = interpolator; + if (interpolator == null) { + mInterpolator = new Scroller.ViscousFluidInterpolator(); + } else { + mInterpolator = interpolator; + } mFlywheel = flywheel; mScrollerX = new SplineOverScroller(context); mScrollerY = new SplineOverScroller(context); @@ -112,7 +116,11 @@ public class OverScroller { } void setInterpolator(Interpolator interpolator) { - mInterpolator = interpolator; + if (interpolator == null) { + mInterpolator = new Scroller.ViscousFluidInterpolator(); + } else { + mInterpolator = interpolator; + } } /** @@ -302,14 +310,7 @@ public class OverScroller { final int duration = mScrollerX.mDuration; if (elapsedTime < duration) { - float q = (float) (elapsedTime) / duration; - - if (mInterpolator == null) { - q = Scroller.viscousFluid(q); - } else { - q = mInterpolator.getInterpolation(q); - } - + final float q = mInterpolator.getInterpolation(elapsedTime / (float) duration); mScrollerX.updateScroll(q); mScrollerY.updateScroll(q); } else { diff --git a/core/java/android/widget/PopupWindow.java b/core/java/android/widget/PopupWindow.java index 5663959..e77a810 100644 --- a/core/java/android/widget/PopupWindow.java +++ b/core/java/android/widget/PopupWindow.java @@ -170,8 +170,8 @@ public class PopupWindow { * * <p>The popup does provide a background.</p> */ - public PopupWindow(Context context, AttributeSet attrs, int defStyle) { - this(context, attrs, defStyle, 0); + public PopupWindow(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); } /** @@ -183,8 +183,7 @@ public class PopupWindow { mContext = context; mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); - TypedArray a = - context.obtainStyledAttributes( + final TypedArray a = context.obtainStyledAttributes( attrs, com.android.internal.R.styleable.PopupWindow, defStyleAttr, defStyleRes); mBackground = a.getDrawable(R.styleable.PopupWindow_popupBackground); diff --git a/core/java/android/widget/ProgressBar.java b/core/java/android/widget/ProgressBar.java index 5392a96..1fbcbcf 100644 --- a/core/java/android/widget/ProgressBar.java +++ b/core/java/android/widget/ProgressBar.java @@ -242,29 +242,26 @@ public class ProgressBar extends View { this(context, attrs, com.android.internal.R.attr.progressBarStyle); } - public ProgressBar(Context context, AttributeSet attrs, int defStyle) { - this(context, attrs, defStyle, 0); + public ProgressBar(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); } - /** - * @hide - */ - public ProgressBar(Context context, AttributeSet attrs, int defStyle, int styleRes) { - super(context, attrs, defStyle); + public ProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + mUiThreadId = Thread.currentThread().getId(); initProgressBar(); - TypedArray a = - context.obtainStyledAttributes(attrs, R.styleable.ProgressBar, defStyle, styleRes); + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.ProgressBar, defStyleAttr, defStyleRes); mNoInvalidate = true; Drawable drawable = a.getDrawable(R.styleable.ProgressBar_progressDrawable); if (drawable != null) { - drawable = tileify(drawable, false); // Calling this method can set mMaxHeight, make sure the corresponding // XML attribute for mMaxHeight is read after calling this method - setProgressDrawable(drawable); + setProgressDrawableTiled(drawable); } @@ -293,8 +290,7 @@ public class ProgressBar extends View { drawable = a.getDrawable(R.styleable.ProgressBar_indeterminateDrawable); if (drawable != null) { - drawable = tileifyIndeterminate(drawable); - setIndeterminateDrawable(drawable); + setIndeterminateDrawableTiled(drawable); } mOnlyIndeterminate = a.getBoolean( @@ -467,11 +463,9 @@ public class ProgressBar extends View { } /** - * <p>Define the drawable used to draw the progress bar in - * indeterminate mode.</p> + * Define the drawable used to draw the progress bar in indeterminate mode. * * @param d the new drawable - * * @see #getIndeterminateDrawable() * @see #setIndeterminate(boolean) */ @@ -488,6 +482,25 @@ public class ProgressBar extends View { postInvalidate(); } } + + /** + * Define the tileable drawable used to draw the progress bar in + * indeterminate mode. + * <p> + * If the drawable is a BitmapDrawable or contains BitmapDrawables, a + * tiled copy will be generated for display as a progress bar. + * + * @param d the new drawable + * @see #getIndeterminateDrawable() + * @see #setIndeterminate(boolean) + */ + public void setIndeterminateDrawableTiled(Drawable d) { + if (d != null) { + d = tileifyIndeterminate(d); + } + + setIndeterminateDrawable(d); + } /** * <p>Get the drawable used to draw the progress bar in @@ -503,11 +516,9 @@ public class ProgressBar extends View { } /** - * <p>Define the drawable used to draw the progress bar in - * progress mode.</p> + * Define the drawable used to draw the progress bar in progress mode. * * @param d the new drawable - * * @see #getProgressDrawable() * @see #setIndeterminate(boolean) */ @@ -546,6 +557,25 @@ public class ProgressBar extends View { doRefreshProgress(R.id.secondaryProgress, mSecondaryProgress, false, false); } } + + /** + * Define the tileable drawable used to draw the progress bar in + * progress mode. + * <p> + * If the drawable is a BitmapDrawable or contains BitmapDrawables, a + * tiled copy will be generated for display as a progress bar. + * + * @param d the new drawable + * @see #getProgressDrawable() + * @see #setIndeterminate(boolean) + */ + public void setProgressDrawableTiled(Drawable d) { + if (d != null) { + d = tileify(d, false); + } + + setProgressDrawable(d); + } /** * @return The drawable currently used to draw the progress bar diff --git a/core/java/android/widget/QuickContactBadge.java b/core/java/android/widget/QuickContactBadge.java index fd2f754..a4f758c 100644 --- a/core/java/android/widget/QuickContactBadge.java +++ b/core/java/android/widget/QuickContactBadge.java @@ -84,8 +84,13 @@ public class QuickContactBadge extends ImageView implements OnClickListener { this(context, attrs, 0); } - public QuickContactBadge(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public QuickContactBadge(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public QuickContactBadge( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); TypedArray styledAttributes = mContext.obtainStyledAttributes(R.styleable.Theme); mOverlay = styledAttributes.getDrawable( diff --git a/core/java/android/widget/RadialTimePickerView.java b/core/java/android/widget/RadialTimePickerView.java new file mode 100644 index 0000000..1c9ab61 --- /dev/null +++ b/core/java/android/widget/RadialTimePickerView.java @@ -0,0 +1,1396 @@ +/* + * Copyright (C) 2013 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.widget; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.Keyframe; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Typeface; +import android.graphics.RectF; +import android.os.Bundle; +import android.text.format.DateUtils; +import android.text.format.Time; +import android.util.AttributeSet; +import android.util.Log; +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 com.android.internal.R; + +import java.text.DateFormatSymbols; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Locale; + +/** + * View to show a clock circle picker (with one or two picking circles) + * + * @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; + + private static final int HOURS = 0; + private static final int MINUTES = 1; + private static final int HOURS_INNER = 2; + private static final int AMPM = 3; + + private static final int SELECTOR_CIRCLE = 0; + private static final int SELECTOR_DOT = 1; + private static final int SELECTOR_LINE = 2; + + private static final int AM = 0; + private static final int PM = 1; + + // Opaque alpha level + private static final int ALPHA_OPAQUE = 255; + + // Transparent alpha level + private static final int ALPHA_TRANSPARENT = 0; + + // Alpha level of color for selector. + private static final int ALPHA_SELECTOR = 51; + + // Alpha level of color for selected circle. + private static final int ALPHA_AMPM_SELECTED = ALPHA_SELECTOR; + + // Alpha level of color for pressed circle. + private static final int ALPHA_AMPM_PRESSED = 175; + + 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; + + private static final int[] HOURS_NUMBERS = {12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; + 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 int[] sSnapPrefer30sMap = new int[361]; + + private final String[] mHours12Texts = new String[12]; + private final String[] mOuterHours24Texts = new String[12]; + private final String[] mInnerHours24Texts = new String[12]; + private final String[] mMinutesTexts = new String[12]; + + private final String[] mAmPmText = new String[2]; + + private final Paint[] mPaint = new Paint[2]; + private final Paint mPaintCenter = new Paint(); + private final Paint[][] mPaintSelector = new Paint[2][3]; + private final Paint mPaintAmPmText = new Paint(); + private final Paint[] mPaintAmPmCircle = new Paint[2]; + + private final Paint mPaintBackground = new Paint(); + private final Paint mPaintDisabled = new Paint(); + private final Paint mPaintDebug = new Paint(); + + private Typeface mTypeface; + + private boolean mIs24HourMode; + private boolean mShowHours; + private boolean mIsOnInnerCircle; + + private int mXCenter; + private int mYCenter; + + private float[] mCircleRadius = new float[3]; + + private int mMinHypotenuseForInnerNumber; + private int mMaxHypotenuseForOuterNumber; + private int mHalfwayHypotenusePoint; + + private float[] mTextSize = new float[2]; + private float mInnerTextSize; + + private float[][] mTextGridHeights = new float[2][7]; + private float[][] mTextGridWidths = new float[2][7]; + + private float[] mInnerTextGridHeights = new float[7]; + private float[] mInnerTextGridWidths = new float[7]; + + private String[] mOuterTextHours; + private String[] mInnerTextHours; + private String[] mOuterTextMinutes; + + private float[] mCircleRadiusMultiplier = new float[2]; + private float[] mNumbersRadiusMultiplier = new float[3]; + + private float[] mTextSizeMultiplier = new float[3]; + + private float[] mAnimationRadiusMultiplier = new float[3]; + + private float mTransitionMidRadiusMultiplier; + private float mTransitionEndRadiusMultiplier; + + private AnimatorSet mTransition; + private InvalidateUpdateListener mInvalidateUpdateListener = new InvalidateUpdateListener(); + + private int[] mLineLength = new int[3]; + private int[] mSelectionRadius = new int[3]; + private float mSelectionRadiusMultiplier; + private int[] mSelectionDegrees = new int[3]; + + private int mAmPmCircleRadius; + private float mAmPmYCenter; + + private float mAmPmCircleRadiusMultiplier; + private int mAmPmTextColor; + + private float mLeftIndicatorXCenter; + private float mRightIndicatorXCenter; + + private int mAmPmUnselectedColor; + private int mAmPmSelectedColor; + + private int mAmOrPm; + private int mAmOrPmPressed; + + private RectF mRectF = new RectF(); + private boolean mInputEnabled = true; + private OnValueSelectedListener mListener; + + private final ArrayList<Animator> mHoursToMinutesAnims = new ArrayList<Animator>(); + private final ArrayList<Animator> mMinuteToHoursAnims = new ArrayList<Animator>(); + + public interface OnValueSelectedListener { + 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 + * to a visible value : space apportioned to a non-visible value will be 14 : 4. + * E.g. the output of 30 degrees should have a higher range of input associated with it than + * the output of 24 degrees, because 30 degrees corresponds to a visible number on the clock + * circle (5 on the minutes, 1 or 13 on the hours). + */ + private static void preparePrefer30sMap() { + // We'll split up the visible output and the non-visible output such that each visible + // output will correspond to a range of 14 associated input degrees, and each non-visible + // output will correspond to a range of 4 associate input degrees, so visible numbers + // are more than 3 times easier to get than non-visible numbers: + // {354-359,0-7}:0, {8-11}:6, {12-15}:12, {16-19}:18, {20-23}:24, {24-37}:30, etc. + // + // If an output of 30 degrees should correspond to a range of 14 associated degrees, then + // we'll need any input between 24 - 37 to snap to 30. Working out from there, 20-23 should + // snap to 24, while 38-41 should snap to 36. This is somewhat counter-intuitive, that you + // can be touching 36 degrees but have the selection snapped to 30 degrees; however, this + // inconsistency isn't noticeable at such fine-grained degrees, and it affords us the + // ability to aggressively prefer the visible values by a factor of more than 3:1, which + // greatly contributes to the selectability of these values. + + // The first output is 0, and each following output will increment by 6 {0, 6, 12, ...}. + int snappedOutputDegrees = 0; + // Count of how many inputs we've designated to the specified output. + int count = 1; + // How many input we expect for a specified output. This will be 14 for output divisible + // by 30, and 4 for the remaining output. We'll special case the outputs of 0 and 360, so + // the caller can decide which they need. + int expectedCount = 8; + // Iterate through the input. + for (int degrees = 0; degrees < 361; degrees++) { + // Save the input-output mapping. + sSnapPrefer30sMap[degrees] = snappedOutputDegrees; + // If this is the last input for the specified output, calculate the next output and + // the next expected count. + if (count == expectedCount) { + snappedOutputDegrees += 6; + if (snappedOutputDegrees == 360) { + expectedCount = 7; + } else if (snappedOutputDegrees % 30 == 0) { + expectedCount = 14; + } else { + expectedCount = 4; + } + count = 1; + } else { + count++; + } + } + } + + /** + * Returns mapping of any input degrees (0 to 360) to one of 60 selectable output degrees, + * where the degrees corresponding to visible numbers (i.e. those divisible by 30) will be + * weighted heavier than the degrees corresponding to non-visible numbers. + * See {@link #preparePrefer30sMap()} documentation for the rationale and generation of the + * mapping. + */ + private static int snapPrefer30s(int degrees) { + if (sSnapPrefer30sMap == null) { + return -1; + } + return sSnapPrefer30sMap[degrees]; + } + + /** + * Returns mapping of any input degrees (0 to 360) to one of 12 visible output degrees (all + * multiples of 30), where the input will be "snapped" to the closest visible degrees. + * @param degrees The input degrees + * @param forceHigherOrLower The output may be forced to either the higher or lower step, or may + * be allowed to snap to whichever is closer. Use 1 to force strictly higher, -1 to force + * strictly lower, and 0 to snap to the closer one. + * @return output degrees, will be a multiple of 30 + */ + private static int snapOnly30s(int degrees, int forceHigherOrLower) { + final int stepSize = DEGREES_FOR_ONE_HOUR; + int floor = (degrees / stepSize) * stepSize; + final int ceiling = floor + stepSize; + if (forceHigherOrLower == 1) { + degrees = ceiling; + } else if (forceHigherOrLower == -1) { + if (degrees == floor) { + floor -= stepSize; + } + degrees = floor; + } else { + if ((degrees - floor) < (ceiling - degrees)) { + degrees = floor; + } else { + degrees = ceiling; + } + } + return degrees; + } + + public RadialTimePickerView(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.timePickerStyle); + } + + public RadialTimePickerView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs); + + // process style attributes + final TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.TimePicker, + defStyle, 0); + + final Resources res = getResources(); + + mAmPmUnselectedColor = a.getColor(R.styleable.TimePicker_amPmUnselectedBackgroundColor, + res.getColor( + R.color.timepicker_default_ampm_unselected_background_color_holo_light)); + + mAmPmSelectedColor = a.getColor(R.styleable.TimePicker_amPmSelectedBackgroundColor, + res.getColor(R.color.timepicker_default_ampm_selected_background_color_holo_light)); + + mAmPmTextColor = a.getColor(R.styleable.TimePicker_amPmTextColor, + res.getColor(R.color.timepicker_default_text_color_holo_light)); + + final int numbersTextColor = a.getColor(R.styleable.TimePicker_numbersTextColor, + res.getColor(R.color.timepicker_default_text_color_holo_light)); + + mTypeface = Typeface.create("sans-serif", Typeface.NORMAL); + + mPaint[HOURS] = new Paint(); + mPaint[HOURS].setColor(numbersTextColor); + mPaint[HOURS].setAntiAlias(true); + mPaint[HOURS].setTextAlign(Paint.Align.CENTER); + + mPaint[MINUTES] = new Paint(); + mPaint[MINUTES].setColor(numbersTextColor); + mPaint[MINUTES].setAntiAlias(true); + mPaint[MINUTES].setTextAlign(Paint.Align.CENTER); + + mPaintCenter.setColor(numbersTextColor); + mPaintCenter.setAntiAlias(true); + mPaintCenter.setTextAlign(Paint.Align.CENTER); + + mPaintSelector[HOURS][SELECTOR_CIRCLE] = new Paint(); + mPaintSelector[HOURS][SELECTOR_CIRCLE].setColor( + a.getColor(R.styleable.TimePicker_numbersSelectorColor, R.color.holo_blue_light)); + mPaintSelector[HOURS][SELECTOR_CIRCLE].setAntiAlias(true); + + mPaintSelector[HOURS][SELECTOR_DOT] = new Paint(); + mPaintSelector[HOURS][SELECTOR_DOT].setColor( + a.getColor(R.styleable.TimePicker_numbersSelectorColor, R.color.holo_blue_light)); + mPaintSelector[HOURS][SELECTOR_DOT].setAntiAlias(true); + + mPaintSelector[HOURS][SELECTOR_LINE] = new Paint(); + mPaintSelector[HOURS][SELECTOR_LINE].setColor( + a.getColor(R.styleable.TimePicker_numbersSelectorColor, R.color.holo_blue_light)); + mPaintSelector[HOURS][SELECTOR_LINE].setAntiAlias(true); + mPaintSelector[HOURS][SELECTOR_LINE].setStrokeWidth(2); + + mPaintSelector[MINUTES][SELECTOR_CIRCLE] = new Paint(); + mPaintSelector[MINUTES][SELECTOR_CIRCLE].setColor( + a.getColor(R.styleable.TimePicker_numbersSelectorColor, R.color.holo_blue_light)); + mPaintSelector[MINUTES][SELECTOR_CIRCLE].setAntiAlias(true); + + mPaintSelector[MINUTES][SELECTOR_DOT] = new Paint(); + mPaintSelector[MINUTES][SELECTOR_DOT].setColor( + a.getColor(R.styleable.TimePicker_numbersSelectorColor, R.color.holo_blue_light)); + mPaintSelector[MINUTES][SELECTOR_DOT].setAntiAlias(true); + + mPaintSelector[MINUTES][SELECTOR_LINE] = new Paint(); + mPaintSelector[MINUTES][SELECTOR_LINE].setColor( + a.getColor(R.styleable.TimePicker_numbersSelectorColor, R.color.holo_blue_light)); + mPaintSelector[MINUTES][SELECTOR_LINE].setAntiAlias(true); + mPaintSelector[MINUTES][SELECTOR_LINE].setStrokeWidth(2); + + mPaintAmPmText.setColor(mAmPmTextColor); + mPaintAmPmText.setTypeface(mTypeface); + mPaintAmPmText.setAntiAlias(true); + mPaintAmPmText.setTextAlign(Paint.Align.CENTER); + + mPaintAmPmCircle[AM] = new Paint(); + mPaintAmPmCircle[AM].setAntiAlias(true); + mPaintAmPmCircle[PM] = new Paint(); + mPaintAmPmCircle[PM].setAntiAlias(true); + + mPaintBackground.setColor( + a.getColor(R.styleable.TimePicker_numbersBackgroundColor, Color.WHITE)); + mPaintBackground.setAntiAlias(true); + + final int disabledColor = a.getColor(R.styleable.TimePicker_disabledColor, + res.getColor(R.color.timepicker_default_disabled_color_holo_light)); + mPaintDisabled.setColor(disabledColor); + mPaintDisabled.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); + } + + mShowHours = true; + mIs24HourMode = false; + mAmOrPm = AM; + mAmOrPmPressed = -1; + + 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)); + + setOnTouchListener(this); + + // Initial values + final Calendar calendar = Calendar.getInstance(Locale.getDefault()); + final int currentHour = calendar.get(Calendar.HOUR_OF_DAY); + final int currentMinute = calendar.get(Calendar.MINUTE); + + setCurrentHour(currentHour); + setCurrentMinute(currentMinute); + + 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) { + mIs24HourMode = is24HourMode; + setCurrentHour(hour); + setCurrentMinute(minute); + } + + public void setCurrentItemShowing(int item, boolean animate) { + switch (item){ + case HOURS: + showHours(animate); + break; + case MINUTES: + showMinutes(animate); + break; + default: + Log.e(TAG, "ClockView does not support showing item " + item); + } + } + + public int getCurrentItemShowing() { + return mShowHours ? HOURS : MINUTES; + } + + public void setOnValueSelectedListener(OnValueSelectedListener listener) { + mListener = listener; + } + + public void setCurrentHour(int hour) { + final int degrees = (hour % 12) * DEGREES_FOR_ONE_HOUR; + mSelectionDegrees[HOURS] = degrees; + mSelectionDegrees[HOURS_INNER] = degrees; + mAmOrPm = ((hour % 24) < 12) ? AM : PM; + if (mIs24HourMode) { + mIsOnInnerCircle = (mAmOrPm == AM); + } else { + mIsOnInnerCircle = false; + } + initData(); + updateLayoutData(); + invalidate(); + } + + // Return hours in 0-23 range + public int getCurrentHour() { + int hours = + mSelectionDegrees[mIsOnInnerCircle ? HOURS_INNER : HOURS] / DEGREES_FOR_ONE_HOUR; + if (mIs24HourMode) { + if (mIsOnInnerCircle) { + hours = hours % 12; + } else { + if (hours != 0) { + hours += 12; + } + } + } else { + hours = hours % 12; + if (hours == 0) { + if (mAmOrPm == PM) { + hours = 12; + } + } else { + if (mAmOrPm == PM) { + hours += 12; + } + } + } + return hours; + } + + public void setCurrentMinute(int minute) { + mSelectionDegrees[MINUTES] = (minute % 60) * DEGREES_FOR_ONE_MINUTE; + invalidate(); + } + + // Returns minutes in 0-59 range + public int getCurrentMinute() { + return (mSelectionDegrees[MINUTES] / DEGREES_FOR_ONE_MINUTE); + } + + public void setAmOrPm(int val) { + mAmOrPm = (val % 2); + invalidate(); + } + + public int getAmOrPm() { + return mAmOrPm; + } + + public void swapAmPm() { + mAmOrPm = (mAmOrPm == AM) ? PM : AM; + invalidate(); + } + + public void showHours(boolean animate) { + if (mShowHours) return; + mShowHours = true; + if (animate) { + startMinutesToHoursAnimation(); + } + initData(); + updateLayoutData(); + invalidate(); + } + + public void showMinutes(boolean animate) { + if (!mShowHours) return; + mShowHours = false; + if (animate) { + startHoursToMinutesAnimation(); + } + initData(); + updateLayoutData(); + invalidate(); + } + + private void initHoursAndMinutesText() { + // Initialize the hours and minutes numbers. + for (int i = 0; i < 12; i++) { + mHours12Texts[i] = String.format("%d", HOURS_NUMBERS[i]); + mOuterHours24Texts[i] = String.format("%02d", HOURS_NUMBERS_24[i]); + mInnerHours24Texts[i] = String.format("%d", HOURS_NUMBERS[i]); + mMinutesTexts[i] = String.format("%02d", MINUTES_NUMBERS[i]); + } + + String[] amPmTexts = new DateFormatSymbols().getAmPmStrings(); + mAmPmText[AM] = amPmTexts[0]; + mAmPmText[PM] = amPmTexts[1]; + } + + private void initData() { + if (mIs24HourMode) { + mOuterTextHours = mOuterHours24Texts; + mInnerTextHours = mInnerHours24Texts; + } else { + mOuterTextHours = mHours12Texts; + mInnerTextHours = null; + } + + 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; + + mAmPmCircleRadiusMultiplier = Float.parseFloat( + res.getString(R.string.timepicker_ampm_circle_radius_multiplier)); + + mPaint[HOURS].setAlpha(mShowHours ? ALPHA_OPAQUE : ALPHA_TRANSPARENT); + mPaint[MINUTES].setAlpha(mShowHours ? ALPHA_TRANSPARENT : ALPHA_OPAQUE); + + mPaintSelector[HOURS][SELECTOR_CIRCLE].setAlpha( + mShowHours ?ALPHA_SELECTOR : ALPHA_TRANSPARENT); + mPaintSelector[HOURS][SELECTOR_DOT].setAlpha( + mShowHours ? ALPHA_OPAQUE : ALPHA_TRANSPARENT); + mPaintSelector[HOURS][SELECTOR_LINE].setAlpha( + mShowHours ? ALPHA_SELECTOR : ALPHA_TRANSPARENT); + + mPaintSelector[MINUTES][SELECTOR_CIRCLE].setAlpha( + mShowHours ? ALPHA_TRANSPARENT : ALPHA_SELECTOR); + mPaintSelector[MINUTES][SELECTOR_DOT].setAlpha( + mShowHours ? ALPHA_TRANSPARENT : ALPHA_OPAQUE); + mPaintSelector[MINUTES][SELECTOR_LINE].setAlpha( + mShowHours ? ALPHA_TRANSPARENT : ALPHA_SELECTOR); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + updateLayoutData(); + } + + private void updateLayoutData() { + mXCenter = getWidth() / 2; + mYCenter = getHeight() / 2; + + final int min = Math.min(mXCenter, mYCenter); + + mCircleRadius[HOURS] = min * mCircleRadiusMultiplier[HOURS]; + mCircleRadius[HOURS_INNER] = min * mCircleRadiusMultiplier[HOURS]; + mCircleRadius[MINUTES] = min * mCircleRadiusMultiplier[MINUTES]; + + if (!mIs24HourMode) { + // We'll need to draw the AM/PM circles, so the main circle will need to have + // a slightly higher center. To keep the entire view centered vertically, we'll + // have to push it up by half the radius of the AM/PM circles. + int amPmCircleRadius = (int) (mCircleRadius[HOURS] * mAmPmCircleRadiusMultiplier); + mYCenter -= amPmCircleRadius / 2; + } + + mMinHypotenuseForInnerNumber = (int) (mCircleRadius[HOURS] + * mNumbersRadiusMultiplier[HOURS_INNER]) - mSelectionRadius[HOURS]; + mMaxHypotenuseForOuterNumber = (int) (mCircleRadius[HOURS] + * 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); + + mAmPmCircleRadius = (int) (mCircleRadius[HOURS] * mAmPmCircleRadiusMultiplier); + mPaintAmPmText.setTextSize(mAmPmCircleRadius * 3 / 4); + + // Line up the vertical center of the AM/PM circles with the bottom of the main circle. + mAmPmYCenter = mYCenter + mCircleRadius[HOURS]; + + // Line up the horizontal edges of the AM/PM circles with the horizontal edges + // of the main circle + mLeftIndicatorXCenter = mXCenter - mCircleRadius[HOURS] + mAmPmCircleRadius; + mRightIndicatorXCenter = mXCenter + mCircleRadius[HOURS] - mAmPmCircleRadius; + } + + @Override + public void onDraw(Canvas canvas) { + canvas.save(); + + calculateGridSizesHours(); + calculateGridSizesMinutes(); + + drawCircleBackground(canvas); + + drawTextElements(canvas, mTextSize[HOURS], mTypeface, mOuterTextHours, + mTextGridWidths[HOURS], mTextGridHeights[HOURS], mPaint[HOURS]); + + if (mIs24HourMode && mInnerTextHours != null) { + drawTextElements(canvas, mInnerTextSize, mTypeface, mInnerTextHours, + mInnerTextGridWidths, mInnerTextGridHeights, mPaint[HOURS]); + } + + drawTextElements(canvas, mTextSize[MINUTES], mTypeface, mOuterTextMinutes, + mTextGridWidths[MINUTES], mTextGridHeights[MINUTES], mPaint[MINUTES]); + + drawCenter(canvas); + drawSelector(canvas); + if (!mIs24HourMode) { + drawAmPm(canvas); + } + + if(!mInputEnabled) { + // Draw outer view rectangle + mRectF.set(0, 0, getWidth(), getHeight()); + canvas.drawRect(mRectF, mPaintDisabled); + } + + if (DEBUG) { + drawDebug(canvas); + } + + canvas.restore(); + } + + private void drawCircleBackground(Canvas canvas) { + canvas.drawCircle(mXCenter, mYCenter, mCircleRadius[HOURS], mPaintBackground); + } + + private void drawCenter(Canvas canvas) { + canvas.drawCircle(mXCenter, mYCenter, CENTER_RADIUS, mPaintCenter); + } + + private void drawSelector(Canvas canvas) { + drawSelector(canvas, mIsOnInnerCircle ? HOURS_INNER : HOURS); + drawSelector(canvas, MINUTES); + } + + private void drawAmPm(Canvas canvas) { + final boolean isLayoutRtl = isLayoutRtl(); + + int amColor = mAmPmUnselectedColor; + int amAlpha = ALPHA_OPAQUE; + int pmColor = mAmPmUnselectedColor; + int pmAlpha = ALPHA_OPAQUE; + if (mAmOrPm == AM) { + amColor = mAmPmSelectedColor; + amAlpha = ALPHA_AMPM_SELECTED; + } else if (mAmOrPm == PM) { + pmColor = mAmPmSelectedColor; + pmAlpha = ALPHA_AMPM_SELECTED; + } + if (mAmOrPmPressed == AM) { + amColor = mAmPmSelectedColor; + amAlpha = ALPHA_AMPM_PRESSED; + } else if (mAmOrPmPressed == PM) { + pmColor = mAmPmSelectedColor; + pmAlpha = ALPHA_AMPM_PRESSED; + } + + // Draw the two circles + mPaintAmPmCircle[AM].setColor(amColor); + mPaintAmPmCircle[AM].setAlpha(amAlpha); + canvas.drawCircle(isLayoutRtl ? mRightIndicatorXCenter : mLeftIndicatorXCenter, + mAmPmYCenter, mAmPmCircleRadius, mPaintAmPmCircle[AM]); + + mPaintAmPmCircle[PM].setColor(pmColor); + mPaintAmPmCircle[PM].setAlpha(pmAlpha); + canvas.drawCircle(isLayoutRtl ? mLeftIndicatorXCenter : mRightIndicatorXCenter, + mAmPmYCenter, mAmPmCircleRadius, mPaintAmPmCircle[PM]); + + // Draw the AM/PM texts on top + mPaintAmPmText.setColor(mAmPmTextColor); + float textYCenter = mAmPmYCenter - + (int) (mPaintAmPmText.descent() + mPaintAmPmText.ascent()) / 2; + + canvas.drawText(isLayoutRtl ? mAmPmText[PM] : mAmPmText[AM], mLeftIndicatorXCenter, + textYCenter, mPaintAmPmText); + canvas.drawText(isLayoutRtl ? mAmPmText[AM] : mAmPmText[PM], mRightIndicatorXCenter, + textYCenter, mPaintAmPmText); + } + + private void drawSelector(Canvas canvas, int index) { + // Calculate the current radius at which to place the selection circle. + mLineLength[index] = (int) (mCircleRadius[index] + * mNumbersRadiusMultiplier[index] * mAnimationRadiusMultiplier[index]); + + double selectionRadians = Math.toRadians(mSelectionDegrees[index]); + + int pointX = mXCenter + (int) (mLineLength[index] * Math.sin(selectionRadians)); + int pointY = mYCenter - (int) (mLineLength[index] * Math.cos(selectionRadians)); + + // Draw the selection circle + canvas.drawCircle(pointX, pointY, mSelectionRadius[index], + mPaintSelector[index % 2][SELECTOR_CIRCLE]); + + // Draw the dot if needed + if (mSelectionDegrees[index] % 30 != 0) { + // We're not on a direct tick + canvas.drawCircle(pointX, pointY, (mSelectionRadius[index] * 2 / 7), + mPaintSelector[index % 2][SELECTOR_DOT]); + } 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)); + } + + // Draw the line + canvas.drawLine(mXCenter, mYCenter, pointX, pointY, + mPaintSelector[index % 2][SELECTOR_LINE]); + } + + 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; + mRectF = new RectF(left, top, right, bottom); + canvas.drawRect(mRectF, mPaintDebug); + + // Draw outer rectangle for background + left = mXCenter - mCircleRadius[HOURS]; + top = mYCenter - mCircleRadius[HOURS]; + right = mXCenter + mCircleRadius[HOURS]; + bottom = mYCenter + mCircleRadius[HOURS]; + mRectF.set(left, top, right, bottom); + canvas.drawRect(mRectF, mPaintDebug); + + // Draw outer view rectangle + mRectF.set(0, 0, getWidth(), getHeight()); + canvas.drawRect(mRectF, 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.toString(), x, y, paint); + } + + private void calculateGridSizesHours() { + // Calculate the text positions + float numbersRadius = mCircleRadius[HOURS] + * mNumbersRadiusMultiplier[HOURS] * mAnimationRadiusMultiplier[HOURS]; + + // Calculate the positions for the 12 numbers in the main circle. + calculateGridSizes(mPaint[HOURS], numbersRadius, mXCenter, mYCenter, + mTextSize[HOURS], mTextGridHeights[HOURS], mTextGridWidths[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); + } + } + + private void calculateGridSizesMinutes() { + // Calculate the text positions + float numbersRadius = mCircleRadius[MINUTES] + * mNumbersRadiusMultiplier[MINUTES] * mAnimationRadiusMultiplier[MINUTES]; + + // Calculate the positions for the 12 numbers in the main circle. + calculateGridSizes(mPaint[MINUTES], numbersRadius, mXCenter, mYCenter, + mTextSize[MINUTES], mTextGridHeights[MINUTES], mTextGridWidths[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; + + 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; + } + + /** + * 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) { + paint.setTextSize(textSize); + paint.setTypeface(typeface); + 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 + private void setAnimationRadiusMultiplierHours(float animationRadiusMultiplier) { + mAnimationRadiusMultiplier[HOURS] = animationRadiusMultiplier; + mAnimationRadiusMultiplier[HOURS_INNER] = animationRadiusMultiplier; + } + + // Used for animating the minutes by changing their radius + private void setAnimationRadiusMultiplierMinutes(float animationRadiusMultiplier) { + mAnimationRadiusMultiplier[MINUTES] = animationRadiusMultiplier; + } + + 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; + } + + 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; + } + + private static ObjectAnimator getFadeOutAnimator(Object target, int startAlpha, int endAlpha, + InvalidateUpdateListener updateListener) { + int duration = 500; + ObjectAnimator animator = ObjectAnimator.ofInt(target, "alpha", startAlpha, endAlpha); + animator.setDuration(duration); + animator.addUpdateListener(updateListener); + + return animator; + } + + private static ObjectAnimator getFadeInAnimator(Object 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; + + kf0 = Keyframe.ofInt(0f, startAlpha); + kf1 = Keyframe.ofInt(delayPoint, startAlpha); + kf2 = Keyframe.ofInt(1f, endAlpha); + PropertyValuesHolder fadeIn = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1, kf2); + + ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder( + target, fadeIn).setDuration(totalDuration); + animator.addUpdateListener(updateListener); + return animator; + } + + private class InvalidateUpdateListener implements ValueAnimator.AnimatorUpdateListener { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + RadialTimePickerView.this.invalidate(); + } + } + + private void startHoursToMinutesAnimation() { + if (mHoursToMinutesAnims.size() == 0) { + mHoursToMinutesAnims.add(getRadiusDisappearAnimator(this, + "animationRadiusMultiplierHours", mInvalidateUpdateListener, + mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier)); + mHoursToMinutesAnims.add(getFadeOutAnimator(mPaint[HOURS], + ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); + mHoursToMinutesAnims.add(getFadeOutAnimator(mPaintSelector[HOURS][SELECTOR_CIRCLE], + ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); + mHoursToMinutesAnims.add(getFadeOutAnimator(mPaintSelector[HOURS][SELECTOR_DOT], + ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); + mHoursToMinutesAnims.add(getFadeOutAnimator(mPaintSelector[HOURS][SELECTOR_LINE], + ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); + + mHoursToMinutesAnims.add(getRadiusReappearAnimator(this, + "animationRadiusMultiplierMinutes", mInvalidateUpdateListener, + mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier)); + mHoursToMinutesAnims.add(getFadeInAnimator(mPaint[MINUTES], + ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); + mHoursToMinutesAnims.add(getFadeInAnimator(mPaintSelector[MINUTES][SELECTOR_CIRCLE], + ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener)); + mHoursToMinutesAnims.add(getFadeInAnimator(mPaintSelector[MINUTES][SELECTOR_DOT], + ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); + mHoursToMinutesAnims.add(getFadeInAnimator(mPaintSelector[MINUTES][SELECTOR_LINE], + ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener)); + } + + if (mTransition != null && mTransition.isRunning()) { + mTransition.end(); + } + mTransition = new AnimatorSet(); + mTransition.playTogether(mHoursToMinutesAnims); + mTransition.start(); + } + + private void startMinutesToHoursAnimation() { + if (mMinuteToHoursAnims.size() == 0) { + mMinuteToHoursAnims.add(getRadiusDisappearAnimator(this, + "animationRadiusMultiplierMinutes", mInvalidateUpdateListener, + mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier)); + mMinuteToHoursAnims.add(getFadeOutAnimator(mPaint[MINUTES], + ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); + mMinuteToHoursAnims.add(getFadeOutAnimator(mPaintSelector[MINUTES][SELECTOR_CIRCLE], + ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); + mMinuteToHoursAnims.add(getFadeOutAnimator(mPaintSelector[MINUTES][SELECTOR_DOT], + ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); + mMinuteToHoursAnims.add(getFadeOutAnimator(mPaintSelector[MINUTES][SELECTOR_LINE], + ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); + + mMinuteToHoursAnims.add(getRadiusReappearAnimator(this, + "animationRadiusMultiplierHours", mInvalidateUpdateListener, + mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier)); + mMinuteToHoursAnims.add(getFadeInAnimator(mPaint[HOURS], + ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); + mMinuteToHoursAnims.add(getFadeInAnimator(mPaintSelector[HOURS][SELECTOR_CIRCLE], + ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener)); + mMinuteToHoursAnims.add(getFadeInAnimator(mPaintSelector[HOURS][SELECTOR_DOT], + ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); + mMinuteToHoursAnims.add(getFadeInAnimator(mPaintSelector[HOURS][SELECTOR_LINE], + ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener)); + } + + if (mTransition != null && mTransition.isRunning()) { + mTransition.end(); + } + mTransition = new AnimatorSet(); + mTransition.playTogether(mMinuteToHoursAnims); + mTransition.start(); + } + + private int getDegreesFromXY(float x, float y) { + 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]) { + return -1; + } + // Check + if (mIs24HourMode && mShowHours) { + if (hypotenuse >= mMinHypotenuseForInnerNumber + && hypotenuse <= mHalfwayHypotenusePoint) { + mIsOnInnerCircle = true; + } else if (hypotenuse <= mMaxHypotenuseForOuterNumber + && hypotenuse >= mHalfwayHypotenusePoint) { + mIsOnInnerCircle = false; + } else { + return -1; + } + } 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) { + return -1; + } + } + + final float opposite = Math.abs(y - mYCenter); + double degrees = Math.toDegrees(Math.asin(opposite / hypotenuse)); + + // Now we have to translate to the correct quadrant. + boolean rightSide = (x > mXCenter); + boolean topSide = (y < mYCenter); + if (rightSide && topSide) { + degrees = 90 - degrees; + } else if (rightSide && !topSide) { + degrees = 90 + degrees; + } else if (!rightSide && !topSide) { + degrees = 270 - degrees; + } else if (!rightSide && topSide) { + degrees = 270 + degrees; + } + return (int) degrees; + } + + private int getIsTouchingAmOrPm(float x, float y) { + final boolean isLayoutRtl = isLayoutRtl(); + int squaredYDistance = (int) ((y - mAmPmYCenter) * (y - mAmPmYCenter)); + + int distanceToAmCenter = (int) Math.sqrt( + (x - mLeftIndicatorXCenter) * (x - mLeftIndicatorXCenter) + squaredYDistance); + if (distanceToAmCenter <= mAmPmCircleRadius) { + return (isLayoutRtl ? PM : AM); + } + + int distanceToPmCenter = (int) Math.sqrt( + (x - mRightIndicatorXCenter) * (x - mRightIndicatorXCenter) + squaredYDistance); + if (distanceToPmCenter <= mAmPmCircleRadius) { + return (isLayoutRtl ? AM : PM); + } + + // Neither was close enough. + return -1; + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if(!mInputEnabled) { + return true; + } + + final float eventX = event.getX(); + final float eventY = event.getY(); + + int degrees; + int snapDegrees; + boolean result = false; + + switch(event.getAction()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: + mAmOrPmPressed = getIsTouchingAmOrPm(eventX, eventY); + if (mAmOrPmPressed != -1) { + result = true; + } else { + degrees = getDegreesFromXY(eventX, eventY); + if (degrees != -1) { + snapDegrees = (mShowHours ? + snapOnly30s(degrees, 0) : snapPrefer30s(degrees)) % 360; + if (mShowHours) { + mSelectionDegrees[HOURS] = snapDegrees; + mSelectionDegrees[HOURS_INNER] = snapDegrees; + } else { + mSelectionDegrees[MINUTES] = snapDegrees; + } + performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK); + if (mListener != null) { + if (mShowHours) { + mListener.onValueSelected(HOURS, getCurrentHour(), false); + } else { + mListener.onValueSelected(MINUTES, getCurrentMinute(), false); + } + } + result = true; + } + } + invalidate(); + return result; + + case MotionEvent.ACTION_UP: + mAmOrPmPressed = getIsTouchingAmOrPm(eventX, eventY); + if (mAmOrPmPressed != -1) { + if (mAmOrPm != mAmOrPmPressed) { + swapAmPm(); + } + mAmOrPmPressed = -1; + if (mListener != null) { + mListener.onValueSelected(AMPM, getCurrentHour(), true); + } + result = true; + } else { + degrees = getDegreesFromXY(eventX, eventY); + if (degrees != -1) { + snapDegrees = (mShowHours ? + snapOnly30s(degrees, 0) : snapPrefer30s(degrees)) % 360; + if (mShowHours) { + mSelectionDegrees[HOURS] = snapDegrees; + mSelectionDegrees[HOURS_INNER] = snapDegrees; + } else { + mSelectionDegrees[MINUTES] = snapDegrees; + } + if (mListener != null) { + if (mShowHours) { + mListener.onValueSelected(HOURS, getCurrentHour(), true); + } else { + mListener.onValueSelected(MINUTES, getCurrentMinute(), true); + } + } + result = true; + } + } + if (result) { + invalidate(); + } + return result; + + default: + break; + } + return false; + } + + /** + * Necessary for accessibility, to ensure we support "scrolling" forward and backward + * in the circle. + */ + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); + } + + /** + * Announce the currently-selected time when launched. + */ + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { + // Clear the event's current text so that only the current time will be spoken. + event.getText().clear(); + Time time = new Time(); + time.hour = getCurrentHour(); + time.minute = getCurrentMinute(); + long millis = time.normalize(true); + int flags = DateUtils.FORMAT_SHOW_TIME; + if (mIs24HourMode) { + flags |= DateUtils.FORMAT_24HOUR; + } + String timeString = DateUtils.formatDateTime(getContext(), millis, flags); + event.getText().add(timeString); + return true; + } + return super.dispatchPopulateAccessibilityEvent(event); + } + + /** + * When scroll forward/backward events are received, jump the time to the higher/lower + * discrete, visible value on the circle. + */ + @SuppressLint("NewApi") + @Override + public boolean performAccessibilityAction(int action, Bundle arguments) { + if (super.performAccessibilityAction(action, arguments)) { + return true; + } + + int changeMultiplier = 0; + if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) { + changeMultiplier = 1; + } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) { + changeMultiplier = -1; + } + if (changeMultiplier != 0) { + int value = 0; + int stepSize = 0; + if (mShowHours) { + stepSize = DEGREES_FOR_ONE_HOUR; + value = getCurrentHour() % 12; + } else { + stepSize = DEGREES_FOR_ONE_MINUTE; + value = getCurrentMinute(); + } + + int degrees = value * stepSize; + degrees = snapOnly30s(degrees, changeMultiplier); + value = degrees / stepSize; + int maxValue = 0; + int minValue = 0; + if (mShowHours) { + if (mIs24HourMode) { + maxValue = 23; + } else { + maxValue = 12; + minValue = 1; + } + } else { + maxValue = 55; + } + if (value > maxValue) { + // If we scrolled forward past the highest number, wrap around to the lowest. + value = minValue; + } else if (value < minValue) { + // If we scrolled backward past the lowest number, wrap around to the highest. + value = maxValue; + } + if (mShowHours) { + setCurrentHour(value); + if (mListener != null) { + mListener.onValueSelected(HOURS, value, false); + } + } else { + setCurrentMinute(value); + if (mListener != null) { + mListener.onValueSelected(MINUTES, value, false); + } + } + return true; + } + + return false; + } + + public void setInputEnabled(boolean inputEnabled) { + mInputEnabled = inputEnabled; + invalidate(); + } +} diff --git a/core/java/android/widget/RadioButton.java b/core/java/android/widget/RadioButton.java index a0fef7d..afc4830 100644 --- a/core/java/android/widget/RadioButton.java +++ b/core/java/android/widget/RadioButton.java @@ -21,8 +21,6 @@ import android.util.AttributeSet; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; -import com.android.internal.R; - /** * <p> @@ -59,8 +57,12 @@ public class RadioButton extends CompoundButton { this(context, attrs, com.android.internal.R.attr.radioButtonStyle); } - public RadioButton(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public RadioButton(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public RadioButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); } /** diff --git a/core/java/android/widget/RatingBar.java b/core/java/android/widget/RatingBar.java index 4d3c56c..82b490e 100644 --- a/core/java/android/widget/RatingBar.java +++ b/core/java/android/widget/RatingBar.java @@ -82,11 +82,15 @@ public class RatingBar extends AbsSeekBar { private OnRatingBarChangeListener mOnRatingBarChangeListener; - public RatingBar(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RatingBar, - defStyle, 0); + public RatingBar(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public RatingBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.RatingBar, defStyleAttr, defStyleRes); final int numStars = a.getInt(R.styleable.RatingBar_numStars, mNumStars); setIsIndicator(a.getBoolean(R.styleable.RatingBar_isIndicator, !mIsUserSeekable)); final float rating = a.getFloat(R.styleable.RatingBar_rating, -1); diff --git a/core/java/android/widget/RelativeLayout.java b/core/java/android/widget/RelativeLayout.java index e03e83d..70ef10b 100644 --- a/core/java/android/widget/RelativeLayout.java +++ b/core/java/android/widget/RelativeLayout.java @@ -228,24 +228,27 @@ public class RelativeLayout extends ViewGroup { private static final int DEFAULT_WIDTH = 0x00010000; public RelativeLayout(Context context) { - super(context); - queryCompatibilityModes(context); + this(context, null); } public RelativeLayout(Context context, AttributeSet attrs) { - super(context, attrs); - initFromAttributes(context, attrs); - queryCompatibilityModes(context); + this(context, attrs, 0); + } + + public RelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); } - public RelativeLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - initFromAttributes(context, attrs); + public RelativeLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + initFromAttributes(context, attrs, defStyleAttr, defStyleRes); queryCompatibilityModes(context); } - private void initFromAttributes(Context context, AttributeSet attrs) { - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RelativeLayout); + private void initFromAttributes( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.RelativeLayout, defStyleAttr, defStyleRes); mIgnoreGravity = a.getResourceId(R.styleable.RelativeLayout_ignoreGravity, View.NO_ID); mGravity = a.getInt(R.styleable.RelativeLayout_gravity, mGravity); a.recycle(); diff --git a/core/java/android/widget/RemoteViewsAdapter.java b/core/java/android/widget/RemoteViewsAdapter.java index 3ff0cee..bbe6f9e 100644 --- a/core/java/android/widget/RemoteViewsAdapter.java +++ b/core/java/android/widget/RemoteViewsAdapter.java @@ -32,7 +32,6 @@ import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.Message; -import android.os.Process; import android.os.RemoteException; import android.os.UserHandle; import android.util.Log; @@ -45,7 +44,6 @@ import android.widget.RemoteViews.OnClickHandler; import com.android.internal.widget.IRemoteViewsAdapterConnection; import com.android.internal.widget.IRemoteViewsFactory; -import com.android.internal.widget.LockPatternUtils; /** * An adapter to a RemoteViewsService which fetches and caches RemoteViews diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java index 6680393..082d728 100644 --- a/core/java/android/widget/ScrollView.java +++ b/core/java/android/widget/ScrollView.java @@ -162,12 +162,16 @@ public class ScrollView extends FrameLayout { this(context, attrs, com.android.internal.R.attr.scrollViewStyle); } - public ScrollView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public ScrollView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); initScrollView(); - TypedArray a = - context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ScrollView, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.ScrollView, defStyleAttr, defStyleRes); setFillViewport(a.getBoolean(R.styleable.ScrollView_fillViewport, false)); diff --git a/core/java/android/widget/Scroller.java b/core/java/android/widget/Scroller.java index 3bfd39d..1a0ce9c 100644 --- a/core/java/android/widget/Scroller.java +++ b/core/java/android/widget/Scroller.java @@ -61,6 +61,8 @@ import android.view.animation.Interpolator; * }</pre> */ public class Scroller { + private final Interpolator mInterpolator; + private int mMode; private int mStartX; @@ -81,7 +83,6 @@ public class Scroller { private float mDeltaX; private float mDeltaY; private boolean mFinished; - private Interpolator mInterpolator; private boolean mFlywheel; private float mVelocity; @@ -142,18 +143,8 @@ public class Scroller { SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y; } SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f; - - // This controls the viscous fluid effect (how much of it) - sViscousFluidScale = 8.0f; - // must be set to 1.0 (used in viscousFluid()) - sViscousFluidNormalize = 1.0f; - sViscousFluidNormalize = 1.0f / viscousFluid(1.0f); - } - private static float sViscousFluidScale; - private static float sViscousFluidNormalize; - /** * Create a Scroller with the default duration and interpolator. */ @@ -178,7 +169,11 @@ public class Scroller { */ public Scroller(Context context, Interpolator interpolator, boolean flywheel) { mFinished = true; - mInterpolator = interpolator; + if (interpolator == null) { + mInterpolator = new ViscousFluidInterpolator(); + } else { + mInterpolator = interpolator; + } mPpi = context.getResources().getDisplayMetrics().density * 160.0f; mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction()); mFlywheel = flywheel; @@ -312,13 +307,7 @@ public class Scroller { if (timePassed < mDuration) { switch (mMode) { case SCROLL_MODE: - float x = timePassed * mDurationReciprocal; - - if (mInterpolator == null) - x = viscousFluid(x); - else - x = mInterpolator.getInterpolation(x); - + final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal); mCurrX = mStartX + Math.round(x * mDeltaX); mCurrY = mStartY + Math.round(x * mDeltaY); break; @@ -499,20 +488,6 @@ public class Scroller { return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l); } - static float viscousFluid(float x) - { - x *= sViscousFluidScale; - if (x < 1.0f) { - x -= (1.0f - (float)Math.exp(-x)); - } else { - float start = 0.36787944117f; // 1/e == exp(-1) - x = 1.0f - (float)Math.exp(1.0f - x); - x = start + x * (1.0f - start); - } - x *= sViscousFluidNormalize; - return x; - } - /** * Stops the animation. Contrary to {@link #forceFinished(boolean)}, * aborting the animating cause the scroller to move to the final x and y @@ -583,4 +558,41 @@ public class Scroller { return !mFinished && Math.signum(xvel) == Math.signum(mFinalX - mStartX) && Math.signum(yvel) == Math.signum(mFinalY - mStartY); } + + static class ViscousFluidInterpolator implements Interpolator { + /** Controls the viscous fluid effect (how much of it). */ + private static final float VISCOUS_FLUID_SCALE = 8.0f; + + private static final float VISCOUS_FLUID_NORMALIZE; + private static final float VISCOUS_FLUID_OFFSET; + + static { + + // must be set to 1.0 (used in viscousFluid()) + VISCOUS_FLUID_NORMALIZE = 1.0f / viscousFluid(1.0f); + // account for very small floating-point error + VISCOUS_FLUID_OFFSET = 1.0f - VISCOUS_FLUID_NORMALIZE * viscousFluid(1.0f); + } + + private static float viscousFluid(float x) { + x *= VISCOUS_FLUID_SCALE; + if (x < 1.0f) { + x -= (1.0f - (float)Math.exp(-x)); + } else { + float start = 0.36787944117f; // 1/e == exp(-1) + x = 1.0f - (float)Math.exp(1.0f - x); + x = start + x * (1.0f - start); + } + return x; + } + + @Override + public float getInterpolation(float input) { + final float interpolated = VISCOUS_FLUID_NORMALIZE * viscousFluid(input); + if (input > 0) { + return input + VISCOUS_FLUID_OFFSET; + } + return input; + } + } } diff --git a/core/java/android/widget/SearchView.java b/core/java/android/widget/SearchView.java index 0281602..3791258 100644 --- a/core/java/android/widget/SearchView.java +++ b/core/java/android/widget/SearchView.java @@ -242,7 +242,15 @@ public class SearchView extends LinearLayout implements CollapsibleActionView { } public SearchView(Context context, AttributeSet attrs) { - super(context, attrs); + this(context, attrs, 0); + } + + public SearchView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public SearchView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); LayoutInflater inflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); @@ -281,7 +289,8 @@ public class SearchView extends LinearLayout implements CollapsibleActionView { } }); - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SearchView, 0, 0); + TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.SearchView, defStyleAttr, defStyleRes); setIconifiedByDefault(a.getBoolean(R.styleable.SearchView_iconifiedByDefault, true)); int maxWidth = a.getDimensionPixelSize(R.styleable.SearchView_maxWidth, -1); if (maxWidth != -1) { @@ -304,7 +313,7 @@ public class SearchView extends LinearLayout implements CollapsibleActionView { boolean focusable = true; - a = context.obtainStyledAttributes(attrs, R.styleable.View, 0, 0); + a = context.obtainStyledAttributes(attrs, R.styleable.View, defStyleAttr, defStyleRes); focusable = a.getBoolean(R.styleable.View_focusable, focusable); a.recycle(); setFocusable(focusable); @@ -1661,8 +1670,14 @@ public class SearchView extends LinearLayout implements CollapsibleActionView { mThreshold = getThreshold(); } - public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public SearchAutoComplete(Context context, AttributeSet attrs, int defStyleAttrs) { + super(context, attrs, defStyleAttrs); + mThreshold = getThreshold(); + } + + public SearchAutoComplete( + Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) { + super(context, attrs, defStyleAttrs, defStyleRes); mThreshold = getThreshold(); } diff --git a/core/java/android/widget/SeekBar.java b/core/java/android/widget/SeekBar.java index 2737f94..dc7c04c 100644 --- a/core/java/android/widget/SeekBar.java +++ b/core/java/android/widget/SeekBar.java @@ -79,8 +79,12 @@ public class SeekBar extends AbsSeekBar { this(context, attrs, com.android.internal.R.attr.seekBarStyle); } - public SeekBar(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public SeekBar(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public SeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); } @Override diff --git a/core/java/android/widget/SlidingDrawer.java b/core/java/android/widget/SlidingDrawer.java index 517246b..ec06c02 100644 --- a/core/java/android/widget/SlidingDrawer.java +++ b/core/java/android/widget/SlidingDrawer.java @@ -192,11 +192,32 @@ public class SlidingDrawer extends ViewGroup { * * @param context The application's environment. * @param attrs The attributes defined in XML. - * @param defStyle The style to apply to this widget. + * @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. */ - public SlidingDrawer(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SlidingDrawer, defStyle, 0); + public SlidingDrawer(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + /** + * Creates a new SlidingDrawer from a specified set of attributes defined in XML. + * + * @param context The application's environment. + * @param attrs The attributes defined in XML. + * @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. + */ + public SlidingDrawer(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.SlidingDrawer, defStyleAttr, defStyleRes); int orientation = a.getInt(R.styleable.SlidingDrawer_orientation, ORIENTATION_VERTICAL); mVertical = orientation == ORIENTATION_VERTICAL; diff --git a/core/java/android/widget/Space.java b/core/java/android/widget/Space.java index bb53a77..c4eaeb7 100644 --- a/core/java/android/widget/Space.java +++ b/core/java/android/widget/Space.java @@ -20,7 +20,6 @@ import android.content.Context; import android.graphics.Canvas; import android.util.AttributeSet; import android.view.View; -import android.view.ViewGroup; /** * Space is a lightweight View subclass that may be used to create gaps between components @@ -30,8 +29,8 @@ public final class Space extends View { /** * {@inheritDoc} */ - public Space(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public Space(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); if (getVisibility() == VISIBLE) { setVisibility(INVISIBLE); } @@ -40,6 +39,13 @@ public final class Space extends View { /** * {@inheritDoc} */ + public Space(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + /** + * {@inheritDoc} + */ public Space(Context context, AttributeSet attrs) { this(context, attrs, 0); } diff --git a/core/java/android/widget/Spinner.java b/core/java/android/widget/Spinner.java index b75d36f..afe5804 100644 --- a/core/java/android/widget/Spinner.java +++ b/core/java/android/widget/Spinner.java @@ -130,18 +130,17 @@ public class Spinner extends AbsSpinner implements OnClickListener { /** * Construct a new spinner with the given context's theme, the supplied attribute set, - * and default style. + * and default style attribute. * * @param context The Context the view is running in, through which it can * access the current theme, resources, etc. * @param attrs The attributes of the XML tag that is inflating the view. - * @param defStyle The default style to apply to this view. If 0, no style - * will be applied (beyond what is included in the theme). This may - * either be an attribute resource, whose value will be retrieved - * from the current theme, or an explicit style resource. + * @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. */ - public Spinner(Context context, AttributeSet attrs, int defStyle) { - this(context, attrs, defStyle, MODE_THEME); + public Spinner(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0, MODE_THEME); } /** @@ -152,20 +151,44 @@ public class Spinner extends AbsSpinner implements OnClickListener { * @param context The Context the view is running in, through which it can * access the current theme, resources, etc. * @param attrs The attributes of the XML tag that is inflating the view. - * @param defStyle The default style to apply to this view. If 0, no style - * will be applied (beyond what is included in the theme). This may - * either be an attribute resource, whose value will be retrieved - * from the current theme, or an explicit style resource. + * @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 mode Constant describing how the user will select choices from the spinner. - * + * + * @see #MODE_DIALOG + * @see #MODE_DROPDOWN + */ + public Spinner(Context context, AttributeSet attrs, int defStyleAttr, int mode) { + this(context, attrs, defStyleAttr, 0, mode); + } + + /** + * Construct a new spinner with the given context's theme, the supplied attribute set, + * and default style. <code>mode</code> may be one of {@link #MODE_DIALOG} or + * {@link #MODE_DROPDOWN} and determines how the user will select choices from the spinner. + * + * @param context The Context the view is running in, through which it can + * access 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. + * * @see #MODE_DIALOG * @see #MODE_DROPDOWN */ - public Spinner(Context context, AttributeSet attrs, int defStyle, int mode) { - super(context, attrs, defStyle); + public Spinner( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes, int mode) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.Spinner, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.Spinner, defStyleAttr, defStyleRes); if (mode == MODE_THEME) { mode = a.getInt(com.android.internal.R.styleable.Spinner_spinnerMode, MODE_DIALOG); @@ -178,7 +201,7 @@ public class Spinner extends AbsSpinner implements OnClickListener { } case MODE_DROPDOWN: { - final DropdownPopup popup = new DropdownPopup(context, attrs, defStyle); + final DropdownPopup popup = new DropdownPopup(context, attrs, defStyleAttr, defStyleRes); mDropDownWidth = a.getLayoutDimension( com.android.internal.R.styleable.Spinner_dropDownWidth, @@ -1031,8 +1054,9 @@ public class Spinner extends AbsSpinner implements OnClickListener { private CharSequence mHintText; private ListAdapter mAdapter; - public DropdownPopup(Context context, AttributeSet attrs, int defStyleRes) { - super(context, attrs, 0, defStyleRes); + public DropdownPopup( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); setAnchorView(Spinner.this); setModal(true); diff --git a/core/java/android/widget/StackView.java b/core/java/android/widget/StackView.java index 6853660..d2e718c 100644 --- a/core/java/android/widget/StackView.java +++ b/core/java/android/widget/StackView.java @@ -168,9 +168,16 @@ public class StackView extends AdapterViewAnimator { * {@inheritDoc} */ public StackView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.StackView, defStyleAttr, 0); + this(context, attrs, defStyleAttr, 0); + } + + /** + * {@inheritDoc} + */ + public StackView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.StackView, defStyleAttr, defStyleRes); mResOutColor = a.getColor( com.android.internal.R.styleable.StackView_resOutColor, 0); diff --git a/core/java/android/widget/Switch.java b/core/java/android/widget/Switch.java index e754c17..c9a1ca4 100644 --- a/core/java/android/widget/Switch.java +++ b/core/java/android/widget/Switch.java @@ -16,6 +16,7 @@ package android.widget; +import android.animation.ObjectAnimator; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; @@ -32,6 +33,8 @@ import android.text.TextUtils; import android.text.method.AllCapsTransformationMethod; import android.text.method.TransformationMethod2; import android.util.AttributeSet; +import android.util.FloatProperty; +import android.util.MathUtils; import android.view.Gravity; import android.view.MotionEvent; import android.view.VelocityTracker; @@ -66,6 +69,8 @@ import com.android.internal.R; * @attr ref android.R.styleable#Switch_track */ public class Switch extends CompoundButton { + private static final int THUMB_ANIMATION_DURATION = 250; + private static final int TOUCH_MODE_IDLE = 0; private static final int TOUCH_MODE_DOWN = 1; private static final int TOUCH_MODE_DRAGGING = 2; @@ -105,6 +110,7 @@ public class Switch extends CompoundButton { private Layout mOnLayout; private Layout mOffLayout; private TransformationMethod2 mSwitchTransformationMethod; + private ObjectAnimator mPositionAnimator; @SuppressWarnings("hiding") private final Rect mTempRect = new Rect(); @@ -139,19 +145,41 @@ public class Switch extends CompoundButton { * * @param context The Context that will determine this widget's theming. * @param attrs Specification of attributes that should deviate from the default styling. - * @param defStyle An attribute ID within the active theme containing a reference to the - * default style for this widget. e.g. android.R.attr.switchStyle. + * @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. */ - public Switch(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public Switch(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + + /** + * Construct a new Switch with a default style determined by the given theme + * attribute or style resource, overriding specific style attributes as + * requested. + * + * @param context The Context that will determine this widget's theming. + * @param attrs Specification of attributes that should deviate from the + * default styling. + * @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. + */ + public Switch(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); Resources res = getResources(); mTextPaint.density = res.getDisplayMetrics().density; mTextPaint.setCompatibilityScaling(res.getCompatibilityInfo().applicationScale); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.Switch, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.Switch, defStyleAttr, defStyleRes); mThumbDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_thumb); mTrackDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_track); @@ -528,9 +556,12 @@ public class Switch extends CompoundButton { * @return true if (x, y) is within the target area of the switch thumb */ private boolean hitThumb(float x, float y) { + // Relies on mTempRect, MUST be called first! + final int thumbOffset = getThumbOffset(); + mThumbDrawable.getPadding(mTempRect); final int thumbTop = mSwitchTop - mTouchSlop; - final int thumbLeft = mSwitchLeft + (int) (mThumbPosition + 0.5f) - mTouchSlop; + final int thumbLeft = mSwitchLeft + thumbOffset - mTouchSlop; final int thumbRight = thumbLeft + mThumbWidth + mTempRect.left + mTempRect.right + mTouchSlop; final int thumbBottom = mSwitchBottom + mTouchSlop; @@ -575,13 +606,23 @@ public class Switch extends CompoundButton { case TOUCH_MODE_DRAGGING: { final float x = ev.getX(); - final float dx = x - mTouchX; - float newPos = Math.max(0, - Math.min(mThumbPosition + dx, getThumbScrollRange())); + final int thumbScrollRange = getThumbScrollRange(); + final float thumbScrollOffset = x - mTouchX; + float dPos; + if (thumbScrollRange != 0) { + dPos = thumbScrollOffset / thumbScrollRange; + } else { + // If the thumb scroll range is empty, just use the + // movement direction to snap on or off. + dPos = thumbScrollOffset > 0 ? 1 : -1; + } + if (isLayoutRtl()) { + dPos = -dPos; + } + final float newPos = MathUtils.constrain(mThumbPosition + dPos, 0, 1); if (newPos != mThumbPosition) { - mThumbPosition = newPos; mTouchX = x; - invalidate(); + setThumbPosition(newPos); } return true; } @@ -618,62 +659,77 @@ public class Switch extends CompoundButton { */ private void stopDrag(MotionEvent ev) { mTouchMode = TOUCH_MODE_IDLE; - // Up and not canceled, also checks the switch has not been disabled during the drag - boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled(); - - cancelSuperTouch(ev); + // Commit the change if the event is up and not canceled and the switch + // has not been disabled during the drag. + final boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled(); + final boolean newState; if (commitChange) { - boolean newState; mVelocityTracker.computeCurrentVelocity(1000); - float xvel = mVelocityTracker.getXVelocity(); + final float xvel = mVelocityTracker.getXVelocity(); if (Math.abs(xvel) > mMinFlingVelocity) { newState = isLayoutRtl() ? (xvel < 0) : (xvel > 0); } else { newState = getTargetCheckedState(); } - animateThumbToCheckedState(newState); } else { - animateThumbToCheckedState(isChecked()); + newState = isChecked(); } + + setChecked(newState); + cancelSuperTouch(ev); } private void animateThumbToCheckedState(boolean newCheckedState) { - // TODO animate! - //float targetPos = newCheckedState ? 0 : getThumbScrollRange(); - //mThumbPosition = targetPos; - setChecked(newCheckedState); + final float targetPosition = newCheckedState ? 1 : 0; + mPositionAnimator = ObjectAnimator.ofFloat(this, THUMB_POS, targetPosition); + mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION); + mPositionAnimator.setAutoCancel(true); + mPositionAnimator.start(); } - private boolean getTargetCheckedState() { - if (isLayoutRtl()) { - return mThumbPosition <= getThumbScrollRange() / 2; - } else { - return mThumbPosition >= getThumbScrollRange() / 2; + private void cancelPositionAnimator() { + if (mPositionAnimator != null) { + mPositionAnimator.cancel(); } } - private void setThumbPosition(boolean checked) { - if (isLayoutRtl()) { - mThumbPosition = checked ? 0 : getThumbScrollRange(); - } else { - mThumbPosition = checked ? getThumbScrollRange() : 0; - } + private boolean getTargetCheckedState() { + return mThumbPosition > 0.5f; + } + + /** + * Sets the thumb position as a decimal value between 0 (off) and 1 (on). + * + * @param position new position between [0,1] + */ + private void setThumbPosition(float position) { + mThumbPosition = position; + invalidate(); + } + + @Override + public void toggle() { + setChecked(!isChecked()); } @Override public void setChecked(boolean checked) { super.setChecked(checked); - setThumbPosition(isChecked()); - invalidate(); + + if (isAttachedToWindow() && isLaidOut()) { + animateThumbToCheckedState(checked); + } else { + // Immediately move the thumb to the new position. + cancelPositionAnimator(); + setThumbPosition(checked ? 1 : 0); + } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); - setThumbPosition(isChecked()); - int switchRight; int switchLeft; @@ -734,11 +790,12 @@ public class Switch extends CompoundButton { int switchInnerBottom = switchBottom - mTempRect.bottom; canvas.clipRect(switchInnerLeft, switchTop, switchInnerRight, switchBottom); + // Relies on mTempRect, MUST be called first! + final int thumbPos = getThumbOffset(); + mThumbDrawable.getPadding(mTempRect); - final int thumbPos = (int) (mThumbPosition + 0.5f); int thumbLeft = switchInnerLeft - mTempRect.left + thumbPos; int thumbRight = switchInnerLeft + thumbPos + mThumbWidth + mTempRect.right; - mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom); mThumbDrawable.draw(canvas); @@ -783,6 +840,22 @@ public class Switch extends CompoundButton { return padding; } + /** + * Translates thumb position to offset according to current RTL setting and + * thumb scroll range. + * + * @return thumb offset + */ + private int getThumbOffset() { + final float thumbPosition; + if (isLayoutRtl()) { + thumbPosition = 1 - mThumbPosition; + } else { + thumbPosition = mThumbPosition; + } + return (int) (thumbPosition * getThumbScrollRange() + 0.5f); + } + private int getThumbScrollRange() { if (mTrackDrawable == null) { return 0; @@ -848,4 +921,16 @@ public class Switch extends CompoundButton { } } } + + private static final FloatProperty<Switch> THUMB_POS = new FloatProperty<Switch>("thumbPos") { + @Override + public Float get(Switch object) { + return object.mThumbPosition; + } + + @Override + public void setValue(Switch object, float value) { + object.setThumbPosition(value); + } + }; } diff --git a/core/java/android/widget/TabHost.java b/core/java/android/widget/TabHost.java index 238dc55..89df51a 100644 --- a/core/java/android/widget/TabHost.java +++ b/core/java/android/widget/TabHost.java @@ -77,11 +77,18 @@ public class TabHost extends FrameLayout implements ViewTreeObserver.OnTouchMode } public TabHost(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.tabWidgetStyle); + } + + public TabHost(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public TabHost(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.TabWidget, - com.android.internal.R.attr.tabWidgetStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.TabWidget, defStyleAttr, defStyleRes); mTabLayoutId = a.getResourceId(R.styleable.TabWidget_tabLayout, 0); a.recycle(); diff --git a/core/java/android/widget/TabWidget.java b/core/java/android/widget/TabWidget.java index 6bced1c..568b3e6 100644 --- a/core/java/android/widget/TabWidget.java +++ b/core/java/android/widget/TabWidget.java @@ -74,11 +74,15 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener { this(context, attrs, com.android.internal.R.attr.tabWidgetStyle); } - public TabWidget(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public TabWidget(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public TabWidget(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); final TypedArray a = context.obtainStyledAttributes( - attrs, com.android.internal.R.styleable.TabWidget, defStyle, 0); + attrs, com.android.internal.R.styleable.TabWidget, defStyleAttr, defStyleRes); setStripEnabled(a.getBoolean(R.styleable.TabWidget_tabStripEnabled, true)); setLeftStripDrawable(a.getDrawable(R.styleable.TabWidget_tabStripLeft)); diff --git a/core/java/android/widget/TextClock.java b/core/java/android/widget/TextClock.java index b3b95d9..4c5c71d 100644 --- a/core/java/android/widget/TextClock.java +++ b/core/java/android/widget/TextClock.java @@ -198,15 +198,19 @@ public class TextClock extends TextView { * @param context The Context the view is running in, through which it can * access the current theme, resources, etc. * @param attrs The attributes of the XML tag that is inflating the view - * @param defStyle The default style to apply to this view. If 0, no style - * will be applied (beyond what is included in the theme). This may - * either be an attribute resource, whose value will be retrieved - * from the current theme, or an explicit style resource + * @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. */ - public TextClock(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public TextClock(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public TextClock(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TextClock, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.TextClock, defStyleAttr, defStyleRes); try { mFormat12 = a.getText(R.styleable.TextClock_format12Hour); mFormat24 = a.getText(R.styleable.TextClock_format24Hour); diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 7a9809f..5d42589 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -136,7 +136,6 @@ import java.io.IOException; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Locale; -import java.util.concurrent.locks.ReentrantLock; import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; @@ -618,9 +617,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener this(context, attrs, com.android.internal.R.attr.textViewStyle); } + public TextView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + @SuppressWarnings("deprecation") - public TextView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public TextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + mText = ""; final Resources res = getResources(); @@ -657,8 +661,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * to be able to parse the appearance first and then let specific tags * for this View override it. */ - TypedArray a = theme.obtainStyledAttributes( - attrs, com.android.internal.R.styleable.TextViewAppearance, defStyle, 0); + TypedArray a = theme.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.TextViewAppearance, defStyleAttr, defStyleRes); TypedArray appearance = null; int ap = a.getResourceId( com.android.internal.R.styleable.TextViewAppearance_textAppearance, -1); @@ -751,7 +755,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int inputType = EditorInfo.TYPE_NULL; a = theme.obtainStyledAttributes( - attrs, com.android.internal.R.styleable.TextView, defStyle, 0); + attrs, com.android.internal.R.styleable.TextView, defStyleAttr, defStyleRes); int n = a.getIndexCount(); for (int i = 0; i < n; i++) { @@ -1275,9 +1279,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * However, TextViews that have input or movement methods *are* * focusable by default. */ - a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.View, - defStyle, 0); + a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes); boolean focusable = mMovement != null || getKeyListener() != null; boolean clickable = focusable; diff --git a/core/java/android/widget/TimePicker.java b/core/java/android/widget/TimePicker.java index c26cb24..8e4ba0d 100644 --- a/core/java/android/widget/TimePicker.java +++ b/core/java/android/widget/TimePicker.java @@ -20,26 +20,17 @@ import android.annotation.Widget; import android.content.Context; import android.content.res.Configuration; import android.content.res.TypedArray; -import android.os.Parcel; import android.os.Parcelable; -import android.text.format.DateFormat; -import android.text.format.DateUtils; import android.util.AttributeSet; -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 android.widget.NumberPicker.OnValueChangeListener; import com.android.internal.R; -import java.text.DateFormatSymbols; -import java.util.Calendar; import java.util.Locale; +import static android.os.Build.VERSION_CODES.KITKAT; + /** * A view for selecting the time of day, in either 24 hour or AM/PM mode. The * hour, each minute digit, and AM/PM (if applicable) can be conrolled by @@ -57,58 +48,12 @@ import java.util.Locale; @Widget public class TimePicker extends FrameLayout { - private static final boolean DEFAULT_ENABLED_STATE = true; + private TimePickerDelegate mDelegate; - private static final int HOURS_IN_HALF_DAY = 12; - - /** - * A no-op callback used in the constructor to avoid null checks later in - * the code. - */ - private static final OnTimeChangedListener NO_OP_CHANGE_LISTENER = new OnTimeChangedListener() { - public void onTimeChanged(TimePicker view, int hourOfDay, int minute) { - } - }; - - // state - private boolean mIs24HourView; - - private boolean mIsAm; - - // ui components - private final NumberPicker mHourSpinner; - - private final NumberPicker mMinuteSpinner; - - private final NumberPicker mAmPmSpinner; - - private final EditText mHourSpinnerInput; - - private final EditText mMinuteSpinnerInput; - - private final EditText mAmPmSpinnerInput; - - private final TextView mDivider; - - // Note that the legacy implementation of the TimePicker is - // using a button for toggling between AM/PM while the new - // version uses a NumberPicker spinner. Therefore the code - // accommodates these two cases to be backwards compatible. - private final Button mAmPmButton; - - private final String[] mAmPmStrings; - - private boolean mIsEnabled = DEFAULT_ENABLED_STATE; - - // callbacks - private OnTimeChangedListener mOnTimeChangedListener; - - private Calendar mTempCalendar; - - private Locale mCurrentLocale; - - private boolean mHourWithTwoDigit; - private char mHourFormat; + private AttributeSet mAttrs; + private int mDefStyleAttr; + private int mDefStyleRes; + private Context mContext; /** * The callback interface used to indicate the time has been adjusted. @@ -131,345 +76,79 @@ public class TimePicker extends FrameLayout { this(context, attrs, R.attr.timePickerStyle); } - public TimePicker(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - // initialization based on locale - setCurrentLocale(Locale.getDefault()); - - // process style attributes - TypedArray attributesArray = context.obtainStyledAttributes( - attrs, R.styleable.TimePicker, defStyle, 0); - int layoutResourceId = attributesArray.getResourceId( - R.styleable.TimePicker_internalLayout, R.layout.time_picker); - attributesArray.recycle(); - - LayoutInflater inflater = (LayoutInflater) context.getSystemService( - Context.LAYOUT_INFLATER_SERVICE); - inflater.inflate(layoutResourceId, this, true); - - // hour - mHourSpinner = (NumberPicker) findViewById(R.id.hour); - mHourSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { - public void onValueChange(NumberPicker spinner, int oldVal, int newVal) { - updateInputState(); - if (!is24HourView()) { - if ((oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) - || (oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1)) { - mIsAm = !mIsAm; - updateAmPmControl(); - } - } - onTimeChanged(); - } - }); - mHourSpinnerInput = (EditText) mHourSpinner.findViewById(R.id.numberpicker_input); - mHourSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); - - // divider (only for the new widget style) - mDivider = (TextView) findViewById(R.id.divider); - if (mDivider != null) { - setDividerText(); - } - - // minute - mMinuteSpinner = (NumberPicker) findViewById(R.id.minute); - mMinuteSpinner.setMinValue(0); - mMinuteSpinner.setMaxValue(59); - mMinuteSpinner.setOnLongPressUpdateInterval(100); - mMinuteSpinner.setFormatter(NumberPicker.getTwoDigitFormatter()); - mMinuteSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { - public void onValueChange(NumberPicker spinner, int oldVal, int newVal) { - updateInputState(); - int minValue = mMinuteSpinner.getMinValue(); - int maxValue = mMinuteSpinner.getMaxValue(); - if (oldVal == maxValue && newVal == minValue) { - int newHour = mHourSpinner.getValue() + 1; - if (!is24HourView() && newHour == HOURS_IN_HALF_DAY) { - mIsAm = !mIsAm; - updateAmPmControl(); - } - mHourSpinner.setValue(newHour); - } else if (oldVal == minValue && newVal == maxValue) { - int newHour = mHourSpinner.getValue() - 1; - if (!is24HourView() && newHour == HOURS_IN_HALF_DAY - 1) { - mIsAm = !mIsAm; - updateAmPmControl(); - } - mHourSpinner.setValue(newHour); - } - onTimeChanged(); - } - }); - mMinuteSpinnerInput = (EditText) mMinuteSpinner.findViewById(R.id.numberpicker_input); - mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); - - /* Get the localized am/pm strings and use them in the spinner */ - mAmPmStrings = new DateFormatSymbols().getAmPmStrings(); - - // am/pm - View amPmView = findViewById(R.id.amPm); - if (amPmView instanceof Button) { - mAmPmSpinner = null; - mAmPmSpinnerInput = null; - mAmPmButton = (Button) amPmView; - mAmPmButton.setOnClickListener(new OnClickListener() { - public void onClick(View button) { - button.requestFocus(); - mIsAm = !mIsAm; - updateAmPmControl(); - onTimeChanged(); - } - }); - } else { - mAmPmButton = null; - mAmPmSpinner = (NumberPicker) amPmView; - mAmPmSpinner.setMinValue(0); - mAmPmSpinner.setMaxValue(1); - mAmPmSpinner.setDisplayedValues(mAmPmStrings); - mAmPmSpinner.setOnValueChangedListener(new OnValueChangeListener() { - public void onValueChange(NumberPicker picker, int oldVal, int newVal) { - updateInputState(); - picker.requestFocus(); - mIsAm = !mIsAm; - updateAmPmControl(); - onTimeChanged(); - } - }); - mAmPmSpinnerInput = (EditText) mAmPmSpinner.findViewById(R.id.numberpicker_input); - mAmPmSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE); - } - - if (isAmPmAtStart()) { - // Move the am/pm view to the beginning - ViewGroup amPmParent = (ViewGroup) findViewById(R.id.timePickerLayout); - amPmParent.removeView(amPmView); - amPmParent.addView(amPmView, 0); - // Swap layout margins if needed. They may be not symmetrical (Old Standard Theme for - // example and not for Holo Theme) - ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) amPmView.getLayoutParams(); - final int startMargin = lp.getMarginStart(); - final int endMargin = lp.getMarginEnd(); - if (startMargin != endMargin) { - lp.setMarginStart(endMargin); - lp.setMarginEnd(startMargin); - } - } - - getHourFormatData(); - - // update controls to initial state - updateHourControl(); - updateMinuteControl(); - updateAmPmControl(); - - setOnTimeChangedListener(NO_OP_CHANGE_LISTENER); - - // set to current time - setCurrentHour(mTempCalendar.get(Calendar.HOUR_OF_DAY)); - setCurrentMinute(mTempCalendar.get(Calendar.MINUTE)); - - if (!isEnabled()) { - setEnabled(false); - } - - // set the content descriptions - setContentDescriptions(); - - // If not explicitly specified this view is important for accessibility. - if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { - setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); - } + public TimePicker(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); } - private void getHourFormatData() { - final Locale defaultLocale = Locale.getDefault(); - final String bestDateTimePattern = DateFormat.getBestDateTimePattern(defaultLocale, - (mIs24HourView) ? "Hm" : "hm"); - final int lengthPattern = bestDateTimePattern.length(); - mHourWithTwoDigit = false; - char hourFormat = '\0'; - // Check if the returned pattern is single or double 'H', 'h', 'K', 'k'. We also save - // the hour format that we found. - for (int i = 0; i < lengthPattern; i++) { - final char c = bestDateTimePattern.charAt(i); - if (c == 'H' || c == 'h' || c == 'K' || c == 'k') { - mHourFormat = c; - if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) { - mHourWithTwoDigit = true; - } - break; - } - } - } + public TimePicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - private boolean isAmPmAtStart() { - final Locale defaultLocale = Locale.getDefault(); - final String bestDateTimePattern = DateFormat.getBestDateTimePattern(defaultLocale, - "hm" /* skeleton */); + mContext = context; + mAttrs = attrs; + mDefStyleAttr = defStyleAttr; + mDefStyleRes = defStyleRes; - return bestDateTimePattern.startsWith("a"); - } + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TimePicker, + mDefStyleAttr, mDefStyleRes); - @Override - public void setEnabled(boolean enabled) { - if (mIsEnabled == enabled) { - return; - } - super.setEnabled(enabled); - mMinuteSpinner.setEnabled(enabled); - if (mDivider != null) { - mDivider.setEnabled(enabled); - } - mHourSpinner.setEnabled(enabled); - if (mAmPmSpinner != null) { - mAmPmSpinner.setEnabled(enabled); - } else { - mAmPmButton.setEnabled(enabled); - } - mIsEnabled = enabled; + // Create the correct UI delegate. Default is the legacy one. + final boolean isLegacyMode = shouldForceLegacyMode() ? + true : a.getBoolean(R.styleable.TimePicker_legacyMode, true); + setLegacyMode(isLegacyMode); } - @Override - public boolean isEnabled() { - return mIsEnabled; + private boolean shouldForceLegacyMode() { + final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion; + return targetSdkVersion < KITKAT; } - @Override - protected void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - setCurrentLocale(newConfig.locale); + private TimePickerDelegate createLegacyUIDelegate(Context context, AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + return new LegacyTimePickerDelegate(this, context, attrs, defStyleAttr, defStyleRes); } - /** - * Sets the current locale. - * - * @param locale The current locale. - */ - private void setCurrentLocale(Locale locale) { - if (locale.equals(mCurrentLocale)) { - return; - } - mCurrentLocale = locale; - mTempCalendar = Calendar.getInstance(locale); + private TimePickerDelegate createNewUIDelegate(Context context, AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + return new android.widget.TimePickerDelegate(this, context, attrs, defStyleAttr, + defStyleRes); } /** - * Used to save / restore state of time picker + * @hide */ - private static class SavedState extends BaseSavedState { - - private final int mHour; - - private final int mMinute; - - private SavedState(Parcelable superState, int hour, int minute) { - super(superState); - mHour = hour; - mMinute = minute; - } - - private SavedState(Parcel in) { - super(in); - mHour = in.readInt(); - mMinute = in.readInt(); - } - - public int getHour() { - return mHour; - } - - public int getMinute() { - return mMinute; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - super.writeToParcel(dest, flags); - dest.writeInt(mHour); - dest.writeInt(mMinute); - } - - @SuppressWarnings({"unused", "hiding"}) - public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() { - public SavedState createFromParcel(Parcel in) { - return new SavedState(in); - } - - public SavedState[] newArray(int size) { - return new SavedState[size]; - } - }; - } - - @Override - protected Parcelable onSaveInstanceState() { - Parcelable superState = super.onSaveInstanceState(); - return new SavedState(superState, getCurrentHour(), getCurrentMinute()); - } - - @Override - protected void onRestoreInstanceState(Parcelable state) { - SavedState ss = (SavedState) state; - super.onRestoreInstanceState(ss.getSuperState()); - setCurrentHour(ss.getHour()); - setCurrentMinute(ss.getMinute()); + public void setLegacyMode(boolean isLegacyMode) { + removeAllViewsInLayout(); + mDelegate = isLegacyMode ? + createLegacyUIDelegate(mContext, mAttrs, mDefStyleAttr, mDefStyleRes) : + createNewUIDelegate(mContext, mAttrs, mDefStyleAttr, mDefStyleRes); } /** - * Set the callback that indicates the time has been adjusted by the user. - * - * @param onTimeChangedListener the callback, should not be null. + * Set the current hour. */ - public void setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener) { - mOnTimeChangedListener = onTimeChangedListener; + public void setCurrentHour(Integer currentHour) { + mDelegate.setCurrentHour(currentHour); } /** * @return The current hour in the range (0-23). */ public Integer getCurrentHour() { - int currentHour = mHourSpinner.getValue(); - if (is24HourView()) { - return currentHour; - } else if (mIsAm) { - return currentHour % HOURS_IN_HALF_DAY; - } else { - return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY; - } + return mDelegate.getCurrentHour(); } /** - * Set the current hour. + * Set the current minute (0-59). */ - public void setCurrentHour(Integer currentHour) { - setCurrentHour(currentHour, true); + public void setCurrentMinute(Integer currentMinute) { + mDelegate.setCurrentMinute(currentMinute); } - private void setCurrentHour(Integer currentHour, boolean notifyTimeChanged) { - // why was Integer used in the first place? - if (currentHour == null || currentHour == getCurrentHour()) { - return; - } - if (!is24HourView()) { - // convert [0,23] ordinal to wall clock display - if (currentHour >= HOURS_IN_HALF_DAY) { - mIsAm = false; - if (currentHour > HOURS_IN_HALF_DAY) { - currentHour = currentHour - HOURS_IN_HALF_DAY; - } - } else { - mIsAm = true; - if (currentHour == 0) { - currentHour = HOURS_IN_HALF_DAY; - } - } - updateAmPmControl(); - } - mHourSpinner.setValue(currentHour); - if (notifyTimeChanged) { - onTimeChanged(); - } + /** + * @return The current minute. + */ + public Integer getCurrentMinute() { + return mDelegate.getCurrentMinute(); } /** @@ -478,223 +157,174 @@ public class TimePicker extends FrameLayout { * @param is24HourView True = 24 hour mode. False = AM/PM. */ public void setIs24HourView(Boolean is24HourView) { - if (mIs24HourView == is24HourView) { - return; - } - // cache the current hour since spinner range changes and BEFORE changing mIs24HourView!! - int currentHour = getCurrentHour(); - // Order is important here. - mIs24HourView = is24HourView; - getHourFormatData(); - updateHourControl(); - // set value after spinner range is updated - be aware that because mIs24HourView has - // changed then getCurrentHour() is not equal to the currentHour we cached before so - // explicitly ask for *not* propagating any onTimeChanged() - setCurrentHour(currentHour, false /* no onTimeChanged() */); - updateMinuteControl(); - updateAmPmControl(); + mDelegate.setIs24HourView(is24HourView); } /** * @return true if this is in 24 hour view else false. */ public boolean is24HourView() { - return mIs24HourView; + return mDelegate.is24HourView(); } /** - * @return The current minute. + * Set the callback that indicates the time has been adjusted by the user. + * + * @param onTimeChangedListener the callback, should not be null. */ - public Integer getCurrentMinute() { - return mMinuteSpinner.getValue(); + public void setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener) { + mDelegate.setOnTimeChangedListener(onTimeChangedListener); } - /** - * Set the current minute (0-59). - */ - public void setCurrentMinute(Integer currentMinute) { - if (currentMinute == getCurrentMinute()) { + @Override + public void setEnabled(boolean enabled) { + if (mDelegate.isEnabled() == enabled) { return; } - mMinuteSpinner.setValue(currentMinute); - onTimeChanged(); + super.setEnabled(enabled); + mDelegate.setEnabled(enabled); + } + + @Override + public boolean isEnabled() { + return mDelegate.isEnabled(); } /** - * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":". - * - * See http://unicode.org/cldr/trac/browser/trunk/common/main - * - * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the - * separator as the character which is just after the hour marker in the returned pattern. + * @hide */ - private void setDividerText() { - final Locale defaultLocale = Locale.getDefault(); - final String skeleton = (mIs24HourView) ? "Hm" : "hm"; - final String bestDateTimePattern = DateFormat.getBestDateTimePattern(defaultLocale, - skeleton); - final String separatorText; - int hourIndex = bestDateTimePattern.lastIndexOf('H'); - if (hourIndex == -1) { - hourIndex = bestDateTimePattern.lastIndexOf('h'); - } - if (hourIndex == -1) { - // Default case - separatorText = ":"; - } else { - int minuteIndex = bestDateTimePattern.indexOf('m', hourIndex + 1); - if (minuteIndex == -1) { - separatorText = Character.toString(bestDateTimePattern.charAt(hourIndex + 1)); - } else { - separatorText = bestDateTimePattern.substring(hourIndex + 1, minuteIndex); - } - } - mDivider.setText(separatorText); + public void setShowDoneButton(boolean showDoneButton) { + mDelegate.setShowDoneButton(showDoneButton); + } + + /** + * @hide + */ + public void setDismissCallback(TimePickerDismissCallback callback) { + mDelegate.setDismissCallback(callback); } @Override public int getBaseline() { - return mHourSpinner.getBaseline(); + return mDelegate.getBaseline(); + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mDelegate.onConfigurationChanged(newConfig); + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + return mDelegate.onSaveInstanceState(superState); + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + BaseSavedState ss = (BaseSavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + mDelegate.onRestoreInstanceState(ss); } @Override public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - onPopulateAccessibilityEvent(event); - return true; + return mDelegate.dispatchPopulateAccessibilityEvent(event); } @Override public void onPopulateAccessibilityEvent(AccessibilityEvent event) { super.onPopulateAccessibilityEvent(event); - - int flags = DateUtils.FORMAT_SHOW_TIME; - if (mIs24HourView) { - flags |= DateUtils.FORMAT_24HOUR; - } else { - flags |= DateUtils.FORMAT_12HOUR; - } - mTempCalendar.set(Calendar.HOUR_OF_DAY, getCurrentHour()); - mTempCalendar.set(Calendar.MINUTE, getCurrentMinute()); - String selectedDateUtterance = DateUtils.formatDateTime(mContext, - mTempCalendar.getTimeInMillis(), flags); - event.getText().add(selectedDateUtterance); + mDelegate.onPopulateAccessibilityEvent(event); } @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); - event.setClassName(TimePicker.class.getName()); + mDelegate.onInitializeAccessibilityEvent(event); } @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(TimePicker.class.getName()); + mDelegate.onInitializeAccessibilityNodeInfo(info); } - private void updateHourControl() { - if (is24HourView()) { - // 'k' means 1-24 hour - if (mHourFormat == 'k') { - mHourSpinner.setMinValue(1); - mHourSpinner.setMaxValue(24); - } else { - mHourSpinner.setMinValue(0); - mHourSpinner.setMaxValue(23); - } - } else { - // 'K' means 0-11 hour - if (mHourFormat == 'K') { - mHourSpinner.setMinValue(0); - mHourSpinner.setMaxValue(11); - } else { - mHourSpinner.setMinValue(1); - mHourSpinner.setMaxValue(12); - } - } - mHourSpinner.setFormatter(mHourWithTwoDigit ? NumberPicker.getTwoDigitFormatter() : null); - } + /** + * A delegate interface that defined the public API of the TimePicker. Allows different + * TimePicker implementations. This would need to be implemented by the TimePicker delegates + * for the real behavior. + */ + interface TimePickerDelegate { + void setCurrentHour(Integer currentHour); + Integer getCurrentHour(); - private void updateMinuteControl() { - if (is24HourView()) { - mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE); - } else { - mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); - } - } + void setCurrentMinute(Integer currentMinute); + Integer getCurrentMinute(); - private void updateAmPmControl() { - if (is24HourView()) { - if (mAmPmSpinner != null) { - mAmPmSpinner.setVisibility(View.GONE); - } else { - mAmPmButton.setVisibility(View.GONE); - } - } else { - int index = mIsAm ? Calendar.AM : Calendar.PM; - if (mAmPmSpinner != null) { - mAmPmSpinner.setValue(index); - mAmPmSpinner.setVisibility(View.VISIBLE); - } else { - mAmPmButton.setText(mAmPmStrings[index]); - mAmPmButton.setVisibility(View.VISIBLE); - } - } - sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); - } + void setIs24HourView(Boolean is24HourView); + boolean is24HourView(); - private void onTimeChanged() { - sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); - if (mOnTimeChangedListener != null) { - mOnTimeChangedListener.onTimeChanged(this, getCurrentHour(), getCurrentMinute()); - } + void setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener); + + void setEnabled(boolean enabled); + boolean isEnabled(); + + void setShowDoneButton(boolean showDoneButton); + void setDismissCallback(TimePickerDismissCallback callback); + + int getBaseline(); + + void onConfigurationChanged(Configuration newConfig); + + Parcelable onSaveInstanceState(Parcelable superState); + void onRestoreInstanceState(Parcelable state); + + boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event); + void onPopulateAccessibilityEvent(AccessibilityEvent event); + void onInitializeAccessibilityEvent(AccessibilityEvent event); + void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info); } - private void setContentDescriptions() { - // Minute - trySetContentDescription(mMinuteSpinner, R.id.increment, - R.string.time_picker_increment_minute_button); - trySetContentDescription(mMinuteSpinner, R.id.decrement, - R.string.time_picker_decrement_minute_button); - // Hour - trySetContentDescription(mHourSpinner, R.id.increment, - R.string.time_picker_increment_hour_button); - trySetContentDescription(mHourSpinner, R.id.decrement, - R.string.time_picker_decrement_hour_button); - // AM/PM - if (mAmPmSpinner != null) { - trySetContentDescription(mAmPmSpinner, R.id.increment, - R.string.time_picker_increment_set_pm_button); - trySetContentDescription(mAmPmSpinner, R.id.decrement, - R.string.time_picker_decrement_set_am_button); - } + /** + * A callback interface for dismissing the TimePicker when included into a Dialog + * + * @hide + */ + public static interface TimePickerDismissCallback { + void dismiss(TimePicker view, boolean isCancel, int hourOfDay, int minute); } - private void trySetContentDescription(View root, int viewId, int contDescResId) { - View target = root.findViewById(viewId); - if (target != null) { - target.setContentDescription(mContext.getString(contDescResId)); + /** + * An abstract class which can be used as a start for TimePicker implementations + */ + abstract static class AbstractTimePickerDelegate implements TimePickerDelegate { + // The delegator + protected TimePicker mDelegator; + + // The context + protected Context mContext; + + // The current locale + protected Locale mCurrentLocale; + + // Callbacks + protected OnTimeChangedListener mOnTimeChangedListener; + + public AbstractTimePickerDelegate(TimePicker delegator, Context context) { + mDelegator = delegator; + mContext = context; + + // initialization based on locale + setCurrentLocale(Locale.getDefault()); } - } - private void updateInputState() { - // Make sure that if the user changes the value and the IME is active - // for one of the inputs if this widget, the IME is closed. If the user - // changed the value via the IME and there is a next input the IME will - // be shown, otherwise the user chose another means of changing the - // value and having the IME up makes no sense. - InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); - if (inputMethodManager != null) { - if (inputMethodManager.isActive(mHourSpinnerInput)) { - mHourSpinnerInput.clearFocus(); - inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); - } else if (inputMethodManager.isActive(mMinuteSpinnerInput)) { - mMinuteSpinnerInput.clearFocus(); - inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); - } else if (inputMethodManager.isActive(mAmPmSpinnerInput)) { - mAmPmSpinnerInput.clearFocus(); - inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); + public void setCurrentLocale(Locale locale) { + if (locale.equals(mCurrentLocale)) { + return; } + mCurrentLocale = locale; } } } diff --git a/core/java/android/widget/TimePickerDelegate.java b/core/java/android/widget/TimePickerDelegate.java new file mode 100644 index 0000000..182d370 --- /dev/null +++ b/core/java/android/widget/TimePickerDelegate.java @@ -0,0 +1,1380 @@ +/* + * Copyright (C) 2013 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.widget; + +import android.animation.Keyframe; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.text.format.DateFormat; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.view.HapticFeedbackConstants; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; + +import com.android.internal.R; + +import java.text.DateFormatSymbols; +import java.util.ArrayList; +import java.util.Calendar; + +/** + * A view for selecting the time of day, in either 24 hour or AM/PM mode. + */ +class TimePickerDelegate extends TimePicker.AbstractTimePickerDelegate implements + RadialTimePickerView.OnValueSelectedListener { + + private static final String TAG = "TimePickerDelegate"; + + // Index used by RadialPickerLayout + private static final int HOUR_INDEX = 0; + private static final int MINUTE_INDEX = 1; + + // NOT a real index for the purpose of what's showing. + private static final int AMPM_INDEX = 2; + + // Also NOT a real index, just used for keyboard mode. + private static final int ENABLE_PICKER_INDEX = 3; + + private static final int AM = 0; + private static final int PM = 1; + + private static final boolean DEFAULT_ENABLED_STATE = true; + private boolean mIsEnabled = DEFAULT_ENABLED_STATE; + + private static final int HOURS_IN_HALF_DAY = 12; + + // Delay in ms before starting the pulse animation + private static final int PULSE_ANIMATOR_DELAY = 300; + + // Duration in ms of the pulse animation + private static final int PULSE_ANIMATOR_DURATION = 544; + + private static int[] TEXT_APPEARANCE_TIME_LABEL_ATTR = + new int[] { R.attr.timePickerHeaderTimeLabelTextAppearance }; + + private final View mMainView; + private TextView mHourView; + private TextView mMinuteView; + private TextView mAmPmTextView; + private RadialTimePickerView mRadialTimePickerView; + private TextView mSeparatorView; + + private ViewGroup mLayoutButtons; + + private int mHeaderSelectedColor; + private int mHeaderUnSelectedColor; + private String mAmText; + private String mPmText; + + private boolean mAllowAutoAdvance; + private int mInitialHourOfDay; + private int mInitialMinute; + private boolean mIs24HourView; + + // For hardware IME input. + private char mPlaceholderText; + private String mDoublePlaceholderText; + private String mDeletedKeyFormat; + private boolean mInKbMode; + private ArrayList<Integer> mTypedTimes = new ArrayList<Integer>(); + private Node mLegalTimesTree; + private int mAmKeyCode; + private int mPmKeyCode; + + // For showing the done button when in a Dialog + private Button mDoneButton; + private boolean mShowDoneButton; + private TimePicker.TimePickerDismissCallback mDismissCallback; + + // Accessibility strings. + private String mHourPickerDescription; + private String mSelectHours; + private String mMinutePickerDescription; + private String mSelectMinutes; + + public TimePickerDelegate(TimePicker delegator, Context context, AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + super(delegator, context); + + // process style attributes + final TypedArray a = mContext.obtainStyledAttributes(attrs, + R.styleable.TimePicker, defStyleAttr, defStyleRes); + + final Resources res = mContext.getResources(); + + mHourPickerDescription = res.getString(R.string.hour_picker_description); + mSelectHours = res.getString(R.string.select_hours); + mMinutePickerDescription = res.getString(R.string.minute_picker_description); + mSelectMinutes = res.getString(R.string.select_minutes); + + mHeaderSelectedColor = a.getColor(R.styleable.TimePicker_headerSelectedTextColor, + android.R.color.holo_blue_light); + + mHeaderUnSelectedColor = getUnselectedColor( + R.color.timepicker_default_text_color_holo_light); + if (mHeaderUnSelectedColor == -1) { + mHeaderUnSelectedColor = a.getColor(R.styleable.TimePicker_headerUnselectedTextColor, + R.color.timepicker_default_text_color_holo_light); + } + + final int headerBackgroundColor = a.getColor( + R.styleable.TimePicker_headerBackgroundColor, 0); + + a.recycle(); + + final int layoutResourceId = a.getResourceId( + R.styleable.TimePicker_internalLayout, R.layout.time_picker_holo); + + final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + + mMainView = inflater.inflate(layoutResourceId, null); + mDelegator.addView(mMainView); + + if (headerBackgroundColor != 0) { + RelativeLayout header = (RelativeLayout) mMainView.findViewById(R.id.time_header); + header.setBackgroundColor(headerBackgroundColor); + } + + mHourView = (TextView) mMainView.findViewById(R.id.hours); + mMinuteView = (TextView) mMainView.findViewById(R.id.minutes); + mAmPmTextView = (TextView) mMainView.findViewById(R.id.ampm_label); + mSeparatorView = (TextView) mMainView.findViewById(R.id.separator); + mRadialTimePickerView = (RadialTimePickerView) mMainView.findViewById(R.id.radial_picker); + + mLayoutButtons = (ViewGroup) mMainView.findViewById(R.id.layout_buttons); + mDoneButton = (Button) mMainView.findViewById(R.id.done_button); + + String[] amPmTexts = new DateFormatSymbols().getAmPmStrings(); + mAmText = amPmTexts[0]; + mPmText = amPmTexts[1]; + + setupListeners(); + + mAllowAutoAdvance = true; + + // Set up for keyboard mode. + mDoublePlaceholderText = res.getString(R.string.time_placeholder); + mDeletedKeyFormat = res.getString(R.string.deleted_key); + mPlaceholderText = mDoublePlaceholderText.charAt(0); + mAmKeyCode = mPmKeyCode = -1; + generateLegalTimesTree(); + + // Initialize with current time + final Calendar calendar = Calendar.getInstance(mCurrentLocale); + final int currentHour = calendar.get(Calendar.HOUR_OF_DAY); + final int currentMinute = calendar.get(Calendar.MINUTE); + initialize(currentHour, currentMinute, false /* 12h */, HOUR_INDEX, false); + } + + private int getUnselectedColor(int defColor) { + int result = -1; + final Resources.Theme theme = mContext.getTheme(); + final TypedValue outValue = new TypedValue(); + theme.resolveAttribute(R.attr.timePickerHeaderTimeLabelTextAppearance, outValue, true); + final int appearanceResId = outValue.resourceId; + TypedArray appearance = null; + if (appearanceResId != -1) { + appearance = theme.obtainStyledAttributes(appearanceResId, + com.android.internal.R.styleable.TextAppearance); + } + if (appearance != null) { + result = appearance.getColor( + com.android.internal.R.styleable.TextAppearance_textColor, defColor); + appearance.recycle(); + } + return result; + } + + private void initialize(int hourOfDay, int minute, boolean is24HourView, int index, + boolean showDoneButton) { + mInitialHourOfDay = hourOfDay; + mInitialMinute = minute; + mIs24HourView = is24HourView; + mInKbMode = false; + mShowDoneButton = showDoneButton; + updateUI(index); + } + + private void setupListeners() { + KeyboardListener keyboardListener = new KeyboardListener(); + mDelegator.setOnKeyListener(keyboardListener); + + mHourView.setOnKeyListener(keyboardListener); + mMinuteView.setOnKeyListener(keyboardListener); + mAmPmTextView.setOnKeyListener(keyboardListener); + mRadialTimePickerView.setOnValueSelectedListener(this); + mRadialTimePickerView.setOnKeyListener(keyboardListener); + + mHourView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + setCurrentItemShowing(HOUR_INDEX, true, false, true); + tryVibrate(); + } + }); + mMinuteView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + setCurrentItemShowing(MINUTE_INDEX, true, false, true); + tryVibrate(); + } + }); + mDoneButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mInKbMode && isTypedTimeFullyLegal()) { + finishKbMode(false); + } else { + tryVibrate(); + } + if (mDismissCallback != null) { + mDismissCallback.dismiss(mDelegator, false, getCurrentHour(), + getCurrentMinute()); + } + } + }); + mDoneButton.setOnKeyListener(keyboardListener); + } + + private void updateUI(int index) { + // Update RadialPicker values + updateRadialPicker(index); + // Enable or disable the AM/PM view. + updateHeaderAmPm(); + // Show or hide Done button + updateDoneButton(); + // Update Hour and Minutes + updateHeaderHour(mInitialHourOfDay, true); + // Update time separator + updateHeaderSeparator(); + // Update Minutes + updateHeaderMinute(mInitialMinute); + // Invalidate everything + mDelegator.invalidate(); + } + + private void updateRadialPicker(int index) { + mRadialTimePickerView.initialize(mInitialHourOfDay, mInitialMinute, mIs24HourView); + setCurrentItemShowing(index, false, true, true); + } + + private int computeMaxWidthOfNumbers(int max) { + TextView tempView = new TextView(mContext); + TypedArray a = mContext.obtainStyledAttributes(TEXT_APPEARANCE_TIME_LABEL_ATTR); + final int textAppearanceResId = a.getResourceId(0, 0); + tempView.setTextAppearance(mContext, (textAppearanceResId != 0) ? + textAppearanceResId : R.style.TextAppearance_Holo_TimePicker_TimeLabel); + a.recycle(); + ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + tempView.setLayoutParams(lp); + int maxWidth = 0; + for (int minutes = 0; minutes < max; minutes++) { + final String text = String.format("%02d", minutes); + tempView.setText(text); + tempView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); + maxWidth = Math.max(maxWidth, tempView.getMeasuredWidth()); + } + return maxWidth; + } + + private void updateHeaderAmPm() { + if (mIs24HourView) { + mAmPmTextView.setVisibility(View.GONE); + } else { + mAmPmTextView.setVisibility(View.VISIBLE); + final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, + "hm"); + + boolean amPmOnLeft = bestDateTimePattern.startsWith("a"); + if (TextUtils.getLayoutDirectionFromLocale(mCurrentLocale) == + View.LAYOUT_DIRECTION_RTL) { + amPmOnLeft = !amPmOnLeft; + } + + RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) + mAmPmTextView.getLayoutParams(); + + if (amPmOnLeft) { + layoutParams.rightMargin = computeMaxWidthOfNumbers(12 /* for hours */); + layoutParams.removeRule(RelativeLayout.RIGHT_OF); + layoutParams.addRule(RelativeLayout.LEFT_OF, R.id.separator); + } else { + layoutParams.leftMargin = computeMaxWidthOfNumbers(60 /* for minutes */); + layoutParams.removeRule(RelativeLayout.LEFT_OF); + layoutParams.addRule(RelativeLayout.RIGHT_OF, R.id.separator); + } + + updateAmPmDisplay(mInitialHourOfDay < 12 ? AM : PM); + mAmPmTextView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + tryVibrate(); + int amOrPm = mRadialTimePickerView.getAmOrPm(); + if (amOrPm == AM) { + amOrPm = PM; + } else if (amOrPm == PM){ + amOrPm = AM; + } + updateAmPmDisplay(amOrPm); + mRadialTimePickerView.setAmOrPm(amOrPm); + } + }); + } + } + + private void updateDoneButton() { + mLayoutButtons.setVisibility(mShowDoneButton ? View.VISIBLE : View.GONE); + } + + /** + * Set the current hour. + */ + @Override + public void setCurrentHour(Integer currentHour) { + if (mInitialHourOfDay == currentHour) { + return; + } + mInitialHourOfDay = currentHour; + updateHeaderHour(currentHour, true /* accessibility announce */); + updateHeaderAmPm(); + mRadialTimePickerView.setCurrentHour(currentHour); + mRadialTimePickerView.setAmOrPm(mInitialHourOfDay < 12 ? AM : PM); + mDelegator.invalidate(); + onTimeChanged(); + } + + /** + * @return The current hour in the range (0-23). + */ + @Override + public Integer getCurrentHour() { + int currentHour = mRadialTimePickerView.getCurrentHour(); + if (mIs24HourView) { + return currentHour; + } else { + switch(mRadialTimePickerView.getAmOrPm()) { + case PM: + return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY; + case AM: + default: + return currentHour % HOURS_IN_HALF_DAY; + } + } + } + + /** + * Set the current minute (0-59). + */ + @Override + public void setCurrentMinute(Integer currentMinute) { + if (mInitialMinute == currentMinute) { + return; + } + mInitialMinute = currentMinute; + updateHeaderMinute(currentMinute); + mRadialTimePickerView.setCurrentMinute(currentMinute); + mDelegator.invalidate(); + onTimeChanged(); + } + + /** + * @return The current minute. + */ + @Override + public Integer getCurrentMinute() { + return mRadialTimePickerView.getCurrentMinute(); + } + + /** + * Set whether in 24 hour or AM/PM mode. + * + * @param is24HourView True = 24 hour mode. False = AM/PM. + */ + @Override + public void setIs24HourView(Boolean is24HourView) { + if (is24HourView == mIs24HourView) { + return; + } + mIs24HourView = is24HourView; + generateLegalTimesTree(); + int hour = mRadialTimePickerView.getCurrentHour(); + mInitialHourOfDay = hour; + updateHeaderHour(hour, false /* no accessibility announce */); + updateHeaderAmPm(); + updateRadialPicker(mRadialTimePickerView.getCurrentItemShowing()); + mDelegator.invalidate(); + } + + /** + * @return true if this is in 24 hour view else false. + */ + @Override + public boolean is24HourView() { + return mIs24HourView; + } + + @Override + public void setOnTimeChangedListener(TimePicker.OnTimeChangedListener callback) { + mOnTimeChangedListener = callback; + } + + @Override + public void setEnabled(boolean enabled) { + mHourView.setEnabled(enabled); + mMinuteView.setEnabled(enabled); + mAmPmTextView.setEnabled(enabled); + mRadialTimePickerView.setEnabled(enabled); + mIsEnabled = enabled; + } + + @Override + public boolean isEnabled() { + return mIsEnabled; + } + + @Override + public void setShowDoneButton(boolean showDoneButton) { + mShowDoneButton = showDoneButton; + updateDoneButton(); + } + + @Override + public void setDismissCallback(TimePicker.TimePickerDismissCallback callback) { + mDismissCallback = callback; + } + + @Override + public int getBaseline() { + // does not support baseline alignment + return -1; + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + updateUI(mRadialTimePickerView.getCurrentItemShowing()); + } + + @Override + public Parcelable onSaveInstanceState(Parcelable superState) { + return new SavedState(superState, getCurrentHour(), getCurrentMinute(), + is24HourView(), inKbMode(), getTypedTimes(), getCurrentItemShowing(), + isShowDoneButton()); + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + setInKbMode(ss.inKbMode()); + setTypedTimes(ss.getTypesTimes()); + initialize(ss.getHour(), ss.getMinute(), ss.is24HourMode(), ss.getCurrentItemShowing(), + ss.isShowDoneButton()); + mRadialTimePickerView.invalidate(); + if (mInKbMode) { + tryStartingKbMode(-1); + mHourView.invalidate(); + } + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + return mRadialTimePickerView.dispatchPopulateAccessibilityEvent(event); + } + + @Override + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + mRadialTimePickerView.onPopulateAccessibilityEvent(event); + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + mRadialTimePickerView.onInitializeAccessibilityEvent(event); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + mRadialTimePickerView.onInitializeAccessibilityNodeInfo(info); + } + + /** + * Set whether in keyboard mode or not. + * + * @param inKbMode True means in keyboard mode. + */ + private void setInKbMode(boolean inKbMode) { + mInKbMode = inKbMode; + } + + /** + * @return true if in keyboard mode + */ + private boolean inKbMode() { + return mInKbMode; + } + + private void setTypedTimes(ArrayList<Integer> typeTimes) { + mTypedTimes = typeTimes; + } + + /** + * @return an array of typed times + */ + private ArrayList<Integer> getTypedTimes() { + return mTypedTimes; + } + + /** + * @return the index of the current item showing + */ + private int getCurrentItemShowing() { + return mRadialTimePickerView.getCurrentItemShowing(); + } + + private boolean isShowDoneButton() { + return mShowDoneButton; + } + + /** + * Propagate the time change + */ + private void onTimeChanged() { + mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + if (mOnTimeChangedListener != null) { + mOnTimeChangedListener.onTimeChanged(mDelegator, + getCurrentHour(), getCurrentMinute()); + } + } + + /** + * Used to save / restore state of time picker + */ + private static class SavedState extends View.BaseSavedState { + + private final int mHour; + private final int mMinute; + private final boolean mIs24HourMode; + private final boolean mInKbMode; + private final ArrayList<Integer> mTypedTimes; + private final int mCurrentItemShowing; + private final boolean mShowDoneButton; + + private SavedState(Parcelable superState, int hour, int minute, boolean is24HourMode, + boolean isKbMode, ArrayList<Integer> typedTimes, + int currentItemShowing, boolean showDoneButton) { + super(superState); + mHour = hour; + mMinute = minute; + mIs24HourMode = is24HourMode; + mInKbMode = isKbMode; + mTypedTimes = typedTimes; + mCurrentItemShowing = currentItemShowing; + mShowDoneButton = showDoneButton; + } + + private SavedState(Parcel in) { + super(in); + mHour = in.readInt(); + mMinute = in.readInt(); + mIs24HourMode = (in.readInt() == 1); + mInKbMode = (in.readInt() == 1); + mTypedTimes = in.readArrayList(getClass().getClassLoader()); + mCurrentItemShowing = in.readInt(); + mShowDoneButton = (in.readInt() == 1); + } + + public int getHour() { + return mHour; + } + + public int getMinute() { + return mMinute; + } + + public boolean is24HourMode() { + return mIs24HourMode; + } + + public boolean inKbMode() { + return mInKbMode; + } + + public ArrayList<Integer> getTypesTimes() { + return mTypedTimes; + } + + public int getCurrentItemShowing() { + return mCurrentItemShowing; + } + + public boolean isShowDoneButton() { + return mShowDoneButton; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(mHour); + dest.writeInt(mMinute); + dest.writeInt(mIs24HourMode ? 1 : 0); + dest.writeInt(mInKbMode ? 1 : 0); + dest.writeList(mTypedTimes); + dest.writeInt(mCurrentItemShowing); + dest.writeInt(mShowDoneButton ? 1 : 0); + } + + @SuppressWarnings({"unused", "hiding"}) + public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + private void tryVibrate() { + mDelegator.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK); + } + + private void updateAmPmDisplay(int amOrPm) { + if (amOrPm == AM) { + mAmPmTextView.setText(mAmText); + mRadialTimePickerView.announceForAccessibility(mAmText); + } else if (amOrPm == PM){ + mAmPmTextView.setText(mPmText); + mRadialTimePickerView.announceForAccessibility(mPmText); + } else { + mAmPmTextView.setText(mDoublePlaceholderText); + } + } + + /** + * Called by the picker for updating the header display. + */ + @Override + public void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance) { + if (pickerIndex == HOUR_INDEX) { + updateHeaderHour(newValue, false); + String announcement = String.format("%d", newValue); + if (mAllowAutoAdvance && autoAdvance) { + setCurrentItemShowing(MINUTE_INDEX, true, true, false); + announcement += ". " + mSelectMinutes; + } else { + mRadialTimePickerView.setContentDescription( + mHourPickerDescription + ": " + newValue); + } + + mRadialTimePickerView.announceForAccessibility(announcement); + } else if (pickerIndex == MINUTE_INDEX){ + updateHeaderMinute(newValue); + mRadialTimePickerView.setContentDescription(mMinutePickerDescription + ": " + newValue); + } else if (pickerIndex == AMPM_INDEX) { + updateAmPmDisplay(newValue); + } else if (pickerIndex == ENABLE_PICKER_INDEX) { + if (!isTypedTimeFullyLegal()) { + mTypedTimes.clear(); + } + finishKbMode(true); + } + } + + private void updateHeaderHour(int value, boolean announce) { + final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, + (mIs24HourView) ? "Hm" : "hm"); + final int lengthPattern = bestDateTimePattern.length(); + boolean hourWithTwoDigit = false; + char hourFormat = '\0'; + // Check if the returned pattern is single or double 'H', 'h', 'K', 'k'. We also save + // the hour format that we found. + for (int i = 0; i < lengthPattern; i++) { + final char c = bestDateTimePattern.charAt(i); + if (c == 'H' || c == 'h' || c == 'K' || c == 'k') { + hourFormat = c; + if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) { + hourWithTwoDigit = true; + } + break; + } + } + final String format; + if (hourWithTwoDigit) { + format = "%02d"; + } else { + format = "%d"; + } + if (mIs24HourView) { + // 'k' means 1-24 hour + if (hourFormat == 'k' && value == 0) { + value = 24; + } + } else { + // 'K' means 0-11 hour + value = modulo12(value, hourFormat == 'K'); + } + CharSequence text = String.format(format, value); + mHourView.setText(text); + if (announce) { + mRadialTimePickerView.announceForAccessibility(text); + } + } + + private static int modulo12(int n, boolean startWithZero) { + int value = n % 12; + if (value == 0 && !startWithZero) { + value = 12; + } + return value; + } + + /** + * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":". + * + * See http://unicode.org/cldr/trac/browser/trunk/common/main + * + * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the + * separator as the character which is just after the hour marker in the returned pattern. + */ + private void updateHeaderSeparator() { + final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, + (mIs24HourView) ? "Hm" : "hm"); + final String separatorText; + // See http://www.unicode.org/reports/tr35/tr35-dates.html for hour formats + final char[] hourFormats = {'H', 'h', 'K', 'k'}; + int hIndex = lastIndexOfAny(bestDateTimePattern, hourFormats); + if (hIndex == -1) { + // Default case + separatorText = ":"; + } else { + separatorText = Character.toString(bestDateTimePattern.charAt(hIndex + 1)); + } + mSeparatorView.setText(separatorText); + } + + static private int lastIndexOfAny(String str, char[] any) { + final int lengthAny = any.length; + if (lengthAny > 0) { + for (int i = str.length() - 1; i >= 0; i--) { + char c = str.charAt(i); + for (int j = 0; j < lengthAny; j++) { + if (c == any[j]) { + return i; + } + } + } + } + return -1; + } + + private void updateHeaderMinute(int value) { + if (value == 60) { + value = 0; + } + CharSequence text = String.format(mCurrentLocale, "%02d", value); + mRadialTimePickerView.announceForAccessibility(text); + mMinuteView.setText(text); + } + + /** + * Show either Hours or Minutes. + */ + private void setCurrentItemShowing(int index, boolean animateCircle, boolean delayLabelAnimate, + boolean announce) { + mRadialTimePickerView.setCurrentItemShowing(index, animateCircle); + + TextView labelToAnimate; + if (index == HOUR_INDEX) { + int hours = mRadialTimePickerView.getCurrentHour(); + if (!mIs24HourView) { + hours = hours % 12; + } + mRadialTimePickerView.setContentDescription(mHourPickerDescription + ": " + hours); + if (announce) { + mRadialTimePickerView.announceForAccessibility(mSelectHours); + } + labelToAnimate = mHourView; + } else { + int minutes = mRadialTimePickerView.getCurrentMinute(); + mRadialTimePickerView.setContentDescription(mMinutePickerDescription + ": " + minutes); + if (announce) { + mRadialTimePickerView.announceForAccessibility(mSelectMinutes); + } + labelToAnimate = mMinuteView; + } + + int hourColor = (index == HOUR_INDEX) ? mHeaderSelectedColor : mHeaderUnSelectedColor; + int minuteColor = (index == MINUTE_INDEX) ? mHeaderSelectedColor : mHeaderUnSelectedColor; + mHourView.setTextColor(hourColor); + mMinuteView.setTextColor(minuteColor); + + ObjectAnimator pulseAnimator = getPulseAnimator(labelToAnimate, 0.85f, 1.1f); + if (delayLabelAnimate) { + pulseAnimator.setStartDelay(PULSE_ANIMATOR_DELAY); + } + pulseAnimator.start(); + } + + /** + * For keyboard mode, processes key events. + * + * @param keyCode the pressed key. + * + * @return true if the key was successfully processed, false otherwise. + */ + private boolean processKeyUp(int keyCode) { + if (keyCode == KeyEvent.KEYCODE_ESCAPE || keyCode == KeyEvent.KEYCODE_BACK) { + if (mDismissCallback != null) { + mDismissCallback.dismiss(mDelegator, true, getCurrentHour(), getCurrentMinute()); + } + return true; + } else if (keyCode == KeyEvent.KEYCODE_TAB) { + if(mInKbMode) { + if (isTypedTimeFullyLegal()) { + finishKbMode(true); + } + return true; + } + } else if (keyCode == KeyEvent.KEYCODE_ENTER) { + if (mInKbMode) { + if (!isTypedTimeFullyLegal()) { + return true; + } + finishKbMode(false); + } + if (mOnTimeChangedListener != null) { + mOnTimeChangedListener.onTimeChanged(mDelegator, + mRadialTimePickerView.getCurrentHour(), + mRadialTimePickerView.getCurrentMinute()); + } + if (mDismissCallback != null) { + mDismissCallback.dismiss(mDelegator, false, getCurrentHour(), getCurrentMinute()); + } + return true; + } else if (keyCode == KeyEvent.KEYCODE_DEL) { + if (mInKbMode) { + if (!mTypedTimes.isEmpty()) { + int deleted = deleteLastTypedKey(); + String deletedKeyStr; + if (deleted == getAmOrPmKeyCode(AM)) { + deletedKeyStr = mAmText; + } else if (deleted == getAmOrPmKeyCode(PM)) { + deletedKeyStr = mPmText; + } else { + deletedKeyStr = String.format("%d", getValFromKeyCode(deleted)); + } + mRadialTimePickerView.announceForAccessibility( + String.format(mDeletedKeyFormat, deletedKeyStr)); + updateDisplay(true); + } + } + } else if (keyCode == KeyEvent.KEYCODE_0 || keyCode == KeyEvent.KEYCODE_1 + || keyCode == KeyEvent.KEYCODE_2 || keyCode == KeyEvent.KEYCODE_3 + || keyCode == KeyEvent.KEYCODE_4 || keyCode == KeyEvent.KEYCODE_5 + || keyCode == KeyEvent.KEYCODE_6 || keyCode == KeyEvent.KEYCODE_7 + || keyCode == KeyEvent.KEYCODE_8 || keyCode == KeyEvent.KEYCODE_9 + || (!mIs24HourView && + (keyCode == getAmOrPmKeyCode(AM) || keyCode == getAmOrPmKeyCode(PM)))) { + if (!mInKbMode) { + if (mRadialTimePickerView == null) { + // Something's wrong, because time picker should definitely not be null. + Log.e(TAG, "Unable to initiate keyboard mode, TimePicker was null."); + return true; + } + mTypedTimes.clear(); + tryStartingKbMode(keyCode); + return true; + } + // We're already in keyboard mode. + if (addKeyIfLegal(keyCode)) { + updateDisplay(false); + } + return true; + } + return false; + } + + /** + * Try to start keyboard mode with the specified key. + * + * @param keyCode The key to use as the first press. Keyboard mode will not be started if the + * key is not legal to start with. Or, pass in -1 to get into keyboard mode without a starting + * key. + */ + private void tryStartingKbMode(int keyCode) { + if (keyCode == -1 || addKeyIfLegal(keyCode)) { + mInKbMode = true; + mDoneButton.setEnabled(false); + updateDisplay(false); + mRadialTimePickerView.setInputEnabled(false); + } + } + + private boolean addKeyIfLegal(int keyCode) { + // If we're in 24hour mode, we'll need to check if the input is full. If in AM/PM mode, + // we'll need to see if AM/PM have been typed. + if ((mIs24HourView && mTypedTimes.size() == 4) || + (!mIs24HourView && isTypedTimeFullyLegal())) { + return false; + } + + mTypedTimes.add(keyCode); + if (!isTypedTimeLegalSoFar()) { + deleteLastTypedKey(); + return false; + } + + int val = getValFromKeyCode(keyCode); + mRadialTimePickerView.announceForAccessibility(String.format("%d", val)); + // Automatically fill in 0's if AM or PM was legally entered. + if (isTypedTimeFullyLegal()) { + if (!mIs24HourView && mTypedTimes.size() <= 3) { + mTypedTimes.add(mTypedTimes.size() - 1, KeyEvent.KEYCODE_0); + mTypedTimes.add(mTypedTimes.size() - 1, KeyEvent.KEYCODE_0); + } + mDoneButton.setEnabled(true); + } + + return true; + } + + /** + * Traverse the tree to see if the keys that have been typed so far are legal as is, + * or may become legal as more keys are typed (excluding backspace). + */ + private boolean isTypedTimeLegalSoFar() { + Node node = mLegalTimesTree; + for (int keyCode : mTypedTimes) { + node = node.canReach(keyCode); + if (node == null) { + return false; + } + } + return true; + } + + /** + * Check if the time that has been typed so far is completely legal, as is. + */ + private boolean isTypedTimeFullyLegal() { + if (mIs24HourView) { + // For 24-hour mode, the time is legal if the hours and minutes are each legal. Note: + // getEnteredTime() will ONLY call isTypedTimeFullyLegal() when NOT in 24hour mode. + int[] values = getEnteredTime(null); + return (values[0] >= 0 && values[1] >= 0 && values[1] < 60); + } else { + // For AM/PM mode, the time is legal if it contains an AM or PM, as those can only be + // legally added at specific times based on the tree's algorithm. + return (mTypedTimes.contains(getAmOrPmKeyCode(AM)) || + mTypedTimes.contains(getAmOrPmKeyCode(PM))); + } + } + + private int deleteLastTypedKey() { + int deleted = mTypedTimes.remove(mTypedTimes.size() - 1); + if (!isTypedTimeFullyLegal()) { + mDoneButton.setEnabled(false); + } + return deleted; + } + + /** + * Get out of keyboard mode. If there is nothing in typedTimes, revert to TimePicker's time. + * @param updateDisplays If true, update the displays with the relevant time. + */ + private void finishKbMode(boolean updateDisplays) { + mInKbMode = false; + if (!mTypedTimes.isEmpty()) { + int values[] = getEnteredTime(null); + mRadialTimePickerView.setCurrentHour(values[0]); + mRadialTimePickerView.setCurrentMinute(values[1]); + if (!mIs24HourView) { + mRadialTimePickerView.setAmOrPm(values[2]); + } + mTypedTimes.clear(); + } + if (updateDisplays) { + updateDisplay(false); + mRadialTimePickerView.setInputEnabled(true); + } + } + + /** + * Update the hours, minutes, and AM/PM displays with the typed times. If the typedTimes is + * empty, either show an empty display (filled with the placeholder text), or update from the + * timepicker's values. + * + * @param allowEmptyDisplay if true, then if the typedTimes is empty, use the placeholder text. + * Otherwise, revert to the timepicker's values. + */ + private void updateDisplay(boolean allowEmptyDisplay) { + if (!allowEmptyDisplay && mTypedTimes.isEmpty()) { + int hour = mRadialTimePickerView.getCurrentHour(); + int minute = mRadialTimePickerView.getCurrentMinute(); + updateHeaderHour(hour, true); + updateHeaderMinute(minute); + if (!mIs24HourView) { + updateAmPmDisplay(hour < 12 ? AM : PM); + } + setCurrentItemShowing(mRadialTimePickerView.getCurrentItemShowing(), true, true, true); + mDoneButton.setEnabled(true); + } else { + boolean[] enteredZeros = {false, false}; + int[] values = getEnteredTime(enteredZeros); + String hourFormat = enteredZeros[0] ? "%02d" : "%2d"; + String minuteFormat = (enteredZeros[1]) ? "%02d" : "%2d"; + String hourStr = (values[0] == -1) ? mDoublePlaceholderText : + String.format(hourFormat, values[0]).replace(' ', mPlaceholderText); + String minuteStr = (values[1] == -1) ? mDoublePlaceholderText : + String.format(minuteFormat, values[1]).replace(' ', mPlaceholderText); + mHourView.setText(hourStr); + mHourView.setTextColor(mHeaderUnSelectedColor); + mMinuteView.setText(minuteStr); + mMinuteView.setTextColor(mHeaderUnSelectedColor); + if (!mIs24HourView) { + updateAmPmDisplay(values[2]); + } + } + } + + private int getValFromKeyCode(int keyCode) { + switch (keyCode) { + case KeyEvent.KEYCODE_0: + return 0; + case KeyEvent.KEYCODE_1: + return 1; + case KeyEvent.KEYCODE_2: + return 2; + case KeyEvent.KEYCODE_3: + return 3; + case KeyEvent.KEYCODE_4: + return 4; + case KeyEvent.KEYCODE_5: + return 5; + case KeyEvent.KEYCODE_6: + return 6; + case KeyEvent.KEYCODE_7: + return 7; + case KeyEvent.KEYCODE_8: + return 8; + case KeyEvent.KEYCODE_9: + return 9; + default: + return -1; + } + } + + /** + * Get the currently-entered time, as integer values of the hours and minutes typed. + * + * @param enteredZeros A size-2 boolean array, which the caller should initialize, and which + * may then be used for the caller to know whether zeros had been explicitly entered as either + * hours of minutes. This is helpful for deciding whether to show the dashes, or actual 0's. + * + * @return A size-3 int array. The first value will be the hours, the second value will be the + * minutes, and the third will be either AM or PM. + */ + private int[] getEnteredTime(boolean[] enteredZeros) { + int amOrPm = -1; + int startIndex = 1; + if (!mIs24HourView && isTypedTimeFullyLegal()) { + int keyCode = mTypedTimes.get(mTypedTimes.size() - 1); + if (keyCode == getAmOrPmKeyCode(AM)) { + amOrPm = AM; + } else if (keyCode == getAmOrPmKeyCode(PM)){ + amOrPm = PM; + } + startIndex = 2; + } + int minute = -1; + int hour = -1; + for (int i = startIndex; i <= mTypedTimes.size(); i++) { + int val = getValFromKeyCode(mTypedTimes.get(mTypedTimes.size() - i)); + if (i == startIndex) { + minute = val; + } else if (i == startIndex+1) { + minute += 10 * val; + if (enteredZeros != null && val == 0) { + enteredZeros[1] = true; + } + } else if (i == startIndex+2) { + hour = val; + } else if (i == startIndex+3) { + hour += 10 * val; + if (enteredZeros != null && val == 0) { + enteredZeros[0] = true; + } + } + } + + int[] ret = {hour, minute, amOrPm}; + return ret; + } + + /** + * Get the keycode value for AM and PM in the current language. + */ + private int getAmOrPmKeyCode(int amOrPm) { + // Cache the codes. + if (mAmKeyCode == -1 || mPmKeyCode == -1) { + // Find the first character in the AM/PM text that is unique. + KeyCharacterMap kcm = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); + char amChar; + char pmChar; + for (int i = 0; i < Math.max(mAmText.length(), mPmText.length()); i++) { + amChar = mAmText.toLowerCase(mCurrentLocale).charAt(i); + pmChar = mPmText.toLowerCase(mCurrentLocale).charAt(i); + if (amChar != pmChar) { + KeyEvent[] events = kcm.getEvents(new char[]{amChar, pmChar}); + // There should be 4 events: a down and up for both AM and PM. + if (events != null && events.length == 4) { + mAmKeyCode = events[0].getKeyCode(); + mPmKeyCode = events[2].getKeyCode(); + } else { + Log.e(TAG, "Unable to find keycodes for AM and PM."); + } + break; + } + } + } + if (amOrPm == AM) { + return mAmKeyCode; + } else if (amOrPm == PM) { + return mPmKeyCode; + } + + return -1; + } + + /** + * Create a tree for deciding what keys can legally be typed. + */ + private void generateLegalTimesTree() { + // Create a quick cache of numbers to their keycodes. + final int k0 = KeyEvent.KEYCODE_0; + final int k1 = KeyEvent.KEYCODE_1; + final int k2 = KeyEvent.KEYCODE_2; + final int k3 = KeyEvent.KEYCODE_3; + final int k4 = KeyEvent.KEYCODE_4; + final int k5 = KeyEvent.KEYCODE_5; + final int k6 = KeyEvent.KEYCODE_6; + final int k7 = KeyEvent.KEYCODE_7; + final int k8 = KeyEvent.KEYCODE_8; + final int k9 = KeyEvent.KEYCODE_9; + + // The root of the tree doesn't contain any numbers. + mLegalTimesTree = new Node(); + if (mIs24HourView) { + // We'll be re-using these nodes, so we'll save them. + Node minuteFirstDigit = new Node(k0, k1, k2, k3, k4, k5); + Node minuteSecondDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); + // The first digit must be followed by the second digit. + minuteFirstDigit.addChild(minuteSecondDigit); + + // The first digit may be 0-1. + Node firstDigit = new Node(k0, k1); + mLegalTimesTree.addChild(firstDigit); + + // When the first digit is 0-1, the second digit may be 0-5. + Node secondDigit = new Node(k0, k1, k2, k3, k4, k5); + firstDigit.addChild(secondDigit); + // We may now be followed by the first minute digit. E.g. 00:09, 15:58. + secondDigit.addChild(minuteFirstDigit); + + // When the first digit is 0-1, and the second digit is 0-5, the third digit may be 6-9. + Node thirdDigit = new Node(k6, k7, k8, k9); + // The time must now be finished. E.g. 0:55, 1:08. + secondDigit.addChild(thirdDigit); + + // When the first digit is 0-1, the second digit may be 6-9. + secondDigit = new Node(k6, k7, k8, k9); + firstDigit.addChild(secondDigit); + // We must now be followed by the first minute digit. E.g. 06:50, 18:20. + secondDigit.addChild(minuteFirstDigit); + + // The first digit may be 2. + firstDigit = new Node(k2); + mLegalTimesTree.addChild(firstDigit); + + // When the first digit is 2, the second digit may be 0-3. + secondDigit = new Node(k0, k1, k2, k3); + firstDigit.addChild(secondDigit); + // We must now be followed by the first minute digit. E.g. 20:50, 23:09. + secondDigit.addChild(minuteFirstDigit); + + // When the first digit is 2, the second digit may be 4-5. + secondDigit = new Node(k4, k5); + firstDigit.addChild(secondDigit); + // We must now be followd by the last minute digit. E.g. 2:40, 2:53. + secondDigit.addChild(minuteSecondDigit); + + // The first digit may be 3-9. + firstDigit = new Node(k3, k4, k5, k6, k7, k8, k9); + mLegalTimesTree.addChild(firstDigit); + // We must now be followed by the first minute digit. E.g. 3:57, 8:12. + firstDigit.addChild(minuteFirstDigit); + } else { + // We'll need to use the AM/PM node a lot. + // Set up AM and PM to respond to "a" and "p". + Node ampm = new Node(getAmOrPmKeyCode(AM), getAmOrPmKeyCode(PM)); + + // The first hour digit may be 1. + Node firstDigit = new Node(k1); + mLegalTimesTree.addChild(firstDigit); + // We'll allow quick input of on-the-hour times. E.g. 1pm. + firstDigit.addChild(ampm); + + // When the first digit is 1, the second digit may be 0-2. + Node secondDigit = new Node(k0, k1, k2); + firstDigit.addChild(secondDigit); + // Also for quick input of on-the-hour times. E.g. 10pm, 12am. + secondDigit.addChild(ampm); + + // When the first digit is 1, and the second digit is 0-2, the third digit may be 0-5. + Node thirdDigit = new Node(k0, k1, k2, k3, k4, k5); + secondDigit.addChild(thirdDigit); + // The time may be finished now. E.g. 1:02pm, 1:25am. + thirdDigit.addChild(ampm); + + // When the first digit is 1, the second digit is 0-2, and the third digit is 0-5, + // the fourth digit may be 0-9. + Node fourthDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); + thirdDigit.addChild(fourthDigit); + // The time must be finished now. E.g. 10:49am, 12:40pm. + fourthDigit.addChild(ampm); + + // When the first digit is 1, and the second digit is 0-2, the third digit may be 6-9. + thirdDigit = new Node(k6, k7, k8, k9); + secondDigit.addChild(thirdDigit); + // The time must be finished now. E.g. 1:08am, 1:26pm. + thirdDigit.addChild(ampm); + + // When the first digit is 1, the second digit may be 3-5. + secondDigit = new Node(k3, k4, k5); + firstDigit.addChild(secondDigit); + + // When the first digit is 1, and the second digit is 3-5, the third digit may be 0-9. + thirdDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); + secondDigit.addChild(thirdDigit); + // The time must be finished now. E.g. 1:39am, 1:50pm. + thirdDigit.addChild(ampm); + + // The hour digit may be 2-9. + firstDigit = new Node(k2, k3, k4, k5, k6, k7, k8, k9); + mLegalTimesTree.addChild(firstDigit); + // We'll allow quick input of on-the-hour-times. E.g. 2am, 5pm. + firstDigit.addChild(ampm); + + // When the first digit is 2-9, the second digit may be 0-5. + secondDigit = new Node(k0, k1, k2, k3, k4, k5); + firstDigit.addChild(secondDigit); + + // When the first digit is 2-9, and the second digit is 0-5, the third digit may be 0-9. + thirdDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); + secondDigit.addChild(thirdDigit); + // The time must be finished now. E.g. 2:57am, 9:30pm. + thirdDigit.addChild(ampm); + } + } + + /** + * Simple node class to be used for traversal to check for legal times. + * mLegalKeys represents the keys that can be typed to get to the node. + * mChildren are the children that can be reached from this node. + */ + private class Node { + private int[] mLegalKeys; + private ArrayList<Node> mChildren; + + public Node(int... legalKeys) { + mLegalKeys = legalKeys; + mChildren = new ArrayList<Node>(); + } + + public void addChild(Node child) { + mChildren.add(child); + } + + public boolean containsKey(int key) { + for (int i = 0; i < mLegalKeys.length; i++) { + if (mLegalKeys[i] == key) { + return true; + } + } + return false; + } + + public Node canReach(int key) { + if (mChildren == null) { + return null; + } + for (Node child : mChildren) { + if (child.containsKey(key)) { + return child; + } + } + return null; + } + } + + private class KeyboardListener implements View.OnKeyListener { + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_UP) { + return processKeyUp(keyCode); + } + return false; + } + } + + /** + * Render an animator to pulsate a view in place. + * + * @param labelToAnimate the view to pulsate. + * @return The animator object. Use .start() to begin. + */ + private static ObjectAnimator getPulseAnimator(View labelToAnimate, float decreaseRatio, + float increaseRatio) { + final Keyframe k0 = Keyframe.ofFloat(0f, 1f); + final Keyframe k1 = Keyframe.ofFloat(0.275f, decreaseRatio); + final Keyframe k2 = Keyframe.ofFloat(0.69f, increaseRatio); + final Keyframe k3 = Keyframe.ofFloat(1f, 1f); + + PropertyValuesHolder scaleX = PropertyValuesHolder.ofKeyframe("scaleX", k0, k1, k2, k3); + PropertyValuesHolder scaleY = PropertyValuesHolder.ofKeyframe("scaleY", k0, k1, k2, k3); + ObjectAnimator pulseAnimator = + ObjectAnimator.ofPropertyValuesHolder(labelToAnimate, scaleX, scaleY); + pulseAnimator.setDuration(PULSE_ANIMATOR_DURATION); + + return pulseAnimator; + } +} diff --git a/core/java/android/widget/Toast.java b/core/java/android/widget/Toast.java index e38dfa7..bf5e49b 100644 --- a/core/java/android/widget/Toast.java +++ b/core/java/android/widget/Toast.java @@ -16,6 +16,7 @@ package android.widget; +import android.annotation.IntDef; import android.app.INotificationManager; import android.app.ITransientNotification; import android.content.Context; @@ -29,11 +30,13 @@ import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; -import android.view.View.OnClickListener; import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * A toast is a view containing a quick little message for the user. The toast class * helps you create and show those. @@ -61,6 +64,11 @@ public class Toast { static final String TAG = "Toast"; static final boolean localLOGV = false; + /** @hide */ + @IntDef({LENGTH_SHORT, LENGTH_LONG}) + @Retention(RetentionPolicy.SOURCE) + public @interface Duration {} + /** * Show the view or text notification for a short period of time. This time * could be user-definable. This is the default. @@ -152,7 +160,7 @@ public class Toast { * @see #LENGTH_SHORT * @see #LENGTH_LONG */ - public void setDuration(int duration) { + public void setDuration(@Duration int duration) { mDuration = duration; } @@ -160,6 +168,7 @@ public class Toast { * Return the duration. * @see #setDuration */ + @Duration public int getDuration() { return mDuration; } @@ -237,7 +246,7 @@ public class Toast { * {@link #LENGTH_LONG} * */ - public static Toast makeText(Context context, CharSequence text, int duration) { + public static Toast makeText(Context context, CharSequence text, @Duration int duration) { Toast result = new Toast(context); LayoutInflater inflate = (LayoutInflater) @@ -263,7 +272,7 @@ public class Toast { * * @throws Resources.NotFoundException if the resource can't be found. */ - public static Toast makeText(Context context, int resId, int duration) + public static Toast makeText(Context context, int resId, @Duration int duration) throws Resources.NotFoundException { return makeText(context, context.getResources().getText(resId), duration); } diff --git a/core/java/android/widget/ToggleButton.java b/core/java/android/widget/ToggleButton.java index cedc777..28519d1 100644 --- a/core/java/android/widget/ToggleButton.java +++ b/core/java/android/widget/ToggleButton.java @@ -16,7 +16,6 @@ package android.widget; - import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; @@ -25,8 +24,6 @@ import android.util.AttributeSet; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; -import com.android.internal.R; - /** * Displays checked/unchecked states as a button * with a "light" indicator and by default accompanied with the text "ON" or "OFF". @@ -46,13 +43,12 @@ public class ToggleButton extends CompoundButton { private static final int NO_ALPHA = 0xFF; private float mDisabledAlpha; - - public ToggleButton(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - TypedArray a = - context.obtainStyledAttributes( - attrs, com.android.internal.R.styleable.ToggleButton, defStyle, 0); + + public ToggleButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.ToggleButton, defStyleAttr, defStyleRes); mTextOn = a.getText(com.android.internal.R.styleable.ToggleButton_textOn); mTextOff = a.getText(com.android.internal.R.styleable.ToggleButton_textOff); mDisabledAlpha = a.getFloat(com.android.internal.R.styleable.ToggleButton_disabledAlpha, 0.5f); @@ -60,6 +56,10 @@ public class ToggleButton extends CompoundButton { a.recycle(); } + public ToggleButton(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + public ToggleButton(Context context, AttributeSet attrs) { this(context, attrs, com.android.internal.R.attr.buttonStyleToggle); } diff --git a/core/java/android/widget/TwoLineListItem.java b/core/java/android/widget/TwoLineListItem.java index f7e5266..5606c60 100644 --- a/core/java/android/widget/TwoLineListItem.java +++ b/core/java/android/widget/TwoLineListItem.java @@ -56,11 +56,15 @@ public class TwoLineListItem extends RelativeLayout { this(context, attrs, 0); } - public TwoLineListItem(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public TwoLineListItem(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public TwoLineListItem(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.TwoLineListItem, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.TwoLineListItem, defStyleAttr, defStyleRes); a.recycle(); } diff --git a/core/java/android/widget/VideoView.java b/core/java/android/widget/VideoView.java index d57b739..f23c64f 100644 --- a/core/java/android/widget/VideoView.java +++ b/core/java/android/widget/VideoView.java @@ -19,7 +19,6 @@ package android.widget; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; -import android.content.Intent; import android.content.res.Resources; import android.graphics.Canvas; import android.media.AudioManager; @@ -127,8 +126,12 @@ public class VideoView extends SurfaceView initVideoView(); } - public VideoView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public VideoView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public VideoView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); initVideoView(); } @@ -297,11 +300,8 @@ public class VideoView extends SurfaceView // not ready for playback just yet, will try again later return; } - // Tell the music playback service to pause - // TODO: these constants need to be published somewhere in the framework. - Intent i = new Intent("com.android.music.musicservicecommand"); - i.putExtra("command", "pause"); - mContext.sendBroadcast(i); + AudioManager am = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + am.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); // we shouldn't clear the target state, because somebody might have // called start() previously diff --git a/core/java/android/widget/ZoomButton.java b/core/java/android/widget/ZoomButton.java index af17c94..715e868 100644 --- a/core/java/android/widget/ZoomButton.java +++ b/core/java/android/widget/ZoomButton.java @@ -49,8 +49,12 @@ public class ZoomButton extends ImageButton implements OnLongClickListener { this(context, attrs, 0); } - public ZoomButton(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public ZoomButton(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ZoomButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); mHandler = new Handler(); setOnLongClickListener(this); } diff --git a/core/java/android/widget/ZoomButtonsController.java b/core/java/android/widget/ZoomButtonsController.java index 50c803b..f7e9648 100644 --- a/core/java/android/widget/ZoomButtonsController.java +++ b/core/java/android/widget/ZoomButtonsController.java @@ -32,7 +32,6 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; -import android.view.ViewParent; import android.view.ViewRootImpl; import android.view.WindowManager; import android.view.View.OnClickListener; |