diff options
65 files changed, 1991 insertions, 541 deletions
diff --git a/api/current.txt b/api/current.txt index f8cc4fa19026..4285e5b8df8c 100644 --- a/api/current.txt +++ b/api/current.txt @@ -5362,7 +5362,7 @@ package android.app.task { public class Task implements android.os.Parcelable { method public int describeContents(); method public int getBackoffPolicy(); - method public android.os.Bundle getExtras(); + method public android.os.PersistableBundle getExtras(); method public int getId(); method public long getInitialBackoffMillis(); method public long getIntervalMillis(); @@ -5386,7 +5386,7 @@ package android.app.task { ctor public Task.Builder(int, android.content.ComponentName); method public android.app.task.Task build(); method public android.app.task.Task.Builder setBackoffCriteria(long, int); - method public android.app.task.Task.Builder setExtras(android.os.Bundle); + method public android.app.task.Task.Builder setExtras(android.os.PersistableBundle); method public android.app.task.Task.Builder setMinimumLatency(long); method public android.app.task.Task.Builder setOverrideDeadline(long); method public android.app.task.Task.Builder setPeriodic(long); @@ -5413,7 +5413,7 @@ package android.app.task { public class TaskParams implements android.os.Parcelable { method public int describeContents(); - method public android.os.Bundle getExtras(); + method public android.os.PersistableBundle getExtras(); method public int getTaskId(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator CREATOR; @@ -12146,7 +12146,6 @@ package android.hardware.camera2 { field public static final android.hardware.camera2.CameraCharacteristics.Key LENS_INFO_MINIMUM_FOCUS_DISTANCE; field public static final android.hardware.camera2.CameraCharacteristics.Key NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES; field public static final android.hardware.camera2.CameraCharacteristics.Key REQUEST_AVAILABLE_CAPABILITIES; - field public static final android.hardware.camera2.CameraCharacteristics.Key REQUEST_MAX_NUM_INPUT_STREAMS; field public static final android.hardware.camera2.CameraCharacteristics.Key REQUEST_MAX_NUM_OUTPUT_PROC; field public static final android.hardware.camera2.CameraCharacteristics.Key REQUEST_MAX_NUM_OUTPUT_PROC_STALLING; field public static final android.hardware.camera2.CameraCharacteristics.Key REQUEST_MAX_NUM_OUTPUT_RAW; @@ -12371,7 +12370,6 @@ package android.hardware.camera2 { field public static final int REQUEST_AVAILABLE_CAPABILITIES_DNG = 5; // 0x5 field public static final int REQUEST_AVAILABLE_CAPABILITIES_MANUAL_POST_PROCESSING = 3; // 0x3 field public static final int REQUEST_AVAILABLE_CAPABILITIES_MANUAL_SENSOR = 2; // 0x2 - field public static final int REQUEST_AVAILABLE_CAPABILITIES_ZSL = 4; // 0x4 field public static final int SCALER_CROPPING_TYPE_CENTER_ONLY = 0; // 0x0 field public static final int SCALER_CROPPING_TYPE_FREEFORM = 1; // 0x1 field public static final int SENSOR_INFO_COLOR_FILTER_ARRANGEMENT_BGGR = 3; // 0x3 @@ -15825,6 +15823,8 @@ package android.media.session { package android.media.tv { public final class TvContract { + method public static final android.net.Uri buildChannelLogoUri(long); + method public static final android.net.Uri buildChannelLogoUri(android.net.Uri); method public static final android.net.Uri buildChannelUri(long); method public static final android.net.Uri buildChannelsUriForInput(android.content.ComponentName); method public static final android.net.Uri buildChannelsUriForInput(android.content.ComponentName, boolean); @@ -15882,6 +15882,10 @@ package android.media.tv { field public static final int TYPE_T_DMB = 393216; // 0x60000 } + public static final class TvContract.Channels.Logo { + field public static final java.lang.String CONTENT_DIRECTORY = "logo"; + } + public static final class TvContract.Programs implements android.media.tv.TvContract.BaseTvColumns { field public static final java.lang.String COLUMN_AUDIO_LANGUAGE = "audio_language"; field public static final java.lang.String COLUMN_BROADCAST_GENRE = "broadcast_genre"; @@ -15890,8 +15894,10 @@ package android.media.tv { field public static final java.lang.String COLUMN_END_TIME_UTC_MILLIS = "end_time_utc_millis"; field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_DATA = "internal_provider_data"; field public static final java.lang.String COLUMN_LONG_DESCRIPTION = "long_description"; + field public static final java.lang.String COLUMN_POSTER_ART_URI = "poster_art_uri"; field public static final java.lang.String COLUMN_SHORT_DESCRIPTION = "short_description"; field public static final java.lang.String COLUMN_START_TIME_UTC_MILLIS = "start_time_utc_millis"; + field public static final java.lang.String COLUMN_THUMBNAIL_URI = "thumbnail_uri"; field public static final java.lang.String COLUMN_TITLE = "title"; field public static final java.lang.String COLUMN_VERSION_NUMBER = "version_number"; field public static final java.lang.String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/program"; @@ -27395,55 +27401,6 @@ package android.telephony { field public static final android.os.Parcelable.Creator CREATOR; } - public class DisconnectCause { - method public static java.lang.String toString(int); - field public static final int BUSY = 4; // 0x4 - field public static final int CALL_BARRED = 20; // 0x14 - field public static final int CDMA_ACCESS_BLOCKED = 35; // 0x23 - field public static final int CDMA_ACCESS_FAILURE = 32; // 0x20 - field public static final int CDMA_CALL_LOST = 41; // 0x29 - field public static final int CDMA_DROP = 27; // 0x1b - field public static final int CDMA_INTERCEPT = 28; // 0x1c - field public static final int CDMA_LOCKED_UNTIL_POWER_CYCLE = 26; // 0x1a - field public static final int CDMA_NOT_EMERGENCY = 34; // 0x22 - field public static final int CDMA_PREEMPTED = 33; // 0x21 - field public static final int CDMA_REORDER = 29; // 0x1d - field public static final int CDMA_RETRY_ORDER = 31; // 0x1f - field public static final int CDMA_SO_REJECT = 30; // 0x1e - field public static final int CONGESTION = 5; // 0x5 - field public static final int CS_RESTRICTED = 22; // 0x16 - field public static final int CS_RESTRICTED_EMERGENCY = 24; // 0x18 - field public static final int CS_RESTRICTED_NORMAL = 23; // 0x17 - field public static final int DIALED_MMI = 39; // 0x27 - field public static final int EMERGENCY_ONLY = 37; // 0x25 - field public static final int ERROR_UNSPECIFIED = 36; // 0x24 - field public static final int FDN_BLOCKED = 21; // 0x15 - field public static final int ICC_ERROR = 19; // 0x13 - field public static final int INCOMING_MISSED = 1; // 0x1 - field public static final int INCOMING_REJECTED = 16; // 0x10 - field public static final int INVALID_CREDENTIALS = 10; // 0xa - field public static final int INVALID_NUMBER = 7; // 0x7 - field public static final int LIMIT_EXCEEDED = 15; // 0xf - field public static final int LOCAL = 3; // 0x3 - field public static final int LOST_SIGNAL = 14; // 0xe - field public static final int MAXIMUM_VALID_VALUE = 42; // 0x2a - field public static final int MINIMUM_VALID_VALUE = 0; // 0x0 - field public static final int MMI = 6; // 0x6 - field public static final int NORMAL = 2; // 0x2 - field public static final int NOT_DISCONNECTED = 0; // 0x0 - field public static final int NOT_VALID = -1; // 0xffffffff - field public static final int NO_PHONE_NUMBER_SUPPLIED = 38; // 0x26 - field public static final int NUMBER_UNREACHABLE = 8; // 0x8 - field public static final int OUT_OF_NETWORK = 11; // 0xb - field public static final int OUT_OF_SERVICE = 18; // 0x12 - field public static final int POWER_OFF = 17; // 0x11 - field public static final int SERVER_ERROR = 12; // 0xc - field public static final int SERVER_UNREACHABLE = 9; // 0x9 - field public static final int TIMED_OUT = 13; // 0xd - field public static final int UNOBTAINABLE_NUMBER = 25; // 0x19 - field public static final int VOICEMAIL_NUMBER_MISSING = 40; // 0x28 - } - public class NeighboringCellInfo implements android.os.Parcelable { ctor public deprecated NeighboringCellInfo(); ctor public deprecated NeighboringCellInfo(int, int); @@ -27495,7 +27452,7 @@ package android.telephony { method public static boolean isEmergencyNumber(java.lang.String); method public static boolean isGlobalPhoneNumber(java.lang.String); method public static boolean isISODigit(char); - method public static boolean isLocalEmergencyNumber(java.lang.String, android.content.Context); + method public static boolean isLocalEmergencyNumber(android.content.Context, java.lang.String); method public static final boolean isNonSeparator(char); method public static final boolean isReallyDialable(char); method public static final boolean isStartsPostDial(char); @@ -31991,7 +31948,6 @@ package android.view { method protected int computeVerticalScrollRange(); method public android.view.accessibility.AccessibilityNodeInfo createAccessibilityNodeInfo(); method public void createContextMenu(android.view.ContextMenu); - method public final android.animation.ValueAnimator createRevealAnimator(int, int, float, float); method public void destroyDrawingCache(); method public android.view.WindowInsets dispatchApplyWindowInsets(android.view.WindowInsets); method public void dispatchConfigurationChanged(android.content.res.Configuration); @@ -32639,6 +32595,10 @@ package android.view { method public abstract boolean onTouch(android.view.View, android.view.MotionEvent); } + public class ViewAnimationUtils { + method public static final android.animation.ValueAnimator createCircularReveal(android.view.View, int, int, float, float); + } + public class ViewConfiguration { ctor public deprecated ViewConfiguration(); method public static android.view.ViewConfiguration get(android.content.Context); @@ -33798,11 +33758,17 @@ package android.view.accessibility { public static final class CaptioningManager.CaptionStyle { method public android.graphics.Typeface getTypeface(); + method public boolean hasBackgroundColor(); + method public boolean hasEdgeColor(); + method public boolean hasEdgeType(); + method public boolean hasForegroundColor(); + method public boolean hasWindowColor(); field public static final int EDGE_TYPE_DEPRESSED = 4; // 0x4 field public static final int EDGE_TYPE_DROP_SHADOW = 2; // 0x2 field public static final int EDGE_TYPE_NONE = 0; // 0x0 field public static final int EDGE_TYPE_OUTLINE = 1; // 0x1 field public static final int EDGE_TYPE_RAISED = 3; // 0x3 + field public static final int EDGE_TYPE_UNSPECIFIED = -1; // 0xffffffff field public final int backgroundColor; field public final int edgeColor; field public final int edgeType; diff --git a/core/java/android/app/task/Task.java b/core/java/android/app/task/Task.java index ca4aeb217cd3..87d57fb41319 100644 --- a/core/java/android/app/task/Task.java +++ b/core/java/android/app/task/Task.java @@ -20,6 +20,7 @@ import android.content.ComponentName; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; +import android.os.PersistableBundle; /** * Container of data passed to the {@link android.app.task.TaskManager} fully encapsulating the @@ -37,6 +38,18 @@ public class Task implements Parcelable { } /** + * Amount of backoff a task has initially by default, in milliseconds. + * @hide. + */ + public static final long DEFAULT_INITIAL_BACKOFF_MILLIS = 5000L; + + /** + * Default type of backoff. + * @hide + */ + public static final int DEFAULT_BACKOFF_POLICY = BackoffPolicy.EXPONENTIAL; + + /** * Linear: retry_time(failure_time, t) = failure_time + initial_retry_delay * t, t >= 1 * Expon: retry_time(failure_time, t) = failure_time + initial_retry_delay ^ t, t >= 1 */ @@ -47,7 +60,7 @@ public class Task implements Parcelable { private final int taskId; // TODO: Change this to use PersistableBundle when that lands in master. - private final Bundle extras; + private final PersistableBundle extras; private final ComponentName service; private final boolean requireCharging; private final boolean requireDeviceIdle; @@ -71,7 +84,7 @@ public class Task implements Parcelable { /** * Bundle of extras which are returned to your application at execution time. */ - public Bundle getExtras() { + public PersistableBundle getExtras() { return extras; } @@ -171,7 +184,7 @@ public class Task implements Parcelable { private Task(Parcel in) { taskId = in.readInt(); - extras = in.readBundle(); + extras = in.readPersistableBundle(); service = ComponentName.readFromParcel(in); requireCharging = in.readInt() == 1; requireDeviceIdle = in.readInt() == 1; @@ -188,7 +201,7 @@ public class Task implements Parcelable { private Task(Task.Builder b) { taskId = b.mTaskId; - extras = new Bundle(b.mExtras); + extras = new PersistableBundle(b.mExtras); service = b.mTaskService; requireCharging = b.mRequiresCharging; requireDeviceIdle = b.mRequiresDeviceIdle; @@ -211,7 +224,7 @@ public class Task implements Parcelable { @Override public void writeToParcel(Parcel out, int flags) { out.writeInt(taskId); - out.writeBundle(extras); + out.writePersistableBundle(extras); ComponentName.writeToParcel(service, out); out.writeInt(requireCharging ? 1 : 0); out.writeInt(requireDeviceIdle ? 1 : 0); @@ -238,12 +251,10 @@ public class Task implements Parcelable { } }; - /** - * Builder class for constructing {@link Task} objects. - */ + /** Builder class for constructing {@link Task} objects. */ public static final class Builder { private int mTaskId; - private Bundle mExtras; + private PersistableBundle mExtras = PersistableBundle.EMPTY; private ComponentName mTaskService; // Requirements. private boolean mRequiresCharging; @@ -258,8 +269,8 @@ public class Task implements Parcelable { private boolean mHasLateConstraint; private long mIntervalMillis; // Back-off parameters. - private long mInitialBackoffMillis = 5000L; - private int mBackoffPolicy = BackoffPolicy.EXPONENTIAL; + private long mInitialBackoffMillis = DEFAULT_INITIAL_BACKOFF_MILLIS; + private int mBackoffPolicy = DEFAULT_BACKOFF_POLICY; /** Easy way to track whether the client has tried to set a back-off policy. */ private boolean mBackoffPolicySet = false; @@ -279,7 +290,7 @@ public class Task implements Parcelable { * Set optional extras. This is persisted, so we only allow primitive types. * @param extras Bundle containing extras you want the scheduler to hold on to for you. */ - public Builder setExtras(Bundle extras) { + public Builder setExtras(PersistableBundle extras) { mExtras = extras; return this; } @@ -394,18 +405,13 @@ public class Task implements Parcelable { * @return The task object to hand to the TaskManager. This object is immutable. */ public Task build() { - if (mExtras == null) { - mExtras = Bundle.EMPTY; - } - if (mTaskId < 0) { - throw new IllegalArgumentException("Task id must be greater than 0."); - } + mExtras = new PersistableBundle(mExtras); // Make our own copy. // Check that a deadline was not set on a periodic task. - if (mIsPeriodic && mHasLateConstraint) { + if (mIsPeriodic && (mMaxExecutionDelayMillis != 0L)) { throw new IllegalArgumentException("Can't call setOverrideDeadline() on a " + "periodic task."); } - if (mIsPeriodic && mHasEarlyConstraint) { + if (mIsPeriodic && (mMinLatencyMillis != 0L)) { throw new IllegalArgumentException("Can't call setMinimumLatency() on a " + "periodic task"); } diff --git a/core/java/android/app/task/TaskParams.java b/core/java/android/app/task/TaskParams.java index dacb348008bf..f4908c664f20 100644 --- a/core/java/android/app/task/TaskParams.java +++ b/core/java/android/app/task/TaskParams.java @@ -16,10 +16,10 @@ package android.app.task; -import android.os.Bundle; import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; +import android.os.PersistableBundle; /** * Contains the parameters used to configure/identify your task. You do not create this object @@ -28,11 +28,11 @@ import android.os.Parcelable; public class TaskParams implements Parcelable { private final int taskId; - private final Bundle extras; + private final PersistableBundle extras; private final IBinder callback; /** @hide */ - public TaskParams(int taskId, Bundle extras, IBinder callback) { + public TaskParams(int taskId, PersistableBundle extras, IBinder callback) { this.taskId = taskId; this.extras = extras; this.callback = callback; @@ -47,10 +47,10 @@ public class TaskParams implements Parcelable { /** * @return The extras you passed in when constructing this task with - * {@link android.app.task.Task.Builder#setExtras(android.os.Bundle)}. This will + * {@link android.app.task.Task.Builder#setExtras(android.os.PersistableBundle)}. This will * never be null. If you did not set any extras this will be an empty bundle. */ - public Bundle getExtras() { + public PersistableBundle getExtras() { return extras; } @@ -61,7 +61,7 @@ public class TaskParams implements Parcelable { private TaskParams(Parcel in) { taskId = in.readInt(); - extras = in.readBundle(); + extras = in.readPersistableBundle(); callback = in.readStrongBinder(); } @@ -73,7 +73,7 @@ public class TaskParams implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(taskId); - dest.writeBundle(extras); + dest.writePersistableBundle(extras); dest.writeStrongBinder(callback); } diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java index 222374a0e72a..08cfc875c169 100644 --- a/core/java/android/hardware/camera2/CameraCharacteristics.java +++ b/core/java/android/hardware/camera2/CameraCharacteristics.java @@ -751,6 +751,7 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri * <p>For example, for Zero Shutter Lag (ZSL) still capture use case, the input * stream image format will be RAW_OPAQUE, the associated output stream image format * should be JPEG.</p> + * @hide */ public static final Key<Integer> REQUEST_MAX_NUM_INPUT_STREAMS = new Key<Integer>("android.request.maxNumInputStreams", int.class); @@ -974,7 +975,7 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri * <p>The mapping of image formats that are supported by this * camera device for input streams, to their corresponding output formats.</p> * <p>All camera devices with at least 1 - * {@link CameraCharacteristics#REQUEST_MAX_NUM_INPUT_STREAMS android.request.maxNumInputStreams} will have at least one + * android.request.maxNumInputStreams will have at least one * available input format.</p> * <p>The camera device will support the following map of formats, * if its dependent capability is supported:</p> @@ -1021,8 +1022,6 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri * <p>Attempting to configure an input stream with output streams not * listed as available in this map is not valid.</p> * <p>TODO: typedef to ReprocessFormatMap</p> - * - * @see CameraCharacteristics#REQUEST_MAX_NUM_INPUT_STREAMS * @hide */ public static final Key<int[]> SCALER_AVAILABLE_INPUT_OUTPUT_FORMATS_MAP = diff --git a/core/java/android/hardware/camera2/CameraMetadata.java b/core/java/android/hardware/camera2/CameraMetadata.java index b3e165e793eb..94a5a79cadeb 100644 --- a/core/java/android/hardware/camera2/CameraMetadata.java +++ b/core/java/android/hardware/camera2/CameraMetadata.java @@ -340,6 +340,7 @@ public abstract class CameraMetadata<TKey> { * (both input/output) will match the maximum available * resolution of JPEG streams.</li> * </ul> + * <p>@hide this, TODO: remove it when input related APIs are ready.</p> * @see CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES */ public static final int REQUEST_AVAILABLE_CAPABILITIES_ZSL = 4; diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index c7c007ec001f..634ae609b01f 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -10609,24 +10609,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * Returns a ValueAnimator which can animate a clipping circle. - * <p> - * The View will be clipped to the animating circle. - * <p> - * Any shadow cast by the View will respect the circular clip from this animator. - * - * @param centerX The x coordinate of the center of the animating circle. - * @param centerY The y coordinate of the center of the animating circle. - * @param startRadius The starting radius of the animating circle. - * @param endRadius The ending radius of the animating circle. - */ - public final ValueAnimator createRevealAnimator(int centerX, int centerY, - float startRadius, float endRadius) { - return RevealAnimator.ofRevealCircle(this, centerX, centerY, - startRadius, endRadius, false); - } - - /** * Returns a ValueAnimator which can animate a clearing circle. * <p> * The View is prevented from drawing within the circle, so the content diff --git a/core/java/android/view/ViewAnimationUtils.java b/core/java/android/view/ViewAnimationUtils.java new file mode 100644 index 000000000000..3854f34faeb2 --- /dev/null +++ b/core/java/android/view/ViewAnimationUtils.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.animation.RevealAnimator; +import android.animation.ValueAnimator; + +/** + * Defines common utilities for working with View's animations. + * + */ +public class ViewAnimationUtils { + private ViewAnimationUtils() {} + /** + * Returns a ValueAnimator which can animate a clipping circle. + * + * Any shadow cast by the View will respect the circular clip from this animator. + * + * @param view The View will be clipped to the animating circle. + * @param centerX The x coordinate of the center of the animating circle. + * @param centerY The y coordinate of the center of the animating circle. + * @param startRadius The starting radius of the animating circle. + * @param endRadius The ending radius of the animating circle. + */ + public static final ValueAnimator createCircularReveal(View view, + int centerX, int centerY, float startRadius, float endRadius) { + return RevealAnimator.ofRevealCircle(view, centerX, centerY, + startRadius, endRadius, false); + } +} diff --git a/core/java/android/view/accessibility/CaptioningManager.java b/core/java/android/view/accessibility/CaptioningManager.java index a0134d6ebc36..334ff43206f6 100644 --- a/core/java/android/view/accessibility/CaptioningManager.java +++ b/core/java/android/view/accessibility/CaptioningManager.java @@ -16,6 +16,8 @@ package android.view.accessibility; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; @@ -78,6 +80,7 @@ public class CaptioningManager { * language * @hide */ + @Nullable public final String getRawLocale() { return Secure.getString(mContentResolver, Secure.ACCESSIBILITY_CAPTIONING_LOCALE); } @@ -86,6 +89,7 @@ public class CaptioningManager { * @return the locale for the user's preferred captioning language, or null * if not specified */ + @Nullable public final Locale getLocale() { final String rawLocale = getRawLocale(); if (!TextUtils.isEmpty(rawLocale)) { @@ -125,6 +129,7 @@ public class CaptioningManager { * @return the user's preferred visual properties for captions as a * {@link CaptionStyle}, or the default style if not specified */ + @NonNull public CaptionStyle getUserStyle() { final int preset = getRawUserStyle(); if (preset == CaptionStyle.PRESET_CUSTOM) { @@ -140,17 +145,19 @@ public class CaptioningManager { * * @param listener the listener to add */ - public void addCaptioningChangeListener(CaptioningChangeListener listener) { + public void addCaptioningChangeListener(@NonNull CaptioningChangeListener listener) { synchronized (mListeners) { if (mListeners.isEmpty()) { registerObserver(Secure.ACCESSIBILITY_CAPTIONING_ENABLED); registerObserver(Secure.ACCESSIBILITY_CAPTIONING_FOREGROUND_COLOR); registerObserver(Secure.ACCESSIBILITY_CAPTIONING_BACKGROUND_COLOR); + registerObserver(Secure.ACCESSIBILITY_CAPTIONING_WINDOW_COLOR); registerObserver(Secure.ACCESSIBILITY_CAPTIONING_EDGE_TYPE); registerObserver(Secure.ACCESSIBILITY_CAPTIONING_EDGE_COLOR); registerObserver(Secure.ACCESSIBILITY_CAPTIONING_TYPEFACE); registerObserver(Secure.ACCESSIBILITY_CAPTIONING_FONT_SCALE); registerObserver(Secure.ACCESSIBILITY_CAPTIONING_LOCALE); + registerObserver(Secure.ACCESSIBILITY_CAPTIONING_PRESET); } mListeners.add(listener); @@ -167,7 +174,7 @@ public class CaptioningManager { * * @param listener the listener to remove */ - public void removeCaptioningChangeListener(CaptioningChangeListener listener) { + public void removeCaptioningChangeListener(@NonNull CaptioningChangeListener listener) { synchronized (mListeners) { mListeners.remove(listener); @@ -253,11 +260,18 @@ public class CaptioningManager { /** Packed value for a color of 'none' and a cached opacity of 100%. */ private static final int COLOR_NONE_OPAQUE = 0x000000FF; + /** Packed value for an unspecified color and opacity. */ + private static final int COLOR_UNSPECIFIED = 0x000001FF; + private static final CaptionStyle WHITE_ON_BLACK; private static final CaptionStyle BLACK_ON_WHITE; private static final CaptionStyle YELLOW_ON_BLACK; private static final CaptionStyle YELLOW_ON_BLUE; private static final CaptionStyle DEFAULT_CUSTOM; + private static final CaptionStyle UNSPECIFIED; + + /** The default caption style used to fill in unspecified values. @hide */ + public static final CaptionStyle DEFAULT; /** @hide */ public static final CaptionStyle[] PRESETS; @@ -265,6 +279,9 @@ public class CaptioningManager { /** @hide */ public static final int PRESET_CUSTOM = -1; + /** Unspecified edge type value. */ + public static final int EDGE_TYPE_UNSPECIFIED = -1; + /** Edge type value specifying no character edges. */ public static final int EDGE_TYPE_NONE = 0; @@ -289,6 +306,7 @@ public class CaptioningManager { /** * The preferred edge type for video captions, one of: * <ul> + * <li>{@link #EDGE_TYPE_UNSPECIFIED} * <li>{@link #EDGE_TYPE_NONE} * <li>{@link #EDGE_TYPE_OUTLINE} * <li>{@link #EDGE_TYPE_DROP_SHADOW} @@ -326,9 +344,81 @@ public class CaptioningManager { } /** + * Applies a caption style, overriding any properties that are specified + * in the overlay caption. + * + * @param overlay The style to apply + * @return A caption style with the overlay style applied + * @hide + */ + @NonNull + public CaptionStyle applyStyle(@NonNull CaptionStyle overlay) { + final int newForegroundColor = overlay.hasForegroundColor() ? + overlay.foregroundColor : foregroundColor; + final int newBackgroundColor = overlay.hasBackgroundColor() ? + overlay.backgroundColor : backgroundColor; + final int newEdgeType = overlay.hasEdgeType() ? + overlay.edgeType : edgeType; + final int newEdgeColor = overlay.hasEdgeColor() ? + overlay.edgeColor : edgeColor; + final int newWindowColor = overlay.hasWindowColor() ? + overlay.windowColor : windowColor; + final String newRawTypeface = overlay.mRawTypeface != null ? + overlay.mRawTypeface : mRawTypeface; + return new CaptionStyle(newForegroundColor, newBackgroundColor, newEdgeType, + newEdgeColor, newWindowColor, newRawTypeface); + } + + /** + * @return {@code true} if the user has specified a background color + * that should override the application default, {@code false} + * otherwise + */ + public boolean hasBackgroundColor() { + return backgroundColor != COLOR_UNSPECIFIED; + } + + /** + * @return {@code true} if the user has specified a foreground color + * that should override the application default, {@code false} + * otherwise + */ + public boolean hasForegroundColor() { + return foregroundColor != COLOR_UNSPECIFIED; + } + + /** + * @return {@code true} if the user has specified an edge type that + * should override the application default, {@code false} + * otherwise + */ + public boolean hasEdgeType() { + return edgeType != EDGE_TYPE_UNSPECIFIED; + } + + /** + * @return {@code true} if the user has specified an edge color that + * should override the application default, {@code false} + * otherwise + */ + public boolean hasEdgeColor() { + return edgeColor != COLOR_UNSPECIFIED; + } + + /** + * @return {@code true} if the user has specified a window color that + * should override the application default, {@code false} + * otherwise + */ + public boolean hasWindowColor() { + return windowColor != COLOR_UNSPECIFIED; + } + + /** * @return the preferred {@link Typeface} for video captions, or null if * not specified */ + @Nullable public Typeface getTypeface() { if (mParsedTypeface == null && !TextUtils.isEmpty(mRawTypeface)) { mParsedTypeface = Typeface.create(mRawTypeface, Typeface.NORMAL); @@ -339,6 +429,7 @@ public class CaptioningManager { /** * @hide */ + @NonNull public static CaptionStyle getCustomStyle(ContentResolver cr) { final CaptionStyle defStyle = CaptionStyle.DEFAULT_CUSTOM; final int foregroundColor = Secure.getInt( @@ -370,12 +461,17 @@ public class CaptioningManager { Color.BLACK, COLOR_NONE_OPAQUE, null); YELLOW_ON_BLUE = new CaptionStyle(Color.YELLOW, Color.BLUE, EDGE_TYPE_NONE, Color.BLACK, COLOR_NONE_OPAQUE, null); + UNSPECIFIED = new CaptionStyle(COLOR_UNSPECIFIED, COLOR_UNSPECIFIED, + EDGE_TYPE_UNSPECIFIED, COLOR_UNSPECIFIED, COLOR_UNSPECIFIED, null); + // The ordering of these cannot change since we store the index + // directly in preferences. PRESETS = new CaptionStyle[] { - WHITE_ON_BLACK, BLACK_ON_WHITE, YELLOW_ON_BLACK, YELLOW_ON_BLUE + WHITE_ON_BLACK, BLACK_ON_WHITE, YELLOW_ON_BLACK, YELLOW_ON_BLUE, UNSPECIFIED }; DEFAULT_CUSTOM = WHITE_ON_BLACK; + DEFAULT = WHITE_ON_BLACK; } } @@ -389,8 +485,7 @@ public class CaptioningManager { * * @param enabled the user's new preferred captioning enabled state */ - public void onEnabledChanged(boolean enabled) { - } + public void onEnabledChanged(boolean enabled) {} /** * Called when the captioning user style changes. @@ -398,17 +493,15 @@ public class CaptioningManager { * @param userStyle the user's new preferred style * @see CaptioningManager#getUserStyle() */ - public void onUserStyleChanged(CaptionStyle userStyle) { - } + public void onUserStyleChanged(@NonNull CaptionStyle userStyle) {} /** * Called when the captioning locale changes. * - * @param locale the preferred captioning locale + * @param locale the preferred captioning locale, or {@code null} if not specified * @see CaptioningManager#getLocale() */ - public void onLocaleChanged(Locale locale) { - } + public void onLocaleChanged(@Nullable Locale locale) {} /** * Called when the captioning font scaling factor changes. @@ -416,7 +509,6 @@ public class CaptioningManager { * @param fontScale the preferred font scaling factor * @see CaptioningManager#getFontScale() */ - public void onFontScaleChanged(float fontScale) { - } + public void onFontScaleChanged(float fontScale) {} } } diff --git a/core/java/com/android/internal/widget/SubtitleView.java b/core/java/com/android/internal/widget/SubtitleView.java index 117463ab1ae5..2f987e98888a 100644 --- a/core/java/com/android/internal/widget/SubtitleView.java +++ b/core/java/com/android/internal/widget/SubtitleView.java @@ -271,10 +271,13 @@ public class SubtitleView extends View { style = CaptionStyle.PRESETS[styleId]; } - mForegroundColor = style.foregroundColor; - mBackgroundColor = style.backgroundColor; - mEdgeType = style.edgeType; - mEdgeColor = style.edgeColor; + final CaptionStyle defStyle = CaptionStyle.DEFAULT; + mForegroundColor = style.hasForegroundColor() ? + style.foregroundColor : defStyle.foregroundColor; + mBackgroundColor = style.hasBackgroundColor() ? + style.backgroundColor : defStyle.backgroundColor; + mEdgeType = style.hasEdgeType() ? style.edgeType : defStyle.edgeType; + mEdgeColor = style.hasEdgeColor() ? style.edgeColor : defStyle.edgeColor; mHasMeasurements = false; final Typeface typeface = style.getTypeface(); diff --git a/core/jni/android/graphics/Canvas.cpp b/core/jni/android/graphics/Canvas.cpp index ec935cc215ad..8e56eecb080f 100644 --- a/core/jni/android/graphics/Canvas.cpp +++ b/core/jni/android/graphics/Canvas.cpp @@ -879,8 +879,8 @@ public: #ifdef USE_MINIKIN Layout layout; - MinikinUtils::SetLayoutProperties(&layout, paint, flags, typeface); - layout.doLayout(textArray + start, count); + std::string css = MinikinUtils::setLayoutProperties(&layout, paint, flags, typeface); + layout.doLayout(textArray, start, count, contextCount, css); drawGlyphsToSkia(canvas, paint, layout, x, y); #else sp<TextLayoutValue> value = TextLayoutEngine::getInstance().getValue(paint, diff --git a/core/jni/android/graphics/MinikinSkia.cpp b/core/jni/android/graphics/MinikinSkia.cpp index 243fa10df647..2b96f1b66104 100644 --- a/core/jni/android/graphics/MinikinSkia.cpp +++ b/core/jni/android/graphics/MinikinSkia.cpp @@ -46,8 +46,10 @@ bool MinikinFontSkia::GetGlyph(uint32_t codepoint, uint32_t *glyph) const { static void MinikinFontSkia_SetSkiaPaint(SkTypeface* typeface, SkPaint* skPaint, const MinikinPaint& paint) { skPaint->setTypeface(typeface); skPaint->setTextEncoding(SkPaint::kGlyphID_TextEncoding); - // TODO: set more paint parameters from Minikin skPaint->setTextSize(paint.size); + skPaint->setTextScaleX(paint.scaleX); + skPaint->setTextSkewX(paint.skewX); + MinikinFontSkia::unpackPaintFlags(skPaint, paint.paintFlags); } float MinikinFontSkia::GetHorizontalAdvance(uint32_t glyph_id, @@ -96,4 +98,21 @@ int32_t MinikinFontSkia::GetUniqueId() const { return mTypeface->uniqueID(); } +uint32_t MinikinFontSkia::packPaintFlags(const SkPaint* paint) { + uint32_t flags = paint->getFlags(); + SkPaint::Hinting hinting = paint->getHinting(); + // select only flags that might affect text layout + flags &= (SkPaint::kAntiAlias_Flag | SkPaint::kFakeBoldText_Flag | SkPaint::kLinearText_Flag | + SkPaint::kSubpixelText_Flag | SkPaint::kDevKernText_Flag | + SkPaint::kEmbeddedBitmapText_Flag | SkPaint::kAutoHinting_Flag | + SkPaint::kVerticalText_Flag); + flags |= (hinting << 16); + return flags; +} + +void MinikinFontSkia::unpackPaintFlags(SkPaint* paint, uint32_t paintFlags) { + paint->setFlags(paintFlags & SkPaint::kAllFlags); + paint->setHinting(static_cast<SkPaint::Hinting>(paintFlags >> 16)); +} + } diff --git a/core/jni/android/graphics/MinikinSkia.h b/core/jni/android/graphics/MinikinSkia.h index 1cc2c51b3ae4..0452c57ed2fd 100644 --- a/core/jni/android/graphics/MinikinSkia.h +++ b/core/jni/android/graphics/MinikinSkia.h @@ -38,6 +38,8 @@ public: SkTypeface *GetSkTypeface(); + static uint32_t packPaintFlags(const SkPaint* paint); + static void unpackPaintFlags(SkPaint* paint, uint32_t paintFlags); private: SkTypeface *mTypeface; }; diff --git a/core/jni/android/graphics/MinikinUtils.cpp b/core/jni/android/graphics/MinikinUtils.cpp index a88b747772e5..a9360ea1ecbb 100644 --- a/core/jni/android/graphics/MinikinUtils.cpp +++ b/core/jni/android/graphics/MinikinUtils.cpp @@ -14,6 +14,10 @@ * limitations under the License. */ +#define LOG_TAG "Minikin" +#include <cutils/log.h> +#include <string> + #include "SkPaint.h" #include "minikin/Layout.h" #include "TypefaceImpl.h" @@ -23,24 +27,39 @@ namespace android { -void MinikinUtils::SetLayoutProperties(Layout* layout, const SkPaint* paint, int flags, - TypefaceImpl* typeface) { +// Do an sprintf starting at offset n, abort on overflow +static int snprintfcat(char* buf, int off, int size, const char* format, ...) { + va_list args; + va_start(args, format); + int n = vsnprintf(buf + off, size - off, format, args); + LOG_ALWAYS_FATAL_IF(n >= size - off, "String overflow in setting layout properties"); + va_end(args); + return off + n; +} + +std::string MinikinUtils::setLayoutProperties(Layout* layout, const SkPaint* paint, int bidiFlags, + TypefaceImpl* typeface) { TypefaceImpl* resolvedFace = TypefaceImpl_resolveDefault(typeface); layout->setFontCollection(resolvedFace->fFontCollection); FontStyle style = resolvedFace->fStyle; char css[256]; - int off = snprintf(css, sizeof(css), - "font-size: %d; font-weight: %d; font-style: %s; -minikin-bidi: %d;", + int off = snprintfcat(css, 0, sizeof(css), + "font-size: %d; font-scale-x: %f; font-skew-x: %f; -paint-flags: %d;" + " font-weight: %d; font-style: %s; -minikin-bidi: %d;", (int)paint->getTextSize(), + paint->getTextScaleX(), + paint->getTextSkewX(), + MinikinFontSkia::packPaintFlags(paint), style.getWeight() * 100, style.getItalic() ? "italic" : "normal", - flags); + bidiFlags); SkString langString = paint->getPaintOptionsAndroid().getLanguage().getTag(); - off += snprintf(css + off, sizeof(css) - off, " lang: %s;", langString.c_str()); + off = snprintfcat(css, off, sizeof(css), " lang: %s;", langString.c_str()); SkPaintOptionsAndroid::FontVariant var = paint->getPaintOptionsAndroid().getFontVariant(); const char* varstr = var == SkPaintOptionsAndroid::kElegant_Variant ? "elegant" : "compact"; - off += snprintf(css + off, sizeof(css) - off, " -minikin-variant: %s;", varstr); + off = snprintfcat(css, off, sizeof(css), " -minikin-variant: %s;", varstr); layout->setProperties(css); + return std::string(css); } float MinikinUtils::xOffsetForTextAlign(SkPaint* paint, const Layout& layout) { diff --git a/core/jni/android/graphics/MinikinUtils.h b/core/jni/android/graphics/MinikinUtils.h index 997d6e30b35e..ea7eb5d69e7c 100644 --- a/core/jni/android/graphics/MinikinUtils.h +++ b/core/jni/android/graphics/MinikinUtils.h @@ -26,10 +26,14 @@ namespace android { +class Layout; +class TypefaceImpl; + class MinikinUtils { public: - static void SetLayoutProperties(Layout* layout, const SkPaint* paint, int flags, - TypefaceImpl* face); + static std::string setLayoutProperties(Layout* layout, const SkPaint* paint, int bidiFlags, + TypefaceImpl* typeface); + static float xOffsetForTextAlign(SkPaint* paint, const Layout& layout); // f is a functor of type void f(SkTypeface *, size_t start, size_t end); diff --git a/core/jni/android/graphics/Paint.cpp b/core/jni/android/graphics/Paint.cpp index 4000b077519b..3dc874e77bea 100644 --- a/core/jni/android/graphics/Paint.cpp +++ b/core/jni/android/graphics/Paint.cpp @@ -520,8 +520,8 @@ public: #ifdef USE_MINIKIN Layout layout; TypefaceImpl* typeface = GraphicsJNI::getNativeTypeface(env, jpaint); - MinikinUtils::SetLayoutProperties(&layout, paint, bidiFlags, typeface); - layout.doLayout(textArray + index, count); + std::string css = MinikinUtils::setLayoutProperties(&layout, paint, bidiFlags, typeface); + layout.doLayout(textArray, index, count, textLength, css); result = layout.getAdvance(); #else TextLayout::getTextRunAdvances(paint, textArray, index, count, textLength, @@ -554,8 +554,8 @@ public: #ifdef USE_MINIKIN Layout layout; TypefaceImpl* typeface = GraphicsJNI::getNativeTypeface(env, jpaint); - MinikinUtils::SetLayoutProperties(&layout, paint, bidiFlags, typeface); - layout.doLayout(textArray + start, count); + std::string css = MinikinUtils::setLayoutProperties(&layout, paint, bidiFlags, typeface); + layout.doLayout(textArray, start, count, textLength, css); width = layout.getAdvance(); #else TextLayout::getTextRunAdvances(paint, textArray, start, count, textLength, @@ -582,8 +582,8 @@ public: #ifdef USE_MINIKIN Layout layout; TypefaceImpl* typeface = GraphicsJNI::getNativeTypeface(env, jpaint); - MinikinUtils::SetLayoutProperties(&layout, paint, bidiFlags, typeface); - layout.doLayout(textArray, textLength); + std::string css = MinikinUtils::setLayoutProperties(&layout, paint, bidiFlags, typeface); + layout.doLayout(textArray, 0, textLength, textLength, css); width = layout.getAdvance(); #else TextLayout::getTextRunAdvances(paint, textArray, 0, textLength, textLength, @@ -617,8 +617,8 @@ public: #ifdef USE_MINIKIN Layout layout; - MinikinUtils::SetLayoutProperties(&layout, paint, bidiFlags, typeface); - layout.doLayout(text, count); + std::string css = MinikinUtils::setLayoutProperties(&layout, paint, bidiFlags, typeface); + layout.doLayout(text, 0, count, count, css); layout.getAdvances(widthsArray); #else TextLayout::getTextRunAdvances(paint, text, 0, count, count, @@ -715,8 +715,8 @@ public: #ifdef USE_MINIKIN Layout layout; - MinikinUtils::SetLayoutProperties(&layout, paint, flags, typeface); - layout.doLayout(text + start, count); + std::string css = MinikinUtils::setLayoutProperties(&layout, paint, flags, typeface); + layout.doLayout(text, start, count, contextCount, css); layout.getAdvances(advancesArray); totalAdvance = layout.getAdvance(); #else @@ -860,8 +860,8 @@ public: jint count, jint bidiFlags, jfloat x, jfloat y, SkPath* path) { #ifdef USE_MINIKIN Layout layout; - MinikinUtils::SetLayoutProperties(&layout, paint, bidiFlags, typeface); - layout.doLayout(text, count); + std::string css = MinikinUtils::setLayoutProperties(&layout, paint, bidiFlags, typeface); + layout.doLayout(text, 0, count, count, css); size_t nGlyphs = layout.nGlyphs(); uint16_t* glyphs = new uint16_t[nGlyphs]; SkPoint* pos = new SkPoint[nGlyphs]; @@ -992,8 +992,8 @@ public: #ifdef USE_MINIKIN Layout layout; - MinikinUtils::SetLayoutProperties(&layout, &paint, bidiFlags, typeface); - layout.doLayout(text, count); + std::string css = MinikinUtils::setLayoutProperties(&layout, &paint, bidiFlags, typeface); + layout.doLayout(text, 0, count, count, css); MinikinRect rect; layout.getBounds(&rect); r.fLeft = rect.mLeft; diff --git a/core/jni/android/graphics/TypefaceImpl.cpp b/core/jni/android/graphics/TypefaceImpl.cpp index 786d19c49486..27df7cf0e215 100644 --- a/core/jni/android/graphics/TypefaceImpl.cpp +++ b/core/jni/android/graphics/TypefaceImpl.cpp @@ -32,6 +32,7 @@ #include <minikin/FontCollection.h> #include <minikin/FontFamily.h> #include <minikin/Layout.h> +#include "SkPaint.h" #include "MinikinSkia.h" #endif diff --git a/core/jni/android_view_GLES20Canvas.cpp b/core/jni/android_view_GLES20Canvas.cpp index 820da17d88f9..a46ccd631ea6 100644 --- a/core/jni/android_view_GLES20Canvas.cpp +++ b/core/jni/android_view_GLES20Canvas.cpp @@ -702,8 +702,8 @@ static void renderText(OpenGLRenderer* renderer, const jchar* text, int count, jfloat x, jfloat y, int flags, SkPaint* paint, TypefaceImpl* typeface) { #ifdef USE_MINIKIN Layout layout; - MinikinUtils::SetLayoutProperties(&layout, paint, flags, typeface); - layout.doLayout(text, count); + std::string css = MinikinUtils::setLayoutProperties(&layout, paint, flags, typeface); + layout.doLayout(text, 0, count, count, css); x += xOffsetForTextAlign(paint, layout.getAdvance()); renderTextLayout(renderer, &layout, x, y, paint); #else @@ -746,8 +746,8 @@ static void renderTextRun(OpenGLRenderer* renderer, const jchar* text, int flags, SkPaint* paint, TypefaceImpl* typeface) { #ifdef USE_MINIKIN Layout layout; - MinikinUtils::SetLayoutProperties(&layout, paint, flags, typeface); - layout.doLayout(text + start, count); + std::string css = MinikinUtils::setLayoutProperties(&layout, paint, flags, typeface); + layout.doLayout(text, start, count, contextCount, css); x += xOffsetForTextAlign(paint, layout.getAdvance()); renderTextLayout(renderer, &layout, x, y, paint); #else diff --git a/core/res/res/layout/preference_category_quantum.xml b/core/res/res/layout/preference_category_quantum.xml index 032e09d8af35..9dd0d8625061 100644 --- a/core/res/res/layout/preference_category_quantum.xml +++ b/core/res/res/layout/preference_category_quantum.xml @@ -21,8 +21,7 @@ android:layout_height="wrap_content" android:layout_marginBottom="16dip" android:textAppearance="@style/TextAppearance.Quantum.Body2" - android:textColor="?android:attr/textColorSecondary" - android:textStyle="bold" + android:textColor="?android:attr/colorAccent" android:paddingStart="?attr/listPreferredItemPaddingStart" android:paddingEnd="?attr/listPreferredItemPaddingEnd" android:paddingTop="16dip" /> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 8af45db43124..865d92a8f37e 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -364,6 +364,9 @@ <!-- If this is true, the screen will come on when you unplug usb/power/whatever. --> <bool name="config_unplugTurnsOnScreen">false</bool> + <!-- Set this true only if the device has separate attention and notification lights. --> + <bool name="config_useAttentionLight">false</bool> + <!-- If this is true, the screen will fade off. --> <bool name="config_animateScreenLights">true</bool> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 6a032e2a12b6..61b6a0d92c76 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -1456,6 +1456,7 @@ <java-symbol type="array" name="config_defaultNotificationVibePattern" /> <java-symbol type="array" name="config_notificationFallbackVibePattern" /> <java-symbol type="array" name="config_onlySingleDcAllowed" /> + <java-symbol type="bool" name="config_useAttentionLight" /> <java-symbol type="bool" name="config_animateScreenLights" /> <java-symbol type="bool" name="config_automatic_brightness_available" /> <java-symbol type="bool" name="config_enableFusedLocationOverlay" /> diff --git a/data/fonts/Roboto-MediumItalic.ttf b/data/fonts/Roboto-MediumItalic.ttf Binary files differindex a30aa0cf5dbf..b82820554134 100644 --- a/data/fonts/Roboto-MediumItalic.ttf +++ b/data/fonts/Roboto-MediumItalic.ttf diff --git a/graphics/java/android/graphics/drawable/AnimationDrawable.java b/graphics/java/android/graphics/drawable/AnimationDrawable.java index 0ee253a2e460..16548d0801ef 100644 --- a/graphics/java/android/graphics/drawable/AnimationDrawable.java +++ b/graphics/java/android/graphics/drawable/AnimationDrawable.java @@ -94,7 +94,7 @@ public class AnimationDrawable extends DrawableContainer implements Runnable, An boolean changed = super.setVisible(visible, restart); if (visible) { if (changed || restart) { - setFrame(0, true, mCurFrame >= 0); + setFrame(0, true, restart || mCurFrame >= 0); } } else { unscheduleSelf(this); diff --git a/graphics/java/android/graphics/drawable/GradientDrawable.java b/graphics/java/android/graphics/drawable/GradientDrawable.java index 1512da5b3ff8..241b89e2d6b8 100644 --- a/graphics/java/android/graphics/drawable/GradientDrawable.java +++ b/graphics/java/android/graphics/drawable/GradientDrawable.java @@ -1203,8 +1203,11 @@ public class GradientDrawable extends Drawable { st.mAttrStroke = a.extractThemeAttrs(); + // We have an explicit stroke defined, so the default stroke width + // must be at least 0 or the current stroke width. + final int defaultStrokeWidth = Math.max(0, st.mStrokeWidth); final int width = a.getDimensionPixelSize( - R.styleable.GradientDrawableStroke_width, st.mStrokeWidth); + R.styleable.GradientDrawableStroke_width, defaultStrokeWidth); final float dashWidth = a.getDimension( R.styleable.GradientDrawableStroke_dashWidth, st.mStrokeDashWidth); @@ -1406,10 +1409,13 @@ public class GradientDrawable extends Drawable { outline.setOval(bounds); return true; case LINE: - float halfStrokeWidth = mStrokePaint.getStrokeWidth() * 0.5f; - float centerY = bounds.centerY(); - int top = (int) Math.floor(centerY - halfStrokeWidth); - int bottom = (int) Math.ceil(centerY + halfStrokeWidth); + // Hairlines (0-width stroke) must have a non-empty outline for + // shadows to draw correctly, so we'll use a very small width. + final float halfStrokeWidth = mStrokePaint == null ? + 0.0001f : mStrokePaint.getStrokeWidth() * 0.5f; + final float centerY = bounds.centerY(); + final int top = (int) Math.floor(centerY - halfStrokeWidth); + final int bottom = (int) Math.ceil(centerY + halfStrokeWidth); outline.setRect(bounds.left, top, bounds.right, bottom); return true; diff --git a/libs/hwui/DisplayListRenderer.h b/libs/hwui/DisplayListRenderer.h index f0ae00fe855a..dff4f6c54d7e 100644 --- a/libs/hwui/DisplayListRenderer.h +++ b/libs/hwui/DisplayListRenderer.h @@ -153,6 +153,11 @@ public: // TODO: rename for consistency virtual status_t callDrawGLFunction(Functor* functor, Rect& dirty); +protected: + // NOTE: must override these to avoid calling into super class, which calls GL. These may be + // removed once DisplayListRenderer no longer inherits from OpenGLRenderer + virtual void onViewportInitialized() {}; + virtual void onSnapshotRestored() {}; private: void insertRestoreToCount(); diff --git a/media/java/android/media/WebVttRenderer.java b/media/java/android/media/WebVttRenderer.java index 1c9730fbd943..7977988626f0 100644 --- a/media/java/android/media/WebVttRenderer.java +++ b/media/java/android/media/WebVttRenderer.java @@ -1103,6 +1103,9 @@ class WebVttTrack extends SubtitleTrack implements WebVttCueListener { */ class WebVttRenderingWidget extends ViewGroup implements SubtitleTrack.RenderingWidget { private static final boolean DEBUG = false; + + private static final CaptionStyle DEFAULT_CAPTION_STYLE = CaptionStyle.DEFAULT; + private static final int DEBUG_REGION_BACKGROUND = 0x800000FF; private static final int DEBUG_CUE_BACKGROUND = 0x80FF0000; @@ -1144,7 +1147,8 @@ class WebVttRenderingWidget extends ViewGroup implements SubtitleTrack.Rendering this(context, attrs, defStyleAttr, 0); } - public WebVttRenderingWidget(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + public WebVttRenderingWidget( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); // Cannot render text over video when layer type is hardware. @@ -1259,6 +1263,7 @@ class WebVttRenderingWidget extends ViewGroup implements SubtitleTrack.Rendering } private void setCaptionStyle(CaptionStyle captionStyle, float fontSize) { + captionStyle = DEFAULT_CAPTION_STYLE.applyStyle(captionStyle); mCaptionStyle = captionStyle; mFontSize = fontSize; diff --git a/media/java/android/media/tv/TvContract.java b/media/java/android/media/tv/TvContract.java index 6e0586ee79f7..5e650c2373f6 100644 --- a/media/java/android/media/tv/TvContract.java +++ b/media/java/android/media/tv/TvContract.java @@ -86,6 +86,27 @@ public final class TvContract { } /** + * Builds a URI that points to a channel logo. See {@link Channels.Logo}. + * + * @param channelId The ID of the channel whose logo is pointed to. + */ + public static final Uri buildChannelLogoUri(long channelId) { + return buildChannelLogoUri(buildChannelUri(channelId)); + } + + /** + * Builds a URI that points to a channel logo. See {@link Channels.Logo}. + * + * @param channelUri The URI of the channel whose logo is pointed to. + */ + public static final Uri buildChannelLogoUri(Uri channelUri) { + if (!PATH_CHANNEL.equals(channelUri.getPathSegments().get(0))) { + throw new IllegalArgumentException("Not a channel: " + channelUri); + } + return Uri.withAppendedPath(channelUri, Channels.Logo.CONTENT_DIRECTORY); + } + + /** * Builds a URI that points to all browsable channels from a given TV input. * * @param name {@link ComponentName} of the {@link android.media.tv.TvInputService} that @@ -523,6 +544,48 @@ public final class TvContract { public static final String COLUMN_VERSION_NUMBER = "version_number"; private Channels() {} + + /** + * A sub-directory of a single TV channel that represents its primary logo. + * <p> + * To access this directory, append {@link Channels.Logo#CONTENT_DIRECTORY} to the raw + * channel URI. The resulting URI represents an image file, and should be interacted + * using ContentResolver.openAssetFileDescriptor. + * </p> + * <p> + * Note that this sub-directory also supports opening the logo as an asset file in write + * mode. Callers can create or replace the primary logo associated with this channel by + * opening the asset file and writing the full-size photo contents into it. When the file + * is closed, the image will be parsed, sized down if necessary, and stored. + * </p> + * <p> + * Usage example: + * <pre> + * public void writeChannelLogo(long channelId, byte[] logo) { + * Uri channelLogoUri = TvContract.buildChannelLogoUri(channelId); + * try { + * AssetFileDescriptor fd = + * getContentResolver().openAssetFileDescriptor(channelLogoUri, "rw"); + * OutputStream os = fd.createOutputStream(); + * os.write(logo); + * os.close(); + * fd.close(); + * } catch (IOException e) { + * // Handle error cases. + * } + * } + * </pre> + * </p> + */ + public static final class Logo { + + /** + * The directory twig for this sub-table. + */ + public static final String CONTENT_DIRECTORY = "logo"; + + private Logo() {} + } } /** Column definitions for the TV programs table. */ @@ -631,6 +694,26 @@ public final class TvContract { public static final String COLUMN_AUDIO_LANGUAGE = "audio_language"; /** + * The URI for the poster art of this TV program. + * <p> + * Can be empty. + * </p><p> + * Type: TEXT + * </p> + */ + public static final String COLUMN_POSTER_ART_URI = "poster_art_uri"; + + /** + * The URI for the thumbnail of this TV program. + * <p> + * Can be empty. + * </p><p> + * Type: TEXT + * </p> + */ + public static final String COLUMN_THUMBNAIL_URI = "thumbnail_uri"; + + /** * Internal data used by individual TV input services. * <p> * This is internal to the provider that inserted it, and should not be decoded by other diff --git a/packages/SystemUI/src/com/android/systemui/recent/Recents.java b/packages/SystemUI/src/com/android/systemui/recent/Recents.java index 0cc09c8bb371..116d755da7c8 100644 --- a/packages/SystemUI/src/com/android/systemui/recent/Recents.java +++ b/packages/SystemUI/src/com/android/systemui/recent/Recents.java @@ -61,6 +61,11 @@ public class Recents extends SystemUI implements RecentsComponent { @Override protected void onBootCompleted() { + if (mUseAlternateRecents) { + if (mAlternateRecents != null) { + mAlternateRecents.onBootCompleted(); + } + } mBootCompleted = true; } diff --git a/packages/SystemUI/src/com/android/systemui/recents/AlternateRecentsComponent.java b/packages/SystemUI/src/com/android/systemui/recents/AlternateRecentsComponent.java index bb19415499bf..2f6d58fecd6a 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/AlternateRecentsComponent.java +++ b/packages/SystemUI/src/com/android/systemui/recents/AlternateRecentsComponent.java @@ -153,6 +153,7 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta Messenger mService = null; Messenger mMessenger; RecentsMessageHandler mHandler; + boolean mBootCompleted = false; boolean mServiceIsBound = false; boolean mToggleRecentsUponServiceBound; RecentsServiceConnection mConnection = new RecentsServiceConnection(); @@ -182,6 +183,10 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta bindToRecentsService(false); } + public void onBootCompleted() { + mBootCompleted = true; + } + /** Shows the recents */ public void onShowRecents(boolean triggeredFromAltTab, View statusBarView) { if (Console.Enabled) { @@ -208,7 +213,7 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta if (Console.Enabled) { Console.log(Constants.Log.App.RecentsComponent, "[RecentsComponent|hideRecents]"); } - if (mServiceIsBound) { + if (mServiceIsBound && mBootCompleted) { // Notify recents to close it try { Bundle data = new Bundle(); @@ -278,7 +283,7 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta /** Updates each of the task animation rects. */ void updateAnimationRects() { - if (mServiceIsBound) { + if (mServiceIsBound && mBootCompleted) { Resources res = mContext.getResources(); int statusBarHeight = res.getDimensionPixelSize( com.android.internal.R.dimen.status_bar_height); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java index dcd187cebe9c..f013d13f7024 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java @@ -33,11 +33,13 @@ import android.graphics.Shader; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; +import android.view.ViewAnimationUtils; import android.view.ViewConfiguration; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import android.view.animation.PathInterpolator; + import com.android.systemui.R; import com.android.systemui.statusbar.stack.StackStateAnimator; @@ -219,7 +221,8 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView int heightHalf = mBackgroundNormal.getActualHeight()/2; float radius = (float) Math.sqrt(widthHalf*widthHalf + heightHalf*heightHalf); ValueAnimator animator = - mBackgroundNormal.createRevealAnimator(widthHalf, heightHalf, 0, radius); + ViewAnimationUtils.createCircularReveal(mBackgroundNormal, + widthHalf, heightHalf, 0, radius); mBackgroundNormal.setVisibility(View.VISIBLE); Interpolator interpolator; Interpolator alphaInterpolator; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java index 7926d03fd259..34179cb5b971 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java @@ -48,7 +48,6 @@ public class NotificationPanelView extends PanelView implements View.OnClickListener, NotificationStackScrollLayout.OnOverscrollTopChangedListener, KeyguardPageSwipeHelper.Callback { - private static final float EXPANSION_RUBBER_BAND_EXTRA_FACTOR = 0.6f; private static final float LOCK_ICON_ACTIVE_SCALE = 1.2f; private KeyguardPageSwipeHelper mPageSwiper; @@ -719,6 +718,16 @@ public class NotificationPanelView extends PanelView implements updateUnlockIcon(); } + @Override + protected float getOverExpansionAmount() { + return mNotificationStackScroller.getCurrentOverScrollAmount(true /* top */); + } + + @Override + protected float getOverExpansionPixels() { + return mNotificationStackScroller.getCurrentOverScrolledPixels(true /* top */); + } + private void updateUnlockIcon() { if (mStatusBar.getBarState() == StatusBarState.KEYGUARD || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) { @@ -805,14 +814,17 @@ public class NotificationPanelView extends PanelView implements } @Override - protected void onOverExpansionChanged(float overExpansion) { + protected void setOverExpansion(float overExpansion, boolean isPixels) { if (mStatusBar.getBarState() != StatusBarState.KEYGUARD) { - float currentOverScroll = mNotificationStackScroller.getCurrentOverScrolledPixels(true); - float expansionChange = overExpansion - mOverExpansion; - expansionChange *= EXPANSION_RUBBER_BAND_EXTRA_FACTOR; - mNotificationStackScroller.setOverScrolledPixels(currentOverScroll + expansionChange, - true /* onTop */, - false /* animate */); + mNotificationStackScroller.setOnHeightChangedListener(null); + if (isPixels) { + mNotificationStackScroller.setOverScrolledPixels( + overExpansion, true /* onTop */, false /* animate */); + } else { + mNotificationStackScroller.setOverScrollAmount( + overExpansion, true /* onTop */, false /* animate */); + } + mNotificationStackScroller.setOnHeightChangedListener(this); } } @@ -828,7 +840,10 @@ public class NotificationPanelView extends PanelView implements @Override protected void onTrackingStopped(boolean expand) { super.onTrackingStopped(expand); - mNotificationStackScroller.setOverScrolledPixels(0.0f, true /* onTop */, true /* animate */); + if (expand) { + mNotificationStackScroller.setOverScrolledPixels( + 0.0f, true /* onTop */, true /* animate */); + } if (expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) { mPageSwiper.showAllIcons(true); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java index 89a19076b1e0..772d0e7e9a3b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java @@ -41,7 +41,6 @@ import java.io.PrintWriter; public abstract class PanelView extends FrameLayout { public static final boolean DEBUG = PanelBar.DEBUG; public static final String TAG = PanelView.class.getSimpleName(); - protected float mOverExpansion; private final void logf(String fmt, Object... args) { Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args)); @@ -61,6 +60,7 @@ public abstract class PanelView extends FrameLayout { private int mTrackingPointer; protected int mTouchSlop; protected boolean mHintAnimationRunning; + private boolean mOverExpandedBeforeFling; private ValueAnimator mHeightAnimator; private ObjectAnimator mPeekAnimator; @@ -370,13 +370,12 @@ public abstract class PanelView extends FrameLayout { protected void fling(float vel, boolean expand) { cancelPeek(); float target = expand ? getMaxPanelHeight() : 0.0f; - if (target == mExpandedHeight || mOverExpansion > 0) { + if (target == mExpandedHeight || getOverExpansionAmount() > 0f && expand) { onExpandingFinished(); - mExpandedHeight = target; - mOverExpansion = 0.0f; mBar.panelExpansionChanged(this, mExpandedFraction); return; } + mOverExpandedBeforeFling = getOverExpansionAmount() > 0f; ValueAnimator animator = createHeightAnimator(target); if (expand) { mFlingAnimationUtils.apply(animator, mExpandedHeight, target, vel, getHeight()); @@ -396,8 +395,8 @@ public abstract class PanelView extends FrameLayout { onExpandingFinished(); } }); - animator.start(); mHeightAnimator = animator; + animator.start(); } @Override @@ -433,7 +432,7 @@ public abstract class PanelView extends FrameLayout { public void setExpandedHeight(float height) { if (DEBUG) logf("setExpandedHeight(%.1f)", height); - setExpandedHeightInternal(height); + setExpandedHeightInternal(height + getOverExpansionPixels()); mBar.panelExpansionChanged(PanelView.this, mExpandedFraction); } @@ -451,32 +450,39 @@ public abstract class PanelView extends FrameLayout { // If the user isn't actively poking us, let's update the height if (!mTracking && mHeightAnimator == null && mExpandedHeight > 0 && currentMaxPanelHeight != mExpandedHeight) { - setExpandedHeightInternal(currentMaxPanelHeight); + setExpandedHeight(currentMaxPanelHeight); } } public void setExpandedHeightInternal(float h) { - float fh = getMaxPanelHeight(); - mExpandedHeight = Math.max(0, Math.min(fh, h)); - float overExpansion = h - fh; - overExpansion = Math.max(0, overExpansion); - if (overExpansion != mOverExpansion) { - onOverExpansionChanged(overExpansion); - mOverExpansion = overExpansion; - } - - if (DEBUG) { - logf("setExpansion: height=%.1f fh=%.1f tracking=%s", h, fh, mTracking ? "T" : "f"); + float fhWithoutOverExpansion = getMaxPanelHeight() - getOverExpansionAmount(); + if (mHeightAnimator == null) { + float overExpansionPixels = Math.max(0, h - fhWithoutOverExpansion); + if (getOverExpansionPixels() != overExpansionPixels && mTracking) { + setOverExpansion(overExpansionPixels, true /* isPixels */); + } + mExpandedHeight = Math.min(h, fhWithoutOverExpansion) + getOverExpansionAmount(); + } else { + mExpandedHeight = h; + if (mOverExpandedBeforeFling) { + setOverExpansion(Math.max(0, h - fhWithoutOverExpansion), false /* isPixels */); + } } onHeightUpdated(mExpandedHeight); - mExpandedFraction = Math.min(1f, (fh == 0) ? 0 : mExpandedHeight / fh); + mExpandedFraction = Math.min(1f, fhWithoutOverExpansion == 0 + ? 0 + : mExpandedHeight / fhWithoutOverExpansion); } - protected abstract void onOverExpansionChanged(float overExpansion); + protected abstract void setOverExpansion(float overExpansion, boolean isPixels); protected abstract void onHeightUpdated(float expandedHeight); + protected abstract float getOverExpansionAmount(); + + protected abstract float getOverExpansionPixels(); + /** * This returns the maximum height of the panel. Children should override this if their * desired height is not the full height. @@ -624,7 +630,8 @@ public abstract class PanelView extends FrameLayout { animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { - setExpandedHeight((Float) animation.getAnimatedValue()); + setExpandedHeightInternal((Float) animation.getAnimatedValue()); + mBar.panelExpansionChanged(PanelView.this, mExpandedFraction); } }); return animator; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java index e55de947c75d..227304c2e30b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java @@ -77,6 +77,7 @@ import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; +import android.view.ViewAnimationUtils; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.ViewPropertyAnimator; @@ -757,7 +758,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, @Override public ValueAnimator createRevealAnimator(View v, int centerX, int centerY, float startRadius, float endRadius) { - return v.createRevealAnimator(centerX, centerY, startRadius, endRadius); + return ViewAnimationUtils.createCircularReveal(v, centerX, centerY, + startRadius, endRadius); } }); final QSTileHost qsh = new QSTileHost(mContext, this, @@ -2851,7 +2853,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } private void updatePublicMode() { - setLockscreenPublicMode(mState == StatusBarState.KEYGUARD + setLockscreenPublicMode( + (mStatusBarKeyguardViewManager.isShowing() || + mStatusBarKeyguardViewManager.isOccluded()) && mStatusBarKeyguardViewManager.isSecure()); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index e3145a658785..09e4d940b2e1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -181,6 +181,10 @@ public class StatusBarKeyguardViewManager { reset(); } + public boolean isOccluded() { + return mOccluded; + } + /** * Hides the keyguard view */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java index 038370606985..4d862139b5e3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java @@ -52,6 +52,7 @@ public class NotificationStackScrollLayout extends ViewGroup private static final boolean DEBUG = false; private static final float RUBBER_BAND_FACTOR_NORMAL = 0.35f; private static final float RUBBER_BAND_FACTOR_AFTER_EXPAND = 0.15f; + private static final float RUBBER_BAND_FACTOR_ON_PANEL_EXPAND = 0.21f; /** * Sentinel value for no current active pointer. Used by {@link #mActivePointerId}. @@ -1267,11 +1268,14 @@ public class NotificationStackScrollLayout extends ViewGroup } private float getRubberBandFactor() { - return mExpandedInThisMotion - ? RUBBER_BAND_FACTOR_AFTER_EXPAND - : (mScrolledToTopOnFirstDown - ? 1.0f - : RUBBER_BAND_FACTOR_NORMAL); + if (mExpandedInThisMotion) { + return RUBBER_BAND_FACTOR_AFTER_EXPAND; + } else if (mIsExpansionChanging) { + return RUBBER_BAND_FACTOR_ON_PANEL_EXPAND; + } else if (mScrolledToTopOnFirstDown) { + return 1.0f; + } + return RUBBER_BAND_FACTOR_NORMAL; } private void endDrag() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java index 2edd7d1143e0..225398a4c939 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java @@ -726,11 +726,21 @@ public class StackStateAnimator { @Override public void onAnimationUpdate(ValueAnimator animation) { float currentOverScroll = (float) animation.getAnimatedValue(); - mHostLayout.setOverScrollAmount(currentOverScroll, onTop, false /* animate */, - false /* cancelAnimators */); + mHostLayout.setOverScrollAmount( + currentOverScroll, onTop, false /* animate */, false /* cancelAnimators */); } }); overScrollAnimator.setInterpolator(mFastOutSlowInInterpolator); + overScrollAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (onTop) { + mTopOverScrollAnimator = null; + } else { + mBottomOverScrollAnimator = null; + } + } + }); overScrollAnimator.start(); if (onTop) { mTopOverScrollAnimator = overScrollAnimator; diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 495db20ba7e3..386402b83ed9 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -163,6 +163,7 @@ public class NotificationManagerService extends SystemService { private long[] mDefaultVibrationPattern; private long[] mFallbackVibrationPattern; + private boolean mUseAttentionLight; boolean mSystemReady; private boolean mDisableNotificationAlerts; @@ -182,7 +183,7 @@ public class NotificationManagerService extends SystemService { new ArrayMap<String, NotificationRecord>(); final ArrayList<ToastRecord> mToastQueue = new ArrayList<ToastRecord>(); - ArrayList<NotificationRecord> mLights = new ArrayList<NotificationRecord>(); + ArrayList<String> mLights = new ArrayList<String>(); NotificationRecord mLedNotification; private AppOpsManager mAppOps; @@ -797,6 +798,8 @@ public class NotificationManagerService extends SystemService { VIBRATE_PATTERN_MAXLEN, DEFAULT_VIBRATE_PATTERN); + mUseAttentionLight = resources.getBoolean(R.bool.config_useAttentionLight); + // Don't start allowing notifications until the setup wizard has run once. // After that, including subsequent boots, init with notifications turned on. // This works on the first boot because the setup wizard will toggle this @@ -1478,14 +1481,14 @@ public class NotificationManagerService extends SystemService { } } - // 1. initial score: buckets of 10, around the app - int score = notification.priority * NOTIFICATION_PRIORITY_MULTIPLIER; //[-20..20] + // 1. initial score: buckets of 10, around the app [-20..20] + final int score = notification.priority * NOTIFICATION_PRIORITY_MULTIPLIER; // 2. extract ranking signals from the notification data final StatusBarNotification n = new StatusBarNotification( pkg, opPkg, id, tag, callingUid, callingPid, score, notification, user); - NotificationRecord r = new NotificationRecord(n); + NotificationRecord r = new NotificationRecord(n, score); NotificationRecord old = mNotificationsByKey.get(n.getKey()); if (old != null) { // Retain ranking information from previous record @@ -1507,13 +1510,13 @@ public class NotificationManagerService extends SystemService { // blocked apps if (ENABLE_BLOCKED_NOTIFICATIONS && !noteNotificationOp(pkg, callingUid)) { if (!isSystemNotification) { - score = JUNK_SCORE; + r.score = JUNK_SCORE; Slog.e(TAG, "Suppressing notification from package " + pkg + " by user request."); } } - if (score < SCORE_DISPLAY_THRESHOLD) { + if (r.score < SCORE_DISPLAY_THRESHOLD) { // Notification will be blocked because the score is too low. return; } @@ -1529,28 +1532,16 @@ public class NotificationManagerService extends SystemService { mUsageStats.registerUpdatedByApp(r, old); // Make sure we don't lose the foreground service state. notification.flags |= - old.getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE; + old.getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE; mNotificationsByKey.remove(old.sbn.getKey()); + r.isUpdate = true; } mNotificationsByKey.put(n.getKey(), r); applyZenModeLocked(r); - // Should this notification make noise, vibe, or use the LED? - final boolean canInterrupt = (score >= SCORE_INTERRUPTION_THRESHOLD) && - !r.isIntercepted(); - if (DBG || r.isIntercepted()) Slog.v(TAG, - "pkg=" + pkg + " canInterrupt=" + canInterrupt + - " intercept=" + r.isIntercepted()); Collections.sort(mNotificationList, mRankingComparator); - // Ensure if this is a foreground service that the proper additional - // flags are set. - if ((notification.flags&Notification.FLAG_FOREGROUND_SERVICE) != 0) { - notification.flags |= Notification.FLAG_ONGOING_EVENT - | Notification.FLAG_NO_CLEAR; - } - final int currentUser; final long token = Binder.clearCallingIdentity(); try { @@ -1571,20 +1562,11 @@ public class NotificationManagerService extends SystemService { final long identity = Binder.clearCallingIdentity(); try { mStatusBar.addNotification(n); - if ((n.getNotification().flags & Notification.FLAG_SHOW_LIGHTS) != 0 - && canInterrupt) { - mAttentionLight.pulse(); - } } finally { Binder.restoreCallingIdentity(identity); } } - // Send accessibility events only for the current user. - if (currentUser == userId) { - sendAccessibilityEvent(notification, pkg); - } - - mListeners.notifyPostedLocked(r.sbn); + mListeners.notifyPostedLocked(n); } else { Slog.e(TAG, "Not posting notification with icon==0: " + notification); if (old != null && !old.isCanceled) { @@ -1595,7 +1577,7 @@ public class NotificationManagerService extends SystemService { Binder.restoreCallingIdentity(identity); } - mListeners.notifyRemovedLocked(r.sbn); + mListeners.notifyRemovedLocked(n); } // ATTENTION: in a future release we will bail out here // so that we do not play sounds, show lights, etc. for invalid @@ -1604,141 +1586,171 @@ public class NotificationManagerService extends SystemService { + n.getPackageName()); } - // If we're not supposed to beep, vibrate, etc. then don't. - if (!mDisableNotificationAlerts - && (!(old != null - && (notification.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0 )) - && (r.getUserId() == UserHandle.USER_ALL || - (r.getUserId() == userId && r.getUserId() == currentUser) || - mUserProfiles.isCurrentProfile(r.getUserId())) - && canInterrupt - && mSystemReady - && mAudioManager != null) { - if (DBG) Slog.v(TAG, "Interrupting!"); - // sound - - // should we use the default notification sound? (indicated either by - // DEFAULT_SOUND or because notification.sound is pointing at - // Settings.System.NOTIFICATION_SOUND) - final boolean useDefaultSound = - (notification.defaults & Notification.DEFAULT_SOUND) != 0 || - Settings.System.DEFAULT_NOTIFICATION_URI - .equals(notification.sound); - - Uri soundUri = null; - boolean hasValidSound = false; - - if (useDefaultSound) { - soundUri = Settings.System.DEFAULT_NOTIFICATION_URI; - - // check to see if the default notification sound is silent - ContentResolver resolver = getContext().getContentResolver(); - hasValidSound = Settings.System.getString(resolver, - Settings.System.NOTIFICATION_SOUND) != null; - } else if (notification.sound != null) { - soundUri = notification.sound; - hasValidSound = (soundUri != null); - } + // Ensure if this is a foreground service that the proper additional + // flags are set. + if ((notification.flags & Notification.FLAG_FOREGROUND_SERVICE) != 0) { + notification.flags |= Notification.FLAG_ONGOING_EVENT + | Notification.FLAG_NO_CLEAR; + } - if (hasValidSound) { - boolean looping = - (notification.flags & Notification.FLAG_INSISTENT) != 0; - int audioStreamType; - if (notification.audioStreamType >= 0) { - audioStreamType = notification.audioStreamType; - } else { - audioStreamType = DEFAULT_STREAM_TYPE; - } - mSoundNotification = r; - // do not play notifications if stream volume is 0 (typically because - // ringer mode is silent) or if there is a user of exclusive audio focus - if ((mAudioManager.getStreamVolume(audioStreamType) != 0) - && !mAudioManager.isAudioFocusExclusive()) { - final long identity = Binder.clearCallingIdentity(); - try { - final IRingtonePlayer player = - mAudioManager.getRingtonePlayer(); - if (player != null) { - if (DBG) Slog.v(TAG, "Playing sound " + soundUri - + " on stream " + audioStreamType); - player.playAsync(soundUri, user, looping, audioStreamType); - } - } catch (RemoteException e) { - } finally { - Binder.restoreCallingIdentity(identity); - } - } - } + buzzBeepBlinkLocked(r); + } + } + }); - // vibrate - // Does the notification want to specify its own vibration? - final boolean hasCustomVibrate = notification.vibrate != null; - - // new in 4.2: if there was supposed to be a sound and we're in vibrate - // mode, and no other vibration is specified, we fall back to vibration - final boolean convertSoundToVibration = - !hasCustomVibrate - && hasValidSound - && (mAudioManager.getRingerMode() - == AudioManager.RINGER_MODE_VIBRATE); - - // The DEFAULT_VIBRATE flag trumps any custom vibration AND the fallback. - final boolean useDefaultVibrate = - (notification.defaults & Notification.DEFAULT_VIBRATE) != 0; - - if ((useDefaultVibrate || convertSoundToVibration || hasCustomVibrate) - && !(mAudioManager.getRingerMode() - == AudioManager.RINGER_MODE_SILENT)) { - mVibrateNotification = r; - - if (useDefaultVibrate || convertSoundToVibration) { - // Escalate privileges so we can use the vibrator even if the - // notifying app does not have the VIBRATE permission. - long identity = Binder.clearCallingIdentity(); - try { - mVibrator.vibrate(r.sbn.getUid(), r.sbn.getOpPkg(), - useDefaultVibrate ? mDefaultVibrationPattern - : mFallbackVibrationPattern, - ((notification.flags & Notification.FLAG_INSISTENT) != 0) - ? 0: -1, notification.audioStreamType); - } finally { - Binder.restoreCallingIdentity(identity); - } - } else if (notification.vibrate.length > 1) { - // If you want your own vibration pattern, you need the VIBRATE - // permission - mVibrator.vibrate(r.sbn.getUid(), r.sbn.getOpPkg(), - notification.vibrate, - ((notification.flags & Notification.FLAG_INSISTENT) != 0) - ? 0: -1, notification.audioStreamType); - } + idOut[0] = id; + } + + private void buzzBeepBlinkLocked(NotificationRecord record) { + final Notification notification = record.sbn.getNotification(); + + // Should this notification make noise, vibe, or use the LED? + final boolean canInterrupt = (record.score >= SCORE_INTERRUPTION_THRESHOLD) && + !record.isIntercepted(); + if (DBG || record.isIntercepted()) + Slog.v(TAG, + "pkg=" + record.sbn.getPackageName() + " canInterrupt=" + canInterrupt + + " intercept=" + record.isIntercepted() + ); + + final int currentUser; + final long token = Binder.clearCallingIdentity(); + try { + currentUser = ActivityManager.getCurrentUser(); + } finally { + Binder.restoreCallingIdentity(token); + } + + // If we're not supposed to beep, vibrate, etc. then don't. + if (!mDisableNotificationAlerts + && (!(record.isUpdate + && (notification.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0 )) + && (record.getUserId() == UserHandle.USER_ALL || + record.getUserId() == currentUser || + mUserProfiles.isCurrentProfile(record.getUserId())) + && canInterrupt + && mSystemReady + && mAudioManager != null) { + if (DBG) Slog.v(TAG, "Interrupting!"); + + sendAccessibilityEvent(notification, record.sbn.getPackageName()); + + // sound + + // should we use the default notification sound? (indicated either by + // DEFAULT_SOUND or because notification.sound is pointing at + // Settings.System.NOTIFICATION_SOUND) + final boolean useDefaultSound = + (notification.defaults & Notification.DEFAULT_SOUND) != 0 || + Settings.System.DEFAULT_NOTIFICATION_URI + .equals(notification.sound); + + Uri soundUri = null; + boolean hasValidSound = false; + + if (useDefaultSound) { + soundUri = Settings.System.DEFAULT_NOTIFICATION_URI; + + // check to see if the default notification sound is silent + ContentResolver resolver = getContext().getContentResolver(); + hasValidSound = Settings.System.getString(resolver, + Settings.System.NOTIFICATION_SOUND) != null; + } else if (notification.sound != null) { + soundUri = notification.sound; + hasValidSound = (soundUri != null); + } + + if (hasValidSound) { + boolean looping = + (notification.flags & Notification.FLAG_INSISTENT) != 0; + int audioStreamType; + if (notification.audioStreamType >= 0) { + audioStreamType = notification.audioStreamType; + } else { + audioStreamType = DEFAULT_STREAM_TYPE; + } + mSoundNotification = record; + // do not play notifications if stream volume is 0 (typically because + // ringer mode is silent) or if there is a user of exclusive audio focus + if ((mAudioManager.getStreamVolume(audioStreamType) != 0) + && !mAudioManager.isAudioFocusExclusive()) { + final long identity = Binder.clearCallingIdentity(); + try { + final IRingtonePlayer player = + mAudioManager.getRingtonePlayer(); + if (player != null) { + if (DBG) Slog.v(TAG, "Playing sound " + soundUri + + " on stream " + audioStreamType); + player.playAsync(soundUri, record.sbn.getUser(), looping, + audioStreamType); } + } catch (RemoteException e) { + } finally { + Binder.restoreCallingIdentity(identity); } + } + } - // light - // the most recent thing gets the light - mLights.remove(old); - if (mLedNotification == old) { - mLedNotification = null; - } - //Slog.i(TAG, "notification.lights=" - // + ((old.notification.lights.flags & Notification.FLAG_SHOW_LIGHTS) - // != 0)); - if ((notification.flags & Notification.FLAG_SHOW_LIGHTS) != 0 - && canInterrupt) { - mLights.add(r); - updateLightsLocked(); - } else { - if (old != null - && ((old.getFlags() & Notification.FLAG_SHOW_LIGHTS) != 0)) { - updateLightsLocked(); - } + // vibrate + // Does the notification want to specify its own vibration? + final boolean hasCustomVibrate = notification.vibrate != null; + + // new in 4.2: if there was supposed to be a sound and we're in vibrate + // mode, and no other vibration is specified, we fall back to vibration + final boolean convertSoundToVibration = + !hasCustomVibrate + && hasValidSound + && (mAudioManager.getRingerMode() + == AudioManager.RINGER_MODE_VIBRATE); + + // The DEFAULT_VIBRATE flag trumps any custom vibration AND the fallback. + final boolean useDefaultVibrate = + (notification.defaults & Notification.DEFAULT_VIBRATE) != 0; + + if ((useDefaultVibrate || convertSoundToVibration || hasCustomVibrate) + && !(mAudioManager.getRingerMode() + == AudioManager.RINGER_MODE_SILENT)) { + mVibrateNotification = record; + + if (useDefaultVibrate || convertSoundToVibration) { + // Escalate privileges so we can use the vibrator even if the + // notifying app does not have the VIBRATE permission. + long identity = Binder.clearCallingIdentity(); + try { + mVibrator.vibrate(record.sbn.getUid(), record.sbn.getOpPkg(), + useDefaultVibrate ? mDefaultVibrationPattern + : mFallbackVibrationPattern, + ((notification.flags & Notification.FLAG_INSISTENT) != 0) + ? 0: -1, notification.audioStreamType); + } finally { + Binder.restoreCallingIdentity(identity); } + } else if (notification.vibrate.length > 1) { + // If you want your own vibration pattern, you need the VIBRATE + // permission + mVibrator.vibrate(record.sbn.getUid(), record.sbn.getOpPkg(), + notification.vibrate, + ((notification.flags & Notification.FLAG_INSISTENT) != 0) + ? 0: -1, notification.audioStreamType); } } - }); + } - idOut[0] = id; + // light + // release the light + boolean wasShowLights = mLights.remove(record.getKey()); + if (mLedNotification != null && record.getKey().equals(mLedNotification.getKey())) { + mLedNotification = null; + } + if ((notification.flags & Notification.FLAG_SHOW_LIGHTS) != 0 && canInterrupt) { + mLights.add(record.getKey()); + updateLightsLocked(); + if (mUseAttentionLight) { + mAttentionLight.pulse(); + } + } else if (wasShowLights) { + updateLightsLocked(); + } } void showNextToastLocked() { @@ -1866,6 +1878,9 @@ public class NotificationManagerService extends SystemService { int indexAfter = findNotificationRecordIndexLocked(record); boolean interceptAfter = record.isIntercepted(); changed = indexBefore != indexAfter || interceptBefore != interceptAfter; + if (interceptBefore && !interceptAfter) { + buzzBeepBlinkLocked(record); + } } if (changed) { scheduleSendRankingUpdate(); @@ -2011,7 +2026,7 @@ public class NotificationManagerService extends SystemService { } // light - mLights.remove(r); + mLights.remove(r.getKey()); if (mLedNotification == r) { mLedNotification = null; } @@ -2196,7 +2211,7 @@ public class NotificationManagerService extends SystemService { // get next notification, if any int n = mLights.size(); if (n > 0) { - mLedNotification = mLights.get(n-1); + mLedNotification = mNotificationsByKey.get(mLights.get(n-1)); } } diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index 08f8eb40b155..30d4fecdbd9d 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -42,6 +42,7 @@ public final class NotificationRecord { final StatusBarNotification sbn; NotificationUsageStats.SingleNotificationStats stats; boolean isCanceled; + int score; // These members are used by NotificationSignalExtractors // to communicate with the ranking module. @@ -53,9 +54,13 @@ public final class NotificationRecord { // InterceptedNotifications needs to know if this has been previously evaluated. private boolean mTouchedByZen; - NotificationRecord(StatusBarNotification sbn) + // Is this record an update of an old record? + public boolean isUpdate; + + NotificationRecord(StatusBarNotification sbn, int score) { this.sbn = sbn; + this.score = score; } // copy any notes that the ranking system may have made before the update diff --git a/services/core/java/com/android/server/task/TaskManagerService.java b/services/core/java/com/android/server/task/TaskManagerService.java index d5b70e675a73..a5f865fd95f5 100644 --- a/services/core/java/com/android/server/task/TaskManagerService.java +++ b/services/core/java/com/android/server/task/TaskManagerService.java @@ -36,6 +36,7 @@ import android.os.SystemClock; import android.util.Slog; import android.util.SparseArray; +import com.android.server.task.controllers.BatteryController; import com.android.server.task.controllers.ConnectivityController; import com.android.server.task.controllers.IdleController; import com.android.server.task.controllers.StateController; @@ -48,12 +49,19 @@ import java.util.LinkedList; * Responsible for taking tasks representing work to be performed by a client app, and determining * based on the criteria specified when that task should be run against the client application's * endpoint. + * Implements logic for scheduling, and rescheduling tasks. The TaskManagerService knows nothing + * about constraints, or the state of active tasks. It receives callbacks from the various + * controllers and completed tasks and operates accordingly. + * + * Note on locking: Any operations that manipulate {@link #mTasks} need to lock on that object, and + * similarly for {@link #mActiveServices}. If both locks need to be held take mTasksSet first and then + * mActiveService afterwards. * @hide */ public class TaskManagerService extends com.android.server.SystemService - implements StateChangedListener, TaskCompletedListener { + implements StateChangedListener, TaskCompletedListener, TaskMapReadFinishedListener { // TODO: Switch this off for final version. - private static final boolean DEBUG = true; + static final boolean DEBUG = true; /** The number of concurrent tasks we run at one time. */ private static final int MAX_TASK_CONTEXTS_COUNT = 3; static final String TAG = "TaskManager"; @@ -113,8 +121,8 @@ public class TaskManagerService extends com.android.server.SystemService */ public int schedule(Task task, int uId, boolean canPersistTask) { TaskStatus taskStatus = new TaskStatus(task, uId, canPersistTask); - return startTrackingTask(taskStatus) ? - TaskManager.RESULT_SUCCESS : TaskManager.RESULT_FAILURE; + startTrackingTask(taskStatus); + return TaskManager.RESULT_SUCCESS; } public List<Task> getPendingTasks(int uid) { @@ -210,7 +218,7 @@ public class TaskManagerService extends com.android.server.SystemService */ public TaskManagerService(Context context) { super(context); - mTasks = new TaskStore(context); + mTasks = TaskStore.initAndGet(this); mHandler = new TaskHandler(context.getMainLooper()); mTaskManagerStub = new TaskManagerStub(); // Create the "runners". @@ -218,12 +226,12 @@ public class TaskManagerService extends com.android.server.SystemService mActiveServices.add( new TaskServiceContext(this, context.getMainLooper())); } - + // Create the controllers. mControllers = new LinkedList<StateController>(); mControllers.add(ConnectivityController.get(this)); mControllers.add(TimeController.get(this)); mControllers.add(IdleController.get(this)); - // TODO: Add BatteryStateController when implemented. + mControllers.add(BatteryController.get(this)); } @Override @@ -236,17 +244,14 @@ public class TaskManagerService extends com.android.server.SystemService * {@link com.android.server.task.TaskStore}, and make sure all the relevant controllers know * about. */ - private boolean startTrackingTask(TaskStatus taskStatus) { - boolean added = false; + private void startTrackingTask(TaskStatus taskStatus) { synchronized (mTasks) { - added = mTasks.add(taskStatus); + mTasks.add(taskStatus); } - if (added) { - for (StateController controller : mControllers) { - controller.maybeStartTrackingTask(taskStatus); - } + for (StateController controller : mControllers) { + controller.maybeStartTrackingTask(taskStatus); + } - return added; } /** @@ -404,6 +409,27 @@ public class TaskManagerService extends com.android.server.SystemService mHandler.obtainMessage(MSG_TASK_EXPIRED, taskStatus); } + /** + * Disk I/O is finished, take the list of tasks we read from disk and add them to our + * {@link TaskStore}. + * This is run on the {@link com.android.server.IoThread} instance, which is a separate thread, + * and is called once at boot. + */ + @Override + public void onTaskMapReadFinished(List<TaskStatus> tasks) { + synchronized (mTasks) { + for (TaskStatus ts : tasks) { + if (mTasks.contains(ts)) { + // An app with BOOT_COMPLETED *might* have decided to reschedule their task, in + // the same amount of time it took us to read it from disk. If this is the case + // we leave it be. + continue; + } + startTrackingTask(ts); + } + } + } + private class TaskHandler extends Handler { public TaskHandler(Looper looper) { diff --git a/services/core/java/com/android/server/task/TaskMapReadFinishedListener.java b/services/core/java/com/android/server/task/TaskMapReadFinishedListener.java new file mode 100644 index 000000000000..c68d8db4da03 --- /dev/null +++ b/services/core/java/com/android/server/task/TaskMapReadFinishedListener.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.task; + +import java.util.List; + +import com.android.server.task.controllers.TaskStatus; + +/** + * Callback definition for I/O thread to let the TaskManagerService know when + * I/O read has completed. Done this way so we don't stall the main thread on + * boot. + */ +public interface TaskMapReadFinishedListener { + + /** + * Called by the {@link TaskStore} at boot, when the disk read is finished. + */ + public void onTaskMapReadFinished(List<TaskStatus> tasks); +} diff --git a/services/core/java/com/android/server/task/TaskStore.java b/services/core/java/com/android/server/task/TaskStore.java index f72ab22f546c..6bb00b1fac42 100644 --- a/services/core/java/com/android/server/task/TaskStore.java +++ b/services/core/java/com/android/server/task/TaskStore.java @@ -16,17 +16,37 @@ package com.android.server.task; +import android.content.ComponentName; import android.app.task.Task; import android.content.Context; +import android.os.Environment; +import android.os.Handler; +import android.os.PersistableBundle; +import android.os.SystemClock; +import android.util.AtomicFile; import android.util.ArraySet; +import android.util.Pair; import android.util.Slog; -import android.util.SparseArray; +import android.util.Xml; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.FastXmlSerializer; +import com.android.server.IoThread; import com.android.server.task.controllers.TaskStatus; -import java.util.HashSet; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; import java.util.Iterator; -import java.util.Set; +import java.util.List; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; /** * Maintain a list of classes, and accessor methods/logic for these tasks. @@ -38,57 +58,108 @@ import java.util.Set; * - Handles rescheduling of tasks. * - When a periodic task is executed and must be re-added. * - When a task fails and the client requests that it be retried with backoff. - * - This class is <strong>not</strong> thread-safe. + * - This class <strong>is not</strong> thread-safe. + * + * Note on locking: + * All callers to this class must <strong>lock on the class object they are calling</strong>. + * This is important b/c {@link com.android.server.task.TaskStore.WriteTasksMapToDiskRunnable} + * and {@link com.android.server.task.TaskStore.ReadTaskMapFromDiskRunnable} lock on that + * object. */ public class TaskStore { private static final String TAG = "TaskManagerStore"; + private static final boolean DEBUG = TaskManagerService.DEBUG; + /** Threshold to adjust how often we want to write to the db. */ private static final int MAX_OPS_BEFORE_WRITE = 1; - final ArraySet<TaskStatus> mTasks; + final ArraySet<TaskStatus> mTasksSet; final Context mContext; private int mDirtyOperations; - TaskStore(Context context) { - mTasks = intialiseTasksFromDisk(); + private static final Object sSingletonLock = new Object(); + private final AtomicFile mTasksFile; + /** Handler backed by IoThread for writing to disk. */ + private final Handler mIoHandler = IoThread.getHandler(); + private static TaskStore sSingleton; + + /** Used by the {@Link TaskManagerService} to instantiate the TaskStore. */ + static TaskStore initAndGet(TaskManagerService taskManagerService) { + synchronized (sSingletonLock) { + if (sSingleton == null) { + sSingleton = new TaskStore(taskManagerService.getContext(), + Environment.getDataDirectory(), taskManagerService); + } + return sSingleton; + } + } + + @VisibleForTesting + public static TaskStore initAndGetForTesting(Context context, File dataDir, + TaskMapReadFinishedListener callback) { + return new TaskStore(context, dataDir, callback); + } + + private TaskStore(Context context, File dataDir, TaskMapReadFinishedListener callback) { mContext = context; mDirtyOperations = 0; + + File systemDir = new File(dataDir, "system"); + File taskDir = new File(systemDir, "task"); + taskDir.mkdirs(); + mTasksFile = new AtomicFile(new File(taskDir, "tasks.xml")); + + mTasksSet = new ArraySet<TaskStatus>(); + + readTaskMapFromDiskAsync(callback); } /** * Add a task to the master list, persisting it if necessary. If the TaskStatus already exists, * it will be replaced. * @param taskStatus Task to add. - * @return true if the operation succeeded. + * @return Whether or not an equivalent TaskStatus was replaced by this operation. */ public boolean add(TaskStatus taskStatus) { + boolean replaced = mTasksSet.remove(taskStatus); + mTasksSet.add(taskStatus); if (taskStatus.isPersisted()) { - if (!maybeWriteStatusToDisk()) { - return false; - } + maybeWriteStatusToDiskAsync(); } - mTasks.remove(taskStatus); - mTasks.add(taskStatus); - return true; + return replaced; + } + + /** + * Whether this taskStatus object already exists in the TaskStore. + */ + public boolean contains(TaskStatus taskStatus) { + return mTasksSet.contains(taskStatus); } public int size() { - return mTasks.size(); + return mTasksSet.size(); } /** * Remove the provided task. Will also delete the task if it was persisted. - * @return The TaskStatus that was removed, or null if an invalid token was provided. + * @return Whether or not the task existed to be removed. */ public boolean remove(TaskStatus taskStatus) { - boolean removed = mTasks.remove(taskStatus); + boolean removed = mTasksSet.remove(taskStatus); if (!removed) { - Slog.e(TAG, "Error removing task: " + taskStatus); + if (DEBUG) { + Slog.d(TAG, "Couldn't remove task: didn't exist: " + taskStatus); + } return false; - } else { - maybeWriteStatusToDisk(); } - return true; + maybeWriteStatusToDiskAsync(); + return removed; + } + + @VisibleForTesting + public void clear() { + mTasksSet.clear(); + maybeWriteStatusToDiskAsync(); } /** @@ -100,19 +171,16 @@ public class TaskStore { * was found. */ public boolean removeAllByUid(int uid) { - Iterator<TaskStatus> it = mTasks.iterator(); - boolean removed = false; + Iterator<TaskStatus> it = mTasksSet.iterator(); while (it.hasNext()) { TaskStatus ts = it.next(); if (ts.getUid() == uid) { it.remove(); - removed = true; + maybeWriteStatusToDiskAsync(); + return true; } } - if (removed) { - maybeWriteStatusToDisk(); - } - return removed; + return false; } /** @@ -124,48 +192,464 @@ public class TaskStore { * @return true if a removal occurred, false if the provided parameters didn't match anything. */ public boolean remove(int uid, int taskId) { - Iterator<TaskStatus> it = mTasks.iterator(); + boolean changed = false; + Iterator<TaskStatus> it = mTasksSet.iterator(); while (it.hasNext()) { TaskStatus ts = it.next(); if (ts.getUid() == uid && ts.getTaskId() == taskId) { it.remove(); - maybeWriteStatusToDisk(); - return true; + changed = true; } } - return false; + if (changed) { + maybeWriteStatusToDiskAsync(); + } + return changed; } /** * @return The live array of TaskStatus objects. */ - public Set<TaskStatus> getTasks() { - return mTasks; + public ArraySet<TaskStatus> getTasks() { + return mTasksSet; } + /** Version of the db schema. */ + private static final int TASKS_FILE_VERSION = 0; + /** Tag corresponds to constraints this task needs. */ + private static final String XML_TAG_PARAMS_CONSTRAINTS = "constraints"; + /** Tag corresponds to execution parameters. */ + private static final String XML_TAG_PERIODIC = "periodic"; + private static final String XML_TAG_ONEOFF = "one-off"; + private static final String XML_TAG_EXTRAS = "extras"; + /** * Every time the state changes we write all the tasks in one swathe, instead of trying to * track incremental changes. * @return Whether the operation was successful. This will only fail for e.g. if the system is * low on storage. If this happens, we continue as normal */ - private boolean maybeWriteStatusToDisk() { + private void maybeWriteStatusToDiskAsync() { mDirtyOperations++; - if (mDirtyOperations > MAX_OPS_BEFORE_WRITE) { - for (TaskStatus ts : mTasks) { - // + if (mDirtyOperations >= MAX_OPS_BEFORE_WRITE) { + if (DEBUG) { + Slog.v(TAG, "Writing tasks to disk."); + } + mIoHandler.post(new WriteTasksMapToDiskRunnable()); + } + } + + private void readTaskMapFromDiskAsync(TaskMapReadFinishedListener callback) { + mIoHandler.post(new ReadTaskMapFromDiskRunnable(callback)); + } + + public void readTaskMapFromDisk(TaskMapReadFinishedListener callback) { + new ReadTaskMapFromDiskRunnable(callback).run(); + } + + /** + * Runnable that writes {@link #mTasksSet} out to xml. + * NOTE: This Runnable locks on TaskStore.this + */ + private class WriteTasksMapToDiskRunnable implements Runnable { + @Override + public void run() { + final long startElapsed = SystemClock.elapsedRealtime(); + synchronized (TaskStore.this) { + writeTasksMapImpl(); + } + if (TaskManagerService.DEBUG) { + Slog.v(TAG, "Finished writing, took " + (SystemClock.elapsedRealtime() + - startElapsed) + "ms"); + } + } + + private void writeTasksMapImpl() { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + XmlSerializer out = new FastXmlSerializer(); + out.setOutput(baos, "utf-8"); + out.startDocument(null, true); + out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); + + out.startTag(null, "task-info"); + out.attribute(null, "version", Integer.toString(TASKS_FILE_VERSION)); + for (int i = 0; i < mTasksSet.size(); i++) { + final TaskStatus taskStatus = mTasksSet.valueAt(i); + if (DEBUG) { + Slog.d(TAG, "Saving task " + taskStatus.getTaskId()); + } + out.startTag(null, "task"); + addIdentifierAttributesToTaskTag(out, taskStatus); + writeConstraintsToXml(out, taskStatus); + writeExecutionCriteriaToXml(out, taskStatus); + writeBundleToXml(taskStatus.getExtras(), out); + out.endTag(null, "task"); + } + out.endTag(null, "task-info"); + out.endDocument(); + + // Write out to disk in one fell sweep. + FileOutputStream fos = mTasksFile.startWrite(); + fos.write(baos.toByteArray()); + mTasksFile.finishWrite(fos); + mDirtyOperations = 0; + } catch (IOException e) { + if (DEBUG) { + Slog.v(TAG, "Error writing out task data.", e); + } + } catch (XmlPullParserException e) { + if (DEBUG) { + Slog.d(TAG, "Error persisting bundle.", e); + } + } + } + + /** Write out a tag with data comprising the required fields of this task and its client. */ + private void addIdentifierAttributesToTaskTag(XmlSerializer out, TaskStatus taskStatus) + throws IOException { + out.attribute(null, "taskid", Integer.toString(taskStatus.getTaskId())); + out.attribute(null, "package", taskStatus.getServiceComponent().getPackageName()); + out.attribute(null, "class", taskStatus.getServiceComponent().getClassName()); + out.attribute(null, "uid", Integer.toString(taskStatus.getUid())); + } + + private void writeBundleToXml(PersistableBundle extras, XmlSerializer out) + throws IOException, XmlPullParserException { + out.startTag(null, XML_TAG_EXTRAS); + extras.saveToXml(out); + out.endTag(null, XML_TAG_EXTRAS); + } + /** + * Write out a tag with data identifying this tasks constraints. If the constraint isn't here + * it doesn't apply. + */ + private void writeConstraintsToXml(XmlSerializer out, TaskStatus taskStatus) throws IOException { + out.startTag(null, XML_TAG_PARAMS_CONSTRAINTS); + if (taskStatus.hasMeteredConstraint()) { + out.attribute(null, "unmetered", Boolean.toString(true)); + } + if (taskStatus.hasConnectivityConstraint()) { + out.attribute(null, "connectivity", Boolean.toString(true)); + } + if (taskStatus.hasIdleConstraint()) { + out.attribute(null, "idle", Boolean.toString(true)); + } + if (taskStatus.hasChargingConstraint()) { + out.attribute(null, "charging", Boolean.toString(true)); + } + out.endTag(null, XML_TAG_PARAMS_CONSTRAINTS); + } + + private void writeExecutionCriteriaToXml(XmlSerializer out, TaskStatus taskStatus) + throws IOException { + final Task task = taskStatus.getTask(); + if (taskStatus.getTask().isPeriodic()) { + out.startTag(null, XML_TAG_PERIODIC); + out.attribute(null, "period", Long.toString(task.getIntervalMillis())); + } else { + out.startTag(null, XML_TAG_ONEOFF); + } + + if (taskStatus.hasDeadlineConstraint()) { + // Wall clock deadline. + final long deadlineWallclock = System.currentTimeMillis() + + (taskStatus.getLatestRunTimeElapsed() - SystemClock.elapsedRealtime()); + out.attribute(null, "deadline", Long.toString(deadlineWallclock)); + } + if (taskStatus.hasTimingDelayConstraint()) { + final long delayWallclock = System.currentTimeMillis() + + (taskStatus.getEarliestRunTime() - SystemClock.elapsedRealtime()); + out.attribute(null, "delay", Long.toString(delayWallclock)); + } + + // Only write out back-off policy if it differs from the default. + // This also helps the case where the task is idle -> these aren't allowed to specify + // back-off. + if (taskStatus.getTask().getInitialBackoffMillis() != Task.DEFAULT_INITIAL_BACKOFF_MILLIS + || taskStatus.getTask().getBackoffPolicy() != Task.DEFAULT_BACKOFF_POLICY) { + out.attribute(null, "backoff-policy", Integer.toString(task.getBackoffPolicy())); + out.attribute(null, "initial-backoff", Long.toString(task.getInitialBackoffMillis())); + } + if (task.isPeriodic()) { + out.endTag(null, XML_TAG_PERIODIC); + } else { + out.endTag(null, XML_TAG_ONEOFF); } - mDirtyOperations = 0; } - return true; } /** - * - * @return + * Runnable that reads list of persisted task from xml. + * NOTE: This Runnable locks on TaskStore.this */ - // TODO: Implement this. - private ArraySet<TaskStatus> intialiseTasksFromDisk() { - return new ArraySet<TaskStatus>(); + private class ReadTaskMapFromDiskRunnable implements Runnable { + private TaskMapReadFinishedListener mCallback; + public ReadTaskMapFromDiskRunnable(TaskMapReadFinishedListener callback) { + mCallback = callback; + } + + @Override + public void run() { + try { + List<TaskStatus> tasks; + synchronized (TaskStore.this) { + tasks = readTaskMapImpl(); + } + if (tasks != null) { + mCallback.onTaskMapReadFinished(tasks); + } + } catch (FileNotFoundException e) { + if (TaskManagerService.DEBUG) { + Slog.d(TAG, "Could not find tasks file, probably there was nothing to load."); + } + } catch (XmlPullParserException e) { + if (TaskManagerService.DEBUG) { + Slog.d(TAG, "Error parsing xml.", e); + } + } catch (IOException e) { + if (TaskManagerService.DEBUG) { + Slog.d(TAG, "Error parsing xml.", e); + } + } + } + + private List<TaskStatus> readTaskMapImpl() throws XmlPullParserException, IOException { + FileInputStream fis = mTasksFile.openRead(); + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(fis, null); + + int eventType = parser.getEventType(); + while (eventType != XmlPullParser.START_TAG && + eventType != XmlPullParser.END_DOCUMENT) { + eventType = parser.next(); + Slog.d(TAG, parser.getName()); + } + if (eventType == XmlPullParser.END_DOCUMENT) { + if (DEBUG) { + Slog.d(TAG, "No persisted tasks."); + } + return null; + } + + String tagName = parser.getName(); + if ("task-info".equals(tagName)) { + final List<TaskStatus> tasks = new ArrayList<TaskStatus>(); + // Read in version info. + try { + int version = Integer.valueOf(parser.getAttributeValue(null, "version")); + if (version != TASKS_FILE_VERSION) { + Slog.d(TAG, "Invalid version number, aborting tasks file read."); + return null; + } + } catch (NumberFormatException e) { + Slog.e(TAG, "Invalid version number, aborting tasks file read."); + return null; + } + eventType = parser.next(); + do { + // Read each <task/> + if (eventType == XmlPullParser.START_TAG) { + tagName = parser.getName(); + // Start reading task. + if ("task".equals(tagName)) { + TaskStatus persistedTask = restoreTaskFromXml(parser); + if (persistedTask != null) { + if (DEBUG) { + Slog.d(TAG, "Read out " + persistedTask); + } + tasks.add(persistedTask); + } else { + Slog.d(TAG, "Error reading task from file."); + } + } + } + eventType = parser.next(); + } while (eventType != XmlPullParser.END_DOCUMENT); + return tasks; + } + return null; + } + + /** + * @param parser Xml parser at the beginning of a "<task/>" tag. The next "parser.next()" call + * will take the parser into the body of the task tag. + * @return Newly instantiated task holding all the information we just read out of the xml tag. + */ + private TaskStatus restoreTaskFromXml(XmlPullParser parser) throws XmlPullParserException, + IOException { + Task.Builder taskBuilder; + int uid; + + // Read out task identifier attributes. + try { + taskBuilder = buildBuilderFromXml(parser); + uid = Integer.valueOf(parser.getAttributeValue(null, "uid")); + } catch (NumberFormatException e) { + Slog.e(TAG, "Error parsing task's required fields, skipping"); + return null; + } + + int eventType; + // Read out constraints tag. + do { + eventType = parser.next(); + } while (eventType == XmlPullParser.TEXT); // Push through to next START_TAG. + + if (!(eventType == XmlPullParser.START_TAG && + XML_TAG_PARAMS_CONSTRAINTS.equals(parser.getName()))) { + // Expecting a <constraints> start tag. + return null; + } + try { + buildConstraintsFromXml(taskBuilder, parser); + } catch (NumberFormatException e) { + Slog.d(TAG, "Error reading constraints, skipping."); + return null; + } + parser.next(); // Consume </constraints> + + // Read out execution parameters tag. + do { + eventType = parser.next(); + } while (eventType == XmlPullParser.TEXT); + if (eventType != XmlPullParser.START_TAG) { + return null; + } + + Pair<Long, Long> runtimes; + try { + runtimes = buildExecutionTimesFromXml(parser); + } catch (NumberFormatException e) { + if (DEBUG) { + Slog.d(TAG, "Error parsing execution time parameters, skipping."); + } + return null; + } + + if (XML_TAG_PERIODIC.equals(parser.getName())) { + try { + String val = parser.getAttributeValue(null, "period"); + taskBuilder.setPeriodic(Long.valueOf(val)); + } catch (NumberFormatException e) { + Slog.d(TAG, "Error reading periodic execution criteria, skipping."); + return null; + } + } else if (XML_TAG_ONEOFF.equals(parser.getName())) { + try { + if (runtimes.first != TaskStatus.DEFAULT_EARLIEST_RUNTIME) { + taskBuilder.setMinimumLatency(runtimes.first - SystemClock.elapsedRealtime()); + } + if (runtimes.second != TaskStatus.DEFAULT_LATEST_RUNTIME) { + taskBuilder.setOverrideDeadline( + runtimes.second - SystemClock.elapsedRealtime()); + } + } catch (NumberFormatException e) { + Slog.d(TAG, "Error reading task execution criteria, skipping."); + return null; + } + } else { + if (DEBUG) { + Slog.d(TAG, "Invalid parameter tag, skipping - " + parser.getName()); + } + // Expecting a parameters start tag. + return null; + } + maybeBuildBackoffPolicyFromXml(taskBuilder, parser); + + parser.nextTag(); // Consume parameters end tag. + + // Read out extras Bundle. + do { + eventType = parser.next(); + } while (eventType == XmlPullParser.TEXT); + if (!(eventType == XmlPullParser.START_TAG && XML_TAG_EXTRAS.equals(parser.getName()))) { + if (DEBUG) { + Slog.d(TAG, "Error reading extras, skipping."); + } + return null; + } + + PersistableBundle extras = PersistableBundle.restoreFromXml(parser); + taskBuilder.setExtras(extras); + parser.nextTag(); // Consume </extras> + + return new TaskStatus(taskBuilder.build(), uid, runtimes.first, runtimes.second); + } + + private Task.Builder buildBuilderFromXml(XmlPullParser parser) throws NumberFormatException { + // Pull out required fields from <task> attributes. + int taskId = Integer.valueOf(parser.getAttributeValue(null, "taskid")); + String packageName = parser.getAttributeValue(null, "package"); + String className = parser.getAttributeValue(null, "class"); + ComponentName cname = new ComponentName(packageName, className); + + return new Task.Builder(taskId, cname); + } + + private void buildConstraintsFromXml(Task.Builder taskBuilder, XmlPullParser parser) { + String val = parser.getAttributeValue(null, "unmetered"); + if (val != null) { + taskBuilder.setRequiredNetworkCapabilities(Task.NetworkType.UNMETERED); + } + val = parser.getAttributeValue(null, "connectivity"); + if (val != null) { + taskBuilder.setRequiredNetworkCapabilities(Task.NetworkType.ANY); + } + val = parser.getAttributeValue(null, "idle"); + if (val != null) { + taskBuilder.setRequiresDeviceIdle(true); + } + val = parser.getAttributeValue(null, "charging"); + if (val != null) { + taskBuilder.setRequiresCharging(true); + } + } + + /** + * Builds the back-off policy out of the params tag. These attributes may not exist, depending + * on whether the back-off was set when the task was first scheduled. + */ + private void maybeBuildBackoffPolicyFromXml(Task.Builder taskBuilder, XmlPullParser parser) { + String val = parser.getAttributeValue(null, "initial-backoff"); + if (val != null) { + long initialBackoff = Long.valueOf(val); + val = parser.getAttributeValue(null, "backoff-policy"); + int backoffPolicy = Integer.valueOf(val); // Will throw NFE which we catch higher up. + taskBuilder.setBackoffCriteria(initialBackoff, backoffPolicy); + } + } + + /** + * Convenience function to read out and convert deadline and delay from xml into elapsed real + * time. + * @return A {@link android.util.Pair}, where the first value is the earliest elapsed runtime + * and the second is the latest elapsed runtime. + */ + private Pair<Long, Long> buildExecutionTimesFromXml(XmlPullParser parser) + throws NumberFormatException { + // Pull out execution time data. + final long nowWallclock = System.currentTimeMillis(); + final long nowElapsed = SystemClock.elapsedRealtime(); + + long earliestRunTimeElapsed = TaskStatus.DEFAULT_EARLIEST_RUNTIME; + long latestRunTimeElapsed = TaskStatus.DEFAULT_LATEST_RUNTIME; + String val = parser.getAttributeValue(null, "deadline"); + if (val != null) { + long latestRuntimeWallclock = Long.valueOf(val); + long maxDelayElapsed = + Math.max(latestRuntimeWallclock - nowWallclock, 0); + latestRunTimeElapsed = nowElapsed + maxDelayElapsed; + } + val = parser.getAttributeValue(null, "delay"); + if (val != null) { + long earliestRuntimeWallclock = Long.valueOf(val); + long minDelayElapsed = + Math.max(earliestRuntimeWallclock - nowWallclock, 0); + earliestRunTimeElapsed = nowElapsed + minDelayElapsed; + + } + return Pair.create(earliestRunTimeElapsed, latestRunTimeElapsed); + } } } diff --git a/services/core/java/com/android/server/task/controllers/BatteryController.java b/services/core/java/com/android/server/task/controllers/BatteryController.java new file mode 100644 index 000000000000..585b41fce23b --- /dev/null +++ b/services/core/java/com/android/server/task/controllers/BatteryController.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.task.controllers; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.BatteryManager; +import android.os.BatteryProperty; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.BatteryService; +import com.android.server.task.StateChangedListener; +import com.android.server.task.TaskManagerService; + +import java.util.ArrayList; +import java.util.List; + +/** + * Simple controller that tracks whether the phone is charging or not. The phone is considered to + * be charging when it's been plugged in for more than two minutes, and the system has broadcast + * ACTION_BATTERY_OK. + */ +public class BatteryController extends StateController { + private static final String TAG = "BatteryController"; + + private static final Object sCreationLock = new Object(); + private static volatile BatteryController sController; + private static final String ACTION_CHARGING_STABLE = + "com.android.server.task.controllers.BatteryController.ACTION_CHARGING_STABLE"; + /** Wait this long after phone is plugged in before doing any work. */ + private static final long STABLE_CHARGING_THRESHOLD_MILLIS = 2 * 60 * 1000; // 2 minutes. + + private List<TaskStatus> mTrackedTasks = new ArrayList<TaskStatus>(); + private ChargingTracker mChargeTracker; + + public static BatteryController get(TaskManagerService taskManagerService) { + synchronized (sCreationLock) { + if (sController == null) { + sController = new BatteryController(taskManagerService, + taskManagerService.getContext()); + } + } + return sController; + } + + @VisibleForTesting + public ChargingTracker getTracker() { + return mChargeTracker; + } + + @VisibleForTesting + public static BatteryController getForTesting(StateChangedListener stateChangedListener, + Context context) { + return new BatteryController(stateChangedListener, context); + } + + private BatteryController(StateChangedListener stateChangedListener, Context context) { + super(stateChangedListener, context); + mChargeTracker = new ChargingTracker(); + mChargeTracker.startTracking(); + } + + @Override + public void maybeStartTrackingTask(TaskStatus taskStatus) { + if (taskStatus.hasChargingConstraint()) { + synchronized (mTrackedTasks) { + mTrackedTasks.add(taskStatus); + taskStatus.chargingConstraintSatisfied.set(mChargeTracker.isOnStablePower()); + } + } + + } + + @Override + public void maybeStopTrackingTask(TaskStatus taskStatus) { + if (taskStatus.hasChargingConstraint()) { + synchronized (mTrackedTasks) { + mTrackedTasks.remove(taskStatus); + } + } + } + + private void maybeReportNewChargingState() { + final boolean stablePower = mChargeTracker.isOnStablePower(); + boolean reportChange = false; + synchronized (mTrackedTasks) { + for (TaskStatus ts : mTrackedTasks) { + boolean previous = ts.chargingConstraintSatisfied.getAndSet(stablePower); + if (previous != stablePower) { + reportChange = true; + } + } + } + if (reportChange) { + mStateChangedListener.onControllerStateChanged(); + } + } + + public class ChargingTracker extends BroadcastReceiver { + private final AlarmManager mAlarm; + private final PendingIntent mStableChargingTriggerIntent; + /** + * Track whether we're "charging", where charging means that we're ready to commit to + * doing work. + */ + private boolean mCharging; + /** Keep track of whether the battery is charged enough that we want to do work. */ + private boolean mBatteryHealthy; + + public ChargingTracker() { + mAlarm = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); + Intent intent = new Intent(ACTION_CHARGING_STABLE) + .setComponent(new ComponentName(mContext, this.getClass())); + mStableChargingTriggerIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0); + } + + public void startTracking() { + IntentFilter filter = new IntentFilter(); + + // Battery health. + filter.addAction(Intent.ACTION_BATTERY_LOW); + filter.addAction(Intent.ACTION_BATTERY_OKAY); + // Charging/not charging. + filter.addAction(Intent.ACTION_POWER_CONNECTED); + filter.addAction(Intent.ACTION_POWER_DISCONNECTED); + mContext.registerReceiver(this, filter); + + // Initialise tracker state. + BatteryService batteryService = (BatteryService) ServiceManager.getService("battery"); + if (batteryService != null) { + mBatteryHealthy = !batteryService.isBatteryLow(); + mCharging = batteryService.isPowered(BatteryManager.BATTERY_PLUGGED_ANY); + } else { + // Unavailable for some reason, we default to false and let ACTION_BATTERY_[OK,LOW] + // sort it out. + } + } + + boolean isOnStablePower() { + return mCharging && mBatteryHealthy; + } + + @Override + public void onReceive(Context context, Intent intent) { + onReceiveInternal(intent); + } + + @VisibleForTesting + public void onReceiveInternal(Intent intent) { + final String action = intent.getAction(); + if (Intent.ACTION_BATTERY_LOW.equals(action)) { + if (DEBUG) { + Slog.d(TAG, "Battery life too low to do work. @ " + + SystemClock.elapsedRealtime()); + } + // If we get this action, the battery is discharging => it isn't plugged in so + // there's no work to cancel. We track this variable for the case where it is + // charging, but hasn't been for long enough to be healthy. + mBatteryHealthy = false; + } else if (Intent.ACTION_BATTERY_OKAY.equals(action)) { + if (DEBUG) { + Slog.d(TAG, "Battery life healthy enough to do work. @ " + + SystemClock.elapsedRealtime()); + } + mBatteryHealthy = true; + maybeReportNewChargingState(); + } else if (Intent.ACTION_POWER_CONNECTED.equals(action)) { + // Set up an alarm for ACTION_CHARGING_STABLE - we don't want to kick off tasks + // here if the user unplugs the phone immediately. + mAlarm.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + STABLE_CHARGING_THRESHOLD_MILLIS, + mStableChargingTriggerIntent); + mCharging = true; + } else if (Intent.ACTION_POWER_DISCONNECTED.equals(action)) { + // If an alarm is set, breathe a sigh of relief and cancel it - crisis averted. + mAlarm.cancel(mStableChargingTriggerIntent); + mCharging = false; + maybeReportNewChargingState(); + }else if (ACTION_CHARGING_STABLE.equals(action)) { + // Here's where we actually do the notify for a task being ready. + if (DEBUG) { + Slog.d(TAG, "Battery connected fired @ " + SystemClock.elapsedRealtime()); + } + if (mCharging) { // Should never receive this intent if mCharging is false. + maybeReportNewChargingState(); + } + } + } + } +} diff --git a/services/core/java/com/android/server/task/controllers/ConnectivityController.java b/services/core/java/com/android/server/task/controllers/ConnectivityController.java index 474af8ff0a16..48194604d2cd 100644 --- a/services/core/java/com/android/server/task/controllers/ConnectivityController.java +++ b/services/core/java/com/android/server/task/controllers/ConnectivityController.java @@ -27,6 +27,7 @@ import android.os.UserHandle; import android.util.Log; import android.util.Slog; +import com.android.server.task.StateChangedListener; import com.android.server.task.TaskManagerService; import java.util.LinkedList; @@ -39,7 +40,6 @@ import java.util.List; */ public class ConnectivityController extends StateController { private static final String TAG = "TaskManager.Connectivity"; - private static final boolean DEBUG = true; private final List<TaskStatus> mTrackedTasks = new LinkedList<TaskStatus>(); private final BroadcastReceiver mConnectivityChangedReceiver = @@ -54,13 +54,13 @@ public class ConnectivityController extends StateController { public static synchronized ConnectivityController get(TaskManagerService taskManager) { if (mSingleton == null) { - mSingleton = new ConnectivityController(taskManager); + mSingleton = new ConnectivityController(taskManager, taskManager.getContext()); } return mSingleton; } - private ConnectivityController(TaskManagerService service) { - super(service); + private ConnectivityController(StateChangedListener stateChangedListener, Context context) { + super(stateChangedListener, context); // Register connectivity changed BR. IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); diff --git a/services/core/java/com/android/server/task/controllers/IdleController.java b/services/core/java/com/android/server/task/controllers/IdleController.java index 948964482782..c47facae2df5 100644 --- a/services/core/java/com/android/server/task/controllers/IdleController.java +++ b/services/core/java/com/android/server/task/controllers/IdleController.java @@ -28,11 +28,11 @@ import android.content.IntentFilter; import android.os.SystemClock; import android.util.Slog; +import com.android.server.task.StateChangedListener; import com.android.server.task.TaskManagerService; public class IdleController extends StateController { private static final String TAG = "IdleController"; - private static final boolean DEBUG = false; // Policy: we decide that we're "idle" if the device has been unused / // screen off or dreaming for at least this long @@ -52,14 +52,14 @@ public class IdleController extends StateController { public static IdleController get(TaskManagerService service) { synchronized (sCreationLock) { if (sController == null) { - sController = new IdleController(service); + sController = new IdleController(service, service.getContext()); } return sController; } } - private IdleController(TaskManagerService service) { - super(service); + private IdleController(StateChangedListener stateChangedListener, Context context) { + super(stateChangedListener, context); initIdleStateTracking(); } diff --git a/services/core/java/com/android/server/task/controllers/StateController.java b/services/core/java/com/android/server/task/controllers/StateController.java index ed31eaced3b3..cbe6ff8d9cdd 100644 --- a/services/core/java/com/android/server/task/controllers/StateController.java +++ b/services/core/java/com/android/server/task/controllers/StateController.java @@ -27,13 +27,13 @@ import com.android.server.task.TaskManagerService; * are ready to run, or whether they must be stopped. */ public abstract class StateController { - + protected static final boolean DEBUG = true; protected Context mContext; protected StateChangedListener mStateChangedListener; - public StateController(TaskManagerService service) { - mStateChangedListener = service; - mContext = service.getContext(); + public StateController(StateChangedListener stateChangedListener, Context context) { + mStateChangedListener = stateChangedListener; + mContext = context; } /** diff --git a/services/core/java/com/android/server/task/controllers/TaskStatus.java b/services/core/java/com/android/server/task/controllers/TaskStatus.java index b7f84ec34cf9..33670a122026 100644 --- a/services/core/java/com/android/server/task/controllers/TaskStatus.java +++ b/services/core/java/com/android/server/task/controllers/TaskStatus.java @@ -18,7 +18,7 @@ package com.android.server.task.controllers; import android.app.task.Task; import android.content.ComponentName; -import android.os.Bundle; +import android.os.PersistableBundle; import android.os.SystemClock; import android.os.UserHandle; @@ -37,6 +37,9 @@ import java.util.concurrent.atomic.AtomicBoolean; * @hide */ public class TaskStatus { + public static final long DEFAULT_LATEST_RUNTIME = Long.MAX_VALUE; + public static final long DEFAULT_EARLIEST_RUNTIME = 0L; + final Task task; final int uId; @@ -61,7 +64,7 @@ public class TaskStatus { * indicates there is no deadline constraint. See {@link #hasDeadlineConstraint()}. */ private long latestRunTimeElapsedMillis; - + /** How many times this task has failed, used to compute back-off. */ private final int numFailures; /** Provide a handle to the service that this task will be run on. */ @@ -69,36 +72,52 @@ public class TaskStatus { return uId; } - /** Create a newly scheduled task. */ - public TaskStatus(Task task, int uId, boolean persisted) { + private TaskStatus(Task task, int uId, boolean persisted, int numFailures) { this.task = task; this.uId = uId; - this.numFailures = 0; + this.numFailures = numFailures; this.persisted = persisted; + } + + /** Create a newly scheduled task. */ + public TaskStatus(Task task, int uId, boolean persisted) { + this(task, uId, persisted, 0); final long elapsedNow = SystemClock.elapsedRealtime(); - // Timing constraints + if (task.isPeriodic()) { earliestRunTimeElapsedMillis = elapsedNow; latestRunTimeElapsedMillis = elapsedNow + task.getIntervalMillis(); } else { earliestRunTimeElapsedMillis = task.hasEarlyConstraint() ? - elapsedNow + task.getMinLatencyMillis() : 0L; + elapsedNow + task.getMinLatencyMillis() : DEFAULT_EARLIEST_RUNTIME; latestRunTimeElapsedMillis = task.hasLateConstraint() ? - elapsedNow + task.getMaxExecutionDelayMillis() : Long.MAX_VALUE; + elapsedNow + task.getMaxExecutionDelayMillis() : DEFAULT_LATEST_RUNTIME; } } - public TaskStatus(TaskStatus rescheduling, long newEarliestRuntimeElapsed, - long newLatestRuntimeElapsed, int backoffAttempt) { - this.task = rescheduling.task; + /** + * Create a new TaskStatus that was loaded from disk. We ignore the provided + * {@link android.app.task.Task} time criteria because we can load a persisted periodic task + * from the {@link com.android.server.task.TaskStore} and still want to respect its + * wallclock runtime rather than resetting it on every boot. + * We consider a freshly loaded task to no longer be in back-off. + */ + public TaskStatus(Task task, int uId, long earliestRunTimeElapsedMillis, + long latestRunTimeElapsedMillis) { + this(task, uId, true, 0); + + this.earliestRunTimeElapsedMillis = earliestRunTimeElapsedMillis; + this.latestRunTimeElapsedMillis = latestRunTimeElapsedMillis; + } - this.uId = rescheduling.getUid(); - this.persisted = rescheduling.isPersisted(); - this.numFailures = backoffAttempt; + /** Create a new task to be rescheduled with the provided parameters. */ + public TaskStatus(TaskStatus rescheduling, long newEarliestRuntimeElapsedMillis, + long newLatestRuntimeElapsedMillis, int backoffAttempt) { + this(rescheduling.task, rescheduling.getUid(), rescheduling.isPersisted(), backoffAttempt); - earliestRunTimeElapsedMillis = newEarliestRuntimeElapsed; - latestRunTimeElapsedMillis = newLatestRuntimeElapsed; + earliestRunTimeElapsedMillis = newEarliestRuntimeElapsedMillis; + latestRunTimeElapsedMillis = newLatestRuntimeElapsedMillis; } public Task getTask() { @@ -125,7 +144,7 @@ public class TaskStatus { return uId; } - public Bundle getExtras() { + public PersistableBundle getExtras() { return task.getExtras(); } @@ -142,11 +161,11 @@ public class TaskStatus { } public boolean hasTimingDelayConstraint() { - return earliestRunTimeElapsedMillis != 0L; + return earliestRunTimeElapsedMillis != DEFAULT_EARLIEST_RUNTIME; } public boolean hasDeadlineConstraint() { - return latestRunTimeElapsedMillis != Long.MAX_VALUE; + return latestRunTimeElapsedMillis != DEFAULT_LATEST_RUNTIME; } public boolean hasIdleConstraint() { diff --git a/services/core/java/com/android/server/task/controllers/TimeController.java b/services/core/java/com/android/server/task/controllers/TimeController.java index 72f312c36f43..8c6dd2715ce8 100644 --- a/services/core/java/com/android/server/task/controllers/TimeController.java +++ b/services/core/java/com/android/server/task/controllers/TimeController.java @@ -24,6 +24,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.os.SystemClock; +import com.android.server.task.StateChangedListener; import com.android.server.task.TaskManagerService; import java.util.Iterator; @@ -58,13 +59,13 @@ public class TimeController extends StateController { public static synchronized TimeController get(TaskManagerService taskManager) { if (mSingleton == null) { - mSingleton = new TimeController(taskManager); + mSingleton = new TimeController(taskManager, taskManager.getContext()); } return mSingleton; } - private TimeController(TaskManagerService service) { - super(service); + private TimeController(StateChangedListener stateChangedListener, Context context) { + super(stateChangedListener, context); mTaskExpiredAlarmIntent = PendingIntent.getBroadcast(mContext, 0 /* ignored */, new Intent(ACTION_TASK_EXPIRED), 0); diff --git a/services/tests/servicestests/src/com/android/server/task/TaskStoreTest.java b/services/tests/servicestests/src/com/android/server/task/TaskStoreTest.java new file mode 100644 index 000000000000..e7f9ca02b840 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/task/TaskStoreTest.java @@ -0,0 +1,199 @@ +package com.android.server.task; + + +import android.content.ComponentName; +import android.content.Context; +import android.app.task.Task; +import android.app.task.Task.Builder; +import android.os.PersistableBundle; +import android.test.AndroidTestCase; +import android.test.RenamingDelegatingContext; +import android.util.Log; + +import com.android.server.task.controllers.TaskStatus; + +import java.util.List; + +import static com.android.server.task.TaskStore.initAndGet; +/** + * Test reading and writing correctly from file. + */ +public class TaskStoreTest extends AndroidTestCase { + private static final String TAG = "TaskStoreTest"; + private static final String TEST_PREFIX = "_test_"; + // private static final int USER_NON_0 = 3; + private static final int SOME_UID = 34234; + private ComponentName mComponent; + private static final long IO_WAIT = 600L; + + TaskStore mTaskStoreUnderTest; + Context mTestContext; + TaskMapReadFinishedListener mTaskMapReadFinishedListenerStub = + new TaskMapReadFinishedListener() { + @Override + public void onTaskMapReadFinished(List<TaskStatus> tasks) { + // do nothing. + } + }; + + @Override + public void setUp() throws Exception { + mTestContext = new RenamingDelegatingContext(getContext(), TEST_PREFIX); + Log.d(TAG, "Saving tasks to '" + mTestContext.getFilesDir() + "'"); + mTaskStoreUnderTest = TaskStore.initAndGetForTesting(mTestContext, + mTestContext.getFilesDir(), mTaskMapReadFinishedListenerStub); + mComponent = new ComponentName(getContext().getPackageName(), StubClass.class.getName()); + } + + @Override + public void tearDown() throws Exception { + mTaskStoreUnderTest.clear(); + } + + public void testMaybeWriteStatusToDisk() throws Exception { + int taskId = 5; + long runByMillis = 20000L; // 20s + long runFromMillis = 2000L; // 2s + long initialBackoff = 10000L; // 10s + + final Task task = new Builder(taskId, mComponent) + .setRequiresCharging(true) + .setRequiredNetworkCapabilities(Task.NetworkType.ANY) + .setBackoffCriteria(initialBackoff, Task.BackoffPolicy.EXPONENTIAL) + .setOverrideDeadline(runByMillis) + .setMinimumLatency(runFromMillis) + .build(); + final TaskStatus ts = new TaskStatus(task, SOME_UID, true /* persisted */); + mTaskStoreUnderTest.add(ts); + Thread.sleep(IO_WAIT); + // Manually load tasks from xml file. + mTaskStoreUnderTest.readTaskMapFromDisk(new TaskMapReadFinishedListener() { + @Override + public void onTaskMapReadFinished(List<TaskStatus> tasks) { + assertEquals("Didn't get expected number of persisted tasks.", 1, tasks.size()); + TaskStatus loadedTaskStatus = tasks.get(0); + assertTasksEqual(task, loadedTaskStatus.getTask()); + assertEquals("Different uids.", SOME_UID, tasks.get(0).getUid()); + compareTimestampsSubjectToIoLatency("Early run-times not the same after read.", + ts.getEarliestRunTime(), loadedTaskStatus.getEarliestRunTime()); + compareTimestampsSubjectToIoLatency("Late run-times not the same after read.", + ts.getLatestRunTimeElapsed(), loadedTaskStatus.getLatestRunTimeElapsed()); + } + }); + + } + + public void testWritingTwoFilesToDisk() throws Exception { + final Task task1 = new Builder(8, mComponent) + .setRequiresDeviceIdle(true) + .setPeriodic(10000L) + .setRequiresCharging(true) + .build(); + final Task task2 = new Builder(12, mComponent) + .setMinimumLatency(5000L) + .setBackoffCriteria(15000L, Task.BackoffPolicy.LINEAR) + .setOverrideDeadline(30000L) + .setRequiredNetworkCapabilities(Task.NetworkType.UNMETERED) + .build(); + final TaskStatus taskStatus1 = new TaskStatus(task1, SOME_UID, true /* persisted */); + final TaskStatus taskStatus2 = new TaskStatus(task2, SOME_UID, true /* persisted */); + mTaskStoreUnderTest.add(taskStatus1); + mTaskStoreUnderTest.add(taskStatus2); + Thread.sleep(IO_WAIT); + mTaskStoreUnderTest.readTaskMapFromDisk(new TaskMapReadFinishedListener() { + @Override + public void onTaskMapReadFinished(List<TaskStatus> tasks) { + assertEquals("Incorrect # of persisted tasks.", 2, tasks.size()); + TaskStatus loaded1 = tasks.get(0); + TaskStatus loaded2 = tasks.get(1); + assertTasksEqual(task1, loaded1.getTask()); + assertTasksEqual(task2, loaded2.getTask()); + + // Check that the loaded task has the correct runtimes. + compareTimestampsSubjectToIoLatency("Early run-times not the same after read.", + taskStatus1.getEarliestRunTime(), loaded1.getEarliestRunTime()); + compareTimestampsSubjectToIoLatency("Late run-times not the same after read.", + taskStatus1.getLatestRunTimeElapsed(), loaded1.getLatestRunTimeElapsed()); + compareTimestampsSubjectToIoLatency("Early run-times not the same after read.", + taskStatus2.getEarliestRunTime(), loaded2.getEarliestRunTime()); + compareTimestampsSubjectToIoLatency("Late run-times not the same after read.", + taskStatus2.getLatestRunTimeElapsed(), loaded2.getLatestRunTimeElapsed()); + } + }); + + } + + public void testWritingTaskWithExtras() throws Exception { + Task.Builder b = new Builder(8, mComponent) + .setRequiresDeviceIdle(true) + .setPeriodic(10000L) + .setRequiresCharging(true); + + PersistableBundle extras = new PersistableBundle(); + extras.putDouble("hello", 3.2); + extras.putString("hi", "there"); + extras.putInt("into", 3); + b.setExtras(extras); + final Task task = b.build(); + TaskStatus taskStatus = new TaskStatus(task, SOME_UID, true /* persisted */); + + mTaskStoreUnderTest.add(taskStatus); + Thread.sleep(IO_WAIT); + mTaskStoreUnderTest.readTaskMapFromDisk(new TaskMapReadFinishedListener() { + @Override + public void onTaskMapReadFinished(List<TaskStatus> tasks) { + assertEquals("Incorrect # of persisted tasks.", 1, tasks.size()); + TaskStatus loaded = tasks.get(0); + assertTasksEqual(task, loaded.getTask()); + } + }); + + } + + /** + * Helper function to throw an error if the provided task and TaskStatus objects are not equal. + */ + private void assertTasksEqual(Task first, Task second) { + assertEquals("Different task ids.", first.getId(), second.getId()); + assertEquals("Different components.", first.getService(), second.getService()); + assertEquals("Different periodic status.", first.isPeriodic(), second.isPeriodic()); + assertEquals("Different period.", first.getIntervalMillis(), second.getIntervalMillis()); + assertEquals("Different inital backoff.", first.getInitialBackoffMillis(), + second.getInitialBackoffMillis()); + assertEquals("Different backoff policy.", first.getBackoffPolicy(), + second.getBackoffPolicy()); + + assertEquals("Invalid charging constraint.", first.isRequireCharging(), + second.isRequireCharging()); + assertEquals("Invalid idle constraint.", first.isRequireDeviceIdle(), + second.isRequireDeviceIdle()); + assertEquals("Invalid unmetered constraint.", + first.getNetworkCapabilities() == Task.NetworkType.UNMETERED, + second.getNetworkCapabilities() == Task.NetworkType.UNMETERED); + assertEquals("Invalid connectivity constraint.", + first.getNetworkCapabilities() == Task.NetworkType.ANY, + second.getNetworkCapabilities() == Task.NetworkType.ANY); + assertEquals("Invalid deadline constraint.", + first.hasLateConstraint(), + second.hasLateConstraint()); + assertEquals("Invalid delay constraint.", + first.hasEarlyConstraint(), + second.hasEarlyConstraint()); + assertEquals("Extras don't match", + first.getExtras().toString(), second.getExtras().toString()); + } + + /** + * When comparing timestamps before and after DB read/writes (to make sure we're saving/loading + * the correct values), there is some latency involved that terrorises a naive assertEquals(). + * We define a <code>DELTA_MILLIS</code> as a function variable here to make this comparision + * more reasonable. + */ + private void compareTimestampsSubjectToIoLatency(String error, long ts1, long ts2) { + final long DELTA_MILLIS = 700L; // We allow up to 700ms of latency for IO read/writes. + assertTrue(error, Math.abs(ts1 - ts2) < DELTA_MILLIS + IO_WAIT); + } + + private static class StubClass {} + +}
\ No newline at end of file diff --git a/services/tests/servicestests/src/com/android/server/task/controllers/BatteryControllerTest.java b/services/tests/servicestests/src/com/android/server/task/controllers/BatteryControllerTest.java new file mode 100644 index 000000000000..e617caf0cfa1 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/task/controllers/BatteryControllerTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.task.controllers; + + +import android.content.ComponentName; +import android.content.Intent; +import android.test.AndroidTestCase; + +import com.android.server.task.StateChangedListener; + +import static com.android.server.task.controllers.BatteryController.getForTesting; + +import static org.mockito.Mockito.*; + +/** + * + */ +public class BatteryControllerTest extends AndroidTestCase { + BatteryController mBatteryControllerUnderTest; + + StateChangedListener mStateChangedListenerStub = new StateChangedListener() { + @Override + public void onControllerStateChanged() { + + } + + @Override + public void onTaskDeadlineExpired(TaskStatus taskStatus) { + + } + }; + BatteryController.ChargingTracker mTrackerUnderTest; + + public void setUp() throws Exception { + mBatteryControllerUnderTest = getForTesting(mStateChangedListenerStub, getTestContext()); + mTrackerUnderTest = mBatteryControllerUnderTest.getTracker(); + } + + public void testSendBatteryChargingIntent() throws Exception { + Intent batteryConnectedIntent = new Intent(Intent.ACTION_POWER_CONNECTED) + .setComponent(new ComponentName(getContext(), mTrackerUnderTest.getClass())); + Intent batteryHealthyIntent = new Intent(Intent.ACTION_BATTERY_OKAY) + .setComponent(new ComponentName(getContext(), mTrackerUnderTest.getClass())); + + mTrackerUnderTest.onReceiveInternal(batteryConnectedIntent); + mTrackerUnderTest.onReceiveInternal(batteryHealthyIntent); + + assertTrue(mTrackerUnderTest.isOnStablePower()); + } + +} diff --git a/telecomm/java/android/telecomm/CallService.java b/telecomm/java/android/telecomm/CallService.java index d4521727bb45..a254459442a3 100644 --- a/telecomm/java/android/telecomm/CallService.java +++ b/telecomm/java/android/telecomm/CallService.java @@ -63,6 +63,7 @@ public abstract class CallService extends Service { private static final int MSG_STOP_DTMF_TONE = 13; private static final int MSG_ADD_TO_CONFERENCE = 14; private static final int MSG_SPLIT_FROM_CONFERENCE = 15; + private static final int MSG_ON_POST_DIAL_CONTINUE = 16; /** * Default Handler used to consolidate binder method calls onto a single thread. @@ -150,6 +151,17 @@ public abstract class CallService extends Service { } break; } + case MSG_ON_POST_DIAL_CONTINUE: { + SomeArgs args = (SomeArgs) msg.obj; + try { + String callId = (String) args.arg1; + boolean proceed = (args.argi1 == 1); + onPostDialContinue(callId, proceed); + } finally { + args.recycle(); + } + break; + } default: break; } @@ -247,6 +259,14 @@ public abstract class CallService extends Service { args.arg2 = callId; mMessageHandler.obtainMessage(MSG_SPLIT_FROM_CONFERENCE, args).sendToTarget(); } + + @Override + public void onPostDialContinue(String callId, boolean proceed) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.argi1 = proceed ? 1 : 0; + mMessageHandler.obtainMessage(MSG_ON_POST_DIAL_CONTINUE, args).sendToTarget(); + } } /** @@ -422,4 +442,7 @@ public abstract class CallService extends Service { * @hide */ public abstract void splitFromConference(String conferenceCallId, String callId); + + public void onPostDialContinue(String callId, boolean proceed) {} + public void onPostDialWait(Connection conn, String remaining) {} } diff --git a/telecomm/java/android/telecomm/CallServiceAdapter.java b/telecomm/java/android/telecomm/CallServiceAdapter.java index 0c57828f9472..fb5c87160c74 100644 --- a/telecomm/java/android/telecomm/CallServiceAdapter.java +++ b/telecomm/java/android/telecomm/CallServiceAdapter.java @@ -217,4 +217,11 @@ public final class CallServiceAdapter { } catch (RemoteException ignored) { } } + + public void onPostDialWait(String callId, String remaining) { + try { + mAdapter.onPostDialWait(callId, remaining); + } catch (RemoteException ignored) { + } + } } diff --git a/telecomm/java/android/telecomm/Connection.java b/telecomm/java/android/telecomm/Connection.java index 8cce8e62c1aa..344814fcd1fc 100644 --- a/telecomm/java/android/telecomm/Connection.java +++ b/telecomm/java/android/telecomm/Connection.java @@ -444,6 +444,11 @@ public abstract class Connection { */ protected void onReject() {} + /** + * Notifies this Connection whether the user wishes to proceed with the post-dial DTMF codes. + */ + protected void onPostDialContinue(boolean proceed) {} + private void setState(int state) { Log.d(this, "setState: %s", stateToString(state)); onSetState(state); diff --git a/telecomm/java/android/telecomm/ConnectionService.java b/telecomm/java/android/telecomm/ConnectionService.java index 31de15c354dc..59e977de076f 100644 --- a/telecomm/java/android/telecomm/ConnectionService.java +++ b/telecomm/java/android/telecomm/ConnectionService.java @@ -146,7 +146,8 @@ public abstract class ConnectionService extends CallService { } } else { addConnection(callInfo.getId(), result[0]); - Log.d(this, "adapter handleSuccessfulOutgoingCall %s", callInfo.getId()); + Log.d(this, "adapter handleSuccessfulOutgoingCall %s", + callInfo.getId()); getAdapter().handleSuccessfulOutgoingCall(callInfo.getId()); } } @@ -288,6 +289,25 @@ public abstract class ConnectionService extends CallService { // TODO(santoscordon): Find existing conference call and invoke split(connection). } + @Override + public final void onPostDialContinue(String callId, boolean proceed) { + Log.d(this, "onPostDialContinue(%s)", callId); + + Connection connection = findConnectionForAction(callId, "onPostDialContinue"); + if (connection == NULL_CONNECTION) { + Log.w(this, "Connection missing in post-dial request %s.", callId); + return; + } + connection.onPostDialContinue(proceed); + } + + @Override + public final void onPostDialWait(Connection conn, String remaining) { + Log.d(this, "onPostDialWait(%s, %s)", conn, remaining); + + getAdapter().onPostDialWait(mIdByConnection.get(conn), remaining); + } + /** * Find a set of Subscriptions matching a given handle (e.g. phone number). * diff --git a/telecomm/java/android/telecomm/InCallAdapter.java b/telecomm/java/android/telecomm/InCallAdapter.java index 6838ede45470..0bef4194b8bd 100644 --- a/telecomm/java/android/telecomm/InCallAdapter.java +++ b/telecomm/java/android/telecomm/InCallAdapter.java @@ -174,13 +174,14 @@ public final class InCallAdapter { * will pause playing the tones and notify the {@link InCallService} that the call is in the * {@link InCallService#setPostDialWait(String,String)} state. When the user decides to continue * the postdial sequence, the {@link InCallService} should invoke the - * {@link #postDialContinue(String)} method. + * {@link #postDialContinue(String,boolean)} method. * * @param callId The unique ID of the call for which postdial string playing should continue. + * @param proceed Whether or not to continue with the post-dial sequence. */ - public void postDialContinue(String callId) { + public void postDialContinue(String callId, boolean proceed) { try { - mAdapter.postDialContinue(callId); + mAdapter.postDialContinue(callId, proceed); } catch (RemoteException e) { } } diff --git a/telecomm/java/com/android/internal/telecomm/ICallService.aidl b/telecomm/java/com/android/internal/telecomm/ICallService.aidl index 771a3ae9ef38..9139aa6bbdf8 100644 --- a/telecomm/java/com/android/internal/telecomm/ICallService.aidl +++ b/telecomm/java/com/android/internal/telecomm/ICallService.aidl @@ -59,4 +59,6 @@ oneway interface ICallService { void addToConference(String conferenceCallId, in List<String> callIds); void splitFromConference(String conferenceCallId, String callId); + + void onPostDialContinue(String callId, boolean proceed); } diff --git a/telecomm/java/com/android/internal/telecomm/ICallServiceAdapter.aidl b/telecomm/java/com/android/internal/telecomm/ICallServiceAdapter.aidl index f94eb322d025..17e04879bba0 100644 --- a/telecomm/java/com/android/internal/telecomm/ICallServiceAdapter.aidl +++ b/telecomm/java/com/android/internal/telecomm/ICallServiceAdapter.aidl @@ -52,4 +52,6 @@ oneway interface ICallServiceAdapter { void setIsConferenced(String conferenceCallId, String callId, boolean isConferenced); void removeCall(String callId); + + void onPostDialWait(String callId, String remaining); } diff --git a/telecomm/java/com/android/internal/telecomm/IInCallAdapter.aidl b/telecomm/java/com/android/internal/telecomm/IInCallAdapter.aidl index 6a27217a9275..f14404319ab9 100644 --- a/telecomm/java/com/android/internal/telecomm/IInCallAdapter.aidl +++ b/telecomm/java/com/android/internal/telecomm/IInCallAdapter.aidl @@ -44,7 +44,7 @@ oneway interface IInCallAdapter { void stopDtmfTone(String callId); - void postDialContinue(String callId); + void postDialContinue(String callId, boolean proceed); void handoffCall(String callId); diff --git a/telephony/java/android/telephony/DisconnectCause.java b/telephony/java/android/telephony/DisconnectCause.java index d2044be4dc0b..604b895fa8cd 100644 --- a/telephony/java/android/telephony/DisconnectCause.java +++ b/telephony/java/android/telephony/DisconnectCause.java @@ -18,6 +18,8 @@ package android.telephony; /** * Contains disconnect call causes generated by the framework and the RIL. + * + * @hide */ public class DisconnectCause { diff --git a/telephony/java/android/telephony/PhoneNumberUtils.java b/telephony/java/android/telephony/PhoneNumberUtils.java index 9da032a87807..6996659dcd15 100644 --- a/telephony/java/android/telephony/PhoneNumberUtils.java +++ b/telephony/java/android/telephony/PhoneNumberUtils.java @@ -1742,16 +1742,14 @@ public class PhoneNumberUtils /** * Checks if a given number is an emergency number for the country that the user is in. - * - * @param number the number to look up. * @param context the specific context which the number should be checked against + * @param number the number to look up. + * * @return true if the specified number is an emergency number for the country the user * is currently in. */ - public static boolean isLocalEmergencyNumber(String number, Context context) { - return isLocalEmergencyNumberInternal(number, - context, - true /* useExactMatch */); + public static boolean isLocalEmergencyNumber(Context context, String number) { + return isLocalEmergencyNumberInternal(context, number, true /* useExactMatch */); } /** @@ -1767,27 +1765,24 @@ public class PhoneNumberUtils * This method is intended for internal use by the phone app when * deciding whether to allow ACTION_CALL intents from 3rd party apps * (where we're required to *not* allow emergency calls to be placed.) - * - * @param number the number to look up. * @param context the specific context which the number should be checked against + * @param number the number to look up. + * * @return true if the specified number is an emergency number for a local country, based on the * CountryDetector. * * @see android.location.CountryDetector * @hide */ - public static boolean isPotentialLocalEmergencyNumber(String number, Context context) { - return isLocalEmergencyNumberInternal(number, - context, - false /* useExactMatch */); + public static boolean isPotentialLocalEmergencyNumber(Context context, String number) { + return isLocalEmergencyNumberInternal(context, number, false /* useExactMatch */); } /** * Helper function for isLocalEmergencyNumber() and * isPotentialLocalEmergencyNumber(). - * - * @param number the number to look up. * @param context the specific context which the number should be checked against + * @param number the number to look up. * @param useExactMatch if true, consider a number to be an emergency * number only if it *exactly* matches a number listed in * the RIL / SIM. If false, a number is considered to be an @@ -1799,9 +1794,8 @@ public class PhoneNumberUtils * * @see android.location.CountryDetector */ - private static boolean isLocalEmergencyNumberInternal(String number, - Context context, - boolean useExactMatch) { + private static boolean isLocalEmergencyNumberInternal(Context context, String number, + boolean useExactMatch) { String countryIso; CountryDetector detector = (CountryDetector) context.getSystemService( Context.COUNTRY_DETECTOR); diff --git a/telephony/java/com/android/internal/telephony/CallerInfo.java b/telephony/java/com/android/internal/telephony/CallerInfo.java index f6143eddbd38..f8dd7cf3de41 100644 --- a/telephony/java/com/android/internal/telephony/CallerInfo.java +++ b/telephony/java/com/android/internal/telephony/CallerInfo.java @@ -276,7 +276,7 @@ public class CallerInfo { // Change the callerInfo number ONLY if it is an emergency number // or if it is the voicemail number. If it is either, take a // shortcut and skip the query. - if (PhoneNumberUtils.isLocalEmergencyNumber(number, context)) { + if (PhoneNumberUtils.isLocalEmergencyNumber(context, number)) { return new CallerInfo().markAsEmergency(context); } else if (PhoneNumberUtils.isVoiceMailNumber(number)) { return new CallerInfo().markAsVoiceMail(); diff --git a/telephony/java/com/android/internal/telephony/CallerInfoAsyncQuery.java b/telephony/java/com/android/internal/telephony/CallerInfoAsyncQuery.java index 74f73b52fbd1..34fed5ececcf 100644 --- a/telephony/java/com/android/internal/telephony/CallerInfoAsyncQuery.java +++ b/telephony/java/com/android/internal/telephony/CallerInfoAsyncQuery.java @@ -399,7 +399,7 @@ public class CallerInfoAsyncQuery { cw.number = number; // check to see if these are recognized numbers, and use shortcuts if we can. - if (PhoneNumberUtils.isLocalEmergencyNumber(number, context)) { + if (PhoneNumberUtils.isLocalEmergencyNumber(context, number)) { cw.event = EVENT_EMERGENCY_NUMBER; } else if (PhoneNumberUtils.isVoiceMailNumber(number)) { cw.event = EVENT_VOICEMAIL_NUMBER; diff --git a/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java index e9daffd8b024..a4174790b17b 100644 --- a/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java @@ -977,7 +977,7 @@ public final class Canvas_Delegate { /*package*/ static void native_drawText(long nativeCanvas, final char[] text, final int index, final int count, final float startX, final float startY, final int flags, long paint, - long typeface) { + final long typeface) { draw(nativeCanvas, paint, false /*compositeOnly*/, false /*forceSrcMode*/, new GcSnapshot.Drawable() { @@ -985,6 +985,11 @@ public final class Canvas_Delegate { public void draw(Graphics2D graphics, Paint_Delegate paintDelegate) { // WARNING: the logic in this method is similar to Paint_Delegate.measureText. // Any change to this method should be reflected in Paint.measureText + + // assert that the typeface passed is actually the one stored in paint. + assert (typeface == paintDelegate.mNativeTypeface); + + // Paint.TextAlign indicates how the text is positioned relative to X. // LEFT is the default and there's nothing to do. float x = startX; diff --git a/tools/layoutlib/bridge/src/android/graphics/FontFamily_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/FontFamily_Delegate.java index 9ea45386055f..d73adab65da1 100644 --- a/tools/layoutlib/bridge/src/android/graphics/FontFamily_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/FontFamily_Delegate.java @@ -21,6 +21,8 @@ import com.android.layoutlib.bridge.Bridge; import com.android.layoutlib.bridge.impl.DelegateManager; import com.android.tools.layoutlib.annotations.LayoutlibDelegate; +import android.content.res.AssetManager; + import java.awt.Font; import java.io.File; import java.util.ArrayList; @@ -47,7 +49,6 @@ public class FontFamily_Delegate { private static final String FONT_SUFFIX_BOLDITALIC = "BoldItalic.ttf"; private static final String FONT_SUFFIX_BOLD = "Bold.ttf"; private static final String FONT_SUFFIX_ITALIC = "Italic.ttf"; - private static final String FONT_SUBSTRING_COMPACT = "UI"; /** * A class associating {@link Font} with its metadata. @@ -56,11 +57,6 @@ public class FontFamily_Delegate { Font mFont; /** Regular, Bold, Italic, or BoldItalic. */ int mStyle; - /** - * The variant of the Font - compact or elegant. - * @see Paint#setElegantTextHeight(boolean) - */ - boolean mIsCompact; } // ---- delegate manager ---- @@ -75,6 +71,14 @@ public class FontFamily_Delegate { // ---- delegate data ---- private List<FontInfo> mFonts = new ArrayList<FontInfo>(); + /** + * The variant of the Font Family - compact or elegant. + * 0 is unspecified, 1 is compact and 2 is elegant. This needs to be kept in sync with values in + * android.graphics.FontFamily + * + * @see Paint#setElegantTextHeight(boolean) + */ + private FontVariant mVariant; // Path of fonts that haven't been created since sFontLoader hasn't been initialized. private List<String> mPath = new ArrayList<String>(); @@ -93,37 +97,22 @@ public class FontFamily_Delegate { sPostInitDelegate.clear(); } - public Font getFont(int style, boolean isCompact) { + public Font getFont(int style) { FontInfo plainFont = null; - FontInfo styledFont = null; // Font matching the style but not isCompact for (FontInfo font : mFonts) { if (font.mStyle == style) { - if (font.mIsCompact == isCompact) { - return font.mFont; - } - styledFont = font; + return font.mFont; } - if (font.mStyle == Font.PLAIN) { - if (plainFont == null) { - plainFont = font; - continue; - } - if (font.mIsCompact == isCompact) { - // Override the previous selection of plain font since we've found a better one. - plainFont = font; - } + if (font.mStyle == Font.PLAIN && plainFont == null) { + plainFont = font; } } - if (styledFont != null) { - return styledFont.mFont; - } // No font with the mentioned style is found. Try to derive one. if (plainFont != null && style > 0 && style < 4) { - styledFont = new FontInfo(); + FontInfo styledFont = new FontInfo(); styledFont.mFont = plainFont.mFont.deriveFont(style); styledFont.mStyle = style; - styledFont.mIsCompact = plainFont.mIsCompact; // Add the font to the list of fonts so that we don't have to derive it the next time. mFonts.add(styledFont); return styledFont.mFont; @@ -131,11 +120,20 @@ public class FontFamily_Delegate { return null; } + public FontVariant getVariant() { + return mVariant; + } + + // ---- native methods ---- @LayoutlibDelegate - /*package*/ static long nCreateFamily() { + /*package*/ static long nCreateFamily(String lang, int variant) { + // TODO: support lang. This is required for japanese locale. FontFamily_Delegate delegate = new FontFamily_Delegate(); + // variant can be 0, 1 or 2. + assert variant < 3; + delegate.mVariant = FontVariant.values()[variant]; if (sFontLocation != null) { delegate.init(); } else { @@ -164,6 +162,13 @@ public class FontFamily_Delegate { return false; } + @LayoutlibDelegate + /*package*/ static boolean nAddFontFromAsset(long nativeFamily, AssetManager mgr, String path) { + Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, + "FontFamily.addFontFromAsset is not supported.", null /*throwable*/, null /*data*/); + return false; + } + private void init() { for (String path : mPath) { addFont(path); @@ -195,13 +200,6 @@ public class FontFamily_Delegate { style = Font.ITALIC; } fontInfo.mStyle = style; - - // Names of compact fonts end with UI-<style>.ttf. For example, NotoNakshUI-Regular.ttf. - // This should go away when this info is passed on by nAddFont(). - int hyphenIndex = fontName.lastIndexOf('-'); - fontInfo.mIsCompact = hyphenIndex > 0 && - fontName.substring(0, hyphenIndex).endsWith(FONT_SUBSTRING_COMPACT); - } private static Font loadFont(String path) { @@ -214,7 +212,7 @@ public class FontFamily_Delegate { } catch (Exception e) { Bridge.getLog().fidelityWarning(LayoutLog.TAG_BROKEN, String.format("Unable to load font %1$s", relativePath), - null /*throwable*/, null /*data*/); + e /*throwable*/, null /*data*/); } } else { Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, @@ -224,4 +222,12 @@ public class FontFamily_Delegate { return null; } + + + // ---- Public helper class ---- + + public enum FontVariant { + // The order needs to be kept in sync with android.graphics.FontFamily. + NONE, COMPACT, ELEGANT + } } diff --git a/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java index 911f4e7af94a..6ee307eeb92a 100644 --- a/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java @@ -21,6 +21,7 @@ import com.android.layoutlib.bridge.Bridge; import com.android.layoutlib.bridge.impl.DelegateManager; import com.android.tools.layoutlib.annotations.LayoutlibDelegate; +import android.graphics.FontFamily_Delegate.FontVariant; import android.graphics.Paint.FontMetrics; import android.graphics.Paint.FontMetricsInt; import android.text.TextUtils; @@ -30,7 +31,6 @@ import java.awt.Font; import java.awt.Shape; import java.awt.Stroke; import java.awt.Toolkit; -import java.awt.font.FontRenderContext; import java.awt.geom.AffineTransform; import java.util.ArrayList; import java.util.Collections; @@ -81,7 +81,8 @@ public class Paint_Delegate { private float mTextScaleX; private float mTextSkewX; private int mHintingMode = Paint.HINTING_ON; - private boolean mIsCompact = true; + // Variant of the font. + private FontVariant mFontVariant = FontVariant.NONE; private Xfermode_Delegate mXfermode; private ColorFilter_Delegate mColorFilter; @@ -92,6 +93,8 @@ public class Paint_Delegate { private Locale mLocale = Locale.getDefault(); + // Used only to assert invariants. + public long mNativeTypeface; // ---- Public Helper methods ---- @@ -437,7 +440,7 @@ public class Paint_Delegate { /*package*/ static boolean isElegantTextHeight(Paint thisPaint) { // get the delegate from the native int. Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); - return delegate != null && !delegate.mIsCompact; + return delegate != null && delegate.mFontVariant == FontVariant.ELEGANT; } @LayoutlibDelegate @@ -448,7 +451,7 @@ public class Paint_Delegate { return; } - delegate.mIsCompact = !elegant; + delegate.mFontVariant = elegant ? FontVariant.ELEGANT : FontVariant.COMPACT; } @LayoutlibDelegate @@ -887,6 +890,7 @@ public class Paint_Delegate { } delegate.mTypeface = Typeface_Delegate.getDelegate(typeface); + delegate.mNativeTypeface = typeface; delegate.updateFontObject(); return typeface; } @@ -965,15 +969,10 @@ public class Paint_Delegate { } @LayoutlibDelegate - /*package*/ static float native_getTextRunAdvances(long native_object, - long native_typeface /*ignored*/, + /*package*/ static float native_getTextRunAdvances(long native_object, long native_typeface, char[] text, int index, int count, int contextIndex, int contextCount, int flags, float[] advances, int advancesIndex) { - // native_typeface is passed here since Framework's old implementation did not have the - // typeface object associated with the Paint. Since, we follow the new framework way, - // we store the typeface with the paint and use it directly. - if (advances != null) for (int i = advancesIndex; i< advancesIndex+count; i++) advances[i]=0; @@ -982,6 +981,12 @@ public class Paint_Delegate { if (delegate == null) { return 0.f; } + + // native_typeface is passed here since Framework's old implementation did not have the + // typeface object associated with the Paint. Since, we follow the new framework way, + // we store the typeface with the paint and use it directly. + assert (native_typeface == delegate.mNativeTypeface); + boolean isRtl = isRtl(flags); int limit = index + count; @@ -1022,37 +1027,41 @@ public class Paint_Delegate { } @LayoutlibDelegate - /*package*/ static void native_getTextPath(long native_object, int bidiFlags, - char[] text, int index, int count, float x, float y, long path) { + /*package*/ static void native_getTextPath(long native_object, long native_typeface, + int bidiFlags, char[] text, int index, int count, float x, float y, long path) { // FIXME Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, "Paint.getTextPath is not supported.", null, null /*data*/); } @LayoutlibDelegate - /*package*/ static void native_getTextPath(long native_object, int bidiFlags, - String text, int start, int end, float x, float y, long path) { + /*package*/ static void native_getTextPath(long native_object, long native_typeface, + int bidiFlags, String text, int start, int end, float x, float y, long path) { // FIXME Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, "Paint.getTextPath is not supported.", null, null /*data*/); } @LayoutlibDelegate - /*package*/ static void nativeGetStringBounds(long nativePaint, String text, int start, - int end, int bidiFlags, Rect bounds) { - nativeGetCharArrayBounds(nativePaint, text.toCharArray(), start, end - start, bidiFlags, - bounds); + /*package*/ static void nativeGetStringBounds(long nativePaint, long native_typeface, + String text, int start, int end, int bidiFlags, Rect bounds) { + nativeGetCharArrayBounds(nativePaint, native_typeface, text.toCharArray(), start, + end - start, bidiFlags, bounds); } @LayoutlibDelegate - /*package*/ static void nativeGetCharArrayBounds(long nativePaint, char[] text, int index, - int count, int bidiFlags, Rect bounds) { + /*package*/ static void nativeGetCharArrayBounds(long nativePaint, long native_typeface, + char[] text, int index, int count, int bidiFlags, Rect bounds) { // get the delegate from the native int. Paint_Delegate delegate = sManager.getDelegate(nativePaint); if (delegate == null) { return; } + + // assert that the typeface passed is actually the one that we had stored. + assert (native_typeface == delegate.mNativeTypeface); + delegate.measureText(text, index, count, isRtl(bidiFlags)).roundOut(bounds); } @@ -1079,6 +1088,7 @@ public class Paint_Delegate { mJoin = paint.mJoin; mTextAlign = paint.mTextAlign; mTypeface = paint.mTypeface; + mNativeTypeface = paint.mNativeTypeface; mStrokeWidth = paint.mStrokeWidth; mStrokeMiter = paint.mStrokeMiter; mTextSize = paint.mTextSize; @@ -1102,6 +1112,7 @@ public class Paint_Delegate { mJoin = Paint.Join.MITER.nativeInt; mTextAlign = 0; mTypeface = Typeface_Delegate.getDelegate(Typeface.sDefaults[0].native_instance); + mNativeTypeface = 0; mStrokeWidth = 1.f; mStrokeMiter = 4.f; mTextSize = 20.f; @@ -1124,7 +1135,7 @@ public class Paint_Delegate { private void updateFontObject() { if (mTypeface != null) { // Get the fonts from the TypeFace object. - List<Font> fonts = mTypeface.getFonts(mIsCompact); + List<Font> fonts = mTypeface.getFonts(mFontVariant); // create new font objects as well as FontMetrics, based on the current text size // and skew info. diff --git a/tools/layoutlib/bridge/src/android/graphics/Typeface_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Typeface_Delegate.java index 9746b48faf05..908bb642bc8d 100644 --- a/tools/layoutlib/bridge/src/android/graphics/Typeface_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/Typeface_Delegate.java @@ -22,6 +22,7 @@ import com.android.layoutlib.bridge.impl.DelegateManager; import com.android.tools.layoutlib.annotations.LayoutlibDelegate; import android.content.res.AssetManager; +import android.graphics.FontFamily_Delegate.FontVariant; import java.awt.Font; import java.io.File; @@ -69,17 +70,28 @@ public final class Typeface_Delegate { return sManager.getDelegate(nativeTypeface); } - public List<Font> getFonts(boolean compact) { + public List<Font> getFonts(FontVariant variant) { List<Font> fonts = new ArrayList<Font>(mFontFamilies.length); + // If we are unable to find fonts matching the variant, we return the fonts from the + // other variant since we always want to draw something, rather than nothing. + // TODO: check this behaviour with platform. + List<Font> otherVariantFonts = new ArrayList<Font>(); for (FontFamily_Delegate ffd : mFontFamilies) { if (ffd != null) { - Font font = ffd.getFont(mStyle, compact); + Font font = ffd.getFont(mStyle); if (font != null) { - fonts.add(font); + if (ffd.getVariant() == variant || ffd.getVariant() == FontVariant.NONE) { + fonts.add(font); + } else { + otherVariantFonts.add(font); + } } } } - return fonts; + if (fonts.size() > 0) { + return fonts; + } + return otherVariantFonts; } // ---- native methods ---- diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java index ffab4ded0db1..cc69af278d5d 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java @@ -214,7 +214,8 @@ public final class Bridge extends com.android.ide.common.rendering.api.Bridge { Capability.EXTENDED_VIEWINFO, Capability.FIXED_SCALABLE_NINE_PATCH, Capability.RTL, - Capability.ACTION_BAR); + Capability.ACTION_BAR, + Capability.SIMULATE_PLATFORM); BridgeAssetManager.initSystem(); |