Merge "Apply minimized offset when fetching new aspect ratio bounds."
diff --git a/api/current.txt b/api/current.txt
index bcc3fb3..5ecef52 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -9925,6 +9925,15 @@
method public final int compare(android.content.pm.ApplicationInfo, android.content.pm.ApplicationInfo);
}
+ public final class ChangedPackages implements android.os.Parcelable {
+ ctor public ChangedPackages(int, java.util.List<java.lang.String>);
+ method public int describeContents();
+ method public java.util.List<java.lang.String> getPackageNames();
+ method public int getSequenceNumber();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.content.pm.ChangedPackages> CREATOR;
+ }
+
public class ComponentInfo extends android.content.pm.PackageItemInfo {
ctor public ComponentInfo();
ctor public ComponentInfo(android.content.pm.ComponentInfo);
@@ -10271,6 +10280,7 @@
method public abstract java.lang.CharSequence getApplicationLabel(android.content.pm.ApplicationInfo);
method public abstract android.graphics.drawable.Drawable getApplicationLogo(android.content.pm.ApplicationInfo);
method public abstract android.graphics.drawable.Drawable getApplicationLogo(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException;
+ method public abstract android.content.pm.ChangedPackages getChangedPackages(int);
method public abstract int getComponentEnabledSetting(android.content.ComponentName);
method public abstract android.graphics.drawable.Drawable getDefaultActivityIcon();
method public abstract android.graphics.drawable.Drawable getDrawable(java.lang.String, int, android.content.pm.ApplicationInfo);
@@ -20715,7 +20725,7 @@
method public int getStreamMaxVolume(int);
method public int getStreamVolume(int);
method public deprecated int getVibrateSetting(int);
- method public boolean isBluetoothA2dpOn();
+ method public deprecated boolean isBluetoothA2dpOn();
method public boolean isBluetoothScoAvailableOffCall();
method public boolean isBluetoothScoOn();
method public boolean isMicrophoneMute();
@@ -22321,6 +22331,7 @@
method public void setAuxEffectSendLevel(float);
method public void setBufferingParams(android.media.BufferingParams);
method public void setDataSource(android.content.Context, android.net.Uri) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
+ method public void setDataSource(android.content.Context, android.net.Uri, java.util.Map<java.lang.String, java.lang.String>, java.util.List<java.net.HttpCookie>) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
method public void setDataSource(android.content.Context, android.net.Uri, java.util.Map<java.lang.String, java.lang.String>) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
method public void setDataSource(java.lang.String) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
method public void setDataSource(android.content.res.AssetFileDescriptor) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException;
@@ -24086,6 +24097,7 @@
field public static final java.lang.String COLUMN_AUTHOR = "author";
field public static final java.lang.String COLUMN_AVAILABILITY = "availability";
field public static final java.lang.String COLUMN_BROADCAST_GENRE = "broadcast_genre";
+ field public static final java.lang.String COLUMN_BROWSABLE = "browsable";
field public static final java.lang.String COLUMN_DURATION_MILLIS = "duration_millis";
field public static final java.lang.String COLUMN_END_TIME_UTC_MILLIS = "end_time_utc_millis";
field public static final deprecated java.lang.String COLUMN_EPISODE_NUMBER = "episode_number";
@@ -24228,11 +24240,13 @@
field public static final java.lang.String ACTION_BLOCKED_RATINGS_CHANGED = "android.media.tv.action.BLOCKED_RATINGS_CHANGED";
field public static final java.lang.String ACTION_MAKE_CHANNEL_BROWSABLE = "android.media.tv.action.MAKE_CHANNEL_BROWSABLE";
field public static final java.lang.String ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED = "android.media.tv.action.PARENTAL_CONTROLS_ENABLED_CHANGED";
+ field public static final java.lang.String ACTION_PROGRAM_BROWSABLE_DISABLED = "android.media.tv.action.PROGRAM_BROWSABLE_DISABLED";
field public static final java.lang.String ACTION_QUERY_CONTENT_RATING_SYSTEMS = "android.media.tv.action.QUERY_CONTENT_RATING_SYSTEMS";
field public static final java.lang.String ACTION_SETUP_INPUTS = "android.media.tv.action.SETUP_INPUTS";
field public static final java.lang.String ACTION_VIEW_RECORDING_SCHEDULES = "android.media.tv.action.VIEW_RECORDING_SCHEDULES";
field public static final java.lang.String EXTRA_CHANNEL_ID = "android.media.tv.extra.CHANNEL_ID";
field public static final java.lang.String EXTRA_PACKAGE_NAME = "android.media.tv.extra.PACKAGE_NAME";
+ field public static final java.lang.String EXTRA_PROGRAM_ID = "android.media.tv.extra.PROGRAM_ID";
field public static final int INPUT_STATE_CONNECTED = 0; // 0x0
field public static final int INPUT_STATE_CONNECTED_STANDBY = 1; // 0x1
field public static final int INPUT_STATE_DISCONNECTED = 2; // 0x2
@@ -40065,6 +40079,7 @@
method public java.lang.CharSequence getApplicationLabel(android.content.pm.ApplicationInfo);
method public android.graphics.drawable.Drawable getApplicationLogo(android.content.pm.ApplicationInfo);
method public android.graphics.drawable.Drawable getApplicationLogo(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException;
+ method public android.content.pm.ChangedPackages getChangedPackages(int);
method public int getComponentEnabledSetting(android.content.ComponentName);
method public android.graphics.drawable.Drawable getDefaultActivityIcon();
method public android.graphics.drawable.Drawable getDrawable(java.lang.String, int, android.content.pm.ApplicationInfo);
@@ -47318,7 +47333,7 @@
public final class TextClassificationManager {
method public java.util.List<android.view.textclassifier.TextLanguage> detectLanguages(java.lang.CharSequence);
- method public android.view.textclassifier.TextClassifier getDefaultTextClassifier();
+ method public synchronized android.view.textclassifier.TextClassifier getDefaultTextClassifier();
}
public final class TextClassificationResult {
diff --git a/api/system-current.txt b/api/system-current.txt
index 5ac97c1..e89c25c 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -10373,6 +10373,15 @@
method public final int compare(android.content.pm.ApplicationInfo, android.content.pm.ApplicationInfo);
}
+ public final class ChangedPackages implements android.os.Parcelable {
+ ctor public ChangedPackages(int, java.util.List<java.lang.String>);
+ method public int describeContents();
+ method public java.util.List<java.lang.String> getPackageNames();
+ method public int getSequenceNumber();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.content.pm.ChangedPackages> CREATOR;
+ }
+
public class ComponentInfo extends android.content.pm.PackageItemInfo {
ctor public ComponentInfo();
ctor public ComponentInfo(android.content.pm.ComponentInfo);
@@ -10770,6 +10779,7 @@
method public abstract java.lang.CharSequence getApplicationLabel(android.content.pm.ApplicationInfo);
method public abstract android.graphics.drawable.Drawable getApplicationLogo(android.content.pm.ApplicationInfo);
method public abstract android.graphics.drawable.Drawable getApplicationLogo(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException;
+ method public abstract android.content.pm.ChangedPackages getChangedPackages(int);
method public abstract int getComponentEnabledSetting(android.content.ComponentName);
method public abstract android.graphics.drawable.Drawable getDefaultActivityIcon();
method public abstract java.lang.String getDefaultBrowserPackageNameAsUser(int);
@@ -22329,7 +22339,7 @@
method public int getStreamMaxVolume(int);
method public int getStreamVolume(int);
method public deprecated int getVibrateSetting(int);
- method public boolean isBluetoothA2dpOn();
+ method public deprecated boolean isBluetoothA2dpOn();
method public boolean isBluetoothScoAvailableOffCall();
method public boolean isBluetoothScoOn();
method public boolean isHdmiSystemAudioSupported();
@@ -23964,6 +23974,7 @@
method public void setAuxEffectSendLevel(float);
method public void setBufferingParams(android.media.BufferingParams);
method public void setDataSource(android.content.Context, android.net.Uri) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
+ method public void setDataSource(android.content.Context, android.net.Uri, java.util.Map<java.lang.String, java.lang.String>, java.util.List<java.net.HttpCookie>) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
method public void setDataSource(android.content.Context, android.net.Uri, java.util.Map<java.lang.String, java.lang.String>) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
method public void setDataSource(java.lang.String) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
method public void setDataSource(android.content.res.AssetFileDescriptor) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException;
@@ -25875,6 +25886,7 @@
field public static final java.lang.String COLUMN_AUTHOR = "author";
field public static final java.lang.String COLUMN_AVAILABILITY = "availability";
field public static final java.lang.String COLUMN_BROADCAST_GENRE = "broadcast_genre";
+ field public static final java.lang.String COLUMN_BROWSABLE = "browsable";
field public static final java.lang.String COLUMN_DURATION_MILLIS = "duration_millis";
field public static final java.lang.String COLUMN_END_TIME_UTC_MILLIS = "end_time_utc_millis";
field public static final deprecated java.lang.String COLUMN_EPISODE_NUMBER = "episode_number";
@@ -26098,11 +26110,13 @@
field public static final java.lang.String ACTION_BLOCKED_RATINGS_CHANGED = "android.media.tv.action.BLOCKED_RATINGS_CHANGED";
field public static final java.lang.String ACTION_MAKE_CHANNEL_BROWSABLE = "android.media.tv.action.MAKE_CHANNEL_BROWSABLE";
field public static final java.lang.String ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED = "android.media.tv.action.PARENTAL_CONTROLS_ENABLED_CHANGED";
+ field public static final java.lang.String ACTION_PROGRAM_BROWSABLE_DISABLED = "android.media.tv.action.PROGRAM_BROWSABLE_DISABLED";
field public static final java.lang.String ACTION_QUERY_CONTENT_RATING_SYSTEMS = "android.media.tv.action.QUERY_CONTENT_RATING_SYSTEMS";
field public static final java.lang.String ACTION_SETUP_INPUTS = "android.media.tv.action.SETUP_INPUTS";
field public static final java.lang.String ACTION_VIEW_RECORDING_SCHEDULES = "android.media.tv.action.VIEW_RECORDING_SCHEDULES";
field public static final java.lang.String EXTRA_CHANNEL_ID = "android.media.tv.extra.CHANNEL_ID";
field public static final java.lang.String EXTRA_PACKAGE_NAME = "android.media.tv.extra.PACKAGE_NAME";
+ field public static final java.lang.String EXTRA_PROGRAM_ID = "android.media.tv.extra.PROGRAM_ID";
field public static final int INPUT_STATE_CONNECTED = 0; // 0x0
field public static final int INPUT_STATE_CONNECTED_STANDBY = 1; // 0x1
field public static final int INPUT_STATE_DISCONNECTED = 2; // 0x2
@@ -43490,6 +43504,7 @@
method public java.lang.CharSequence getApplicationLabel(android.content.pm.ApplicationInfo);
method public android.graphics.drawable.Drawable getApplicationLogo(android.content.pm.ApplicationInfo);
method public android.graphics.drawable.Drawable getApplicationLogo(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException;
+ method public android.content.pm.ChangedPackages getChangedPackages(int);
method public int getComponentEnabledSetting(android.content.ComponentName);
method public android.graphics.drawable.Drawable getDefaultActivityIcon();
method public java.lang.String getDefaultBrowserPackageNameAsUser(int);
@@ -50759,7 +50774,7 @@
public final class TextClassificationManager {
method public java.util.List<android.view.textclassifier.TextLanguage> detectLanguages(java.lang.CharSequence);
- method public android.view.textclassifier.TextClassifier getDefaultTextClassifier();
+ method public synchronized android.view.textclassifier.TextClassifier getDefaultTextClassifier();
}
public final class TextClassificationResult {
diff --git a/api/test-current.txt b/api/test-current.txt
index a5265b0..b38e2b2 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -9953,6 +9953,15 @@
method public final int compare(android.content.pm.ApplicationInfo, android.content.pm.ApplicationInfo);
}
+ public final class ChangedPackages implements android.os.Parcelable {
+ ctor public ChangedPackages(int, java.util.List<java.lang.String>);
+ method public int describeContents();
+ method public java.util.List<java.lang.String> getPackageNames();
+ method public int getSequenceNumber();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.content.pm.ChangedPackages> CREATOR;
+ }
+
public class ComponentInfo extends android.content.pm.PackageItemInfo {
ctor public ComponentInfo();
ctor public ComponentInfo(android.content.pm.ComponentInfo);
@@ -10300,6 +10309,7 @@
method public abstract java.lang.CharSequence getApplicationLabel(android.content.pm.ApplicationInfo);
method public abstract android.graphics.drawable.Drawable getApplicationLogo(android.content.pm.ApplicationInfo);
method public abstract android.graphics.drawable.Drawable getApplicationLogo(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException;
+ method public abstract android.content.pm.ChangedPackages getChangedPackages(int);
method public abstract int getComponentEnabledSetting(android.content.ComponentName);
method public abstract android.graphics.drawable.Drawable getDefaultActivityIcon();
method public abstract java.lang.String getDefaultBrowserPackageNameAsUser(int);
@@ -20808,7 +20818,7 @@
method public int getStreamMaxVolume(int);
method public int getStreamVolume(int);
method public deprecated int getVibrateSetting(int);
- method public boolean isBluetoothA2dpOn();
+ method public deprecated boolean isBluetoothA2dpOn();
method public boolean isBluetoothScoAvailableOffCall();
method public boolean isBluetoothScoOn();
method public boolean isMicrophoneMute();
@@ -22414,6 +22424,7 @@
method public void setAuxEffectSendLevel(float);
method public void setBufferingParams(android.media.BufferingParams);
method public void setDataSource(android.content.Context, android.net.Uri) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
+ method public void setDataSource(android.content.Context, android.net.Uri, java.util.Map<java.lang.String, java.lang.String>, java.util.List<java.net.HttpCookie>) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
method public void setDataSource(android.content.Context, android.net.Uri, java.util.Map<java.lang.String, java.lang.String>) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
method public void setDataSource(java.lang.String) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
method public void setDataSource(android.content.res.AssetFileDescriptor) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException;
@@ -24179,6 +24190,7 @@
field public static final java.lang.String COLUMN_AUTHOR = "author";
field public static final java.lang.String COLUMN_AVAILABILITY = "availability";
field public static final java.lang.String COLUMN_BROADCAST_GENRE = "broadcast_genre";
+ field public static final java.lang.String COLUMN_BROWSABLE = "browsable";
field public static final java.lang.String COLUMN_DURATION_MILLIS = "duration_millis";
field public static final java.lang.String COLUMN_END_TIME_UTC_MILLIS = "end_time_utc_millis";
field public static final deprecated java.lang.String COLUMN_EPISODE_NUMBER = "episode_number";
@@ -24321,11 +24333,13 @@
field public static final java.lang.String ACTION_BLOCKED_RATINGS_CHANGED = "android.media.tv.action.BLOCKED_RATINGS_CHANGED";
field public static final java.lang.String ACTION_MAKE_CHANNEL_BROWSABLE = "android.media.tv.action.MAKE_CHANNEL_BROWSABLE";
field public static final java.lang.String ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED = "android.media.tv.action.PARENTAL_CONTROLS_ENABLED_CHANGED";
+ field public static final java.lang.String ACTION_PROGRAM_BROWSABLE_DISABLED = "android.media.tv.action.PROGRAM_BROWSABLE_DISABLED";
field public static final java.lang.String ACTION_QUERY_CONTENT_RATING_SYSTEMS = "android.media.tv.action.QUERY_CONTENT_RATING_SYSTEMS";
field public static final java.lang.String ACTION_SETUP_INPUTS = "android.media.tv.action.SETUP_INPUTS";
field public static final java.lang.String ACTION_VIEW_RECORDING_SCHEDULES = "android.media.tv.action.VIEW_RECORDING_SCHEDULES";
field public static final java.lang.String EXTRA_CHANNEL_ID = "android.media.tv.extra.CHANNEL_ID";
field public static final java.lang.String EXTRA_PACKAGE_NAME = "android.media.tv.extra.PACKAGE_NAME";
+ field public static final java.lang.String EXTRA_PROGRAM_ID = "android.media.tv.extra.PROGRAM_ID";
field public static final int INPUT_STATE_CONNECTED = 0; // 0x0
field public static final int INPUT_STATE_CONNECTED_STANDBY = 1; // 0x1
field public static final int INPUT_STATE_DISCONNECTED = 2; // 0x2
@@ -40202,6 +40216,7 @@
method public java.lang.CharSequence getApplicationLabel(android.content.pm.ApplicationInfo);
method public android.graphics.drawable.Drawable getApplicationLogo(android.content.pm.ApplicationInfo);
method public android.graphics.drawable.Drawable getApplicationLogo(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException;
+ method public android.content.pm.ChangedPackages getChangedPackages(int);
method public int getComponentEnabledSetting(android.content.ComponentName);
method public android.graphics.drawable.Drawable getDefaultActivityIcon();
method public java.lang.String getDefaultBrowserPackageNameAsUser(int);
@@ -47632,7 +47647,7 @@
public final class TextClassificationManager {
method public java.util.List<android.view.textclassifier.TextLanguage> detectLanguages(java.lang.CharSequence);
- method public android.view.textclassifier.TextClassifier getDefaultTextClassifier();
+ method public synchronized android.view.textclassifier.TextClassifier getDefaultTextClassifier();
}
public final class TextClassificationResult {
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 376823e..1f8e6db 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -5726,7 +5726,7 @@
// Preload fonts resources
try {
final ApplicationInfo info =
- sPackageManager.getApplicationInfo(
+ getPackageManager().getApplicationInfo(
data.appInfo.packageName,
PackageManager.GET_META_DATA /*flags*/,
UserHandle.myUserId());
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index 0c6c4ba..333e412 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -29,6 +29,7 @@
import android.content.IntentSender;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
+import android.content.pm.ChangedPackages;
import android.content.pm.ComponentInfo;
import android.content.pm.InstantAppInfo;
import android.content.pm.FeatureInfo;
@@ -506,6 +507,15 @@
}
@Override
+ public ChangedPackages getChangedPackages(int sequenceNumber) {
+ try {
+ return mPM.getChangedPackages(sequenceNumber, mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
@SuppressWarnings("unchecked")
public FeatureInfo[] getSystemAvailableFeatures() {
try {
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 45a46b3..812daf8 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -3684,7 +3684,8 @@
mContext, backgroundColor);
mSecondaryTextColor = NotificationColorUtil.resolveSecondaryColor(
mContext, backgroundColor);
- mActionBarColor = NotificationColorUtil.resolveActionBarColor(backgroundColor);
+ mActionBarColor = NotificationColorUtil.resolveActionBarColor(mContext,
+ backgroundColor);
}
}
@@ -4166,12 +4167,21 @@
mN.extras.putCharSequence(EXTRA_SUB_TEXT, newSummary);
}
}
+ Boolean colorized = (Boolean) mN.extras.get(EXTRA_COLORIZED);
+ mN.extras.putBoolean(EXTRA_COLORIZED, false);
+
RemoteViews header = makeNotificationHeader();
+
if (summary != null) {
mN.extras.putCharSequence(EXTRA_SUB_TEXT, summary);
} else {
mN.extras.remove(EXTRA_SUB_TEXT);
}
+ if (colorized != null) {
+ mN.extras.putBoolean(EXTRA_COLORIZED, colorized);
+ } else {
+ mN.extras.remove(EXTRA_COLORIZED);
+ }
mN.color = color;
return header;
}
diff --git a/core/java/android/app/QueuedWork.java b/core/java/android/app/QueuedWork.java
index 6ee4780..a38fd43 100644
--- a/core/java/android/app/QueuedWork.java
+++ b/core/java/android/app/QueuedWork.java
@@ -16,86 +16,249 @@
package android.app;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.LinkedList;
/**
- * Internal utility class to keep track of process-global work that's
- * outstanding and hasn't been finished yet.
+ * Internal utility class to keep track of process-global work that's outstanding and hasn't been
+ * finished yet.
*
- * This was created for writing SharedPreference edits out
- * asynchronously so we'd have a mechanism to wait for the writes in
- * Activity.onPause and similar places, but we may use this mechanism
- * for other things in the future.
+ * New work will be {@link #queue queued}.
+ *
+ * It is possible to add 'finisher'-runnables that are {@link #waitToFinish guaranteed to be run}.
+ * This is used to make sure the work has been finished.
+ *
+ * This was created for writing SharedPreference edits out asynchronously so we'd have a mechanism
+ * to wait for the writes in Activity.onPause and similar places, but we may use this mechanism for
+ * other things in the future.
+ *
+ * The queued asynchronous work is performed on a separate, dedicated thread.
*
* @hide
*/
public class QueuedWork {
+ private static final String LOG_TAG = QueuedWork.class.getSimpleName();
+ private static final boolean DEBUG = true;
- // The set of Runnables that will finish or wait on any async
- // activities started by the application.
- private static final ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers =
- new ConcurrentLinkedQueue<Runnable>();
+ /** Delay for delayed runnables, as big as possible but low enough to be barely perceivable */
+ private static final long DELAY = 100;
- private static ExecutorService sSingleThreadExecutor = null; // lazy, guarded by class
+ /** Lock for this class */
+ private static final Object sLock = new Object();
/**
- * Returns a single-thread Executor shared by the entire process,
- * creating it if necessary.
+ * Used to make sure that only one thread is processing work items at a time. This means that
+ * they are processed in the order added.
+ *
+ * This is separate from {@link #sLock} as this is held the whole time while work is processed
+ * and we do not want to stall the whole class.
*/
- public static ExecutorService singleThreadExecutor() {
- synchronized (QueuedWork.class) {
- if (sSingleThreadExecutor == null) {
- // TODO: can we give this single thread a thread name?
- sSingleThreadExecutor = Executors.newSingleThreadExecutor();
+ private static Object sProcessingWork = new Object();
+
+ /** Finishers {@link #addFinisher added} and not yet {@link #removeFinisher removed} */
+ @GuardedBy("sLock")
+ private static final LinkedList<Runnable> sFinishers = new LinkedList<>();
+
+ /** {@link #getHandler() Lazily} created handler */
+ @GuardedBy("sLock")
+ private static Handler sHandler = null;
+
+ /** Work queued via {@link #queue} */
+ @GuardedBy("sLock")
+ private static final LinkedList<Runnable> sWork = new LinkedList<>();
+
+ /** If new work can be delayed or not */
+ @GuardedBy("sLock")
+ private static boolean sCanDelay = true;
+
+ /**
+ * Lazily create a handler on a separate thread.
+ *
+ * @return the handler
+ */
+ private static Handler getHandler() {
+ synchronized (sLock) {
+ if (sHandler == null) {
+ HandlerThread handlerThread = new HandlerThread("queued-work-looper",
+ Process.THREAD_PRIORITY_FOREGROUND);
+ handlerThread.start();
+
+ sHandler = new QueuedWorkHandler(handlerThread.getLooper());
}
- return sSingleThreadExecutor;
+ return sHandler;
}
}
/**
- * Add a runnable to finish (or wait for) a deferred operation
- * started in this context earlier. Typically finished by e.g.
- * an Activity#onPause. Used by SharedPreferences$Editor#startCommit().
+ * Add a finisher-runnable to wait for {@link #queue asynchronously processed work}.
*
- * Note that this doesn't actually start it running. This is just
- * a scratch set for callers doing async work to keep updated with
- * what's in-flight. In the common case, caller code
- * (e.g. SharedPreferences) will pretty quickly call remove()
- * after an add(). The only time these Runnables are run is from
- * waitToFinish(), below.
+ * Used by SharedPreferences$Editor#startCommit().
+ *
+ * Note that this doesn't actually start it running. This is just a scratch set for callers
+ * doing async work to keep updated with what's in-flight. In the common case, caller code
+ * (e.g. SharedPreferences) will pretty quickly call remove() after an add(). The only time
+ * these Runnables are run is from {@link #waitToFinish}.
+ *
+ * @param finisher The runnable to add as finisher
*/
- public static void add(Runnable finisher) {
- sPendingWorkFinishers.add(finisher);
- }
-
- public static void remove(Runnable finisher) {
- sPendingWorkFinishers.remove(finisher);
+ public static void addFinisher(Runnable finisher) {
+ synchronized (sLock) {
+ sFinishers.add(finisher);
+ }
}
/**
- * Finishes or waits for async operations to complete.
- * (e.g. SharedPreferences$Editor#startCommit writes)
+ * Remove a previously {@link #addFinisher added} finisher-runnable.
*
- * Is called from the Activity base class's onPause(), after
- * BroadcastReceiver's onReceive, after Service command handling,
- * etc. (so async work is never lost)
+ * @param finisher The runnable to remove.
+ */
+ public static void removeFinisher(Runnable finisher) {
+ synchronized (sLock) {
+ sFinishers.remove(finisher);
+ }
+ }
+
+ /**
+ * Trigger queued work to be processed immediately. The queued work is processed on a separate
+ * thread asynchronous. While doing that run and process all finishers on this thread. The
+ * finishers can be implemented in a way to check weather the queued work is finished.
+ *
+ * Is called from the Activity base class's onPause(), after BroadcastReceiver's onReceive,
+ * after Service command handling, etc. (so async work is never lost)
*/
public static void waitToFinish() {
- Runnable toFinish;
- while ((toFinish = sPendingWorkFinishers.poll()) != null) {
- toFinish.run();
+ long startTime = 0;
+ boolean hadMessages = false;
+
+ if (DEBUG) {
+ startTime = System.currentTimeMillis();
+ }
+
+ Handler handler = getHandler();
+
+ synchronized (sLock) {
+ if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
+ // Delayed work will be processed at processPendingWork() below
+ handler.removeMessages(QueuedWorkHandler.MSG_RUN);
+
+ if (DEBUG) {
+ hadMessages = true;
+ Log.d(LOG_TAG, "waiting");
+ }
+ }
+
+ // We should not delay any work as this might delay the finishers
+ sCanDelay = false;
+ }
+
+ processPendingWork();
+
+ try {
+ while (true) {
+ Runnable finisher;
+
+ synchronized (sLock) {
+ finisher = sFinishers.poll();
+ }
+
+ if (finisher == null) {
+ break;
+ }
+
+ finisher.run();
+ }
+ } finally {
+ sCanDelay = true;
+ }
+
+ if (DEBUG) {
+ long waitTime = System.currentTimeMillis() - startTime;
+
+ if (waitTime > 0 || hadMessages) {
+ Log.d(LOG_TAG, "waited " + waitTime + " ms");
+ }
}
}
-
+
/**
- * Returns true if there is pending work to be done. Note that the
- * result is out of data as soon as you receive it, so be careful how you
- * use it.
+ * Queue a work-runnable for processing asynchronously.
+ *
+ * @param work The new runnable to process
+ * @param shouldDelay If the message should be delayed
+ */
+ public static void queue(Runnable work, boolean shouldDelay) {
+ Handler handler = getHandler();
+
+ synchronized (sLock) {
+ sWork.add(work);
+
+ if (shouldDelay && sCanDelay) {
+ handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
+ } else {
+ handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
+ }
+ }
+ }
+
+ /**
+ * @return True iff there is any {@link #queue async work queued}.
*/
public static boolean hasPendingWork() {
- return !sPendingWorkFinishers.isEmpty();
+ synchronized (sLock) {
+ return !sWork.isEmpty();
+ }
}
-
+
+ private static void processPendingWork() {
+ long startTime = 0;
+
+ if (DEBUG) {
+ startTime = System.currentTimeMillis();
+ }
+
+ synchronized (sProcessingWork) {
+ LinkedList<Runnable> work;
+
+ synchronized (sLock) {
+ work = (LinkedList<Runnable>) sWork.clone();
+ sWork.clear();
+
+ // Remove all msg-s as all work will be processed now
+ getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
+ }
+
+ if (work.size() > 0) {
+ for (Runnable w : work) {
+ w.run();
+ }
+
+ if (DEBUG) {
+ Log.d(LOG_TAG, "processing " + work.size() + " items took " +
+ +(System.currentTimeMillis() - startTime) + " ms");
+ }
+ }
+ }
+ }
+
+ private static class QueuedWorkHandler extends Handler {
+ static final int MSG_RUN = 1;
+
+ QueuedWorkHandler(Looper looper) {
+ super(looper);
+ }
+
+ public void handleMessage(Message msg) {
+ if (msg.what == MSG_RUN) {
+ processPendingWork();
+ }
+ }
+ }
}
diff --git a/core/java/android/app/SharedPreferencesImpl.java b/core/java/android/app/SharedPreferencesImpl.java
index 3bb7019..11ba7ee 100644
--- a/core/java/android/app/SharedPreferencesImpl.java
+++ b/core/java/android/app/SharedPreferencesImpl.java
@@ -53,7 +53,7 @@
final class SharedPreferencesImpl implements SharedPreferences {
private static final String TAG = "SharedPreferencesImpl";
- private static final boolean DEBUG = false;
+ private static final boolean DEBUG = true;
private static final Object CONTENT = new Object();
// Lock ordering rules:
@@ -318,6 +318,7 @@
@GuardedBy("mWritingToDiskLock")
volatile boolean writeToDiskResult = false;
+ boolean wasWritten = false;
private MemoryCommitResult(long memoryStateGeneration, @Nullable List<String> keysModified,
@Nullable Set<OnSharedPreferenceChangeListener> listeners,
@@ -328,7 +329,8 @@
this.mapToWriteToDisk = mapToWriteToDisk;
}
- void setDiskWriteResult(boolean result) {
+ void setDiskWriteResult(boolean wasWritten, boolean result) {
+ this.wasWritten = wasWritten;
writeToDiskResult = result;
writtenToDiskLatch.countDown();
}
@@ -396,6 +398,8 @@
}
public void apply() {
+ final long startTime = System.currentTimeMillis();
+
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
public void run() {
@@ -403,15 +407,21 @@
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
+
+ if (DEBUG && mcr.wasWritten) {
+ Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ + " applied after " + (System.currentTimeMillis() - startTime)
+ + " ms");
+ }
}
};
- QueuedWork.add(awaitCommit);
+ QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
- QueuedWork.remove(awaitCommit);
+ QueuedWork.removeFinisher(awaitCommit);
}
};
@@ -503,13 +513,26 @@
}
public boolean commit() {
+ long startTime = 0;
+
+ if (DEBUG) {
+ startTime = System.currentTimeMillis();
+ }
+
MemoryCommitResult mcr = commitToMemory();
+
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
+ } finally {
+ if (DEBUG) {
+ Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ + " committed after " + (System.currentTimeMillis() - startTime)
+ + " ms");
+ }
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
@@ -587,11 +610,7 @@
}
}
- if (DEBUG) {
- Log.d(TAG, "added " + mcr.memoryStateGeneration + " -> " + mFile.getName());
- }
-
- QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
+ QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
private static FileOutputStream createFileOutputStream(File file) {
@@ -619,8 +638,31 @@
// Note: must hold mWritingToDiskLock
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
+ long startTime = 0;
+ long existsTime = 0;
+ long backupExistsTime = 0;
+ long outputStreamCreateTime = 0;
+ long writeTime = 0;
+ long fsyncTime = 0;
+ long setPermTime = 0;
+ long fstatTime = 0;
+ long deleteTime = 0;
+
+ if (DEBUG) {
+ startTime = System.currentTimeMillis();
+ }
+
+ boolean fileExists = mFile.exists();
+
+ if (DEBUG) {
+ existsTime = System.currentTimeMillis();
+
+ // Might not be set, hence init them to a default value
+ backupExistsTime = existsTime;
+ }
+
// Rename the current file so it may be used as a backup during the next read
- if (mFile.exists()) {
+ if (fileExists) {
boolean needsWrite = false;
// Only need to write if the disk state is older than this commit
@@ -639,18 +681,21 @@
}
if (!needsWrite) {
- if (DEBUG) {
- Log.d(TAG, "skipped " + mcr.memoryStateGeneration + " -> " + mFile.getName());
- }
- mcr.setDiskWriteResult(true);
+ mcr.setDiskWriteResult(false, true);
return;
}
- if (!mBackupFile.exists()) {
+ boolean backupFileExists = mBackupFile.exists();
+
+ if (DEBUG) {
+ backupExistsTime = System.currentTimeMillis();
+ }
+
+ if (!backupFileExists) {
if (!mFile.renameTo(mBackupFile)) {
Log.e(TAG, "Couldn't rename file " + mFile
+ " to backup file " + mBackupFile);
- mcr.setDiskWriteResult(false);
+ mcr.setDiskWriteResult(false, false);
return;
}
} else {
@@ -663,19 +708,34 @@
// from the backup.
try {
FileOutputStream str = createFileOutputStream(mFile);
+
+ if (DEBUG) {
+ outputStreamCreateTime = System.currentTimeMillis();
+ }
+
if (str == null) {
- mcr.setDiskWriteResult(false);
+ mcr.setDiskWriteResult(false, false);
return;
}
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
+
+ if (DEBUG) {
+ writeTime = System.currentTimeMillis();
+ }
+
FileUtils.sync(str);
if (DEBUG) {
- Log.d(TAG, "wrote " + mcr.memoryStateGeneration + " -> " + mFile.getName());
+ fsyncTime = System.currentTimeMillis();
}
str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
+
+ if (DEBUG) {
+ setPermTime = System.currentTimeMillis();
+ }
+
try {
final StructStat stat = Os.stat(mFile.getPath());
synchronized (mLock) {
@@ -685,12 +745,30 @@
} catch (ErrnoException e) {
// Do nothing
}
+
+ if (DEBUG) {
+ fstatTime = System.currentTimeMillis();
+ }
+
// Writing was successful, delete the backup file if there is one.
mBackupFile.delete();
+ if (DEBUG) {
+ deleteTime = System.currentTimeMillis();
+ }
+
mDiskStateGeneration = mcr.memoryStateGeneration;
- mcr.setDiskWriteResult(true);
+ mcr.setDiskWriteResult(true, true);
+
+ Log.d(TAG, "write: " + (existsTime - startTime) + "/"
+ + (backupExistsTime - startTime) + "/"
+ + (outputStreamCreateTime - startTime) + "/"
+ + (writeTime - startTime) + "/"
+ + (fsyncTime - startTime) + "/"
+ + (setPermTime - startTime) + "/"
+ + (fstatTime - startTime) + "/"
+ + (deleteTime - startTime));
return;
} catch (XmlPullParserException e) {
@@ -698,12 +776,13 @@
} catch (IOException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
}
+
// Clean up an unsuccessfully written file
if (mFile.exists()) {
if (!mFile.delete()) {
Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
}
}
- mcr.setDiskWriteResult(false);
+ mcr.setDiskWriteResult(false, false);
}
}
diff --git a/core/java/android/app/assist/AssistStructure.java b/core/java/android/app/assist/AssistStructure.java
index 08aa5f2..6591fc9 100644
--- a/core/java/android/app/assist/AssistStructure.java
+++ b/core/java/android/app/assist/AssistStructure.java
@@ -420,18 +420,17 @@
mRoot = new ViewNode();
ViewNodeBuilder builder = new ViewNodeBuilder(assist, mRoot, false, 0);
- if ((root.getWindowFlags()& WindowManager.LayoutParams.FLAG_SECURE) != 0) {
- // This is a secure window, so it doesn't want a screenshot, and that
- // means we should also not copy out its view hierarchy.
-
+ if ((root.getWindowFlags() & WindowManager.LayoutParams.FLAG_SECURE) != 0) {
if (forAutoFill) {
// NOTE: flags are currently not supported, hence 0
view.onProvideAutoFillStructure(builder, 0);
} else {
+ // This is a secure window, so it doesn't want a screenshot, and that
+ // means we should also not copy out its view hierarchy for Assist
view.onProvideStructure(builder);
+ builder.setAssistBlocked(true);
+ return;
}
- builder.setAssistBlocked(true);
- return;
}
if (forAutoFill) {
// NOTE: flags are currently not supported, hence 0
diff --git a/core/java/android/content/BroadcastReceiver.java b/core/java/android/content/BroadcastReceiver.java
index 485d078..c3d6606 100644
--- a/core/java/android/content/BroadcastReceiver.java
+++ b/core/java/android/content/BroadcastReceiver.java
@@ -211,16 +211,16 @@
// of the list to finish the broadcast, so we don't block this
// thread (which may be the main thread) to have it finished.
//
- // Note that we don't need to use QueuedWork.add() with the
+ // Note that we don't need to use QueuedWork.addFinisher() with the
// runnable, since we know the AM is waiting for us until the
// executor gets to it.
- QueuedWork.singleThreadExecutor().execute( new Runnable() {
+ QueuedWork.queue(new Runnable() {
@Override public void run() {
if (ActivityThread.DEBUG_BROADCAST) Slog.i(ActivityThread.TAG,
"Finishing broadcast after work to component " + mToken);
sendFinished(mgr);
}
- });
+ }, false);
} else {
if (ActivityThread.DEBUG_BROADCAST) Slog.i(ActivityThread.TAG,
"Finishing broadcast to component " + mToken);
diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java
index 5bbbcc2..4480b41 100644
--- a/core/java/android/content/ContentResolver.java
+++ b/core/java/android/content/ContentResolver.java
@@ -1886,7 +1886,7 @@
ContentProvider.getUriWithoutUserId(uri),
notifyForDescendants,
observer,
- ContentProvider.getUserIdFromUri(uri, mContext.getUserId()));
+ ContentProvider.getUserIdFromUri(uri, UserHandle.myUserId()));
}
/** @hide - designated user version */
@@ -1956,7 +1956,7 @@
ContentProvider.getUriWithoutUserId(uri),
observer,
syncToNetwork,
- ContentProvider.getUserIdFromUri(uri, mContext.getUserId()));
+ ContentProvider.getUserIdFromUri(uri, UserHandle.myUserId()));
}
/**
@@ -1982,7 +1982,7 @@
ContentProvider.getUriWithoutUserId(uri),
observer,
flags,
- ContentProvider.getUserIdFromUri(uri, mContext.getUserId()));
+ ContentProvider.getUserIdFromUri(uri, UserHandle.myUserId()));
}
/**
diff --git a/core/java/android/content/pm/ChangedPackages.aidl b/core/java/android/content/pm/ChangedPackages.aidl
new file mode 100644
index 0000000..1a9f5a1
--- /dev/null
+++ b/core/java/android/content/pm/ChangedPackages.aidl
@@ -0,0 +1,19 @@
+/**
+ * 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.
+ * 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;
+
+parcelable ChangedPackages;
\ No newline at end of file
diff --git a/core/java/android/content/pm/ChangedPackages.java b/core/java/android/content/pm/ChangedPackages.java
new file mode 100644
index 0000000..94b8a5d
--- /dev/null
+++ b/core/java/android/content/pm/ChangedPackages.java
@@ -0,0 +1,82 @@
+/**
+ * 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.
+ * 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;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Intent;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.List;
+
+/**
+ * Packages that have been changed since the last time they
+ * were requested.
+ */
+public final class ChangedPackages implements Parcelable {
+ /** The last known sequence number for these changes */
+ private final int mSequenceNumber;
+ /** The names of the packages that have changed */
+ private final List<String> mPackageNames;
+
+ public ChangedPackages(int sequenceNumber, @NonNull List<String> packageNames) {
+ this.mSequenceNumber = sequenceNumber;
+ this.mPackageNames = packageNames;
+ }
+
+ /** @hide */
+ protected ChangedPackages(Parcel in) {
+ mSequenceNumber = in.readInt();
+ mPackageNames = in.createStringArrayList();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mSequenceNumber);
+ dest.writeStringList(mPackageNames);
+ }
+
+ /**
+ * Returns the last known sequence number for these changes.
+ */
+ public int getSequenceNumber() {
+ return mSequenceNumber;
+ }
+
+ /**
+ * Returns the names of the packages that have changed.
+ */
+ public @NonNull List<String> getPackageNames() {
+ return mPackageNames;
+ }
+
+ public static final Parcelable.Creator<ChangedPackages> CREATOR =
+ new Parcelable.Creator<ChangedPackages>() {
+ public ChangedPackages createFromParcel(Parcel in) {
+ return new ChangedPackages(in);
+ }
+
+ public ChangedPackages[] newArray(int size) {
+ return new ChangedPackages[size];
+ }
+ };
+}
diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl
index 3fb46cf..9d36a73 100644
--- a/core/java/android/content/pm/IPackageManager.aidl
+++ b/core/java/android/content/pm/IPackageManager.aidl
@@ -23,6 +23,7 @@
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.ContainerEncryptionParams;
+import android.content.pm.ChangedPackages;
import android.content.pm.InstantAppInfo;
import android.content.pm.FeatureInfo;
import android.content.pm.IPackageInstallObserver2;
@@ -611,6 +612,8 @@
String getServicesSystemSharedLibraryPackageName();
String getSharedSystemSharedLibraryPackageName();
+ ChangedPackages getChangedPackages(int sequenceNumber, int userId);
+
boolean isPackageDeviceAdminOnAnyUser(String packageName);
List<String> getPreviousCodePaths(in String packageName);
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index b20b5e2..308153d 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -20,6 +20,7 @@
import android.annotation.CheckResult;
import android.annotation.DrawableRes;
import android.annotation.IntDef;
+import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -3853,6 +3854,17 @@
public abstract @NonNull String getSharedSystemSharedLibraryPackageName();
/**
+ * Returns the names of the packages that have been changed
+ * [eg. added, removed or updated] since the given sequence
+ * number.
+ * <p>If no packages have been changed, returns <code>null</code>.
+ * <p>The sequence number starts at <code>0</code> and is
+ * reset every boot.
+ */
+ public abstract @Nullable ChangedPackages getChangedPackages(
+ @IntRange(from=0) int sequenceNumber);
+
+ /**
* Get a list of features that are available on the
* system.
*
diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java
index dc5750d..acb1d07 100644
--- a/core/java/android/net/ConnectivityManager.java
+++ b/core/java/android/net/ConnectivityManager.java
@@ -1453,8 +1453,9 @@
private void sendExpireMsgForFeature(NetworkCapabilities netCap, int seqNum, int delay) {
if (delay >= 0) {
Log.d(TAG, "sending expire msg with seqNum " + seqNum + " and delay " + delay);
- Message msg = sCallbackHandler.obtainMessage(EXPIRE_LEGACY_REQUEST, seqNum, 0, netCap);
- sCallbackHandler.sendMessageDelayed(msg, delay);
+ CallbackHandler handler = getHandler();
+ Message msg = handler.obtainMessage(EXPIRE_LEGACY_REQUEST, seqNum, 0, netCap);
+ handler.sendMessageDelayed(msg, delay);
}
}
@@ -2897,19 +2898,19 @@
}
}
- static final HashMap<NetworkRequest, NetworkCallback> sCallbacks = new HashMap<>();
- static CallbackHandler sCallbackHandler;
+ private static final HashMap<NetworkRequest, NetworkCallback> sCallbacks = new HashMap<>();
+ private static CallbackHandler sCallbackHandler;
- private final static int LISTEN = 1;
- private final static int REQUEST = 2;
+ private static final int LISTEN = 1;
+ private static final int REQUEST = 2;
private NetworkRequest sendRequestForNetwork(NetworkCapabilities need,
NetworkCallback callback, int timeoutMs, int action, int legacyType) {
- return sendRequestForNetwork(need, callback, getHandler(), timeoutMs, action, legacyType);
+ return sendRequestForNetwork(need, callback, timeoutMs, action, legacyType, getHandler());
}
- private NetworkRequest sendRequestForNetwork(NetworkCapabilities need,
- NetworkCallback callback, Handler handler, int timeoutMs, int action, int legacyType) {
+ private NetworkRequest sendRequestForNetwork(NetworkCapabilities need, NetworkCallback callback,
+ int timeoutMs, int action, int legacyType, CallbackHandler handler) {
if (callback == null) {
throw new IllegalArgumentException("null NetworkCallback");
}
diff --git a/core/java/android/os/ZygoteProcess.java b/core/java/android/os/ZygoteProcess.java
index fa9f394..b3366d8 100644
--- a/core/java/android/os/ZygoteProcess.java
+++ b/core/java/android/os/ZygoteProcess.java
@@ -506,4 +506,24 @@
state.writer.flush();
}
}
+
+ /**
+ * Instructs the zygote to preload the default set of classes and resources. Returns
+ * {@code true} if a preload was performed as a result of this call, and {@code false}
+ * otherwise. The latter usually means that the zygote eagerly preloaded at startup
+ * or due to a previous call to {@code preloadDefault}. Note that this call is synchronous.
+ */
+ public boolean preloadDefault(String abi) throws ZygoteStartFailedEx, IOException {
+ synchronized (mLock) {
+ ZygoteState state = openZygoteSocketIfNeeded(abi);
+ // Each query starts with the argument count (1 in this case)
+ state.writer.write("1");
+ state.writer.newLine();
+ state.writer.write("--preload-default");
+ state.writer.newLine();
+ state.writer.flush();
+
+ return (state.inputStream.readInt() == 0);
+ }
+ }
}
diff --git a/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java b/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java
index cb5f220..491eabc 100644
--- a/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java
+++ b/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java
@@ -97,6 +97,8 @@
}
mAudioTrack.setPlaybackPositionUpdateListener(this);
+ // Ensure we set the first marker if there is one.
+ updateMarker();
try {
byte[] buffer = null;
diff --git a/core/java/android/text/method/LinkMovementMethod.java b/core/java/android/text/method/LinkMovementMethod.java
index 24c119f..31ed549 100644
--- a/core/java/android/text/method/LinkMovementMethod.java
+++ b/core/java/android/text/method/LinkMovementMethod.java
@@ -29,9 +29,6 @@
/**
* A movement method that traverses links in the text buffer and scrolls if necessary.
* Supports clicking on links with DPad Center or Enter.
- *
- * <p>Note: Starting from Android 8.0 (API level 25) this class no longer handles the touch
- * clicks.
*/
public class LinkMovementMethod extends ScrollingMovementMethod {
private static final int CLICK = 1;
@@ -198,7 +195,7 @@
MotionEvent event) {
int action = event.getAction();
- if (action == MotionEvent.ACTION_DOWN) {
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
int x = (int) event.getX();
int y = (int) event.getY();
@@ -215,9 +212,13 @@
ClickableSpan[] links = buffer.getSpans(off, off, ClickableSpan.class);
if (links.length != 0) {
- Selection.setSelection(buffer,
+ if (action == MotionEvent.ACTION_UP) {
+ links[0].onClick(widget);
+ } else if (action == MotionEvent.ACTION_DOWN) {
+ Selection.setSelection(buffer,
buffer.getSpanStart(links[0]),
buffer.getSpanEnd(links[0]));
+ }
return true;
} else {
Selection.removeSelection(buffer);
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 17cd446..5572cbb 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -121,7 +121,6 @@
import android.view.Choreographer;
import android.view.ContextMenu;
import android.view.DragEvent;
-import android.view.GestureDetector;
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
import android.view.KeyCharacterMap;
@@ -682,8 +681,6 @@
*/
private Editor mEditor;
- private GestureDetector mClickableSpanOnClickGestureDetector;
-
private static final int DEVICE_PROVISIONED_UNKNOWN = 0;
private static final int DEVICE_PROVISIONED_NO = 1;
private static final int DEVICE_PROVISIONED_YES = 2;
@@ -9319,24 +9316,21 @@
handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);
}
- // Lazily create the clickable span gesture detector only if it looks like it
- // might be useful.
- if (action == MotionEvent.ACTION_DOWN && mClickableSpanOnClickGestureDetector == null
- && shouldUseClickableSpanOnClickGestureDetector()) {
- ClickableSpan[] links = ((Spannable) mText).getSpans(
- getSelectionStart(), getSelectionEnd(),
- ClickableSpan.class);
+ final boolean textIsSelectable = isTextSelectable();
+ if (touchIsFinished && mLinksClickable && mAutoLinkMask != 0 && textIsSelectable) {
+ // The LinkMovementMethod which should handle taps on links has not been installed
+ // on non editable text that support text selection.
+ // We reproduce its behavior here to open links for these.
+ ClickableSpan[] links = ((Spannable) mText).getSpans(getSelectionStart(),
+ getSelectionEnd(), ClickableSpan.class);
+
if (links.length > 0) {
- mClickableSpanOnClickGestureDetector =
- createClickableSpanOnClickGestureDetector();
+ links[0].onClick(this);
+ handled = true;
}
}
- if (mClickableSpanOnClickGestureDetector != null) {
- handled |= mClickableSpanOnClickGestureDetector.onTouchEvent(event);
- }
-
- if (touchIsFinished && (isTextEditable() || isTextSelectable())) {
+ if (touchIsFinished && (isTextEditable() || textIsSelectable)) {
// Show the IME, except when selecting in read-only text.
final InputMethodManager imm = InputMethodManager.peekInstance();
viewClicked(imm);
@@ -9754,31 +9748,6 @@
mEditor.onLocaleChanged();
}
- private GestureDetector createClickableSpanOnClickGestureDetector() {
- return new GestureDetector(mContext,
- new GestureDetector.SimpleOnGestureListener() {
- @Override
- public boolean onSingleTapUp(MotionEvent e) {
- if (shouldUseClickableSpanOnClickGestureDetector()) {
- ClickableSpan[] links = ((Spannable) mText).getSpans(
- getSelectionStart(), getSelectionEnd(),
- ClickableSpan.class);
- if (links.length > 0) {
- links[0].onClick(TextView.this);
- return true;
- }
- }
- return false;
- }
- });
- }
-
- private boolean shouldUseClickableSpanOnClickGestureDetector() {
- return mLinksClickable && (mMovement != null) &&
- (mMovement instanceof LinkMovementMethod
- || (mAutoLinkMask != 0 && isTextSelectable()));
- }
-
/**
* This method is used by the ArrowKeyMovementMethod to jump from one word to the other.
* Made available to achieve a consistent behavior.
diff --git a/core/java/com/android/internal/os/WebViewZygoteInit.java b/core/java/com/android/internal/os/WebViewZygoteInit.java
index d82a211..f27c0d4 100644
--- a/core/java/com/android/internal/os/WebViewZygoteInit.java
+++ b/core/java/com/android/internal/os/WebViewZygoteInit.java
@@ -55,8 +55,15 @@
}
@Override
- protected void maybePreload() {
- // Do nothing, we don't need to call ZygoteInit.maybePreload() for the WebView zygote.
+ protected void preload() {
+ // Nothing to preload by default.
+ }
+
+ @Override
+ protected boolean isPreloadComplete() {
+ // Webview zygotes don't preload any classes or resources or defaults, all of their
+ // preloading is package specific.
+ return true;
}
@Override
diff --git a/core/java/com/android/internal/os/Zygote.java b/core/java/com/android/internal/os/Zygote.java
index 59416dd..fa71a62 100644
--- a/core/java/com/android/internal/os/Zygote.java
+++ b/core/java/com/android/internal/os/Zygote.java
@@ -173,6 +173,10 @@
VM_HOOKS.postForkChild(debugFlags, isSystemServer, instructionSet);
}
+ /**
+ * Resets this process' priority to the default value (0).
+ */
+ native static void nativeResetNicePriority();
/**
* Executes "/system/bin/sh -c <command>" using the exec() system call.
diff --git a/core/java/com/android/internal/os/ZygoteConnection.java b/core/java/com/android/internal/os/ZygoteConnection.java
index a7f311b..e2485e9 100644
--- a/core/java/com/android/internal/os/ZygoteConnection.java
+++ b/core/java/com/android/internal/os/ZygoteConnection.java
@@ -171,7 +171,9 @@
return handleAbiListQuery();
}
- maybePreload();
+ if (parsedArgs.preloadDefault) {
+ return handlePreload();
+ }
if (parsedArgs.preloadPackage != null) {
return handlePreloadPackage(parsedArgs.preloadPackage,
@@ -282,8 +284,34 @@
}
}
- protected void maybePreload() {
- ZygoteInit.maybePreload();
+ /**
+ * Preloads resources if the zygote is in lazily preload mode. Writes the result of the
+ * preload operation; {@code 0} when a preload was initiated due to this request and {@code 1}
+ * if no preload was initiated. The latter implies that the zygote is not configured to load
+ * resources lazy or that the zygote has already handled a previous request to handlePreload.
+ */
+ private boolean handlePreload() {
+ try {
+ if (isPreloadComplete()) {
+ mSocketOutStream.writeInt(1);
+ } else {
+ preload();
+ mSocketOutStream.writeInt(0);
+ }
+
+ return false;
+ } catch (IOException ioe) {
+ Log.e(TAG, "Error writing to command socket", ioe);
+ return true;
+ }
+ }
+
+ protected void preload() {
+ ZygoteInit.lazyPreload();
+ }
+
+ protected boolean isPreloadComplete() {
+ return ZygoteInit.isPreloadComplete();
}
protected boolean handlePreloadPackage(String packagePath, String libsPath) {
@@ -402,6 +430,13 @@
String preloadPackageLibs;
/**
+ * Whether this is a request to start preloading the default resources and classes.
+ * This argument only makes sense when the zygote is in lazy preload mode (i.e, when
+ * it's started with --enable-lazy-preload).
+ */
+ boolean preloadDefault;
+
+ /**
* Constructs instance and parses args
* @param args zygote command-line args
* @throws IllegalArgumentException
@@ -564,6 +599,8 @@
} else if (arg.equals("--preload-package")) {
preloadPackage = args[++curArg];
preloadPackageLibs = args[++curArg];
+ } else if (arg.equals("--preload-default")) {
+ preloadDefault = true;
} else {
break;
}
@@ -578,7 +615,7 @@
throw new IllegalArgumentException(
"Unexpected arguments after --preload-package.");
}
- } else {
+ } else if (!preloadDefault) {
if (!seenRuntimeArgs) {
throw new IllegalArgumentException("Unexpected argument : " + args[curArg]);
}
diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java
index a72b66a..0b5a1b7 100644
--- a/core/java/com/android/internal/os/ZygoteInit.java
+++ b/core/java/com/android/internal/os/ZygoteInit.java
@@ -51,6 +51,7 @@
import com.android.internal.logging.MetricsLogger;
+import com.android.internal.util.Preconditions;
import dalvik.system.DexFile;
import dalvik.system.PathClassLoader;
import dalvik.system.VMRuntime;
@@ -146,11 +147,11 @@
sPreloadComplete = true;
}
- public static void maybePreload() {
- if (!sPreloadComplete) {
- Log.i(TAG, "Lazily preloading resources.");
- preload(new BootTimingsTraceLog("ZygoteInitTiming_lazy", Trace.TRACE_TAG_DALVIK));
- }
+ public static void lazyPreload() {
+ Preconditions.checkState(!sPreloadComplete);
+ Log.i(TAG, "Lazily preloading resources.");
+
+ preload(new BootTimingsTraceLog("ZygoteInitTiming_lazy", Trace.TRACE_TAG_DALVIK));
}
private static void beginIcuCachePinning() {
@@ -712,6 +713,8 @@
EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_END,
SystemClock.uptimeMillis());
bootTimingsTraceLog.traceEnd(); // ZygotePreload
+ } else {
+ Zygote.nativeResetNicePriority();
}
// Finish profiling the zygote initialization.
@@ -783,6 +786,10 @@
}
}
+ static boolean isPreloadComplete() {
+ return sPreloadComplete;
+ }
+
/**
* Class not instantiable.
*/
diff --git a/core/java/com/android/internal/util/NotificationColorUtil.java b/core/java/com/android/internal/util/NotificationColorUtil.java
index b4890b7..44b21b4 100644
--- a/core/java/com/android/internal/util/NotificationColorUtil.java
+++ b/core/java/com/android/internal/util/NotificationColorUtil.java
@@ -456,7 +456,10 @@
}
}
- public static int resolveActionBarColor(int backgroundColor) {
+ public static int resolveActionBarColor(Context context, int backgroundColor) {
+ if (backgroundColor == Notification.COLOR_DEFAULT) {
+ return context.getColor(com.android.internal.R.color.notification_action_list);
+ }
boolean useDark = shouldUseDark(backgroundColor);
final double[] result = ColorUtilsFromCompat.getTempDouble3Array();
ColorUtilsFromCompat.colorToLAB(backgroundColor, result);
diff --git a/core/java/com/android/internal/widget/ILockSettings.aidl b/core/java/com/android/internal/widget/ILockSettings.aidl
index b380b13..b8c062e 100644
--- a/core/java/com/android/internal/widget/ILockSettings.aidl
+++ b/core/java/com/android/internal/widget/ILockSettings.aidl
@@ -45,4 +45,10 @@
void systemReady();
void userPresent(int userId);
int getStrongAuthForUser(int userId);
+
+ long addEscrowToken(in byte[] token, int userId);
+ boolean removeEscrowToken(long handle, int userId);
+ boolean isEscrowTokenActive(long handle, int userId);
+ boolean setLockCredentialWithToken(String credential, int type, long tokenHandle, in byte[] token, int userId);
+ void unlockUserWithToken(long tokenHandle, in byte[] token, int userId);
}
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index ef6e6c2..0aba9c2 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -147,6 +147,10 @@
public static final String PROFILE_KEY_NAME_ENCRYPT = "profile_key_name_encrypt_";
public static final String PROFILE_KEY_NAME_DECRYPT = "profile_key_name_decrypt_";
+ public static final String SYNTHETIC_PASSWORD_KEY_PREFIX = "synthetic_password_";
+
+ public static final String SYNTHETIC_PASSWORD_HANDLE_KEY = "sp-handle";
+ public static final String SYNTHETIC_PASSWORD_ENABLED_KEY = "enable-sp";
private final Context mContext;
private final ContentResolver mContentResolver;
@@ -769,7 +773,7 @@
getLockSettings().setLockCredential(password, CREDENTIAL_TYPE_PASSWORD, savedPassword,
userHandle);
- addEncryptionPassword(password, computedQuality, userHandle);
+ updateEncryptionPasswordIfNeeded(password, computedQuality, userHandle);
updatePasswordHistory(password, userHandle);
} catch (RemoteException re) {
// Cant do much
@@ -777,7 +781,11 @@
}
}
- private void addEncryptionPassword(String password, int quality, int userHandle) {
+ /**
+ * Update device encryption password if calling user is USER_SYSTEM and device supports
+ * encryption.
+ */
+ private void updateEncryptionPasswordIfNeeded(String password, int quality, int userHandle) {
// Update the device encryption password.
if (userHandle == UserHandle.USER_SYSTEM
&& LockPatternUtils.isDeviceEncryptionEnabled()) {
@@ -1398,6 +1406,104 @@
}
/**
+ * Create an escrow token for the current user, which can later be used to unlock FBE
+ * or change user password.
+ *
+ * After adding, if the user currently has lockscreen password, he will need to perform a
+ * confirm credential operation in order to activate the token for future use. If the user
+ * has no secure lockscreen, then the token is activated immediately.
+ *
+ * @return a unique 64-bit token handle which is needed to refer to this token later.
+ */
+ public long addEscrowToken(byte[] token, int userId) {
+ try {
+ return getLockSettings().addEscrowToken(token, userId);
+ } catch (RemoteException re) {
+ return 0L;
+ }
+ }
+
+ /**
+ * Remove an escrow token.
+ * @return true if the given handle refers to a valid token previously returned from
+ * {@link #addEscrowToken}, whether it's active or not. return false otherwise.
+ */
+ public boolean removeEscrowToken(long handle, int userId) {
+ try {
+ return getLockSettings().removeEscrowToken(handle, userId);
+ } catch (RemoteException re) {
+ return false;
+ }
+ }
+
+ /**
+ * Check if the given escrow token is active or not. Only active token can be used to call
+ * {@link #setLockCredentialWithToken} and {@link #unlockUserWithToken}
+ */
+ public boolean isEscrowTokenActive(long handle, int userId) {
+ try {
+ return getLockSettings().isEscrowTokenActive(handle, userId);
+ } catch (RemoteException re) {
+ return false;
+ }
+ }
+
+ public boolean setLockCredentialWithToken(String credential, int type, long tokenHandle,
+ byte[] token, int userId) {
+ try {
+ if (type != CREDENTIAL_TYPE_NONE) {
+ if (TextUtils.isEmpty(credential) || credential.length() < MIN_LOCK_PASSWORD_SIZE) {
+ throw new IllegalArgumentException("password must not be null and at least "
+ + "of length " + MIN_LOCK_PASSWORD_SIZE);
+ }
+
+ final int computedQuality = PasswordMetrics.computeForPassword(credential).quality;
+ if (!getLockSettings().setLockCredentialWithToken(credential, type, tokenHandle,
+ token, userId)) {
+ return false;
+ }
+ setLong(PASSWORD_TYPE_KEY, Math.max(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC,
+ computedQuality), userId);
+
+ updateEncryptionPasswordIfNeeded(credential, computedQuality, userId);
+ updatePasswordHistory(credential, userId);
+ } else {
+ if (!TextUtils.isEmpty(credential)) {
+ throw new IllegalArgumentException("password must be emtpy for NONE type");
+ }
+ if (!getLockSettings().setLockCredentialWithToken(null, CREDENTIAL_TYPE_NONE,
+ tokenHandle, token, userId)) {
+ return false;
+ }
+ setLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED,
+ userId);
+
+ if (userId == UserHandle.USER_SYSTEM) {
+ // Set the encryption password to default.
+ updateEncryptionPassword(StorageManager.CRYPT_TYPE_DEFAULT, null);
+ setCredentialRequiredToDecrypt(false);
+ }
+ }
+ onAfterChangingPassword(userId);
+ return true;
+ } catch (RemoteException re) {
+ Log.e(TAG, "Unable to save lock password ", re);
+ re.rethrowFromSystemServer();
+ }
+ return false;
+ }
+
+ public void unlockUserWithToken(long tokenHandle, byte[] token, int userId) {
+ try {
+ getLockSettings().unlockUserWithToken(tokenHandle, token, userId);
+ } catch (RemoteException re) {
+ Log.e(TAG, "Unable to unlock user with token", re);
+ re.rethrowFromSystemServer();
+ }
+ }
+
+
+ /**
* Callback to be notified about progress when checking credentials.
*/
public interface CheckCredentialProgressCallback {
@@ -1559,6 +1665,14 @@
break;
}
}
- };
+ }
+ }
+
+ public void enableSyntheticPassword() {
+ setLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 1L, UserHandle.USER_SYSTEM);
+ }
+
+ public boolean isSyntheticPasswordEnabled() {
+ return getLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 0, UserHandle.USER_SYSTEM) != 0;
}
}
diff --git a/core/java/com/android/internal/widget/SwipeDismissLayout.java b/core/java/com/android/internal/widget/SwipeDismissLayout.java
index 261fa43..6d814bf 100644
--- a/core/java/com/android/internal/widget/SwipeDismissLayout.java
+++ b/core/java/com/android/internal/widget/SwipeDismissLayout.java
@@ -79,7 +79,6 @@
private boolean mDismissed;
private boolean mDiscardIntercept;
private VelocityTracker mVelocityTracker;
- private float mTranslationX;
private boolean mBlockGesture = false;
private boolean mActivityTranslucencyConverted = false;
@@ -166,8 +165,10 @@
return super.onInterceptTouchEvent(ev);
}
- // offset because the view is translated during swipe
- ev.offsetLocation(mTranslationX, 0);
+ // Offset because the view is translated during swipe, match X with raw X. Active touch
+ // coordinates are mostly used by the velocity tracker, so offset it to match the raw
+ // coordinates which is what is primarily used elsewhere.
+ ev.offsetLocation(ev.getRawX() - ev.getX(), 0);
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
@@ -232,8 +233,12 @@
if (mVelocityTracker == null || !mDismissable) {
return super.onTouchEvent(ev);
}
- // offset because the view is translated during swipe
- ev.offsetLocation(mTranslationX, 0);
+
+ // Offset because the view is translated during swipe, match X with raw X. Active touch
+ // coordinates are mostly used by the velocity tracker, so offset it to match the raw
+ // coordinates which is what is primarily used elsewhere.
+ ev.offsetLocation(ev.getRawX() - ev.getX(), 0);
+
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_UP:
updateDismiss(ev);
@@ -266,7 +271,6 @@
}
private void setProgress(float deltaX) {
- mTranslationX = deltaX;
if (mProgressListener != null && deltaX >= 0) {
mProgressListener.onSwipeProgressChanged(
this, progressToAlpha(deltaX / getWidth()), deltaX);
@@ -300,7 +304,6 @@
mVelocityTracker.recycle();
}
mVelocityTracker = null;
- mTranslationX = 0;
mDownX = 0;
mLastX = Integer.MIN_VALUE;
mDownY = 0;
diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp
index e2fc444..c3f0e9d 100644
--- a/core/jni/com_android_internal_os_Zygote.cpp
+++ b/core/jni/com_android_internal_os_Zygote.cpp
@@ -806,6 +806,10 @@
UnmountTree("/storage");
}
+static void com_android_internal_os_Zygote_nativeResetNicePriority(JNIEnv* env, jclass) {
+ ResetNicePriority(env);
+}
+
static const JNINativeMethod gMethods[] = {
{ "nativeForkAndSpecialize",
"(II[II[[IILjava/lang/String;Ljava/lang/String;[I[ILjava/lang/String;Ljava/lang/String;)I",
@@ -815,7 +819,9 @@
{ "nativeAllowFileAcrossFork", "(Ljava/lang/String;)V",
(void *) com_android_internal_os_Zygote_nativeAllowFileAcrossFork },
{ "nativeUnmountStorageOnInit", "()V",
- (void *) com_android_internal_os_Zygote_nativeUnmountStorageOnInit }
+ (void *) com_android_internal_os_Zygote_nativeUnmountStorageOnInit },
+ { "nativeResetNicePriority", "()V",
+ (void *) com_android_internal_os_Zygote_nativeResetNicePriority }
};
int register_com_android_internal_os_Zygote(JNIEnv* env) {
diff --git a/core/proto/android/service/notification.proto b/core/proto/android/service/notification.proto
index bc257e0..819460e 100644
--- a/core/proto/android/service/notification.proto
+++ b/core/proto/android/service/notification.proto
@@ -23,6 +23,8 @@
message NotificationServiceDumpProto {
repeated NotificationRecordProto records = 1;
+
+ ZenModeProto zen = 2;
}
message NotificationRecordProto {
@@ -42,4 +44,21 @@
ENQUEUED = 0;
POSTED = 1;
+
+ SNOOZED = 2;
+}
+
+message ZenModeProto {
+ ZenMode zen_mode = 1;
+ repeated string enabled_active_conditions = 2;
+ int32 suppressed_effects = 3;
+ repeated string suppressors = 4;
+ string policy = 5;
+}
+
+enum ZenMode {
+ ZEN_MODE_OFF = 0;
+ ZEN_MODE_IMPORTANT_INTERRUPTIONS = 1;
+ ZEN_MODE_NO_INTERRUPTIONS = 2;
+ ZEN_MODE_ALARMS = 3;
}
\ No newline at end of file
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 36bb821..4d5e45b 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -5846,8 +5846,6 @@
<!-- Drawable used to draw masked icons with foreground and background layers. -->
<declare-styleable name="MaskableIconDrawableLayer">
- <!-- The color to use for the layer, only if drawable is not defined. -->
- <attr name="color" />
<!-- The drawable to use for the layer. -->
<attr name="drawable" />
</declare-styleable>
diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml
index 7b8c229..91ce7a4 100644
--- a/core/tests/coretests/AndroidManifest.xml
+++ b/core/tests/coretests/AndroidManifest.xml
@@ -1357,9 +1357,6 @@
</intent-filter>
</service>
- <service android:name="android.content.CrossUserContentService"
- android:exported="true" />
-
</application>
<instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
diff --git a/core/tests/coretests/src/android/app/activity/LocalProvider.java b/core/tests/coretests/src/android/app/activity/LocalProvider.java
index b7a63ce..d6a10c2 100644
--- a/core/tests/coretests/src/android/app/activity/LocalProvider.java
+++ b/core/tests/coretests/src/android/app/activity/LocalProvider.java
@@ -29,19 +29,6 @@
public class LocalProvider extends ContentProvider {
private static final String TAG = "LocalProvider";
- private static final String AUTHORITY = "com.android.frameworks.coretests.LocalProvider";
- private static final String TABLE_DATA_NAME = "data";
- public static final Uri TABLE_DATA_URI =
- Uri.parse("content://" + AUTHORITY + "/" + TABLE_DATA_NAME);
-
- public static final String COLUMN_TEXT_NAME = "text";
- public static final String COLUMN_INTEGER_NAME = "integer";
-
- public static final String TEXT1 = "first data";
- public static final String TEXT2 = "second data";
- public static final int INTEGER1 = 100;
- public static final int INTEGER2 = 101;
-
private SQLiteOpenHelper mOpenHelper;
private static final int DATA = 1;
@@ -64,20 +51,13 @@
@Override
public void onCreate(SQLiteDatabase db) {
- db.execSQL("CREATE TABLE " + TABLE_DATA_NAME + " (" +
+ db.execSQL("CREATE TABLE data (" +
"_id INTEGER PRIMARY KEY," +
- COLUMN_TEXT_NAME + " TEXT, " +
- COLUMN_INTEGER_NAME + " INTEGER);");
+ "text TEXT, " +
+ "integer INTEGER);");
// insert alarms
- db.execSQL(getInsertCommand(TEXT1, INTEGER1));
- db.execSQL(getInsertCommand(TEXT2, INTEGER2));
- }
-
- private String getInsertCommand(String textValue, int integerValue) {
- return "INSERT INTO " + TABLE_DATA_NAME
- + " (" + COLUMN_TEXT_NAME + ", " + COLUMN_INTEGER_NAME + ") "
- + "VALUES ('" + textValue + "', " + integerValue + ");";
+ db.execSQL("INSERT INTO data (text, integer) VALUES ('first data', 100);");
}
@Override
@@ -94,10 +74,6 @@
public LocalProvider() {
}
- static public Uri getTableDataUriForRow(int rowId) {
- return Uri.parse("content://" + AUTHORITY + "/" + TABLE_DATA_NAME + "/" + rowId);
- }
-
@Override
public boolean onCreate() {
mOpenHelper = new DatabaseHelper(getContext());
diff --git a/core/tests/coretests/src/android/content/CrossUserContentResolverTest.java b/core/tests/coretests/src/android/content/CrossUserContentResolverTest.java
deleted file mode 100644
index 027ba8e..0000000
--- a/core/tests/coretests/src/android/content/CrossUserContentResolverTest.java
+++ /dev/null
@@ -1,175 +0,0 @@
-/*
- * 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.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.content;
-
-
-import static org.junit.Assert.fail;
-
-import android.app.ActivityManager;
-import android.app.activity.LocalProvider;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.pm.UserInfo;
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.os.IBinder;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-@MediumTest
-@RunWith(AndroidJUnit4.class)
-public class CrossUserContentResolverTest {
- private final static int TIMEOUT_SERVICE_CONNECTION_SEC = 4;
- private final static int TIMEOUT_CONTENT_CHANGE_SEC = 4;
-
- private Context mContext;
- private UserManager mUm;
- private int mSecondaryUserId = -1;
- private CrossUserContentServiceConnection mServiceConnection;
-
- @Before
- public void setUp() throws Exception {
- mContext = InstrumentationRegistry.getContext();
- mUm = UserManager.get(mContext);
- final UserInfo userInfo = mUm.createUser("Test user", 0);
- mSecondaryUserId = userInfo.id;
- final PackageManager pm = mContext.getPackageManager();
- pm.installExistingPackageAsUser(mContext.getPackageName(), mSecondaryUserId);
- ActivityManager.getService().startUserInBackground(mSecondaryUserId);
-
- final CountDownLatch connectionLatch = new CountDownLatch(1);
- mServiceConnection = new CrossUserContentServiceConnection(connectionLatch);
- mContext.bindServiceAsUser(
- new Intent(mContext, CrossUserContentService.class),
- mServiceConnection,
- Context.BIND_AUTO_CREATE,
- UserHandle.of(mSecondaryUserId));
- if (!connectionLatch.await(TIMEOUT_SERVICE_CONNECTION_SEC, TimeUnit.SECONDS)) {
- fail("Timed out waiting for service connection to establish");
- }
- }
-
- @After
- public void tearDown() throws Exception {
- if (mSecondaryUserId != -1) {
- mUm.removeUser(mSecondaryUserId);
- }
- if (mServiceConnection != null) {
- mContext.unbindService(mServiceConnection);
- }
- }
-
- /**
- * Register an observer for an URI in the secondary user and verify that it receives
- * onChange callback when data at the URI changes.
- */
- @Test
- public void testRegisterContentObserver() throws Exception {
- Context secondaryUserContext = null;
- String packageName = null;
- try {
- packageName = InstrumentationRegistry.getContext().getPackageName();
- secondaryUserContext =
- InstrumentationRegistry.getContext().createPackageContextAsUser(
- packageName, 0 /* flags */, UserHandle.of(mSecondaryUserId));
- } catch (NameNotFoundException e) {
- fail("Couldn't find package " + packageName + " in u" + mSecondaryUserId);
- }
-
- final CountDownLatch updateLatch = new CountDownLatch(1);
- final Uri uriToUpdate = LocalProvider.getTableDataUriForRow(2);
- final TestContentObserver observer = new TestContentObserver(updateLatch,
- uriToUpdate, mSecondaryUserId);
- secondaryUserContext.getContentResolver().registerContentObserver(
- LocalProvider.TABLE_DATA_URI, true, observer, mSecondaryUserId);
- mServiceConnection.getService().updateContent(uriToUpdate, "New Text", 42);
- if (!updateLatch.await(TIMEOUT_CONTENT_CHANGE_SEC, TimeUnit.SECONDS)) {
- fail("Timed out waiting for the content change callback");
- }
- }
-
- /**
- * Register an observer for an URI in the current user and verify that secondary user can
- * notify changes for this URI.
- */
- @Test
- public void testNotifyChange() throws Exception {
- final CountDownLatch notifyLatch = new CountDownLatch(1);
- final Uri notifyUri = LocalProvider.TABLE_DATA_URI;
- final TestContentObserver observer = new TestContentObserver(notifyLatch,
- notifyUri, UserHandle.myUserId());
- mContext.getContentResolver().registerContentObserver(notifyUri, true, observer);
- mServiceConnection.getService().notifyForUriAsUser(notifyUri, UserHandle.myUserId());
- if (!notifyLatch.await(TIMEOUT_CONTENT_CHANGE_SEC, TimeUnit.SECONDS)) {
- fail("Timed out waiting for the notify callback");
- }
- }
-
- private static final class CrossUserContentServiceConnection implements ServiceConnection {
- private ICrossUserContentService mService;
- private final CountDownLatch mLatch;
-
- public CrossUserContentServiceConnection(CountDownLatch latch) {
- mLatch = latch;
- }
-
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- mService = ICrossUserContentService.Stub.asInterface(service);
- mLatch.countDown();
- }
-
- @Override
- public void onServiceDisconnected(ComponentName name) {
- }
-
- public ICrossUserContentService getService() {
- return mService;
- }
- }
-
- private static final class TestContentObserver extends ContentObserver {
- private final CountDownLatch mLatch;
- private final Uri mExpectedUri;
- private final int mExpectedUserId;
-
- public TestContentObserver(CountDownLatch latch, Uri exptectedUri, int expectedUserId) {
- super(null);
- mLatch = latch;
- mExpectedUri = exptectedUri;
- mExpectedUserId = expectedUserId;
- }
-
- @Override
- public void onChange(boolean selfChange, Uri uri, int userId) {
- if (mExpectedUri.equals(uri) && mExpectedUserId == userId) {
- mLatch.countDown();
- }
- }
- }
-}
\ No newline at end of file
diff --git a/core/tests/coretests/src/android/content/CrossUserContentService.java b/core/tests/coretests/src/android/content/CrossUserContentService.java
deleted file mode 100644
index 9cbe549..0000000
--- a/core/tests/coretests/src/android/content/CrossUserContentService.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * 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.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package android.content;
-
-import android.app.Service;
-import android.app.activity.LocalProvider;
-import android.net.Uri;
-import android.os.IBinder;
-
-public class CrossUserContentService extends Service {
-
- @Override
- public IBinder onBind(Intent intent) {
- return mLocalService.asBinder();
- }
-
- private ICrossUserContentService mLocalService = new ICrossUserContentService.Stub() {
- @Override
- public void updateContent(Uri uri, String key, int value) {
- final ContentValues values = new ContentValues();
- values.put(LocalProvider.COLUMN_TEXT_NAME, key);
- values.put(LocalProvider.COLUMN_INTEGER_NAME, value);
- getContentResolver().update(uri, values, null, null);
- }
-
- @Override
- public void notifyForUriAsUser(Uri uri, int userId) {
- getContentResolver().notifyChange(uri, null, false, userId);
- }
- };
-}
\ No newline at end of file
diff --git a/core/tests/packagemanagertests/Android.mk b/core/tests/packagemanagertests/Android.mk
new file mode 100644
index 0000000..c1e8c98
--- /dev/null
+++ b/core/tests/packagemanagertests/Android.mk
@@ -0,0 +1,20 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+# We only want this apk build for tests.
+LOCAL_MODULE_TAGS := tests
+
+# Include all test java files.
+LOCAL_SRC_FILES := \
+ $(call all-java-files-under, src)
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ android-support-test \
+ frameworks-base-testutils
+
+LOCAL_JAVA_LIBRARIES := android.test.runner
+LOCAL_PACKAGE_NAME := FrameworksCorePackageManagerTests
+
+LOCAL_CERTIFICATE := platform
+
+include $(BUILD_PACKAGE)
diff --git a/core/tests/packagemanagertests/AndroidManifest.xml b/core/tests/packagemanagertests/AndroidManifest.xml
new file mode 100644
index 0000000..8f49008
--- /dev/null
+++ b/core/tests/packagemanagertests/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+ 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ android:installLocation="internalOnly"
+ package="com.android.frameworks.coretests.packagemanager"
+ android:sharedUserId="android.uid.system">
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:name="android.support.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.frameworks.coretests.packagemanager"
+ android:label="Frameworks PackageManager Core Tests" />
+
+</manifest>
diff --git a/core/tests/packagemanagertests/src/android/content/pm/KernelPackageMappingTests.java b/core/tests/packagemanagertests/src/android/content/pm/KernelPackageMappingTests.java
new file mode 100644
index 0000000..1097bc7
--- /dev/null
+++ b/core/tests/packagemanagertests/src/android/content/pm/KernelPackageMappingTests.java
@@ -0,0 +1,109 @@
+/*
+ * 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.
+ * 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.os.FileUtils;
+import android.os.Process;
+import android.os.ServiceManager;
+import android.os.UserManager;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * This test needs to be run without any secondary users on the device,
+ * and selinux needs to be disabled with "adb shell setenforce 0".
+ */
+@RunWith(AndroidJUnit4.class)
+public class KernelPackageMappingTests {
+
+ private static final String TAG = "KernelPackageMapping";
+ private static final String SDCARDFS_PATH = "/config/sdcardfs";
+
+ private UserInfo mSecondaryUser;
+
+ private static File getKernelPackageDir(String packageName) {
+ return new File(new File(SDCARDFS_PATH), packageName);
+ }
+
+ private static File getKernelPackageFile(String packageName, String filename) {
+ return new File(getKernelPackageDir(packageName), filename);
+ }
+
+ private UserManager getUserManager() {
+ UserManager um = (UserManager) InstrumentationRegistry.getContext().getSystemService(
+ Context.USER_SERVICE);
+ return um;
+ }
+
+ private IPackageManager getIPackageManager() {
+ IPackageManager ipm = IPackageManager.Stub.asInterface(
+ ServiceManager.getService("package"));
+ return ipm;
+ }
+
+ private static String getContent(File file) {
+ try {
+ return FileUtils.readTextFile(file, 0, null).trim();
+ } catch (IOException ioe) {
+ Log.w(TAG, "Couldn't read file " + file.getAbsolutePath() + "\n" + ioe);
+ return "<error>";
+ }
+ }
+
+ @Test
+ public void testInstalledPrimary() throws Exception {
+ assertEquals("1000", getContent(getKernelPackageFile("com.android.settings", "appid")));
+ }
+
+ @Test
+ public void testInstalledAll() throws Exception {
+ assertEquals("", getContent(getKernelPackageFile("com.android.settings",
+ "excluded_userids")));
+ }
+
+ @Test
+ public void testNotInstalledSecondary() throws Exception {
+ mSecondaryUser = getUserManager().createUser("Secondary", 0);
+ assertEquals(Integer.toString(mSecondaryUser.id),
+ getContent(
+ getKernelPackageFile("com.android.frameworks.coretests.packagemanager",
+ "excluded_userids")));
+ }
+
+ @After
+ public void shutDown() throws Exception {
+ if (mSecondaryUser != null) {
+ getUserManager().removeUser(mSecondaryUser.id);
+ }
+ }
+}
diff --git a/graphics/java/android/graphics/drawable/MaskableIconDrawable.java b/graphics/java/android/graphics/drawable/MaskableIconDrawable.java
index e4f1788a..472b229 100644
--- a/graphics/java/android/graphics/drawable/MaskableIconDrawable.java
+++ b/graphics/java/android/graphics/drawable/MaskableIconDrawable.java
@@ -427,7 +427,7 @@
}
if (type != XmlPullParser.START_TAG) {
throw new XmlPullParserException(parser.getPositionDescription()
- + ": <foreground> or <background> tag requires a 'color' or 'drawable'"
+ + ": <foreground> or <background> tag requires a 'drawable'"
+ "attribute or child tag defining a drawable");
}
@@ -451,12 +451,6 @@
layer.mThemeAttrs = a.extractThemeAttrs();
Drawable dr = a.getDrawable(R.styleable.MaskableIconDrawableLayer_drawable);
- if (dr == null) {
- int color = a.getColor(R.styleable.MaskableIconDrawableLayer_color, Color.TRANSPARENT);
- if (color != Color.TRANSPARENT) {
- dr = new ColorDrawable(color);
- }
- }
if (dr != null) {
if (layer.mDrawable != null) {
// It's possible that a drawable was already set, in which case
diff --git a/libs/hwui/DeferredLayerUpdater.cpp b/libs/hwui/DeferredLayerUpdater.cpp
index 00e8c05..ff90160 100644
--- a/libs/hwui/DeferredLayerUpdater.cpp
+++ b/libs/hwui/DeferredLayerUpdater.cpp
@@ -98,6 +98,8 @@
mUpdateTexImage = false;
doUpdateTexImage();
}
+ GLenum renderTarget = mSurfaceTexture->getCurrentTextureTarget();
+ static_cast<GlLayer*>(mLayer)->setRenderTarget(renderTarget);
}
if (mTransform) {
mLayer->getTransform().load(*mTransform);
@@ -140,12 +142,8 @@
}
#endif
mSurfaceTexture->getTransformMatrix(transform);
- GLenum renderTarget = mSurfaceTexture->getCurrentTextureTarget();
- LOG_ALWAYS_FATAL_IF(renderTarget != GL_TEXTURE_2D && renderTarget != GL_TEXTURE_EXTERNAL_OES,
- "doUpdateTexImage target %x, 2d %x, EXT %x",
- renderTarget, GL_TEXTURE_2D, GL_TEXTURE_EXTERNAL_OES);
- updateLayer(forceFilter, renderTarget, transform);
+ updateLayer(forceFilter, transform);
}
}
@@ -155,28 +153,17 @@
mLayer->getApi(), Layer::Api::OpenGL, Layer::Api::Vulkan);
static const mat4 identityMatrix;
- updateLayer(false, GL_NONE, identityMatrix.data);
+ updateLayer(false, identityMatrix.data);
VkLayer* vkLayer = static_cast<VkLayer*>(mLayer);
vkLayer->updateTexture();
}
-void DeferredLayerUpdater::updateLayer(bool forceFilter, GLenum renderTarget,
- const float* textureTransform) {
+void DeferredLayerUpdater::updateLayer(bool forceFilter, const float* textureTransform) {
mLayer->setBlend(mBlend);
mLayer->setForceFilter(forceFilter);
mLayer->setSize(mWidth, mHeight);
mLayer->getTexTransform().load(textureTransform);
-
- if (mLayer->getApi() == Layer::Api::OpenGL) {
- GlLayer* glLayer = static_cast<GlLayer*>(mLayer);
- if (renderTarget != glLayer->getRenderTarget()) {
- glLayer->setRenderTarget(renderTarget);
- glLayer->bindTexture();
- glLayer->setFilter(GL_NEAREST, false, true);
- glLayer->setWrap(GL_CLAMP_TO_EDGE, false, true);
- }
- }
}
void DeferredLayerUpdater::detachSurfaceTexture() {
diff --git a/libs/hwui/DeferredLayerUpdater.h b/libs/hwui/DeferredLayerUpdater.h
index 6717361..6164e47 100644
--- a/libs/hwui/DeferredLayerUpdater.h
+++ b/libs/hwui/DeferredLayerUpdater.h
@@ -101,7 +101,7 @@
void detachSurfaceTexture();
- void updateLayer(bool forceFilter, GLenum renderTarget, const float* textureTransform);
+ void updateLayer(bool forceFilter, const float* textureTransform);
void destroyLayer();
diff --git a/libs/hwui/GlLayer.cpp b/libs/hwui/GlLayer.cpp
index aacad54..070e954 100644
--- a/libs/hwui/GlLayer.cpp
+++ b/libs/hwui/GlLayer.cpp
@@ -55,9 +55,15 @@
texture.deleteTexture();
}
-void GlLayer::bindTexture() const {
- if (texture.mId) {
- caches.textureState().bindTexture(texture.target(), texture.mId);
+void GlLayer::setRenderTarget(GLenum renderTarget) {
+ if (renderTarget != getRenderTarget()) {
+ // new render target: bind with new target, and update filter/wrap
+ texture.mTarget = renderTarget;
+ if (texture.mId) {
+ caches.textureState().bindTexture(texture.target(), texture.mId);
+ }
+ texture.setFilter(GL_NEAREST, false, true);
+ texture.setWrap(GL_CLAMP_TO_EDGE, false, true);
}
}
diff --git a/libs/hwui/GlLayer.h b/libs/hwui/GlLayer.h
index 85ddaff..20aaf4a 100644
--- a/libs/hwui/GlLayer.h
+++ b/libs/hwui/GlLayer.h
@@ -68,23 +68,12 @@
return texture.target();
}
- inline void setRenderTarget(GLenum renderTarget) {
- texture.mTarget = renderTarget;
- }
-
inline bool isRenderable() const {
return texture.target() != GL_NONE;
}
- void setWrap(GLenum wrap, bool bindTexture = false, bool force = false) {
- texture.setWrap(wrap, bindTexture, force);
- }
+ void setRenderTarget(GLenum renderTarget);
- void setFilter(GLenum filter, bool bindTexture = false, bool force = false) {
- texture.setFilter(filter, bindTexture, force);
- }
-
- void bindTexture() const;
void generateTexture();
/**
diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp
index de80ee3..f2b0eb3 100644
--- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp
+++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp
@@ -128,6 +128,8 @@
return false;
}
+ // acquire most recent buffer for drawing
+ deferredLayer->updateTexImage();
deferredLayer->apply();
SkCanvas canvas(*bitmap);
diff --git a/libs/hwui/renderthread/OpenGLPipeline.cpp b/libs/hwui/renderthread/OpenGLPipeline.cpp
index 8a5d9cc..acd6110 100644
--- a/libs/hwui/renderthread/OpenGLPipeline.cpp
+++ b/libs/hwui/renderthread/OpenGLPipeline.cpp
@@ -120,6 +120,8 @@
bool OpenGLPipeline::copyLayerInto(DeferredLayerUpdater* layer, SkBitmap* bitmap) {
ATRACE_CALL();
+ // acquire most recent buffer for drawing
+ layer->updateTexImage();
layer->apply();
return OpenGLReadbackImpl::copyLayerInto(mRenderThread,
static_cast<GlLayer&>(*layer->backingLayer()), bitmap);
diff --git a/libs/hwui/tests/common/TestUtils.cpp b/libs/hwui/tests/common/TestUtils.cpp
index 3e52c39..64ec58d 100644
--- a/libs/hwui/tests/common/TestUtils.cpp
+++ b/libs/hwui/tests/common/TestUtils.cpp
@@ -74,7 +74,11 @@
layerUpdater->setTransform(&transform);
// updateLayer so it's ready to draw
- layerUpdater->updateLayer(true, GL_TEXTURE_EXTERNAL_OES, Matrix4::identity().data);
+ layerUpdater->updateLayer(true, Matrix4::identity().data);
+ if (layerUpdater->backingLayer()->getApi() == Layer::Api::OpenGL) {
+ static_cast<GlLayer*>(layerUpdater->backingLayer())->setRenderTarget(
+ GL_TEXTURE_EXTERNAL_OES);
+ }
return layerUpdater;
}
diff --git a/libs/hwui/tests/unit/DeferredLayerUpdaterTests.cpp b/libs/hwui/tests/unit/DeferredLayerUpdaterTests.cpp
index 1ef9dba..87d897e 100644
--- a/libs/hwui/tests/unit/DeferredLayerUpdaterTests.cpp
+++ b/libs/hwui/tests/unit/DeferredLayerUpdaterTests.cpp
@@ -44,7 +44,12 @@
// push the deferred updates to the layer
Matrix4 scaledMatrix;
scaledMatrix.loadScale(0.5, 0.5, 0.0);
- layerUpdater->updateLayer(true, GL_TEXTURE_EXTERNAL_OES, scaledMatrix.data);
+ layerUpdater->updateLayer(true, scaledMatrix.data);
+ if (layerUpdater->backingLayer()->getApi() == Layer::Api::OpenGL) {
+ GlLayer* glLayer = static_cast<GlLayer*>(layerUpdater->backingLayer());
+ glLayer->setRenderTarget(GL_TEXTURE_EXTERNAL_OES);
+ }
+
// the backing layer should now have all the properties applied.
if (layerUpdater->backingLayer()->getApi() == Layer::Api::OpenGL) {
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index fb3f5b3..a4f2a7e 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -1456,10 +1456,11 @@
}
/**
- * Checks whether A2DP audio routing to the Bluetooth headset is on or off.
+ * Checks whether a Bluetooth A2DP audio peripheral is connected or not.
*
- * @return true if A2DP audio is being routed to/from Bluetooth headset;
+ * @return true if a Bluetooth A2DP peripheral is connected
* false if otherwise
+ * @deprecated Use {@link AudioManager#getDevices(int)} instead to list available audio devices.
*/
public boolean isBluetoothA2dpOn() {
if (AudioSystem.getDeviceConnectionState(DEVICE_OUT_BLUETOOTH_A2DP,"")
@@ -1492,7 +1493,7 @@
*
* @return true if a wired headset is connected.
* false if otherwise
- * @deprecated Use only to check is a headset is connected or not.
+ * @deprecated Use {@link AudioManager#getDevices(int)} instead to list available audio devices.
*/
public boolean isWiredHeadsetOn() {
if (AudioSystem.getDeviceConnectionState(DEVICE_OUT_WIRED_HEADSET,"")
diff --git a/media/java/android/media/MediaHTTPConnection.java b/media/java/android/media/MediaHTTPConnection.java
index d6bf421..228a6de 100644
--- a/media/java/android/media/MediaHTTPConnection.java
+++ b/media/java/android/media/MediaHTTPConnection.java
@@ -61,8 +61,9 @@
private final static int MAX_REDIRECTS = 20;
public MediaHTTPConnection() {
- if (CookieHandler.getDefault() == null) {
- CookieHandler.setDefault(new CookieManager());
+ CookieManager cookieManager = (CookieManager)CookieHandler.getDefault();
+ if (cookieManager == null) {
+ Log.w(TAG, "MediaHTTPConnection: Unexpected. No CookieManager found.");
}
native_setup();
diff --git a/media/java/android/media/MediaHTTPService.java b/media/java/android/media/MediaHTTPService.java
index 52a68bf..b678630 100644
--- a/media/java/android/media/MediaHTTPService.java
+++ b/media/java/android/media/MediaHTTPService.java
@@ -19,25 +19,78 @@
import android.os.IBinder;
import android.util.Log;
+import java.net.CookieHandler;
+import java.net.CookieManager;
+import java.net.CookieStore;
+import java.net.HttpCookie;
+import java.util.List;
+
/** @hide */
public class MediaHTTPService extends IMediaHTTPService.Stub {
private static final String TAG = "MediaHTTPService";
+ private List<HttpCookie> mCookies;
+ private Boolean mCookieStoreInitialized = new Boolean(false);
- public MediaHTTPService() {
+ public MediaHTTPService(List<HttpCookie> cookies) {
+ mCookies = cookies;
+ Log.v(TAG, "MediaHTTPService(" + this + "): Cookies: " + cookies);
}
public IMediaHTTPConnection makeHTTPConnection() {
+
+ synchronized (mCookieStoreInitialized) {
+ // Only need to do it once for all connections
+ if ( !mCookieStoreInitialized ) {
+ CookieManager cookieManager = (CookieManager)CookieHandler.getDefault();
+ if (cookieManager == null) {
+ cookieManager = new CookieManager();
+ CookieHandler.setDefault(cookieManager);
+ Log.v(TAG, "makeHTTPConnection: CookieManager created: " + cookieManager);
+ }
+ else {
+ Log.v(TAG, "makeHTTPConnection: CookieManager(" + cookieManager + ") exists.");
+ }
+
+ // Applying the bootstrapping cookies
+ if ( mCookies != null ) {
+ CookieStore store = cookieManager.getCookieStore();
+ for ( HttpCookie cookie : mCookies ) {
+ try {
+ store.add(null, cookie);
+ } catch ( Exception e ) {
+ Log.v(TAG, "makeHTTPConnection: CookieStore.add" + e);
+ }
+ //for extended debugging when needed
+ //Log.v(TAG, "MediaHTTPConnection adding Cookie[" + cookie.getName() +
+ // "]: " + cookie);
+ }
+ } // mCookies
+
+ mCookieStoreInitialized = true;
+
+ Log.v(TAG, "makeHTTPConnection(" + this + "): cookieManager: " + cookieManager +
+ " Cookies: " + mCookies);
+ } // mCookieStoreInitialized
+ } // synchronized
+
return new MediaHTTPConnection();
}
/* package private */static IBinder createHttpServiceBinderIfNecessary(
String path) {
+ return createHttpServiceBinderIfNecessary(path, null);
+ }
+
+ // when cookies are provided
+ static IBinder createHttpServiceBinderIfNecessary(
+ String path, List<HttpCookie> cookies) {
if (path.startsWith("http://") || path.startsWith("https://")) {
- return (new MediaHTTPService()).asBinder();
+ return (new MediaHTTPService(cookies)).asBinder();
} else if (path.startsWith("widevine://")) {
Log.d(TAG, "Widevine classic is no longer supported");
}
return null;
}
+
}
diff --git a/media/java/android/media/MediaPlayer.java b/media/java/android/media/MediaPlayer.java
index c5a47ec..85c3c1c 100644
--- a/media/java/android/media/MediaPlayer.java
+++ b/media/java/android/media/MediaPlayer.java
@@ -73,6 +73,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
+import java.net.HttpCookie;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.URL;
@@ -80,6 +81,7 @@
import java.util.Arrays;
import java.util.BitSet;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
@@ -998,7 +1000,7 @@
*/
public void setDataSource(@NonNull Context context, @NonNull Uri uri)
throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {
- setDataSource(context, uri, null);
+ setDataSource(context, uri, null, null);
}
/**
@@ -1011,11 +1013,13 @@
* changed with key/value pairs through the headers parameter with
* "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value
* to disallow or allow cross domain redirection.
+ * The headers must not include cookies. Instead, use the cookies param.
+ * @param cookies the cookies to be sent together with the request
* @throws IllegalStateException if it is called in an invalid state
*/
public void setDataSource(@NonNull Context context, @NonNull Uri uri,
- @Nullable Map<String, String> headers) throws IOException, IllegalArgumentException,
- SecurityException, IllegalStateException {
+ @Nullable Map<String, String> headers, @Nullable List<HttpCookie> cookies)
+ throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {
// The context and URI usually belong to the calling user. Get a resolver for that user
// and strip out the userId from the URI if present.
final ContentResolver resolver = context.getContentResolver();
@@ -1036,18 +1040,36 @@
} else if (attemptDataSource(resolver, actualUri)) {
return;
} else {
- setDataSource(uri.toString(), headers);
+ setDataSource(uri.toString(), headers, cookies);
}
} else {
// Try requested Uri locally first, or fallback to media server
if (attemptDataSource(resolver, uri)) {
return;
} else {
- setDataSource(uri.toString(), headers);
+ setDataSource(uri.toString(), headers, cookies);
}
}
}
+ /**
+ * Sets the data source as a content Uri.
+ *
+ * @param context the Context to use when resolving the Uri
+ * @param uri the Content URI of the data you want to play
+ * @param headers the headers to be sent together with the request for the data
+ * Note that the cross domain redirection is allowed by default, but that can be
+ * changed with key/value pairs through the headers parameter with
+ * "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value
+ * to disallow or allow cross domain redirection.
+ * @throws IllegalStateException if it is called in an invalid state
+ */
+ public void setDataSource(@NonNull Context context, @NonNull Uri uri,
+ @Nullable Map<String, String> headers)
+ throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {
+ setDataSource(context, uri, headers, null);
+ }
+
private boolean attemptDataSource(ContentResolver resolver, Uri uri) {
try (AssetFileDescriptor afd = resolver.openAssetFileDescriptor(uri, "r")) {
setDataSource(afd);
@@ -1085,6 +1107,11 @@
* @hide pending API council
*/
public void setDataSource(String path, Map<String, String> headers)
+ throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {
+ setDataSource(path, headers, null);
+ }
+
+ private void setDataSource(String path, Map<String, String> headers, List<HttpCookie> cookies)
throws IOException, IllegalArgumentException, SecurityException, IllegalStateException
{
String[] keys = null;
@@ -1101,10 +1128,11 @@
++i;
}
}
- setDataSource(path, keys, values);
+ setDataSource(path, keys, values, cookies);
}
- private void setDataSource(String path, String[] keys, String[] values)
+ private void setDataSource(String path, String[] keys, String[] values,
+ List<HttpCookie> cookies)
throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {
final Uri uri = Uri.parse(path);
final String scheme = uri.getScheme();
@@ -1113,7 +1141,7 @@
} else if (scheme != null) {
// handle non-file sources
nativeSetDataSource(
- MediaHTTPService.createHttpServiceBinderIfNecessary(path),
+ MediaHTTPService.createHttpServiceBinderIfNecessary(path, cookies),
path,
keys,
values);
diff --git a/media/java/android/media/tv/TvContract.java b/media/java/android/media/tv/TvContract.java
index ddbd542e..45f88f3 100644
--- a/media/java/android/media/tv/TvContract.java
+++ b/media/java/android/media/tv/TvContract.java
@@ -1977,6 +1977,24 @@
public static final String COLUMN_RECORDING_PROHIBITED = "recording_prohibited";
/**
+ * The flag indicating whether this TV program is browsable or not.
+ *
+ * <p>This column can only be set by system apps. For other applications, it is a read-only
+ * column. Trying to modify it may cause {@link SecurityException}.
+ *
+ * <p>A value of 1 indicates that the program is browsable and can be shown to users in
+ * the UI. A value of 0 indicates that the program should be hidden from users and the
+ * application who changes this value to 0 should send
+ * {@link TvInputManager#ACTION_PROGRAM_BROWSABLE_DISABLED} to the owner of the program
+ * to notify this change.
+ *
+ * <p>This value is set to 1 (browsable) by default.
+ *
+ * <p>Type: INTEGER (boolean)
+ */
+ public static final String COLUMN_BROWSABLE = "browsable";
+
+ /**
* The internal ID used by individual TV input services.
*
* <p>This is internal to the provider that inserted it, and should not be decoded by other
diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java
index b630270..4c2b031 100644
--- a/media/java/android/media/tv/TvInputManager.java
+++ b/media/java/android/media/tv/TvInputManager.java
@@ -325,23 +325,39 @@
"android.media.tv.action.VIEW_RECORDING_SCHEDULES";
/**
+ * Action sent by the system to tell the target TV input that one of its program's browsable
+ * state is disabled, i.e., it will no longer be shown to users, which, for example, might
+ * be a result of users' interaction with UI.
+ *
+ * <p>The intent must contain the following bundle parameter:
+ * <ul>
+ * <li>{@link #EXTRA_PROGRAM_ID} the program ID as a long integer.
+ * </ul>
+ */
+ public static final String ACTION_PROGRAM_BROWSABLE_DISABLED =
+ "android.media.tv.action.PROGRAM_BROWSABLE_DISABLED";
+
+ /**
* Action sent by an application telling the system to set the given channel as browsable.
*
* <p>The intent must contain the following bundle parameters:
* <ul>
- * <li>{@link #EXTRA_CHANNEL_ID} then channel ID as an integer.
+ * <li>{@link #EXTRA_CHANNEL_ID} the channel ID as a long integer.
* <li>{@link #EXTRA_PACKAGE_NAME} the package name of the requesting application.
* </ul>
*/
public static final String ACTION_MAKE_CHANNEL_BROWSABLE
= "android.media.tv.action.MAKE_CHANNEL_BROWSABLE";
- /** The key for a bundle parameter containing a channel ID as an integer */
+ /** The key for a bundle parameter containing a channel ID as a long integer */
public static final String EXTRA_CHANNEL_ID = "android.media.tv.extra.CHANNEL_ID";
/** The key for a bundle parameter containing a package name as a string. */
public static final String EXTRA_PACKAGE_NAME = "android.media.tv.extra.PACKAGE_NAME";
+ /** The key for a bundle parameter containing a program ID as a long integer */
+ public static final String EXTRA_PROGRAM_ID = "android.media.tv.extra.PROGRAM_ID";
+
private final ITvInputManager mService;
private final Object mLock = new Object();
diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java
index 11e2a71..8653523 100644
--- a/packages/SettingsLib/src/com/android/settingslib/Utils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java
@@ -215,6 +215,13 @@
return colorAccent;
}
+ public static Drawable getDrawable(Context context, int attr) {
+ TypedArray ta = context.obtainStyledAttributes(new int[]{attr});
+ Drawable drawable = ta.getDrawable(0);
+ ta.recycle();
+ return drawable;
+ }
+
/**
* Determine whether a package is a "system package", in which case certain things (like
* disabling notifications or disabling the package altogether) should be disallowed.
diff --git a/packages/SystemUI/plugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/SampleOverlayPlugin.java b/packages/SystemUI/plugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/SampleOverlayPlugin.java
index 13fc76c..79a0c35 100644
--- a/packages/SystemUI/plugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/SampleOverlayPlugin.java
+++ b/packages/SystemUI/plugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/SampleOverlayPlugin.java
@@ -24,7 +24,9 @@
import android.view.ViewTreeObserver.InternalInsetsInfo;
import android.view.ViewTreeObserver.OnComputeInternalInsetsListener;
import com.android.systemui.plugins.OverlayPlugin;
+import com.android.systemui.plugins.annotations.Requires;
+@Requires(target = OverlayPlugin.class, version = OverlayPlugin.VERSION)
public class SampleOverlayPlugin implements OverlayPlugin {
private static final String TAG = "SampleOverlayPlugin";
private Context mPluginContext;
@@ -36,12 +38,6 @@
private float mStatusBarHeight;
@Override
- public int getVersion() {
- Log.d(TAG, "getVersion " + VERSION);
- return VERSION;
- }
-
- @Override
public void onCreate(Context sysuiContext, Context pluginContext) {
Log.d(TAG, "onCreate");
mPluginContext = pluginContext;
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/IntentButtonProvider.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/IntentButtonProvider.java
index 9c173bd..97dbafd 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/IntentButtonProvider.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/IntentButtonProvider.java
@@ -14,6 +14,8 @@
package com.android.systemui.plugins;
+import com.android.systemui.plugins.annotations.ProvidesInterface;
+
import android.content.Intent;
import android.graphics.drawable.Drawable;
@@ -21,6 +23,7 @@
* An Intent Button represents a triggerable element in SysUI that consists of an
* Icon and an intent to trigger when it is activated (clicked, swiped, etc.).
*/
+@ProvidesInterface(version = IntentButtonProvider.VERSION)
public interface IntentButtonProvider extends Plugin {
public static final int VERSION = 1;
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/OverlayPlugin.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/OverlayPlugin.java
index f5074f7..61aa60b 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/OverlayPlugin.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/OverlayPlugin.java
@@ -13,12 +13,15 @@
*/
package com.android.systemui.plugins;
+import com.android.systemui.plugins.annotations.ProvidesInterface;
+
import android.view.View;
+@ProvidesInterface(action = OverlayPlugin.ACTION, version = OverlayPlugin.VERSION)
public interface OverlayPlugin extends Plugin {
String ACTION = "com.android.systemui.action.PLUGIN_OVERLAY";
- int VERSION = 1;
+ int VERSION = 2;
void setup(View statusBar, View navBar);
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/Plugin.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/Plugin.java
index e75ecb7..bb93367 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/Plugin.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/Plugin.java
@@ -13,6 +13,8 @@
*/
package com.android.systemui.plugins;
+import com.android.systemui.plugins.annotations.Requires;
+
import android.content.Context;
/**
@@ -111,18 +113,13 @@
public interface Plugin {
/**
- * Should be implemented as the following directly referencing the version constant
- * from the plugin interface being implemented, this will allow recompiles to automatically
- * pick up the current version.
- * <pre class="prettyprint">
- * {@literal
- * public int getVersion() {
- * return VERSION;
- * }
- * }
- * @return
+ * @deprecated
+ * @see Requires
*/
- int getVersion();
+ default int getVersion() {
+ // Default of -1 indicates the plugin supports the new Requires model.
+ return -1;
+ }
default void onCreate(Context sysuiContext, Context pluginContext) {
}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/Dependencies.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/Dependencies.java
new file mode 100644
index 0000000..dbbf047
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/Dependencies.java
@@ -0,0 +1,27 @@
+/*
+ * 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. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.plugins.annotations;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Used for repeated @DependsOn internally, not for plugin
+ * use.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Dependencies {
+ DependsOn[] value();
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/DependsOn.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/DependsOn.java
new file mode 100644
index 0000000..b81d673
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/DependsOn.java
@@ -0,0 +1,32 @@
+/*
+ * 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. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.plugins.annotations;
+
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Used to indicate that an interface in the plugin library needs another
+ * interface to function properly. When this is added, it will be enforced
+ * that all plugins that @Requires the annotated interface also @Requires
+ * the specified class as well.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Repeatable(value = Dependencies.class)
+public @interface DependsOn {
+ Class<?> target();
+
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/ProvidesInterface.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/ProvidesInterface.java
new file mode 100644
index 0000000..d0e14b8
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/ProvidesInterface.java
@@ -0,0 +1,30 @@
+/*
+ * 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. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.plugins.annotations;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Should be added to all interfaces in plugin lib to specify their
+ * current version and optionally their action to implement the plugin.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ProvidesInterface {
+ int version();
+
+ String action() default "";
+
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/Requirements.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/Requirements.java
new file mode 100644
index 0000000..9cfa279
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/Requirements.java
@@ -0,0 +1,27 @@
+/*
+ * 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. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.plugins.annotations;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Used for repeated @Requires internally, not for plugin
+ * use.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Requirements {
+ Requires[] value();
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/Requires.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/Requires.java
new file mode 100644
index 0000000..e1b1303
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/Requires.java
@@ -0,0 +1,33 @@
+/*
+ * 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. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.plugins.annotations;
+
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Used to annotate which interfaces a given plugin depends on.
+ *
+ * At minimum all plugins should have at least one @Requires annotation
+ * for the plugin interface that they are implementing. They will also
+ * need an @Requires for each class that the plugin interface @DependsOn.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Repeatable(value = Requirements.class)
+public @interface Requires {
+ Class<?> target();
+ int version();
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/doze/DozeProvider.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/doze/DozeProvider.java
index 688df46..0688481 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/doze/DozeProvider.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/doze/DozeProvider.java
@@ -20,10 +20,12 @@
import android.content.Context;
import com.android.systemui.plugins.Plugin;
+import com.android.systemui.plugins.annotations.ProvidesInterface;
/**
* Provides a {@link DozeUi}.
*/
+@ProvidesInterface(action = DozeProvider.ACTION, version = DozeProvider.VERSION)
public interface DozeProvider extends Plugin {
String ACTION = "com.android.systemui.action.PLUGIN_DOZE";
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java
index e21a282..b7467eb 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java
@@ -14,29 +14,32 @@
package com.android.systemui.plugins.qs;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.app.PendingIntent;
+import com.android.systemui.plugins.FragmentBase;
+import com.android.systemui.plugins.annotations.DependsOn;
+import com.android.systemui.plugins.annotations.ProvidesInterface;
+import com.android.systemui.plugins.qs.QS.Callback;
+import com.android.systemui.plugins.qs.QS.DetailAdapter;
+import com.android.systemui.plugins.qs.QS.HeightListener;
+
import android.content.Context;
import android.content.Intent;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.FrameLayout;
import android.widget.RelativeLayout;
-import com.android.systemui.plugins.FragmentBase;
-
/**
* Fragment that contains QS in the notification shade. Most of the interface is for
* handling the expand/collapsing of the view interaction.
*/
+@ProvidesInterface(action = QS.ACTION, version = QS.VERSION)
+@DependsOn(target = HeightListener.class)
+@DependsOn(target = Callback.class)
+@DependsOn(target = DetailAdapter.class)
public interface QS extends FragmentBase {
public static final String ACTION = "com.android.systemui.action.PLUGIN_QS";
- // This should be incremented any time this class or ActivityStarter or BaseStatusBarHeader
- // change in incompatible ways.
public static final int VERSION = 5;
String TAG = "QS";
@@ -64,17 +67,23 @@
public abstract void setContainer(ViewGroup container);
+ @ProvidesInterface(version = HeightListener.VERSION)
public interface HeightListener {
+ public static final int VERSION = 1;
void onQsHeightChanged();
}
+ @ProvidesInterface(version = Callback.VERSION)
public interface Callback {
+ public static final int VERSION = 1;
void onShowingDetail(DetailAdapter detail, int x, int y);
void onToggleStateChanged(boolean state);
void onScanStateChanged(boolean state);
}
+ @ProvidesInterface(version = DetailAdapter.VERSION)
public interface DetailAdapter {
+ public static final int VERSION = 1;
CharSequence getTitle();
Boolean getToggleState();
default boolean getToggleEnabled() {
@@ -92,7 +101,9 @@
default boolean hasHeader() { return true; }
}
+ @ProvidesInterface(version = BaseStatusBarHeader.VERSION)
public abstract static class BaseStatusBarHeader extends RelativeLayout {
+ public static final int VERSION = 1;
public BaseStatusBarHeader(Context context, AttributeSet attrs) {
super(context, attrs);
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowProvider.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowProvider.java
index 41a0907..bc98c8e 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowProvider.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowProvider.java
@@ -10,7 +10,10 @@
import java.util.ArrayList;
import com.android.systemui.plugins.Plugin;
+import com.android.systemui.plugins.annotations.ProvidesInterface;
+@ProvidesInterface(action = NotificationMenuRowProvider.ACTION,
+ version = NotificationMenuRowProvider.VERSION)
public interface NotificationMenuRowProvider extends Plugin {
public static final String ACTION = "com.android.systemui.action.PLUGIN_NOTIFICATION_MENU_ROW";
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavBarButtonProvider.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavBarButtonProvider.java
index d54e33f..5243228 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavBarButtonProvider.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavBarButtonProvider.java
@@ -14,14 +14,15 @@
package com.android.systemui.plugins.statusbar.phone;
-import android.annotation.DrawableRes;
import android.annotation.Nullable;
import android.graphics.drawable.Drawable;
import android.view.View;
import android.view.ViewGroup;
import com.android.systemui.plugins.Plugin;
+import com.android.systemui.plugins.annotations.ProvidesInterface;
+@ProvidesInterface(action = NavBarButtonProvider.ACTION, version = NavBarButtonProvider.VERSION)
public interface NavBarButtonProvider extends Plugin {
public static final String ACTION = "com.android.systemui.action.PLUGIN_NAV_BUTTON";
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavGesture.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavGesture.java
index 918d6e9..ddee89e 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavGesture.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavGesture.java
@@ -17,7 +17,9 @@
import android.view.MotionEvent;
import com.android.systemui.plugins.Plugin;
+import com.android.systemui.plugins.annotations.ProvidesInterface;
+@ProvidesInterface(action = NavGesture.ACTION, version = NavBarButtonProvider.VERSION)
public interface NavGesture extends Plugin {
public static final String ACTION = "com.android.systemui.action.PLUGIN_NAV_GESTURE";
diff --git a/packages/SystemUI/res/drawable/ic_remove_circle.xml b/packages/SystemUI/res/drawable/ic_remove_circle.xml
new file mode 100644
index 0000000..439cc78
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_remove_circle.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="48dp"
+ android:width="48dp"
+ android:tint="#db4437"
+ android:viewportHeight="48"
+ android:viewportWidth="48" >
+ <path android:fillColor="@android:color/white"
+ android:pathData="M24,4C12.95,4,4,12.95,4,24
+ s8.95,20,20,20,20-8.95,20-20
+ S35.05,4,24,4zm10,22H14v-4h20v4z"/>
+</vector>
diff --git a/packages/SystemUI/res/layout/preference_widget_radiobutton.xml b/packages/SystemUI/res/layout/preference_widget_radiobutton.xml
new file mode 100644
index 0000000..b3ec43d
--- /dev/null
+++ b/packages/SystemUI/res/layout/preference_widget_radiobutton.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2006 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- Layout used by CheckBoxPreference for the checkbox style. This is inflated
+ inside android.R.layout.preference. -->
+<RadioButton xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@android:id/checkbox"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:focusable="false"
+ android:clickable="false" />
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 67def4f..77de9a2 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1527,14 +1527,12 @@
<!-- SysUI Tuner: Button that controls layout of navigation bar [CHAR LIMIT=60] -->
<string name="nav_bar_layout">Layout</string>
- <!-- SysUI Tuner: Label for section of settings about the left nav button [CHAR LIMIT=60] -->
- <string name="nav_bar_left">Left</string>
-
- <!-- SysUI Tuner: Label for section of settings about the right nav button [CHAR LIMIT=60] -->
- <string name="nav_bar_right">Right</string>
+ <!-- SysUI Tuner: Setting for button type in nav bar [CHAR LIMIT=60] -->
+ <string name="left_nav_bar_button_type">Extra left button type</string>
<!-- SysUI Tuner: Setting for button type in nav bar [CHAR LIMIT=60] -->
- <string name="nav_bar_button_type">Button type</string>
+ <string name="right_nav_bar_button_type">Extra right button type</string>
+
<!-- SysUI Tuner: Added to nav bar option to indicate it is the default [CHAR LIMIT=60] -->
<string name="nav_bar_default"> (default)</string>
@@ -1543,7 +1541,7 @@
<string-array name="nav_bar_buttons">
<item>Clipboard</item>
<item>Keycode</item>
- <item>Menu / Keyboard Switcher</item>
+ <item>Keyboard switcher</item>
<item>None</item>
</string-array>
<string-array name="nav_bar_button_values" translatable="false">
@@ -1555,10 +1553,10 @@
<!-- SysUI Tuner: Labels for different types of navigation bar layouts [CHAR LIMIT=60] -->
<string-array name="nav_bar_layouts">
- <item>Divided (default)</item>
- <item>Centered</item>
- <item>Left-aligned</item>
- <item>Right-aligned</item>
+ <item>Normal</item>
+ <item>Compact</item>
+ <item>Left-leaning</item>
+ <item>Right-leaning</item>
</string-array>
<string-array name="nav_bar_layouts_values" translatable="false">
@@ -1569,7 +1567,7 @@
</string-array>
<!-- SysUI Tuner: Name of Combination Menu / Keyboard Switcher button [CHAR LIMIT=30] -->
- <string name="menu_ime">Menu / Keyboard Switcher</string>
+ <string name="menu_ime">Keyboard switcher</string>
<!-- SysUI Tuner: Save the current settings [CHAR LIMIT=30] -->
<string name="save">Save</string>
<!-- SysUI Tuner: Reset to default settings [CHAR LIMIT=30] -->
@@ -1585,10 +1583,16 @@
<string name="accessibility_key">Custom navigation button</string>
<!-- SysUI Tuner: Nav bar button that emulates a keycode [CHAR LIMIT=30] -->
- <string name="keycode">Keycode</string>
+ <string name="left_keycode">Left keycode</string>
+
+ <!-- SysUI Tuner: Nav bar button that emulates a keycode [CHAR LIMIT=30] -->
+ <string name="right_keycode">Right keycode</string>
<!-- SysUI Tuner: Settings to change nav bar icon [CHAR LIMIT=30] -->
- <string name="icon">Icon</string>
+ <string name="left_icon">Left icon</string>
+
+ <!-- SysUI Tuner: Settings to change nav bar icon [CHAR LIMIT=30] -->
+ <string name="right_icon">Right icon</string>
<!-- Label for area where tiles can be dragged out of [CHAR LIMIT=60] -->
<string name="drag_to_add_tiles">Drag to add tiles</string>
@@ -1725,6 +1729,9 @@
not appear on production builds ever. -->
<string name="tuner_doze_always_on" translatable="false">Always on</string>
+ <!-- SysUI Tuner: Section to customize lockscreen shortcuts [CHAR LIMIT=60] -->
+ <string name="tuner_lock_screen">Lock screen</string>
+
<!-- Making the PIP fullscreen [CHAR LIMIT=25] -->
<string name="pip_phone_expand">Expand</string>
@@ -1760,20 +1767,47 @@
<!-- Text body for dialog alerting user that their phone has reached a certain temperature and may start to slow down in order to cool down. [CHAR LIMIT=300] -->
<string name="high_temp_dialog_message">Your phone will automatically try to cool down. You can still use your phone, but it may run slower.\n\nOnce your phone has cooled down, it will run normally.</string>
- <!-- SysUI Tuner: Group of settings for left lock screen affordance [CHAR LIMIT=60] -->
- <string name="lockscreen_left">Left</string>
-
- <!-- SysUI Tuner: Group of settings for right lock screen affordance [CHAR LIMIT=60] -->
- <string name="lockscreen_right">Right</string>
-
- <!-- SysUI Tuner: Switch controlling whether to customize lock screen button [CHAR LIMIT=60] -->
- <string name="lockscreen_customize">Customize shortcut</string>
+ <!-- SysUI Tuner: Button to select lock screen shortcut [CHAR LIMIT=60] -->
+ <string name="lockscreen_shortcut_left">Left shortcut</string>
<!-- SysUI Tuner: Button to select lock screen shortcut [CHAR LIMIT=60] -->
- <string name="lockscreen_shortcut">Shortcut</string>
+ <string name="lockscreen_shortcut_right">Right shortcut</string>
- <!-- SysUI Tuner: Switch to control if device gets unlocked [CHAR LIMIT=60] -->
- <string name="lockscreen_unlock">Prompt for password</string>
+ <!-- SysUI Tuner: Switch to control if device gets unlocked by left shortcut [CHAR LIMIT=60] -->
+ <string name="lockscreen_unlock_left">Left shortcut also unlocks</string>
+
+ <!-- SysUI Tuner: Switch to control if device gets unlocked by right shortcut [CHAR LIMIT=60] -->
+ <string name="lockscreen_unlock_right">Right shortcut also unlocks</string>
+
+ <!-- SysUI Tuner: Summary of no shortcut being selected [CHAR LIMIT=60] -->
+ <string name="lockscreen_none">None</string>
+
+ <!-- SysUI Tuner: Format string for describing launching an app [CHAR LIMIT=60] -->
+ <string name="tuner_launch_app">Launch <xliff:g id="app" example="Settings">%1$s</xliff:g></string>
+
+ <!-- SysUI Tuner: Label for section of other apps that can be launched [CHAR LIMIT=60] -->
+ <string name="tuner_other_apps">Other apps</string>
+
+ <!-- SysUI Tuner: Label for icon shaped like a circle [CHAR LIMIT=60] -->
+ <string name="tuner_circle">Circle</string>
+
+ <!-- SysUI Tuner: Label for icon shaped like a plus [CHAR LIMIT=60] -->
+ <string name="tuner_plus">Plus</string>
+
+ <!-- SysUI Tuner: Label for icon shaped like a minus [CHAR LIMIT=60] -->
+ <string name="tuner_minus">Minus</string>
+
+ <!-- SysUI Tuner: Label for icon shaped like a left [CHAR LIMIT=60] -->
+ <string name="tuner_left">Left</string>
+
+ <!-- SysUI Tuner: Label for icon shaped like a right [CHAR LIMIT=60] -->
+ <string name="tuner_right">Right</string>
+
+ <!-- SysUI Tuner: Label for icon shaped like a menu [CHAR LIMIT=60] -->
+ <string name="tuner_menu">Menu</string>
+
+ <!-- SysUI Tuner: App subheading for shortcut selection [CHAR LIMIT=60] -->
+ <string name="tuner_app"><xliff:g id="app">%1$s</xliff:g> app</string>
<!-- Title for the notification channel containing important alerts like low battery. [CHAR LIMIT=NONE] -->
<string name="notification_channel_alerts">Alerts</string>
diff --git a/packages/SystemUI/res/xml/lockscreen_settings.xml b/packages/SystemUI/res/xml/lockscreen_settings.xml
index 73e29af..1e7d266 100644
--- a/packages/SystemUI/res/xml/lockscreen_settings.xml
+++ b/packages/SystemUI/res/xml/lockscreen_settings.xml
@@ -18,42 +18,25 @@
xmlns:sysui="http://schemas.android.com/apk/res-auto"
android:title="@string/other">
- <PreferenceCategory
- android:key="left"
- android:title="@string/lockscreen_left">
- <SwitchPreference
- android:key="customize"
- android:title="@string/lockscreen_customize" />
+ <Preference
+ android:key="sysui_keyguard_left"
+ android:title="@string/lockscreen_shortcut_left"
+ android:fragment="com.android.systemui.tuner.ShortcutPicker" />
- <Preference
- android:key="shortcut"
- android:title="@string/lockscreen_shortcut" />
+ <com.android.systemui.tuner.TunerSwitch
+ android:key="sysui_keyguard_left_unlock"
+ android:title="@string/lockscreen_unlock_left"
+ sysui:defValue="true" />
- <com.android.systemui.tuner.TunerSwitch
- android:key="sysui_keyguard_left_unlock"
- android:title="@string/lockscreen_unlock"
- sysui:defValue="true" />
+ <Preference
+ android:key="sysui_keyguard_right"
+ android:title="@string/lockscreen_shortcut_right"
+ android:fragment="com.android.systemui.tuner.ShortcutPicker" />
- </PreferenceCategory>
-
- <PreferenceCategory
- android:key="right"
- android:title="@string/lockscreen_right">
-
- <SwitchPreference
- android:key="customize"
- android:title="@string/lockscreen_customize" />
-
- <Preference
- android:key="shortcut"
- android:title="@string/lockscreen_shortcut" />
-
- <com.android.systemui.tuner.TunerSwitch
- android:key="sysui_keyguard_right_unlock"
- android:title="@string/lockscreen_unlock"
- sysui:defValue="true" />
-
- </PreferenceCategory>
+ <com.android.systemui.tuner.TunerSwitch
+ android:key="sysui_keyguard_right_unlock"
+ android:title="@string/lockscreen_unlock_right"
+ sysui:defValue="true" />
</PreferenceScreen>
diff --git a/packages/SystemUI/res/xml/nav_bar_tuner.xml b/packages/SystemUI/res/xml/nav_bar_tuner.xml
index 6fa8bec..68e8fad 100644
--- a/packages/SystemUI/res/xml/nav_bar_tuner.xml
+++ b/packages/SystemUI/res/xml/nav_bar_tuner.xml
@@ -18,7 +18,7 @@
xmlns:sysui="http://schemas.android.com/apk/res-auto"
android:title="@string/nav_bar">
- <ListPreference
+ <com.android.systemui.tuner.RadioListPreference
android:key="layout"
android:title="@string/nav_bar_layout"
android:summary="%s"
@@ -26,54 +26,42 @@
android:entries="@array/nav_bar_layouts"
android:entryValues="@array/nav_bar_layouts_values" />
- <PreferenceCategory
- android:key="left"
- android:title="@string/nav_bar_left">
+ <com.android.systemui.tuner.RadioListPreference
+ android:key="type_left"
+ android:title="@string/left_nav_bar_button_type"
+ android:persistent="false"
+ android:summary="%s"
+ android:entries="@array/nav_bar_buttons"
+ android:entryValues="@array/nav_bar_button_values" />
- <DropDownPreference
- android:key="type_left"
- android:title="@string/nav_bar_button_type"
- android:persistent="false"
- android:summary="%s"
- android:entries="@array/nav_bar_buttons"
- android:entryValues="@array/nav_bar_button_values" />
+ <Preference
+ android:key="keycode_left"
+ android:persistent="false"
+ android:title="@string/left_keycode" />
- <Preference
- android:key="keycode_left"
- android:persistent="false"
- android:title="@string/keycode" />
+ <com.android.systemui.tuner.RadioListPreference
+ android:key="icon_left"
+ android:persistent="false"
+ android:summary="%s"
+ android:title="@string/left_icon" />
- <com.android.systemui.tuner.BetterListPreference
- android:key="icon_left"
- android:persistent="false"
- android:summary="%s"
- android:title="@string/icon" />
+ <com.android.systemui.tuner.RadioListPreference
+ android:key="type_right"
+ android:title="@string/right_nav_bar_button_type"
+ android:summary="%s"
+ android:persistent="false"
+ android:entries="@array/nav_bar_buttons"
+ android:entryValues="@array/nav_bar_button_values" />
- </PreferenceCategory>
+ <Preference
+ android:key="keycode_right"
+ android:persistent="false"
+ android:title="@string/right_keycode" />
- <PreferenceCategory
- android:key="right"
- android:title="@string/nav_bar_right">
-
- <DropDownPreference
- android:key="type_right"
- android:title="@string/nav_bar_button_type"
- android:summary="%s"
- android:persistent="false"
- android:entries="@array/nav_bar_buttons"
- android:entryValues="@array/nav_bar_button_values" />
-
- <Preference
- android:key="keycode_right"
- android:persistent="false"
- android:title="@string/keycode" />
-
- <com.android.systemui.tuner.BetterListPreference
- android:key="icon_right"
- android:persistent="false"
- android:summary="%s"
- android:title="@string/icon" />
-
- </PreferenceCategory>
+ <com.android.systemui.tuner.RadioListPreference
+ android:key="icon_right"
+ android:persistent="false"
+ android:summary="%s"
+ android:title="@string/right_icon" />
</PreferenceScreen>
diff --git a/packages/SystemUI/res/xml/tuner_prefs.xml b/packages/SystemUI/res/xml/tuner_prefs.xml
index 6198ab7..c354811 100644
--- a/packages/SystemUI/res/xml/tuner_prefs.xml
+++ b/packages/SystemUI/res/xml/tuner_prefs.xml
@@ -121,6 +121,7 @@
</PreferenceScreen>
+ <!--
<PreferenceScreen
android:key="picture_in_picture"
android:title="@string/picture_in_picture">
@@ -143,6 +144,7 @@
sysui:defValue="false" />
</PreferenceScreen>
+ -->
<Preference
android:key="nav_bar"
@@ -151,15 +153,10 @@
<Preference
android:key="lockscreen"
- android:title="@string/accessibility_desc_lock_screen"
+ android:title="@string/tuner_lock_screen"
android:fragment="com.android.systemui.tuner.LockscreenFragment" />
<Preference
- android:key="other"
- android:title="@string/other"
- android:fragment="com.android.systemui.tuner.OtherPrefs" />
-
- <Preference
android:key="plugins"
android:title="@string/plugins"
android:fragment="com.android.systemui.tuner.PluginFragment" />
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index 273b5e3..f1e7d53 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -25,6 +25,8 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import com.android.systemui.assist.AssistManager;
+import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.fragments.FragmentService;
import com.android.systemui.plugins.PluginManager;
import com.android.systemui.statusbar.phone.ConfigurationControllerImpl;
import com.android.systemui.statusbar.phone.DarkIconDispatcherImpl;
@@ -74,6 +76,7 @@
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.HashMap;
+import java.util.function.Consumer;
/**
* Class to handle ugly dependencies throughout sysui until we determine the
@@ -227,6 +230,9 @@
mProviders.put(StatusBarIconController.class, () ->
new StatusBarIconControllerImpl(mContext));
+ mProviders.put(FragmentService.class, () ->
+ new FragmentService(mContext));
+
// Put all dependencies above here so the factory can override them if it wants.
SystemUIFactory.getInstance().injectDependencies(mProviders, mContext);
}
@@ -282,17 +288,42 @@
T createDependency();
}
+ private <T> void destroyDependency(Class<T> cls, Consumer<T> destroy) {
+ T dep = (T) mDependencies.remove(cls);
+ if (dep != null && destroy != null) {
+ destroy.accept(dep);
+ }
+ }
+
/**
* Used in separate processes (like tuner settings) to init the dependencies.
*/
public static void initDependencies(Context context) {
if (sDependency != null) return;
Dependency d = new Dependency();
- d.mContext = context.getApplicationContext();
+ d.mContext = context;
d.mComponents = new HashMap<>();
d.start();
}
+ /**
+ * Used in separate process teardown to ensure the context isn't leaked.
+ *
+ * TODO: Remove once PreferenceFragment doesn't reference getActivity()
+ * anymore and these context hacks are no longer needed.
+ */
+ public static void clearDependencies() {
+ sDependency = null;
+ }
+
+ /**
+ * Checks to see if a dependency is instantiated, if it is it removes it from
+ * the cache and calls the destroy callback.
+ */
+ public static <T> void destroy(Class<T> cls, Consumer<T> destroy) {
+ sDependency.destroyDependency(cls, destroy);
+ }
+
public static <T> T get(Class<T> cls) {
return sDependency.getDependency(cls);
}
diff --git a/packages/SystemUI/src/com/android/systemui/PluginInflateContainer.java b/packages/SystemUI/src/com/android/systemui/PluginInflateContainer.java
index 9cc6613..ddd4833 100644
--- a/packages/SystemUI/src/com/android/systemui/PluginInflateContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/PluginInflateContainer.java
@@ -53,8 +53,7 @@
private static final String TAG = "PluginInflateContainer";
- private String mAction;
- private int mVersion;
+ private Class<?> mClass;
private View mPluginView;
public PluginInflateContainer(Context context, @Nullable AttributeSet attrs) {
@@ -62,28 +61,25 @@
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PluginInflateContainer);
String viewType = a.getString(R.styleable.PluginInflateContainer_viewType);
try {
- Class c = Class.forName(viewType);
- mAction = (String) c.getDeclaredField("ACTION").get(null);
- mVersion = (int) c.getDeclaredField("VERSION").get(null);
+ mClass = Class.forName(viewType);
} catch (Exception e) {
Log.d(TAG, "Problem getting class info " + viewType, e);
- mAction = null;
- mVersion = 0;
+ mClass = null;
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
- if (mAction != null) {
- Dependency.get(PluginManager.class).addPluginListener(mAction, this, mVersion);
+ if (mClass != null) {
+ Dependency.get(PluginManager.class).addPluginListener(this, mClass);
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
- if (mAction != null) {
+ if (mClass != null) {
Dependency.get(PluginManager.class).removePluginListener(this);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
index 187b557..be69867 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
@@ -36,6 +36,7 @@
import com.android.systemui.media.RingtonePlayer;
import com.android.systemui.pip.PipUI;
import com.android.systemui.plugins.OverlayPlugin;
+import com.android.systemui.plugins.Plugin;
import com.android.systemui.plugins.PluginListener;
import com.android.systemui.plugins.PluginManager;
import com.android.systemui.power.PowerUI;
@@ -67,7 +68,6 @@
*/
private final Class<?>[] SERVICES = new Class[] {
Dependency.class,
- FragmentService.class,
NotificationChannels.class,
CommandQueue.CommandQueueStart.class,
KeyguardViewMediator.class,
@@ -207,7 +207,7 @@
mServices[i].onBootCompleted();
}
}
- Dependency.get(PluginManager.class).addPluginListener(OverlayPlugin.ACTION,
+ Dependency.get(PluginManager.class).addPluginListener(
new PluginListener<OverlayPlugin>() {
private ArraySet<OverlayPlugin> mOverlays;
@@ -236,7 +236,7 @@
Dependency.get(StatusBarWindowManager.class).setForcePluginOpen(
mOverlays.size() != 0);
}
- }, OverlayPlugin.VERSION, true /* Allow multiple plugins */);
+ }, OverlayPlugin.class, true /* Allow multiple plugins */);
mServicesStarted = true;
}
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
index 94dc9a3..6186df1 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
@@ -49,7 +49,7 @@
}
DozeProvider provider = Dependency.get(PluginManager.class)
- .getOneShotPlugin(DozeProvider.ACTION, DozeProvider.VERSION);
+ .getOneShotPlugin(DozeProvider.class);
mDozeMachine = new DozeFactory(provider).assembleMachine(this);
}
diff --git a/packages/SystemUI/src/com/android/systemui/fragments/FragmentHostManager.java b/packages/SystemUI/src/com/android/systemui/fragments/FragmentHostManager.java
index 0c6bf52..57c75bf 100644
--- a/packages/SystemUI/src/com/android/systemui/fragments/FragmentHostManager.java
+++ b/packages/SystemUI/src/com/android/systemui/fragments/FragmentHostManager.java
@@ -32,6 +32,7 @@
import android.view.View;
import com.android.settingslib.applications.InterestingConfigChanges;
+import com.android.systemui.Dependency;
import com.android.systemui.SystemUIApplication;
import com.android.systemui.plugins.Plugin;
import com.android.systemui.plugins.PluginManager;
@@ -171,6 +172,10 @@
return mPlugins;
}
+ void destroy() {
+ mFragments.dispatchDestroy();
+ }
+
public interface FragmentListener {
void onFragmentViewCreated(String tag, Fragment fragment);
@@ -182,8 +187,7 @@
public static FragmentHostManager get(View view) {
try {
- return ((SystemUIApplication) view.getContext().getApplicationContext())
- .getComponent(FragmentService.class).getFragmentHostManager(view);
+ return Dependency.get(FragmentService.class).getFragmentHostManager(view);
} catch (ClassCastException e) {
// TODO: Some auto handling here?
throw e;
diff --git a/packages/SystemUI/src/com/android/systemui/fragments/FragmentService.java b/packages/SystemUI/src/com/android/systemui/fragments/FragmentService.java
index 85cde10..9a8512d 100644
--- a/packages/SystemUI/src/com/android/systemui/fragments/FragmentService.java
+++ b/packages/SystemUI/src/com/android/systemui/fragments/FragmentService.java
@@ -14,6 +14,7 @@
package com.android.systemui.fragments;
+import android.content.Context;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.Handler;
@@ -21,6 +22,7 @@
import android.util.Log;
import android.view.View;
+import com.android.systemui.ConfigurationChangedReceiver;
import com.android.systemui.SystemUI;
import com.android.systemui.SystemUIApplication;
@@ -28,16 +30,16 @@
* Holds a map of root views to FragmentHostStates and generates them as needed.
* Also dispatches the configuration changes to all current FragmentHostStates.
*/
-public class FragmentService extends SystemUI {
+public class FragmentService implements ConfigurationChangedReceiver {
private static final String TAG = "FragmentService";
private final ArrayMap<View, FragmentHostState> mHosts = new ArrayMap<>();
private final Handler mHandler = new Handler();
+ private final Context mContext;
- @Override
- public void start() {
- putComponent(FragmentService.class, this);
+ public FragmentService(Context context) {
+ mContext = context;
}
public FragmentHostManager getFragmentHostManager(View view) {
@@ -50,8 +52,14 @@
return state.getFragmentHostManager();
}
+ public void destroyAll() {
+ for (FragmentHostState state : mHosts.values()) {
+ state.mFragmentHostManager.destroy();
+ }
+ }
+
@Override
- protected void onConfigurationChanged(Configuration newConfig) {
+ public void onConfigurationChanged(Configuration newConfig) {
for (FragmentHostState state : mHosts.values()) {
state.sendConfigurationChange(newConfig);
}
diff --git a/packages/SystemUI/src/com/android/systemui/fragments/PluginFragmentListener.java b/packages/SystemUI/src/com/android/systemui/fragments/PluginFragmentListener.java
index 1eaca6f..03bb73d 100644
--- a/packages/SystemUI/src/com/android/systemui/fragments/PluginFragmentListener.java
+++ b/packages/SystemUI/src/com/android/systemui/fragments/PluginFragmentListener.java
@@ -44,8 +44,9 @@
mDefaultClass = defaultFragment;
}
- public void startListening(String action, int version) {
- mPluginManager.addPluginListener(action, this, version, false /* Only allow one */);
+ public void startListening() {
+ mPluginManager.addPluginListener(this, mExpectedInterface,
+ false /* Only allow one */);
}
public void stopListening() {
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java b/packages/SystemUI/src/com/android/systemui/plugins/PluginInstanceManager.java
similarity index 91%
rename from packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java
rename to packages/SystemUI/src/com/android/systemui/plugins/PluginInstanceManager.java
index dd1614b..e895fa2 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java
+++ b/packages/SystemUI/src/com/android/systemui/plugins/PluginInstanceManager.java
@@ -18,12 +18,10 @@
import android.app.Notification.Action;
import android.app.NotificationManager;
import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
-import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
@@ -40,6 +38,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
+import com.android.systemui.plugins.VersionInfo.InvalidVersionException;
import java.util.ArrayList;
import java.util.List;
@@ -55,7 +54,7 @@
private final PluginListener<T> mListener;
private final String mAction;
private final boolean mAllowMultiple;
- private final int mVersion;
+ private final VersionInfo mVersion;
@VisibleForTesting
final MainHandler mMainHandler;
@@ -66,14 +65,14 @@
private final PluginManager mManager;
PluginInstanceManager(Context context, String action, PluginListener<T> listener,
- boolean allowMultiple, Looper looper, int version, PluginManager manager) {
+ boolean allowMultiple, Looper looper, VersionInfo version, PluginManager manager) {
this(context, context.getPackageManager(), action, listener, allowMultiple, looper, version,
manager, Build.IS_DEBUGGABLE);
}
@VisibleForTesting
PluginInstanceManager(Context context, PackageManager pm, String action,
- PluginListener<T> listener, boolean allowMultiple, Looper looper, int version,
+ PluginListener<T> listener, boolean allowMultiple, Looper looper, VersionInfo version,
PluginManager manager, boolean debuggable) {
mMainHandler = new MainHandler(Looper.getMainLooper());
mPluginHandler = new PluginHandler(looper);
@@ -301,8 +300,14 @@
Context pluginContext = new PluginContextWrapper(
mContext.createApplicationContext(info, 0), classLoader);
Class<?> pluginClass = Class.forName(cls, true, classLoader);
+ // TODO: Only create the plugin before version check if we need it for
+ // legacy version check.
T plugin = (T) pluginClass.newInstance();
- if (plugin.getVersion() != mVersion) {
+ try {
+ checkVersion(pluginClass, plugin, mVersion);
+ if (DEBUG) Log.d(TAG, "createPlugin");
+ return new PluginInfo(pkg, cls, plugin, pluginContext);
+ } catch (InvalidVersionException e) {
final int icon = mContext.getResources().getIdentifier("tuner", "drawable",
mContext.getPackageName());
final int color = Resources.getSystem().getIdentifier(
@@ -318,20 +323,18 @@
String label = cls;
try {
label = mPm.getServiceInfo(component, 0).loadLabel(mPm).toString();
- } catch (NameNotFoundException e) {
+ } catch (NameNotFoundException e2) {
}
- if (plugin.getVersion() < mVersion) {
+ if (!e.isTooNew()) {
// Localization not required as this will never ever appear in a user build.
nb.setContentTitle("Plugin \"" + label + "\" is too old")
.setContentText("Contact plugin developer to get an updated"
- + " version.\nPlugin version: " + plugin.getVersion()
- + "\nSystem version: " + mVersion);
+ + " version.\n" + e.getMessage());
} else {
// Localization not required as this will never ever appear in a user build.
nb.setContentTitle("Plugin \"" + label + "\" is too new")
.setContentText("Check to see if an OTA is available.\n"
- + "Plugin version: " + plugin.getVersion()
- + "\nSystem version: " + mVersion);
+ + e.getMessage());
}
Intent i = new Intent(PluginManager.DISABLE_PLUGIN).setData(
Uri.parse("package://" + component.flattenToString()));
@@ -345,13 +348,24 @@
+ ", expected " + mVersion);
return null;
}
- if (DEBUG) Log.d(TAG, "createPlugin");
- return new PluginInfo(pkg, cls, plugin, pluginContext);
} catch (Exception e) {
Log.w(TAG, "Couldn't load plugin: " + pkg, e);
return null;
}
}
+
+ private void checkVersion(Class<?> pluginClass, T plugin, VersionInfo version)
+ throws InvalidVersionException {
+ VersionInfo pv = new VersionInfo().addClass(pluginClass);
+ if (pv.hasVersionInfo()) {
+ version.checkVersion(pv);
+ } else {
+ int fallbackVersion = plugin.getVersion();
+ if (fallbackVersion != version.getDefaultVersion()) {
+ throw new InvalidVersionException("Invalid legacy version", false);
+ }
+ }
+ }
}
public static class PluginContextWrapper extends ContextWrapper {
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java b/packages/SystemUI/src/com/android/systemui/plugins/PluginManager.java
similarity index 89%
rename from packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java
rename to packages/SystemUI/src/com/android/systemui/plugins/PluginManager.java
index cef485e..8b4bd7b 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java
+++ b/packages/SystemUI/src/com/android/systemui/plugins/PluginManager.java
@@ -33,6 +33,7 @@
import android.os.Looper;
import android.os.SystemProperties;
import android.os.UserHandle;
+import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -40,6 +41,7 @@
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.systemui.plugins.PluginInstanceManager.PluginContextWrapper;
import com.android.systemui.plugins.PluginInstanceManager.PluginInfo;
+import com.android.systemui.plugins.annotations.ProvidesInterface;
import dalvik.system.PathClassLoader;
@@ -93,7 +95,18 @@
Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler);
}
- public <T extends Plugin> T getOneShotPlugin(String action, int version) {
+ public <T extends Plugin> T getOneShotPlugin(Class<T> cls) {
+ ProvidesInterface info = cls.getDeclaredAnnotation(ProvidesInterface.class);
+ if (info == null) {
+ throw new RuntimeException(cls + " doesn't provide an interface");
+ }
+ if (TextUtils.isEmpty(info.action())) {
+ throw new RuntimeException(cls + " doesn't provide an action");
+ }
+ return getOneShotPlugin(info.action(), cls);
+ }
+
+ public <T extends Plugin> T getOneShotPlugin(String action, Class<?> cls) {
if (!isDebuggable) {
// Never ever ever allow these on production builds, they are only for prototyping.
return null;
@@ -102,7 +115,7 @@
throw new RuntimeException("Must be called from UI thread");
}
PluginInstanceManager<T> p = mFactory.createPluginInstanceManager(mContext, action, null,
- false, mBackgroundThread.getLooper(), version, this);
+ false, mBackgroundThread.getLooper(), cls, this);
mPluginPrefs.addAction(action);
PluginInfo<T> info = p.getPlugin();
if (info != null) {
@@ -114,20 +127,36 @@
return null;
}
- public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
- int version) {
- addPluginListener(action, listener, version, false);
+ public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls) {
+ addPluginListener(listener, cls, false);
+ }
+
+ public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls,
+ boolean allowMultiple) {
+ ProvidesInterface info = cls.getDeclaredAnnotation(ProvidesInterface.class);
+ if (info == null) {
+ throw new RuntimeException(cls + " doesn't provide an interface");
+ }
+ if (TextUtils.isEmpty(info.action())) {
+ throw new RuntimeException(cls + " doesn't provide an action");
+ }
+ addPluginListener(info.action(), listener, cls, allowMultiple);
}
public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
- int version, boolean allowMultiple) {
+ Class<?> cls) {
+ addPluginListener(action, listener, cls, false);
+ }
+
+ public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
+ Class cls, boolean allowMultiple) {
if (!isDebuggable) {
// Never ever ever allow these on production builds, they are only for prototyping.
return;
}
mPluginPrefs.addAction(action);
PluginInstanceManager p = mFactory.createPluginInstanceManager(mContext, action, listener,
- allowMultiple, mBackgroundThread.getLooper(), version, this);
+ allowMultiple, mBackgroundThread.getLooper(), cls, this);
p.loadAll();
mPluginMap.put(listener, p);
startListening();
@@ -282,9 +311,9 @@
public static class PluginInstanceManagerFactory {
public <T extends Plugin> PluginInstanceManager createPluginInstanceManager(Context context,
String action, PluginListener<T> listener, boolean allowMultiple, Looper looper,
- int version, PluginManager manager) {
+ Class<?> cls, PluginManager manager) {
return new PluginInstanceManager(context, action, listener, allowMultiple, looper,
- version, manager);
+ new VersionInfo().addClass(cls), manager);
}
}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginPrefs.java b/packages/SystemUI/src/com/android/systemui/plugins/PluginPrefs.java
similarity index 100%
rename from packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginPrefs.java
rename to packages/SystemUI/src/com/android/systemui/plugins/PluginPrefs.java
diff --git a/packages/SystemUI/src/com/android/systemui/plugins/VersionInfo.java b/packages/SystemUI/src/com/android/systemui/plugins/VersionInfo.java
new file mode 100644
index 0000000..84f7761
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/plugins/VersionInfo.java
@@ -0,0 +1,134 @@
+/*
+ * 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. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.plugins;
+
+import com.android.systemui.plugins.annotations.Dependencies;
+import com.android.systemui.plugins.annotations.DependsOn;
+import com.android.systemui.plugins.annotations.ProvidesInterface;
+import com.android.systemui.plugins.annotations.Requirements;
+import com.android.systemui.plugins.annotations.Requires;
+
+import android.util.ArrayMap;
+
+public class VersionInfo {
+
+ private final ArrayMap<Class<?>, Version> mVersions = new ArrayMap<>();
+ private Class<?> mDefault;
+
+ public boolean hasVersionInfo() {
+ return !mVersions.isEmpty();
+ }
+
+ public int getDefaultVersion() {
+ return mVersions.get(mDefault).mVersion;
+ }
+
+ public VersionInfo addClass(Class<?> cls) {
+ if (mDefault == null) {
+ // The legacy default version is from the first class we add.
+ mDefault = cls;
+ }
+ addClass(cls, false);
+ return this;
+ }
+
+ private void addClass(Class<?> cls, boolean required) {
+ ProvidesInterface provider = cls.getDeclaredAnnotation(ProvidesInterface.class);
+ if (provider != null) {
+ mVersions.put(cls, new Version(provider.version(), true));
+ }
+ Requires requires = cls.getDeclaredAnnotation(Requires.class);
+ if (requires != null) {
+ mVersions.put(requires.target(), new Version(requires.version(), required));
+ }
+ Requirements requirements = cls.getDeclaredAnnotation(Requirements.class);
+ if (requirements != null) {
+ for (Requires r : requirements.value()) {
+ mVersions.put(r.target(), new Version(r.version(), required));
+ }
+ }
+ DependsOn depends = cls.getDeclaredAnnotation(DependsOn.class);
+ if (depends != null) {
+ addClass(depends.target(), true);
+ }
+ Dependencies dependencies = cls.getDeclaredAnnotation(Dependencies.class);
+ if (dependencies != null) {
+ for (DependsOn d : dependencies.value()) {
+ addClass(d.target(), true);
+ }
+ }
+ }
+
+ public void checkVersion(VersionInfo plugin) throws InvalidVersionException {
+ ArrayMap<Class<?>, Version> versions = new ArrayMap<>(mVersions);
+ plugin.mVersions.forEach((aClass, version) -> {
+ Version v = versions.remove(aClass);
+ if (v == null) {
+ v = createVersion(aClass);
+ }
+ if (v == null) {
+ throw new InvalidVersionException(aClass.getSimpleName()
+ + " does not provide an interface", false);
+ }
+ if (v.mVersion != version.mVersion) {
+ throw new InvalidVersionException(aClass, v.mVersion < version.mVersion, v.mVersion,
+ version.mVersion);
+ }
+ });
+ versions.forEach((aClass, version) -> {
+ if (version.mRequired) {
+ throw new InvalidVersionException("Missing required dependency "
+ + aClass.getSimpleName(), false);
+ }
+ });
+ }
+
+ private Version createVersion(Class<?> cls) {
+ ProvidesInterface provider = cls.getDeclaredAnnotation(ProvidesInterface.class);
+ if (provider != null) {
+ return new Version(provider.version(), false);
+ }
+ return null;
+ }
+
+ public static class InvalidVersionException extends RuntimeException {
+ private final boolean mTooNew;
+
+ public InvalidVersionException(String str, boolean tooNew) {
+ super(str);
+ mTooNew = tooNew;
+ }
+
+ public InvalidVersionException(Class<?> cls, boolean tooNew, int expected, int actual) {
+ super(cls.getSimpleName() + " expected version " + expected + " but had " + actual);
+ mTooNew = tooNew;
+ }
+
+ public boolean isTooNew() {
+ return mTooNew;
+ }
+ }
+
+ private static class Version {
+
+ private final int mVersion;
+ private final boolean mRequired;
+
+ public Version(int version, boolean required) {
+ mVersion = version;
+ mRequired = required;
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 504678c..1569b0c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -86,6 +86,10 @@
setOrientation(VERTICAL);
+ mBrightnessView = LayoutInflater.from(context).inflate(
+ R.layout.quick_settings_brightness_dialog, this, false);
+ addView(mBrightnessView);
+
setupTileLayout();
mFooter = new QSFooter(this, context);
@@ -100,10 +104,6 @@
updateResources();
- mBrightnessView = LayoutInflater.from(context).inflate(
- R.layout.quick_settings_brightness_dialog, this, false);
- addView(mBrightnessView);
-
mBrightnessController = new BrightnessController(getContext(),
(ImageView) findViewById(R.id.brightness_icon),
(ToggleSliderView) findViewById(R.id.brightness_slider));
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java
index f2c3e61..4e30797 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java
@@ -301,6 +301,7 @@
if (position == mEditIndex) position--;
move(mAccessibilityFromIndex, position, v);
+
notifyDataSetChanged();
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
index ce72942..ec4ca7a6 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
@@ -102,12 +102,7 @@
mTokenMap.remove(service.getToken());
mTiles.remove(tile.getComponent());
final String slot = tile.getComponent().getClassName();
- mMainHandler.post(new Runnable() {
- @Override
- public void run() {
- mHost.getIconController().removeIcon(slot);
- }
- });
+ mMainHandler.post(() -> mHost.getIconController().removeIcon(slot));
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
index 55491b2..8de4e58 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
@@ -130,6 +130,7 @@
launchOpts.numVisibleTaskThumbnails = 2;
launchOpts.onlyLoadForCache = true;
launchOpts.onlyLoadPausedActivities = true;
+ launchOpts.loadThumbnails = !ActivityManager.ENABLE_TASK_SNAPSHOTS;
loader.loadTasks(mContext, plan, launchOpts);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 5366da1..995901b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -164,15 +164,19 @@
}
}
- public void disable(int state1, int state2) {
+ public void disable(int state1, int state2, boolean animate) {
synchronized (mLock) {
mDisable1 = state1;
mDisable2 = state2;
mHandler.removeMessages(MSG_DISABLE);
- mHandler.obtainMessage(MSG_DISABLE, state1, state2, null).sendToTarget();
+ mHandler.obtainMessage(MSG_DISABLE, state1, state2, animate).sendToTarget();
}
}
+ public void disable(int state1, int state2) {
+ disable(state1, state2, true);
+ }
+
public void animateExpandNotificationsPanel() {
synchronized (mLock) {
mHandler.removeMessages(MSG_EXPAND_NOTIFICATIONS);
@@ -433,7 +437,7 @@
}
case MSG_DISABLE:
for (int i = 0; i < mCallbacks.size(); i++) {
- mCallbacks.get(i).disable(msg.arg1, msg.arg2, true /* animate */);
+ mCallbacks.get(i).disable(msg.arg1, msg.arg2, (Boolean) msg.obj);
}
break;
case MSG_EXPAND_NOTIFICATIONS:
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
index 3bbaf99..3648a06 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
@@ -22,7 +22,6 @@
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.annotation.Nullable;
import android.content.Context;
-import android.content.res.ColorStateList;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.AnimationDrawable;
import android.graphics.drawable.ColorDrawable;
@@ -331,10 +330,11 @@
boolean isPreL = Boolean.TRUE.equals(expandedIcon.getTag(R.id.icon_is_pre_L));
boolean colorize = !isPreL || NotificationUtils.isGrayscale(expandedIcon,
NotificationColorUtil.getInstance(mContext));
+ int color = StatusBarIconView.NO_COLOR;
if (colorize) {
- int color = mEntry.getContrastedColor(mContext, mIsLowPriority && !isExpanded());
- expandedIcon.setImageTintList(ColorStateList.valueOf(color));
+ color = mEntry.getContrastedColor(mContext, mIsLowPriority && !isExpanded());
}
+ expandedIcon.setStaticDrawableColor(color);
}
private void updateLimits() {
@@ -1990,7 +1990,7 @@
mAboveShelf = aboveShelf;
}
- public class NotificationViewState extends ExpandableViewState {
+ public static class NotificationViewState extends ExpandableViewState {
private final StackScrollState mOverallState;
@@ -2011,8 +2011,11 @@
@Override
protected void onYTranslationAnimationFinished(View view) {
super.onYTranslationAnimationFinished(view);
- if (mHeadsupDisappearRunning) {
- setHeadsUpAnimatingAway(false);
+ if (view instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+ if (row.isHeadsUpAnimatingAway()) {
+ row.setHeadsUpAnimatingAway(false);
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMenuRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMenuRow.java
index 355022f..534a719 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMenuRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMenuRow.java
@@ -90,8 +90,7 @@
protected void onAttachedToWindow() {
super.onAttachedToWindow();
Dependency.get(PluginManager.class).addPluginListener(
- NotificationMenuRowProvider.ACTION, this,
- NotificationMenuRowProvider.VERSION, false /* Allow multiple */);
+ this, NotificationMenuRowProvider.class, false /* Allow multiple */);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index 2425076..d4ed1dc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -23,6 +23,7 @@
import android.view.View;
import android.view.ViewGroup;
+import com.android.internal.widget.CachingIconView;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
import com.android.systemui.ViewInvertHelper;
@@ -414,7 +415,8 @@
transitionAmount);
float shelfIconSize = icon.getHeight() * icon.getIconScale();
float alpha = 1.0f;
- if (!row.isShowingIcon()) {
+ boolean noIcon = !row.isShowingIcon();
+ if (noIcon) {
// The view currently doesn't have an icon, lets transform it in!
alpha = transitionAmount;
notificationIconSize = shelfIconSize / 2.0f;
@@ -438,6 +440,13 @@
if (row.isAboveShelf()) {
iconState.hidden = true;
}
+ int shelfColor = icon.getStaticDrawableColor();
+ if (!noIcon && shelfColor != StatusBarIconView.NO_COLOR) {
+ int notificationColor = row.getNotificationHeader().getOriginalNotificationColor();
+ shelfColor = NotificationUtils.interpolateColors(notificationColor, shelfColor,
+ iconState.iconAppearAmount);
+ }
+ iconState.iconColor = shelfColor;
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
index 6283148..aec9a4b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
@@ -19,9 +19,11 @@
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
import android.app.Notification;
import android.content.Context;
import android.content.pm.ApplicationInfo;
+import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Canvas;
@@ -37,7 +39,6 @@
import android.util.Log;
import android.util.Property;
import android.util.TypedValue;
-import android.view.View;
import android.view.ViewDebug;
import android.view.accessibility.AccessibilityEvent;
import android.view.animation.Interpolator;
@@ -50,6 +51,9 @@
import java.text.NumberFormat;
public class StatusBarIconView extends AnimatedImageView {
+ public static final int NO_COLOR = 0;
+ private final int ANIMATION_DURATION_FAST = 100;
+
public static final int STATE_ICON = 0;
public static final int STATE_DOT = 1;
public static final int STATE_HIDDEN = 2;
@@ -104,6 +108,17 @@
private ObjectAnimator mDotAnimator;
private float mDotAppearAmount;
private OnVisibilityChangedListener mOnVisibilityChangedListener;
+ private int mDrawableColor;
+ private int mIconColor;
+ private ValueAnimator mColorAnimator;
+ private int mCurrentSetColor = NO_COLOR;
+ private int mAnimationStartColor = NO_COLOR;
+ private final ValueAnimator.AnimatorUpdateListener mColorUpdater
+ = animation -> {
+ int newColor = NotificationUtils.interpolateColors(mAnimationStartColor, mIconColor,
+ animation.getAnimatedFraction());
+ setColorInternal(newColor);
+ };
public StatusBarIconView(Context context, String slot, Notification notification) {
this(context, slot, notification, false);
@@ -123,7 +138,7 @@
setScaleType(ScaleType.CENTER);
mDensity = context.getResources().getDisplayMetrics().densityDpi;
if (mNotification != null) {
- setIconTint(getContext().getColor(
+ setDecorColor(getContext().getColor(
com.android.internal.R.color.notification_icon_default_color));
}
reloadDimens();
@@ -446,10 +461,66 @@
return c.getString(R.string.accessibility_desc_notification_icon, appName, desc);
}
- public void setIconTint(int iconTint) {
+ /**
+ * Set the color that is used to draw decoration like the overflow dot. This will not be applied
+ * to the drawable.
+ */
+ public void setDecorColor(int iconTint) {
mDotPaint.setColor(iconTint);
}
+ /**
+ * Set the static color that should be used for the drawable of this icon if it's not
+ * transitioning this also immediately sets the color.
+ */
+ public void setStaticDrawableColor(int color) {
+ mDrawableColor = color;
+ setColorInternal(color);
+ mIconColor = color;
+ }
+
+ private void setColorInternal(int color) {
+ if (color != NO_COLOR) {
+ setImageTintList(ColorStateList.valueOf(color));
+ } else {
+ setImageTintList(null);
+ }
+ mCurrentSetColor = color;
+ }
+
+ public void setIconColor(int iconColor, boolean animate) {
+ if (mIconColor != iconColor) {
+ mIconColor = iconColor;
+ if (mColorAnimator != null) {
+ mColorAnimator.cancel();
+ }
+ if (mCurrentSetColor == iconColor) {
+ return;
+ }
+ if (animate && mCurrentSetColor != NO_COLOR) {
+ mAnimationStartColor = mCurrentSetColor;
+ mColorAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
+ mColorAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+ mColorAnimator.setDuration(ANIMATION_DURATION_FAST);
+ mColorAnimator.addUpdateListener(mColorUpdater);
+ mColorAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mColorAnimator = null;
+ mAnimationStartColor = NO_COLOR;
+ }
+ });
+ mColorAnimator.start();
+ } else {
+ setColorInternal(iconColor);
+ }
+ }
+ }
+
+ public int getStaticDrawableColor() {
+ return mDrawableColor;
+ }
+
public void setVisibleState(int state) {
setVisibleState(state, true /* animate */, null /* endRunnable */);
}
@@ -467,10 +538,13 @@
boolean runnableAdded = false;
if (visibleState != mVisibleState) {
mVisibleState = visibleState;
+ if (mIconAppearAnimator != null) {
+ mIconAppearAnimator.cancel();
+ }
+ if (mDotAnimator != null) {
+ mDotAnimator.cancel();
+ }
if (animate) {
- if (mIconAppearAnimator != null) {
- mIconAppearAnimator.cancel();
- }
float targetAmount = 0.0f;
Interpolator interpolator = Interpolators.FAST_OUT_LINEAR_IN;
if (visibleState == STATE_ICON) {
@@ -482,7 +556,7 @@
mIconAppearAnimator = ObjectAnimator.ofFloat(this, ICON_APPEAR_AMOUNT,
currentAmount, targetAmount);
mIconAppearAnimator.setInterpolator(interpolator);
- mIconAppearAnimator.setDuration(100);
+ mIconAppearAnimator.setDuration(ANIMATION_DURATION_FAST);
mIconAppearAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
@@ -494,9 +568,6 @@
runnableAdded = true;
}
- if (mDotAnimator != null) {
- mDotAnimator.cancel();
- }
targetAmount = visibleState == STATE_ICON ? 2.0f : 0.0f;
interpolator = Interpolators.FAST_OUT_LINEAR_IN;
if (visibleState == STATE_DOT) {
@@ -508,7 +579,7 @@
mDotAnimator = ObjectAnimator.ofFloat(this, DOT_APPEAR_AMOUNT,
currentAmount, targetAmount);
mDotAnimator.setInterpolator(interpolator);
- mDotAnimator.setDuration(100);
+ mDotAnimator.setDuration(ANIMATION_DURATION_FAST);
final boolean runRunnable = !runnableAdded;
mDotAnimator.addListener(new AnimatorListenerAdapter() {
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
index 2836f41..2b335f4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
@@ -261,9 +261,9 @@
super.onAttachedToWindow();
mAccessibilityController.addStateChangedCallback(this);
Dependency.get(PluginManager.class).addPluginListener(RIGHT_BUTTON_PLUGIN,
- mRightListener, IntentButtonProvider.VERSION, false /* Only allow one */);
+ mRightListener, IntentButtonProvider.class, false /* Only allow one */);
Dependency.get(PluginManager.class).addPluginListener(LEFT_BUTTON_PLUGIN,
- mLeftListener, IntentButtonProvider.VERSION, false /* Only allow one */);
+ mLeftListener, IntentButtonProvider.class, false /* Only allow one */);
Dependency.get(TunerService.class).addTunable(this, LockscreenFragment.LOCKSCREEN_LEFT_BUTTON,
LockscreenFragment.LOCKSCREEN_RIGHT_BUTTON);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
index 5fb99da..720ca14 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
@@ -138,8 +138,8 @@
super.onAttachedToWindow();
Dependency.get(TunerService.class).addTunable(this, NAV_BAR_VIEWS, NAV_BAR_LEFT,
NAV_BAR_RIGHT);
- Dependency.get(PluginManager.class).addPluginListener(NavBarButtonProvider.ACTION, this,
- NavBarButtonProvider.VERSION, true /* Allow multiple */);
+ Dependency.get(PluginManager.class).addPluginListener(this,
+ NavBarButtonProvider.class, true /* Allow multiple */);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
index 5d13289..ad875f1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
@@ -783,8 +783,8 @@
protected void onAttachedToWindow() {
super.onAttachedToWindow();
onPluginDisconnected(null); // Create default gesture helper
- Dependency.get(PluginManager.class).addPluginListener(NavGesture.ACTION, this,
- NavGesture.VERSION, false /* Only one */);
+ Dependency.get(PluginManager.class).addPluginListener(this,
+ NavGesture.class, false /* Only one */);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
index 6d7ab47..707997d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
@@ -1,7 +1,6 @@
package com.android.systemui.statusbar.phone;
import android.content.Context;
-import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Rect;
@@ -226,12 +225,13 @@
for (int i = 0; i < mNotificationIcons.getChildCount(); i++) {
StatusBarIconView v = (StatusBarIconView) mNotificationIcons.getChildAt(i);
boolean isPreL = Boolean.TRUE.equals(v.getTag(R.id.icon_is_pre_L));
+ int color = StatusBarIconView.NO_COLOR;
boolean colorize = !isPreL || NotificationUtils.isGrayscale(v, mNotificationColorUtil);
if (colorize) {
- v.setImageTintList(ColorStateList.valueOf(
- DarkIconDispatcher.getTint(mTintArea, v, mIconTint)));
+ color = DarkIconDispatcher.getTint(mTintArea, v, mIconTint);
}
- v.setIconTint(mIconTint);
+ v.setStaticDrawableColor(color);
+ v.setDecorColor(mIconTint);
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
index 571ae26..dc5f98c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
@@ -446,6 +446,7 @@
public boolean useFullTransitionAmount;
public boolean useLinearTransitionAmount;
public boolean translateContent;
+ public int iconColor = StatusBarIconView.NO_COLOR;
@Override
public void applyToView(View view) {
@@ -505,6 +506,7 @@
}
}
icon.setVisibleState(visibleState, animationsAllowed);
+ icon.setIconColor(iconColor, needsCannedAnimation && animationsAllowed);
if (animate) {
animateTo(icon, animationProperties);
} else {
@@ -515,6 +517,14 @@
needsCannedAnimation = false;
}
+ @Override
+ public void initFrom(View view) {
+ super.initFrom(view);
+ if (view instanceof StatusBarIconView) {
+ iconColor = ((StatusBarIconView) view).getStaticDrawableColor();
+ }
+ }
+
protected void onYTranslationAnimationFinished(View view) {
if (hidden) {
view.setVisibility(INVISIBLE);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index 2667d84..cfbcf8c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -839,7 +839,7 @@
createAndAddWindows();
mSettingsObserver.onChange(false); // set up
- disable(switches[0], switches[6], false /* animate */);
+ mCommandQueue.disable(switches[0], switches[6], false /* animate */);
setSystemUiVisibility(switches[1], switches[7], switches[8], 0xffffffff,
fullscreenStackBounds, dockedStackBounds);
topAppWindowChanged(switches[2] != 0);
@@ -1129,7 +1129,7 @@
.replace(R.id.qs_frame, new QSFragment(), QS.TAG)
.commit();
new PluginFragmentListener(container, QS.TAG, QSFragment.class, QS.class)
- .startListening(QS.ACTION, QS.VERSION);
+ .startListening();
final QSTileHost qsh = SystemUIFactory.getInstance().createQSTileHost(mContext, this,
mIconController);
mBrightnessMirrorController = new BrightnessMirrorController(mStatusBarWindow);
@@ -2511,7 +2511,7 @@
* This needs to be called if state used by {@link #adjustDisableFlags} changes.
*/
public void recomputeDisableFlags(boolean animate) {
- disable(mDisabledUnmodified1, mDisabledUnmodified2, animate);
+ mCommandQueue.disable(mDisabledUnmodified1, mDisabledUnmodified2, animate);
}
protected H createHandler() {
@@ -6937,7 +6937,10 @@
return false;
}
- if (mNotificationData.getImportance(sbn.getKey()) < NotificationManager.IMPORTANCE_HIGH) {
+ // Allow peeking for DEFAULT notifications only if we're on Ambient Display.
+ int importanceLevel = isDozing() ? NotificationManager.IMPORTANCE_DEFAULT
+ : NotificationManager.IMPORTANCE_HIGH;
+ if (mNotificationData.getImportance(sbn.getKey()) < importanceLevel) {
if (DEBUG) Log.d(TAG, "No peeking: unimportant notification: " + sbn.getKey());
return false;
}
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/CustomListPreference.java b/packages/SystemUI/src/com/android/systemui/tuner/CustomListPreference.java
new file mode 100644
index 0000000..e50fd5e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/tuner/CustomListPreference.java
@@ -0,0 +1,173 @@
+/*
+ * 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. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.tuner;
+
+import android.annotation.Nullable;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.os.Bundle;
+import android.support.v14.preference.ListPreferenceDialogFragment;
+import android.support.v7.preference.ListPreference;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+public class CustomListPreference extends ListPreference {
+
+ public CustomListPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public CustomListPreference(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ protected void onPrepareDialogBuilder(AlertDialog.Builder builder,
+ OnClickListener listener) {
+ }
+
+ protected void onDialogClosed(boolean positiveResult) {
+ }
+
+ protected Dialog onDialogCreated(DialogFragment fragment, Dialog dialog) {
+ return dialog;
+ }
+
+ protected boolean isAutoClosePreference() {
+ return true;
+ }
+
+ /**
+ * Called when a user is about to choose the given value, to determine if we
+ * should show a confirmation dialog.
+ *
+ * @param value the value the user is about to choose
+ * @return the message to show in a confirmation dialog, or {@code null} to
+ * not request confirmation
+ */
+ protected CharSequence getConfirmationMessage(String value) {
+ return null;
+ }
+
+ protected void onDialogStateRestored(DialogFragment fragment, Dialog dialog,
+ Bundle savedInstanceState) {
+ }
+
+ public static class CustomListPreferenceDialogFragment extends ListPreferenceDialogFragment {
+
+ private static final String KEY_CLICKED_ENTRY_INDEX
+ = "settings.CustomListPrefDialog.KEY_CLICKED_ENTRY_INDEX";
+
+ private int mClickedDialogEntryIndex;
+
+ public static ListPreferenceDialogFragment newInstance(String key) {
+ final ListPreferenceDialogFragment fragment = new CustomListPreferenceDialogFragment();
+ final Bundle b = new Bundle(1);
+ b.putString(ARG_KEY, key);
+ fragment.setArguments(b);
+ return fragment;
+ }
+
+ public CustomListPreference getCustomizablePreference() {
+ return (CustomListPreference) getPreference();
+ }
+
+ @Override
+ protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
+ super.onPrepareDialogBuilder(builder);
+ mClickedDialogEntryIndex = getCustomizablePreference()
+ .findIndexOfValue(getCustomizablePreference().getValue());
+ getCustomizablePreference().onPrepareDialogBuilder(builder, getOnItemClickListener());
+ if (!getCustomizablePreference().isAutoClosePreference()) {
+ builder.setPositiveButton(com.android.internal.R.string.ok, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ onItemConfirmed();
+ }
+ });
+ }
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ Dialog dialog = super.onCreateDialog(savedInstanceState);
+ if (savedInstanceState != null) {
+ mClickedDialogEntryIndex = savedInstanceState.getInt(KEY_CLICKED_ENTRY_INDEX,
+ mClickedDialogEntryIndex);
+ }
+ return getCustomizablePreference().onDialogCreated(this, dialog);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(KEY_CLICKED_ENTRY_INDEX, mClickedDialogEntryIndex);
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ getCustomizablePreference().onDialogStateRestored(this, getDialog(), savedInstanceState);
+ }
+
+ protected OnClickListener getOnItemClickListener() {
+ return new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ setClickedDialogEntryIndex(which);
+ if (getCustomizablePreference().isAutoClosePreference()) {
+ onItemConfirmed();
+ }
+ }
+ };
+ }
+
+ protected void setClickedDialogEntryIndex(int which) {
+ mClickedDialogEntryIndex = which;
+ }
+
+ private String getValue() {
+ final ListPreference preference = getCustomizablePreference();
+ if (mClickedDialogEntryIndex >= 0 && preference.getEntryValues() != null) {
+ return preference.getEntryValues()[mClickedDialogEntryIndex].toString();
+ } else {
+ return null;
+ }
+ }
+
+ protected void onItemConfirmed() {
+ onClick(getDialog(), DialogInterface.BUTTON_POSITIVE);
+ getDialog().dismiss();
+ }
+
+ @Override
+ public void onDialogClosed(boolean positiveResult) {
+ getCustomizablePreference().onDialogClosed(positiveResult);
+ final ListPreference preference = getCustomizablePreference();
+ final String value = getValue();
+ if (positiveResult && value != null) {
+ if (preference.callChangeListener(value)) {
+ preference.setValue(value);
+ }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/LockscreenFragment.java b/packages/SystemUI/src/com/android/systemui/tuner/LockscreenFragment.java
index 6f4a3a4..410d3d2 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/LockscreenFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/LockscreenFragment.java
@@ -80,10 +80,8 @@
mTunerService = Dependency.get(TunerService.class);
mHandler = new Handler();
addPreferencesFromResource(R.xml.lockscreen_settings);
- setupGroup((PreferenceGroup) findPreference(KEY_LEFT), LOCKSCREEN_LEFT_BUTTON,
- LOCKSCREEN_LEFT_UNLOCK);
- setupGroup((PreferenceGroup) findPreference(KEY_RIGHT), LOCKSCREEN_RIGHT_BUTTON,
- LOCKSCREEN_RIGHT_UNLOCK);
+ setupGroup(LOCKSCREEN_LEFT_BUTTON, LOCKSCREEN_LEFT_UNLOCK);
+ setupGroup(LOCKSCREEN_RIGHT_BUTTON, LOCKSCREEN_RIGHT_UNLOCK);
}
@Override
@@ -92,30 +90,14 @@
mTunables.forEach(t -> mTunerService.removeTunable(t));
}
- private void setupGroup(PreferenceGroup group, String buttonSetting, String unlockKey) {
- SwitchPreference customize = (SwitchPreference) group.findPreference(KEY_CUSTOMIZE);
- Preference shortcut = group.findPreference(KEY_SHORTCUT);
- SwitchPreference unlock = (SwitchPreference) group.findPreference(unlockKey);
+ private void setupGroup(String buttonSetting, String unlockKey) {
+ Preference shortcut = findPreference(buttonSetting);
+ SwitchPreference unlock = (SwitchPreference) findPreference(unlockKey);
addTunable((k, v) -> {
- boolean visible = v != null;
- customize.setChecked(visible);
- shortcut.setVisible(visible);
+ boolean visible = !TextUtils.isEmpty(v);
unlock.setVisible(visible);
- if (visible) {
- setSummary(shortcut, v);
- }
+ setSummary(shortcut, v);
}, buttonSetting);
- customize.setOnPreferenceChangeListener((preference, newValue) -> {
- boolean hasSetting = mTunerService.getValue(buttonSetting) != null;
- if (hasSetting != (boolean) newValue) {
- mHandler.post(() -> mTunerService.setValue(buttonSetting, hasSetting ? null : ""));
- }
- return true;
- });
- shortcut.setOnPreferenceClickListener(preference -> {
- showSelectDialog(buttonSetting);
- return true;
- });
}
private void showSelectDialog(String buttonSetting) {
@@ -129,24 +111,15 @@
mTunerService.setValue(buttonSetting, item.getSettingValue());
dialog.dismiss();
});
- LauncherApps apps = getContext().getSystemService(LauncherApps.class);
- List<LauncherActivityInfo> activities = apps.getActivityList(null,
- Process.myUserHandle());
-
- activities.forEach(info -> {
- App app = new App(getContext(), info);
- try {
- new ShortcutParser(getContext(), info.getComponentName()).getShortcuts().forEach(
- shortcut -> app.addChild(new StaticShortcut(getContext(), shortcut)));
- } catch (NameNotFoundException e) {
- }
- adapter.addItem(app);
- });
v.setAdapter(adapter);
}
private void setSummary(Preference shortcut, String value) {
+ if (value == null) {
+ shortcut.setSummary(R.string.lockscreen_none);
+ return;
+ }
if (value.contains("::")) {
Shortcut info = getShortcutInfo(getContext(), value);
shortcut.setSummary(info != null ? info.label : null);
@@ -155,7 +128,7 @@
shortcut.setSummary(info != null ? info.loadLabel(getContext().getPackageManager())
: null);
} else {
- shortcut.setSummary(null);
+ shortcut.setSummary(R.string.lockscreen_none);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/NavBarTuner.java b/packages/SystemUI/src/com/android/systemui/tuner/NavBarTuner.java
index 28a0057..45abd45 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/NavBarTuner.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/NavBarTuner.java
@@ -33,6 +33,9 @@
import android.content.DialogInterface.OnClickListener;
import android.content.res.Resources;
import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.FontMetricsInt;
+import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Bundle;
@@ -58,7 +61,7 @@
import java.util.ArrayList;
-public class NavBarTuner extends PreferenceFragment {
+public class NavBarTuner extends TunerPreferenceFragment {
private static final String LAYOUT = "layout";
private static final String LEFT = "left";
@@ -68,13 +71,13 @@
private static final String KEYCODE = "keycode";
private static final String ICON = "icon";
- private static final int[] ICONS = new int[]{
- R.drawable.ic_qs_circle,
- R.drawable.ic_add,
- R.drawable.ic_remove,
- R.drawable.ic_left,
- R.drawable.ic_right,
- R.drawable.ic_menu,
+ private static final int[][] ICONS = new int[][]{
+ {R.drawable.ic_qs_circle, R.string.tuner_circle},
+ {R.drawable.ic_add, R.string.tuner_plus},
+ {R.drawable.ic_remove, R.string.tuner_minus},
+ {R.drawable.ic_left, R.string.tuner_left},
+ {R.drawable.ic_right, R.string.tuner_right},
+ {R.drawable.ic_menu, R.string.tuner_menu},
};
private final ArrayList<Tunable> mTunables = new ArrayList<>();
@@ -96,10 +99,8 @@
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.nav_bar_tuner);
bindLayout((ListPreference) findPreference(LAYOUT));
- bindButton((PreferenceCategory) findPreference(LEFT),
- NAV_BAR_LEFT, NAVSPACE);
- bindButton((PreferenceCategory) findPreference(RIGHT),
- NAV_BAR_RIGHT, MENU_IME);
+ bindButton(NAV_BAR_LEFT, NAVSPACE, LEFT);
+ bindButton(NAV_BAR_RIGHT, MENU_IME, RIGHT);
}
@Override
@@ -129,9 +130,8 @@
});
}
- private void bindButton(PreferenceCategory parent, String setting, String def) {
- String k = parent.getKey();
- DropDownPreference type = (DropDownPreference) findPreference(TYPE + "_" + k);
+ private void bindButton(String setting, String def, String k) {
+ ListPreference type = (ListPreference) findPreference(TYPE + "_" + k);
Preference keycode = findPreference(KEYCODE + "_" + k);
ListPreference icon = (ListPreference) findPreference(ICON + "_" + k);
setupIcons(icon);
@@ -195,8 +195,14 @@
.loadDrawable(getContext());
d.setTint(Color.BLACK);
d.setBounds(0, 0, size, size);
- ImageSpan span = new ImageSpan(d);
+ ImageSpan span = new ImageSpan(d, ImageSpan.ALIGN_BASELINE);
builder.append(" ", span, 0);
+ builder.append(" ");
+ for (int i = 0; i < ICONS.length; i++) {
+ if (ICONS[i][0] == id) {
+ builder.append(getString(ICONS[i][1]));
+ }
+ }
icon.setSummary(builder);
} catch (Exception e) {
Log.d("NavButton", "Problem with summary", e);
@@ -204,7 +210,7 @@
}
}
- private void setValue(String setting, DropDownPreference type, Preference keycode,
+ private void setValue(String setting, ListPreference type, Preference keycode,
ListPreference icon) {
String button = type.getValue();
if (KEY.equals(button)) {
@@ -226,14 +232,16 @@
getContext().getResources().getDisplayMetrics());
for (int i = 0; i < ICONS.length; i++) {
SpannableStringBuilder builder = new SpannableStringBuilder();
- Drawable d = Icon.createWithResource(getContext().getPackageName(), ICONS[i])
+ Drawable d = Icon.createWithResource(getContext().getPackageName(), ICONS[i][0])
.loadDrawable(getContext());
d.setTint(Color.BLACK);
d.setBounds(0, 0, size, size);
- ImageSpan span = new ImageSpan(d);
+ ImageSpan span = new ImageSpan(d, ImageSpan.ALIGN_BASELINE);
builder.append(" ", span, 0);
+ builder.append(" ");
+ builder.append(getString(ICONS[i][1]));
labels[i] = builder;
- values[i] = getContext().getPackageName() + "/" + ICONS[i];
+ values[i] = getContext().getPackageName() + "/" + ICONS[i][0];
}
icon.setEntries(labels);
icon.setEntryValues(values);
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/RadioListPreference.java b/packages/SystemUI/src/com/android/systemui/tuner/RadioListPreference.java
new file mode 100644
index 0000000..dc0d8bf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/tuner/RadioListPreference.java
@@ -0,0 +1,145 @@
+/*
+ * 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. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.tuner;
+
+import android.app.AlertDialog.Builder;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.Fragment;
+import android.content.Context;
+import android.content.DialogInterface.OnClickListener;
+import android.os.Bundle;
+import android.support.v14.preference.PreferenceFragment;
+import android.support.v7.preference.Preference;
+import android.support.v7.preference.PreferenceScreen;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.Toolbar;
+
+import com.android.settingslib.Utils;
+import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.R;
+
+import libcore.util.Objects;
+
+public class RadioListPreference extends CustomListPreference {
+
+ private OnClickListener mOnClickListener;
+ private CharSequence mSummary;
+
+ public RadioListPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onPrepareDialogBuilder(Builder builder, OnClickListener listener) {
+ mOnClickListener = listener;
+ }
+
+ @Override
+ public void setSummary(CharSequence summary) {
+ super.setSummary(summary);
+ mSummary = summary;
+ }
+
+ @Override
+ public CharSequence getSummary() {
+ if (mSummary == null || mSummary.toString().contains("%s")) {
+ return super.getSummary();
+ }
+ return mSummary;
+ }
+
+ @Override
+ protected Dialog onDialogCreated(DialogFragment fragment, Dialog dialog) {
+ Dialog d = new Dialog(getContext(), android.R.style.Theme_DeviceDefault_Settings);
+ Toolbar t = (Toolbar) d.findViewById(com.android.internal.R.id.action_bar);
+ View v = new View(getContext());
+ v.setId(R.id.content);
+ d.setContentView(v);
+ t.setTitle(getTitle());
+ t.setNavigationIcon(Utils.getDrawable(d.getContext(), android.R.attr.homeAsUpIndicator));
+ t.setNavigationOnClickListener(view -> d.dismiss());
+
+ RadioFragment f = new RadioFragment();
+ f.setPreference(this);
+ FragmentHostManager.get(v).getFragmentManager()
+ .beginTransaction()
+ .add(android.R.id.content, f)
+ .commit();
+ return d;
+ }
+
+ @Override
+ protected void onDialogStateRestored(DialogFragment fragment, Dialog dialog,
+ Bundle savedInstanceState) {
+ super.onDialogStateRestored(fragment, dialog, savedInstanceState);
+ View view = dialog.findViewById(R.id.content);
+ RadioFragment radioFragment = (RadioFragment) FragmentHostManager.get(view)
+ .getFragmentManager().findFragmentById(R.id.content);
+ if (radioFragment != null) {
+ radioFragment.setPreference(this);
+ }
+ }
+
+ @Override
+ protected void onDialogClosed(boolean positiveResult) {
+ super.onDialogClosed(positiveResult);
+ }
+
+ public static class RadioFragment extends TunerPreferenceFragment {
+ private RadioListPreference mListPref;
+
+ @Override
+ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+ Context context = getPreferenceManager().getContext();
+ PreferenceScreen screen = getPreferenceManager().createPreferenceScreen(context);
+ setPreferenceScreen(screen);
+ if (mListPref != null) {
+ update();
+ }
+ }
+
+ private void update() {
+ Context context = getPreferenceManager().getContext();
+
+ CharSequence[] entries = mListPref.getEntries();
+ CharSequence[] values = mListPref.getEntryValues();
+ CharSequence current = mListPref.getValue();
+ for (int i = 0; i < entries.length; i++) {
+ CharSequence entry = entries[i];
+ SelectablePreference pref = new SelectablePreference(context);
+ getPreferenceScreen().addPreference(pref);
+ pref.setTitle(entry);
+ pref.setChecked(Objects.equal(current, values[i]));
+ pref.setKey(String.valueOf(i));
+ }
+ }
+
+ @Override
+ public boolean onPreferenceTreeClick(Preference preference) {
+ mListPref.mOnClickListener.onClick(null, Integer.parseInt(preference.getKey()));
+ return true;
+ }
+
+ public void setPreference(RadioListPreference radioListPreference) {
+ mListPref = radioListPreference;
+ if (getPreferenceManager() != null) {
+ update();
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/SelectablePreference.java b/packages/SystemUI/src/com/android/systemui/tuner/SelectablePreference.java
new file mode 100644
index 0000000..1d15708
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/tuner/SelectablePreference.java
@@ -0,0 +1,45 @@
+/*
+ * 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. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.tuner;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.support.v7.preference.CheckBoxPreference;
+import android.util.TypedValue;
+
+import com.android.systemui.statusbar.ScalingDrawableWrapper;
+
+public class SelectablePreference extends CheckBoxPreference {
+ private final int mSize;
+
+ public SelectablePreference(Context context) {
+ super(context);
+ setWidgetLayoutResource(com.android.systemui.R.layout.preference_widget_radiobutton);
+ setSelectable(true);
+ mSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 32,
+ context.getResources().getDisplayMetrics());
+ }
+
+ @Override
+ public void setIcon(Drawable icon) {
+ super.setIcon(new ScalingDrawableWrapper(icon,
+ mSize / (float) icon.getIntrinsicWidth()));
+ }
+
+ @Override
+ public String toString() {
+ return "";
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/ShortcutPicker.java b/packages/SystemUI/src/com/android/systemui/tuner/ShortcutPicker.java
new file mode 100644
index 0000000..533388a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/tuner/ShortcutPicker.java
@@ -0,0 +1,200 @@
+/*
+ * 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. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.tuner;
+
+import static com.android.systemui.tuner.LockscreenFragment.LOCKSCREEN_LEFT_BUTTON;
+
+import android.content.Context;
+import android.content.pm.LauncherActivityInfo;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.graphics.drawable.ColorDrawable;
+import android.os.Bundle;
+import android.os.Process;
+import android.support.v14.preference.PreferenceFragment;
+import android.support.v7.preference.Preference;
+import android.support.v7.preference.PreferenceCategory;
+import android.support.v7.preference.PreferenceScreen;
+import android.support.v7.preference.PreferenceViewHolder;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.tuner.ShortcutParser.Shortcut;
+import com.android.systemui.tuner.TunerService.Tunable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ShortcutPicker extends PreferenceFragment implements Tunable {
+
+ private final ArrayList<SelectablePreference> mSelectablePreferences = new ArrayList<>();
+ private String mKey;
+ private SelectablePreference mNonePreference;
+ private TunerService mTunerService;
+
+ @Override
+ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+ Context context = getPreferenceManager().getContext();
+ PreferenceScreen screen = getPreferenceManager().createPreferenceScreen(context);
+ screen.setOrderingAsAdded(true);
+ PreferenceCategory otherApps = new PreferenceCategory(context);
+ otherApps.setTitle(R.string.tuner_other_apps);
+
+ mNonePreference = new SelectablePreference(context);
+ mSelectablePreferences.add(mNonePreference);
+ mNonePreference.setTitle(R.string.lockscreen_none);
+ mNonePreference.setIcon(R.drawable.ic_remove_circle);
+ screen.addPreference(mNonePreference);
+
+ LauncherApps apps = getContext().getSystemService(LauncherApps.class);
+ List<LauncherActivityInfo> activities = apps.getActivityList(null,
+ Process.myUserHandle());
+
+ screen.addPreference(otherApps);
+ activities.forEach(info -> {
+ try {
+ List<Shortcut> shortcuts = new ShortcutParser(getContext(),
+ info.getComponentName()).getShortcuts();
+ AppPreference appPreference = new AppPreference(context, info);
+ mSelectablePreferences.add(appPreference);
+ if (shortcuts.size() != 0) {
+ //PreferenceCategory category = new PreferenceCategory(context);
+ //screen.addPreference(category);
+ //category.setTitle(info.getLabel());
+ screen.addPreference(appPreference);
+ shortcuts.forEach(shortcut -> {
+ ShortcutPreference shortcutPref = new ShortcutPreference(context, shortcut,
+ info.getLabel());
+ mSelectablePreferences.add(shortcutPref);
+ screen.addPreference(shortcutPref);
+ });
+ return;
+ }
+ otherApps.addPreference(appPreference);
+ } catch (NameNotFoundException e) {
+ }
+ });
+ // Move other apps to the bottom.
+ screen.removePreference(otherApps);
+ for (int i = 0; i < otherApps.getPreferenceCount(); i++) {
+ Preference p = otherApps.getPreference(0);
+ otherApps.removePreference(p);
+ p.setOrder(Preference.DEFAULT_ORDER);
+ screen.addPreference(p);
+ }
+ //screen.addPreference(otherApps);
+
+ setPreferenceScreen(screen);
+ mKey = getArguments().getString(ARG_PREFERENCE_ROOT);
+ mTunerService = Dependency.get(TunerService.class);
+ mTunerService.addTunable(this, mKey);
+ }
+
+ @Override
+ public boolean onPreferenceTreeClick(Preference preference) {
+ mTunerService.setValue(mKey, preference.toString());
+ getActivity().onBackPressed();
+ return true;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ if (LOCKSCREEN_LEFT_BUTTON.equals(mKey)) {
+ getActivity().setTitle(R.string.lockscreen_shortcut_left);
+ } else {
+ getActivity().setTitle(R.string.lockscreen_shortcut_right);
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mTunerService.removeTunable(this);
+ }
+
+ @Override
+ public void onTuningChanged(String key, String newValue) {
+ String v = newValue != null ? newValue : "";
+ mSelectablePreferences.forEach(p -> p.setChecked(v.equals(p.toString())));
+ }
+
+ private static class AppPreference extends SelectablePreference {
+ private final LauncherActivityInfo mInfo;
+ private boolean mBinding;
+
+ public AppPreference(Context context, LauncherActivityInfo info) {
+ super(context);
+ mInfo = info;
+ setTitle(context.getString(R.string.tuner_launch_app, info.getLabel()));
+ setSummary(context.getString(R.string.tuner_app, info.getLabel()));
+ }
+
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder holder) {
+ mBinding = true;
+ if (getIcon() == null) {
+ setIcon(mInfo.getBadgedIcon(
+ getContext().getResources().getConfiguration().densityDpi));
+ }
+ mBinding = false;
+ super.onBindViewHolder(holder);
+ }
+
+ @Override
+ protected void notifyChanged() {
+ if (mBinding) return;
+ super.notifyChanged();
+ }
+
+ @Override
+ public String toString() {
+ return mInfo.getComponentName().flattenToString();
+ }
+ }
+
+ private static class ShortcutPreference extends SelectablePreference {
+ private final Shortcut mShortcut;
+ private boolean mBinding;
+
+ public ShortcutPreference(Context context, Shortcut shortcut, CharSequence appLabel) {
+ super(context);
+ mShortcut = shortcut;
+ setTitle(shortcut.label);
+ setSummary(context.getString(R.string.tuner_app, appLabel));
+ }
+
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder holder) {
+ mBinding = true;
+ if (getIcon() == null) {
+ setIcon(mShortcut.icon.loadDrawable(getContext()));
+ }
+ mBinding = false;
+ super.onBindViewHolder(holder);
+ }
+
+ @Override
+ protected void notifyChanged() {
+ if (mBinding) return;
+ super.notifyChanged();
+ }
+
+ @Override
+ public String toString() {
+ return mShortcut.toString();
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java
index 74280a3..4eb1db6 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java
@@ -22,10 +22,12 @@
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceScreen;
import android.util.Log;
+import android.view.MenuItem;
import com.android.settingslib.drawer.SettingsDrawerActivity;
import com.android.systemui.Dependency;
import com.android.systemui.R;
+import com.android.systemui.fragments.FragmentService;
public class TunerActivity extends SettingsDrawerActivity implements
PreferenceFragment.OnPreferenceStartFragmentCallback,
@@ -51,6 +53,22 @@
}
@Override
+ protected void onDestroy() {
+ super.onDestroy();
+ Dependency.destroy(FragmentService.class, s -> s.destroyAll());
+ Dependency.clearDependencies();
+ }
+
+ @Override
+ public boolean onMenuItemSelected(int featureId, MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ onBackPressed();
+ return true;
+ }
+ return super.onMenuItemSelected(featureId, item);
+ }
+
+ @Override
public void onBackPressed() {
if (!getFragmentManager().popBackStackImmediate()) {
super.onBackPressed();
@@ -62,6 +80,9 @@
try {
Class<?> cls = Class.forName(pref.getFragment());
Fragment fragment = (Fragment) cls.newInstance();
+ final Bundle b = new Bundle(1);
+ b.putString(PreferenceFragment.ARG_PREFERENCE_ROOT, pref.getKey());
+ fragment.setArguments(b);
FragmentTransaction transaction = getFragmentManager().beginTransaction();
setTitle(pref.getTitle());
transaction.replace(R.id.content_frame, fragment);
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerPreferenceFragment.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerPreferenceFragment.java
new file mode 100644
index 0000000..06d40da
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerPreferenceFragment.java
@@ -0,0 +1,36 @@
+/*
+ * 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. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.tuner;
+
+import android.app.DialogFragment;
+import android.os.Bundle;
+import android.support.v14.preference.PreferenceFragment;
+import android.support.v7.preference.Preference;
+
+public abstract class TunerPreferenceFragment extends PreferenceFragment {
+
+ @Override
+ public void onDisplayPreferenceDialog(Preference preference) {
+ DialogFragment f = null;
+ if (preference instanceof CustomListPreference) {
+ f = CustomListPreference.CustomListPreferenceDialogFragment
+ .newInstance(preference.getKey());
+ } else {
+ super.onDisplayPreferenceDialog(preference);
+ }
+ f.setTargetFragment(this, 0);
+ f.show(getFragmentManager(), "dialog_preference");
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginInstanceManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginInstanceManagerTest.java
index 3715df2..658966c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginInstanceManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginInstanceManagerTest.java
@@ -17,14 +17,27 @@
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertTrue;
-
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.PluginInstanceManager.PluginInfo;
+import com.android.systemui.plugins.VersionInfo.InvalidVersionException;
+import com.android.systemui.plugins.annotations.Requires;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
import android.app.Activity;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
@@ -35,7 +48,6 @@
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.os.HandlerThread;
@@ -44,17 +56,6 @@
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
-import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.plugins.PluginInstanceManager.PluginInfo;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mockito;
-
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -72,6 +73,7 @@
private PluginListener mMockListener;
private PluginInstanceManager mPluginInstanceManager;
private PluginManager mMockManager;
+ private VersionInfo mMockVersionInfo;
@Before
public void setup() throws Exception {
@@ -83,8 +85,10 @@
mMockManager = mock(PluginManager.class);
when(mMockManager.getClassLoader(any(), any()))
.thenReturn(getClass().getClassLoader());
+ mMockVersionInfo = mock(VersionInfo.class);
mPluginInstanceManager = new PluginInstanceManager(mContextWrapper, mMockPm, "myAction",
- mMockListener, true, mHandlerThread.getLooper(), 1, mMockManager, true);
+ mMockListener, true, mHandlerThread.getLooper(), mMockVersionInfo,
+ mMockManager, true);
sMockPlugin = mock(Plugin.class);
when(sMockPlugin.getVersion()).thenReturn(1);
}
@@ -145,7 +149,7 @@
NotificationManager nm = mock(NotificationManager.class);
mContext.addMockSystemService(Context.NOTIFICATION_SERVICE, nm);
setupFakePmQuery();
- when(sMockPlugin.getVersion()).thenReturn(2);
+ doThrow(new InvalidVersionException("", false)).when(mMockVersionInfo).checkVersion(any());
mPluginInstanceManager.loadAll();
@@ -181,7 +185,8 @@
public void testNonDebuggable() throws Exception {
// Create a version that thinks the build is not debuggable.
mPluginInstanceManager = new PluginInstanceManager(mContextWrapper, mMockPm, "myAction",
- mMockListener, true, mHandlerThread.getLooper(), 1, mMockManager, false);
+ mMockListener, true, mHandlerThread.getLooper(), mMockVersionInfo,
+ mMockManager, false);
setupFakePmQuery();
mPluginInstanceManager.loadAll();
@@ -270,6 +275,9 @@
}
}
+ // This target class doesn't matter, it just needs to have a Requires to hit the flow where
+ // the mock version info is called.
+ @Requires(target = PluginManagerTest.class, version = 1)
public static class TestPlugin implements Plugin {
@Override
public int getVersion() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginManagerTest.java
index a58407b..09ac5a6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginManagerTest.java
@@ -32,6 +32,7 @@
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.annotations.ProvidesInterface;
import com.android.systemui.plugins.PluginInstanceManager.PluginInfo;
import com.android.systemui.plugins.PluginManager.PluginInstanceManagerFactory;
@@ -63,7 +64,7 @@
mMockFactory = mock(PluginInstanceManagerFactory.class);
mMockPluginInstance = mock(PluginInstanceManager.class);
when(mMockFactory.createPluginInstanceManager(Mockito.any(), Mockito.any(), Mockito.any(),
- Mockito.anyBoolean(), Mockito.any(), Mockito.anyInt(), Mockito.any()))
+ Mockito.anyBoolean(), Mockito.any(), Mockito.any(), Mockito.any()))
.thenReturn(mMockPluginInstance);
mPluginManager = new PluginManager(getContext(), mMockFactory, true, mMockExceptionHandler);
resetExceptionHandler();
@@ -76,20 +77,20 @@
Plugin mockPlugin = mock(Plugin.class);
when(mMockPluginInstance.getPlugin()).thenReturn(new PluginInfo(null, null, mockPlugin,
null));
- Plugin result = mPluginManager.getOneShotPlugin("myAction", 1);
+ Plugin result = mPluginManager.getOneShotPlugin("myAction", TestPlugin.class);
assertTrue(result == mockPlugin);
}
@Test
public void testAddListener() {
- mPluginManager.addPluginListener("myAction", mMockListener, 1);
+ mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class);
verify(mMockPluginInstance).loadAll();
}
@Test
public void testRemoveListener() {
- mPluginManager.addPluginListener("myAction", mMockListener, 1);
+ mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class);
mPluginManager.removePluginListener(mMockListener);
verify(mMockPluginInstance).destroy();
@@ -101,16 +102,16 @@
mMockExceptionHandler);
resetExceptionHandler();
- mPluginManager.addPluginListener("myAction", mMockListener, 1);
+ mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class);
verify(mMockPluginInstance, Mockito.never()).loadAll();
- assertNull(mPluginManager.getOneShotPlugin("myPlugin", 1));
+ assertNull(mPluginManager.getOneShotPlugin("myPlugin", TestPlugin.class));
verify(mMockPluginInstance, Mockito.never()).getPlugin();
}
@Test
public void testExceptionHandler_foundPlugin() {
- mPluginManager.addPluginListener("myAction", mMockListener, 1);
+ mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class);
when(mMockPluginInstance.checkAndDisable(Mockito.any())).thenReturn(true);
mPluginExceptionHandler.uncaughtException(Thread.currentThread(), new Throwable());
@@ -125,7 +126,7 @@
@Test
public void testExceptionHandler_noFoundPlugin() {
- mPluginManager.addPluginListener("myAction", mMockListener, 1);
+ mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class);
when(mMockPluginInstance.checkAndDisable(Mockito.any())).thenReturn(false);
mPluginExceptionHandler.uncaughtException(Thread.currentThread(), new Throwable());
@@ -161,4 +162,10 @@
// Set back the real exception handler so the test can crash if it wants to.
Thread.setDefaultUncaughtExceptionHandler(mRealExceptionHandler);
}
+
+ @ProvidesInterface(action = TestPlugin.ACTION, version = TestPlugin.VERSION)
+ public static interface TestPlugin extends Plugin {
+ public static final String ACTION = "testAction";
+ public static final int VERSION = 1;
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/plugins/VersionInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/plugins/VersionInfoTest.java
new file mode 100644
index 0000000..0d87d6b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/plugins/VersionInfoTest.java
@@ -0,0 +1,103 @@
+/*
+ * 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. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.plugins;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.VersionInfo.InvalidVersionException;
+import com.android.systemui.plugins.annotations.Requires;
+import com.android.systemui.plugins.qs.QS;
+import com.android.systemui.plugins.qs.QS.Callback;
+import com.android.systemui.plugins.qs.QS.DetailAdapter;
+import com.android.systemui.plugins.qs.QS.HeightListener;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+public class VersionInfoTest extends SysuiTestCase {
+
+ @Rule
+ public ExpectedException mThrown = ExpectedException.none();
+
+ @Test
+ public void testHasInfo() {
+ VersionInfo info = new VersionInfo();
+ info.addClass(VersionInfoTest.class); // Has no annotations.
+ assertFalse(info.hasVersionInfo());
+
+ info.addClass(OverlayPlugin.class);
+ assertTrue(info.hasVersionInfo());
+ }
+
+ @Test
+ public void testSingleProvides() {
+ VersionInfo overlay = new VersionInfo().addClass(OverlayPlugin.class);
+ VersionInfo impl = new VersionInfo().addClass(OverlayImpl.class);
+ overlay.checkVersion(impl);
+ }
+
+ @Test
+ public void testIncorrectVersion() {
+ VersionInfo overlay = new VersionInfo().addClass(OverlayPlugin.class);
+ VersionInfo impl = new VersionInfo().addClass(OverlayImplIncorrectVersion.class);
+ mThrown.expect(InvalidVersionException.class);
+ overlay.checkVersion(impl);
+ }
+
+ @Test
+ public void testMissingRequired() {
+ VersionInfo overlay = new VersionInfo().addClass(OverlayPlugin.class);
+ VersionInfo impl = new VersionInfo();
+ mThrown.expect(InvalidVersionException.class);
+ overlay.checkVersion(impl);
+ }
+
+ @Test
+ public void testMissingDependencies() {
+ VersionInfo overlay = new VersionInfo().addClass(QS.class);
+ VersionInfo impl = new VersionInfo().addClass(QSImplNoDeps.class);
+ mThrown.expect(InvalidVersionException.class);
+ overlay.checkVersion(impl);
+ }
+
+ @Test
+ public void testHasDependencies() {
+ VersionInfo overlay = new VersionInfo().addClass(QS.class);
+ VersionInfo impl = new VersionInfo().addClass(QSImpl.class);
+ overlay.checkVersion(impl);
+ }
+
+ @Requires(target = OverlayPlugin.class, version = OverlayPlugin.VERSION)
+ public static class OverlayImpl {
+ }
+
+ @Requires(target = OverlayPlugin.class, version = 0)
+ public static class OverlayImplIncorrectVersion {
+ }
+
+ @Requires(target = QS.class, version = QS.VERSION)
+ public static class QSImplNoDeps {
+ }
+
+ @Requires(target = QS.class, version = QS.VERSION)
+ @Requires(target = Callback.class, version = Callback.VERSION)
+ @Requires(target = HeightListener.class, version = HeightListener.VERSION)
+ @Requires(target = DetailAdapter.class, version = DetailAdapter.VERSION)
+ public static class QSImpl {
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java
index 4146cb81..70c7d3e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java
@@ -18,26 +18,32 @@
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
import android.content.ComponentName;
-import android.content.Context;
import android.os.Looper;
import android.service.quicksettings.Tile;
-import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.systemui.SysUIRunner;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.statusbar.phone.QSTileHost;
-import com.android.systemui.statusbar.policy.DataSaverController;
-import com.android.systemui.statusbar.policy.HotspotController;
-import com.android.systemui.statusbar.policy.NetworkController;
-import java.util.ArrayList;
+import com.android.systemui.statusbar.phone.StatusBarIconController;
+import com.android.systemui.utils.TestableLooper;
+import com.android.systemui.utils.TestableLooper.RunWithLooper;
+
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
+import java.util.ArrayList;
+
@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(SysUIRunner.class)
+@RunWithLooper(setAsMainLooper = true)
public class TileServicesTest extends SysuiTestCase {
private static int NUM_FAKES = TileServices.DEFAULT_MAX_BOUND * 2;
@@ -46,16 +52,24 @@
@Before
public void setUp() throws Exception {
+ TestableLooper.get(this).setAsMainLooper();
mManagers = new ArrayList<>();
- QSTileHost host = new QSTileHost(mContext, null, null);
+ QSTileHost host = new QSTileHost(mContext, null,
+ mock(StatusBarIconController.class));
mTileService = new TestTileServices(host, Looper.getMainLooper());
}
+ @After
+ public void tearDown() throws Exception {
+ mTileService.getHost().destroy();
+ TestableLooper.get(this).processAllMessages();
+ }
+
@Test
public void testRecalculateBindAllowance() {
// Add some fake tiles.
for (int i = 0; i < NUM_FAKES; i++) {
- mTileService.getTileWrapper(Mockito.mock(CustomTile.class));
+ mTileService.getTileWrapper(mock(CustomTile.class));
}
assertEquals(NUM_FAKES, mManagers.size());
@@ -91,7 +105,7 @@
@Test
public void testCalcFew() {
for (int i = 0; i < TileServices.DEFAULT_MAX_BOUND - 1; i++) {
- mTileService.getTileWrapper(Mockito.mock(CustomTile.class));
+ mTileService.getTileWrapper(mock(CustomTile.class));
}
mTileService.recalculateBindAllowance();
@@ -115,7 +129,7 @@
@Override
protected TileServiceManager onCreateTileService(ComponentName component, Tile qsTile) {
- TileServiceManager manager = Mockito.mock(TileServiceManager.class);
+ TileServiceManager manager = mock(TileServiceManager.class);
mManagers.add(manager);
return manager;
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakePluginManager.java b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakePluginManager.java
index d1abcca..59a9361 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakePluginManager.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakePluginManager.java
@@ -31,13 +31,7 @@
@Override
public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
- int version) {
- mLeakChecker.addCallback(listener);
- }
-
- @Override
- public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
- int version, boolean allowMultiple) {
+ Class cls, boolean allowMultiple) {
mLeakChecker.addCallback(listener);
}
@@ -47,7 +41,7 @@
}
@Override
- public <T extends Plugin> T getOneShotPlugin(String action, int version) {
+ public <T extends Plugin> T getOneShotPlugin(String action, Class<?> cls) {
return null;
}
}
diff --git a/proto/src/metrics_constants.proto b/proto/src/metrics_constants.proto
index 653e80a..245bf9e 100644
--- a/proto/src/metrics_constants.proto
+++ b/proto/src/metrics_constants.proto
@@ -41,6 +41,9 @@
// The view or control was dismissed.
TYPE_DISMISS = 5;
+
+ // The view or control was updated.
+ TYPE_UPDATE = 6;
}
// Known visual elements: views or controls.
@@ -3397,6 +3400,16 @@
// ACTION: A tile in Settings information architecture is clicked
ACTION_SETTINGS_TILE_CLICK = 830;
+ // OPEN: Notification unsnoozed. CLOSE: Notification snoozed. UPDATE: snoozed notification
+ // updated
+ // CATEGORY: NOTIFICATION
+ // OS: O
+ NOTIFICATION_SNOOZED = 831;
+
+ // Tagged data for NOTIFICATION_SNOOZED. TRUE: snoozed until context, FALSE: snoozed for time.
+ // OS: O
+ NOTIFICATION_SNOOZED_CRITERIA = 832;
+
// ---- 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/LockSettingsService.java b/services/core/java/com/android/server/LockSettingsService.java
index a073d8e..4a44530 100644
--- a/services/core/java/com/android/server/LockSettingsService.java
+++ b/services/core/java/com/android/server/LockSettingsService.java
@@ -20,6 +20,8 @@
import static android.Manifest.permission.READ_CONTACTS;
import static android.content.Context.KEYGUARD_SERVICE;
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT;
+import static com.android.internal.widget.LockPatternUtils.SYNTHETIC_PASSWORD_ENABLED_KEY;
+import static com.android.internal.widget.LockPatternUtils.SYNTHETIC_PASSWORD_HANDLE_KEY;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
@@ -61,6 +63,7 @@
import android.provider.Settings;
import android.provider.Settings.Secure;
import android.provider.Settings.SettingNotFoundException;
+import android.security.GateKeeper;
import android.security.KeyStore;
import android.security.keystore.AndroidKeyStoreProvider;
import android.security.keystore.KeyProperties;
@@ -79,6 +82,8 @@
import com.android.internal.widget.LockPatternUtils;
import com.android.internal.widget.VerifyCredentialResponse;
import com.android.server.LockSettingsStorage.CredentialHash;
+import com.android.server.SyntheticPasswordManager.AuthenticationResult;
+import com.android.server.SyntheticPasswordManager.AuthenticationToken;
import libcore.util.HexEncoding;
@@ -86,6 +91,7 @@
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.IOException;
+import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
@@ -145,12 +151,14 @@
private boolean mFirstCallToVold;
protected IGateKeeperService mGateKeeperService;
+ private SyntheticPasswordManager mSpManager;
+
/**
* The UIDs that are used for system credential storage in keystore.
*/
private static final int[] SYSTEM_CREDENTIAL_UIDS = {
Process.WIFI_UID, Process.VPN_UID,
- Process.ROOT_UID, Process.SYSTEM_UID };
+ Process.ROOT_UID };
// This class manages life cycle events for encrypted users on File Based Encryption (FBE)
// devices. The most basic of these is to show/hide notifications about missing features until
@@ -335,6 +343,14 @@
}
return null;
}
+
+ public SyntheticPasswordManager getSyntheticPasswordManager(LockSettingsStorage storage) {
+ return new SyntheticPasswordManager(storage);
+ }
+
+ public int binderGetCallingUid() {
+ return Binder.getCallingUid();
+ }
}
public LockSettingsService(Context context) {
@@ -365,6 +381,8 @@
mUserManager = injector.getUserManager();
mStrongAuthTracker = injector.getStrongAuthTracker();
mStrongAuthTracker.register(mStrongAuth);
+
+ mSpManager = injector.getSyntheticPasswordManager(mStorage);
}
/**
@@ -801,17 +819,42 @@
@Override
public boolean havePassword(int userId) throws RemoteException {
+ synchronized (mSpManager) {
+ if (isSyntheticPasswordBasedCredentialLocked(userId)) {
+ long handle = getSyntheticPasswordHandleLocked(userId);
+ return mSpManager.getCredentialType(handle, userId) ==
+ LockPatternUtils.CREDENTIAL_TYPE_PASSWORD;
+ }
+ }
// Do we need a permissions check here?
return mStorage.hasPassword(userId);
}
@Override
public boolean havePattern(int userId) throws RemoteException {
+ synchronized (mSpManager) {
+ if (isSyntheticPasswordBasedCredentialLocked(userId)) {
+ long handle = getSyntheticPasswordHandleLocked(userId);
+ return mSpManager.getCredentialType(handle, userId) ==
+ LockPatternUtils.CREDENTIAL_TYPE_PATTERN;
+ }
+ }
// Do we need a permissions check here?
return mStorage.hasPattern(userId);
}
private boolean isUserSecure(int userId) {
+ synchronized (mSpManager) {
+ try {
+ if (isSyntheticPasswordBasedCredentialLocked(userId)) {
+ long handle = getSyntheticPasswordHandleLocked(userId);
+ return mSpManager.getCredentialType(handle, userId) !=
+ LockPatternUtils.CREDENTIAL_TYPE_NONE;
+ }
+ } catch (RemoteException e) {
+ // fall through
+ }
+ }
return mStorage.hasCredential(userId);
}
@@ -1021,6 +1064,13 @@
private void setLockCredentialInternal(String credential, int credentialType,
String savedCredential, int userId) throws RemoteException {
+ synchronized (mSpManager) {
+ if (isSyntheticPasswordBasedCredentialLocked(userId)) {
+ spBasedSetLockCredentialInternalLocked(credential, credentialType, savedCredential,
+ userId);
+ return;
+ }
+ }
if (credentialType == LockPatternUtils.CREDENTIAL_TYPE_NONE) {
if (credential != null) {
Slog.wtf(TAG, "CredentialType is none, but credential is non-null.");
@@ -1061,7 +1111,16 @@
savedCredential = null;
}
}
-
+ synchronized (mSpManager) {
+ if (shouldMigrateToSyntheticPasswordLocked(userId)) {
+ initializeSyntheticPasswordLocked(currentHandle.hash, savedCredential,
+ currentHandle.type, userId);
+ spBasedSetLockCredentialInternalLocked(credential, credentialType, savedCredential,
+ userId);
+ return;
+ }
+ }
+ if (DEBUG) Slog.d(TAG, "setLockCredentialInternal: user=" + userId);
byte[] enrolledHandle = enrollCredential(currentHandle.hash, savedCredential, credential,
userId);
if (enrolledHandle != null) {
@@ -1189,6 +1248,11 @@
return hash;
}
+ private void setAuthlessUserKeyProtection(int userId, byte[] key) throws RemoteException {
+ if (DEBUG) Slog.d(TAG, "setAuthlessUserKeyProtectiond: user=" + userId);
+ addUserKeyAuth(userId, null, key);
+ }
+
private void setUserKeyProtection(int userId, String credential, VerifyCredentialResponse vcr)
throws RemoteException {
if (DEBUG) Slog.d(TAG, "setUserKeyProtection: user=" + userId);
@@ -1320,7 +1384,16 @@
if (TextUtils.isEmpty(credential)) {
throw new IllegalArgumentException("Credential can't be null or empty");
}
-
+ synchronized (mSpManager) {
+ if (isSyntheticPasswordBasedCredentialLocked(userId)) {
+ VerifyCredentialResponse response = spBasedDoVerifyCredentialLocked(credential,
+ credentialType, hasChallenge, challenge, userId, progressCallback);
+ if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) {
+ mStrongAuth.reportSuccessfulStrongAuthUnlock(userId);
+ }
+ return response;
+ }
+ }
CredentialHash storedHash = mStorage.readCredentialHash(userId);
if (storedHash.type != credentialType) {
Slog.wtf(TAG, "doVerifyCredential type mismatch with stored credential??"
@@ -1456,8 +1529,8 @@
notifyActivePasswordMetricsAvailable(credential, userId);
unlockKeystore(credential, userId);
- Slog.i(TAG, "Unlocking user " + userId +
- " with token length " + response.getPayload().length);
+ Slog.i(TAG, "Unlocking user " + userId + " with token length "
+ + response.getPayload().length);
unlockUser(userId, response.getPayload(), secretFromCredential(credential));
if (isManagedProfileWithSeparatedLock(userId)) {
@@ -1467,6 +1540,15 @@
}
if (shouldReEnroll) {
setLockCredentialInternal(credential, storedHash.type, credential, userId);
+ } else {
+ // Now that we've cleared of all required GK migration, let's do the final
+ // migration to synthetic password.
+ synchronized (mSpManager) {
+ if (shouldMigrateToSyntheticPasswordLocked(userId)) {
+ initializeSyntheticPasswordLocked(storedHash.hash, credential,
+ storedHash.type, userId);
+ }
+ }
}
} else if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_RETRY) {
if (response.getTimeout() > 0) {
@@ -1697,7 +1779,7 @@
}
}
- private synchronized IGateKeeperService getGateKeeperService()
+ protected synchronized IGateKeeperService getGateKeeperService()
throws RemoteException {
if (mGateKeeperService != null) {
return mGateKeeperService;
@@ -1713,4 +1795,431 @@
Slog.e(TAG, "Unable to acquire GateKeeperService");
return null;
}
+
+ /**
+ * Precondition: vold and keystore unlocked.
+ *
+ * Create new synthetic password, set up synthetic password blob protected by the supplied
+ * user credential, and make the newly-created SP blob active.
+ *
+ * The invariant under a synthetic password is:
+ * 1. If user credential exists, then both vold and keystore and protected with keys derived
+ * from the synthetic password.
+ * 2. If user credential does not exist, vold and keystore protection are cleared. This is to
+ * make it consistent with current behaviour. It also allows ActivityManager to call
+ * unlockUser() with empty secret.
+ * 3. Once a user is migrated to have synthetic password, its value will never change, no matter
+ * whether the user changes his lockscreen PIN or clear/reset it. When the user clears its
+ * lockscreen PIN, we still maintain the existing synthetic password in a password blob
+ * protected by a default PIN. The only exception is when the DPC performs an untrusted
+ * credential change, in which case we have no way to derive the existing synthetic password
+ * and has to create a new one.
+ * 4. The user SID is linked with synthetic password, but its cleared/re-created when the user
+ * clears/re-creates his lockscreen PIN.
+ *
+ *
+ * Different cases of calling this method:
+ * 1. credentialHash != null
+ * This implies credential != null, a new SP blob will be provisioned, and existing SID
+ * migrated to associate with the new SP.
+ * This happens during a normal migration case when the user currently has password.
+ *
+ * 2. credentialhash == null and credential == null
+ * A new SP blob and a new SID will be created, while the user has no credentials.
+ * This can happens when we are activating an escrow token on a unsecured device, during
+ * which we want to create the SP structure with an empty user credential.
+ *
+ * 3. credentialhash == null and credential != null
+ * This is the untrusted credential reset, OR the user sets a new lockscreen password
+ * FOR THE FIRST TIME on a SP-enabled device. New credential and new SID will be created
+ */
+ private AuthenticationToken initializeSyntheticPasswordLocked(byte[] credentialHash,
+ String credential, int credentialType, int userId) throws RemoteException {
+ Slog.i(TAG, "Initialize SyntheticPassword for user: " + userId);
+ AuthenticationToken auth = mSpManager.newSyntheticPasswordAndSid(getGateKeeperService(),
+ credentialHash, credential, userId);
+ if (auth == null) {
+ Slog.wtf(TAG, "initializeSyntheticPasswordLocked returns null auth token");
+ return null;
+ }
+ long handle = mSpManager.createPasswordBasedSyntheticPassword(getGateKeeperService(),
+ credential, credentialType, auth, userId);
+ if (credential != null) {
+ if (credentialHash == null) {
+ // Since when initializing SP, we didn't provide an existing password handle
+ // for it to migrate SID, we need to create a new SID for the user.
+ mSpManager.newSidForUser(getGateKeeperService(), auth, userId);
+ }
+ mSpManager.verifyChallenge(getGateKeeperService(), auth, 0L, userId);
+ setAuthlessUserKeyProtection(userId, auth.deriveDiskEncryptionKey());
+ setKeystorePassword(auth.deriveKeyStorePassword(), userId);
+ } else {
+ clearUserKeyProtection(userId);
+ setKeystorePassword(null, userId);
+ getGateKeeperService().clearSecureUserId(userId);
+ }
+ fixateNewestUserKeyAuth(userId);
+ setLong(SYNTHETIC_PASSWORD_HANDLE_KEY, handle, userId);
+ return auth;
+ }
+
+ private long getSyntheticPasswordHandleLocked(int userId) {
+ try {
+ return getLong(SYNTHETIC_PASSWORD_HANDLE_KEY, 0, userId);
+ } catch (RemoteException e) {
+ return SyntheticPasswordManager.DEFAULT_HANDLE;
+ }
+ }
+
+ private boolean isSyntheticPasswordBasedCredentialLocked(int userId) throws RemoteException {
+ long handle = getSyntheticPasswordHandleLocked(userId);
+ // This is a global setting
+ long enabled = getLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 0, UserHandle.USER_SYSTEM);
+ return enabled != 0 && handle != SyntheticPasswordManager.DEFAULT_HANDLE;
+ }
+
+ private boolean shouldMigrateToSyntheticPasswordLocked(int userId) throws RemoteException {
+ long handle = getSyntheticPasswordHandleLocked(userId);
+ // This is a global setting
+ long enabled = getLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 0, UserHandle.USER_SYSTEM);
+ return enabled != 0 && handle == SyntheticPasswordManager.DEFAULT_HANDLE;
+ }
+
+ private void enableSyntheticPasswordLocked() throws RemoteException {
+ setLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 1, UserHandle.USER_SYSTEM);
+ }
+
+ private VerifyCredentialResponse spBasedDoVerifyCredentialLocked(String userCredential, int
+ credentialType, boolean hasChallenge, long challenge, int userId,
+ ICheckCredentialProgressCallback progressCallback) throws RemoteException {
+ if (DEBUG) Slog.d(TAG, "spBasedDoVerifyCredentialLocked: user=" + userId);
+ if (credentialType == LockPatternUtils.CREDENTIAL_TYPE_NONE) {
+ userCredential = null;
+ }
+ long handle = getSyntheticPasswordHandleLocked(userId);
+ AuthenticationResult authResult = mSpManager.unwrapPasswordBasedSyntheticPassword(
+ getGateKeeperService(), handle, userCredential, userId);
+
+ VerifyCredentialResponse response = authResult.gkResponse;
+ if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) {
+ // credential has matched
+ // perform verifyChallenge with synthetic password which generates the real auth
+ // token for the current user
+ response = mSpManager.verifyChallenge(getGateKeeperService(), authResult.authToken,
+ challenge, userId);
+ if (response.getResponseCode() != VerifyCredentialResponse.RESPONSE_OK) {
+ Slog.wtf(TAG, "verifyChallenge with SP failed.");
+ return VerifyCredentialResponse.ERROR;
+ }
+ if (progressCallback != null) {
+ progressCallback.onCredentialVerified();
+ }
+ notifyActivePasswordMetricsAvailable(userCredential, userId);
+ unlockKeystore(authResult.authToken.deriveKeyStorePassword(), userId);
+
+ final byte[] secret = authResult.authToken.deriveDiskEncryptionKey();
+ Slog.i(TAG, "Unlocking user " + userId + " with secret only, length " + secret.length);
+ unlockUser(userId, null, secret);
+
+ if (isManagedProfileWithSeparatedLock(userId)) {
+ TrustManager trustManager =
+ (TrustManager) mContext.getSystemService(Context.TRUST_SERVICE);
+ trustManager.setDeviceLockedForUser(userId, false);
+ }
+ activateEscrowTokens(authResult.authToken, userId);
+ } else if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_RETRY) {
+ if (response.getTimeout() > 0) {
+ requireStrongAuth(STRONG_AUTH_REQUIRED_AFTER_LOCKOUT, userId);
+ }
+ }
+
+ return response;
+ }
+
+ /**
+ * Change the user's lockscreen password by creating a new SP blob and update the handle, based
+ * on an existing authentication token. Even though a new SP blob is created, the underlying
+ * synthetic password is never changed.
+ *
+ * When clearing credential, we keep the SP unchanged, but clear its password handle so its
+ * SID is gone. We also clear password from (software-based) keystore and vold, which will be
+ * added back when new password is set in future.
+ */
+ private long setLockCredentialWithAuthTokenLocked(String credential, int credentialType,
+ AuthenticationToken auth, int userId) throws RemoteException {
+ if (DEBUG) Slog.d(TAG, "setLockCredentialWithAuthTokenLocked: user=" + userId);
+ long newHandle = mSpManager.createPasswordBasedSyntheticPassword(getGateKeeperService(),
+ credential, credentialType, auth, userId);
+ final Map<Integer, String> profilePasswords;
+ if (credential != null) {
+ // // not needed by synchronizeUnifiedWorkChallengeForProfiles()
+ profilePasswords = null;
+
+ if (mSpManager.hasSidForUser(userId)) {
+ // We are changing password of a secured device, nothing more needed as
+ // createPasswordBasedSyntheticPassword has already taken care of maintaining
+ // the password handle and SID unchanged.
+
+ //refresh auth token
+ mSpManager.verifyChallenge(getGateKeeperService(), auth, 0L, userId);
+ } else {
+ // A new password is set on a previously-unsecured device, we need to generate
+ // a new SID, and re-add keys to vold and keystore.
+ mSpManager.newSidForUser(getGateKeeperService(), auth, userId);
+ mSpManager.verifyChallenge(getGateKeeperService(), auth, 0L, userId);
+ setAuthlessUserKeyProtection(userId, auth.deriveDiskEncryptionKey());
+ fixateNewestUserKeyAuth(userId);
+ setKeystorePassword(auth.deriveKeyStorePassword(), userId);
+ }
+ } else {
+ // Cache all profile password if they use unified work challenge. This will later be
+ // used to clear the profile's password in synchronizeUnifiedWorkChallengeForProfiles()
+ profilePasswords = getDecryptedPasswordsForAllTiedProfiles(userId);
+
+ // we are clearing password of a secured device, so need to nuke SID as well.
+ mSpManager.clearSidForUser(userId);
+ getGateKeeperService().clearSecureUserId(userId);
+ // Clear key from vold so ActivityManager can just unlock the user with empty secret
+ // during boot.
+ clearUserKeyProtection(userId);
+ fixateNewestUserKeyAuth(userId);
+ setKeystorePassword(null, userId);
+ }
+ setLong(SYNTHETIC_PASSWORD_HANDLE_KEY, newHandle, userId);
+ synchronizeUnifiedWorkChallengeForProfiles(userId, profilePasswords);
+ return newHandle;
+ }
+
+ private void spBasedSetLockCredentialInternalLocked(String credential, int credentialType,
+ String savedCredential, int userId) throws RemoteException {
+ if (DEBUG) Slog.d(TAG, "spBasedSetLockCredentialInternalLocked: user=" + userId);
+ if (isManagedProfileWithUnifiedLock(userId)) {
+ // get credential from keystore when managed profile has unified lock
+ try {
+ savedCredential = getDecryptedPasswordForTiedProfile(userId);
+ } catch (FileNotFoundException e) {
+ Slog.i(TAG, "Child profile key not found");
+ } catch (UnrecoverableKeyException | InvalidKeyException | KeyStoreException
+ | NoSuchAlgorithmException | NoSuchPaddingException
+ | InvalidAlgorithmParameterException | IllegalBlockSizeException
+ | BadPaddingException | CertificateException | IOException e) {
+ Slog.e(TAG, "Failed to decrypt child profile key", e);
+ }
+ }
+ long handle = getSyntheticPasswordHandleLocked(userId);
+ AuthenticationToken auth = mSpManager.unwrapPasswordBasedSyntheticPassword(
+ getGateKeeperService(), handle, savedCredential, userId).authToken;
+ if (auth != null) {
+ // We are performing a trusted credential change i.e. a correct existing credential
+ // is provided
+ setLockCredentialWithAuthTokenLocked(credential, credentialType, auth, userId);
+ mSpManager.destroyPasswordBasedSyntheticPassword(handle, userId);
+ } else {
+ // We are performing an untrusted credential change i.e. by DevicePolicyManager.
+ // So provision a new SP and SID. This would invalidate existing escrow tokens.
+ // Still support this for now but this flow will be removed in the next release.
+
+ Slog.w(TAG, "Untrusted credential change invoked");
+ initializeSyntheticPasswordLocked(null, credential, credentialType, userId);
+ synchronizeUnifiedWorkChallengeForProfiles(userId, null);
+ mSpManager.destroyPasswordBasedSyntheticPassword(handle, userId);
+ }
+ notifyActivePasswordMetricsAvailable(credential, userId);
+
+ }
+
+ @Override
+ public long addEscrowToken(byte[] token, int userId) throws RemoteException {
+ ensureCallerSystemUid();
+ if (DEBUG) Slog.d(TAG, "addEscrowToken: user=" + userId);
+ synchronized (mSpManager) {
+ enableSyntheticPasswordLocked();
+ // Migrate to synthetic password based credentials if ther user has no password,
+ // the token can then be activated immediately.
+ AuthenticationToken auth = null;
+ if (!isUserSecure(userId)) {
+ if (shouldMigrateToSyntheticPasswordLocked(userId)) {
+ auth = initializeSyntheticPasswordLocked(null, null,
+ LockPatternUtils.CREDENTIAL_TYPE_NONE, userId);
+ } else /* isSyntheticPasswordBasedCredentialLocked(userId) */ {
+ long pwdHandle = getSyntheticPasswordHandleLocked(userId);
+ auth = mSpManager.unwrapPasswordBasedSyntheticPassword(getGateKeeperService(),
+ pwdHandle, null, userId).authToken;
+ }
+ }
+ disableEscrowTokenOnNonManagedDevicesIfNeeded(userId);
+ if (!mSpManager.hasEscrowData(userId)) {
+ throw new SecurityException("Escrow token is disabled on the current user");
+ }
+ long handle = mSpManager.createTokenBasedSyntheticPassword(token, userId);
+ if (auth != null) {
+ mSpManager.activateTokenBasedSyntheticPassword(handle, auth, userId);
+ }
+ return handle;
+ }
+ }
+
+ private void activateEscrowTokens(AuthenticationToken auth, int userId) throws RemoteException {
+ if (DEBUG) Slog.d(TAG, "activateEscrowTokens: user=" + userId);
+ synchronized (mSpManager) {
+ for (long handle : mSpManager.getPendingTokensForUser(userId)) {
+ Slog.i(TAG, String.format("activateEscrowTokens: %x %d ", handle, userId));
+ mSpManager.activateTokenBasedSyntheticPassword(handle, auth, userId);
+ }
+ }
+ }
+
+ @Override
+ public boolean isEscrowTokenActive(long handle, int userId) throws RemoteException {
+ ensureCallerSystemUid();
+ synchronized (mSpManager) {
+ return mSpManager.existsHandle(handle, userId);
+ }
+ }
+
+ @Override
+ public boolean removeEscrowToken(long handle, int userId) throws RemoteException {
+ ensureCallerSystemUid();
+ synchronized (mSpManager) {
+ if (handle == getSyntheticPasswordHandleLocked(userId)) {
+ Slog.w(TAG, "Cannot remove password handle");
+ return false;
+ }
+ if (mSpManager.removePendingToken(handle, userId)) {
+ return true;
+ }
+ if (mSpManager.existsHandle(handle, userId)) {
+ mSpManager.destroyTokenBasedSyntheticPassword(handle, userId);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ @Override
+ public boolean setLockCredentialWithToken(String credential, int type, long tokenHandle,
+ byte[] token, int userId) throws RemoteException {
+ ensureCallerSystemUid();
+ boolean result;
+ synchronized (mSpManager) {
+ if (!mSpManager.hasEscrowData(userId)) {
+ throw new SecurityException("Escrow token is disabled on the current user");
+ }
+ result = setLockCredentialWithTokenInternal(credential, type, tokenHandle, token,
+ userId);
+ }
+ if (result) {
+ synchronized (mSeparateChallengeLock) {
+ setSeparateProfileChallengeEnabled(userId, true, null);
+ }
+ notifyPasswordChanged(userId);
+ }
+ return result;
+ }
+
+ private boolean setLockCredentialWithTokenInternal(String credential, int type,
+ long tokenHandle, byte[] token, int userId) throws RemoteException {
+ synchronized (mSpManager) {
+ AuthenticationResult result = mSpManager.unwrapTokenBasedSyntheticPassword(
+ getGateKeeperService(), tokenHandle, token, userId);
+ if (result.authToken == null) {
+ Slog.w(TAG, "Invalid escrow token supplied");
+ return false;
+ }
+ long oldHandle = getSyntheticPasswordHandleLocked(userId);
+ setLockCredentialWithAuthTokenLocked(credential, type, result.authToken, userId);
+ mSpManager.destroyPasswordBasedSyntheticPassword(oldHandle, userId);
+ return true;
+ }
+ }
+
+ @Override
+ public void unlockUserWithToken(long tokenHandle, byte[] token, int userId)
+ throws RemoteException {
+ ensureCallerSystemUid();
+ AuthenticationResult authResult;
+ synchronized (mSpManager) {
+ if (!mSpManager.hasEscrowData(userId)) {
+ throw new SecurityException("Escrow token is disabled on the current user");
+ }
+ authResult = mSpManager.unwrapTokenBasedSyntheticPassword(getGateKeeperService(),
+ tokenHandle, token, userId);
+ if (authResult.authToken == null) {
+ Slog.w(TAG, "Invalid escrow token supplied");
+ return;
+ }
+ }
+ unlockUser(userId, null, authResult.authToken.deriveDiskEncryptionKey());
+ }
+
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter pw, String[] args){
+ if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
+ != PackageManager.PERMISSION_GRANTED) {
+
+ pw.println("Permission Denial: can't dump LockSettingsService from from pid="
+ + Binder.getCallingPid()
+ + ", uid=" + Binder.getCallingUid());
+ return;
+ }
+
+ synchronized (this) {
+ pw.println("Current lock settings service state:");
+ pw.println(String.format("SP Enabled = %b",
+ mLockPatternUtils.isSyntheticPasswordEnabled()));
+
+ List<UserInfo> users = mUserManager.getUsers();
+ for (int user = 0; user < users.size(); user++) {
+ final int userId = users.get(user).id;
+ pw.println(" User " + userId);
+ pw.println(String.format(" SP Handle = %x",
+ getSyntheticPasswordHandleLocked(userId)));
+ try {
+ pw.println(String.format(" SID = %x",
+ getGateKeeperService().getSecureUserId(userId)));
+ } catch (RemoteException e) {
+ // ignore.
+ }
+ }
+ }
+ }
+
+ private void disableEscrowTokenOnNonManagedDevicesIfNeeded(int userId) {
+ long ident = Binder.clearCallingIdentity();
+ try {
+ // Managed profile should have escrow enabled
+ if (mUserManager.getUserInfo(userId).isManagedProfile()) {
+ return;
+ }
+ DevicePolicyManager dpm = (DevicePolicyManager)
+ mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
+ // Devices with Device Owner should have escrow enabled on all users.
+ if (dpm.getDeviceOwnerComponentOnAnyUser() != null) {
+ return;
+ }
+ // If the device is yet to be provisioned (still in SUW), there is still
+ // a chance that Device Owner will be set on the device later, so postpone
+ // disabling escrow token for now.
+ if (!dpm.isDeviceProvisioned()) {
+ return;
+ }
+ // Disable escrow token permanently on all other device/user types.
+ Slog.i(TAG, "Disabling escrow token on user " + userId);
+ if (isSyntheticPasswordBasedCredentialLocked(userId)) {
+ mSpManager.destroyEscrowData(userId);
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG, "disableEscrowTokenOnNonManagedDevices", e);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ private void ensureCallerSystemUid() throws SecurityException {
+ final int callingUid = mInjector.binderGetCallingUid();
+ if (callingUid != Process.SYSTEM_UID) {
+ throw new SecurityException("Only system can call this API.");
+ }
+ }
}
diff --git a/services/core/java/com/android/server/LockSettingsShellCommand.java b/services/core/java/com/android/server/LockSettingsShellCommand.java
index 1ab5303..91bd98e 100644
--- a/services/core/java/com/android/server/LockSettingsShellCommand.java
+++ b/services/core/java/com/android/server/LockSettingsShellCommand.java
@@ -22,12 +22,9 @@
import android.app.ActivityManager;
import android.content.Context;
-import android.os.Binder;
-import android.os.Process;
import android.os.RemoteException;
import android.os.ShellCommand;
-import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.widget.LockPatternUtils;
import com.android.internal.widget.LockPatternUtils.RequestThrottledException;
@@ -37,6 +34,7 @@
private static final String COMMAND_SET_PIN = "set-pin";
private static final String COMMAND_SET_PASSWORD = "set-password";
private static final String COMMAND_CLEAR = "clear";
+ private static final String COMMAND_SP = "sp";
private int mCurrentUserId;
private final LockPatternUtils mLockPatternUtils;
@@ -71,6 +69,9 @@
case COMMAND_CLEAR:
runClear();
break;
+ case COMMAND_SP:
+ runEnableSp();
+ break;
default:
getErrPrintWriter().println("Unknown command: " + cmd);
break;
@@ -92,6 +93,8 @@
while ((opt = getNextOption()) != null) {
if ("--old".equals(opt)) {
mOld = getNextArgRequired();
+ } else if ("--user".equals(opt)) {
+ mCurrentUserId = Integer.parseInt(getNextArgRequired());
} else {
getErrPrintWriter().println("Unknown option: " + opt);
throw new IllegalArgumentException();
@@ -100,6 +103,15 @@
mNew = getNextArg();
}
+ private void runEnableSp() {
+ if (mNew != null) {
+ mLockPatternUtils.enableSyntheticPassword();
+ getOutPrintWriter().println("Synthetic password enabled");
+ }
+ getOutPrintWriter().println(String.format("SP Enabled = %b",
+ mLockPatternUtils.isSyntheticPasswordEnabled()));
+ }
+
private void runSetPattern() throws RemoteException {
mLockPatternUtils.saveLockPattern(stringToPattern(mNew), mOld, mCurrentUserId);
getOutPrintWriter().println("Pattern set to '" + mNew + "'");
diff --git a/services/core/java/com/android/server/LockSettingsStorage.java b/services/core/java/com/android/server/LockSettingsStorage.java
index 385b1cf..f5bae7c 100644
--- a/services/core/java/com/android/server/LockSettingsStorage.java
+++ b/services/core/java/com/android/server/LockSettingsStorage.java
@@ -66,6 +66,8 @@
private static final String LEGACY_LOCK_PASSWORD_FILE = "password.key";
private static final String CHILD_PROFILE_LOCK_FILE = "gatekeeper.profile.key";
+ private static final String SYNTHETIC_PASSWORD_DIRECTORY = "spblob/";
+
private static final Object DEFAULT = new Object();
private final DatabaseHelper mOpenHelper;
@@ -412,8 +414,7 @@
}
private String getLockCredentialFilePathForUser(int userId, String basename) {
- String dataSystemDirectory =
- android.os.Environment.getDataDirectory().getAbsolutePath() +
+ String dataSystemDirectory = Environment.getDataDirectory().getAbsolutePath() +
SYSTEM_DIRECTORY;
if (userId == 0) {
// Leave it in the same place for user 0
@@ -423,6 +424,40 @@
}
}
+ public void writeSyntheticPasswordState(int userId, long handle, String name, byte[] data) {
+ writeFile(getSynthenticPasswordStateFilePathForUser(userId, handle, name), data);
+ }
+
+ public byte[] readSyntheticPasswordState(int userId, long handle, String name) {
+ return readFile(getSynthenticPasswordStateFilePathForUser(userId, handle, name));
+ }
+
+ public void deleteSyntheticPasswordState(int userId, long handle, String name, boolean secure) {
+ String path = getSynthenticPasswordStateFilePathForUser(userId, handle, name);
+ File file = new File(path);
+ if (file.exists()) {
+ //TODO: (b/34600579) invoke secdiscardable
+ file.delete();
+ mCache.putFile(path, null);
+ }
+ }
+
+ @VisibleForTesting
+ protected File getSyntheticPasswordDirectoryForUser(int userId) {
+ return new File(Environment.getDataSystemDeDirectory(userId) ,SYNTHETIC_PASSWORD_DIRECTORY);
+ }
+
+ @VisibleForTesting
+ protected String getSynthenticPasswordStateFilePathForUser(int userId, long handle,
+ String name) {
+ File baseDir = getSyntheticPasswordDirectoryForUser(userId);
+ String baseName = String.format("%016x.%s", handle, name);
+ if (!baseDir.exists()) {
+ baseDir.mkdir();
+ }
+ return new File(baseDir, baseName).getAbsolutePath();
+ }
+
public void removeUser(int userId) {
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
@@ -446,15 +481,20 @@
}
}
} else {
- // Manged profile
+ // Managed profile
removeChildProfileLock(userId);
}
+ File spStateDir = getSyntheticPasswordDirectoryForUser(userId);
try {
db.beginTransaction();
db.delete(TABLE, COLUMN_USERID + "='" + userId + "'", null);
db.setTransactionSuccessful();
mCache.removeUser(userId);
+ // The directory itself will be deleted as part of user deletion operation by the
+ // framework, so only need to purge cache here.
+ //TODO: (b/34600579) invoke secdiscardable
+ mCache.purgePath(spStateDir.getAbsolutePath());
} finally {
db.endTransaction();
}
@@ -619,6 +659,16 @@
mVersion++;
}
+ synchronized void purgePath(String path) {
+ for (int i = mCache.size() - 1; i >= 0; i--) {
+ CacheKey entry = mCache.keyAt(i);
+ if (entry.type == CacheKey.TYPE_FILE && entry.key.startsWith(path)) {
+ mCache.removeAt(i);
+ }
+ }
+ mVersion++;
+ }
+
synchronized void clear() {
mCache.clear();
mVersion++;
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index 32b7e4d..0415971 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -2922,10 +2922,10 @@
waitForReady();
if (StorageManager.isFileEncryptedNativeOrEmulated()) {
- // When a user has secure lock screen, require a challenge token to
- // actually unlock. This check is mostly in place for emulation mode.
- if (mLockPatternUtils.isSecure(userId) && ArrayUtils.isEmpty(token)) {
- throw new IllegalStateException("Token required to unlock secure user " + userId);
+ // When a user has secure lock screen, require secret to actually unlock.
+ // This check is mostly in place for emulation mode.
+ if (mLockPatternUtils.isSecure(userId) && ArrayUtils.isEmpty(secret)) {
+ throw new IllegalStateException("Secret required to unlock secure user " + userId);
}
try {
diff --git a/services/core/java/com/android/server/SyntheticPasswordCrypto.java b/services/core/java/com/android/server/SyntheticPasswordCrypto.java
new file mode 100644
index 0000000..12d91c5
--- /dev/null
+++ b/services/core/java/com/android/server/SyntheticPasswordCrypto.java
@@ -0,0 +1,194 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import android.security.keystore.KeyProperties;
+import android.security.keystore.KeyProtection;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.util.Arrays;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.KeyGenerator;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+public class SyntheticPasswordCrypto {
+ private static final int PROFILE_KEY_IV_SIZE = 12;
+ private static final int AES_KEY_LENGTH = 32; // 256-bit AES key
+ private static final byte[] APPLICATION_ID_PERSONALIZATION = "application-id".getBytes();
+ // Time between the user credential is verified with GK and the decryption of synthetic password
+ // under the auth-bound key. This should always happen one after the other, but give it 15
+ // seconds just to be sure.
+ private static final int USER_AUTHENTICATION_VALIDITY = 15;
+
+ private static byte[] decrypt(SecretKey key, byte[] blob)
+ throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
+ InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
+ if (blob == null) {
+ return null;
+ }
+ byte[] iv = Arrays.copyOfRange(blob, 0, PROFILE_KEY_IV_SIZE);
+ byte[] ciphertext = Arrays.copyOfRange(blob, PROFILE_KEY_IV_SIZE, blob.length);
+ Cipher cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
+ + KeyProperties.BLOCK_MODE_GCM + "/" + KeyProperties.ENCRYPTION_PADDING_NONE);
+ cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv));
+ return cipher.doFinal(ciphertext);
+ }
+
+ private static byte[] encrypt(SecretKey key, byte[] blob)
+ throws IOException, NoSuchAlgorithmException, NoSuchPaddingException,
+ InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
+ if (blob == null) {
+ return null;
+ }
+ Cipher cipher = Cipher.getInstance(
+ KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_GCM + "/"
+ + KeyProperties.ENCRYPTION_PADDING_NONE);
+ cipher.init(Cipher.ENCRYPT_MODE, key);
+ byte[] ciphertext = cipher.doFinal(blob);
+ byte[] iv = cipher.getIV();
+ if (iv.length != PROFILE_KEY_IV_SIZE) {
+ throw new RuntimeException("Invalid iv length: " + iv.length);
+ }
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ outputStream.write(iv);
+ outputStream.write(ciphertext);
+ return outputStream.toByteArray();
+ }
+
+ public static byte[] encrypt(byte[] keyBytes, byte[] personalisation, byte[] message) {
+ byte[] keyHash = personalisedHash(personalisation, keyBytes);
+ SecretKeySpec key = new SecretKeySpec(Arrays.copyOf(keyHash, AES_KEY_LENGTH),
+ KeyProperties.KEY_ALGORITHM_AES);
+ try {
+ return encrypt(key, message);
+ } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException
+ | IllegalBlockSizeException | BadPaddingException | IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ public static byte[] decrypt(byte[] keyBytes, byte[] personalisation, byte[] ciphertext) {
+ byte[] keyHash = personalisedHash(personalisation, keyBytes);
+ SecretKeySpec key = new SecretKeySpec(Arrays.copyOf(keyHash, AES_KEY_LENGTH),
+ KeyProperties.KEY_ALGORITHM_AES);
+ try {
+ return decrypt(key, ciphertext);
+ } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException
+ | IllegalBlockSizeException | BadPaddingException
+ | InvalidAlgorithmParameterException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ public static byte[] decryptBlob(String keyAlias, byte[] blob, byte[] applicationId) {
+ try {
+ KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
+ keyStore.load(null);
+
+ SecretKey decryptionKey = (SecretKey) keyStore.getKey(keyAlias, null);
+ byte[] intermediate = decrypt(applicationId, APPLICATION_ID_PERSONALIZATION, blob);
+ return decrypt(decryptionKey, intermediate);
+ } catch (CertificateException | IOException | BadPaddingException
+ | IllegalBlockSizeException
+ | KeyStoreException | NoSuchPaddingException | NoSuchAlgorithmException
+ | InvalidKeyException | UnrecoverableKeyException
+ | InvalidAlgorithmParameterException e) {
+ e.printStackTrace();
+ throw new RuntimeException("Failed to decrypt blob", e);
+ }
+ }
+
+ public static byte[] createBlob(String keyAlias, byte[] data, byte[] applicationId, long sid) {
+ try {
+ KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES);
+ keyGenerator.init(new SecureRandom());
+ SecretKey secretKey = keyGenerator.generateKey();
+ KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
+ keyStore.load(null);
+ KeyProtection.Builder builder = new KeyProtection.Builder(KeyProperties.PURPOSE_DECRYPT)
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE);
+ if (sid != 0) {
+ builder.setUserAuthenticationRequired(true)
+ .setBoundToSpecificSecureUserId(sid)
+ .setUserAuthenticationValidityDurationSeconds(USER_AUTHENTICATION_VALIDITY);
+ }
+ keyStore.setEntry(keyAlias,
+ new KeyStore.SecretKeyEntry(secretKey),
+ builder.build());
+ byte[] intermediate = encrypt(secretKey, data);
+ return encrypt(applicationId, APPLICATION_ID_PERSONALIZATION, intermediate);
+
+ } catch (CertificateException | IOException | BadPaddingException
+ | IllegalBlockSizeException
+ | KeyStoreException | NoSuchPaddingException | NoSuchAlgorithmException
+ | InvalidKeyException e) {
+ e.printStackTrace();
+ throw new RuntimeException("Failed to encrypt blob", e);
+ }
+ }
+
+ public static void destroyBlobKey(String keyAlias) {
+ KeyStore keyStore;
+ try {
+ keyStore = KeyStore.getInstance("AndroidKeyStore");
+ keyStore.load(null);
+ keyStore.deleteEntry(keyAlias);
+ } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException
+ | IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ protected static byte[] personalisedHash(byte[] personalisation, byte[]... message) {
+ try {
+ final int PADDING_LENGTH = 128;
+ MessageDigest digest = MessageDigest.getInstance("SHA-512");
+ if (personalisation.length > PADDING_LENGTH) {
+ throw new RuntimeException("Personalisation too long");
+ }
+ // Personalize the hash
+ // Pad it to the block size of the hash function
+ personalisation = Arrays.copyOf(personalisation, PADDING_LENGTH);
+ digest.update(personalisation);
+ for (byte[] data : message) {
+ digest.update(data);
+ }
+ return digest.digest();
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException("NoSuchAlgorithmException for SHA-512", e);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/SyntheticPasswordManager.java b/services/core/java/com/android/server/SyntheticPasswordManager.java
new file mode 100644
index 0000000..6267880
--- /dev/null
+++ b/services/core/java/com/android/server/SyntheticPasswordManager.java
@@ -0,0 +1,692 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.RemoteException;
+import android.service.gatekeeper.GateKeeperResponse;
+import android.service.gatekeeper.IGateKeeperService;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.widget.LockPatternUtils;
+import com.android.internal.widget.VerifyCredentialResponse;
+
+import libcore.util.HexEncoding;
+
+import java.nio.ByteBuffer;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+
+
+/**
+ * A class that maintains the wrapping of synthetic password by user credentials or escrow tokens.
+ * It's (mostly) a pure storage for synthetic passwords, providing APIs to creating and destroying
+ * synthetic password blobs which are wrapped by user credentials or escrow tokens.
+ *
+ * Here is the assumptions it makes:
+ * Each user has one single synthetic password at any time.
+ * The SP has an associated password handle, which binds to the SID for that user. The password
+ * handle is persisted by SyntheticPasswordManager internally.
+ * If the user credential is null, it's treated as if the credential is DEFAULT_PASSWORD
+ */
+public class SyntheticPasswordManager {
+ private static final String SP_BLOB_NAME = "spblob";
+ private static final String SP_E0_NAME = "e0";
+ private static final String SP_P1_NAME = "p1";
+ private static final String SP_HANDLE_NAME = "handle";
+ private static final String SECDISCARDABLE_NAME = "secdis";
+ private static final int SECDISCARDABLE_LENGTH = 16 * 1024;
+ private static final String PASSWORD_DATA_NAME = "pwd";
+
+ public static final long DEFAULT_HANDLE = 0;
+ private static final String DEFAULT_PASSWORD = "default-password";
+
+ private static final byte SYNTHETIC_PASSWORD_VERSION = 1;
+ private static final byte SYNTHETIC_PASSWORD_PASSWORD_BASED = 0;
+ private static final byte SYNTHETIC_PASSWORD_TOKEN_BASED = 1;
+
+ // 256-bit synthetic password
+ private static final byte SYNTHETIC_PASSWORD_LENGTH = 256 / 8;
+
+ private static final int PASSWORD_SCRYPT_N = 13;
+ private static final int PASSWORD_SCRYPT_R = 3;
+ private static final int PASSWORD_SCRYPT_P = 1;
+ private static final int PASSWORD_SALT_LENGTH = 16;
+ private static final int PASSWORD_TOKEN_LENGTH = 32;
+ private static final String TAG = "SyntheticPasswordManager";
+
+ private static final byte[] PERSONALISATION_SECDISCARDABLE = "secdiscardable-transform".getBytes();
+ private static final byte[] PERSONALIZATION_KEY_STORE_PASSWORD = "keystore-password".getBytes();
+ private static final byte[] PERSONALIZATION_USER_GK_AUTH = "user-gk-authentication".getBytes();
+ private static final byte[] PERSONALIZATION_SP_GK_AUTH = "sp-gk-authentication".getBytes();
+ private static final byte[] PERSONALIZATION_FBE_KEY = "fbe-key".getBytes();
+ private static final byte[] PERSONALIZATION_SP_SPLIT = "sp-split".getBytes();
+ private static final byte[] PERSONALIZATION_E0 = "e0-encryption".getBytes();
+
+ static class AuthenticationResult {
+ public AuthenticationToken authToken;
+ public VerifyCredentialResponse gkResponse;
+ }
+
+ static class AuthenticationToken {
+ /*
+ * Here is the relationship between all three fields:
+ * P0 and P1 are two randomly-generated blocks. P1 is stored on disk but P0 is not.
+ * syntheticPassword = hash(P0 || P1)
+ * E0 = P0 encrypted under syntheticPassword, stored on disk.
+ */
+ private @Nullable byte[] E0;
+ private @Nullable byte[] P1;
+ private @NonNull String syntheticPassword;
+
+ public String deriveKeyStorePassword() {
+ return bytesToHex(SyntheticPasswordCrypto.personalisedHash(
+ PERSONALIZATION_KEY_STORE_PASSWORD, syntheticPassword.getBytes()));
+ }
+
+ public byte[] deriveGkPassword() {
+ return SyntheticPasswordCrypto.personalisedHash(PERSONALIZATION_SP_GK_AUTH,
+ syntheticPassword.getBytes());
+ }
+
+ public byte[] deriveDiskEncryptionKey() {
+ return SyntheticPasswordCrypto.personalisedHash(PERSONALIZATION_FBE_KEY,
+ syntheticPassword.getBytes());
+ }
+
+ private void initialize(byte[] P0, byte[] P1) {
+ this.P1 = P1;
+ this.syntheticPassword = String.valueOf(HexEncoding.encode(
+ SyntheticPasswordCrypto.personalisedHash(
+ PERSONALIZATION_SP_SPLIT, P0, P1)));
+ this.E0 = SyntheticPasswordCrypto.encrypt(this.syntheticPassword.getBytes(),
+ PERSONALIZATION_E0, P0);
+ }
+
+ public void recreate(byte[] secret) {
+ initialize(secret, this.P1);
+ }
+
+ protected static AuthenticationToken create() {
+ AuthenticationToken result = new AuthenticationToken();
+ result.initialize(secureRandom(SYNTHETIC_PASSWORD_LENGTH),
+ secureRandom(SYNTHETIC_PASSWORD_LENGTH));
+ return result;
+ }
+
+ public byte[] computeP0() {
+ if (E0 == null) {
+ return null;
+ }
+ return SyntheticPasswordCrypto.decrypt(syntheticPassword.getBytes(), PERSONALIZATION_E0,
+ E0);
+ }
+ }
+
+ static class PasswordData {
+ byte scryptN;
+ byte scryptR;
+ byte scryptP;
+ public int passwordType;
+ byte[] salt;
+ public byte[] passwordHandle;
+
+ public static PasswordData create(int passwordType) {
+ PasswordData result = new PasswordData();
+ result.scryptN = PASSWORD_SCRYPT_N;
+ result.scryptR = PASSWORD_SCRYPT_R;
+ result.scryptP = PASSWORD_SCRYPT_P;
+ result.passwordType = passwordType;
+ result.salt = secureRandom(PASSWORD_SALT_LENGTH);
+ return result;
+ }
+
+ public static PasswordData fromBytes(byte[] data) {
+ PasswordData result = new PasswordData();
+ ByteBuffer buffer = ByteBuffer.allocate(data.length);
+ buffer.put(data, 0, data.length);
+ buffer.flip();
+ result.passwordType = buffer.getInt();
+ result.scryptN = buffer.get();
+ result.scryptR = buffer.get();
+ result.scryptP = buffer.get();
+ int saltLen = buffer.getInt();
+ result.salt = new byte[saltLen];
+ buffer.get(result.salt);
+ int handleLen = buffer.getInt();
+ result.passwordHandle = new byte[handleLen];
+ buffer.get(result.passwordHandle);
+ return result;
+ }
+
+ public byte[] toBytes() {
+ ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES + 3 * Byte.BYTES
+ + Integer.BYTES + salt.length + Integer.BYTES + passwordHandle.length);
+ buffer.putInt(passwordType);
+ buffer.put(scryptN);
+ buffer.put(scryptR);
+ buffer.put(scryptP);
+ buffer.putInt(salt.length);
+ buffer.put(salt);
+ buffer.putInt(passwordHandle.length);
+ buffer.put(passwordHandle);
+ return buffer.array();
+ }
+ }
+
+ private LockSettingsStorage mStorage;
+
+ public SyntheticPasswordManager(LockSettingsStorage storage) {
+ mStorage = storage;
+ }
+
+
+ public int getCredentialType(long handle, int userId) {
+ byte[] passwordData = loadState(PASSWORD_DATA_NAME, handle, userId);
+ if (passwordData == null) {
+ Log.w(TAG, "getCredentialType: encountered empty password data for user " + userId);
+ return LockPatternUtils.CREDENTIAL_TYPE_NONE;
+ }
+ return PasswordData.fromBytes(passwordData).passwordType;
+ }
+
+ /**
+ * Initializing a new Authentication token, possibly from an existing credential and hash.
+ *
+ * The authentication token would bear a randomly-generated synthetic password.
+ *
+ * This method has the side effect of rebinding the SID of the given user to the
+ * newly-generated SP.
+ *
+ * If the existing credential hash is non-null, the existing SID mill be migrated so
+ * the synthetic password in the authentication token will produce the same SID
+ * (the corresponding synthetic password handle is persisted by SyntheticPasswordManager
+ * in a per-user data storage.
+ *
+ * If the existing credential hash is null, it means the given user should have no SID so
+ * SyntheticPasswordManager will nuke any SP handle previously persisted. In this case,
+ * the supplied credential parameter is also ignored.
+ *
+ * Also saves the escrow information necessary to re-generate the synthetic password under
+ * an escrow scheme. This information can be removed with {@link #destroyEscrowData} if
+ * password escrow should be disabled completely on the given user.
+ *
+ */
+ public AuthenticationToken newSyntheticPasswordAndSid(IGateKeeperService gatekeeper,
+ byte[] hash, String credential, int userId) throws RemoteException {
+ AuthenticationToken result = AuthenticationToken.create();
+ GateKeeperResponse response;
+ if (hash != null) {
+ response = gatekeeper.enroll(userId, hash, credential.getBytes(),
+ result.deriveGkPassword());
+ if (response.getResponseCode() != GateKeeperResponse.RESPONSE_OK) {
+ Log.w(TAG, "Fail to migrate SID, assuming no SID, user " + userId);
+ clearSidForUser(userId);
+ } else {
+ saveSyntheticPasswordHandle(response.getPayload(), userId);
+ }
+ } else {
+ clearSidForUser(userId);
+ }
+ saveEscrowData(result, userId);
+ return result;
+ }
+
+ /**
+ * Enroll a new password handle and SID for the given synthetic password and persist it on disk.
+ * Used when adding password to previously-unsecured devices.
+ */
+ public void newSidForUser(IGateKeeperService gatekeeper, AuthenticationToken authToken,
+ int userId) throws RemoteException {
+ GateKeeperResponse response = gatekeeper.enroll(userId, null, null,
+ authToken.deriveGkPassword());
+ if (response.getResponseCode() != GateKeeperResponse.RESPONSE_OK) {
+ Log.e(TAG, "Fail to create new SID for user " + userId);
+ return;
+ }
+ saveSyntheticPasswordHandle(response.getPayload(), userId);
+ }
+
+ // Nuke the SP handle (and as a result, its SID) for the given user.
+ public void clearSidForUser(int userId) {
+ destroyState(SP_HANDLE_NAME, true, DEFAULT_HANDLE, userId);
+ }
+
+ public boolean hasSidForUser(int userId) {
+ return hasState(SP_HANDLE_NAME, DEFAULT_HANDLE, userId);
+ }
+
+ // if null, it means there is no SID associated with the user
+ // This can happen if the user is migrated to SP but currently
+ // do not have a lockscreen password.
+ private byte[] loadSyntheticPasswordHandle(int userId) {
+ return loadState(SP_HANDLE_NAME, DEFAULT_HANDLE, userId);
+ }
+
+ private void saveSyntheticPasswordHandle(byte[] spHandle, int userId) {
+ saveState(SP_HANDLE_NAME, spHandle, DEFAULT_HANDLE, userId);
+ }
+
+ private boolean loadEscrowData(AuthenticationToken authToken, int userId) {
+ authToken.E0 = loadState(SP_E0_NAME, DEFAULT_HANDLE, userId);
+ authToken.P1 = loadState(SP_P1_NAME, DEFAULT_HANDLE, userId);
+ return authToken.E0 != null && authToken.P1 != null;
+ }
+
+ private void saveEscrowData(AuthenticationToken authToken, int userId) {
+ saveState(SP_E0_NAME, authToken.E0, DEFAULT_HANDLE, userId);
+ saveState(SP_P1_NAME, authToken.P1, DEFAULT_HANDLE, userId);
+ }
+
+ public boolean hasEscrowData(int userId) {
+ return hasState(SP_E0_NAME, DEFAULT_HANDLE, userId)
+ && hasState(SP_P1_NAME, DEFAULT_HANDLE, userId);
+ }
+
+ public void destroyEscrowData(int userId) {
+ destroyState(SP_E0_NAME, true, DEFAULT_HANDLE, userId);
+ destroyState(SP_P1_NAME, true, DEFAULT_HANDLE, userId);
+ }
+
+ /**
+ * Create a new password based SP blob based on the supplied authentication token, such that
+ * a future successful authentication with unwrapPasswordBasedSyntheticPassword() would result
+ * in the same authentication token.
+ *
+ * This method only creates SP blob wrapping around the given synthetic password and does not
+ * handle logic around SID or SP handle. The caller should separately ensure that the user's SID
+ * is consistent with the device state by calling other APIs in this class.
+ *
+ * @see #newSidForUser
+ * @see #clearSidForUser
+ */
+ public long createPasswordBasedSyntheticPassword(IGateKeeperService gatekeeper,
+ String credential, int credentialType, AuthenticationToken authToken, int userId)
+ throws RemoteException {
+ if (credential == null || credentialType == LockPatternUtils.CREDENTIAL_TYPE_NONE) {
+ credentialType = LockPatternUtils.CREDENTIAL_TYPE_NONE;
+ credential = DEFAULT_PASSWORD;
+ }
+
+ long handle = generateHandle();
+ PasswordData pwd = PasswordData.create(credentialType);
+ byte[] pwdToken = computePasswordToken(credential, pwd);
+
+ GateKeeperResponse response = gatekeeper.enroll(fakeUid(userId), null, null,
+ passwordTokenToGkInput(pwdToken));
+ if (response.getResponseCode() != GateKeeperResponse.RESPONSE_OK) {
+ Log.e(TAG, "Fail to enroll user password when creating SP for user " + userId);
+ return 0;
+ }
+ pwd.passwordHandle = response.getPayload();
+ long sid = sidFromPasswordHandle(pwd.passwordHandle);
+ saveState(PASSWORD_DATA_NAME, pwd.toBytes(), handle, userId);
+
+ byte[] applicationId = transformUnderSecdiscardable(pwdToken,
+ createSecdiscardable(handle, userId));
+ createSyntheticPasswordBlob(handle, SYNTHETIC_PASSWORD_PASSWORD_BASED, authToken,
+ applicationId, sid, userId);
+ return handle;
+ }
+
+ private ArrayMap<Integer, ArrayMap<Long, byte[]>> tokenMap = new ArrayMap<>();
+
+ public long createTokenBasedSyntheticPassword(byte[] token, int userId) {
+ long handle = generateHandle();
+ byte[] applicationId = transformUnderSecdiscardable(token,
+ createSecdiscardable(handle, userId));
+ if (!tokenMap.containsKey(userId)) {
+ tokenMap.put(userId, new ArrayMap<>());
+ }
+ tokenMap.get(userId).put(handle, applicationId);
+ return handle;
+ }
+
+ public Set<Long> getPendingTokensForUser(int userId) {
+ if (!tokenMap.containsKey(userId)) {
+ return Collections.emptySet();
+ }
+ return tokenMap.get(userId).keySet();
+ }
+
+ public boolean removePendingToken(long handle, int userId) {
+ if (!tokenMap.containsKey(userId)) {
+ return false;
+ }
+ return tokenMap.get(userId).remove(handle) != null;
+ }
+
+ public boolean activateTokenBasedSyntheticPassword(long handle, AuthenticationToken authToken,
+ int userId) {
+ if (!tokenMap.containsKey(userId)) {
+ return false;
+ }
+ byte[] applicationId = tokenMap.get(userId).get(handle);
+ if (applicationId == null) {
+ return false;
+ }
+ if (!loadEscrowData(authToken, userId)) {
+ Log.w(TAG, "User is not escrowable");
+ return false;
+ }
+ createSyntheticPasswordBlob(handle, SYNTHETIC_PASSWORD_TOKEN_BASED, authToken,
+ applicationId, 0L, userId);
+ tokenMap.get(userId).remove(handle);
+ return true;
+ }
+
+ private void createSyntheticPasswordBlob(long handle, byte type, AuthenticationToken authToken,
+ byte[] applicationId, long sid, int userId) {
+ final byte[] secret;
+ if (type == SYNTHETIC_PASSWORD_TOKEN_BASED) {
+ secret = authToken.computeP0();
+ } else {
+ secret = authToken.syntheticPassword.getBytes();
+ }
+ byte[] content = createSPBlob(getHandleName(handle), secret, applicationId, sid);
+ byte[] blob = new byte[content.length + 1 + 1];
+ blob[0] = SYNTHETIC_PASSWORD_VERSION;
+ blob[1] = type;
+ System.arraycopy(content, 0, blob, 2, content.length);
+ saveState(SP_BLOB_NAME, blob, handle, userId);
+ }
+
+ /**
+ * Decrypt a synthetic password by supplying the user credential and corresponding password
+ * blob handle generated previously. If the decryption is successful, initiate a GateKeeper
+ * verification to referesh the SID & Auth token maintained by the system.
+ */
+ public AuthenticationResult unwrapPasswordBasedSyntheticPassword(IGateKeeperService gatekeeper,
+ long handle, String credential, int userId) throws RemoteException {
+ if (credential == null) {
+ credential = DEFAULT_PASSWORD;
+ }
+ AuthenticationResult result = new AuthenticationResult();
+ PasswordData pwd = PasswordData.fromBytes(loadState(PASSWORD_DATA_NAME, handle, userId));
+ byte[] pwdToken = computePasswordToken(credential, pwd);
+ byte[] gkPwdToken = passwordTokenToGkInput(pwdToken);
+
+ GateKeeperResponse response = gatekeeper.verifyChallenge(fakeUid(userId), 0L,
+ pwd.passwordHandle, gkPwdToken);
+ int responseCode = response.getResponseCode();
+ if (responseCode == GateKeeperResponse.RESPONSE_OK) {
+ result.gkResponse = VerifyCredentialResponse.OK;
+ if (response.getShouldReEnroll()) {
+ GateKeeperResponse reenrollResponse = gatekeeper.enroll(fakeUid(userId),
+ pwd.passwordHandle, gkPwdToken, gkPwdToken);
+ if (reenrollResponse.getResponseCode() == GateKeeperResponse.RESPONSE_OK) {
+ pwd.passwordHandle = reenrollResponse.getPayload();
+ saveState(PASSWORD_DATA_NAME, pwd.toBytes(), handle, userId);
+ } else {
+ Log.w(TAG, "Fail to re-enroll user password for user " + userId);
+ // continue the flow anyway
+ }
+ }
+ } else if (responseCode == GateKeeperResponse.RESPONSE_RETRY) {
+ result.gkResponse = new VerifyCredentialResponse(response.getTimeout());
+ return result;
+ } else {
+ result.gkResponse = VerifyCredentialResponse.ERROR;
+ return result;
+ }
+
+
+ byte[] applicationId = transformUnderSecdiscardable(pwdToken,
+ loadSecdiscardable(handle, userId));
+ result.authToken = unwrapSyntheticPasswordBlob(handle, SYNTHETIC_PASSWORD_PASSWORD_BASED,
+ applicationId, userId);
+
+ // Perform verifyChallenge to refresh auth tokens for GK if user password exists.
+ result.gkResponse = verifyChallenge(gatekeeper, result.authToken, 0L, userId);
+ return result;
+ }
+
+ /**
+ * Decrypt a synthetic password by supplying an escrow token and corresponding token
+ * blob handle generated previously. If the decryption is successful, initiate a GateKeeper
+ * verification to referesh the SID & Auth token maintained by the system.
+ */
+ public @NonNull AuthenticationResult unwrapTokenBasedSyntheticPassword(
+ IGateKeeperService gatekeeper, long handle, byte[] token, int userId)
+ throws RemoteException {
+ AuthenticationResult result = new AuthenticationResult();
+ byte[] applicationId = transformUnderSecdiscardable(token,
+ loadSecdiscardable(handle, userId));
+ result.authToken = unwrapSyntheticPasswordBlob(handle, SYNTHETIC_PASSWORD_TOKEN_BASED,
+ applicationId, userId);
+ if (result.authToken != null) {
+ result.gkResponse = verifyChallenge(gatekeeper, result.authToken, 0L, userId);
+ if (result.gkResponse == null) {
+ // The user currently has no password. return OK with null payload so null
+ // is propagated to unlockUser()
+ result.gkResponse = VerifyCredentialResponse.OK;
+ }
+ } else {
+ result.gkResponse = VerifyCredentialResponse.ERROR;
+ }
+ return result;
+ }
+
+ private AuthenticationToken unwrapSyntheticPasswordBlob(long handle, byte type,
+ byte[] applicationId, int userId) {
+ byte[] blob = loadState(SP_BLOB_NAME, handle, userId);
+ if (blob == null) {
+ return null;
+ }
+ if (blob[0] != SYNTHETIC_PASSWORD_VERSION) {
+ throw new RuntimeException("Unknown blob version");
+ }
+ if (blob[1] != type) {
+ throw new RuntimeException("Invalid blob type");
+ }
+ byte[] secret = decryptSPBlob(getHandleName(handle),
+ Arrays.copyOfRange(blob, 2, blob.length), applicationId);
+ if (secret == null) {
+ Log.e(TAG, "Fail to decrypt SP for user " + userId);
+ return null;
+ }
+ AuthenticationToken result = new AuthenticationToken();
+ if (type == SYNTHETIC_PASSWORD_TOKEN_BASED) {
+ if (!loadEscrowData(result, userId)) {
+ Log.e(TAG, "User is not escrowable: " + userId);
+ return null;
+ }
+ result.recreate(secret);
+ } else {
+ result.syntheticPassword = new String(secret);
+ }
+ return result;
+ }
+
+ /**
+ * performs GK verifyChallenge and returns auth token, re-enrolling SP password handle
+ * if required.
+ *
+ * Normally performing verifyChallenge with an AuthenticationToken should always return
+ * RESPONSE_OK, since user authentication failures are detected earlier when trying to
+ * decrypt SP.
+ */
+ public VerifyCredentialResponse verifyChallenge(IGateKeeperService gatekeeper,
+ @NonNull AuthenticationToken auth, long challenge, int userId) throws RemoteException {
+ byte[] spHandle = loadSyntheticPasswordHandle(userId);
+ if (spHandle == null) {
+ // There is no password handle associated with the given user, i.e. the user is not
+ // secured by lockscreen and has no SID, so just return here;
+ return null;
+ }
+ VerifyCredentialResponse result;
+ GateKeeperResponse response = gatekeeper.verifyChallenge(userId, challenge,
+ spHandle, auth.deriveGkPassword());
+ int responseCode = response.getResponseCode();
+ if (responseCode == GateKeeperResponse.RESPONSE_OK) {
+ result = new VerifyCredentialResponse(response.getPayload());
+ if (response.getShouldReEnroll()) {
+ response = gatekeeper.enroll(userId, spHandle,
+ spHandle, auth.deriveGkPassword());
+ if (response.getResponseCode() == GateKeeperResponse.RESPONSE_OK) {
+ spHandle = response.getPayload();
+ saveSyntheticPasswordHandle(spHandle, userId);
+ // Call self again to re-verify with updated handle
+ return verifyChallenge(gatekeeper, auth, challenge, userId);
+ } else {
+ Log.w(TAG, "Fail to re-enroll SP handle for user " + userId);
+ // Fall through, return existing handle
+ }
+ }
+ } else if (responseCode == GateKeeperResponse.RESPONSE_RETRY) {
+ result = new VerifyCredentialResponse(response.getTimeout());
+ } else {
+ result = VerifyCredentialResponse.ERROR;
+ }
+ return result;
+ }
+
+ public boolean existsHandle(long handle, int userId) {
+ return hasState(SP_BLOB_NAME, handle, userId);
+ }
+
+ public void destroyTokenBasedSyntheticPassword(long handle, int userId) {
+ destroySyntheticPassword(handle, userId);
+ destroyState(SECDISCARDABLE_NAME, true, handle, userId);
+ }
+
+ public void destroyPasswordBasedSyntheticPassword(long handle, int userId) {
+ destroySyntheticPassword(handle, userId);
+ destroyState(SECDISCARDABLE_NAME, true, handle, userId);
+ destroyState(PASSWORD_DATA_NAME, true, handle, userId);
+ }
+
+ private void destroySyntheticPassword(long handle, int userId) {
+ destroyState(SP_BLOB_NAME, true, handle, userId);
+ destroyState(SP_E0_NAME, true, handle, userId);
+ destroyState(SP_P1_NAME, true, handle, userId);
+ destroySPBlobKey(getHandleName(handle));
+ }
+
+ private byte[] transformUnderSecdiscardable(byte[] data, byte[] rawSecdiscardable) {
+ byte[] secdiscardable = SyntheticPasswordCrypto.personalisedHash(
+ PERSONALISATION_SECDISCARDABLE, rawSecdiscardable);
+ byte[] result = new byte[data.length + secdiscardable.length];
+ System.arraycopy(data, 0, result, 0, data.length);
+ System.arraycopy(secdiscardable, 0, result, data.length, secdiscardable.length);
+ return result;
+ }
+
+ private byte[] createSecdiscardable(long handle, int userId) {
+ byte[] data = secureRandom(SECDISCARDABLE_LENGTH);
+ saveState(SECDISCARDABLE_NAME, data, handle, userId);
+ return data;
+ }
+
+ private byte[] loadSecdiscardable(long handle, int userId) {
+ return loadState(SECDISCARDABLE_NAME, handle, userId);
+ }
+
+ private boolean hasState(String stateName, long handle, int userId) {
+ return !ArrayUtils.isEmpty(loadState(stateName, handle, userId));
+ }
+
+ private byte[] loadState(String stateName, long handle, int userId) {
+ return mStorage.readSyntheticPasswordState(userId, handle, stateName);
+ }
+
+ private void saveState(String stateName, byte[] data, long handle, int userId) {
+ mStorage.writeSyntheticPasswordState(userId, handle, stateName, data);
+ }
+
+ private void destroyState(String stateName, boolean secure, long handle, int userId) {
+ mStorage.deleteSyntheticPasswordState(userId, handle, stateName, secure);
+ }
+
+ protected byte[] decryptSPBlob(String blobKeyName, byte[] blob, byte[] applicationId) {
+ return SyntheticPasswordCrypto.decryptBlob(blobKeyName, blob, applicationId);
+ }
+
+ protected byte[] createSPBlob(String blobKeyName, byte[] data, byte[] applicationId, long sid) {
+ return SyntheticPasswordCrypto.createBlob(blobKeyName, data, applicationId, sid);
+ }
+
+ protected void destroySPBlobKey(String keyAlias) {
+ SyntheticPasswordCrypto.destroyBlobKey(keyAlias);
+ }
+
+ public static long generateHandle() {
+ SecureRandom rng = new SecureRandom();
+ long result;
+ do {
+ result = rng.nextLong();
+ } while (result == DEFAULT_HANDLE);
+ return result;
+ }
+
+ private int fakeUid(int uid) {
+ return 100000 + uid;
+ }
+
+ protected static byte[] secureRandom(int length) {
+ try {
+ return SecureRandom.getInstance("SHA1PRNG").generateSeed(length);
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ private String getHandleName(long handle) {
+ return String.format("%s%x", LockPatternUtils.SYNTHETIC_PASSWORD_KEY_PREFIX, handle);
+ }
+
+ private byte[] computePasswordToken(String password, PasswordData data) {
+ return scrypt(password, data.salt, 1 << data.scryptN, 1 << data.scryptR, 1 << data.scryptP,
+ PASSWORD_TOKEN_LENGTH);
+ }
+
+ private byte[] passwordTokenToGkInput(byte[] token) {
+ return SyntheticPasswordCrypto.personalisedHash(PERSONALIZATION_USER_GK_AUTH, token);
+ }
+
+ protected long sidFromPasswordHandle(byte[] handle) {
+ return nativeSidFromPasswordHandle(handle);
+ }
+
+ protected byte[] scrypt(String password, byte[] salt, int N, int r, int p, int outLen) {
+ return nativeScrypt(password.getBytes(), salt, N, r, p, outLen);
+ }
+
+ native long nativeSidFromPasswordHandle(byte[] handle);
+ native byte[] nativeScrypt(byte[] password, byte[] salt, int N, int r, int p, int outLen);
+
+ final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();
+ public static String bytesToHex(byte[] bytes) {
+ if (bytes == null) {
+ return "null";
+ }
+ char[] hexChars = new char[bytes.length * 2];
+ for ( int j = 0; j < bytes.length; j++ ) {
+ int v = bytes[j] & 0xFF;
+ hexChars[j * 2] = hexArray[v >>> 4];
+ hexChars[j * 2 + 1] = hexArray[v & 0x0F];
+ }
+ return new String(hexChars);
+ }
+}
diff --git a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
index bf1018f..a95a627 100644
--- a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
+++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
@@ -103,6 +103,10 @@
final boolean change;
synchronized(mPlayerLock) {
final AudioPlaybackConfiguration apc = mPlayers.get(new Integer(piid));
+ // FIXME SoundPool not ready for state reporting
+ if (apc.getPlayerType() == AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL) {
+ return;
+ }
if (checkConfigurationCaller(piid, apc, binderUid)) {
//TODO add generation counter to only update to the latest state
change = apc.handleStateEvent(event);
diff --git a/services/core/java/com/android/server/connectivity/Tethering.java b/services/core/java/com/android/server/connectivity/Tethering.java
index 9d63462..6c608a2 100644
--- a/services/core/java/com/android/server/connectivity/Tethering.java
+++ b/services/core/java/com/android/server/connectivity/Tethering.java
@@ -1008,10 +1008,9 @@
return false;
}
- protected boolean requestUpstreamMobileConnection() {
+ protected void requestUpstreamMobileConnection() {
mUpstreamNetworkMonitor.updateMobileRequiresDun(mConfig.isDunRequired);
mUpstreamNetworkMonitor.registerMobileNetworkRequest();
- return true;
}
protected void unrequestUpstreamMobileConnection() {
@@ -1058,9 +1057,13 @@
}
protected void chooseUpstreamType(boolean tryCell) {
+ final int upstreamType = findPreferredUpstreamType(tryCell);
+ setUpstreamByType(upstreamType);
+ }
+
+ protected int findPreferredUpstreamType(boolean tryCell) {
final ConnectivityManager cm = getConnectivityManager();
int upType = ConnectivityManager.TYPE_NONE;
- String iface = null;
updateConfiguration(); // TODO - remove?
@@ -1100,7 +1103,8 @@
requestUpstreamMobileConnection();
break;
case ConnectivityManager.TYPE_NONE:
- if (tryCell && requestUpstreamMobileConnection()) {
+ if (tryCell) {
+ requestUpstreamMobileConnection();
// We think mobile should be coming up; don't set a retry.
} else {
sendMessageDelayed(CMD_RETRY_UPSTREAM, UPSTREAM_SETTLE_TIME_MS);
@@ -1117,7 +1121,13 @@
break;
}
+ return upType;
+ }
+
+ protected void setUpstreamByType(int upType) {
+ final ConnectivityManager cm = getConnectivityManager();
Network network = null;
+ String iface = null;
if (upType != ConnectivityManager.TYPE_NONE) {
LinkProperties linkProperties = cm.getLinkProperties(upType);
if (linkProperties != null) {
@@ -1354,9 +1364,9 @@
simChange.startListening();
mUpstreamNetworkMonitor.start();
- mTryCell = true; // better try something first pass or crazy tests cases will fail
- chooseUpstreamType(mTryCell);
- mTryCell = !mTryCell;
+ // Better try something first pass or crazy tests cases will fail.
+ chooseUpstreamType(true);
+ mTryCell = false;
}
@Override
@@ -1407,10 +1417,9 @@
break;
}
case CMD_UPSTREAM_CHANGED:
- // need to try DUN immediately if Wifi goes down
- mTryCell = true;
- chooseUpstreamType(mTryCell);
- mTryCell = !mTryCell;
+ // Need to try DUN immediately if Wi-Fi goes down.
+ chooseUpstreamType(true);
+ mTryCell = false;
break;
case CMD_RETRY_UPSTREAM:
chooseUpstreamType(mTryCell);
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 771ae9a..f2b5564 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -121,6 +121,7 @@
import android.service.notification.SnoozeCriterion;
import android.service.notification.StatusBarNotification;
import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenModeProto;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
@@ -140,6 +141,7 @@
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.statusbar.NotificationVisibility;
import com.android.internal.util.FastXmlSerializer;
@@ -2812,8 +2814,26 @@
proto.write(NotificationRecordProto.STATE, NotificationServiceProto.ENQUEUED);
}
}
+ List<NotificationRecord> snoozed = mSnoozeHelper.getSnoozed();
+ N = snoozed.size();
+ if (N > 0) {
+ for (int i = 0; i < N; i++) {
+ final NotificationRecord nr = snoozed.get(i);
+ if (filter.filtered && !filter.matches(nr.sbn)) continue;
+ nr.dump(proto, filter.redact);
+ proto.write(NotificationRecordProto.STATE, NotificationServiceProto.SNOOZED);
+ }
+ }
proto.end(records);
}
+
+ long zenLog = proto.start(NotificationServiceDumpProto.ZEN);
+ mZenModeHelper.dump(proto);
+ for (ComponentName suppressor : mEffectsSuppressors) {
+ proto.write(ZenModeProto.SUPPRESSORS, suppressor.toString());
+ }
+ proto.end(zenLog);
+
proto.flush();
}
@@ -2899,24 +2919,12 @@
}
pw.println(" ");
}
+
+ mSnoozeHelper.dump(pw, filter);
}
}
if (!zenOnly) {
- pw.println("\n Usage Stats:");
- mUsageStats.dump(pw, " ", filter);
- }
-
- if (!filter.filtered || zenOnly) {
- pw.println("\n Zen Mode:");
- pw.print(" mInterruptionFilter="); pw.println(mInterruptionFilter);
- mZenModeHelper.dump(pw, " ");
-
- pw.println("\n Zen Log:");
- ZenLog.dump(pw, " ");
- }
-
- if (!zenOnly) {
pw.println("\n Ranking Config:");
mRankingHelper.dump(pw, " ", filter);
@@ -2945,8 +2953,13 @@
mNotificationAssistants.dump(pw, filter);
}
- if (!zenOnly) {
- mSnoozeHelper.dump(pw, filter);
+ if (!filter.filtered || zenOnly) {
+ pw.println("\n Zen Mode:");
+ pw.print(" mInterruptionFilter="); pw.println(mInterruptionFilter);
+ mZenModeHelper.dump(pw, " ");
+
+ pw.println("\n Zen Log:");
+ ZenLog.dump(pw, " ");
}
pw.println("\n Policy access:");
@@ -2964,6 +2977,11 @@
r.dump(pw, " ", getContext(), filter.redact);
}
}
+
+ if (!zenOnly) {
+ pw.println("\n Usage Stats:");
+ mUsageStats.dump(pw, " ", filter);
+ }
}
}
@@ -3131,7 +3149,9 @@
// snoozed apps
if (mSnoozeHelper.isSnoozed(userId, pkg, r.getKey())) {
- // TODO: log to event log
+ MetricsLogger.action(r.getLogMaker()
+ .setType(MetricsProto.MetricsEvent.TYPE_UPDATE)
+ .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED));
if (DBG) {
Slog.d(TAG, "Ignored enqueue for snoozed notification " + r.getKey());
}
@@ -3867,14 +3887,18 @@
private void cancelNotificationLocked(NotificationRecord r, boolean sendDelete, int reason) {
final String canceledKey = r.getKey();
- // Remove from either list
- boolean wasPosted;
- if (mNotificationList.remove(r)) {
- mNotificationsByKey.remove(r.sbn.getKey());
+ // Remove from both lists, either list could have a separate Record for what is effectively
+ // the same notification.
+ boolean wasPosted = false;
+ NotificationRecord recordInList = null;
+ if ((recordInList = findNotificationByListLocked(mNotificationList, r.getKey())) != null) {
+ mNotificationList.remove(recordInList);
+ mNotificationsByKey.remove(recordInList.sbn.getKey());
wasPosted = true;
- } else {
- mEnqueuedNotifications.remove(r);
- wasPosted = false;
+ }
+ if ((recordInList = findNotificationByListLocked(mEnqueuedNotifications, r.getKey()))
+ != null) {
+ mEnqueuedNotifications.remove(recordInList);
}
// Record caller.
@@ -4155,7 +4179,7 @@
if (until < System.currentTimeMillis() && snoozeCriterionId == null) {
return;
}
- // TODO: write to event log
+
if (DBG) {
Slog.d(TAG, String.format("snooze event(%s, %d, %s, %s)", key, until, snoozeCriterionId,
listenerName));
@@ -4167,6 +4191,11 @@
synchronized (mNotificationLock) {
final NotificationRecord r = findNotificationByKeyLocked(key);
if (r != null) {
+ MetricsLogger.action(r.getLogMaker()
+ .setCategory(MetricsEvent.NOTIFICATION_SNOOZED)
+ .setType(MetricsEvent.TYPE_CLOSE)
+ .addTaggedData(MetricsEvent.NOTIFICATION_SNOOZED_CRITERIA,
+ snoozeCriterionId == null ? false : true));
cancelNotificationLocked(r, false, REASON_SNOOZED);
updateLightsLocked();
if (snoozeCriterionId != null) {
@@ -4185,7 +4214,6 @@
void unsnoozeNotificationInt(String key, ManagedServiceInfo listener) {
String listenerName = listener == null ? null : listener.component.toShortString();
- // TODO: write to event log
if (DBG) {
Slog.d(TAG, String.format("unsnooze event(%s, %s)", key, listenerName));
}
@@ -4298,17 +4326,12 @@
// TODO: need to combine a bunch of these getters with slightly different behavior.
// TODO: Should enqueuing just add to mNotificationsByKey instead?
private NotificationRecord findNotificationByKeyLocked(String key) {
- final int N = mNotificationList.size();
- for (int i = 0; i < N; i++) {
- if (key.equals(mNotificationList.get(i).getKey())) {
- return mNotificationList.get(i);
- }
+ NotificationRecord r;
+ if ((r = findNotificationByListLocked(mNotificationList, key)) != null) {
+ return r;
}
- final int M = mEnqueuedNotifications.size();
- for (int i = 0; i < M; i++) {
- if (key.equals(mEnqueuedNotifications.get(i).getKey())) {
- return mEnqueuedNotifications.get(i);
- }
+ if ((r = findNotificationByListLocked(mEnqueuedNotifications, key)) != null) {
+ return r;
}
return null;
}
@@ -4326,8 +4349,7 @@
}
private NotificationRecord findNotificationByListLocked(ArrayList<NotificationRecord> list,
- String pkg, String tag, int id, int userId)
- {
+ String pkg, String tag, int id, int userId) {
final int len = list.size();
for (int i = 0; i < len; i++) {
NotificationRecord r = list.get(i);
@@ -4339,6 +4361,18 @@
return null;
}
+ private NotificationRecord findNotificationByListLocked(ArrayList<NotificationRecord> list,
+ String key)
+ {
+ final int N = list.size();
+ for (int i = 0; i < N; i++) {
+ if (key.equals(list.get(i).getKey())) {
+ return list.get(i);
+ }
+ }
+ return null;
+ }
+
// lock on mNotificationList
int indexOfNotificationLocked(String key) {
final int N = mNotificationList.size();
diff --git a/services/core/java/com/android/server/notification/SnoozeHelper.java b/services/core/java/com/android/server/notification/SnoozeHelper.java
index f2aff11..0cd8cea 100644
--- a/services/core/java/com/android/server/notification/SnoozeHelper.java
+++ b/services/core/java/com/android/server/notification/SnoozeHelper.java
@@ -16,11 +16,14 @@
package com.android.server.notification;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
+import android.annotation.NonNull;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
@@ -99,7 +102,7 @@
return Collections.EMPTY_LIST;
}
- protected List<NotificationRecord> getSnoozed() {
+ protected @NonNull List<NotificationRecord> getSnoozed() {
List<NotificationRecord> snoozedForUser = new ArrayList<>();
int[] userIds = mUserProfiles.getCurrentProfileIds();
final int N = userIds.length;
@@ -270,6 +273,9 @@
final NotificationRecord record = pkgRecords.remove(key);
if (record != null) {
+ MetricsLogger.action(record.getLogMaker()
+ .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED)
+ .setType(MetricsProto.MetricsEvent.TYPE_OPEN));
mCallback.repost(userId, record);
}
}
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 66fb976..75190f3 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -50,14 +50,18 @@
import android.os.SystemClock;
import android.os.UserHandle;
import android.provider.Settings.Global;
+import android.service.notification.Condition;
import android.service.notification.ConditionProviderService;
+import android.service.notification.NotificationServiceDumpProto;
import android.service.notification.ZenModeConfig;
import android.service.notification.ZenModeConfig.EventInfo;
import android.service.notification.ZenModeConfig.ScheduleInfo;
import android.service.notification.ZenModeConfig.ZenRule;
+import android.service.notification.ZenModeProto;
import android.util.AndroidRuntimeException;
import android.util.Log;
import android.util.SparseArray;
+import android.util.proto.ProtoOutputStream;
import com.android.internal.R;
import com.android.internal.logging.MetricsLogger;
@@ -488,6 +492,24 @@
}
}
+ void dump(ProtoOutputStream proto) {
+
+ proto.write(ZenModeProto.ZEN_MODE, mZenMode);
+ synchronized (mConfig) {
+ if (mConfig.manualRule != null) {
+ proto.write(ZenModeProto.ENABLED_ACTIVE_CONDITIONS, mConfig.manualRule.toString());
+ }
+ for (ZenRule rule : mConfig.automaticRules.values()) {
+ if (rule.enabled && rule.condition.state == Condition.STATE_TRUE
+ && !rule.snoozing) {
+ proto.write(ZenModeProto.ENABLED_ACTIVE_CONDITIONS, rule.toString());
+ }
+ }
+ proto.write(ZenModeProto.POLICY, mConfig.toNotificationPolicy().toString());
+ proto.write(ZenModeProto.SUPPRESSED_EFFECTS, mSuppressedEffects);
+ }
+ }
+
public void dump(PrintWriter pw, String prefix) {
pw.print(prefix); pw.print("mZenMode=");
pw.println(Global.zenModeToString(mZenMode));
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index ebd0b34..0d63f72 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -126,6 +126,7 @@
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.AppsQueryHelper;
+import android.content.pm.ChangedPackages;
import android.content.pm.ComponentInfo;
import android.content.pm.InstantAppInfo;
import android.content.pm.EphemeralRequest;
@@ -720,6 +721,21 @@
private final InstantAppRegistry mInstantAppRegistry;
+ @GuardedBy("mPackages")
+ int mChangedPackagesSequenceNumber;
+ /**
+ * List of changed [installed, removed or updated] packages.
+ * mapping from user id -> sequence number -> package name
+ */
+ @GuardedBy("mPackages")
+ final SparseArray<SparseArray<String>> mChangedPackages = new SparseArray<>();
+ /**
+ * The sequence number of the last change to a package.
+ * mapping from user id -> package name -> sequence number
+ */
+ @GuardedBy("mPackages")
+ final SparseArray<Map<String, Integer>> mChangedPackagesSequenceNumbers = new SparseArray<>();
+
public static final class SharedLibraryEntry {
public final String path;
public final String apk;
@@ -2141,6 +2157,7 @@
pkgSetting.setInstalled(install, UserHandle.USER_SYSTEM);
}
}
+ scheduleWritePackageRestrictionsLocked(UserHandle.USER_SYSTEM);
}
}
@@ -4220,6 +4237,52 @@
}
}
+ private void updateSequenceNumberLP(String packageName, int[] userList) {
+ for (int i = userList.length - 1; i >= 0; --i) {
+ final int userId = userList[i];
+ SparseArray<String> changedPackages = mChangedPackages.get(userId);
+ if (changedPackages == null) {
+ changedPackages = new SparseArray<>();
+ mChangedPackages.put(userId, changedPackages);
+ }
+ Map<String, Integer> sequenceNumbers = mChangedPackagesSequenceNumbers.get(userId);
+ if (sequenceNumbers == null) {
+ sequenceNumbers = new HashMap<>();
+ mChangedPackagesSequenceNumbers.put(userId, sequenceNumbers);
+ }
+ final Integer sequenceNumber = sequenceNumbers.get(packageName);
+ if (sequenceNumber != null) {
+ changedPackages.remove(sequenceNumber);
+ }
+ changedPackages.put(mChangedPackagesSequenceNumber, packageName);
+ sequenceNumbers.put(packageName, mChangedPackagesSequenceNumber);
+ }
+ mChangedPackagesSequenceNumber++;
+ }
+
+ @Override
+ public ChangedPackages getChangedPackages(int sequenceNumber, int userId) {
+ synchronized (mPackages) {
+ if (sequenceNumber >= mChangedPackagesSequenceNumber) {
+ return null;
+ }
+ final SparseArray<String> changedPackages = mChangedPackages.get(userId);
+ if (changedPackages == null) {
+ return null;
+ }
+ final List<String> packageNames =
+ new ArrayList<>(mChangedPackagesSequenceNumber - sequenceNumber);
+ for (int i = sequenceNumber; i < mChangedPackagesSequenceNumber; i++) {
+ final String packageName = changedPackages.get(i);
+ if (packageName != null) {
+ packageNames.add(packageName);
+ }
+ }
+ return packageNames.isEmpty()
+ ? null : new ChangedPackages(mChangedPackagesSequenceNumber, packageNames);
+ }
+ }
+
@Override
public @NonNull ParceledListSlice<FeatureInfo> getSystemAvailableFeatures() {
ArrayList<FeatureInfo> res;
@@ -13251,6 +13314,7 @@
pkgSetting.setHidden(false, userId);
pkgSetting.setInstallReason(installReason, userId);
mSettings.writePackageRestrictionsLPr(userId);
+ mSettings.writeKernelMappingLPr(pkgSetting);
installed = true;
}
}
@@ -13263,6 +13327,9 @@
}
}
sendPackageAddedForUser(packageName, pkgSetting, userId);
+ synchronized (mPackages) {
+ updateSequenceNumberLP(packageName, new int[]{ userId });
+ }
}
} finally {
Binder.restoreCallingIdentity(callingId);
@@ -16345,6 +16412,7 @@
//note that the new package setting would have already been
//added to mPackages. It hasn't been persisted yet.
mSettings.setInstallStatus(pkgName, PackageSettingBase.PKG_INSTALL_INCOMPLETE);
+ // TODO: Remove this write? It's also written at the end of this method
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "writeSettings");
mSettings.writeLPr();
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
@@ -16418,6 +16486,7 @@
} else if (!previousUserIds.contains(userId)) {
ps.setInstallReason(installReason, userId);
}
+ mSettings.writeKernelMappingLPr(ps);
}
res.name = pkgName;
res.uid = newPackage.applicationInfo.uid;
@@ -16847,6 +16916,10 @@
sUserManager.getUserIds(), true);
}
}
+
+ if (res.returnCode == PackageManager.INSTALL_SUCCEEDED) {
+ updateSequenceNumberLP(pkgName, res.newUsers);
+ }
}
}
@@ -17429,6 +17502,7 @@
if (res) {
mInstantAppRegistry.onPackageUninstalledLPw(uninstalledPs.pkg,
info.removedUsers);
+ updateSequenceNumberLP(packageName, info.removedUsers);
}
}
}
@@ -17597,6 +17671,7 @@
// writer
synchronized (mPackages) {
+ boolean installedStateChanged = false;
if (deletedPs != null) {
if ((flags&PackageManager.DELETE_KEEP_DATA) == 0) {
clearIntentFilterVerificationsLPw(deletedPs.name, UserHandle.USER_ALL);
@@ -17644,6 +17719,9 @@
if (DEBUG_REMOVE) {
Slog.d(TAG, " user " + userId + " => " + installed);
}
+ if (installed != ps.getInstalled(userId)) {
+ installedStateChanged = true;
+ }
ps.setInstalled(installed, userId);
}
}
@@ -17653,6 +17731,9 @@
// Save settings now
mSettings.writeLPr();
}
+ if (installedStateChanged) {
+ mSettings.writeKernelMappingLPr(ps);
+ }
}
if (removedAppId != -1) {
// A user ID was deleted here. Go through all users and remove it
@@ -17795,6 +17876,7 @@
UPDATE_PERMISSIONS_ALL | UPDATE_PERMISSIONS_REPLACE_PKG);
if (applyUserRestrictions) {
+ boolean installedStateChanged = false;
if (DEBUG_REMOVE) {
Slog.d(TAG, "Propagating install state across reinstall");
}
@@ -17803,6 +17885,9 @@
if (DEBUG_REMOVE) {
Slog.d(TAG, " user " + userId + " => " + installed);
}
+ if (installed != ps.getInstalled(userId)) {
+ installedStateChanged = true;
+ }
ps.setInstalled(installed, userId);
mSettings.writeRuntimePermissionsForUserLPr(userId, false);
@@ -17810,6 +17895,9 @@
// Regardless of writeSettings we need to ensure that this restriction
// state propagation is persisted
mSettings.writeAllUsersPackageRestrictionsLPr();
+ if (installedStateChanged) {
+ mSettings.writeKernelMappingLPr(ps);
+ }
}
// can downgrade to reader here
if (writeSettings) {
@@ -18013,6 +18101,7 @@
// broadcasts will be sent correctly.
if (DEBUG_REMOVE) Slog.d(TAG, "Not installed by other users, full delete");
ps.setInstalled(true, user.getIdentifier());
+ mSettings.writeKernelMappingLPr(ps);
}
} else {
// This is a system app, so we assume that the
@@ -18122,6 +18211,7 @@
ps.readUserState(nextUserId).domainVerificationStatus, 0,
PackageManager.INSTALL_REASON_UNKNOWN);
}
+ mSettings.writeKernelMappingLPr(ps);
}
private boolean clearPackageStateForUserLIF(PackageSetting ps, int userId,
@@ -19725,6 +19815,7 @@
}
}
scheduleWritePackageRestrictionsLocked(userId);
+ updateSequenceNumberLP(packageName, new int[] { userId });
components = mPendingBroadcasts.get(userId, packageName);
final boolean newPackage = components == null;
if (newPackage) {
diff --git a/services/core/java/com/android/server/pm/PackageSettingBase.java b/services/core/java/com/android/server/pm/PackageSettingBase.java
index b63edfd..0e11b0c 100644
--- a/services/core/java/com/android/server/pm/PackageSettingBase.java
+++ b/services/core/java/com/android/server/pm/PackageSettingBase.java
@@ -40,6 +40,9 @@
* Settings base class for pending and resolved classes.
*/
abstract class PackageSettingBase extends SettingBase {
+
+ private static final int[] EMPTY_INT_ARRAY = new int[0];
+
/**
* Indicates the state of installation. Used by PackageManager to figure out
* incomplete installations. Say a package is being installed (the state is
@@ -502,6 +505,25 @@
userState.delete(userId);
}
+ public int[] getNotInstalledUserIds() {
+ int count = 0;
+ int userStateCount = userState.size();
+ for (int i = 0; i < userStateCount; i++) {
+ if (userState.valueAt(i).installed == false) {
+ count++;
+ }
+ }
+ if (count == 0) return EMPTY_INT_ARRAY;
+ int[] excludedUserIds = new int[count];
+ int idx = 0;
+ for (int i = 0; i < userStateCount; i++) {
+ if (userState.valueAt(i).installed == false) {
+ excludedUserIds[idx++] = userState.keyAt(i);
+ }
+ }
+ return excludedUserIds;
+ }
+
IntentFilterVerificationInfo getIntentFilterVerificationInfo() {
return verificationInfo;
}
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 281e445..6156802 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -252,6 +252,7 @@
private final File mPackageListFilename;
private final File mStoppedPackagesFilename;
private final File mBackupStoppedPackagesFilename;
+ /** The top level directory in configfs for sdcardfs to push the package->uid,userId mappings */
private final File mKernelMappingFilename;
/** Map from package name to settings */
@@ -260,8 +261,8 @@
/** List of packages that installed other packages */
final ArraySet<String> mInstallerPackages = new ArraySet<>();
- /** Map from package name to appId */
- private final ArrayMap<String, Integer> mKernelMapping = new ArrayMap<>();
+ /** Map from package name to appId and excluded userids */
+ private final ArrayMap<String, KernelPackageState> mKernelMapping = new ArrayMap<>();
// List of replaced system applications
private final ArrayMap<String, PackageSetting> mDisabledSysPackages =
@@ -271,6 +272,11 @@
private final ArrayMap<String, IntentFilterVerificationInfo> mRestoredIntentFilterVerifications =
new ArrayMap<String, IntentFilterVerificationInfo>();
+ private static final class KernelPackageState {
+ int appId;
+ int[] excludedUserIds;
+ }
+
// Bookkeeping for restored user permission grants
final class RestoredPermissionGrant {
String permissionName;
@@ -2512,6 +2518,15 @@
//Debug.stopMethodTracing();
}
+ private void writeKernelRemoveUserLPr(int userId) {
+ if (mKernelMappingFilename == null) return;
+
+ File removeUserIdFile = new File(mKernelMappingFilename, "remove_userid");
+ if (DEBUG_KERNEL) Slog.d(TAG, "Writing " + userId + " to " + removeUserIdFile
+ .getAbsolutePath());
+ writeIntToFile(removeUserIdFile, userId);
+ }
+
void writeKernelMappingLPr() {
if (mKernelMappingFilename == null) return;
@@ -2538,27 +2553,63 @@
}
void writeKernelMappingLPr(PackageSetting ps) {
- if (mKernelMappingFilename == null) return;
+ if (mKernelMappingFilename == null || ps == null || ps.name == null) return;
- final Integer cur = mKernelMapping.get(ps.name);
- if (cur != null && cur.intValue() == ps.appId) {
- // Ignore when mapping already matches
- return;
+ KernelPackageState cur = mKernelMapping.get(ps.name);
+ final boolean firstTime = cur == null;
+ int[] excludedUserIds = ps.getNotInstalledUserIds();
+ final boolean userIdsChanged = firstTime
+ || !Arrays.equals(excludedUserIds, cur.excludedUserIds);
+
+ // Package directory
+ final File dir = new File(mKernelMappingFilename, ps.name);
+
+ if (firstTime) {
+ dir.mkdir();
+ // Create a new mapping state
+ cur = new KernelPackageState();
+ mKernelMapping.put(ps.name, cur);
}
- if (DEBUG_KERNEL) Slog.d(TAG, "Mapping " + ps.name + " to " + ps.appId);
+ // If mapping is incorrect or non-existent, write the appid file
+ if (cur.appId != ps.appId) {
+ final File appIdFile = new File(dir, "appid");
+ writeIntToFile(appIdFile, ps.appId);
+ if (DEBUG_KERNEL) Slog.d(TAG, "Mapping " + ps.name + " to " + ps.appId);
+ }
- final File dir = new File(mKernelMappingFilename, ps.name);
- dir.mkdir();
+ if (userIdsChanged) {
+ // Build the exclusion list -- the ids to add to the exclusion list
+ for (int i = 0; i < excludedUserIds.length; i++) {
+ if (cur.excludedUserIds == null || !ArrayUtils.contains(cur.excludedUserIds,
+ excludedUserIds[i])) {
+ writeIntToFile(new File(dir, "excluded_userids"), excludedUserIds[i]);
+ if (DEBUG_KERNEL) Slog.d(TAG, "Writing " + excludedUserIds[i] + " to "
+ + ps.name + "/excluded_userids");
+ }
+ }
+ // Build the inclusion list -- the ids to remove from the exclusion list
+ if (cur.excludedUserIds != null) {
+ for (int i = 0; i < cur.excludedUserIds.length; i++) {
+ if (!ArrayUtils.contains(excludedUserIds, cur.excludedUserIds[i])) {
+ writeIntToFile(new File(dir, "clear_userid"),
+ cur.excludedUserIds[i]);
+ if (DEBUG_KERNEL) Slog.d(TAG, "Writing " + cur.excludedUserIds[i] + " to "
+ + ps.name + "/clear_userid");
- final File file = new File(dir, "appid");
+ }
+ }
+ }
+ cur.excludedUserIds = excludedUserIds;
+ }
+ }
+
+ private void writeIntToFile(File file, int value) {
try {
- // Note that the use of US_ASCII here is safe, we're only writing a decimal
- // number to the file.
FileUtils.bytesToFile(file.getAbsolutePath(),
- Integer.toString(ps.appId).getBytes(StandardCharsets.US_ASCII));
- mKernelMapping.put(ps.name, ps.appId);
+ Integer.toString(value).getBytes(StandardCharsets.US_ASCII));
} catch (IOException ignored) {
+ Slog.w(TAG, "Couldn't write " + value + " to " + file.getAbsolutePath());
}
}
@@ -4081,6 +4132,9 @@
!ArrayUtils.contains(disallowedPackages, ps.name);
// Only system apps are initially installed.
ps.setInstalled(shouldInstall, userHandle);
+ if (!shouldInstall) {
+ writeKernelMappingLPr(ps);
+ }
// Need to create a data directory for all apps under this user. Accumulate all
// required args and call the installer after mPackages lock has been released
volumeUuids[i] = ps.volumeUuid;
@@ -4123,6 +4177,10 @@
mRuntimePermissionsPersistence.onUserRemovedLPw(userId);
writePackageListLPr();
+
+ // Inform kernel that the user was removed, so that packages are marked uninstalled
+ // for sdcardfs
+ writeKernelRemoveUserLPr(userId);
}
void removeCrossProfileIntentFiltersLPw(int userId) {
diff --git a/services/core/jni/Android.mk b/services/core/jni/Android.mk
index eab5d8a..2c3cda5 100644
--- a/services/core/jni/Android.mk
+++ b/services/core/jni/Android.mk
@@ -19,6 +19,7 @@
$(LOCAL_REL_DIR)/com_android_server_location_GnssLocationProvider.cpp \
$(LOCAL_REL_DIR)/com_android_server_power_PowerManagerService.cpp \
$(LOCAL_REL_DIR)/com_android_server_SerialService.cpp \
+ $(LOCAL_REL_DIR)/com_android_server_SyntheticPasswordManager.cpp \
$(LOCAL_REL_DIR)/com_android_server_storage_AppFuseBridge.cpp \
$(LOCAL_REL_DIR)/com_android_server_SystemServer.cpp \
$(LOCAL_REL_DIR)/com_android_server_tv_TvUinputBridge.cpp \
@@ -33,12 +34,14 @@
LOCAL_C_INCLUDES += \
$(JNI_H_INCLUDE) \
+ external/scrypt/lib/crypto \
frameworks/base/services \
frameworks/base/libs \
frameworks/base/libs/hwui \
frameworks/base/core/jni \
frameworks/native/services \
system/core/libappfuse/include \
+ system/gatekeeper/include \
system/security/keystore/include \
$(call include-path-for, libhardware)/hardware \
$(call include-path-for, libhardware_legacy)/hardware_legacy \
@@ -50,6 +53,7 @@
libappfuse \
libbinder \
libcutils \
+ libcrypto \
liblog \
libhardware \
libhardware_legacy \
@@ -83,3 +87,5 @@
android.hardware.tv.input@1.0 \
android.hardware.vibrator@1.0 \
android.hardware.vr@1.0 \
+
+LOCAL_STATIC_LIBRARIES += libscrypt_static
\ No newline at end of file
diff --git a/services/core/jni/com_android_server_SyntheticPasswordManager.cpp b/services/core/jni/com_android_server_SyntheticPasswordManager.cpp
new file mode 100644
index 0000000..a9f7b9f
--- /dev/null
+++ b/services/core/jni/com_android_server_SyntheticPasswordManager.cpp
@@ -0,0 +1,90 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "SyntheticPasswordManager"
+
+#include "JNIHelp.h"
+#include "jni.h"
+
+#include <android_runtime/Log.h>
+#include <utils/Timers.h>
+#include <utils/misc.h>
+#include <utils/String8.h>
+#include <utils/Log.h>
+#include <gatekeeper/password_handle.h>
+
+
+extern "C" {
+#include "crypto_scrypt.h"
+}
+
+namespace android {
+
+static jlong android_server_SyntheticPasswordManager_nativeSidFromPasswordHandle(JNIEnv* env, jobject, jbyteArray handleArray) {
+
+ jbyte* data = (jbyte*)env->GetPrimitiveArrayCritical(handleArray, NULL);
+
+ if (data != NULL) {
+ const gatekeeper::password_handle_t *handle =
+ reinterpret_cast<const gatekeeper::password_handle_t *>(data);
+ jlong sid = handle->user_id;
+ env->ReleasePrimitiveArrayCritical(handleArray, data, JNI_ABORT);
+ return sid;
+ } else {
+ return 0;
+ }
+}
+
+static jbyteArray android_server_SyntheticPasswordManager_nativeScrypt(JNIEnv* env, jobject, jbyteArray password, jbyteArray salt, jint N, jint r, jint p, jint outLen) {
+ if (!password || !salt) {
+ return NULL;
+ }
+
+ int passwordLen = env->GetArrayLength(password);
+ int saltLen = env->GetArrayLength(salt);
+ jbyteArray ret = env->NewByteArray(outLen);
+
+ jbyte* passwordPtr = (jbyte*)env->GetByteArrayElements(password, NULL);
+ jbyte* saltPtr = (jbyte*)env->GetByteArrayElements(salt, NULL);
+ jbyte* retPtr = (jbyte*)env->GetByteArrayElements(ret, NULL);
+
+ int rc = crypto_scrypt((const uint8_t *)passwordPtr, passwordLen,
+ (const uint8_t *)saltPtr, saltLen, N, r, p, (uint8_t *)retPtr,
+ outLen);
+ env->ReleaseByteArrayElements(password, passwordPtr, JNI_ABORT);
+ env->ReleaseByteArrayElements(salt, saltPtr, JNI_ABORT);
+ env->ReleaseByteArrayElements(ret, retPtr, 0);
+
+ if (!rc) {
+ return ret;
+ } else {
+ SLOGE("scrypt failed");
+ return NULL;
+ }
+}
+
+static const JNINativeMethod sMethods[] = {
+ /* name, signature, funcPtr */
+ {"nativeSidFromPasswordHandle", "([B)J", (void*)android_server_SyntheticPasswordManager_nativeSidFromPasswordHandle},
+ {"nativeScrypt", "([B[BIIII)[B", (void*)android_server_SyntheticPasswordManager_nativeScrypt},
+};
+
+int register_android_server_SyntheticPasswordManager(JNIEnv* env) {
+ return jniRegisterNativeMethods(env, "com/android/server/SyntheticPasswordManager",
+ sMethods, NELEM(sMethods));
+}
+
+} /* namespace android */
diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp
index 6f505d5..899640e 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -45,6 +45,7 @@
int register_android_server_PersistentDataBlockService(JNIEnv* env);
int register_android_server_Watchdog(JNIEnv* env);
int register_android_server_HardwarePropertiesManagerService(JNIEnv* env);
+int register_android_server_SyntheticPasswordManager(JNIEnv* env);
};
using namespace android;
@@ -85,6 +86,7 @@
register_android_server_Watchdog(env);
register_android_server_HardwarePropertiesManagerService(env);
register_android_server_storage_AppFuse(env);
+ register_android_server_SyntheticPasswordManager(env);
return JNI_VERSION_1_4;
}
diff --git a/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java
index db010b8..88f1a53 100644
--- a/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -280,6 +280,21 @@
@Test
@UiThreadTest
+ public void testCancelNotificationWhilePostedAndEnqueued() throws Exception {
+ mBinderService.enqueueNotificationWithTag(mContext.getPackageName(), "opPkg", "tag", 0,
+ generateNotificationRecord(null).getNotification(), new int[1], 0);
+ waitForIdle();
+ mBinderService.enqueueNotificationWithTag(mContext.getPackageName(), "opPkg", "tag", 0,
+ generateNotificationRecord(null).getNotification(), new int[1], 0);
+ mBinderService.cancelNotificationWithTag(mContext.getPackageName(), "tag", 0, 0);
+ waitForIdle();
+ StatusBarNotification[] notifs =
+ mBinderService.getActiveNotifications(mContext.getPackageName());
+ assertEquals(0, notifs.length);
+ }
+
+ @Test
+ @UiThreadTest
public void testCancelNotificationsFromListenerImmediatelyAfterEnqueue() throws Exception {
final StatusBarNotification sbn = generateNotificationRecord(null).sbn;
mBinderService.enqueueNotificationWithTag(sbn.getPackageName(), "opPkg", "tag",
diff --git a/services/tests/servicestests/src/com/android/server/BaseLockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/BaseLockSettingsServiceTests.java
index c89d35c..c6265bc 100644
--- a/services/tests/servicestests/src/com/android/server/BaseLockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/BaseLockSettingsServiceTests.java
@@ -134,5 +134,13 @@
File storageDir = mStorage.mStorageDir;
assertTrue(FileUtils.deleteContents(storageDir));
}
+
+ protected static void assertArrayEquals(byte[] expected, byte[] actual) {
+ assertTrue(Arrays.equals(expected, actual));
+ }
+
+ protected static void assertArrayNotSame(byte[] expected, byte[] actual) {
+ assertFalse(Arrays.equals(expected, actual));
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/LockSettingsServiceTestable.java b/services/tests/servicestests/src/com/android/server/LockSettingsServiceTestable.java
index 613ec0b..cfdb5b1 100644
--- a/services/tests/servicestests/src/com/android/server/LockSettingsServiceTestable.java
+++ b/services/tests/servicestests/src/com/android/server/LockSettingsServiceTestable.java
@@ -21,9 +21,11 @@
import android.app.IActivityManager;
import android.content.Context;
import android.os.Handler;
+import android.os.Process;
+import android.os.RemoteException;
import android.os.storage.IStorageManager;
import android.security.KeyStore;
-import android.service.gatekeeper.IGateKeeperService;
+import android.security.keystore.KeyPermanentlyInvalidatedException;
import com.android.internal.widget.LockPatternUtils;
@@ -38,16 +40,18 @@
private IActivityManager mActivityManager;
private LockPatternUtils mLockPatternUtils;
private IStorageManager mStorageManager;
+ private MockGateKeeperService mGatekeeper;
public MockInjector(Context context, LockSettingsStorage storage, KeyStore keyStore,
IActivityManager activityManager, LockPatternUtils lockPatternUtils,
- IStorageManager storageManager) {
+ IStorageManager storageManager, MockGateKeeperService gatekeeper) {
super(context);
mLockSettingsStorage = storage;
mKeyStore = keyStore;
mActivityManager = activityManager;
mLockPatternUtils = lockPatternUtils;
mStorageManager = storageManager;
+ mGatekeeper = gatekeeper;
}
@Override
@@ -89,13 +93,25 @@
public IStorageManager getStorageManager() {
return mStorageManager;
}
+
+ @Override
+ public SyntheticPasswordManager getSyntheticPasswordManager(LockSettingsStorage storage) {
+ return new MockSyntheticPasswordManager(storage, mGatekeeper);
+ }
+
+ @Override
+ public int binderGetCallingUid() {
+ return Process.SYSTEM_UID;
+ }
+
+
}
protected LockSettingsServiceTestable(Context context, LockPatternUtils lockPatternUtils,
- LockSettingsStorage storage, IGateKeeperService gatekeeper, KeyStore keystore,
+ LockSettingsStorage storage, MockGateKeeperService gatekeeper, KeyStore keystore,
IStorageManager storageManager, IActivityManager mActivityManager) {
super(new MockInjector(context, storage, keystore, mActivityManager, lockPatternUtils,
- storageManager));
+ storageManager, gatekeeper));
mGateKeeperService = gatekeeper;
}
@@ -105,12 +121,18 @@
}
@Override
- protected String getDecryptedPasswordForTiedProfile(int userId) throws FileNotFoundException {
+ protected String getDecryptedPasswordForTiedProfile(int userId) throws FileNotFoundException, KeyPermanentlyInvalidatedException {
byte[] storedData = mStorage.readChildProfileLock(userId);
if (storedData == null) {
throw new FileNotFoundException("Child profile lock file not found");
}
+ try {
+ if (mGateKeeperService.getSecureUserId(userId) == 0) {
+ throw new KeyPermanentlyInvalidatedException();
+ }
+ } catch (RemoteException e) {
+ // shouldn't happen.
+ }
return new String(storedData);
}
-
}
diff --git a/services/tests/servicestests/src/com/android/server/LockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/LockSettingsServiceTests.java
index 4c2e171..ae9762a 100644
--- a/services/tests/servicestests/src/com/android/server/LockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/LockSettingsServiceTests.java
@@ -123,6 +123,12 @@
UnifiedPassword, PRIMARY_USER_ID);
mStorageManager.setIgnoreBadUnlock(false);
assertEquals(profileSid, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID));
+
+ //Clear unified challenge
+ mService.setLockCredential(null, LockPatternUtils.CREDENTIAL_TYPE_NONE, UnifiedPassword,
+ PRIMARY_USER_ID);
+ assertEquals(0, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
+ assertEquals(0, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID));
}
public void testManagedProfileSeparateChallenge() throws RemoteException {
diff --git a/services/tests/servicestests/src/com/android/server/LockSettingsStorageTestable.java b/services/tests/servicestests/src/com/android/server/LockSettingsStorageTestable.java
index e81b02f..18da1a5 100644
--- a/services/tests/servicestests/src/com/android/server/LockSettingsStorageTestable.java
+++ b/services/tests/servicestests/src/com/android/server/LockSettingsStorageTestable.java
@@ -31,19 +31,36 @@
@Override
String getLockPatternFilename(int userId) {
- return new File(mStorageDir,
- super.getLockPatternFilename(userId).replace('/', '-')).getAbsolutePath();
+ return makeDirs(mStorageDir,
+ super.getLockPatternFilename(userId)).getAbsolutePath();
}
@Override
String getLockPasswordFilename(int userId) {
- return new File(mStorageDir,
- super.getLockPasswordFilename(userId).replace('/', '-')).getAbsolutePath();
+ return makeDirs(mStorageDir,
+ super.getLockPasswordFilename(userId)).getAbsolutePath();
}
@Override
String getChildProfileLockFile(int userId) {
- return new File(mStorageDir,
- super.getChildProfileLockFile(userId).replace('/', '-')).getAbsolutePath();
+ return makeDirs(mStorageDir,
+ super.getChildProfileLockFile(userId)).getAbsolutePath();
+ }
+
+ @Override
+ protected File getSyntheticPasswordDirectoryForUser(int userId) {
+ return makeDirs(mStorageDir, super.getSyntheticPasswordDirectoryForUser(
+ userId).getAbsolutePath());
+ }
+
+ private File makeDirs(File baseDir, String filePath) {
+ File path = new File(filePath);
+ if (path.getParent() == null) {
+ return new File(baseDir, filePath);
+ } else {
+ File mappedDir = new File(baseDir, path.getParent());
+ mappedDir.mkdirs();
+ return new File(mappedDir, path.getName());
+ }
}
}
diff --git a/services/tests/servicestests/src/com/android/server/LockSettingsStorageTests.java b/services/tests/servicestests/src/com/android/server/LockSettingsStorageTests.java
index d110fea..c68fbdc 100644
--- a/services/tests/servicestests/src/com/android/server/LockSettingsStorageTests.java
+++ b/services/tests/servicestests/src/com/android/server/LockSettingsStorageTests.java
@@ -329,6 +329,16 @@
assertEquals("/data/system/users/3/gatekeeper.password.key", storage.getLockPasswordFilename(3));
}
+ public void testSyntheticPasswordState() {
+ final byte[] data = {1,2,3,4};
+ mStorage.writeSyntheticPasswordState(10, 1234L, "state", data);
+ assertArrayEquals(data, mStorage.readSyntheticPasswordState(10, 1234L, "state"));
+ assertEquals(null, mStorage.readSyntheticPasswordState(0, 1234L, "state"));
+
+ mStorage.deleteSyntheticPasswordState(10, 1234L, "state", true);
+ assertEquals(null, mStorage.readSyntheticPasswordState(10, 1234L, "state"));
+ }
+
private static void assertArrayEquals(byte[] expected, byte[] actual) {
if (!Arrays.equals(expected, actual)) {
fail("expected:<" + Arrays.toString(expected) +
diff --git a/services/tests/servicestests/src/com/android/server/MockGateKeeperService.java b/services/tests/servicestests/src/com/android/server/MockGateKeeperService.java
index 15983ca..bc93341 100644
--- a/services/tests/servicestests/src/com/android/server/MockGateKeeperService.java
+++ b/services/tests/servicestests/src/com/android/server/MockGateKeeperService.java
@@ -149,6 +149,15 @@
return authTokenMap.get(uid);
}
+ public AuthToken getAuthTokenForSid(long sid) {
+ for(AuthToken token : authTokenMap.values()) {
+ if (token.sid == sid) {
+ return token;
+ }
+ }
+ return null;
+ }
+
public void clearAuthToken(int uid) {
authTokenMap.remove(uid);
}
diff --git a/services/tests/servicestests/src/com/android/server/MockSyntheticPasswordManager.java b/services/tests/servicestests/src/com/android/server/MockSyntheticPasswordManager.java
new file mode 100644
index 0000000..93e3fc6
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/MockSyntheticPasswordManager.java
@@ -0,0 +1,102 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server;
+
+import android.util.ArrayMap;
+
+import junit.framework.AssertionFailedError;
+
+import java.nio.ByteBuffer;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Arrays;
+
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+
+public class MockSyntheticPasswordManager extends SyntheticPasswordManager {
+
+ private MockGateKeeperService mGateKeeper;
+
+ public MockSyntheticPasswordManager(LockSettingsStorage storage,
+ MockGateKeeperService gatekeeper) {
+ super(storage);
+ mGateKeeper = gatekeeper;
+ }
+
+ private ArrayMap<String, byte[]> mBlobs = new ArrayMap<>();
+
+ @Override
+ protected byte[] decryptSPBlob(String blobKeyName, byte[] blob, byte[] applicationId) {
+ if (mBlobs.containsKey(blobKeyName) && !Arrays.equals(mBlobs.get(blobKeyName), blob)) {
+ throw new AssertionFailedError("blobKeyName content is overwritten: " + blobKeyName);
+ }
+ ByteBuffer buffer = ByteBuffer.allocate(blob.length);
+ buffer.put(blob, 0, blob.length);
+ buffer.flip();
+ int len;
+ len = buffer.getInt();
+ byte[] data = new byte[len];
+ buffer.get(data);
+ len = buffer.getInt();
+ byte[] appId = new byte[len];
+ buffer.get(appId);
+ long sid = buffer.getLong();
+ if (!Arrays.equals(appId, applicationId)) {
+ throw new AssertionFailedError("Invalid application id");
+ }
+ if (sid != 0 && mGateKeeper.getAuthTokenForSid(sid) == null) {
+ throw new AssertionFailedError("No valid auth token");
+ }
+ return data;
+ }
+
+ @Override
+ protected byte[] createSPBlob(String blobKeyName, byte[] data, byte[] applicationId, long sid) {
+ ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES + data.length + Integer.BYTES
+ + applicationId.length + Long.BYTES);
+ buffer.putInt(data.length);
+ buffer.put(data);
+ buffer.putInt(applicationId.length);
+ buffer.put(applicationId);
+ buffer.putLong(sid);
+ byte[] result = buffer.array();
+ mBlobs.put(blobKeyName, result);
+ return result;
+ }
+
+ @Override
+ protected void destroySPBlobKey(String keyAlias) {
+ }
+
+ @Override
+ protected long sidFromPasswordHandle(byte[] handle) {
+ return new MockGateKeeperService.VerifyHandle(handle).sid;
+ }
+
+ @Override
+ protected byte[] scrypt(String password, byte[] salt, int N, int r, int p, int outLen) {
+ try {
+ PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 10, outLen * 8);
+ SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
+ return f.generateSecret(spec).getEncoded();
+ } catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+}
diff --git a/services/tests/servicestests/src/com/android/server/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/SyntheticPasswordTests.java
new file mode 100644
index 0000000..6e5ade1
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/SyntheticPasswordTests.java
@@ -0,0 +1,329 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server;
+
+import static com.android.internal.widget.LockPatternUtils.SYNTHETIC_PASSWORD_ENABLED_KEY;
+import static com.android.internal.widget.LockPatternUtils.SYNTHETIC_PASSWORD_HANDLE_KEY;
+
+import android.os.RemoteException;
+import android.os.UserHandle;
+
+import com.android.internal.widget.LockPatternUtils;
+import com.android.internal.widget.VerifyCredentialResponse;
+import com.android.server.SyntheticPasswordManager.AuthenticationResult;
+import com.android.server.SyntheticPasswordManager.AuthenticationToken;
+
+
+/**
+ * runtest frameworks-services -c com.android.server.SyntheticPasswordTests
+ */
+public class SyntheticPasswordTests extends BaseLockSettingsServiceTests {
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ public void testPasswordBasedSyntheticPassword() throws RemoteException {
+ final int USER_ID = 10;
+ final String PASSWORD = "user-password";
+ final String BADPASSWORD = "bad-password";
+ MockSyntheticPasswordManager manager = new MockSyntheticPasswordManager(mStorage, mGateKeeperService);
+ AuthenticationToken authToken = manager.newSyntheticPasswordAndSid(mGateKeeperService, null,
+ null, USER_ID);
+ long handle = manager.createPasswordBasedSyntheticPassword(mGateKeeperService, PASSWORD,
+ LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, authToken, USER_ID);
+
+ AuthenticationResult result = manager.unwrapPasswordBasedSyntheticPassword(mGateKeeperService, handle, PASSWORD, USER_ID);
+ assertEquals(result.authToken.deriveKeyStorePassword(), authToken.deriveKeyStorePassword());
+
+ result = manager.unwrapPasswordBasedSyntheticPassword(mGateKeeperService, handle, BADPASSWORD, USER_ID);
+ assertNull(result.authToken);
+ }
+
+ private void disableSyntheticPassword(int userId) throws RemoteException {
+ mService.setLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 0, UserHandle.USER_SYSTEM);
+ }
+
+ private void enableSyntheticPassword(int userId) throws RemoteException {
+ mService.setLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 1, UserHandle.USER_SYSTEM);
+ }
+
+ private boolean hasSyntheticPassword(int userId) throws RemoteException {
+ return mService.getLong(SYNTHETIC_PASSWORD_HANDLE_KEY, 0, userId) != 0;
+ }
+
+ public void testPasswordMigration() throws RemoteException {
+ final String PASSWORD = "testPasswordMigration-password";
+
+ disableSyntheticPassword(PRIMARY_USER_ID);
+ mService.setLockCredential(PASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, PRIMARY_USER_ID);
+ long sid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
+ final byte[] primaryStorageKey = mStorageManager.getUserUnlockToken(PRIMARY_USER_ID);
+ enableSyntheticPassword(PRIMARY_USER_ID);
+ // Performs migration
+ assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+ mService.verifyCredential(PASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode());
+ assertEquals(sid, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
+ assertTrue(hasSyntheticPassword(PRIMARY_USER_ID));
+
+ // SP-based verification
+ assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+ mService.verifyCredential(PASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode());
+ assertArrayNotSame(primaryStorageKey, mStorageManager.getUserUnlockToken(PRIMARY_USER_ID));
+ }
+
+ private void initializeCredentialUnderSP(String password, int userId) throws RemoteException {
+ enableSyntheticPassword(userId);
+ mService.setLockCredential(password, password != null ? LockPatternUtils.CREDENTIAL_TYPE_PASSWORD : LockPatternUtils.CREDENTIAL_TYPE_NONE, null, userId);
+ }
+
+ public void testSyntheticPasswordChangeCredential() throws RemoteException {
+ final String PASSWORD = "testSyntheticPasswordChangeCredential-password";
+ final String NEWPASSWORD = "testSyntheticPasswordChangeCredential-newpassword";
+
+ initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID);
+ long sid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
+ mService.setLockCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, PASSWORD, PRIMARY_USER_ID);
+ mGateKeeperService.clearSecureUserId(PRIMARY_USER_ID);
+ assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+ mService.verifyCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode());
+ assertEquals(sid, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
+ }
+
+ public void testSyntheticPasswordVerifyCredential() throws RemoteException {
+ final String PASSWORD = "testSyntheticPasswordVerifyCredential-password";
+ final String BADPASSWORD = "testSyntheticPasswordVerifyCredential-badpassword";
+
+ initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID);
+ assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+ mService.verifyCredential(PASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode());
+
+ assertEquals(VerifyCredentialResponse.RESPONSE_ERROR,
+ mService.verifyCredential(BADPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode());
+ }
+
+ public void testSyntheticPasswordClearCredential() throws RemoteException {
+ final String PASSWORD = "testSyntheticPasswordClearCredential-password";
+ final String NEWPASSWORD = "testSyntheticPasswordClearCredential-newpassword";
+
+ initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID);
+ long sid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
+ // clear password
+ mService.setLockCredential(null, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, PASSWORD, PRIMARY_USER_ID);
+ assertEquals(0 ,mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
+
+ // set a new password
+ mService.setLockCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, PRIMARY_USER_ID);
+ assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+ mService.verifyCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode());
+ assertNotSame(sid, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
+ }
+
+ public void testSyntheticPasswordClearCredentialUntrusted() throws RemoteException {
+ final String PASSWORD = "testSyntheticPasswordClearCredential-password";
+ final String NEWPASSWORD = "testSyntheticPasswordClearCredential-newpassword";
+
+ initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID);
+ long sid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
+ // clear password
+ mService.setLockCredential(null, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, PRIMARY_USER_ID);
+ assertEquals(0 ,mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
+
+ // set a new password
+ mService.setLockCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, PRIMARY_USER_ID);
+ assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+ mService.verifyCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode());
+ assertNotSame(sid, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
+ }
+
+ public void testSyntheticPasswordChangeCredentialUntrusted() throws RemoteException {
+ final String PASSWORD = "testSyntheticPasswordClearCredential-password";
+ final String NEWPASSWORD = "testSyntheticPasswordClearCredential-newpassword";
+
+ initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID);
+ long sid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
+ // Untrusted change password
+ mService.setLockCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, PRIMARY_USER_ID);
+ assertNotSame(0 ,mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
+ assertNotSame(sid ,mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
+
+ // Verify the password
+ assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+ mService.verifyCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode());
+ }
+
+
+ public void testManagedProfileUnifiedChallengeMigration() throws RemoteException {
+ final String UnifiedPassword = "testManagedProfileUnifiedChallengeMigration-pwd";
+ disableSyntheticPassword(PRIMARY_USER_ID);
+ disableSyntheticPassword(MANAGED_PROFILE_USER_ID);
+ mService.setLockCredential(UnifiedPassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, PRIMARY_USER_ID);
+ mService.setSeparateProfileChallengeEnabled(MANAGED_PROFILE_USER_ID, false, null);
+ final long primarySid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
+ final long profileSid = mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID);
+ final byte[] primaryStorageKey = mStorageManager.getUserUnlockToken(PRIMARY_USER_ID);
+ final byte[] profileStorageKey = mStorageManager.getUserUnlockToken(MANAGED_PROFILE_USER_ID);
+ assertTrue(primarySid != 0);
+ assertTrue(profileSid != 0);
+ assertTrue(profileSid != primarySid);
+
+ // do migration
+ enableSyntheticPassword(PRIMARY_USER_ID);
+ enableSyntheticPassword(MANAGED_PROFILE_USER_ID);
+ assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+ mService.verifyCredential(UnifiedPassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode());
+
+ // verify
+ assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+ mService.verifyCredential(UnifiedPassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode());
+ assertEquals(primarySid, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
+ assertEquals(profileSid, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID));
+ assertArrayNotSame(primaryStorageKey, mStorageManager.getUserUnlockToken(PRIMARY_USER_ID));
+ assertArrayNotSame(profileStorageKey, mStorageManager.getUserUnlockToken(MANAGED_PROFILE_USER_ID));
+ assertTrue(hasSyntheticPassword(PRIMARY_USER_ID));
+ assertTrue(hasSyntheticPassword(MANAGED_PROFILE_USER_ID));
+ }
+
+ public void testManagedProfileSeparateChallengeMigration() throws RemoteException {
+ final String primaryPassword = "testManagedProfileSeparateChallengeMigration-primary";
+ final String profilePassword = "testManagedProfileSeparateChallengeMigration-profile";
+ mService.setLockCredential(primaryPassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, PRIMARY_USER_ID);
+ mService.setLockCredential(profilePassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, MANAGED_PROFILE_USER_ID);
+ final long primarySid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
+ final long profileSid = mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID);
+ final byte[] primaryStorageKey = mStorageManager.getUserUnlockToken(PRIMARY_USER_ID);
+ final byte[] profileStorageKey = mStorageManager.getUserUnlockToken(MANAGED_PROFILE_USER_ID);
+ assertTrue(primarySid != 0);
+ assertTrue(profileSid != 0);
+ assertTrue(profileSid != primarySid);
+
+ // do migration
+ enableSyntheticPassword(PRIMARY_USER_ID);
+ enableSyntheticPassword(MANAGED_PROFILE_USER_ID);
+ assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+ mService.verifyCredential(primaryPassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode());
+ assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+ mService.verifyCredential(profilePassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, MANAGED_PROFILE_USER_ID).getResponseCode());
+
+ // verify
+ assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+ mService.verifyCredential(primaryPassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode());
+ assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+ mService.verifyCredential(profilePassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, MANAGED_PROFILE_USER_ID).getResponseCode());
+ assertEquals(primarySid, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
+ assertEquals(profileSid, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID));
+ assertArrayNotSame(primaryStorageKey, mStorageManager.getUserUnlockToken(PRIMARY_USER_ID));
+ assertArrayNotSame(profileStorageKey, mStorageManager.getUserUnlockToken(MANAGED_PROFILE_USER_ID));
+ assertTrue(hasSyntheticPassword(PRIMARY_USER_ID));
+ assertTrue(hasSyntheticPassword(MANAGED_PROFILE_USER_ID));
+ }
+
+ public void testTokenBasedResetPassword() throws RemoteException {
+ final String PASSWORD = "password";
+ final String PATTERN = "123654";
+ final String TOKEN = "some-high-entropy-secure-token";
+ initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID);
+ final byte[] storageKey = mStorageManager.getUserUnlockToken(PRIMARY_USER_ID);
+
+ long handle = mService.addEscrowToken(TOKEN.getBytes(), PRIMARY_USER_ID);
+ assertFalse(mService.isEscrowTokenActive(handle, PRIMARY_USER_ID));
+
+ mService.verifyCredential(PASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode();
+ assertTrue(mService.isEscrowTokenActive(handle, PRIMARY_USER_ID));
+
+ mService.setLockCredentialWithToken(PATTERN, LockPatternUtils.CREDENTIAL_TYPE_PATTERN, handle, TOKEN.getBytes(), PRIMARY_USER_ID);
+
+ assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+ mService.verifyCredential(PATTERN, LockPatternUtils.CREDENTIAL_TYPE_PATTERN, 0, PRIMARY_USER_ID).getResponseCode());
+ assertArrayEquals(storageKey, mStorageManager.getUserUnlockToken(PRIMARY_USER_ID));
+ }
+
+ public void testTokenBasedClearPassword() throws RemoteException {
+ final String PASSWORD = "password";
+ final String PATTERN = "123654";
+ final String TOKEN = "some-high-entropy-secure-token";
+ initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID);
+ final byte[] storageKey = mStorageManager.getUserUnlockToken(PRIMARY_USER_ID);
+
+ long handle = mService.addEscrowToken(TOKEN.getBytes(), PRIMARY_USER_ID);
+ assertFalse(mService.isEscrowTokenActive(handle, PRIMARY_USER_ID));
+
+ mService.verifyCredential(PASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode();
+ assertTrue(mService.isEscrowTokenActive(handle, PRIMARY_USER_ID));
+
+ mService.setLockCredentialWithToken(null, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, handle, TOKEN.getBytes(), PRIMARY_USER_ID);
+ mService.setLockCredentialWithToken(PATTERN, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, handle, TOKEN.getBytes(), PRIMARY_USER_ID);
+
+ assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+ mService.verifyCredential(PATTERN, LockPatternUtils.CREDENTIAL_TYPE_PATTERN, 0, PRIMARY_USER_ID).getResponseCode());
+ assertArrayEquals(storageKey, mStorageManager.getUserUnlockToken(PRIMARY_USER_ID));
+ }
+
+ public void testTokenBasedResetPasswordAfterCredentialChanges() throws RemoteException {
+ final String PASSWORD = "password";
+ final String PATTERN = "123654";
+ final String NEWPASSWORD = "password";
+ final String TOKEN = "some-high-entropy-secure-token";
+ initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID);
+ final byte[] storageKey = mStorageManager.getUserUnlockToken(PRIMARY_USER_ID);
+
+ long handle = mService.addEscrowToken(TOKEN.getBytes(), PRIMARY_USER_ID);
+ assertFalse(mService.isEscrowTokenActive(handle, PRIMARY_USER_ID));
+
+ mService.verifyCredential(PASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode();
+ assertTrue(mService.isEscrowTokenActive(handle, PRIMARY_USER_ID));
+
+ mService.setLockCredential(PATTERN, LockPatternUtils.CREDENTIAL_TYPE_PATTERN, PASSWORD, PRIMARY_USER_ID);
+
+ mService.setLockCredentialWithToken(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, handle, TOKEN.getBytes(), PRIMARY_USER_ID);
+
+ assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+ mService.verifyCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode());
+ assertArrayEquals(storageKey, mStorageManager.getUserUnlockToken(PRIMARY_USER_ID));
+ }
+
+ public void testEscrowTokenActivatedImmediatelyIfNoUserPasswordNeedsMigration() throws RemoteException {
+ final String TOKEN = "some-high-entropy-secure-token";
+ enableSyntheticPassword(PRIMARY_USER_ID);
+ long handle = mService.addEscrowToken(TOKEN.getBytes(), PRIMARY_USER_ID);
+ assertTrue(mService.isEscrowTokenActive(handle, PRIMARY_USER_ID));
+ assertEquals(0, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
+ assertTrue(hasSyntheticPassword(PRIMARY_USER_ID));
+ }
+
+ public void testEscrowTokenActivatedImmediatelyIfNoUserPasswordNoMigration() throws RemoteException {
+ final String TOKEN = "some-high-entropy-secure-token";
+ initializeCredentialUnderSP(null, PRIMARY_USER_ID);
+ long handle = mService.addEscrowToken(TOKEN.getBytes(), PRIMARY_USER_ID);
+ assertTrue(mService.isEscrowTokenActive(handle, PRIMARY_USER_ID));
+ assertEquals(0, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
+ assertTrue(hasSyntheticPassword(PRIMARY_USER_ID));
+ }
+
+ // b/34600579
+ //TODO: add non-migration work profile case, and unify/un-unify transition.
+ //TODO: test token after user resets password
+ //TODO: test token based reset after unified work challenge
+ //TODO: test clear password after unified work challenge
+}
+
diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java
index 96070b8..7964cf2 100644
--- a/telecomm/java/android/telecom/TelecomManager.java
+++ b/telecomm/java/android/telecom/TelecomManager.java
@@ -317,6 +317,15 @@
public static final String EXTRA_CALL_BACK_NUMBER = "android.telecom.extra.CALL_BACK_NUMBER";
/**
+ * The number of milliseconds that Telecom should wait after disconnecting a call via the
+ * ACTION_NEW_OUTGOING_CALL broadcast, in order to wait for the app which cancelled the call
+ * to make a new one.
+ * @hide
+ */
+ public static final String EXTRA_NEW_OUTGOING_CALL_CANCEL_TIMEOUT =
+ "android.telecom.extra.NEW_OUTGOING_CALL_CANCEL_TIMEOUT";
+
+ /**
* A boolean meta-data value indicating whether an {@link InCallService} implements an
* in-call user interface. Dialer implementations (see {@link #getDefaultDialerPackage()}) which
* would also like to replace the in-call interface should set this meta-data to {@code true} in
diff --git a/test-runner/src/android/test/mock/MockPackageManager.java b/test-runner/src/android/test/mock/MockPackageManager.java
index 0dcd0f1..d51d75e 100644
--- a/test-runner/src/android/test/mock/MockPackageManager.java
+++ b/test-runner/src/android/test/mock/MockPackageManager.java
@@ -24,6 +24,7 @@
import android.content.IntentSender;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
+import android.content.pm.ChangedPackages;
import android.content.pm.InstantAppInfo;
import android.content.pm.FeatureInfo;
import android.content.pm.IPackageDataObserver;
@@ -374,6 +375,12 @@
throw new UnsupportedOperationException();
}
+ /** @hide */
+ @Override
+ public ChangedPackages getChangedPackages(int sequenceNumber) {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public ResolveInfo resolveActivity(Intent intent, int flags) {
throw new UnsupportedOperationException();
diff --git a/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java b/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java
index 35cf903..9bc8e18 100644
--- a/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java
+++ b/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java
@@ -31,6 +31,7 @@
import android.annotation.Nullable;
import android.content.res.Resources.NotFoundException;
import android.content.res.Resources.Theme;
+import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.util.DisplayMetrics;
import android.util.TypedValue;
@@ -697,6 +698,22 @@
/**
+ * Retrieve the Typeface for the attribute at <var>index</var>.
+ * @param index Index of attribute to retrieve.
+ *
+ * @return Typeface for the attribute, or null if not defined.
+ */
+ @Override
+ public Typeface getFont(int index) {
+ if (!hasValue(index)) {
+ return null;
+ }
+
+ ResourceValue value = mResourceData[index];
+ return ResourceHelper.getFont(value, mContext, mTheme);
+ }
+
+ /**
* Retrieve the CharSequence[] for the attribute at <var>index</var>.
* This gets the resource ID of the selected attribute, and uses
* {@link Resources#getTextArray Resources.getTextArray} of the owning
diff --git a/tools/layoutlib/bridge/src/android/content/res/Resources_Delegate.java b/tools/layoutlib/bridge/src/android/content/res/Resources_Delegate.java
index e0f8e1c..d71cc6f 100644
--- a/tools/layoutlib/bridge/src/android/content/res/Resources_Delegate.java
+++ b/tools/layoutlib/bridge/src/android/content/res/Resources_Delegate.java
@@ -43,6 +43,7 @@
import android.annotation.Nullable;
import android.content.res.Resources.NotFoundException;
import android.content.res.Resources.Theme;
+import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.icu.text.PluralRules;
import android.util.AttributeSet;
@@ -779,6 +780,35 @@
}
@LayoutlibDelegate
+ static Typeface getFont(Resources resources, int id) throws
+ NotFoundException {
+ Pair<String, ResourceValue> value = getResourceValue(resources, id, mPlatformResourceFlag);
+ if (value != null) {
+ return ResourceHelper.getFont(value.getSecond(), resources.mContext, null);
+ }
+
+ throwException(resources, id);
+
+ // this is not used since the method above always throws
+ return null;
+ }
+
+ @LayoutlibDelegate
+ static Typeface getFont(Resources resources, TypedValue outValue, int id) throws
+ NotFoundException {
+ Resources_Delegate.getValue(resources, id, outValue, true);
+ if (outValue.string != null) {
+ return ResourceHelper.getFont(outValue.string.toString(), resources.mContext, null,
+ mPlatformResourceFlag[0]);
+ }
+
+ throwException(resources, id);
+
+ // this is not used since the method above always throws
+ return null;
+ }
+
+ @LayoutlibDelegate
static void getValue(Resources resources, int id, TypedValue outValue, boolean resolveRefs)
throws NotFoundException {
Pair<String, ResourceValue> value = getResourceValue(resources, id, mPlatformResourceFlag);
diff --git a/tools/layoutlib/bridge/src/android/graphics/FontFamily_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/FontFamily_Delegate.java
index a43e545..fb24c01 100644
--- a/tools/layoutlib/bridge/src/android/graphics/FontFamily_Delegate.java
+++ b/tools/layoutlib/bridge/src/android/graphics/FontFamily_Delegate.java
@@ -344,7 +344,9 @@
ffd.addFont(fontInfo);
return true;
}
- fontStream = assetRepository.openAsset(path, AssetManager.ACCESS_STREAMING);
+ fontStream = isAsset ?
+ assetRepository.openAsset(path, AssetManager.ACCESS_STREAMING) :
+ assetRepository.openNonAsset(cookie, path, AssetManager.ACCESS_STREAMING);
if (fontStream == null) {
Bridge.getLog().error(LayoutLog.TAG_MISSING_ASSET, "Asset not found: " + path,
path);
diff --git a/core/tests/coretests/src/android/content/ICrossUserContentService.aidl b/tools/layoutlib/bridge/src/android/graphics/Typeface_Accessor.java
similarity index 63%
rename from core/tests/coretests/src/android/content/ICrossUserContentService.aidl
rename to tools/layoutlib/bridge/src/android/graphics/Typeface_Accessor.java
index 2c5cde4..ce669cb 100644
--- a/core/tests/coretests/src/android/content/ICrossUserContentService.aidl
+++ b/tools/layoutlib/bridge/src/android/graphics/Typeface_Accessor.java
@@ -11,14 +11,18 @@
* 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
+ * limitations under the License.
*/
-package android.content;
+package android.graphics;
-import android.net.Uri;
+import android.annotation.NonNull;
-interface ICrossUserContentService {
- void updateContent(in Uri uri, String key, int value);
- void notifyForUriAsUser(in Uri uri, int userId);
-}
\ No newline at end of file
+/**
+ * Class allowing access to package-protected methods/fields.
+ */
+public class Typeface_Accessor {
+ public static boolean isSystemFont(@NonNull String fontName) {
+ return Typeface.sSystemFontMap.containsKey(fontName);
+ }
+}
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgePackageManager.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgePackageManager.java
index 00799a1..0039476 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgePackageManager.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgePackageManager.java
@@ -24,6 +24,7 @@
import android.content.IntentSender;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
+import android.content.pm.ChangedPackages;
import android.content.pm.InstantAppInfo;
import android.content.pm.FeatureInfo;
import android.content.pm.IPackageDataObserver;
@@ -859,6 +860,11 @@
}
@Override
+ public ChangedPackages getChangedPackages(int sequenceNumber) {
+ return null;
+ }
+
+ @Override
public boolean isUpgrade() {
return false;
}
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ResourceHelper.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ResourceHelper.java
index c197e40..b3a2d3e 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ResourceHelper.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ResourceHelper.java
@@ -38,16 +38,20 @@
import android.content.res.ColorStateList;
import android.content.res.ComplexColor;
import android.content.res.ComplexColor_Accessor;
+import android.content.res.FontResourcesParser;
import android.content.res.GradientColor;
import android.content.res.Resources.Theme;
import android.graphics.Bitmap;
import android.graphics.Bitmap_Delegate;
import android.graphics.NinePatch_Delegate;
import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.graphics.Typeface_Accessor;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.NinePatchDrawable;
+import android.text.FontConfig;
import android.util.TypedValue;
import java.io.File;
@@ -367,6 +371,89 @@
return null;
}
+ /**
+ * Returns a {@link Typeface} given a font name. The font name, can be a system font family
+ * (like sans-serif) or a full path if the font is to be loaded from resources.
+ */
+ public static Typeface getFont(String fontName, BridgeContext context, Theme theme, boolean
+ isFramework) {
+ if (fontName == null) {
+ return null;
+ }
+
+ if (Typeface_Accessor.isSystemFont(fontName)) {
+ // Shortcut for the case where we are asking for a system font name. Those are not
+ // loaded using external resources.
+ return null;
+ }
+
+ // Check if this is an asset that we've already loaded dynamically
+ Typeface typeface = Typeface.findFromCache(context.getAssets(), fontName);
+ if (typeface != null) {
+ return typeface;
+ }
+
+ String lowerCaseValue = fontName.toLowerCase();
+ if (lowerCaseValue.endsWith(".xml")) {
+ // create a block parser for the file
+ Boolean psiParserSupport = context.getLayoutlibCallback().getFlag(
+ RenderParamsFlags.FLAG_KEY_XML_FILE_PARSER_SUPPORT);
+ XmlPullParser parser = null;
+ if (psiParserSupport != null && psiParserSupport) {
+ parser = context.getLayoutlibCallback().getXmlFileParser(fontName);
+ }
+ else {
+ File f = new File(fontName);
+ if (f.isFile()) {
+ try {
+ parser = ParserFactory.create(f);
+ } catch (XmlPullParserException | FileNotFoundException e) {
+ // this is an error and not warning since the file existence is checked before
+ // attempting to parse it.
+ Bridge.getLog().error(null, "Failed to parse file " + fontName,
+ e, null /*data*/);
+ }
+ }
+ }
+
+ if (parser != null) {
+ BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser(
+ parser, context, isFramework);
+ try {
+ FontConfig config = FontResourcesParser.parse(blockParser, context
+ .getResources());
+ typeface = Typeface.createFromResources(config, context.getAssets(),
+ fontName);
+ } catch (XmlPullParserException | IOException e) {
+ Bridge.getLog().error(null, "Failed to parse file " + fontName,
+ e, null /*data*/);
+ } finally {
+ blockParser.ensurePopped();
+ }
+ } else {
+ Bridge.getLog().error(LayoutLog.TAG_BROKEN,
+ String.format("File %s does not exist (or is not a file)", fontName),
+ null /*data*/);
+ }
+ } else {
+ typeface = Typeface.createFromResources(context.getAssets(), fontName, 0);
+ }
+
+ return typeface;
+ }
+
+ /**
+ * Returns a {@link Typeface} given a font name. The font name, can be a system font family
+ * (like sans-serif) or a full path if the font is to be loaded from resources.
+ */
+ public static Typeface getFont(ResourceValue value, BridgeContext context, Theme theme) {
+ if (value == null) {
+ return null;
+ }
+
+ return getFont(value.getValue(), context, theme, value.isFramework());
+ }
+
private static Drawable getNinePatchDrawable(InputStream inputStream, Density density,
boolean isFramework, String cacheKey, BridgeContext context) throws IOException {
// see if we still have both the chunk and the bitmap in the caches
diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/font_test.png b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/font_test.png
new file mode 100644
index 0000000..b2baa98
--- /dev/null
+++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/font_test.png
Binary files differ
diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/font/testfamily.xml b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/font/testfamily.xml
new file mode 100644
index 0000000..b1e9206
--- /dev/null
+++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/font/testfamily.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<font-family xmlns:android="http://schemas.android.com/apk/res/android">
+ <font android:fontStyle="normal" android:fontWeight="400" android:font="@font/testfont" />
+ <font android:fontStyle="italic" android:fontWeight="400" android:font="@font/testfont2" />
+</font-family>
\ No newline at end of file
diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/font/testfont.ttf b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/font/testfont.ttf
new file mode 100644
index 0000000..2852302
--- /dev/null
+++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/font/testfont.ttf
Binary files differ
diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/font/testfont2.ttf b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/font/testfont2.ttf
new file mode 100644
index 0000000..b7bf5b4
--- /dev/null
+++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/font/testfont2.ttf
Binary files differ
diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/layout/fonts_test.xml b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/layout/fonts_test.xml
new file mode 100644
index 0000000..c63b211
--- /dev/null
+++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/layout/fonts_test.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical" android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="CONDENSED"
+ android:textSize="50sp"
+ android:fontFamily="sans-serif-condensed"
+ />
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="CONDENSED ITALIC"
+ android:textSize="30sp"
+ android:fontFamily="sans-serif-condensed"
+ android:textStyle="italic"
+ />
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="MONOSPACE"
+ android:textSize="50sp"
+ android:fontFamily="monospace"/>
+
+ <Space
+ android:layout_width="wrap_content"
+ android:layout_height="30dp" />
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Custom"
+ android:textSize="20sp"
+ android:fontFamily="@font/testfont"/>
+
+ <Space
+ android:layout_width="wrap_content"
+ android:layout_height="30dp" />
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Custom family"
+ android:textSize="20sp"
+ android:fontFamily="@font/testfamily"/>
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Custom family"
+ android:textSize="20sp"
+ android:fontFamily="@font/testfamily"
+ android:textStyle="italic"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/RenderTestBase.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/RenderTestBase.java
index 3e5f9e0..67b42a7 100644
--- a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/RenderTestBase.java
+++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/RenderTestBase.java
@@ -35,6 +35,7 @@
import com.android.layoutlib.bridge.intensive.setup.LayoutPullParser;
import com.android.layoutlib.bridge.intensive.util.ImageUtils;
import com.android.layoutlib.bridge.intensive.util.ModuleClassLoader;
+import com.android.layoutlib.bridge.intensive.util.TestAssetRepository;
import com.android.layoutlib.bridge.intensive.util.TestUtils;
import com.android.tools.layoutlib.java.System_Delegate;
import com.android.utils.ILogger;
@@ -537,6 +538,7 @@
configGenerator.getHardwareConfig(), resourceResolver, layoutLibCallback, 0,
targetSdk, getLayoutLog());
sessionParams.setFlag(RenderParamsFlags.FLAG_DO_NOT_RENDER_ON_CREATE, true);
+ sessionParams.setAssetRepository(new TestAssetRepository());
return sessionParams;
}
}
diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/RenderTests.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/RenderTests.java
index 73e51ec..913519c 100644
--- a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/RenderTests.java
+++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/RenderTests.java
@@ -384,4 +384,10 @@
strings);
assertTrue(sRenderMessages.isEmpty());
}
+
+ @Test
+ public void testFonts() throws ClassNotFoundException {
+ // TODO: styles seem to be broken in TextView
+ renderAndVerify("fonts_test.xml", "font_test.png");
+ }
}
diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/TestAssetRepository.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/TestAssetRepository.java
new file mode 100644
index 0000000..0856ac9
--- /dev/null
+++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/TestAssetRepository.java
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.layoutlib.bridge.intensive.util;
+
+import com.android.ide.common.rendering.api.AssetRepository;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * {@link AssetRepository} used for render tests.
+ */
+public class TestAssetRepository extends AssetRepository {
+ private static InputStream open(String path) throws FileNotFoundException {
+ File asset = new File(path);
+ if (asset.isFile()) {
+ return new FileInputStream(asset);
+ }
+
+ return null;
+ }
+
+ @Override
+ public boolean isSupported() {
+ return true;
+ }
+
+ @Override
+ public InputStream openAsset(String path, int mode) throws IOException {
+ return open(path);
+ }
+
+ @Override
+ public InputStream openNonAsset(int cookie, String path, int mode) throws IOException {
+ return open(path);
+ }
+}
diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java
index 94302d3..a8582c6 100644
--- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java
+++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java
@@ -156,6 +156,7 @@
"android.content.res.Resources#getDimensionPixelOffset",
"android.content.res.Resources#getDimensionPixelSize",
"android.content.res.Resources#getDrawable",
+ "android.content.res.Resources#getFont",
"android.content.res.Resources#getIntArray",
"android.content.res.Resources#getInteger",
"android.content.res.Resources#getLayout",