Merge changes I50d2903e,Ieb347ce3
* changes:
Add DPMS delegation scopes.
Add block uninstall delegation in DPMS.
diff --git a/api/current.txt b/api/current.txt
index 9538253..faee5aa 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -739,6 +739,7 @@
field public static final int isScrollContainer = 16843342; // 0x101024e
field public static final int isSticky = 16843335; // 0x1010247
field public static final int isolatedProcess = 16843689; // 0x10103a9
+ field public static final int isolatedSplits = 16844109; // 0x101054d
field public static final int itemBackground = 16843056; // 0x1010130
field public static final int itemIconDisabledAlpha = 16843057; // 0x1010131
field public static final int itemPadding = 16843565; // 0x101032d
@@ -3067,8 +3068,10 @@
public static abstract interface Animator.AnimatorListener {
method public abstract void onAnimationCancel(android.animation.Animator);
+ method public default void onAnimationEnd(android.animation.Animator, boolean);
method public abstract void onAnimationEnd(android.animation.Animator);
method public abstract void onAnimationRepeat(android.animation.Animator);
+ method public default void onAnimationStart(android.animation.Animator, boolean);
method public abstract void onAnimationStart(android.animation.Animator);
}
@@ -3104,6 +3107,8 @@
method public void playSequentially(java.util.List<android.animation.Animator>);
method public void playTogether(android.animation.Animator...);
method public void playTogether(java.util.Collection<android.animation.Animator>);
+ method public void reverse();
+ method public void setCurrentPlayTime(long);
method public android.animation.AnimatorSet setDuration(long);
method public void setInterpolator(android.animation.TimeInterpolator);
method public void setStartDelay(long);
@@ -5017,6 +5022,7 @@
method public android.graphics.drawable.Icon getLargeIcon();
method public android.graphics.drawable.Icon getSmallIcon();
method public java.lang.String getSortKey();
+ method public long getTimeout();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.media.AudioAttributes AUDIO_ATTRIBUTES_DEFAULT;
field public static final java.lang.String CATEGORY_ALARM = "alarm";
@@ -5245,6 +5251,7 @@
method public android.app.Notification.Builder setSubText(java.lang.CharSequence);
method public android.app.Notification.Builder setTicker(java.lang.CharSequence);
method public deprecated android.app.Notification.Builder setTicker(java.lang.CharSequence, android.widget.RemoteViews);
+ method public android.app.Notification.Builder setTimeout(long);
method public android.app.Notification.Builder setUsesChronometer(boolean);
method public android.app.Notification.Builder setVibrate(long[]);
method public android.app.Notification.Builder setVisibility(int);
@@ -8367,6 +8374,7 @@
method public abstract int checkUriPermission(android.net.Uri, java.lang.String, java.lang.String, int, int, int);
method public abstract deprecated void clearWallpaper() throws java.io.IOException;
method public abstract android.content.Context createConfigurationContext(android.content.res.Configuration);
+ method public abstract android.content.Context createContextForSplit(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException;
method public abstract android.content.Context createDeviceProtectedStorageContext();
method public abstract android.content.Context createDisplayContext(android.view.Display);
method public abstract android.content.Context createPackageContext(java.lang.String, int) throws android.content.pm.PackageManager.NameNotFoundException;
@@ -8565,6 +8573,7 @@
method public int checkUriPermission(android.net.Uri, java.lang.String, java.lang.String, int, int, int);
method public deprecated void clearWallpaper() throws java.io.IOException;
method public android.content.Context createConfigurationContext(android.content.res.Configuration);
+ method public android.content.Context createContextForSplit(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException;
method public android.content.Context createDeviceProtectedStorageContext();
method public android.content.Context createDisplayContext(android.view.Display);
method public android.content.Context createPackageContext(java.lang.String, int) throws android.content.pm.PackageManager.NameNotFoundException;
@@ -9749,6 +9758,7 @@
field public int requiresSmallestWidthDp;
field public java.lang.String[] sharedLibraryFiles;
field public java.lang.String sourceDir;
+ field public java.lang.String[] splitNames;
field public java.lang.String[] splitPublicSourceDirs;
field public java.lang.String[] splitSourceDirs;
field public int targetSdkVersion;
@@ -9831,6 +9841,7 @@
field public boolean handleProfiling;
field public java.lang.String publicSourceDir;
field public java.lang.String sourceDir;
+ field public java.lang.String[] splitNames;
field public java.lang.String[] splitPublicSourceDirs;
field public java.lang.String[] splitSourceDirs;
field public java.lang.String targetPackage;
@@ -14196,9 +14207,10 @@
method public int getWidth();
method public boolean isDestroyed();
method public void writeToParcel(android.os.Parcel, int);
+ field public static final int BLOB = 33; // 0x21
field public static final android.os.Parcelable.Creator<android.hardware.HardwareBuffer> CREATOR;
field public static final int RGBA_8888 = 1; // 0x1
- field public static final int RGBA_FP16 = 5; // 0x5
+ field public static final int RGBA_FP16 = 22; // 0x16
field public static final int RGBX_8888 = 2; // 0x2
field public static final int RGB_565 = 4; // 0x4
field public static final int RGB_888 = 3; // 0x3
@@ -29979,6 +29991,7 @@
method public final android.util.SizeF readSizeF();
method public final android.util.SparseArray readSparseArray(java.lang.ClassLoader);
method public final android.util.SparseBooleanArray readSparseBooleanArray();
+ method public final android.util.SparseIntArray readSparseIntArray();
method public final java.lang.String readString();
method public final void readStringArray(java.lang.String[]);
method public final void readStringList(java.util.List<java.lang.String>);
@@ -30023,6 +30036,7 @@
method public final void writeSizeF(android.util.SizeF);
method public final void writeSparseArray(android.util.SparseArray<java.lang.Object>);
method public final void writeSparseBooleanArray(android.util.SparseBooleanArray);
+ method public final void writeSparseIntArray(android.util.SparseIntArray);
method public final void writeString(java.lang.String);
method public final void writeStringArray(java.lang.String[]);
method public final void writeStringList(java.util.List<java.lang.String>);
@@ -35958,9 +35972,9 @@
field public static final int REASON_PACKAGE_SUSPENDED = 14; // 0xe
field public static final int REASON_PROFILE_TURNED_OFF = 15; // 0xf
field public static final int REASON_SNOOZED = 18; // 0x12
+ field public static final int REASON_TIMEOUT = 19; // 0x13
field public static final int REASON_UNAUTOBUNDLED = 16; // 0x10
field public static final int REASON_USER_STOPPED = 6; // 0x6
- field public static final int REASON_USER_SWITCH = 19; // 0x13
field public static final java.lang.String SERVICE_INTERFACE = "android.service.notification.NotificationListenerService";
field public static final int SUPPRESSED_EFFECT_SCREEN_OFF = 1; // 0x1
field public static final int SUPPRESSED_EFFECT_SCREEN_ON = 2; // 0x2
@@ -39264,6 +39278,7 @@
method public int checkUriPermission(android.net.Uri, java.lang.String, java.lang.String, int, int, int);
method public void clearWallpaper();
method public android.content.Context createConfigurationContext(android.content.res.Configuration);
+ method public android.content.Context createContextForSplit(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException;
method public android.content.Context createDeviceProtectedStorageContext();
method public android.content.Context createDisplayContext(android.view.Display);
method public android.content.Context createPackageContext(java.lang.String, int) throws android.content.pm.PackageManager.NameNotFoundException;
diff --git a/api/system-current.txt b/api/system-current.txt
index c649c23..510fb70 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -850,6 +850,7 @@
field public static final int isScrollContainer = 16843342; // 0x101024e
field public static final int isSticky = 16843335; // 0x1010247
field public static final int isolatedProcess = 16843689; // 0x10103a9
+ field public static final int isolatedSplits = 16844109; // 0x101054d
field public static final int itemBackground = 16843056; // 0x1010130
field public static final int itemIconDisabledAlpha = 16843057; // 0x1010131
field public static final int itemPadding = 16843565; // 0x101032d
@@ -3186,8 +3187,10 @@
public static abstract interface Animator.AnimatorListener {
method public abstract void onAnimationCancel(android.animation.Animator);
+ method public default void onAnimationEnd(android.animation.Animator, boolean);
method public abstract void onAnimationEnd(android.animation.Animator);
method public abstract void onAnimationRepeat(android.animation.Animator);
+ method public default void onAnimationStart(android.animation.Animator, boolean);
method public abstract void onAnimationStart(android.animation.Animator);
}
@@ -3223,6 +3226,8 @@
method public void playSequentially(java.util.List<android.animation.Animator>);
method public void playTogether(android.animation.Animator...);
method public void playTogether(java.util.Collection<android.animation.Animator>);
+ method public void reverse();
+ method public void setCurrentPlayTime(long);
method public android.animation.AnimatorSet setDuration(long);
method public void setInterpolator(android.animation.TimeInterpolator);
method public void setStartDelay(long);
@@ -5177,6 +5182,7 @@
method public static java.lang.Class<? extends android.app.Notification.Style> getNotificationStyleClass(java.lang.String);
method public android.graphics.drawable.Icon getSmallIcon();
method public java.lang.String getSortKey();
+ method public long getTimeout();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.media.AudioAttributes AUDIO_ATTRIBUTES_DEFAULT;
field public static final java.lang.String CATEGORY_ALARM = "alarm";
@@ -5407,6 +5413,7 @@
method public android.app.Notification.Builder setSubText(java.lang.CharSequence);
method public android.app.Notification.Builder setTicker(java.lang.CharSequence);
method public deprecated android.app.Notification.Builder setTicker(java.lang.CharSequence, android.widget.RemoteViews);
+ method public android.app.Notification.Builder setTimeout(long);
method public android.app.Notification.Builder setUsesChronometer(boolean);
method public android.app.Notification.Builder setVibrate(long[]);
method public android.app.Notification.Builder setVisibility(int);
@@ -8749,6 +8756,7 @@
method public abstract int checkUriPermission(android.net.Uri, java.lang.String, java.lang.String, int, int, int);
method public abstract deprecated void clearWallpaper() throws java.io.IOException;
method public abstract android.content.Context createConfigurationContext(android.content.res.Configuration);
+ method public abstract android.content.Context createContextForSplit(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException;
method public abstract android.content.Context createCredentialProtectedStorageContext();
method public abstract android.content.Context createDeviceProtectedStorageContext();
method public abstract android.content.Context createDisplayContext(android.view.Display);
@@ -8959,6 +8967,7 @@
method public int checkUriPermission(android.net.Uri, java.lang.String, java.lang.String, int, int, int);
method public deprecated void clearWallpaper() throws java.io.IOException;
method public android.content.Context createConfigurationContext(android.content.res.Configuration);
+ method public android.content.Context createContextForSplit(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException;
method public android.content.Context createCredentialProtectedStorageContext();
method public android.content.Context createDeviceProtectedStorageContext();
method public android.content.Context createDisplayContext(android.view.Display);
@@ -10164,6 +10173,7 @@
field public int requiresSmallestWidthDp;
field public java.lang.String[] sharedLibraryFiles;
field public java.lang.String sourceDir;
+ field public java.lang.String[] splitNames;
field public java.lang.String[] splitPublicSourceDirs;
field public java.lang.String[] splitSourceDirs;
field public int targetSdkVersion;
@@ -10281,6 +10291,7 @@
field public boolean handleProfiling;
field public java.lang.String publicSourceDir;
field public java.lang.String sourceDir;
+ field public java.lang.String[] splitNames;
field public java.lang.String[] splitPublicSourceDirs;
field public java.lang.String[] splitSourceDirs;
field public java.lang.String targetPackage;
@@ -14754,9 +14765,10 @@
method public int getWidth();
method public boolean isDestroyed();
method public void writeToParcel(android.os.Parcel, int);
+ field public static final int BLOB = 33; // 0x21
field public static final android.os.Parcelable.Creator<android.hardware.HardwareBuffer> CREATOR;
field public static final int RGBA_8888 = 1; // 0x1
- field public static final int RGBA_FP16 = 5; // 0x5
+ field public static final int RGBA_FP16 = 22; // 0x16
field public static final int RGBX_8888 = 2; // 0x2
field public static final int RGB_565 = 4; // 0x4
field public static final int RGB_888 = 3; // 0x3
@@ -32695,6 +32707,7 @@
method public final android.util.SizeF readSizeF();
method public final android.util.SparseArray readSparseArray(java.lang.ClassLoader);
method public final android.util.SparseBooleanArray readSparseBooleanArray();
+ method public final android.util.SparseIntArray readSparseIntArray();
method public final java.lang.String readString();
method public final void readStringArray(java.lang.String[]);
method public final void readStringList(java.util.List<java.lang.String>);
@@ -32739,6 +32752,7 @@
method public final void writeSizeF(android.util.SizeF);
method public final void writeSparseArray(android.util.SparseArray<java.lang.Object>);
method public final void writeSparseBooleanArray(android.util.SparseBooleanArray);
+ method public final void writeSparseIntArray(android.util.SparseIntArray);
method public final void writeString(java.lang.String);
method public final void writeStringArray(java.lang.String[]);
method public final void writeStringList(java.util.List<java.lang.String>);
@@ -38979,9 +38993,9 @@
field public static final int REASON_PACKAGE_SUSPENDED = 14; // 0xe
field public static final int REASON_PROFILE_TURNED_OFF = 15; // 0xf
field public static final int REASON_SNOOZED = 18; // 0x12
+ field public static final int REASON_TIMEOUT = 19; // 0x13
field public static final int REASON_UNAUTOBUNDLED = 16; // 0x10
field public static final int REASON_USER_STOPPED = 6; // 0x6
- field public static final int REASON_USER_SWITCH = 19; // 0x13
field public static final java.lang.String SERVICE_INTERFACE = "android.service.notification.NotificationListenerService";
field public static final int SUPPRESSED_EFFECT_SCREEN_OFF = 1; // 0x1
field public static final int SUPPRESSED_EFFECT_SCREEN_ON = 2; // 0x2
@@ -42627,6 +42641,7 @@
method public int checkUriPermission(android.net.Uri, java.lang.String, java.lang.String, int, int, int);
method public void clearWallpaper();
method public android.content.Context createConfigurationContext(android.content.res.Configuration);
+ method public android.content.Context createContextForSplit(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException;
method public android.content.Context createCredentialProtectedStorageContext();
method public android.content.Context createDeviceProtectedStorageContext();
method public android.content.Context createDisplayContext(android.view.Display);
diff --git a/api/test-current.txt b/api/test-current.txt
index ab421ce..d16be73 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -739,6 +739,7 @@
field public static final int isScrollContainer = 16843342; // 0x101024e
field public static final int isSticky = 16843335; // 0x1010247
field public static final int isolatedProcess = 16843689; // 0x10103a9
+ field public static final int isolatedSplits = 16844109; // 0x101054d
field public static final int itemBackground = 16843056; // 0x1010130
field public static final int itemIconDisabledAlpha = 16843057; // 0x1010131
field public static final int itemPadding = 16843565; // 0x101032d
@@ -3067,8 +3068,10 @@
public static abstract interface Animator.AnimatorListener {
method public abstract void onAnimationCancel(android.animation.Animator);
+ method public default void onAnimationEnd(android.animation.Animator, boolean);
method public abstract void onAnimationEnd(android.animation.Animator);
method public abstract void onAnimationRepeat(android.animation.Animator);
+ method public default void onAnimationStart(android.animation.Animator, boolean);
method public abstract void onAnimationStart(android.animation.Animator);
}
@@ -3104,6 +3107,8 @@
method public void playSequentially(java.util.List<android.animation.Animator>);
method public void playTogether(android.animation.Animator...);
method public void playTogether(java.util.Collection<android.animation.Animator>);
+ method public void reverse();
+ method public void setCurrentPlayTime(long);
method public android.animation.AnimatorSet setDuration(long);
method public void setInterpolator(android.animation.TimeInterpolator);
method public void setStartDelay(long);
@@ -5027,6 +5032,7 @@
method public android.graphics.drawable.Icon getLargeIcon();
method public android.graphics.drawable.Icon getSmallIcon();
method public java.lang.String getSortKey();
+ method public long getTimeout();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.media.AudioAttributes AUDIO_ATTRIBUTES_DEFAULT;
field public static final java.lang.String CATEGORY_ALARM = "alarm";
@@ -5255,6 +5261,7 @@
method public android.app.Notification.Builder setSubText(java.lang.CharSequence);
method public android.app.Notification.Builder setTicker(java.lang.CharSequence);
method public deprecated android.app.Notification.Builder setTicker(java.lang.CharSequence, android.widget.RemoteViews);
+ method public android.app.Notification.Builder setTimeout(long);
method public android.app.Notification.Builder setUsesChronometer(boolean);
method public android.app.Notification.Builder setVibrate(long[]);
method public android.app.Notification.Builder setVisibility(int);
@@ -8390,6 +8397,7 @@
method public abstract int checkUriPermission(android.net.Uri, java.lang.String, java.lang.String, int, int, int);
method public abstract deprecated void clearWallpaper() throws java.io.IOException;
method public abstract android.content.Context createConfigurationContext(android.content.res.Configuration);
+ method public abstract android.content.Context createContextForSplit(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException;
method public abstract android.content.Context createDeviceProtectedStorageContext();
method public abstract android.content.Context createDisplayContext(android.view.Display);
method public abstract android.content.Context createPackageContext(java.lang.String, int) throws android.content.pm.PackageManager.NameNotFoundException;
@@ -8589,6 +8597,7 @@
method public int checkUriPermission(android.net.Uri, java.lang.String, java.lang.String, int, int, int);
method public deprecated void clearWallpaper() throws java.io.IOException;
method public android.content.Context createConfigurationContext(android.content.res.Configuration);
+ method public android.content.Context createContextForSplit(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException;
method public android.content.Context createDeviceProtectedStorageContext();
method public android.content.Context createDisplayContext(android.view.Display);
method public android.content.Context createPackageContext(java.lang.String, int) throws android.content.pm.PackageManager.NameNotFoundException;
@@ -9776,6 +9785,7 @@
field public int requiresSmallestWidthDp;
field public java.lang.String[] sharedLibraryFiles;
field public java.lang.String sourceDir;
+ field public java.lang.String[] splitNames;
field public java.lang.String[] splitPublicSourceDirs;
field public java.lang.String[] splitSourceDirs;
field public int targetSdkVersion;
@@ -9858,6 +9868,7 @@
field public boolean handleProfiling;
field public java.lang.String publicSourceDir;
field public java.lang.String sourceDir;
+ field public java.lang.String[] splitNames;
field public java.lang.String[] splitPublicSourceDirs;
field public java.lang.String[] splitSourceDirs;
field public java.lang.String targetPackage;
@@ -14228,9 +14239,10 @@
method public int getWidth();
method public boolean isDestroyed();
method public void writeToParcel(android.os.Parcel, int);
+ field public static final int BLOB = 33; // 0x21
field public static final android.os.Parcelable.Creator<android.hardware.HardwareBuffer> CREATOR;
field public static final int RGBA_8888 = 1; // 0x1
- field public static final int RGBA_FP16 = 5; // 0x5
+ field public static final int RGBA_FP16 = 22; // 0x16
field public static final int RGBX_8888 = 2; // 0x2
field public static final int RGB_565 = 4; // 0x4
field public static final int RGB_888 = 3; // 0x3
@@ -30090,6 +30102,7 @@
method public final android.util.SizeF readSizeF();
method public final android.util.SparseArray readSparseArray(java.lang.ClassLoader);
method public final android.util.SparseBooleanArray readSparseBooleanArray();
+ method public final android.util.SparseIntArray readSparseIntArray();
method public final java.lang.String readString();
method public final void readStringArray(java.lang.String[]);
method public final void readStringList(java.util.List<java.lang.String>);
@@ -30134,6 +30147,7 @@
method public final void writeSizeF(android.util.SizeF);
method public final void writeSparseArray(android.util.SparseArray<java.lang.Object>);
method public final void writeSparseBooleanArray(android.util.SparseBooleanArray);
+ method public final void writeSparseIntArray(android.util.SparseIntArray);
method public final void writeString(java.lang.String);
method public final void writeStringArray(java.lang.String[]);
method public final void writeStringList(java.util.List<java.lang.String>);
@@ -36091,9 +36105,9 @@
field public static final int REASON_PACKAGE_SUSPENDED = 14; // 0xe
field public static final int REASON_PROFILE_TURNED_OFF = 15; // 0xf
field public static final int REASON_SNOOZED = 18; // 0x12
+ field public static final int REASON_TIMEOUT = 19; // 0x13
field public static final int REASON_UNAUTOBUNDLED = 16; // 0x10
field public static final int REASON_USER_STOPPED = 6; // 0x6
- field public static final int REASON_USER_SWITCH = 19; // 0x13
field public static final java.lang.String SERVICE_INTERFACE = "android.service.notification.NotificationListenerService";
field public static final int SUPPRESSED_EFFECT_SCREEN_OFF = 1; // 0x1
field public static final int SUPPRESSED_EFFECT_SCREEN_ON = 2; // 0x2
@@ -39397,6 +39411,7 @@
method public int checkUriPermission(android.net.Uri, java.lang.String, java.lang.String, int, int, int);
method public void clearWallpaper();
method public android.content.Context createConfigurationContext(android.content.res.Configuration);
+ method public android.content.Context createContextForSplit(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException;
method public android.content.Context createDeviceProtectedStorageContext();
method public android.content.Context createDisplayContext(android.view.Display);
method public android.content.Context createPackageContext(java.lang.String, int) throws android.content.pm.PackageManager.NameNotFoundException;
diff --git a/cmds/pm/src/com/android/commands/pm/Pm.java b/cmds/pm/src/com/android/commands/pm/Pm.java
index ac5fea3..7015381 100644
--- a/cmds/pm/src/com/android/commands/pm/Pm.java
+++ b/cmds/pm/src/com/android/commands/pm/Pm.java
@@ -408,7 +408,7 @@
if (file.isFile()) {
try {
ApkLite baseApk = PackageParser.parseApkLite(file, 0);
- PackageLite pkgLite = new PackageLite(null, baseApk, null, null, null);
+ PackageLite pkgLite = new PackageLite(null, baseApk, null, null, null, null);
params.sessionParams.setSize(
PackageHelper.calculateInstalledSize(pkgLite, false,
params.sessionParams.abiOverride));
diff --git a/core/java/android/animation/AnimationHandler.java b/core/java/android/animation/AnimationHandler.java
index 95262ab..e2e5a8f 100644
--- a/core/java/android/animation/AnimationHandler.java
+++ b/core/java/android/animation/AnimationHandler.java
@@ -276,8 +276,9 @@
* Run animation based on the frame time.
* @param frameTime The frame start time, in the {@link SystemClock#uptimeMillis()} time
* base.
+ * @return if the animation has finished.
*/
- void doAnimationFrame(long frameTime);
+ boolean doAnimationFrame(long frameTime);
/**
* This notifies the callback of frame commit time. Frame commit time is the time after
diff --git a/core/java/android/animation/Animator.java b/core/java/android/animation/Animator.java
index c51725a..634dc1fd 100644
--- a/core/java/android/animation/Animator.java
+++ b/core/java/android/animation/Animator.java
@@ -26,7 +26,7 @@
* This is the superclass for classes which provide basic support for animations which can be
* started, ended, and have <code>AnimatorListeners</code> added to them.
*/
-public abstract class Animator implements Cloneable {
+public abstract class Animator implements Cloneable, AnimationHandler.AnimationFrameCallback {
/**
* The value used to indicate infinite duration (e.g. when Animators repeat infinitely).
@@ -465,11 +465,102 @@
}
/**
+ * @hide
+ */
+ @Override
+ public boolean doAnimationFrame(long frameTime) {
+ // TODO: Need to find a better signal than this
+ return getDuration() + getStartDelay() >= frameTime;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public void commitAnimationFrame(long frameTime) {}
+
+
+ /**
+ * Internal use only.
+ * This call starts the animation in regular or reverse direction without requiring them to
+ * register frame callbacks. The caller will be responsible for all the subsequent animation
+ * pulses. Specifically, the caller needs to call doAnimationFrame(...) for the animation on
+ * every frame.
+ *
+ * @param inReverse whether the animation should play in reverse direction
+ */
+ void startWithoutPulsing(boolean inReverse) {
+ if (inReverse) {
+ reverse();
+ } else {
+ start();
+ }
+ }
+
+ /**
+ * Internal use only.
+ * Skips the animation value to end/start, depending on whether the play direction is forward
+ * or backward.
+ *
+ * @param inReverse whether the end value is based on a reverse direction. If yes, this is
+ * equivalent to skip to start value in a forward playing direction.
+ */
+ void skipToEndValue(boolean inReverse) {}
+
+
+ /**
+ * Internal use only.
+ *
+ * Returns whether the animation has start/end values setup. For most of the animations, this
+ * should always be true. For ObjectAnimators, the start values are setup in the initialization
+ * of the animation.
+ */
+ boolean isInitialized() {
+ return true;
+ }
+
+ /**
+ * Internal use only.
+ */
+ void animateBasedOnPlayTime(long currentPlayTime, long lastPlayTime, boolean inReverse) {}
+
+ /**
* <p>An animation listener receives notifications from an animation.
* Notifications indicate animation related events, such as the end or the
* repetition of the animation.</p>
*/
public static interface AnimatorListener {
+
+ /**
+ * <p>Notifies the start of the animation as well as the animation's overall play direction.
+ * This method's default behavior is to call {@link #onAnimationStart(Animator)}. This
+ * method can be overridden, though not required, to get the additional play direction info
+ * when an animation starts. Skipping calling super when overriding this method results in
+ * {@link #onAnimationStart(Animator)} not getting called.
+ *
+ * @param animation The started animation.
+ * @param isReverse Whether the animation is playing in reverse.
+ */
+ default void onAnimationStart(Animator animation, boolean isReverse) {
+ onAnimationStart(animation);
+ }
+
+ /**
+ * <p>Notifies the end of the animation. This callback is not invoked
+ * for animations with repeat count set to INFINITE.</p>
+ *
+ * <p>This method's default behavior is to call {@link #onAnimationEnd(Animator)}. This
+ * method can be overridden, though not required, to get the additional play direction info
+ * when an animation ends. Skipping calling super when overriding this method results in
+ * {@link #onAnimationEnd(Animator)} not getting called.
+ *
+ * @param animation The animation which reached its end.
+ * @param isReverse Whether the animation is playing in reverse.
+ */
+ default void onAnimationEnd(Animator animation, boolean isReverse) {
+ onAnimationEnd(animation);
+ }
+
/**
* <p>Notifies the start of the animation.</p>
*
diff --git a/core/java/android/animation/AnimatorSet.java b/core/java/android/animation/AnimatorSet.java
index a904f9d..d5814a3 100644
--- a/core/java/android/animation/AnimatorSet.java
+++ b/core/java/android/animation/AnimatorSet.java
@@ -19,11 +19,15 @@
import android.app.ActivityThread;
import android.app.Application;
import android.os.Build;
+import android.os.Looper;
+import android.util.AndroidRuntimeException;
import android.util.ArrayMap;
import android.util.Log;
+import android.view.animation.Animation;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Comparator;
import java.util.List;
/**
@@ -52,7 +56,7 @@
* Animation</a> developer guide.</p>
* </div>
*/
-public final class AnimatorSet extends Animator {
+public final class AnimatorSet extends Animator implements AnimationHandler.AnimationFrameCallback {
private static final String TAG = "AnimatorSet";
/**
@@ -66,7 +70,7 @@
* Tracks animations currently being played, so that we know what to
* cancel or end when cancel() or end() is called on this AnimatorSet
*/
- private ArrayList<Animator> mPlayingSet = new ArrayList<Animator>();
+ private ArrayList<Node> mPlayingSet = new ArrayList<Node>();
/**
* Contains all nodes, mapped to their respective Animators. When new
@@ -77,6 +81,11 @@
private ArrayMap<Animator, Node> mNodeMap = new ArrayMap<Animator, Node>();
/**
+ * Contains the start and end events of all the nodes. All these events are sorted in this list.
+ */
+ private ArrayList<AnimationEvent> mEvents = new ArrayList<>();
+
+ /**
* Set of all nodes created for this AnimatorSet. This list is used upon
* starting the set, and the nodes are placed in sorted order into the
* sortedNodes collection.
@@ -84,21 +93,6 @@
private ArrayList<Node> mNodes = new ArrayList<Node>();
/**
- * Animator Listener that tracks the lifecycle of each Animator in the set. It will be added
- * to each Animator before they start and removed after they end.
- */
- private AnimatorSetListener mSetListener = new AnimatorSetListener(this);
-
- /**
- * Flag indicating that the AnimatorSet has been manually
- * terminated (by calling cancel() or end()).
- * This flag is used to avoid starting other animations when currently-playing
- * child animations of this AnimatorSet end. It also determines whether cancel/end
- * notifications are sent out via the normal AnimatorSetListener mechanism.
- */
- private boolean mTerminated = false;
-
- /**
* Tracks whether any change has been made to the AnimatorSet, which is then used to
* determine whether the dependency graph should be re-constructed.
*/
@@ -131,8 +125,6 @@
// was set on this AnimatorSet, so it should not be passed down to the children.
private TimeInterpolator mInterpolator = null;
- // Whether the AnimatorSet can be reversed.
- private boolean mReversible = true;
// The total duration of finishing all the Animators in the set.
private long mTotalDuration = 0;
@@ -142,6 +134,46 @@
// the animator set and immediately end it for N and forward.
private final boolean mShouldIgnoreEndWithoutStart;
+ // In pre-O releases, calling start() doesn't reset all the animators values to start values.
+ // As a result, the start of the animation is inconsistent with what setCurrentPlayTime(0) would
+ // look like on O. Also it is inconsistent with what reverse() does on O, as reverse would
+ // advance all the animations to the right beginning values for before starting to reverse.
+ // From O and forward, we will add an additional step of resetting the animation values (unless
+ // the animation was previously seeked and therefore doesn't start from the beginning).
+ private final boolean mShouldResetValuesAtStart;
+
+ // The time, in milliseconds, when last frame of the animation came in. -1 when the animation is
+ // not running.
+ private long mLastFrameTime = -1;
+
+ // The time, in milliseconds, when the first frame of the animation came in.
+ // -1 when the animation is not running.
+ private long mFirstFrame = -1;
+
+ // The time, in milliseconds, when the first frame of the animation came in.
+ // -1 when the animation is not running.
+ private int mLastEventId = -1;
+
+ // Indicates whether the animation is reversing.
+ private boolean mReversing = false;
+
+ // Indicates whether the animation should register frame callbacks. If false, the animation will
+ // passively wait for an AnimatorSet to pulse it.
+ private boolean mSelfPulse = true;
+
+ // SeekState stores the last seeked play time as well as seek direction.
+ private SeekState mSeekState = new SeekState();
+
+ // Indicates where children animators are all initialized with their start values captured.
+ private boolean mChildrenInitialized = false;
+
+ /**
+ * Set on the next frame after pause() is called, used to calculate a new startTime
+ * or delayStartTime which allows the animator set to continue from the point at which
+ * it was paused. If negative, has not yet been set.
+ */
+ private long mPauseTime = -1;
+
public AnimatorSet() {
super();
mNodeMap.put(mDelayAnim, mRootNode);
@@ -150,10 +182,19 @@
Application app = ActivityThread.currentApplication();
if (app == null || app.getApplicationInfo() == null) {
mShouldIgnoreEndWithoutStart = true;
- } else if (app.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N) {
- mShouldIgnoreEndWithoutStart = true;
+ mShouldResetValuesAtStart = false;
} else {
- mShouldIgnoreEndWithoutStart = false;
+ if (app.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N) {
+ mShouldIgnoreEndWithoutStart = true;
+ } else {
+ mShouldIgnoreEndWithoutStart = false;
+ }
+
+ if (app.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.O) {
+ mShouldResetValuesAtStart = false;
+ } else {
+ mShouldResetValuesAtStart = true;
+ }
}
}
@@ -206,7 +247,6 @@
if (items.length == 1) {
play(items[0]);
} else {
- mReversible = false;
for (int i = 0; i < items.length - 1; ++i) {
play(items[i]).before(items[i + 1]);
}
@@ -225,7 +265,6 @@
if (items.size() == 1) {
play(items.get(0));
} else {
- mReversible = false;
for (int i = 0; i < items.size() - 1; ++i) {
play(items.get(i)).before(items.get(i + 1));
}
@@ -350,7 +389,9 @@
@SuppressWarnings("unchecked")
@Override
public void cancel() {
- mTerminated = true;
+ if (Looper.myLooper() == null) {
+ throw new AndroidRuntimeException("Animators may only be run on Looper threads");
+ }
if (isStarted()) {
ArrayList<AnimatorListener> tmpListeners = null;
if (mListeners != null) {
@@ -360,18 +401,13 @@
tmpListeners.get(i).onAnimationCancel(this);
}
}
- ArrayList<Animator> playingSet = new ArrayList<>(mPlayingSet);
+ ArrayList<Node> playingSet = new ArrayList<>(mPlayingSet);
int setSize = playingSet.size();
for (int i = 0; i < setSize; i++) {
- playingSet.get(i).cancel();
+ playingSet.get(i).mAnimation.cancel();
}
- if (tmpListeners != null) {
- int size = tmpListeners.size();
- for (int i = 0; i < size; i++) {
- tmpListeners.get(i).onAnimationEnd(this);
- }
- }
- mStarted = false;
+ mPlayingSet.clear();
+ endAnimation();
}
}
@@ -383,50 +419,40 @@
*/
@Override
public void end() {
+ if (Looper.myLooper() == null) {
+ throw new AndroidRuntimeException("Animators may only be run on Looper threads");
+ }
if (mShouldIgnoreEndWithoutStart && !isStarted()) {
return;
}
- mTerminated = true;
if (isStarted()) {
- endRemainingAnimations();
- }
- if (mListeners != null) {
- ArrayList<AnimatorListener> tmpListeners =
- (ArrayList<AnimatorListener>) mListeners.clone();
- for (int i = 0; i < tmpListeners.size(); i++) {
- tmpListeners.get(i).onAnimationEnd(this);
- }
- }
- mStarted = false;
- }
-
- /**
- * Iterate the animations that haven't finished or haven't started, and end them.
- */
- private void endRemainingAnimations() {
- ArrayList<Animator> remainingList = new ArrayList<Animator>(mNodes.size());
- remainingList.addAll(mPlayingSet);
-
- int index = 0;
- while (index < remainingList.size()) {
- Animator anim = remainingList.get(index);
- anim.end();
- index++;
- Node node = mNodeMap.get(anim);
- if (node.mChildNodes != null) {
- int childSize = node.mChildNodes.size();
- for (int i = 0; i < childSize; i++) {
- Node child = node.mChildNodes.get(i);
- if (child.mLatestParent != node) {
- continue;
+ // Iterate the animations that haven't finished or haven't started, and end them.
+ if (mReversing) {
+ // Between start() and first frame, mLastEventId would be unset (i.e. -1)
+ mLastEventId = mLastEventId == -1 ? mEvents.size() : mLastEventId;
+ for (int j = mLastEventId - 1; j >= 0; j--) {
+ AnimationEvent event = mEvents.get(j);
+ if (event.mEvent == AnimationEvent.ANIMATION_END) {
+ event.mNode.mAnimation.reverse();
+ } else if (event.mEvent == AnimationEvent.ANIMATION_DELAY_ENDED) {
+ event.mNode.mAnimation.end();
}
- remainingList.add(child.mAnimation);
+ }
+ } else {
+ for (int j = mLastEventId + 1; j < mEvents.size(); j++) {
+ AnimationEvent event = mEvents.get(j);
+ if (event.mEvent == AnimationEvent.ANIMATION_START) {
+ event.mNode.mAnimation.start();
+ } else if (event.mEvent == AnimationEvent.ANIMATION_END) {
+ event.mNode.mAnimation.end();
+ }
}
}
+ mPlayingSet.clear();
}
+ endAnimation();
}
-
/**
* Returns true if any of the child animations of this AnimatorSet have been started and have
* not yet ended. Child animations will not be started until the AnimatorSet has gone past
@@ -437,14 +463,12 @@
*/
@Override
public boolean isRunning() {
- int size = mNodes.size();
- for (int i = 0; i < size; i++) {
- Node node = mNodes.get(i);
- if (node != mRootNode && node.mAnimation.isStarted()) {
- return true;
- }
+ if (mStartDelay > 0) {
+ return mStarted && !mDelayAnim.isRunning();
+ } else {
+ // No start delay, animation should start right away
+ return mStarted;
}
- return false;
}
@Override
@@ -482,9 +506,6 @@
return;
}
mStartDelay = startDelay;
- if (mStartDelay > 0) {
- mReversible = false;
- }
if (!mDependencyDirty) {
// Dependency graph already constructed, update all the nodes' start/end time
int size = mNodes.size();
@@ -562,40 +583,26 @@
@Override
public void pause() {
+ if (Looper.myLooper() == null) {
+ throw new AndroidRuntimeException("Animators may only be run on Looper threads");
+ }
boolean previouslyPaused = mPaused;
super.pause();
if (!previouslyPaused && mPaused) {
- if (mDelayAnim.isStarted()) {
- // If delay hasn't passed, pause the start delay animator.
- mDelayAnim.pause();
- } else {
- int size = mNodes.size();
- for (int i = 0; i < size; i++) {
- Node node = mNodes.get(i);
- if (node != mRootNode) {
- node.mAnimation.pause();
- }
- }
- }
+ mPauseTime = -1;
}
}
@Override
public void resume() {
+ if (Looper.myLooper() == null) {
+ throw new AndroidRuntimeException("Animators may only be run on Looper threads");
+ }
boolean previouslyPaused = mPaused;
super.resume();
if (previouslyPaused && !mPaused) {
- if (mDelayAnim.isStarted()) {
- // If start delay hasn't passed, resume the previously paused start delay animator
- mDelayAnim.resume();
- } else {
- int size = mNodes.size();
- for (int i = 0; i < size; i++) {
- Node node = mNodes.get(i);
- if (node != mRootNode) {
- node.mAnimation.resume();
- }
- }
+ if (mPauseTime >= 0) {
+ addAnimationCallback(0);
}
}
}
@@ -610,9 +617,33 @@
@SuppressWarnings("unchecked")
@Override
public void start() {
- mTerminated = false;
+ start(false, true);
+ }
+
+ @Override
+ void startWithoutPulsing(boolean inReverse) {
+ start(inReverse, false);
+ }
+
+ private void initAnimation() {
+ if (mInterpolator != null) {
+ for (int i = 0; i < mNodes.size(); i++) {
+ Node node = mNodes.get(i);
+ node.mAnimation.setInterpolator(mInterpolator);
+ }
+ }
+ updateAnimatorsDuration();
+ createDependencyGraph();
+ }
+
+ private void start(boolean inReverse, boolean selfPulse) {
+ if (Looper.myLooper() == null) {
+ throw new AndroidRuntimeException("Animators may only be run on Looper threads");
+ }
mStarted = true;
+ mSelfPulse = selfPulse;
mPaused = false;
+ mPauseTime = -1;
int size = mNodes.size();
for (int i = 0; i < size; i++) {
@@ -621,26 +652,17 @@
node.mAnimation.setAllowRunningAsynchronously(false);
}
- if (mInterpolator != null) {
- for (int i = 0; i < size; i++) {
- Node node = mNodes.get(i);
- node.mAnimation.setInterpolator(mInterpolator);
- }
+ initAnimation();
+ if (inReverse && !canReverse()) {
+ throw new UnsupportedOperationException("Cannot reverse infinite AnimatorSet");
}
- updateAnimatorsDuration();
- createDependencyGraph();
+ mReversing = inReverse;
// Now that all dependencies are set up, start the animations that should be started.
- boolean setIsEmpty = false;
- if (mStartDelay > 0) {
- start(mRootNode);
- } else if (isEmptySet(this)) {
- // Set is empty or contains only empty animator sets. Skip to end in this case.
- setIsEmpty = true;
- } else {
- // No delay, but there are other animators in the set
- onChildAnimatorEnded(mDelayAnim);
+ boolean setIsEmpty = isEmptySet(this);
+ if (!setIsEmpty) {
+ startAnimation();
}
if (mListeners != null) {
@@ -648,12 +670,12 @@
(ArrayList<AnimatorListener>) mListeners.clone();
int numListeners = tmpListeners.size();
for (int i = 0; i < numListeners; ++i) {
- tmpListeners.get(i).onAnimationStart(this);
+ tmpListeners.get(i).onAnimationStart(this, inReverse);
}
}
if (setIsEmpty) {
// In the case of empty AnimatorSet, we will trigger the onAnimationEnd() right away.
- onChildAnimatorEnded(mDelayAnim);
+ end();
}
}
@@ -690,11 +712,419 @@
mDelayAnim.setDuration(mStartDelay);
}
- void start(final Node node) {
- final Animator anim = node.mAnimation;
- mPlayingSet.add(anim);
- anim.addListener(mSetListener);
- anim.start();
+ @Override
+ void skipToEndValue(boolean inReverse) {
+ if (!isInitialized()) {
+ throw new UnsupportedOperationException("Children must be initialized.");
+ }
+
+ // This makes sure the animation events are sorted an up to date.
+ initAnimation();
+
+ // Calling skip to the end in the sequence that they would be called in a forward/reverse
+ // run, such that the sequential animations modifying the same property would have
+ // the right value in the end.
+ if (inReverse) {
+ for (int i = mEvents.size() - 1; i >= 0; i--) {
+ if (mEvents.get(i).mEvent == AnimationEvent.ANIMATION_DELAY_ENDED) {
+ mEvents.get(i).mNode.mAnimation.skipToEndValue(true);
+ }
+ }
+ } else {
+ for (int i = 0; i < mEvents.size(); i++) {
+ if (mEvents.get(i).mEvent == AnimationEvent.ANIMATION_END) {
+ mEvents.get(i).mNode.mAnimation.skipToEndValue(false);
+ }
+ }
+ }
+ }
+
+ /**
+ * Internal only.
+ *
+ * This method sets the animation values based on the play time. It also fast forward or
+ * backward all the child animations progress accordingly.
+ *
+ * This method is also responsible for calling
+ * {@link android.view.animation.Animation.AnimationListener#onAnimationRepeat(Animation)},
+ * as needed, based on the last play time and current play time.
+ */
+ @Override
+ void animateBasedOnPlayTime(long currentPlayTime, long lastPlayTime, boolean inReverse) {
+ if (currentPlayTime < 0 || lastPlayTime < 0) {
+ throw new UnsupportedOperationException("Error: Play time should never be negative.");
+ }
+ // TODO: take into account repeat counts and repeat callback when repeat is implemented.
+ // Clamp currentPlayTime and lastPlayTime
+
+ // TODO: Make this more efficient
+
+ // Convert the play times to the forward direction.
+ if (inReverse) {
+ if (getTotalDuration() == DURATION_INFINITE) {
+ throw new UnsupportedOperationException("Cannot reverse AnimatorSet with infinite"
+ + " duration");
+ }
+ long duration = getTotalDuration() - mStartDelay;
+ currentPlayTime = Math.min(currentPlayTime, duration);
+ currentPlayTime = duration - currentPlayTime;
+ lastPlayTime = duration - lastPlayTime;
+ inReverse = false;
+ }
+ // Skip all values to start, and iterate mEvents to get animations to the right fraction.
+ skipToStartValue(false);
+
+ ArrayList<Node> unfinishedNodes = new ArrayList<>();
+ // Assumes forward playing from here on.
+ for (int i = 0; i < mEvents.size(); i++) {
+ AnimationEvent event = mEvents.get(i);
+ if (event.getTime() > currentPlayTime) {
+ break;
+ }
+
+ // This animation started prior to the current play time, and won't finish before the
+ // play time, add to the unfinished list.
+ if (event.mEvent == AnimationEvent.ANIMATION_DELAY_ENDED) {
+ if (event.mNode.mEndTime == DURATION_INFINITE
+ || event.mNode.mEndTime > currentPlayTime) {
+ unfinishedNodes.add(event.mNode);
+ }
+ }
+ // For animations that do finish before the play time, end them in the sequence that
+ // they would in a normal run.
+ if (event.mEvent == AnimationEvent.ANIMATION_END) {
+ // Skip to the end of the animation.
+ event.mNode.mAnimation.skipToEndValue(false);
+ }
+ }
+
+ // Seek unfinished animation to the right time.
+ for (int i = 0; i < unfinishedNodes.size(); i++) {
+ Node node = unfinishedNodes.get(i);
+ long playTime = getPlayTimeForNode(currentPlayTime, node, inReverse);
+ node.mAnimation.animateBasedOnPlayTime(playTime, lastPlayTime, inReverse);
+ }
+ }
+
+ @Override
+ boolean isInitialized() {
+ if (mChildrenInitialized) {
+ return true;
+ }
+
+ boolean allInitialized = true;
+ for (int i = 0; i < mNodes.size(); i++) {
+ if (!mNodes.get(i).mAnimation.isInitialized()) {
+ allInitialized = false;
+ break;
+ }
+ }
+ mChildrenInitialized = allInitialized;
+ return mChildrenInitialized;
+ }
+
+ private void skipToStartValue(boolean inReverse) {
+ skipToEndValue(!inReverse);
+ }
+
+ /**
+ * Sets the position of the animation to the specified point in time. This time should
+ * be between 0 and the total duration of the animation, including any repetition. If
+ * the animation has not yet been started, then it will not advance forward after it is
+ * set to this time; it will simply set the time to this value and perform any appropriate
+ * actions based on that time. If the animation is already running, then setCurrentPlayTime()
+ * will set the current playing time to this value and continue playing from that point.
+ *
+ * @param playTime The time, in milliseconds, to which the animation is advanced or rewound.
+ * Unless the animation is reversing, the playtime is considered the time since
+ * the end of the start delay of the AnimatorSet in a forward playing direction.
+ *
+ */
+ public void setCurrentPlayTime(long playTime) {
+ if (mReversing && getTotalDuration() == DURATION_INFINITE) {
+ // Should never get here
+ throw new UnsupportedOperationException("Error: Cannot seek in reverse in an infinite"
+ + " AnimatorSet");
+ }
+
+ if ((getTotalDuration() != DURATION_INFINITE && playTime > getTotalDuration() - mStartDelay)
+ || playTime < 0) {
+ throw new UnsupportedOperationException("Error: Play time should always be in between"
+ + "0 and duration.");
+ }
+
+ initAnimation();
+
+ if (!isStarted()) {
+ if (mReversing) {
+ throw new UnsupportedOperationException("Error: Something went wrong. mReversing"
+ + " should not be set when AnimatorSet is not started.");
+ }
+ if (!mSeekState.isActive()) {
+ findLatestEventIdForTime(0);
+ // Set all the values to start values.
+ initChildren();
+ skipToStartValue(mReversing);
+ mSeekState.setPlayTime(0, mReversing);
+ }
+ animateBasedOnPlayTime(playTime, 0, mReversing);
+ mSeekState.setPlayTime(playTime, mReversing);
+ } else {
+ // If the animation is running, just set the seek time and wait until the next frame
+ // (i.e. doAnimationFrame(...)) to advance the animation.
+ mSeekState.setPlayTime(playTime, mReversing);
+ }
+ }
+
+ private void initChildren() {
+ if (!isInitialized()) {
+ mChildrenInitialized = true;
+ // Forcefully initialize all children based on their end time, so that if the start
+ // value of a child is dependent on a previous animation, the animation will be
+ // initialized after the the previous animations have been advanced to the end.
+ skipToEndValue(false);
+ }
+ }
+
+ /**
+ * @param frameTime The frame start time, in the {@link SystemClock#uptimeMillis()} time
+ * base.
+ * @return
+ * @hide
+ */
+ @Override
+ public boolean doAnimationFrame(long frameTime) {
+ if (mLastFrameTime < 0) {
+ mFirstFrame = mLastFrameTime = frameTime;
+ }
+
+ // Handle pause/resume
+ if (mPaused) {
+ // Note: Child animations don't receive pause events. Since it's never a contract that
+ // the child animators will be paused when set is paused, this is unlikely to be an
+ // issue.
+ mPauseTime = frameTime;
+ removeAnimationCallback();
+ return false;
+ } else if (mPauseTime > 0) {
+ // Offset by the duration that the animation was paused
+ mFirstFrame += (frameTime - mPauseTime);
+ mPauseTime = -1;
+ }
+
+ // Continue at seeked position
+ if (mSeekState.isActive()) {
+ mSeekState.updateSeekDirection(mReversing);
+ mFirstFrame = frameTime - mSeekState.getPlayTime() - mStartDelay;
+ mSeekState.reset();
+ }
+
+ // This playTime includes the start delay.
+ long playTime = frameTime - mFirstFrame;
+
+ // 1. Pulse the animators that will start or end in this frame
+ // 2. Pulse the animators that will finish in a later frame
+ int latestId = findLatestEventIdForTime(playTime);
+ int startId = mLastEventId;
+
+ handleAnimationEvents(startId, latestId, playTime);
+
+ mLastEventId = latestId;
+
+ // Pump a frame to the on-going animators
+ for (int i = 0; i < mPlayingSet.size(); i++) {
+ Node node = mPlayingSet.get(i);
+ if (!node.mEnded) {
+ node.mEnded = node.mAnimation.doAnimationFrame(getPlayTimeForNode(playTime, node));
+ }
+ }
+
+ // Remove all the finished anims
+ for (int i = mPlayingSet.size() - 1; i >= 0; i--) {
+ if (mPlayingSet.get(i).mEnded) {
+ mPlayingSet.remove(i);
+ }
+ }
+
+ mLastFrameTime = frameTime;
+ if (mPlayingSet.isEmpty()) {
+ boolean finished;
+ if (mReversing) {
+ // Make sure there's no more END event before current event id and after start delay
+ finished = mLastEventId <= 3;
+ } else {
+ // Make sure there's no more START event before current event id:
+ finished = (mLastEventId == mEvents.size() - 1);
+ }
+ if (finished) {
+ endAnimation();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * When playing forward, we call start() at the animation's scheduled start time, and make sure
+ * to pump a frame at the animation's scheduled end time.
+ *
+ * When playing in reverse, we should reverse the animation when we hit animation's end event,
+ * and expect the animation to end at the its delay ended event, rather than start event.
+ */
+ private void handleAnimationEvents(int startId, int latestId, long playTime) {
+ if (mReversing) {
+ startId = startId == -1 ? mEvents.size() : startId;
+ for (int i = startId - 1; i >= latestId; i--) {
+ AnimationEvent event = mEvents.get(i);
+ Node node = event.mNode;
+ if (event.mEvent == AnimationEvent.ANIMATION_END) {
+ mPlayingSet.add(event.mNode);
+ node.mAnimation.startWithoutPulsing(true);
+ node.mAnimation.doAnimationFrame(0);
+ } else if (event.mEvent == AnimationEvent.ANIMATION_DELAY_ENDED && !node.mEnded) {
+ // end event:
+ node.mEnded =
+ node.mAnimation.doAnimationFrame(getPlayTimeForNode(playTime, node));
+ }
+ }
+ } else {
+ for (int i = startId + 1; i <= latestId; i++) {
+ AnimationEvent event = mEvents.get(i);
+ Node node = event.mNode;
+ if (event.mEvent == AnimationEvent.ANIMATION_START) {
+ mPlayingSet.add(event.mNode);
+ node.mAnimation.startWithoutPulsing(false);
+ node.mAnimation.doAnimationFrame(0);
+ } else if (event.mEvent == AnimationEvent.ANIMATION_END && !node.mEnded) {
+ // start event:
+ node.mEnded =
+ node.mAnimation.doAnimationFrame(getPlayTimeForNode(playTime, node));
+ }
+ }
+ }
+ }
+
+ private long getPlayTimeForNode(long overallPlayTime, Node node) {
+ return getPlayTimeForNode(overallPlayTime, node, mReversing);
+ }
+
+ private long getPlayTimeForNode(long overallPlayTime, Node node, boolean inReverse) {
+ if (inReverse) {
+ overallPlayTime = getTotalDuration() - overallPlayTime;
+ return node.mEndTime - overallPlayTime;
+ } else {
+ return overallPlayTime - node.mStartTime;
+ }
+ }
+
+ private void startAnimation() {
+ // Register animation callback
+ addAnimationCallback(mStartDelay);
+
+ if (mSeekState.getPlayTimeNormalized() == 0 && mReversing) {
+ // Maintain old behavior, if seeked to 0 then call reverse, we'll treat the case
+ // the same as no seeking at all.
+ mSeekState.reset();
+ }
+ // Set the child animators to the right end:
+ if (mShouldResetValuesAtStart) {
+ if (mReversing || isInitialized()) {
+ skipToEndValue(!mReversing);
+ } else {
+ // If not all children are initialized and play direction is forward
+ for (int i = mEvents.size() - 1; i >= 0; i--) {
+ if (mEvents.get(i).mEvent == AnimationEvent.ANIMATION_DELAY_ENDED) {
+ Animator anim = mEvents.get(i).mNode.mAnimation;
+ // Only reset the animations that have been initialized to start value,
+ // so that if they are defined without a start value, they will get the
+ // values set at the right time (i.e. the next animation run)
+ if (anim.isInitialized()) {
+ anim.skipToEndValue(true);
+ }
+ }
+ }
+ }
+ }
+
+ if (mReversing || mStartDelay == 0 || mSeekState.isActive()) {
+ long playTime;
+ // If no delay, we need to call start on the first animations to be consistent with old
+ // behavior.
+ if (mSeekState.isActive()) {
+ mSeekState.updateSeekDirection(mReversing);
+ playTime = mSeekState.getPlayTime();
+ } else {
+ playTime = 0;
+ }
+ int toId = findLatestEventIdForTime(playTime);
+ handleAnimationEvents(-1, toId, playTime);
+ mLastEventId = toId;
+ }
+ }
+
+ private int findLatestEventIdForTime(long currentPlayTime) {
+ int size = mEvents.size();
+ int latestId = mLastEventId;
+ // Call start on the first animations now to be consistent with the old behavior
+ if (mReversing) {
+ currentPlayTime = getTotalDuration() - currentPlayTime;
+ mLastEventId = mLastEventId == -1 ? size : mLastEventId;
+ for (int j = mLastEventId - 1; j >= 0; j--) {
+ AnimationEvent event = mEvents.get(j);
+ if (event.getTime() >= currentPlayTime) {
+ latestId = j;
+ }
+ }
+ } else {
+ for (int i = mLastEventId + 1; i < size; i++) {
+ AnimationEvent event = mEvents.get(i);
+ if (event.getTime() <= currentPlayTime) {
+ latestId = i;
+ }
+ }
+ }
+ return latestId;
+ }
+
+ private void endAnimation() {
+ mStarted = false;
+ mLastFrameTime = -1;
+ mFirstFrame = -1;
+ mLastEventId = -1;
+ mPaused = false;
+ mPauseTime = -1;
+ mSeekState.reset();
+ mPlayingSet.clear();
+
+ // No longer receive callbacks
+ removeAnimationCallback();
+ // Call end listener
+ if (mListeners != null) {
+ ArrayList<AnimatorListener> tmpListeners =
+ (ArrayList<AnimatorListener>) mListeners.clone();
+ int numListeners = tmpListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ tmpListeners.get(i).onAnimationEnd(this, mReversing);
+ }
+ }
+ mSelfPulse = true;
+ mReversing = false;
+ }
+
+ private void removeAnimationCallback() {
+ if (!mSelfPulse) {
+ return;
+ }
+ AnimationHandler handler = AnimationHandler.getInstance();
+ handler.removeCallback(this);
+ }
+
+ private void addAnimationCallback(long delay) {
+ if (!mSelfPulse) {
+ return;
+ }
+ AnimationHandler handler = AnimationHandler.getInstance();
+ handler.addAnimationFrameCallback(this, delay);
}
@Override
@@ -709,13 +1139,20 @@
* and will populate any appropriate lists, when it is started.
*/
final int nodeCount = mNodes.size();
- anim.mTerminated = false;
anim.mStarted = false;
- anim.mPlayingSet = new ArrayList<Animator>();
+ anim.mLastFrameTime = -1;
+ anim.mFirstFrame = -1;
+ anim.mLastEventId = -1;
+ anim.mPaused = false;
+ anim.mPauseTime = -1;
+ anim.mSeekState = new SeekState();
+ anim.mSelfPulse = true;
+ anim.mPlayingSet = new ArrayList<Node>();
anim.mNodeMap = new ArrayMap<Animator, Node>();
anim.mNodes = new ArrayList<Node>(nodeCount);
- anim.mReversible = mReversible;
- anim.mSetListener = new AnimatorSetListener(anim);
+ anim.mEvents = new ArrayList<AnimationEvent>();
+ anim.mReversing = false;
+ anim.mDependencyDirty = true;
// Walk through the old nodes list, cloning each node and adding it to the new nodemap.
// One problem is that the old node dependencies point to nodes in the old AnimatorSet.
@@ -727,17 +1164,6 @@
node.mTmpClone = nodeClone;
anim.mNodes.add(nodeClone);
anim.mNodeMap.put(nodeClone.mAnimation, nodeClone);
-
- // clear out any listeners that were set up by the AnimatorSet
- final ArrayList<AnimatorListener> cloneListeners = nodeClone.mAnimation.getListeners();
- if (cloneListeners != null) {
- for (int i = cloneListeners.size() - 1; i >= 0; i--) {
- final AnimatorListener listener = cloneListeners.get(i);
- if (listener instanceof AnimatorSetListener) {
- cloneListeners.remove(i);
- }
- }
- }
}
anim.mRootNode = mRootNode.mTmpClone;
@@ -771,89 +1197,6 @@
}
- private static class AnimatorSetListener implements AnimatorListener {
-
- private AnimatorSet mAnimatorSet;
-
- AnimatorSetListener(AnimatorSet animatorSet) {
- mAnimatorSet = animatorSet;
- }
-
- public void onAnimationCancel(Animator animation) {
-
- if (!mAnimatorSet.mTerminated) {
- // Listeners are already notified of the AnimatorSet canceling in cancel().
- // The logic below only kicks in when animations end normally
- if (mAnimatorSet.mPlayingSet.size() == 0) {
- ArrayList<AnimatorListener> listeners = mAnimatorSet.mListeners;
- if (listeners != null) {
- int numListeners = listeners.size();
- for (int i = 0; i < numListeners; ++i) {
- listeners.get(i).onAnimationCancel(mAnimatorSet);
- }
- }
- }
- }
- }
-
- @SuppressWarnings("unchecked")
- public void onAnimationEnd(Animator animation) {
- animation.removeListener(this);
- mAnimatorSet.mPlayingSet.remove(animation);
- mAnimatorSet.onChildAnimatorEnded(animation);
- }
-
- // Nothing to do
- public void onAnimationRepeat(Animator animation) {
- }
-
- // Nothing to do
- public void onAnimationStart(Animator animation) {
- }
-
- }
-
- private void onChildAnimatorEnded(Animator animation) {
- Node animNode = mNodeMap.get(animation);
- animNode.mEnded = true;
-
- if (!mTerminated) {
- List<Node> children = animNode.mChildNodes;
- // Start children animations, if any.
- int childrenSize = children == null ? 0 : children.size();
- for (int i = 0; i < childrenSize; i++) {
- if (children.get(i).mLatestParent == animNode) {
- start(children.get(i));
- }
- }
- // Listeners are already notified of the AnimatorSet ending in cancel() or
- // end(); the logic below only kicks in when animations end normally
- boolean allDone = true;
- // Traverse the tree and find if there's any unfinished node
- int size = mNodes.size();
- for (int i = 0; i < size; i++) {
- if (!mNodes.get(i).mEnded) {
- allDone = false;
- break;
- }
- }
- if (allDone) {
- mStarted = false;
- mPaused = false;
- // If this was the last child animation to end, then notify listeners that this
- // AnimatorSet has ended
- if (mListeners != null) {
- ArrayList<AnimatorListener> tmpListeners =
- (ArrayList<AnimatorListener>) mListeners.clone();
- int numListeners = tmpListeners.size();
- for (int i = 0; i < numListeners; ++i) {
- tmpListeners.get(i).onAnimationEnd(this);
- }
- }
- }
- }
- }
-
/**
* AnimatorSet is only reversible when the set contains no sequential animation, and no child
* animators have a start delay.
@@ -861,32 +1204,21 @@
*/
@Override
public boolean canReverse() {
- if (!mReversible) {
- return false;
- }
- // Loop to make sure all the Nodes can reverse.
- int size = mNodes.size();
- for (int i = 0; i < size; i++) {
- Node node = mNodes.get(i);
- if (!node.mAnimation.canReverse() || node.mAnimation.getStartDelay() > 0) {
- return false;
- }
- }
- return true;
+ return getTotalDuration() != DURATION_INFINITE;
}
/**
- * @hide
+ * Plays the AnimatorSet in reverse. If the animation has been seeked to a specific play time
+ * using {@link #setCurrentPlayTime(long)}, it will play backwards from the point seeked when
+ * reverse was called. Otherwise, then it will start from the end and play backwards. This
+ * behavior is only set for the current animation; future playing of the animation will use the
+ * default behavior of playing forward.
+ * <p>
+ * Note: reverse is not supported for infinite AnimatorSet.
*/
@Override
public void reverse() {
- if (canReverse()) {
- int size = mNodes.size();
- for (int i = 0; i < size; i++) {
- Node node = mNodes.get(i);
- node.mAnimation.reverse();
- }
- }
+ start(true, true);
}
@Override
@@ -993,18 +1325,61 @@
mRootNode.mEndTime = mDelayAnim.getDuration();
updatePlayTime(mRootNode, visited);
- long maxEndTime = 0;
- for (int i = 0; i < size; i++) {
+ sortAnimationEvents();
+ mTotalDuration = mEvents.get(mEvents.size() - 1).getTime();
+ }
+
+ private void sortAnimationEvents() {
+ // Sort the list of events in ascending order of their time
+ // Create the list including the delay animation.
+ mEvents.clear();
+ for (int i = 0; i < mNodes.size(); i++) {
Node node = mNodes.get(i);
- node.mTotalDuration = node.mAnimation.getTotalDuration();
- if (node.mEndTime == DURATION_INFINITE) {
- maxEndTime = DURATION_INFINITE;
- break;
- } else {
- maxEndTime = node.mEndTime > maxEndTime ? node.mEndTime : maxEndTime;
- }
+ mEvents.add(new AnimationEvent(node, AnimationEvent.ANIMATION_START));
+ mEvents.add(new AnimationEvent(node, AnimationEvent.ANIMATION_DELAY_ENDED));
+ mEvents.add(new AnimationEvent(node, AnimationEvent.ANIMATION_END));
}
- mTotalDuration = maxEndTime;
+ mEvents.sort(new Comparator<AnimationEvent>() {
+ @Override
+ public int compare(AnimationEvent e1, AnimationEvent e2) {
+ long t1 = e1.getTime();
+ long t2 = e2.getTime();
+ if (t1 == t2) {
+ if (e1.mNode == e2.mNode) {
+ // For the same animation, start event has to happen before end.
+ return e1.mEvent - e2.mEvent;
+ }
+ // For different animation, end events need to happen before start, to ensure
+ // sequential animations finish the previous one before starting the next one.
+ return e2.mEvent - e1.mEvent;
+ }
+ if (t2 == DURATION_INFINITE) {
+ return -1;
+ }
+ if (t1 == DURATION_INFINITE) {
+ return 1;
+ }
+ // When neither event happens at INFINITE time:
+ return (int) (t1 - t2);
+ }
+ });
+
+ if (mEvents.get(mEvents.size() - 1).mEvent == AnimationEvent.ANIMATION_START
+ || mEvents.get(mEvents.size() - 1).mEvent == AnimationEvent.ANIMATION_DELAY_ENDED) {
+ throw new UnsupportedOperationException(
+ "Something went wrong, the last event is not an end event");
+ }
+ if (mEvents.get(1).mEvent != AnimationEvent.ANIMATION_DELAY_ENDED
+ || mEvents.get(1).mNode != mRootNode) {
+ throw new UnsupportedOperationException(
+ "Sorting went bad, the root node's start delay end event should always be at"
+ + " index 1");
+ }
+ if (mEvents.get(2).mEvent != AnimationEvent.ANIMATION_END
+ || mEvents.get(2).mNode != mRootNode) {
+ throw new UnsupportedOperationException(
+ "Sorting went bad, the start delay end event should always be at index 2");
+ }
}
/**
@@ -1235,6 +1610,95 @@
}
/**
+ * This class is a wrapper around a node and an event for the animation corresponding to the
+ * node. The 3 types of events represent the start of an animation, the end of a start delay of
+ * an animation, and the end of an animation. When playing forward (i.e. in the non-reverse
+ * direction), start event marks when start() should be called, and end event corresponds to
+ * when the animation should finish. When playing in reverse, start delay will not be a part
+ * of the animation. Therefore, reverse() is called at the end event, and animation should end
+ * at the delay ended event.
+ */
+ private static class AnimationEvent {
+ static final int ANIMATION_START = 0;
+ static final int ANIMATION_DELAY_ENDED = 1;
+ static final int ANIMATION_END = 2;
+ final Node mNode;
+ final int mEvent;
+
+ AnimationEvent(Node node, int event) {
+ mNode = node;
+ mEvent = event;
+ }
+
+ long getTime() {
+ if (mEvent == ANIMATION_START) {
+ return mNode.mStartTime;
+ } else if (mEvent == ANIMATION_DELAY_ENDED) {
+ return mNode.mStartTime + mNode.mAnimation.getStartDelay();
+ } else {
+ return mNode.mEndTime;
+ }
+ }
+
+ public String toString() {
+ String eventStr = mEvent == ANIMATION_START ? "start" : (
+ mEvent == ANIMATION_DELAY_ENDED ? "delay ended" : "end");
+ return eventStr + " " + mNode.mAnimation.toString();
+ }
+ }
+
+ private class SeekState {
+ private long mPlayTime = -1;
+ private boolean mSeekingInReverse = false;
+ void reset() {
+ mPlayTime = -1;
+ mSeekingInReverse = false;
+ }
+
+ void setPlayTime(long playTime, boolean inReverse) {
+ // TODO: This can be simplified.
+
+ // Clamp the play time
+ if (getTotalDuration() != DURATION_INFINITE) {
+ mPlayTime = Math.min(playTime, getTotalDuration() - mStartDelay);
+ }
+ mPlayTime = Math.max(0, mPlayTime);
+ mSeekingInReverse = inReverse;
+ }
+
+ void updateSeekDirection(boolean inReverse) {
+ // Change seek direction without changing the overall fraction
+ if (inReverse && getTotalDuration() == DURATION_INFINITE) {
+ throw new UnsupportedOperationException("Error: Cannot reverse infinite animator"
+ + " set");
+ }
+ if (mPlayTime >= 0) {
+ if (inReverse != mSeekingInReverse) {
+ mPlayTime = getTotalDuration() - mStartDelay - mPlayTime;
+ }
+ }
+ }
+
+ long getPlayTime() {
+ return mPlayTime;
+ }
+
+ /**
+ * Returns the playtime assuming the animation is forward playing
+ */
+ long getPlayTimeNormalized() {
+ if (mReversing) {
+ return getTotalDuration() - mStartDelay - mPlayTime;
+ }
+ return mPlayTime;
+ }
+
+ boolean isActive() {
+ return mPlayTime != -1;
+ }
+ }
+
+ /**
* The <code>Builder</code> object is a utility class to facilitate adding animations to a
* <code>AnimatorSet</code> along with the relationships between the various animations. The
* intention of the <code>Builder</code> methods, along with the {@link
@@ -1328,7 +1792,6 @@
* {@link AnimatorSet#play(Animator)} method ends.
*/
public Builder before(Animator anim) {
- mReversible = false;
Node node = getNodeForAnimation(anim);
mCurrentNode.addChild(node);
return this;
@@ -1343,7 +1806,6 @@
* {@link AnimatorSet#play(Animator)} method to play.
*/
public Builder after(Animator anim) {
- mReversible = false;
Node node = getNodeForAnimation(anim);
mCurrentNode.addParent(node);
return this;
diff --git a/core/java/android/animation/ObjectAnimator.java b/core/java/android/animation/ObjectAnimator.java
index 4707bed..1e1f155 100644
--- a/core/java/android/animation/ObjectAnimator.java
+++ b/core/java/android/animation/ObjectAnimator.java
@@ -992,6 +992,11 @@
}
@Override
+ boolean isInitialized() {
+ return mInitialized;
+ }
+
+ @Override
public ObjectAnimator clone() {
final ObjectAnimator anim = (ObjectAnimator) super.clone();
return anim;
diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java
index f0fc8af..470523f 100644
--- a/core/java/android/animation/ValueAnimator.java
+++ b/core/java/android/animation/ValueAnimator.java
@@ -24,6 +24,7 @@
import android.util.AndroidRuntimeException;
import android.util.Log;
import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.LinearInterpolator;
@@ -67,7 +68,7 @@
* </div>
*/
@SuppressWarnings("unchecked")
-public class ValueAnimator extends Animator implements AnimationHandler.AnimationFrameCallback {
+public class ValueAnimator extends Animator {
private static final String TAG = "ValueAnimator";
private static final boolean DEBUG = false;
@@ -90,7 +91,7 @@
*
* Whenever mStartTime is set, you must also update mStartTimeCommitted.
*/
- long mStartTime;
+ long mStartTime = -1;
/**
* When true, the start time has been firmly committed as a chosen reference point in
@@ -152,7 +153,13 @@
/**
* Tracks the time (in milliseconds) when the last frame arrived.
*/
- private long mLastFrameTime = 0;
+ private long mLastFrameTime = -1;
+
+ /**
+ * Tracks the time (in milliseconds) when the first frame arrived. Note the frame may arrive
+ * during the start delay.
+ */
+ private long mFirstFrameTime = -1;
/**
* Additional playing state to indicate whether an animator has been start()'d. There is
@@ -212,6 +219,12 @@
private int mRepeatMode = RESTART;
/**
+ * Whether or not the animator should register for its own animation callback to receive
+ * animation pulse.
+ */
+ private boolean mSelfPulse = true;
+
+ /**
* The time interpolator to be used. The elapsed fraction of the animation will be passed
* through this interpolator to calculate the interpolated fraction, which is then used to
* calculate the animated values.
@@ -628,7 +641,7 @@
mSeekFraction = fraction;
}
mOverallFraction = fraction;
- final float currentIterationFraction = getCurrentIterationFraction(fraction);
+ final float currentIterationFraction = getCurrentIterationFraction(fraction, mReversing);
animateValue(currentIterationFraction);
}
@@ -654,11 +667,11 @@
* should be played backwards. E.g. When the animation is played backwards in an iteration,
* the fraction for that iteration will go from 1f to 0f.
*/
- private float getCurrentIterationFraction(float fraction) {
+ private float getCurrentIterationFraction(float fraction, boolean inReverse) {
fraction = clampFraction(fraction);
int iteration = getCurrentIteration(fraction);
float currentFraction = fraction - iteration;
- return shouldPlayBackward(iteration) ? 1f - currentFraction : currentFraction;
+ return shouldPlayBackward(iteration, inReverse) ? 1f - currentFraction : currentFraction;
}
/**
@@ -682,18 +695,18 @@
* whether the entire animation is being reversed, 2) repeat mode applied to the current
* iteration.
*/
- private boolean shouldPlayBackward(int iteration) {
+ private boolean shouldPlayBackward(int iteration, boolean inReverse) {
if (iteration > 0 && mRepeatMode == REVERSE &&
(iteration < (mRepeatCount + 1) || mRepeatCount == INFINITE)) {
// if we were seeked to some other iteration in a reversing animator,
// figure out the correct direction to start playing based on the iteration
- if (mReversing) {
+ if (inReverse) {
return (iteration % 2) == 0;
} else {
return (iteration % 2) != 0;
}
} else {
- return mReversing;
+ return inReverse;
}
}
@@ -965,7 +978,7 @@
(ArrayList<AnimatorListener>) mListeners.clone();
int numListeners = tmpListeners.size();
for (int i = 0; i < numListeners; ++i) {
- tmpListeners.get(i).onAnimationStart(this);
+ tmpListeners.get(i).onAnimationStart(this, mReversing);
}
}
mStartListenersCalled = true;
@@ -984,11 +997,12 @@
*
* @param playBackwards Whether the ValueAnimator should start playing in reverse.
*/
- private void start(boolean playBackwards) {
+ private void start(boolean playBackwards, boolean selfPulse) {
if (Looper.myLooper() == null) {
throw new AndroidRuntimeException("Animators may only be run on Looper threads");
}
mReversing = playBackwards;
+ mSelfPulse = selfPulse;
// Special case: reversing from seek-to-0 should act as if not seeked at all.
if (playBackwards && mSeekFraction != -1 && mSeekFraction != 0) {
if (mRepeatCount == INFINITE) {
@@ -1006,11 +1020,11 @@
// Resets mLastFrameTime when start() is called, so that if the animation was running,
// calling start() would put the animation in the
// started-but-not-yet-reached-the-first-frame phase.
- mLastFrameTime = 0;
- AnimationHandler animationHandler = AnimationHandler.getInstance();
- animationHandler.addAnimationFrameCallback(this, (long) (mStartDelay * sDurationScale));
+ mLastFrameTime = -1;
+ mFirstFrameTime = -1;
+ addAnimationCallback((long) (mStartDelay * sDurationScale));
- if (mStartDelay == 0 || mSeekFraction >= 0) {
+ if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {
// If there's no start delay, init the animation and notify start listeners right away
// to be consistent with the previous behavior. Otherwise, postpone this until the first
// frame after the start delay.
@@ -1026,9 +1040,13 @@
}
}
+ void startWithoutPulsing(boolean inReverse) {
+ start(inReverse, false);
+ }
+
@Override
public void start() {
- start(false);
+ start(false, true);
}
@Override
@@ -1073,7 +1091,7 @@
} else if (!mInitialized) {
initAnimation();
}
- animateValue(shouldPlayBackward(mRepeatCount) ? 0f : 1f);
+ animateValue(shouldPlayBackward(mRepeatCount, mReversing) ? 0f : 1f);
endAnimation();
}
@@ -1086,8 +1104,7 @@
if (mPaused && !mResumed) {
mResumed = true;
if (mPauseTime > 0) {
- AnimationHandler handler = AnimationHandler.getInstance();
- handler.addAnimationFrameCallback(this, 0);
+ addAnimationCallback(0);
}
}
super.resume();
@@ -1133,7 +1150,7 @@
mReversing = !mReversing;
end();
} else {
- start(true);
+ start(true, true);
}
}
@@ -1153,8 +1170,7 @@
if (mAnimationEndRequested) {
return;
}
- AnimationHandler handler = AnimationHandler.getInstance();
- handler.removeCallback(this);
+ removeAnimationCallback();
mAnimationEndRequested = true;
mPaused = false;
@@ -1166,16 +1182,18 @@
mRunning = false;
mStarted = false;
mStartListenersCalled = false;
+ mLastFrameTime = -1;
+ mFirstFrameTime = -1;
mReversing = false;
- mLastFrameTime = 0;
if (notify && mListeners != null) {
ArrayList<AnimatorListener> tmpListeners =
(ArrayList<AnimatorListener>) mListeners.clone();
int numListeners = tmpListeners.size();
for (int i = 0; i < numListeners; ++i) {
- tmpListeners.get(i).onAnimationEnd(this);
+ tmpListeners.get(i).onAnimationEnd(this, mReversing);
}
}
+ mReversing = false;
if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
Trace.asyncTraceEnd(Trace.TRACE_TAG_VIEW, getNameForTrace(),
System.identityHashCode(this));
@@ -1211,7 +1229,7 @@
* is called (or after start delay if any), which may be before the animation loop starts.
*/
private boolean isPulsingInternal() {
- return mLastFrameTime > 0;
+ return mLastFrameTime >= 0;
}
/**
@@ -1276,24 +1294,116 @@
done = true;
}
mOverallFraction = clampFraction(fraction);
- float currentIterationFraction = getCurrentIterationFraction(mOverallFraction);
+ float currentIterationFraction = getCurrentIterationFraction(
+ mOverallFraction, mReversing);
animateValue(currentIterationFraction);
}
return done;
}
/**
+ * Internal use only.
+ *
+ * This method does not modify any fields of the animation. It should be called when seeking
+ * in an AnimatorSet. When the last play time and current play time are of different repeat
+ * iterations,
+ * {@link android.view.animation.Animation.AnimationListener#onAnimationRepeat(Animation)}
+ * will be called.
+ */
+ @Override
+ void animateBasedOnPlayTime(long currentPlayTime, long lastPlayTime, boolean inReverse) {
+ if (currentPlayTime < 0 || lastPlayTime < 0) {
+ throw new UnsupportedOperationException("Error: Play time should never be negative.");
+ }
+
+ initAnimation();
+ // Check whether repeat callback is needed only when repeat count is non-zero
+ if (mRepeatCount > 0) {
+ int iteration = (int) (currentPlayTime / mDuration);
+ int lastIteration = (int) (lastPlayTime / mDuration);
+
+ // Clamp iteration to [0, mRepeatCount]
+ iteration = Math.min(iteration, mRepeatCount);
+ lastIteration = Math.min(lastIteration, mRepeatCount);
+
+ if (iteration != lastIteration) {
+ if (mListeners != null) {
+ int numListeners = mListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ mListeners.get(i).onAnimationRepeat(this);
+ }
+ }
+ }
+ }
+
+ if (mRepeatCount != INFINITE && currentPlayTime >= (mRepeatCount + 1) * mDuration) {
+ skipToEndValue(inReverse);
+ } else {
+ // Find the current fraction:
+ float fraction = currentPlayTime / (float) mDuration;
+ fraction = getCurrentIterationFraction(fraction, inReverse);
+ animateValue(fraction);
+ }
+ }
+
+ /**
+ * Internal use only.
+ * Skips the animation value to end/start, depending on whether the play direction is forward
+ * or backward.
+ *
+ * @param inReverse whether the end value is based on a reverse direction. If yes, this is
+ * equivalent to skip to start value in a forward playing direction.
+ */
+ void skipToEndValue(boolean inReverse) {
+ initAnimation();
+ float endFraction = inReverse ? 0f : 1f;
+ if (mRepeatCount % 2 == 1 && mRepeatMode == REVERSE) {
+ // This would end on fraction = 0
+ endFraction = 0f;
+ }
+ animateValue(endFraction);
+ }
+
+ /**
* Processes a frame of the animation, adjusting the start time if needed.
*
* @param frameTime The frame time.
* @return true if the animation has ended.
* @hide
*/
- public final void doAnimationFrame(long frameTime) {
- AnimationHandler handler = AnimationHandler.getInstance();
- if (mLastFrameTime == 0) {
+ public final boolean doAnimationFrame(long frameTime) {
+ if (!mRunning && mStartTime < 0) {
+ // First frame during delay
+ mStartTime = frameTime + mStartDelay;
+ }
+
+ // Handle pause/resume
+ if (mPaused) {
+ mPauseTime = frameTime;
+ removeAnimationCallback();
+ return false;
+ } else if (mResumed) {
+ mResumed = false;
+ if (mPauseTime > 0) {
+ // Offset by the duration that the animation was paused
+ mStartTime += (frameTime - mPauseTime);
+ }
+ }
+
+ if (!mRunning) {
+ // If not running, that means the animation is in the start delay phase. In the case of
+ // reversing, we want to run start delay in the end.
+ if (mStartTime > frameTime) {
+ // During start delay
+ return false;
+ } else {
+ // Start delay has passed.
+ mRunning = true;
+ }
+ }
+
+ if (mLastFrameTime < 0) {
// First frame
- handler.addOneShotCommitCallback(this);
if (mStartDelay > 0) {
startAnimation();
}
@@ -1307,19 +1417,6 @@
mStartTimeCommitted = false; // allow start time to be compensated for jank
}
mLastFrameTime = frameTime;
- if (mPaused) {
- mPauseTime = frameTime;
- handler.removeCallback(this);
- return;
- } else if (mResumed) {
- mResumed = false;
- if (mPauseTime > 0) {
- // Offset by the duration that the animation was paused
- mStartTime += (frameTime - mPauseTime);
- mStartTimeCommitted = false; // allow start time to be compensated for jank
- }
- handler.addOneShotCommitCallback(this);
- }
// The frame time might be before the start time during the first frame of
// an animation. The "current time" must always be on or after the start
// time to avoid animating frames at negative time intervals. In practice, this
@@ -1330,6 +1427,31 @@
if (finished) {
endAnimation();
}
+ return finished;
+ }
+
+ private void addOneShotCommitCallback() {
+ if (!mSelfPulse) {
+ return;
+ }
+ AnimationHandler handler = AnimationHandler.getInstance();
+ handler.addOneShotCommitCallback(this);
+ }
+
+ private void removeAnimationCallback() {
+ if (!mSelfPulse) {
+ return;
+ }
+ AnimationHandler handler = AnimationHandler.getInstance();
+ handler.removeCallback(this);
+ }
+
+ private void addAnimationCallback(long delay) {
+ if (!mSelfPulse) {
+ return;
+ }
+ AnimationHandler handler = AnimationHandler.getInstance();
+ handler.addAnimationFrameCallback(this, delay);
}
/**
@@ -1384,11 +1506,12 @@
anim.mPaused = false;
anim.mResumed = false;
anim.mStartListenersCalled = false;
- anim.mStartTime = 0;
+ anim.mStartTime = -1;
anim.mStartTimeCommitted = false;
anim.mAnimationEndRequested = false;
- anim.mPauseTime = 0;
- anim.mLastFrameTime = 0;
+ anim.mPauseTime = -1;
+ anim.mLastFrameTime = -1;
+ anim.mFirstFrameTime = -1;
anim.mOverallFraction = 0;
anim.mCurrentFraction = 0;
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index d814ddc..34eaa0b 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -2616,9 +2616,10 @@
r.activityInfo.targetActivity);
}
+ ContextImpl appContext = createBaseContextForActivity(r);
Activity activity = null;
try {
- java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
+ java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
@@ -2647,7 +2648,6 @@
+ ", dir=" + r.packageInfo.getAppDir());
if (activity != null) {
- Context appContext = createBaseContextForActivity(r, activity);
CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
Configuration config = new Configuration(mCompatConfiguration);
if (r.overrideConfig != null) {
@@ -2661,6 +2661,7 @@
r.mPendingRemoveWindow = null;
r.mPendingRemoveWindowManager = null;
}
+ appContext.setOuterContext(activity);
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
@@ -2736,8 +2737,8 @@
return activity;
}
- private Context createBaseContextForActivity(ActivityClientRecord r, final Activity activity) {
- int displayId = Display.DEFAULT_DISPLAY;
+ private ContextImpl createBaseContextForActivity(ActivityClientRecord r) {
+ final int displayId;
try {
displayId = ActivityManager.getService().getActivityDisplayId(r.token);
} catch (RemoteException e) {
@@ -2745,9 +2746,7 @@
}
ContextImpl appContext = ContextImpl.createActivityContext(
- this, r.packageInfo, r.token, displayId, r.overrideConfig);
- appContext.setOuterContext(activity);
- Context baseContext = appContext;
+ this, r.packageInfo, r.activityInfo, r.token, displayId, r.overrideConfig);
final DisplayManagerGlobal dm = DisplayManagerGlobal.getInstance();
// For debugging purposes, if the activity's package name contains the value of
@@ -2760,12 +2759,12 @@
if (id != Display.DEFAULT_DISPLAY) {
Display display =
dm.getCompatibleDisplay(id, appContext.getDisplayAdjustments(id));
- baseContext = appContext.createDisplayContext(display);
+ appContext = (ContextImpl) appContext.createDisplayContext(display);
break;
}
}
}
- return baseContext;
+ return appContext;
}
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
@@ -3119,9 +3118,16 @@
IActivityManager mgr = ActivityManager.getService();
+ Application app;
BroadcastReceiver receiver;
+ ContextImpl context;
try {
- java.lang.ClassLoader cl = packageInfo.getClassLoader();
+ app = packageInfo.makeApplication(false, mInstrumentation);
+ context = (ContextImpl) app.getBaseContext();
+ if (data.info.splitName != null) {
+ context = (ContextImpl) context.createContextForSplit(data.info.splitName);
+ }
+ java.lang.ClassLoader cl = context.getClassLoader();
data.intent.setExtrasClassLoader(cl);
data.intent.prepareToEnterProcess();
data.setExtrasClassLoader(cl);
@@ -3136,8 +3142,6 @@
}
try {
- Application app = packageInfo.makeApplication(false, mInstrumentation);
-
if (localLOGV) Slog.v(
TAG, "Performing receive of " + data.intent
+ ": app=" + app
@@ -3146,7 +3150,6 @@
+ ", comp=" + data.intent.getComponent().toShortString()
+ ", dir=" + packageInfo.getAppDir());
- ContextImpl context = (ContextImpl)app.getBaseContext();
sCurrentBroadcastIntent.set(data.intent);
receiver.setPendingResult(data);
receiver.onReceive(context.getReceiverRestrictedContext(),
@@ -6031,6 +6034,15 @@
info.name);
return null;
}
+
+ if (info.splitName != null) {
+ try {
+ c = c.createContextForSplit(info.splitName);
+ } catch (NameNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
try {
final java.lang.ClassLoader cl = c.getClassLoader();
localProvider = (ContentProvider)cl.
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index d37888d..9db2b92 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -32,6 +32,7 @@
import android.content.ReceiverCallNotAllowedException;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
@@ -58,21 +59,27 @@
import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceManager;
+import android.os.Trace;
import android.os.UserHandle;
import android.os.storage.IStorageManager;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
+import android.text.TextUtils;
import android.util.AndroidRuntimeException;
import android.util.ArrayMap;
+import android.util.IntArray;
import android.util.Log;
import android.util.Slog;
+import android.util.SparseBooleanArray;
import android.view.Display;
import android.view.DisplayAdjustments;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;
+import dalvik.system.PathClassLoader;
+
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
@@ -80,6 +87,8 @@
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Objects;
class ReceiverRestrictedContext extends ContextWrapper {
@@ -147,6 +156,7 @@
final ActivityThread mMainThread;
final LoadedApk mPackageInfo;
+ private ClassLoader mClassLoader;
private final IBinder mActivityToken;
@@ -272,8 +282,7 @@
@Override
public ClassLoader getClassLoader() {
- return mPackageInfo != null ?
- mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader();
+ return mClassLoader != null ? mClassLoader : (mPackageInfo != null ? mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader());
}
@Override
@@ -1887,7 +1896,7 @@
pi.getResDir(),
pi.getSplitResDirs(),
pi.getOverlayDirs(),
- pi.getApplicationInfo().sharedLibraryFiles,
+ pi.getSharedLibraries(),
displayId,
overrideConfig,
compatInfo,
@@ -1901,7 +1910,8 @@
flags | CONTEXT_REGISTER_PACKAGE);
if (pi != null) {
ContextImpl c = new ContextImpl(this, mMainThread, pi, mActivityToken,
- new UserHandle(UserHandle.getUserId(application.uid)), flags);
+ new UserHandle(UserHandle.getUserId(application.uid)), flags,
+ null);
final int displayId = mDisplay != null
? mDisplay.getDisplayId() : Display.DEFAULT_DISPLAY;
@@ -1930,13 +1940,15 @@
if (packageName.equals("system") || packageName.equals("android")) {
// The system resources are loaded in every application, so we can safely copy
// the context without reloading Resources.
- return new ContextImpl(this, mMainThread, mPackageInfo, mActivityToken, user, flags);
+ return new ContextImpl(this, mMainThread, mPackageInfo, mActivityToken, user, flags,
+ null);
}
LoadedApk pi = mMainThread.getPackageInfo(packageName, mResources.getCompatibilityInfo(),
flags | CONTEXT_REGISTER_PACKAGE, user.getIdentifier());
if (pi != null) {
- ContextImpl c = new ContextImpl(this, mMainThread, pi, mActivityToken, user, flags);
+ ContextImpl c = new ContextImpl(this, mMainThread, pi, mActivityToken, user, flags,
+ null);
final int displayId = mDisplay != null
? mDisplay.getDisplayId() : Display.DEFAULT_DISPLAY;
@@ -1954,13 +1966,42 @@
}
@Override
+ public Context createContextForSplit(String splitName) throws NameNotFoundException {
+ if (!mPackageInfo.getApplicationInfo().requestsIsolatedSplitLoading()) {
+ // All Splits are always loaded.
+ return this;
+ }
+
+ final ClassLoader classLoader = mPackageInfo.getSplitClassLoader(splitName);
+ final String[] paths = mPackageInfo.getSplitPaths(splitName);
+
+ final ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo,
+ mActivityToken, mUser, mFlags, classLoader);
+
+ final int displayId = mDisplay != null
+ ? mDisplay.getDisplayId() : Display.DEFAULT_DISPLAY;
+
+ context.mResources = ResourcesManager.getInstance().getResources(
+ mActivityToken,
+ mPackageInfo.getResDir(),
+ paths,
+ mPackageInfo.getOverlayDirs(),
+ mPackageInfo.getApplicationInfo().sharedLibraryFiles,
+ displayId,
+ null,
+ mPackageInfo.getCompatibilityInfo(),
+ classLoader);
+ return context;
+ }
+
+ @Override
public Context createConfigurationContext(Configuration overrideConfiguration) {
if (overrideConfiguration == null) {
throw new IllegalArgumentException("overrideConfiguration must not be null");
}
ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo, mActivityToken,
- mUser, mFlags);
+ mUser, mFlags, mClassLoader);
final int displayId = mDisplay != null ? mDisplay.getDisplayId() : Display.DEFAULT_DISPLAY;
context.mResources = createResources(mActivityToken, mPackageInfo, displayId,
@@ -1975,7 +2016,7 @@
}
ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo, mActivityToken,
- mUser, mFlags);
+ mUser, mFlags, mClassLoader);
final int displayId = display.getDisplayId();
context.mResources = createResources(mActivityToken, mPackageInfo, displayId, null,
@@ -1988,14 +2029,16 @@
public Context createDeviceProtectedStorageContext() {
final int flags = (mFlags & ~Context.CONTEXT_CREDENTIAL_PROTECTED_STORAGE)
| Context.CONTEXT_DEVICE_PROTECTED_STORAGE;
- return new ContextImpl(this, mMainThread, mPackageInfo, mActivityToken, mUser, flags);
+ return new ContextImpl(this, mMainThread, mPackageInfo, mActivityToken, mUser, flags,
+ mClassLoader);
}
@Override
public Context createCredentialProtectedStorageContext() {
final int flags = (mFlags & ~Context.CONTEXT_DEVICE_PROTECTED_STORAGE)
| Context.CONTEXT_CREDENTIAL_PROTECTED_STORAGE;
- return new ContextImpl(this, mMainThread, mPackageInfo, mActivityToken, mUser, flags);
+ return new ContextImpl(this, mMainThread, mPackageInfo, mActivityToken, mUser, flags,
+ mClassLoader);
}
@Override
@@ -2082,8 +2125,9 @@
static ContextImpl createSystemContext(ActivityThread mainThread) {
LoadedApk packageInfo = new LoadedApk(mainThread);
- ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, 0);
- context.mResources = packageInfo.getResources(mainThread);
+ ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, 0,
+ null);
+ context.mResources = packageInfo.getResources();
context.mResources.updateConfiguration(context.mResourcesManager.getConfiguration(),
context.mResourcesManager.getDisplayMetrics());
return context;
@@ -2091,18 +2135,35 @@
static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
- ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, 0);
- context.mResources = packageInfo.getResources(mainThread);
+ ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, 0,
+ null);
+ context.mResources = packageInfo.getResources();
return context;
}
static ContextImpl createActivityContext(ActivityThread mainThread,
- LoadedApk packageInfo, IBinder activityToken, int displayId,
+ LoadedApk packageInfo, ActivityInfo activityInfo, IBinder activityToken, int displayId,
Configuration overrideConfiguration) {
if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
+ String[] splitDirs = packageInfo.getSplitResDirs();
+ ClassLoader classLoader = packageInfo.getClassLoader();
+
+ if (packageInfo.getApplicationInfo().requestsIsolatedSplitLoading()) {
+ Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "SplitDependencies");
+ try {
+ classLoader = packageInfo.getSplitClassLoader(activityInfo.splitName);
+ splitDirs = packageInfo.getSplitPaths(activityInfo.splitName);
+ } catch (NameNotFoundException e) {
+ // Nothing above us can handle a NameNotFoundException, better crash.
+ throw new RuntimeException(e);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
+ }
+ }
+
ContextImpl context = new ContextImpl(null, mainThread, packageInfo, activityToken, null,
- 0);
+ 0, classLoader);
// Clamp display ID to DEFAULT_DISPLAY if it is INVALID_DISPLAY.
displayId = (displayId != Display.INVALID_DISPLAY) ? displayId : Display.DEFAULT_DISPLAY;
@@ -2117,20 +2178,21 @@
// will be rebased upon.
context.mResources = resourcesManager.createBaseActivityResources(activityToken,
packageInfo.getResDir(),
- packageInfo.getSplitResDirs(),
+ splitDirs,
packageInfo.getOverlayDirs(),
- packageInfo.getApplicationInfo().sharedLibraryFiles,
+ packageInfo.getSharedLibraries(),
displayId,
overrideConfiguration,
compatInfo,
- packageInfo.getClassLoader());
+ classLoader);
context.mDisplay = resourcesManager.getAdjustedDisplay(displayId,
context.mResources.getDisplayAdjustments());
return context;
}
private ContextImpl(ContextImpl container, ActivityThread mainThread,
- LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags) {
+ LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags,
+ ClassLoader classLoader) {
mOuterContext = this;
// If creator didn't specify which storage to use, use the default
@@ -2155,6 +2217,7 @@
mUser = user;
mPackageInfo = packageInfo;
+ mClassLoader = classLoader;
mResourcesManager = ResourcesManager.getInstance();
if (container != null) {
diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java
index 4ab0743..17f5edd 100644
--- a/core/java/android/app/LoadedApk.java
+++ b/core/java/android/app/LoadedApk.java
@@ -27,9 +27,11 @@
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
+import android.content.pm.split.SplitDependencyLoaderHelper;
import android.content.res.AssetManager;
import android.content.res.CompatibilityInfo;
import android.content.res.Resources;
+import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.FileUtils;
@@ -41,23 +43,22 @@
import android.os.SystemProperties;
import android.os.Trace;
import android.os.UserHandle;
-import android.system.Os;
-import android.system.OsConstants;
-import android.system.ErrnoException;
import android.text.TextUtils;
import android.util.AndroidRuntimeException;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
+import android.util.SparseIntArray;
import android.view.Display;
import android.view.DisplayAdjustments;
+import com.android.internal.util.ArrayUtils;
+
import dalvik.system.BaseDexClassLoader;
import dalvik.system.VMRuntime;
import java.io.File;
-import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
@@ -65,13 +66,12 @@
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Objects;
-import libcore.io.IoUtils;
-
final class IntentReceiverLeaked extends AndroidRuntimeException {
public IntentReceiverLeaked(String msg) {
super(msg);
@@ -97,8 +97,6 @@
private ApplicationInfo mApplicationInfo;
private String mAppDir;
private String mResDir;
- private String[] mSplitAppDirs;
- private String[] mSplitResDirs;
private String[] mOverlayDirs;
private String[] mSharedLibraries;
private String mDataDir;
@@ -116,14 +114,18 @@
private ClassLoader mClassLoader;
private Application mApplication;
+ private String[] mSplitNames;
+ private String[] mSplitAppDirs;
+ private String[] mSplitResDirs;
+
private final ArrayMap<Context, ArrayMap<BroadcastReceiver, ReceiverDispatcher>> mReceivers
- = new ArrayMap<Context, ArrayMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher>>();
+ = new ArrayMap<>();
private final ArrayMap<Context, ArrayMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher>> mUnregisteredReceivers
- = new ArrayMap<Context, ArrayMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher>>();
+ = new ArrayMap<>();
private final ArrayMap<Context, ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher>> mServices
- = new ArrayMap<Context, ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher>>();
+ = new ArrayMap<>();
private final ArrayMap<Context, ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher>> mUnboundServices
- = new ArrayMap<Context, ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher>>();
+ = new ArrayMap<>();
int mClientCount = 0;
@@ -300,9 +302,18 @@
synchronized (this) {
createOrUpdateClassLoaderLocked(addedPaths);
if (mResources != null) {
- mResources = mActivityThread.getTopLevelResources(mResDir, mSplitResDirs,
- mOverlayDirs, mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY,
- this);
+ final String[] splitPaths;
+ try {
+ splitPaths = getSplitPaths(null);
+ } catch (PackageManager.NameNotFoundException e) {
+ // This should NEVER fail.
+ throw new AssertionError("null split not found");
+ }
+
+ mResources = ResourcesManager.getInstance().getResources(null, mResDir,
+ splitPaths, mOverlayDirs, mSharedLibraries,
+ Display.DEFAULT_DISPLAY, null, getCompatibilityInfo(),
+ getClassLoader());
}
}
}
@@ -313,8 +324,6 @@
mApplicationInfo = aInfo;
mAppDir = aInfo.sourceDir;
mResDir = aInfo.uid == myUid ? aInfo.sourceDir : aInfo.publicSourceDir;
- mSplitAppDirs = aInfo.splitSourceDirs;
- mSplitResDirs = aInfo.uid == myUid ? aInfo.splitSourceDirs : aInfo.splitPublicSourceDirs;
mOverlayDirs = aInfo.resourceDirs;
mSharedLibraries = aInfo.sharedLibraryFiles;
mDataDir = aInfo.dataDir;
@@ -322,19 +331,28 @@
mDataDirFile = FileUtils.newFileOrNull(aInfo.dataDir);
mDeviceProtectedDataDirFile = FileUtils.newFileOrNull(aInfo.deviceProtectedDataDir);
mCredentialProtectedDataDirFile = FileUtils.newFileOrNull(aInfo.credentialProtectedDataDir);
+
+ mSplitNames = aInfo.splitNames;
+ mSplitAppDirs = aInfo.splitSourceDirs;
+ mSplitResDirs = aInfo.uid == myUid ? aInfo.splitSourceDirs : aInfo.splitPublicSourceDirs;
+
+ if (aInfo.requestsIsolatedSplitLoading() && !ArrayUtils.isEmpty(mSplitNames)) {
+ mSplitLoader = new SplitDependencyLoader(aInfo.splitDependencies);
+ }
}
public static void makePaths(ActivityThread activityThread, ApplicationInfo aInfo,
List<String> outZipPaths, List<String> outLibPaths) {
final String appDir = aInfo.sourceDir;
- final String[] splitAppDirs = aInfo.splitSourceDirs;
final String libDir = aInfo.nativeLibraryDir;
final String[] sharedLibraries = aInfo.sharedLibraryFiles;
outZipPaths.clear();
outZipPaths.add(appDir);
- if (splitAppDirs != null) {
- Collections.addAll(outZipPaths, splitAppDirs);
+
+ // Do not load all available splits if the app requested isolated split loading.
+ if (aInfo.splitSourceDirs != null && !aInfo.requestsIsolatedSplitLoading()) {
+ Collections.addAll(outZipPaths, aInfo.splitSourceDirs);
}
if (outLibPaths != null) {
@@ -367,13 +385,18 @@
|| appDir.equals(instrumentedAppDir)) {
outZipPaths.clear();
outZipPaths.add(instrumentationAppDir);
- if (instrumentationSplitAppDirs != null) {
- Collections.addAll(outZipPaths, instrumentationSplitAppDirs);
- }
- if (!instrumentationAppDir.equals(instrumentedAppDir)) {
- outZipPaths.add(instrumentedAppDir);
- if (instrumentedSplitAppDirs != null) {
- Collections.addAll(outZipPaths, instrumentedSplitAppDirs);
+
+ // Only add splits if the app did not request isolated split loading.
+ if (!aInfo.requestsIsolatedSplitLoading()) {
+ if (instrumentationSplitAppDirs != null) {
+ Collections.addAll(outZipPaths, instrumentationSplitAppDirs);
+ }
+
+ if (!instrumentationAppDir.equals(instrumentedAppDir)) {
+ outZipPaths.add(instrumentedAppDir);
+ if (instrumentedSplitAppDirs != null) {
+ Collections.addAll(outZipPaths, instrumentedSplitAppDirs);
+ }
}
}
@@ -399,7 +422,7 @@
// will be added to zipPaths that shouldn't be part of the library path.
if (aInfo.primaryCpuAbi != null) {
// Add fake libs into the library search path if we target prior to N.
- if (aInfo.targetSdkVersion <= 23) {
+ if (aInfo.targetSdkVersion < Build.VERSION_CODES.N) {
outLibPaths.add("/system/fake-libs" +
(VMRuntime.is64BitAbi(aInfo.primaryCpuAbi) ? "64" : ""));
}
@@ -434,6 +457,116 @@
}
}
+ private class SplitDependencyLoader
+ extends SplitDependencyLoaderHelper<PackageManager.NameNotFoundException> {
+ private String[] mCachedBaseResourcePath;
+ private final String[][] mCachedResourcePaths;
+ private final ClassLoader[] mCachedSplitClassLoaders;
+
+ SplitDependencyLoader(SparseIntArray dependencies) {
+ super(dependencies);
+ mCachedResourcePaths = new String[mSplitNames.length][];
+ mCachedSplitClassLoaders = new ClassLoader[mSplitNames.length];
+ }
+
+ @Override
+ protected boolean isSplitCached(int splitIdx) {
+ if (splitIdx != -1) {
+ return mCachedSplitClassLoaders[splitIdx] != null;
+ }
+ return mClassLoader != null && mCachedBaseResourcePath != null;
+ }
+
+ private void addAllConfigSplits(String splitName, ArrayList<String> outAssetPaths) {
+ for (int i = 0; i < mSplitNames.length; i++) {
+ if (isConfigurationSplitOf(mSplitNames[i], splitName)) {
+ outAssetPaths.add(mSplitResDirs[i]);
+ }
+ }
+ }
+
+ @Override
+ protected void constructSplit(int splitIdx, int parentSplitIdx) throws
+ PackageManager.NameNotFoundException {
+ final ArrayList<String> splitPaths = new ArrayList<>();
+ if (splitIdx == -1) {
+ createOrUpdateClassLoaderLocked(null);
+ addAllConfigSplits(null, splitPaths);
+ mCachedBaseResourcePath = splitPaths.toArray(new String[splitPaths.size()]);
+ return;
+ }
+
+ final ClassLoader parent;
+ if (parentSplitIdx == -1) {
+ // The parent is the base APK, so use its ClassLoader as parent
+ // and its configuration splits as part of our own too.
+ parent = mClassLoader;
+ Collections.addAll(splitPaths, mCachedBaseResourcePath);
+ } else {
+ parent = mCachedSplitClassLoaders[parentSplitIdx];
+ Collections.addAll(splitPaths, mCachedResourcePaths[parentSplitIdx]);
+ }
+
+ mCachedSplitClassLoaders[splitIdx] = ApplicationLoaders.getDefault().getClassLoader(
+ mSplitAppDirs[splitIdx], getTargetSdkVersion(), false, null, null, parent);
+
+ splitPaths.add(mSplitResDirs[splitIdx]);
+ addAllConfigSplits(mSplitNames[splitIdx], splitPaths);
+ mCachedResourcePaths[splitIdx] = splitPaths.toArray(new String[splitPaths.size()]);
+ }
+
+ private int ensureSplitLoaded(String splitName)
+ throws PackageManager.NameNotFoundException {
+ final int idx;
+ if (splitName == null) {
+ idx = -1;
+ } else {
+ idx = Arrays.binarySearch(mSplitNames, splitName);
+ if (idx < 0) {
+ throw new PackageManager.NameNotFoundException(
+ "Split name '" + splitName + "' is not installed");
+ }
+ }
+
+ loadDependenciesForSplit(idx);
+ return idx;
+ }
+
+ ClassLoader getClassLoaderForSplit(String splitName)
+ throws PackageManager.NameNotFoundException {
+ final int idx = ensureSplitLoaded(splitName);
+ if (idx < 0) {
+ return mClassLoader;
+ }
+ return mCachedSplitClassLoaders[idx];
+ }
+
+ String[] getSplitPathsForSplit(String splitName)
+ throws PackageManager.NameNotFoundException {
+ final int idx = ensureSplitLoaded(splitName);
+ if (idx < 0) {
+ return mCachedBaseResourcePath;
+ }
+ return mCachedResourcePaths[idx];
+ }
+ }
+
+ private SplitDependencyLoader mSplitLoader;
+
+ ClassLoader getSplitClassLoader(String splitName) throws PackageManager.NameNotFoundException {
+ if (mSplitLoader == null) {
+ return mClassLoader;
+ }
+ return mSplitLoader.getClassLoaderForSplit(splitName);
+ }
+
+ String[] getSplitPaths(String splitName) throws PackageManager.NameNotFoundException {
+ if (mSplitLoader == null) {
+ return mSplitResDirs;
+ }
+ return mSplitLoader.getSplitPathsForSplit(splitName);
+ }
+
private void createOrUpdateClassLoaderLocked(List<String> addedPaths) {
if (mPackageName.equals("android")) {
// Note: This branch is taken for system server and we don't need to setup
@@ -790,6 +923,10 @@
return mOverlayDirs;
}
+ public String[] getSharedLibraries() {
+ return mSharedLibraries;
+ }
+
public String getDataDir() {
return mDataDir;
}
@@ -806,14 +943,24 @@
return mCredentialProtectedDataDirFile;
}
- public AssetManager getAssets(ActivityThread mainThread) {
- return getResources(mainThread).getAssets();
+ public AssetManager getAssets() {
+ return getResources().getAssets();
}
- public Resources getResources(ActivityThread mainThread) {
+ public Resources getResources() {
if (mResources == null) {
- mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
- mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, this);
+ final String[] splitPaths;
+ try {
+ splitPaths = getSplitPaths(null);
+ } catch (PackageManager.NameNotFoundException e) {
+ // This should never fail.
+ throw new AssertionError("null split not found");
+ }
+
+ mResources = ResourcesManager.getInstance().getResources(null, mResDir,
+ splitPaths, mOverlayDirs, mSharedLibraries,
+ Display.DEFAULT_DISPLAY, null, getCompatibilityInfo(),
+ getClassLoader());
}
return mResources;
}
@@ -870,8 +1017,7 @@
}
// Rewrite the R 'constants' for all library apks.
- SparseArray<String> packageIdentifiers = getAssets(mActivityThread)
- .getAssignedPackageIdentifiers();
+ SparseArray<String> packageIdentifiers = getAssets().getAssignedPackageIdentifiers();
final int N = packageIdentifiers.size();
for (int i = 0; i < N; i++) {
final int id = packageIdentifiers.keyAt(i);
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 82917d2..4172ed7 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -1022,6 +1022,7 @@
private Icon mLargeIcon;
private String mChannelId;
+ private long mTimeout;
/**
* Structure to encapsulate a named action that can be shown as part of this notification.
@@ -1766,6 +1767,7 @@
if (parcel.readInt() != 0) {
mChannelId = parcel.readString();
}
+ mTimeout = parcel.readLong();
}
@Override
@@ -1872,6 +1874,7 @@
that.color = this.color;
that.mChannelId = this.mChannelId;
+ that.mTimeout = this.mTimeout;
if (!heavy) {
that.lightenPayload(); // will clean out extras
@@ -2128,6 +2131,7 @@
} else {
parcel.writeInt(0);
}
+ parcel.writeLong(mTimeout);
}
/**
@@ -2325,6 +2329,13 @@
}
/**
+ * Returns the time at which this notification should be canceled, if it's not canceled already.
+ */
+ public long getTimeout() {
+ return mTimeout;
+ }
+
+ /**
* The small icon representing this notification in the status bar and content view.
*
* @return the small icon representing this notification.
@@ -2532,6 +2543,15 @@
}
/**
+ * Specifies the time at which this notification should be canceled, if it is not already
+ * canceled.
+ */
+ public Builder setTimeout(long when) {
+ mN.mTimeout = when;
+ return this;
+ }
+
+ /**
* Add a timestamp pertaining to the notification (usually the time the event occurred).
*
* For apps targeting {@link android.os.Build.VERSION_CODES#N} and above, this time is not
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 38e6fbe..f00f605 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -4253,6 +4253,20 @@
int flags) throws PackageManager.NameNotFoundException;
/**
+ * Return a new Context object for the given split name. The new Context has a ClassLoader and
+ * Resources object that can access the split's and all of its dependencies' code/resources.
+ * Each call to this method returns a new instance of a Context object;
+ * Context objects are not shared, however common state (ClassLoader, other Resources for
+ * the same split) may be so the Context itself can be fairly lightweight.
+ *
+ * @param splitName The name of the split to include, as declared in the split's
+ * <code>AndroidManifest.xml</code>.
+ * @return A {@link Context} with the given split's code and/or resources loaded.
+ */
+ public abstract Context createContextForSplit(String splitName)
+ throws PackageManager.NameNotFoundException;
+
+ /**
* Get the userId associated with this context
* @return user id
*
diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java
index b131ecc..546bfc4 100644
--- a/core/java/android/content/ContextWrapper.java
+++ b/core/java/android/content/ContextWrapper.java
@@ -16,7 +16,6 @@
package android.content;
-import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.app.IApplicationThread;
import android.app.IServiceConnection;
@@ -826,6 +825,13 @@
/** @hide */
@Override
+ public Context createContextForSplit(String splitName)
+ throws PackageManager.NameNotFoundException {
+ return mBase.createContextForSplit(splitName);
+ }
+
+ /** @hide */
+ @Override
public int getUserId() {
return mBase.getUserId();
}
diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java
index 04ab239..3d9ba96 100644
--- a/core/java/android/content/pm/ApplicationInfo.java
+++ b/core/java/android/content/pm/ApplicationInfo.java
@@ -31,6 +31,7 @@
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Printer;
+import android.util.SparseIntArray;
import com.android.internal.util.ArrayUtils;
@@ -558,6 +559,14 @@
public static final int PRIVATE_FLAG_STATIC_SHARED_LIBRARY = 1 << 13;
/**
+ * Value for {@linl #privateFlags}: When set, the application will only have its splits loaded
+ * if they are required to load a component. Splits can be loaded on demand using the
+ * {@link Context#createContextForSplit(String)} API.
+ * @hide
+ */
+ public static final int PRIVATE_FLAG_ISOLATED_SPLIT_LOADING = 1 << 14;
+
+ /**
* Private/hidden flags. See {@code PRIVATE_FLAG_...} constants.
* {@hide}
*/
@@ -607,8 +616,12 @@
public String publicSourceDir;
/**
- * Full paths to zero or more split APKs that, when combined with the base
- * APK defined in {@link #sourceDir}, form a complete application.
+ * The names of all installed split APKs, ordered lexicographically.
+ */
+ public String[] splitNames;
+
+ /**
+ * Full paths to zero or more split APKs, indexed by the same order as {@link #splitNames}.
*/
public String[] splitSourceDirs;
@@ -616,14 +629,35 @@
* Full path to the publicly available parts of {@link #splitSourceDirs},
* including resources and manifest. This may be different from
* {@link #splitSourceDirs} if an application is forward locked.
+ *
+ * @see #splitSourceDirs
*/
public String[] splitPublicSourceDirs;
/**
- * Full paths to the locations of extra resource packages this application
- * uses. This field is only used if there are extra resource packages,
- * otherwise it is null.
- *
+ * Maps the dependencies between split APKs. All splits implicitly depend on the base APK.
+ *
+ * Available since platform version O.
+ *
+ * Only populated if the application opts in to isolated split loading via the
+ * {@link android.R.attr.isolatedSplits} attribute in the <manifest> tag of the app's
+ * AndroidManifest.xml.
+ *
+ * The keys and values are all indices into the {@link #splitNames}, {@link #splitSourceDirs},
+ * and {@link #splitPublicSourceDirs} arrays.
+ * Each key represents a split and its value is its parent split.
+ * Cycles do not exist because they are illegal and screened for during installation.
+ *
+ * May be null if no splits are installed, or if no dependencies exist between them.
+ * @hide
+ */
+ public SparseIntArray splitDependencies;
+
+ /**
+ * Full paths to the locations of extra resource packages (runtime overlays)
+ * this application uses. This field is only used if there are extra resource
+ * packages, otherwise it is null.
+ *
* {@hide}
*/
public String[] resourceDirs;
@@ -1058,8 +1092,10 @@
scanPublicSourceDir = orig.scanPublicSourceDir;
sourceDir = orig.sourceDir;
publicSourceDir = orig.publicSourceDir;
+ splitNames = orig.splitNames;
splitSourceDirs = orig.splitSourceDirs;
splitPublicSourceDirs = orig.splitPublicSourceDirs;
+ splitDependencies = orig.splitDependencies;
nativeLibraryDir = orig.nativeLibraryDir;
secondaryNativeLibraryDir = orig.secondaryNativeLibraryDir;
nativeLibraryRootDir = orig.nativeLibraryRootDir;
@@ -1098,6 +1134,7 @@
return 0;
}
+ @SuppressWarnings("unchecked")
public void writeToParcel(Parcel dest, int parcelableFlags) {
super.writeToParcel(dest, parcelableFlags);
dest.writeString(taskAffinity);
@@ -1115,8 +1152,10 @@
dest.writeString(scanPublicSourceDir);
dest.writeString(sourceDir);
dest.writeString(publicSourceDir);
+ dest.writeStringArray(splitNames);
dest.writeStringArray(splitSourceDirs);
dest.writeStringArray(splitPublicSourceDirs);
+ dest.writeSparseIntArray(splitDependencies);
dest.writeString(nativeLibraryDir);
dest.writeString(secondaryNativeLibraryDir);
dest.writeString(nativeLibraryRootDir);
@@ -1155,6 +1194,7 @@
}
};
+ @SuppressWarnings("unchecked")
private ApplicationInfo(Parcel source) {
super(source);
taskAffinity = source.readString();
@@ -1172,8 +1212,10 @@
scanPublicSourceDir = source.readString();
sourceDir = source.readString();
publicSourceDir = source.readString();
+ splitNames = source.readStringArray();
splitSourceDirs = source.readStringArray();
splitPublicSourceDirs = source.readStringArray();
+ splitDependencies = source.readSparseIntArray();
nativeLibraryDir = source.readString();
secondaryNativeLibraryDir = source.readString();
nativeLibraryRootDir = source.readString();
@@ -1363,6 +1405,15 @@
}
/**
+ * Returns true if the app has declared in its manifest that it wants its split APKs to be
+ * loaded into isolated Contexts, with their own ClassLoaders and Resources objects.
+ * @hide
+ */
+ public boolean requestsIsolatedSplitLoading() {
+ return (privateFlags & ApplicationInfo.PRIVATE_FLAG_ISOLATED_SPLIT_LOADING) != 0;
+ }
+
+ /**
* @hide
*/
public boolean isStaticSharedLibrary() {
diff --git a/core/java/android/content/pm/ComponentInfo.java b/core/java/android/content/pm/ComponentInfo.java
index b091d7e..53be953 100644
--- a/core/java/android/content/pm/ComponentInfo.java
+++ b/core/java/android/content/pm/ComponentInfo.java
@@ -45,6 +45,12 @@
public String processName;
/**
+ * The name of the split in which this component is declared.
+ * Null if the component was declared in the base APK.
+ */
+ public String splitName;
+
+ /**
* A string resource identifier (in the package's resources) containing
* a user-readable description of the component. From the "description"
* attribute or, if not set, 0.
@@ -53,7 +59,7 @@
/**
* Indicates whether or not this component may be instantiated. Note that this value can be
- * overriden by the one in its parent {@link ApplicationInfo}.
+ * overridden by the one in its parent {@link ApplicationInfo}.
*/
public boolean enabled = true;
@@ -72,11 +78,6 @@
*/
public boolean directBootAware = false;
- /**
- * The name of the split that contains the code for this component.
- */
- public String splitName;
-
/** @removed */
@Deprecated
public boolean encryptionAware = false;
@@ -88,6 +89,7 @@
super(orig);
applicationInfo = orig.applicationInfo;
processName = orig.processName;
+ splitName = orig.splitName;
descriptionRes = orig.descriptionRes;
enabled = orig.enabled;
exported = orig.exported;
@@ -168,6 +170,9 @@
if (processName != null && !packageName.equals(processName)) {
pw.println(prefix + "processName=" + processName);
}
+ if (splitName != null) {
+ pw.println(prefix + "splitName=" + splitName);
+ }
pw.println(prefix + "enabled=" + enabled + " exported=" + exported
+ " directBootAware=" + directBootAware);
if (descriptionRes != 0) {
@@ -200,6 +205,7 @@
applicationInfo.writeToParcel(dest, parcelableFlags);
}
dest.writeString(processName);
+ dest.writeString(splitName);
dest.writeInt(descriptionRes);
dest.writeInt(enabled ? 1 : 0);
dest.writeInt(exported ? 1 : 0);
@@ -213,6 +219,7 @@
applicationInfo = ApplicationInfo.CREATOR.createFromParcel(source);
}
processName = source.readString();
+ splitName = source.readString();
descriptionRes = source.readInt();
enabled = (source.readInt() != 0);
exported = (source.readInt() != 0);
diff --git a/core/java/android/content/pm/InstrumentationInfo.java b/core/java/android/content/pm/InstrumentationInfo.java
index 9d88cdd..a135d8f 100644
--- a/core/java/android/content/pm/InstrumentationInfo.java
+++ b/core/java/android/content/pm/InstrumentationInfo.java
@@ -18,6 +18,8 @@
import android.os.Parcel;
import android.os.Parcelable;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
/**
* Information you can retrieve about a particular piece of test
@@ -44,8 +46,12 @@
public String publicSourceDir;
/**
- * Full paths to zero or more split APKs that, when combined with the base
- * APK defined in {@link #sourceDir}, form a complete application.
+ * The names of all installed split APKs, ordered lexicographically.
+ */
+ public String[] splitNames;
+
+ /**
+ * Full paths to zero or more split APKs, indexed by the same order as {@link #splitNames}.
*/
public String[] splitSourceDirs;
@@ -53,10 +59,31 @@
* Full path to the publicly available parts of {@link #splitSourceDirs},
* including resources and manifest. This may be different from
* {@link #splitSourceDirs} if an application is forward locked.
+ *
+ * @see #splitSourceDirs
*/
public String[] splitPublicSourceDirs;
/**
+ * Maps the dependencies between split APKs. All splits implicitly depend on the base APK.
+ *
+ * Available since platform version O.
+ *
+ * Only populated if the application opts in to isolated split loading via the
+ * {@link android.R.attr.isolatedSplits} attribute in the <manifest> tag of the app's
+ * AndroidManifest.xml.
+ *
+ * The keys and values are all indices into the {@link #splitNames}, {@link #splitSourceDirs},
+ * and {@link #splitPublicSourceDirs} arrays.
+ * Each key represents a split and its value is its parent split.
+ * Cycles do not exist because they are illegal and screened for during installation.
+ *
+ * May be null if no splits are installed, or if no dependencies exist between them.
+ * @hide
+ */
+ public SparseIntArray splitDependencies;
+
+ /**
* Full path to a directory assigned to the package for its persistent data.
*/
public String dataDir;
@@ -88,8 +115,10 @@
targetPackage = orig.targetPackage;
sourceDir = orig.sourceDir;
publicSourceDir = orig.publicSourceDir;
+ splitNames = orig.splitNames;
splitSourceDirs = orig.splitSourceDirs;
splitPublicSourceDirs = orig.splitPublicSourceDirs;
+ splitDependencies = orig.splitDependencies;
dataDir = orig.dataDir;
deviceProtectedDataDir = orig.deviceProtectedDataDir;
credentialProtectedDataDir = orig.credentialProtectedDataDir;
@@ -114,8 +143,10 @@
dest.writeString(targetPackage);
dest.writeString(sourceDir);
dest.writeString(publicSourceDir);
+ dest.writeStringArray(splitNames);
dest.writeStringArray(splitSourceDirs);
dest.writeStringArray(splitPublicSourceDirs);
+ dest.writeSparseIntArray(splitDependencies);
dest.writeString(dataDir);
dest.writeString(deviceProtectedDataDir);
dest.writeString(credentialProtectedDataDir);
@@ -135,13 +166,16 @@
}
};
+ @SuppressWarnings("unchecked")
private InstrumentationInfo(Parcel source) {
super(source);
targetPackage = source.readString();
sourceDir = source.readString();
publicSourceDir = source.readString();
+ splitNames = source.readStringArray();
splitSourceDirs = source.readStringArray();
splitPublicSourceDirs = source.readStringArray();
+ splitDependencies = source.readSparseIntArray();
dataDir = source.readString();
deviceProtectedDataDir = source.readString();
credentialProtectedDataDir = source.readString();
@@ -156,8 +190,10 @@
ai.packageName = packageName;
ai.sourceDir = sourceDir;
ai.publicSourceDir = publicSourceDir;
+ ai.splitNames = splitNames;
ai.splitSourceDirs = splitSourceDirs;
ai.splitPublicSourceDirs = splitPublicSourceDirs;
+ ai.splitDependencies = splitDependencies;
ai.dataDir = dataDir;
ai.deviceProtectedDataDir = deviceProtectedDataDir;
ai.credentialProtectedDataDir = credentialProtectedDataDir;
diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java
index cd51bce..8223726 100644
--- a/core/java/android/content/pm/PackageParser.java
+++ b/core/java/android/content/pm/PackageParser.java
@@ -49,6 +49,9 @@
import android.content.ComponentName;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.pm.split.SplitAssetDependencyLoader;
+import android.content.pm.split.SplitAssetLoader;
+import android.content.pm.split.DefaultSplitAssetLoader;
import android.content.res.AssetManager;
import android.content.res.Configuration;
import android.content.res.Resources;
@@ -74,6 +77,7 @@
import android.util.Log;
import android.util.Pair;
import android.util.Slog;
+import android.util.SparseIntArray;
import android.util.TypedValue;
import android.util.apk.ApkSignatureSchemeV2Verifier;
import android.util.jar.StrictJarFile;
@@ -106,6 +110,7 @@
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.BitSet;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
@@ -155,6 +160,7 @@
private static final String TAG_MANIFEST = "manifest";
private static final String TAG_APPLICATION = "application";
+ private static final String TAG_PACKAGE_VERIFIER = "package-verifier";
private static final String TAG_OVERLAY = "overlay";
private static final String TAG_KEY_SETS = "key-sets";
private static final String TAG_PERMISSION_GROUP = "permission-group";
@@ -178,6 +184,7 @@
private static final String TAG_EAT_COMMENT = "eat-comment";
private static final String TAG_PACKAGE = "package";
private static final String TAG_RESTRICT_UPDATE = "restrict-update";
+ private static final String TAG_USES_SPLIT = "uses-split";
/**
* Bit mask of all the valid bits that can be set in restartOnConfigChanges.
@@ -352,6 +359,9 @@
/** Names of any split APKs, ordered by parsed splitName */
public final String[] splitNames;
+ /** Dependencies of any split APKs, ordered by parsed splitName */
+ public final String[] usesSplitNames;
+
/**
* Path where this package was found on disk. For monolithic packages
* this is path to single base APK file; for cluster packages this is
@@ -374,14 +384,17 @@
public final boolean multiArch;
public final boolean use32bitAbi;
public final boolean extractNativeLibs;
+ public final boolean isolatedSplits;
public PackageLite(String codePath, ApkLite baseApk, String[] splitNames,
- String[] splitCodePaths, int[] splitRevisionCodes) {
+ String[] usesSplitNames, String[] splitCodePaths,
+ int[] splitRevisionCodes) {
this.packageName = baseApk.packageName;
this.versionCode = baseApk.versionCode;
this.installLocation = baseApk.installLocation;
this.verifiers = baseApk.verifiers;
this.splitNames = splitNames;
+ this.usesSplitNames = usesSplitNames;
this.codePath = codePath;
this.baseCodePath = baseApk.codePath;
this.splitCodePaths = splitCodePaths;
@@ -392,6 +405,7 @@
this.multiArch = baseApk.multiArch;
this.use32bitAbi = baseApk.use32bitAbi;
this.extractNativeLibs = baseApk.extractNativeLibs;
+ this.isolatedSplits = baseApk.isolatedSplits;
}
public List<String> getAllCodePaths() {
@@ -411,6 +425,7 @@
public final String codePath;
public final String packageName;
public final String splitName;
+ public final String usesSplitName;
public final int versionCode;
public final int revisionCode;
public final int installLocation;
@@ -422,15 +437,17 @@
public final boolean multiArch;
public final boolean use32bitAbi;
public final boolean extractNativeLibs;
+ public final boolean isolatedSplits;
- public ApkLite(String codePath, String packageName, String splitName, int versionCode,
- int revisionCode, int installLocation, List<VerifierInfo> verifiers,
+ public ApkLite(String codePath, String packageName, String splitName, String usesSplitName,
+ int versionCode, int revisionCode, int installLocation, List<VerifierInfo> verifiers,
Signature[] signatures, Certificate[][] certificates, boolean coreApp,
boolean debuggable, boolean multiArch, boolean use32bitAbi,
- boolean extractNativeLibs) {
+ boolean extractNativeLibs, boolean isolatedSplits) {
this.codePath = codePath;
this.packageName = packageName;
this.splitName = splitName;
+ this.usesSplitName = usesSplitName;
this.versionCode = versionCode;
this.revisionCode = revisionCode;
this.installLocation = installLocation;
@@ -442,6 +459,7 @@
this.multiArch = multiArch;
this.use32bitAbi = use32bitAbi;
this.extractNativeLibs = extractNativeLibs;
+ this.isolatedSplits = isolatedSplits;
}
}
@@ -492,7 +510,7 @@
return isApkPath(file.getName());
}
- private static boolean isApkPath(String path) {
+ public static boolean isApkPath(String path) {
return path.endsWith(".apk");
}
@@ -738,23 +756,23 @@
public static PackageLite parsePackageLite(File packageFile, int flags)
throws PackageParserException {
if (packageFile.isDirectory()) {
- return parseClusterPackageLite(packageFile, flags, null);
+ return parseClusterPackageLite(packageFile, flags);
} else {
- return parseMonolithicPackageLite(packageFile, flags, null);
+ return parseMonolithicPackageLite(packageFile, flags);
}
}
- private static PackageLite parseMonolithicPackageLite(File packageFile, int flags,
- AssetManager cachedAssetManager) throws PackageParserException {
+ private static PackageLite parseMonolithicPackageLite(File packageFile, int flags)
+ throws PackageParserException {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "parseApkLite");
- final ApkLite baseApk = parseApkLite(packageFile, flags, cachedAssetManager);
+ final ApkLite baseApk = parseApkLite(packageFile, flags);
final String packagePath = packageFile.getAbsolutePath();
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
- return new PackageLite(packagePath, baseApk, null, null, null);
+ return new PackageLite(packagePath, baseApk, null, null, null, null);
}
- private static PackageLite parseClusterPackageLite(File packageDir, int flags,
- AssetManager cachedAssetManager) throws PackageParserException {
+ private static PackageLite parseClusterPackageLite(File packageDir, int flags)
+ throws PackageParserException {
final File[] files = packageDir.listFiles();
if (ArrayUtils.isEmpty(files)) {
throw new PackageParserException(INSTALL_PARSE_FAILED_NOT_APK,
@@ -768,7 +786,7 @@
final ArrayMap<String, ApkLite> apks = new ArrayMap<>();
for (File file : files) {
if (isApkFile(file)) {
- final ApkLite lite = parseApkLite(file, flags, cachedAssetManager);
+ final ApkLite lite = parseApkLite(file, flags);
// Assert that all package names and version codes are
// consistent with the first one we encounter.
@@ -808,10 +826,12 @@
final int size = apks.size();
String[] splitNames = null;
+ String[] usesSplitNames = null;
String[] splitCodePaths = null;
int[] splitRevisionCodes = null;
if (size > 0) {
splitNames = new String[size];
+ usesSplitNames = new String[size];
splitCodePaths = new String[size];
splitRevisionCodes = new int[size];
@@ -819,13 +839,15 @@
Arrays.sort(splitNames, sSplitNameComparator);
for (int i = 0; i < size; i++) {
- splitCodePaths[i] = apks.get(splitNames[i]).codePath;
- splitRevisionCodes[i] = apks.get(splitNames[i]).revisionCode;
+ final ApkLite apk = apks.get(splitNames[i]);
+ usesSplitNames[i] = apk.usesSplitName;
+ splitCodePaths[i] = apk.codePath;
+ splitRevisionCodes[i] = apk.revisionCode;
}
}
final String codePath = packageDir.getAbsolutePath();
- return new PackageLite(codePath, baseApk, splitNames, splitCodePaths,
+ return new PackageLite(codePath, baseApk, splitNames, usesSplitNames, splitCodePaths,
splitRevisionCodes);
}
@@ -1004,6 +1026,42 @@
}
}
+ private static SparseIntArray buildSplitDependencyTree(PackageLite pkg)
+ throws PackageParserException {
+ SparseIntArray splitDependencies = new SparseIntArray();
+ for (int splitIdx = 0; splitIdx < pkg.splitNames.length; splitIdx++) {
+ final String splitDependency = pkg.usesSplitNames[splitIdx];
+ if (splitDependency != null) {
+ final int depIdx = Arrays.binarySearch(pkg.splitNames, splitDependency);
+ if (depIdx < 0) {
+ throw new PackageParserException(
+ PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST,
+ "Split '" + pkg.splitNames[splitIdx] + "' requires split '"
+ + splitDependency + "', which is missing.");
+ }
+ splitDependencies.put(splitIdx, depIdx);
+ }
+ }
+
+ // Verify that there are no cycles.
+ final BitSet bitset = new BitSet();
+ for (int i = 0; i < splitDependencies.size(); i++) {
+ int splitIdx = splitDependencies.keyAt(i);
+
+ bitset.clear();
+ while (splitIdx != -1) {
+ if (bitset.get(splitIdx)) {
+ throw new PackageParserException(
+ PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST,
+ "Cycle detected in split dependencies.");
+ }
+ bitset.set(splitIdx);
+ splitIdx = splitDependencies.get(splitIdx, -1);
+ }
+ }
+ return splitDependencies.size() != 0 ? splitDependencies : null;
+ }
+
/**
* Parse all APKs contained in the given directory, treating them as a
* single package. This also performs sanity checking, such as requiring
@@ -1014,25 +1072,24 @@
* must be done separately in {@link #collectCertificates(Package, int)}.
*/
private Package parseClusterPackage(File packageDir, int flags) throws PackageParserException {
- final AssetManager assets = newConfiguredAssetManager();
- final PackageLite lite = parseClusterPackageLite(packageDir, 0, assets);
-
+ final PackageLite lite = parseClusterPackageLite(packageDir, 0);
if (mOnlyCoreApps && !lite.coreApp) {
throw new PackageParserException(INSTALL_PARSE_FAILED_MANIFEST_MALFORMED,
"Not a coreApp: " + packageDir);
}
+ // Build the split dependency tree.
+ SparseIntArray splitDependencies = null;
+ final SplitAssetLoader assetLoader;
+ if (lite.isolatedSplits && !ArrayUtils.isEmpty(lite.splitNames)) {
+ splitDependencies = buildSplitDependencyTree(lite);
+ assetLoader = new SplitAssetDependencyLoader(lite, splitDependencies, flags);
+ } else {
+ assetLoader = new DefaultSplitAssetLoader(lite, flags);
+ }
+
try {
- // Load all splits into the AssetManager (base has already been loaded earlier)
- // so that resources can be overriden when parsing the manifests.
- loadApkIntoAssetManager(assets, lite.baseCodePath, flags);
-
- if (!ArrayUtils.isEmpty(lite.splitCodePaths)) {
- for (String path : lite.splitCodePaths) {
- loadApkIntoAssetManager(assets, path, flags);
- }
- }
-
+ final AssetManager assets = assetLoader.getBaseAssetManager();
final File baseApk = new File(lite.baseCodePath);
final Package pkg = parseBaseApk(baseApk, assets, flags);
if (pkg == null) {
@@ -1047,9 +1104,12 @@
pkg.splitRevisionCodes = lite.splitRevisionCodes;
pkg.splitFlags = new int[num];
pkg.splitPrivateFlags = new int[num];
+ pkg.applicationInfo.splitNames = pkg.splitNames;
+ pkg.applicationInfo.splitDependencies = splitDependencies;
for (int i = 0; i < num; i++) {
- parseSplitApk(pkg, i, assets, flags);
+ final AssetManager splitAssets = assetLoader.getSplitAssetManager(i);
+ parseSplitApk(pkg, i, splitAssets, flags);
}
}
@@ -1057,7 +1117,7 @@
pkg.setUse32bitAbi(lite.use32bitAbi);
return pkg;
} finally {
- IoUtils.closeQuietly(assets);
+ IoUtils.closeQuietly(assetLoader);
}
}
@@ -1074,7 +1134,7 @@
@Deprecated
public Package parseMonolithicPackage(File apkFile, int flags) throws PackageParserException {
final AssetManager assets = newConfiguredAssetManager();
- final PackageLite lite = parseMonolithicPackageLite(apkFile, flags, assets);
+ final PackageLite lite = parseMonolithicPackageLite(apkFile, flags);
if (mOnlyCoreApps) {
if (!lite.coreApp) {
throw new PackageParserException(INSTALL_PARSE_FAILED_MANIFEST_MALFORMED,
@@ -1168,7 +1228,7 @@
final int cookie = loadApkIntoAssetManager(assets, apkPath, flags);
- Resources res = null;
+ final Resources res;
XmlResourceParser parser = null;
try {
res = new Resources(assets, mMetrics, null);
@@ -1225,7 +1285,7 @@
}
String tagName = parser.getName();
- if (tagName.equals("application")) {
+ if (tagName.equals(TAG_APPLICATION)) {
if (foundApp) {
if (RIGID_PARSER) {
outError[0] = "<manifest> has more than one <application>";
@@ -1523,17 +1583,12 @@
*/
public static ApkLite parseApkLite(File apkFile, int flags)
throws PackageParserException {
- return parseApkLite(apkFile, flags, null);
- }
-
- private static ApkLite parseApkLite(File apkFile, int flags,
- @Nullable AssetManager cachedAssetManager) throws PackageParserException {
final String apkPath = apkFile.getAbsolutePath();
AssetManager assets = null;
XmlResourceParser parser = null;
try {
- assets = cachedAssetManager == null ? newConfiguredAssetManager() : cachedAssetManager;
+ assets = newConfiguredAssetManager();
int cookie = assets.addAssetPath(apkPath);
if (cookie == 0) {
throw new PackageParserException(INSTALL_PARSE_FAILED_NOT_APK,
@@ -1543,7 +1598,6 @@
final DisplayMetrics metrics = new DisplayMetrics();
metrics.setToDefaults();
- final Resources res = new Resources(assets, metrics, null);
parser = assets.openXmlResourceParser(cookie, ANDROID_MANIFEST_FILENAME);
final Signature[] signatures;
@@ -1565,16 +1619,14 @@
}
final AttributeSet attrs = parser;
- return parseApkLite(apkPath, res, parser, attrs, flags, signatures, certificates);
+ return parseApkLite(apkPath, parser, attrs, flags, signatures, certificates);
} catch (XmlPullParserException | IOException | RuntimeException e) {
throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,
"Failed to parse " + apkPath, e);
} finally {
IoUtils.closeQuietly(parser);
- if (cachedAssetManager == null) {
- IoUtils.closeQuietly(assets);
- }
+ IoUtils.closeQuietly(assets);
}
}
@@ -1652,9 +1704,9 @@
(splitName != null) ? splitName.intern() : splitName);
}
- private static ApkLite parseApkLite(String codePath, Resources res, XmlPullParser parser,
- AttributeSet attrs, int flags, Signature[] signatures, Certificate[][] certificates)
- throws IOException, XmlPullParserException, PackageParserException {
+ private static ApkLite parseApkLite(String codePath, XmlPullParser parser, AttributeSet attrs,
+ int flags, Signature[] signatures, Certificate[][] certificates)
+ throws IOException, XmlPullParserException, PackageParserException {
final Pair<String, String> packageSplit = parsePackageSplitNames(parser, attrs);
int installLocation = PARSE_DEFAULT_INSTALL_LOCATION;
@@ -1665,6 +1717,8 @@
boolean multiArch = false;
boolean use32bitAbi = false;
boolean extractNativeLibs = true;
+ boolean isolatedSplits = false;
+ String usesSplitName = null;
for (int i = 0; i < attrs.getAttributeCount(); i++) {
final String attr = attrs.getAttributeName(i);
@@ -1677,6 +1731,8 @@
revisionCode = attrs.getAttributeIntValue(i, 0);
} else if (attr.equals("coreApp")) {
coreApp = attrs.getAttributeBooleanValue(i, false);
+ } else if (attr.equals("isolatedSplits")) {
+ isolatedSplits = attrs.getAttributeBooleanValue(i, false);
}
}
@@ -1691,14 +1747,16 @@
continue;
}
- if (parser.getDepth() == searchDepth && "package-verifier".equals(parser.getName())) {
- final VerifierInfo verifier = parseVerifier(res, parser, attrs, flags);
+ if (parser.getDepth() != searchDepth) {
+ continue;
+ }
+
+ if (TAG_PACKAGE_VERIFIER.equals(parser.getName())) {
+ final VerifierInfo verifier = parseVerifier(attrs);
if (verifier != null) {
verifiers.add(verifier);
}
- }
-
- if (parser.getDepth() == searchDepth && "application".equals(parser.getName())) {
+ } else if (TAG_APPLICATION.equals(parser.getName())) {
for (int i = 0; i < attrs.getAttributeCount(); ++i) {
final String attr = attrs.getAttributeName(i);
if ("debuggable".equals(attr)) {
@@ -1714,12 +1772,25 @@
extractNativeLibs = attrs.getAttributeBooleanValue(i, true);
}
}
+ } else if (TAG_USES_SPLIT.equals(parser.getName())) {
+ if (usesSplitName != null) {
+ Slog.w(TAG, "Only one <uses-split> permitted. Ignoring others.");
+ continue;
+ }
+
+ usesSplitName = attrs.getAttributeValue(ANDROID_RESOURCES, "name");
+ if (usesSplitName == null) {
+ throw new PackageParserException(
+ PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED,
+ "<uses-split> tag requires 'android:name' attribute");
+ }
}
}
- return new ApkLite(codePath, packageSplit.first, packageSplit.second, versionCode,
- revisionCode, installLocation, verifiers, signatures, certificates, coreApp,
- debuggable, multiArch, use32bitAbi, extractNativeLibs);
+ return new ApkLite(codePath, packageSplit.first, packageSplit.second, usesSplitName,
+ versionCode, revisionCode, installLocation, verifiers, signatures,
+ certificates, coreApp, debuggable, multiArch, use32bitAbi, extractNativeLibs,
+ isolatedSplits);
}
/**
@@ -1940,6 +2011,10 @@
pkg.applicationInfo.privateFlags |= ApplicationInfo.PRIVATE_FLAG_EPHEMERAL;
}
+ if (sa.getBoolean(com.android.internal.R.styleable.AndroidManifest_isolatedSplits, false)) {
+ pkg.applicationInfo.privateFlags |= ApplicationInfo.PRIVATE_FLAG_ISOLATED_SPLIT_LOADING;
+ }
+
// Resource boolean are -1, so 1 means we don't know the value.
int supportsSmallScreens = 1;
int supportsNormalScreens = 1;
@@ -3657,6 +3732,8 @@
continue;
}
+ ComponentInfo parsedComponent = null;
+
String tagName = parser.getName();
if (tagName.equals("activity")) {
Activity a = parseActivity(owner, res, parser, flags, outError, false,
@@ -3667,6 +3744,7 @@
}
owner.activities.add(a);
+ parsedComponent = a.info;
} else if (tagName.equals("receiver")) {
Activity a = parseActivity(owner, res, parser, flags, outError, true, false);
@@ -3676,6 +3754,7 @@
}
owner.receivers.add(a);
+ parsedComponent = a.info;
} else if (tagName.equals("service")) {
Service s = parseService(owner, res, parser, flags, outError);
@@ -3685,6 +3764,7 @@
}
owner.services.add(s);
+ parsedComponent = s.info;
} else if (tagName.equals("provider")) {
Provider p = parseProvider(owner, res, parser, flags, outError);
@@ -3694,6 +3774,7 @@
}
owner.providers.add(p);
+ parsedComponent = p.info;
} else if (tagName.equals("activity-alias")) {
Activity a = parseActivityAlias(owner, res, parser, flags, outError);
@@ -3703,6 +3784,7 @@
}
owner.activities.add(a);
+ parsedComponent = a.info;
} else if (parser.getName().equals("meta-data")) {
// note: application meta-data is stored off to the side, so it can
@@ -3769,6 +3851,14 @@
return false;
}
}
+
+ if (parsedComponent != null && parsedComponent.splitName == null) {
+ // If the loaded component did not specify a split, inherit the split name
+ // based on the split it is defined in.
+ // This is used to later load the correct split when starting this
+ // component.
+ parsedComponent.splitName = owner.splitNames[splitIndex];
+ }
}
return true;
@@ -5039,18 +5129,23 @@
return data;
}
- private static VerifierInfo parseVerifier(Resources res, XmlPullParser parser,
- AttributeSet attrs, int flags) {
- final TypedArray sa = res.obtainAttributes(attrs,
- com.android.internal.R.styleable.AndroidManifestPackageVerifier);
+ private static VerifierInfo parseVerifier(AttributeSet attrs) {
+ String packageName = null;
+ String encodedPublicKey = null;
- final String packageName = sa.getNonResourceString(
- com.android.internal.R.styleable.AndroidManifestPackageVerifier_name);
+ final int attrCount = attrs.getAttributeCount();
+ for (int i = 0; i < attrCount; i++) {
+ final int attrResId = attrs.getAttributeNameResource(i);
+ switch (attrResId) {
+ case com.android.internal.R.attr.name:
+ packageName = attrs.getAttributeValue(i);
+ break;
- final String encodedPublicKey = sa.getNonResourceString(
- com.android.internal.R.styleable.AndroidManifestPackageVerifier_publicKey);
-
- sa.recycle();
+ case com.android.internal.R.attr.publicKey:
+ encodedPublicKey = attrs.getAttributeValue(i);
+ break;
+ }
+ }
if (packageName == null || packageName.length() == 0) {
Slog.i(TAG, "verifier package name was null; skipping");
diff --git a/core/java/android/content/pm/split/DefaultSplitAssetLoader.java b/core/java/android/content/pm/split/DefaultSplitAssetLoader.java
new file mode 100644
index 0000000..5a9966d
--- /dev/null
+++ b/core/java/android/content/pm/split/DefaultSplitAssetLoader.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.content.pm.split;
+
+import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST;
+import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_NOT_APK;
+
+import android.content.pm.PackageParser;
+import android.content.res.AssetManager;
+import android.os.Build;
+
+import com.android.internal.util.ArrayUtils;
+
+import libcore.io.IoUtils;
+
+/**
+ * Loads the base and split APKs into a single AssetManager.
+ * @hide
+ */
+public class DefaultSplitAssetLoader implements SplitAssetLoader {
+ private final String mBaseCodePath;
+ private final String[] mSplitCodePaths;
+ private final int mFlags;
+
+ private AssetManager mCachedAssetManager;
+
+ public DefaultSplitAssetLoader(PackageParser.PackageLite pkg, int flags) {
+ mBaseCodePath = pkg.baseCodePath;
+ mSplitCodePaths = pkg.splitCodePaths;
+ mFlags = flags;
+ }
+
+ private static void loadApkIntoAssetManager(AssetManager assets, String apkPath, int flags)
+ throws PackageParser.PackageParserException {
+ if ((flags & PackageParser.PARSE_MUST_BE_APK) != 0 && !PackageParser.isApkPath(apkPath)) {
+ throw new PackageParser.PackageParserException(INSTALL_PARSE_FAILED_NOT_APK,
+ "Invalid package file: " + apkPath);
+ }
+
+ if (assets.addAssetPath(apkPath) == 0) {
+ throw new PackageParser.PackageParserException(
+ INSTALL_PARSE_FAILED_BAD_MANIFEST,
+ "Failed adding asset path: " + apkPath);
+ }
+ }
+
+ @Override
+ public AssetManager getBaseAssetManager() throws PackageParser.PackageParserException {
+ if (mCachedAssetManager != null) {
+ return mCachedAssetManager;
+ }
+
+ AssetManager assets = new AssetManager();
+ try {
+ assets.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ Build.VERSION.RESOURCES_SDK_INT);
+ loadApkIntoAssetManager(assets, mBaseCodePath, mFlags);
+
+ if (!ArrayUtils.isEmpty(mSplitCodePaths)) {
+ for (String apkPath : mSplitCodePaths) {
+ loadApkIntoAssetManager(assets, apkPath, mFlags);
+ }
+ }
+
+ mCachedAssetManager = assets;
+ assets = null;
+ return mCachedAssetManager;
+ } finally {
+ if (assets != null) {
+ IoUtils.closeQuietly(assets);
+ }
+ }
+ }
+
+ @Override
+ public AssetManager getSplitAssetManager(int splitIdx)
+ throws PackageParser.PackageParserException {
+ return getBaseAssetManager();
+ }
+
+ @Override
+ public void close() throws Exception {
+ if (mCachedAssetManager != null) {
+ IoUtils.closeQuietly(mCachedAssetManager);
+ }
+ }
+}
diff --git a/core/java/android/content/pm/split/SplitAssetDependencyLoader.java b/core/java/android/content/pm/split/SplitAssetDependencyLoader.java
new file mode 100644
index 0000000..3ad45b6
--- /dev/null
+++ b/core/java/android/content/pm/split/SplitAssetDependencyLoader.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.content.pm.split;
+
+import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST;
+import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_NOT_APK;
+
+import android.content.pm.PackageParser;
+import android.content.res.AssetManager;
+import android.os.Build;
+import android.util.SparseIntArray;
+
+import libcore.io.IoUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+/**
+ * Loads AssetManagers for splits and their dependencies. This SplitAssetLoader implementation
+ * is to be used when an application opts-in to isolated split loading.
+ * @hide
+ */
+public class SplitAssetDependencyLoader
+ extends SplitDependencyLoaderHelper<PackageParser.PackageParserException>
+ implements SplitAssetLoader {
+ private static final int BASE_ASSET_PATH_IDX = -1;
+ private final String mBasePath;
+ private final String[] mSplitNames;
+ private final String[] mSplitPaths;
+ private final int mFlags;
+
+ private String[] mCachedBasePaths;
+ private AssetManager mCachedBaseAssetManager;
+
+ private String[][] mCachedSplitPaths;
+ private AssetManager[] mCachedAssetManagers;
+
+ public SplitAssetDependencyLoader(PackageParser.PackageLite pkg, SparseIntArray dependencies,
+ int flags) {
+ super(dependencies);
+ mBasePath = pkg.baseCodePath;
+ mSplitNames = pkg.splitNames;
+ mSplitPaths = pkg.splitCodePaths;
+ mFlags = flags;
+ mCachedBasePaths = null;
+ mCachedBaseAssetManager = null;
+ mCachedSplitPaths = new String[mSplitNames.length][];
+ mCachedAssetManagers = new AssetManager[mSplitNames.length];
+ }
+
+ @Override
+ protected boolean isSplitCached(int splitIdx) {
+ if (splitIdx != -1) {
+ return mCachedAssetManagers[splitIdx] != null;
+ }
+ return mCachedBaseAssetManager != null;
+ }
+
+ // Adds all non-code configuration splits for this split name. The split name is expected
+ // to represent a feature split.
+ private void addAllConfigSplits(String splitName, ArrayList<String> outAssetPaths) {
+ for (int i = 0; i < mSplitNames.length; i++) {
+ if (isConfigurationSplitOf(mSplitNames[i], splitName)) {
+ outAssetPaths.add(mSplitPaths[i]);
+ }
+ }
+ }
+
+ private static AssetManager createAssetManagerWithPaths(String[] assetPaths, int flags)
+ throws PackageParser.PackageParserException {
+ final AssetManager assets = new AssetManager();
+ try {
+ assets.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ Build.VERSION.RESOURCES_SDK_INT);
+
+ for (String assetPath : assetPaths) {
+ if ((flags & PackageParser.PARSE_MUST_BE_APK) != 0 &&
+ !PackageParser.isApkPath(assetPath)) {
+ throw new PackageParser.PackageParserException(INSTALL_PARSE_FAILED_NOT_APK,
+ "Invalid package file: " + assetPath);
+ }
+
+ if (assets.addAssetPath(assetPath) == 0) {
+ throw new PackageParser.PackageParserException(
+ INSTALL_PARSE_FAILED_BAD_MANIFEST,
+ "Failed adding asset path: " + assetPath);
+ }
+ }
+ return assets;
+ } catch (Throwable e) {
+ IoUtils.closeQuietly(assets);
+ throw e;
+ }
+ }
+
+ @Override
+ protected void constructSplit(int splitIdx, int parentSplitIdx) throws
+ PackageParser.PackageParserException {
+ final ArrayList<String> assetPaths = new ArrayList<>();
+ if (splitIdx == BASE_ASSET_PATH_IDX) {
+ assetPaths.add(mBasePath);
+ addAllConfigSplits(null, assetPaths);
+ mCachedBasePaths = assetPaths.toArray(new String[assetPaths.size()]);
+ mCachedBaseAssetManager = createAssetManagerWithPaths(mCachedBasePaths, mFlags);
+ return;
+ }
+
+ if (parentSplitIdx == BASE_ASSET_PATH_IDX) {
+ Collections.addAll(assetPaths, mCachedBasePaths);
+ } else {
+ Collections.addAll(assetPaths, mCachedSplitPaths[parentSplitIdx]);
+ }
+
+ assetPaths.add(mSplitPaths[splitIdx]);
+ addAllConfigSplits(mSplitNames[splitIdx], assetPaths);
+ mCachedSplitPaths[splitIdx] = assetPaths.toArray(new String[assetPaths.size()]);
+ mCachedAssetManagers[splitIdx] = createAssetManagerWithPaths(mCachedSplitPaths[splitIdx],
+ mFlags);
+ }
+
+ @Override
+ public AssetManager getBaseAssetManager() throws PackageParser.PackageParserException {
+ loadDependenciesForSplit(BASE_ASSET_PATH_IDX);
+ return mCachedBaseAssetManager;
+ }
+
+ @Override
+ public AssetManager getSplitAssetManager(int idx) throws PackageParser.PackageParserException {
+ loadDependenciesForSplit(idx);
+ return mCachedAssetManagers[idx];
+ }
+
+ @Override
+ public void close() throws Exception {
+ IoUtils.closeQuietly(mCachedBaseAssetManager);
+ for (AssetManager assets : mCachedAssetManagers) {
+ IoUtils.closeQuietly(assets);
+ }
+ }
+}
diff --git a/core/java/android/content/pm/split/SplitAssetLoader.java b/core/java/android/content/pm/split/SplitAssetLoader.java
new file mode 100644
index 0000000..108fb95
--- /dev/null
+++ b/core/java/android/content/pm/split/SplitAssetLoader.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.content.pm.split;
+
+import android.content.pm.PackageParser;
+import android.content.res.AssetManager;
+
+/**
+ * Simple interface for loading base Assets and Splits. Used by PackageParser when parsing
+ * split APKs.
+ *
+ * @hide
+ */
+public interface SplitAssetLoader extends AutoCloseable {
+ AssetManager getBaseAssetManager() throws PackageParser.PackageParserException;
+ AssetManager getSplitAssetManager(int splitIdx) throws PackageParser.PackageParserException;
+}
diff --git a/core/java/android/content/pm/split/SplitDependencyLoaderHelper.java b/core/java/android/content/pm/split/SplitDependencyLoaderHelper.java
new file mode 100644
index 0000000..b493480
--- /dev/null
+++ b/core/java/android/content/pm/split/SplitDependencyLoaderHelper.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.content.pm.split;
+
+import android.annotation.Nullable;
+import android.util.IntArray;
+import android.util.SparseIntArray;
+
+/**
+ * A helper class that implements the dependency tree traversal for splits. Callbacks
+ * are implemented by subclasses to notify whether a split has already been constructed
+ * and is cached, and to actually create the split requested.
+ *
+ * This helper is meant to be subclassed so as to reduce the number of allocations
+ * needed to make use of it.
+ *
+ * All inputs and outputs are assumed to be indices into an array of splits.
+ *
+ * @hide
+ */
+public abstract class SplitDependencyLoaderHelper<E extends Exception> {
+ @Nullable private final SparseIntArray mDependencies;
+
+ /**
+ * Construct a new SplitDependencyLoaderHelper. Meant to be called from the
+ * subclass constructor.
+ * @param dependencies The dependency tree of splits. Can be null, which leads to
+ * just the implicit dependency of all splits on the base.
+ */
+ protected SplitDependencyLoaderHelper(@Nullable SparseIntArray dependencies) {
+ mDependencies = dependencies;
+ }
+
+ /**
+ * Traverses the dependency tree and constructs any splits that are not already
+ * cached. This routine short-circuits and skips the creation of splits closer to the
+ * root if they are cached, as reported by the subclass implementation of
+ * {@link #isSplitCached(int)}. The construction of splits is delegated to the subclass
+ * implementation of {@link #constructSplit(int, int)}.
+ * @param splitIdx The index of the split to load. Can be -1, which represents the
+ * base Application.
+ */
+ protected void loadDependenciesForSplit(int splitIdx) throws E {
+ // Quick check before any allocations are done.
+ if (isSplitCached(splitIdx)) {
+ return;
+ }
+
+ final IntArray linearDependencies = new IntArray();
+ linearDependencies.add(splitIdx);
+
+ // Collect all the dependencies that need to be constructed.
+ // They will be listed from leaf to root.
+ while (splitIdx >= 0) {
+ splitIdx = mDependencies != null ? mDependencies.get(splitIdx, -1) : -1;
+ if (isSplitCached(splitIdx)) {
+ break;
+ }
+ linearDependencies.add(splitIdx);
+ }
+
+ // Visit each index, from right to left (root to leaf).
+ int parentIdx = splitIdx;
+ for (int i = linearDependencies.size() - 1; i >= 0; i--) {
+ final int idx = linearDependencies.get(i);
+ constructSplit(idx, parentIdx);
+ parentIdx = idx;
+ }
+ }
+
+ /**
+ * Subclass to report whether the split at `splitIdx` is cached and need not be constructed.
+ * It is assumed that if `splitIdx` is cached, any parent of `splitIdx` is also cached.
+ * @param splitIdx The index of the split to check for in the cache.
+ * @return true if the split is cached and does not need to be constructed.
+ */
+ protected abstract boolean isSplitCached(int splitIdx);
+
+ /**
+ * Subclass to construct a split at index `splitIdx` with parent split `parentSplitIdx`.
+ * The result is expected to be cached by the subclass in its own structures.
+ * @param splitIdx The index of the split to construct. Can be -1, which represents the
+ * base Application.
+ * @param parentSplitIdx The index of the parent split. Can be -1, which represents the
+ * base Application.
+ * @throws E
+ */
+ protected abstract void constructSplit(int splitIdx, int parentSplitIdx) throws E;
+
+ /**
+ * Returns true if `splitName` represents a Configuration split of `featureSplitName`.
+ *
+ * A Configuration split's name is prefixed with the associated Feature split's name
+ * or the empty string if the split is for the base Application APK. It is then followed by the
+ * dollar sign character "$" and some unique string that should represent the configurations
+ * the split contains.
+ *
+ * Example:
+ * <table>
+ * <tr>
+ * <th>Feature split name</th>
+ * <th>Configuration split name: xhdpi</th>
+ * <th>Configuration split name: fr-rFR</th>
+ * </tr>
+ * <tr>
+ * <td>(base APK)</td>
+ * <td><code>$xhdpi</code></td>
+ * <td><code>$fr-rFR</code></td>
+ * </tr>
+ * <tr>
+ * <td><code>Extras</code></td>
+ * <td><code>Extras$xhdpi</code></td>
+ * <td><code>Extras$fr-rFR</code></td>
+ * </tr>
+ * </table>
+ *
+ * @param splitName The name of the split to check.
+ * @param featureSplitName The name of the Feature split. May be null or "" if checking
+ * the base Application APK.
+ * @return true if the splitName represents a Configuration split of featureSplitName.
+ */
+ protected static boolean isConfigurationSplitOf(String splitName, String featureSplitName) {
+ if (featureSplitName == null || featureSplitName.length() == 0) {
+ // We are looking for configuration splits of the base, which have some legacy support.
+ if (splitName.startsWith("config_")) {
+ return true;
+ } else if (splitName.startsWith("$")) {
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return splitName.startsWith(featureSplitName + "$");
+ }
+ }
+}
diff --git a/core/java/android/hardware/HardwareBuffer.java b/core/java/android/hardware/HardwareBuffer.java
index fffb1d7..e97bb2f 100644
--- a/core/java/android/hardware/HardwareBuffer.java
+++ b/core/java/android/hardware/HardwareBuffer.java
@@ -40,7 +40,7 @@
public final class HardwareBuffer implements Parcelable {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef({RGBA_8888, RGBA_FP16, RGBX_8888, RGB_888, RGB_565})
+ @IntDef({RGBA_8888, RGBA_FP16, RGBX_8888, RGB_888, RGB_565, BLOB})
public @interface Format {};
/** Format: 8 bits each red, green, blue, alpha */
@@ -52,7 +52,9 @@
/** Format: 5 bits each red and blue, 6 bits green, no alpha */
public static final int RGB_565 = 4;
/** Format: 16 bits each red, green, blue, alpha */
- public static final int RGBA_FP16 = 5;
+ public static final int RGBA_FP16 = 0x16;
+ /** Format: opaque format used for raw data transfer; must have a height of 1 */
+ public static final int BLOB = 0x21;
// Note: do not rename, this field is used by native code
private long mNativeObject;
@@ -135,6 +137,9 @@
if (layers <= 0) {
throw new IllegalArgumentException("Invalid layer count " + layers);
}
+ if (format == BLOB && height != 1) {
+ throw new IllegalArgumentException("Height must be 1 when using the BLOB format");
+ }
long nativeObject = nCreateHardwareBuffer(width, height, format, layers, usage);
if (nativeObject == 0) {
throw new IllegalArgumentException("Unable to create a HardwareBuffer, either the " +
@@ -295,6 +300,7 @@
case RGBX_8888:
case RGB_565:
case RGB_888:
+ case BLOB:
return true;
}
return false;
diff --git a/core/java/android/metrics/LogMaker.java b/core/java/android/metrics/LogMaker.java
index 0aef532..2bf841c 100644
--- a/core/java/android/metrics/LogMaker.java
+++ b/core/java/android/metrics/LogMaker.java
@@ -106,7 +106,7 @@
* @return
*/
public LogMaker addTaggedData(int tag, Object value) {
- if (isValidValue(value)) {
+ if (!isValidValue(value)) {
throw new IllegalArgumentException(
"Value must be loggable type - int, long, float, String");
}
@@ -119,10 +119,14 @@
}
public boolean isValidValue(Object value) {
- return !(value instanceof Integer ||
+ if (value == null) {
+ Log.i("LogBuilder", "Logging a null value.");
+ return true;
+ }
+ return value instanceof Integer ||
value instanceof String ||
value instanceof Long ||
- value instanceof Float);
+ value instanceof Float;
}
public Object getTaggedData(int tag) {
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index d6d5cb6..1db685a 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -26,6 +26,7 @@
import android.util.SizeF;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
+import android.util.SparseIntArray;
import libcore.util.SneakyThrow;
@@ -891,6 +892,21 @@
}
}
+ public final void writeSparseIntArray(SparseIntArray val) {
+ if (val == null) {
+ writeInt(-1);
+ return;
+ }
+ int N = val.size();
+ writeInt(N);
+ int i=0;
+ while (i < N) {
+ writeInt(val.keyAt(i));
+ writeInt(val.valueAt(i));
+ i++;
+ }
+ }
+
public final void writeBooleanArray(boolean[] val) {
if (val != null) {
int N = val.length;
@@ -2154,6 +2170,20 @@
}
/**
+ * Read and return a new SparseIntArray object from the parcel at the current
+ * dataPosition(). Returns null if the previously written array object was null.
+ */
+ public final SparseIntArray readSparseIntArray() {
+ int N = readInt();
+ if (N < 0) {
+ return null;
+ }
+ SparseIntArray sa = new SparseIntArray(N);
+ readSparseIntArrayInternal(sa, N);
+ return sa;
+ }
+
+ /**
* Read and return a new ArrayList containing a particular object type from
* the parcel that was written with {@link #writeTypedList} at the
* current dataPosition(). Returns null if the
@@ -2922,6 +2952,15 @@
}
}
+ private void readSparseIntArrayInternal(SparseIntArray outVal, int N) {
+ while (N > 0) {
+ int key = readInt();
+ int value = readInt();
+ outVal.append(key, value);
+ N--;
+ }
+ }
+
/**
* @hide For testing
*/
diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java
index 517b305..417be60 100644
--- a/core/java/android/service/notification/NotificationListenerService.java
+++ b/core/java/android/service/notification/NotificationListenerService.java
@@ -183,8 +183,8 @@
public static final int REASON_CHANNEL_BANNED = 17;
/** Notification was snoozed. */
public static final int REASON_SNOOZED = 18;
- /** Notification no longer visible because of user switch */
- public static final int REASON_USER_SWITCH = 19;
+ /** Notification was canceled due to timeout */
+ public static final int REASON_TIMEOUT = 19;
/**
* The full trim of the StatusBarNotification including all its features.
diff --git a/core/jni/android_hardware_HardwareBuffer.cpp b/core/jni/android_hardware_HardwareBuffer.cpp
index 74527d9..fadf8a4 100644
--- a/core/jni/android_hardware_HardwareBuffer.cpp
+++ b/core/jni/android_hardware_HardwareBuffer.cpp
@@ -223,16 +223,18 @@
uint32_t android_hardware_HardwareBuffer_convertFromPixelFormat(uint32_t format) {
switch (format) {
- case PIXEL_FORMAT_RGBA_8888:
+ case HAL_PIXEL_FORMAT_RGBA_8888:
return AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM;
- case PIXEL_FORMAT_RGBX_8888:
+ case HAL_PIXEL_FORMAT_RGBX_8888:
return AHARDWAREBUFFER_FORMAT_R8G8B8X8_UNORM;
- case PIXEL_FORMAT_RGB_565:
+ case HAL_PIXEL_FORMAT_RGB_565:
return AHARDWAREBUFFER_FORMAT_R5G6B5_UNORM;
- case PIXEL_FORMAT_RGB_888:
+ case HAL_PIXEL_FORMAT_RGB_888:
return AHARDWAREBUFFER_FORMAT_R8G8B8_UNORM;
- case PIXEL_FORMAT_RGBA_FP16:
+ case HAL_PIXEL_FORMAT_RGBA_FP16:
return AHARDWAREBUFFER_FORMAT_R16G16B16A16_SFLOAT;
+ case HAL_PIXEL_FORMAT_BLOB:
+ return AHARDWAREBUFFER_FORMAT_BLOB;
default:
ALOGE("Unknown pixel format %u", format);
return 0;
@@ -242,15 +244,17 @@
uint32_t android_hardware_HardwareBuffer_convertToPixelFormat(uint32_t format) {
switch (format) {
case AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM:
- return PIXEL_FORMAT_RGBA_8888;
+ return HAL_PIXEL_FORMAT_RGBA_8888;
case AHARDWAREBUFFER_FORMAT_R8G8B8X8_UNORM:
- return PIXEL_FORMAT_RGBX_8888;
+ return HAL_PIXEL_FORMAT_RGBX_8888;
case AHARDWAREBUFFER_FORMAT_R5G6B5_UNORM:
- return PIXEL_FORMAT_RGB_565;
+ return HAL_PIXEL_FORMAT_RGB_565;
case AHARDWAREBUFFER_FORMAT_R8G8B8_UNORM:
- return PIXEL_FORMAT_RGB_888;
+ return HAL_PIXEL_FORMAT_RGB_888;
case AHARDWAREBUFFER_FORMAT_R16G16B16A16_SFLOAT:
- return PIXEL_FORMAT_RGBA_FP16;
+ return HAL_PIXEL_FORMAT_RGBA_FP16;
+ case AHARDWAREBUFFER_FORMAT_BLOB:
+ return HAL_PIXEL_FORMAT_BLOB;
default:
ALOGE("Unknown AHardwareBuffer format %u", format);
return 0;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index d1227eb..094e2b8 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -443,6 +443,7 @@
<protected-broadcast android:name="com.android.server.telecom.intent.action.CALLS_ADD_ENTRY" />
<protected-broadcast android:name="com.android.settings.location.MODE_CHANGING" />
+ <protected-broadcast android:name="NotificationManagerService.TIMEOUT" />
<protected-broadcast android:name="ScheduleConditionProvider.EVALUATE" />
<protected-broadcast android:name="EventConditionProvider.EVALUATE" />
<protected-broadcast android:name="SnoozeHelper.EVALUATE" />
diff --git a/core/res/res/drawable-nodpi/platlogo.xml b/core/res/res/drawable-nodpi/platlogo.xml
index 516f252..182ba24 100644
--- a/core/res/res/drawable-nodpi/platlogo.xml
+++ b/core/res/res/drawable-nodpi/platlogo.xml
@@ -1,5 +1,5 @@
<!--
-Copyright (C) 2016 The Android Open Source Project
+Copyright (C) 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,24 +14,27 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="512dp"
- android:height="512dp"
+ android:width="480dp"
+ android:height="480dp"
android:viewportWidth="48.0"
android:viewportHeight="48.0">
<path
- android:fillColor="#FFc7d4b6"
- android:pathData="M32.0,12.5l0.0,28.0l12.0,-5.0l0.0,-28.0z"/>
+ android:pathData="M25.0,25.0m-20.5,0.0a20.5,20.5,0,1,1,41.0,0.0a20.5,20.5,0,1,1,-41.0,0.0"
+ android:fillAlpha="0.066"
+ android:fillColor="#000000"/>
<path
- android:fillColor="#FFfbd3cb"
- android:pathData="M4.0,40.5l12.0,-5.0l0.0,-11.0l-12.0,-12.0z"/>
+ android:pathData="M24.0,24.0m-20.0,0.0a20.0,20.0,0,1,1,40.0,0.0a20.0,20.0,0,1,1,-40.0,0.0"
+ android:fillColor="#FFC107"/>
<path
- android:fillColor="#40000000"
- android:pathData="M44.0,35.5l-12.0,-12.0l0.0,-4.0z"/>
+ android:pathData="M44,24.2010101 L33.9004889,14.101499 L14.101499,33.9004889 L24.2010101,44 C29.2525804,43.9497929 34.2887564,41.9975027 38.1431296,38.1431296 C41.9975027,34.2887564 43.9497929,29.2525804 44,24.2010101 Z"
+ android:fillColor="#FE9F00"/>
<path
- android:fillColor="#40000000"
- android:pathData="M4.0,12.5l12.0,12.0l0.0,4.0z"/>
+ android:pathData="M24.0,24.0m-14.0,0.0a14.0,14.0,0,1,1,28.0,0.0a14.0,14.0,0,1,1,-28.0,0.0"
+ android:fillColor="#FED44F"/>
<path
- android:fillColor="#FFe0e0d6"
- android:pathData="M32.0,23.5l-16.0,-16.0l-12.0,5.0l0.0,0.0l12.0,12.0l16.0,16.0l12.0,-5.0l0.0,0.0z"/>
+ android:pathData="M37.7829445,26.469236 L29.6578482,18.3441397 L18.3441397,29.6578482 L26.469236,37.7829445 C29.1911841,37.2979273 31.7972024,36.0037754 33.9004889,33.9004889 C36.0037754,31.7972024 37.2979273,29.1911841 37.7829445,26.469236 Z"
+ android:fillColor="#FFC107"/>
+ <path
+ android:pathData="M24.0,24.0m-8.0,0.0a8.0,8.0,0,1,1,16.0,0.0a8.0,8.0,0,1,1,-16.0,0.0"
+ android:fillColor="#FFFFFF"/>
</vector>
-
diff --git a/core/res/res/drawable-nodpi/stat_sys_adb.xml b/core/res/res/drawable-nodpi/stat_sys_adb.xml
index 5043cba..89e42e6 100644
--- a/core/res/res/drawable-nodpi/stat_sys_adb.xml
+++ b/core/res/res/drawable-nodpi/stat_sys_adb.xml
@@ -1,5 +1,5 @@
<!--
-Copyright (C) 2016 The Android Open Source Project
+Copyright (C) 2017 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.
@@ -16,21 +16,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
- android:viewportWidth="48.0"
- android:viewportHeight="48.0">
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
<path
- android:fillColor="#A0FFFFFF"
- android:pathData="M32.0,12.5l0.0,28.0l12.0,-5.0l0.0,-28.0z"/>
+ android:fillColor="#FF000000"
+ android:pathData="M12.0,12.0m-10.0,0.0a10.0,10.0,0,1,1,20.0,0.0a10.0,10.0,0,1,1,-20.0,0.0"
+ android:fillAlpha="0.25"/>
<path
- android:fillColor="#A0FFFFFF"
- android:pathData="M4.0,40.5l12.0,-5.0l0.0,-11.0l-12.0,-12.0z"/>
+ android:fillColor="#FF000000"
+ android:pathData="M12,22 C6.4771525,22 2,17.5228475 2,12 C2,6.4771525 6.4771525,2 12,2 C17.5228475,2 22,6.4771525 22,12 C22,17.5228475 17.5228475,22 12,22 Z M12,18.5 C15.5898509,18.5 18.5,15.5898509 18.5,12 C18.5,8.41014913 15.5898509,5.5 12,5.5 C8.41014913,5.5 5.5,8.41014913 5.5,12 C5.5,15.5898509 8.41014913,18.5 12,18.5 Z"/>
<path
- android:fillColor="#40000000"
- android:pathData="M44.0,35.5l-12.0,-12.0l0.0,-4.0z"/>
- <path
- android:fillColor="#40000000"
- android:pathData="M4.0,12.5l12.0,12.0l0.0,4.0z"/>
- <path
- android:fillColor="#FFFFFFFF"
- android:pathData="M32.0,23.5l-16.0,-16.0l-12.0,5.0l0.0,0.0l12.0,12.0l16.0,16.0l12.0,-5.0l0.0,0.0z"/>
+ android:fillColor="#FF000000"
+ android:pathData="M12,18.5 C8.41014913,18.5 5.5,15.5898509 5.5,12 C5.5,8.41014913 8.41014913,5.5 12,5.5 C15.5898509,5.5 18.5,8.41014913 18.5,12 C18.5,15.5898509 15.5898509,18.5 12,18.5 Z M12,15 C13.6568542,15 15,13.6568542 15,12 C15,10.3431458 13.6568542,9 12,9 C10.3431458,9 9,10.3431458 9,12 C9,13.6568542 10.3431458,15 12,15 Z"
+ android:fillAlpha="0.25"/>
</vector>
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index 113ace3..0dde91b 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -1001,6 +1001,13 @@
<enum name="preferExternal" value="2" />
</attr>
+ <!-- If set to <code>true</code>, indicates to the platform that any split APKs
+ installed for this application should be loaded into their own Context
+ objects and not appear in the base application's Context.
+
+ <p>The default value of this attribute is <code>false</code>. -->
+ <attr name="isolatedSplits" format="boolean" />
+
<!-- Extra options for an activity's UI. Applies to either the {@code <activity>} or
{@code <application>} tag. If specified on the {@code <application>}
tag these will be considered defaults for all activities in the
@@ -1266,6 +1273,7 @@
<attr name="sharedUserId" />
<attr name="sharedUserLabel" />
<attr name="installLocation" />
+ <attr name="isolatedSplits" />
</declare-styleable>
<!-- The <code>application</code> tag describes application-level components
@@ -2462,4 +2470,8 @@
<attr name="hash" format="string" />
</declare-styleable>
+ <declare-styleable name="AndroidManifestUsesSplit" parent="AndroidManifest">
+ <attr name="name" format="string" />
+ </declare-styleable>
+
</resources>
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index e387650..1146871 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -2789,6 +2789,7 @@
<public name="certDigest" />
<public name="splitName" />
<public name="colorMode" />
+ <public name="isolatedSplits" />
</public-group>
<public-group type="style" first-id="0x010302e0">
diff --git a/core/tests/coretests/src/android/metrics/LogMakerTest.java b/core/tests/coretests/src/android/metrics/LogMakerTest.java
index 0f75cb6..35d8d93 100644
--- a/core/tests/coretests/src/android/metrics/LogMakerTest.java
+++ b/core/tests/coretests/src/android/metrics/LogMakerTest.java
@@ -100,6 +100,7 @@
builder.addTaggedData(2, 123);
builder.addTaggedData(3, 123L);
builder.addTaggedData(4, 123.0F);
+ builder.addTaggedData(5, null);
Object[] out = builder.serialize();
assertEquals("onetwothree", out[1]);
assertEquals(123, out[3]);
diff --git a/libs/hwui/SkiaCanvas.cpp b/libs/hwui/SkiaCanvas.cpp
index 89e2a01..e54bc36 100644
--- a/libs/hwui/SkiaCanvas.cpp
+++ b/libs/hwui/SkiaCanvas.cpp
@@ -367,7 +367,7 @@
// (see https://code.google.com/p/skia/issues/detail?id=1303)
bool SkiaCanvas::getClipBounds(SkRect* outRect) const {
SkIRect ibounds;
- if (!mCanvas->getClipDeviceBounds(&ibounds)) {
+ if (!mCanvas->getDeviceClipBounds(&ibounds)) {
return false;
}
diff --git a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp
index 6ca8d8b..ea302a1 100644
--- a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp
+++ b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp
@@ -59,8 +59,7 @@
SkImageInfo canvasInfo = canvas->imageInfo();
SkMatrix44 mat4(canvas->getTotalMatrix());
- SkIRect ibounds;
- canvas->getClipDeviceBounds(&ibounds);
+ SkIRect ibounds = canvas->getDeviceClipBounds();
DrawGlInfo info;
info.clipLeft = ibounds.fLeft;
diff --git a/libs/hwui/pipeline/skia/GLFunctorDrawable.h b/libs/hwui/pipeline/skia/GLFunctorDrawable.h
index bf39dad..012c948 100644
--- a/libs/hwui/pipeline/skia/GLFunctorDrawable.h
+++ b/libs/hwui/pipeline/skia/GLFunctorDrawable.h
@@ -37,9 +37,9 @@
public:
GLFunctorDrawable(Functor* functor, GlFunctorLifecycleListener* listener, SkCanvas* canvas)
: mFunctor(functor)
- , mListener(listener) {
- canvas->getClipBounds(&mBounds);
- }
+ , mListener(listener)
+ , mBounds(canvas->getLocalClipBounds())
+ {}
virtual ~GLFunctorDrawable();
void syncFunctor() const;
@@ -51,7 +51,7 @@
private:
Functor* mFunctor;
sp<GlFunctorLifecycleListener> mListener;
- SkRect mBounds;
+ const SkRect mBounds;
};
}; // namespace skiapipeline
diff --git a/libs/hwui/tests/common/TestUtils.cpp b/libs/hwui/tests/common/TestUtils.cpp
index 79daa3f..0916d72 100644
--- a/libs/hwui/tests/common/TestUtils.cpp
+++ b/libs/hwui/tests/common/TestUtils.cpp
@@ -193,9 +193,7 @@
}
SkRect TestUtils::getClipBounds(const SkCanvas* canvas) {
- SkIRect bounds;
- (void)canvas->getClipDeviceBounds(&bounds);
- return SkRect::Make(bounds);
+ return SkRect::Make(canvas->getDeviceClipBounds());
}
SkRect TestUtils::getLocalClipBounds(const SkCanvas* canvas) {
diff --git a/native/android/hardware_buffer.cpp b/native/android/hardware_buffer.cpp
index 6a10cb5..2f75c10 100644
--- a/native/android/hardware_buffer.cpp
+++ b/native/android/hardware_buffer.cpp
@@ -89,6 +89,12 @@
return BAD_VALUE;
}
+ if (desc->format == AHARDWAREBUFFER_FORMAT_BLOB && desc->height != 1) {
+ ALOGE("Height must be 1 when using the AHARDWAREBUFFER_FORMAT_BLOB "
+ "format");
+ return BAD_VALUE;
+ }
+
status_t err;
uint32_t usage = android_hardware_HardwareBuffer_convertToGrallocUsageBits(
desc->usage0, desc->usage1);
diff --git a/packages/SystemUI/res/drawable-nodpi/icon.xml b/packages/SystemUI/res/drawable-nodpi/icon.xml
index 5e08fcb..abafb68 100644
--- a/packages/SystemUI/res/drawable-nodpi/icon.xml
+++ b/packages/SystemUI/res/drawable-nodpi/icon.xml
@@ -1,5 +1,5 @@
<!--
-Copyright (C) 2016 The Android Open Source Project
+Copyright (C) 2017 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.
@@ -19,20 +19,22 @@
android:viewportWidth="48.0"
android:viewportHeight="48.0">
<path
- android:fillColor="#00796B"
- android:pathData="M32.0,12.5l0.0,28.0l12.0,-5.0l0.0,-28.0z"/>
+ android:pathData="M25.0,25.0m-20.5,0.0a20.5,20.5,0,1,1,41.0,0.0a20.5,20.5,0,1,1,-41.0,0.0"
+ android:fillAlpha="0.066"
+ android:fillColor="#000000"/>
<path
- android:fillColor="#00796B"
- android:pathData="M4.0,40.5l12.0,-5.0l0.0,-11.0l-12.0,-12.0z"/>
+ android:pathData="M24.0,24.0m-20.0,0.0a20.0,20.0,0,1,1,40.0,0.0a20.0,20.0,0,1,1,-40.0,0.0"
+ android:fillColor="#FFC107"/>
<path
- android:fillColor="#40000000"
- android:pathData="M44.0,35.5l-12.0,-12.0l0.0,-4.0z"/>
+ android:pathData="M44,24.2010101 L33.9004889,14.101499 L14.101499,33.9004889 L24.2010101,44 C29.2525804,43.9497929 34.2887564,41.9975027 38.1431296,38.1431296 C41.9975027,34.2887564 43.9497929,29.2525804 44,24.2010101 Z"
+ android:fillColor="#FE9F00"/>
<path
- android:fillColor="#40000000"
- android:pathData="M4.0,12.5l12.0,12.0l0.0,4.0z"/>
+ android:pathData="M24.0,24.0m-14.0,0.0a14.0,14.0,0,1,1,28.0,0.0a14.0,14.0,0,1,1,-28.0,0.0"
+ android:fillColor="#FED44F"/>
<path
- android:fillColor="#4DB6AC"
- android:pathData="M32.0,23.5l-16.0,-16.0l-12.0,5.0l0.0,0.0l12.0,12.0l16.0,16.0l12.0,-5.0l0.0,0.0z"/>
+ android:pathData="M37.7829445,26.469236 L29.6578482,18.3441397 L18.3441397,29.6578482 L26.469236,37.7829445 C29.1911841,37.2979273 31.7972024,36.0037754 33.9004889,33.9004889 C36.0037754,31.7972024 37.2979273,29.1911841 37.7829445,26.469236 Z"
+ android:fillColor="#FFC107"/>
+ <path
+ android:pathData="M24.0,24.0m-8.0,0.0a8.0,8.0,0,1,1,16.0,0.0a8.0,8.0,0,1,1,-16.0,0.0"
+ android:fillColor="#FFFFFF"/>
</vector>
-
-
diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
index 71dda2d..2fe9e77 100644
--- a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
+++ b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
@@ -217,8 +217,16 @@
return;
}
mTempWarning = false;
- mNoMan.cancelAsUser(TAG_TEMPERATURE, SystemMessage.NOTE_HIGH_TEMP,
- UserHandle.ALL);
+ dismissTemperatureWarningInternal();
+ }
+
+ /**
+ * Internal only version of {@link #dismissTemperatureWarning()} that simply dismisses
+ * the notification. As such, the notification will not show again until
+ * {@link #dismissTemperatureWarning()} is called.
+ */
+ private void dismissTemperatureWarningInternal() {
+ mNoMan.cancelAsUser(TAG_TEMPERATURE, SystemMessage.NOTE_HIGH_TEMP, UserHandle.ALL);
}
@Override
@@ -390,10 +398,10 @@
} else if (action.equals(ACTION_DISMISSED_WARNING)) {
dismissLowBatteryWarning();
} else if (ACTION_CLICKED_TEMP_WARNING.equals(action)) {
- dismissTemperatureWarning();
+ dismissTemperatureWarningInternal();
showTemperatureDialog();
} else if (ACTION_DISMISSED_TEMP_WARNING.equals(action)) {
- dismissTemperatureWarning();
+ dismissTemperatureWarningInternal();
}
}
}
diff --git a/proto/src/metrics_constants.proto b/proto/src/metrics_constants.proto
index 341438d..036b6c2 100644
--- a/proto/src/metrics_constants.proto
+++ b/proto/src/metrics_constants.proto
@@ -3331,6 +3331,11 @@
// ACTION: Deny "Enable picture-in-picture on hide" for an app
APP_PICTURE_IN_PICTURE_ON_HIDE_DENY = 814;
+ // OPEN: Settings > Language & input > Text-to-speech output -> Speech rate & pitch
+ // CATEGORY: SETTINGS
+ // OS: 8.0
+ TTS_SLIDERS = 815;
+
// ---- End O Constants, all O constants go above this line ----
// Add new aosp constants above this line.
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 8b53b97..45bdb9c 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -32,6 +32,7 @@
import static android.service.notification.NotificationListenerService.REASON_PACKAGE_SUSPENDED;
import static android.service.notification.NotificationListenerService.REASON_PROFILE_TURNED_OFF;
import static android.service.notification.NotificationListenerService.REASON_SNOOZED;
+import static android.service.notification.NotificationListenerService.REASON_TIMEOUT;
import static android.service.notification.NotificationListenerService.REASON_UNAUTOBUNDLED;
import static android.service.notification.NotificationListenerService.REASON_USER_STOPPED;
import static android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_EFFECTS;
@@ -50,6 +51,7 @@
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
+import android.app.AlarmManager;
import android.app.AppGlobals;
import android.app.AppOpsManager;
import android.app.AutomaticZenRule;
@@ -165,6 +167,7 @@
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.ArrayList;
+import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -229,6 +232,12 @@
private static final long DELAY_FOR_ASSISTANT_TIME = 100;
+ private static final String ACTION_NOTIFICATION_TIMEOUT =
+ NotificationManagerService.class.getSimpleName() + ".TIMEOUT";
+ private static final int REQUEST_CODE_TIMEOUT = 1;
+ private static final String SCHEME_TIMEOUT = "timeout";
+ private static final String EXTRA_KEY = "key";
+
private IActivityManager mAm;
private IPackageManager mPackageManager;
private PackageManager mPackageManagerClient;
@@ -237,6 +246,7 @@
@Nullable StatusBarManagerInternal mStatusBar;
Vibrator mVibrator;
private WindowManagerInternal mWindowManagerInternal;
+ private AlarmManager mAlarmManager;
final IBinder mForegroundToken = new Binder();
private Handler mHandler;
@@ -682,6 +692,29 @@
updateLightsLocked();
}
+ private final BroadcastReceiver mNotificationTimeoutReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (action == null) {
+ return;
+ }
+ if (ACTION_NOTIFICATION_TIMEOUT.equals(action)) {
+ final NotificationRecord record;
+ synchronized (mNotificationLock) {
+ record = findNotificationByKeyLocked(intent.getStringExtra(EXTRA_KEY));
+ }
+ if (record != null) {
+ cancelNotification(record.sbn.getUid(), record.sbn.getInitialPid(),
+ record.sbn.getPackageName(), record.sbn.getTag(),
+ record.sbn.getId(), 0,
+ Notification.FLAG_FOREGROUND_SERVICE, true, record.getUserId(),
+ REASON_TIMEOUT, null);
+ }
+ }
+ }
+ };
+
private final BroadcastReceiver mPackageIntentReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@@ -966,6 +999,7 @@
mAppOps = (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE);
mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE);
mAppUsageStats = LocalServices.getService(UsageStatsManagerInternal.class);
+ mAlarmManager = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE);
mHandler = new WorkerHandler(looper);
mRankingThread.start();
@@ -1132,6 +1166,10 @@
getContext().registerReceiverAsUser(mPackageIntentReceiver, UserHandle.ALL, sdFilter, null,
null);
+ IntentFilter timeoutFilter = new IntentFilter(ACTION_NOTIFICATION_TIMEOUT);
+ timeoutFilter.addDataScheme(SCHEME_TIMEOUT);
+ getContext().registerReceiver(mNotificationTimeoutReceiver, timeoutFilter);
+
mSettingsObserver = new SettingsObserver(mHandler);
mArchive = new Archive(resources.getInteger(
@@ -3011,6 +3049,7 @@
public void run() {
synchronized (mNotificationLock) {
mEnqueuedNotifications.add(r);
+ scheduleTimeoutLocked(r);
if (mSnoozeHelper.isSnoozed(userId, r.sbn.getPackageName(), r.getKey())) {
// TODO: log to event log
@@ -3241,6 +3280,22 @@
}
@VisibleForTesting
+ void scheduleTimeoutLocked(NotificationRecord record) {
+ if (record.getNotification().getTimeout() > System.currentTimeMillis()) {
+ final PendingIntent pi = PendingIntent.getBroadcast(getContext(),
+ REQUEST_CODE_TIMEOUT,
+ new Intent(ACTION_NOTIFICATION_TIMEOUT)
+ .setData(new Uri.Builder().scheme(SCHEME_TIMEOUT)
+ .appendPath(record.getKey()).build())
+ .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
+ .putExtra(EXTRA_KEY, record.getKey()),
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ mAlarmManager.setExactAndAllowWhileIdle(
+ AlarmManager.RTC_WAKEUP, record.getNotification().getTimeout(), pi);
+ }
+ }
+
+ @VisibleForTesting
void buzzBeepBlinkLocked(NotificationRecord record) {
boolean buzz = false;
boolean beep = false;
diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java
index 2a5a25f..8998128 100644
--- a/services/core/java/com/android/server/notification/NotificationRecord.java
+++ b/services/core/java/com/android/server/notification/NotificationRecord.java
@@ -41,6 +41,7 @@
import android.text.TextUtils;
import android.util.Log;
import android.util.Slog;
+import android.util.TimeUtils;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.EventLogTags;
@@ -318,6 +319,7 @@
pw.println(prefix + " vibrate=" + Arrays.toString(notification.vibrate));
pw.println(prefix + String.format(" led=0x%08x onMs=%d offMs=%d",
notification.ledARGB, notification.ledOnMS, notification.ledOffMS));
+ pw.println(prefix + " timeout=" + TimeUtils.formatForLogging(notification.getTimeout()));
if (notification.actions != null && notification.actions.length > 0) {
pw.println(prefix + " actions={");
final int N = notification.actions.length;
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index d1aed3e..067a136 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -894,7 +894,7 @@
// This is kind of hacky; we're creating a half-parsed package that is
// straddled between the inherited and staged APKs.
- final PackageLite pkg = new PackageLite(null, baseApk, null,
+ final PackageLite pkg = new PackageLite(null, baseApk, null, null,
splitPaths.toArray(new String[splitPaths.size()]), null);
final boolean isForwardLocked =
(params.installFlags & PackageManager.INSTALL_FORWARD_LOCK) != 0;
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 63a5d14..9899cd4 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -10101,8 +10101,10 @@
a.info.packageName = pkg.applicationInfo.packageName;
a.info.sourceDir = pkg.applicationInfo.sourceDir;
a.info.publicSourceDir = pkg.applicationInfo.publicSourceDir;
+ a.info.splitNames = pkg.splitNames;
a.info.splitSourceDirs = pkg.applicationInfo.splitSourceDirs;
a.info.splitPublicSourceDirs = pkg.applicationInfo.splitPublicSourceDirs;
+ a.info.splitDependencies = pkg.applicationInfo.splitDependencies;
a.info.dataDir = pkg.applicationInfo.dataDir;
a.info.deviceProtectedDataDir = pkg.applicationInfo.deviceProtectedDataDir;
a.info.credentialProtectedDataDir = pkg.applicationInfo.credentialProtectedDataDir;
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index 49b96b0..2f8d749 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -162,7 +162,7 @@
if (file.isFile()) {
try {
ApkLite baseApk = PackageParser.parseApkLite(file, 0);
- PackageLite pkgLite = new PackageLite(null, baseApk, null, null, null);
+ PackageLite pkgLite = new PackageLite(null, baseApk, null, null, null, null);
params.sessionParams.setSize(PackageHelper.calculateInstalledSize(
pkgLite, false, params.sessionParams.abiOverride));
} catch (PackageParserException | IOException e) {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 3082e8b..7558e3c 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -1630,6 +1630,11 @@
mContext.getSystemService(PowerManager.class).reboot(reason);
}
+ void recoverySystemRebootWipeUserData(boolean shutdown, String reason, boolean force)
+ throws IOException {
+ RecoverySystem.rebootWipeUserData(mContext, shutdown, reason, force);
+ }
+
boolean systemPropertiesGetBoolean(String key, boolean def) {
return SystemProperties.getBoolean(key, def);
}
@@ -5148,7 +5153,7 @@
}
}
- private void wipeDataNoLock(boolean wipeExtRequested, String reason, boolean force) {
+ private void forceWipeDeviceNoLock(boolean wipeExtRequested, String reason) {
wtfIfInLock();
if (wipeExtRequested) {
@@ -5157,94 +5162,87 @@
sm.wipeAdoptableDisks();
}
try {
- RecoverySystem.rebootWipeUserData(mContext, false /* shutdown */, reason, force);
+ mInjector.recoverySystemRebootWipeUserData(
+ /*shutdown=*/ false, reason, /*force=*/ true);
} catch (IOException | SecurityException e) {
Slog.w(LOG_TAG, "Failed requesting data wipe", e);
}
}
+ private void forceWipeUser(int userId) {
+ try {
+ IActivityManager am = mInjector.getIActivityManager();
+ if (am.getCurrentUser().id == userId) {
+ am.switchUser(UserHandle.USER_SYSTEM);
+ }
+
+ boolean userRemoved = mUserManagerInternal.removeUserEvenWhenDisallowed(userId);
+ if (!userRemoved) {
+ Slog.w(LOG_TAG, "Couldn't remove user " + userId);
+ } else if (isManagedProfile(userId)) {
+ sendWipeProfileNotification();
+ }
+ } catch (RemoteException re) {
+ // Shouldn't happen
+ }
+ }
+
@Override
public void wipeData(int flags) {
if (!mHasFeature) {
return;
}
- final int userHandle = mInjector.userHandleGetCallingUserId();
- enforceFullCrossUsersPermission(userHandle);
+ enforceFullCrossUsersPermission(mInjector.userHandleGetCallingUserId());
- final String source;
+ final ActiveAdmin admin;
synchronized (this) {
- // This API can only be called by an active device admin,
- // so try to retrieve it to check that the caller is one.
- final ActiveAdmin admin = getActiveAdminForCallerLocked(null,
- DeviceAdminInfo.USES_POLICY_WIPE_DATA);
- source = admin.info.getComponent().flattenToShortString();
-
- long ident = mInjector.binderClearCallingIdentity();
- try {
- final String restriction;
- if (userHandle == UserHandle.USER_SYSTEM) {
- restriction = UserManager.DISALLOW_FACTORY_RESET;
- } else if (isManagedProfile(userHandle)) {
- restriction = UserManager.DISALLOW_REMOVE_MANAGED_PROFILE;
- } else {
- restriction = UserManager.DISALLOW_REMOVE_USER;
- }
- if (isAdminAffectedByRestriction(
- admin.info.getComponent(), restriction, userHandle)) {
- throw new SecurityException("Cannot wipe data. " + restriction
- + " restriction is set for user " + userHandle);
- }
-
- if ((flags & WIPE_RESET_PROTECTION_DATA) != 0) {
- if (!isDeviceOwner(admin.info.getComponent(), userHandle)) {
- throw new SecurityException(
- "Only device owner admins can set WIPE_RESET_PROTECTION_DATA");
- }
- PersistentDataBlockManager manager = (PersistentDataBlockManager)
- mContext.getSystemService(Context.PERSISTENT_DATA_BLOCK_SERVICE);
- if (manager != null) {
- manager.wipe();
- }
- }
-
- } finally {
- mInjector.binderRestoreCallingIdentity(ident);
- }
+ admin = getActiveAdminForCallerLocked(null, DeviceAdminInfo.USES_POLICY_WIPE_DATA);
}
- final boolean wipeExtRequested = (flags & WIPE_EXTERNAL_STORAGE) != 0;
- wipeDeviceNoLock(wipeExtRequested, userHandle,
- "DevicePolicyManager.wipeData() from " + source, /*force=*/ true);
+ String reason = "DevicePolicyManager.wipeData() from "
+ + admin.info.getComponent().flattenToShortString();
+ wipeDataNoLock(
+ admin.info.getComponent(), flags, reason, admin.getUserHandle().getIdentifier());
}
- private void wipeDeviceNoLock(
- boolean wipeExtRequested, final int userHandle, String reason, boolean force) {
+ private void wipeDataNoLock(ComponentName admin, int flags, String reason, int userId) {
wtfIfInLock();
long ident = mInjector.binderClearCallingIdentity();
try {
- // TODO If split user is enabled and the device owner is set in the primary user (rather
- // than system), we should probably trigger factory reset. Current code just remove
- // that user (but still clears FRP...)
- if (userHandle == UserHandle.USER_SYSTEM) {
- wipeDataNoLock(wipeExtRequested, reason, force);
+ // First check whether the admin is allowed to wipe the device/user/profile.
+ final String restriction;
+ if (userId == UserHandle.USER_SYSTEM) {
+ restriction = UserManager.DISALLOW_FACTORY_RESET;
+ } else if (isManagedProfile(userId)) {
+ restriction = UserManager.DISALLOW_REMOVE_MANAGED_PROFILE;
} else {
- try {
- IActivityManager am = mInjector.getIActivityManager();
- if (am.getCurrentUser().id == userHandle) {
- am.switchUser(UserHandle.USER_SYSTEM);
- }
+ restriction = UserManager.DISALLOW_REMOVE_USER;
+ }
+ if (isAdminAffectedByRestriction(admin, restriction, userId)) {
+ throw new SecurityException("Cannot wipe data. " + restriction
+ + " restriction is set for user " + userId);
+ }
- boolean userRemoved = force
- ? mUserManagerInternal.removeUserEvenWhenDisallowed(userHandle)
- : mUserManager.removeUser(userHandle);
- if (!userRemoved) {
- Slog.w(LOG_TAG, "Couldn't remove user " + userHandle);
- } else if (isManagedProfile(userHandle)) {
- sendWipeProfileNotification();
- }
- } catch (RemoteException re) {
- // Shouldn't happen
+ if ((flags & WIPE_RESET_PROTECTION_DATA) != 0) {
+ if (!isDeviceOwner(admin, userId)) {
+ throw new SecurityException(
+ "Only device owner admins can set WIPE_RESET_PROTECTION_DATA");
}
+ PersistentDataBlockManager manager = (PersistentDataBlockManager)
+ mContext.getSystemService(Context.PERSISTENT_DATA_BLOCK_SERVICE);
+ if (manager != null) {
+ manager.wipe();
+ }
+ }
+
+ // TODO If split user is enabled and the device owner is set in the primary user
+ // (rather than system), we should probably trigger factory reset. Current code just
+ // removes that user (but still clears FRP...)
+ if (userId == UserHandle.USER_SYSTEM) {
+ forceWipeDeviceNoLock(/*wipeExtRequested=*/ (flags & WIPE_EXTERNAL_STORAGE) != 0,
+ reason);
+ } else {
+ forceWipeUser(userId);
}
} finally {
mInjector.binderRestoreCallingIdentity(ident);
@@ -5384,25 +5382,21 @@
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.BIND_DEVICE_ADMIN, null);
+ boolean wipeData = false;
+ ActiveAdmin strictestAdmin = null;
final long ident = mInjector.binderClearCallingIdentity();
try {
- boolean wipeData = false;
- int identifier = 0;
synchronized (this) {
DevicePolicyData policy = getUserData(userHandle);
policy.mFailedPasswordAttempts++;
saveSettingsLocked(userHandle);
if (mHasFeature) {
- ActiveAdmin strictestAdmin = getAdminWithMinimumFailedPasswordsForWipeLocked(
+ strictestAdmin = getAdminWithMinimumFailedPasswordsForWipeLocked(
userHandle, /* parent */ false);
int max = strictestAdmin != null
? strictestAdmin.maximumFailedPasswordsForWipe : 0;
if (max > 0 && policy.mFailedPasswordAttempts >= max) {
- // Wipe the user/profile associated with the policy that was violated. This
- // is not necessarily calling user: if the policy that fired was from a
- // managed profile rather than the main user profile, we wipe former only.
wipeData = true;
- identifier = strictestAdmin.getUserHandle().getIdentifier();
}
sendAdminCommandForLockscreenPoliciesLocked(
@@ -5410,14 +5404,33 @@
DeviceAdminInfo.USES_POLICY_WATCH_LOGIN, userHandle);
}
}
- if (wipeData) {
- // Call without holding lock.
- wipeDeviceNoLock(false, identifier, "reportFailedPasswordAttempt()", false);
- }
} finally {
mInjector.binderRestoreCallingIdentity(ident);
}
+ if (wipeData && strictestAdmin != null) {
+ final int userId = strictestAdmin.getUserHandle().getIdentifier();
+ Slog.i(LOG_TAG, "Max failed password attempts policy reached for admin: "
+ + strictestAdmin.info.getComponent().flattenToShortString()
+ + ". Calling wipeData for user " + userId);
+
+ // Attempt to wipe the device/user/profile associated with the admin, as if the
+ // admin had called wipeData(). That way we can check whether the admin is actually
+ // allowed to wipe the device (e.g. a regular device admin shouldn't be able to wipe the
+ // device if the device owner has set DISALLOW_FACTORY_RESET, but the DO should be
+ // able to do so).
+ // IMPORTANT: Call without holding the lock to prevent deadlock.
+ try {
+ wipeDataNoLock(strictestAdmin.info.getComponent(),
+ /*flags=*/ 0,
+ /*reason=*/ "reportFailedPasswordAttempt()",
+ userId);
+ } catch (SecurityException e) {
+ Slog.w(LOG_TAG, "Failed to wipe user " + userId
+ + " after max failed password attempts reached.", e);
+ }
+ }
+
if (mInjector.securityLogIsLoggingEnabled()) {
SecurityLog.writeEvent(SecurityLog.TAG_KEYGUARD_DISMISS_AUTH_ATTEMPT, /*result*/ 0,
/*method strength*/ 1);
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
index 4927f0c..3b92a34 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
@@ -37,6 +37,7 @@
import com.android.internal.widget.LockPatternUtils;
import java.io.File;
+import java.io.IOException;
import java.util.Map;
/**
@@ -264,6 +265,12 @@
}
@Override
+ void recoverySystemRebootWipeUserData(boolean shutdown, String reason, boolean force)
+ throws IOException {
+ context.recoverySystem.rebootWipeUserData(shutdown, reason, force);
+ }
+
+ @Override
boolean systemPropertiesGetBoolean(String key, boolean def) {
return context.systemProperties.getBoolean(key, def);
}
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index 3ceab6f..5d4c3cf 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -85,6 +85,7 @@
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
/**
@@ -3344,6 +3345,140 @@
}
}
+ public void testWipeDataDeviceOwner() throws Exception {
+ setDeviceOwner();
+ when(mContext.userManager.getUserRestrictionSource(
+ UserManager.DISALLOW_FACTORY_RESET,
+ UserHandle.SYSTEM))
+ .thenReturn(UserManager.RESTRICTION_SOURCE_DEVICE_OWNER);
+
+ dpm.wipeData(0);
+ verify(mContext.recoverySystem).rebootWipeUserData(
+ /*shutdown=*/ eq(false), anyString(), /*force=*/ eq(true));
+ }
+
+ public void testWipeDataDeviceOwnerDisallowed() throws Exception {
+ setDeviceOwner();
+ when(mContext.userManager.getUserRestrictionSource(
+ UserManager.DISALLOW_FACTORY_RESET,
+ UserHandle.SYSTEM))
+ .thenReturn(UserManager.RESTRICTION_SOURCE_SYSTEM);
+ try {
+ // The DO is not allowed to wipe the device if the user restriction was set
+ // by the system
+ dpm.wipeData(0);
+ fail("SecurityException not thrown");
+ } catch (SecurityException expected) {
+ }
+ }
+
+ public void testMaximumFailedPasswordAttemptsReachedManagedProfile() throws Exception {
+ final int MANAGED_PROFILE_USER_ID = 15;
+ final int MANAGED_PROFILE_ADMIN_UID = UserHandle.getUid(MANAGED_PROFILE_USER_ID, 19436);
+ addManagedProfile(admin1, MANAGED_PROFILE_ADMIN_UID, admin1);
+
+ // Even if the caller is the managed profile, the current user is the user 0
+ when(mContext.iactivityManager.getCurrentUser())
+ .thenReturn(new UserInfo(UserHandle.USER_SYSTEM, "user system", 0));
+
+ when(mContext.userManager.getUserRestrictionSource(
+ UserManager.DISALLOW_REMOVE_MANAGED_PROFILE,
+ UserHandle.of(MANAGED_PROFILE_USER_ID)))
+ .thenReturn(UserManager.RESTRICTION_SOURCE_PROFILE_OWNER);
+
+ mContext.binder.callingUid = MANAGED_PROFILE_ADMIN_UID;
+ dpm.setMaximumFailedPasswordsForWipe(admin1, 3);
+
+ mContext.binder.callingUid = DpmMockContext.SYSTEM_UID;
+ mContext.callerPermissions.add(permission.BIND_DEVICE_ADMIN);
+ // Failed password attempts on the parent user are taken into account, as there isn't a
+ // separate work challenge.
+ dpm.reportFailedPasswordAttempt(UserHandle.USER_SYSTEM);
+ dpm.reportFailedPasswordAttempt(UserHandle.USER_SYSTEM);
+ dpm.reportFailedPasswordAttempt(UserHandle.USER_SYSTEM);
+
+ // The profile should be wiped even if DISALLOW_REMOVE_MANAGED_PROFILE is enabled, because
+ // both the user restriction and the policy were set by the PO.
+ verify(mContext.userManagerInternal).removeUserEvenWhenDisallowed(
+ MANAGED_PROFILE_USER_ID);
+ verifyZeroInteractions(mContext.recoverySystem);
+ }
+
+ public void testMaximumFailedPasswordAttemptsReachedManagedProfileDisallowed()
+ throws Exception {
+ final int MANAGED_PROFILE_USER_ID = 15;
+ final int MANAGED_PROFILE_ADMIN_UID = UserHandle.getUid(MANAGED_PROFILE_USER_ID, 19436);
+ addManagedProfile(admin1, MANAGED_PROFILE_ADMIN_UID, admin1);
+
+ // Even if the caller is the managed profile, the current user is the user 0
+ when(mContext.iactivityManager.getCurrentUser())
+ .thenReturn(new UserInfo(UserHandle.USER_SYSTEM, "user system", 0));
+
+ when(mContext.userManager.getUserRestrictionSource(
+ UserManager.DISALLOW_REMOVE_MANAGED_PROFILE,
+ UserHandle.of(MANAGED_PROFILE_USER_ID)))
+ .thenReturn(UserManager.RESTRICTION_SOURCE_SYSTEM);
+
+ mContext.binder.callingUid = MANAGED_PROFILE_ADMIN_UID;
+ dpm.setMaximumFailedPasswordsForWipe(admin1, 3);
+
+ mContext.binder.callingUid = DpmMockContext.SYSTEM_UID;
+ mContext.callerPermissions.add(permission.BIND_DEVICE_ADMIN);
+ // Failed password attempts on the parent user are taken into account, as there isn't a
+ // separate work challenge.
+ dpm.reportFailedPasswordAttempt(UserHandle.USER_SYSTEM);
+ dpm.reportFailedPasswordAttempt(UserHandle.USER_SYSTEM);
+ dpm.reportFailedPasswordAttempt(UserHandle.USER_SYSTEM);
+
+ // DISALLOW_REMOVE_MANAGED_PROFILE was set by the system, not the PO, so the profile is
+ // not wiped.
+ verify(mContext.userManagerInternal, never())
+ .removeUserEvenWhenDisallowed(anyInt());
+ verifyZeroInteractions(mContext.recoverySystem);
+ }
+
+ public void testMaximumFailedPasswordAttemptsReachedDeviceOwner() throws Exception {
+ setDeviceOwner();
+ when(mContext.userManager.getUserRestrictionSource(
+ UserManager.DISALLOW_FACTORY_RESET,
+ UserHandle.SYSTEM))
+ .thenReturn(UserManager.RESTRICTION_SOURCE_DEVICE_OWNER);
+
+ dpm.setMaximumFailedPasswordsForWipe(admin1, 3);
+
+ mContext.binder.callingUid = DpmMockContext.SYSTEM_UID;
+ mContext.callerPermissions.add(permission.BIND_DEVICE_ADMIN);
+ dpm.reportFailedPasswordAttempt(UserHandle.USER_SYSTEM);
+ dpm.reportFailedPasswordAttempt(UserHandle.USER_SYSTEM);
+ dpm.reportFailedPasswordAttempt(UserHandle.USER_SYSTEM);
+
+ // The device should be wiped even if DISALLOW_FACTORY_RESET is enabled, because both the
+ // user restriction and the policy were set by the DO.
+ verify(mContext.recoverySystem).rebootWipeUserData(
+ /*shutdown=*/ eq(false), anyString(), /*force=*/ eq(true));
+ }
+
+ public void testMaximumFailedPasswordAttemptsReachedDeviceOwnerDisallowed() throws Exception {
+ setDeviceOwner();
+ when(mContext.userManager.getUserRestrictionSource(
+ UserManager.DISALLOW_FACTORY_RESET,
+ UserHandle.SYSTEM))
+ .thenReturn(UserManager.RESTRICTION_SOURCE_SYSTEM);
+
+ dpm.setMaximumFailedPasswordsForWipe(admin1, 3);
+
+ mContext.binder.callingUid = DpmMockContext.SYSTEM_UID;
+ mContext.callerPermissions.add(permission.BIND_DEVICE_ADMIN);
+ dpm.reportFailedPasswordAttempt(UserHandle.USER_SYSTEM);
+ dpm.reportFailedPasswordAttempt(UserHandle.USER_SYSTEM);
+ dpm.reportFailedPasswordAttempt(UserHandle.USER_SYSTEM);
+
+ // DISALLOW_FACTORY_RESET was set by the system, not the DO, so the device is not wiped.
+ verifyZeroInteractions(mContext.recoverySystem);
+ verify(mContext.userManagerInternal, never())
+ .removeUserEvenWhenDisallowed(anyInt());
+ }
+
public void testGetPermissionGrantState() throws Exception {
final String permission = "some.permission";
final String app1 = "com.example.app1";
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
index 5c4a658..22cd135 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
@@ -55,6 +55,7 @@
import org.mockito.stubbing.Answer;
import java.io.File;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@@ -158,6 +159,12 @@
}
}
+ public static class RecoverySystemForMock {
+ public void rebootWipeUserData(
+ boolean shutdown, String reason, boolean force) throws IOException {
+ }
+ }
+
public static class SystemPropertiesForMock {
public boolean getBoolean(String key, boolean def) {
return false;
@@ -267,6 +274,7 @@
public final UserManagerForMock userManagerForMock;
public final PowerManagerForMock powerManager;
public final PowerManagerInternal powerManagerInternal;
+ public final RecoverySystemForMock recoverySystem;
public final NotificationManager notificationManager;
public final IIpConnectivityMetrics iipConnectivityMetrics;
public final IWindowManager iwindowManager;
@@ -312,6 +320,7 @@
packageManagerInternal = mock(PackageManagerInternal.class);
powerManager = mock(PowerManagerForMock.class);
powerManagerInternal = mock(PowerManagerInternal.class);
+ recoverySystem = mock(RecoverySystemForMock.class);
notificationManager = mock(NotificationManager.class);
iipConnectivityMetrics = mock(IIpConnectivityMetrics.class);
iwindowManager = mock(IWindowManager.class);
diff --git a/test-runner/src/android/test/mock/MockContext.java b/test-runner/src/android/test/mock/MockContext.java
index b6e701e..9ffd92d 100644
--- a/test-runner/src/android/test/mock/MockContext.java
+++ b/test-runner/src/android/test/mock/MockContext.java
@@ -692,6 +692,13 @@
return null;
}
+ /** @hide */
+ @Override
+ public Context createContextForSplit(String splitName)
+ throws PackageManager.NameNotFoundException {
+ throw new UnsupportedOperationException();
+ }
+
/** {@hide} */
@Override
public Context createPackageContextAsUser(String packageName, int flags, UserHandle user)
diff --git a/tests/StatusBar/src/com/android/statusbartest/NotificationTestList.java b/tests/StatusBar/src/com/android/statusbartest/NotificationTestList.java
index da27ea9..56aad23 100644
--- a/tests/StatusBar/src/com/android/statusbartest/NotificationTestList.java
+++ b/tests/StatusBar/src/com/android/statusbartest/NotificationTestList.java
@@ -424,6 +424,17 @@
mNM.notify("secret", 7012, n);
}
},
+ new Test("1 minute timeout") {
+ public void run()
+ {
+ Notification n = new Notification.Builder(NotificationTestList.this)
+ .setSmallIcon(R.drawable.icon2)
+ .setContentTitle("timeout in a minute")
+ .setTimeout(System.currentTimeMillis() + (1000 * 60))
+ .build();
+ mNM.notify("timeout_min", 7013, n);
+ }
+ },
new Test("Off") {
public void run() {
PowerManager pm = (PowerManager)NotificationTestList.this.getSystemService(Context.POWER_SERVICE);
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java
index 68680d5..dff4f69 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java
@@ -1325,6 +1325,12 @@
}
@Override
+ public Context createContextForSplit(String splitName) {
+ // pass
+ return null;
+ }
+
+ @Override
public String[] databaseList() {
// pass
return null;