diff options
97 files changed, 4009 insertions, 306 deletions
diff --git a/api/current.txt b/api/current.txt index 6e054fc5f240..693d512d8a9c 100644 --- a/api/current.txt +++ b/api/current.txt @@ -298,6 +298,7 @@ package android { field public static final int authorities = 16842776; // 0x1010018 field public static final int autoAdvanceViewId = 16843535; // 0x101030f field public static final int autoCompleteTextViewStyle = 16842859; // 0x101006b + field public static final int autoFillHint = 16844121; // 0x1010559 field public static final int autoFillMode = 16844116; // 0x1010554 field public static final int autoLink = 16842928; // 0x10100b0 field public static final int autoMirrored = 16843754; // 0x10103ea @@ -5519,12 +5520,14 @@ package android.app { public final class NotificationChannelGroup implements android.os.Parcelable { ctor public NotificationChannelGroup(java.lang.String, java.lang.CharSequence); + ctor public NotificationChannelGroup(java.lang.String, int); ctor protected NotificationChannelGroup(android.os.Parcel); method public android.app.NotificationChannelGroup clone(); method public int describeContents(); method public java.util.List<android.app.NotificationChannel> getChannels(); method public java.lang.String getId(); method public java.lang.CharSequence getName(); + method public int getNameResId(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator<android.app.NotificationChannelGroup> CREATOR; } @@ -6554,6 +6557,7 @@ package android.app.assist { public static class AssistStructure.ViewNode { method public float getAlpha(); + method public int getAutoFillHint(); method public android.view.autofill.AutoFillId getAutoFillId(); method public java.lang.String[] getAutoFillOptions(); method public android.view.autofill.AutoFillType getAutoFillType(); @@ -8451,8 +8455,10 @@ package android.content { field public static final java.lang.String ANY_CURSOR_ITEM_TYPE = "vnd.android.cursor.item/*"; field public static final java.lang.String CURSOR_DIR_BASE_TYPE = "vnd.android.cursor.dir"; field public static final java.lang.String CURSOR_ITEM_BASE_TYPE = "vnd.android.cursor.item"; + field public static final java.lang.String EXTRA_HONORED_ARGS = "android.content.extra.HONORED_ARGS"; field public static final java.lang.String EXTRA_REFRESH_SUPPORTED = "android.content.extra.REFRESH_SUPPORTED"; field public static final java.lang.String EXTRA_SIZE = "android.content.extra.SIZE"; + field public static final java.lang.String EXTRA_TOTAL_SIZE = "android.content.extra.TOTAL_SIZE"; field public static final int NOTIFY_SKIP_NOTIFY_FOR_DESCENDANTS = 2; // 0x2 field public static final int NOTIFY_SYNC_TO_NETWORK = 1; // 0x1 field public static final java.lang.String QUERY_ARG_LIMIT = "android:query-page-limit"; @@ -8463,7 +8469,6 @@ package android.content { field public static final java.lang.String QUERY_ARG_SQL_SELECTION = "android:query-sql-selection"; field public static final java.lang.String QUERY_ARG_SQL_SELECTION_ARGS = "android:query-sql-selection-args"; field public static final java.lang.String QUERY_ARG_SQL_SORT_ORDER = "android:query-sql-sort-order"; - field public static final java.lang.String QUERY_RESULT_SIZE = "android:query-result-size"; field public static final int QUERY_SORT_DIRECTION_ASCENDING = 0; // 0x0 field public static final int QUERY_SORT_DIRECTION_DESCENDING = 1; // 0x1 field public static final java.lang.String SCHEME_ANDROID_RESOURCE = "android.resource"; @@ -44822,6 +44827,7 @@ package android.view { method public float getAlpha(); method public android.view.animation.Animation getAnimation(); method public android.os.IBinder getApplicationWindowToken(); + method public int getAutoFillHint(); method public int getAutoFillMode(); method public android.view.autofill.AutoFillType getAutoFillType(); method public android.view.autofill.AutoFillValue getAutoFillValue(); @@ -45139,6 +45145,7 @@ package android.view { method public void setActivated(boolean); method public void setAlpha(float); method public void setAnimation(android.view.animation.Animation); + method public void setAutoFillHint(int); method public void setAutoFillMode(int); method public void setBackground(android.graphics.drawable.Drawable); method public void setBackgroundColor(int); @@ -45280,6 +45287,20 @@ package android.view { field public static final int ACCESSIBILITY_LIVE_REGION_NONE = 0; // 0x0 field public static final int ACCESSIBILITY_LIVE_REGION_POLITE = 1; // 0x1 field public static final android.util.Property<android.view.View, java.lang.Float> ALPHA; + field public static final int AUTO_FILL_HINT_CREDIT_CARD_EXPIRATION_DATE = 512; // 0x200 + field public static final int AUTO_FILL_HINT_CREDIT_CARD_EXPIRATION_DAY = 4096; // 0x1000 + field public static final int AUTO_FILL_HINT_CREDIT_CARD_EXPIRATION_MONTH = 1024; // 0x400 + field public static final int AUTO_FILL_HINT_CREDIT_CARD_EXPIRATION_YEAR = 2048; // 0x800 + field public static final int AUTO_FILL_HINT_CREDIT_CARD_NUMBER = 128; // 0x80 + field public static final int AUTO_FILL_HINT_CREDIT_CARD_SECURITY_CODE = 256; // 0x100 + field public static final int AUTO_FILL_HINT_EMAIL_ADDRESS = 1; // 0x1 + field public static final int AUTO_FILL_HINT_NAME = 2; // 0x2 + field public static final int AUTO_FILL_HINT_NONE = 0; // 0x0 + field public static final int AUTO_FILL_HINT_PASSWORD = 8; // 0x8 + field public static final int AUTO_FILL_HINT_PHONE = 16; // 0x10 + field public static final int AUTO_FILL_HINT_POSTAL_ADDRESS = 32; // 0x20 + field public static final int AUTO_FILL_HINT_POSTAL_CODE = 64; // 0x40 + field public static final int AUTO_FILL_HINT_USERNAME = 4; // 0x4 field public static final int AUTO_FILL_MODE_AUTO = 1; // 0x1 field public static final int AUTO_FILL_MODE_INHERIT = 0; // 0x0 field public static final int AUTO_FILL_MODE_MANUAL = 2; // 0x2 @@ -45932,6 +45953,7 @@ package android.view { method public abstract void setAccessibilityFocused(boolean); method public abstract void setActivated(boolean); method public abstract void setAlpha(float); + method public abstract void setAutoFillHint(int); method public abstract void setAutoFillOptions(java.lang.String[]); method public abstract void setAutoFillType(android.view.autofill.AutoFillType); method public abstract void setAutoFillValue(android.view.autofill.AutoFillValue); @@ -47203,9 +47225,8 @@ package android.view.autofill { method public int describeContents(); method public static android.view.autofill.AutoFillType forDate(); method public static android.view.autofill.AutoFillType forList(); - method public static android.view.autofill.AutoFillType forText(int); + method public static android.view.autofill.AutoFillType forText(); method public static android.view.autofill.AutoFillType forToggle(); - method public int getSubType(); method public boolean isDate(); method public boolean isList(); method public boolean isText(); diff --git a/api/system-current.txt b/api/system-current.txt index cfcc40a618cd..f0959360b458 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -410,6 +410,7 @@ package android { field public static final int authorities = 16842776; // 0x1010018 field public static final int autoAdvanceViewId = 16843535; // 0x101030f field public static final int autoCompleteTextViewStyle = 16842859; // 0x101006b + field public static final int autoFillHint = 16844121; // 0x1010559 field public static final int autoFillMode = 16844116; // 0x1010554 field public static final int autoLink = 16842928; // 0x10100b0 field public static final int autoMirrored = 16843754; // 0x10103ea @@ -5712,6 +5713,7 @@ package android.app { public final class NotificationChannelGroup implements android.os.Parcelable { ctor public NotificationChannelGroup(java.lang.String, java.lang.CharSequence); + ctor public NotificationChannelGroup(java.lang.String, int); ctor protected NotificationChannelGroup(android.os.Parcel); method public void addChannel(android.app.NotificationChannel); method public android.app.NotificationChannelGroup clone(); @@ -5719,6 +5721,7 @@ package android.app { method public java.util.List<android.app.NotificationChannel> getChannels(); method public java.lang.String getId(); method public java.lang.CharSequence getName(); + method public int getNameResId(); method public org.json.JSONObject toJson() throws org.json.JSONException; method public void writeToParcel(android.os.Parcel, int); method public void writeXml(org.xmlpull.v1.XmlSerializer) throws java.io.IOException; @@ -6793,6 +6796,7 @@ package android.app.assist { public static class AssistStructure.ViewNode { method public float getAlpha(); + method public int getAutoFillHint(); method public android.view.autofill.AutoFillId getAutoFillId(); method public java.lang.String[] getAutoFillOptions(); method public android.view.autofill.AutoFillType getAutoFillType(); @@ -8941,8 +8945,10 @@ package android.content { field public static final java.lang.String ANY_CURSOR_ITEM_TYPE = "vnd.android.cursor.item/*"; field public static final java.lang.String CURSOR_DIR_BASE_TYPE = "vnd.android.cursor.dir"; field public static final java.lang.String CURSOR_ITEM_BASE_TYPE = "vnd.android.cursor.item"; + field public static final java.lang.String EXTRA_HONORED_ARGS = "android.content.extra.HONORED_ARGS"; field public static final java.lang.String EXTRA_REFRESH_SUPPORTED = "android.content.extra.REFRESH_SUPPORTED"; field public static final java.lang.String EXTRA_SIZE = "android.content.extra.SIZE"; + field public static final java.lang.String EXTRA_TOTAL_SIZE = "android.content.extra.TOTAL_SIZE"; field public static final int NOTIFY_SKIP_NOTIFY_FOR_DESCENDANTS = 2; // 0x2 field public static final int NOTIFY_SYNC_TO_NETWORK = 1; // 0x1 field public static final java.lang.String QUERY_ARG_LIMIT = "android:query-page-limit"; @@ -8953,7 +8959,6 @@ package android.content { field public static final java.lang.String QUERY_ARG_SQL_SELECTION = "android:query-sql-selection"; field public static final java.lang.String QUERY_ARG_SQL_SELECTION_ARGS = "android:query-sql-selection-args"; field public static final java.lang.String QUERY_ARG_SQL_SORT_ORDER = "android:query-sql-sort-order"; - field public static final java.lang.String QUERY_RESULT_SIZE = "android:query-result-size"; field public static final int QUERY_SORT_DIRECTION_ASCENDING = 0; // 0x0 field public static final int QUERY_SORT_DIRECTION_DESCENDING = 1; // 0x1 field public static final java.lang.String SCHEME_ANDROID_RESOURCE = "android.resource"; @@ -48208,6 +48213,7 @@ package android.view { method public float getAlpha(); method public android.view.animation.Animation getAnimation(); method public android.os.IBinder getApplicationWindowToken(); + method public int getAutoFillHint(); method public int getAutoFillMode(); method public android.view.autofill.AutoFillType getAutoFillType(); method public android.view.autofill.AutoFillValue getAutoFillValue(); @@ -48525,6 +48531,7 @@ package android.view { method public void setActivated(boolean); method public void setAlpha(float); method public void setAnimation(android.view.animation.Animation); + method public void setAutoFillHint(int); method public void setAutoFillMode(int); method public void setBackground(android.graphics.drawable.Drawable); method public void setBackgroundColor(int); @@ -48666,6 +48673,20 @@ package android.view { field public static final int ACCESSIBILITY_LIVE_REGION_NONE = 0; // 0x0 field public static final int ACCESSIBILITY_LIVE_REGION_POLITE = 1; // 0x1 field public static final android.util.Property<android.view.View, java.lang.Float> ALPHA; + field public static final int AUTO_FILL_HINT_CREDIT_CARD_EXPIRATION_DATE = 512; // 0x200 + field public static final int AUTO_FILL_HINT_CREDIT_CARD_EXPIRATION_DAY = 4096; // 0x1000 + field public static final int AUTO_FILL_HINT_CREDIT_CARD_EXPIRATION_MONTH = 1024; // 0x400 + field public static final int AUTO_FILL_HINT_CREDIT_CARD_EXPIRATION_YEAR = 2048; // 0x800 + field public static final int AUTO_FILL_HINT_CREDIT_CARD_NUMBER = 128; // 0x80 + field public static final int AUTO_FILL_HINT_CREDIT_CARD_SECURITY_CODE = 256; // 0x100 + field public static final int AUTO_FILL_HINT_EMAIL_ADDRESS = 1; // 0x1 + field public static final int AUTO_FILL_HINT_NAME = 2; // 0x2 + field public static final int AUTO_FILL_HINT_NONE = 0; // 0x0 + field public static final int AUTO_FILL_HINT_PASSWORD = 8; // 0x8 + field public static final int AUTO_FILL_HINT_PHONE = 16; // 0x10 + field public static final int AUTO_FILL_HINT_POSTAL_ADDRESS = 32; // 0x20 + field public static final int AUTO_FILL_HINT_POSTAL_CODE = 64; // 0x40 + field public static final int AUTO_FILL_HINT_USERNAME = 4; // 0x4 field public static final int AUTO_FILL_MODE_AUTO = 1; // 0x1 field public static final int AUTO_FILL_MODE_INHERIT = 0; // 0x0 field public static final int AUTO_FILL_MODE_MANUAL = 2; // 0x2 @@ -49318,6 +49339,7 @@ package android.view { method public abstract void setAccessibilityFocused(boolean); method public abstract void setActivated(boolean); method public abstract void setAlpha(float); + method public abstract void setAutoFillHint(int); method public abstract void setAutoFillOptions(java.lang.String[]); method public abstract void setAutoFillType(android.view.autofill.AutoFillType); method public abstract void setAutoFillValue(android.view.autofill.AutoFillValue); @@ -50592,9 +50614,8 @@ package android.view.autofill { method public int describeContents(); method public static android.view.autofill.AutoFillType forDate(); method public static android.view.autofill.AutoFillType forList(); - method public static android.view.autofill.AutoFillType forText(int); + method public static android.view.autofill.AutoFillType forText(); method public static android.view.autofill.AutoFillType forToggle(); - method public int getSubType(); method public boolean isDate(); method public boolean isList(); method public boolean isText(); diff --git a/api/test-current.txt b/api/test-current.txt index 478a3ed42444..4585b8ebc176 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -298,6 +298,7 @@ package android { field public static final int authorities = 16842776; // 0x1010018 field public static final int autoAdvanceViewId = 16843535; // 0x101030f field public static final int autoCompleteTextViewStyle = 16842859; // 0x101006b + field public static final int autoFillHint = 16844121; // 0x1010559 field public static final int autoFillMode = 16844116; // 0x1010554 field public static final int autoLink = 16842928; // 0x10100b0 field public static final int autoMirrored = 16843754; // 0x10103ea @@ -5529,12 +5530,14 @@ package android.app { public final class NotificationChannelGroup implements android.os.Parcelable { ctor public NotificationChannelGroup(java.lang.String, java.lang.CharSequence); + ctor public NotificationChannelGroup(java.lang.String, int); ctor protected NotificationChannelGroup(android.os.Parcel); method public android.app.NotificationChannelGroup clone(); method public int describeContents(); method public java.util.List<android.app.NotificationChannel> getChannels(); method public java.lang.String getId(); method public java.lang.CharSequence getName(); + method public int getNameResId(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator<android.app.NotificationChannelGroup> CREATOR; } @@ -6264,6 +6267,7 @@ package android.app.admin { method public long getMaximumTimeToLock(android.content.ComponentName); method public int getOrganizationColor(android.content.ComponentName); method public java.lang.CharSequence getOrganizationName(android.content.ComponentName); + method public java.util.List<java.lang.String> getOwnerInstalledCaCerts(android.os.UserHandle); method public android.app.admin.DevicePolicyManager getParentProfileInstance(android.content.ComponentName); method public long getPasswordExpiration(android.content.ComponentName); method public long getPasswordExpirationTimeout(android.content.ComponentName); @@ -6580,6 +6584,7 @@ package android.app.assist { public static class AssistStructure.ViewNode { method public float getAlpha(); + method public int getAutoFillHint(); method public android.view.autofill.AutoFillId getAutoFillId(); method public java.lang.String[] getAutoFillOptions(); method public android.view.autofill.AutoFillType getAutoFillType(); @@ -8478,8 +8483,10 @@ package android.content { field public static final java.lang.String ANY_CURSOR_ITEM_TYPE = "vnd.android.cursor.item/*"; field public static final java.lang.String CURSOR_DIR_BASE_TYPE = "vnd.android.cursor.dir"; field public static final java.lang.String CURSOR_ITEM_BASE_TYPE = "vnd.android.cursor.item"; + field public static final java.lang.String EXTRA_HONORED_ARGS = "android.content.extra.HONORED_ARGS"; field public static final java.lang.String EXTRA_REFRESH_SUPPORTED = "android.content.extra.REFRESH_SUPPORTED"; field public static final java.lang.String EXTRA_SIZE = "android.content.extra.SIZE"; + field public static final java.lang.String EXTRA_TOTAL_SIZE = "android.content.extra.TOTAL_SIZE"; field public static final int NOTIFY_SKIP_NOTIFY_FOR_DESCENDANTS = 2; // 0x2 field public static final int NOTIFY_SYNC_TO_NETWORK = 1; // 0x1 field public static final java.lang.String QUERY_ARG_LIMIT = "android:query-page-limit"; @@ -8490,7 +8497,6 @@ package android.content { field public static final java.lang.String QUERY_ARG_SQL_SELECTION = "android:query-sql-selection"; field public static final java.lang.String QUERY_ARG_SQL_SELECTION_ARGS = "android:query-sql-selection-args"; field public static final java.lang.String QUERY_ARG_SQL_SORT_ORDER = "android:query-sql-sort-order"; - field public static final java.lang.String QUERY_RESULT_SIZE = "android:query-result-size"; field public static final int QUERY_SORT_DIRECTION_ASCENDING = 0; // 0x0 field public static final int QUERY_SORT_DIRECTION_DESCENDING = 1; // 0x1 field public static final java.lang.String SCHEME_ANDROID_RESOURCE = "android.resource"; @@ -45177,6 +45183,7 @@ package android.view { method public float getAlpha(); method public android.view.animation.Animation getAnimation(); method public android.os.IBinder getApplicationWindowToken(); + method public int getAutoFillHint(); method public int getAutoFillMode(); method public android.view.autofill.AutoFillType getAutoFillType(); method public android.view.autofill.AutoFillValue getAutoFillValue(); @@ -45497,6 +45504,7 @@ package android.view { method public void setActivated(boolean); method public void setAlpha(float); method public void setAnimation(android.view.animation.Animation); + method public void setAutoFillHint(int); method public void setAutoFillMode(int); method public void setBackground(android.graphics.drawable.Drawable); method public void setBackgroundColor(int); @@ -45638,6 +45646,20 @@ package android.view { field public static final int ACCESSIBILITY_LIVE_REGION_NONE = 0; // 0x0 field public static final int ACCESSIBILITY_LIVE_REGION_POLITE = 1; // 0x1 field public static final android.util.Property<android.view.View, java.lang.Float> ALPHA; + field public static final int AUTO_FILL_HINT_CREDIT_CARD_EXPIRATION_DATE = 512; // 0x200 + field public static final int AUTO_FILL_HINT_CREDIT_CARD_EXPIRATION_DAY = 4096; // 0x1000 + field public static final int AUTO_FILL_HINT_CREDIT_CARD_EXPIRATION_MONTH = 1024; // 0x400 + field public static final int AUTO_FILL_HINT_CREDIT_CARD_EXPIRATION_YEAR = 2048; // 0x800 + field public static final int AUTO_FILL_HINT_CREDIT_CARD_NUMBER = 128; // 0x80 + field public static final int AUTO_FILL_HINT_CREDIT_CARD_SECURITY_CODE = 256; // 0x100 + field public static final int AUTO_FILL_HINT_EMAIL_ADDRESS = 1; // 0x1 + field public static final int AUTO_FILL_HINT_NAME = 2; // 0x2 + field public static final int AUTO_FILL_HINT_NONE = 0; // 0x0 + field public static final int AUTO_FILL_HINT_PASSWORD = 8; // 0x8 + field public static final int AUTO_FILL_HINT_PHONE = 16; // 0x10 + field public static final int AUTO_FILL_HINT_POSTAL_ADDRESS = 32; // 0x20 + field public static final int AUTO_FILL_HINT_POSTAL_CODE = 64; // 0x40 + field public static final int AUTO_FILL_HINT_USERNAME = 4; // 0x4 field public static final int AUTO_FILL_MODE_AUTO = 1; // 0x1 field public static final int AUTO_FILL_MODE_INHERIT = 0; // 0x0 field public static final int AUTO_FILL_MODE_MANUAL = 2; // 0x2 @@ -46294,6 +46316,7 @@ package android.view { method public abstract void setAccessibilityFocused(boolean); method public abstract void setActivated(boolean); method public abstract void setAlpha(float); + method public abstract void setAutoFillHint(int); method public abstract void setAutoFillOptions(java.lang.String[]); method public abstract void setAutoFillType(android.view.autofill.AutoFillType); method public abstract void setAutoFillValue(android.view.autofill.AutoFillValue); @@ -47567,9 +47590,8 @@ package android.view.autofill { method public int describeContents(); method public static android.view.autofill.AutoFillType forDate(); method public static android.view.autofill.AutoFillType forList(); - method public static android.view.autofill.AutoFillType forText(int); + method public static android.view.autofill.AutoFillType forText(); method public static android.view.autofill.AutoFillType forToggle(); - method public int getSubType(); method public boolean isDate(); method public boolean isList(); method public boolean isText(); diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index 853833054746..dad2061e004f 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -522,6 +522,7 @@ interface IActivityManager { void startLocalVoiceInteraction(in IBinder token, in Bundle options); void stopLocalVoiceInteraction(in IBinder token); boolean supportsLocalVoiceInteraction(); + void notifyPinnedStackAnimationStarted(); void notifyPinnedStackAnimationEnded(); void removeStack(int stackId); void makePackageIdle(String packageName, int userId); diff --git a/core/java/android/app/ITaskStackListener.aidl b/core/java/android/app/ITaskStackListener.aidl index 983435024a9b..d8d4bb906fba 100644 --- a/core/java/android/app/ITaskStackListener.aidl +++ b/core/java/android/app/ITaskStackListener.aidl @@ -38,6 +38,11 @@ oneway interface ITaskStackListener { void onPinnedActivityRestartAttempt(String launchedFromPackage); /** + * Called whenever the pinned stack is starting animating a resize. + */ + void onPinnedStackAnimationStarted(); + + /** * Called whenever the pinned stack is done animating a resize. */ void onPinnedStackAnimationEnded(); diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java index 1c33e3865b01..7cdd45fe8329 100644 --- a/core/java/android/app/LoadedApk.java +++ b/core/java/android/app/LoadedApk.java @@ -286,7 +286,7 @@ public final class LoadedApk { final String apkName = path.substring(path.lastIndexOf(File.separator)); boolean match = false; for (String oldPath : oldPaths) { - final String oldApkName = oldPath.substring(path.lastIndexOf(File.separator)); + final String oldApkName = oldPath.substring(oldPath.lastIndexOf(File.separator)); if (apkName.equals(oldApkName)) { match = true; break; diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 34a45dd626e2..3fc459e33594 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -4177,7 +4177,7 @@ public class Notification implements Parcelable } RemoteViews header = makeNotificationHeader(); - + header.setBoolean(R.id.notification_header, "setAcceptAllTouches", true); if (summary != null) { mN.extras.putCharSequence(EXTRA_SUB_TEXT, summary); } else { diff --git a/core/java/android/app/NotificationChannelGroup.java b/core/java/android/app/NotificationChannelGroup.java index 8854adcfbc92..288d39a706f9 100644 --- a/core/java/android/app/NotificationChannelGroup.java +++ b/core/java/android/app/NotificationChannelGroup.java @@ -15,6 +15,7 @@ */ package android.app; +import android.annotation.StringRes; import android.annotation.SystemApi; import android.net.Uri; import android.os.Parcel; @@ -40,23 +41,38 @@ public final class NotificationChannelGroup implements Parcelable { private static final String TAG_GROUP = "channelGroup"; private static final String ATT_NAME = "name"; + private static final String ATT_NAME_RES_ID = "name_res_id"; private static final String ATT_ID = "id"; private final String mId; private CharSequence mName; + private int mNameResId = 0; private List<NotificationChannel> mChannels = new ArrayList<>(); /** * Creates a notification channel. * * @param id The id of the group. Must be unique per package. - * @param name The user visible name of the group. + * @param name The user visible name of the group. Unchangeable once created; use this + * constructor if the group represents something user-defined that does not + * need to be translated. */ public NotificationChannelGroup(String id, CharSequence name) { this.mId = id; this.mName = name; } + /** + * Creates a notification channel. + * + * @param id The id of the group. Must be unique per package. + * @param nameResId String resource id of the user visible name of the group. + */ + public NotificationChannelGroup(String id, @StringRes int nameResId) { + this.mId = id; + this.mNameResId = nameResId; + } + protected NotificationChannelGroup(Parcel in) { if (in.readByte() != 0) { mId = in.readString(); @@ -64,6 +80,7 @@ public final class NotificationChannelGroup implements Parcelable { mId = null; } mName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + mNameResId = in.readInt(); in.readParcelableList(mChannels, NotificationChannel.class.getClassLoader()); } @@ -76,6 +93,7 @@ public final class NotificationChannelGroup implements Parcelable { dest.writeByte((byte) 0); } TextUtils.writeToParcel(mName, dest, flags); + dest.writeInt(mNameResId); dest.writeParcelableList(mChannels, flags); } @@ -93,6 +111,13 @@ public final class NotificationChannelGroup implements Parcelable { return mName; } + /** + * Returns the resource id of the user visible name of this group. + */ + public @StringRes int getNameResId() { + return mNameResId; + } + /* * Returns the list of channels that belong to this group * @@ -119,7 +144,12 @@ public final class NotificationChannelGroup implements Parcelable { out.startTag(null, TAG_GROUP); out.attribute(null, ATT_ID, getId()); - out.attribute(null, ATT_NAME, getName().toString()); + if (getName() != null) { + out.attribute(null, ATT_NAME, getName().toString()); + } + if (getNameResId() != 0) { + out.attribute(null, ATT_NAME_RES_ID, Integer.toString(getNameResId())); + } out.endTag(null, TAG_GROUP); } @@ -132,6 +162,7 @@ public final class NotificationChannelGroup implements Parcelable { JSONObject record = new JSONObject(); record.put(ATT_ID, getId()); record.put(ATT_NAME, getName()); + record.put(ATT_NAME_RES_ID, getNameResId()); return record; } @@ -160,6 +191,7 @@ public final class NotificationChannelGroup implements Parcelable { NotificationChannelGroup that = (NotificationChannelGroup) o; + if (getNameResId() != that.getNameResId()) return false; if (getId() != null ? !getId().equals(that.getId()) : that.getId() != null) return false; if (getName() != null ? !getName().equals(that.getName()) : that.getName() != null) { return false; @@ -171,13 +203,18 @@ public final class NotificationChannelGroup implements Parcelable { @Override public NotificationChannelGroup clone() { - return new NotificationChannelGroup(getId(), getName()); + if (getName() != null) { + return new NotificationChannelGroup(getId(), getName()); + } else { + return new NotificationChannelGroup(getId(), getNameResId()); + } } @Override public int hashCode() { int result = getId() != null ? getId().hashCode() : 0; result = 31 * result + (getName() != null ? getName().hashCode() : 0); + result = 31 * result + getNameResId(); result = 31 * result + (getChannels() != null ? getChannels().hashCode() : 0); return result; } @@ -187,6 +224,8 @@ public final class NotificationChannelGroup implements Parcelable { return "NotificationChannelGroup{" + "mId='" + mId + '\'' + ", mName=" + mName + + ", mNameResId=" + mNameResId + + ", mChannels=" + mChannels + '}'; } } diff --git a/core/java/android/app/TaskStackListener.java b/core/java/android/app/TaskStackListener.java index 5a0845f02b29..7a569fc322a6 100644 --- a/core/java/android/app/TaskStackListener.java +++ b/core/java/android/app/TaskStackListener.java @@ -39,6 +39,10 @@ public abstract class TaskStackListener extends ITaskStackListener.Stub { } @Override + public void onPinnedStackAnimationStarted() throws RemoteException { + } + + @Override public void onPinnedStackAnimationEnded() throws RemoteException { } diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 65857935b07f..449cca365678 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -7958,4 +7958,28 @@ public class DevicePolicyManager { throw re.rethrowFromSystemServer(); } } + + + /** + * Called by the system to get a list of CA certificates that were installed by the device or + * profile owner. + * + * <p> The caller must be the target user's Device Owner/Profile owner or hold the + * {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL} permission. + * + * @param user The user for whom to retrieve information. + * @return list of aliases identifying CA certificates installed by the device or profile owner + * @throws SecurityException if the caller does not have permission to retrieve information + * about the given user's CA certificates. + * + * @hide + */ + @TestApi + public List<String> getOwnerInstalledCaCerts(@NonNull UserHandle user) { + try { + return mService.getOwnerInstalledCaCerts(user).getList(); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } } diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index ec97c2c2084c..97a4678070c4 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -27,6 +27,7 @@ import android.content.ComponentName; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ParceledListSlice; +import android.content.pm.StringParceledListSlice; import android.graphics.Bitmap; import android.net.ProxyInfo; import android.net.Uri; @@ -349,4 +350,5 @@ interface IDevicePolicyManager { boolean resetPasswordWithToken(in ComponentName admin, String password, in byte[] token, int flags); boolean isDefaultInputMethodSetByOwner(in UserHandle user); + StringParceledListSlice getOwnerInstalledCaCerts(in UserHandle user); } diff --git a/core/java/android/app/assist/AssistStructure.java b/core/java/android/app/assist/AssistStructure.java index d1b2a158979c..a1ef4a6401a3 100644 --- a/core/java/android/app/assist/AssistStructure.java +++ b/core/java/android/app/assist/AssistStructure.java @@ -539,6 +539,7 @@ public class AssistStructure implements Parcelable { // fields (viewId and childId) of the field. AutoFillId mAutoFillId; AutoFillType mAutoFillType; + @View.AutoFillHint int mAutoFillHint; AutoFillValue mAutoFillValue; String[] mAutoFillOptions; boolean mSanitized; @@ -623,6 +624,7 @@ public class AssistStructure implements Parcelable { mSanitized = in.readInt() == 1; mAutoFillId = in.readParcelable(null); mAutoFillType = in.readParcelable(null); + mAutoFillHint = in.readInt(); mAutoFillValue = in.readParcelable(null); mAutoFillOptions = in.readStringArray(); } @@ -756,6 +758,7 @@ public class AssistStructure implements Parcelable { out.writeInt(mSanitized ? 1 : 0); out.writeParcelable(mAutoFillId, 0); out.writeParcelable(mAutoFillType, 0); + out.writeInt(mAutoFillHint); final AutoFillValue sanitizedValue = writeSensitive ? mAutoFillValue : null; out.writeParcelable(sanitizedValue, 0); out.writeStringArray(mAutoFillOptions); @@ -859,6 +862,20 @@ public class AssistStructure implements Parcelable { } /** + * Describes the content of a view so that a auto-fill service can fill in the appropriate + * data. + * + * <p>It's only set when the {@link AssistStructure} is used for auto-filling purposes, not + * for assist.</p> + * + * @return The hint for this view + */ + // TODO(b/35364993): add CTS/unit test + @View.AutoFillHint public int getAutoFillHint() { + return mAutoFillHint; + } + + /** * Gets the the value of this view. * * <p>It's only set when the {@link AssistStructure} is used for auto-filling purposes, not @@ -1549,6 +1566,11 @@ public class AssistStructure implements Parcelable { } @Override + public void setAutoFillHint(@View.AutoFillHint int hint) { + mNode.mAutoFillHint = hint; + } + + @Override public void setAutoFillValue(AutoFillValue value) { mNode.mAutoFillValue = value; } @@ -1695,6 +1717,7 @@ public class AssistStructure implements Parcelable { + ", type=" + node.getAutoFillType() + ", options=" + Arrays.toString(node.getAutoFillOptions()) + ", inputType=" + node.getInputType() + + ", hint=" + Integer.toHexString(node.getAutoFillHint()) + ", value=" + node.getAutoFillValue() + ", sanitized=" + node.isSanitized()); } diff --git a/core/java/android/app/usage/CacheQuotaHint.java b/core/java/android/app/usage/CacheQuotaHint.java index 4b6f99b43a09..1d5c2b05488e 100644 --- a/core/java/android/app/usage/CacheQuotaHint.java +++ b/core/java/android/app/usage/CacheQuotaHint.java @@ -24,8 +24,10 @@ import android.os.Parcelable; import com.android.internal.util.Preconditions; +import java.util.Objects; + /** - * CacheQuotaRequest represents a triplet of a uid, the volume UUID it is stored upon, and + * CacheQuotaHint represents a triplet of a uid, the volume UUID it is stored upon, and * its usage stats. When processed, it obtains a cache quota as defined by the system which * allows apps to understand how much cache to use. * {@hide} @@ -78,6 +80,23 @@ public final class CacheQuotaHint implements Parcelable { return 0; } + @Override + public boolean equals(Object o) { + if (o instanceof CacheQuotaHint) { + final CacheQuotaHint other = (CacheQuotaHint) o; + return Objects.equals(mUuid, other.mUuid) + && Objects.equals(mUsageStats, other.mUsageStats) + && mUid == other.mUid && mQuota == other.mQuota; + } + + return false; + } + + @Override + public int hashCode() { + return Objects.hash(this.mUuid, this.mUid, this.mUsageStats, this.mQuota); + } + public static final class Builder { private String mUuid; private int mUid; @@ -100,7 +119,7 @@ public final class CacheQuotaHint implements Parcelable { } public @NonNull Builder setUid(int uid) { - Preconditions.checkArgumentPositive(uid, "Proposed uid was not positive."); + Preconditions.checkArgumentNonnegative(uid, "Proposed uid was negative."); mUid = uid; return this; } @@ -117,7 +136,6 @@ public final class CacheQuotaHint implements Parcelable { } public @NonNull CacheQuotaHint build() { - Preconditions.checkNotNull(mUsageStats); return new CacheQuotaHint(this); } } diff --git a/core/java/android/bluetooth/BluetoothMapClient.java b/core/java/android/bluetooth/BluetoothMapClient.java index 425248224e1e..7d8459cb48cf 100644 --- a/core/java/android/bluetooth/BluetoothMapClient.java +++ b/core/java/android/bluetooth/BluetoothMapClient.java @@ -50,6 +50,12 @@ public final class BluetoothMapClient implements BluetoothProfile { public static final String ACTION_MESSAGE_DELIVERED_SUCCESSFULLY = "android.bluetooth.mapmce.profile.action.MESSAGE_DELIVERED_SUCCESSFULLY"; + /* Extras used in ACTION_MESSAGE_RECEIVED intent */ + public static final String EXTRA_SENDER_CONTACT_URI = + "android.bluetooth.mapmce.profile.extra.SENDER_CONTACT_URI"; + public static final String EXTRA_SENDER_CONTACT_NAME = + "android.bluetooth.mapmce.profile.extra.SENDER_CONTACT_NAME"; + private IBluetoothMapClient mService; private final Context mContext; private ServiceListener mServiceListener; diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index 9958a79ea3ee..5579c9aed925 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -307,6 +307,18 @@ public abstract class ContentResolver { */ public static final String QUERY_ARG_SORT_COLLATION = "android:query-sort-collation"; + /** + * Allows provider to report back to client which keys were honored. + * + * Key identifying a {@code String[]} containing all QUERY_ARG_SORT* arguments + * honored by the provider. Include this in {@link Cursor} extras {@link Bundle} + * when any QUERY_ARG_SORT* value was honored during the preparation of the + * results {@link Cursor}. + * + * @see #QUERY_ARG_SORT_COLUMNS, #QUERY_ARG_SORT_DIRECTION, #QUERY_ARG_SORT_COLLATION. + */ + public static final String EXTRA_HONORED_ARGS = "android.content.extra.HONORED_ARGS"; + /** @hide */ @IntDef(flag = false, value = { QUERY_SORT_DIRECTION_ASCENDING, @@ -354,8 +366,9 @@ public abstract class ContentResolver { public static final String QUERY_ARG_LIMIT = "android:query-page-limit"; /** - * Added to {@link Cursor} extras {@link Bundle} to indicate size of the - * full, un-offset, un-limited recordset. + * Added to {@link Cursor} extras {@link Bundle} to indicate total size of + * recordset when paging is active. Providers must include this when + * implementing paging support. * * <p>When full size of the recordset is unknown a provider may return -1 * to indicate this. @@ -364,7 +377,7 @@ public abstract class ContentResolver { * send content change notification once (if) full recordset size becomes * known. */ - public static final String QUERY_RESULT_SIZE = "android:query-result-size"; + public static final String EXTRA_TOTAL_SIZE = "android.content.extra.TOTAL_SIZE"; /** * This is the Android platform's base MIME type for a content: URI @@ -704,6 +717,13 @@ public abstract class ContentResolver { * <li>Provide an explicit projection, to prevent reading data from storage * that aren't going to be used. * + * Provider must identify which QUERY_ARG_SORT* arguments were honored during + * the preparation of the result set by including the respective argument keys + * in the {@link Cursor} extras {@link Bundle}. See {@link #EXTRA_HONORED_ARGS} + * for details. + * + * @see #QUERY_ARG_SORT_COLUMNS, #QUERY_ARG_SORT_DIRECTION, #QUERY_ARG_SORT_COLLATION. + * * @param uri The URI, using the content:// scheme, for the content to * retrieve. * @param projection A list of which columns to return. Passing null will @@ -3037,17 +3057,19 @@ public abstract class ContentResolver { query += " COLLATE NOCASE"; } - switch (queryArgs.getInt( - QUERY_ARG_SORT_DIRECTION, Integer.MIN_VALUE)) { - case QUERY_SORT_DIRECTION_ASCENDING: - query += " ASC"; - break; - case QUERY_SORT_DIRECTION_DESCENDING: - query += " DESC"; - break; - default: - throw new IllegalArgumentException("Unsupported sort direction value." - + " See ContentResolver documentation for details."); + int sortDir = queryArgs.getInt(QUERY_ARG_SORT_DIRECTION, Integer.MIN_VALUE); + if (sortDir != Integer.MIN_VALUE) { + switch (sortDir) { + case QUERY_SORT_DIRECTION_ASCENDING: + query += " ASC"; + break; + case QUERY_SORT_DIRECTION_DESCENDING: + query += " DESC"; + break; + default: + throw new IllegalArgumentException("Unsupported sort direction value." + + " See ContentResolver documentation for details."); + } } return query; } diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java index 71a0349bce54..33b59038c106 100644 --- a/core/java/android/provider/DocumentsContract.java +++ b/core/java/android/provider/DocumentsContract.java @@ -119,6 +119,9 @@ public final class DocumentsContract { * <p>Location should specify a document URI or a tree URI with document ID. If * this URI identifies a non-directory, document navigator will attempt to use the parent * of the document as the initial location. + * + * <p>The initial location is system specific if this extra is missing or document navigator + * failed to locate the desired initial location. */ public static final String EXTRA_INITIAL_URI = "android.provider.extra.INITIAL_URI"; diff --git a/core/java/android/view/NotificationHeaderView.java b/core/java/android/view/NotificationHeaderView.java index c3d3f3918ccf..16d46666732d 100644 --- a/core/java/android/view/NotificationHeaderView.java +++ b/core/java/android/view/NotificationHeaderView.java @@ -94,6 +94,7 @@ public class NotificationHeaderView extends ViewGroup { } } }; + private boolean mAcceptAllTouches; public NotificationHeaderView(Context context) { this(context, null); @@ -374,6 +375,8 @@ public class NotificationHeaderView extends ViewGroup { case MotionEvent.ACTION_DOWN: mTrackGesture = false; if (isInside(x, y)) { + mDownX = x; + mDownY = y; mTrackGesture = true; return true; } @@ -396,11 +399,12 @@ public class NotificationHeaderView extends ViewGroup { } private boolean isInside(float x, float y) { + if (mAcceptAllTouches) { + return true; + } for (int i = 0; i < mTouchRects.size(); i++) { Rect r = mTouchRects.get(i); if (r.contains((int) x, (int) y)) { - mDownX = x; - mDownY = y; return true; } } @@ -433,4 +437,9 @@ public class NotificationHeaderView extends ViewGroup { } return mTouchListener.isInside(x, y); } + + @RemotableViewMethod + public void setAcceptAllTouches(boolean acceptAllTouches) { + mAcceptAllTouches = acceptAllTouches; + } } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index e3491706d268..99051a784cb3 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -974,6 +974,143 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public static final int AUTO_FILL_MODE_MANUAL = 2; + /** @hide */ + @IntDef({ + AUTO_FILL_HINT_NONE, + AUTO_FILL_HINT_EMAIL_ADDRESS, + AUTO_FILL_HINT_NAME, + AUTO_FILL_HINT_POSTAL_ADDRESS, + AUTO_FILL_HINT_PASSWORD, + AUTO_FILL_HINT_PHONE, + AUTO_FILL_HINT_USERNAME, + AUTO_FILL_HINT_POSTAL_CODE, + AUTO_FILL_HINT_CREDIT_CARD_NUMBER, + AUTO_FILL_HINT_CREDIT_CARD_SECURITY_CODE, + AUTO_FILL_HINT_CREDIT_CARD_EXPIRATION_DATE, + AUTO_FILL_HINT_CREDIT_CARD_EXPIRATION_MONTH, + AUTO_FILL_HINT_CREDIT_CARD_EXPIRATION_YEAR, + AUTO_FILL_HINT_CREDIT_CARD_EXPIRATION_DAY, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface AutoFillHint {} + + /** + * No auto-fill hint is set. + * + * Use with {@link #setAutoFillHint(int)} and <a href="#attr_android:autoFillHint"> + * {@code android:autoFillHint}. + */ + public static final int AUTO_FILL_HINT_NONE = 0; + + /** + * This view contains an email address. + * + * Use with {@link #setAutoFillHint(int)} and <a href="#attr_android:autoFillHint"> + * {@code android:autoFillHint}. + */ + public static final int AUTO_FILL_HINT_EMAIL_ADDRESS = 0x1; + + /** + * The view contains a real name. + * + * Use with {@link #setAutoFillHint(int)} and <a href="#attr_android:autoFillHint"> + * {@code android:autoFillHint}. + */ + public static final int AUTO_FILL_HINT_NAME = 0x2; + + /** + * The view contains a user name. + * + * Use with {@link #setAutoFillHint(int)} and <a href="#attr_android:autoFillHint"> + * {@code android:autoFillHint}. + */ + public static final int AUTO_FILL_HINT_USERNAME = 0x4; + + /** + * The view contains a password. + * + * Use with {@link #setAutoFillHint(int)} and <a href="#attr_android:autoFillHint"> + * {@code android:autoFillHint}. + */ + public static final int AUTO_FILL_HINT_PASSWORD = 0x8; + + /** + * The view contains a phone number. + * + * Use with {@link #setAutoFillHint(int)} and <a href="#attr_android:autoFillHint"> + * {@code android:autoFillHint}. + */ + public static final int AUTO_FILL_HINT_PHONE = 0x10; + + /** + * The view contains a postal address. + * + * Use with {@link #setAutoFillHint(int)} and <a href="#attr_android:autoFillHint"> + * {@code android:autoFillHint}. + */ + public static final int AUTO_FILL_HINT_POSTAL_ADDRESS = 0x20; + + /** + * The view contains a postal code. + * + * Use with {@link #setAutoFillHint(int)} and <a href="#attr_android:autoFillHint"> + * {@code android:autoFillHint}. + */ + public static final int AUTO_FILL_HINT_POSTAL_CODE = 0x40; + + /** + * The view contains a credit card number. + * + * Use with {@link #setAutoFillHint(int)} and <a href="#attr_android:autoFillHint"> + * {@code android:autoFillHint}. + */ + public static final int AUTO_FILL_HINT_CREDIT_CARD_NUMBER = 0x80; + + /** + * The view contains a credit card security code. + * + * Use with {@link #setAutoFillHint(int)} and <a href="#attr_android:autoFillHint"> + * {@code android:autoFillHint}. + */ + public static final int AUTO_FILL_HINT_CREDIT_CARD_SECURITY_CODE = 0x100; + + /** + * The view contains a credit card expiration date. + * + * Use with {@link #setAutoFillHint(int)} and <a href="#attr_android:autoFillHint"> + * {@code android:autoFillHint}. + */ + public static final int AUTO_FILL_HINT_CREDIT_CARD_EXPIRATION_DATE = 0x200; + + /** + * The view contains the month a credit card expires. + * + * Use with {@link #setAutoFillHint(int)} and <a href="#attr_android:autoFillHint"> + * {@code android:autoFillHint}. + */ + public static final int AUTO_FILL_HINT_CREDIT_CARD_EXPIRATION_MONTH = 0x400; + + /** + * The view contains the year a credit card expires. + * + * Use with {@link #setAutoFillHint(int)} and <a href="#attr_android:autoFillHint"> + * {@code android:autoFillHint}. + */ + public static final int AUTO_FILL_HINT_CREDIT_CARD_EXPIRATION_YEAR = 0x800; + + /** + * The view contains the day a credit card expires. + * + * Use with {@link #setAutoFillHint(int)} and <a href="#attr_android:autoFillHint"> + * {@code android:autoFillHint}. + */ + public static final int AUTO_FILL_HINT_CREDIT_CARD_EXPIRATION_DAY = 0x1000; + + /** + * Hint for the auto-fill services that describes the content of the view. + */ + @AutoFillHint private int mAutoFillHint; + /** * This view is enabled. Interpretation varies by subclass. * Use with ENABLED_MASK when calling setFlags. @@ -4799,11 +4936,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, setFocusedByDefault(a.getBoolean(attr, true)); } break; - case com.android.internal.R.styleable.View_autoFillMode: + case R.styleable.View_autoFillMode: if (a.peekValue(attr) != null) { setAutoFillMode(a.getInt(attr, AUTO_FILL_MODE_INHERIT)); } break; + case R.styleable.View_autoFillHint: + if (a.peekValue(attr) != null) { + setAutoFillHint(a.getInt(attr, AUTO_FILL_HINT_NONE)); + } + break; } } @@ -7035,6 +7177,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // to reuse the accessibility id to save space. structure.setAutoFillId(getAccessibilityViewId()); structure.setAutoFillType(autoFillType); + structure.setAutoFillHint(getAutoFillHint()); structure.setAutoFillValue(getAutoFillValue()); } } @@ -7178,7 +7321,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, /** * Describes the auto-fill type that should be used on calls to * {@link #autoFill(AutoFillValue)} and {@link #autoFillVirtual(int, AutoFillValue)}. - + * * <p>By default returns {@code null}, but views should override it (and * {@link #autoFill(AutoFillValue)} to support the AutoFill Framework. */ @@ -7188,9 +7331,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Describes the content of a view so that a auto-fill service can fill in the appropriate data. + * + * @return The hint set via the attribute + * + * @attr ref android.R.styleable#View_autoFillHint + */ + @ViewDebug.ExportedProperty() + @AutoFillHint public int getAutoFillHint() { + return mAutoFillHint; + } + + /** * Gets the {@link View}'s current auto-fill value. * - * <p>By default returns {@code null}, but views should override it, + * <p>By default returns {@code null}, but views should override it (and * {@link #autoFill(AutoFillValue)}, and {@link #getAutoFillType()} to support the AutoFill * Framework. */ @@ -8698,6 +8853,17 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Sets the a hint that helps the auto-fill service to select the appropriate data to fill the + * view. + * + * @param autoFillHint The auto-fill hint to set + * @attr ref android.R.styleable#View_autoFillHint + */ + public void setAutoFillHint(@AutoFillHint int autoFillHint) { + mAutoFillHint = autoFillHint; + } + + /** * Set whether this view should have sound effects enabled for events such as * clicking and touching. * diff --git a/core/java/android/view/ViewStructure.java b/core/java/android/view/ViewStructure.java index 43560f0cab1c..bc0960c62c62 100644 --- a/core/java/android/view/ViewStructure.java +++ b/core/java/android/view/ViewStructure.java @@ -301,6 +301,13 @@ public abstract class ViewStructure { public abstract void setAutoFillType(AutoFillType info); /** + * Sets the a hint that helps the auto-fill service to select the appropriate data to fill the + * view. + */ + // TODO(b/35364993): add CTS/unit test + public abstract void setAutoFillHint(@View.AutoFillHint int hint); + + /** * Sets the {@link AutoFillValue} representing the current value of this node. */ // TODO(b/33197203, b/33802548): add CTS/unit test diff --git a/core/java/android/view/autofill/AutoFillType.java b/core/java/android/view/autofill/AutoFillType.java index e97470517833..62913d89e64e 100644 --- a/core/java/android/view/autofill/AutoFillType.java +++ b/core/java/android/view/autofill/AutoFillType.java @@ -21,16 +21,11 @@ import static android.view.autofill.Helper.DEBUG; import android.os.Parcel; import android.os.Parcelable; import android.view.View; -import android.widget.TextView; /** * Defines the type of a object that can be used to auto-fill a {@link View} so the * {@link android.service.autofill.AutoFillService} can use the proper {@link AutoFillValue} to * fill it. - * - * <p>Some {@link AutoFillType}s can have an optional {@code sub-type}: the - * main {@code type} defines the view's UI control category (like a text field), while the optional - * {@code sub-type} define its semantics (like a postal address). */ public final class AutoFillType implements Parcelable { @@ -38,9 +33,10 @@ public final class AutoFillType implements Parcelable { // class idiom" (Effective Java, Item 71) to avoid memory utilization when auto-fill is not // enabled. private static class DefaultTypesHolder { - static final AutoFillType TOGGLE = new AutoFillType(TYPE_TOGGLE, 0); - static final AutoFillType LIST = new AutoFillType(TYPE_LIST, 0); - static final AutoFillType DATE = new AutoFillType(TYPE_DATE, 0); + static final AutoFillType TEXT = new AutoFillType(TYPE_TEXT); + static final AutoFillType TOGGLE = new AutoFillType(TYPE_TOGGLE); + static final AutoFillType LIST = new AutoFillType(TYPE_LIST); + static final AutoFillType DATE = new AutoFillType(TYPE_DATE); } private static final int TYPE_TEXT = 1; @@ -49,11 +45,9 @@ public final class AutoFillType implements Parcelable { private static final int TYPE_DATE = 4; private final int mType; - private final int mSubType; - private AutoFillType(int type, int subType) { + private AutoFillType(int type) { mType = type; - mSubType = subType; } /** @@ -62,8 +56,6 @@ public final class AutoFillType implements Parcelable { * <p>{@link AutoFillValue} instances for auto-filling a {@link View} can be obtained through * {@link AutoFillValue#forText(CharSequence)}, and the value passed to auto-fill a * {@link View} can be fetched through {@link AutoFillValue#getTextValue()}. - * - * <p>Sub-type for this type is the value defined by {@link TextView#getInputType()}. */ public boolean isText() { return mType == TYPE_TEXT; @@ -75,8 +67,6 @@ public final class AutoFillType implements Parcelable { * <p>{@link AutoFillValue} instances for auto-filling a {@link View} can be obtained through * {@link AutoFillValue#forToggle(boolean)}, and the value passed to auto-fill a * {@link View} can be fetched through {@link AutoFillValue#getToggleValue()}. - * - * <p>This type has no sub-types. */ public boolean isToggle() { return mType == TYPE_TOGGLE; @@ -89,8 +79,6 @@ public final class AutoFillType implements Parcelable { * <p>{@link AutoFillValue} instances for auto-filling a {@link View} can be obtained through * {@link AutoFillValue#forList(int)}, and the value passed to auto-fill a * {@link View} can be fetched through {@link AutoFillValue#getListValue()}. - * - * <p>This type has no sub-types. */ public boolean isList() { return mType == TYPE_LIST; @@ -111,15 +99,6 @@ public final class AutoFillType implements Parcelable { return mType == TYPE_DATE; } - /** - * Gets the optional sub-type, representing the {@link View}'s semantic. - * - * @return {@code 0} if type does not support sub-types. - */ - public int getSubType() { - return mSubType; - } - ///////////////////////////////////// // Object "contract" methods. // ///////////////////////////////////// @@ -128,14 +107,13 @@ public final class AutoFillType implements Parcelable { public String toString() { if (!DEBUG) return super.toString(); - return "AutoFillType [type=" + mType + ", subType=" + mSubType + "]"; + return "AutoFillType [type=" + mType + "]"; } @Override public int hashCode() { final int prime = 31; int result = 1; - result = prime * result + mSubType; result = prime * result + mType; return result; } @@ -146,7 +124,6 @@ public final class AutoFillType implements Parcelable { if (obj == null) return false; if (getClass() != obj.getClass()) return false; final AutoFillType other = (AutoFillType) obj; - if (mSubType != other.mSubType) return false; if (mType != other.mType) return false; return true; } @@ -163,12 +140,10 @@ public final class AutoFillType implements Parcelable { @Override public void writeToParcel(Parcel parcel, int flags) { parcel.writeInt(mType); - parcel.writeInt(mSubType); } private AutoFillType(Parcel parcel) { mType = parcel.readInt(); - mSubType = parcel.readInt(); } public static final Parcelable.Creator<AutoFillType> CREATOR = @@ -193,8 +168,8 @@ public final class AutoFillType implements Parcelable { * * <p>See {@link #isText()} for more info. */ - public static AutoFillType forText(int inputType) { - return new AutoFillType(TYPE_TEXT, inputType); + public static AutoFillType forText() { + return DefaultTypesHolder.TEXT; } /** diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 3d10917a6448..97fc83584908 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -10019,7 +10019,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Override @Nullable public AutoFillType getAutoFillType() { - return isTextEditable() ? AutoFillType.forText(getInputType()) : null; + return isTextEditable() ? AutoFillType.forText() : null; } @Override diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index c137ae2afdd5..55b88847da8e 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -2304,6 +2304,39 @@ <enum name="manual" value="2" /> </attr> + <!-- Describes the content of a view so that a auto-fill service can fill in the appropriate + data. Multiple flags can be combined to mean e.g. emailAddress or postalAddress. --> + <attr name="autoFillHint"> + <!-- No hint. --> + <flag name="none" value="0" /> + <!-- The view contains an email address. --> + <flag name="emailAddress" value="0x1" /> + <!-- The view contains a real name. --> + <flag name="name" value="0x2" /> + <!-- The view contains a user name. --> + <flag name="username" value="0x4" /> + <!-- The view contains a password. --> + <flag name="password" value="0x8" /> + <!-- The view contains a phone number. --> + <flag name="phone" value="0x10" /> + <!-- The view contains a postal address. --> + <flag name="postalAddress" value="0x20" /> + <!-- The view contains a postal code. --> + <flag name="postalCode" value="0x40" /> + <!-- The view contains a credit card number. --> + <flag name="creditCardNumber" value="0x80" /> + <!-- The view contains a credit card security code --> + <flag name="creditCardSecurityCode" value="0x100" /> + <!-- The view contains a credit card expiration date --> + <flag name="creditCardExpirationDate" value="0x200" /> + <!-- The view contains the month a credit card expires --> + <flag name="creditCardExpirationMonth" value="0x400" /> + <!-- The view contains the year a credit card expires --> + <flag name="creditCardExpirationYear" value="0x800" /> + <!-- The view contains the day a credit card expires --> + <flag name="creditCardExpirationDay" value="0x1000" /> + </attr> + <!-- Boolean that controls whether a view can take focus while in touch mode. If this is true for a view, that view can gain focus when clicked on, and can keep focus if another view is clicked on that doesn't have this attribute set to true. --> diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index 15764a9ceeef..df3962c5dd5c 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -2801,6 +2801,7 @@ <public name="secondaryContentAlpha" /> <public name="requiredFeature" /> <public name="requiredNotFeature" /> + <public name="autoFillHint" /> </public-group> <public-group type="style" first-id="0x010302e0"> diff --git a/keystore/java/android/security/IKeyChainService.aidl b/keystore/java/android/security/IKeyChainService.aidl index b68543146a51..c4bb72c71d17 100644 --- a/keystore/java/android/security/IKeyChainService.aidl +++ b/keystore/java/android/security/IKeyChainService.aidl @@ -29,8 +29,8 @@ interface IKeyChainService { byte[] getCertificate(String alias); byte[] getCaCertificates(String alias); - // APIs used by CertInstaller - void installCaCertificate(in byte[] caCertificate); + // APIs used by CertInstaller and DevicePolicyManager + String installCaCertificate(in byte[] caCertificate); // APIs used by DevicePolicyManager boolean installKeyPair(in byte[] privateKey, in byte[] userCert, in byte[] certChain, String alias); diff --git a/libs/hwui/RecordingCanvas.cpp b/libs/hwui/RecordingCanvas.cpp index 2e33609665b0..374c1b174640 100644 --- a/libs/hwui/RecordingCanvas.cpp +++ b/libs/hwui/RecordingCanvas.cpp @@ -268,7 +268,7 @@ static Rect calcBoundsOfPoints(const float* points, int floatCount) { // Geometry void RecordingCanvas::drawPoints(const float* points, int floatCount, const SkPaint& paint) { - if (CC_UNLIKELY(floatCount < 2 || PaintUtils::paintWillNotDraw(paint))) return; + if (CC_UNLIKELY(floatCount < 2 || paint.nothingToDraw())) return; floatCount &= ~0x1; // round down to nearest two addOp(alloc().create_trivial<PointsOp>( @@ -279,7 +279,7 @@ void RecordingCanvas::drawPoints(const float* points, int floatCount, const SkPa } void RecordingCanvas::drawLines(const float* points, int floatCount, const SkPaint& paint) { - if (CC_UNLIKELY(floatCount < 4 || PaintUtils::paintWillNotDraw(paint))) return; + if (CC_UNLIKELY(floatCount < 4 || paint.nothingToDraw())) return; floatCount &= ~0x3; // round down to nearest four addOp(alloc().create_trivial<LinesOp>( @@ -290,7 +290,7 @@ void RecordingCanvas::drawLines(const float* points, int floatCount, const SkPai } void RecordingCanvas::drawRect(float left, float top, float right, float bottom, const SkPaint& paint) { - if (CC_UNLIKELY(PaintUtils::paintWillNotDraw(paint))) return; + if (CC_UNLIKELY(paint.nothingToDraw())) return; addOp(alloc().create_trivial<RectOp>( Rect(left, top, right, bottom), @@ -333,7 +333,7 @@ void RecordingCanvas::drawSimpleRects(const float* rects, int vertexCount, const } void RecordingCanvas::drawRegion(const SkRegion& region, const SkPaint& paint) { - if (CC_UNLIKELY(PaintUtils::paintWillNotDraw(paint))) return; + if (CC_UNLIKELY(paint.nothingToDraw())) return; if (paint.getStyle() == SkPaint::kFill_Style && (!paint.isAntiAlias() || mState.currentTransform()->isSimple())) { @@ -362,7 +362,7 @@ void RecordingCanvas::drawRegion(const SkRegion& region, const SkPaint& paint) { void RecordingCanvas::drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, const SkPaint& paint) { - if (CC_UNLIKELY(PaintUtils::paintWillNotDraw(paint))) return; + if (CC_UNLIKELY(paint.nothingToDraw())) return; if (CC_LIKELY(MathUtils::isPositive(rx) || MathUtils::isPositive(ry))) { addOp(alloc().create_trivial<RoundRectOp>( @@ -398,7 +398,7 @@ void RecordingCanvas::drawRoundRect( void RecordingCanvas::drawCircle(float x, float y, float radius, const SkPaint& paint) { // TODO: move to Canvas.h - if (CC_UNLIKELY(radius <= 0 || PaintUtils::paintWillNotDraw(paint))) return; + if (CC_UNLIKELY(radius <= 0 || paint.nothingToDraw())) return; drawOval(x - radius, y - radius, x + radius, y + radius, paint); } @@ -419,7 +419,7 @@ void RecordingCanvas::drawCircle( } void RecordingCanvas::drawOval(float left, float top, float right, float bottom, const SkPaint& paint) { - if (CC_UNLIKELY(PaintUtils::paintWillNotDraw(paint))) return; + if (CC_UNLIKELY(paint.nothingToDraw())) return; addOp(alloc().create_trivial<OvalOp>( Rect(left, top, right, bottom), @@ -430,7 +430,7 @@ void RecordingCanvas::drawOval(float left, float top, float right, float bottom, void RecordingCanvas::drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, bool useCenter, const SkPaint& paint) { - if (CC_UNLIKELY(PaintUtils::paintWillNotDraw(paint))) return; + if (CC_UNLIKELY(paint.nothingToDraw())) return; if (fabs(sweepAngle) >= 360.0f) { drawOval(left, top, right, bottom, paint); @@ -445,7 +445,7 @@ void RecordingCanvas::drawArc(float left, float top, float right, float bottom, } void RecordingCanvas::drawPath(const SkPath& path, const SkPaint& paint) { - if (CC_UNLIKELY(PaintUtils::paintWillNotDraw(paint))) return; + if (CC_UNLIKELY(paint.nothingToDraw())) return; addOp(alloc().create_trivial<PathOp>( Rect(path.getBounds()), @@ -543,7 +543,7 @@ void RecordingCanvas::drawNinePatch(Bitmap& bitmap, const android::Res_png_9patc void RecordingCanvas::drawGlyphs(const uint16_t* glyphs, const float* positions, int glyphCount, const SkPaint& paint, float x, float y, float boundsLeft, float boundsTop, float boundsRight, float boundsBottom, float totalAdvance) { - if (!glyphs || !positions || glyphCount <= 0 || PaintUtils::paintWillNotDrawText(paint)) return; + if (!glyphs || !positions || glyphCount <= 0 || paint.nothingToDraw()) return; glyphs = refBuffer<glyph_t>(glyphs, glyphCount); positions = refBuffer<float>(positions, glyphCount * 2); @@ -563,7 +563,7 @@ void RecordingCanvas::drawLayoutOnPath(const minikin::Layout& layout, float hOff glyphs[0] = layout.getGlyphId(i); float x = hOffset + layout.getX(i); float y = vOffset + layout.getY(i); - if (PaintUtils::paintWillNotDrawText(paint)) return; + if (paint.nothingToDraw()) return; const uint16_t* tempGlyphs = refBuffer<glyph_t>(glyphs, 1); addOp(alloc().create_trivial<TextOnPathOp>( *(mState.currentSnapshot()->transform), diff --git a/libs/hwui/SkiaCanvas.cpp b/libs/hwui/SkiaCanvas.cpp index c57b1b38bf7b..51a7d0093128 100644 --- a/libs/hwui/SkiaCanvas.cpp +++ b/libs/hwui/SkiaCanvas.cpp @@ -443,7 +443,7 @@ void SkiaCanvas::drawPaint(const SkPaint& paint) { void SkiaCanvas::drawPoints(const float* points, int count, const SkPaint& paint, SkCanvas::PointMode mode) { - if (CC_UNLIKELY(count < 2 || PaintUtils::paintWillNotDraw(paint))) return; + if (CC_UNLIKELY(count < 2 || paint.nothingToDraw())) return; // convert the floats into SkPoints count >>= 1; // now it is the number of points std::unique_ptr<SkPoint[]> pts(new SkPoint[count]); @@ -469,49 +469,49 @@ void SkiaCanvas::drawLine(float startX, float startY, float stopX, float stopY, } void SkiaCanvas::drawLines(const float* points, int count, const SkPaint& paint) { - if (CC_UNLIKELY(count < 4 || PaintUtils::paintWillNotDraw(paint))) return; + if (CC_UNLIKELY(count < 4 || paint.nothingToDraw())) return; this->drawPoints(points, count, paint, SkCanvas::kLines_PointMode); } void SkiaCanvas::drawRect(float left, float top, float right, float bottom, const SkPaint& paint) { - if (CC_UNLIKELY(PaintUtils::paintWillNotDraw(paint))) return; + if (CC_UNLIKELY(paint.nothingToDraw())) return; mCanvas->drawRectCoords(left, top, right, bottom, paint); } void SkiaCanvas::drawRegion(const SkRegion& region, const SkPaint& paint) { - if (CC_UNLIKELY(PaintUtils::paintWillNotDraw(paint))) return; + if (CC_UNLIKELY(paint.nothingToDraw())) return; mCanvas->drawRegion(region, paint); } void SkiaCanvas::drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, const SkPaint& paint) { - if (CC_UNLIKELY(PaintUtils::paintWillNotDraw(paint))) return; + if (CC_UNLIKELY(paint.nothingToDraw())) return; SkRect rect = SkRect::MakeLTRB(left, top, right, bottom); mCanvas->drawRoundRect(rect, rx, ry, paint); } void SkiaCanvas::drawCircle(float x, float y, float radius, const SkPaint& paint) { - if (CC_UNLIKELY(radius <= 0 || PaintUtils::paintWillNotDraw(paint))) return; + if (CC_UNLIKELY(radius <= 0 || paint.nothingToDraw())) return; mCanvas->drawCircle(x, y, radius, paint); } void SkiaCanvas::drawOval(float left, float top, float right, float bottom, const SkPaint& paint) { - if (CC_UNLIKELY(PaintUtils::paintWillNotDraw(paint))) return; + if (CC_UNLIKELY(paint.nothingToDraw())) return; SkRect oval = SkRect::MakeLTRB(left, top, right, bottom); mCanvas->drawOval(oval, paint); } void SkiaCanvas::drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, bool useCenter, const SkPaint& paint) { - if (CC_UNLIKELY(PaintUtils::paintWillNotDraw(paint))) return; + if (CC_UNLIKELY(paint.nothingToDraw())) return; SkRect arc = SkRect::MakeLTRB(left, top, right, bottom); mCanvas->drawArc(arc, startAngle, sweepAngle, useCenter, paint); } void SkiaCanvas::drawPath(const SkPath& path, const SkPaint& paint) { - if (CC_UNLIKELY(PaintUtils::paintWillNotDraw(paint))) return; + if (CC_UNLIKELY(paint.nothingToDraw())) return; SkRect rect; SkRRect roundRect; if (path.isOval(&rect)) { @@ -698,7 +698,7 @@ void SkiaCanvas::drawGlyphs(const uint16_t* text, const float* positions, int co const SkPaint& paint, float x, float y, float boundsLeft, float boundsTop, float boundsRight, float boundsBottom, float totalAdvance) { - if (!text || !positions || count <= 0 || PaintUtils::paintWillNotDrawText(paint)) return; + if (!text || !positions || count <= 0 || paint.nothingToDraw()) return; // Set align to left for drawing, as we don't want individual // glyphs centered or right-aligned; the offset above takes // care of all alignment. diff --git a/libs/hwui/tests/unit/SkiaCanvasTests.cpp b/libs/hwui/tests/unit/SkiaCanvasTests.cpp index 0ac09ac49f5d..7fb75dce8a19 100644 --- a/libs/hwui/tests/unit/SkiaCanvasTests.cpp +++ b/libs/hwui/tests/unit/SkiaCanvasTests.cpp @@ -18,6 +18,7 @@ #include <gtest/gtest.h> #include <RecordingCanvas.h> +#include <SkBlurDrawLooper.h> #include <SkPicture.h> #include <SkPictureRecorder.h> @@ -59,3 +60,21 @@ OPENGL_PIPELINE_TEST(SkiaCanvasProxy, drawGlyphsViaPicture) { EXPECT_EQ(directOp->unmappedBounds, pictureOp->unmappedBounds); EXPECT_EQ(directOp->localMatrix, pictureOp->localMatrix); } + +TEST(SkiaCanvas, drawShadowLayer) { + auto surface = SkSurface::MakeRasterN32Premul(10, 10); + SkiaCanvas canvas(surface->getCanvas()); + + // clear to white + canvas.drawColor(SK_ColorWHITE, SkBlendMode::kSrc); + + SkPaint paint; + // it is transparent to ensure that we still draw the rect since it has a looper + paint.setColor(SK_ColorTRANSPARENT); + // this is how view's shadow layers are implemented + paint.setLooper(SkBlurDrawLooper::Make(0xF0000000, 6.0f, 0, 10)); + canvas.drawRect(3, 3, 7, 7, paint); + + ASSERT_EQ(TestUtils::getColor(surface, 0, 0), SK_ColorWHITE); + ASSERT_NE(TestUtils::getColor(surface, 5, 5), SK_ColorWHITE); +} diff --git a/libs/hwui/utils/PaintUtils.h b/libs/hwui/utils/PaintUtils.h index 845a3eac35f8..2673be1cb6fa 100644 --- a/libs/hwui/utils/PaintUtils.h +++ b/libs/hwui/utils/PaintUtils.h @@ -39,21 +39,6 @@ public: return GL_NEAREST; } - // TODO: move to a method on android:Paint? replace with SkPaint::nothingToDraw()? - static inline bool paintWillNotDraw(const SkPaint& paint) { - return paint.getAlpha() == 0 - && !paint.getColorFilter() - && paint.getBlendMode() == SkBlendMode::kSrcOver; - } - - // TODO: move to a method on android:Paint? replace with SkPaint::nothingToDraw()? - static inline bool paintWillNotDrawText(const SkPaint& paint) { - return paint.getAlpha() == 0 - && paint.getLooper() == nullptr - && !paint.getColorFilter() - && paint.getBlendMode() == SkBlendMode::kSrcOver; - } - static bool isOpaquePaint(const SkPaint* paint) { if (!paint) return true; // default (paintless) behavior is SrcOver, black diff --git a/media/java/android/media/midi/MidiDevice.java b/media/java/android/media/midi/MidiDevice.java index f79cd06a3b5a..4b43260ece01 100644 --- a/media/java/android/media/midi/MidiDevice.java +++ b/media/java/android/media/midi/MidiDevice.java @@ -35,6 +35,10 @@ import java.io.IOException; * Instances of this class are created by {@link MidiManager#openDevice}. */ public final class MidiDevice implements Closeable { + static { + System.loadLibrary("media_jni"); + } + private static final String TAG = "MidiDevice"; private final MidiDeviceInfo mDeviceInfo; @@ -43,6 +47,7 @@ public final class MidiDevice implements Closeable { private final IBinder mClientToken; private final IBinder mDeviceToken; private boolean mIsDeviceClosed; + private boolean mIsMirroredToNative; private final CloseGuard mGuard = CloseGuard.get(); @@ -209,10 +214,45 @@ public final class MidiDevice implements Closeable { } } + /** + * Makes Midi Device available to the Native API + * @hide + */ + public void mirrorToNative() throws IOException { + if (mIsDeviceClosed || mIsMirroredToNative) { + return; + } + + int result = mirrorToNative(mDeviceServer.asBinder(), mDeviceInfo.getId()); + if (result != 0) { + throw new IOException("Failed mirroring to native: " + result); + } + + mIsMirroredToNative = true; + } + + /** + * Makes Midi Device no longer available to the Native API + * @hide + */ + public void removeFromNative() throws IOException { + if (!mIsMirroredToNative) { + return; + } + + int result = removeFromNative(mDeviceInfo.getId()); + if (result != 0) { + throw new IOException("Failed removing from native: " + result); + } + + mIsMirroredToNative = false; + } + @Override public void close() throws IOException { synchronized (mGuard) { if (!mIsDeviceClosed) { + removeFromNative(); mGuard.close(); mIsDeviceClosed = true; try { @@ -238,4 +278,7 @@ public final class MidiDevice implements Closeable { public String toString() { return ("MidiDevice: " + mDeviceInfo.toString()); } + + private native int mirrorToNative(IBinder deviceServerBinder, int uid); + private native int removeFromNative(int uid); } diff --git a/media/jni/Android.mk b/media/jni/Android.mk index 861ed0a3b08a..23bf3d6711f6 100644 --- a/media/jni/Android.mk +++ b/media/jni/Android.mk @@ -26,6 +26,7 @@ LOCAL_SRC_FILES:= \ android_mtp_MtpDatabase.cpp \ android_mtp_MtpDevice.cpp \ android_mtp_MtpServer.cpp \ + midi/android_media_midi_MidiDevice.cpp \ LOCAL_SHARED_LIBRARIES := \ libandroid_runtime \ @@ -34,6 +35,7 @@ LOCAL_SHARED_LIBRARIES := \ libbinder \ libmedia \ libmediadrm \ + libmidi \ libskia \ libui \ liblog \ @@ -55,6 +57,7 @@ LOCAL_C_INCLUDES += \ external/tremor/Tremor \ frameworks/base/core/jni \ frameworks/base/libs/hwui \ + frameworks/base/media/native \ frameworks/av/media/libmedia \ frameworks/av/media/libstagefright \ frameworks/av/media/mtp \ diff --git a/media/jni/midi/android_media_midi_MidiDevice.cpp b/media/jni/midi/android_media_midi_MidiDevice.cpp new file mode 100644 index 000000000000..1e54bacc469b --- /dev/null +++ b/media/jni/midi/android_media_midi_MidiDevice.cpp @@ -0,0 +1,49 @@ +/* + * 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. + */ + +//#define LOG_NDEBUG 0 +#define LOG_TAG "Midi-JNI" + +#include <android_util_Binder.h> +#include <midi/MidiDeviceRegistry.h> +#include <nativehelper/jni.h> +#include <utils/Log.h> + +using namespace android; +using namespace android::media::midi; + +extern "C" jint Java_android_media_midi_MidiDevice_mirrorToNative( + JNIEnv *env, jobject thiz, jobject midiDeviceServer, jint id) +{ + (void)thiz; + sp<IBinder> serverBinder = ibinderForJavaObject(env, midiDeviceServer); + if (serverBinder.get() == NULL) { + ALOGE("Could not obtain IBinder from passed jobject"); + return -EINVAL; + } + // return MidiDeviceManager::getInstance().addDevice(serverBinder, uid); + return MidiDeviceRegistry::getInstance().addDevice( + new BpMidiDeviceServer(serverBinder), id); +} + +extern "C" jint Java_android_media_midi_MidiDevice_removeFromNative( + JNIEnv *env, jobject thiz, jint uid) +{ + (void)env; + (void)thiz; + // return MidiDeviceManager::getInstance().removeDevice(uid); + return MidiDeviceRegistry::getInstance().removeDevice(uid); +} diff --git a/media/native/midi/Android.bp b/media/native/midi/Android.bp new file mode 100644 index 000000000000..3500805d2794 --- /dev/null +++ b/media/native/midi/Android.bp @@ -0,0 +1,21 @@ +// 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. + +// The headers module is in frameworks/media/native/midi/Android.bp. +ndk_library { + name: "libmidi.ndk", + symbol_file: "libmidi.map.txt", + first_version: "26", +// unversioned_until: "current", +} diff --git a/media/native/midi/Android.mk b/media/native/midi/Android.mk new file mode 100644 index 000000000000..b91c430e2bca --- /dev/null +++ b/media/native/midi/Android.mk @@ -0,0 +1,22 @@ +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := \ + ../../java/android/media/midi/IMidiDeviceServer.aidl \ + midi.cpp \ + MidiDeviceRegistry.cpp \ + MidiPortRegistry.cpp + +LOCAL_AIDL_INCLUDES := \ + $(FRAMEWORKS_BASE_JAVA_SRC_DIRS) \ + frameworks/native/aidl/binder + +LOCAL_CFLAGS += -Wall -Werror -O0 + +LOCAL_MODULE := libmidi +LOCAL_MODULE_TAGS := optional + +LOCAL_SHARED_LIBRARIES := liblog libbinder libutils libmedia + +include $(BUILD_SHARED_LIBRARY) diff --git a/media/native/midi/MidiDeviceRegistry.cpp b/media/native/midi/MidiDeviceRegistry.cpp new file mode 100644 index 000000000000..8854a08a0a45 --- /dev/null +++ b/media/native/midi/MidiDeviceRegistry.cpp @@ -0,0 +1,106 @@ +/* + * 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. + */ + +#include "MidiDeviceRegistry.h" + +namespace android { + +ANDROID_SINGLETON_STATIC_INSTANCE(media::midi::MidiDeviceRegistry); + +namespace media { +namespace midi { + +MidiDeviceRegistry::MidiDeviceRegistry() : mNextDeviceToken(1) { +} + +status_t MidiDeviceRegistry::addDevice(sp<BpMidiDeviceServer> server, int32_t deviceId) { + if (server.get() == nullptr) { + return -EINVAL; + } + + std::lock_guard<std::mutex> guard(mMapsLock); + mServers[deviceId] = server; + return OK; +} + +status_t MidiDeviceRegistry::removeDevice(int32_t deviceId) { + std::lock_guard<std::mutex> guard(mMapsLock); + mServers.erase(deviceId); + const auto& iter = mUidToToken.find(deviceId); + if (iter != mUidToToken.end()) { + mTokenToUid.erase(iter->second); + mUidToToken.erase(iter); + } + return OK; +} + +//NOTE: This creates an entry if not found, or returns an existing one. +status_t MidiDeviceRegistry::obtainDeviceToken(int32_t deviceId, AMIDI_Device *deviceTokenPtr) { + std::lock_guard<std::mutex> guard(mMapsLock); + const auto& serversIter = mServers.find(deviceId); + if (serversIter == mServers.end()) { + // Not found. + return -EINVAL; + } + + const auto& iter = mUidToToken.find(deviceId); + if (iter != mUidToToken.end()) { + *deviceTokenPtr = iter->second; + } else { + *deviceTokenPtr = mNextDeviceToken++; + mTokenToUid[*deviceTokenPtr] = deviceId; + mUidToToken[deviceId] = *deviceTokenPtr; + } + return OK; +} + +status_t MidiDeviceRegistry::releaseDevice(AMIDI_Device deviceToken) { + std::lock_guard<std::mutex> guard(mMapsLock); + const auto& iter = mTokenToUid.find(deviceToken); + if (iter == mTokenToUid.end()) { + // Not found + return -EINVAL; + } + + mServers.erase(iter->second); + mUidToToken.erase(iter->second); + mTokenToUid.erase(iter); + return OK; +} + +status_t MidiDeviceRegistry::getDeviceByToken( + AMIDI_Device deviceToken, sp<BpMidiDeviceServer> *devicePtr) { + std::lock_guard<std::mutex> guard(mMapsLock); + int32_t id = -1; + { + const auto& iter = mTokenToUid.find(deviceToken); + if (iter == mTokenToUid.end()) { + return -EINVAL; + } + id = iter->second; + } + const auto& iter = mServers.find(id); + if (iter == mServers.end()) { + return -EINVAL; + } + + *devicePtr = iter->second; + return OK; +} + +} // namespace midi +} // namespace media +} // namespace android diff --git a/media/native/midi/MidiDeviceRegistry.h b/media/native/midi/MidiDeviceRegistry.h new file mode 100644 index 000000000000..93be7338385b --- /dev/null +++ b/media/native/midi/MidiDeviceRegistry.h @@ -0,0 +1,104 @@ +/* + * 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. + */ + +#ifndef ANDROID_MEDIA_MIDI_DEVICE_REGISTRY_H_ +#define ANDROID_MEDIA_MIDI_DEVICE_REGISTRY_H_ + +#include <map> +#include <mutex> + +#include <binder/IBinder.h> +#include <utils/Errors.h> +#include <utils/Singleton.h> + +#include "android/media/midi/BpMidiDeviceServer.h" +#include "midi.h" + +namespace android { +namespace media { +namespace midi { + +/* + * Maintains a thread-safe, (singleton) list of MIDI devices with associated Binder interfaces, + * which are exposed to the Native API via (Java) MidiDevice.mirrorToNative() & + * MidiDevice.removeFromNative(). + * (Called via MidiDeviceManager::addDevice() MidiManager::removeDevice()). + */ +class MidiDeviceRegistry : public Singleton<MidiDeviceRegistry> { + public: + /* Add a MIDI Device to the registry. + * + * server The Binder interface to the MIDI device server. + * deviceUId The unique ID of the device obtained from + * the Java API via MidiDeviceInfo.getId(). + */ + status_t addDevice(sp<BpMidiDeviceServer> server, int32_t deviceId); + + /* Remove the device (and associated server) from the Device registry. + * + * deviceUid The ID of the device which was used in the call to addDevice(). + */ + status_t removeDevice(int32_t deviceId); + + /* Gets a device token associated with the device ID. This is used by the + * native API to identify/access the device. + * Multiple calls without releasing the token will return the same value. + * + * deviceUid The ID of the device. + * deviceTokenPtr Receives the device (native) token associated with the device ID. + * returns: OK on success, error code otherwise. + */ + status_t obtainDeviceToken(int32_t deviceId, AMIDI_Device *deviceTokenPtr); + + /* + * Releases the native API device token associated with a MIDI device. + * + * deviceToken The device (native) token associated with the device ID. + */ + status_t releaseDevice(AMIDI_Device deviceToken); + + /* + * Gets the Device server binder interface associated with the device token. + * + * deviceToken The device (native) token associated with the device ID. + */ + status_t getDeviceByToken(AMIDI_Device deviceToken, sp<BpMidiDeviceServer> *devicePtr); + + private: + friend class Singleton<MidiDeviceRegistry>; + MidiDeviceRegistry(); + + // Access Mutex + std::mutex mMapsLock; + + // maps device IDs to servers + std::map<int32_t, sp<BpMidiDeviceServer>> mServers; + + // maps device tokens to device ID + std::map<AMIDI_Device, int32_t> mTokenToUid; + + // maps device IDs to device tokens + std::map<int32_t, AMIDI_Device> mUidToToken; + + // Value of next device token to dole out. + AMIDI_Device mNextDeviceToken; +}; + +} // namespace midi +} // namespace media +} // namespace android + +#endif // ANDROID_MEDIA_MIDI_DEVICE_REGISTRY_H_ diff --git a/media/native/midi/MidiPortRegistry.cpp b/media/native/midi/MidiPortRegistry.cpp new file mode 100644 index 000000000000..fa70af8d0947 --- /dev/null +++ b/media/native/midi/MidiPortRegistry.cpp @@ -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. + */ + +#include "MidiPortRegistry.h" + +namespace android { + +ANDROID_SINGLETON_STATIC_INSTANCE(media::midi::MidiPortRegistry); + +namespace media { +namespace midi { + +//TODO Note that these 2 are identical +struct MidiPortRegistry::OutputPort { + AMIDI_Device device; + sp<IBinder> binderToken; + base::unique_fd ufd; +}; + +struct MidiPortRegistry::InputPort { + AMIDI_Device device; + sp<IBinder> binderToken; + base::unique_fd ufd; +}; + +MidiPortRegistry::MidiPortRegistry() : mNextOutputPortToken(0), mNextInputPortToken(0) { +} + +status_t MidiPortRegistry::addOutputPort( + AMIDI_Device device, + sp<IBinder> portToken, + base::unique_fd &&ufd, + AMIDI_OutputPort *portPtr) { + *portPtr = mNextOutputPortToken++; + + OutputPortEntry* portEntry = new OutputPortEntry; + portEntry->port = new OutputPort; + portEntry->state = MIDI_OUTPUT_PORT_STATE_OPEN_IDLE; + portEntry->port = new OutputPort; + portEntry->port->device = device; + portEntry->port->binderToken = portToken; + portEntry->port->ufd = std::move(ufd); + + mOutputPortMap[*portPtr] = portEntry; + + return OK; +} + +status_t MidiPortRegistry::removeOutputPort( + AMIDI_OutputPort port, + AMIDI_Device *devicePtr, + sp<IBinder> *portTokenPtr) { + OutputPortMap::iterator itr = mOutputPortMap.find(port); + if (itr == mOutputPortMap.end()) { + return -EINVAL; + } + + OutputPortEntry *entry = mOutputPortMap[port]; + int portState = MIDI_OUTPUT_PORT_STATE_OPEN_IDLE; + while (!entry->state.compare_exchange_weak(portState, MIDI_OUTPUT_PORT_STATE_CLOSED)) { + if (portState == MIDI_OUTPUT_PORT_STATE_CLOSED) { + return -EINVAL; // Already closed + } + } + *devicePtr = entry->port->device; + *portTokenPtr = entry->port->binderToken; + delete entry->port; + entry->port = nullptr; + + mOutputPortMap.erase(itr); + + return OK; +} + +status_t MidiPortRegistry::getOutputPortFdAndLock( + AMIDI_OutputPort port, base::unique_fd **ufdPtr) { + if (mOutputPortMap.find(port) == mOutputPortMap.end()) { + return -EINVAL; + } + + OutputPortEntry *entry = mOutputPortMap[port]; + int portState = MIDI_OUTPUT_PORT_STATE_OPEN_IDLE; + if (!entry->state.compare_exchange_strong(portState, MIDI_OUTPUT_PORT_STATE_OPEN_ACTIVE)) { + // The port has been closed. + return -EPIPE; + } + *ufdPtr = &entry->port->ufd; + + return OK; +} + +status_t MidiPortRegistry::unlockOutputPort(AMIDI_OutputPort port) { + if (mOutputPortMap.find(port) == mOutputPortMap.end()) { + return -EINVAL; + } + + OutputPortEntry *entry = mOutputPortMap[port]; + entry->state.store(MIDI_OUTPUT_PORT_STATE_OPEN_IDLE); + return OK; +} + +status_t MidiPortRegistry::addInputPort( + AMIDI_Device device, + sp<IBinder> portToken, + base::unique_fd &&ufd, + AMIDI_InputPort *portPtr) { + *portPtr = mNextInputPortToken++; + + InputPortEntry *entry = new InputPortEntry; + + entry->state = MIDI_INPUT_PORT_STATE_OPEN_IDLE; + entry->port = new InputPort; + entry->port->device = device; + entry->port->binderToken = portToken; + entry->port->ufd = std::move(ufd); + + mInputPortMap[*portPtr] = entry; + + return OK; +} + +status_t MidiPortRegistry::removeInputPort( + AMIDI_InputPort port, + AMIDI_Device *devicePtr, + sp<IBinder> *portTokenPtr) { + InputPortMap::iterator itr = mInputPortMap.find(port); + if (itr == mInputPortMap.end()) { + return -EINVAL; + } + + InputPortEntry *entry = mInputPortMap[port]; + int portState = MIDI_INPUT_PORT_STATE_OPEN_IDLE; + while (!entry->state.compare_exchange_weak(portState, MIDI_INPUT_PORT_STATE_CLOSED)) { + if (portState == MIDI_INPUT_PORT_STATE_CLOSED) return -EINVAL; // Already closed + } + + *devicePtr = entry->port->device; + *portTokenPtr = entry->port->binderToken; + delete entry->port; + entry->port = nullptr; + + mInputPortMap.erase(itr); + + return OK; +} + +status_t MidiPortRegistry::getInputPortFd(AMIDI_InputPort port, base::unique_fd **ufdPtr) { + if (mInputPortMap.find(port) == mInputPortMap.end()) { + return -EINVAL; + } + + InputPortEntry *entry = mInputPortMap[port]; + + *ufdPtr = &entry->port->ufd; + + return OK; +} + +status_t MidiPortRegistry::getInputPortFdAndLock(AMIDI_InputPort port, base::unique_fd **ufdPtr) { + if (mInputPortMap.find(port) == mInputPortMap.end()) { + return -EINVAL; + } + + InputPortEntry *entry = mInputPortMap[port]; + + int portState = MIDI_INPUT_PORT_STATE_OPEN_IDLE; + if (!entry->state.compare_exchange_strong(portState, MIDI_INPUT_PORT_STATE_OPEN_ACTIVE)) { + // The port has been closed. + return -EPIPE; + } + *ufdPtr = &entry->port->ufd; + return OK; +} + +status_t MidiPortRegistry::MidiPortRegistry::unlockInputPort(AMIDI_InputPort port) { + if (mInputPortMap.find(port) == mInputPortMap.end()) { + return -EINVAL; + } + + InputPortEntry *entry = mInputPortMap[port]; + entry->state.store(MIDI_INPUT_PORT_STATE_OPEN_IDLE); + return OK; +} + +} // namespace midi +} // namespace media +} // namespace android diff --git a/media/native/midi/MidiPortRegistry.h b/media/native/midi/MidiPortRegistry.h new file mode 100644 index 000000000000..f1ffb78e98ab --- /dev/null +++ b/media/native/midi/MidiPortRegistry.h @@ -0,0 +1,192 @@ +/* + * 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. + */ + +#ifndef ANDROID_MEDIA_MIDI_PORT_REGISTRY_H_ +#define ANDROID_MEDIA_MIDI_PORT_REGISTRY_H_ + +#include <atomic> +#include <map> + +#include <android-base/unique_fd.h> +#include <binder/IBinder.h> +#include <utils/Errors.h> +#include <utils/Singleton.h> + +#include "midi.h" + +namespace android { +namespace media { +namespace midi { + +/* + * Maintains lists of all active input and output MIDI ports and controls access to them. Provides + * exclusive access to specific MIDI ports. + */ +class MidiPortRegistry : public Singleton<MidiPortRegistry> { + public: + /* + * Creates an output port entry and associates it with the specified MIDI device. + * Called by AMIDI_openOutputPort(); + * + * device The native API device ID. + * portToken The port token (returned from the device server). + * udf File descriptor for the data port associated with the MIDI output port. + * portPtr Receives the native API port ID of the port being opened. + */ + status_t addOutputPort( + AMIDI_Device device, + sp<IBinder> portToken, + base::unique_fd &&ufd, + AMIDI_OutputPort *portPtr); + + /* + * Removes for the output port list a previously added output port. + * Called by AMIDI_closeOutputPort(); + * + * port The native API port ID of the port being closed. + * devicePtr Receives the native API device ID associated with the port. + * portTokenPtr Receives the binder token associated with the port. + */ + status_t removeOutputPort( + AMIDI_OutputPort port, + AMIDI_Device *devicePtr, + sp<IBinder> *portTokenPtr); + + /* + * Creates an input port entry and associates it with the specified MIDI device. + * Called by AMIDI_openInputPort(); + * + * device The native API device ID. + * portToken The port token (returned from the device server). + * udf File descriptor for the data port associated with the MIDI input port. + * portPtr Receives the native API port ID of the port being opened. + */ + status_t addInputPort( + AMIDI_Device device, + sp<IBinder> portToken, + base::unique_fd &&ufd, + AMIDI_InputPort *portPtr); + + /* + * Removes for the input port list a previously added input port. + * Called by AMIDI_closeINputPort(); + * + * port The native API port ID of the port being closed. + * devicePtr Receives the native API device ID associated with the port. + * portTokenPtr Receives the binder token associated with the port. + */ + status_t removeInputPort( + AMIDI_InputPort port, + AMIDI_Device *devicePtr, + sp<IBinder> *portTokenPtr); + + /* + * Retrieves an exclusive-access file descriptor for an output port. + * Called from AMIDI_receive(). + * + * port The native API id of the output port. + * ufdPtr Receives the exclusive-access file descriptor for the output port. + */ + status_t getOutputPortFdAndLock(AMIDI_OutputPort port, base::unique_fd **ufdPtr); + + /* + * Releases exclusive-access to the port and invalidates the previously received file + * descriptor. + * Called from AMIDI_receive(). + * + * port The native API id of the output port. + */ + status_t unlockOutputPort(AMIDI_OutputPort port); + + /* + * Retrieves an exclusive-access file descriptor for an input port. + * (Not being used as (perhaps) AMIDI_sendWithTimestamp() doesn't need exclusive access + * to the port). + * + * port The native API id of the input port. + * ufdPtr Receives the exclusive-access file descriptor for the input port. + */ + status_t getInputPortFdAndLock(AMIDI_InputPort port, base::unique_fd **ufdPtr); + + /* + * Releases exclusive-access to the port and invalidates the previously received file + * descriptor. + * (Not used. See above). + * + * port The native API id of the input port. + */ + status_t unlockInputPort(AMIDI_InputPort port); + + /* + * Retrieves an unlocked (multi-access) file descriptor for an input port. + * Used by AMIDI_sendWith(), AMIDI_sendWithTimestamp & AMIDI_flush. + * + * port The native API id of the input port. + * ufdPtr Receives the multi-access file descriptor for the input port. + */ + status_t getInputPortFd(AMIDI_InputPort port, base::unique_fd **ufdPtr); + + private: + friend class Singleton<MidiPortRegistry>; + MidiPortRegistry(); + + /* + * Output (data receiving) ports. + */ + struct OutputPort; + enum { + MIDI_OUTPUT_PORT_STATE_CLOSED = 0, + MIDI_OUTPUT_PORT_STATE_OPEN_IDLE, + MIDI_OUTPUT_PORT_STATE_OPEN_ACTIVE + }; + + struct OutputPortEntry { + std::atomic_int state; + OutputPort *port; + }; + + typedef std::map<AMIDI_OutputPort, OutputPortEntry*> OutputPortMap; + // Access is synchronized per record via 'state' field. + std::atomic<AMIDI_OutputPort> mNextOutputPortToken; + OutputPortMap mOutputPortMap; + + /* + * Input (data sending) ports. + */ + struct InputPort; + enum { + MIDI_INPUT_PORT_STATE_CLOSED = 0, + MIDI_INPUT_PORT_STATE_OPEN_IDLE, + MIDI_INPUT_PORT_STATE_OPEN_ACTIVE + }; + + struct InputPortEntry { + std::atomic_int state; + InputPort *port; + }; + + typedef std::map<AMIDI_OutputPort, InputPortEntry*> InputPortMap; + // Access is synchronized per record via 'state' field. + std::atomic<AMIDI_InputPort> mNextInputPortToken; + InputPortMap mInputPortMap; + +}; + +} // namespace midi +} // namespace media +} // namespace android + +#endif // ANDROID_MEDIA_MIDI_PORT_REGISTRY_H_ diff --git a/media/native/midi/midi.cpp b/media/native/midi/midi.cpp new file mode 100644 index 000000000000..1bf0bd080e94 --- /dev/null +++ b/media/native/midi/midi.cpp @@ -0,0 +1,317 @@ +/* + * 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. + */ + +#define LOG_TAG "NativeMIDI" + +#include <poll.h> +#include <unistd.h> + +#include <binder/Binder.h> +#include <utils/Errors.h> +#include <utils/Log.h> + +#include "android/media/midi/BpMidiDeviceServer.h" +#include "media/MidiDeviceInfo.h" +#include "MidiDeviceRegistry.h" +#include "MidiPortRegistry.h" + +#include "midi.h" + +using android::IBinder; +using android::BBinder; +using android::OK; +using android::sp; +using android::status_t; +using android::base::unique_fd; +using android::binder::Status; +using android::media::midi::BpMidiDeviceServer; +using android::media::midi::MidiDeviceInfo; +using android::media::midi::MidiDeviceRegistry; +using android::media::midi::MidiPortRegistry; + +#define SIZE_MIDIRECEIVEBUFFER AMIDI_BUFFER_SIZE + +/* TRANSFER PACKET FORMAT (as defined in MidiPortImpl.java) + * + * Transfer packet format is as follows (see MidiOutputPort.mThread.run() to see decomposition): + * |oc|md|md| ......... |md|ts|ts|ts|ts|ts|ts|ts|ts| + * ^ +--------------------+-----------------------+ + * | ^ ^ + * | | | + * | | + timestamp (8 bytes) + * | | + * | + MIDI data bytes (numBytes bytes) + * | + * + OpCode (AMIDI_OPCODE_DATA) + * + * NOTE: The socket pair is configured to use SOCK_SEQPACKET mode. + * SOCK_SEQPACKET, for a sequenced-packet socket that is connection-oriented, preserves message + * boundaries, and delivers messages in the order that they were sent. + * So 'read()' always returns a whole message. + */ + +status_t AMIDI_getDeviceById(int32_t id, AMIDI_Device *devicePtr) { + return MidiDeviceRegistry::getInstance().obtainDeviceToken(id, devicePtr); +} + +status_t AMIDI_getDeviceInfo(AMIDI_Device device, AMIDI_DeviceInfo *deviceInfoPtr) { + sp<BpMidiDeviceServer> deviceServer; + status_t result = MidiDeviceRegistry::getInstance().getDeviceByToken(device, &deviceServer); + if (result != OK) { + ALOGE("AMIDI_getDeviceInfo bad device token %d: %d", device, result); + return result; + } + + MidiDeviceInfo deviceInfo; + Status txResult = deviceServer->getDeviceInfo(&deviceInfo); + if (!txResult.isOk()) { + ALOGE("AMIDI_getDeviceInfo transaction error: %d", txResult.transactionError()); + return txResult.transactionError(); + } + + deviceInfoPtr->type = deviceInfo.getType(); + deviceInfoPtr->uid = deviceInfo.getUid(); + deviceInfoPtr->isPrivate = deviceInfo.isPrivate(); + deviceInfoPtr->inputPortCount = deviceInfo.getInputPortNames().size(); + deviceInfoPtr->outputPortCount = deviceInfo.getOutputPortNames().size(); + return OK; +} + +/* + * Output (receiving) API + */ +status_t AMIDI_openOutputPort(AMIDI_Device device, int portNumber, AMIDI_OutputPort *outputPortPtr) { + sp<BpMidiDeviceServer> deviceServer; + status_t result = MidiDeviceRegistry::getInstance().getDeviceByToken(device, &deviceServer); + if (result != OK) { + ALOGE("AMIDI_openOutputPort bad device token %d: %d", device, result); + return result; + } + + sp<BBinder> portToken(new BBinder()); + unique_fd ufd; + Status txResult = deviceServer->openOutputPort(portToken, portNumber, &ufd); + if (!txResult.isOk()) { + ALOGE("AMIDI_openOutputPort transaction error: %d", txResult.transactionError()); + return txResult.transactionError(); + } + + result = MidiPortRegistry::getInstance().addOutputPort( + device, portToken, std::move(ufd), outputPortPtr); + if (result != OK) { + ALOGE("AMIDI_openOutputPort port registration error: %d", result); + // Close port + return result; + } + return OK; +} + +ssize_t AMIDI_receive(AMIDI_OutputPort outputPort, AMIDI_Message *messages, ssize_t maxMessages) { + unique_fd *ufd; + // TODO: May return a nicer self-unlocking object + status_t result = MidiPortRegistry::getInstance().getOutputPortFdAndLock(outputPort, &ufd); + if (result != OK) { + return result; + } + + ssize_t messagesRead = 0; + while (messagesRead < maxMessages) { + struct pollfd checkFds[1] = { { *ufd, POLLIN, 0 } }; + int pollResult = poll(checkFds, 1, 0); + if (pollResult < 1) { + result = android::INVALID_OPERATION; + break; + } + + AMIDI_Message *message = &messages[messagesRead]; + uint8_t readBuffer[AMIDI_PACKET_SIZE]; + memset(readBuffer, 0, sizeof(readBuffer)); + ssize_t readCount = read(*ufd, readBuffer, sizeof(readBuffer)); + if (readCount == EINTR) { + continue; + } + if (readCount < 1) { + result = android::NOT_ENOUGH_DATA; + break; + } + + // set Packet Format definition at the top of this file. + size_t dataSize = 0; + message->opcode = readBuffer[0]; + message->timestamp = 0; + if (message->opcode == AMIDI_OPCODE_DATA && readCount >= AMIDI_PACKET_OVERHEAD) { + dataSize = readCount - AMIDI_PACKET_OVERHEAD; + if (dataSize) { + memcpy(message->buffer, readBuffer + 1, dataSize); + } + message->timestamp = *(uint64_t*) (readBuffer + readCount - sizeof(uint64_t)); + } + message->len = dataSize; + ++messagesRead; + } + + MidiPortRegistry::getInstance().unlockOutputPort(outputPort); + return result == OK ? messagesRead : result; +} + +status_t AMIDI_closeOutputPort(AMIDI_OutputPort outputPort) { + AMIDI_Device device; + sp<IBinder> portToken; + status_t result = + MidiPortRegistry::getInstance().removeOutputPort(outputPort, &device, &portToken); + if (result != OK) { + return result; + } + + sp<BpMidiDeviceServer> deviceServer; + result = MidiDeviceRegistry::getInstance().getDeviceByToken(device, &deviceServer); + if (result != OK) { + return result; + } + + Status txResult = deviceServer->closePort(portToken); + if (!txResult.isOk()) { + return txResult.transactionError(); + } + return OK; +} + +/* + * Input (sending) API + */ +status_t AMIDI_openInputPort(AMIDI_Device device, int portNumber, AMIDI_InputPort *inputPortPtr) { + sp<BpMidiDeviceServer> deviceServer; + status_t result = MidiDeviceRegistry::getInstance().getDeviceByToken(device, &deviceServer); + if (result != OK) { + ALOGE("AMIDI_openInputPort bad device token %d: %d", device, result); + return result; + } + + sp<BBinder> portToken(new BBinder()); + unique_fd ufd; // this is the file descriptor of the "receive" port s + Status txResult = deviceServer->openInputPort(portToken, portNumber, &ufd); + if (!txResult.isOk()) { + ALOGE("AMIDI_openInputPort transaction error: %d", txResult.transactionError()); + return txResult.transactionError(); + } + + result = MidiPortRegistry::getInstance().addInputPort( + device, portToken, std::move(ufd), inputPortPtr); + if (result != OK) { + ALOGE("AMIDI_openInputPort port registration error: %d", result); + // Close port + return result; + } + + return OK; +} + +status_t AMIDI_closeInputPort(AMIDI_InputPort inputPort) { + AMIDI_Device device; + sp<IBinder> portToken; + status_t result = MidiPortRegistry::getInstance().removeInputPort( + inputPort, &device, &portToken); + if (result != OK) { + ALOGE("AMIDI_closeInputPort remove port error: %d", result); + return result; + } + + sp<BpMidiDeviceServer> deviceServer; + result = MidiDeviceRegistry::getInstance().getDeviceByToken(device, &deviceServer); + if (result != OK) { + ALOGE("AMIDI_closeInputPort can't find device error: %d", result); + return result; + } + + Status txResult = deviceServer->closePort(portToken); + if (!txResult.isOk()) { + result = txResult.transactionError(); + ALOGE("AMIDI_closeInputPort transaction error: %d", result); + return result; + } + + return OK; +} + +ssize_t AMIDI_getMaxMessageSizeInBytes(AMIDI_InputPort /*inputPort*/) { + return SIZE_MIDIRECEIVEBUFFER; +} + +static ssize_t AMIDI_makeSendBuffer(uint8_t *buffer, uint8_t *data, ssize_t numBytes, uint64_t timestamp) { + buffer[0] = AMIDI_OPCODE_DATA; + memcpy(buffer + 1, data, numBytes); + memcpy(buffer + 1 + numBytes, ×tamp, sizeof(timestamp)); + return numBytes + AMIDI_PACKET_OVERHEAD; +} + +// Handy debugging function. +//static void AMIDI_logBuffer(uint8_t *data, size_t numBytes) { +// for (size_t index = 0; index < numBytes; index++) { +// ALOGI(" data @%zu [0x%X]", index, data[index]); +// } +//} + +ssize_t AMIDI_send(AMIDI_InputPort inputPort, uint8_t *buffer, ssize_t numBytes) { + return AMIDI_sendWithTimestamp(inputPort, buffer, numBytes, 0); +} + +ssize_t AMIDI_sendWithTimestamp(AMIDI_InputPort inputPort, uint8_t *data, + ssize_t numBytes, int64_t timestamp) { + + if (numBytes > SIZE_MIDIRECEIVEBUFFER) { + return android::BAD_VALUE; + } + + // AMIDI_logBuffer(data, numBytes); + + unique_fd *ufd = NULL; + status_t result = MidiPortRegistry::getInstance().getInputPortFd(inputPort, &ufd); + if (result != OK) { + return result; + } + + uint8_t writeBuffer[SIZE_MIDIRECEIVEBUFFER + AMIDI_PACKET_OVERHEAD]; + ssize_t numTransferBytes = AMIDI_makeSendBuffer(writeBuffer, data, numBytes, timestamp); + ssize_t numWritten = write(*ufd, writeBuffer, numTransferBytes); + + if (numWritten < numTransferBytes) { + ALOGE("AMIDI_sendWithTimestamp Couldn't write MIDI data buffer. requested:%zu, written%zu", + numTransferBytes, numWritten); + } + + return numWritten - AMIDI_PACKET_OVERHEAD; +} + +status_t AMIDI_flush(AMIDI_InputPort inputPort) { + unique_fd *ufd = NULL; + status_t result = MidiPortRegistry::getInstance().getInputPortFd(inputPort, &ufd); + if (result != OK) { + return result; + } + + uint8_t opCode = AMIDI_OPCODE_FLUSH; + ssize_t numTransferBytes = 1; + ssize_t numWritten = write(*ufd, &opCode, numTransferBytes); + + if (numWritten < numTransferBytes) { + ALOGE("AMIDI_flush Couldn't write MIDI flush. requested:%zu, written%zu", + numTransferBytes, numWritten); + return android::INVALID_OPERATION; + } + + return OK; +} + diff --git a/media/native/midi/midi.h b/media/native/midi/midi.h new file mode 100644 index 000000000000..717bc666253e --- /dev/null +++ b/media/native/midi/midi.h @@ -0,0 +1,202 @@ +/* + * 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. + */ + +#ifndef ANDROID_MEDIA_MIDI_H_ +#define ANDROID_MEDIA_MIDI_H_ + +#include <stdarg.h> +#include <stdint.h> +#include <sys/types.h> + +#include <utils/Errors.h> + +using android::status_t; + +#ifdef __cplusplus +extern "C" { +#endif + +//typedef struct _AMIDI_Device; +//typedef struct _AMIDI_InputPort; +//typedef struct _AMIDI_OutputPort; +//typedef _AMIDI_Device* AMIDI_Device; +//typedef _AMIDI_InputPort* AMIDI_InputPort; +//typedef _AMIDI_OutputPort* AMIDI_OutputPort; + +typedef int32_t AMIDI_Device; +typedef int32_t AMIDI_InputPort; +typedef int32_t AMIDI_OutputPort; + +//TODO - Do we want to wrap this stuff in namespace android { namespace media { namespace midi {? + +enum { + AMIDI_INVALID_HANDLE = -1 +}; + +enum { + AMIDI_OPCODE_DATA = 1, + AMIDI_OPCODE_FLUSH = 2, + AMIDI_PACKET_SIZE = 1024, /* !!! Currently MidiPortImpl.MAX_PACKET_SIZE !!! */ + AMIDI_PACKET_OVERHEAD = 9, + AMIDI_BUFFER_SIZE = AMIDI_PACKET_SIZE - AMIDI_PACKET_OVERHEAD + /* !!! TBD, currently MidiPortImpl.MAX_PACKET_DATA_SIZE !!! */ +}; + +typedef struct { + uint32_t opcode; + uint8_t buffer[AMIDI_BUFFER_SIZE]; + size_t len; + int64_t timestamp; +} AMIDI_Message; + +enum { + AMIDI_DEVICE_TYPE_USB = 1, + AMIDI_DEVICE_TYPE_VIRTUAL = 2, + AMIDI_DEVICE_TYPE_BLUETOOTH = 3 +}; + +typedef struct { + int32_t type; + int32_t uid; + int32_t isPrivate; + int32_t inputPortCount; + int32_t outputPortCount; +} AMIDI_DeviceInfo; + +/* + * Device API + */ +/* + * Retrieves the native API device token for the specified Java API device ID. + * + * uid The Java API id of the device. + * devicePtr Receives the associated native API token for the device. + * + * Returns OK or a (negative) error code. + */ +status_t AMIDI_getDeviceById(int32_t id, AMIDI_Device *devicePtr); + +/* + * Retrieves information for the native MIDI device. + * + * device The Native API token for the device. + * deviceInfoPtr Receives the associated device info. + * + * Returns OK or a (negative) error code. + */ +status_t AMIDI_getDeviceInfo(AMIDI_Device device, AMIDI_DeviceInfo *deviceInfoPtr); + +/* + * API for receiving data from the Output port of a device. + */ +/* + * Opens the output port. + * + * device Identifies the device. + * portNumber Specifies the zero-based port index on the device to open. + * outputPortPtr Receives the native API port identifier of the opened port. + * + * Returns OK, or a (negative) error code. + */ +status_t AMIDI_openOutputPort(AMIDI_Device device, int portNumber, AMIDI_OutputPort *outputPortPtr); + +/* + * Receives any pending MIDI messages (up to the specified maximum number of messages). + * + * outputPort Identifies the port to receive messages from. + * messages Points to an array (size maxMessages) to receive the MIDI messages. + * maxMessages The number of messages allocated in the messages array. + * + * Returns the number of messages received, or a (negative) error code. + */ +ssize_t AMIDI_receive(AMIDI_OutputPort outputPort, AMIDI_Message *messages, ssize_t maxMessages); + +/* + * Closes the output port. + * + * outputPort The native API port identifier of the port. + * + * Returns OK, or a (negative) error code. + */ +status_t AMIDI_closeOutputPort(AMIDI_OutputPort outputPort); + +/* + * API for sending data to the Input port of a device. + */ +/* + * Opens the input port. + * + * device Identifies the device. + * portNumber Specifies the zero-based port index on the device to open. + * inputPortPtr Receives the native API port identifier of the opened port. + * + * Returns OK, or a (negative) error code. + */ +status_t AMIDI_openInputPort(AMIDI_Device device, int portNumber, AMIDI_InputPort *inputPortPtr); + +/* + * Returns the maximum number of bytes that can be received in a single MIDI message. + */ +ssize_t AMIDI_getMaxMessageSizeInBytes(AMIDI_InputPort inputPort); + +/* + * Sends data to the specified input port. + * + * inputPort The native API identifier of the port to send data to. + * buffer Points to the array of bytes containing the data to send. + * numBytes Specifies the number of bytes to write. + * + * Returns The number of bytes sent or a (negative) error code. + */ +ssize_t AMIDI_send(AMIDI_InputPort inputPort, uint8_t *buffer, ssize_t numBytes); + +/* + * Sends data to the specified input port with a timestamp. + * + * inputPort The native API identifier of the port to send data to. + * buffer Points to the array of bytes containing the data to send. + * numBytes Specifies the number of bytes to write. + * timestamp The time stamp to associate with the sent data. + * + * Returns The number of bytes sent or a (negative) error code. + */ +ssize_t AMIDI_sendWithTimestamp(AMIDI_InputPort inputPort, uint8_t *buffer, + ssize_t numBytes, int64_t timestamp); + +/* + * Sends a message with a 'MIDI flush command code' to the specified port. + * + * inputPort The native API identifier of the port to send the flush message to. + * + * Returns OK, or a (negative) error code. + */ +status_t AMIDI_flush(AMIDI_InputPort inputPort); + +/* + * Closes the input port. + * + * inputPort The native API port identifier of the port. + * + * + * Returns OK, or a (negative) error code. + */ +status_t AMIDI_closeInputPort(AMIDI_InputPort inputPort); + +#ifdef __cplusplus +} +#endif + +#endif /* ANDROID_MEDIA_MIDI_H_ */ diff --git a/media/tests/NativeMidiDemo/Android.mk b/media/tests/NativeMidiDemo/Android.mk new file mode 100644 index 000000000000..6b08f6bba3f4 --- /dev/null +++ b/media/tests/NativeMidiDemo/Android.mk @@ -0,0 +1,30 @@ +# Copyright (C) 2016 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := tests +LOCAL_PACKAGE_NAME := NativeMidiDemo + +#LOCAL_SDK_VERSION := current +LOCAL_PROGUARD_ENABLED := disabled +LOCAL_SRC_FILES := $(call all-java-files-under, java) + +LOCAL_JNI_SHARED_LIBRARIES := libnativemidi_jni + +include $(BUILD_PACKAGE) + +# Include packages in subdirectories +include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/media/tests/NativeMidiDemo/AndroidManifest.xml b/media/tests/NativeMidiDemo/AndroidManifest.xml new file mode 100644 index 000000000000..322873f11895 --- /dev/null +++ b/media/tests/NativeMidiDemo/AndroidManifest.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.example.android.nativemididemo" + android:versionCode="1" + android:versionName="1.0"> + <application + android:allowBackup="false" + android:fullBackupContent="false" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name"> + <uses-feature android:name="android.software.midi" android:required="true"/> + <activity android:name=".NativeMidi" + android:label="@string/app_name"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/media/tests/NativeMidiDemo/java/com/example/android/nativemididemo/NativeMidi.java b/media/tests/NativeMidiDemo/java/com/example/android/nativemididemo/NativeMidi.java new file mode 100644 index 000000000000..0969b10b1f90 --- /dev/null +++ b/media/tests/NativeMidiDemo/java/com/example/android/nativemididemo/NativeMidi.java @@ -0,0 +1,354 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.example.android.nativemididemo; + +import android.app.Activity; +import android.content.Context; +import android.media.midi.MidiDevice; +import android.media.midi.MidiDeviceInfo; +import android.media.midi.MidiManager; +import android.media.midi.MidiOutputPort; +import android.media.midi.MidiReceiver; +import android.media.AudioManager; +import android.os.Bundle; +import android.os.Handler; +import android.view.View; +import android.view.WindowManager; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.TextView; + +import java.io.IOException; + +public class NativeMidi extends Activity +{ + private TextView mCallbackStatusTextView; + private TextView mJavaMidiStatusTextView; + private TextView mMessagesTextView; + private RadioGroup mMidiDevicesRadioGroup; + private Handler mTimerHandler = new Handler(); + private boolean mAudioWorks; + private final int mMinFramesPerBuffer = 32; // See {min|max}PlaySamples in nativemidi-jni.cpp + private final int mMaxFramesPerBuffer = 1000; + private int mFramesPerBuffer; + + private TouchableScrollView mMessagesContainer; + private MidiManager mMidiManager; + private MidiOutputPortSelector mActivePortSelector; + + private Runnable mTimerRunnable = new Runnable() { + private long mLastTime; + private long mLastPlaybackCounter; + private int mLastCallbackRate; + private long mLastUntouchedTime; + + @Override + public void run() { + final long checkIntervalMs = 1000; + long currentTime = System.currentTimeMillis(); + long currentPlaybackCounter = getPlaybackCounter(); + if (currentTime - mLastTime >= checkIntervalMs) { + int callbackRate = Math.round( + (float)(currentPlaybackCounter - mLastPlaybackCounter) / + ((float)(currentTime - mLastTime) / (float)1000)); + if (mLastCallbackRate != callbackRate) { + mCallbackStatusTextView.setText( + "CB: " + callbackRate + " Hz"); + mLastCallbackRate = callbackRate; + } + mLastTime = currentTime; + mLastPlaybackCounter = currentPlaybackCounter; + } + + String[] newMessages = getRecentMessages(); + if (newMessages != null) { + for (String message : newMessages) { + mMessagesTextView.append(message); + mMessagesTextView.append("\n"); + } + if (!mMessagesContainer.isTouched) { + if (mLastUntouchedTime == 0) mLastUntouchedTime = currentTime; + if (currentTime - mLastUntouchedTime > 3000) { + mMessagesContainer.fullScroll(View.FOCUS_DOWN); + } + } else { + mLastUntouchedTime = 0; + } + } + + mTimerHandler.postDelayed(this, checkIntervalMs / 4); + } + }; + + private void addMessage(final String message) { + mTimerHandler.postDelayed(new Runnable() { + @Override + public void run() { + mMessagesTextView.append(message); + } + }, 0); + } + + private class MidiOutputPortSelector implements View.OnClickListener { + private final MidiDeviceInfo mDeviceInfo; + private final int mPortNumber; + private MidiDevice mDevice; + private MidiOutputPort mOutputPort; + + MidiOutputPortSelector() { + mDeviceInfo = null; + mPortNumber = -1; + } + + MidiOutputPortSelector(MidiDeviceInfo info, int portNumber) { + mDeviceInfo = info; + mPortNumber = portNumber; + } + + MidiDeviceInfo getDeviceInfo() { return mDeviceInfo; } + + @Override + public void onClick(View v) { + if (mActivePortSelector != null) { + mActivePortSelector.close(); + mActivePortSelector = null; + } + if (mDeviceInfo == null) { + mActivePortSelector = this; + return; + } + mMidiManager.openDevice(mDeviceInfo, new MidiManager.OnDeviceOpenedListener() { + @Override + public void onDeviceOpened(MidiDevice device) { + if (device == null) { + addMessage("! Failed to open MIDI device !\n"); + } else { + mDevice = device; + try { + mDevice.mirrorToNative(); + startReadingMidi(mDevice.getInfo().getId(), mPortNumber); + } catch (IOException e) { + addMessage("! Failed to mirror to native !\n" + e.getMessage() + "\n"); + } + + mActivePortSelector = MidiOutputPortSelector.this; + + mOutputPort = device.openOutputPort(mPortNumber); + mOutputPort.connect(mMidiReceiver); + } + } + }, null); + } + + void closePortOnly() { + stopReadingMidi(); + } + + void close() { + closePortOnly(); + try { + if (mOutputPort != null) { + mOutputPort.close(); + } + } catch (IOException e) { + mMessagesTextView.append("! Port close error: " + e + "\n"); + } finally { + mOutputPort = null; + } + try { + if (mDevice != null) { + mDevice.close(); + } + } catch (IOException e) { + mMessagesTextView.append("! Device close error: " + e + "\n"); + } finally { + mDevice = null; + } + } + } + + private MidiManager.DeviceCallback mMidiDeviceCallback = new MidiManager.DeviceCallback() { + @Override + public void onDeviceAdded(MidiDeviceInfo info) { + Bundle deviceProps = info.getProperties(); + String deviceName = deviceProps.getString(MidiDeviceInfo.PROPERTY_NAME); + if (deviceName == null) { + deviceName = deviceProps.getString(MidiDeviceInfo.PROPERTY_MANUFACTURER); + } + + for (MidiDeviceInfo.PortInfo port : info.getPorts()) { + if (port.getType() != MidiDeviceInfo.PortInfo.TYPE_OUTPUT) continue; + String portName = port.getName(); + int portNumber = port.getPortNumber(); + if (portName.length() == 0) portName = "[" + portNumber + "]"; + portName += "@" + deviceName; + RadioButton outputDevice = new RadioButton(NativeMidi.this); + outputDevice.setText(portName); + outputDevice.setTag(info); + outputDevice.setOnClickListener(new MidiOutputPortSelector(info, portNumber)); + mMidiDevicesRadioGroup.addView(outputDevice); + } + + NativeMidi.this.updateKeepScreenOn(); + } + + @Override + public void onDeviceRemoved(MidiDeviceInfo info) { + if (mActivePortSelector != null && info.equals(mActivePortSelector.getDeviceInfo())) { + mActivePortSelector.close(); + mActivePortSelector = null; + } + int removeButtonStart = -1, removeButtonCount = 0; + final int buttonCount = mMidiDevicesRadioGroup.getChildCount(); + boolean checked = false; + for (int i = 0; i < buttonCount; ++i) { + RadioButton button = (RadioButton) mMidiDevicesRadioGroup.getChildAt(i); + if (!info.equals(button.getTag())) continue; + if (removeButtonStart == -1) removeButtonStart = i; + ++removeButtonCount; + if (button.isChecked()) checked = true; + } + if (removeButtonStart != -1) { + mMidiDevicesRadioGroup.removeViews(removeButtonStart, removeButtonCount); + if (checked) { + mMidiDevicesRadioGroup.check(R.id.device_none); + } + } + + NativeMidi.this.updateKeepScreenOn(); + } + }; + + private class JavaMidiReceiver extends MidiReceiver implements Runnable { + @Override + public void onSend(byte[] data, int offset, + int count, long timestamp) throws IOException { + mTimerHandler.removeCallbacks(this); + mTimerHandler.postDelayed(new Runnable() { + @Override + public void run() { + mJavaMidiStatusTextView.setText("Java: MSG"); + } + }, 0); + mTimerHandler.postDelayed(this, 100); + } + + @Override + public void run() { + mJavaMidiStatusTextView.setText("Java: ---"); + } + } + + private JavaMidiReceiver mMidiReceiver = new JavaMidiReceiver(); + + private void updateKeepScreenOn() { + if (mMidiDevicesRadioGroup.getChildCount() > 1) { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } else { + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main); + + mCallbackStatusTextView = (TextView) findViewById(R.id.callback_status); + mJavaMidiStatusTextView = (TextView) findViewById(R.id.java_midi_status); + mMessagesTextView = (TextView) findViewById(R.id.messages); + mMessagesContainer = (TouchableScrollView) findViewById(R.id.messages_scroll); + mMidiDevicesRadioGroup = (RadioGroup) findViewById(R.id.devices); + RadioButton deviceNone = (RadioButton) findViewById(R.id.device_none); + deviceNone.setOnClickListener(new MidiOutputPortSelector()); + + AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + String sampleRate = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE); + if (sampleRate == null) sampleRate = "48000"; + String framesPerBuffer = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER); + if (framesPerBuffer == null) framesPerBuffer = Integer.toString(mMaxFramesPerBuffer); + mFramesPerBuffer = Integer.parseInt(framesPerBuffer); + String audioInitResult = initAudio(Integer.parseInt(sampleRate), mFramesPerBuffer); + mMessagesTextView.append("Open SL ES init: " + audioInitResult + "\n"); + + if (audioInitResult.startsWith("Success")) { + mAudioWorks = true; + mTimerHandler.postDelayed(mTimerRunnable, 0); + mTimerHandler.postDelayed(mMidiReceiver, 0); + } + + mMidiManager = (MidiManager) getSystemService(Context.MIDI_SERVICE); + mMidiManager.registerDeviceCallback(mMidiDeviceCallback, new Handler()); + for (MidiDeviceInfo info : mMidiManager.getDevices()) { + mMidiDeviceCallback.onDeviceAdded(info); + } + } + + @Override + public void onPause() { + super.onPause(); + if (mAudioWorks) { + mTimerHandler.removeCallbacks(mTimerRunnable); + pauseAudio(); + } + } + + @Override + public void onResume() { + super.onResume(); + if (mAudioWorks) { + mTimerHandler.postDelayed(mTimerRunnable, 0); + resumeAudio(); + } + } + + @Override + protected void onDestroy() { + if (mActivePortSelector != null) { + mActivePortSelector.close(); + mActivePortSelector = null; + } + shutdownAudio(); + super.onDestroy(); + } + + public void onClearMessages(View v) { + mMessagesTextView.setText(""); + } + + public void onClosePort(View v) { + if (mActivePortSelector != null) { + mActivePortSelector.closePortOnly(); + } + } + + private native String initAudio(int sampleRate, int playSamples); + private native void pauseAudio(); + private native void resumeAudio(); + private native void shutdownAudio(); + + private native long getPlaybackCounter(); + private native String[] getRecentMessages(); + + private native void startReadingMidi(int deviceId, int portNumber); + private native void stopReadingMidi(); + + static { + System.loadLibrary("nativemidi_jni"); + } +} diff --git a/media/tests/NativeMidiDemo/java/com/example/android/nativemididemo/TouchableScrollView.java b/media/tests/NativeMidiDemo/java/com/example/android/nativemididemo/TouchableScrollView.java new file mode 100644 index 000000000000..645aafa88507 --- /dev/null +++ b/media/tests/NativeMidiDemo/java/com/example/android/nativemididemo/TouchableScrollView.java @@ -0,0 +1,32 @@ +package com.example.android.nativemididemo; + +import android.content.Context; +import android.view.MotionEvent; +import android.util.AttributeSet; +import android.widget.ScrollView; + +public class TouchableScrollView extends ScrollView { + public boolean isTouched; + + public TouchableScrollView(Context context) { + super(context); + } + + public TouchableScrollView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + isTouched = true; + break; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + isTouched = false; + break; + } + return super.onTouchEvent(event); + } +} diff --git a/media/tests/NativeMidiDemo/jni/Android.mk b/media/tests/NativeMidiDemo/jni/Android.mk new file mode 100644 index 000000000000..69a64bd0218b --- /dev/null +++ b/media/tests/NativeMidiDemo/jni/Android.mk @@ -0,0 +1,35 @@ +# 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. + +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := tests + +LOCAL_MODULE := libnativemidi_jni + +LOCAL_SRC_FILES := \ + nativemidi-jni.cpp \ + messagequeue.cpp + +LOCAL_CFLAGS += -Wall -Wextra -Werror -O0 + +LOCAL_C_INCLUDES += \ + frameworks/base/media/native + +LOCAL_CXX_STL := libc++_static + +LOCAL_SHARED_LIBRARIES := libOpenSLES libmidi + +include $(BUILD_SHARED_LIBRARY) diff --git a/media/tests/NativeMidiDemo/jni/messagequeue.cpp b/media/tests/NativeMidiDemo/jni/messagequeue.cpp new file mode 100644 index 000000000000..ffaef38bed8c --- /dev/null +++ b/media/tests/NativeMidiDemo/jni/messagequeue.cpp @@ -0,0 +1,138 @@ +/* + * 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. + * + */ + +#include <atomic> +#include <stdio.h> +#include <string.h> + +#include "messagequeue.h" + +namespace nativemididemo { + +static const int messageBufferSize = 64 * 1024; +static char messageBuffer[messageBufferSize]; +static std::atomic_ullong messagesLastWritePosition; + +void writeMessage(const char* message) +{ + static unsigned long long lastWritePos = 0; + size_t messageLen = strlen(message); + if (messageLen == 0) return; + + messageLen += 1; // Also count in the null terminator. + char buffer[1024]; + if (messageLen >= messageBufferSize) { + snprintf(buffer, sizeof(buffer), "!!! Message too long: %zu bytes !!!", messageLen); + message = buffer; + messageLen = strlen(message); + } + + size_t wrappedWritePos = lastWritePos % messageBufferSize; + if (wrappedWritePos + messageLen >= messageBufferSize) { + size_t tailLen = messageBufferSize - wrappedWritePos; + memset(messageBuffer + wrappedWritePos, 0, tailLen); + lastWritePos += tailLen; + wrappedWritePos = 0; + } + + memcpy(messageBuffer + wrappedWritePos, message, messageLen); + lastWritePos += messageLen; + messagesLastWritePosition.store(lastWritePos); +} + +static char messageBufferCopy[messageBufferSize]; + +jobjectArray getRecentMessagesForJava(JNIEnv* env, jobject) +{ + static unsigned long long lastReadPos = 0; + const char* overrunMessage = ""; + size_t messagesCount = 0; + jobjectArray result = NULL; + + // First we copy the portion of the message buffer into messageBufferCopy. If after finishing + // the copy we notice that the writer has mutated the portion of the buffer that we were + // copying, we report an overrun. Afterwards we can safely read messages from the copy. + memset(messageBufferCopy, 0, sizeof(messageBufferCopy)); + unsigned long long lastWritePos = messagesLastWritePosition.load(); + if (lastWritePos - lastReadPos > messageBufferSize) { + overrunMessage = "!!! Message buffer overrun !!!"; + messagesCount = 1; + lastReadPos = lastWritePos; + goto create_array; + } + if (lastWritePos == lastReadPos) return result; + if (lastWritePos / messageBufferSize == lastReadPos / messageBufferSize) { + size_t wrappedReadPos = lastReadPos % messageBufferSize; + memcpy(messageBufferCopy + wrappedReadPos, + messageBuffer + wrappedReadPos, + lastWritePos % messageBufferSize - wrappedReadPos); + } else { + size_t wrappedReadPos = lastReadPos % messageBufferSize; + memcpy(messageBufferCopy, messageBuffer, lastWritePos % messageBufferSize); + memcpy(messageBufferCopy + wrappedReadPos, + messageBuffer + wrappedReadPos, + messageBufferSize - wrappedReadPos); + } + { + unsigned long long newLastWritePos = messagesLastWritePosition.load(); + if (newLastWritePos - lastReadPos > messageBufferSize) { + overrunMessage = "!!! Message buffer overrun !!!"; + messagesCount = 1; + lastReadPos = lastWritePos = newLastWritePos; + goto create_array; + } + } + // Otherwise we ignore newLastWritePos, since we only have a copy of the buffer + // up to lastWritePos. + + for (unsigned long long readPos = lastReadPos; readPos < lastWritePos; ) { + size_t messageLen = strlen(messageBufferCopy + (readPos % messageBufferSize)); + if (messageLen != 0) { + readPos += messageLen + 1; + messagesCount++; + } else { + // Skip to the beginning of the buffer. + readPos = (readPos / messageBufferSize + 1) * messageBufferSize; + } + } + if (messagesCount == 0) { + lastReadPos = lastWritePos; + return result; + } + +create_array: + result = env->NewObjectArray( + messagesCount, env->FindClass("java/lang/String"), env->NewStringUTF(overrunMessage)); + if (lastWritePos == lastReadPos) return result; + + jsize arrayIndex = 0; + while (lastReadPos < lastWritePos) { + size_t wrappedReadPos = lastReadPos % messageBufferSize; + if (messageBufferCopy[wrappedReadPos] != '\0') { + jstring message = env->NewStringUTF(messageBufferCopy + wrappedReadPos); + env->SetObjectArrayElement(result, arrayIndex++, message); + lastReadPos += env->GetStringLength(message) + 1; + env->DeleteLocalRef(message); + } else { + // Skip to the beginning of the buffer. + lastReadPos = (lastReadPos / messageBufferSize + 1) * messageBufferSize; + } + } + return result; +} + +} // namespace nativemididemo diff --git a/media/tests/NativeMidiDemo/jni/messagequeue.h b/media/tests/NativeMidiDemo/jni/messagequeue.h new file mode 100644 index 000000000000..20aa9e805d12 --- /dev/null +++ b/media/tests/NativeMidiDemo/jni/messagequeue.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#ifndef NATIVEMIDIDEMO_MESSAGEQUEUE_H +#define NATIVEMIDIDEMO_MESSAGEQUEUE_H + +#include <jni.h> + +namespace nativemididemo { + +void writeMessage(const char* message); +jobjectArray getRecentMessagesForJava(JNIEnv* env, jobject thiz); + +} // namespace nativemididemo + +#endif // NATIVEMIDIDEMO_MESSAGEQUEUE_H diff --git a/media/tests/NativeMidiDemo/jni/nativemidi-jni.cpp b/media/tests/NativeMidiDemo/jni/nativemidi-jni.cpp new file mode 100644 index 000000000000..8aa874ae0748 --- /dev/null +++ b/media/tests/NativeMidiDemo/jni/nativemidi-jni.cpp @@ -0,0 +1,285 @@ +#include <atomic> +#include <inttypes.h> +#include <stdio.h> +#include <string.h> + +#include <jni.h> + +#include <midi/midi.h> +#include <SLES/OpenSLES.h> +#include <SLES/OpenSLES_Android.h> + +#include "messagequeue.h" + +extern "C" { +JNIEXPORT jstring JNICALL Java_com_example_android_nativemididemo_NativeMidi_initAudio( + JNIEnv* env, jobject thiz, jint sampleRate, jint playSamples); +JNIEXPORT void JNICALL Java_com_example_android_nativemididemo_NativeMidi_pauseAudio( + JNIEnv* env, jobject thiz); +JNIEXPORT void JNICALL Java_com_example_android_nativemididemo_NativeMidi_resumeAudio( + JNIEnv* env, jobject thiz); +JNIEXPORT void JNICALL Java_com_example_android_nativemididemo_NativeMidi_shutdownAudio( + JNIEnv* env, jobject thiz); +JNIEXPORT jlong JNICALL Java_com_example_android_nativemididemo_NativeMidi_getPlaybackCounter( + JNIEnv* env, jobject thiz); +JNIEXPORT jobjectArray JNICALL Java_com_example_android_nativemididemo_NativeMidi_getRecentMessages( + JNIEnv* env, jobject thiz); +JNIEXPORT void JNICALL Java_com_example_android_nativemididemo_NativeMidi_startReadingMidi( + JNIEnv* env, jobject thiz, jint deviceId, jint portNumber); +JNIEXPORT void JNICALL Java_com_example_android_nativemididemo_NativeMidi_stopReadingMidi( + JNIEnv* env, jobject thiz); +} + +static const char* errStrings[] = { + "SL_RESULT_SUCCESS", // 0 + "SL_RESULT_PRECONDITIONS_VIOLATED", // 1 + "SL_RESULT_PARAMETER_INVALID", // 2 + "SL_RESULT_MEMORY_FAILURE", // 3 + "SL_RESULT_RESOURCE_ERROR", // 4 + "SL_RESULT_RESOURCE_LOST", // 5 + "SL_RESULT_IO_ERROR", // 6 + "SL_RESULT_BUFFER_INSUFFICIENT", // 7 + "SL_RESULT_CONTENT_CORRUPTED", // 8 + "SL_RESULT_CONTENT_UNSUPPORTED", // 9 + "SL_RESULT_CONTENT_NOT_FOUND", // 10 + "SL_RESULT_PERMISSION_DENIED", // 11 + "SL_RESULT_FEATURE_UNSUPPORTED", // 12 + "SL_RESULT_INTERNAL_ERROR", // 13 + "SL_RESULT_UNKNOWN_ERROR", // 14 + "SL_RESULT_OPERATION_ABORTED", // 15 + "SL_RESULT_CONTROL_LOST" }; // 16 +static const char* getSLErrStr(int code) { + return errStrings[code]; +} + +static SLObjectItf engineObject; +static SLEngineItf engineEngine; +static SLObjectItf outputMixObject; +static SLObjectItf playerObject; +static SLPlayItf playerPlay; +static SLAndroidSimpleBufferQueueItf playerBufferQueue; + +static const int minPlaySamples = 32; +static const int maxPlaySamples = 1000; +static std::atomic_int playSamples(maxPlaySamples); +static short playBuffer[maxPlaySamples]; + +static std::atomic_ullong sharedCounter; + +static AMIDI_Device midiDevice = AMIDI_INVALID_HANDLE; +static std::atomic<AMIDI_OutputPort> midiOutputPort(AMIDI_INVALID_HANDLE); + +static int setPlaySamples(int newPlaySamples) +{ + if (newPlaySamples < minPlaySamples) newPlaySamples = minPlaySamples; + if (newPlaySamples > maxPlaySamples) newPlaySamples = maxPlaySamples; + playSamples.store(newPlaySamples); + return newPlaySamples; +} + +// Amount of messages we are ready to handle during one callback cycle. +static const size_t MAX_INCOMING_MIDI_MESSAGES = 20; +// Static allocation to save time in the callback. +static AMIDI_Message incomingMidiMessages[MAX_INCOMING_MIDI_MESSAGES]; + +static void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void */*context*/) +{ + sharedCounter++; + + AMIDI_OutputPort outputPort = midiOutputPort.load(); + if (outputPort != AMIDI_INVALID_HANDLE) { + char midiDumpBuffer[1024]; + ssize_t midiReceived = AMIDI_receive( + outputPort, incomingMidiMessages, MAX_INCOMING_MIDI_MESSAGES); + if (midiReceived >= 0) { + for (ssize_t i = 0; i < midiReceived; ++i) { + AMIDI_Message* msg = &incomingMidiMessages[i]; + if (msg->opcode == AMIDI_OPCODE_DATA) { + memset(midiDumpBuffer, 0, sizeof(midiDumpBuffer)); + int pos = snprintf(midiDumpBuffer, sizeof(midiDumpBuffer), + "%" PRIx64 " ", msg->timestamp); + for (uint8_t *b = msg->buffer, *e = b + msg->len; b < e; ++b) { + pos += snprintf(midiDumpBuffer + pos, sizeof(midiDumpBuffer) - pos, + "%02x ", *b); + } + nativemididemo::writeMessage(midiDumpBuffer); + } else if (msg->opcode == AMIDI_OPCODE_FLUSH) { + nativemididemo::writeMessage("MIDI flush"); + } + } + } else { + snprintf(midiDumpBuffer, sizeof(midiDumpBuffer), + "! MIDI Receive error: %s !", strerror(-midiReceived)); + nativemididemo::writeMessage(midiDumpBuffer); + } + } + + size_t usedBufferSize = playSamples.load() * sizeof(playBuffer[0]); + if (usedBufferSize > sizeof(playBuffer)) { + usedBufferSize = sizeof(playBuffer); + } + (*bq)->Enqueue(bq, playBuffer, usedBufferSize); +} + +jstring Java_com_example_android_nativemididemo_NativeMidi_initAudio( + JNIEnv* env, jobject, jint sampleRate, jint playSamples) { + const char* stage; + SLresult result; + char printBuffer[1024]; + + playSamples = setPlaySamples(playSamples); + + result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL); + if (SL_RESULT_SUCCESS != result) { stage = "slCreateEngine"; goto handle_error; } + + result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE); + if (SL_RESULT_SUCCESS != result) { stage = "realize Engine object"; goto handle_error; } + + result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine); + if (SL_RESULT_SUCCESS != result) { stage = "get Engine interface"; goto handle_error; } + + result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 0, NULL, NULL); + if (SL_RESULT_SUCCESS != result) { stage = "CreateOutputMix"; goto handle_error; } + + result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE); + if (SL_RESULT_SUCCESS != result) { stage = "realize OutputMix object"; goto handle_error; } + + { + SLDataFormat_PCM format_pcm = { SL_DATAFORMAT_PCM, 1, (SLuint32)sampleRate * 1000, + SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16, + SL_SPEAKER_FRONT_LEFT, SL_BYTEORDER_LITTLEENDIAN }; + SLDataLocator_AndroidSimpleBufferQueue loc_bufq = + { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 1 }; + SLDataSource audioSrc = { &loc_bufq, &format_pcm }; + SLDataLocator_OutputMix loc_outmix = { SL_DATALOCATOR_OUTPUTMIX, outputMixObject }; + SLDataSink audioSnk = { &loc_outmix, NULL }; + const SLInterfaceID ids[1] = { SL_IID_BUFFERQUEUE }; + const SLboolean req[1] = { SL_BOOLEAN_TRUE }; + result = (*engineEngine)->CreateAudioPlayer( + engineEngine, &playerObject, &audioSrc, &audioSnk, 1, ids, req); + if (SL_RESULT_SUCCESS != result) { stage = "CreateAudioPlayer"; goto handle_error; } + + result = (*playerObject)->Realize(playerObject, SL_BOOLEAN_FALSE); + if (SL_RESULT_SUCCESS != result) { stage = "realize Player object"; goto handle_error; } + } + + result = (*playerObject)->GetInterface(playerObject, SL_IID_PLAY, &playerPlay); + if (SL_RESULT_SUCCESS != result) { stage = "get Play interface"; goto handle_error; } + + result = (*playerObject)->GetInterface(playerObject, SL_IID_BUFFERQUEUE, &playerBufferQueue); + if (SL_RESULT_SUCCESS != result) { stage = "get BufferQueue interface"; goto handle_error; } + + result = (*playerBufferQueue)->RegisterCallback(playerBufferQueue, bqPlayerCallback, NULL); + if (SL_RESULT_SUCCESS != result) { stage = "register BufferQueue callback"; goto handle_error; } + + result = (*playerBufferQueue)->Enqueue(playerBufferQueue, playBuffer, sizeof(playBuffer)); + if (SL_RESULT_SUCCESS != result) { + stage = "enqueue into PlayerBufferQueue"; goto handle_error; } + + result = (*playerPlay)->SetPlayState(playerPlay, SL_PLAYSTATE_PLAYING); + if (SL_RESULT_SUCCESS != result) { + stage = "SetPlayState(SL_PLAYSTATE_PLAYING)"; goto handle_error; } + + snprintf(printBuffer, sizeof(printBuffer), + "Success, sample rate %d, buffer samples %d", sampleRate, playSamples); + return env->NewStringUTF(printBuffer); + +handle_error: + snprintf(printBuffer, sizeof(printBuffer), "Error at %s: %s", stage, getSLErrStr(result)); + return env->NewStringUTF(printBuffer); +} + +void Java_com_example_android_nativemididemo_NativeMidi_pauseAudio( + JNIEnv*, jobject) { + if (playerPlay != NULL) { + (*playerPlay)->SetPlayState(playerPlay, SL_PLAYSTATE_PAUSED); + } +} + +void Java_com_example_android_nativemididemo_NativeMidi_resumeAudio( + JNIEnv*, jobject) { + if (playerBufferQueue != NULL && playerPlay != NULL) { + (*playerBufferQueue)->Enqueue(playerBufferQueue, playBuffer, sizeof(playBuffer)); + (*playerPlay)->SetPlayState(playerPlay, SL_PLAYSTATE_PLAYING); + } +} + +void Java_com_example_android_nativemididemo_NativeMidi_shutdownAudio( + JNIEnv*, jobject) { + if (playerObject != NULL) { + (*playerObject)->Destroy(playerObject); + playerObject = NULL; + playerPlay = NULL; + playerBufferQueue = NULL; + } + + if (outputMixObject != NULL) { + (*outputMixObject)->Destroy(outputMixObject); + outputMixObject = NULL; + } + + if (engineObject != NULL) { + (*engineObject)->Destroy(engineObject); + engineObject = NULL; + engineEngine = NULL; + } +} + +jlong Java_com_example_android_nativemididemo_NativeMidi_getPlaybackCounter(JNIEnv*, jobject) { + return sharedCounter.load(); +} + +jobjectArray Java_com_example_android_nativemididemo_NativeMidi_getRecentMessages( + JNIEnv* env, jobject thiz) { + return nativemididemo::getRecentMessagesForJava(env, thiz); +} + +void Java_com_example_android_nativemididemo_NativeMidi_startReadingMidi( + JNIEnv*, jobject, jint deviceId, jint portNumber) { + char buffer[1024]; + + int result = AMIDI_getDeviceById(deviceId, &midiDevice); + if (result == 0) { + snprintf(buffer, sizeof(buffer), "Obtained device token for uid %d: token %d", deviceId, midiDevice); + } else { + snprintf(buffer, sizeof(buffer), "Could not obtain device token for uid %d: %d", deviceId, result); + } + nativemididemo::writeMessage(buffer); + if (result) return; + + AMIDI_DeviceInfo deviceInfo; + result = AMIDI_getDeviceInfo(midiDevice, &deviceInfo); + if (result == 0) { + snprintf(buffer, sizeof(buffer), "Device info: uid %d, type %d, priv %d, ports %d I / %d O", + deviceInfo.uid, deviceInfo.type, deviceInfo.isPrivate, + (int)deviceInfo.inputPortCount, (int)deviceInfo.outputPortCount); + } else { + snprintf(buffer, sizeof(buffer), "Could not obtain device info %d", result); + } + nativemididemo::writeMessage(buffer); + if (result) return; + + AMIDI_OutputPort outputPort; + result = AMIDI_openOutputPort(midiDevice, portNumber, &outputPort); + if (result == 0) { + snprintf(buffer, sizeof(buffer), "Opened port %d: token %d", portNumber, outputPort); + midiOutputPort.store(outputPort); + } else { + snprintf(buffer, sizeof(buffer), "Could not open port %d: %d", deviceId, result); + } + nativemididemo::writeMessage(buffer); +} + +void Java_com_example_android_nativemididemo_NativeMidi_stopReadingMidi( + JNIEnv*, jobject) { + AMIDI_OutputPort outputPort = midiOutputPort.exchange(AMIDI_INVALID_HANDLE); + if (outputPort == AMIDI_INVALID_HANDLE) return; + int result = AMIDI_closeOutputPort(outputPort); + char buffer[1024]; + if (result == 0) { + snprintf(buffer, sizeof(buffer), "Closed port by token %d", outputPort); + } else { + snprintf(buffer, sizeof(buffer), "Could not close port by token %d: %d", outputPort, result); + } + nativemididemo::writeMessage(buffer); +} diff --git a/media/tests/NativeMidiDemo/res/layout/main.xml b/media/tests/NativeMidiDemo/res/layout/main.xml new file mode 100644 index 000000000000..465d471a2de7 --- /dev/null +++ b/media/tests/NativeMidiDemo/res/layout/main.xml @@ -0,0 +1,93 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + > + <LinearLayout + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + > + <TextView + android:id="@+id/callback_status" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="13sp" + android:typeface="monospace" + /> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="13sp" + android:typeface="monospace" + android:text=" | " + /> + <TextView + android:id="@+id/java_midi_status" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="13sp" + android:typeface="monospace" + /> + <TextView + android:layout_width="0px" + android:layout_height="wrap_content" + android:layout_weight="1" + android:textSize="13sp" + android:typeface="monospace" + android:text=" " + /> + </LinearLayout> + <HorizontalScrollView + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:paddingTop="6sp" + android:paddingBottom="6sp" + > + <RadioGroup + android:id="@+id/devices" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:checkedButton="@+id/device_none" + > + <RadioButton + android:id="@+id/device_none" + android:text="None" + /> + </RadioGroup> + </HorizontalScrollView> + <com.example.android.nativemididemo.TouchableScrollView android:id="@+id/messages_scroll" + android:layout_width="match_parent" + android:layout_height="0px" + android:layout_weight="1" + > + <TextView android:id="@+id/messages" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textSize="13sp" + android:typeface="monospace" + /> + </com.example.android.nativemididemo.TouchableScrollView> + <LinearLayout + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + > + <Button android:id="@+id/clear_messages" + android:layout_width="0px" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/clear_messages" + android:onClick="onClearMessages" + /> + <Button android:id="@+id/close_port" + android:layout_width="0px" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/close_port" + android:onClick="onClosePort" + /> + </LinearLayout> +</LinearLayout> diff --git a/media/tests/NativeMidiDemo/res/mipmap-hdpi/ic_launcher.png b/media/tests/NativeMidiDemo/res/mipmap-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000000..cde69bcccec6 --- /dev/null +++ b/media/tests/NativeMidiDemo/res/mipmap-hdpi/ic_launcher.png diff --git a/media/tests/NativeMidiDemo/res/mipmap-mdpi/ic_launcher.png b/media/tests/NativeMidiDemo/res/mipmap-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000000..c133a0cbd379 --- /dev/null +++ b/media/tests/NativeMidiDemo/res/mipmap-mdpi/ic_launcher.png diff --git a/media/tests/NativeMidiDemo/res/mipmap-xhdpi/ic_launcher.png b/media/tests/NativeMidiDemo/res/mipmap-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000000..bfa42f0e7b91 --- /dev/null +++ b/media/tests/NativeMidiDemo/res/mipmap-xhdpi/ic_launcher.png diff --git a/media/tests/NativeMidiDemo/res/mipmap-xxhdpi/ic_launcher.png b/media/tests/NativeMidiDemo/res/mipmap-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000000..324e72cdd748 --- /dev/null +++ b/media/tests/NativeMidiDemo/res/mipmap-xxhdpi/ic_launcher.png diff --git a/media/tests/NativeMidiDemo/res/values/strings.xml b/media/tests/NativeMidiDemo/res/values/strings.xml new file mode 100644 index 000000000000..5b69b52a5836 --- /dev/null +++ b/media/tests/NativeMidiDemo/res/values/strings.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="app_name">NativeMidiDemo</string> + <string name="clear_messages">Clear Messages</string> + <string name="close_port">Close Port</string> +</resources> diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt index 3ede2ee639bf..1b1f28c37469 100644 --- a/native/android/libandroid.map.txt +++ b/native/android/libandroid.map.txt @@ -139,16 +139,27 @@ LIBANDROID { ANativeActivity_setWindowFlags; ANativeActivity_setWindowFormat; ANativeActivity_showSoftInput; - ANativeWindow_acquire; # removed=26 - ANativeWindow_fromSurface; # removed=26 - ANativeWindow_fromSurfaceTexture; # removed=26 - ANativeWindow_getFormat; # removed=26 - ANativeWindow_getHeight; # removed=26 - ANativeWindow_getWidth; # removed=26 - ANativeWindow_lock; # removed=26 - ANativeWindow_release; # removed=26 - ANativeWindow_setBuffersGeometry; # removed=26 - ANativeWindow_unlockAndPost; # removed=26 + AHardwareBuffer_acquire; # introduced=26 + AHardwareBuffer_allocate; # introduced=26 + AHardwareBuffer_describe; # introduced=26 + AHardwareBuffer_fromHardwareBuffer; # introduced=26 + AHardwareBuffer_getNativeHandle; # introduced=26 + AHardwareBuffer_lock; # introduced=26 + AHardwareBuffer_recvHandleFromUnixSocket; # introduced=26 + AHardwareBuffer_release; # introduced=26 + AHardwareBuffer_sendHandleToUnixSocket; # introduced=26 + AHardwareBuffer_toHardwareBuffer; # introduced=26 + AHardwareBuffer_unlock; # introduced=26 + ANativeWindow_acquire; + ANativeWindow_fromSurface; + ANativeWindow_fromSurfaceTexture; # introduced-arm=13 introduced-mips=13 introduced-x86=13 + ANativeWindow_getFormat; + ANativeWindow_getHeight; + ANativeWindow_getWidth; + ANativeWindow_lock; + ANativeWindow_release; + ANativeWindow_setBuffersGeometry; + ANativeWindow_unlockAndPost; AObbInfo_delete; AObbInfo_getFlags; AObbInfo_getPackageName; diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java b/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java index d4623d6f46f6..82da9a38d756 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java @@ -31,7 +31,8 @@ public class InterestingConfigChanges { public InterestingConfigChanges(int extraFlags) { mFlags = extraFlags | ActivityInfo.CONFIG_LOCALE - | ActivityInfo.CONFIG_UI_MODE | ActivityInfo.CONFIG_SCREEN_LAYOUT; + | ActivityInfo.CONFIG_UI_MODE | ActivityInfo.CONFIG_SCREEN_LAYOUT + | ActivityInfo.CONFIG_ASSETS_PATHS; } public boolean applyNewConfig(Resources res) { diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/PermissionsSummaryHelper.java b/packages/SettingsLib/src/com/android/settingslib/applications/PermissionsSummaryHelper.java new file mode 100644 index 000000000000..ec8e9561ea83 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/applications/PermissionsSummaryHelper.java @@ -0,0 +1,80 @@ +/* + * 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.settingslib.applications; + +import android.annotation.NonNull; +import android.content.Context; +import android.content.pm.permission.RuntimePermissionPresentationInfo; +import android.content.pm.permission.RuntimePermissionPresenter; + +import java.text.Collator; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class PermissionsSummaryHelper { + + public static void getPermissionSummary(Context context, String pkg, + final PermissionsResultCallback callback) { + final RuntimePermissionPresenter presenter = + RuntimePermissionPresenter.getInstance(context); + presenter.getAppPermissions(pkg, new RuntimePermissionPresenter.OnResultCallback() { + @Override + public void onGetAppPermissions( + @NonNull List<RuntimePermissionPresentationInfo> permissions) { + final int permissionCount = permissions.size(); + + int grantedStandardCount = 0; + int grantedAdditionalCount = 0; + int requestedCount = 0; + List<CharSequence> grantedStandardLabels = new ArrayList<>(); + + for (int i = 0; i < permissionCount; i++) { + RuntimePermissionPresentationInfo permission = permissions.get(i); + requestedCount++; + if (permission.isGranted()) { + if (permission.isStandard()) { + grantedStandardLabels.add(permission.getLabel()); + grantedStandardCount++; + } else { + grantedAdditionalCount++; + } + } + } + + Collator collator = Collator.getInstance(); + collator.setStrength(Collator.PRIMARY); + Collections.sort(grantedStandardLabels, collator); + + callback.onPermissionSummaryResult(grantedStandardCount, requestedCount, + grantedAdditionalCount, grantedStandardLabels); + } + }, null); + } + + public static abstract class PermissionsResultCallback { + public void onAppWithPermissionsCountsResult(int standardGrantedPermissionAppCount, + int standardUsedPermissionAppCount) { + /* do nothing - stub */ + } + + public void onPermissionSummaryResult(int standardGrantedPermissionCount, + int requestedPermissionCount, int additionalGrantedPermissionCount, + List<CharSequence> grantedGroupLabels) { + /* do nothing - stub */ + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/SystemBars.java b/packages/SystemUI/src/com/android/systemui/SystemBars.java index 6623cabe4bd7..b5093b3ce52a 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemBars.java +++ b/packages/SystemUI/src/com/android/systemui/SystemBars.java @@ -43,13 +43,6 @@ public class SystemBars extends SystemUI { } @Override - protected void onConfigurationChanged(Configuration newConfig) { - if (mStatusBar != null) { - mStatusBar.onConfigurationChanged(newConfig); - } - } - - @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (mStatusBar != null) { mStatusBar.dump(fd, pw, args); diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java index ae402efabc7f..e7256d14de77 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java @@ -71,8 +71,15 @@ public class PipManager implements BasePipManager { } @Override + public void onPinnedStackAnimationStarted() { + // Disable touches while the animation is running + mTouchHandler.setTouchEnabled(false); + } + + @Override public void onPinnedStackAnimationEnded() { - // TODO(winsonc): Disable touch interaction with the PiP until the animation ends + // Re-enable touches after the animation completes + mTouchHandler.setTouchEnabled(true); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java index d832810e4812..4100b66b07b6 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java @@ -181,6 +181,10 @@ public class PipTouchHandler { registerInputConsumer(); } + public void setTouchEnabled(boolean enabled) { + mTouchState.setAllowTouches(enabled); + } + public void onActivityPinned() { // Reset some states once we are pinned if (mIsTappingThrough) { @@ -294,6 +298,7 @@ public class PipTouchHandler { // Fall through to clean up } case MotionEvent.ACTION_CANCEL: { + mTouchState.reset(); break; } } @@ -418,6 +423,10 @@ public class PipTouchHandler { @Override public void onDown(PipTouchState touchState) { + if (!touchState.isUserInteracting()) { + return; + } + if (ENABLE_DRAG_TO_DISMISS) { mDismissViewController.createDismissTarget(); mHandler.postDelayed(mShowDismissAffordance, SHOW_DISMISS_AFFORDANCE_DELAY); @@ -426,6 +435,10 @@ public class PipTouchHandler { @Override boolean onMove(PipTouchState touchState) { + if (!touchState.isUserInteracting()) { + return false; + } + if (touchState.startedDragging()) { mSavedSnapFraction = -1f; } @@ -458,6 +471,10 @@ public class PipTouchHandler { @Override public boolean onUp(PipTouchState touchState) { + if (!touchState.isUserInteracting()) { + return false; + } + try { if (ENABLE_DRAG_TO_DISMISS) { mHandler.removeCallbacks(mShowDismissAffordance); diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchState.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchState.java index 702ad0af0447..a317dc32ba45 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchState.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchState.java @@ -37,6 +37,7 @@ public class PipTouchState { private final PointF mLastTouch = new PointF(); private final PointF mLastDelta = new PointF(); private final PointF mVelocity = new PointF(); + private boolean mAllowTouches = true; private boolean mIsUserInteracting = false; private boolean mIsDragging = false; private boolean mStartedDragging = false; @@ -48,23 +49,41 @@ public class PipTouchState { } /** + * Resets this state. + */ + public void reset() { + mAllowDraggingOffscreen = false; + mIsDragging = false; + mStartedDragging = false; + mIsUserInteracting = false; + } + + /** * Processess a given touch event and updates the state. */ public void onTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: { + if (!mAllowTouches) { + return; + } + // Initialize the velocity tracker initOrResetVelocityTracker(); + mActivePointerId = ev.getPointerId(0); mLastTouch.set(ev.getX(), ev.getY()); mDownTouch.set(mLastTouch); - mIsDragging = false; - mStartedDragging = false; mAllowDraggingOffscreen = true; mIsUserInteracting = true; break; } case MotionEvent.ACTION_MOVE: { + // Skip event if we did not start processing this touch gesture + if (!mIsUserInteracting) { + break; + } + // Update the velocity tracker mVelocityTracker.addMovement(ev); int pointerIndex = ev.findPointerIndex(mActivePointerId); @@ -86,6 +105,11 @@ public class PipTouchState { break; } case MotionEvent.ACTION_POINTER_UP: { + // Skip event if we did not start processing this touch gesture + if (!mIsUserInteracting) { + break; + } + // Update the velocity tracker mVelocityTracker.addMovement(ev); @@ -100,6 +124,11 @@ public class PipTouchState { break; } case MotionEvent.ACTION_UP: { + // Skip event if we did not start processing this touch gesture + if (!mIsUserInteracting) { + break; + } + // Update the velocity tracker mVelocityTracker.addMovement(ev); mVelocityTracker.computeCurrentVelocity(1000, @@ -112,7 +141,6 @@ public class PipTouchState { // Fall through to clean up } case MotionEvent.ACTION_CANCEL: { - mIsUserInteracting = false; recycleVelocityTracker(); break; } @@ -171,6 +199,19 @@ public class PipTouchState { } /** + * Sets whether touching is currently allowed. + */ + public void setAllowTouches(boolean allowTouches) { + mAllowTouches = allowTouches; + + // If the user happens to touch down before this is sent from the system during a transition + // then block any additional handling by resetting the state now + if (mIsUserInteracting) { + reset(); + } + } + + /** * Disallows dragging offscreen for the duration of the current gesture. */ public void setDisallowDraggingOffscreen() { @@ -202,6 +243,7 @@ public class PipTouchState { public void dump(PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; pw.println(prefix + TAG); + pw.println(innerPrefix + "mAllowTouches=" + mAllowTouches); pw.println(innerPrefix + "mDownTouch=" + mDownTouch); pw.println(innerPrefix + "mDownDelta=" + mDownDelta); pw.println(innerPrefix + "mLastTouch=" + mLastTouch); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileView.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileView.java index 428fe9bbff7e..7d13f76778fe 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileView.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileView.java @@ -84,7 +84,6 @@ public class QSTileView extends QSTileBaseView { mLabel = (TextView) mLabelContainer.findViewById(R.id.tile_label); mPadLock = (ImageView) mLabelContainer.findViewById(R.id.restricted_padlock); - mLabelContainer.setBackground(newTileBackground()); addView(mLabelContainer); } @@ -102,6 +101,11 @@ public class QSTileView extends QSTileBaseView { mLabel.setText(state.label); } mDivider.setVisibility(state.dualTarget ? View.VISIBLE : View.INVISIBLE); + if (state.dualTarget != mLabelContainer.isClickable()) { + mLabelContainer.setClickable(state.dualTarget); + mLabelContainer.setLongClickable(state.dualTarget); + mLabelContainer.setBackground(state.dualTarget ? newTileBackground() : null); + } mLabel.setEnabled(!state.disabledByPolicy); mPadLock.setVisibility(state.disabledByPolicy ? View.VISIBLE : View.GONE); } @@ -110,7 +114,9 @@ public class QSTileView extends QSTileBaseView { public void init(OnClickListener click, OnClickListener secondaryClick, OnLongClickListener longClick) { super.init(click, secondaryClick, longClick); - mLabelContainer.setClickable(true); mLabelContainer.setOnClickListener(secondaryClick); + mLabelContainer.setOnLongClickListener(longClick); + mLabelContainer.setClickable(false); + mLabelContainer.setLongClickable(false); } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java index b28b0e70ceb5..a76299d07626 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java @@ -288,7 +288,7 @@ public class CustomTile extends QSTile<QSTile.State> implements TileChangeListen drawable = mTile.getIcon().loadDrawable(mContext); } catch (Exception e) { Log.w(TAG, "Invalid icon, forcing into unavailable state"); - tileState = Tile.STATE_UNAVAILABLE; + state.state = Tile.STATE_UNAVAILABLE; drawable = mDefaultIcon.loadDrawable(mContext); } state.icon = new DrawableIcon(drawable); diff --git a/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java b/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java index cda902b6fb69..1042356042f8 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java +++ b/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java @@ -154,6 +154,7 @@ public class SystemServicesProxy { public void onTaskSnapshotChanged(int taskId, TaskSnapshot snapshot) { } public void onActivityPinned() { } public void onPinnedActivityRestartAttempt(String launchedFromPackage) { } + public void onPinnedStackAnimationStarted() { } public void onPinnedStackAnimationEnded() { } public void onActivityForcedResizable(String packageName, int taskId) { } public void onActivityDismissingDockedStack() { } @@ -206,6 +207,12 @@ public class SystemServicesProxy { } @Override + public void onPinnedStackAnimationStarted() throws RemoteException { + mHandler.removeMessages(H.ON_PINNED_STACK_ANIMATION_STARTED); + mHandler.sendEmptyMessage(H.ON_PINNED_STACK_ANIMATION_STARTED); + } + + @Override public void onPinnedStackAnimationEnded() throws RemoteException { mHandler.removeMessages(H.ON_PINNED_STACK_ANIMATION_ENDED); mHandler.sendEmptyMessage(H.ON_PINNED_STACK_ANIMATION_ENDED); @@ -1219,6 +1226,7 @@ public class SystemServicesProxy { private static final int ON_ACTIVITY_FORCED_RESIZABLE = 6; private static final int ON_ACTIVITY_DISMISSING_DOCKED_STACK = 7; private static final int ON_TASK_PROFILE_LOCKED = 8; + private static final int ON_PINNED_STACK_ANIMATION_STARTED = 9; @Override public void handleMessage(Message msg) { @@ -1248,6 +1256,12 @@ public class SystemServicesProxy { } break; } + case ON_PINNED_STACK_ANIMATION_STARTED: { + for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { + mTaskStackListeners.get(i).onPinnedStackAnimationStarted(); + } + break; + } case ON_PINNED_STACK_ANIMATION_ENDED: { for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { mTaskStackListeners.get(i).onPinnedStackAnimationEnded(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index 995901b467b9..09b7bec673f4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -177,6 +177,10 @@ public class CommandQueue extends IStatusBar.Stub { disable(state1, state2, true); } + public void recomputeDisableFlags(boolean animate) { + disable(mDisable1, mDisable2, animate); + } + public void animateExpandNotificationsPanel() { synchronized (mLock) { mHandler.removeMessages(MSG_EXPAND_NOTIFICATIONS); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInfo.java index 807d902e4eaf..af464c6d138f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInfo.java @@ -154,7 +154,12 @@ public class NotificationInfo extends LinearLayout implements GutsContent { iNotificationManager.getNotificationChannelGroupForPackage( channel.getGroup(), pkg, appUid); if (notificationChannelGroup != null) { - groupName = notificationChannelGroup.getName(); + if (info != null && notificationChannelGroup.getNameResId() != 0) { + groupName = pm.getText(pkg, notificationChannelGroup.getNameResId(), info); + } + if (notificationChannelGroup.getName() != null) { + groupName = notificationChannelGroup.getName(); + } } } catch (RemoteException e) { Log.e(TAG, e.toString()); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java index 36ed551ec4bc..8da17fa76bd7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java @@ -41,7 +41,8 @@ import com.android.systemui.statusbar.stack.ViewState; * A notification shelf view that is placed inside the notification scroller. It manages the * overflow icons that don't fit into the regular list anymore. */ -public class NotificationShelf extends ActivatableNotificationView { +public class NotificationShelf extends ActivatableNotificationView implements + View.OnLayoutChangeListener { public static final boolean SHOW_AMBIENT_ICONS = true; private static final boolean USE_ANIMATIONS_WHEN_OPENING = @@ -494,6 +495,10 @@ public class NotificationShelf extends ActivatableNotificationView { @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); + updateRelativeOffset(); + } + + private void updateRelativeOffset() { mCollapsedIcons.getLocationOnScreen(mTmp); mRelativeOffset = mTmp[0]; getLocationOnScreen(mTmp); @@ -560,6 +565,7 @@ public class NotificationShelf extends ActivatableNotificationView { public void setCollapsedIcons(NotificationIconContainer collapsedIcons) { mCollapsedIcons = collapsedIcons; + mCollapsedIcons.addOnLayoutChangeListener(this); } public void setStatusBarState(int statusBarState) { @@ -595,6 +601,12 @@ public class NotificationShelf extends ActivatableNotificationView { } } + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, + int oldTop, int oldRight, int oldBottom) { + updateRelativeOffset(); + } + private class ShelfState extends ExpandableViewState { private float openedAmount; private boolean hasItemsInStableShelf; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java index aec9a4b421ae..110170194294 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java @@ -612,8 +612,10 @@ public class StatusBarIconView extends AnimatedImageView { } public void setIconAppearAmount(float iconAppearAmount) { - mIconAppearAmount = iconAppearAmount; - invalidate(); + if (mIconAppearAmount != iconAppearAmount) { + mIconAppearAmount = iconAppearAmount; + invalidate(); + } } public float getIconAppearAmount() { @@ -625,8 +627,10 @@ public class StatusBarIconView extends AnimatedImageView { } public void setDotAppearAmount(float dotAppearAmount) { - mDotAppearAmount = dotAppearAmount; - invalidate(); + if (mDotAppearAmount != dotAppearAmount) { + mDotAppearAmount = dotAppearAmount; + invalidate(); + } } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java index 1f56c563bef7..6cd3eae366e8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java @@ -153,7 +153,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue } private boolean shouldHideNotificationIcons() { - return !mStatusBar.isClosed() && mStatusBarComponent.shouldHideNotificationIcons(); + return !mStatusBar.isClosed() && mStatusBarComponent.hideStatusBarIconsWhenExpanded(); } public void hideSystemIconArea(boolean animate) { 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 ad875f13d27e..3f7e340eff21 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java @@ -21,8 +21,8 @@ import android.animation.LayoutTransition.TransitionListener; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; -import android.app.ActivityManager; import android.annotation.DrawableRes; +import android.app.ActivityManager; import android.app.StatusBarManager; import android.content.Context; import android.content.res.Configuration; 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 707997d5be45..45812042412c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java @@ -79,7 +79,9 @@ public class NotificationIconAreaController implements DarkReceiver { for (int i = 0; i < mNotificationIcons.getChildCount(); i++) { View child = mNotificationIcons.getChildAt(i); child.setLayoutParams(params); - child = mShelfIcons.getChildAt(i); + } + for (int i = 0; i < mShelfIcons.getChildCount(); i++) { + View child = mShelfIcons.getChildAt(i); child.setLayoutParams(params); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java index 468fb57069ee..e6d3168693a8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java @@ -26,7 +26,6 @@ import android.app.StatusBarManager; import android.content.Context; import android.content.pm.ResolveInfo; import android.content.res.Configuration; -import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; @@ -47,7 +46,6 @@ import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.keyguard.KeyguardStatusView; import com.android.systemui.DejankUtils; -import com.android.systemui.EventLogConstants; import com.android.systemui.EventLogTags; import com.android.systemui.Interpolators; import com.android.systemui.R; @@ -179,7 +177,7 @@ public class NotificationPanelView extends PanelView implements private boolean mKeyguardStatusViewAnimating; private ValueAnimator mQsSizeChangeAnimator; - private boolean mShadeEmpty; + private boolean mShowEmptyShadeView; private boolean mQsScrimEnabled = true; private boolean mLastAnnouncementWasQuickSettings; @@ -211,11 +209,12 @@ public class NotificationPanelView extends PanelView implements } }; private NotificationGroupManager mGroupManager; - private boolean mOpening; + private boolean mShowIconsWhenExpanded; private int mIndicationBottomPadding; private boolean mIsFullWidth; private boolean mDark; private LockscreenGestureLogger mLockscreenGestureLogger = new LockscreenGestureLogger(); + private boolean mNoVisibleNotifications = true; public NotificationPanelView(Context context, AttributeSet attrs) { super(context, attrs); @@ -1518,7 +1517,7 @@ public class NotificationPanelView extends PanelView implements // it in expanded QS state as well so we don't run into troubles when fading the view in/out // and expanding/collapsing the whole panel from/to quick settings. if (mNotificationStackScroller.getNotGoneChildCount() == 0 - && mShadeEmpty) { + && mShowEmptyShadeView) { notificationHeight = mNotificationStackScroller.getEmptyShadeViewHeight(); } int maxQsHeight = mQsMaxExpansionHeight; @@ -2118,15 +2117,15 @@ public class NotificationPanelView extends PanelView implements return mDozing; } - public void setShadeEmpty(boolean shadeEmpty) { - mShadeEmpty = shadeEmpty; + public void showEmptyShadeView(boolean emptyShadeViewVisible) { + mShowEmptyShadeView = emptyShadeViewVisible; updateEmptyShadeView(); } private void updateEmptyShadeView() { // Hide "No notifications" in QS. - mNotificationStackScroller.updateEmptyShadeView(mShadeEmpty && !mQsExpanded); + mNotificationStackScroller.updateEmptyShadeView(mShowEmptyShadeView && !mQsExpanded); } public void setQsScrimEnabled(boolean qsScrimEnabled) { @@ -2306,7 +2305,7 @@ public class NotificationPanelView extends PanelView implements } mNotificationStackScroller.setExpandedHeight(expandedHeight); updateKeyguardBottomAreaAlpha(); - setOpening(isFullWidth() && expandedHeight < getOpeningHeight()); + updateStatusBarIcons(); } /** @@ -2317,13 +2316,21 @@ public class NotificationPanelView extends PanelView implements return mIsFullWidth; } - private void setOpening(boolean opening) { - if (opening != mOpening) { - mOpening = opening; + private void updateStatusBarIcons() { + boolean showIconsWhenExpanded = isFullWidth() && getExpandedHeight() < getOpeningHeight(); + if (showIconsWhenExpanded && mNoVisibleNotifications && isOnKeyguard()) { + showIconsWhenExpanded = false; + } + if (showIconsWhenExpanded != mShowIconsWhenExpanded) { + mShowIconsWhenExpanded = showIconsWhenExpanded; mStatusBar.recomputeDisableFlags(false); } } + private boolean isOnKeyguard() { + return mStatusBar.getBarState() == StatusBarState.KEYGUARD; + } + public void setPanelScrimMinFraction(float minFraction) { mBar.panelScrimMinFractionChanged(minFraction); } @@ -2426,12 +2433,8 @@ public class NotificationPanelView extends PanelView implements mGroupManager = groupManager; } - public boolean shouldHideNotificationIcons() { - return !isFullWidth() || (!mOpening && !isFullyCollapsed()); - } - - public boolean shouldAnimateIconHiding() { - return !isFullWidth(); + public boolean hideStatusBarIconsWhenExpanded() { + return !isFullWidth() || !mShowIconsWhenExpanded; } private final FragmentListener mFragmentListener = new FragmentListener() { @@ -2473,4 +2476,8 @@ public class NotificationPanelView extends PanelView implements mKeyguardStatusView.setDark(dark); positionClockAndNotifications(); } + + public void setNoVisibleNotifications(boolean noNotifications) { + mNoVisibleNotifications = noNotifications; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java index 9e93ed3c813f..bb6c8f2e1e73 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.phone; import android.app.ActivityManager; -import android.app.ActivityManager.RunningAppProcessInfo; import android.app.ActivityManager.StackId; import android.app.ActivityManager.StackInfo; import android.app.AlarmManager; @@ -25,8 +24,6 @@ import android.app.AlarmManager.AlarmClockInfo; import android.app.AppGlobals; import android.app.Notification; import android.app.Notification.Action; -import android.app.Notification.BigTextStyle; -import android.app.Notification.Style; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.SynchronousUserSwitchObserver; @@ -37,7 +34,6 @@ import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.UserInfo; import android.graphics.drawable.Icon; import android.media.AudioManager; @@ -51,7 +47,6 @@ import android.provider.Settings; import android.provider.Settings.Global; import android.service.notification.StatusBarNotification; import android.telecom.TelecomManager; -import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; import android.util.Pair; @@ -61,11 +56,9 @@ import com.android.internal.telephony.IccCardConstants; import com.android.internal.telephony.TelephonyIntents; import com.android.systemui.Dependency; import com.android.systemui.R; -import com.android.systemui.R.string; import com.android.systemui.SysUiServiceProvider; import com.android.systemui.qs.tiles.DndTile; import com.android.systemui.qs.tiles.RotationLockTile; -import com.android.systemui.recents.Recents; import com.android.systemui.recents.misc.SystemServicesProxy; import com.android.systemui.recents.misc.SystemServicesProxy.TaskStackListener; import com.android.systemui.statusbar.CommandQueue; @@ -87,9 +80,6 @@ import com.android.systemui.statusbar.policy.UserInfoController; import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.util.NotificationChannels; -import java.util.ArrayList; -import java.util.List; - /** * This class contains all of the policy about which icons are installed in the status * bar at boot time. It goes through the normal API for icons, even though it probably @@ -137,8 +127,6 @@ public class PhoneStatusBarPolicy implements Callback, Callbacks, private boolean mVolumeVisible; private boolean mCurrentUserSetup; - private int mZen; - private boolean mManagedProfileFocused = false; private boolean mManagedProfileIconVisible = false; private boolean mManagedProfileInQuietMode = false; @@ -275,14 +263,14 @@ public class PhoneStatusBarPolicy implements Callback, Callbacks, @Override public void onZenChanged(int zen) { - mZen = zen; updateVolumeZen(); } private void updateAlarm() { final AlarmClockInfo alarm = mAlarmManager.getNextAlarmClock(UserHandle.USER_CURRENT); final boolean hasAlarm = alarm != null && alarm.getTriggerTime() > 0; - final boolean zenNone = mZen == Global.ZEN_MODE_NO_INTERRUPTIONS; + int zen = mZenController.getZen(); + final boolean zenNone = zen == Global.ZEN_MODE_NO_INTERRUPTIONS; mIconController.setIcon(mSlotAlarmClock, zenNone ? R.drawable.stat_sys_alarm_dim : R.drawable.stat_sys_alarm, null); mIconController.setIconVisibility(mSlotAlarmClock, mCurrentUserSetup && hasAlarm); @@ -323,17 +311,18 @@ public class PhoneStatusBarPolicy implements Callback, Callbacks, boolean volumeVisible = false; int volumeIconId = 0; String volumeDescription = null; + int zen = mZenController.getZen(); if (DndTile.isVisible(mContext) || DndTile.isCombinedIcon(mContext)) { - zenVisible = mZen != Global.ZEN_MODE_OFF; - zenIconId = mZen == Global.ZEN_MODE_NO_INTERRUPTIONS + zenVisible = zen != Global.ZEN_MODE_OFF; + zenIconId = zen == Global.ZEN_MODE_NO_INTERRUPTIONS ? R.drawable.stat_sys_dnd_total_silence : R.drawable.stat_sys_dnd; zenDescription = mContext.getString(R.string.quick_settings_dnd_label); - } else if (mZen == Global.ZEN_MODE_NO_INTERRUPTIONS) { + } else if (zen == Global.ZEN_MODE_NO_INTERRUPTIONS) { zenVisible = true; zenIconId = R.drawable.stat_sys_zen_none; zenDescription = mContext.getString(R.string.interruption_level_none); - } else if (mZen == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS) { + } else if (zen == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS) { zenVisible = true; zenIconId = R.drawable.stat_sys_zen_important; zenDescription = mContext.getString(R.string.interruption_level_priority); @@ -344,7 +333,7 @@ public class PhoneStatusBarPolicy implements Callback, Callbacks, volumeVisible = true; volumeIconId = R.drawable.stat_sys_ringer_silent; volumeDescription = mContext.getString(R.string.accessibility_ringer_silent); - } else if (mZen != Global.ZEN_MODE_NO_INTERRUPTIONS && mZen != Global.ZEN_MODE_ALARMS && + } else if (zen != Global.ZEN_MODE_NO_INTERRUPTIONS && zen != Global.ZEN_MODE_ALARMS && audioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_VIBRATE) { volumeVisible = true; volumeIconId = R.drawable.stat_sys_ringer_vibrate; 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 53d2d735b14e..816a39d7ab93 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -172,7 +172,6 @@ import com.android.systemui.statusbar.ScrimView; import com.android.systemui.statusbar.SignalClusterView; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.InflationException; -import com.android.systemui.statusbar.notification.NotificationInflater; import com.android.systemui.statusbar.notification.VisualStabilityManager; import com.android.systemui.statusbar.phone.StatusBarIconController.IconManager; import com.android.systemui.statusbar.phone.UnlockMethodCache.OnUnlockMethodChangedListener; @@ -717,7 +716,7 @@ public class StatusBar extends SystemUI implements DemoMode, private LogMaker mStatusBarStateLog; private LockscreenGestureLogger mLockscreenGestureLogger = new LockscreenGestureLogger(); private NotificationIconAreaController mNotificationIconAreaController; - private ConfigurationListener mDensityChangeListener; + private ConfigurationListener mConfigurationListener; private InflationExceptionHandler mInflationExceptionHandler = this::handleInflationException; private void recycleAllVisibilityObjects(ArraySet<NotificationVisibility> array) { @@ -945,13 +944,18 @@ public class StatusBar extends SystemUI implements DemoMode, Dependency.get(ActivityStarterDelegate.class).setActivityStarterImpl(this); - mDensityChangeListener = new ConfigurationListener() { + mConfigurationListener = new ConfigurationListener() { + @Override + public void onConfigChanged(Configuration newConfig) { + StatusBar.this.onConfigurationChanged(newConfig); + } + @Override public void onDensityOrFontScaleChanged() { StatusBar.this.onDensityOrFontScaleChanged(); } }; - Dependency.get(ConfigurationController.class).addCallback(mDensityChangeListener); + Dependency.get(ConfigurationController.class).addCallback(mConfigurationListener); } protected void createIconController() { @@ -2034,10 +2038,10 @@ public class StatusBar extends SystemUI implements DemoMode, } private void updateEmptyShadeView() { - boolean showEmptyShade = + boolean showEmptyShadeView = mState != StatusBarState.KEYGUARD && mNotificationData.getActiveNotifications().size() == 0; - mNotificationPanel.setShadeEmpty(showEmptyShade); + mNotificationPanel.showEmptyShadeView(showEmptyShadeView); } private void updateSpeedBumpIndex() { @@ -2506,7 +2510,7 @@ public class StatusBar extends SystemUI implements DemoMode, * This needs to be called if state used by {@link #adjustDisableFlags} changes. */ public void recomputeDisableFlags(boolean animate) { - mCommandQueue.disable(mDisabledUnmodified1, mDisabledUnmodified2, animate); + mCommandQueue.recomputeDisableFlags(animate); } protected H createHandler() { @@ -2720,8 +2724,8 @@ public class StatusBar extends SystemUI implements DemoMode, return mLaunchTransitionFadingAway; } - public boolean shouldHideNotificationIcons() { - return mNotificationPanel.shouldHideNotificationIcons(); + public boolean hideStatusBarIconsWhenExpanded() { + return mNotificationPanel.hideStatusBarIconsWhenExpanded(); } /** @@ -2963,7 +2967,7 @@ public class StatusBar extends SystemUI implements DemoMode, runPostCollapseRunnables(); setInteracting(StatusBarManager.WINDOW_STATUS_BAR, false); showBouncerIfKeyguard(); - recomputeDisableFlags(shouldAnimatIconHiding() /* animate */); + recomputeDisableFlags(mNotificationPanel.hideStatusBarIconsWhenExpanded() /* animate */); // Trimming will happen later if Keyguard is showing - doing it here might cause a jank in // the bouncer appear animation. @@ -2972,10 +2976,6 @@ public class StatusBar extends SystemUI implements DemoMode, } } - private boolean shouldAnimatIconHiding() { - return mNotificationPanel.shouldAnimateIconHiding(); - } - public boolean interceptTouchEvent(MotionEvent event) { if (DEBUG_GESTURES) { if (event.getActionMasked() != MotionEvent.ACTION_MOVE) { @@ -3910,7 +3910,7 @@ public class StatusBar extends SystemUI implements DemoMode, } Dependency.get(ActivityStarterDelegate.class).setActivityStarterImpl(null); mDeviceProvisionedController.removeCallback(mUserSetupObserver); - Dependency.get(ConfigurationController.class).removeCallback(mDensityChangeListener); + Dependency.get(ConfigurationController.class).removeCallback(mConfigurationListener); } private boolean mDemoModeAllowed; @@ -6570,6 +6570,7 @@ public class StatusBar extends SystemUI implements DemoMode, } } } + mNotificationPanel.setNoVisibleNotifications(visibleNotifications == 0); mStackScroller.changeViewPosition(mDismissView, mStackScroller.getChildCount() - 1); mStackScroller.changeViewPosition(mEmptyShadeView, mStackScroller.getChildCount() - 2); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationInfoTest.java index 9a3fabbbdfaa..92f8c7ce7fe4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationInfoTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationInfoTest.java @@ -100,10 +100,6 @@ public class NotificationInfoTest extends SysuiTestCase { mNotificationChannel = new NotificationChannel( TEST_CHANNEL, TEST_CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW); when(mMockStatusBarNotification.getPackageName()).thenReturn(TEST_PACKAGE_NAME); - when(mMockPackageManager.getText(eq(TEST_PACKAGE_NAME), - eq(R.string.notification_menu_accessibility), anyObject())).thenReturn( - getContext().getString(R.string.notification_menu_accessibility)); - when(mMockINotificationManager.getNumNotificationChannelsForPackage( eq(TEST_PACKAGE_NAME), anyInt(), anyBoolean())).thenReturn(1); } @@ -174,6 +170,28 @@ public class NotificationInfoTest extends SysuiTestCase { @Test @UiThreadTest + public void testBindNotification_SetsGroupName_resId() throws Exception { + when(mMockPackageManager.getText(eq(TEST_PACKAGE_NAME), + eq(R.string.legacy_vpn_name), anyObject())).thenReturn( + getContext().getString(R.string.legacy_vpn_name)); + mNotificationChannel.setGroup("test_group_id"); + final NotificationChannelGroup notificationChannelGroup = + new NotificationChannelGroup("test_group_id", R.string.legacy_vpn_name); + when(mMockINotificationManager.getNotificationChannelGroupForPackage( + eq("test_group_id"), eq(TEST_PACKAGE_NAME), anyInt())) + .thenReturn(notificationChannelGroup); + mNotificationInfo.bindNotification(mMockPackageManager, mMockINotificationManager, + mMockStatusBarNotification, mNotificationChannel, null, null, null); + final TextView groupNameView = (TextView) mNotificationInfo.findViewById(R.id.group_name); + assertEquals(View.VISIBLE, groupNameView.getVisibility()); + assertEquals(mContext.getString(R.string.legacy_vpn_name), groupNameView.getText()); + final TextView groupDividerView = + (TextView) mNotificationInfo.findViewById(R.id.pkg_group_divider); + assertEquals(View.VISIBLE, groupDividerView.getVisibility()); + } + + @Test + @UiThreadTest public void testBindNotification_SetsTextChannelName() throws Exception { mNotificationInfo.bindNotification(mMockPackageManager, mMockINotificationManager, mMockStatusBarNotification, mNotificationChannel, null, null, null); @@ -184,6 +202,9 @@ public class NotificationInfoTest extends SysuiTestCase { @Test @UiThreadTest public void testBindNotification_SetsTextChannelName_resId() throws Exception { + when(mMockPackageManager.getText(eq(TEST_PACKAGE_NAME), + eq(R.string.notification_menu_accessibility), anyObject())).thenReturn( + getContext().getString(R.string.notification_menu_accessibility)); NotificationChannel notificationChannelResId = new NotificationChannel( TEST_CHANNEL, R.string.notification_menu_accessibility, NotificationManager.IMPORTANCE_LOW); diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index e9e73cc20510..54ee5dc43034 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -12111,6 +12111,12 @@ public class ActivityManagerService extends IActivityManager.Stub mRecentTasks.notifyTaskPersisterLocked(task, flush); } + /** Notifies all listeners when the pinned stack animation starts. */ + @Override + public void notifyPinnedStackAnimationStarted() { + mTaskChangeNotificationController.notifyPinnedStackAnimationStarted(); + } + /** Notifies all listeners when the pinned stack animation ends. */ @Override public void notifyPinnedStackAnimationEnded() { @@ -19013,8 +19019,7 @@ public class ActivityManagerService extends IActivityManager.Stub return ActivityManager.BROADCAST_SUCCESS; } - final void addBroadcastStatLocked(String action, String srcPackage, int receiveCount, - int skipCount, long dispatchTime) { + final void rotateBroadcastStatsIfNeededLocked() { final long now = SystemClock.elapsedRealtime(); if (mCurBroadcastStats == null || (mCurBroadcastStats.mStartRealtime +(24*60*60*1000) < now)) { @@ -19025,9 +19030,19 @@ public class ActivityManagerService extends IActivityManager.Stub } mCurBroadcastStats = new BroadcastStats(); } + } + + final void addBroadcastStatLocked(String action, String srcPackage, int receiveCount, + int skipCount, long dispatchTime) { + rotateBroadcastStatsIfNeededLocked(); mCurBroadcastStats.addBroadcast(action, srcPackage, receiveCount, skipCount, dispatchTime); } + final void addBackgroundCheckViolationLocked(String action, String targetPackage) { + rotateBroadcastStatsIfNeededLocked(); + mCurBroadcastStats.addBackgroundCheckViolation(action, targetPackage); + } + final Intent verifyBroadcastLocked(Intent intent) { // Refuse possible leaked file descriptors if (intent != null && intent.hasFileDescriptors() == true) { diff --git a/services/core/java/com/android/server/am/BroadcastQueue.java b/services/core/java/com/android/server/am/BroadcastQueue.java index 75b5929b0c65..c9d19cb31832 100644 --- a/services/core/java/com/android/server/am/BroadcastQueue.java +++ b/services/core/java/com/android/server/am/BroadcastQueue.java @@ -1195,6 +1195,8 @@ public final class BroadcastQueue { && r.intent.getPackage() == null && ((r.intent.getFlags() & Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND) == 0))) { + mService.addBackgroundCheckViolationLocked(r.intent.getAction(), + component.getPackageName()); Slog.w(TAG, "Background execution not allowed: receiving " + r.intent + " to " + component.flattenToShortString()); diff --git a/services/core/java/com/android/server/am/BroadcastStats.java b/services/core/java/com/android/server/am/BroadcastStats.java index fdbaada49091..fd2458222a89 100644 --- a/services/core/java/com/android/server/am/BroadcastStats.java +++ b/services/core/java/com/android/server/am/BroadcastStats.java @@ -47,6 +47,7 @@ public final class BroadcastStats { static final class ActionEntry { final String mAction; final ArrayMap<String, PackageEntry> mPackages = new ArrayMap<>(); + final ArrayMap<String, ViolationEntry> mBackgroundCheckViolations = new ArrayMap<>(); int mReceiveCount; int mSkipCount; long mTotalDispatchTime; @@ -61,6 +62,10 @@ public final class BroadcastStats { int mSendCount; } + static final class ViolationEntry { + int mCount; + } + public BroadcastStats() { mStartRealtime = SystemClock.elapsedRealtime(); mStartUptime = SystemClock.uptimeMillis(); @@ -87,6 +92,20 @@ public final class BroadcastStats { pe.mSendCount++; } + public void addBackgroundCheckViolation(String action, String targetPackage) { + ActionEntry ae = mActions.get(action); + if (ae == null) { + ae = new ActionEntry(action); + mActions.put(action, ae); + } + ViolationEntry ve = ae.mBackgroundCheckViolations.get(targetPackage); + if (ve == null) { + ve = new ViolationEntry(); + ae.mBackgroundCheckViolations.put(targetPackage, ve); + } + ve.mCount++; + } + public boolean dumpStats(PrintWriter pw, String prefix, String dumpPackage) { boolean printedSomething = false; ArrayList<ActionEntry> actions = new ArrayList<>(mActions.size()); @@ -123,6 +142,15 @@ public final class BroadcastStats { pw.print(pe.mSendCount); pw.println(" times"); } + for (int j=ae.mBackgroundCheckViolations.size()-1; j>=0; j--) { + pw.print(prefix); + pw.print(" Bg Check Violation "); + pw.print(ae.mBackgroundCheckViolations.keyAt(j)); + pw.print(": "); + ViolationEntry ve = ae.mBackgroundCheckViolations.valueAt(j); + pw.print(ve.mCount); + pw.println(" times"); + } } return printedSomething; } @@ -158,6 +186,14 @@ public final class BroadcastStats { pw.print(pe.mSendCount); pw.println(); } + for (int j=ae.mBackgroundCheckViolations.size()-1; j>=0; j--) { + pw.print("v,"); + pw.print(ae.mBackgroundCheckViolations.keyAt(j)); + ViolationEntry ve = ae.mBackgroundCheckViolations.valueAt(j); + pw.print(","); + pw.print(ve.mCount); + pw.println(); + } } } } diff --git a/services/core/java/com/android/server/am/TaskChangeNotificationController.java b/services/core/java/com/android/server/am/TaskChangeNotificationController.java index 9dfc7cd84f40..9a98bc6ec6f8 100644 --- a/services/core/java/com/android/server/am/TaskChangeNotificationController.java +++ b/services/core/java/com/android/server/am/TaskChangeNotificationController.java @@ -46,6 +46,7 @@ class TaskChangeNotificationController { static final int NOTIFY_TASK_REMOVAL_STARTED_LISTENERS = 13; static final int NOTIFY_TASK_PROFILE_LOCKED_LISTENERS_MSG = 14; static final int NOTIFY_TASK_SNAPSHOT_CHANGED_LISTENERS_MSG = 15; + static final int NOTIFY_PINNED_STACK_ANIMATION_STARTED_LISTENERS_MSG = 16; // Delay in notifying task stack change listeners (in millis) static final int NOTIFY_TASK_STACK_CHANGE_LISTENERS_DELAY = 100; @@ -100,6 +101,10 @@ class TaskChangeNotificationController { l.onPinnedActivityRestartAttempt((String) m.obj); }; + private final TaskStackConsumer mNotifyPinnedStackAnimationStarted = (l, m) -> { + l.onPinnedStackAnimationStarted(); + }; + private final TaskStackConsumer mNotifyPinnedStackAnimationEnded = (l, m) -> { l.onPinnedStackAnimationEnded(); }; @@ -166,6 +171,9 @@ class TaskChangeNotificationController { case NOTIFY_PINNED_ACTIVITY_RESTART_ATTEMPT_LISTENERS_MSG: forAllRemoteListeners(mNotifyPinnedActivityRestartAttempt, msg); break; + case NOTIFY_PINNED_STACK_ANIMATION_STARTED_LISTENERS_MSG: + forAllRemoteListeners(mNotifyPinnedStackAnimationStarted, msg); + break; case NOTIFY_PINNED_STACK_ANIMATION_ENDED_LISTENERS_MSG: forAllRemoteListeners(mNotifyPinnedStackAnimationEnded, msg); break; @@ -276,6 +284,15 @@ class TaskChangeNotificationController { msg.sendToTarget(); } + /** Notifies all listeners when the pinned stack animation starts. */ + void notifyPinnedStackAnimationStarted() { + mHandler.removeMessages(NOTIFY_PINNED_STACK_ANIMATION_STARTED_LISTENERS_MSG); + final Message msg = + mHandler.obtainMessage(NOTIFY_PINNED_STACK_ANIMATION_STARTED_LISTENERS_MSG); + forAllLocalListeners(mNotifyPinnedStackAnimationStarted, msg); + msg.sendToTarget(); + } + /** Notifies all listeners when the pinned stack animation ends. */ void notifyPinnedStackAnimationEnded() { mHandler.removeMessages(NOTIFY_PINNED_STACK_ANIMATION_ENDED_LISTENERS_MSG); diff --git a/services/core/java/com/android/server/notification/RankingHelper.java b/services/core/java/com/android/server/notification/RankingHelper.java index e72f7ffea8e3..476eb1060698 100644 --- a/services/core/java/com/android/server/notification/RankingHelper.java +++ b/services/core/java/com/android/server/notification/RankingHelper.java @@ -192,9 +192,14 @@ public class RankingHelper implements RankingConfig { if (TAG_GROUP.equals(tagName)) { String id = parser.getAttributeValue(null, ATT_ID); CharSequence groupName = parser.getAttributeValue(null, ATT_NAME); + int groupNameRes = safeInt(parser, ATT_NAME_RES_ID, 0); if (!TextUtils.isEmpty(id)) { - final NotificationChannelGroup group = - new NotificationChannelGroup(id, groupName); + NotificationChannelGroup group = null; + if (groupName != null) { + group = new NotificationChannelGroup(id, groupName); + } else { + group = new NotificationChannelGroup(id, groupNameRes); + } r.groups.put(id, group); } } @@ -202,7 +207,7 @@ public class RankingHelper implements RankingConfig { if (TAG_CHANNEL.equals(tagName)) { String id = parser.getAttributeValue(null, ATT_ID); CharSequence channelName = parser.getAttributeValue(null, ATT_NAME); - int channelNameRes = safeInt(parser, ATT_NAME_RES_ID, -1); + int channelNameRes = safeInt(parser, ATT_NAME_RES_ID, 0); int channelImportance = safeInt(parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE); @@ -473,7 +478,8 @@ public class RankingHelper implements RankingConfig { Preconditions.checkNotNull(pkg); Preconditions.checkNotNull(group); Preconditions.checkNotNull(group.getId()); - Preconditions.checkNotNull(group.getName()); + Preconditions.checkNotNull(!TextUtils.isEmpty(group.getName()) + || group.getNameResId() != 0); Record r = getOrCreateRecord(pkg, uid); if (r == null) { throw new IllegalArgumentException("Invalid package"); diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java index ba4d46aa360b..a692559129ad 100644 --- a/services/core/java/com/android/server/om/OverlayManagerService.java +++ b/services/core/java/com/android/server/om/OverlayManagerService.java @@ -44,8 +44,11 @@ import android.os.IBinder; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ShellCallback; +import android.os.SystemProperties; import android.os.UserHandle; +import android.text.TextUtils; import android.util.ArrayMap; +import android.util.ArraySet; import android.util.AtomicFile; import android.util.Slog; import android.util.SparseArray; @@ -70,6 +73,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -193,6 +197,14 @@ public final class OverlayManagerService extends SystemService { static final String PERMISSION_DENIED = "Operation not permitted for user shell"; + /** + * The system property that specifies the default overlays to apply. + * This is a semicolon separated list of package names. + * + * Ex: com.android.vendor.overlay_one;com.android.vendor.overlay_two + */ + private static final String DEFAULT_OVERLAYS_PROP = "ro.boot.vendor.overlay.theme"; + private final Object mLock = new Object(); private final AtomicFile mSettingsFile; @@ -216,7 +228,8 @@ public final class OverlayManagerService extends SystemService { mUserManager = UserManagerService.getInstance(); IdmapManager im = new IdmapManager(installer); mSettings = new OverlayManagerSettings(); - mImpl = new OverlayManagerServiceImpl(mPackageManager, im, mSettings); + mImpl = new OverlayManagerServiceImpl(mPackageManager, im, mSettings, + getDefaultOverlayPackages()); final IntentFilter packageFilter = new IntentFilter(); packageFilter.addAction(ACTION_PACKAGE_ADDED); @@ -257,6 +270,21 @@ public final class OverlayManagerService extends SystemService { updateAssets(newUserId, targets); } + private static Set<String> getDefaultOverlayPackages() { + final String str = SystemProperties.get(DEFAULT_OVERLAYS_PROP); + if (TextUtils.isEmpty(str)) { + return Collections.emptySet(); + } + + final ArraySet<String> defaultPackages = new ArraySet<>(); + for (String packageName : str.split(";")) { + if (!TextUtils.isEmpty(packageName)) { + defaultPackages.add(packageName); + } + } + return defaultPackages; + } + private final class PackageReceiver extends BroadcastReceiver { @Override public void onReceive(@NonNull final Context context, @NonNull final Intent intent) { diff --git a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java index 0e33409cd0ea..ed493833139a 100644 --- a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java +++ b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java @@ -27,8 +27,8 @@ import static com.android.server.om.OverlayManagerService.TAG; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.om.OverlayInfo; -import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; +import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Slog; @@ -53,13 +53,16 @@ final class OverlayManagerServiceImpl { private final PackageManagerHelper mPackageManager; private final IdmapManager mIdmapManager; private final OverlayManagerSettings mSettings; + private final Set<String> mDefaultOverlays; OverlayManagerServiceImpl(@NonNull final PackageManagerHelper packageManager, @NonNull final IdmapManager idmapManager, - @NonNull final OverlayManagerSettings settings) { + @NonNull final OverlayManagerSettings settings, + @NonNull final Set<String> defaultOverlays) { mPackageManager = packageManager; mIdmapManager = idmapManager; mSettings = settings; + mDefaultOverlays = defaultOverlays; } /* @@ -92,12 +95,22 @@ final class OverlayManagerServiceImpl { final PackageInfo overlayPackage = overlayPackages.get(i); final OverlayInfo oi = storedOverlayInfos.get(overlayPackage.packageName); if (oi == null || !oi.targetPackageName.equals(overlayPackage.overlayTarget)) { - if (oi != null) { - packagesToUpdateAssets.add(oi.targetPackageName); - } + // Update the overlay if it didn't exist or had the wrong target package. mSettings.init(overlayPackage.packageName, newUserId, overlayPackage.overlayTarget, overlayPackage.applicationInfo.getBaseCodePath()); + + if (oi == null) { + // This overlay does not exist in our settings. + if (mDefaultOverlays.contains(overlayPackage.packageName)) { + // Enable this overlay by default. + mSettings.setEnabled(overlayPackage.packageName, newUserId, true); + } + } else { + // The targetPackageName we have stored doesn't match the overlay's target. + // Queue the old target for an update as well. + packagesToUpdateAssets.add(oi.targetPackageName); + } } try { @@ -132,7 +145,7 @@ final class OverlayManagerServiceImpl { } } - return new ArrayList<String>(packagesToUpdateAssets); + return new ArrayList<>(packagesToUpdateAssets); } void onUserRemoved(final int userId) { @@ -303,6 +316,7 @@ final class OverlayManagerServiceImpl { void onDump(@NonNull final PrintWriter pw) { mSettings.dump(pw); + pw.println("Default overlays: " + TextUtils.join(";", mDefaultOverlays)); } List<String> getEnabledOverlayPackageNames(@NonNull final String targetPackageName, diff --git a/services/core/java/com/android/server/om/OverlayManagerSettings.java b/services/core/java/com/android/server/om/OverlayManagerSettings.java index 44908a748b32..ff5c594c3dde 100644 --- a/services/core/java/com/android/server/om/OverlayManagerSettings.java +++ b/services/core/java/com/android/server/om/OverlayManagerSettings.java @@ -45,7 +45,6 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.ListIterator; -import java.util.Map; /** * Data structure representing the current state of all overlay packages in the diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index caacc46a8ebd..1fe4ccca765e 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -496,18 +496,6 @@ public class PackageManagerService extends IPackageManager.Stub { private static final String PACKAGE_SCHEME = "package"; private static final String VENDOR_OVERLAY_DIR = "/vendor/overlay"; - /** - * If VENDOR_OVERLAY_THEME_PROPERTY is set, search for runtime resource overlay APKs also in - * VENDOR_OVERLAY_DIR/<value of VENDOR_OVERLAY_THEME_PROPERTY> in addition to - * VENDOR_OVERLAY_DIR. - */ - private static final String VENDOR_OVERLAY_THEME_PROPERTY = "ro.boot.vendor.overlay.theme"; - /** - * Same as VENDOR_OVERLAY_THEME_PROPERTY, except persistent. If set will override whatever - * is in VENDOR_OVERLAY_THEME_PROPERTY. - */ - private static final String VENDOR_OVERLAY_THEME_PERSIST_PROPERTY - = "persist.vendor.overlay.theme"; /** Permission grant: not grant the permission. */ private static final int GRANT_DENIED = 1; @@ -2476,16 +2464,6 @@ public class PackageManagerService extends IPackageManager.Stub { // Collect vendor overlay packages. (Do this before scanning any apps.) // For security and version matching reason, only consider // overlay packages if they reside in the right directory. - String overlayThemeDir = SystemProperties.get(VENDOR_OVERLAY_THEME_PERSIST_PROPERTY); - if (overlayThemeDir.isEmpty()) { - overlayThemeDir = SystemProperties.get(VENDOR_OVERLAY_THEME_PROPERTY); - } - if (!overlayThemeDir.isEmpty()) { - scanDirTracedLI(new File(VENDOR_OVERLAY_DIR, overlayThemeDir), mDefParseFlags - | PackageParser.PARSE_IS_SYSTEM - | PackageParser.PARSE_IS_SYSTEM_DIR - | PackageParser.PARSE_TRUSTED_OVERLAY, scanFlags | SCAN_TRUSTED_OVERLAY, 0); - } scanDirTracedLI(new File(VENDOR_OVERLAY_DIR), mDefParseFlags | PackageParser.PARSE_IS_SYSTEM | PackageParser.PARSE_IS_SYSTEM_DIR @@ -3415,6 +3393,8 @@ public class PackageManagerService extends IPackageManager.Stub { return null; } + rebaseEnabledOverlays(packageInfo.applicationInfo, userId); + packageInfo.packageName = packageInfo.applicationInfo.packageName = resolveExternalPackageNameLPr(p); @@ -4147,8 +4127,12 @@ public class PackageManagerService extends IPackageManager.Stub { if (a != null && mSettings.isEnabledAndMatchLPr(a.info, flags, userId)) { PackageSetting ps = mSettings.mPackages.get(component.getPackageName()); if (ps == null) return null; - return PackageParser.generateActivityInfo(a, flags, ps.readUserState(userId), - userId); + ActivityInfo ri = PackageParser.generateActivityInfo(a, flags, + ps.readUserState(userId), userId); + if (ri != null) { + rebaseEnabledOverlays(ri.applicationInfo, userId); + } + return ri; } } return null; @@ -4274,8 +4258,12 @@ public class PackageManagerService extends IPackageManager.Stub { if (s != null && mSettings.isEnabledAndMatchLPr(s.info, flags, userId)) { PackageSetting ps = mSettings.mPackages.get(component.getPackageName()); if (ps == null) return null; - return PackageParser.generateServiceInfo(s, flags, ps.readUserState(userId), - userId); + ServiceInfo si = PackageParser.generateServiceInfo(s, flags, + ps.readUserState(userId), userId); + if (si != null) { + rebaseEnabledOverlays(si.applicationInfo, userId); + } + return si; } } return null; @@ -4294,8 +4282,12 @@ public class PackageManagerService extends IPackageManager.Stub { if (p != null && mSettings.isEnabledAndMatchLPr(p.info, flags, userId)) { PackageSetting ps = mSettings.mPackages.get(component.getPackageName()); if (ps == null) return null; - return PackageParser.generateProviderInfo(p, flags, ps.readUserState(userId), - userId); + ProviderInfo pi = PackageParser.generateProviderInfo(p, flags, + ps.readUserState(userId), userId); + if (pi != null) { + rebaseEnabledOverlays(pi.applicationInfo, userId); + } + return pi; } } return null; @@ -15919,14 +15911,6 @@ public class PackageManagerService extends IPackageManager.Stub { final PackageSetting ps = mSettings.mPackages.get(pkgName); - // don't allow an upgrade from full to ephemeral - if (isInstantApp && !ps.getInstantApp(user.getIdentifier())) { - // can't downgrade from full to instant - Slog.w(TAG, "Can't replace app with instant app: " + pkgName); - res.setReturnCode(PackageManager.INSTALL_FAILED_INSTANT_APP_INVALID); - return; - } - // verify signatures are valid if (shouldCheckUpgradeKeySetLP(ps, scanFlags)) { if (!checkUpgradeKeySetLP(ps, pkg)) { @@ -15984,6 +15968,27 @@ public class PackageManagerService extends IPackageManager.Stub { // In case of rollback, remember per-user/profile install state allUsers = sUserManager.getUserIds(); installedUsers = ps.queryInstalledUsers(allUsers, true); + + // don't allow an upgrade from full to ephemeral + if (isInstantApp) { + if (user == null || user.getIdentifier() == UserHandle.USER_ALL) { + for (int currentUser : allUsers) { + if (!ps.getInstantApp(currentUser)) { + // can't downgrade from full to instant + Slog.w(TAG, "Can't replace full app with instant app: " + pkgName + + " for user: " + currentUser); + res.setReturnCode(PackageManager.INSTALL_FAILED_INSTANT_APP_INVALID); + return; + } + } + } else if (!ps.getInstantApp(user.getIdentifier())) { + // can't downgrade from full to instant + Slog.w(TAG, "Can't replace full app with instant app: " + pkgName + + " for user: " + user.getIdentifier()); + res.setReturnCode(PackageManager.INSTALL_FAILED_INSTANT_APP_INVALID); + return; + } + } } // Update what is removed @@ -23207,7 +23212,7 @@ Slog.v(TAG, ":: stepped forward, applying functor at tag " + parser.getName()); ArrayList<String> paths = null; if (overlayPackageNames != null) { final int N = overlayPackageNames.size(); - paths = new ArrayList<String>(N); + paths = new ArrayList<>(N); for (int i = 0; i < N; i++) { final String packageName = overlayPackageNames.get(i); final PackageParser.Package pkg = mPackages.get(packageName); @@ -23222,7 +23227,7 @@ Slog.v(TAG, ":: stepped forward, applying functor at tag " + parser.getName()); ArrayMap<String, ArrayList<String>> userSpecificOverlays = mEnabledOverlayPaths.get(userId); if (userSpecificOverlays == null) { - userSpecificOverlays = new ArrayMap<String, ArrayList<String>>(); + userSpecificOverlays = new ArrayMap<>(); mEnabledOverlayPaths.put(userId, userSpecificOverlays); } diff --git a/services/core/java/com/android/server/storage/CacheQuotaStrategy.java b/services/core/java/com/android/server/storage/CacheQuotaStrategy.java index 10d30aae86ce..c06439278954 100644 --- a/services/core/java/com/android/server/storage/CacheQuotaStrategy.java +++ b/services/core/java/com/android/server/storage/CacheQuotaStrategy.java @@ -34,21 +34,37 @@ import android.content.pm.ServiceInfo; import android.content.pm.UserInfo; import android.os.AsyncTask; import android.os.Bundle; +import android.os.Environment; import android.os.IBinder; import android.os.RemoteCallback; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.text.format.DateUtils; +import android.util.Pair; import android.util.Slog; +import android.util.Xml; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.AtomicFile; +import com.android.internal.util.FastXmlSerializer; import com.android.internal.util.Preconditions; import com.android.server.pm.Installer; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; - /** * CacheQuotaStrategy is a strategy for determining cache quotas using usage stats and foreground * time using the calculation as defined in the refuel rocket. @@ -58,17 +74,28 @@ public class CacheQuotaStrategy implements RemoteCallback.OnResultListener { private final Object mLock = new Object(); + // XML Constants + private static final String CACHE_INFO_TAG = "cache-info"; + private static final String ATTR_PREVIOUS_BYTES = "previousBytes"; + private static final String TAG_QUOTA = "quota"; + private static final String ATTR_UUID = "uuid"; + private static final String ATTR_UID = "uid"; + private static final String ATTR_QUOTA_IN_BYTES = "bytes"; + private final Context mContext; private final UsageStatsManagerInternal mUsageStats; private final Installer mInstaller; private ServiceConnection mServiceConnection; private ICacheQuotaService mRemoteService; + private AtomicFile mPreviousValuesFile; public CacheQuotaStrategy( Context context, UsageStatsManagerInternal usageStatsManager, Installer installer) { mContext = Preconditions.checkNotNull(context); mUsageStats = Preconditions.checkNotNull(usageStatsManager); mInstaller = Preconditions.checkNotNull(installer); + mPreviousValuesFile = new AtomicFile(new File( + new File(Environment.getDataDirectory(), "system"), "cachequota.xml")); } /** @@ -128,7 +155,7 @@ public class CacheQuotaStrategy implements RemoteCallback.OnResultListener { } /** - * Returns a list of CacheQuotaRequests which do not have their quotas filled out for apps + * Returns a list of CacheQuotaHints which do not have their quotas filled out for apps * which have been used in the last year. */ private List<CacheQuotaHint> getUnfulfilledRequests() { @@ -176,6 +203,11 @@ public class CacheQuotaStrategy implements RemoteCallback.OnResultListener { final List<CacheQuotaHint> processedRequests = data.getParcelableArrayList( CacheQuotaService.REQUEST_LIST_KEY); + pushProcessedQuotas(processedRequests); + writeXmlToFile(processedRequests); + } + + private void pushProcessedQuotas(List<CacheQuotaHint> processedRequests) { final int requestSize = processedRequests.size(); for (int i = 0; i < requestSize; i++) { CacheQuotaHint request = processedRequests.get(i); @@ -200,8 +232,10 @@ public class CacheQuotaStrategy implements RemoteCallback.OnResultListener { } private void disconnectService() { - mContext.unbindService(mServiceConnection); - mServiceConnection = null; + if (mServiceConnection != null) { + mContext.unbindService(mServiceConnection); + mServiceConnection = null; + } } private ComponentName getServiceComponentName() { @@ -223,4 +257,131 @@ public class CacheQuotaStrategy implements RemoteCallback.OnResultListener { ServiceInfo serviceInfo = resolveInfo.serviceInfo; return new ComponentName(serviceInfo.packageName, serviceInfo.name); } + + private void writeXmlToFile(List<CacheQuotaHint> processedRequests) { + FileOutputStream fileStream = null; + try { + XmlSerializer out = new FastXmlSerializer(); + fileStream = mPreviousValuesFile.startWrite(); + out.setOutput(fileStream, StandardCharsets.UTF_8.name()); + saveToXml(out, processedRequests, 0); + mPreviousValuesFile.finishWrite(fileStream); + } catch (Exception e) { + Slog.e(TAG, "An error occurred while writing the cache quota file.", e); + mPreviousValuesFile.failWrite(fileStream); + } + } + + /** + * Initializes the quotas from the file. + * @return the number of bytes that were free on the device when the quotas were last calced. + */ + public long setupQuotasFromFile() throws IOException { + FileInputStream stream; + try { + stream = mPreviousValuesFile.openRead(); + } catch (FileNotFoundException e) { + // The file may not exist yet -- this isn't truly exceptional. + return -1; + } + + Pair<Long, List<CacheQuotaHint>> cachedValues = null; + try { + cachedValues = readFromXml(stream); + } catch (XmlPullParserException e) { + throw new IllegalStateException(e.getMessage()); + } + + if (cachedValues == null) { + Slog.e(TAG, "An error occurred while parsing the cache quota file."); + return -1; + } + pushProcessedQuotas(cachedValues.second); + return cachedValues.first; + } + + @VisibleForTesting + static void saveToXml(XmlSerializer out, + List<CacheQuotaHint> requests, long bytesWhenCalculated) throws IOException { + out.startDocument(null, true); + out.startTag(null, CACHE_INFO_TAG); + int requestSize = requests.size(); + out.attribute(null, ATTR_PREVIOUS_BYTES, Long.toString(bytesWhenCalculated)); + + for (int i = 0; i < requestSize; i++) { + CacheQuotaHint request = requests.get(i); + out.startTag(null, TAG_QUOTA); + String uuid = request.getVolumeUuid(); + if (uuid != null) { + out.attribute(null, ATTR_UUID, request.getVolumeUuid()); + } + out.attribute(null, ATTR_UID, Integer.toString(request.getUid())); + out.attribute(null, ATTR_QUOTA_IN_BYTES, Long.toString(request.getQuota())); + out.endTag(null, TAG_QUOTA); + } + out.endTag(null, CACHE_INFO_TAG); + out.endDocument(); + } + + protected static Pair<Long, List<CacheQuotaHint>> readFromXml(InputStream inputStream) + throws XmlPullParserException, IOException { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(inputStream, StandardCharsets.UTF_8.name()); + + int eventType = parser.getEventType(); + while (eventType != XmlPullParser.START_TAG && + eventType != XmlPullParser.END_DOCUMENT) { + eventType = parser.next(); + } + + if (eventType == XmlPullParser.END_DOCUMENT) { + Slog.d(TAG, "No quotas found in quota file."); + return null; + } + + String tagName = parser.getName(); + if (!CACHE_INFO_TAG.equals(tagName)) { + throw new IllegalStateException("Invalid starting tag."); + } + + final List<CacheQuotaHint> quotas = new ArrayList<>(); + long previousBytes; + try { + previousBytes = Long.parseLong(parser.getAttributeValue( + null, ATTR_PREVIOUS_BYTES)); + } catch (NumberFormatException e) { + throw new IllegalStateException( + "Previous bytes formatted incorrectly; aborting quota read."); + } + + eventType = parser.next(); + do { + if (eventType == XmlPullParser.START_TAG) { + tagName = parser.getName(); + if (TAG_QUOTA.equals(tagName)) { + CacheQuotaHint request = getRequestFromXml(parser); + if (request == null) { + continue; + } + quotas.add(request); + } + } + eventType = parser.next(); + } while (eventType != XmlPullParser.END_DOCUMENT); + return new Pair<>(previousBytes, quotas); + } + + @VisibleForTesting + static CacheQuotaHint getRequestFromXml(XmlPullParser parser) { + try { + String uuid = parser.getAttributeValue(null, ATTR_UUID); + int uid = Integer.parseInt(parser.getAttributeValue(null, ATTR_UID)); + long bytes = Long.parseLong(parser.getAttributeValue(null, ATTR_QUOTA_IN_BYTES)); + return new CacheQuotaHint.Builder() + .setVolumeUuid(uuid).setUid(uid).setQuota(bytes).build(); + } catch (NumberFormatException e) { + Slog.e(TAG, "Invalid cache quota request, skipping."); + return null; + } + } } diff --git a/services/core/java/com/android/server/wm/TaskStack.java b/services/core/java/com/android/server/wm/TaskStack.java index 6cc2efb78526..7588b71dced7 100644 --- a/services/core/java/com/android/server/wm/TaskStack.java +++ b/services/core/java/com/android/server/wm/TaskStack.java @@ -1439,6 +1439,14 @@ public class TaskStack extends WindowContainer<Task> implements DimLayer.DimLaye mBoundsAnimating = true; mBoundsAnimatingToFullscreen = toFullscreen; } + + if (mStackId == PINNED_STACK_ID) { + try { + mService.mActivityManager.notifyPinnedStackAnimationStarted(); + } catch (RemoteException e) { + // I don't believe you... + } + } } @Override // AnimatesBounds @@ -1448,6 +1456,7 @@ public class TaskStack extends WindowContainer<Task> implements DimLayer.DimLaye mBoundsAnimationTarget.setEmpty(); mService.requestTraversal(); } + if (mStackId == PINNED_STACK_ID) { try { mService.mActivityManager.notifyPinnedStackAnimationEnded(); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 8b94ca067621..9c3ecd094b8a 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -99,6 +99,7 @@ import android.content.pm.PackageManagerInternal; import android.content.pm.ParceledListSlice; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; +import android.content.pm.StringParceledListSlice; import android.content.pm.UserInfo; import android.content.res.Resources; import android.database.ContentObserver; @@ -166,6 +167,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.internal.notification.SystemNotificationChannels; +import com.android.internal.os.BackgroundThread; import com.android.internal.statusbar.IStatusBarService; import com.android.internal.util.FastXmlSerializer; import com.android.internal.util.JournaledFile; @@ -243,10 +245,14 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { private static final String TAG_DEFAULT_INPUT_METHOD_SET = "default-ime-set"; + private static final String TAG_OWNER_INSTALLED_CA_CERT = "owner-installed-ca-cert"; + private static final String ATTR_ID = "id"; private static final String ATTR_VALUE = "value"; + private static final String ATTR_ALIAS = "alias"; + private static final String TAG_INITIALIZATION_BUNDLE = "initialization-bundle"; private static final String TAG_PASSWORD_TOKEN_HANDLE = "password-token"; @@ -480,6 +486,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { final ArrayList<ActiveAdmin> mAdminList = new ArrayList<>(); final ArrayList<ComponentName> mRemovingAdmins = new ArrayList<>(); + // TODO(b/35385311): Keep track of metadata in TrustedCertificateStore instead. final ArraySet<String> mAcceptedCaCertificates = new ArraySet<>(); // This is the list of component allowed to start lock task mode. @@ -504,6 +511,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { boolean mDefaultInputMethodSet = false; + // TODO(b/35385311): Keep track of metadata in TrustedCertificateStore instead. + Set<String> mOwnerInstalledCaCerts = new ArraySet<>(); + // Used for initialization of users created by createAndManageUsers. boolean mAdminBroadcastPending = false; PersistableBundle mInitBundle = null; @@ -518,6 +528,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { final SparseArray<DevicePolicyData> mUserData = new SparseArray<>(); final Handler mHandler; + final Handler mBackgroundHandler; /** Listens on any device, even when mHasFeature == false. */ final BroadcastReceiver mRootCaReceiver = new BroadcastReceiver() { @@ -619,6 +630,25 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { handlePackagesChanged(intent.getData().getSchemeSpecificPart(), userHandle); } else if (Intent.ACTION_MANAGED_PROFILE_ADDED.equals(action)) { clearWipeProfileNotification(); + } else if (KeyChain.ACTION_TRUST_STORE_CHANGED.equals(intent.getAction())) { + mBackgroundHandler.post(() -> { + try (final KeyChainConnection keyChainConnection = mInjector.keyChainBindAsUser( + UserHandle.of(userHandle))) { + final List<String> caCerts = + keyChainConnection.getService().getUserCaAliases().getList(); + synchronized (DevicePolicyManagerService.this) { + if (getUserData(userHandle).mOwnerInstalledCaCerts + .retainAll(caCerts)) { + saveSettingsLocked(userHandle); + } + } + } catch (InterruptedException e) { + Slog.w(LOG_TAG, "error talking to IKeyChainService", e); + Thread.currentThread().interrupt(); + } catch (RemoteException e) { + Slog.w(LOG_TAG, "error talking to IKeyChainService", e); + } + }); } } @@ -1786,6 +1816,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { .hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN); mIsWatch = mInjector.getPackageManager() .hasSystemFeature(PackageManager.FEATURE_WATCH); + mBackgroundHandler = BackgroundThread.getHandler(); // Broadcast filter for changes to the trusted certificate store. These changes get a // separate intent filter so we can listen to them even when device_admin is off. @@ -1807,6 +1838,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { filter.addAction(Intent.ACTION_USER_ADDED); filter.addAction(Intent.ACTION_USER_REMOVED); filter.addAction(Intent.ACTION_USER_STARTED); + filter.addAction(KeyChain.ACTION_TRUST_STORE_CHANGED); filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, filter, null, mHandler); filter = new IntentFilter(); @@ -2580,6 +2612,12 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { out.endTag(null, TAG_DEFAULT_INPUT_METHOD_SET); } + for (final String cert : policy.mOwnerInstalledCaCerts) { + out.startTag(null, TAG_OWNER_INSTALLED_CA_CERT); + out.attribute(null, ATTR_ALIAS, cert); + out.endTag(null, TAG_OWNER_INSTALLED_CA_CERT); + } + out.endTag(null, "policies"); out.endDocument(); @@ -2696,6 +2734,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { policy.mAdminList.clear(); policy.mAdminMap.clear(); policy.mAffiliationIds.clear(); + policy.mOwnerInstalledCaCerts.clear(); while ((type=parser.next()) != XmlPullParser.END_DOCUMENT && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { @@ -2791,6 +2830,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { parser.getAttributeValue(null, ATTR_VALUE)); } else if (TAG_DEFAULT_INPUT_METHOD_SET.equals(tag)) { policy.mDefaultInputMethodSet = true; + } else if (TAG_OWNER_INSTALLED_CA_CERT.equals(tag)) { + policy.mOwnerInstalledCaCerts.add(parser.getAttributeValue(null, ATTR_ALIAS)); } else { Slog.w(LOG_TAG, "Unknown tag: " + tag); XmlUtils.skipCurrentTag(parser); @@ -4691,17 +4732,15 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return false; } - final UserHandle userHandle = new UserHandle(UserHandle.getCallingUserId()); + final UserHandle userHandle = UserHandle.of(mInjector.userHandleGetCallingUserId()); final long id = mInjector.binderClearCallingIdentity(); + String alias = null; try { - final KeyChainConnection keyChainConnection = KeyChain.bindAsUser(mContext, userHandle); - try { - keyChainConnection.getService().installCaCertificate(pemCert); - return true; + try (final KeyChainConnection keyChainConnection = mInjector.keyChainBindAsUser( + userHandle)) { + alias = keyChainConnection.getService().installCaCertificate(pemCert); } catch (RemoteException e) { Log.e(LOG_TAG, "installCaCertsToKeyChain(): ", e); - } finally { - keyChainConnection.close(); } } catch (InterruptedException e1) { Log.w(LOG_TAG, "installCaCertsToKeyChain(): ", e1); @@ -4709,6 +4748,16 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } finally { mInjector.binderRestoreCallingIdentity(id); } + if (alias == null) { + Log.w(LOG_TAG, "Problem installing cert"); + } else { + synchronized (this) { + final int userId = userHandle.getIdentifier(); + getUserData(userId).mOwnerInstalledCaCerts.add(alias); + saveSettingsLocked(userId); + } + return true; + } return false; } @@ -4722,25 +4771,31 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { public void uninstallCaCerts(ComponentName admin, String callerPackage, String[] aliases) { enforceCanManageCaCerts(admin, callerPackage); - final UserHandle userHandle = new UserHandle(UserHandle.getCallingUserId()); + final int userId = mInjector.userHandleGetCallingUserId(); + final UserHandle userHandle = UserHandle.of(userId); final long id = mInjector.binderClearCallingIdentity(); try { - final KeyChainConnection keyChainConnection = KeyChain.bindAsUser(mContext, userHandle); - try { + try (final KeyChainConnection keyChainConnection = mInjector.keyChainBindAsUser( + userHandle)) { for (int i = 0 ; i < aliases.length; i++) { keyChainConnection.getService().deleteCaCertificate(aliases[i]); } } catch (RemoteException e) { Log.e(LOG_TAG, "from CaCertUninstaller: ", e); - } finally { - keyChainConnection.close(); + return; } } catch (InterruptedException ie) { Log.w(LOG_TAG, "CaCertUninstaller: ", ie); Thread.currentThread().interrupt(); + return; } finally { mInjector.binderRestoreCallingIdentity(id); } + synchronized (this) { + if (getUserData(userId).mOwnerInstalledCaCerts.removeAll(Arrays.asList(aliases))) { + saveSettingsLocked(userId); + } + } } @Override @@ -6731,6 +6786,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } final DevicePolicyData policyData = getUserData(userId); policyData.mDefaultInputMethodSet = false; + policyData.mOwnerInstalledCaCerts.clear(); saveSettingsLocked(userId); clearUserPoliciesLocked(userId); mOwners.removeProfileOwner(userId); @@ -7127,27 +7183,30 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } private void enforceFullCrossUsersPermission(int userHandle) { - enforceSystemUserOrPermission(userHandle, + enforceSystemUserOrPermissionIfCrossUser(userHandle, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL); } private void enforceCrossUsersPermission(int userHandle) { - enforceSystemUserOrPermission(userHandle, + enforceSystemUserOrPermissionIfCrossUser(userHandle, android.Manifest.permission.INTERACT_ACROSS_USERS); } - private void enforceSystemUserOrPermission(int userHandle, String permission) { + private void enforceSystemUserOrPermission(String permission) { + if (!(isCallerWithSystemUid() || mInjector.binderGetCallingUid() == Process.ROOT_UID)) { + mContext.enforceCallingOrSelfPermission(permission, + "Must be system or have " + permission + " permission"); + } + } + + private void enforceSystemUserOrPermissionIfCrossUser(int userHandle, String permission) { if (userHandle < 0) { throw new IllegalArgumentException("Invalid userId " + userHandle); } - final int callingUid = mInjector.binderGetCallingUid(); - if (userHandle == UserHandle.getUserId(callingUid)) { + if (userHandle == mInjector.userHandleGetCallingUserId()) { return; } - if (!(isCallerWithSystemUid() || callingUid == Process.ROOT_UID)) { - mContext.enforceCallingOrSelfPermission(permission, - "Must be system or have " + permission + " permission"); - } + enforceSystemUserOrPermission(permission); } private void enforceManagedProfile(int userHandle, String message) { @@ -7184,6 +7243,21 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { "Only profile owner, device owner and system may call this method."); } + private void enforceProfileOwnerOrFullCrossUsersPermission(int userId) { + if (userId == mInjector.userHandleGetCallingUserId()) { + synchronized (this) { + if (getActiveAdminWithPolicyForUidLocked(null, + DeviceAdminInfo.USES_POLICY_PROFILE_OWNER, mInjector.binderGetCallingUid()) + != null) { + // Device Owner/Profile Owner may access the user it runs on. + return; + } + } + } + // Otherwise, INTERACT_ACROSS_USERS_FULL permission, system UID or root UID is required. + enforceSystemUserOrPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL); + } + private void ensureCallerPackage(@Nullable String packageName) { if (packageName == null) { Preconditions.checkState(isCallerWithSystemUid(), @@ -10897,4 +10971,14 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } return getUserData(userId).mDefaultInputMethodSet; } + + @Override + public StringParceledListSlice getOwnerInstalledCaCerts(@NonNull UserHandle user) { + final int userId = user.getIdentifier(); + enforceProfileOwnerOrFullCrossUsersPermission(userId); + synchronized (this) { + return new StringParceledListSlice( + new ArrayList<>(getUserData(userId).mOwnerInstalledCaCerts)); + } + } } diff --git a/services/tests/notification/src/com/android/server/notification/RankingHelperTest.java b/services/tests/notification/src/com/android/server/notification/RankingHelperTest.java index ffb0a9e75d8a..25c29ee9566f 100644 --- a/services/tests/notification/src/com/android/server/notification/RankingHelperTest.java +++ b/services/tests/notification/src/com/android/server/notification/RankingHelperTest.java @@ -209,6 +209,12 @@ public class RankingHelperTest { assertEquals(expected.getLightColor(), actual.getLightColor()); } + private void compareGroups(NotificationChannelGroup expected, NotificationChannelGroup actual) { + assertEquals(expected.getId(), actual.getId()); + assertEquals(expected.getName(), actual.getName()); + assertEquals(expected.getNameResId(), actual.getNameResId()); + } + @Test public void testFindAfterRankingWithASplitGroup() throws Exception { ArrayList<NotificationRecord> notificationList = new ArrayList<NotificationRecord>(3); @@ -262,8 +268,10 @@ public class RankingHelperTest { @Test public void testChannelXml() throws Exception { int nameResId = 924896; + int groupNameResId = 426272; - NotificationChannelGroup ncg = new NotificationChannelGroup("1", "2"); + NotificationChannelGroup ncg = new NotificationChannelGroup("1", groupNameResId); + NotificationChannelGroup ncg2 = new NotificationChannelGroup("2", "hello"); NotificationChannel channel1 = new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); NotificationChannel channel2 = @@ -278,6 +286,7 @@ public class RankingHelperTest { channel2.setLightColor(Color.BLUE); mHelper.createNotificationChannelGroup(pkg, uid, ncg, true); + mHelper.createNotificationChannelGroup(pkg, uid, ncg2, true); mHelper.createNotificationChannel(pkg, uid, channel1, true); mHelper.createNotificationChannel(pkg, uid, channel2, false); @@ -308,7 +317,9 @@ public class RankingHelperTest { for (NotificationChannelGroup actual : actualGroups) { if (ncg.getId().equals(actual.getId())) { foundNcg = true; - break; + compareGroups(ncg, actual); + } else if (ncg2.getId().equals(actual.getId())) { + compareGroups(ncg2, actual); } } assertTrue(foundNcg); diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java index 756514bbecc4..f797f3114565 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java @@ -49,6 +49,8 @@ import android.os.UserHandle; import android.os.UserManager; import android.os.UserManagerInternal; import android.provider.Settings; +import android.security.IKeyChainService; +import android.security.KeyChain; import android.telephony.TelephonyManager; import android.test.MoreAsserts; import android.test.suitebuilder.annotation.SmallTest; @@ -122,6 +124,34 @@ public class DevicePolicyManagerTest extends DpmTestBase { public DevicePolicyManager dpm; public DevicePolicyManagerServiceTestable dpms; + /* + * The CA cert below is the content of cacert.pem as generated by: + * + * openssl req -new -x509 -days 3650 -extensions v3_ca -keyout cakey.pem -out cacert.pem + */ + private static final String TEST_CA = + "-----BEGIN CERTIFICATE-----\n" + + "MIIDXTCCAkWgAwIBAgIJAK9Tl/F9V8kSMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV\n" + + "BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX\n" + + "aWRnaXRzIFB0eSBMdGQwHhcNMTUwMzA2MTczMjExWhcNMjUwMzAzMTczMjExWjBF\n" + + "MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50\n" + + "ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\n" + + "CgKCAQEAvItOutsE75WBTgTyNAHt4JXQ3JoseaGqcC3WQij6vhrleWi5KJ0jh1/M\n" + + "Rpry7Fajtwwb4t8VZa0NuM2h2YALv52w1xivql88zce/HU1y7XzbXhxis9o6SCI+\n" + + "oVQSbPeXRgBPppFzBEh3ZqYTVhAqw451XhwdA4Aqs3wts7ddjwlUzyMdU44osCUg\n" + + "kVg7lfPf9sTm5IoHVcfLSCWH5n6Nr9sH3o2ksyTwxuOAvsN11F/a0mmUoPciYPp+\n" + + "q7DzQzdi7akRG601DZ4YVOwo6UITGvDyuAAdxl5isovUXqe6Jmz2/myTSpAKxGFs\n" + + "jk9oRoG6WXWB1kni490GIPjJ1OceyQIDAQABo1AwTjAdBgNVHQ4EFgQUH1QIlPKL\n" + + "p2OQ/AoLOjKvBW4zK3AwHwYDVR0jBBgwFoAUH1QIlPKLp2OQ/AoLOjKvBW4zK3Aw\n" + + "DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAcMi4voMMJHeQLjtq8Oky\n" + + "Azpyk8moDwgCd4llcGj7izOkIIFqq/lyqKdtykVKUWz2bSHO5cLrtaOCiBWVlaCV\n" + + "DYAnnVLM8aqaA6hJDIfaGs4zmwz0dY8hVMFCuCBiLWuPfiYtbEmjHGSmpQTG6Qxn\n" + + "ZJlaK5CZyt5pgh5EdNdvQmDEbKGmu0wpCq9qjZImwdyAul1t/B0DrsWApZMgZpeI\n" + + "d2od0VBrCICB1K4p+C51D93xyQiva7xQcCne+TAnGNy9+gjQ/MyR8MRpwRLv5ikD\n" + + "u0anJCN8pXo6IMglfMAsoton1J6o5/ae5uhC6caQU8bNUsCK570gpNfjkzo6rbP0\n" + + "wQ==\n" + + "-----END CERTIFICATE-----\n"; + @Override protected void setUp() throws Exception { super.setUp(); @@ -3916,6 +3946,124 @@ public class DevicePolicyManagerTest extends DpmTestBase { assertFalse(dpm.isDefaultInputMethodSetByOwner(secondUser)); } + public void testGetOwnerInstalledCaCertsForDeviceOwner() throws Exception { + setDeviceOwner(); + + mContext.packageName = admin1.getPackageName(); + mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID; + verifyCanGetOwnerInstalledCaCerts(admin1); + } + + public void testGetOwnerInstalledCaCertsForProfileOwner() throws Exception { + setAsProfileOwner(admin1); + + mContext.packageName = admin1.getPackageName(); + verifyCanGetOwnerInstalledCaCerts(admin1); + verifyCantGetOwnerInstalledCaCertsProfileOwnerRemoval(admin1); + } + + public void testGetOwnerInstalledCaCertsForDelegate() throws Exception { + setAsProfileOwner(admin1); + + final String delegate = "com.example.delegate"; + final int delegateUid = setupPackageInPackageManager(delegate, 20988); + dpm.setCertInstallerPackage(admin1, delegate); + + mContext.packageName = delegate; + mContext.binder.callingUid = delegateUid; + verifyCanGetOwnerInstalledCaCerts(null); + verifyCantGetOwnerInstalledCaCertsProfileOwnerRemoval(null); + } + + private void verifyCanGetOwnerInstalledCaCerts(ComponentName caller) throws Exception { + final UserHandle user = UserHandle.getUserHandleForUid(mContext.binder.callingUid); + final int ownerUid = user.equals(UserHandle.SYSTEM) ? + DpmMockContext.CALLER_SYSTEM_USER_UID : DpmMockContext.CALLER_UID; + + mContext.applicationInfo = new ApplicationInfo(); + mContext.userContexts.put(user, mContext); + when(mContext.resources.getColor(anyInt(), anyObject())).thenReturn(Color.WHITE); + + // Install a CA cert. + final String alias = "cert"; + final byte[] caCert = TEST_CA.getBytes(); + when(mContext.keyChainConnection.getService().installCaCertificate(caCert)) + .thenReturn(alias); + assertTrue(dpm.installCaCert(caller, caCert)); + when(mContext.keyChainConnection.getService().getUserCaAliases()) + .thenReturn(asSlice(new String[] {alias})); + mContext.injectBroadcast(new Intent(KeyChain.ACTION_TRUST_STORE_CHANGED)); + flushTasks(); + + // Device Owner / Profile Owner can find out which CA certs were installed by itself. + final String packageName = mContext.packageName; + mContext.packageName = admin1.getPackageName(); + final long callerIdentity = mContext.binder.clearCallingIdentity(); + mContext.binder.callingUid = ownerUid; + List<String> ownerInstalledCaCerts = dpm.getOwnerInstalledCaCerts(user); + assertNotNull(ownerInstalledCaCerts); + assertEquals(1, ownerInstalledCaCerts.size()); + assertTrue(ownerInstalledCaCerts.contains(alias)); + + // Restarting the DPMS should not lose information. + initializeDpms(); + assertEquals(ownerInstalledCaCerts, dpm.getOwnerInstalledCaCerts(user)); + + // System can find out which CA certs were installed by the Device Owner / Profile Owner. + mContext.packageName = "com.android.frameworks.servicestests"; + mContext.binder.clearCallingIdentity(); + assertEquals(ownerInstalledCaCerts, dpm.getOwnerInstalledCaCerts(user)); + + // Remove the CA cert. + mContext.packageName = packageName; + mContext.binder.restoreCallingIdentity(callerIdentity); + reset(mContext.keyChainConnection.getService()); + mContext.injectBroadcast(new Intent(KeyChain.ACTION_TRUST_STORE_CHANGED)); + flushTasks(); + + // Verify that the CA cert is no longer reported as installed by the Device Owner / Profile + // Owner. + mContext.packageName = admin1.getPackageName(); + mContext.binder.callingUid = ownerUid; + ownerInstalledCaCerts = dpm.getOwnerInstalledCaCerts(user); + assertNotNull(ownerInstalledCaCerts); + assertTrue(ownerInstalledCaCerts.isEmpty()); + + mContext.packageName = packageName; + mContext.binder.restoreCallingIdentity(callerIdentity); + } + + private void verifyCantGetOwnerInstalledCaCertsProfileOwnerRemoval(ComponentName caller) + throws Exception { + final UserHandle user = UserHandle.of(DpmMockContext.CALLER_USER_HANDLE); + + mContext.applicationInfo = new ApplicationInfo(); + mContext.userContexts.put(user, mContext); + when(mContext.resources.getColor(anyInt(), anyObject())).thenReturn(Color.WHITE); + + // Install a CA cert. + final String alias = "cert"; + final byte[] caCert = TEST_CA.getBytes(); + when(mContext.keyChainConnection.getService().installCaCertificate(caCert)) + .thenReturn(alias); + assertTrue(dpm.installCaCert(caller, caCert)); + when(mContext.keyChainConnection.getService().getUserCaAliases()) + .thenReturn(asSlice(new String[] {alias})); + mContext.injectBroadcast(new Intent(KeyChain.ACTION_TRUST_STORE_CHANGED)); + flushTasks(); + + // Removing the Profile Owner should clear the information which CA certs were installed + // by it. + mContext.packageName = admin1.getPackageName(); + mContext.binder.callingUid = DpmMockContext.CALLER_UID; + dpm.clearProfileOwner(admin1); + mContext.packageName = "com.android.frameworks.servicestests"; + mContext.binder.clearCallingIdentity(); + final List<String> ownerInstalledCaCerts = dpm.getOwnerInstalledCaCerts(user); + assertNotNull(ownerInstalledCaCerts); + assertTrue(ownerInstalledCaCerts.isEmpty()); + } + private void setUserSetupCompleteForUser(boolean isUserSetupComplete, int userhandle) { when(mContext.settings.settingsSecureGetIntForUser(Settings.Secure.USER_SETUP_COMPLETE, 0, userhandle)).thenReturn(isUserSetupComplete ? 1 : 0); @@ -3978,4 +4126,31 @@ public class DevicePolicyManagerTest extends DpmTestBase { private static StringParceledListSlice asSlice(String[] s) { return new StringParceledListSlice(Arrays.asList(s)); } + + private void flushTasks() throws Exception { + Boolean tasksFlushed[] = new Boolean[] {false}; + final Runnable tasksFlushedNotifier = () -> { + synchronized (tasksFlushed) { + tasksFlushed[0] = true; + tasksFlushed.notify(); + } + }; + + // Flush main thread handler. + dpms.mHandler.post(tasksFlushedNotifier); + synchronized (tasksFlushed) { + if (!tasksFlushed[0]) { + tasksFlushed.wait(); + } + } + + // Flush background thread handler. + tasksFlushed[0] = false; + dpms.mBackgroundHandler.post(tasksFlushedNotifier); + synchronized (tasksFlushed) { + if (!tasksFlushed[0]) { + tasksFlushed.wait(); + } + } + } } diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java index 258b3933a294..7d017c54002c 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java @@ -316,6 +316,41 @@ public class DpmMockContext extends MockContext { public ApplicationInfo applicationInfo = null; + // We have to keep track of broadcast receivers registered for a given intent ourselves as the + // DPM unit tests mock out the package manager and PackageManager.queryBroadcastReceivers() does + // not work. + private class BroadcastReceiverRegistration { + public final BroadcastReceiver receiver; + public final IntentFilter filter; + public final Handler scheduler; + + public BroadcastReceiverRegistration(BroadcastReceiver receiver, IntentFilter filter, + Handler scheduler) { + this.receiver = receiver; + this.filter = filter; + this.scheduler = scheduler; + } + + public void sendBroadcastIfApplicable(int userId, Intent intent) { + final BroadcastReceiver.PendingResult result = new BroadcastReceiver.PendingResult( + 0 /* resultCode */, null /* resultData */, null /* resultExtras */, + 0 /* type */, false /* ordered */, false /* sticky */, null /* token */, userId, + 0 /* flags */); + if (filter.match(null, intent, false, "DpmMockContext") > 0) { + if (scheduler != null) { + scheduler.post(() -> { + receiver.setPendingResult(result); + receiver.onReceive(DpmMockContext.this, intent); + }); + } else { + receiver.setPendingResult(result); + receiver.onReceive(DpmMockContext.this, intent); + } + } + } + } + private List<BroadcastReceiverRegistration> mBroadcastReceivers = new ArrayList<>(); + public DpmMockContext(Context context, File dataDir) { realTestContext = context; @@ -476,6 +511,13 @@ public class DpmMockContext extends MockContext { .thenReturn(isRunning); } + public void injectBroadcast(Intent intent) { + final int userId = UserHandle.getUserId(binder.getCallingUid()); + for (final BroadcastReceiverRegistration receiver : mBroadcastReceivers) { + receiver.sendBroadcastIfApplicable(userId, intent); + } + } + @Override public Resources getResources() { return resources; @@ -681,24 +723,28 @@ public class DpmMockContext extends MockContext { @Override public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { + mBroadcastReceivers.add(new BroadcastReceiverRegistration(receiver, filter, null)); return spiedContext.registerReceiver(receiver, filter); } @Override public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission, Handler scheduler) { + mBroadcastReceivers.add(new BroadcastReceiverRegistration(receiver, filter, scheduler)); return spiedContext.registerReceiver(receiver, filter, broadcastPermission, scheduler); } @Override public Intent registerReceiverAsUser(BroadcastReceiver receiver, UserHandle user, IntentFilter filter, String broadcastPermission, Handler scheduler) { + mBroadcastReceivers.add(new BroadcastReceiverRegistration(receiver, filter, scheduler)); return spiedContext.registerReceiverAsUser(receiver, user, filter, broadcastPermission, scheduler); } @Override public void unregisterReceiver(BroadcastReceiver receiver) { + mBroadcastReceivers.removeIf(r -> r.receiver == receiver); spiedContext.unregisterReceiver(receiver); } diff --git a/services/tests/servicestests/src/com/android/server/storage/CacheQuotaStrategyTest.java b/services/tests/servicestests/src/com/android/server/storage/CacheQuotaStrategyTest.java new file mode 100644 index 000000000000..1d62e01c068d --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/storage/CacheQuotaStrategyTest.java @@ -0,0 +1,128 @@ +/* + * 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.storage; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.usage.CacheQuotaHint; +import android.test.AndroidTestCase; +import android.util.Pair; + +import com.android.internal.util.FastXmlSerializer; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.ByteArrayInputStream; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; + +@RunWith(JUnit4.class) +public class CacheQuotaStrategyTest extends AndroidTestCase { + StringWriter mWriter; + FastXmlSerializer mOut; + + @Before + public void setUp() throws Exception { + mWriter = new StringWriter(); + mOut = new FastXmlSerializer(); + mOut.setOutput(mWriter); + } + + @Test + public void testEmptyWrite() throws Exception { + CacheQuotaStrategy.saveToXml(mOut, new ArrayList<>(), 0); + mOut.flush(); + + assertThat(mWriter.toString()).isEqualTo( + "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" + + "<cache-info previousBytes=\"0\" />\n"); + } + + @Test + public void testWriteOneQuota() throws Exception { + ArrayList<CacheQuotaHint> requests = new ArrayList<>(); + requests.add(buildCacheQuotaHint("uuid", 0, 100)); + + CacheQuotaStrategy.saveToXml(mOut, requests, 1000); + mOut.flush(); + + assertThat(mWriter.toString()).isEqualTo( + "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" + + "<cache-info previousBytes=\"1000\">\n" + + "<quota uuid=\"uuid\" uid=\"0\" bytes=\"100\" />\n" + + "</cache-info>\n"); + } + + @Test + public void testWriteMultipleQuotas() throws Exception { + ArrayList<CacheQuotaHint> requests = new ArrayList<>(); + requests.add(buildCacheQuotaHint("uuid", 0, 100)); + requests.add(buildCacheQuotaHint("uuid2", 10, 250)); + + CacheQuotaStrategy.saveToXml(mOut, requests, 1000); + mOut.flush(); + + assertThat(mWriter.toString()).isEqualTo( + "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" + + "<cache-info previousBytes=\"1000\">\n" + + "<quota uuid=\"uuid\" uid=\"0\" bytes=\"100\" />\n" + + "<quota uuid=\"uuid2\" uid=\"10\" bytes=\"250\" />\n" + + "</cache-info>\n"); + } + + @Test + public void testNullUuidDoesntCauseCrash() throws Exception { + ArrayList<CacheQuotaHint> requests = new ArrayList<>(); + requests.add(buildCacheQuotaHint(null, 0, 100)); + requests.add(buildCacheQuotaHint(null, 10, 250)); + + CacheQuotaStrategy.saveToXml(mOut, requests, 1000); + mOut.flush(); + + assertThat(mWriter.toString()).isEqualTo( + "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" + + "<cache-info previousBytes=\"1000\">\n" + + "<quota uid=\"0\" bytes=\"100\" />\n" + + "<quota uid=\"10\" bytes=\"250\" />\n" + + "</cache-info>\n"); + } + + @Test + public void testReadMultipleQuotas() throws Exception { + String input = "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" + + "<cache-info previousBytes=\"1000\">\n" + + "<quota uuid=\"uuid\" uid=\"0\" bytes=\"100\" />\n" + + "<quota uuid=\"uuid2\" uid=\"10\" bytes=\"250\" />\n" + + "</cache-info>\n"; + + Pair<Long, List<CacheQuotaHint>> output = + CacheQuotaStrategy.readFromXml(new ByteArrayInputStream(input.getBytes("UTF-8"))); + + assertThat(output.first).isEqualTo(1000); + assertThat(output.second).containsExactly(buildCacheQuotaHint("uuid", 0, 100), + buildCacheQuotaHint("uuid2", 10, 250)); + } + + private CacheQuotaHint buildCacheQuotaHint(String volumeUuid, int uid, long quota) { + return new CacheQuotaHint.Builder() + .setVolumeUuid(volumeUuid).setUid(uid).setQuota(quota).build(); + } +}
\ No newline at end of file diff --git a/services/usage/java/com/android/server/usage/StorageStatsService.java b/services/usage/java/com/android/server/usage/StorageStatsService.java index ed1530a4eddb..89e68a63ac3b 100644 --- a/services/usage/java/com/android/server/usage/StorageStatsService.java +++ b/services/usage/java/com/android/server/usage/StorageStatsService.java @@ -55,6 +55,8 @@ import com.android.server.pm.Installer; import com.android.server.pm.Installer.InstallerException; import com.android.server.storage.CacheQuotaStrategy; +import java.io.IOException; + public class StorageStatsService extends IStorageStatsManager.Stub { private static final String TAG = "StorageStatsService"; @@ -97,7 +99,7 @@ public class StorageStatsService extends IStorageStatsManager.Stub { invalidateMounts(); mHandler = new H(IoThread.get().getLooper()); - mHandler.sendEmptyMessageDelayed(H.MSG_CHECK_STORAGE_DELTA, DELAY_IN_MILLIS); + mHandler.sendEmptyMessageDelayed(H.MSG_LOAD_CACHED_QUOTAS_FROM_FILE, DELAY_IN_MILLIS); mStorage.registerListener(new StorageEventListener() { @Override @@ -343,12 +345,14 @@ public class StorageStatsService extends IStorageStatsManager.Stub { private class H extends Handler { private static final int MSG_CHECK_STORAGE_DELTA = 100; + private static final int MSG_LOAD_CACHED_QUOTAS_FROM_FILE = 101; /** * By only triggering a re-calculation after the storage has changed sizes, we can avoid * recalculating quotas too often. Minimum change delta defines the percentage of change * we need to see before we recalculate. */ private static final double MINIMUM_CHANGE_DELTA = 0.05; + private static final int UNSET = -1; private static final boolean DEBUG = false; private final StatFs mStats; @@ -361,7 +365,6 @@ public class StorageStatsService extends IStorageStatsManager.Stub { mStats = new StatFs(Environment.getDataDirectory().getAbsolutePath()); mPreviousBytes = mStats.getFreeBytes(); mMinimumThresholdBytes = mStats.getTotalBytes() * MINIMUM_CHANGE_DELTA; - // TODO: Load cache quotas from a file to avoid re-doing work. } public void handleMessage(Message msg) { @@ -378,7 +381,26 @@ public class StorageStatsService extends IStorageStatsManager.Stub { long bytesDelta = Math.abs(mPreviousBytes - mStats.getFreeBytes()); if (bytesDelta > mMinimumThresholdBytes) { mPreviousBytes = mStats.getFreeBytes(); - recalculateQuotas(); + recalculateQuotas(getInitializedStrategy()); + } + sendEmptyMessageDelayed(MSG_CHECK_STORAGE_DELTA, DELAY_IN_MILLIS); + break; + } + case MSG_LOAD_CACHED_QUOTAS_FROM_FILE: { + CacheQuotaStrategy strategy = getInitializedStrategy(); + mPreviousBytes = UNSET; + try { + mPreviousBytes = strategy.setupQuotasFromFile(); + } catch (IOException e) { + Slog.e(TAG, "An error occurred while reading the cache quota file.", e); + } catch (IllegalStateException e) { + Slog.e(TAG, "Cache quota XML file is malformed?", e); + } + + // If errors occurred getting the quotas from disk, let's re-calc them. + if (mPreviousBytes < 0) { + mPreviousBytes = mStats.getFreeBytes(); + recalculateQuotas(strategy); } sendEmptyMessageDelayed(MSG_CHECK_STORAGE_DELTA, DELAY_IN_MILLIS); break; @@ -391,17 +413,18 @@ public class StorageStatsService extends IStorageStatsManager.Stub { } } - private void recalculateQuotas() { + private void recalculateQuotas(CacheQuotaStrategy strategy) { if (DEBUG) { Slog.v(TAG, ">>> recalculating quotas "); } + strategy.recalculateQuotas(); + } + + private CacheQuotaStrategy getInitializedStrategy() { UsageStatsManagerInternal usageStatsManager = LocalServices.getService(UsageStatsManagerInternal.class); - CacheQuotaStrategy strategy = new CacheQuotaStrategy( - mContext, usageStatsManager, mInstaller); - // TODO: Save cache quotas to an XML file. - strategy.recalculateQuotas(); + return new CacheQuotaStrategy(mContext, usageStatsManager, mInstaller); } } diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java index ab988b2a50d8..3c7ee43f6200 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UsageStatsService.java @@ -98,7 +98,7 @@ public class UsageStatsService extends SystemService implements static final String TAG = "UsageStatsService"; public static final boolean ENABLE_TIME_CHANGE_CORRECTION - = SystemProperties.getBoolean("debug.time_change_correction", true); + = SystemProperties.getBoolean("persist.debug.time_correction", true); static final boolean DEBUG = false; // Never submit with true static final boolean COMPRESS_TIME = false; diff --git a/tools/aapt2/flatten/XmlFlattener_test.cpp b/tools/aapt2/flatten/XmlFlattener_test.cpp index ffc2de141860..ec3d75e354c7 100644 --- a/tools/aapt2/flatten/XmlFlattener_test.cpp +++ b/tools/aapt2/flatten/XmlFlattener_test.cpp @@ -76,7 +76,7 @@ TEST_F(XmlFlattenerTest, FlattenXmlWithNoCompiledAttributes) { <View xmlns:test="http://com.test" attr="hey"> <Layout test:hello="hi" /> - <Layout>Some text</Layout> + <Layout>Some text\\</Layout> </View>)EOF"); android::ResXMLTree tree; @@ -128,7 +128,7 @@ TEST_F(XmlFlattenerTest, FlattenXmlWithNoCompiledAttributes) { ASSERT_EQ(tree.next(), android::ResXMLTree::TEXT); const char16_t* text = tree.getText(&len); - EXPECT_EQ(StringPiece16(text, len), u"Some text"); + EXPECT_EQ(StringPiece16(text, len), u"Some text\\"); ASSERT_EQ(tree.next(), android::ResXMLTree::END_TAG); ASSERT_EQ(tree.getElementNamespace(&len), nullptr); diff --git a/tools/aapt2/util/Util.h b/tools/aapt2/util/Util.h index f8fa80ed7d66..7210d21f64ab 100644 --- a/tools/aapt2/util/Util.h +++ b/tools/aapt2/util/Util.h @@ -164,6 +164,7 @@ class StringBuilder { StringBuilder& Append(const android::StringPiece& str); const std::string& ToString() const; const std::string& Error() const; + bool IsEmpty() const; // When building StyledStrings, we need UTF-16 indices into the string, // which is what the Java layer expects when dealing with java @@ -185,6 +186,8 @@ inline const std::string& StringBuilder::ToString() const { return str_; } inline const std::string& StringBuilder::Error() const { return error_; } +inline bool StringBuilder::IsEmpty() const { return str_.empty(); } + inline size_t StringBuilder::Utf16Len() const { return utf16_len_; } inline StringBuilder::operator bool() const { return error_.empty(); } diff --git a/tools/aapt2/xml/XmlDom.cpp b/tools/aapt2/xml/XmlDom.cpp index fab2f19fc0ac..d9ea1bcf3766 100644 --- a/tools/aapt2/xml/XmlDom.cpp +++ b/tools/aapt2/xml/XmlDom.cpp @@ -18,7 +18,6 @@ #include <expat.h> -#include <cassert> #include <memory> #include <stack> #include <string> @@ -41,6 +40,8 @@ struct Stack { std::unique_ptr<xml::Node> root; std::stack<xml::Node*> node_stack; std::string pending_comment; + std::unique_ptr<xml::Text> last_text_node; + util::StringBuilder pending_text; }; /** @@ -62,6 +63,19 @@ static void SplitName(const char* name, std::string* out_ns, } } +static void FinishPendingText(Stack* stack) { + if (stack->last_text_node != nullptr) { + if (!stack->pending_text.IsEmpty()) { + stack->last_text_node->text = stack->pending_text.ToString(); + stack->pending_text = {}; + stack->node_stack.top()->AppendChild(std::move(stack->last_text_node)); + } else { + // Drop an empty text node. + stack->last_text_node = nullptr; + } + } +} + static void AddToStack(Stack* stack, XML_Parser parser, std::unique_ptr<Node> node) { node->line_number = XML_GetCurrentLineNumber(parser); @@ -83,6 +97,7 @@ static void XMLCALL StartNamespaceHandler(void* user_data, const char* prefix, const char* uri) { XML_Parser parser = reinterpret_cast<XML_Parser>(user_data); Stack* stack = reinterpret_cast<Stack*>(XML_GetUserData(parser)); + FinishPendingText(stack); std::unique_ptr<Namespace> ns = util::make_unique<Namespace>(); if (prefix) { @@ -99,6 +114,7 @@ static void XMLCALL StartNamespaceHandler(void* user_data, const char* prefix, static void XMLCALL EndNamespaceHandler(void* user_data, const char* prefix) { XML_Parser parser = reinterpret_cast<XML_Parser>(user_data); Stack* stack = reinterpret_cast<Stack*>(XML_GetUserData(parser)); + FinishPendingText(stack); CHECK(!stack->node_stack.empty()); stack->node_stack.pop(); @@ -113,6 +129,7 @@ static void XMLCALL StartElementHandler(void* user_data, const char* name, const char** attrs) { XML_Parser parser = reinterpret_cast<XML_Parser>(user_data); Stack* stack = reinterpret_cast<Stack*>(XML_GetUserData(parser)); + FinishPendingText(stack); std::unique_ptr<Element> el = util::make_unique<Element>(); SplitName(name, &el->namespace_uri, &el->name); @@ -120,7 +137,9 @@ static void XMLCALL StartElementHandler(void* user_data, const char* name, while (*attrs) { Attribute attribute; SplitName(*attrs++, &attribute.namespace_uri, &attribute.name); - attribute.value = *attrs++; + util::StringBuilder builder; + builder.Append(*attrs++); + attribute.value = builder.ToString(); // Insert in sorted order. auto iter = std::lower_bound(el->attributes.begin(), el->attributes.end(), @@ -135,41 +154,38 @@ static void XMLCALL StartElementHandler(void* user_data, const char* name, static void XMLCALL EndElementHandler(void* user_data, const char* name) { XML_Parser parser = reinterpret_cast<XML_Parser>(user_data); Stack* stack = reinterpret_cast<Stack*>(XML_GetUserData(parser)); + FinishPendingText(stack); CHECK(!stack->node_stack.empty()); // stack->nodeStack.top()->comment = std::move(stack->pendingComment); stack->node_stack.pop(); } -static void XMLCALL CharacterDataHandler(void* user_data, const char* s, - int len) { +static void XMLCALL CharacterDataHandler(void* user_data, const char* s, int len) { XML_Parser parser = reinterpret_cast<XML_Parser>(user_data); Stack* stack = reinterpret_cast<Stack*>(XML_GetUserData(parser)); - if (!s || len <= 0) { + const StringPiece str(s, len); + if (str.empty()) { return; } // See if we can just append the text to a previous text node. - if (!stack->node_stack.empty()) { - Node* currentParent = stack->node_stack.top(); - if (!currentParent->children.empty()) { - Node* last_child = currentParent->children.back().get(); - if (Text* text = NodeCast<Text>(last_child)) { - text->text.append(s, len); - return; - } - } + if (stack->last_text_node != nullptr) { + stack->pending_text.Append(str); + return; } - std::unique_ptr<Text> text = util::make_unique<Text>(); - text->text.assign(s, len); - AddToStack(stack, parser, std::move(text)); + stack->last_text_node = util::make_unique<Text>(); + stack->last_text_node->line_number = XML_GetCurrentLineNumber(parser); + stack->last_text_node->column_number = XML_GetCurrentColumnNumber(parser); + stack->pending_text.Append(str); } static void XMLCALL CommentDataHandler(void* user_data, const char* comment) { XML_Parser parser = reinterpret_cast<XML_Parser>(user_data); Stack* stack = reinterpret_cast<Stack*>(XML_GetUserData(parser)); + FinishPendingText(stack); if (!stack->pending_comment.empty()) { stack->pending_comment += '\n'; diff --git a/tools/aapt2/xml/XmlDom_test.cpp b/tools/aapt2/xml/XmlDom_test.cpp index a414afe92fc0..0fc3cec66666 100644 --- a/tools/aapt2/xml/XmlDom_test.cpp +++ b/tools/aapt2/xml/XmlDom_test.cpp @@ -49,4 +49,23 @@ TEST(XmlDomTest, Inflate) { EXPECT_EQ(ns->namespace_prefix, "android"); } +TEST(XmlDomTest, HandleEscapes) { + std::unique_ptr<xml::XmlResource> doc = test::BuildXmlDom( + R"EOF(<shortcode pattern="\\d{5}">\\d{5}</shortcode>)EOF"); + + xml::Element* el = xml::FindRootElement(doc->root.get()); + ASSERT_NE(nullptr, el); + + xml::Attribute* attr = el->FindAttribute({}, "pattern"); + ASSERT_NE(nullptr, attr); + + EXPECT_EQ("\\d{5}", attr->value); + + ASSERT_EQ(1u, el->children.size()); + + xml::Text* text = xml::NodeCast<xml::Text>(el->children[0].get()); + ASSERT_NE(nullptr, text); + EXPECT_EQ("\\d{5}", text->text); +} + } // namespace aapt diff --git a/wifi/java/android/net/wifi/WifiConfiguration.java b/wifi/java/android/net/wifi/WifiConfiguration.java index a4ab64f9aed2..a1099f8045c5 100644 --- a/wifi/java/android/net/wifi/WifiConfiguration.java +++ b/wifi/java/android/net/wifi/WifiConfiguration.java @@ -376,7 +376,9 @@ public class WifiConfiguration implements Parcelable { /** * Flag indicating if this network is provided by a home Passpoint provider or a roaming - * Passpoint provider. + * Passpoint provider. This flag will be {@code true} if this network is provided by + * a home Passpoint provider and {@code false} if is provided by a roaming Passpoint provider + * or is a non-Passpoint network. */ public boolean isHomeProviderNetwork; |