Merge "Move framework-specific portions of pathmap.mk into frameworks/base"
diff --git a/api/current.txt b/api/current.txt
index 6e054fc..693d512 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -298,6 +298,7 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 cfcc40a..f095936 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -410,6 +410,7 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 478a3ed..4585b8e 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -298,6 +298,7 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 8538330..dad2061 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -522,6 +522,7 @@
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 9834350..d8d4bb9 100644
--- a/core/java/android/app/ITaskStackListener.aidl
+++ b/core/java/android/app/ITaskStackListener.aidl
@@ -38,6 +38,11 @@
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 1c33e38..7cdd45f 100644
--- a/core/java/android/app/LoadedApk.java
+++ b/core/java/android/app/LoadedApk.java
@@ -286,7 +286,7 @@
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 34a45dd6..3fc459e 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -4177,7 +4177,7 @@
}
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 8854adc..288d39a 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 @@
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 @@
mId = null;
}
mName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+ mNameResId = in.readInt();
in.readParcelableList(mChannels, NotificationChannel.class.getClassLoader());
}
@@ -76,6 +93,7 @@
dest.writeByte((byte) 0);
}
TextUtils.writeToParcel(mName, dest, flags);
+ dest.writeInt(mNameResId);
dest.writeParcelableList(mChannels, flags);
}
@@ -93,6 +111,13 @@
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 @@
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 @@
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 @@
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 @@
@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 @@
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 5a0845f..7a569fc 100644
--- a/core/java/android/app/TaskStackListener.java
+++ b/core/java/android/app/TaskStackListener.java
@@ -39,6 +39,10 @@
}
@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 6585793..449cca3 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -7958,4 +7958,28 @@
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 ec97c2c..97a4678 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -27,6 +27,7 @@
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 @@
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 d1b2a15..a1ef4a6 100644
--- a/core/java/android/app/assist/AssistStructure.java
+++ b/core/java/android/app/assist/AssistStructure.java
@@ -539,6 +539,7 @@
// 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 @@
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 @@
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 @@
}
/**
+ * 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 @@
}
@Override
+ public void setAutoFillHint(@View.AutoFillHint int hint) {
+ mNode.mAutoFillHint = hint;
+ }
+
+ @Override
public void setAutoFillValue(AutoFillValue value) {
mNode.mAutoFillValue = value;
}
@@ -1695,6 +1717,7 @@
+ ", 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 4b6f99b..1d5c2b0 100644
--- a/core/java/android/app/usage/CacheQuotaHint.java
+++ b/core/java/android/app/usage/CacheQuotaHint.java
@@ -24,8 +24,10 @@
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 @@
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 @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 @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 4252482..7d8459c 100644
--- a/core/java/android/bluetooth/BluetoothMapClient.java
+++ b/core/java/android/bluetooth/BluetoothMapClient.java
@@ -50,6 +50,12 @@
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 9958a79..5579c9a 100644
--- a/core/java/android/content/ContentResolver.java
+++ b/core/java/android/content/ContentResolver.java
@@ -307,6 +307,18 @@
*/
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 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 @@
* 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 @@
* <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 @@
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 71a0349..33b5903 100644
--- a/core/java/android/provider/DocumentsContract.java
+++ b/core/java/android/provider/DocumentsContract.java
@@ -119,6 +119,9 @@
* <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 c3d3f39..16d4666 100644
--- a/core/java/android/view/NotificationHeaderView.java
+++ b/core/java/android/view/NotificationHeaderView.java
@@ -94,6 +94,7 @@
}
}
};
+ private boolean mAcceptAllTouches;
public NotificationHeaderView(Context context) {
this(context, null);
@@ -374,6 +375,8 @@
case MotionEvent.ACTION_DOWN:
mTrackGesture = false;
if (isInside(x, y)) {
+ mDownX = x;
+ mDownY = y;
mTrackGesture = true;
return true;
}
@@ -396,11 +399,12 @@
}
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 @@
}
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 e349170..99051a7 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -974,6 +974,143 @@
*/
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 @@
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 @@
// to reuse the accessibility id to save space.
structure.setAutoFillId(getAccessibilityViewId());
structure.setAutoFillType(autoFillType);
+ structure.setAutoFillHint(getAutoFillHint());
structure.setAutoFillValue(getAutoFillValue());
}
}
@@ -7178,7 +7321,7 @@
/**
* 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 @@
}
/**
+ * 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 @@
}
/**
+ * 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 43560f0..bc0960c 100644
--- a/core/java/android/view/ViewStructure.java
+++ b/core/java/android/view/ViewStructure.java
@@ -301,6 +301,13 @@
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 e974705..62913d8 100644
--- a/core/java/android/view/autofill/AutoFillType.java
+++ b/core/java/android/view/autofill/AutoFillType.java
@@ -21,16 +21,11 @@
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 @@
// 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 @@
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 @@
* <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 @@
* <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 @@
* <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 @@
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 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 @@
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 @@
@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 @@
*
* <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 3d10917..97fc835 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -10019,7 +10019,7 @@
@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 c137ae2..55b8884 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 15764a9..df3962c 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 b685431..c4bb72c 100644
--- a/keystore/java/android/security/IKeyChainService.aidl
+++ b/keystore/java/android/security/IKeyChainService.aidl
@@ -29,8 +29,8 @@
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 2e33609..374c1b1 100644
--- a/libs/hwui/RecordingCanvas.cpp
+++ b/libs/hwui/RecordingCanvas.cpp
@@ -268,7 +268,7 @@
// 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::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::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::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::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::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::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::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::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::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 @@
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 c57b1b3..51a7d00 100644
--- a/libs/hwui/SkiaCanvas.cpp
+++ b/libs/hwui/SkiaCanvas.cpp
@@ -443,7 +443,7 @@
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::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 @@
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 0ac09ac..7fb75dc 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 @@
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 845a3ea..2673be1c 100644
--- a/libs/hwui/utils/PaintUtils.h
+++ b/libs/hwui/utils/PaintUtils.h
@@ -39,21 +39,6 @@
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 f79cd06..4b43260 100644
--- a/media/java/android/media/midi/MidiDevice.java
+++ b/media/java/android/media/midi/MidiDevice.java
@@ -35,6 +35,10 @@
* 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 @@
private final IBinder mClientToken;
private final IBinder mDeviceToken;
private boolean mIsDeviceClosed;
+ private boolean mIsMirroredToNative;
private final CloseGuard mGuard = CloseGuard.get();
@@ -209,10 +214,45 @@
}
}
+ /**
+ * 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 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 861ed0a..23bf3d6 100644
--- a/media/jni/Android.mk
+++ b/media/jni/Android.mk
@@ -26,6 +26,7 @@
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 @@
libbinder \
libmedia \
libmediadrm \
+ libmidi \
libskia \
libui \
liblog \
@@ -55,6 +57,7 @@
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 0000000..1e54bac
--- /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 0000000..3500805
--- /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 0000000..b91c430
--- /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 0000000..8854a08
--- /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 0000000..93be733
--- /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 0000000..fa70af8
--- /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 0000000..f1ffb78
--- /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 0000000..1bf0bd0
--- /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 0000000..717bc66
--- /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 0000000..6b08f6b
--- /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 0000000..322873f
--- /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 0000000..0969b10
--- /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 0000000..645aafa
--- /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 0000000..69a64bd
--- /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 0000000..ffaef38
--- /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 0000000..20aa9e8
--- /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 0000000..8aa874a
--- /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 0000000..465d471
--- /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
new file mode 100644
index 0000000..cde69bc
--- /dev/null
+++ b/media/tests/NativeMidiDemo/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/media/tests/NativeMidiDemo/res/mipmap-mdpi/ic_launcher.png b/media/tests/NativeMidiDemo/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c133a0c
--- /dev/null
+++ b/media/tests/NativeMidiDemo/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/media/tests/NativeMidiDemo/res/mipmap-xhdpi/ic_launcher.png b/media/tests/NativeMidiDemo/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..bfa42f0
--- /dev/null
+++ b/media/tests/NativeMidiDemo/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/media/tests/NativeMidiDemo/res/mipmap-xxhdpi/ic_launcher.png b/media/tests/NativeMidiDemo/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..324e72c
--- /dev/null
+++ b/media/tests/NativeMidiDemo/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/media/tests/NativeMidiDemo/res/values/strings.xml b/media/tests/NativeMidiDemo/res/values/strings.xml
new file mode 100644
index 0000000..5b69b52
--- /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 3ede2ee..1b1f28c 100644
--- a/native/android/libandroid.map.txt
+++ b/native/android/libandroid.map.txt
@@ -139,16 +139,27 @@
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 d4623d6..82da9a3 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 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 0000000..ec8e956
--- /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 6623cabe..b5093b3 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemBars.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemBars.java
@@ -43,13 +43,6 @@
}
@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 ae402ef..e7256d1 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 @@
}
@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 d832810..4100b66 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 @@
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 @@
// Fall through to clean up
}
case MotionEvent.ACTION_CANCEL: {
+ mTouchState.reset();
break;
}
}
@@ -418,6 +423,10 @@
@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 @@
@Override
boolean onMove(PipTouchState touchState) {
+ if (!touchState.isUserInteracting()) {
+ return false;
+ }
+
if (touchState.startedDragging()) {
mSavedSnapFraction = -1f;
}
@@ -458,6 +471,10 @@
@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 702ad0a..a317dc3 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 @@
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 @@
}
/**
+ * 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 @@
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 @@
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 @@
// Fall through to clean up
}
case MotionEvent.ACTION_CANCEL: {
- mIsUserInteracting = false;
recycleVelocityTracker();
break;
}
@@ -171,6 +199,19 @@
}
/**
+ * 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 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 428fe9b..7d13f76 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 @@
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 @@
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 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 b28b0e7..a76299d 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 @@
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 cda902b..1042356 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 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 @@
}
@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 @@
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 @@
}
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 995901b..09b7bec 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 @@
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 807d902..af464c6 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 @@
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 36ed551..8da17fa 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 @@
* 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 @@
@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 void setCollapsedIcons(NotificationIconContainer collapsedIcons) {
mCollapsedIcons = collapsedIcons;
+ mCollapsedIcons.addOnLayoutChangeListener(this);
}
public void setStatusBarState(int statusBarState) {
@@ -595,6 +601,12 @@
}
}
+ @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 aec9a4b..1101701 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 void setIconAppearAmount(float iconAppearAmount) {
- mIconAppearAmount = iconAppearAmount;
- invalidate();
+ if (mIconAppearAmount != iconAppearAmount) {
+ mIconAppearAmount = iconAppearAmount;
+ invalidate();
+ }
}
public float getIconAppearAmount() {
@@ -625,8 +627,10 @@
}
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 1f56c56..6cd3eae 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 @@
}
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 ad875f1..3f7e340 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.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 707997d..4581204 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 @@
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 468fb57..e6d3168 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.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.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 @@
private boolean mKeyguardStatusViewAnimating;
private ValueAnimator mQsSizeChangeAnimator;
- private boolean mShadeEmpty;
+ private boolean mShowEmptyShadeView;
private boolean mQsScrimEnabled = true;
private boolean mLastAnnouncementWasQuickSettings;
@@ -211,11 +209,12 @@
}
};
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 @@
// 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 @@
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 @@
}
mNotificationStackScroller.setExpandedHeight(expandedHeight);
updateKeyguardBottomAreaAlpha();
- setOpening(isFullWidth() && expandedHeight < getOpeningHeight());
+ updateStatusBarIcons();
}
/**
@@ -2317,13 +2316,21 @@
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 @@
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 @@
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 9e93ed3..bb6c8f2 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.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.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.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.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.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 @@
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 @@
@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 @@
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 @@
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 53d2d73..816a39d 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.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 @@
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 @@
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 @@
}
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 @@
* 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 @@
return mLaunchTransitionFadingAway;
}
- public boolean shouldHideNotificationIcons() {
- return mNotificationPanel.shouldHideNotificationIcons();
+ public boolean hideStatusBarIconsWhenExpanded() {
+ return mNotificationPanel.hideStatusBarIconsWhenExpanded();
}
/**
@@ -2963,7 +2967,7 @@
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 @@
}
}
- private boolean shouldAnimatIconHiding() {
- return mNotificationPanel.shouldAnimateIconHiding();
- }
-
public boolean interceptTouchEvent(MotionEvent event) {
if (DEBUG_GESTURES) {
if (event.getActionMasked() != MotionEvent.ACTION_MOVE) {
@@ -3910,7 +3910,7 @@
}
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 @@
}
}
}
+ 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 9a3fabb..92f8c7c 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 @@
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 @@
@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 @@
@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 e9e73cc..54ee5dc 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 @@
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 @@
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 @@
}
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 75b5929..c9d19cb 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 @@
&& 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 fdbaada..fd24582 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 @@
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 @@
int mSendCount;
}
+ static final class ViolationEntry {
+ int mCount;
+ }
+
public BroadcastStats() {
mStartRealtime = SystemClock.elapsedRealtime();
mStartUptime = SystemClock.uptimeMillis();
@@ -87,6 +92,20 @@
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 @@
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 @@
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 9dfc7cd..9a98bc6 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 @@
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 @@
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 @@
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 @@
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 e72f7ff..476eb10 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 @@
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 @@
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 @@
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 ba4d46a..a692559 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.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.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
/**
@@ -193,6 +197,14 @@
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 @@
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 @@
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 0e33409..ed49383 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 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 @@
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 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 @@
}
}
- return new ArrayList<String>(packagesToUpdateAssets);
+ return new ArrayList<>(packagesToUpdateAssets);
}
void onUserRemoved(final int userId) {
@@ -303,6 +316,7 @@
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 44908a7..ff5c594 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.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 caacc46..1fe4ccc 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 @@
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 @@
// 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 @@
return null;
}
+ rebaseEnabledOverlays(packageInfo.applicationInfo, userId);
+
packageInfo.packageName = packageInfo.applicationInfo.packageName =
resolveExternalPackageNameLPr(p);
@@ -4147,8 +4127,12 @@
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 @@
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 @@
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 @@
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 @@
// 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 @@
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 @@
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 10d30aa..c064392 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.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 @@
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 @@
}
/**
- * 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 @@
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 @@
}
private void disconnectService() {
- mContext.unbindService(mServiceConnection);
- mServiceConnection = null;
+ if (mServiceConnection != null) {
+ mContext.unbindService(mServiceConnection);
+ mServiceConnection = null;
+ }
}
private ComponentName getServiceComponentName() {
@@ -223,4 +257,131 @@
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 6cc2efb..7588b71 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 @@
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 @@
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 8b94ca0..9c3ecd0 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.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.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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
.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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
} 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 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 @@
}
final DevicePolicyData policyData = getUserData(userId);
policyData.mDefaultInputMethodSet = false;
+ policyData.mOwnerInstalledCaCerts.clear();
saveSettingsLocked(userId);
clearUserPoliciesLocked(userId);
mOwners.removeProfileOwner(userId);
@@ -7127,29 +7183,32 @@
}
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) {
- if (userHandle < 0) {
- throw new IllegalArgumentException("Invalid userId " + userHandle);
- }
- final int callingUid = mInjector.binderGetCallingUid();
- if (userHandle == UserHandle.getUserId(callingUid)) {
- return;
- }
- if (!(isCallerWithSystemUid() || callingUid == Process.ROOT_UID)) {
+ 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);
+ }
+ if (userHandle == mInjector.userHandleGetCallingUserId()) {
+ return;
+ }
+ enforceSystemUserOrPermission(permission);
+ }
+
private void enforceManagedProfile(int userHandle, String message) {
if(!isManagedProfile(userHandle)) {
throw new SecurityException("You can not " + message + " outside a managed profile.");
@@ -7184,6 +7243,21 @@
"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 @@
}
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 ffb0a9e..25c29ee 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 @@
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 @@
@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 @@
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 @@
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 756514b..f797f31 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.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 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 @@
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 @@
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 258b393..7d017c5 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 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 @@
.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 @@
@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 0000000..1d62e01
--- /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 ed1530a..89e68a6 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.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 @@
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 @@
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 @@
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 @@
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 @@
}
}
- 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 ab988b2..3c7ee43 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 @@
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 ffc2de1..ec3d75e 100644
--- a/tools/aapt2/flatten/XmlFlattener_test.cpp
+++ b/tools/aapt2/flatten/XmlFlattener_test.cpp
@@ -76,7 +76,7 @@
<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 @@
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 f8fa80e..7210d21 100644
--- a/tools/aapt2/util/Util.h
+++ b/tools/aapt2/util/Util.h
@@ -164,6 +164,7 @@
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::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 fab2f19..d9ea1bc 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 @@
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 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 @@
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 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 @@
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 @@
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 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 a414afe..0fc3cec6 100644
--- a/tools/aapt2/xml/XmlDom_test.cpp
+++ b/tools/aapt2/xml/XmlDom_test.cpp
@@ -49,4 +49,23 @@
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 a4ab64f..a1099f8 100644
--- a/wifi/java/android/net/wifi/WifiConfiguration.java
+++ b/wifi/java/android/net/wifi/WifiConfiguration.java
@@ -376,7 +376,9 @@
/**
* 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;