Merge "Make statsd dogfood app to be a system app."
diff --git a/Android.bp b/Android.bp
index dc51884..129d676 100644
--- a/Android.bp
+++ b/Android.bp
@@ -319,6 +319,10 @@
"core/java/android/service/chooser/IChooserTargetResult.aidl",
"core/java/android/service/resolver/IResolverRankerService.aidl",
"core/java/android/service/resolver/IResolverRankerResult.aidl",
+ "core/java/android/service/textclassifier/ITextClassificationCallback.aidl",
+ "core/java/android/service/textclassifier/ITextClassifierService.aidl",
+ "core/java/android/service/textclassifier/ITextLinksCallback.aidl",
+ "core/java/android/service/textclassifier/ITextSelectionCallback.aidl",
"core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl",
"core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl",
"core/java/android/view/accessibility/IAccessibilityManager.aidl",
diff --git a/api/current.txt b/api/current.txt
index 800412b..44f2f08 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -6498,7 +6498,7 @@
method public boolean isUsingUnifiedPassword(android.content.ComponentName);
method public void lockNow();
method public void lockNow(int);
- method public boolean logoutUser(android.content.ComponentName);
+ method public int logoutUser(android.content.ComponentName);
method public void reboot(android.content.ComponentName);
method public void removeActiveAdmin(android.content.ComponentName);
method public boolean removeCrossProfileWidgetProvider(android.content.ComponentName, java.lang.String);
@@ -6583,8 +6583,8 @@
method public void setTrustAgentConfiguration(android.content.ComponentName, android.content.ComponentName, android.os.PersistableBundle);
method public void setUninstallBlocked(android.content.ComponentName, java.lang.String, boolean);
method public void setUserIcon(android.content.ComponentName, android.graphics.Bitmap);
- method public boolean startUserInBackground(android.content.ComponentName, android.os.UserHandle);
- method public boolean stopUser(android.content.ComponentName, android.os.UserHandle);
+ method public int startUserInBackground(android.content.ComponentName, android.os.UserHandle);
+ method public int stopUser(android.content.ComponentName, android.os.UserHandle);
method public boolean switchUser(android.content.ComponentName, android.os.UserHandle);
method public void transferOwnership(android.content.ComponentName, android.content.ComponentName, android.os.PersistableBundle);
method public void uninstallAllUserCaCerts(android.content.ComponentName);
@@ -6698,6 +6698,11 @@
field public static final int RESET_PASSWORD_DO_NOT_ASK_CREDENTIALS_ON_BOOT = 2; // 0x2
field public static final int RESET_PASSWORD_REQUIRE_ENTRY = 1; // 0x1
field public static final int SKIP_SETUP_WIZARD = 1; // 0x1
+ field public static final int USER_OPERATION_ERROR_CURRENT_USER = 4; // 0x4
+ field public static final int USER_OPERATION_ERROR_MANAGED_PROFILE = 2; // 0x2
+ field public static final int USER_OPERATION_ERROR_MAX_RUNNING_USERS = 3; // 0x3
+ field public static final int USER_OPERATION_ERROR_UNKNOWN = 1; // 0x1
+ field public static final int USER_OPERATION_SUCCESS = 0; // 0x0
field public static final int WIPE_EXTERNAL_STORAGE = 1; // 0x1
field public static final int WIPE_RESET_PROTECTION_DATA = 2; // 0x2
}
@@ -7161,7 +7166,8 @@
method public android.net.Uri getUri();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<android.app.slice.Slice> CREATOR;
- field public static final java.lang.String EXTRA_SLIDER_VALUE = "android.app.slice.extra.SLIDER_VALUE";
+ field public static final java.lang.String EXTRA_RANGE_VALUE = "android.app.slice.extra.RANGE_VALUE";
+ field public static final deprecated java.lang.String EXTRA_SLIDER_VALUE = "android.app.slice.extra.SLIDER_VALUE";
field public static final java.lang.String EXTRA_TOGGLE_STATE = "android.app.slice.extra.TOGGLE_STATE";
field public static final java.lang.String HINT_ACTIONS = "actions";
field public static final java.lang.String HINT_CALLER_NEEDED = "caller_needed";
@@ -7181,7 +7187,8 @@
field public static final java.lang.String SUBTYPE_MAX = "max";
field public static final java.lang.String SUBTYPE_MESSAGE = "message";
field public static final java.lang.String SUBTYPE_PRIORITY = "priority";
- field public static final java.lang.String SUBTYPE_SLIDER = "slider";
+ field public static final java.lang.String SUBTYPE_RANGE = "range";
+ field public static final deprecated java.lang.String SUBTYPE_SLIDER = "slider";
field public static final java.lang.String SUBTYPE_SOURCE = "source";
field public static final java.lang.String SUBTYPE_TOGGLE = "toggle";
field public static final java.lang.String SUBTYPE_VALUE = "value";
@@ -10043,7 +10050,6 @@
field public static final int FLAG_ACTIVITY_FORWARD_RESULT = 33554432; // 0x2000000
field public static final int FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY = 1048576; // 0x100000
field public static final int FLAG_ACTIVITY_LAUNCH_ADJACENT = 4096; // 0x1000
- field public static final int FLAG_ACTIVITY_MATCH_EXTERNAL = 2048; // 0x800
field public static final int FLAG_ACTIVITY_MULTIPLE_TASK = 134217728; // 0x8000000
field public static final int FLAG_ACTIVITY_NEW_DOCUMENT = 524288; // 0x80000
field public static final int FLAG_ACTIVITY_NEW_TASK = 268435456; // 0x10000000
@@ -16368,7 +16374,7 @@
method public void addSurface(android.view.Surface);
method public int describeContents();
method public void enableSurfaceSharing();
- method public static int getMaxSharedSurfaceCount();
+ method public int getMaxSharedSurfaceCount();
method public android.view.Surface getSurface();
method public int getSurfaceGroupId();
method public java.util.List<android.view.Surface> getSurfaces();
@@ -38531,6 +38537,20 @@
method public android.service.autofill.Dataset.Builder setValue(android.view.autofill.AutofillId, android.view.autofill.AutofillValue, java.util.regex.Pattern, android.widget.RemoteViews);
}
+ public final class DateTransformation implements android.os.Parcelable android.service.autofill.Transformation {
+ ctor public DateTransformation(android.view.autofill.AutofillId, java.text.DateFormat);
+ method public int describeContents();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.service.autofill.DateTransformation> CREATOR;
+ }
+
+ public final class DateValueSanitizer implements android.os.Parcelable android.service.autofill.Sanitizer {
+ ctor public DateValueSanitizer(java.text.DateFormat);
+ method public int describeContents();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.service.autofill.DateValueSanitizer> CREATOR;
+ }
+
public final class FieldClassification {
method public java.util.List<android.service.autofill.FieldClassification.Match> getMatches();
}
@@ -38700,6 +38720,7 @@
public final class UserData implements android.os.Parcelable {
method public int describeContents();
method public java.lang.String getFieldClassificationAlgorithm();
+ method public java.lang.String getId();
method public static int getMaxFieldClassificationIdsSize();
method public static int getMaxUserDataSize();
method public static int getMaxValueLength();
@@ -38709,7 +38730,7 @@
}
public static final class UserData.Builder {
- ctor public UserData.Builder(java.lang.String, java.lang.String);
+ ctor public UserData.Builder(java.lang.String, java.lang.String, java.lang.String);
method public android.service.autofill.UserData.Builder add(java.lang.String, java.lang.String);
method public android.service.autofill.UserData build();
method public android.service.autofill.UserData.Builder setFieldClassificationAlgorithm(java.lang.String, android.os.Bundle);
@@ -44321,6 +44342,11 @@
method public static final boolean addLinks(android.text.Spannable, java.util.regex.Pattern, java.lang.String);
method public static final boolean addLinks(android.text.Spannable, java.util.regex.Pattern, java.lang.String, android.text.util.Linkify.MatchFilter, android.text.util.Linkify.TransformFilter);
method public static final boolean addLinks(android.text.Spannable, java.util.regex.Pattern, java.lang.String, java.lang.String[], android.text.util.Linkify.MatchFilter, android.text.util.Linkify.TransformFilter);
+ method public static java.util.concurrent.Future<java.lang.Void> addLinksAsync(android.widget.TextView, android.view.textclassifier.TextLinks.Options);
+ method public static java.util.concurrent.Future<java.lang.Void> addLinksAsync(android.widget.TextView, android.view.textclassifier.TextLinks.Options, java.util.concurrent.Executor, java.util.function.Consumer<java.lang.Integer>);
+ method public static java.util.concurrent.Future<java.lang.Void> addLinksAsync(android.text.Spannable, android.view.textclassifier.TextClassifier, android.view.textclassifier.TextLinks.Options);
+ method public static java.util.concurrent.Future<java.lang.Void> addLinksAsync(android.text.Spannable, android.view.textclassifier.TextClassifier, int);
+ method public static java.util.concurrent.Future<java.lang.Void> addLinksAsync(android.text.Spannable, android.view.textclassifier.TextClassifier, android.view.textclassifier.TextLinks.Options, java.util.concurrent.Executor, java.util.function.Consumer<java.lang.Integer>);
field public static final int ALL = 15; // 0xf
field public static final int EMAIL_ADDRESSES = 2; // 0x2
field public static final int MAP_ADDRESSES = 8; // 0x8
@@ -49561,6 +49587,7 @@
method public java.util.List<java.lang.String> getAvailableFieldClassificationAlgorithms();
method public java.lang.String getDefaultFieldClassificationAlgorithm();
method public android.service.autofill.UserData getUserData();
+ method public java.lang.String getUserDataId();
method public boolean hasEnabledAutofillServices();
method public boolean isAutofillSupported();
method public boolean isEnabled();
@@ -50016,7 +50043,8 @@
package android.view.textclassifier {
- public final class TextClassification {
+ public final class TextClassification implements android.os.Parcelable {
+ method public int describeContents();
method public float getConfidenceScore(java.lang.String);
method public java.lang.String getEntity(int);
method public int getEntityCount();
@@ -50030,6 +50058,8 @@
method public java.lang.CharSequence getSecondaryLabel(int);
method public java.lang.String getSignature();
method public java.lang.String getText();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.view.textclassifier.TextClassification> CREATOR;
}
public static final class TextClassification.Builder {
@@ -50070,6 +50100,7 @@
method public default android.view.textclassifier.TextLinks generateLinks(java.lang.CharSequence, android.view.textclassifier.TextLinks.Options);
method public default android.view.textclassifier.TextLinks generateLinks(java.lang.CharSequence);
method public default java.util.Collection<java.lang.String> getEntitiesForPreset(int);
+ method public default android.view.textclassifier.logging.Logger getLogger(android.view.textclassifier.logging.Logger.Config);
method public default android.view.textclassifier.TextSelection suggestSelection(java.lang.CharSequence, int, int, android.view.textclassifier.TextSelection.Options);
method public default android.view.textclassifier.TextSelection suggestSelection(java.lang.CharSequence, int, int);
method public default android.view.textclassifier.TextSelection suggestSelection(java.lang.CharSequence, int, int, android.os.LocaleList);
@@ -50099,32 +50130,42 @@
}
public final class TextLinks implements android.os.Parcelable {
- method public boolean apply(android.text.SpannableString, java.util.function.Function<android.view.textclassifier.TextLinks.TextLink, android.text.style.ClickableSpan>);
method public int describeContents();
method public java.util.Collection<android.view.textclassifier.TextLinks.TextLink> getLinks();
method public void writeToParcel(android.os.Parcel, int);
+ field public static final int APPLY_STRATEGY_IGNORE = 0; // 0x0
+ field public static final int APPLY_STRATEGY_REPLACE = 1; // 0x1
field public static final android.os.Parcelable.Creator<android.view.textclassifier.TextLinks> CREATOR;
+ field public static final int STATUS_DIFFERENT_TEXT = 3; // 0x3
+ field public static final int STATUS_LINKS_APPLIED = 0; // 0x0
+ field public static final int STATUS_NO_LINKS_APPLIED = 2; // 0x2
+ field public static final int STATUS_NO_LINKS_FOUND = 1; // 0x1
}
public static final class TextLinks.Builder {
ctor public TextLinks.Builder(java.lang.String);
- method public android.view.textclassifier.TextLinks.Builder addLink(android.view.textclassifier.TextLinks.TextLink);
+ method public android.view.textclassifier.TextLinks.Builder addLink(int, int, java.util.Map<java.lang.String, java.lang.Float>);
method public android.view.textclassifier.TextLinks build();
+ method public android.view.textclassifier.TextLinks.Builder clearTextLinks();
}
public static final class TextLinks.Options implements android.os.Parcelable {
ctor public TextLinks.Options();
method public int describeContents();
+ method public static android.view.textclassifier.TextLinks.Options fromLinkMask(int);
+ method public int getApplyStrategy();
method public android.os.LocaleList getDefaultLocales();
method public android.view.textclassifier.TextClassifier.EntityConfig getEntityConfig();
+ method public java.util.function.Function<android.view.textclassifier.TextLinks.TextLink, android.view.textclassifier.TextLinks.TextLinkSpan> getSpanFactory();
+ method public android.view.textclassifier.TextLinks.Options setApplyStrategy(int);
method public android.view.textclassifier.TextLinks.Options setDefaultLocales(android.os.LocaleList);
method public android.view.textclassifier.TextLinks.Options setEntityConfig(android.view.textclassifier.TextClassifier.EntityConfig);
+ method public android.view.textclassifier.TextLinks.Options setSpanFactory(java.util.function.Function<android.view.textclassifier.TextLinks.TextLink, android.view.textclassifier.TextLinks.TextLinkSpan>);
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<android.view.textclassifier.TextLinks.Options> CREATOR;
}
public static final class TextLinks.TextLink implements android.os.Parcelable {
- ctor public TextLinks.TextLink(java.lang.String, int, int, java.util.Map<java.lang.String, java.lang.Float>);
method public int describeContents();
method public float getConfidenceScore(java.lang.String);
method public int getEnd();
@@ -50135,13 +50176,22 @@
field public static final android.os.Parcelable.Creator<android.view.textclassifier.TextLinks.TextLink> CREATOR;
}
- public final class TextSelection {
+ public static class TextLinks.TextLinkSpan extends android.text.style.ClickableSpan {
+ ctor public TextLinks.TextLinkSpan(android.view.textclassifier.TextLinks.TextLink);
+ method public final android.view.textclassifier.TextLinks.TextLink getTextLink();
+ method public void onClick(android.view.View);
+ }
+
+ public final class TextSelection implements android.os.Parcelable {
+ method public int describeContents();
method public float getConfidenceScore(java.lang.String);
method public java.lang.String getEntity(int);
method public int getEntityCount();
method public int getSelectionEndIndex();
method public int getSelectionStartIndex();
method public java.lang.String getSignature();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.view.textclassifier.TextSelection> CREATOR;
}
public static final class TextSelection.Builder {
@@ -50162,6 +50212,75 @@
}
+package android.view.textclassifier.logging {
+
+ public abstract class Logger {
+ ctor public Logger(android.view.textclassifier.logging.Logger.Config);
+ method public java.text.BreakIterator getTokenIterator(java.util.Locale);
+ method public boolean isSmartSelection(java.lang.String);
+ method public final void logSelectionActionEvent(int, int, int);
+ method public final void logSelectionActionEvent(int, int, int, android.view.textclassifier.TextClassification);
+ method public final void logSelectionModifiedEvent(int, int);
+ method public final void logSelectionModifiedEvent(int, int, android.view.textclassifier.TextClassification);
+ method public final void logSelectionModifiedEvent(int, int, android.view.textclassifier.TextSelection);
+ method public final void logSelectionStartedEvent(int);
+ method public abstract void writeEvent(android.view.textclassifier.logging.SelectionEvent);
+ field public static final int OUT_OF_BOUNDS = 2147483647; // 0x7fffffff
+ field public static final int OUT_OF_BOUNDS_NEGATIVE = -2147483648; // 0x80000000
+ field public static final java.lang.String WIDGET_CUSTOM_EDITTEXT = "customedit";
+ field public static final java.lang.String WIDGET_CUSTOM_TEXTVIEW = "customview";
+ field public static final java.lang.String WIDGET_CUSTOM_UNSELECTABLE_TEXTVIEW = "nosel-customview";
+ field public static final java.lang.String WIDGET_EDITTEXT = "edittext";
+ field public static final java.lang.String WIDGET_EDIT_WEBVIEW = "edit-webview";
+ field public static final java.lang.String WIDGET_TEXTVIEW = "textview";
+ field public static final java.lang.String WIDGET_UNKNOWN = "unknown";
+ field public static final java.lang.String WIDGET_UNSELECTABLE_TEXTVIEW = "nosel-textview";
+ field public static final java.lang.String WIDGET_WEBVIEW = "webview";
+ }
+
+ public static final class Logger.Config {
+ ctor public Logger.Config(android.content.Context, java.lang.String, java.lang.String);
+ method public java.lang.String getPackageName();
+ method public java.lang.String getWidgetType();
+ method public java.lang.String getWidgetVersion();
+ }
+
+ public final class SelectionEvent {
+ method public long getDurationSincePreviousEvent();
+ method public long getDurationSinceSessionStart();
+ method public int getEnd();
+ method public java.lang.String getEntityType();
+ method public int getEventIndex();
+ method public long getEventTime();
+ method public int getEventType();
+ method public java.lang.String getPackageName();
+ method public java.lang.String getSessionId();
+ method public java.lang.String getSignature();
+ method public int getSmartEnd();
+ method public int getSmartStart();
+ method public int getStart();
+ method public java.lang.String getWidgetType();
+ method public java.lang.String getWidgetVersion();
+ field public static final int ACTION_ABANDON = 107; // 0x6b
+ field public static final int ACTION_COPY = 101; // 0x65
+ field public static final int ACTION_CUT = 103; // 0x67
+ field public static final int ACTION_DRAG = 106; // 0x6a
+ field public static final int ACTION_OTHER = 108; // 0x6c
+ field public static final int ACTION_OVERTYPE = 100; // 0x64
+ field public static final int ACTION_PASTE = 102; // 0x66
+ field public static final int ACTION_RESET = 201; // 0xc9
+ field public static final int ACTION_SELECT_ALL = 200; // 0xc8
+ field public static final int ACTION_SHARE = 104; // 0x68
+ field public static final int ACTION_SMART_SHARE = 105; // 0x69
+ field public static final int EVENT_AUTO_SELECTION = 5; // 0x5
+ field public static final int EVENT_SELECTION_MODIFIED = 2; // 0x2
+ field public static final int EVENT_SELECTION_STARTED = 1; // 0x1
+ field public static final int EVENT_SMART_SELECTION_MULTI = 4; // 0x4
+ field public static final int EVENT_SMART_SELECTION_SINGLE = 3; // 0x3
+ }
+
+}
+
package android.view.textservice {
public final class SentenceSuggestionsInfo implements android.os.Parcelable {
@@ -52286,6 +52405,7 @@
ctor public Magnifier(android.view.View);
method public void dismiss();
method public void show(float, float);
+ method public void update();
}
public class MediaController extends android.widget.FrameLayout {
diff --git a/api/system-current.txt b/api/system-current.txt
index 5da5864..034ee30 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -29,6 +29,7 @@
field public static final java.lang.String BIND_RESOLVER_RANKER_SERVICE = "android.permission.BIND_RESOLVER_RANKER_SERVICE";
field public static final java.lang.String BIND_RUNTIME_PERMISSION_PRESENTER_SERVICE = "android.permission.BIND_RUNTIME_PERMISSION_PRESENTER_SERVICE";
field public static final java.lang.String BIND_SETTINGS_SUGGESTIONS_SERVICE = "android.permission.BIND_SETTINGS_SUGGESTIONS_SERVICE";
+ field public static final java.lang.String BIND_TEXTCLASSIFIER_SERVICE = "android.permission.BIND_TEXTCLASSIFIER_SERVICE";
field public static final java.lang.String BIND_TRUST_AGENT = "android.permission.BIND_TRUST_AGENT";
field public static final java.lang.String BIND_TV_REMOTE_SERVICE = "android.permission.BIND_TV_REMOTE_SERVICE";
field public static final java.lang.String BLUETOOTH_PRIVILEGED = "android.permission.BLUETOOTH_PRIVILEGED";
@@ -310,10 +311,8 @@
ctor public InstantAppResolverService();
method public final void attachBaseContext(android.content.Context);
method public final android.os.IBinder onBind(android.content.Intent);
- method public deprecated void onGetInstantAppIntentFilter(int[], java.lang.String, android.app.InstantAppResolverService.InstantAppResolutionCallback);
- method public void onGetInstantAppIntentFilter(android.content.Intent, int[], java.lang.String, android.app.InstantAppResolverService.InstantAppResolutionCallback);
- method public deprecated void onGetInstantAppResolveInfo(int[], java.lang.String, android.app.InstantAppResolverService.InstantAppResolutionCallback);
- method public void onGetInstantAppResolveInfo(android.content.Intent, int[], java.lang.String, android.app.InstantAppResolverService.InstantAppResolutionCallback);
+ method public void onGetInstantAppIntentFilter(int[], java.lang.String, android.app.InstantAppResolverService.InstantAppResolutionCallback);
+ method public void onGetInstantAppResolveInfo(int[], java.lang.String, android.app.InstantAppResolverService.InstantAppResolutionCallback);
}
public static final class InstantAppResolverService.InstantAppResolutionCallback {
@@ -822,14 +821,12 @@
field public static final java.lang.String ACTION_VOICE_ASSIST = "android.intent.action.VOICE_ASSIST";
field public static final java.lang.String CATEGORY_LEANBACK_SETTINGS = "android.intent.category.LEANBACK_SETTINGS";
field public static final java.lang.String EXTRA_FORCE_FACTORY_RESET = "android.intent.extra.FORCE_FACTORY_RESET";
- field public static final java.lang.String EXTRA_INSTANT_APP_BUNDLES = "android.intent.extra.INSTANT_APP_BUNDLES";
field public static final java.lang.String EXTRA_ORIGINATING_UID = "android.intent.extra.ORIGINATING_UID";
field public static final java.lang.String EXTRA_PACKAGES = "android.intent.extra.PACKAGES";
field public static final java.lang.String EXTRA_PERMISSION_NAME = "android.intent.extra.PERMISSION_NAME";
field public static final java.lang.String EXTRA_REASON = "android.intent.extra.REASON";
field public static final java.lang.String EXTRA_REMOTE_CALLBACK = "android.intent.extra.REMOTE_CALLBACK";
field public static final java.lang.String EXTRA_RESULT_NEEDED = "android.intent.extra.RESULT_NEEDED";
- field public static final java.lang.String EXTRA_UNKNOWN_INSTANT_APP = "android.intent.extra.UNKNOWN_INSTANT_APP";
}
public class IntentFilter implements android.os.Parcelable {
@@ -872,7 +869,6 @@
ctor public InstantAppResolveInfo(android.content.pm.InstantAppResolveInfo.InstantAppDigest, java.lang.String, java.util.List<android.content.pm.InstantAppIntentFilter>, int);
ctor public InstantAppResolveInfo(android.content.pm.InstantAppResolveInfo.InstantAppDigest, java.lang.String, java.util.List<android.content.pm.InstantAppIntentFilter>, long, android.os.Bundle);
ctor public InstantAppResolveInfo(java.lang.String, java.lang.String, java.util.List<android.content.pm.InstantAppIntentFilter>);
- ctor public InstantAppResolveInfo(android.os.Bundle);
method public int describeContents();
method public byte[] getDigestBytes();
method public int getDigestPrefix();
@@ -881,7 +877,6 @@
method public long getLongVersionCode();
method public java.lang.String getPackageName();
method public deprecated int getVersionCode();
- method public boolean shouldLetInstallerDecide();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<android.content.pm.InstantAppResolveInfo> CREATOR;
}
@@ -893,7 +888,6 @@
method public int[] getDigestPrefix();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<android.content.pm.InstantAppResolveInfo.InstantAppDigest> CREATOR;
- field public static final android.content.pm.InstantAppResolveInfo.InstantAppDigest UNDEFINED;
}
public final class IntentFilterVerificationInfo implements android.os.Parcelable {
@@ -4464,6 +4458,24 @@
}
+package android.service.textclassifier {
+
+ public abstract class TextClassifierService extends android.app.Service {
+ ctor public TextClassifierService();
+ method public final android.os.IBinder onBind(android.content.Intent);
+ method public abstract void onClassifyText(java.lang.CharSequence, int, int, android.view.textclassifier.TextClassification.Options, android.os.CancellationSignal, android.service.textclassifier.TextClassifierService.Callback<android.view.textclassifier.TextClassification>);
+ method public abstract void onGenerateLinks(java.lang.CharSequence, android.view.textclassifier.TextLinks.Options, android.os.CancellationSignal, android.service.textclassifier.TextClassifierService.Callback<android.view.textclassifier.TextLinks>);
+ method public abstract void onSuggestSelection(java.lang.CharSequence, int, int, android.view.textclassifier.TextSelection.Options, android.os.CancellationSignal, android.service.textclassifier.TextClassifierService.Callback<android.view.textclassifier.TextSelection>);
+ field public static final java.lang.String SERVICE_INTERFACE = "android.service.textclassifier.TextClassifierService";
+ }
+
+ public static abstract interface TextClassifierService.Callback<T> {
+ method public abstract void onFailure(java.lang.CharSequence);
+ method public abstract void onSuccess(T);
+ }
+
+}
+
package android.service.trust {
public class TrustAgentService extends android.app.Service {
diff --git a/api/test-current.txt b/api/test-current.txt
index 4e8f904..d834cf7 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -565,6 +565,14 @@
method public void apply(android.service.autofill.ValueFinder, android.widget.RemoteViews, int) throws java.lang.Exception;
}
+ public final class DateTransformation extends android.service.autofill.InternalTransformation implements android.os.Parcelable android.service.autofill.Transformation {
+ method public void apply(android.service.autofill.ValueFinder, android.widget.RemoteViews, int) throws java.lang.Exception;
+ }
+
+ public final class DateValueSanitizer extends android.service.autofill.InternalSanitizer implements android.os.Parcelable android.service.autofill.Sanitizer {
+ method public android.view.autofill.AutofillValue sanitize(android.view.autofill.AutofillValue);
+ }
+
public final class FillResponse implements android.os.Parcelable {
method public int getFlags();
}
@@ -599,7 +607,8 @@
}
public abstract interface ValueFinder {
- method public abstract java.lang.String findByAutofillId(android.view.autofill.AutofillId);
+ method public default java.lang.String findByAutofillId(android.view.autofill.AutofillId);
+ method public abstract android.view.autofill.AutofillValue findRawValueByAutofillId(android.view.autofill.AutofillId);
}
}
diff --git a/cmds/incidentd/src/Privacy.cpp b/cmds/incidentd/src/Privacy.cpp
index 44adaec..3f0e331 100644
--- a/cmds/incidentd/src/Privacy.cpp
+++ b/cmds/incidentd/src/Privacy.cpp
@@ -65,7 +65,7 @@
bool
PrivacySpec::RequireAll() const { return dest == android::os::DEST_LOCAL; }
-PrivacySpec new_spec_from_args(int dest)
+PrivacySpec PrivacySpec::new_spec(int dest)
{
switch (dest) {
case android::os::DEST_AUTOMATIC:
@@ -77,4 +77,7 @@
}
}
-PrivacySpec get_default_dropbox_spec() { return PrivacySpec(android::os::DEST_AUTOMATIC); }
\ No newline at end of file
+PrivacySpec PrivacySpec::get_default_dropbox_spec()
+{
+ return PrivacySpec(android::os::DEST_AUTOMATIC);
+}
diff --git a/cmds/incidentd/src/Privacy.h b/cmds/incidentd/src/Privacy.h
index 9e15ff4..4f3db67 100644
--- a/cmds/incidentd/src/Privacy.h
+++ b/cmds/incidentd/src/Privacy.h
@@ -65,8 +65,6 @@
const uint8_t dest;
PrivacySpec() : dest(DEST_DEFAULT_VALUE) {}
- PrivacySpec(uint8_t dest) : dest(dest) {}
-
bool operator<(const PrivacySpec& other) const;
// check permission of a policy, if returns true, don't strip the data.
@@ -74,9 +72,12 @@
// if returns true, no data need to be stripped.
bool RequireAll() const;
-};
-PrivacySpec new_spec_from_args(int dest);
-PrivacySpec get_default_dropbox_spec();
+ // Constructs spec using static methods below.
+ static PrivacySpec new_spec(int dest);
+ static PrivacySpec get_default_dropbox_spec();
+private:
+ PrivacySpec(uint8_t dest) : dest(dest) {}
+};
#endif // PRIVACY_H
diff --git a/cmds/incidentd/src/Reporter.cpp b/cmds/incidentd/src/Reporter.cpp
index bd559d6..b9f479b 100644
--- a/cmds/incidentd/src/Reporter.cpp
+++ b/cmds/incidentd/src/Reporter.cpp
@@ -64,7 +64,8 @@
ReportRequestSet::ReportRequestSet()
:mRequests(),
mSections(),
- mMainFd(-1)
+ mMainFd(-1),
+ mMainDest(-1)
{
}
@@ -86,6 +87,12 @@
mMainFd = fd;
}
+void
+ReportRequestSet::setMainDest(int dest)
+{
+ mMainDest = dest;
+}
+
bool
ReportRequestSet::containsSection(int id) {
return mSections.containsSection(id);
@@ -125,12 +132,14 @@
status_t err = NO_ERROR;
bool needMainFd = false;
int mainFd = -1;
+ int mainDest = -1;
HeaderSection headers;
// See if we need the main file
for (ReportRequestSet::iterator it=batch.begin(); it!=batch.end(); it++) {
if ((*it)->fd < 0 && mainFd < 0) {
needMainFd = true;
+ mainDest = (*it)->args.dest();
break;
}
}
@@ -154,6 +163,7 @@
// Add to the set
batch.setMainFd(mainFd);
+ batch.setMainDest(mainDest);
}
// Tell everyone that we're starting.
diff --git a/cmds/incidentd/src/Reporter.h b/cmds/incidentd/src/Reporter.h
index 2615c62..f30ecf0 100644
--- a/cmds/incidentd/src/Reporter.h
+++ b/cmds/incidentd/src/Reporter.h
@@ -53,6 +53,7 @@
void add(const sp<ReportRequest>& request);
void setMainFd(int fd);
+ void setMainDest(int dest);
typedef vector<sp<ReportRequest>>::iterator iterator;
@@ -61,10 +62,12 @@
int mainFd() { return mMainFd; }
bool containsSection(int id);
+ int mainDest() { return mMainDest; }
private:
vector<sp<ReportRequest>> mRequests;
IncidentReportArgs mSections;
int mMainFd;
+ int mMainDest;
};
// ================================================================================
diff --git a/cmds/incidentd/src/Section.cpp b/cmds/incidentd/src/Section.cpp
index 0827785..faeab87 100644
--- a/cmds/incidentd/src/Section.cpp
+++ b/cmds/incidentd/src/Section.cpp
@@ -152,36 +152,40 @@
// The streaming ones, group requests by spec in order to save unnecessary strip operations
map<PrivacySpec, vector<sp<ReportRequest>>> requestsBySpec;
- for (ReportRequestSet::iterator it = requests->begin(); it != requests->end(); it++) {
+ for (auto it = requests->begin(); it != requests->end(); it++) {
sp<ReportRequest> request = *it;
if (!request->ok() || !request->args.containsSection(id)) {
continue; // skip invalid request
}
- PrivacySpec spec = new_spec_from_args(request->args.dest());
+ PrivacySpec spec = PrivacySpec::new_spec(request->args.dest());
requestsBySpec[spec].push_back(request);
}
- for (map<PrivacySpec, vector<sp<ReportRequest>>>::iterator mit = requestsBySpec.begin(); mit != requestsBySpec.end(); mit++) {
+ for (auto mit = requestsBySpec.begin(); mit != requestsBySpec.end(); mit++) {
PrivacySpec spec = mit->first;
err = privacyBuffer.strip(spec);
if (err != NO_ERROR) return err; // it means the privacyBuffer data is corrupted.
if (privacyBuffer.size() == 0) continue;
- for (vector<sp<ReportRequest>>::iterator it = mit->second.begin(); it != mit->second.end(); it++) {
+ for (auto it = mit->second.begin(); it != mit->second.end(); it++) {
sp<ReportRequest> request = *it;
err = write_section_header(request->fd, id, privacyBuffer.size());
if (err != NO_ERROR) { request->err = err; continue; }
err = privacyBuffer.flush(request->fd);
if (err != NO_ERROR) { request->err = err; continue; }
writeable++;
- ALOGD("Section %d flushed %zu bytes to fd %d with spec %d", id, privacyBuffer.size(), request->fd, spec.dest);
+ ALOGD("Section %d flushed %zu bytes to fd %d with spec %d", id,
+ privacyBuffer.size(), request->fd, spec.dest);
}
privacyBuffer.clear();
}
// The dropbox file
if (requests->mainFd() >= 0) {
- err = privacyBuffer.strip(get_default_dropbox_spec());
+ PrivacySpec spec = requests->mainDest() < 0 ?
+ PrivacySpec::get_default_dropbox_spec() :
+ PrivacySpec::new_spec(requests->mainDest());
+ err = privacyBuffer.strip(spec);
if (err != NO_ERROR) return err; // the buffer data is corrupted.
if (privacyBuffer.size() == 0) goto DONE;
@@ -190,7 +194,8 @@
err = privacyBuffer.flush(requests->mainFd());
if (err != NO_ERROR) { requests->setMainFd(-1); goto DONE; }
writeable++;
- ALOGD("Section %d flushed %zu bytes to dropbox %d", id, privacyBuffer.size(), requests->mainFd());
+ ALOGD("Section %d flushed %zu bytes to dropbox %d with spec %d", id,
+ privacyBuffer.size(), requests->mainFd(), spec.dest);
}
DONE:
diff --git a/cmds/incidentd/tests/PrivacyBuffer_test.cpp b/cmds/incidentd/tests/PrivacyBuffer_test.cpp
index 32b9e42..c7bfe55 100644
--- a/cmds/incidentd/tests/PrivacyBuffer_test.cpp
+++ b/cmds/incidentd/tests/PrivacyBuffer_test.cpp
@@ -73,7 +73,7 @@
}
void assertStrip(uint8_t dest, string expected, Privacy* policy) {
- PrivacySpec spec(dest);
+ PrivacySpec spec = PrivacySpec::new_spec(dest);
EncodedBuffer::iterator bufData = buffer.data();
PrivacyBuffer privacyBuf(policy, bufData);
ASSERT_EQ(privacyBuf.strip(spec), NO_ERROR);
@@ -224,7 +224,8 @@
Privacy* list[] = { create_privacy(1, OTHER_TYPE, DEST_LOCAL), NULL };
EncodedBuffer::iterator bufData = buffer.data();
PrivacyBuffer privacyBuf(create_message_privacy(300, list), bufData);
- PrivacySpec spec1(DEST_EXPLICIT), spec2(DEST_LOCAL);
+ PrivacySpec spec1 = PrivacySpec::new_spec(DEST_EXPLICIT);
+ PrivacySpec spec2 = PrivacySpec::new_spec(DEST_LOCAL);
ASSERT_EQ(privacyBuf.strip(spec1), NO_ERROR);
assertBuffer(privacyBuf, STRING_FIELD_0);
diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto
index b32af02..4e570a6 100644
--- a/cmds/statsd/src/atoms.proto
+++ b/cmds/statsd/src/atoms.proto
@@ -93,6 +93,8 @@
WifiMulticastLockStateChanged wifi_multicast_lock_state_changed = 53;
LmkStateChanged lmk_state_changed = 54;
AppStartMemoryStateCaptured app_start_memory_state_captured = 55;
+ ShutdownSequenceReported shutdown_sequence_reported = 56;
+ BootSequenceReported boot_sequence_reported = 57;
// TODO: Reorder the numbering so that the most frequent occur events occur in the first 15.
}
@@ -669,6 +671,56 @@
}
/**
+ * Logs shutdown reason and duration on next boot.
+ *
+ * Logged from:
+ * frameworks/base/core/java/com/android/server/BootReceiver.java
+ */
+message ShutdownSequenceReported {
+ // True if shutdown is for a reboot. Default: false if we do not know.
+ optional bool reboot = 1;
+
+ // Reason for shutdown. Eg: userrequested. Default: "<EMPTY>".
+ optional string reason = 2;
+
+ // Beginning of shutdown time in ms using wall clock time since unix epoch.
+ // Default: 0 if no start time received.
+ optional int64 start_time_ms = 3;
+
+ // Duration of shutdown in ms. Default: 0 if no duration received.
+ optional int64 duration_ms = 4;
+}
+
+
+/**
+ * Logs boot reason and duration.
+ *
+ * Logged from:
+ * system/core/bootstat/bootstat.cpp
+ */
+message BootSequenceReported {
+ // Reason for bootloader boot. Eg. reboot. See bootstat.cpp for larger list
+ // Default: "<EMPTY>" if not available.
+ optional string bootloader_reason = 1;
+
+ // Reason for system boot. Eg. bootloader, reboot,userrequested
+ // Default: "<EMPTY>" if not available.
+ optional string system_reason = 2;
+
+ // End of boot time in ms from unix epoch using system wall clock.
+ optional int64 end_time_ms = 3;
+
+ // Total boot duration in ms.
+ optional int64 total_duration_ms = 4;
+
+ // Bootloader duration in ms.
+ optional int64 bootloader_duration_ms = 5;
+
+ // Time since last boot in ms. Default: 0 if not available.
+ optional int64 time_since_last_boot = 6;
+}
+
+/**
* Logs phone signal strength changes.
*
* Logged from:
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 6c3dbf4..e8535cd1 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -3969,8 +3969,7 @@
}
r.activity.mConfigChangeFlags |= configChanges;
- performPauseActivity(token, finished, r.isPreHoneycomb(), "handlePauseActivity",
- pendingActions);
+ performPauseActivity(r, finished, "handlePauseActivity", pendingActions);
// Make sure any pending writes are now committed.
if (r.isPreHoneycomb()) {
@@ -3993,16 +3992,18 @@
mInstrumentation.callActivityOnUserLeaving(r.activity);
}
- final Bundle performPauseActivity(IBinder token, boolean finished,
- boolean saveState, String reason, PendingTransactionActions pendingActions) {
+ final Bundle performPauseActivity(IBinder token, boolean finished, String reason,
+ PendingTransactionActions pendingActions) {
ActivityClientRecord r = mActivities.get(token);
- return r != null
- ? performPauseActivity(r, finished, saveState, reason, pendingActions)
- : null;
+ return r != null ? performPauseActivity(r, finished, reason, pendingActions) : null;
}
- private Bundle performPauseActivity(ActivityClientRecord r, boolean finished, boolean saveState,
- String reason, PendingTransactionActions pendingActions) {
+ /**
+ * Pause the activity.
+ * @return Saved instance state for pre-Honeycomb apps if it was saved, {@code null} otherwise.
+ */
+ private Bundle performPauseActivity(ActivityClientRecord r, boolean finished, String reason,
+ PendingTransactionActions pendingActions) {
if (r.paused) {
if (r.activity.mFinished) {
// If we are finishing, we won't call onResume() in certain cases.
@@ -4019,9 +4020,10 @@
r.activity.mFinished = true;
}
- // Next have the activity save its current state and managed dialogs...
- if (!r.activity.mFinished && saveState) {
- callCallActivityOnSaveInstanceState(r);
+ // Pre-Honeycomb apps always save their state before pausing
+ final boolean shouldSaveState = !r.activity.mFinished && r.isPreHoneycomb();
+ if (shouldSaveState) {
+ callActivityOnSaveInstanceState(r);
}
performPauseActivityIfNeeded(r, reason);
@@ -4048,7 +4050,7 @@
}
}
- return !r.activity.mFinished && saveState ? r.state : null;
+ return shouldSaveState ? r.state : null;
}
private void performPauseActivityIfNeeded(ActivityClientRecord r, String reason) {
@@ -4149,32 +4151,42 @@
}
}
- // Next have the activity save its current state and managed dialogs...
- if (!r.activity.mFinished && saveState) {
- if (r.state == null) {
- callCallActivityOnSaveInstanceState(r);
- }
- }
-
if (!keepShown) {
- try {
- // Now we are idle.
- r.activity.performStop(false /*preserveWindow*/);
- } catch (Exception e) {
- if (!mInstrumentation.onException(r.activity, e)) {
- throw new RuntimeException(
- "Unable to stop activity "
- + r.intent.getComponent().toShortString()
- + ": " + e.toString(), e);
- }
- }
- r.setState(ON_STOP);
- EventLog.writeEvent(LOG_AM_ON_STOP_CALLED, UserHandle.myUserId(),
- r.activity.getComponentName().getClassName(), reason);
+ callActivityOnStop(r, saveState, reason);
}
}
}
+ /**
+ * Calls {@link Activity#onStop()} and {@link Activity#onSaveInstanceState(Bundle)}, and updates
+ * the client record's state.
+ * All calls to stop an activity must be done through this method to make sure that
+ * {@link Activity#onSaveInstanceState(Bundle)} is also executed in the same call.
+ */
+ private void callActivityOnStop(ActivityClientRecord r, boolean saveState, String reason) {
+ final boolean shouldSaveState = saveState && !r.activity.mFinished && r.state == null
+ && !r.isPreHoneycomb();
+ if (shouldSaveState) {
+ callActivityOnSaveInstanceState(r);
+ }
+
+ try {
+ r.activity.performStop(false /*preserveWindow*/);
+ } catch (SuperNotCalledException e) {
+ throw e;
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(r.activity, e)) {
+ throw new RuntimeException(
+ "Unable to stop activity "
+ + r.intent.getComponent().toShortString()
+ + ": " + e.toString(), e);
+ }
+ }
+ r.setState(ON_STOP);
+ EventLog.writeEvent(LOG_AM_ON_STOP_CALLED, UserHandle.myUserId(),
+ r.activity.getComponentName().getClassName(), reason);
+ }
+
private void updateVisibility(ActivityClientRecord r, boolean show) {
View v = r.activity.mDecor;
if (v != null) {
@@ -4292,24 +4304,7 @@
if (sleeping) {
if (!r.stopped && !r.isPreHoneycomb()) {
- if (!r.activity.mFinished && r.state == null) {
- callCallActivityOnSaveInstanceState(r);
- }
-
- try {
- // Now we are idle.
- r.activity.performStop(false /*preserveWindow*/);
- } catch (Exception e) {
- if (!mInstrumentation.onException(r.activity, e)) {
- throw new RuntimeException(
- "Unable to stop activity "
- + r.intent.getComponent().toShortString()
- + ": " + e.toString(), e);
- }
- }
- r.setState(ON_STOP);
- EventLog.writeEvent(LOG_AM_ON_STOP_CALLED, UserHandle.myUserId(),
- r.activity.getComponentName().getClassName(), "sleeping");
+ callActivityOnStop(r, true /* saveState */, "sleeping");
}
// Make sure any pending writes are now committed.
@@ -4459,21 +4454,7 @@
performPauseActivityIfNeeded(r, "destroy");
if (!r.stopped) {
- try {
- r.activity.performStop(r.mPreserveWindow);
- } catch (SuperNotCalledException e) {
- throw e;
- } catch (Exception e) {
- if (!mInstrumentation.onException(r.activity, e)) {
- throw new RuntimeException(
- "Unable to stop activity "
- + safeToComponentShortString(r.intent)
- + ": " + e.toString(), e);
- }
- }
- r.setState(ON_STOP);
- EventLog.writeEvent(LOG_AM_ON_STOP_CALLED, UserHandle.myUserId(),
- r.activity.getComponentName().getClassName(), "destroy");
+ callActivityOnStop(r, false /* saveState */, "destroy");
}
if (getNonConfigInstance) {
try {
@@ -4779,11 +4760,11 @@
// Need to ensure state is saved.
if (!r.paused) {
- performPauseActivity(r.token, false, r.isPreHoneycomb(), "handleRelaunchActivity",
+ performPauseActivity(r, false, "handleRelaunchActivity",
null /* pendingActions */);
}
- if (r.state == null && !r.stopped && !r.isPreHoneycomb()) {
- callCallActivityOnSaveInstanceState(r);
+ if (!r.stopped) {
+ callActivityOnStop(r, true /* saveState */, "handleRelaunchActivity");
}
handleDestroyActivity(r.token, false, configChanges, true);
@@ -4816,8 +4797,7 @@
handleStartActivity(r, pendingActions);
handleResumeActivity(r.token, false /* clearHide */, r.isForward, "relaunch");
if (r.startsNotResumed) {
- performPauseActivity(r, false /* finished */, r.isPreHoneycomb(), "relaunch",
- pendingActions);
+ performPauseActivity(r, false /* finished */, "relaunch", pendingActions);
}
if (!tmp.onlyLocalRequest) {
@@ -4832,7 +4812,7 @@
}
}
- private void callCallActivityOnSaveInstanceState(ActivityClientRecord r) {
+ private void callActivityOnSaveInstanceState(ActivityClientRecord r) {
r.state = new Bundle();
r.state.setAllowFds(false);
if (r.isPersistable()) {
diff --git a/core/java/android/app/EphemeralResolverService.java b/core/java/android/app/EphemeralResolverService.java
index d1c2441..427a0386 100644
--- a/core/java/android/app/EphemeralResolverService.java
+++ b/core/java/android/app/EphemeralResolverService.java
@@ -17,10 +17,20 @@
package android.app;
import android.annotation.SystemApi;
+import android.app.Service;
+import android.app.InstantAppResolverService.InstantAppResolutionCallback;
+import android.content.Context;
+import android.content.Intent;
import android.content.pm.EphemeralResolveInfo;
import android.content.pm.InstantAppResolveInfo;
import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.IRemoteCallback;
import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
import android.util.Log;
import java.util.ArrayList;
@@ -75,6 +85,7 @@
return super.getLooper();
}
+ @Override
void _onGetInstantAppResolveInfo(int[] digestPrefix, String token,
InstantAppResolutionCallback callback) {
if (DEBUG_EPHEMERAL) {
@@ -90,6 +101,7 @@
callback.onInstantAppResolveInfo(resultList);
}
+ @Override
void _onGetInstantAppIntentFilter(int[] digestPrefix, String token,
String hostName, InstantAppResolutionCallback callback) {
if (DEBUG_EPHEMERAL) {
diff --git a/core/java/android/app/IInstantAppResolver.aidl b/core/java/android/app/IInstantAppResolver.aidl
index ae20057..805d8c0 100644
--- a/core/java/android/app/IInstantAppResolver.aidl
+++ b/core/java/android/app/IInstantAppResolver.aidl
@@ -16,15 +16,13 @@
package android.app;
-import android.content.Intent;
import android.os.IRemoteCallback;
/** @hide */
oneway interface IInstantAppResolver {
- void getInstantAppResolveInfoList(in Intent sanitizedIntent, in int[] hostDigestPrefix,
+ void getInstantAppResolveInfoList(in int[] digestPrefix,
String token, int sequence, IRemoteCallback callback);
- void getInstantAppIntentFilterList(in Intent sanitizedIntent, in int[] hostDigestPrefix,
- String token, IRemoteCallback callback);
-
+ void getInstantAppIntentFilterList(in int[] digestPrefix,
+ String token, String hostName, IRemoteCallback callback);
}
diff --git a/core/java/android/app/InstantAppResolverService.java b/core/java/android/app/InstantAppResolverService.java
index 89aff36..c5dc86c 100644
--- a/core/java/android/app/InstantAppResolverService.java
+++ b/core/java/android/app/InstantAppResolverService.java
@@ -17,6 +17,7 @@
package android.app;
import android.annotation.SystemApi;
+import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.pm.InstantAppResolveInfo;
@@ -34,7 +35,6 @@
import com.android.internal.os.SomeArgs;
import java.util.Arrays;
-import java.util.Collections;
import java.util.List;
/**
@@ -53,65 +53,23 @@
Handler mHandler;
/**
- * Called to retrieve resolve info for instant applications immediately.
+ * Called to retrieve resolve info for instant applications.
*
* @param digestPrefix The hash prefix of the instant app's domain.
- * @deprecated should implement {@link #onGetInstantAppResolveInfo(Intent, int[], String,
- * InstantAppResolutionCallback)}
*/
- @Deprecated
public void onGetInstantAppResolveInfo(
int digestPrefix[], String token, InstantAppResolutionCallback callback) {
throw new IllegalStateException("Must define");
}
/**
- * Called to retrieve intent filters for instant applications from potentially expensive
- * sources.
+ * Called to retrieve intent filters for instant applications.
*
* @param digestPrefix The hash prefix of the instant app's domain.
- * @deprecated should implement {@link #onGetInstantAppIntentFilter(Intent, int[], String,
- * InstantAppResolutionCallback)}
*/
- @Deprecated
public void onGetInstantAppIntentFilter(
int digestPrefix[], String token, InstantAppResolutionCallback callback) {
- throw new IllegalStateException("Must define onGetInstantAppIntentFilter");
- }
-
- /**
- * Called to retrieve resolve info for instant applications immediately.
- *
- * @param sanitizedIntent The sanitized {@link Intent} used for resolution.
- * @param hostDigestPrefix The hash prefix of the instant app's domain.
- */
- public void onGetInstantAppResolveInfo(Intent sanitizedIntent, int[] hostDigestPrefix,
- String token, InstantAppResolutionCallback callback) {
- // if not overridden, forward to old methods and filter out non-web intents
- if (sanitizedIntent.isBrowsableWebIntent()) {
- onGetInstantAppResolveInfo(hostDigestPrefix, token, callback);
- } else {
- callback.onInstantAppResolveInfo(Collections.emptyList());
- }
- }
-
- /**
- * Called to retrieve intent filters for instant applications from potentially expensive
- * sources.
- *
- * @param sanitizedIntent The sanitized {@link Intent} used for resolution.
- * @param hostDigestPrefix The hash prefix of the instant app's domain or null if no host is
- * defined.
- */
- public void onGetInstantAppIntentFilter(Intent sanitizedIntent, int[] hostDigestPrefix,
- String token, InstantAppResolutionCallback callback) {
- Log.e(TAG, "New onGetInstantAppIntentFilter is not overridden");
- // if not overridden, forward to old methods and filter out non-web intents
- if (sanitizedIntent.isBrowsableWebIntent()) {
- onGetInstantAppIntentFilter(hostDigestPrefix, token, callback);
- } else {
- callback.onInstantAppResolveInfo(Collections.emptyList());
- }
+ throw new IllegalStateException("Must define");
}
/**
@@ -131,8 +89,8 @@
public final IBinder onBind(Intent intent) {
return new IInstantAppResolver.Stub() {
@Override
- public void getInstantAppResolveInfoList(Intent sanitizedIntent, int[] digestPrefix,
- String token, int sequence, IRemoteCallback callback) {
+ public void getInstantAppResolveInfoList(
+ int digestPrefix[], String token, int sequence, IRemoteCallback callback) {
if (DEBUG_EPHEMERAL) {
Slog.v(TAG, "[" + token + "] Phase1 called; posting");
}
@@ -140,14 +98,14 @@
args.arg1 = callback;
args.arg2 = digestPrefix;
args.arg3 = token;
- args.arg4 = sanitizedIntent;
- mHandler.obtainMessage(ServiceHandler.MSG_GET_INSTANT_APP_RESOLVE_INFO,
- sequence, 0, args).sendToTarget();
+ mHandler.obtainMessage(
+ ServiceHandler.MSG_GET_INSTANT_APP_RESOLVE_INFO, sequence, 0, args)
+ .sendToTarget();
}
@Override
- public void getInstantAppIntentFilterList(Intent sanitizedIntent,
- int[] digestPrefix, String token, IRemoteCallback callback) {
+ public void getInstantAppIntentFilterList(
+ int digestPrefix[], String token, String hostName, IRemoteCallback callback) {
if (DEBUG_EPHEMERAL) {
Slog.v(TAG, "[" + token + "] Phase2 called; posting");
}
@@ -155,9 +113,9 @@
args.arg1 = callback;
args.arg2 = digestPrefix;
args.arg3 = token;
- args.arg4 = sanitizedIntent;
- mHandler.obtainMessage(ServiceHandler.MSG_GET_INSTANT_APP_INTENT_FILTER,
- callback).sendToTarget();
+ args.arg4 = hostName;
+ mHandler.obtainMessage(
+ ServiceHandler.MSG_GET_INSTANT_APP_INTENT_FILTER, callback).sendToTarget();
}
};
}
@@ -184,9 +142,29 @@
}
}
+ @Deprecated
+ void _onGetInstantAppResolveInfo(int[] digestPrefix, String token,
+ InstantAppResolutionCallback callback) {
+ if (DEBUG_EPHEMERAL) {
+ Slog.d(TAG, "[" + token + "] Phase1 request;"
+ + " prefix: " + Arrays.toString(digestPrefix));
+ }
+ onGetInstantAppResolveInfo(digestPrefix, token, callback);
+ }
+ @Deprecated
+ void _onGetInstantAppIntentFilter(int digestPrefix[], String token, String hostName,
+ InstantAppResolutionCallback callback) {
+ if (DEBUG_EPHEMERAL) {
+ Slog.d(TAG, "[" + token + "] Phase2 request;"
+ + " prefix: " + Arrays.toString(digestPrefix));
+ }
+ onGetInstantAppIntentFilter(digestPrefix, token, callback);
+ }
+
private final class ServiceHandler extends Handler {
public static final int MSG_GET_INSTANT_APP_RESOLVE_INFO = 1;
public static final int MSG_GET_INSTANT_APP_INTENT_FILTER = 2;
+
public ServiceHandler(Looper looper) {
super(looper, null /*callback*/, true /*async*/);
}
@@ -201,13 +179,9 @@
final IRemoteCallback callback = (IRemoteCallback) args.arg1;
final int[] digestPrefix = (int[]) args.arg2;
final String token = (String) args.arg3;
- final Intent intent = (Intent) args.arg4;
final int sequence = message.arg1;
- if (DEBUG_EPHEMERAL) {
- Slog.d(TAG, "[" + token + "] Phase1 request;"
- + " prefix: " + Arrays.toString(digestPrefix));
- }
- onGetInstantAppResolveInfo(intent, digestPrefix, token,
+ _onGetInstantAppResolveInfo(
+ digestPrefix, token,
new InstantAppResolutionCallback(sequence, callback));
} break;
@@ -216,12 +190,9 @@
final IRemoteCallback callback = (IRemoteCallback) args.arg1;
final int[] digestPrefix = (int[]) args.arg2;
final String token = (String) args.arg3;
- final Intent intent = (Intent) args.arg4;
- if (DEBUG_EPHEMERAL) {
- Slog.d(TAG, "[" + token + "] Phase2 request;"
- + " prefix: " + Arrays.toString(digestPrefix));
- }
- onGetInstantAppIntentFilter(intent, digestPrefix, token,
+ final String hostName = (String) args.arg4;
+ _onGetInstantAppIntentFilter(
+ digestPrefix, token, hostName,
new InstantAppResolutionCallback(-1 /*sequence*/, callback));
} break;
diff --git a/core/java/android/app/LocalActivityManager.java b/core/java/android/app/LocalActivityManager.java
index c34f4d9..1d34595 100644
--- a/core/java/android/app/LocalActivityManager.java
+++ b/core/java/android/app/LocalActivityManager.java
@@ -224,8 +224,8 @@
private void performPause(LocalActivityRecord r, boolean finishing) {
final boolean needState = r.instanceState == null;
- final Bundle instanceState = mActivityThread.performPauseActivity(
- r, finishing, needState, "performPause", null /* pendingActions */);
+ final Bundle instanceState = mActivityThread.performPauseActivity(r, finishing,
+ "performPause", null /* pendingActions */);
if (needState) {
r.instanceState = instanceState;
}
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 131abb5..e190fd4 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -6609,15 +6609,81 @@
}
/**
+ * Indicates user operation is successful.
+ *
+ * @see #startUserInBackground(ComponentName, UserHandle)
+ * @see #stopUser(ComponentName, UserHandle)
+ * @see #logoutUser(ComponentName)
+ */
+ public static final int USER_OPERATION_SUCCESS = 0;
+
+ /**
+ * Indicates user operation failed for unknown reason.
+ *
+ * @see #startUserInBackground(ComponentName, UserHandle)
+ * @see #stopUser(ComponentName, UserHandle)
+ * @see #logoutUser(ComponentName)
+ */
+ public static final int USER_OPERATION_ERROR_UNKNOWN = 1;
+
+ /**
+ * Indicates user operation failed because target user is a managed profile.
+ *
+ * @see #startUserInBackground(ComponentName, UserHandle)
+ * @see #stopUser(ComponentName, UserHandle)
+ * @see #logoutUser(ComponentName)
+ */
+ public static final int USER_OPERATION_ERROR_MANAGED_PROFILE = 2;
+
+ /**
+ * Indicates user operation failed because maximum running user limit has reached.
+ *
+ * @see #startUserInBackground(ComponentName, UserHandle)
+ */
+ public static final int USER_OPERATION_ERROR_MAX_RUNNING_USERS = 3;
+
+ /**
+ * Indicates user operation failed because the target user is in foreground.
+ *
+ * @see #stopUser(ComponentName, UserHandle)
+ * @see #logoutUser(ComponentName)
+ */
+ public static final int USER_OPERATION_ERROR_CURRENT_USER = 4;
+
+ /**
+ * Result returned from
+ * <ul>
+ * <li>{@link #startUserInBackground(ComponentName, UserHandle)}</li>
+ * <li>{@link #stopUser(ComponentName, UserHandle)}</li>
+ * <li>{@link #logoutUser(ComponentName)}</li>
+ * </ul>
+ *
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "USER_OPERATION_" }, value = {
+ USER_OPERATION_SUCCESS,
+ USER_OPERATION_ERROR_UNKNOWN,
+ USER_OPERATION_ERROR_MANAGED_PROFILE,
+ USER_OPERATION_ERROR_MAX_RUNNING_USERS,
+ USER_OPERATION_ERROR_CURRENT_USER
+ })
+ public @interface UserOperationResult {}
+
+ /**
* Called by a device owner to start the specified secondary user in background.
*
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
- * @param userHandle the user to be stopped.
- * @return {@code true} if the user can be started, {@code false} otherwise.
+ * @param userHandle the user to be started in background.
+ * @return one of the following result codes:
+ * {@link #USER_OPERATION_ERROR_UNKNOWN},
+ * {@link #USER_OPERATION_SUCCESS},
+ * {@link #USER_OPERATION_ERROR_MANAGED_PROFILE},
+ * {@link #USER_OPERATION_ERROR_MAX_RUNNING_USERS},
* @throws SecurityException if {@code admin} is not a device owner.
* @see #getSecondaryUsers(ComponentName)
*/
- public boolean startUserInBackground(
+ public @UserOperationResult int startUserInBackground(
@NonNull ComponentName admin, @NonNull UserHandle userHandle) {
throwIfParentInstance("startUserInBackground");
try {
@@ -6632,11 +6698,16 @@
*
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @param userHandle the user to be stopped.
- * @return {@code true} if the user can be stopped, {@code false} otherwise.
+ * @return one of the following result codes:
+ * {@link #USER_OPERATION_ERROR_UNKNOWN},
+ * {@link #USER_OPERATION_SUCCESS},
+ * {@link #USER_OPERATION_ERROR_MANAGED_PROFILE},
+ * {@link #USER_OPERATION_ERROR_CURRENT_USER}
* @throws SecurityException if {@code admin} is not a device owner.
* @see #getSecondaryUsers(ComponentName)
*/
- public boolean stopUser(@NonNull ComponentName admin, @NonNull UserHandle userHandle) {
+ public @UserOperationResult int stopUser(
+ @NonNull ComponentName admin, @NonNull UserHandle userHandle) {
throwIfParentInstance("stopUser");
try {
return mService.stopUser(admin, userHandle);
@@ -6650,11 +6721,15 @@
* calling user and switch back to primary.
*
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
- * @return {@code true} if the exit was successful, {@code false} otherwise.
+ * @return one of the following result codes:
+ * {@link #USER_OPERATION_ERROR_UNKNOWN},
+ * {@link #USER_OPERATION_SUCCESS},
+ * {@link #USER_OPERATION_ERROR_MANAGED_PROFILE},
+ * {@link #USER_OPERATION_ERROR_CURRENT_USER}
* @throws SecurityException if {@code admin} is not a profile owner affiliated with the device.
* @see #getSecondaryUsers(ComponentName)
*/
- public boolean logoutUser(@NonNull ComponentName admin) {
+ public @UserOperationResult int logoutUser(@NonNull ComponentName admin) {
throwIfParentInstance("logoutUser");
try {
return mService.logoutUser(admin);
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index cba9311..5197de4 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -227,9 +227,9 @@
UserHandle createAndManageUser(in ComponentName who, in String name, in ComponentName profileOwner, in PersistableBundle adminExtras, in int flags);
boolean removeUser(in ComponentName who, in UserHandle userHandle);
boolean switchUser(in ComponentName who, in UserHandle userHandle);
- boolean startUserInBackground(in ComponentName who, in UserHandle userHandle);
- boolean stopUser(in ComponentName who, in UserHandle userHandle);
- boolean logoutUser(in ComponentName who);
+ int startUserInBackground(in ComponentName who, in UserHandle userHandle);
+ int stopUser(in ComponentName who, in UserHandle userHandle);
+ int logoutUser(in ComponentName who);
List<UserHandle> getSecondaryUsers(in ComponentName who);
void enableSystemApp(in ComponentName admin, in String callerPackage, in String packageName);
diff --git a/core/java/android/app/slice/Slice.java b/core/java/android/app/slice/Slice.java
index 4c24a2d..126deef 100644
--- a/core/java/android/app/slice/Slice.java
+++ b/core/java/android/app/slice/Slice.java
@@ -164,9 +164,15 @@
public static final String EXTRA_TOGGLE_STATE = "android.app.slice.extra.TOGGLE_STATE";
/**
* Key to retrieve an extra added to an intent when the value of a slider is changed.
+ * @deprecated remove once support lib is update to use EXTRA_RANGE_VALUE instead
*/
+ @Deprecated
public static final String EXTRA_SLIDER_VALUE = "android.app.slice.extra.SLIDER_VALUE";
/**
+ * Key to retrieve an extra added to an intent when the value of an input range is changed.
+ */
+ public static final String EXTRA_RANGE_VALUE = "android.app.slice.extra.RANGE_VALUE";
+ /**
* Subtype to indicate that this is a message as part of a communication
* sequence in this slice.
*/
@@ -181,14 +187,20 @@
public static final String SUBTYPE_COLOR = "color";
/**
* Subtype to tag an item as representing a slider.
+ * @deprecated remove once support lib is update to use SUBTYPE_RANGE instead
*/
+ @Deprecated
public static final String SUBTYPE_SLIDER = "slider";
/**
- * Subtype to tag an item as representing the max int value for a {@link #SUBTYPE_SLIDER}.
+ * Subtype to tag an item as representing a range.
+ */
+ public static final String SUBTYPE_RANGE = "range";
+ /**
+ * Subtype to tag an item as representing the max int value for a {@link #SUBTYPE_RANGE}.
*/
public static final String SUBTYPE_MAX = "max";
/**
- * Subtype to tag an item as representing the current int value for a {@link #SUBTYPE_SLIDER}.
+ * Subtype to tag an item as representing the current int value for a {@link #SUBTYPE_RANGE}.
*/
public static final String SUBTYPE_VALUE = "value";
/**
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index b3c8737..e02a294 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -50,7 +50,6 @@
import android.provider.DocumentsProvider;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
-import android.text.TextUtils;
import android.util.ArraySet;
import android.util.AttributeSet;
import android.util.Log;
@@ -4473,15 +4472,7 @@
public static final String EXTRA_INSTANT_APP_ACTION = "android.intent.extra.INSTANT_APP_ACTION";
/**
- * An array of {@link Bundle}s containing details about resolved instant apps..
- * @hide
- */
- @SystemApi
- public static final String EXTRA_INSTANT_APP_BUNDLES =
- "android.intent.extra.INSTANT_APP_BUNDLES";
-
- /**
- * A {@link Bundle} of metadata that describes the instant application that needs to be
+ * A {@link Bundle} of metadata that describes the instanta application that needs to be
* installed. This data is populated from the response to
* {@link android.content.pm.InstantAppResolveInfo#getExtras()} as provided by the registered
* instant application resolver.
@@ -4491,16 +4482,6 @@
"android.intent.extra.INSTANT_APP_EXTRAS";
/**
- * A boolean value indicating that the instant app resolver was unable to state with certainty
- * that it did or did not have an app for the sanitized {@link Intent} defined at
- * {@link #EXTRA_INTENT}.
- * @hide
- */
- @SystemApi
- public static final String EXTRA_UNKNOWN_INSTANT_APP =
- "android.intent.extra.UNKNOWN_INSTANT_APP";
-
- /**
* The version code of the app to install components from.
* @deprecated Use {@link #EXTRA_LONG_VERSION_CODE).
* @hide
@@ -5048,7 +5029,6 @@
FLAG_GRANT_PREFIX_URI_PERMISSION,
FLAG_DEBUG_TRIAGED_MISSING,
FLAG_IGNORE_EPHEMERAL,
- FLAG_ACTIVITY_MATCH_EXTERNAL,
FLAG_ACTIVITY_NO_HISTORY,
FLAG_ACTIVITY_SINGLE_TOP,
FLAG_ACTIVITY_NEW_TASK,
@@ -5092,7 +5072,6 @@
FLAG_INCLUDE_STOPPED_PACKAGES,
FLAG_DEBUG_TRIAGED_MISSING,
FLAG_IGNORE_EPHEMERAL,
- FLAG_ACTIVITY_MATCH_EXTERNAL,
FLAG_ACTIVITY_NO_HISTORY,
FLAG_ACTIVITY_SINGLE_TOP,
FLAG_ACTIVITY_NEW_TASK,
@@ -5496,14 +5475,6 @@
*/
public static final int FLAG_ACTIVITY_LAUNCH_ADJACENT = 0x00001000;
-
- /**
- * If set, resolution of this intent may take place via an instant app not
- * yet on the device if there does not yet exist an app on device to
- * resolve it.
- */
- public static final int FLAG_ACTIVITY_MATCH_EXTERNAL = 0x00000800;
-
/**
* If set, when sending a broadcast only registered receivers will be
* called -- no BroadcastReceiver components will be launched.
@@ -10057,25 +10028,6 @@
}
}
- /** @hide */
- public boolean hasWebURI() {
- if (getData() == null) {
- return false;
- }
- final String scheme = getScheme();
- if (TextUtils.isEmpty(scheme)) {
- return false;
- }
- return scheme.equals(IntentFilter.SCHEME_HTTP) || scheme.equals(IntentFilter.SCHEME_HTTPS);
- }
-
- /** @hide */
- public boolean isBrowsableWebIntent() {
- return ACTION_VIEW.equals(mAction)
- && hasCategory(CATEGORY_BROWSABLE)
- && hasWebURI();
- }
-
/**
* @hide
*/
diff --git a/core/java/android/content/pm/AuxiliaryResolveInfo.java b/core/java/android/content/pm/AuxiliaryResolveInfo.java
index 202df50..6bdcefb 100644
--- a/core/java/android/content/pm/AuxiliaryResolveInfo.java
+++ b/core/java/android/content/pm/AuxiliaryResolveInfo.java
@@ -21,10 +21,6 @@
import android.content.ComponentName;
import android.content.Intent;
import android.content.IntentFilter;
-import android.os.Bundle;
-
-import java.util.Collections;
-import java.util.List;
/**
* Auxiliary application resolution response.
@@ -35,95 +31,56 @@
* hasn't been installed.
* @hide
*/
-public final class AuxiliaryResolveInfo {
+public final class AuxiliaryResolveInfo extends IntentFilter {
+ /** Resolved information returned from the external instant resolver */
+ public final InstantAppResolveInfo resolveInfo;
+ /** The resolved package. Copied from {@link #resolveInfo}. */
+ public final String packageName;
/** The activity to launch if there's an installation failure. */
public final ComponentName installFailureActivity;
+ /** The resolve split. Copied from the matched filter in {@link #resolveInfo}. */
+ public final String splitName;
/** Whether or not instant resolution needs the second phase */
public final boolean needsPhaseTwo;
/** Opaque token to track the instant application resolution */
public final String token;
+ /** The version code of the package */
+ public final long versionCode;
/** An intent to start upon failure to install */
public final Intent failureIntent;
- /** The matching filters for this resolve info. */
- public final List<AuxiliaryFilter> filters;
/** Create a response for installing an instant application. */
- public AuxiliaryResolveInfo(@NonNull String token,
+ public AuxiliaryResolveInfo(@NonNull InstantAppResolveInfo resolveInfo,
+ @NonNull IntentFilter orig,
+ @Nullable String splitName,
+ @NonNull String token,
boolean needsPhase2,
- @Nullable Intent failureIntent,
- @Nullable List<AuxiliaryFilter> filters) {
+ @Nullable Intent failureIntent) {
+ super(orig);
+ this.resolveInfo = resolveInfo;
+ this.packageName = resolveInfo.getPackageName();
+ this.splitName = splitName;
this.token = token;
this.needsPhaseTwo = needsPhase2;
+ this.versionCode = resolveInfo.getVersionCode();
this.failureIntent = failureIntent;
- this.filters = filters;
this.installFailureActivity = null;
}
/** Create a response for installing a split on demand. */
- public AuxiliaryResolveInfo(@Nullable ComponentName failureActivity,
- @Nullable Intent failureIntent,
- @Nullable List<AuxiliaryFilter> filters) {
+ public AuxiliaryResolveInfo(@NonNull String packageName,
+ @Nullable String splitName,
+ @Nullable ComponentName failureActivity,
+ long versionCode,
+ @Nullable Intent failureIntent) {
super();
+ this.packageName = packageName;
this.installFailureActivity = failureActivity;
- this.filters = filters;
+ this.splitName = splitName;
+ this.versionCode = versionCode;
+ this.resolveInfo = null;
this.token = null;
this.needsPhaseTwo = false;
this.failureIntent = failureIntent;
}
-
- /** Create a response for installing a split on demand. */
- public AuxiliaryResolveInfo(@Nullable ComponentName failureActivity,
- String packageName, long versionCode, String splitName) {
- this(failureActivity, null, Collections.singletonList(
- new AuxiliaryResolveInfo.AuxiliaryFilter(packageName, versionCode, splitName)));
- }
-
- /** @hide */
- public static final class AuxiliaryFilter extends IntentFilter {
- /** Resolved information returned from the external instant resolver */
- public final InstantAppResolveInfo resolveInfo;
- /** The resolved package. Copied from {@link #resolveInfo}. */
- public final String packageName;
- /** The version code of the package */
- public final long versionCode;
- /** The resolve split. Copied from the matched filter in {@link #resolveInfo}. */
- public final String splitName;
- /** The extras to pass on to the installer for this filter. */
- public final Bundle extras;
-
- public AuxiliaryFilter(IntentFilter orig, InstantAppResolveInfo resolveInfo,
- String splitName, Bundle extras) {
- super(orig);
- this.resolveInfo = resolveInfo;
- this.packageName = resolveInfo.getPackageName();
- this.versionCode = resolveInfo.getLongVersionCode();
- this.splitName = splitName;
- this.extras = extras;
- }
-
- public AuxiliaryFilter(InstantAppResolveInfo resolveInfo,
- String splitName, Bundle extras) {
- this.resolveInfo = resolveInfo;
- this.packageName = resolveInfo.getPackageName();
- this.versionCode = resolveInfo.getLongVersionCode();
- this.splitName = splitName;
- this.extras = extras;
- }
-
- public AuxiliaryFilter(String packageName, long versionCode, String splitName) {
- this.resolveInfo = null;
- this.packageName = packageName;
- this.versionCode = versionCode;
- this.splitName = splitName;
- this.extras = null;
- }
-
- @Override
- public String toString() {
- return "AuxiliaryFilter{"
- + "packageName='" + packageName + '\''
- + ", versionCode=" + versionCode
- + ", splitName='" + splitName + '\'' + '}';
- }
- }
}
\ No newline at end of file
diff --git a/core/java/android/content/pm/InstantAppResolveInfo.java b/core/java/android/content/pm/InstantAppResolveInfo.java
index 112c5da..19cb932 100644
--- a/core/java/android/content/pm/InstantAppResolveInfo.java
+++ b/core/java/android/content/pm/InstantAppResolveInfo.java
@@ -19,7 +19,6 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
-import android.content.Intent;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
@@ -27,35 +26,11 @@
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
import java.util.Locale;
/**
- * Describes an externally resolvable instant application. There are three states that this class
- * can represent: <p/>
- * <ul>
- * <li>
- * The first, usable only for non http/s intents, implies that the resolver cannot
- * immediately resolve this intent and would prefer that resolution be deferred to the
- * instant app installer. Represent this state with {@link #InstantAppResolveInfo(Bundle)}.
- * If the {@link android.content.Intent} has the scheme set to http/s and a set of digest
- * prefixes were passed into one of the resolve methods in
- * {@link android.app.InstantAppResolverService}, this state cannot be used.
- * </li>
- * <li>
- * The second represents a partial match and is constructed with any of the other
- * constructors. By setting one or more of the {@link Nullable}arguments to null, you
- * communicate to the resolver in response to
- * {@link android.app.InstantAppResolverService#onGetInstantAppResolveInfo(Intent, int[],
- * String, InstantAppResolverService.InstantAppResolutionCallback)}
- * that you need a 2nd round of resolution to complete the request.
- * </li>
- * <li>
- * The third represents a complete match and is constructed with all @Nullable parameters
- * populated.
- * </li>
- * </ul>
+ * Information about an instant application.
* @hide
*/
@SystemApi
@@ -63,8 +38,6 @@
/** Algorithm that will be used to generate the domain digest */
private static final String SHA_ALGORITHM = "SHA-256";
- private static final byte[] EMPTY_DIGEST = new byte[0];
-
private final InstantAppDigest mDigest;
private final String mPackageName;
/** The filters used to match domain */
@@ -73,30 +46,15 @@
private final long mVersionCode;
/** Data about the app that should be passed along to the Instant App installer on resolve */
private final Bundle mExtras;
- /**
- * A flag that indicates that the resolver is aware that an app may match, but would prefer
- * that the installer get the sanitized intent to decide. This should not be used for
- * resolutions that include a host and will be ignored in such cases.
- */
- private final boolean mShouldLetInstallerDecide;
- /** Constructor for intent-based InstantApp resolution results. */
public InstantAppResolveInfo(@NonNull InstantAppDigest digest, @Nullable String packageName,
@Nullable List<InstantAppIntentFilter> filters, int versionCode) {
this(digest, packageName, filters, (long) versionCode, null /* extras */);
}
- /** Constructor for intent-based InstantApp resolution results with extras. */
public InstantAppResolveInfo(@NonNull InstantAppDigest digest, @Nullable String packageName,
@Nullable List<InstantAppIntentFilter> filters, long versionCode,
@Nullable Bundle extras) {
- this(digest, packageName, filters, versionCode, extras, false);
- }
-
- /** Constructor for intent-based InstantApp resolution results with extras. */
- private InstantAppResolveInfo(@NonNull InstantAppDigest digest, @Nullable String packageName,
- @Nullable List<InstantAppIntentFilter> filters, long versionCode,
- @Nullable Bundle extras, boolean shouldLetInstallerDecide) {
// validate arguments
if ((packageName == null && (filters != null && filters.size() != 0))
|| (packageName != null && (filters == null || filters.size() == 0))) {
@@ -104,7 +62,7 @@
}
mDigest = digest;
if (filters != null) {
- mFilters = new ArrayList<>(filters.size());
+ mFilters = new ArrayList<InstantAppIntentFilter>(filters.size());
mFilters.addAll(filters);
} else {
mFilters = null;
@@ -112,48 +70,25 @@
mPackageName = packageName;
mVersionCode = versionCode;
mExtras = extras;
- mShouldLetInstallerDecide = shouldLetInstallerDecide;
}
- /** Constructor for intent-based InstantApp resolution results by hostname. */
public InstantAppResolveInfo(@NonNull String hostName, @Nullable String packageName,
@Nullable List<InstantAppIntentFilter> filters) {
this(new InstantAppDigest(hostName), packageName, filters, -1 /*versionCode*/,
null /* extras */);
}
- /**
- * Constructor that creates a "let the installer decide" response with optional included
- * extras.
- */
- public InstantAppResolveInfo(@Nullable Bundle extras) {
- this(InstantAppDigest.UNDEFINED, null, null, -1, extras, true);
- }
-
InstantAppResolveInfo(Parcel in) {
- mShouldLetInstallerDecide = in.readBoolean();
+ mDigest = in.readParcelable(null /*loader*/);
+ mPackageName = in.readString();
+ mFilters = new ArrayList<InstantAppIntentFilter>();
+ in.readList(mFilters, null /*loader*/);
+ mVersionCode = in.readLong();
mExtras = in.readBundle();
- if (mShouldLetInstallerDecide) {
- mDigest = InstantAppDigest.UNDEFINED;
- mPackageName = null;
- mFilters = Collections.emptyList();
- mVersionCode = -1;
- } else {
- mDigest = in.readParcelable(null /*loader*/);
- mPackageName = in.readString();
- mFilters = new ArrayList<>();
- in.readList(mFilters, null /*loader*/);
- mVersionCode = in.readLong();
- }
- }
-
- /** Returns true if the installer should be notified that it should query for packages. */
- public boolean shouldLetInstallerDecide() {
- return mShouldLetInstallerDecide;
}
public byte[] getDigestBytes() {
- return mDigest.mDigestBytes.length > 0 ? mDigest.getDigestBytes()[0] : EMPTY_DIGEST;
+ return mDigest.getDigestBytes()[0];
}
public int getDigestPrefix() {
@@ -192,15 +127,11 @@
@Override
public void writeToParcel(Parcel out, int flags) {
- out.writeBoolean(mShouldLetInstallerDecide);
- out.writeBundle(mExtras);
- if (mShouldLetInstallerDecide) {
- return;
- }
out.writeParcelable(mDigest, flags);
out.writeString(mPackageName);
out.writeList(mFilters);
out.writeLong(mVersionCode);
+ out.writeBundle(mExtras);
}
public static final Parcelable.Creator<InstantAppResolveInfo> CREATOR
@@ -228,9 +159,7 @@
@SystemApi
public static final class InstantAppDigest implements Parcelable {
private static final int DIGEST_MASK = 0xfffff000;
-
- public static final InstantAppDigest UNDEFINED =
- new InstantAppDigest(new byte[][]{}, new int[]{});
+ private static final int DIGEST_PREFIX_COUNT = 5;
/** Full digest of the domain hashes */
private final byte[][] mDigestBytes;
/** The first 4 bytes of the domain hashes */
@@ -257,11 +186,6 @@
}
}
- private InstantAppDigest(byte[][] digestBytes, int[] prefix) {
- this.mDigestPrefix = prefix;
- this.mDigestBytes = digestBytes;
- }
-
private static byte[][] generateDigest(String hostName, int maxDigests) {
ArrayList<byte[]> digests = new ArrayList<>();
try {
diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java
index 3d26af1..2da8937 100644
--- a/core/java/android/content/pm/PackageParser.java
+++ b/core/java/android/content/pm/PackageParser.java
@@ -78,8 +78,10 @@
import android.util.ArraySet;
import android.util.AttributeSet;
import android.util.Base64;
+import android.util.ByteStringUtils;
import android.util.DisplayMetrics;
import android.util.Log;
+import android.util.PackageUtils;
import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
@@ -5683,7 +5685,10 @@
return true;
}
- /** A container for signing-related data of an application package. */
+ /**
+ * A container for signing-related data of an application package.
+ * @hide
+ */
public static final class SigningDetails implements Parcelable {
@IntDef({SigningDetails.SignatureSchemeVersion.UNKNOWN,
@@ -5705,15 +5710,54 @@
public final ArraySet<PublicKey> publicKeys;
/**
- * Collection of {@code Signature} objects, each of which is formed from a former signing
- * certificate of this APK before it was changed by signing certificate rotation.
+ * APK Signature Scheme v3 includes support for adding a proof-of-rotation record that
+ * contains two pieces of information:
+ * 1) the past signing certificates
+ * 2) the flags that APK wants to assign to each of the past signing certificates.
+ *
+ * This collection of {@code Signature} objects, each of which is formed from a former
+ * signing certificate of this APK before it was changed by signing certificate rotation,
+ * represents the first piece of information. It is the APK saying to the rest of the
+ * world: "hey if you trust the old cert, you can trust me!" This is useful, if for
+ * instance, the platform would like to determine whether or not to allow this APK to do
+ * something it would've allowed it to do under the old cert (like upgrade).
*/
@Nullable
public final Signature[] pastSigningCertificates;
+ /** special value used to see if cert is in package - not exposed to callers */
+ private static final int PAST_CERT_EXISTS = 0;
+
+ @IntDef(
+ flag = true,
+ value = {CertCapabilities.INSTALLED_DATA,
+ CertCapabilities.SHARED_USER_ID,
+ CertCapabilities.PERMISSION })
+ public @interface CertCapabilities {
+
+ /** accept data from already installed pkg with this cert */
+ int INSTALLED_DATA = 1;
+
+ /** accept sharedUserId with pkg with this cert */
+ int SHARED_USER_ID = 2;
+
+ /** grant SIGNATURE permissions to pkgs with this cert */
+ int PERMISSION = 4;
+ }
+
/**
- * Flags for the {@code pastSigningCertificates} collection, which indicate the capabilities
- * the including APK wishes to grant to its past signing certificates.
+ * APK Signature Scheme v3 includes support for adding a proof-of-rotation record that
+ * contains two pieces of information:
+ * 1) the past signing certificates
+ * 2) the flags that APK wants to assign to each of the past signing certificates.
+ *
+ * These flags, which have a one-to-one relationship for the {@code pastSigningCertificates}
+ * collection, represent the second piece of information and are viewed as capabilities.
+ * They are an APK's way of telling the platform: "this is how I want to trust my old certs,
+ * please enforce that." This is useful for situation where this app itself is using its
+ * signing certificate as an authorization mechanism, like whether or not to allow another
+ * app to have its SIGNATURE permission. An app could specify whether to allow other apps
+ * signed by its old cert 'X' to still get a signature permission it defines, for example.
*/
@Nullable
public final int[] pastSigningCertificatesFlags;
@@ -5784,6 +5828,244 @@
return pastSigningCertificates != null && pastSigningCertificates.length > 0;
}
+ /**
+ * Determines if the provided {@code oldDetails} is an ancestor of or the same as this one.
+ * If the {@code oldDetails} signing certificate appears in our pastSigningCertificates,
+ * then that means it has authorized a signing certificate rotation, which eventually leads
+ * to our certificate, and thus can be trusted. If this method evaluates to true, this
+ * SigningDetails object should be trusted if the previous one is.
+ */
+ public boolean hasAncestorOrSelf(SigningDetails oldDetails) {
+ if (this == UNKNOWN || oldDetails == UNKNOWN) {
+ return false;
+ }
+ if (oldDetails.signatures.length > 1) {
+
+ // multiple-signer packages cannot rotate signing certs, so we just compare current
+ // signers for an exact match
+ return signaturesMatchExactly(oldDetails);
+ } else {
+
+ // we may have signing certificate rotation history, check to see if the oldDetails
+ // was one of our old signing certificates
+ return hasCertificate(oldDetails.signatures[0]);
+ }
+ }
+
+ /**
+ * Similar to {@code hasAncestorOrSelf}. Returns true only if this {@code SigningDetails}
+ * is a descendant of {@code oldDetails}, not if they're the same. This is used to
+ * determine if this object is newer than the provided one.
+ */
+ public boolean hasAncestor(SigningDetails oldDetails) {
+ if (this == UNKNOWN || oldDetails == UNKNOWN) {
+ return false;
+ }
+ if (this.hasPastSigningCertificates() && oldDetails.signatures.length == 1) {
+
+ // the last entry in pastSigningCertificates is the current signer, ignore it
+ for (int i = 0; i < pastSigningCertificates.length - 1; i++) {
+ if (pastSigningCertificates[i].equals(oldDetails.signatures[i])) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Determines if the provided {@code oldDetails} is an ancestor of this one, and whether or
+ * not this one grants it the provided capability, represented by the {@code flags}
+ * parameter. In the event of signing certificate rotation, a package may still interact
+ * with entities signed by its old signing certificate and not want to break previously
+ * functioning behavior. The {@code flags} value determines which capabilities the app
+ * signed by the newer signing certificate would like to continue to give to its previous
+ * signing certificate(s).
+ */
+ public boolean checkCapability(SigningDetails oldDetails, @CertCapabilities int flags) {
+ if (this == UNKNOWN || oldDetails == UNKNOWN) {
+ return false;
+ }
+ if (oldDetails.signatures.length > 1) {
+
+ // multiple-signer packages cannot rotate signing certs, so we must have an exact
+ // match, which also means all capabilities are granted
+ return signaturesMatchExactly(oldDetails);
+ } else {
+
+ // we may have signing certificate rotation history, check to see if the oldDetails
+ // was one of our old signing certificates, and if we grant it the capability it's
+ // requesting
+ return hasCertificate(oldDetails.signatures[0], flags);
+ }
+ }
+
+ /**
+ * A special case of {@code checkCapability} which re-encodes both sets of signing
+ * certificates to counteract a previous re-encoding.
+ */
+ public boolean checkCapabilityRecover(SigningDetails oldDetails,
+ @CertCapabilities int flags) throws CertificateException {
+ if (oldDetails == UNKNOWN || this == UNKNOWN) {
+ return false;
+ }
+ if (hasPastSigningCertificates() && oldDetails.signatures.length == 1) {
+
+ // signing certificates may have rotated, check entire history for effective match
+ for (int i = 0; i < pastSigningCertificates.length; i++) {
+ if (Signature.areEffectiveMatch(
+ oldDetails.signatures[0],
+ pastSigningCertificates[i])
+ && pastSigningCertificatesFlags[i] == flags) {
+ return true;
+ }
+ }
+ } else {
+ return Signature.areEffectiveMatch(oldDetails.signatures, signatures);
+ }
+ return false;
+ }
+
+ /**
+ * Determine if {@code signature} is in this SigningDetails' signing certificate history,
+ * including the current signer. Automatically returns false if this object has multiple
+ * signing certificates, since rotation is only supported for single-signers; this is
+ * enforced by {@code hasCertificateInternal}.
+ */
+ public boolean hasCertificate(Signature signature) {
+ return hasCertificateInternal(signature, PAST_CERT_EXISTS);
+ }
+
+ /**
+ * Determine if {@code signature} is in this SigningDetails' signing certificate history,
+ * including the current signer, and whether or not it has the given permission.
+ * Certificates which match our current signer automatically get all capabilities.
+ * Automatically returns false if this object has multiple signing certificates, since
+ * rotation is only supported for single-signers.
+ */
+ public boolean hasCertificate(Signature signature, @CertCapabilities int flags) {
+ return hasCertificateInternal(signature, flags);
+ }
+
+ /** Convenient wrapper for calling {@code hasCertificate} with certificate's raw bytes. */
+ public boolean hasCertificate(byte[] certificate) {
+ Signature signature = new Signature(certificate);
+ return hasCertificate(signature);
+ }
+
+ private boolean hasCertificateInternal(Signature signature, int flags) {
+ if (this == UNKNOWN) {
+ return false;
+ }
+
+ // only single-signed apps can have pastSigningCertificates
+ if (hasPastSigningCertificates()) {
+
+ // check all past certs, except for the current one, which automatically gets all
+ // capabilities, since it is the same as the current signature
+ for (int i = 0; i < pastSigningCertificates.length - 1; i++) {
+ if (pastSigningCertificates[i].equals(signature)) {
+ if (flags == PAST_CERT_EXISTS
+ || (flags & pastSigningCertificatesFlags[i]) == flags) {
+ return true;
+ }
+ }
+ }
+ }
+
+ // not in previous certs signing history, just check the current signer and make sure
+ // we are singly-signed
+ return signatures.length == 1 && signatures[0].equals(signature);
+ }
+
+ /**
+ * Determines if the provided {@code sha256String} is an ancestor of this one, and whether
+ * or not this one grants it the provided capability, represented by the {@code flags}
+ * parameter. In the event of signing certificate rotation, a package may still interact
+ * with entities signed by its old signing certificate and not want to break previously
+ * functioning behavior. The {@code flags} value determines which capabilities the app
+ * signed by the newer signing certificate would like to continue to give to its previous
+ * signing certificate(s).
+ *
+ * @param sha256String A hex-encoded representation of a sha256 digest. In the case of an
+ * app with multiple signers, this represents the hex-encoded sha256
+ * digest of the combined hex-encoded sha256 digests of each individual
+ * signing certificate according to {@link
+ * PackageUtils#computeSignaturesSha256Digest(Signature[])}
+ */
+ public boolean checkCapability(String sha256String, @CertCapabilities int flags) {
+ if (this == UNKNOWN) {
+ return false;
+ }
+
+ // first see if the hash represents a single-signer in our signing history
+ byte[] sha256Bytes = ByteStringUtils.fromHexToByteArray(sha256String);
+ if (hasSha256Certificate(sha256Bytes, flags)) {
+ return true;
+ }
+
+ // Not in signing history, either represents multiple signatures or not a match.
+ // Multiple signers can't rotate, so no need to check flags, just see if the SHAs match.
+ // We already check the single-signer case above as part of hasSha256Certificate, so no
+ // need to verify we have multiple signers, just run the old check
+ // just consider current signing certs
+ final String[] mSignaturesSha256Digests =
+ PackageUtils.computeSignaturesSha256Digests(signatures);
+ final String mSignaturesSha256Digest =
+ PackageUtils.computeSignaturesSha256Digest(mSignaturesSha256Digests);
+ return mSignaturesSha256Digest.equals(sha256String);
+ }
+
+ /**
+ * Determine if the {@code sha256Certificate} is in this SigningDetails' signing certificate
+ * history, including the current signer. Automatically returns false if this object has
+ * multiple signing certificates, since rotation is only supported for single-signers.
+ */
+ public boolean hasSha256Certificate(byte[] sha256Certificate) {
+ return hasSha256CertificateInternal(sha256Certificate, PAST_CERT_EXISTS);
+ }
+
+ /**
+ * Determine if the {@code sha256Certificate} certificate hash corresponds to a signing
+ * certificate in this SigningDetails' signing certificate history, including the current
+ * signer, and whether or not it has the given permission. Certificates which match our
+ * current signer automatically get all capabilities. Automatically returns false if this
+ * object has multiple signing certificates, since rotation is only supported for
+ * single-signers.
+ */
+ public boolean hasSha256Certificate(byte[] sha256Certificate, @CertCapabilities int flags) {
+ return hasSha256CertificateInternal(sha256Certificate, flags);
+ }
+
+ private boolean hasSha256CertificateInternal(byte[] sha256Certificate, int flags) {
+ if (this == UNKNOWN) {
+ return false;
+ }
+ if (hasPastSigningCertificates()) {
+
+ // check all past certs, except for the last one, which automatically gets all
+ // capabilities, since it is the same as the current signature, and is checked below
+ for (int i = 0; i < pastSigningCertificates.length - 1; i++) {
+ byte[] digest = PackageUtils.computeSha256DigestBytes(
+ pastSigningCertificates[i].toByteArray());
+ if (Arrays.equals(sha256Certificate, digest)) {
+ if (flags == PAST_CERT_EXISTS
+ || (flags & pastSigningCertificatesFlags[i]) == flags) {
+ return true;
+ }
+ }
+ }
+ }
+
+ // not in previous certs signing history, just check the current signer
+ if (signatures.length == 1) {
+ byte[] digest =
+ PackageUtils.computeSha256DigestBytes(signatures[0].toByteArray());
+ return Arrays.equals(sha256Certificate, digest);
+ }
+ return false;
+ }
+
/** Returns true if the signatures in this and other match exactly. */
public boolean signaturesMatchExactly(SigningDetails other) {
return Signature.areExactMatch(this.signatures, other.signatures);
diff --git a/core/java/android/content/pm/Signature.java b/core/java/android/content/pm/Signature.java
index fdc54ae..a2a14ed 100644
--- a/core/java/android/content/pm/Signature.java
+++ b/core/java/android/content/pm/Signature.java
@@ -285,6 +285,29 @@
}
/**
+ * Test if given {@link Signature} objects are effectively equal. In rare
+ * cases, certificates can have slightly malformed encoding which causes
+ * exact-byte checks to fail.
+ * <p>
+ * To identify effective equality, we bounce the certificates through an
+ * decode/encode pass before doing the exact-byte check. To reduce attack
+ * surface area, we only allow a byte size delta of a few bytes.
+ *
+ * @throws CertificateException if the before/after length differs
+ * substantially, usually a signal of something fishy going on.
+ * @hide
+ */
+ public static boolean areEffectiveMatch(Signature a, Signature b)
+ throws CertificateException {
+ final CertificateFactory cf = CertificateFactory.getInstance("X.509");
+
+ final Signature aPrime = bounce(cf, a);
+ final Signature bPrime = bounce(cf, b);
+
+ return aPrime.equals(bPrime);
+ }
+
+ /**
* Bounce the given {@link Signature} through a decode/encode cycle.
*
* @throws CertificateException if the before/after length differs
diff --git a/core/java/android/hardware/camera2/params/OutputConfiguration.java b/core/java/android/hardware/camera2/params/OutputConfiguration.java
index f47cd66..eb4bced 100644
--- a/core/java/android/hardware/camera2/params/OutputConfiguration.java
+++ b/core/java/android/hardware/camera2/params/OutputConfiguration.java
@@ -576,7 +576,7 @@
*
* @see #enableSurfaceSharing
*/
- public static int getMaxSharedSurfaceCount() {
+ public int getMaxSharedSurfaceCount() {
return MAX_SURFACES_COUNT;
}
diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java
index eb264d6d..4aadc5b 100644
--- a/core/java/android/os/Binder.java
+++ b/core/java/android/os/Binder.java
@@ -21,7 +21,9 @@
import android.util.ExceptionUtils;
import android.util.Log;
import android.util.Slog;
+import android.util.SparseIntArray;
+import com.android.internal.os.BinderInternal;
import com.android.internal.util.FastPrintWriter;
import com.android.internal.util.FunctionalUtils.ThrowingRunnable;
import com.android.internal.util.FunctionalUtils.ThrowingSupplier;
@@ -934,6 +936,7 @@
final int totalUnclearedSize = unclearedSize();
if (totalUnclearedSize >= CRASH_AT_SIZE) {
dumpProxyInterfaceCounts();
+ dumpPerUidProxyCounts();
Runtime.getRuntime().gc();
throw new AssertionError("Binder ProxyMap has too many entries: "
+ totalSize + " (total), " + totalUnclearedSize + " (uncleared), "
@@ -987,6 +990,20 @@
}
}
+ /**
+ * Dump per uid binder proxy counts to the logcat.
+ */
+ private void dumpPerUidProxyCounts() {
+ SparseIntArray counts = BinderInternal.nGetBinderProxyPerUidCounts();
+ if (counts.size() == 0) return;
+ Log.d(Binder.TAG, "Per Uid Binder Proxy Counts:");
+ for (int i = 0; i < counts.size(); i++) {
+ final int uid = counts.keyAt(i);
+ final int binderCount = counts.valueAt(i);
+ Log.d(Binder.TAG, "UID : " + uid + " count = " + binderCount);
+ }
+ }
+
// Corresponding ArrayLists in the following two arrays always have the same size.
// They contain no empty entries. However WeakReferences in the values ArrayLists
// may have been cleared.
diff --git a/core/java/android/os/HwBinder.java b/core/java/android/os/HwBinder.java
index 2a088a6..cdee110 100644
--- a/core/java/android/os/HwBinder.java
+++ b/core/java/android/os/HwBinder.java
@@ -87,6 +87,9 @@
* Configures how many threads the process-wide hwbinder threadpool
* has to process incoming requests.
*
+ * @param maxThreads total number of threads to create (includes this thread if
+ * callerWillJoin is true)
+ * @param callerWillJoin whether joinRpcThreadpool will be called in advance
* @hide
*/
@SystemApi
@@ -125,6 +128,12 @@
/**
* Enable instrumentation if available.
+ *
+ * On a non-user build, this method:
+ * - tries to enable atracing (if enabled)
+ * - tries to enable coverage dumps (if running in VTS)
+ * - tries to enable record and replay (if running in VTS)
+ *
* @hide
*/
@SystemApi
diff --git a/core/java/android/os/IHwBinder.java b/core/java/android/os/IHwBinder.java
index 0c592e1..a565dee 100644
--- a/core/java/android/os/IHwBinder.java
+++ b/core/java/android/os/IHwBinder.java
@@ -30,6 +30,11 @@
/**
* Process a hwbinder transaction.
*
+ * @param code interface specific code for interface.
+ * @param request parceled transaction
+ * @param reply object to parcel reply into
+ * @param flags transaction flags to be chosen by wire protocol
+ *
* @hide
*/
@SystemApi
@@ -39,6 +44,7 @@
/**
* Return as IHwInterface instance only if this implements descriptor.
+ *
* @param descriptor for example foo.bar@1.0::IBaz
* @hide
*/
@@ -53,6 +59,8 @@
public interface DeathRecipient {
/**
* Callback for a registered process dying.
+ *
+ * @param cookie cookie this death recipient was registered with.
*/
@SystemApi
public void serviceDied(long cookie);
@@ -61,11 +69,16 @@
/**
* Notifies the death recipient with the cookie when the process containing
* this binder dies.
+ *
+ * @param recipient callback object to be called on object death.
+ * @param cookie value to be given to callback on object death.
*/
@SystemApi
public boolean linkToDeath(DeathRecipient recipient, long cookie);
/**
* Unregisters the death recipient from this binder.
+ *
+ * @param recipient callback to no longer recieve death notifications on this binder.
*/
@SystemApi
public boolean unlinkToDeath(DeathRecipient recipient);
diff --git a/core/java/android/os/IHwInterface.java b/core/java/android/os/IHwInterface.java
index a2f59a9..1d9e2b0 100644
--- a/core/java/android/os/IHwInterface.java
+++ b/core/java/android/os/IHwInterface.java
@@ -21,7 +21,7 @@
@SystemApi
public interface IHwInterface {
/**
- * Returns the binder object that corresponds to an interface.
+ * @return the binder object that corresponds to this interface.
*/
@SystemApi
public IHwBinder asBinder();
diff --git a/core/java/android/os/IncidentManager.java b/core/java/android/os/IncidentManager.java
index 1336c66..9b6d6e5 100644
--- a/core/java/android/os/IncidentManager.java
+++ b/core/java/android/os/IncidentManager.java
@@ -33,10 +33,12 @@
@TestApi
@SystemService(Context.INCIDENT_SERVICE)
public class IncidentManager {
- private static final String TAG = "incident";
+ private static final String TAG = "IncidentManager";
private final Context mContext;
+ private IIncidentManager mService;
+
/**
* @hide
*/
@@ -96,19 +98,45 @@
reportIncidentInternal(args);
}
- private void reportIncidentInternal(IncidentReportArgs args) {
- final IIncidentManager service = IIncidentManager.Stub.asInterface(
- ServiceManager.getService(Context.INCIDENT_SERVICE));
- if (service == null) {
- Slog.e(TAG, "reportIncident can't find incident binder service");
- return;
+ private class IncidentdDeathRecipient implements IBinder.DeathRecipient {
+ @Override
+ public void binderDied() {
+ synchronized (this) {
+ mService = null;
+ }
}
+ }
+ private void reportIncidentInternal(IncidentReportArgs args) {
try {
+ final IIncidentManager service = getIIncidentManagerLocked();
+ if (service == null) {
+ Slog.e(TAG, "reportIncident can't find incident binder service");
+ return;
+ }
service.reportIncident(args);
} catch (RemoteException ex) {
Slog.e(TAG, "reportIncident failed", ex);
}
}
+
+ private IIncidentManager getIIncidentManagerLocked() throws RemoteException {
+ if (mService != null) {
+ return mService;
+ }
+
+ synchronized (this) {
+ if (mService != null) {
+ return mService;
+ }
+ mService = IIncidentManager.Stub.asInterface(
+ ServiceManager.getService(Context.INCIDENT_SERVICE));
+ if (mService != null) {
+ mService.asBinder().linkToDeath(new IncidentdDeathRecipient(), 0);
+ }
+ return mService;
+ }
+ }
+
}
diff --git a/core/java/android/os/IncidentReportArgs.java b/core/java/android/os/IncidentReportArgs.java
index fd0ebcf..9fa129c 100644
--- a/core/java/android/os/IncidentReportArgs.java
+++ b/core/java/android/os/IncidentReportArgs.java
@@ -32,6 +32,9 @@
@TestApi
public final class IncidentReportArgs implements Parcelable {
+ private static final int DEST_EXPLICIT = 100;
+ private static final int DEST_AUTO = 200;
+
private final IntArray mSections = new IntArray();
private final ArrayList<byte[]> mHeaders = new ArrayList<byte[]>();
private boolean mAll;
@@ -41,6 +44,7 @@
* Construct an incident report args with no fields.
*/
public IncidentReportArgs() {
+ mDest = DEST_AUTO;
}
/**
@@ -143,7 +147,14 @@
* @hide
*/
public void setPrivacyPolicy(int dest) {
- mDest = dest;
+ switch (dest) {
+ case DEST_EXPLICIT:
+ case DEST_AUTO:
+ mDest = dest;
+ break;
+ default:
+ mDest = DEST_AUTO;
+ }
}
/**
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 2440b48..1f0d683 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -10520,6 +10520,18 @@
public static final String NETWORK_WATCHLIST_ENABLED = "network_watchlist_enabled";
/**
+ * Flag to keep background restricted profiles running after exiting. If disabled,
+ * the restricted profile can be put into stopped state as soon as the user leaves it.
+ * Type: int (0 for false, 1 for true)
+ *
+ * Overridden by the system based on device information. If null, the value specified
+ * by {@code config_keepRestrictedProfilesInBackground} is used.
+ *
+ * @hide
+ */
+ public static final String KEEP_PROFILE_IN_BACKGROUND = "keep_profile_in_background";
+
+ /**
* Get the key that retrieves a bluetooth headset's priority.
* @hide
*/
diff --git a/core/java/android/security/keystore/recovery/KeyChainProtectionParams.java b/core/java/android/security/keystore/recovery/KeyChainProtectionParams.java
index a43952a..aa09f10 100644
--- a/core/java/android/security/keystore/recovery/KeyChainProtectionParams.java
+++ b/core/java/android/security/keystore/recovery/KeyChainProtectionParams.java
@@ -235,17 +235,7 @@
}
/**
- * Removes secret from memory than object is no longer used.
- * Since finalizer call is not reliable, please use @link {#clearSecret} directly.
- */
- @Override
- protected void finalize() throws Throwable {
- clearSecret();
- super.finalize();
- }
-
- /**
- * Fills mSecret with zeroes.
+ * Fills secret with zeroes.
*/
public void clearSecret() {
Arrays.fill(mSecret, (byte) 0);
diff --git a/core/java/android/service/autofill/AutofillServiceInfo.java b/core/java/android/service/autofill/AutofillServiceInfo.java
index 5c7388f..4f2f6cb 100644
--- a/core/java/android/service/autofill/AutofillServiceInfo.java
+++ b/core/java/android/service/autofill/AutofillServiceInfo.java
@@ -25,16 +25,20 @@
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
+import android.metrics.LogMaker;
import android.os.RemoteException;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Xml;
import com.android.internal.R;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
+
import java.io.IOException;
/**
@@ -91,10 +95,20 @@
private static TypedArray getMetaDataArray(PackageManager pm, ServiceInfo si) {
// Check for permissions.
if (!Manifest.permission.BIND_AUTOFILL_SERVICE.equals(si.permission)) {
- Log.w(TAG, "AutofillService from '" + si.packageName + "' does not require permission "
- + Manifest.permission.BIND_AUTOFILL_SERVICE);
- throw new SecurityException("Service does not require permission "
- + Manifest.permission.BIND_AUTOFILL_SERVICE);
+ if (Manifest.permission.BIND_AUTOFILL.equals(si.permission)) {
+ // Let it go for now...
+ Log.w(TAG, "AutofillService from '" + si.packageName + "' uses unsupported "
+ + "permission " + Manifest.permission.BIND_AUTOFILL + ". It works for "
+ + "now, but might not be supported on future releases");
+ new MetricsLogger().write(new LogMaker(MetricsEvent.AUTOFILL_INVALID_PERMISSION)
+ .setPackageName(si.packageName));
+ } else {
+ Log.w(TAG, "AutofillService from '" + si.packageName
+ + "' does not require permission "
+ + Manifest.permission.BIND_AUTOFILL_SERVICE);
+ throw new SecurityException("Service does not require permission "
+ + Manifest.permission.BIND_AUTOFILL_SERVICE);
+ }
}
// Get the AutoFill metadata, if declared.
diff --git a/core/java/android/service/autofill/DateTransformation.java b/core/java/android/service/autofill/DateTransformation.java
new file mode 100644
index 0000000..4e1425d
--- /dev/null
+++ b/core/java/android/service/autofill/DateTransformation.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+import android.widget.RemoteViews;
+import android.widget.TextView;
+
+import com.android.internal.util.Preconditions;
+
+import java.text.DateFormat;
+import java.util.Date;
+
+/**
+ * Replaces a {@link TextView} child of a {@link CustomDescription} with the contents of a field
+ * that is expected to have a {@link AutofillValue#forDate(long) date value}.
+ *
+ * <p>For example, a transformation to display a credit card expiration date as month/year would be:
+ *
+ * <pre class="prettyprint">
+ * new DateTransformation(ccExpDate, new java.text.SimpleDateFormat("MM/yyyy")
+ * </pre>
+ */
+public final class DateTransformation extends InternalTransformation implements
+ Transformation, Parcelable {
+ private static final String TAG = "DateTransformation";
+
+ private final AutofillId mFieldId;
+ private final DateFormat mDateFormat;
+
+ /**
+ * Creates a new transformation.
+ *
+ * @param id id of the screen field.
+ * @param dateFormat object used to transform the date value of the field to a String.
+ */
+ public DateTransformation(@NonNull AutofillId id, @NonNull DateFormat dateFormat) {
+ mFieldId = Preconditions.checkNotNull(id);
+ mDateFormat = Preconditions.checkNotNull(dateFormat);
+ }
+
+ /** @hide */
+ @Override
+ @TestApi
+ public void apply(@NonNull ValueFinder finder, @NonNull RemoteViews parentTemplate,
+ int childViewId) throws Exception {
+ final AutofillValue value = finder.findRawValueByAutofillId(mFieldId);
+ if (value == null) {
+ Log.w(TAG, "No value for id " + mFieldId);
+ return;
+ }
+ if (!value.isDate()) {
+ Log.w(TAG, "Value for " + mFieldId + " is not date: " + value);
+ return;
+ }
+
+ try {
+ final Date date = new Date(value.getDateValue());
+ final String transformed = mDateFormat.format(date);
+ if (sDebug) Log.d(TAG, "Transformed " + date + " to " + transformed);
+
+ parentTemplate.setCharSequence(childViewId, "setText", transformed);
+ } catch (Exception e) {
+ Log.w(TAG, "Could not apply " + mDateFormat + " to " + value + ": " + e);
+ }
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return "DateTransformation: [id=" + mFieldId + ", format=" + mDateFormat + "]";
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeParcelable(mFieldId, flags);
+ parcel.writeSerializable(mDateFormat);
+ }
+
+ public static final Parcelable.Creator<DateTransformation> CREATOR =
+ new Parcelable.Creator<DateTransformation>() {
+ @Override
+ public DateTransformation createFromParcel(Parcel parcel) {
+ return new DateTransformation(parcel.readParcelable(null),
+ (DateFormat) parcel.readSerializable());
+ }
+
+ @Override
+ public DateTransformation[] newArray(int size) {
+ return new DateTransformation[size];
+ }
+ };
+}
diff --git a/core/java/android/service/autofill/DateValueSanitizer.java b/core/java/android/service/autofill/DateValueSanitizer.java
new file mode 100644
index 0000000..0f7b540
--- /dev/null
+++ b/core/java/android/service/autofill/DateValueSanitizer.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+import android.view.autofill.AutofillValue;
+
+import com.android.internal.util.Preconditions;
+
+import java.text.DateFormat;
+import java.util.Date;
+
+/**
+ * Sanitizes a date {@link AutofillValue} using a {@link DateFormat}.
+ *
+ * <p>For example, to sanitize a credit card expiration date to just its month and year:
+ *
+ * <pre class="prettyprint">
+ * new DateValueSanitizer(new java.text.SimpleDateFormat("MM/yyyy");
+ * </pre>
+ */
+public final class DateValueSanitizer extends InternalSanitizer implements Sanitizer, Parcelable {
+
+ private static final String TAG = "DateValueSanitizer";
+
+ private final DateFormat mDateFormat;
+
+ /**
+ * Default constructor.
+ *
+ * @param dateFormat date format applied to the actual date value of an input field.
+ */
+ public DateValueSanitizer(@NonNull DateFormat dateFormat) {
+ mDateFormat = Preconditions.checkNotNull(dateFormat);
+ }
+
+ /** @hide */
+ @Override
+ @TestApi
+ @Nullable
+ public AutofillValue sanitize(@NonNull AutofillValue value) {
+ if (value == null) {
+ Log.w(TAG, "sanitize() called with null value");
+ return null;
+ }
+ if (!value.isDate()) {
+ if (sDebug) Log.d(TAG, value + " is not a date");
+ return null;
+ }
+
+ try {
+ final Date date = new Date(value.getDateValue());
+
+ // First convert it to string
+ final String converted = mDateFormat.format(date);
+ if (sDebug) Log.d(TAG, "Transformed " + date + " to " + converted);
+ // Then parse it back to date
+ final Date sanitized = mDateFormat.parse(converted);
+ if (sDebug) Log.d(TAG, "Sanitized to " + sanitized);
+ return AutofillValue.forDate(sanitized.getTime());
+ } catch (Exception e) {
+ Log.w(TAG, "Could not apply " + mDateFormat + " to " + value + ": " + e);
+ return null;
+ }
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return "DateValueSanitizer: [dateFormat=" + mDateFormat + "]";
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeSerializable(mDateFormat);
+ }
+
+ public static final Parcelable.Creator<DateValueSanitizer> CREATOR =
+ new Parcelable.Creator<DateValueSanitizer>() {
+ @Override
+ public DateValueSanitizer createFromParcel(Parcel parcel) {
+ return new DateValueSanitizer((DateFormat) parcel.readSerializable());
+ }
+
+ @Override
+ public DateValueSanitizer[] newArray(int size) {
+ return new DateValueSanitizer[size];
+ }
+ };
+}
diff --git a/core/java/android/service/autofill/UserData.java b/core/java/android/service/autofill/UserData.java
index 9017848..6bab6aa 100644
--- a/core/java/android/service/autofill/UserData.java
+++ b/core/java/android/service/autofill/UserData.java
@@ -30,6 +30,7 @@
import android.os.Parcelable;
import android.provider.Settings;
import android.service.autofill.FieldClassification.Match;
+import android.text.TextUtils;
import android.util.Log;
import android.view.autofill.AutofillManager;
import android.view.autofill.Helper;
@@ -52,12 +53,14 @@
private static final int DEFAULT_MIN_VALUE_LENGTH = 5;
private static final int DEFAULT_MAX_VALUE_LENGTH = 100;
+ private final String mId;
private final String mAlgorithm;
private final Bundle mAlgorithmArgs;
private final String[] mRemoteIds;
private final String[] mValues;
private UserData(Builder builder) {
+ mId = builder.mId;
mAlgorithm = builder.mAlgorithm;
mAlgorithmArgs = builder.mAlgorithmArgs;
mRemoteIds = new String[builder.mRemoteIds.size()];
@@ -75,6 +78,13 @@
return mAlgorithm;
}
+ /**
+ * Gets the id.
+ */
+ public String getId() {
+ return mId;
+ }
+
/** @hide */
public Bundle getAlgorithmArgs() {
return mAlgorithmArgs;
@@ -92,6 +102,7 @@
/** @hide */
public void dump(String prefix, PrintWriter pw) {
+ pw.print(prefix); pw.print("id: "); pw.print(mId);
pw.print(prefix); pw.print("Algorithm: "); pw.print(mAlgorithm);
pw.print(" Args: "); pw.println(mAlgorithmArgs);
@@ -121,6 +132,7 @@
* A builder for {@link UserData} objects.
*/
public static final class Builder {
+ private final String mId;
private final ArrayList<String> mRemoteIds;
private final ArrayList<String> mValues;
private String mAlgorithm;
@@ -131,16 +143,28 @@
* Creates a new builder for the user data used for <a href="#FieldClassification">field
* classification</a>.
*
+ * <p>The user data must contain at least one pair of {@code remoteId} -> {@code value}, and
+ * more pairs can be added through the {@link #add(String, String)} method.
+ *
+ * @param id id used to identify the whole {@link UserData} object. This id is also returned
+ * by {@link AutofillManager#getUserDataId()}, which can be used to check if the
+ * {@link UserData} is up-to-date without fetching the whole object (through
+ * {@link AutofillManager#getUserData()}).
+ * @param remoteId unique string used to identify a user data value.
+ * @param value value of the user data.
+ *
* @throws IllegalArgumentException if any of the following occurs:
* <ol>
+ * <li>{@code id} is empty
* <li>{@code remoteId} is empty
* <li>{@code value} is empty
* <li>the length of {@code value} is lower than {@link UserData#getMinValueLength()}
* <li>the length of {@code value} is higher than {@link UserData#getMaxValueLength()}
* </ol>
*/
- public Builder(@NonNull String remoteId, @NonNull String value) {
- checkValidRemoteId(remoteId);
+ public Builder(@NonNull String id, @NonNull String remoteId, @NonNull String value) {
+ mId = checkNotEmpty("id", id);
+ checkNotEmpty("remoteId", remoteId);
checkValidValue(value);
final int capacity = getMaxUserDataSize();
mRemoteIds = new ArrayList<>(capacity);
@@ -188,7 +212,7 @@
*/
public Builder add(@NonNull String remoteId, @NonNull String value) {
throwIfDestroyed();
- checkValidRemoteId(remoteId);
+ checkNotEmpty("remoteId", remoteId);
checkValidValue(value);
Preconditions.checkState(!mRemoteIds.contains(remoteId),
@@ -205,9 +229,10 @@
return this;
}
- private void checkValidRemoteId(@Nullable String remoteId) {
- Preconditions.checkNotNull(remoteId);
- Preconditions.checkArgument(!remoteId.isEmpty(), "remoteId cannot be empty");
+ private String checkNotEmpty(@NonNull String name, @Nullable String value) {
+ Preconditions.checkNotNull(value);
+ Preconditions.checkArgument(!TextUtils.isEmpty(value), "%s cannot be empty", name);
+ return value;
}
private void checkValidValue(@Nullable String value) {
@@ -246,7 +271,8 @@
public String toString() {
if (!sDebug) return super.toString();
- final StringBuilder builder = new StringBuilder("UserData: [algorithm=").append(mAlgorithm);
+ final StringBuilder builder = new StringBuilder("UserData: [id=").append(mId)
+ .append(", algorithm=").append(mAlgorithm);
// Cannot disclose remote ids or values because they could contain PII
builder.append(", remoteIds=");
Helper.appendRedacted(builder, mRemoteIds);
@@ -266,6 +292,7 @@
@Override
public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeString(mId);
parcel.writeStringArray(mRemoteIds);
parcel.writeStringArray(mValues);
parcel.writeString(mAlgorithm);
@@ -279,9 +306,10 @@
// Always go through the builder to ensure the data ingested by
// the system obeys the contract of the builder to avoid attacks
// using specially crafted parcels.
+ final String id = parcel.readString();
final String[] remoteIds = parcel.readStringArray();
final String[] values = parcel.readStringArray();
- final Builder builder = new Builder(remoteIds[0], values[0])
+ final Builder builder = new Builder(id, remoteIds[0], values[0])
.setFieldClassificationAlgorithm(parcel.readString(), parcel.readBundle());
for (int i = 1; i < remoteIds.length; i++) {
builder.add(remoteIds[i], values[i]);
diff --git a/core/java/android/service/autofill/ValueFinder.java b/core/java/android/service/autofill/ValueFinder.java
index 1705b7d..7f195d6 100644
--- a/core/java/android/service/autofill/ValueFinder.java
+++ b/core/java/android/service/autofill/ValueFinder.java
@@ -19,6 +19,7 @@
import android.annotation.Nullable;
import android.annotation.TestApi;
import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
/**
* Helper object used to obtain the value of a field in the screen being autofilled.
@@ -29,7 +30,17 @@
public interface ValueFinder {
/**
+ * Gets the value of a field as String, or {@code null} when not found.
+ */
+ @Nullable
+ default String findByAutofillId(@NonNull AutofillId id) {
+ final AutofillValue value = findRawValueByAutofillId(id);
+ return (value == null || !value.isText()) ? null : value.getTextValue().toString();
+ }
+
+ /**
* Gets the value of a field, or {@code null} when not found.
*/
- @Nullable String findByAutofillId(@NonNull AutofillId id);
+ @Nullable
+ AutofillValue findRawValueByAutofillId(@NonNull AutofillId id);
}
diff --git a/core/java/android/service/textclassifier/ITextClassificationCallback.aidl b/core/java/android/service/textclassifier/ITextClassificationCallback.aidl
new file mode 100644
index 0000000..10bfe63
--- /dev/null
+++ b/core/java/android/service/textclassifier/ITextClassificationCallback.aidl
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.textclassifier;
+
+import android.view.textclassifier.TextClassification;
+
+/**
+ * Callback for a TextClassification request.
+ * @hide
+ */
+oneway interface ITextClassificationCallback {
+ void onSuccess(in TextClassification classification);
+ void onFailure();
+}
diff --git a/core/java/android/service/textclassifier/ITextClassifierService.aidl b/core/java/android/service/textclassifier/ITextClassifierService.aidl
new file mode 100644
index 0000000..d2ffe34
--- /dev/null
+++ b/core/java/android/service/textclassifier/ITextClassifierService.aidl
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.textclassifier;
+
+import android.service.textclassifier.ITextClassificationCallback;
+import android.service.textclassifier.ITextLinksCallback;
+import android.service.textclassifier.ITextSelectionCallback;
+import android.view.textclassifier.TextClassification;
+import android.view.textclassifier.TextLinks;
+import android.view.textclassifier.TextSelection;
+
+/**
+ * TextClassifierService binder interface.
+ * See TextClassifier for interface documentation.
+ * {@hide}
+ */
+oneway interface ITextClassifierService {
+
+ void onSuggestSelection(
+ in CharSequence text, int selectionStartIndex, int selectionEndIndex,
+ in TextSelection.Options options,
+ in ITextSelectionCallback c);
+
+ void onClassifyText(
+ in CharSequence text, int startIndex, int endIndex,
+ in TextClassification.Options options,
+ in ITextClassificationCallback c);
+
+ void onGenerateLinks(
+ in CharSequence text,
+ in TextLinks.Options options,
+ in ITextLinksCallback c);
+}
diff --git a/core/java/android/service/textclassifier/ITextLinksCallback.aidl b/core/java/android/service/textclassifier/ITextLinksCallback.aidl
new file mode 100644
index 0000000..a9e0dde
--- /dev/null
+++ b/core/java/android/service/textclassifier/ITextLinksCallback.aidl
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.textclassifier;
+
+import android.view.textclassifier.TextLinks;
+
+/**
+ * Callback for a TextLinks request.
+ * @hide
+ */
+oneway interface ITextLinksCallback {
+ void onSuccess(in TextLinks links);
+ void onFailure();
+}
\ No newline at end of file
diff --git a/core/java/android/service/textclassifier/ITextSelectionCallback.aidl b/core/java/android/service/textclassifier/ITextSelectionCallback.aidl
new file mode 100644
index 0000000..1b4c4d1
--- /dev/null
+++ b/core/java/android/service/textclassifier/ITextSelectionCallback.aidl
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.textclassifier;
+
+import android.view.textclassifier.TextSelection;
+
+/**
+ * Callback for a TextSelection request.
+ * @hide
+ */
+oneway interface ITextSelectionCallback {
+ void onSuccess(in TextSelection selection);
+ void onFailure();
+}
\ No newline at end of file
diff --git a/core/java/android/service/textclassifier/TextClassifierService.java b/core/java/android/service/textclassifier/TextClassifierService.java
new file mode 100644
index 0000000..6c8c8bc
--- /dev/null
+++ b/core/java/android/service/textclassifier/TextClassifierService.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.textclassifier;
+
+import android.Manifest;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.os.CancellationSignal;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.Slog;
+import android.view.textclassifier.TextClassification;
+import android.view.textclassifier.TextClassifier;
+import android.view.textclassifier.TextLinks;
+import android.view.textclassifier.TextSelection;
+
+import com.android.internal.R;
+
+/**
+ * Abstract base class for the TextClassifier service.
+ *
+ * <p>A TextClassifier service provides text classification related features for the system.
+ * The system's default TextClassifierService is configured in
+ * {@code config_defaultTextClassifierService}. If this config has no value, a
+ * {@link android.view.textclassifier.TextClassifierImpl} is loaded in the calling app's process.
+ *
+ * <p>See: {@link TextClassifier}.
+ * See: {@link android.view.textclassifier.TextClassificationManager}.
+ *
+ * <p>Include the following in the manifest:
+ *
+ * <pre>
+ * {@literal
+ * <service android:name=".YourTextClassifierService"
+ * android:permission="android.permission.BIND_TEXTCLASSIFIER_SERVICE">
+ * <intent-filter>
+ * <action android:name="android.service.textclassifier.TextClassifierService" />
+ * </intent-filter>
+ * </service>}</pre>
+ *
+ * @see TextClassifier
+ * @hide
+ */
+@SystemApi
+public abstract class TextClassifierService extends Service {
+
+ private static final String LOG_TAG = "TextClassifierService";
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ * To be supported, the service must also require the
+ * {@link android.Manifest.permission#BIND_TEXTCLASSIFIER_SERVICE} permission so
+ * that other applications can not abuse it.
+ */
+ @SystemApi
+ public static final String SERVICE_INTERFACE =
+ "android.service.textclassifier.TextClassifierService";
+
+ private final ITextClassifierService.Stub mBinder = new ITextClassifierService.Stub() {
+
+ // TODO(b/72533911): Implement cancellation signal
+ @NonNull private final CancellationSignal mCancellationSignal = new CancellationSignal();
+
+ /** {@inheritDoc} */
+ @Override
+ public void onSuggestSelection(
+ CharSequence text, int selectionStartIndex, int selectionEndIndex,
+ TextSelection.Options options, ITextSelectionCallback callback)
+ throws RemoteException {
+ TextClassifierService.this.onSuggestSelection(
+ text, selectionStartIndex, selectionEndIndex, options, mCancellationSignal,
+ new Callback<TextSelection>() {
+ @Override
+ public void onSuccess(TextSelection result) {
+ try {
+ callback.onSuccess(result);
+ } catch (RemoteException e) {
+ Slog.d(LOG_TAG, "Error calling callback");
+ }
+ }
+
+ @Override
+ public void onFailure(CharSequence error) {
+ try {
+ if (callback.asBinder().isBinderAlive()) {
+ callback.onFailure();
+ }
+ } catch (RemoteException e) {
+ Slog.d(LOG_TAG, "Error calling callback");
+ }
+ }
+ });
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void onClassifyText(
+ CharSequence text, int startIndex, int endIndex,
+ TextClassification.Options options, ITextClassificationCallback callback)
+ throws RemoteException {
+ TextClassifierService.this.onClassifyText(
+ text, startIndex, endIndex, options, mCancellationSignal,
+ new Callback<TextClassification>() {
+ @Override
+ public void onSuccess(TextClassification result) {
+ try {
+ callback.onSuccess(result);
+ } catch (RemoteException e) {
+ Slog.d(LOG_TAG, "Error calling callback");
+ }
+ }
+
+ @Override
+ public void onFailure(CharSequence error) {
+ try {
+ callback.onFailure();
+ } catch (RemoteException e) {
+ Slog.d(LOG_TAG, "Error calling callback");
+ }
+ }
+ });
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void onGenerateLinks(
+ CharSequence text, TextLinks.Options options, ITextLinksCallback callback)
+ throws RemoteException {
+ TextClassifierService.this.onGenerateLinks(
+ text, options, mCancellationSignal,
+ new Callback<TextLinks>() {
+ @Override
+ public void onSuccess(TextLinks result) {
+ try {
+ callback.onSuccess(result);
+ } catch (RemoteException e) {
+ Slog.d(LOG_TAG, "Error calling callback");
+ }
+ }
+
+ @Override
+ public void onFailure(CharSequence error) {
+ try {
+ callback.onFailure();
+ } catch (RemoteException e) {
+ Slog.d(LOG_TAG, "Error calling callback");
+ }
+ }
+ });
+ }
+ };
+
+ @Nullable
+ @Override
+ public final IBinder onBind(Intent intent) {
+ if (SERVICE_INTERFACE.equals(intent.getAction())) {
+ return mBinder;
+ }
+ return null;
+ }
+
+ /**
+ * Returns suggested text selection start and end indices, recognized entity types, and their
+ * associated confidence scores. The entity types are ordered from highest to lowest scoring.
+ *
+ * @param text text providing context for the selected text (which is specified
+ * by the sub sequence starting at selectionStartIndex and ending at selectionEndIndex)
+ * @param selectionStartIndex start index of the selected part of text
+ * @param selectionEndIndex end index of the selected part of text
+ * @param options optional input parameters
+ * @param cancellationSignal object to watch for canceling the current operation
+ * @param callback the callback to return the result to
+ */
+ public abstract void onSuggestSelection(
+ @NonNull CharSequence text,
+ @IntRange(from = 0) int selectionStartIndex,
+ @IntRange(from = 0) int selectionEndIndex,
+ @Nullable TextSelection.Options options,
+ @NonNull CancellationSignal cancellationSignal,
+ @NonNull Callback<TextSelection> callback);
+
+ /**
+ * Classifies the specified text and returns a {@link TextClassification} object that can be
+ * used to generate a widget for handling the classified text.
+ *
+ * @param text text providing context for the text to classify (which is specified
+ * by the sub sequence starting at startIndex and ending at endIndex)
+ * @param startIndex start index of the text to classify
+ * @param endIndex end index of the text to classify
+ * @param options optional input parameters
+ * @param cancellationSignal object to watch for canceling the current operation
+ * @param callback the callback to return the result to
+ */
+ public abstract void onClassifyText(
+ @NonNull CharSequence text,
+ @IntRange(from = 0) int startIndex,
+ @IntRange(from = 0) int endIndex,
+ @Nullable TextClassification.Options options,
+ @NonNull CancellationSignal cancellationSignal,
+ @NonNull Callback<TextClassification> callback);
+
+ /**
+ * Generates and returns a {@link TextLinks} that may be applied to the text to annotate it with
+ * links information.
+ *
+ * @param text the text to generate annotations for
+ * @param options configuration for link generation
+ * @param cancellationSignal object to watch for canceling the current operation
+ * @param callback the callback to return the result to
+ */
+ public abstract void onGenerateLinks(
+ @NonNull CharSequence text,
+ @Nullable TextLinks.Options options,
+ @NonNull CancellationSignal cancellationSignal,
+ @NonNull Callback<TextLinks> callback);
+
+ /**
+ * Callbacks for TextClassifierService results.
+ *
+ * @param <T> the type of the result
+ * @hide
+ */
+ @SystemApi
+ public interface Callback<T> {
+ /**
+ * Returns the result.
+ */
+ void onSuccess(T result);
+
+ /**
+ * Signals a failure.
+ */
+ void onFailure(CharSequence error);
+ }
+
+ /**
+ * Returns the component name of the system default textclassifier service if it can be found
+ * on the system. Otherwise, returns null.
+ * @hide
+ */
+ @Nullable
+ public static ComponentName getServiceComponentName(Context context) {
+ final String str = context.getString(R.string.config_defaultTextClassifierService);
+ if (!TextUtils.isEmpty(str)) {
+ try {
+ final ComponentName componentName = ComponentName.unflattenFromString(str);
+ final Intent intent = new Intent(SERVICE_INTERFACE).setComponent(componentName);
+ final ServiceInfo si = context.getPackageManager()
+ .getServiceInfo(intent.getComponent(), 0);
+ final String permission = si == null ? null : si.permission;
+ if (Manifest.permission.BIND_TEXTCLASSIFIER_SERVICE.equals(permission)) {
+ return componentName;
+ }
+ Slog.w(LOG_TAG, String.format(
+ "Service %s should require %s permission. Found %s permission",
+ intent.getComponent().flattenToString(),
+ Manifest.permission.BIND_TEXTCLASSIFIER_SERVICE,
+ si.permission));
+ } catch (PackageManager.NameNotFoundException e) {
+ Slog.w(LOG_TAG, String.format("Service %s not found", str));
+ }
+ } else {
+ Slog.d(LOG_TAG, "No configured system TextClassifierService");
+ }
+ return null;
+ }
+}
diff --git a/core/java/android/text/format/DateFormat.java b/core/java/android/text/format/DateFormat.java
index de2dcce..413cd10 100755
--- a/core/java/android/text/format/DateFormat.java
+++ b/core/java/android/text/format/DateFormat.java
@@ -431,7 +431,7 @@
int c = s.charAt(i);
if (c == QUOTE) {
- count = appendQuotedText(s, i, len);
+ count = appendQuotedText(s, i);
len = s.length();
continue;
}
@@ -574,36 +574,48 @@
: String.format(Locale.getDefault(), "%d", year);
}
- private static int appendQuotedText(SpannableStringBuilder s, int i, int len) {
- if (i + 1 < len && s.charAt(i + 1) == QUOTE) {
- s.delete(i, i + 1);
+
+ /**
+ * Strips quotation marks from the {@code formatString} and appends the result back to the
+ * {@code formatString}.
+ *
+ * @param formatString the format string, as described in
+ * {@link android.text.format.DateFormat}, to be modified
+ * @param index index of the first quote
+ * @return the length of the quoted text that was appended.
+ * @hide
+ */
+ public static int appendQuotedText(SpannableStringBuilder formatString, int index) {
+ int length = formatString.length();
+ if (index + 1 < length && formatString.charAt(index + 1) == QUOTE) {
+ formatString.delete(index, index + 1);
return 1;
}
int count = 0;
// delete leading quote
- s.delete(i, i + 1);
- len--;
+ formatString.delete(index, index + 1);
+ length--;
- while (i < len) {
- char c = s.charAt(i);
+ while (index < length) {
+ char c = formatString.charAt(index);
if (c == QUOTE) {
// QUOTEQUOTE -> QUOTE
- if (i + 1 < len && s.charAt(i + 1) == QUOTE) {
+ if (index + 1 < length && formatString.charAt(index + 1) == QUOTE) {
- s.delete(i, i + 1);
- len--;
+ formatString.delete(index, index + 1);
+ length--;
count++;
- i++;
+ index++;
} else {
// Closing QUOTE ends quoted text copying
- s.delete(i, i + 1);
+ formatString.delete(index, index + 1);
break;
}
} else {
- i++;
+ index++;
count++;
}
}
diff --git a/core/java/android/text/util/Linkify.java b/core/java/android/text/util/Linkify.java
index 768aee9..d973d4a 100644
--- a/core/java/android/text/util/Linkify.java
+++ b/core/java/android/text/util/Linkify.java
@@ -19,6 +19,7 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.UiThread;
import android.content.Context;
import android.telephony.PhoneNumberUtils;
import android.telephony.TelephonyManager;
@@ -29,12 +30,16 @@
import android.text.method.MovementMethod;
import android.text.style.URLSpan;
import android.util.Patterns;
+import android.view.textclassifier.TextClassifier;
+import android.view.textclassifier.TextLinks;
+import android.view.textclassifier.TextLinks.TextLinkSpan;
import android.webkit.WebView;
import android.widget.TextView;
import com.android.i18n.phonenumbers.PhoneNumberMatch;
import com.android.i18n.phonenumbers.PhoneNumberUtil;
import com.android.i18n.phonenumbers.PhoneNumberUtil.Leniency;
+import com.android.internal.util.Preconditions;
import libcore.util.EmptyArray;
@@ -46,6 +51,12 @@
import java.util.Collections;
import java.util.Comparator;
import java.util.Locale;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Future;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -479,6 +490,195 @@
return hasMatches;
}
+ /**
+ * Scans the text of the provided TextView and turns all occurrences of the entity types
+ * specified by {@code options} into clickable links. If links are found, this method
+ * removes any pre-existing {@link TextLinkSpan} attached to the text (to avoid
+ * problems if you call it repeatedly on the same text) and sets the movement method for the
+ * TextView to LinkMovementMethod.
+ *
+ * <p><strong>Note:</strong> This method returns immediately but generates the links with
+ * the specified classifier on a background thread. The generated links are applied on the
+ * calling thread.
+ *
+ * @param textView TextView whose text is to be marked-up with links
+ * @param options optional parameters to specify how to generate the links
+ *
+ * @return a future that may be used to interrupt or query the background task
+ */
+ @UiThread
+ public static Future<Void> addLinksAsync(
+ @NonNull TextView textView,
+ @Nullable TextLinks.Options options) {
+ return addLinksAsync(textView, options, null /* executor */, null /* callback */);
+ }
+
+ /**
+ * Scans the text of the provided TextView and turns all occurrences of the entity types
+ * specified by {@code options} into clickable links. If links are found, this method
+ * removes any pre-existing {@link TextLinkSpan} attached to the text (to avoid
+ * problems if you call it repeatedly on the same text) and sets the movement method for the
+ * TextView to LinkMovementMethod.
+ *
+ * <p><strong>Note:</strong> This method returns immediately but generates the links with
+ * the specified classifier on a background thread. The generated links are applied on the
+ * calling thread.
+ *
+ * @param textView TextView whose text is to be marked-up with links
+ * @param options optional parameters to specify how to generate the links
+ * @param executor Executor that runs the background task
+ * @param callback Callback that receives the final status of the background task execution
+ *
+ * @return a future that may be used to interrupt or query the background task
+ */
+ @UiThread
+ public static Future<Void> addLinksAsync(
+ @NonNull TextView textView,
+ @Nullable TextLinks.Options options,
+ @Nullable Executor executor,
+ @Nullable Consumer<Integer> callback) {
+ Preconditions.checkNotNull(textView);
+ final CharSequence text = textView.getText();
+ final Spannable spannable = (text instanceof Spannable)
+ ? (Spannable) text : SpannableString.valueOf(text);
+ final Runnable modifyTextView = () -> {
+ addLinkMovementMethod(textView);
+ if (spannable != text) {
+ textView.setText(spannable);
+ }
+ };
+ return addLinksAsync(spannable, textView.getTextClassifier(),
+ options, executor, callback, modifyTextView);
+ }
+
+ /**
+ * Scans the text of the provided TextView and turns all occurrences of the entity types
+ * specified by {@code options} into clickable links. If links are found, this method
+ * removes any pre-existing {@link TextLinkSpan} attached to the text to avoid
+ * problems if you call it repeatedly on the same text.
+ *
+ * <p><strong>Note:</strong> This method returns immediately but generates the links with
+ * the specified classifier on a background thread. The generated links are applied on the
+ * calling thread.
+ *
+ * <p><strong>Note:</strong> If the text is currently attached to a TextView, this method
+ * should be called on the UI thread.
+ *
+ * @param text Spannable whose text is to be marked-up with links
+ * @param classifier the TextClassifier to use to generate the links
+ * @param options optional parameters to specify how to generate the links
+ *
+ * @return a future that may be used to interrupt or query the background task
+ */
+ public static Future<Void> addLinksAsync(
+ @NonNull Spannable text,
+ @NonNull TextClassifier classifier,
+ @Nullable TextLinks.Options options) {
+ return addLinksAsync(text, classifier, options, null /* executor */, null /* callback */);
+ }
+
+ /**
+ * Scans the text of the provided TextView and turns all occurrences of the entity types
+ * specified by the link {@code mask} into clickable links. If links are found, this method
+ * removes any pre-existing {@link TextLinkSpan} attached to the text to avoid
+ * problems if you call it repeatedly on the same text.
+ *
+ * <p><strong>Note:</strong> This method returns immediately but generates the links with
+ * the specified classifier on a background thread. The generated links are applied on the
+ * calling thread.
+ *
+ * <p><strong>Note:</strong> If the text is currently attached to a TextView, this method
+ * should be called on the UI thread.
+ *
+ * @param text Spannable whose text is to be marked-up with links
+ * @param classifier the TextClassifier to use to generate the links
+ * @param mask mask to define which kinds of links will be generated
+ *
+ * @return a future that may be used to interrupt or query the background task
+ */
+ public static Future<Void> addLinksAsync(
+ @NonNull Spannable text,
+ @NonNull TextClassifier classifier,
+ @LinkifyMask int mask) {
+ return addLinksAsync(text, classifier, TextLinks.Options.fromLinkMask(mask),
+ null /* executor */, null /* callback */);
+ }
+
+ /**
+ * Scans the text of the provided TextView and turns all occurrences of the entity types
+ * specified by {@code options} into clickable links. If links are found, this method
+ * removes any pre-existing {@link TextLinkSpan} attached to the text to avoid
+ * problems if you call it repeatedly on the same text.
+ *
+ * <p><strong>Note:</strong> This method returns immediately but generates the links with
+ * the specified classifier on a background thread. The generated links are applied on the
+ * calling thread.
+ *
+ * <p><strong>Note:</strong> If the text is currently attached to a TextView, this method
+ * should be called on the UI thread.
+ *
+ * @param text Spannable whose text is to be marked-up with links
+ * @param classifier the TextClassifier to use to generate the links
+ * @param options optional parameters to specify how to generate the links
+ * @param executor Executor that runs the background task
+ * @param callback Callback that receives the final status of the background task execution
+ *
+ * @return a future that may be used to interrupt or query the background task
+ */
+ public static Future<Void> addLinksAsync(
+ @NonNull Spannable text,
+ @NonNull TextClassifier classifier,
+ @Nullable TextLinks.Options options,
+ @Nullable Executor executor,
+ @Nullable Consumer<Integer> callback) {
+ return addLinksAsync(text, classifier, options, executor, callback,
+ null /* modifyTextView */);
+ }
+
+ private static Future<Void> addLinksAsync(
+ @NonNull Spannable text,
+ @NonNull TextClassifier classifier,
+ @Nullable TextLinks.Options options,
+ @Nullable Executor executor,
+ @Nullable Consumer<Integer> callback,
+ @Nullable Runnable modifyTextView) {
+ Preconditions.checkNotNull(text);
+ Preconditions.checkNotNull(classifier);
+ final Supplier<TextLinks> supplier = () -> classifier.generateLinks(text, options);
+ final Consumer<TextLinks> consumer = links -> {
+ if (links.getLinks().isEmpty()) {
+ if (callback != null) {
+ callback.accept(TextLinks.STATUS_NO_LINKS_FOUND);
+ }
+ return;
+ }
+
+ final TextLinkSpan[] old = text.getSpans(0, text.length(), TextLinkSpan.class);
+ for (int i = old.length - 1; i >= 0; i--) {
+ text.removeSpan(old[i]);
+ }
+
+ final Function<TextLinks.TextLink, TextLinkSpan> spanFactory = (options == null)
+ ? null : options.getSpanFactory();
+ final @TextLinks.ApplyStrategy int applyStrategy = (options == null)
+ ? TextLinks.APPLY_STRATEGY_IGNORE : options.getApplyStrategy();
+ final @TextLinks.Status int result = links.apply(text, applyStrategy, spanFactory);
+ if (result == TextLinks.STATUS_LINKS_APPLIED) {
+ if (modifyTextView != null) {
+ modifyTextView.run();
+ }
+ }
+ if (callback != null) {
+ callback.accept(result);
+ }
+ };
+ if (executor == null) {
+ return CompletableFuture.supplyAsync(supplier).thenAccept(consumer);
+ } else {
+ return CompletableFuture.supplyAsync(supplier, executor).thenAccept(consumer);
+ }
+ }
+
private static final void applyLink(String url, int start, int end, Spannable text) {
URLSpan span = new URLSpan(url);
diff --git a/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java b/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java
index a4c590f..9436b29 100644
--- a/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java
+++ b/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java
@@ -438,8 +438,8 @@
List<Integer> flagsList = new ArrayList<>();
// Proof-of-rotation struct:
- // is basically a singly linked list of nodes, called levels here, each of which have the
- // following structure:
+ // A uint32 version code followed by basically a singly linked list of nodes, called levels
+ // here, each of which have the following structure:
// * length-prefix for the entire level
// - length-prefixed signed data (if previous level exists)
// * length-prefixed X509 Certificate
@@ -450,9 +450,13 @@
// - length-prefixed signature over the signed data in this level. The signature here
// is verified using the certificate from the previous level.
// The linking is provided by the certificate of each level signing the one of the next.
- while (porBuf.hasRemaining()) {
- levelCount++;
- try {
+
+ try {
+
+ // get the version code, but don't do anything with it: creator knew about all our flags
+ porBuf.getInt();
+ while (porBuf.hasRemaining()) {
+ levelCount++;
ByteBuffer level = getLengthPrefixedSlice(porBuf);
ByteBuffer signedData = getLengthPrefixedSlice(level);
int flags = level.getInt();
@@ -491,17 +495,17 @@
lastSigAlgorithm = sigAlgorithm;
certs.add(lastCert);
flagsList.add(flags);
- } catch (IOException | BufferUnderflowException e) {
- throw new IOException("Failed to parse Proof-of-rotation record", e);
- } catch (NoSuchAlgorithmException | InvalidKeyException
- | InvalidAlgorithmParameterException | SignatureException e) {
- throw new SecurityException(
- "Failed to verify signature over signed data for certificate #"
- + levelCount + " when verifying Proof-of-rotation record", e);
- } catch (CertificateException e) {
- throw new SecurityException("Failed to decode certificate #" + levelCount
- + " when verifying Proof-of-rotation record", e);
}
+ } catch (IOException | BufferUnderflowException e) {
+ throw new IOException("Failed to parse Proof-of-rotation record", e);
+ } catch (NoSuchAlgorithmException | InvalidKeyException
+ | InvalidAlgorithmParameterException | SignatureException e) {
+ throw new SecurityException(
+ "Failed to verify signature over signed data for certificate #"
+ + levelCount + " when verifying Proof-of-rotation record", e);
+ } catch (CertificateException e) {
+ throw new SecurityException("Failed to decode certificate #" + levelCount
+ + " when verifying Proof-of-rotation record", e);
}
return new VerifiedProofOfRotation(certs, flagsList);
}
diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java
index 4b24a71..dac1998 100644
--- a/core/java/android/view/autofill/AutofillManager.java
+++ b/core/java/android/view/autofill/AutofillManager.java
@@ -1099,6 +1099,30 @@
}
/**
+ * Gets the id of the {@link UserData} used for
+ * <a href="AutofillService.html#FieldClassification">field classification</a>.
+ *
+ * <p>This method is useful when the service must check the status of the {@link UserData} in
+ * the device without fetching the whole object.
+ *
+ * <p><b>Note:</b> This method should only be called by an app providing an autofill service,
+ * and it's ignored if the caller currently doesn't have an enabled autofill service for
+ * the user.
+ *
+ * @return id of the {@link UserData} previously set by {@link #setUserData(UserData)}
+ * or {@code null} if it was reset or if the caller currently does not have an enabled autofill
+ * service for the user.
+ */
+ @Nullable public String getUserDataId() {
+ try {
+ return mService.getUserDataId();
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ return null;
+ }
+ }
+
+ /**
* Gets the user data used for
* <a href="AutofillService.html#FieldClassification">field classification</a>.
*
@@ -1119,7 +1143,7 @@
}
/**
- * Sets the user data used for
+ * Sets the {@link UserData} used for
* <a href="AutofillService.html#FieldClassification">field classification</a>
*
* <p><b>Note:</b> This method should only be called by an app providing an autofill service,
diff --git a/core/java/android/view/autofill/IAutoFillManager.aidl b/core/java/android/view/autofill/IAutoFillManager.aidl
index 1a11fbb..0018547 100644
--- a/core/java/android/view/autofill/IAutoFillManager.aidl
+++ b/core/java/android/view/autofill/IAutoFillManager.aidl
@@ -56,6 +56,7 @@
boolean isServiceEnabled(int userId, String packageName);
void onPendingSaveUi(int operation, IBinder token);
UserData getUserData();
+ String getUserDataId();
void setUserData(in UserData userData);
boolean isFieldClassificationEnabled();
ComponentName getAutofillServiceComponentName();
diff --git a/core/java/android/view/textclassifier/SystemTextClassifier.java b/core/java/android/view/textclassifier/SystemTextClassifier.java
new file mode 100644
index 0000000..af55dcd
--- /dev/null
+++ b/core/java/android/view/textclassifier/SystemTextClassifier.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.WorkerThread;
+import android.content.Context;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.service.textclassifier.ITextClassificationCallback;
+import android.service.textclassifier.ITextClassifierService;
+import android.service.textclassifier.ITextLinksCallback;
+import android.service.textclassifier.ITextSelectionCallback;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Proxy to the system's default TextClassifier.
+ */
+final class SystemTextClassifier implements TextClassifier {
+
+ private static final String LOG_TAG = "SystemTextClassifier";
+
+ private final ITextClassifierService mManagerService;
+ private final TextClassifier mFallback;
+
+ SystemTextClassifier(Context context) throws ServiceManager.ServiceNotFoundException {
+ mManagerService = ITextClassifierService.Stub.asInterface(
+ ServiceManager.getServiceOrThrow(Context.TEXT_CLASSIFICATION_SERVICE));
+ mFallback = new TextClassifierImpl(context);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @WorkerThread
+ public TextSelection suggestSelection(
+ @NonNull CharSequence text,
+ @IntRange(from = 0) int selectionStartIndex,
+ @IntRange(from = 0) int selectionEndIndex,
+ @Nullable TextSelection.Options options) {
+ Utils.validate(text, selectionStartIndex, selectionEndIndex, false /* allowInMainThread */);
+ try {
+ final TextSelectionCallback callback = new TextSelectionCallback();
+ mManagerService.onSuggestSelection(
+ text, selectionStartIndex, selectionEndIndex, options, callback);
+ final TextSelection selection = callback.mReceiver.get();
+ if (selection != null) {
+ return selection;
+ }
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ } catch (InterruptedException e) {
+ Log.d(LOG_TAG, e.getMessage());
+ }
+ return mFallback.suggestSelection(text, selectionStartIndex, selectionEndIndex, options);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @WorkerThread
+ public TextClassification classifyText(
+ @NonNull CharSequence text,
+ @IntRange(from = 0) int startIndex,
+ @IntRange(from = 0) int endIndex,
+ @Nullable TextClassification.Options options) {
+ Utils.validate(text, startIndex, endIndex, false /* allowInMainThread */);
+ try {
+ final TextClassificationCallback callback = new TextClassificationCallback();
+ mManagerService.onClassifyText(text, startIndex, endIndex, options, callback);
+ final TextClassification classification = callback.mReceiver.get();
+ if (classification != null) {
+ return classification;
+ }
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ } catch (InterruptedException e) {
+ Log.d(LOG_TAG, e.getMessage());
+ }
+ return mFallback.classifyText(text, startIndex, endIndex, options);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @WorkerThread
+ public TextLinks generateLinks(
+ @NonNull CharSequence text, @Nullable TextLinks.Options options) {
+ Utils.validate(text, false /* allowInMainThread */);
+ try {
+ final TextLinksCallback callback = new TextLinksCallback();
+ mManagerService.onGenerateLinks(text, options, callback);
+ final TextLinks links = callback.mReceiver.get();
+ if (links != null) {
+ return links;
+ }
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ } catch (InterruptedException e) {
+ Log.d(LOG_TAG, e.getMessage());
+ }
+ return mFallback.generateLinks(text, options);
+ }
+
+ private static final class TextSelectionCallback extends ITextSelectionCallback.Stub {
+
+ final ResponseReceiver<TextSelection> mReceiver = new ResponseReceiver<>();
+
+ @Override
+ public void onSuccess(TextSelection selection) {
+ mReceiver.onSuccess(selection);
+ }
+
+ @Override
+ public void onFailure() {
+ mReceiver.onFailure();
+ }
+ }
+
+ private static final class TextClassificationCallback extends ITextClassificationCallback.Stub {
+
+ final ResponseReceiver<TextClassification> mReceiver = new ResponseReceiver<>();
+
+ @Override
+ public void onSuccess(TextClassification classification) {
+ mReceiver.onSuccess(classification);
+ }
+
+ @Override
+ public void onFailure() {
+ mReceiver.onFailure();
+ }
+ }
+
+ private static final class TextLinksCallback extends ITextLinksCallback.Stub {
+
+ final ResponseReceiver<TextLinks> mReceiver = new ResponseReceiver<>();
+
+ @Override
+ public void onSuccess(TextLinks links) {
+ mReceiver.onSuccess(links);
+ }
+
+ @Override
+ public void onFailure() {
+ mReceiver.onFailure();
+ }
+ }
+
+ private static final class ResponseReceiver<T> {
+
+ private final CountDownLatch mLatch = new CountDownLatch(1);
+
+ private T mResponse;
+
+ public void onSuccess(T response) {
+ mResponse = response;
+ mLatch.countDown();
+ }
+
+ public void onFailure() {
+ Log.e(LOG_TAG, "Request failed.", null);
+ mLatch.countDown();
+ }
+
+ @Nullable
+ public T get() throws InterruptedException {
+ // If this is running on the main thread, do not block for a response.
+ // The response will unfortunately be null and the TextClassifier should depend on its
+ // fallback.
+ // NOTE that TextClassifier calls should preferably always be called on a worker thread.
+ if (Looper.myLooper() != Looper.getMainLooper()) {
+ mLatch.await(2, TimeUnit.SECONDS);
+ }
+ return mResponse;
+ }
+ }
+}
diff --git a/core/java/android/view/textclassifier/TextClassification.aidl b/core/java/android/view/textclassifier/TextClassification.aidl
new file mode 100644
index 0000000..9fefe5d
--- /dev/null
+++ b/core/java/android/view/textclassifier/TextClassification.aidl
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier;
+
+parcelable TextClassification;
+parcelable TextClassification.Options;
\ No newline at end of file
diff --git a/core/java/android/view/textclassifier/TextClassification.java b/core/java/android/view/textclassifier/TextClassification.java
index 54e93d5..a6a2a94 100644
--- a/core/java/android/view/textclassifier/TextClassification.java
+++ b/core/java/android/view/textclassifier/TextClassification.java
@@ -97,7 +97,7 @@
* });
* }</pre>
*/
-public final class TextClassification {
+public final class TextClassification implements Parcelable {
/**
* @hide
@@ -310,42 +310,6 @@
mSignature);
}
- /** Helper for parceling via #ParcelableWrapper. */
- private void writeToParcel(Parcel dest, int flags) {
- dest.writeString(mText);
- final Bitmap primaryIconBitmap = drawableToBitmap(mPrimaryIcon, MAX_PRIMARY_ICON_SIZE);
- dest.writeInt(primaryIconBitmap != null ? 1 : 0);
- if (primaryIconBitmap != null) {
- primaryIconBitmap.writeToParcel(dest, flags);
- }
- dest.writeString(mPrimaryLabel);
- dest.writeInt(mPrimaryIntent != null ? 1 : 0);
- if (mPrimaryIntent != null) {
- mPrimaryIntent.writeToParcel(dest, flags);
- }
- // mPrimaryOnClickListener is not parcelable.
- dest.writeTypedList(drawablesToBitmaps(mSecondaryIcons, MAX_SECONDARY_ICON_SIZE));
- dest.writeStringList(mSecondaryLabels);
- dest.writeTypedList(mSecondaryIntents);
- mEntityConfidence.writeToParcel(dest, flags);
- dest.writeString(mSignature);
- }
-
- /** Helper for unparceling via #ParcelableWrapper. */
- private TextClassification(Parcel in) {
- mText = in.readString();
- mPrimaryIcon = in.readInt() == 0
- ? null : new BitmapDrawable(null, Bitmap.CREATOR.createFromParcel(in));
- mPrimaryLabel = in.readString();
- mPrimaryIntent = in.readInt() == 0 ? null : Intent.CREATOR.createFromParcel(in);
- mPrimaryOnClickListener = null; // not parcelable
- mSecondaryIcons = bitmapsToDrawables(in.createTypedArrayList(Bitmap.CREATOR));
- mSecondaryLabels = in.createStringArrayList();
- mSecondaryIntents = in.createTypedArrayList(Intent.CREATOR);
- mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in);
- mSignature = in.readString();
- }
-
/**
* Creates an OnClickListener that starts an activity with the specified intent.
*
@@ -675,46 +639,56 @@
}
}
- /**
- * Parcelable wrapper for TextClassification objects.
- * @hide
- */
- public static final class ParcelableWrapper implements Parcelable {
+ @Override
+ public int describeContents() {
+ return 0;
+ }
- @NonNull private TextClassification mTextClassification;
-
- public ParcelableWrapper(@NonNull TextClassification textClassification) {
- Preconditions.checkNotNull(textClassification);
- mTextClassification = textClassification;
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mText);
+ final Bitmap primaryIconBitmap = drawableToBitmap(mPrimaryIcon, MAX_PRIMARY_ICON_SIZE);
+ dest.writeInt(primaryIconBitmap != null ? 1 : 0);
+ if (primaryIconBitmap != null) {
+ primaryIconBitmap.writeToParcel(dest, flags);
}
-
- @NonNull
- public TextClassification getTextClassification() {
- return mTextClassification;
+ dest.writeString(mPrimaryLabel);
+ dest.writeInt(mPrimaryIntent != null ? 1 : 0);
+ if (mPrimaryIntent != null) {
+ mPrimaryIntent.writeToParcel(dest, flags);
}
+ // mPrimaryOnClickListener is not parcelable.
+ dest.writeTypedList(drawablesToBitmaps(mSecondaryIcons, MAX_SECONDARY_ICON_SIZE));
+ dest.writeStringList(mSecondaryLabels);
+ dest.writeTypedList(mSecondaryIntents);
+ mEntityConfidence.writeToParcel(dest, flags);
+ dest.writeString(mSignature);
+ }
- @Override
- public int describeContents() {
- return 0;
- }
+ public static final Parcelable.Creator<TextClassification> CREATOR =
+ new Parcelable.Creator<TextClassification>() {
+ @Override
+ public TextClassification createFromParcel(Parcel in) {
+ return new TextClassification(in);
+ }
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- mTextClassification.writeToParcel(dest, flags);
- }
+ @Override
+ public TextClassification[] newArray(int size) {
+ return new TextClassification[size];
+ }
+ };
- public static final Parcelable.Creator<ParcelableWrapper> CREATOR =
- new Parcelable.Creator<ParcelableWrapper>() {
- @Override
- public ParcelableWrapper createFromParcel(Parcel in) {
- return new ParcelableWrapper(new TextClassification(in));
- }
-
- @Override
- public ParcelableWrapper[] newArray(int size) {
- return new ParcelableWrapper[size];
- }
- };
-
+ private TextClassification(Parcel in) {
+ mText = in.readString();
+ mPrimaryIcon = in.readInt() == 0
+ ? null : new BitmapDrawable(null, Bitmap.CREATOR.createFromParcel(in));
+ mPrimaryLabel = in.readString();
+ mPrimaryIntent = in.readInt() == 0 ? null : Intent.CREATOR.createFromParcel(in);
+ mPrimaryOnClickListener = null; // not parcelable
+ mSecondaryIcons = bitmapsToDrawables(in.createTypedArrayList(Bitmap.CREATOR));
+ mSecondaryLabels = in.createStringArrayList();
+ mSecondaryIntents = in.createTypedArrayList(Intent.CREATOR);
+ mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in);
+ mSignature = in.readString();
}
}
diff --git a/core/java/android/view/textclassifier/TextClassificationManager.java b/core/java/android/view/textclassifier/TextClassificationManager.java
index d7b0776..300aef2 100644
--- a/core/java/android/view/textclassifier/TextClassificationManager.java
+++ b/core/java/android/view/textclassifier/TextClassificationManager.java
@@ -19,6 +19,8 @@
import android.annotation.Nullable;
import android.annotation.SystemService;
import android.content.Context;
+import android.os.ServiceManager;
+import android.service.textclassifier.TextClassifierService;
import com.android.internal.util.Preconditions;
@@ -28,10 +30,16 @@
@SystemService(Context.TEXT_CLASSIFICATION_SERVICE)
public final class TextClassificationManager {
- private final Object mTextClassifierLock = new Object();
+ // TODO: Make this a configurable flag.
+ private static final boolean SYSTEM_TEXT_CLASSIFIER_ENABLED = true;
+
+ private static final String LOG_TAG = "TextClassificationManager";
+
+ private final Object mLock = new Object();
private final Context mContext;
private TextClassifier mTextClassifier;
+ private TextClassifier mSystemTextClassifier;
/** @hide */
public TextClassificationManager(Context context) {
@@ -39,12 +47,39 @@
}
/**
+ * Returns the system's default TextClassifier.
+ * @hide
+ */
+ // TODO: Unhide when this is ready.
+ public TextClassifier getSystemDefaultTextClassifier() {
+ synchronized (mLock) {
+ if (mSystemTextClassifier == null && isSystemTextClassifierEnabled()) {
+ try {
+ Log.d(LOG_TAG, "Initialized SystemTextClassifier");
+ mSystemTextClassifier = new SystemTextClassifier(mContext);
+ } catch (ServiceManager.ServiceNotFoundException e) {
+ Log.e(LOG_TAG, "Could not initialize SystemTextClassifier", e);
+ }
+ }
+ if (mSystemTextClassifier == null) {
+ Log.d(LOG_TAG, "Using an in-process TextClassifier as the system default");
+ mSystemTextClassifier = new TextClassifierImpl(mContext);
+ }
+ }
+ return mSystemTextClassifier;
+ }
+
+ /**
* Returns the text classifier.
*/
public TextClassifier getTextClassifier() {
- synchronized (mTextClassifierLock) {
+ synchronized (mLock) {
if (mTextClassifier == null) {
- mTextClassifier = new TextClassifierImpl(mContext);
+ if (isSystemTextClassifierEnabled()) {
+ mTextClassifier = getSystemDefaultTextClassifier();
+ } else {
+ mTextClassifier = new TextClassifierImpl(mContext);
+ }
}
return mTextClassifier;
}
@@ -56,8 +91,13 @@
* Set to {@link TextClassifier#NO_OP} to disable text classifier features.
*/
public void setTextClassifier(@Nullable TextClassifier textClassifier) {
- synchronized (mTextClassifierLock) {
+ synchronized (mLock) {
mTextClassifier = textClassifier;
}
}
+
+ private boolean isSystemTextClassifierEnabled() {
+ return SYSTEM_TEXT_CLASSIFIER_ENABLED
+ && TextClassifierService.getServiceComponentName(mContext) != null;
+ }
}
diff --git a/core/java/android/view/textclassifier/TextClassifier.java b/core/java/android/view/textclassifier/TextClassifier.java
index 04ab447..9f75c4a 100644
--- a/core/java/android/view/textclassifier/TextClassifier.java
+++ b/core/java/android/view/textclassifier/TextClassifier.java
@@ -23,9 +23,12 @@
import android.annotation.StringDef;
import android.annotation.WorkerThread;
import android.os.LocaleList;
+import android.os.Looper;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.ArraySet;
+import android.util.Slog;
+import android.view.textclassifier.logging.Logger;
import com.android.internal.util.Preconditions;
@@ -39,8 +42,8 @@
/**
* Interface for providing text classification related features.
*
- * <p>Unless otherwise stated, methods of this interface are blocking operations.
- * Avoid calling them on the UI thread.
+ * <p><strong>NOTE: </strong>Unless otherwise stated, methods of this interface are blocking
+ * operations. Call on a worker thread.
*/
public interface TextClassifier {
@@ -107,6 +110,8 @@
* Returns suggested text selection start and end indices, recognized entity types, and their
* associated confidence scores. The entity types are ordered from highest to lowest scoring.
*
+ * <p><strong>NOTE: </strong>Call on a worker thread.
+ *
* @param text text providing context for the selected text (which is specified
* by the sub sequence starting at selectionStartIndex and ending at selectionEndIndex)
* @param selectionStartIndex start index of the selected part of text
@@ -125,7 +130,7 @@
@IntRange(from = 0) int selectionStartIndex,
@IntRange(from = 0) int selectionEndIndex,
@Nullable TextSelection.Options options) {
- Utils.validateInput(text, selectionStartIndex, selectionEndIndex);
+ Utils.validate(text, selectionStartIndex, selectionEndIndex, false);
return new TextSelection.Builder(selectionStartIndex, selectionEndIndex).build();
}
@@ -137,6 +142,8 @@
* {@link #suggestSelection(CharSequence, int, int, TextSelection.Options)}. If that method
* calls this method, a stack overflow error will happen.
*
+ * <p><strong>NOTE: </strong>Call on a worker thread.
+ *
* @param text text providing context for the selected text (which is specified
* by the sub sequence starting at selectionStartIndex and ending at selectionEndIndex)
* @param selectionStartIndex start index of the selected part of text
@@ -161,6 +168,8 @@
* See {@link #suggestSelection(CharSequence, int, int)} or
* {@link #suggestSelection(CharSequence, int, int, TextSelection.Options)}.
*
+ * <p><strong>NOTE: </strong>Call on a worker thread.
+ *
* <p><b>NOTE:</b> Do not implement. The default implementation of this method calls
* {@link #suggestSelection(CharSequence, int, int, TextSelection.Options)}. If that method
* calls this method, a stack overflow error will happen.
@@ -182,6 +191,8 @@
* Classifies the specified text and returns a {@link TextClassification} object that can be
* used to generate a widget for handling the classified text.
*
+ * <p><strong>NOTE: </strong>Call on a worker thread.
+ *
* @param text text providing context for the text to classify (which is specified
* by the sub sequence starting at startIndex and ending at endIndex)
* @param startIndex start index of the text to classify
@@ -200,7 +211,7 @@
@IntRange(from = 0) int startIndex,
@IntRange(from = 0) int endIndex,
@Nullable TextClassification.Options options) {
- Utils.validateInput(text, startIndex, endIndex);
+ Utils.validate(text, startIndex, endIndex, false);
return TextClassification.EMPTY;
}
@@ -208,6 +219,8 @@
* Classifies the specified text and returns a {@link TextClassification} object that can be
* used to generate a widget for handling the classified text.
*
+ * <p><strong>NOTE: </strong>Call on a worker thread.
+ *
* <p><b>NOTE:</b> Do not implement. The default implementation of this method calls
* {@link #classifyText(CharSequence, int, int, TextClassification.Options)}. If that method
* calls this method, a stack overflow error will happen.
@@ -235,6 +248,8 @@
* See {@link #classifyText(CharSequence, int, int, TextClassification.Options)} or
* {@link #classifyText(CharSequence, int, int)}.
*
+ * <p><strong>NOTE: </strong>Call on a worker thread.
+ *
* <p><b>NOTE:</b> Do not implement. The default implementation of this method calls
* {@link #classifyText(CharSequence, int, int, TextClassification.Options)}. If that method
* calls this method, a stack overflow error will happen.
@@ -253,10 +268,10 @@
}
/**
- * Returns a {@link TextLinks} that may be applied to the text to annotate it with links
- * information.
+ * Generates and returns a {@link TextLinks} that may be applied to the text to annotate it with
+ * links information.
*
- * If no options are supplied, default values will be used, determined by the TextClassifier.
+ * <p><strong>NOTE: </strong>Call on a worker thread.
*
* @param text the text to generate annotations for
* @param options configuration for link generation
@@ -268,13 +283,15 @@
@WorkerThread
default TextLinks generateLinks(
@NonNull CharSequence text, @Nullable TextLinks.Options options) {
- Utils.validateInput(text);
+ Utils.validate(text, false);
return new TextLinks.Builder(text.toString()).build();
}
/**
- * Returns a {@link TextLinks} that may be applied to the text to annotate it with links
- * information.
+ * Generates and returns a {@link TextLinks} that may be applied to the text to annotate it with
+ * links information.
+ *
+ * <p><strong>NOTE: </strong>Call on a worker thread.
*
* <p><b>NOTE:</b> Do not implement. The default implementation of this method calls
* {@link #generateLinks(CharSequence, TextLinks.Options)}. If that method calls this method,
@@ -296,20 +313,22 @@
*
* @see #ENTITY_PRESET_ALL
* @see #ENTITY_PRESET_NONE
+ * @see #ENTITY_PRESET_BASE
*/
default Collection<String> getEntitiesForPreset(@EntityPreset int entityPreset) {
return Collections.EMPTY_LIST;
}
/**
- * Logs a TextClassifier event.
+ * Returns a helper for logging TextClassifier related events.
*
- * @param source the text classifier used to generate this event
- * @param event the text classifier related event
- * @hide
+ * @param config logger configuration
*/
@WorkerThread
- default void logEvent(String source, String event) {}
+ default Logger getLogger(@NonNull Logger.Config config) {
+ Preconditions.checkNotNull(config);
+ return Logger.DISABLED;
+ }
/**
* Returns this TextClassifier's settings.
@@ -424,19 +443,28 @@
* endIndex is greater than text.length() or is not greater than startIndex;
* options is null
*/
- static void validateInput(
- @NonNull CharSequence text, int startIndex, int endIndex) {
+ public static void validate(
+ @NonNull CharSequence text, int startIndex, int endIndex,
+ boolean allowInMainThread) {
Preconditions.checkArgument(text != null);
Preconditions.checkArgument(startIndex >= 0);
Preconditions.checkArgument(endIndex <= text.length());
Preconditions.checkArgument(endIndex > startIndex);
+ checkMainThread(allowInMainThread);
}
/**
* @throws IllegalArgumentException if text is null or options is null
*/
- static void validateInput(@NonNull CharSequence text) {
+ public static void validate(@NonNull CharSequence text, boolean allowInMainThread) {
Preconditions.checkArgument(text != null);
+ checkMainThread(allowInMainThread);
+ }
+
+ private static void checkMainThread(boolean allowInMainThread) {
+ if (!allowInMainThread && Looper.myLooper() == Looper.getMainLooper()) {
+ Slog.w(DEFAULT_LOG_TAG, "TextClassifier called on main thread");
+ }
}
}
}
diff --git a/core/java/android/view/textclassifier/TextClassifierImpl.java b/core/java/android/view/textclassifier/TextClassifierImpl.java
index 6a44fb3..b03c70d 100644
--- a/core/java/android/view/textclassifier/TextClassifierImpl.java
+++ b/core/java/android/view/textclassifier/TextClassifierImpl.java
@@ -35,6 +35,8 @@
import android.provider.Settings;
import android.text.util.Linkify;
import android.util.Patterns;
+import android.view.textclassifier.logging.DefaultLogger;
+import android.view.textclassifier.logging.Logger;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.logging.MetricsLogger;
@@ -43,6 +45,7 @@
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
+import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
@@ -66,13 +69,13 @@
*
* @hide
*/
-final class TextClassifierImpl implements TextClassifier {
+public final class TextClassifierImpl implements TextClassifier {
private static final String LOG_TAG = DEFAULT_LOG_TAG;
private static final String MODEL_DIR = "/etc/textclassifier/";
private static final String MODEL_FILE_REGEX = "textclassifier\\.smartselection\\.(.*)\\.model";
private static final String UPDATED_MODEL_FILE_PATH =
- "/data/misc/textclassifier/textclassifier.smartselection.model";
+ "/data/misc/textclassifier/textclassifier.model";
private static final List<String> ENTITY_TYPES_ALL =
Collections.unmodifiableList(Arrays.asList(
TextClassifier.TYPE_ADDRESS,
@@ -90,30 +93,39 @@
TextClassifier.TYPE_URL));
private final Context mContext;
+ private final TextClassifier mFallback;
private final MetricsLogger mMetricsLogger = new MetricsLogger();
- private final Object mSmartSelectionLock = new Object();
- @GuardedBy("mSmartSelectionLock") // Do not access outside this lock.
+ private final Object mLock = new Object();
+ @GuardedBy("mLock") // Do not access outside this lock.
private Map<Locale, String> mModelFilePaths;
- @GuardedBy("mSmartSelectionLock") // Do not access outside this lock.
+ @GuardedBy("mLock") // Do not access outside this lock.
private Locale mLocale;
- @GuardedBy("mSmartSelectionLock") // Do not access outside this lock.
+ @GuardedBy("mLock") // Do not access outside this lock.
private int mVersion;
- @GuardedBy("mSmartSelectionLock") // Do not access outside this lock.
+ @GuardedBy("mLock") // Do not access outside this lock.
private SmartSelection mSmartSelection;
+ private final Object mLoggerLock = new Object();
+ @GuardedBy("mLoggerLock") // Do not access outside this lock.
+ private WeakReference<Logger.Config> mLoggerConfig = new WeakReference<>(null);
+ @GuardedBy("mLoggerLock") // Do not access outside this lock.
+ private Logger mLogger; // Should never be null if mLoggerConfig.get() is not null.
+
private TextClassifierConstants mSettings;
- TextClassifierImpl(Context context) {
+ public TextClassifierImpl(Context context) {
mContext = Preconditions.checkNotNull(context);
+ mFallback = TextClassifier.NO_OP;
}
+ /** @inheritDoc */
@Override
public TextSelection suggestSelection(
@NonNull CharSequence text, int selectionStartIndex, int selectionEndIndex,
@Nullable TextSelection.Options options) {
- Utils.validateInput(text, selectionStartIndex, selectionEndIndex);
+ Utils.validate(text, selectionStartIndex, selectionEndIndex, false /* allowInMainThread */);
try {
if (text.length() > 0) {
final LocaleList locales = (options == null) ? null : options.getDefaultLocales();
@@ -159,15 +171,16 @@
t);
}
// Getting here means something went wrong, return a NO_OP result.
- return TextClassifier.NO_OP.suggestSelection(
+ return mFallback.suggestSelection(
text, selectionStartIndex, selectionEndIndex, options);
}
+ /** @inheritDoc */
@Override
public TextClassification classifyText(
@NonNull CharSequence text, int startIndex, int endIndex,
@Nullable TextClassification.Options options) {
- Utils.validateInput(text, startIndex, endIndex);
+ Utils.validate(text, startIndex, endIndex, false /* allowInMainThread */);
try {
if (text.length() > 0) {
final String string = text.toString();
@@ -186,13 +199,14 @@
Log.e(LOG_TAG, "Error getting text classification info.", t);
}
// Getting here means something went wrong, return a NO_OP result.
- return TextClassifier.NO_OP.classifyText(text, startIndex, endIndex, options);
+ return mFallback.classifyText(text, startIndex, endIndex, options);
}
+ /** @inheritDoc */
@Override
public TextLinks generateLinks(
@NonNull CharSequence text, @Nullable TextLinks.Options options) {
- Utils.validateInput(text);
+ Utils.validate(text, false /* allowInMainThread */);
final String textString = text.toString();
final TextLinks.Builder builder = new TextLinks.Builder(textString);
@@ -216,14 +230,13 @@
for (int i = 0; i < results.length; i++) {
entityScores.put(results[i].mCollection, results[i].mScore);
}
- builder.addLink(new TextLinks.TextLink(
- textString, span.getStartIndex(), span.getEndIndex(), entityScores));
+ builder.addLink(span.getStartIndex(), span.getEndIndex(), entityScores);
}
} catch (Throwable t) {
// Avoid throwing from this method. Log the error.
Log.e(LOG_TAG, "Error getting links info.", t);
}
- return builder.build();
+ return mFallback.generateLinks(text, options);
}
@Override
@@ -241,12 +254,18 @@
}
@Override
- public void logEvent(String source, String event) {
- if (LOG_TAG.equals(source)) {
- mMetricsLogger.count(event, 1);
+ public Logger getLogger(@NonNull Logger.Config config) {
+ Preconditions.checkNotNull(config);
+ synchronized (mLoggerLock) {
+ if (mLoggerConfig.get() == null || !mLoggerConfig.get().equals(config)) {
+ mLoggerConfig = new WeakReference<>(config);
+ mLogger = new DefaultLogger(config);
+ }
+ return mLogger;
}
}
+ /** @hide */
@Override
public TextClassifierConstants getSettings() {
if (mSettings == null) {
@@ -257,7 +276,7 @@
}
private SmartSelection getSmartSelection(LocaleList localeList) throws FileNotFoundException {
- synchronized (mSmartSelectionLock) {
+ synchronized (mLock) {
localeList = localeList == null ? LocaleList.getEmptyLocaleList() : localeList;
final Locale locale = findBestSupportedLocaleLocked(localeList);
if (locale == null) {
@@ -277,16 +296,12 @@
}
private String getSignature(String text, int start, int end) {
- synchronized (mSmartSelectionLock) {
- final String versionInfo = (mLocale != null)
- ? String.format(Locale.US, "%s_v%d", mLocale.toLanguageTag(), mVersion)
- : "";
- final int hash = Objects.hash(text, start, end, mContext.getPackageName());
- return String.format(Locale.US, "%s|%s|%d", LOG_TAG, versionInfo, hash);
+ synchronized (mLock) {
+ return DefaultLogger.createSignature(text, start, end, mContext, mVersion, mLocale);
}
}
- @GuardedBy("mSmartSelectionLock") // Do not call outside this lock.
+ @GuardedBy("mLock") // Do not call outside this lock.
private ParcelFileDescriptor getFdLocked(Locale locale) throws FileNotFoundException {
ParcelFileDescriptor updateFd;
int updateVersion = -1;
@@ -321,7 +336,7 @@
return factoryFd;
} else {
throw new FileNotFoundException(
- String.format("No model file found for %s", locale));
+ String.format(Locale.US, "No model file found for %s", locale));
}
}
@@ -335,7 +350,7 @@
} else {
closeAndLogError(updateFd);
throw new FileNotFoundException(
- String.format("No model file found for %s", locale));
+ String.format(Locale.US, "No model file found for %s", locale));
}
}
@@ -353,7 +368,7 @@
}
}
- @GuardedBy("mSmartSelectionLock") // Do not call outside this lock.
+ @GuardedBy("mLock") // Do not call outside this lock.
private void destroySmartSelectionIfExistsLocked() {
if (mSmartSelection != null) {
mSmartSelection.close();
@@ -361,7 +376,7 @@
}
}
- @GuardedBy("mSmartSelectionLock") // Do not call outside this lock.
+ @GuardedBy("mLock") // Do not call outside this lock.
@Nullable
private Locale findBestSupportedLocaleLocked(LocaleList localeList) {
// Specified localeList takes priority over the system default, so it is listed first.
@@ -379,7 +394,7 @@
return Locale.lookup(languageRangeList, supportedLocales);
}
- @GuardedBy("mSmartSelectionLock") // Do not call outside this lock.
+ @GuardedBy("mLock") // Do not call outside this lock.
private Map<Locale, String> getFactoryModelFilePathsLocked() {
if (mModelFilePaths == null) {
final Map<Locale, String> modelFilePaths = new HashMap<>();
diff --git a/core/java/android/view/textclassifier/TextLinks.aidl b/core/java/android/view/textclassifier/TextLinks.aidl
new file mode 100644
index 0000000..1bbb798
--- /dev/null
+++ b/core/java/android/view/textclassifier/TextLinks.aidl
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier;
+
+parcelable TextLinks;
+parcelable TextLinks.Options;
\ No newline at end of file
diff --git a/core/java/android/view/textclassifier/TextLinks.java b/core/java/android/view/textclassifier/TextLinks.java
index ba854e0..670efdd 100644
--- a/core/java/android/view/textclassifier/TextLinks.java
+++ b/core/java/android/view/textclassifier/TextLinks.java
@@ -17,18 +17,24 @@
package android.view.textclassifier;
import android.annotation.FloatRange;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.LocaleList;
import android.os.Parcel;
import android.os.Parcelable;
-import android.text.SpannableString;
-import android.text.style.ClickableSpan;
-import android.view.View;
import android.widget.TextView;
+import android.text.Spannable;
+import android.text.style.ClickableSpan;
+import android.text.util.Linkify;
+import android.text.util.Linkify.LinkifyMask;
+import android.view.View;
+import android.view.textclassifier.TextClassifier.EntityType;
import com.android.internal.util.Preconditions;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -41,12 +47,51 @@
* address, url, etc) they may be.
*/
public final class TextLinks implements Parcelable {
+
+ /**
+ * Return status of an attempt to apply TextLinks to text.
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({STATUS_LINKS_APPLIED, STATUS_NO_LINKS_FOUND, STATUS_NO_LINKS_APPLIED,
+ STATUS_DIFFERENT_TEXT})
+ public @interface Status {}
+
+ /** Links were successfully applied to the text. */
+ public static final int STATUS_LINKS_APPLIED = 0;
+
+ /** No links exist to apply to text. Links count is zero. */
+ public static final int STATUS_NO_LINKS_FOUND = 1;
+
+ /** No links applied to text. The links were filtered out. */
+ public static final int STATUS_NO_LINKS_APPLIED = 2;
+
+ /** The specified text does not match the text used to generate the links. */
+ public static final int STATUS_DIFFERENT_TEXT = 3;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({APPLY_STRATEGY_IGNORE, APPLY_STRATEGY_REPLACE})
+ public @interface ApplyStrategy {}
+
+ /**
+ * Do not replace {@link ClickableSpan}s that exist where the {@link TextLinkSpan} needs to
+ * be applied to. Do not apply the TextLinkSpan.
+ */
+ public static final int APPLY_STRATEGY_IGNORE = 0;
+
+ /**
+ * Replace any {@link ClickableSpan}s that exist where the {@link TextLinkSpan} needs to be
+ * applied to.
+ */
+ public static final int APPLY_STRATEGY_REPLACE = 1;
+
private final String mFullText;
private final List<TextLink> mLinks;
- private TextLinks(String fullText, Collection<TextLink> links) {
+ private TextLinks(String fullText, ArrayList<TextLink> links) {
mFullText = fullText;
- mLinks = Collections.unmodifiableList(new ArrayList<>(links));
+ mLinks = Collections.unmodifiableList(links);
}
/**
@@ -60,29 +105,57 @@
* Annotates the given text with the generated links. It will fail if the provided text doesn't
* match the original text used to crete the TextLinks.
*
- * @param text the text to apply the links to. Must match the original text.
- * @param spanFactory a factory to generate spans from TextLinks. Will use a default if null.
+ * @param text the text to apply the links to. Must match the original text
+ * @param applyStrategy strategy for resolving link conflicts
+ * @param spanFactory a factory to generate spans from TextLinks. Will use a default if null
*
- * @return Success or failure.
+ * @return a status code indicating whether or not the links were successfully applied
+ *
+ * @hide
*/
- public boolean apply(
- @NonNull SpannableString text,
- @Nullable Function<TextLink, ClickableSpan> spanFactory) {
+ @Status
+ public int apply(
+ @NonNull Spannable text,
+ @ApplyStrategy int applyStrategy,
+ @Nullable Function<TextLink, TextLinkSpan> spanFactory) {
Preconditions.checkNotNull(text);
+ checkValidApplyStrategy(applyStrategy);
if (!mFullText.equals(text.toString())) {
- return false;
+ return STATUS_DIFFERENT_TEXT;
+ }
+ if (mLinks.isEmpty()) {
+ return STATUS_NO_LINKS_FOUND;
}
if (spanFactory == null) {
spanFactory = DEFAULT_SPAN_FACTORY;
}
+ int applyCount = 0;
for (TextLink link : mLinks) {
- final ClickableSpan span = spanFactory.apply(link);
+ final TextLinkSpan span = spanFactory.apply(link);
if (span != null) {
- text.setSpan(span, link.getStart(), link.getEnd(), 0);
+ final ClickableSpan[] existingSpans = text.getSpans(
+ link.getStart(), link.getEnd(), ClickableSpan.class);
+ if (existingSpans.length > 0) {
+ if (applyStrategy == APPLY_STRATEGY_REPLACE) {
+ for (ClickableSpan existingSpan : existingSpans) {
+ text.removeSpan(existingSpan);
+ }
+ text.setSpan(span, link.getStart(), link.getEnd(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ applyCount++;
+ }
+ } else {
+ text.setSpan(span, link.getStart(), link.getEnd(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ applyCount++;
+ }
}
}
- return true;
+ if (applyCount == 0) {
+ return STATUS_NO_LINKS_APPLIED;
+ }
+ return STATUS_LINKS_APPLIED;
}
@Override
@@ -119,7 +192,6 @@
*/
public static final class TextLink implements Parcelable {
private final EntityConfidence mEntityScores;
- private final String mOriginalText;
private final int mStart;
private final int mEnd;
@@ -128,12 +200,10 @@
*
* @throws IllegalArgumentException if entityScores is null or empty.
*/
- public TextLink(String originalText, int start, int end, Map<String, Float> entityScores) {
- Preconditions.checkNotNull(originalText);
+ TextLink(int start, int end, Map<String, Float> entityScores) {
Preconditions.checkNotNull(entityScores);
Preconditions.checkArgument(!entityScores.isEmpty());
Preconditions.checkArgument(start <= end);
- mOriginalText = originalText;
mStart = start;
mEnd = end;
mEntityScores = new EntityConfidence(entityScores);
@@ -171,7 +241,7 @@
*
* @return the entity type at the provided index.
*/
- @NonNull public @TextClassifier.EntityType String getEntity(int index) {
+ @NonNull public @EntityType String getEntity(int index) {
return mEntityScores.getEntities().get(index);
}
@@ -181,7 +251,7 @@
* @param entityType the entity type.
*/
public @FloatRange(from = 0.0, to = 1.0) float getConfidenceScore(
- @TextClassifier.EntityType String entityType) {
+ @EntityType String entityType) {
return mEntityScores.getConfidenceScore(entityType);
}
@@ -193,7 +263,6 @@
@Override
public void writeToParcel(Parcel dest, int flags) {
mEntityScores.writeToParcel(dest, flags);
- dest.writeString(mOriginalText);
dest.writeInt(mStart);
dest.writeInt(mEnd);
}
@@ -213,7 +282,6 @@
private TextLink(Parcel in) {
mEntityScores = EntityConfidence.CREATOR.createFromParcel(in);
- mOriginalText = in.readString();
mStart = in.readInt();
mEnd = in.readInt();
}
@@ -227,6 +295,32 @@
private LocaleList mDefaultLocales;
private TextClassifier.EntityConfig mEntityConfig;
+ private @ApplyStrategy int mApplyStrategy;
+ private Function<TextLink, TextLinkSpan> mSpanFactory;
+
+ /**
+ * Returns a new options object based on the specified link mask.
+ */
+ public static Options fromLinkMask(@LinkifyMask int mask) {
+ final TextClassifier.EntityConfig entityConfig =
+ new TextClassifier.EntityConfig(TextClassifier.ENTITY_PRESET_NONE);
+
+ if ((mask & Linkify.WEB_URLS) != 0) {
+ entityConfig.includeEntities(TextClassifier.TYPE_URL);
+ }
+ if ((mask & Linkify.EMAIL_ADDRESSES) != 0) {
+ entityConfig.includeEntities(TextClassifier.TYPE_EMAIL);
+ }
+ if ((mask & Linkify.PHONE_NUMBERS) != 0) {
+ entityConfig.includeEntities(TextClassifier.TYPE_PHONE);
+ }
+ if ((mask & Linkify.MAP_ADDRESSES) != 0) {
+ entityConfig.includeEntities(TextClassifier.TYPE_ADDRESS);
+ }
+
+ return new Options().setEntityConfig(entityConfig);
+ }
+
public Options() {}
/**
@@ -251,6 +345,31 @@
}
/**
+ * Sets a strategy for resolving conflicts when applying generated links to text that
+ * already have links.
+ *
+ * @throws IllegalArgumentException if applyStrategy is not valid
+ *
+ * @see #APPLY_STRATEGY_IGNORE
+ * @see #APPLY_STRAGETY_REPLACE
+ */
+ public Options setApplyStrategy(@ApplyStrategy int applyStrategy) {
+ checkValidApplyStrategy(applyStrategy);
+ mApplyStrategy = applyStrategy;
+ return this;
+ }
+
+ /**
+ * Sets a factory for converting a TextLink to a TextLinkSpan.
+ *
+ * <p><strong>Note: </strong>This is not parceled over IPC.
+ */
+ public Options setSpanFactory(@Nullable Function<TextLink, TextLinkSpan> spanFactory) {
+ mSpanFactory = spanFactory;
+ return this;
+ }
+
+ /**
* @return ordered list of locale preferences that can be used to disambiguate
* the provided text.
*/
@@ -260,7 +379,7 @@
}
/**
- * @return The config representing the set of entities to look for.
+ * @return The config representing the set of entities to look for
* @see #setEntityConfig(TextClassifier.EntityConfig)
*/
@Nullable
@@ -268,6 +387,29 @@
return mEntityConfig;
}
+ /**
+ * @return the strategy for resolving conflictswhen applying generated links to text that
+ * already have links.
+ *
+ * @see #APPLY_STATEGY_IGNORE
+ * @see #APPLY_STRAGETY_REPLACE
+ */
+ @ApplyStrategy
+ public int getApplyStrategy() {
+ return mApplyStrategy;
+ }
+
+ /**
+ * Returns a factory for converting a TextLink to a TextLinkSpan.
+ *
+ * <p><strong>Note: </strong>This is not parcelable and will always return null if read
+ * from a parcel
+ */
+ @Nullable
+ public Function<TextLink, TextLinkSpan> getSpanFactory() {
+ return mSpanFactory;
+ }
+
@Override
public int describeContents() {
return 0;
@@ -283,6 +425,7 @@
if (mEntityConfig != null) {
mEntityConfig.writeToParcel(dest, flags);
}
+ dest.writeInt(mApplyStrategy);
}
public static final Parcelable.Creator<Options> CREATOR =
@@ -305,39 +448,53 @@
if (in.readInt() > 0) {
mEntityConfig = TextClassifier.EntityConfig.CREATOR.createFromParcel(in);
}
+ mApplyStrategy = in.readInt();
}
}
/**
* A function to create spans from TextLinks.
- *
- * Applies only to TextViews.
- * We can hide this until we are convinced we want it to be part of the public API.
- *
- * @hide
*/
- public static final Function<TextLink, ClickableSpan> DEFAULT_SPAN_FACTORY =
- textLink -> new ClickableSpan() {
- @Override
- public void onClick(View widget) {
- if (widget instanceof TextView) {
- final TextView textView = (TextView) widget;
- textView.requestActionMode(textLink);
- }
- }
- };
+ private static final Function<TextLink, TextLinkSpan> DEFAULT_SPAN_FACTORY =
+ textLink -> new TextLinkSpan(textLink);
+
+ /**
+ * A ClickableSpan for a TextLink.
+ *
+ * <p>Applies only to TextViews.
+ */
+ public static class TextLinkSpan extends ClickableSpan {
+
+ private final TextLink mTextLink;
+
+ public TextLinkSpan(@Nullable TextLink textLink) {
+ mTextLink = textLink;
+ }
+
+ @Override
+ public void onClick(View widget) {
+ if (widget instanceof TextView) {
+ final TextView textView = (TextView) widget;
+ textView.requestActionMode(mTextLink);
+ }
+ }
+
+ public final TextLink getTextLink() {
+ return mTextLink;
+ }
+ }
/**
* A builder to construct a TextLinks instance.
*/
public static final class Builder {
private final String mFullText;
- private final Collection<TextLink> mLinks;
+ private final ArrayList<TextLink> mLinks;
/**
* Create a new TextLinks.Builder.
*
- * @param fullText The full text that links will be added to.
+ * @param fullText The full text to annotate with links.
*/
public Builder(@NonNull String fullText) {
mFullText = Preconditions.checkNotNull(fullText);
@@ -348,10 +505,19 @@
* Adds a TextLink.
*
* @return this instance.
+ *
+ * @throws IllegalArgumentException if entityScores is null or empty.
*/
- public Builder addLink(TextLink link) {
- Preconditions.checkNotNull(link);
- mLinks.add(link);
+ public Builder addLink(int start, int end, Map<String, Float> entityScores) {
+ mLinks.add(new TextLink(start, end, entityScores));
+ return this;
+ }
+
+ /**
+ * Removes all {@link TextLink}s.
+ */
+ public Builder clearTextLinks() {
+ mLinks.clear();
return this;
}
@@ -364,4 +530,14 @@
return new TextLinks(mFullText, mLinks);
}
}
+
+ /**
+ * @throws IllegalArgumentException if the value is invalid
+ */
+ private static void checkValidApplyStrategy(int applyStrategy) {
+ if (applyStrategy != APPLY_STRATEGY_IGNORE && applyStrategy != APPLY_STRATEGY_REPLACE) {
+ throw new IllegalArgumentException(
+ "Invalid apply strategy. See TextLinks.ApplyStrategy for options.");
+ }
+ }
}
diff --git a/core/java/android/view/textclassifier/TextSelection.aidl b/core/java/android/view/textclassifier/TextSelection.aidl
new file mode 100644
index 0000000..dab1aef
--- /dev/null
+++ b/core/java/android/view/textclassifier/TextSelection.aidl
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier;
+
+parcelable TextSelection;
+parcelable TextSelection.Options;
\ No newline at end of file
diff --git a/core/java/android/view/textclassifier/TextSelection.java b/core/java/android/view/textclassifier/TextSelection.java
index 774d42d..1c93be7 100644
--- a/core/java/android/view/textclassifier/TextSelection.java
+++ b/core/java/android/view/textclassifier/TextSelection.java
@@ -34,7 +34,7 @@
/**
* Information about where text selection should be.
*/
-public final class TextSelection {
+public final class TextSelection implements Parcelable {
private final int mStartIndex;
private final int mEndIndex;
@@ -112,22 +112,6 @@
mStartIndex, mEndIndex, mEntityConfidence, mSignature);
}
- /** Helper for parceling via #ParcelableWrapper. */
- private void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(mStartIndex);
- dest.writeInt(mEndIndex);
- mEntityConfidence.writeToParcel(dest, flags);
- dest.writeString(mSignature);
- }
-
- /** Helper for unparceling via #ParcelableWrapper. */
- private TextSelection(Parcel in) {
- mStartIndex = in.readInt();
- mEndIndex = in.readInt();
- mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in);
- mSignature = in.readString();
- }
-
/**
* Builder used to build {@link TextSelection} objects.
*/
@@ -272,46 +256,36 @@
}
}
- /**
- * Parcelable wrapper for TextSelection objects.
- * @hide
- */
- public static final class ParcelableWrapper implements Parcelable {
+ @Override
+ public int describeContents() {
+ return 0;
+ }
- @NonNull private TextSelection mTextSelection;
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mStartIndex);
+ dest.writeInt(mEndIndex);
+ mEntityConfidence.writeToParcel(dest, flags);
+ dest.writeString(mSignature);
+ }
- public ParcelableWrapper(@NonNull TextSelection textSelection) {
- Preconditions.checkNotNull(textSelection);
- mTextSelection = textSelection;
- }
+ public static final Parcelable.Creator<TextSelection> CREATOR =
+ new Parcelable.Creator<TextSelection>() {
+ @Override
+ public TextSelection createFromParcel(Parcel in) {
+ return new TextSelection(in);
+ }
- @NonNull
- public TextSelection getTextSelection() {
- return mTextSelection;
- }
+ @Override
+ public TextSelection[] newArray(int size) {
+ return new TextSelection[size];
+ }
+ };
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- mTextSelection.writeToParcel(dest, flags);
- }
-
- public static final Parcelable.Creator<ParcelableWrapper> CREATOR =
- new Parcelable.Creator<ParcelableWrapper>() {
- @Override
- public ParcelableWrapper createFromParcel(Parcel in) {
- return new ParcelableWrapper(new TextSelection(in));
- }
-
- @Override
- public ParcelableWrapper[] newArray(int size) {
- return new ParcelableWrapper[size];
- }
- };
-
+ private TextSelection(Parcel in) {
+ mStartIndex = in.readInt();
+ mEndIndex = in.readInt();
+ mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in);
+ mSignature = in.readString();
}
}
diff --git a/core/java/android/view/textclassifier/logging/DefaultLogger.java b/core/java/android/view/textclassifier/logging/DefaultLogger.java
new file mode 100644
index 0000000..6b84835
--- /dev/null
+++ b/core/java/android/view/textclassifier/logging/DefaultLogger.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier.logging;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.metrics.LogMaker;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.util.Preconditions;
+
+import java.util.Locale;
+import java.util.Objects;
+
+/**
+ * Default Logger.
+ * Used internally by TextClassifierImpl.
+ * @hide
+ */
+public final class DefaultLogger extends Logger {
+
+ private static final String LOG_TAG = "DefaultLogger";
+ private static final String CLASSIFIER_ID = "androidtc";
+
+ private static final int START_EVENT_DELTA = MetricsEvent.FIELD_SELECTION_SINCE_START;
+ private static final int PREV_EVENT_DELTA = MetricsEvent.FIELD_SELECTION_SINCE_PREVIOUS;
+ private static final int INDEX = MetricsEvent.FIELD_SELECTION_SESSION_INDEX;
+ private static final int WIDGET_TYPE = MetricsEvent.FIELD_SELECTION_WIDGET_TYPE;
+ private static final int WIDGET_VERSION = MetricsEvent.FIELD_SELECTION_WIDGET_VERSION;
+ private static final int MODEL_NAME = MetricsEvent.FIELD_TEXTCLASSIFIER_MODEL;
+ private static final int ENTITY_TYPE = MetricsEvent.FIELD_SELECTION_ENTITY_TYPE;
+ private static final int SMART_START = MetricsEvent.FIELD_SELECTION_SMART_RANGE_START;
+ private static final int SMART_END = MetricsEvent.FIELD_SELECTION_SMART_RANGE_END;
+ private static final int EVENT_START = MetricsEvent.FIELD_SELECTION_RANGE_START;
+ private static final int EVENT_END = MetricsEvent.FIELD_SELECTION_RANGE_END;
+ private static final int SESSION_ID = MetricsEvent.FIELD_SELECTION_SESSION_ID;
+
+ private static final String ZERO = "0";
+ private static final String UNKNOWN = "unknown";
+
+ private final MetricsLogger mMetricsLogger;
+
+ public DefaultLogger(@NonNull Config config) {
+ super(config);
+ mMetricsLogger = new MetricsLogger();
+ }
+
+ @VisibleForTesting
+ public DefaultLogger(@NonNull Config config, @NonNull MetricsLogger metricsLogger) {
+ super(config);
+ mMetricsLogger = Preconditions.checkNotNull(metricsLogger);
+ }
+
+ @Override
+ public boolean isSmartSelection(@NonNull String signature) {
+ return CLASSIFIER_ID.equals(SignatureParser.getClassifierId(signature));
+ }
+
+ @Override
+ public void writeEvent(@NonNull SelectionEvent event) {
+ Preconditions.checkNotNull(event);
+ final LogMaker log = new LogMaker(MetricsEvent.TEXT_SELECTION_SESSION)
+ .setType(getLogType(event))
+ .setSubtype(MetricsEvent.TEXT_SELECTION_INVOCATION_MANUAL)
+ .setPackageName(event.getPackageName())
+ .addTaggedData(START_EVENT_DELTA, event.getDurationSinceSessionStart())
+ .addTaggedData(PREV_EVENT_DELTA, event.getDurationSincePreviousEvent())
+ .addTaggedData(INDEX, event.getEventIndex())
+ .addTaggedData(WIDGET_TYPE, event.getWidgetType())
+ .addTaggedData(WIDGET_VERSION, event.getWidgetVersion())
+ .addTaggedData(MODEL_NAME, SignatureParser.getModelName(event.getSignature()))
+ .addTaggedData(ENTITY_TYPE, event.getEntityType())
+ .addTaggedData(SMART_START, event.getSmartStart())
+ .addTaggedData(SMART_END, event.getSmartEnd())
+ .addTaggedData(EVENT_START, event.getStart())
+ .addTaggedData(EVENT_END, event.getEnd())
+ .addTaggedData(SESSION_ID, event.getSessionId());
+ mMetricsLogger.write(log);
+ debugLog(log);
+ }
+
+ private static int getLogType(SelectionEvent event) {
+ switch (event.getEventType()) {
+ case SelectionEvent.ACTION_OVERTYPE:
+ return MetricsEvent.ACTION_TEXT_SELECTION_OVERTYPE;
+ case SelectionEvent.ACTION_COPY:
+ return MetricsEvent.ACTION_TEXT_SELECTION_COPY;
+ case SelectionEvent.ACTION_PASTE:
+ return MetricsEvent.ACTION_TEXT_SELECTION_PASTE;
+ case SelectionEvent.ACTION_CUT:
+ return MetricsEvent.ACTION_TEXT_SELECTION_CUT;
+ case SelectionEvent.ACTION_SHARE:
+ return MetricsEvent.ACTION_TEXT_SELECTION_SHARE;
+ case SelectionEvent.ACTION_SMART_SHARE:
+ return MetricsEvent.ACTION_TEXT_SELECTION_SMART_SHARE;
+ case SelectionEvent.ACTION_DRAG:
+ return MetricsEvent.ACTION_TEXT_SELECTION_DRAG;
+ case SelectionEvent.ACTION_ABANDON:
+ return MetricsEvent.ACTION_TEXT_SELECTION_ABANDON;
+ case SelectionEvent.ACTION_OTHER:
+ return MetricsEvent.ACTION_TEXT_SELECTION_OTHER;
+ case SelectionEvent.ACTION_SELECT_ALL:
+ return MetricsEvent.ACTION_TEXT_SELECTION_SELECT_ALL;
+ case SelectionEvent.ACTION_RESET:
+ return MetricsEvent.ACTION_TEXT_SELECTION_RESET;
+ case SelectionEvent.EVENT_SELECTION_STARTED:
+ return MetricsEvent.ACTION_TEXT_SELECTION_START;
+ case SelectionEvent.EVENT_SELECTION_MODIFIED:
+ return MetricsEvent.ACTION_TEXT_SELECTION_MODIFY;
+ case SelectionEvent.EVENT_SMART_SELECTION_SINGLE:
+ return MetricsEvent.ACTION_TEXT_SELECTION_SMART_SINGLE;
+ case SelectionEvent.EVENT_SMART_SELECTION_MULTI:
+ return MetricsEvent.ACTION_TEXT_SELECTION_SMART_MULTI;
+ case SelectionEvent.EVENT_AUTO_SELECTION:
+ return MetricsEvent.ACTION_TEXT_SELECTION_AUTO;
+ default:
+ return MetricsEvent.VIEW_UNKNOWN;
+ }
+ }
+
+ private static String getLogTypeString(int logType) {
+ switch (logType) {
+ case MetricsEvent.ACTION_TEXT_SELECTION_OVERTYPE:
+ return "OVERTYPE";
+ case MetricsEvent.ACTION_TEXT_SELECTION_COPY:
+ return "COPY";
+ case MetricsEvent.ACTION_TEXT_SELECTION_PASTE:
+ return "PASTE";
+ case MetricsEvent.ACTION_TEXT_SELECTION_CUT:
+ return "CUT";
+ case MetricsEvent.ACTION_TEXT_SELECTION_SHARE:
+ return "SHARE";
+ case MetricsEvent.ACTION_TEXT_SELECTION_SMART_SHARE:
+ return "SMART_SHARE";
+ case MetricsEvent.ACTION_TEXT_SELECTION_DRAG:
+ return "DRAG";
+ case MetricsEvent.ACTION_TEXT_SELECTION_ABANDON:
+ return "ABANDON";
+ case MetricsEvent.ACTION_TEXT_SELECTION_OTHER:
+ return "OTHER";
+ case MetricsEvent.ACTION_TEXT_SELECTION_SELECT_ALL:
+ return "SELECT_ALL";
+ case MetricsEvent.ACTION_TEXT_SELECTION_RESET:
+ return "RESET";
+ case MetricsEvent.ACTION_TEXT_SELECTION_START:
+ return "SELECTION_STARTED";
+ case MetricsEvent.ACTION_TEXT_SELECTION_MODIFY:
+ return "SELECTION_MODIFIED";
+ case MetricsEvent.ACTION_TEXT_SELECTION_SMART_SINGLE:
+ return "SMART_SELECTION_SINGLE";
+ case MetricsEvent.ACTION_TEXT_SELECTION_SMART_MULTI:
+ return "SMART_SELECTION_MULTI";
+ case MetricsEvent.ACTION_TEXT_SELECTION_AUTO:
+ return "AUTO_SELECTION";
+ default:
+ return UNKNOWN;
+ }
+ }
+
+ private static void debugLog(LogMaker log) {
+ if (!DEBUG_LOG_ENABLED) return;
+
+ final String widgetType = Objects.toString(log.getTaggedData(WIDGET_TYPE), UNKNOWN);
+ final String widgetVersion = Objects.toString(log.getTaggedData(WIDGET_VERSION), "");
+ final String widget = widgetVersion.isEmpty()
+ ? widgetType : widgetType + "-" + widgetVersion;
+ final int index = Integer.parseInt(Objects.toString(log.getTaggedData(INDEX), ZERO));
+ if (log.getType() == MetricsEvent.ACTION_TEXT_SELECTION_START) {
+ String sessionId = Objects.toString(log.getTaggedData(SESSION_ID), "");
+ sessionId = sessionId.substring(sessionId.lastIndexOf("-") + 1);
+ Log.d(LOG_TAG, String.format("New selection session: %s (%s)", widget, sessionId));
+ }
+
+ final String model = Objects.toString(log.getTaggedData(MODEL_NAME), UNKNOWN);
+ final String entity = Objects.toString(log.getTaggedData(ENTITY_TYPE), UNKNOWN);
+ final String type = getLogTypeString(log.getType());
+ final int smartStart = Integer.parseInt(
+ Objects.toString(log.getTaggedData(SMART_START), ZERO));
+ final int smartEnd = Integer.parseInt(
+ Objects.toString(log.getTaggedData(SMART_END), ZERO));
+ final int eventStart = Integer.parseInt(
+ Objects.toString(log.getTaggedData(EVENT_START), ZERO));
+ final int eventEnd = Integer.parseInt(
+ Objects.toString(log.getTaggedData(EVENT_END), ZERO));
+
+ Log.d(LOG_TAG, String.format("%2d: %s/%s, range=%d,%d - smart_range=%d,%d (%s/%s)",
+ index, type, entity, eventStart, eventEnd, smartStart, smartEnd, widget, model));
+ }
+
+ /**
+ * Creates a signature string that may be used to tag TextClassifier results.
+ */
+ public static String createSignature(
+ String text, int start, int end, Context context, int modelVersion,
+ @Nullable Locale locale) {
+ Preconditions.checkNotNull(text);
+ Preconditions.checkNotNull(context);
+ final String modelName = (locale != null)
+ ? String.format(Locale.US, "%s_v%d", locale.toLanguageTag(), modelVersion)
+ : "";
+ final int hash = Objects.hash(text, start, end, context.getPackageName());
+ return SignatureParser.createSignature(CLASSIFIER_ID, modelName, hash);
+ }
+
+ /**
+ * Helper for creating and parsing signature strings for
+ * {@link android.view.textclassifier.TextClassifierImpl}.
+ */
+ @VisibleForTesting
+ public static final class SignatureParser {
+
+ static String createSignature(String classifierId, String modelName, int hash) {
+ return String.format(Locale.US, "%s|%s|%d", classifierId, modelName, hash);
+ }
+
+ static String getClassifierId(String signature) {
+ Preconditions.checkNotNull(signature);
+ final int end = signature.indexOf("|");
+ if (end >= 0) {
+ return signature.substring(0, end);
+ }
+ return "";
+ }
+
+ static String getModelName(String signature) {
+ Preconditions.checkNotNull(signature);
+ final int start = signature.indexOf("|");
+ final int end = signature.indexOf("|", start);
+ if (start >= 0 && end >= start) {
+ return signature.substring(start, end);
+ }
+ return "";
+ }
+
+ static int getHash(String signature) {
+ Preconditions.checkNotNull(signature);
+ final int index1 = signature.indexOf("|");
+ final int index2 = signature.indexOf("|", index1);
+ if (index2 > 0) {
+ return Integer.parseInt(signature.substring(index2));
+ }
+ return 0;
+ }
+ }
+}
diff --git a/core/java/android/view/textclassifier/logging/Logger.java b/core/java/android/view/textclassifier/logging/Logger.java
new file mode 100644
index 0000000..40e4d8c
--- /dev/null
+++ b/core/java/android/view/textclassifier/logging/Logger.java
@@ -0,0 +1,429 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier.logging;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StringDef;
+import android.content.Context;
+import android.util.Log;
+import android.view.textclassifier.TextClassification;
+import android.view.textclassifier.TextClassifier;
+import android.view.textclassifier.TextSelection;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.text.BreakIterator;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * A helper for logging TextClassifier related events.
+ */
+public abstract class Logger {
+
+ /**
+ * Use this to specify an indeterminate positive index.
+ */
+ public static final int OUT_OF_BOUNDS = Integer.MAX_VALUE;
+
+ /**
+ * Use this to specify an indeterminate negative index.
+ */
+ public static final int OUT_OF_BOUNDS_NEGATIVE = Integer.MIN_VALUE;
+
+ private static final String LOG_TAG = "Logger";
+ /* package */ static final boolean DEBUG_LOG_ENABLED = true;
+
+ private static final String NO_SIGNATURE = "";
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @StringDef({WIDGET_TEXTVIEW, WIDGET_WEBVIEW, WIDGET_EDITTEXT,
+ WIDGET_EDIT_WEBVIEW, WIDGET_CUSTOM_TEXTVIEW, WIDGET_CUSTOM_EDITTEXT,
+ WIDGET_CUSTOM_UNSELECTABLE_TEXTVIEW, WIDGET_UNKNOWN})
+ public @interface WidgetType {}
+
+ public static final String WIDGET_TEXTVIEW = "textview";
+ public static final String WIDGET_EDITTEXT = "edittext";
+ public static final String WIDGET_UNSELECTABLE_TEXTVIEW = "nosel-textview";
+ public static final String WIDGET_WEBVIEW = "webview";
+ public static final String WIDGET_EDIT_WEBVIEW = "edit-webview";
+ public static final String WIDGET_CUSTOM_TEXTVIEW = "customview";
+ public static final String WIDGET_CUSTOM_EDITTEXT = "customedit";
+ public static final String WIDGET_CUSTOM_UNSELECTABLE_TEXTVIEW = "nosel-customview";
+ public static final String WIDGET_UNKNOWN = "unknown";
+
+ private SelectionEvent mPrevEvent;
+ private SelectionEvent mSmartEvent;
+ private SelectionEvent mStartEvent;
+
+ /**
+ * Logger that does not log anything.
+ * @hide
+ */
+ public static final Logger DISABLED = new Logger() {
+ @Override
+ public void writeEvent(SelectionEvent event) {}
+ };
+
+ @Nullable
+ private final Config mConfig;
+
+ public Logger(Config config) {
+ mConfig = Preconditions.checkNotNull(config);
+ }
+
+ private Logger() {
+ mConfig = null;
+ }
+
+ /**
+ * Writes the selection event.
+ *
+ * <p><strong>NOTE: </strong>This method is designed for subclasses.
+ * Apps should not call it directly.
+ */
+ public abstract void writeEvent(@NonNull SelectionEvent event);
+
+ /**
+ * Returns true if the signature matches that of a smart selection event (i.e.
+ * {@link SelectionEvent#EVENT_SMART_SELECTION_SINGLE} or
+ * {@link SelectionEvent#EVENT_SMART_SELECTION_MULTI}).
+ * Returns false otherwise.
+ */
+ public boolean isSmartSelection(@NonNull String signature) {
+ return false;
+ }
+
+
+ /**
+ * Returns a token iterator for tokenizing text for logging purposes.
+ */
+ public BreakIterator getTokenIterator(@NonNull Locale locale) {
+ return BreakIterator.getWordInstance(Preconditions.checkNotNull(locale));
+ }
+
+ /**
+ * Logs a "selection started" event.
+ *
+ * @param start the token index of the selected token
+ */
+ public final void logSelectionStartedEvent(int start) {
+ if (mConfig == null) {
+ return;
+ }
+
+ logEvent(new SelectionEvent(
+ start, start + 1, SelectionEvent.EVENT_SELECTION_STARTED,
+ TextClassifier.TYPE_UNKNOWN, NO_SIGNATURE, mConfig));
+ }
+
+ /**
+ * Logs a "selection modified" event.
+ * Use when the user modifies the selection.
+ *
+ * @param start the start token (inclusive) index of the selection
+ * @param end the end token (exclusive) index of the selection
+ */
+ public final void logSelectionModifiedEvent(int start, int end) {
+ Preconditions.checkArgument(end >= start, "end cannot be less than start");
+
+ if (mConfig == null) {
+ return;
+ }
+
+ logEvent(new SelectionEvent(
+ start, end, SelectionEvent.EVENT_SELECTION_MODIFIED,
+ TextClassifier.TYPE_UNKNOWN, NO_SIGNATURE, mConfig));
+ }
+
+ /**
+ * Logs a "selection modified" event.
+ * Use when the user modifies the selection and the selection's entity type is known.
+ *
+ * @param start the start token (inclusive) index of the selection
+ * @param end the end token (exclusive) index of the selection
+ * @param classification the TextClassification object returned by the TextClassifier that
+ * classified the selected text
+ */
+ public final void logSelectionModifiedEvent(
+ int start, int end, @NonNull TextClassification classification) {
+ Preconditions.checkArgument(end >= start, "end cannot be less than start");
+ Preconditions.checkNotNull(classification);
+
+ if (mConfig == null) {
+ return;
+ }
+
+ final String entityType = classification.getEntityCount() > 0
+ ? classification.getEntity(0)
+ : TextClassifier.TYPE_UNKNOWN;
+ final String signature = classification.getSignature();
+ logEvent(new SelectionEvent(
+ start, end, SelectionEvent.EVENT_SELECTION_MODIFIED,
+ entityType, signature, mConfig));
+ }
+
+ /**
+ * Logs a "selection modified" event.
+ * Use when a TextClassifier modifies the selection.
+ *
+ * @param start the start token (inclusive) index of the selection
+ * @param end the end token (exclusive) index of the selection
+ * @param selection the TextSelection object returned by the TextClassifier for the
+ * specified selection
+ */
+ public final void logSelectionModifiedEvent(
+ int start, int end, @NonNull TextSelection selection) {
+ Preconditions.checkArgument(end >= start, "end cannot be less than start");
+ Preconditions.checkNotNull(selection);
+
+ if (mConfig == null) {
+ return;
+ }
+
+ final int eventType;
+ if (isSmartSelection(selection.getSignature())) {
+ eventType = end - start > 1
+ ? SelectionEvent.EVENT_SMART_SELECTION_MULTI
+ : SelectionEvent.EVENT_SMART_SELECTION_SINGLE;
+
+ } else {
+ eventType = SelectionEvent.EVENT_AUTO_SELECTION;
+ }
+ final String entityType = selection.getEntityCount() > 0
+ ? selection.getEntity(0)
+ : TextClassifier.TYPE_UNKNOWN;
+ final String signature = selection.getSignature();
+ logEvent(new SelectionEvent(start, end, eventType, entityType, signature, mConfig));
+ }
+
+ /**
+ * Logs an event specifying an action taken on a selection.
+ * Use when the user clicks on an action to act on the selected text.
+ *
+ * @param start the start token (inclusive) index of the selection
+ * @param end the end token (exclusive) index of the selection
+ * @param actionType the action that was performed on the selection
+ */
+ public final void logSelectionActionEvent(
+ int start, int end, @SelectionEvent.ActionType int actionType) {
+ Preconditions.checkArgument(end >= start, "end cannot be less than start");
+ checkActionType(actionType);
+
+ if (mConfig == null) {
+ return;
+ }
+
+ logEvent(new SelectionEvent(
+ start, end, actionType, TextClassifier.TYPE_UNKNOWN, NO_SIGNATURE, mConfig));
+ }
+
+ /**
+ * Logs an event specifying an action taken on a selection.
+ * Use when the user clicks on an action to act on the selected text and the selection's
+ * entity type is known.
+ *
+ * @param start the start token (inclusive) index of the selection
+ * @param end the end token (exclusive) index of the selection
+ * @param actionType the action that was performed on the selection
+ * @param classification the TextClassification object returned by the TextClassifier that
+ * classified the selected text
+ *
+ * @throws IllegalArgumentException If actionType is not a valid SelectionEvent actionType
+ */
+ public final void logSelectionActionEvent(
+ int start, int end, @SelectionEvent.ActionType int actionType,
+ @NonNull TextClassification classification) {
+ Preconditions.checkArgument(end >= start, "end cannot be less than start");
+ Preconditions.checkNotNull(classification);
+ checkActionType(actionType);
+
+ if (mConfig == null) {
+ return;
+ }
+
+ final String entityType = classification.getEntityCount() > 0
+ ? classification.getEntity(0)
+ : TextClassifier.TYPE_UNKNOWN;
+ final String signature = classification.getSignature();
+ logEvent(new SelectionEvent(start, end, actionType, entityType, signature, mConfig));
+ }
+
+ private void logEvent(@NonNull SelectionEvent event) {
+ Preconditions.checkNotNull(event);
+
+ if (event.getEventType() != SelectionEvent.EVENT_SELECTION_STARTED
+ && mStartEvent == null) {
+ if (DEBUG_LOG_ENABLED) {
+ Log.d(LOG_TAG, "Selection session not yet started. Ignoring event");
+ }
+ return;
+ }
+
+ final long now = System.currentTimeMillis();
+ switch (event.getEventType()) {
+ case SelectionEvent.EVENT_SELECTION_STARTED:
+ Preconditions.checkArgument(event.getAbsoluteEnd() == event.getAbsoluteStart() + 1);
+ event.setSessionId(startNewSession());
+ mStartEvent = event;
+ break;
+ case SelectionEvent.EVENT_SMART_SELECTION_SINGLE: // fall through
+ case SelectionEvent.EVENT_SMART_SELECTION_MULTI:
+ mSmartEvent = event;
+ break;
+ case SelectionEvent.EVENT_SELECTION_MODIFIED: // fall through
+ case SelectionEvent.EVENT_AUTO_SELECTION:
+ if (mPrevEvent != null
+ && mPrevEvent.getAbsoluteStart() == event.getAbsoluteStart()
+ && mPrevEvent.getAbsoluteEnd() == event.getAbsoluteEnd()) {
+ // Selection did not change. Ignore event.
+ return;
+ }
+ }
+
+ event.setEventTime(now);
+ if (mStartEvent != null) {
+ event.setSessionId(mStartEvent.getSessionId())
+ .setDurationSinceSessionStart(now - mStartEvent.getEventTime())
+ .setStart(event.getAbsoluteStart() - mStartEvent.getAbsoluteStart())
+ .setEnd(event.getAbsoluteEnd() - mStartEvent.getAbsoluteStart());
+ }
+ if (mSmartEvent != null) {
+ event.setSignature(mSmartEvent.getSignature())
+ .setSmartStart(mSmartEvent.getAbsoluteStart() - mStartEvent.getAbsoluteStart())
+ .setSmartEnd(mSmartEvent.getAbsoluteEnd() - mStartEvent.getAbsoluteStart());
+ }
+ if (mPrevEvent != null) {
+ event.setDurationSincePreviousEvent(now - mPrevEvent.getEventTime())
+ .setEventIndex(mPrevEvent.getEventIndex() + 1);
+ }
+ writeEvent(event);
+ mPrevEvent = event;
+
+ if (event.isTerminal()) {
+ endSession();
+ }
+ }
+
+ private String startNewSession() {
+ endSession();
+ return UUID.randomUUID().toString();
+ }
+
+ private void endSession() {
+ mPrevEvent = null;
+ mSmartEvent = null;
+ mStartEvent = null;
+ }
+
+ /**
+ * @throws IllegalArgumentException If eventType is not an {@link SelectionEvent.ActionType}
+ */
+ private static void checkActionType(@SelectionEvent.EventType int eventType)
+ throws IllegalArgumentException {
+ switch (eventType) {
+ case SelectionEvent.ACTION_OVERTYPE: // fall through
+ case SelectionEvent.ACTION_COPY: // fall through
+ case SelectionEvent.ACTION_PASTE: // fall through
+ case SelectionEvent.ACTION_CUT: // fall through
+ case SelectionEvent.ACTION_SHARE: // fall through
+ case SelectionEvent.ACTION_SMART_SHARE: // fall through
+ case SelectionEvent.ACTION_DRAG: // fall through
+ case SelectionEvent.ACTION_ABANDON: // fall through
+ case SelectionEvent.ACTION_SELECT_ALL: // fall through
+ case SelectionEvent.ACTION_RESET: // fall through
+ return;
+ default:
+ throw new IllegalArgumentException(
+ String.format(Locale.US, "%d is not an eventType", eventType));
+ }
+ }
+
+
+ /**
+ * A Logger config.
+ */
+ public static final class Config {
+
+ private final String mPackageName;
+ private final String mWidgetType;
+ @Nullable private final String mWidgetVersion;
+
+ /**
+ * @param context Context of the widget the logger logs for
+ * @param widgetType a name for the widget being logged for. e.g.
+ * {@link #WIDGET_TEXTVIEW}
+ * @param widgetVersion a string version info for the widget the logger logs for
+ */
+ public Config(
+ @NonNull Context context,
+ @WidgetType String widgetType,
+ @Nullable String widgetVersion) {
+ mPackageName = Preconditions.checkNotNull(context).getPackageName();
+ mWidgetType = widgetType;
+ mWidgetVersion = widgetVersion;
+ }
+
+ /**
+ * Returns the package name of the application the logger logs for.
+ */
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * Returns the name for the widget being logged for. e.g. {@link #WIDGET_TEXTVIEW}.
+ */
+ public String getWidgetType() {
+ return mWidgetType;
+ }
+
+ /**
+ * Returns string version info for the logger. This is specific to the text classifier.
+ */
+ @Nullable
+ public String getWidgetVersion() {
+ return mWidgetVersion;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mPackageName, mWidgetType, mWidgetVersion);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+
+ if (!(obj instanceof Config)) {
+ return false;
+ }
+
+ final Config other = (Config) obj;
+ return Objects.equals(mPackageName, other.mPackageName)
+ && Objects.equals(mWidgetType, other.mWidgetType)
+ && Objects.equals(mWidgetVersion, other.mWidgetType);
+ }
+ }
+}
diff --git a/core/java/android/view/textclassifier/logging/SelectionEvent.java b/core/java/android/view/textclassifier/logging/SelectionEvent.java
new file mode 100644
index 0000000..f40b655
--- /dev/null
+++ b/core/java/android/view/textclassifier/logging/SelectionEvent.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier.logging;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.view.textclassifier.TextClassifier.EntityType;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Locale;
+
+/**
+ * A selection event.
+ * Specify index parameters as word token indices.
+ */
+public final class SelectionEvent {
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({ACTION_OVERTYPE, ACTION_COPY, ACTION_PASTE, ACTION_CUT,
+ ACTION_SHARE, ACTION_SMART_SHARE, ACTION_DRAG, ACTION_ABANDON,
+ ACTION_OTHER, ACTION_SELECT_ALL, ACTION_RESET})
+ // NOTE: ActionType values should not be lower than 100 to avoid colliding with the other
+ // EventTypes declared below.
+ public @interface ActionType {
+ /*
+ * Terminal event types range: [100,200).
+ * Non-terminal event types range: [200,300).
+ */
+ }
+
+ /** User typed over the selection. */
+ public static final int ACTION_OVERTYPE = 100;
+ /** User copied the selection. */
+ public static final int ACTION_COPY = 101;
+ /** User pasted over the selection. */
+ public static final int ACTION_PASTE = 102;
+ /** User cut the selection. */
+ public static final int ACTION_CUT = 103;
+ /** User shared the selection. */
+ public static final int ACTION_SHARE = 104;
+ /** User clicked the textAssist menu item. */
+ public static final int ACTION_SMART_SHARE = 105;
+ /** User dragged+dropped the selection. */
+ public static final int ACTION_DRAG = 106;
+ /** User abandoned the selection. */
+ public static final int ACTION_ABANDON = 107;
+ /** User performed an action on the selection. */
+ public static final int ACTION_OTHER = 108;
+
+ // Non-terminal actions.
+ /** User activated Select All */
+ public static final int ACTION_SELECT_ALL = 200;
+ /** User reset the smart selection. */
+ public static final int ACTION_RESET = 201;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({ACTION_OVERTYPE, ACTION_COPY, ACTION_PASTE, ACTION_CUT,
+ ACTION_SHARE, ACTION_SMART_SHARE, ACTION_DRAG, ACTION_ABANDON,
+ ACTION_OTHER, ACTION_SELECT_ALL, ACTION_RESET,
+ EVENT_SELECTION_STARTED, EVENT_SELECTION_MODIFIED,
+ EVENT_SMART_SELECTION_SINGLE, EVENT_SMART_SELECTION_MULTI,
+ EVENT_AUTO_SELECTION})
+ // NOTE: EventTypes declared here must be less than 100 to avoid colliding with the
+ // ActionTypes declared above.
+ public @interface EventType {
+ /*
+ * Range: 1 -> 99.
+ */
+ }
+
+ /** User started a new selection. */
+ public static final int EVENT_SELECTION_STARTED = 1;
+ /** User modified an existing selection. */
+ public static final int EVENT_SELECTION_MODIFIED = 2;
+ /** Smart selection triggered for a single token (word). */
+ public static final int EVENT_SMART_SELECTION_SINGLE = 3;
+ /** Smart selection triggered spanning multiple tokens (words). */
+ public static final int EVENT_SMART_SELECTION_MULTI = 4;
+ /** Something else other than User or the default TextClassifier triggered a selection. */
+ public static final int EVENT_AUTO_SELECTION = 5;
+
+ private final int mAbsoluteStart;
+ private final int mAbsoluteEnd;
+ private final @EventType int mEventType;
+ private final @EntityType String mEntityType;
+ @Nullable private final String mWidgetVersion;
+ private final String mPackageName;
+ private final String mWidgetType;
+
+ // These fields should only be set by creator of a SelectionEvent.
+ private String mSignature;
+ private long mEventTime;
+ private long mDurationSinceSessionStart;
+ private long mDurationSinceLastEvent;
+ private int mEventIndex;
+ private String mSessionId;
+ private int mStart;
+ private int mEnd;
+ private int mSmartStart;
+ private int mSmartEnd;
+
+ SelectionEvent(
+ int start, int end,
+ @EventType int eventType, @EntityType String entityType,
+ String signature, Logger.Config config) {
+ Preconditions.checkArgument(end >= start, "end cannot be less than start");
+ mAbsoluteStart = start;
+ mAbsoluteEnd = end;
+ mEventType = eventType;
+ mEntityType = Preconditions.checkNotNull(entityType);
+ mSignature = Preconditions.checkNotNull(signature);
+ Preconditions.checkNotNull(config);
+ mWidgetVersion = config.getWidgetVersion();
+ mPackageName = Preconditions.checkNotNull(config.getPackageName());
+ mWidgetType = Preconditions.checkNotNull(config.getWidgetType());
+ }
+
+ int getAbsoluteStart() {
+ return mAbsoluteStart;
+ }
+
+ int getAbsoluteEnd() {
+ return mAbsoluteEnd;
+ }
+
+ /**
+ * Returns the type of event that was triggered. e.g. {@link #ACTION_COPY}.
+ */
+ public int getEventType() {
+ return mEventType;
+ }
+
+ /**
+ * Returns the type of entity that is associated with this event. e.g.
+ * {@link android.view.textclassifier.TextClassifier#TYPE_EMAIL}.
+ */
+ @EntityType
+ public String getEntityType() {
+ return mEntityType;
+ }
+
+ /**
+ * Returns the package name of the app that this event originated in.
+ */
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * Returns the type of widget that was involved in triggering this event.
+ */
+ public String getWidgetType() {
+ return mWidgetType;
+ }
+
+ /**
+ * Returns a string version info for the widget this event was triggered in.
+ */
+ public String getWidgetVersion() {
+ return mWidgetVersion;
+ }
+
+ /**
+ * Returns the signature of the text classifier result associated with this event.
+ */
+ public String getSignature() {
+ return mSignature;
+ }
+
+ SelectionEvent setSignature(String signature) {
+ mSignature = Preconditions.checkNotNull(signature);
+ return this;
+ }
+
+ /**
+ * Returns the time this event was triggered.
+ */
+ public long getEventTime() {
+ return mEventTime;
+ }
+
+ SelectionEvent setEventTime(long timeMs) {
+ mEventTime = timeMs;
+ return this;
+ }
+
+ /**
+ * Returns the duration in ms between when this event was triggered and when the first event in
+ * the selection session was triggered.
+ */
+ public long getDurationSinceSessionStart() {
+ return mDurationSinceSessionStart;
+ }
+
+ SelectionEvent setDurationSinceSessionStart(long durationMs) {
+ mDurationSinceSessionStart = durationMs;
+ return this;
+ }
+
+ /**
+ * Returns the duration in ms between when this event was triggered and when the previous event
+ * in the selection session was triggered.
+ */
+ public long getDurationSincePreviousEvent() {
+ return mDurationSinceLastEvent;
+ }
+
+ SelectionEvent setDurationSincePreviousEvent(long durationMs) {
+ this.mDurationSinceLastEvent = durationMs;
+ return this;
+ }
+
+ /**
+ * Returns the index (e.g. 1st event, 2nd event, etc.) of this event in the selection session.
+ */
+ public int getEventIndex() {
+ return mEventIndex;
+ }
+
+ SelectionEvent setEventIndex(int index) {
+ mEventIndex = index;
+ return this;
+ }
+
+ /**
+ * Returns the selection session id.
+ */
+ public String getSessionId() {
+ return mSessionId;
+ }
+
+ SelectionEvent setSessionId(String id) {
+ mSessionId = id;
+ return this;
+ }
+
+ /**
+ * Returns the start index of this events token relative to the index of the start selection
+ * event in the selection session.
+ */
+ public int getStart() {
+ return mStart;
+ }
+
+ SelectionEvent setStart(int start) {
+ mStart = start;
+ return this;
+ }
+
+ /**
+ * Returns the end index of this events token relative to the index of the start selection
+ * event in the selection session.
+ */
+ public int getEnd() {
+ return mEnd;
+ }
+
+ SelectionEvent setEnd(int end) {
+ mEnd = end;
+ return this;
+ }
+
+ /**
+ * Returns the start index of this events token relative to the index of the smart selection
+ * event in the selection session.
+ */
+ public int getSmartStart() {
+ return mSmartStart;
+ }
+
+ SelectionEvent setSmartStart(int start) {
+ this.mSmartStart = start;
+ return this;
+ }
+
+ /**
+ * Returns the end index of this events token relative to the index of the smart selection
+ * event in the selection session.
+ */
+ public int getSmartEnd() {
+ return mSmartEnd;
+ }
+
+ SelectionEvent setSmartEnd(int end) {
+ mSmartEnd = end;
+ return this;
+ }
+
+ boolean isTerminal() {
+ switch (mEventType) {
+ case ACTION_OVERTYPE: // fall through
+ case ACTION_COPY: // fall through
+ case ACTION_PASTE: // fall through
+ case ACTION_CUT: // fall through
+ case ACTION_SHARE: // fall through
+ case ACTION_SMART_SHARE: // fall through
+ case ACTION_DRAG: // fall through
+ case ACTION_ABANDON: // fall through
+ case ACTION_OTHER: // fall through
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.format(Locale.US,
+ "SelectionEvent {absoluteStart=%d, absoluteEnd=%d, eventType=%d, entityType=%s, "
+ + "widgetVersion=%s, packageName=%s, widgetType=%s, signature=%s, "
+ + "eventTime=%d, durationSinceSessionStart=%d, durationSinceLastEvent=%d, "
+ + "eventIndex=%d, sessionId=%s, start=%d, end=%d, smartStart=%d, smartEnd=%d}",
+ mAbsoluteStart, mAbsoluteEnd, mEventType, mEntityType,
+ mWidgetVersion, mPackageName, mWidgetType, mSignature,
+ mEventTime, mDurationSinceSessionStart, mDurationSinceLastEvent,
+ mEventIndex, mSessionId, mStart, mEnd, mSmartStart, mSmartEnd);
+ }
+}
diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java
index d2cb70e..65deb3b 100644
--- a/core/java/android/webkit/WebView.java
+++ b/core/java/android/webkit/WebView.java
@@ -2106,6 +2106,18 @@
* the default directory, and other processes must call this API to define
* a unique suffix.
* <p>
+ * This means that different processes in the same application cannot directly
+ * share WebView-related data, since the data directories must be distinct.
+ * Applications that use this API may have to explicitly pass data between
+ * processes. For example, login cookies may have to be copied from one
+ * process's cookie jar to the other using {@link CookieManager} if both
+ * processes' WebViews are intended to be logged in.
+ * <p>
+ * Most applications should simply ensure that all components of the app
+ * that rely on WebView are in the same process, to avoid needing multiple
+ * data directories. The {@link #disableWebView} method can be used to ensure
+ * that the other processes do not use WebView by accident in this case.
+ * <p>
* This API must be called before any instances of WebView are created in
* this process and before any other methods in the android.webkit package
* are called by this process.
@@ -2126,10 +2138,14 @@
* methods in the android.webkit package are used.
* <p>
* Applications with multiple processes may wish to call this in processes
- * which are not intended to use WebView to prevent potential data directory
- * conflicts (see {@link #setDataDirectorySuffix}) and to avoid accidentally
- * incurring the memory usage of initializing WebView in long-lived
- * processes which have no need for it.
+ * that are not intended to use WebView to avoid accidentally incurring
+ * the memory usage of initializing WebView in long-lived processes that
+ * have no need for it, and to prevent potential data directory conflicts
+ * (see {@link #setDataDirectorySuffix}).
+ * <p>
+ * For example, an audio player application with one process for its
+ * activities and another process for its playback service may wish to call
+ * this method in the playback service's {@link android.app.Service#onCreate}.
*
* @throws IllegalStateException if WebView has already been initialized
* in the current process.
diff --git a/core/java/android/widget/Magnifier.java b/core/java/android/widget/Magnifier.java
index e42217f..7a4c800 100644
--- a/core/java/android/widget/Magnifier.java
+++ b/core/java/android/widget/Magnifier.java
@@ -182,8 +182,6 @@
/**
* Forces the magnifier to update its content. It uses the previous coordinates passed to
* {@link #show(float, float)}. This only happens if the magnifier is currently showing.
- *
- * @hide
*/
public void update() {
if (mWindow.isShowing()) {
diff --git a/core/java/android/widget/MediaControlView2.java b/core/java/android/widget/MediaControlView2.java
index 39c23b4..2e4cccf 100644
--- a/core/java/android/widget/MediaControlView2.java
+++ b/core/java/android/widget/MediaControlView2.java
@@ -19,16 +19,17 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SystemApi;
import android.content.Context;
import android.media.session.MediaController;
import android.media.update.ApiLoader;
+import android.media.update.FrameLayoutHelper;
import android.media.update.MediaControlView2Provider;
-import android.media.update.ViewProvider;
import android.util.AttributeSet;
-import android.view.MotionEvent;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+
/**
* A View that contains the controls for MediaPlayer2.
* It provides a wide range of UI including buttons such as "Play/Pause", "Rewind", "Fast Forward",
@@ -61,7 +62,7 @@
* TODO PUBLIC API
* @hide
*/
-public class MediaControlView2 extends FrameLayout {
+public class MediaControlView2 extends FrameLayoutHelper<MediaControlView2Provider> {
/** @hide */
@IntDef({
BUTTON_PLAY_PAUSE,
@@ -140,8 +141,6 @@
*/
public static final String COMMAND_SET_FULLSCREEN = "setFullscreen";
- private final MediaControlView2Provider mProvider;
-
public MediaControlView2(@NonNull Context context) {
this(context, null);
}
@@ -157,15 +156,16 @@
public MediaControlView2(@NonNull Context context, @Nullable AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
-
- mProvider = ApiLoader.getProvider(context)
- .createMediaControlView2(this, new SuperProvider());
+ super((instance, superProvider) ->
+ ApiLoader.getProvider(context).createMediaControlView2(
+ (MediaControlView2) instance, superProvider),
+ context, attrs, defStyleAttr, defStyleRes);
}
/**
* @hide
*/
+ @SystemApi
public MediaControlView2Provider getProvider() {
return mProvider;
}
@@ -232,80 +232,9 @@
}
@Override
+ // TODO Move this method to ViewProvider
public void onVisibilityAggregated(boolean isVisible) {
mProvider.onVisibilityAggregated_impl(isVisible);
}
-
- @Override
- protected void onAttachedToWindow() {
- mProvider.onAttachedToWindow_impl();
- }
-
- @Override
- protected void onDetachedFromWindow() {
- mProvider.onDetachedFromWindow_impl();
- }
-
- @Override
- public CharSequence getAccessibilityClassName() {
- return mProvider.getAccessibilityClassName_impl();
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent ev) {
- return mProvider.onTouchEvent_impl(ev);
- }
-
- @Override
- public boolean onTrackballEvent(MotionEvent ev) {
- return mProvider.onTrackballEvent_impl(ev);
- }
-
- @Override
- public void onFinishInflate() {
- mProvider.onFinishInflate_impl();
- }
-
- @Override
- public void setEnabled(boolean enabled) {
- mProvider.setEnabled_impl(enabled);
- }
-
- private class SuperProvider implements ViewProvider {
- @Override
- public void onAttachedToWindow_impl() {
- MediaControlView2.super.onAttachedToWindow();
- }
-
- @Override
- public void onDetachedFromWindow_impl() {
- MediaControlView2.super.onDetachedFromWindow();
- }
-
- @Override
- public CharSequence getAccessibilityClassName_impl() {
- return MediaControlView2.super.getAccessibilityClassName();
- }
-
- @Override
- public boolean onTouchEvent_impl(MotionEvent ev) {
- return MediaControlView2.super.onTouchEvent(ev);
- }
-
- @Override
- public boolean onTrackballEvent_impl(MotionEvent ev) {
- return MediaControlView2.super.onTrackballEvent(ev);
- }
-
- @Override
- public void onFinishInflate_impl() {
- MediaControlView2.super.onFinishInflate();
- }
-
- @Override
- public void setEnabled_impl(boolean enabled) {
- MediaControlView2.super.setEnabled(enabled);
- }
- }
}
diff --git a/core/java/android/widget/SelectionActionModeHelper.java b/core/java/android/widget/SelectionActionModeHelper.java
index 3bfa520..2e354c1 100644
--- a/core/java/android/widget/SelectionActionModeHelper.java
+++ b/core/java/android/widget/SelectionActionModeHelper.java
@@ -37,8 +37,8 @@
import android.view.textclassifier.TextClassifier;
import android.view.textclassifier.TextLinks;
import android.view.textclassifier.TextSelection;
-import android.view.textclassifier.logging.SmartSelectionEventTracker;
-import android.view.textclassifier.logging.SmartSelectionEventTracker.SelectionEvent;
+import android.view.textclassifier.logging.Logger;
+import android.view.textclassifier.logging.SelectionEvent;
import android.widget.Editor.SelectionModifierCursorController;
import com.android.internal.annotations.VisibleForTesting;
@@ -65,6 +65,7 @@
private static final String LOG_TAG = "SelectActionModeHelper";
+ // TODO: Make this a configurable flag.
private static final boolean SMART_SELECT_ANIMATION_ENABLED = true;
private final Editor mEditor;
@@ -172,7 +173,7 @@
public void onSelectionDrag() {
mSelectionTracker.onSelectionAction(
mTextView.getSelectionStart(), mTextView.getSelectionEnd(),
- SelectionEvent.ActionType.DRAG, mTextClassification);
+ SelectionEvent.ACTION_DRAG, mTextClassification);
}
public void onTextChanged(int start, int end) {
@@ -574,7 +575,7 @@
mSelectionEnd = editor.getTextView().getSelectionEnd();
mLogger.logSelectionAction(
textView.getSelectionStart(), textView.getSelectionEnd(),
- SelectionEvent.ActionType.RESET, null /* classification */);
+ SelectionEvent.ACTION_RESET, null /* classification */);
}
return selected;
}
@@ -583,7 +584,7 @@
public void onTextChanged(int start, int end, TextClassification classification) {
if (isSelectionStarted() && start == mSelectionStart && end == mSelectionEnd) {
- onSelectionAction(start, end, SelectionEvent.ActionType.OVERTYPE, classification);
+ onSelectionAction(start, end, SelectionEvent.ACTION_OVERTYPE, classification);
}
}
@@ -622,7 +623,7 @@
if (mIsPending) {
mLogger.logSelectionAction(
mSelectionStart, mSelectionEnd,
- SelectionEvent.ActionType.ABANDON, null /* classification */);
+ SelectionEvent.ACTION_ABANDON, null /* classification */);
mSelectionStart = mSelectionEnd = -1;
mIsPending = false;
}
@@ -649,22 +650,29 @@
private static final String LOG_TAG = "SelectionMetricsLogger";
private static final Pattern PATTERN_WHITESPACE = Pattern.compile("\\s+");
- private final SmartSelectionEventTracker mDelegate;
+ private final Logger mLogger;
private final boolean mEditTextLogger;
- private final BreakIterator mWordIterator;
+ private final BreakIterator mTokenIterator;
private int mStartIndex;
private String mText;
SelectionMetricsLogger(TextView textView) {
Preconditions.checkNotNull(textView);
- final @SmartSelectionEventTracker.WidgetType int widgetType = textView.isTextEditable()
- ? SmartSelectionEventTracker.WidgetType.EDITTEXT
- : (textView.isTextSelectable()
- ? SmartSelectionEventTracker.WidgetType.TEXTVIEW
- : SmartSelectionEventTracker.WidgetType.UNSELECTABLE_TEXTVIEW);
- mDelegate = new SmartSelectionEventTracker(textView.getContext(), widgetType);
+ mLogger = textView.getTextClassifier().getLogger(
+ new Logger.Config(textView.getContext(), getWidetType(textView), null));
mEditTextLogger = textView.isTextEditable();
- mWordIterator = BreakIterator.getWordInstance(textView.getTextLocale());
+ mTokenIterator = mLogger.getTokenIterator(textView.getTextLocale());
+ }
+
+ @Logger.WidgetType
+ private static String getWidetType(TextView textView) {
+ if (textView.isTextEditable()) {
+ return Logger.WIDGET_EDITTEXT;
+ }
+ if (textView.isTextSelectable()) {
+ return Logger.WIDGET_TEXTVIEW;
+ }
+ return Logger.WIDGET_UNSELECTABLE_TEXTVIEW;
}
public void logSelectionStarted(CharSequence text, int index) {
@@ -674,9 +682,9 @@
if (mText == null || !mText.contentEquals(text)) {
mText = text.toString();
}
- mWordIterator.setText(mText);
+ mTokenIterator.setText(mText);
mStartIndex = index;
- mDelegate.logEvent(SelectionEvent.selectionStarted(0));
+ mLogger.logSelectionStartedEvent(0);
} catch (Exception e) {
// Avoid crashes due to logging.
Log.d(LOG_TAG, e.getMessage());
@@ -690,14 +698,14 @@
Preconditions.checkArgumentInRange(end, start, mText.length(), "end");
int[] wordIndices = getWordDelta(start, end);
if (selection != null) {
- mDelegate.logEvent(SelectionEvent.selectionModified(
- wordIndices[0], wordIndices[1], selection));
+ mLogger.logSelectionModifiedEvent(
+ wordIndices[0], wordIndices[1], selection);
} else if (classification != null) {
- mDelegate.logEvent(SelectionEvent.selectionModified(
- wordIndices[0], wordIndices[1], classification));
+ mLogger.logSelectionModifiedEvent(
+ wordIndices[0], wordIndices[1], classification);
} else {
- mDelegate.logEvent(SelectionEvent.selectionModified(
- wordIndices[0], wordIndices[1]));
+ mLogger.logSelectionModifiedEvent(
+ wordIndices[0], wordIndices[1]);
}
} catch (Exception e) {
// Avoid crashes due to logging.
@@ -714,11 +722,11 @@
Preconditions.checkArgumentInRange(end, start, mText.length(), "end");
int[] wordIndices = getWordDelta(start, end);
if (classification != null) {
- mDelegate.logEvent(SelectionEvent.selectionAction(
- wordIndices[0], wordIndices[1], action, classification));
+ mLogger.logSelectionActionEvent(
+ wordIndices[0], wordIndices[1], action, classification);
} else {
- mDelegate.logEvent(SelectionEvent.selectionAction(
- wordIndices[0], wordIndices[1], action));
+ mLogger.logSelectionActionEvent(
+ wordIndices[0], wordIndices[1], action);
}
} catch (Exception e) {
// Avoid crashes due to logging.
@@ -741,10 +749,10 @@
wordIndices[0] = countWordsBackward(start);
// For the selection start index, avoid counting a partial word backwards.
- if (!mWordIterator.isBoundary(start)
+ if (!mTokenIterator.isBoundary(start)
&& !isWhitespace(
- mWordIterator.preceding(start),
- mWordIterator.following(start))) {
+ mTokenIterator.preceding(start),
+ mTokenIterator.following(start))) {
// We counted a partial word. Remove it.
wordIndices[0]--;
}
@@ -766,7 +774,7 @@
int wordCount = 0;
int offset = from;
while (offset > mStartIndex) {
- int start = mWordIterator.preceding(offset);
+ int start = mTokenIterator.preceding(offset);
if (!isWhitespace(start, offset)) {
wordCount++;
}
@@ -780,7 +788,7 @@
int wordCount = 0;
int offset = from;
while (offset < mStartIndex) {
- int end = mWordIterator.following(offset);
+ int end = mTokenIterator.following(offset);
if (!isWhitespace(offset, end)) {
wordCount++;
}
@@ -1021,20 +1029,20 @@
private static int getActionType(int menuItemId) {
switch (menuItemId) {
case TextView.ID_SELECT_ALL:
- return SelectionEvent.ActionType.SELECT_ALL;
+ return SelectionEvent.ACTION_SELECT_ALL;
case TextView.ID_CUT:
- return SelectionEvent.ActionType.CUT;
+ return SelectionEvent.ACTION_CUT;
case TextView.ID_COPY:
- return SelectionEvent.ActionType.COPY;
+ return SelectionEvent.ACTION_COPY;
case TextView.ID_PASTE: // fall through
case TextView.ID_PASTE_AS_PLAIN_TEXT:
- return SelectionEvent.ActionType.PASTE;
+ return SelectionEvent.ACTION_PASTE;
case TextView.ID_SHARE:
- return SelectionEvent.ActionType.SHARE;
+ return SelectionEvent.ACTION_SHARE;
case TextView.ID_ASSIST:
- return SelectionEvent.ActionType.SMART_SHARE;
+ return SelectionEvent.ACTION_SMART_SHARE;
default:
- return SelectionEvent.ActionType.OTHER;
+ return SelectionEvent.ACTION_OTHER;
}
}
diff --git a/core/java/android/widget/TimePickerClockDelegate.java b/core/java/android/widget/TimePickerClockDelegate.java
index 706b0ce..77670b3 100644
--- a/core/java/android/widget/TimePickerClockDelegate.java
+++ b/core/java/android/widget/TimePickerClockDelegate.java
@@ -50,6 +50,7 @@
import com.android.internal.widget.NumericTextView;
import com.android.internal.widget.NumericTextView.OnValueChangedListener;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Calendar;
@@ -804,20 +805,56 @@
private void updateHeaderSeparator() {
final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mLocale,
(mIs24Hour) ? "Hm" : "hm");
- final String separatorText;
- // See http://www.unicode.org/reports/tr35/tr35-dates.html for hour formats
- final char[] hourFormats = {'H', 'h', 'K', 'k'};
- int hIndex = lastIndexOfAny(bestDateTimePattern, hourFormats);
- if (hIndex == -1) {
- // Default case
- separatorText = ":";
- } else {
- separatorText = Character.toString(bestDateTimePattern.charAt(hIndex + 1));
- }
+ final String separatorText = getHourMinSeparatorFromPattern(bestDateTimePattern);
mSeparatorView.setText(separatorText);
mTextInputPickerView.updateSeparator(separatorText);
}
+ /**
+ * This helper method extracts the time separator from the {@code datetimePattern}.
+ *
+ * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":".
+ *
+ * See http://unicode.org/cldr/trac/browser/trunk/common/main
+ *
+ * @return Separator string. This is the character or set of quoted characters just after the
+ * hour marker in {@code dateTimePattern}. Returns a colon (:) if it can't locate the
+ * separator.
+ *
+ * @hide
+ */
+ private static String getHourMinSeparatorFromPattern(String dateTimePattern) {
+ final String defaultSeparator = ":";
+ boolean foundHourPattern = false;
+ for (int i = 0; i < dateTimePattern.length(); i++) {
+ switch (dateTimePattern.charAt(i)) {
+ // See http://www.unicode.org/reports/tr35/tr35-dates.html for hour formats.
+ case 'H':
+ case 'h':
+ case 'K':
+ case 'k':
+ foundHourPattern = true;
+ continue;
+ case ' ': // skip spaces
+ continue;
+ case '\'':
+ if (!foundHourPattern) {
+ continue;
+ }
+ SpannableStringBuilder quotedSubstring = new SpannableStringBuilder(
+ dateTimePattern.substring(i));
+ int quotedTextLength = DateFormat.appendQuotedText(quotedSubstring, 0);
+ return quotedSubstring.subSequence(0, quotedTextLength).toString();
+ default:
+ if (!foundHourPattern) {
+ continue;
+ }
+ return Character.toString(dateTimePattern.charAt(i));
+ }
+ }
+ return defaultSeparator;
+ }
+
static private int lastIndexOfAny(String str, char[] any) {
final int lengthAny = any.length;
if (lengthAny > 0) {
diff --git a/core/java/android/widget/VideoView2.java b/core/java/android/widget/VideoView2.java
index 8404223..78ca011 100644
--- a/core/java/android/widget/VideoView2.java
+++ b/core/java/android/widget/VideoView2.java
@@ -27,12 +27,11 @@
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.media.update.ApiLoader;
+import android.media.update.FrameLayoutHelper;
import android.media.update.VideoView2Provider;
-import android.media.update.ViewProvider;
import android.net.Uri;
import android.os.Bundle;
import android.util.AttributeSet;
-import android.view.MotionEvent;
import android.view.View;
import java.lang.annotation.Retention;
@@ -102,7 +101,7 @@
*
* @hide
*/
-public class VideoView2 extends FrameLayout {
+public class VideoView2 extends FrameLayoutHelper<VideoView2Provider> {
/** @hide */
@IntDef({
VIEW_TYPE_TEXTUREVIEW,
@@ -125,8 +124,6 @@
*/
public static final int VIEW_TYPE_TEXTUREVIEW = 2;
- private final VideoView2Provider mProvider;
-
public VideoView2(@NonNull Context context) {
this(context, null);
}
@@ -142,17 +139,10 @@
public VideoView2(
@NonNull Context context, @Nullable AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
-
- mProvider = ApiLoader.getProvider(context).createVideoView2(this, new SuperProvider(),
- attrs, defStyleAttr, defStyleRes);
- }
-
- /**
- * @hide
- */
- public VideoView2Provider getProvider() {
- return mProvider;
+ super((instance, superProvider) ->
+ ApiLoader.getProvider(context).createVideoView2(
+ (VideoView2) instance, superProvider, attrs, defStyleAttr, defStyleRes),
+ context, attrs, defStyleAttr, defStyleRes);
}
/**
@@ -497,76 +487,4 @@
*/
void onCustomAction(String action, Bundle extras);
}
-
- @Override
- protected void onAttachedToWindow() {
- mProvider.onAttachedToWindow_impl();
- }
-
- @Override
- protected void onDetachedFromWindow() {
- mProvider.onDetachedFromWindow_impl();
- }
-
- @Override
- public CharSequence getAccessibilityClassName() {
- return mProvider.getAccessibilityClassName_impl();
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent ev) {
- return mProvider.onTouchEvent_impl(ev);
- }
-
- @Override
- public boolean onTrackballEvent(MotionEvent ev) {
- return mProvider.onTrackballEvent_impl(ev);
- }
-
- @Override
- public void onFinishInflate() {
- mProvider.onFinishInflate_impl();
- }
-
- @Override
- public void setEnabled(boolean enabled) {
- mProvider.setEnabled_impl(enabled);
- }
-
- private class SuperProvider implements ViewProvider {
- @Override
- public void onAttachedToWindow_impl() {
- VideoView2.super.onAttachedToWindow();
- }
-
- @Override
- public void onDetachedFromWindow_impl() {
- VideoView2.super.onDetachedFromWindow();
- }
-
- @Override
- public CharSequence getAccessibilityClassName_impl() {
- return VideoView2.super.getAccessibilityClassName();
- }
-
- @Override
- public boolean onTouchEvent_impl(MotionEvent ev) {
- return VideoView2.super.onTouchEvent(ev);
- }
-
- @Override
- public boolean onTrackballEvent_impl(MotionEvent ev) {
- return VideoView2.super.onTrackballEvent(ev);
- }
-
- @Override
- public void onFinishInflate_impl() {
- VideoView2.super.onFinishInflate();
- }
-
- @Override
- public void setEnabled_impl(boolean enabled) {
- VideoView2.super.setEnabled(enabled);
- }
- }
}
diff --git a/core/java/com/android/internal/app/ResolverListController.java b/core/java/com/android/internal/app/ResolverListController.java
index 1dfff5e..2ab2d20 100644
--- a/core/java/com/android/internal/app/ResolverListController.java
+++ b/core/java/com/android/internal/app/ResolverListController.java
@@ -103,14 +103,11 @@
List<ResolverActivity.ResolvedComponentInfo> resolvedComponents = null;
for (int i = 0, N = intents.size(); i < N; i++) {
final Intent intent = intents.get(i);
- int flags = PackageManager.MATCH_DEFAULT_ONLY
- | (shouldGetResolvedFilter ? PackageManager.GET_RESOLVED_FILTER : 0)
- | (shouldGetActivityMetadata ? PackageManager.GET_META_DATA : 0);
- if (intent.isBrowsableWebIntent()
- || (intent.getFlags() & Intent.FLAG_ACTIVITY_MATCH_EXTERNAL) != 0) {
- flags |= PackageManager.MATCH_INSTANT;
- }
- final List<ResolveInfo> infos = mpm.queryIntentActivities(intent, flags);
+ final List<ResolveInfo> infos = mpm.queryIntentActivities(intent,
+ PackageManager.MATCH_DEFAULT_ONLY
+ | (shouldGetResolvedFilter ? PackageManager.GET_RESOLVED_FILTER : 0)
+ | (shouldGetActivityMetadata ? PackageManager.GET_META_DATA : 0)
+ | PackageManager.MATCH_INSTANT);
// Remove any activities that are not exported.
int totalSize = infos.size();
for (int j = totalSize - 1; j >= 0 ; j--) {
diff --git a/core/java/com/android/internal/util/CollectionUtils.java b/core/java/com/android/internal/util/CollectionUtils.java
index 2f2c747..433d14f 100644
--- a/core/java/com/android/internal/util/CollectionUtils.java
+++ b/core/java/com/android/internal/util/CollectionUtils.java
@@ -138,14 +138,14 @@
public static @NonNull <I, O> List<O> mapNotNull(@Nullable List<I> cur,
Function<? super I, ? extends O> f) {
if (isEmpty(cur)) return Collections.emptyList();
- final ArrayList<O> result = new ArrayList<>();
+ List<O> result = null;
for (int i = 0; i < cur.size(); i++) {
O transformed = f.apply(cur.get(i));
if (transformed != null) {
- result.add(transformed);
+ result = add(result, transformed);
}
}
- return result;
+ return emptyIfNull(result);
}
/**
diff --git a/core/java/com/android/server/BootReceiver.java b/core/java/com/android/server/BootReceiver.java
index e3436e8..fb18669 100644
--- a/core/java/com/android/server/BootReceiver.java
+++ b/core/java/com/android/server/BootReceiver.java
@@ -35,6 +35,7 @@
import android.util.AtomicFile;
import android.util.EventLog;
import android.util.Slog;
+import android.util.StatsLog;
import android.util.Xml;
import com.android.internal.annotations.VisibleForTesting;
@@ -112,6 +113,8 @@
private static final String SHUTDOWN_METRICS_FILE = "/data/system/shutdown-metrics.txt";
private static final String SHUTDOWN_TRON_METRICS_PREFIX = "shutdown_";
+ private static final String METRIC_SYSTEM_SERVER = "shutdown_system_server";
+ private static final String METRIC_SHUTDOWN_TIME_START = "begin_shutdown";
@Override
public void onReceive(final Context context, Intent intent) {
@@ -401,6 +404,10 @@
}
}
if (!TextUtils.isEmpty(metricsStr)) {
+ String reboot = null;
+ String reason = null;
+ String start_time = null;
+ String duration = null;
String[] array = metricsStr.split(",");
for (String keyValueStr : array) {
String[] keyValue = keyValueStr.split(":");
@@ -411,8 +418,19 @@
// Ignore keys that are not indended for tron
if (keyValue[0].startsWith(SHUTDOWN_TRON_METRICS_PREFIX)) {
logTronShutdownMetric(keyValue[0], keyValue[1]);
+ if (keyValue[0].equals(METRIC_SYSTEM_SERVER)) {
+ duration = keyValue[1];
+ }
+ }
+ if (keyValue[0].equals("reboot")) {
+ reboot = keyValue[1];
+ } else if (keyValue[0].equals("reason")) {
+ reason = keyValue[1];
+ } else if (keyValue[0].equals(METRIC_SHUTDOWN_TIME_START)) {
+ start_time = keyValue[1];
}
}
+ logStatsdShutdownAtom(reboot, reason, start_time, duration);
}
metricsFile.delete();
}
@@ -430,6 +448,52 @@
}
}
+ private static void logStatsdShutdownAtom(
+ String rebootStr, String reasonStr, String startStr, String durationStr) {
+ boolean reboot = false;
+ String reason = "<EMPTY>";
+ long start = 0;
+ long duration = 0;
+
+ if (rebootStr != null) {
+ if (rebootStr.equals("y")) {
+ reboot = true;
+ } else if (!rebootStr.equals("n")) {
+ Slog.e(TAG, "Unexpected value for reboot : " + rebootStr);
+ }
+ } else {
+ Slog.e(TAG, "No value received for reboot");
+ }
+
+ if (reasonStr != null) {
+ reason = reasonStr;
+ } else {
+ Slog.e(TAG, "No value received for shutdown reason");
+ }
+
+ if (startStr != null) {
+ try {
+ start = Long.parseLong(startStr);
+ } catch (NumberFormatException e) {
+ Slog.e(TAG, "Cannot parse shutdown start time: " + startStr);
+ }
+ } else {
+ Slog.e(TAG, "No value received for shutdown start time");
+ }
+
+ if (durationStr != null) {
+ try {
+ duration = Long.parseLong(durationStr);
+ } catch (NumberFormatException e) {
+ Slog.e(TAG, "Cannot parse shutdown duration: " + startStr);
+ }
+ } else {
+ Slog.e(TAG, "No value received for shutdown duration");
+ }
+
+ StatsLog.write(StatsLog.SHUTDOWN_SEQUENCE_REPORTED, reboot, reason, start, duration);
+ }
+
private static void logFsShutdownTime() {
File f = null;
for (String fileName : LAST_KMSG_FILES) {
diff --git a/core/jni/android/graphics/ImageDecoder.cpp b/core/jni/android/graphics/ImageDecoder.cpp
index 743a7ef..e2ce1a4 100644
--- a/core/jni/android/graphics/ImageDecoder.cpp
+++ b/core/jni/android/graphics/ImageDecoder.cpp
@@ -74,7 +74,8 @@
// FIXME: Avoid parsing the whole image?
const bool animated = codec->getFrameCount() > 1;
- decoder->mCodec = SkAndroidCodec::MakeFromCodec(std::move(codec));
+ decoder->mCodec = SkAndroidCodec::MakeFromCodec(std::move(codec),
+ SkAndroidCodec::ExifOrientationBehavior::kRespect);
if (!decoder->mCodec.get()) {
doThrowIOE(env, "Could not create AndroidCodec");
return nullptr;
diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp
index 13e6fcd..32945bf 100644
--- a/core/jni/com_android_internal_os_Zygote.cpp
+++ b/core/jni/com_android_internal_os_Zygote.cpp
@@ -141,32 +141,45 @@
errno = saved_errno;
}
-// Configures the SIGCHLD handler for the zygote process. This is configured
-// very late, because earlier in the runtime we may fork() and exec()
-// other processes, and we want to waitpid() for those rather than
+// Configures the SIGCHLD/SIGHUP handlers for the zygote process. This is
+// configured very late, because earlier in the runtime we may fork() and
+// exec() other processes, and we want to waitpid() for those rather than
// have them be harvested immediately.
//
+// Ignore SIGHUP because all processes forked by the zygote are in the same
+// process group as the zygote and we don't want to be notified if we become
+// an orphaned group and have one or more stopped processes. This is not a
+// theoretical concern :
+// - we can become an orphaned group if one of our direct descendants forks
+// and is subsequently killed before its children.
+// - crash_dump routinely STOPs the process it's tracing.
+//
+// See issues b/71965619 and b/25567761 for further details.
+//
// This ends up being called repeatedly before each fork(), but there's
// no real harm in that.
-static void SetSigChldHandler() {
- struct sigaction sa;
- memset(&sa, 0, sizeof(sa));
- sa.sa_handler = SigChldHandler;
+static void SetSignalHandlers() {
+ struct sigaction sig_chld = {};
+ sig_chld.sa_handler = SigChldHandler;
- int err = sigaction(SIGCHLD, &sa, NULL);
- if (err < 0) {
+ if (sigaction(SIGCHLD, &sig_chld, NULL) < 0) {
ALOGW("Error setting SIGCHLD handler: %s", strerror(errno));
}
+
+ struct sigaction sig_hup = {};
+ sig_hup.sa_handler = SIG_IGN;
+ if (sigaction(SIGHUP, &sig_hup, NULL) < 0) {
+ ALOGW("Error setting SIGHUP handler: %s", strerror(errno));
+ }
}
// Sets the SIGCHLD handler back to default behavior in zygote children.
-static void UnsetSigChldHandler() {
+static void UnsetChldSignalHandler() {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = SIG_DFL;
- int err = sigaction(SIGCHLD, &sa, NULL);
- if (err < 0) {
+ if (sigaction(SIGCHLD, &sa, NULL) < 0) {
ALOGW("Error unsetting SIGCHLD handler: %s", strerror(errno));
}
}
@@ -505,7 +518,7 @@
bool is_system_server, jintArray fdsToClose,
jintArray fdsToIgnore,
jstring instructionSet, jstring dataDir) {
- SetSigChldHandler();
+ SetSignalHandlers();
sigset_t sigchld;
sigemptyset(&sigchld);
@@ -682,7 +695,8 @@
delete se_info;
delete se_name;
- UnsetSigChldHandler();
+ // Unset the SIGCHLD handler, but keep ignoring SIGHUP (rationale in SetSignalHandlers).
+ UnsetChldSignalHandler();
env->CallStaticVoidMethod(gZygoteClass, gCallPostForkChildHooks, runtime_flags,
is_system_server, instructionSet);
diff --git a/core/proto/android/app/window_configuration.proto b/core/proto/android/app/window_configuration.proto
index 4d748e8..1e8ace4 100644
--- a/core/proto/android/app/window_configuration.proto
+++ b/core/proto/android/app/window_configuration.proto
@@ -21,9 +21,12 @@
package android.app;
import "frameworks/base/core/proto/android/graphics/rect.proto";
+import "frameworks/base/libs/incident/proto/android/privacy.proto";
/** Proto representation for WindowConfiguration.java class. */
message WindowConfigurationProto {
+ option (android.msg_privacy).dest = DEST_AUTOMATIC;
+
optional .android.graphics.RectProto app_bounds = 1;
optional int32 windowing_mode = 2;
optional int32 activity_type = 3;
diff --git a/core/proto/android/media/audioattributes.proto b/core/proto/android/media/audioattributes.proto
index 860d608..ef04720 100644
--- a/core/proto/android/media/audioattributes.proto
+++ b/core/proto/android/media/audioattributes.proto
@@ -20,15 +20,19 @@
package android.media;
+import "frameworks/base/libs/incident/proto/android/privacy.proto";
+
/**
* An android.media.AudioAttributes object.
*/
message AudioAttributesProto {
+ option (android.msg_privacy).dest = DEST_AUTOMATIC;
+
optional Usage usage = 1;
optional ContentType content_type = 2;
// Bit representation of set flags.
optional int32 flags = 3;
- repeated string tags = 4;
+ repeated string tags = 4 [ (android.privacy).dest = DEST_EXPLICIT ];
}
enum ContentType {
diff --git a/core/proto/android/os/incident.proto b/core/proto/android/os/incident.proto
index 3ec6f05..8a04bf7 100644
--- a/core/proto/android/os/incident.proto
+++ b/core/proto/android/os/incident.proto
@@ -16,7 +16,6 @@
syntax = "proto2";
option java_multiple_files = true;
-option java_outer_classname = "IncidentProtoMetadata";
import "frameworks/base/core/proto/android/os/batterytype.proto";
import "frameworks/base/core/proto/android/os/cpufreq.proto";
diff --git a/core/proto/android/server/forceappstandbytracker.proto b/core/proto/android/server/forceappstandbytracker.proto
index d0fd0b4..43c869c 100644
--- a/core/proto/android/server/forceappstandbytracker.proto
+++ b/core/proto/android/server/forceappstandbytracker.proto
@@ -17,13 +17,12 @@
syntax = "proto2";
import "frameworks/base/core/proto/android/server/statlogger.proto";
+import "frameworks/base/libs/incident/proto/android/privacy.proto";
package com.android.server;
option java_multiple_files = true;
-import "frameworks/base/libs/incident/proto/android/privacy.proto";
-
// Dump from com.android.server.ForceAppStandbyTracker.
message ForceAppStandbyTrackerProto {
option (.android.msg_privacy).dest = DEST_AUTOMATIC;
@@ -62,6 +61,8 @@
optional StatLoggerProto stats = 9;
message ExemptedPackage {
+ option (.android.msg_privacy).dest = DEST_AUTOMATIC;
+
optional int32 userId = 1;
optional string package_name = 2;
}
diff --git a/core/proto/android/server/intentresolver.proto b/core/proto/android/server/intentresolver.proto
index 60c060c..0ada895 100644
--- a/core/proto/android/server/intentresolver.proto
+++ b/core/proto/android/server/intentresolver.proto
@@ -19,9 +19,15 @@
package com.android.server;
+import "frameworks/base/libs/incident/proto/android/privacy.proto";
+
message IntentResolverProto {
+ option (.android.msg_privacy).dest = DEST_AUTOMATIC;
+
message ArrayMapEntry {
+ option (.android.msg_privacy).dest = DEST_EXPLICIT;
+
optional string key = 1;
repeated string values = 2;
}
diff --git a/core/proto/android/server/statlogger.proto b/core/proto/android/server/statlogger.proto
index fa430d8..2ae526a 100644
--- a/core/proto/android/server/statlogger.proto
+++ b/core/proto/android/server/statlogger.proto
@@ -20,8 +20,12 @@
option java_multiple_files = true;
+import "frameworks/base/libs/incident/proto/android/privacy.proto";
+
// Dump from StatLogger.
message StatLoggerProto {
+ option (.android.msg_privacy).dest = DEST_EXPLICIT;
+
message Event {
optional int32 eventId = 1;
optional string label = 2;
diff --git a/core/proto/android/server/wirelesschargerdetector.proto b/core/proto/android/server/wirelesschargerdetector.proto
index 89cf2f8..2118deb 100644
--- a/core/proto/android/server/wirelesschargerdetector.proto
+++ b/core/proto/android/server/wirelesschargerdetector.proto
@@ -19,8 +19,14 @@
option java_multiple_files = true;
+import "frameworks/base/libs/incident/proto/android/privacy.proto";
+
message WirelessChargerDetectorProto {
+ option (.android.msg_privacy).dest = DEST_AUTOMATIC;
+
message VectorProto {
+ option (.android.msg_privacy).dest = DEST_AUTOMATIC;
+
optional float x = 1;
optional float y = 2;
optional float z = 3;
diff --git a/core/proto/android/service/notification.proto b/core/proto/android/service/notification.proto
index 9013a23..5c40e5f 100644
--- a/core/proto/android/service/notification.proto
+++ b/core/proto/android/service/notification.proto
@@ -50,7 +50,7 @@
message NotificationRecordProto {
option (.android.msg_privacy).dest = DEST_AUTOMATIC;
- optional string key = 1 [ (.android.privacy).dest = DEST_EXPLICIT ];
+ optional string key = 1;
enum State {
ENQUEUED = 0;
@@ -88,7 +88,7 @@
message ManagedServicesProto {
option (android.msg_privacy).dest = DEST_AUTOMATIC;
- optional string caption = 1 [ (.android.privacy).dest = DEST_EXPLICIT ];
+ optional string caption = 1;
message ServiceProto {
option (android.msg_privacy).dest = DEST_AUTOMATIC;
diff --git a/core/proto/android/util/event_log_tags.proto b/core/proto/android/util/event_log_tags.proto
index cb039be..457219f 100644
--- a/core/proto/android/util/event_log_tags.proto
+++ b/core/proto/android/util/event_log_tags.proto
@@ -19,17 +19,25 @@
option java_multiple_files = true;
+import "frameworks/base/libs/incident/proto/android/privacy.proto";
+
// Proto representation of event.logtags.
// Usually sit in /system/etc/event-log-tags.
message EventLogTagMapProto {
+ option (.android.msg_privacy).dest = DEST_AUTOMATIC;
+
repeated EventLogTag event_log_tags = 1;
}
message EventLogTag {
+ option (.android.msg_privacy).dest = DEST_AUTOMATIC;
+
optional uint32 tag_number = 1; // keyed by tag number.
optional string tag_name = 2;
message ValueDescriptor {
+ option (.android.msg_privacy).dest = DEST_AUTOMATIC;
+
optional string name = 1;
enum DataType {
@@ -55,4 +63,4 @@
optional DataUnit unit = 3;
}
repeated ValueDescriptor value_descriptors = 3;
-}
\ No newline at end of file
+}
diff --git a/core/proto/android/view/displaycutout.proto b/core/proto/android/view/displaycutout.proto
index ff13fab..ee258b7 100644
--- a/core/proto/android/view/displaycutout.proto
+++ b/core/proto/android/view/displaycutout.proto
@@ -17,11 +17,14 @@
syntax = "proto2";
import "frameworks/base/core/proto/android/graphics/rect.proto";
+import "frameworks/base/libs/incident/proto/android/privacy.proto";
package android.view;
option java_multiple_files = true;
message DisplayCutoutProto {
+ option (.android.msg_privacy).dest = DEST_AUTOMATIC;
+
optional .android.graphics.RectProto insets = 1;
optional .android.graphics.RectProto bounds = 2;
}
diff --git a/core/proto/android/view/surfacecontrol.proto b/core/proto/android/view/surfacecontrol.proto
index 9288b4f..665d688 100644
--- a/core/proto/android/view/surfacecontrol.proto
+++ b/core/proto/android/view/surfacecontrol.proto
@@ -19,10 +19,14 @@
option java_multiple_files = true;
+import "frameworks/base/libs/incident/proto/android/privacy.proto";
+
/**
* Represents a {@link android.view.SurfaceControl} object.
*/
message SurfaceControlProto {
+ option (android.msg_privacy).dest = DEST_AUTOMATIC;
+
optional int32 hash_code = 1;
- optional string name = 2;
+ optional string name = 2 [ (android.privacy).dest = DEST_EXPLICIT ];
}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index a0ba3ad..e5ba6d7 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2723,6 +2723,15 @@
<permission android:name="android.permission.BIND_AUTOFILL_SERVICE"
android:protectionLevel="signature" />
+ <!-- Alternative version of android.permission.BIND_AUTOFILL_FIELD_CLASSIFICATION_SERVICE.
+ This permission was renamed during the O previews but it was supported on the final O
+ release, so we need to carry it over.
+ <p>Protection level: signature
+ @hide
+ -->
+ <permission android:name="android.permission.BIND_AUTOFILL"
+ android:protectionLevel="signature" />
+
<!-- Must be required by an {@link android.service.autofill.AutofillFieldClassificationService}
to ensure that only the system can bind to it.
@hide This is not a third-party API (intended for OEMs and system apps).
@@ -2730,6 +2739,14 @@
<permission android:name="android.permission.BIND_AUTOFILL_FIELD_CLASSIFICATION_SERVICE"
android:protectionLevel="signature" />
+ <!-- Must be required by a android.service.textclassifier.TextClassifierService,
+ to ensure that only the system can bind to it.
+ @SystemApi @hide This is not a third-party API (intended for OEMs and system apps).
+ <p>Protection level: signature
+ -->
+ <permission android:name="android.permission.BIND_TEXTCLASSIFIER_SERVICE"
+ android:protectionLevel="signature" />
+
<!-- Must be required by hotword enrollment application,
to ensure that only the system can interact with it.
@hide <p>Not for use by third-party applications.</p> -->
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index a22ca87..607414d 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2477,9 +2477,9 @@
<item>restart</item>
<item>screenshot</item>
<item>logout</item>
+ <item>lockdown</item>
<item>bugreport</item>
<item>users</item>
- <item>lockdown</item>
</string-array>
<!-- Number of milliseconds to hold a wake lock to ensure that drawing is fully
@@ -3123,6 +3123,15 @@
-->
<string name="config_defaultAutofillService" translatable="false"></string>
+ <!-- The component name, flattened to a string, for the default system textclassifier service.
+ This service must be trusted, as it can be activated without explicit consent of the user.
+ (e.g. com.android.textclassifier/.TextClassifierServiceImpl).
+ If no textclassifier service with the specified name exists on the device (or if this is
+ set to empty string), a default textclassifier will be loaded in the calling app's process.
+ See android.view.textclassifier.TextClassificationManager.
+ -->
+ <string name="config_defaultTextClassifierService" translatable="false"></string>
+
<!-- Whether the device uses the default focus highlight when focus state isn't specified. -->
<bool name="config_useDefaultFocusHighlight">true</bool>
@@ -3266,4 +3275,6 @@
<string name="config_defaultAssistantAccessPackage" translatable="false">android.ext.services</string>
<bool name="config_supportBluetoothPersistedState">true</bool>
+
+ <bool name="config_keepRestrictedProfilesInBackground">true</bool>
</resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 09d3121..a62d49e 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3101,6 +3101,7 @@
<java-symbol type="string" name="notification_channel_usb" />
<java-symbol type="string" name="notification_channel_heavy_weight_app" />
<java-symbol type="string" name="config_defaultAutofillService" />
+ <java-symbol type="string" name="config_defaultTextClassifierService" />
<java-symbol type="string" name="notification_channel_foreground_service" />
<java-symbol type="string" name="foreground_service_app_in_background" />
@@ -3240,4 +3241,6 @@
<java-symbol type="string" name="slices_permission_request" />
<java-symbol type="string" name="screenshot_edit" />
+
+ <java-symbol type="bool" name="config_keepRestrictedProfilesInBackground" />
</resources>
diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
index 733f7a1..0083b01 100644
--- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java
+++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
@@ -245,6 +245,7 @@
Settings.Global.INTENT_FIREWALL_UPDATE_CONTENT_URL,
Settings.Global.INTENT_FIREWALL_UPDATE_METADATA_URL,
Settings.Global.JOB_SCHEDULER_CONSTANTS,
+ Settings.Global.KEEP_PROFILE_IN_BACKGROUND,
Settings.Global.LANG_ID_UPDATE_CONTENT_URL,
Settings.Global.LANG_ID_UPDATE_METADATA_URL,
Settings.Global.LOCATION_BACKGROUND_THROTTLE_INTERVAL_MS,
diff --git a/core/tests/coretests/src/android/view/textclassifier/TextClassificationTest.java b/core/tests/coretests/src/android/view/textclassifier/TextClassificationTest.java
index 8a81743..cf41eb8 100644
--- a/core/tests/coretests/src/android/view/textclassifier/TextClassificationTest.java
+++ b/core/tests/coretests/src/android/view/textclassifier/TextClassificationTest.java
@@ -86,15 +86,11 @@
.setSignature(signature)
.build();
- // Parcel and unparcel using ParcelableWrapper.
- final TextClassification.ParcelableWrapper parcelableReference = new TextClassification
- .ParcelableWrapper(reference);
+ // Parcel and unparcel
final Parcel parcel = Parcel.obtain();
- parcelableReference.writeToParcel(parcel, parcelableReference.describeContents());
+ reference.writeToParcel(parcel, reference.describeContents());
parcel.setDataPosition(0);
- final TextClassification result =
- TextClassification.ParcelableWrapper.CREATOR.createFromParcel(
- parcel).getTextClassification();
+ final TextClassification result = TextClassification.CREATOR.createFromParcel(parcel);
assertEquals(text, result.getText());
assertEquals(signature, result.getSignature());
diff --git a/core/tests/coretests/src/android/view/textclassifier/TextLinksTest.java b/core/tests/coretests/src/android/view/textclassifier/TextLinksTest.java
index a82542c..d6ac845 100644
--- a/core/tests/coretests/src/android/view/textclassifier/TextLinksTest.java
+++ b/core/tests/coretests/src/android/view/textclassifier/TextLinksTest.java
@@ -68,8 +68,8 @@
public void testParcel() {
final String fullText = "this is just a test";
final TextLinks reference = new TextLinks.Builder(fullText)
- .addLink(new TextLinks.TextLink(fullText, 0, 4, getEntityScores(0.f, 0.f, 1.f)))
- .addLink(new TextLinks.TextLink(fullText, 5, 12, getEntityScores(.8f, .1f, .5f)))
+ .addLink(0, 4, getEntityScores(0.f, 0.f, 1.f))
+ .addLink(5, 12, getEntityScores(.8f, .1f, .5f))
.build();
// Parcel and unparcel.
diff --git a/core/tests/coretests/src/android/view/textclassifier/TextSelectionTest.java b/core/tests/coretests/src/android/view/textclassifier/TextSelectionTest.java
index e920236..a6ea021 100644
--- a/core/tests/coretests/src/android/view/textclassifier/TextSelectionTest.java
+++ b/core/tests/coretests/src/android/view/textclassifier/TextSelectionTest.java
@@ -45,15 +45,11 @@
.setSignature(signature)
.build();
- // Parcel and unparcel using ParcelableWrapper.
- final TextSelection.ParcelableWrapper parcelableReference = new TextSelection
- .ParcelableWrapper(reference);
+ // Parcel and unparcel
final Parcel parcel = Parcel.obtain();
- parcelableReference.writeToParcel(parcel, parcelableReference.describeContents());
+ reference.writeToParcel(parcel, reference.describeContents());
parcel.setDataPosition(0);
- final TextSelection result =
- TextSelection.ParcelableWrapper.CREATOR.createFromParcel(
- parcel).getTextSelection();
+ final TextSelection result = TextSelection.CREATOR.createFromParcel(parcel);
assertEquals(startIndex, result.getSelectionStartIndex());
assertEquals(endIndex, result.getSelectionEndIndex());
diff --git a/core/tests/coretests/src/android/widget/TextViewActivityTest.java b/core/tests/coretests/src/android/widget/TextViewActivityTest.java
index bbca12f..69e5670 100644
--- a/core/tests/coretests/src/android/widget/TextViewActivityTest.java
+++ b/core/tests/coretests/src/android/widget/TextViewActivityTest.java
@@ -325,9 +325,9 @@
TextClassificationManager textClassificationManager =
mActivity.getSystemService(TextClassificationManager.class);
TextClassifier textClassifier = textClassificationManager.getTextClassifier();
- SpannableString content = new SpannableString("Call me at +19148277737");
+ Spannable content = new SpannableString("Call me at +19148277737");
TextLinks links = textClassifier.generateLinks(content);
- links.apply(content, null);
+ links.apply(content, TextLinks.APPLY_STRATEGY_REPLACE, null);
mActivityRule.runOnUiThread(() -> {
textView.setText(content);
diff --git a/core/tests/hosttests/test-apps/MultiDexLegacyTestServices/Android.mk b/core/tests/hosttests/test-apps/MultiDexLegacyTestServices/Android.mk
index 99bcd6c..a6c5373 100644
--- a/core/tests/hosttests/test-apps/MultiDexLegacyTestServices/Android.mk
+++ b/core/tests/hosttests/test-apps/MultiDexLegacyTestServices/Android.mk
@@ -36,10 +36,8 @@
include $(BUILD_PACKAGE)
-ifndef LOCAL_JACK_ENABLED
$(mainDexList): $(full_classes_proguard_jar) | $(MAINDEXCLASSES)
$(hide) mkdir -p $(dir $@)
$(MAINDEXCLASSES) $< 1>$@
$(built_dex_intermediate): $(mainDexList)
-endif
diff --git a/core/tests/hosttests/test-apps/MultiDexLegacyTestServices/AndroidManifest.xml b/core/tests/hosttests/test-apps/MultiDexLegacyTestServices/AndroidManifest.xml
index e3068920..7cd01e54 100644
--- a/core/tests/hosttests/test-apps/MultiDexLegacyTestServices/AndroidManifest.xml
+++ b/core/tests/hosttests/test-apps/MultiDexLegacyTestServices/AndroidManifest.xml
@@ -7,6 +7,8 @@
<uses-sdk
android:minSdkVersion="9"
android:targetSdkVersion="19" />
+ <!-- Required for com.android.framework.multidexlegacytestservices.test2 -->
+ <uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES"/>
<application
android:label="MultiDexLegacyTestServices">
@@ -124,6 +126,6 @@
<action android:name="com.android.framework.multidexlegacytestservices.action.Service19" />
</intent-filter>
</service>
- </application>
+ </application>
</manifest>
diff --git a/core/tests/hosttests/test-apps/MultiDexLegacyTestServices/src/com/android/framework/multidexlegacytestservices/AbstractService.java b/core/tests/hosttests/test-apps/MultiDexLegacyTestServices/src/com/android/framework/multidexlegacytestservices/AbstractService.java
index 7b83999..cb0a591 100644
--- a/core/tests/hosttests/test-apps/MultiDexLegacyTestServices/src/com/android/framework/multidexlegacytestservices/AbstractService.java
+++ b/core/tests/hosttests/test-apps/MultiDexLegacyTestServices/src/com/android/framework/multidexlegacytestservices/AbstractService.java
@@ -60,35 +60,40 @@
// of the result file will be too big.
RandomAccessFile raf = new RandomAccessFile(resultFile, "rw");
raf.seek(raf.length());
- Log.i(TAG, "Writing 0x42434445 at " + raf.length() + " in " + resultFile.getPath());
- raf.writeInt(0x42434445);
+ if (raf.length() == 0) {
+ Log.i(TAG, "Writing 0x42434445 at " + raf.length() + " in " + resultFile.getPath());
+ raf.writeInt(0x42434445);
+ } else {
+ Log.w(TAG, "Service was restarted appending 0x42434445 twice at " + raf.length()
+ + " in " + resultFile.getPath());
+ raf.writeInt(0x42434445);
+ raf.writeInt(0x42434445);
+ }
raf.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- MultiDex.install(applicationContext);
- Log.i(TAG, "Multi dex installation done.");
+ MultiDex.install(applicationContext);
+ Log.i(TAG, "Multi dex installation done.");
- int value = getValue();
- Log.i(TAG, "Saving the result (" + value + ") to " + resultFile.getPath());
- try {
+ int value = getValue();
+ Log.i(TAG, "Saving the result (" + value + ") to " + resultFile.getPath());
// Append the check value in result file, keeping the constant values already written.
- RandomAccessFile raf = new RandomAccessFile(resultFile, "rw");
+ raf = new RandomAccessFile(resultFile, "rw");
raf.seek(raf.length());
Log.i(TAG, "Writing result at " + raf.length() + " in " + resultFile.getPath());
raf.writeInt(value);
raf.close();
} catch (IOException e) {
- e.printStackTrace();
- }
- try {
- // Writing end of processing flags, the existence of the file is the criteria
- RandomAccessFile raf = new RandomAccessFile(new File(applicationContext.getFilesDir(), getId() + ".complete"), "rw");
- Log.i(TAG, "creating complete file " + resultFile.getPath());
- raf.writeInt(0x32333435);
- raf.close();
- } catch (IOException e) {
- e.printStackTrace();
+ throw new AssertionError(e);
+ } finally {
+ try {
+ // Writing end of processing flags, the existence of the file is the criteria
+ RandomAccessFile raf = new RandomAccessFile(
+ new File(applicationContext.getFilesDir(), getId() + ".complete"), "rw");
+ Log.i(TAG, "creating complete file " + resultFile.getPath());
+ raf.writeInt(0x32333435);
+ raf.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
}
}
@@ -119,9 +124,10 @@
intermediate = ReflectIntermediateClass.get(45, 80, 20 /* 5 seems enough on a nakasi,
using 20 to get some margin */);
} catch (Exception e) {
- e.printStackTrace();
+ throw new AssertionError(e);
}
- int value = new com.android.framework.multidexlegacytestservices.manymethods.Big001().get1() +
+ int value =
+ new com.android.framework.multidexlegacytestservices.manymethods.Big001().get1() +
new com.android.framework.multidexlegacytestservices.manymethods.Big002().get2() +
new com.android.framework.multidexlegacytestservices.manymethods.Big003().get3() +
new com.android.framework.multidexlegacytestservices.manymethods.Big004().get4() +
diff --git a/core/tests/hosttests/test-apps/MultiDexLegacyTestServicesTests2/Android.mk b/core/tests/hosttests/test-apps/MultiDexLegacyTestServicesTests2/Android.mk
new file mode 100644
index 0000000..f3d98a8
--- /dev/null
+++ b/core/tests/hosttests/test-apps/MultiDexLegacyTestServicesTests2/Android.mk
@@ -0,0 +1,33 @@
+# Copyright (C) 2014 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := MultiDexLegacyTestServicesTests2
+
+LOCAL_JAVA_LIBRARIES := android-support-multidex
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-test
+
+LOCAL_SDK_VERSION := 9
+
+LOCAL_DEX_PREOPT := false
+
+include $(BUILD_PACKAGE)
+
diff --git a/core/tests/hosttests/test-apps/MultiDexLegacyTestServicesTests2/AndroidManifest.xml b/core/tests/hosttests/test-apps/MultiDexLegacyTestServicesTests2/AndroidManifest.xml
new file mode 100644
index 0000000..0ab2959
--- /dev/null
+++ b/core/tests/hosttests/test-apps/MultiDexLegacyTestServicesTests2/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.framework.multidexlegacytestservices.test2"
+ android:versionCode="1"
+ android:versionName="1.0" >
+
+ <uses-sdk android:minSdkVersion="9" />
+ <uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES"/>
+ <instrumentation
+ android:name="android.support.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.framework.multidexlegacytestservices" />
+
+ <application
+ android:label="multidexlegacytestservices.test2" >
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+</manifest>
\ No newline at end of file
diff --git a/core/tests/hosttests/test-apps/MultiDexLegacyTestServicesTests2/src/com/android/framework/multidexlegacytestservices/test2/ServicesTests.java b/core/tests/hosttests/test-apps/MultiDexLegacyTestServicesTests2/src/com/android/framework/multidexlegacytestservices/test2/ServicesTests.java
new file mode 100644
index 0000000..900f203
--- /dev/null
+++ b/core/tests/hosttests/test-apps/MultiDexLegacyTestServicesTests2/src/com/android/framework/multidexlegacytestservices/test2/ServicesTests.java
@@ -0,0 +1,381 @@
+/*
+ * Copyright (C) 2018 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.framework.multidexlegacytestservices.test2;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Log;
+import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.concurrent.TimeoutException;
+import junit.framework.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Run the tests with: <code>adb shell am instrument -w
+ * com.android.framework.multidexlegacytestservices.test2/android.support.test.runner.AndroidJUnitRunner
+ * </code>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ServicesTests {
+ private static final String TAG = "ServicesTests";
+
+ static {
+ Log.i(TAG, "Initializing");
+ }
+
+ private class ExtensionFilter implements FileFilter {
+ private final String ext;
+
+ public ExtensionFilter(String ext) {
+ this.ext = ext;
+ }
+
+ @Override
+ public boolean accept(File file) {
+ return file.getName().endsWith(ext);
+ }
+ }
+
+ private class ExtractedZipFilter extends ExtensionFilter {
+ public ExtractedZipFilter() {
+ super(".zip");
+ }
+
+ @Override
+ public boolean accept(File file) {
+ return super.accept(file) && !file.getName().startsWith("tmp-");
+ }
+ }
+
+ private static final int ENDHDR = 22;
+
+ private static final String SERVICE_BASE_ACTION =
+ "com.android.framework.multidexlegacytestservices.action.Service";
+ private static final int MIN_SERVICE = 1;
+ private static final int MAX_SERVICE = 19;
+ private static final String COMPLETION_SUCCESS = "Success";
+
+ private File targetFilesDir;
+
+ @Before
+ public void setup() throws Exception {
+ Log.i(TAG, "setup");
+ killServices();
+
+ File applicationDataDir =
+ new File(InstrumentationRegistry.getTargetContext().getApplicationInfo().dataDir);
+ clearDirContent(applicationDataDir);
+ targetFilesDir = InstrumentationRegistry.getTargetContext().getFilesDir();
+
+ Log.i(TAG, "setup done");
+ }
+
+ @Test
+ public void testStressConcurentLaunch() throws Exception {
+ startServices();
+ waitServicesCompletion();
+ String completionStatus = getServicesCompletionStatus();
+ if (completionStatus != COMPLETION_SUCCESS) {
+ Assert.fail(completionStatus);
+ }
+ }
+
+ @Test
+ public void testRecoverFromZipCorruption() throws Exception {
+ int serviceId = 1;
+ // Ensure extraction.
+ initServicesWorkFiles();
+ startService(serviceId);
+ waitServicesCompletion(serviceId);
+
+ // Corruption of the extracted zips.
+ tamperAllExtractedZips();
+
+ killServices();
+ checkRecover();
+ }
+
+ @Test
+ public void testRecoverFromDexCorruption() throws Exception {
+ int serviceId = 1;
+ // Ensure extraction.
+ initServicesWorkFiles();
+ startService(serviceId);
+ waitServicesCompletion(serviceId);
+
+ // Corruption of the odex files.
+ tamperAllOdex();
+
+ killServices();
+ checkRecover();
+ }
+
+ @Test
+ public void testRecoverFromZipCorruptionStressTest() throws Exception {
+ Thread startServices =
+ new Thread() {
+ @Override
+ public void run() {
+ startServices();
+ }
+ };
+
+ startServices.start();
+
+ // Start services lasts more than 80s, lets cause a few corruptions during this interval.
+ for (int i = 0; i < 80; i++) {
+ Thread.sleep(1000);
+ tamperAllExtractedZips();
+ }
+ startServices.join();
+ try {
+ waitServicesCompletion();
+ } catch (TimeoutException e) {
+ // Can happen.
+ }
+
+ killServices();
+ checkRecover();
+ }
+
+ @Test
+ public void testRecoverFromDexCorruptionStressTest() throws Exception {
+ Thread startServices =
+ new Thread() {
+ @Override
+ public void run() {
+ startServices();
+ }
+ };
+
+ startServices.start();
+
+ // Start services lasts more than 80s, lets cause a few corruptions during this interval.
+ for (int i = 0; i < 80; i++) {
+ Thread.sleep(1000);
+ tamperAllOdex();
+ }
+ startServices.join();
+ try {
+ waitServicesCompletion();
+ } catch (TimeoutException e) {
+ // Will probably happen most of the time considering what we're doing...
+ }
+
+ killServices();
+ checkRecover();
+ }
+
+ private static void clearDirContent(File dir) {
+ for (File subElement : dir.listFiles()) {
+ if (subElement.isDirectory()) {
+ clearDirContent(subElement);
+ }
+ if (!subElement.delete()) {
+ throw new AssertionError("Failed to clear '" + subElement.getAbsolutePath() + "'");
+ }
+ }
+ }
+
+ private void startServices() {
+ Log.i(TAG, "start services");
+ initServicesWorkFiles();
+ for (int i = MIN_SERVICE; i <= MAX_SERVICE; i++) {
+ startService(i);
+ try {
+ Thread.sleep((i - 1) * (1 << (i / 5)));
+ } catch (InterruptedException e) {
+ }
+ }
+ }
+
+ private void startService(int serviceId) {
+ Log.i(TAG, "start service " + serviceId);
+ InstrumentationRegistry.getContext().startService(new Intent(SERVICE_BASE_ACTION + serviceId));
+ }
+
+ private void initServicesWorkFiles() {
+ for (int i = MIN_SERVICE; i <= MAX_SERVICE; i++) {
+ File resultFile = new File(targetFilesDir, "Service" + i);
+ resultFile.delete();
+ Assert.assertFalse(
+ "Failed to delete result file '" + resultFile.getAbsolutePath() + "'.",
+ resultFile.exists());
+ File completeFile = new File(targetFilesDir, "Service" + i + ".complete");
+ completeFile.delete();
+ Assert.assertFalse(
+ "Failed to delete completion file '" + completeFile.getAbsolutePath() + "'.",
+ completeFile.exists());
+ }
+ }
+
+ private void waitServicesCompletion() throws TimeoutException {
+ Log.i(TAG, "start sleeping");
+ int attempt = 0;
+ int maxAttempt = 50; // 10 is enough for a nexus S
+ do {
+ try {
+ Thread.sleep(5000);
+ } catch (InterruptedException e) {
+ }
+ attempt++;
+ if (attempt >= maxAttempt) {
+ throw new TimeoutException();
+ }
+ } while (!areAllServicesCompleted());
+ }
+
+ private void waitServicesCompletion(int serviceId) throws TimeoutException {
+ Log.i(TAG, "start sleeping");
+ int attempt = 0;
+ int maxAttempt = 50; // 10 is enough for a nexus S
+ do {
+ try {
+ Thread.sleep(5000);
+ } catch (InterruptedException e) {
+ }
+ attempt++;
+ if (attempt >= maxAttempt) {
+ throw new TimeoutException();
+ }
+ } while (isServiceRunning(serviceId));
+ }
+
+ private String getServicesCompletionStatus() {
+ String status = COMPLETION_SUCCESS;
+ for (int i = MIN_SERVICE; i <= MAX_SERVICE; i++) {
+ File resultFile = new File(targetFilesDir, "Service" + i);
+ if (!resultFile.isFile()) {
+ status = "Service" + i + " never completed.";
+ break;
+ }
+ if (resultFile.length() != 8) {
+ status = "Service" + i + " was restarted.";
+ break;
+ }
+ }
+ Log.i(TAG, "Services completion status: " + status);
+ return status;
+ }
+
+ private String getServiceCompletionStatus(int serviceId) {
+ String status = COMPLETION_SUCCESS;
+ File resultFile = new File(targetFilesDir, "Service" + serviceId);
+ if (!resultFile.isFile()) {
+ status = "Service" + serviceId + " never completed.";
+ } else if (resultFile.length() != 8) {
+ status = "Service" + serviceId + " was restarted.";
+ }
+ Log.i(TAG, "Service " + serviceId + " completion status: " + status);
+ return status;
+ }
+
+ private boolean areAllServicesCompleted() {
+ for (int i = MIN_SERVICE; i <= MAX_SERVICE; i++) {
+ if (isServiceRunning(i)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean isServiceRunning(int i) {
+ File completeFile = new File(targetFilesDir, "Service" + i + ".complete");
+ return !completeFile.exists();
+ }
+
+ private File getSecondaryFolder() {
+ File dir =
+ new File(
+ new File(
+ InstrumentationRegistry.getTargetContext().getApplicationInfo().dataDir,
+ "code_cache"),
+ "secondary-dexes");
+ Assert.assertTrue(dir.getAbsolutePath(), dir.isDirectory());
+ return dir;
+ }
+
+ private void tamperAllExtractedZips() throws IOException {
+ // First attempt was to just overwrite zip entries but keep central directory, this was no
+ // trouble for Dalvik that was just ignoring those zip and using the odex files.
+ Log.i(TAG, "Tamper extracted zip files by overwriting all content by '\\0's.");
+ byte[] zeros = new byte[4 * 1024];
+ // Do not tamper tmp zip during their extraction.
+ for (File zip : getSecondaryFolder().listFiles(new ExtractedZipFilter())) {
+ long fileLength = zip.length();
+ Assert.assertTrue(fileLength > ENDHDR);
+ zip.setWritable(true);
+ RandomAccessFile raf = new RandomAccessFile(zip, "rw");
+ try {
+ int index = 0;
+ while (index < fileLength) {
+ int length = (int) Math.min(zeros.length, fileLength - index);
+ raf.write(zeros, 0, length);
+ index += length;
+ }
+ } finally {
+ raf.close();
+ }
+ }
+ }
+
+ private void tamperAllOdex() throws IOException {
+ Log.i(TAG, "Tamper odex files by overwriting some content by '\\0's.");
+ byte[] zeros = new byte[4 * 1024];
+ // I think max size would be 40 (u1[8] + 8 u4) but it's a test so lets take big margins.
+ int savedSizeForOdexHeader = 80;
+ for (File odex : getSecondaryFolder().listFiles(new ExtensionFilter(".dex"))) {
+ long fileLength = odex.length();
+ Assert.assertTrue(fileLength > zeros.length + savedSizeForOdexHeader);
+ odex.setWritable(true);
+ RandomAccessFile raf = new RandomAccessFile(odex, "rw");
+ try {
+ raf.seek(savedSizeForOdexHeader);
+ raf.write(zeros, 0, zeros.length);
+ } finally {
+ raf.close();
+ }
+ }
+ }
+
+ private void checkRecover() throws TimeoutException {
+ Log.i(TAG, "Check recover capability");
+ int serviceId = 1;
+ // Start one service and check it was able to run correctly even if a previous run failed.
+ initServicesWorkFiles();
+ startService(serviceId);
+ waitServicesCompletion(serviceId);
+ String completionStatus = getServiceCompletionStatus(serviceId);
+ if (completionStatus != COMPLETION_SUCCESS) {
+ Assert.fail(completionStatus);
+ }
+ }
+
+ private void killServices() {
+ ((ActivityManager)
+ InstrumentationRegistry.getContext().getSystemService(Context.ACTIVITY_SERVICE))
+ .killBackgroundProcesses("com.android.framework.multidexlegacytestservices");
+ }
+}
diff --git a/graphics/java/android/graphics/drawable/AnimatedImageDrawable.java b/graphics/java/android/graphics/drawable/AnimatedImageDrawable.java
index 4328109..bd49b87 100644
--- a/graphics/java/android/graphics/drawable/AnimatedImageDrawable.java
+++ b/graphics/java/android/graphics/drawable/AnimatedImageDrawable.java
@@ -319,7 +319,8 @@
/**
* Start the animation.
*
- * <p>Does nothing if the animation is already running.
+ * <p>Does nothing if the animation is already running. If the animation is stopped,
+ * this will reset it.</p>
*
* <p>If the animation starts, this will call
* {@link Animatable2.AnimationCallback#onAnimationStart}.</p>
diff --git a/libs/hwui/hwui/AnimatedImageDrawable.cpp b/libs/hwui/hwui/AnimatedImageDrawable.cpp
index 264b95e..5356d3b 100644
--- a/libs/hwui/hwui/AnimatedImageDrawable.cpp
+++ b/libs/hwui/hwui/AnimatedImageDrawable.cpp
@@ -41,6 +41,9 @@
return false;
}
+ // This will trigger a reset.
+ mFinished = true;
+
mRunning = true;
return true;
}
diff --git a/location/java/com/android/internal/location/gnssmetrics/GnssMetrics.java b/location/java/com/android/internal/location/gnssmetrics/GnssMetrics.java
index 603926f..98e67c2 100644
--- a/location/java/com/android/internal/location/gnssmetrics/GnssMetrics.java
+++ b/location/java/com/android/internal/location/gnssmetrics/GnssMetrics.java
@@ -17,7 +17,9 @@
package com.android.internal.location.gnssmetrics;
import android.os.SystemClock;
+import android.os.connectivity.GpsBatteryStats;
+import android.text.format.DateUtils;
import android.util.Base64;
import android.util.Log;
import android.util.TimeUtils;
@@ -26,6 +28,7 @@
import com.android.internal.app.IBatteryStats;
import com.android.internal.location.nano.GnssLogsProto.GnssLog;
+import com.android.internal.location.nano.GnssLogsProto.PowerMetrics;
/**
* GnssMetrics: Is used for logging GNSS metrics
@@ -171,6 +174,7 @@
msg.standardDeviationTopFourAverageCn0DbHz
= topFourAverageCn0Statistics.getStandardDeviation();
}
+ msg.powerMetrics = mGnssPowerMetrics.buildProto();
String s = Base64.encodeToString(GnssLog.toByteArray(msg), Base64.DEFAULT);
reset();
return s;
@@ -218,6 +222,21 @@
topFourAverageCn0Statistics.getStandardDeviation()).append("\n");
}
s.append("GNSS_KPI_END").append("\n");
+ GpsBatteryStats stats = mGnssPowerMetrics.getGpsBatteryStats();
+ if (stats != null) {
+ s.append("Power Metrics").append('\n');
+ long[] t = stats.getTimeInGpsSignalQualityLevel();
+ if (t != null && t.length == NUM_GPS_SIGNAL_QUALITY_LEVELS) {
+ s.append(" Amount of time (while on battery) Top 4 Avg CN0 > " +
+ Double.toString(GnssPowerMetrics.POOR_TOP_FOUR_AVG_CN0_THRESHOLD_DB_HZ) +
+ " dB-Hz (min): ").append(t[1] / ((double) DateUtils.MINUTE_IN_MILLIS)).append("\n");
+ s.append(" Amount of time (while on battery) Top 4 Avg CN0 <= " +
+ Double.toString(GnssPowerMetrics.POOR_TOP_FOUR_AVG_CN0_THRESHOLD_DB_HZ) +
+ " dB-Hz (min): ").append(t[0] / ((double) DateUtils.MINUTE_IN_MILLIS)).append("\n");
+ }
+ s.append(" Energy consumed while on battery (mAh): ").append(
+ stats.getEnergyConsumedMaMs() / ((double) DateUtils.HOUR_IN_MILLIS)).append("\n");
+ }
return s.toString();
}
@@ -294,7 +313,7 @@
private class GnssPowerMetrics {
/* Threshold for Top Four Average CN0 below which GNSS signal quality is declared poor */
- private static final double POOR_TOP_FOUR_AVG_CN0_THRESHOLD_DB_HZ = 20.0;
+ public static final double POOR_TOP_FOUR_AVG_CN0_THRESHOLD_DB_HZ = 20.0;
/* Minimum change in Top Four Average CN0 needed to trigger a report */
private static final double REPORTING_THRESHOLD_DB_HZ = 1.0;
@@ -313,6 +332,38 @@
}
/**
+ * Builds power metrics proto buf. This is included in the gnss proto buf.
+ * @return PowerMetrics
+ */
+ public PowerMetrics buildProto() {
+ PowerMetrics p = new PowerMetrics();
+ GpsBatteryStats stats = mGnssPowerMetrics.getGpsBatteryStats();
+ if (stats != null) {
+ p.loggingDurationMs = stats.getLoggingDurationMs();
+ p.energyConsumedMah = stats.getEnergyConsumedMaMs() / ((double) DateUtils.HOUR_IN_MILLIS);
+ long[] t = stats.getTimeInGpsSignalQualityLevel();
+ p.timeInSignalQualityLevelMs = new long[t.length];
+ for (int i = 0; i < t.length; i++) {
+ p.timeInSignalQualityLevelMs[i] = t[i];
+ }
+ }
+ return p;
+ }
+
+ /**
+ * Returns the GPS power stats
+ * @return GpsBatteryStats
+ */
+ public GpsBatteryStats getGpsBatteryStats() {
+ try {
+ return mBatteryStats.getGpsBatteryStats();
+ } catch (Exception e) {
+ Log.w(TAG, "Exception", e);
+ return null;
+ }
+ }
+
+ /**
* Reports signal quality to BatteryStats. Signal quality is based on Top four average CN0. If
* the number of SVs seen is less than 4, then signal quality is the average CN0.
* Changes are reported only if the average CN0 changes by more than REPORTING_THRESHOLD_DB_HZ.
@@ -347,4 +398,4 @@
return GnssMetrics.GPS_SIGNAL_QUALITY_POOR;
}
}
-}
\ No newline at end of file
+}
diff --git a/media/java/android/media/AudioDeviceInfo.java b/media/java/android/media/AudioDeviceInfo.java
index 41f9f09..3d879f5 100644
--- a/media/java/android/media/AudioDeviceInfo.java
+++ b/media/java/android/media/AudioDeviceInfo.java
@@ -22,6 +22,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
import java.util.TreeSet;
/**
@@ -176,6 +177,19 @@
}
}
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ AudioDeviceInfo that = (AudioDeviceInfo) o;
+ return Objects.equals(getPort(), that.getPort());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getPort());
+ }
+
private final AudioDevicePort mPort;
AudioDeviceInfo(AudioDevicePort port) {
diff --git a/media/java/android/media/MediaController2.java b/media/java/android/media/MediaController2.java
index 47dbde9..b32e539 100644
--- a/media/java/android/media/MediaController2.java
+++ b/media/java/android/media/MediaController2.java
@@ -30,6 +30,7 @@
import android.media.session.MediaSessionManager;
import android.media.update.ApiLoader;
import android.media.update.MediaController2Provider;
+import android.media.update.PlaybackInfoProvider;
import android.net.Uri;
import android.os.Bundle;
import android.os.ResultReceiver;
@@ -160,21 +161,14 @@
*/
public static final int PLAYBACK_TYPE_LOCAL = 1;
- private final int mVolumeType;
- private final int mVolumeControl;
- private final int mMaxVolume;
- private final int mCurrentVolume;
- private final AudioAttributes mAudioAttrs;
+ private final PlaybackInfoProvider mProvider;
/**
* @hide
*/
- public PlaybackInfo(int type, AudioAttributes attrs, int control, int max, int current) {
- mVolumeType = type;
- mAudioAttrs = attrs;
- mVolumeControl = control;
- mMaxVolume = max;
- mCurrentVolume = current;
+ @SystemApi
+ public PlaybackInfo(PlaybackInfoProvider provider) {
+ mProvider = provider;
}
/**
@@ -187,7 +181,7 @@
* @return The type of playback this session is using.
*/
public int getPlaybackType() {
- return mVolumeType;
+ return mProvider.getPlaybackType_impl();
}
/**
@@ -199,7 +193,7 @@
* @return The attributes for this session.
*/
public AudioAttributes getAudioAttributes() {
- return mAudioAttrs;
+ return mProvider.getAudioAttributes_impl();
}
/**
@@ -210,11 +204,10 @@
* <li>{@link VolumeProvider#VOLUME_CONTROL_FIXED}</li>
* </ul>
*
- * @return The type of volume control that may be used with this
- * session.
+ * @return The type of volume control that may be used with this session.
*/
- public int getVolumeControl() {
- return mVolumeControl;
+ public int getControlType() {
+ return mProvider.getControlType_impl();
}
/**
@@ -223,7 +216,7 @@
* @return The maximum allowed volume where this session is playing.
*/
public int getMaxVolume() {
- return mMaxVolume;
+ return mProvider.getMaxVolume_impl();
}
/**
@@ -232,7 +225,7 @@
* @return The current volume where this session is playing.
*/
public int getCurrentVolume() {
- return mCurrentVolume;
+ return mProvider.getCurrentVolume_impl();
}
}
@@ -277,6 +270,9 @@
mProvider.close_impl();
}
+ /**
+ * @hide
+ */
@SystemApi
public MediaController2Provider getProvider() {
return mProvider;
diff --git a/media/java/android/media/MediaItem2.java b/media/java/android/media/MediaItem2.java
index 2e9894b..eae4436 100644
--- a/media/java/android/media/MediaItem2.java
+++ b/media/java/android/media/MediaItem2.java
@@ -24,7 +24,6 @@
import android.media.update.ApiLoader;
import android.media.update.MediaItem2Provider;
import android.os.Bundle;
-import android.text.TextUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -69,7 +68,7 @@
public MediaItem2(@NonNull Context context, @NonNull String mediaId,
@NonNull DataSourceDesc dsd, @Nullable MediaMetadata2 metadata,
@Flags int flags) {
- mProvider = ApiLoader.getProvider(context).createMediaItem2Provider(
+ mProvider = ApiLoader.getProvider(context).createMediaItem2(
context, this, mediaId, dsd, metadata, flags);
}
diff --git a/media/java/android/media/MediaLibraryService2.java b/media/java/android/media/MediaLibraryService2.java
index 79b105f..a901c68 100644
--- a/media/java/android/media/MediaLibraryService2.java
+++ b/media/java/android/media/MediaLibraryService2.java
@@ -19,6 +19,7 @@
import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SystemApi;
import android.app.PendingIntent;
import android.content.Context;
import android.media.MediaSession2.BuilderBase;
@@ -63,26 +64,16 @@
/**
* Session for the media library service.
*/
- public class MediaLibrarySession extends MediaSession2 {
+ public static class MediaLibrarySession extends MediaSession2 {
private final MediaLibrarySessionProvider mProvider;
- MediaLibrarySession(Context context, MediaPlayerInterface player, String id,
- VolumeProvider volumeProvider, int ratingType, PendingIntent sessionActivity,
- Executor callbackExecutor, SessionCallback callback) {
- super(context, player, id, volumeProvider, ratingType, sessionActivity,
- callbackExecutor, callback);
- mProvider = (MediaLibrarySessionProvider) getProvider();
- }
-
- @Override
- MediaSession2Provider createProvider(Context context, MediaPlayerInterface player,
- String id, VolumeProvider volumeProvider, int ratingType,
- PendingIntent sessionActivity, Executor callbackExecutor,
- SessionCallback callback) {
- return ApiLoader.getProvider(context)
- .createMediaLibraryService2MediaLibrarySession(context, this, player, id,
- volumeProvider, ratingType, sessionActivity,
- callbackExecutor, (MediaLibrarySessionCallback) callback);
+ /**
+ * @hide
+ */
+ @SystemApi
+ public MediaLibrarySession(MediaLibrarySessionProvider provider) {
+ super(provider);
+ mProvider = provider;
}
/**
@@ -208,31 +199,15 @@
/**
* Builder for {@link MediaLibrarySession}.
*/
- // TODO(jaewan): Move this to updatable.
- public class MediaLibrarySessionBuilder
- extends BuilderBase<MediaLibrarySessionBuilder, MediaLibrarySessionCallback> {
+ public class MediaLibrarySessionBuilder extends BuilderBase<MediaLibrarySession,
+ MediaLibrarySessionBuilder, MediaLibrarySessionCallback> {
public MediaLibrarySessionBuilder(
@NonNull Context context, @NonNull MediaPlayerInterface player,
@NonNull @CallbackExecutor Executor callbackExecutor,
@NonNull MediaLibrarySessionCallback callback) {
- super(context, player);
- setSessionCallback(callbackExecutor, callback);
- }
-
- @Override
- public MediaLibrarySessionBuilder setSessionCallback(
- @NonNull @CallbackExecutor Executor callbackExecutor,
- @NonNull MediaLibrarySessionCallback callback) {
- if (callback == null) {
- throw new IllegalArgumentException("MediaLibrarySessionCallback cannot be null");
- }
- return super.setSessionCallback(callbackExecutor, callback);
- }
-
- @Override
- public MediaLibrarySession build() {
- return new MediaLibrarySession(mContext, mPlayer, mId, mVolumeProvider, mRatingType,
- mSessionActivity, mCallbackExecutor, mCallback);
+ super((instance) -> ApiLoader.getProvider(context).createMediaLibraryService2Builder(
+ context, (MediaLibrarySessionBuilder) instance, player, callbackExecutor,
+ callback));
}
}
diff --git a/media/java/android/media/MediaMetadata2.java b/media/java/android/media/MediaMetadata2.java
index fcdb4f7..54a9057 100644
--- a/media/java/android/media/MediaMetadata2.java
+++ b/media/java/android/media/MediaMetadata2.java
@@ -16,17 +16,16 @@
package android.media;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.StringDef;
+import android.annotation.SystemApi;
+import android.content.Context;
import android.graphics.Bitmap;
+import android.media.update.ApiLoader;
+import android.media.update.MediaMetadata2Provider;
import android.net.Uri;
-import android.os.Build;
import android.os.Bundle;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.text.TextUtils;
-import android.util.Log;
-import android.util.ArrayMap;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -37,9 +36,11 @@
*
* @hide
*/
-// TODO(jaewan): Move this to updatable
public final class MediaMetadata2 {
- private static final String TAG = "MediaMetadata2";
+ // New version of MediaMetadata that no longer implements Parcelable but added from/toBundle()
+ // for updatable.
+ // MediaDescription is deprecated because it was insufficient for controller to display media
+ // contents. Added getExtra() here to support all the features from the MediaDescription.
/**
* The title of the media.
@@ -365,76 +366,14 @@
@Retention(RetentionPolicy.SOURCE)
public @interface RatingKey {}
- static final int METADATA_TYPE_LONG = 0;
- static final int METADATA_TYPE_TEXT = 1;
- static final int METADATA_TYPE_BITMAP = 2;
- static final int METADATA_TYPE_RATING = 3;
- static final ArrayMap<String, Integer> METADATA_KEYS_TYPE;
-
- static {
- METADATA_KEYS_TYPE = new ArrayMap<String, Integer>();
- METADATA_KEYS_TYPE.put(METADATA_KEY_TITLE, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_ARTIST, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_DURATION, METADATA_TYPE_LONG);
- METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_AUTHOR, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_WRITER, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_COMPOSER, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_COMPILATION, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_DATE, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_YEAR, METADATA_TYPE_LONG);
- METADATA_KEYS_TYPE.put(METADATA_KEY_GENRE, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_TRACK_NUMBER, METADATA_TYPE_LONG);
- METADATA_KEYS_TYPE.put(METADATA_KEY_NUM_TRACKS, METADATA_TYPE_LONG);
- METADATA_KEYS_TYPE.put(METADATA_KEY_DISC_NUMBER, METADATA_TYPE_LONG);
- METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ARTIST, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_ART, METADATA_TYPE_BITMAP);
- METADATA_KEYS_TYPE.put(METADATA_KEY_ART_URI, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ART, METADATA_TYPE_BITMAP);
- METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ART_URI, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_USER_RATING, METADATA_TYPE_RATING);
- METADATA_KEYS_TYPE.put(METADATA_KEY_RATING, METADATA_TYPE_RATING);
- METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_TITLE, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_SUBTITLE, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_DESCRIPTION, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_ICON, METADATA_TYPE_BITMAP);
- METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_ICON_URI, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_MEDIA_ID, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_BT_FOLDER_TYPE, METADATA_TYPE_LONG);
- METADATA_KEYS_TYPE.put(METADATA_KEY_MEDIA_URI, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_ADVERTISEMENT, METADATA_TYPE_LONG);
- METADATA_KEYS_TYPE.put(METADATA_KEY_DOWNLOAD_STATUS, METADATA_TYPE_LONG);
- }
-
- private static final @TextKey String[] PREFERRED_DESCRIPTION_ORDER = {
- METADATA_KEY_TITLE,
- METADATA_KEY_ARTIST,
- METADATA_KEY_ALBUM,
- METADATA_KEY_ALBUM_ARTIST,
- METADATA_KEY_WRITER,
- METADATA_KEY_AUTHOR,
- METADATA_KEY_COMPOSER
- };
-
- private static final @BitmapKey String[] PREFERRED_BITMAP_ORDER = {
- METADATA_KEY_DISPLAY_ICON,
- METADATA_KEY_ART,
- METADATA_KEY_ALBUM_ART
- };
-
- private static final @TextKey String[] PREFERRED_URI_ORDER = {
- METADATA_KEY_DISPLAY_ICON_URI,
- METADATA_KEY_ART_URI,
- METADATA_KEY_ALBUM_ART_URI
- };
-
- final Bundle mBundle;
+ private final MediaMetadata2Provider mProvider;
/**
* @hide
*/
- public MediaMetadata2(Bundle bundle) {
- mBundle = new Bundle(bundle);
+ @SystemApi
+ public MediaMetadata2(MediaMetadata2Provider provider) {
+ mProvider = provider;
}
/**
@@ -443,8 +382,8 @@
* @param key a String key
* @return true if the key exists in this metadata, false otherwise
*/
- public boolean containsKey(String key) {
- return mBundle.containsKey(key);
+ public boolean containsKey(@NonNull String key) {
+ return mProvider.containsKey_impl(key);
}
/**
@@ -455,8 +394,8 @@
* @param key The key the value is stored under
* @return a CharSequence value, or null
*/
- public CharSequence getText(@TextKey String key) {
- return mBundle.getCharSequence(key);
+ public @Nullable CharSequence getText(@TextKey String key) {
+ return mProvider.getText_impl(key);
}
/**
@@ -464,11 +403,11 @@
* the desired type exists for the given key or a null value is explicitly
* associated with the key.
*
- * @
* @return media id. Can be {@code null}
+ * @see #METADATA_KEY_MEDIA_ID
*/
public @Nullable String getMediaId() {
- return getString(METADATA_KEY_MEDIA_ID);
+ return mProvider.getMediaId_impl();
}
/**
@@ -479,12 +418,8 @@
* @param key The key the value is stored under
* @return a String value, or null
*/
- public String getString(@TextKey String key) {
- CharSequence text = mBundle.getCharSequence(key);
- if (text != null) {
- return text.toString();
- }
- return null;
+ public @Nullable String getString(@NonNull @TextKey String key) {
+ return mProvider.getString_impl(key);
}
/**
@@ -494,8 +429,8 @@
* @param key The key the value is stored under
* @return a long value
*/
- public long getLong(@LongKey String key) {
- return mBundle.getLong(key, 0);
+ public long getLong(@NonNull @LongKey String key) {
+ return mProvider.getLong_impl(key);
}
/**
@@ -503,18 +438,10 @@
* the given key.
*
* @param key The key the value is stored under
- * @return A {@link Rating2} or null
+ * @return A {@link Rating2} or {@code null}
*/
- public Rating2 getRating(@RatingKey String key) {
- // TODO(jaewan): Add backward compatibility
- Rating2 rating = null;
- try {
- rating = Rating2.fromBundle(mBundle.getBundle(key));
- } catch (Exception e) {
- // ignore, value was not a rating
- Log.w(TAG, "Failed to retrieve a key as Rating.", e);
- }
- return rating;
+ public @Nullable Rating2 getRating(@RatingKey String key) {
+ return mProvider.getRating_impl(key);
}
/**
@@ -525,14 +452,7 @@
* @return A {@link Bitmap} or null
*/
public Bitmap getBitmap(@BitmapKey String key) {
- Bitmap bmp = null;
- try {
- bmp = mBundle.getParcelable(key);
- } catch (Exception e) {
- // ignore, value was not a bitmap
- Log.w(TAG, "Failed to retrieve a key as Bitmap.", e);
- }
- return bmp;
+ return mProvider.getBitmap_impl(key);
}
/**
@@ -540,14 +460,8 @@
*
* @return A {@link Bundle} or {@code null}
*/
- public Bundle getExtra() {
- try {
- return mBundle.getBundle(METADATA_KEY_EXTRA);
- } catch (Exception e) {
- // ignore, value was not an bundle
- Log.w(TAG, "Failed to retrieve an extra");
- }
- return null;
+ public @Nullable Bundle getExtra() {
+ return mProvider.getExtra_impl();
}
/**
@@ -556,7 +470,7 @@
* @return The number of fields in the metadata.
*/
public int size() {
- return mBundle.size();
+ return mProvider.size_impl();
}
/**
@@ -564,8 +478,8 @@
*
* @return a Set of String keys
*/
- public Set<String> keySet() {
- return mBundle.keySet();
+ public @NonNull Set<String> keySet() {
+ return mProvider.keySet_impl();
}
/**
@@ -574,8 +488,21 @@
*
* @return The Bundle backing this metadata.
*/
- public Bundle getBundle() {
- return mBundle;
+ public @NonNull Bundle toBundle() {
+ return mProvider.toBundle_impl();
+ }
+
+ /**
+ * Creates the {@link MediaMetadata2} from the bundle that previously returned by
+ * {@link #toBundle()}.
+ *
+ * @param context context
+ * @param bundle bundle for the metadata
+ * @return a new MediaMetadata2
+ */
+ public static @NonNull MediaMetadata2 fromBundle(@NonNull Context context,
+ @Nullable Bundle bundle) {
+ return ApiLoader.getProvider(context).fromBundle_MediaMetadata2(context, bundle);
}
/**
@@ -583,14 +510,15 @@
* use the appropriate data type.
*/
public static final class Builder {
- private final Bundle mBundle;
+ private final MediaMetadata2Provider.BuilderProvider mProvider;
/**
* Create an empty Builder. Any field that should be included in the
* {@link MediaMetadata2} must be added.
*/
- public Builder() {
- mBundle = new Bundle();
+ public Builder(@NonNull Context context) {
+ mProvider = ApiLoader.getProvider(context).createMediaMetadata2Builder(
+ context, this);
}
/**
@@ -600,31 +528,17 @@
*
* @param source
*/
- public Builder(MediaMetadata2 source) {
- mBundle = new Bundle(source.mBundle);
+ public Builder(@NonNull Context context, @NonNull MediaMetadata2 source) {
+ mProvider = ApiLoader.getProvider(context).createMediaMetadata2Builder(
+ context, this, source);
}
/**
- * Create a Builder using a {@link MediaMetadata2} instance to set
- * initial values, but replace bitmaps with a scaled down copy if they
- * are larger than maxBitmapSize.
- *
- * @param source The original metadata to copy.
- * @param maxBitmapSize The maximum height/width for bitmaps contained
- * in the metadata.
* @hide
*/
- public Builder(MediaMetadata2 source, int maxBitmapSize) {
- this(source);
- for (String key : mBundle.keySet()) {
- Object value = mBundle.get(key);
- if (value instanceof Bitmap) {
- Bitmap bmp = (Bitmap) value;
- if (bmp.getHeight() > maxBitmapSize || bmp.getWidth() > maxBitmapSize) {
- putBitmap(key, scaleBitmap(bmp, maxBitmapSize));
- }
- }
- }
+ @SystemApi
+ public Builder(@NonNull MediaMetadata2Provider.BuilderProvider provider) {
+ mProvider = provider;
}
/**
@@ -653,15 +567,8 @@
* @param value The CharSequence value to store
* @return The Builder to allow chaining
*/
- public Builder putText(@TextKey String key, CharSequence value) {
- if (METADATA_KEYS_TYPE.containsKey(key)) {
- if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_TEXT) {
- throw new IllegalArgumentException("The " + key
- + " key cannot be used to put a CharSequence");
- }
- }
- mBundle.putCharSequence(key, value);
- return this;
+ public @NonNull Builder putText(@TextKey String key, @Nullable CharSequence value) {
+ return mProvider.putText_impl(key, value);
}
/**
@@ -690,15 +597,8 @@
* @param value The String value to store
* @return The Builder to allow chaining
*/
- public Builder putString(@TextKey String key, String value) {
- if (METADATA_KEYS_TYPE.containsKey(key)) {
- if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_TEXT) {
- throw new IllegalArgumentException("The " + key
- + " key cannot be used to put a String");
- }
- }
- mBundle.putCharSequence(key, value);
- return this;
+ public @NonNull Builder putString(@TextKey String key, @Nullable String value) {
+ return mProvider.putString_impl(key, value);
}
/**
@@ -720,15 +620,8 @@
* @param value The String value to store
* @return The Builder to allow chaining
*/
- public Builder putLong(@LongKey String key, long value) {
- if (METADATA_KEYS_TYPE.containsKey(key)) {
- if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_LONG) {
- throw new IllegalArgumentException("The " + key
- + " key cannot be used to put a long");
- }
- }
- mBundle.putLong(key, value);
- return this;
+ public @NonNull Builder putLong(@NonNull @LongKey String key, long value) {
+ return mProvider.putLong_impl(key, value);
}
/**
@@ -744,16 +637,8 @@
* @param value The String value to store
* @return The Builder to allow chaining
*/
- public Builder putRating(@RatingKey String key, Rating2 value) {
- if (METADATA_KEYS_TYPE.containsKey(key)) {
- if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_RATING) {
- throw new IllegalArgumentException("The " + key
- + " key cannot be used to put a Rating");
- }
- }
- mBundle.putBundle(key, value.toBundle());
-
- return this;
+ public @NonNull Builder putRating(@NonNull @RatingKey String key, @Nullable Rating2 value) {
+ return mProvider.putRating_impl(key, value);
}
/**
@@ -774,23 +659,15 @@
* @param value The Bitmap to store
* @return The Builder to allow chaining
*/
- public Builder putBitmap(@BitmapKey String key, Bitmap value) {
- if (METADATA_KEYS_TYPE.containsKey(key)) {
- if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_BITMAP) {
- throw new IllegalArgumentException("The " + key
- + " key cannot be used to put a Bitmap");
- }
- }
- mBundle.putParcelable(key, value);
- return this;
+ public @NonNull Builder putBitmap(@NonNull @BitmapKey String key, @Nullable Bitmap value) {
+ return mProvider.putBitmap_impl(key, value);
}
/**
* Set an extra {@link Bundle} into the metadata.
*/
- public Builder setExtra(Bundle bundle) {
- mBundle.putBundle(METADATA_KEY_EXTRA, bundle);
- return this;
+ public @NonNull Builder setExtra(@Nullable Bundle bundle) {
+ return mProvider.setExtra_impl(bundle);
}
/**
@@ -798,18 +675,8 @@
*
* @return The new MediaMetadata2 instance
*/
- public MediaMetadata2 build() {
- return new MediaMetadata2(mBundle);
- }
-
- private Bitmap scaleBitmap(Bitmap bmp, int maxSize) {
- float maxSizeF = maxSize;
- float widthScale = maxSizeF / bmp.getWidth();
- float heightScale = maxSizeF / bmp.getHeight();
- float scale = Math.min(widthScale, heightScale);
- int height = (int) (bmp.getHeight() * scale);
- int width = (int) (bmp.getWidth() * scale);
- return Bitmap.createScaledBitmap(bmp, width, height, true);
+ public @NonNull MediaMetadata2 build() {
+ return mProvider.build_impl();
}
}
}
diff --git a/media/java/android/media/MediaSession2.java b/media/java/android/media/MediaSession2.java
index 0ea1e86..acf9615 100644
--- a/media/java/android/media/MediaSession2.java
+++ b/media/java/android/media/MediaSession2.java
@@ -30,9 +30,11 @@
import android.media.session.PlaybackState;
import android.media.update.ApiLoader;
import android.media.update.MediaSession2Provider;
+import android.media.update.MediaSession2Provider.BuilderBaseProvider;
import android.media.update.MediaSession2Provider.CommandGroupProvider;
import android.media.update.MediaSession2Provider.CommandProvider;
import android.media.update.MediaSession2Provider.ControllerInfoProvider;
+import android.media.update.ProviderCreator;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
@@ -81,34 +83,178 @@
public class MediaSession2 implements AutoCloseable {
private final MediaSession2Provider mProvider;
- // Note: Do not define IntDef because subclass can add more command code on top of these.
+ // TODO(jaewan): Should we define IntDef? Currently we don't have to allow subclass to add more.
// TODO(jaewan): Shouldn't we pull out?
- // TODO(jaewan): Should we also protect getPlaybackState()?
+ // TODO(jaewan): Should we also protect getters not related with metadata?
+ // Getters are getRatingType(), getPlaybackState(), getSessionActivity(),
+ // getPlaylistParams())
+ // Next ID: 23
+ /**
+ * Command code for the custom command which can be defined by string action in the
+ * {@link Command}.
+ */
public static final int COMMAND_CODE_CUSTOM = 0;
- public static final int COMMAND_CODE_PLAYBACK_START = 1;
+
+ /**
+ * Command code for {@link MediaController2#play()}.
+ * <p>
+ * Command would be sent directly to the player if the session doesn't reject the request
+ * through the {@link SessionCallback#onCommandRequest(ControllerInfo, Command)}.
+ */
+ public static final int COMMAND_CODE_PLAYBACK_PLAY = 1;
+
+ /**
+ * Command code for {@link MediaController2#pause()}.
+ * <p>
+ * Command would be sent directly to the player if the session doesn't reject the request
+ * through the {@link SessionCallback#onCommandRequest(ControllerInfo, Command)}.
+ */
public static final int COMMAND_CODE_PLAYBACK_PAUSE = 2;
+
+ /**
+ * Command code for {@link MediaController2#stop()}.
+ * <p>
+ * Command would be sent directly to the player if the session doesn't reject the request
+ * through the {@link SessionCallback#onCommandRequest(ControllerInfo, Command)}.
+ */
public static final int COMMAND_CODE_PLAYBACK_STOP = 3;
+
+ /**
+ * Command code for {@link MediaController2#skipToNext()} ()}.
+ * <p>
+ * Command would be sent directly to the player if the session doesn't reject the request
+ * through the {@link SessionCallback#onCommandRequest(ControllerInfo, Command)}.
+ */
public static final int COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM = 4;
+
+ /**
+ * Command code for {@link MediaController2#skipToPrevious()} ()}.
+ * <p>
+ * Command would be sent directly to the player if the session doesn't reject the request
+ * through the {@link SessionCallback#onCommandRequest(ControllerInfo, Command)}.
+ */
public static final int COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM = 5;
+
+ /**
+ * Command code for {@link MediaController2#prepare()}.
+ * <p>
+ * Command would be sent directly to the player if the session doesn't reject the request
+ * through the {@link SessionCallback#onCommandRequest(ControllerInfo, Command)}.
+ */
public static final int COMMAND_CODE_PLAYBACK_PREPARE = 6;
+
+ /**
+ * Command code for {@link MediaController2#fastForward()} ()}.
+ * <p>
+ * This is transport control command. Command would be sent directly to the player if the
+ * session doesn't reject the request through the
+ * {@link SessionCallback#onCommandRequest(ControllerInfo, Command)}.
+ */
public static final int COMMAND_CODE_PLAYBACK_FAST_FORWARD = 7;
+
+ /**
+ * Command code for {@link MediaController2#rewind()}.
+ * <p>
+ * Command would be sent directly to the player if the session doesn't reject the request
+ * through the {@link SessionCallback#onCommandRequest(ControllerInfo, Command)}.
+ */
public static final int COMMAND_CODE_PLAYBACK_REWIND = 8;
+
+ /**
+ * Command code for {@link MediaController2#seekTo(long)} ()}.
+ * <p>
+ * Command would be sent directly to the player if the session doesn't reject the request
+ * through the {@link SessionCallback#onCommandRequest(ControllerInfo, Command)}.
+ */
public static final int COMMAND_CODE_PLAYBACK_SEEK_TO = 9;
+ /**
+ * Command code for {@link MediaController2#setCurrentPlaylistItem(int)} ()}.
+ * <p>
+ * Command would be sent directly to the player if the session doesn't reject the request
+ * through the {@link SessionCallback#onCommandRequest(ControllerInfo, Command)}.
+ */
public static final int COMMAND_CODE_PLAYBACK_SET_CURRENT_PLAYLIST_ITEM = 10;
- public static final int COMMAND_CODE_PLAYLIST_GET = 11;
+ /**
+ * Command code for {@link MediaController2#setPlaylistParams(PlaylistParams)} ()}.
+ * <p>
+ * Command would be sent directly to the player if the session doesn't reject the request
+ * through the {@link SessionCallback#onCommandRequest(ControllerInfo, Command)}.
+ */
+ public static final int COMMAND_CODE_PLAYBACK_SET_PLAYLIST_PARAMS = 11;
+
+ /**
+ * Command code for {@link MediaController2#addPlaylistItem(int, MediaItem2)}.
+ * <p>
+ * Command would be sent directly to the player if the session doesn't reject the request
+ * through the {@link SessionCallback#onCommandRequest(ControllerInfo, Command)}.
+ */
public static final int COMMAND_CODE_PLAYLIST_ADD = 12;
+
+ /**
+ * Command code for {@link MediaController2#addPlaylistItem(int, MediaItem2)}.
+ * <p>
+ * Command would be sent directly to the player if the session doesn't reject the request
+ * through the {@link SessionCallback#onCommandRequest(ControllerInfo, Command)}.
+ */
public static final int COMMAND_CODE_PLAYLIST_REMOVE = 13;
- public static final int COMMAND_CODE_PLAY_FROM_MEDIA_ID = 14;
- public static final int COMMAND_CODE_PLAY_FROM_URI = 15;
- public static final int COMMAND_CODE_PLAY_FROM_SEARCH = 16;
+ /**
+ * Command code for {@link MediaController2#getPlaylist()}.
+ * <p>
+ * Command would be sent directly to the player if the session doesn't reject the request
+ * through the {@link SessionCallback#onCommandRequest(ControllerInfo, Command)}.
+ */
+ public static final int COMMAND_CODE_PLAYLIST_GET = 14;
- public static final int COMMAND_CODE_PREPARE_FROM_MEDIA_ID = 17;
- public static final int COMMAND_CODE_PREPARE_FROM_URI = 18;
- public static final int COMMAND_CODE_PREPARE_FROM_SEARCH = 19;
+ /**
+ * Command code for both {@link MediaController2#setVolumeTo(int, int)} and
+ * {@link MediaController2#adjustVolume(int, int)}.
+ * <p>
+ * Command would adjust the volume or sent to the volume provider directly if the session
+ * doesn't reject the request through the
+ * {@link SessionCallback#onCommandRequest(ControllerInfo, Command)}.
+ */
+ public static final int COMMAND_CODE_SET_VOLUME = 15;
- public static final int COMMAND_CODE_PLAYBACK_SET_PLAYLIST_PARAMS = 20;
+ /**
+ * Command code for {@link MediaController2#playFromMediaId(String, Bundle)}.
+ */
+ public static final int COMMAND_CODE_PLAY_FROM_MEDIA_ID = 16;
+
+ /**
+ * Command code for {@link MediaController2#playFromUri(String, Bundle)}.
+ */
+ public static final int COMMAND_CODE_PLAY_FROM_URI = 17;
+
+ /**
+ * Command code for {@link MediaController2#playFromSearch(String, Bundle)}.
+ */
+ public static final int COMMAND_CODE_PLAY_FROM_SEARCH = 18;
+
+ /**
+ * Command code for {@link MediaController2#prepareFromMediaId(String, Bundle)}.
+ */
+ public static final int COMMAND_CODE_PREPARE_FROM_MEDIA_ID = 19;
+
+ /**
+ * Command code for {@link MediaController2#prepareFromUri(Uri, Bundle)}.
+ */
+ public static final int COMMAND_CODE_PREPARE_FROM_URI = 20;
+
+ /**
+ * Command code for {@link MediaController2#prepareFromSearch(String, Bundle)}.
+ */
+ public static final int COMMAND_CODE_PREPARE_FROM_SEARCH = 21;
+
+ /**
+ * Command code for {@link MediaBrowser2} specific functions that allows navigation and search
+ * from the {@link MediaLibraryService2}. This would be ignored if a {@link MediaSession2},
+ * not {@link android.media.MediaLibraryService2.MediaLibrarySession}, specify this.
+ *
+ * @see MediaBrowser2
+ */
+ public static final int COMMAND_CODE_BROWSER = 22;
/**
* Define a command that a {@link MediaController2} can send to a {@link MediaSession2}.
@@ -212,6 +358,9 @@
return mProvider.hasCommand_impl(code);
}
+ /**
+ * @hide
+ */
@SystemApi
public CommandGroupProvider getProvider() {
return mProvider;
@@ -274,22 +423,36 @@
public void onDisconnected(@NonNull ControllerInfo controller) { }
/**
- * Called when a controller sent a command to the session, and the command will be sent to
- * the player directly unless you reject the request by {@code false}.
+ * Called when a controller sent a command that will be sent directly to the player. Return
+ * {@code false} here to reject the request and stop sending command to the player.
*
* @param controller controller information.
* @param command a command. This method will be called for every single command.
* @return {@code true} if you want to accept incoming command. {@code false} otherwise.
+ * @see #COMMAND_CODE_PLAYBACK_PLAY
+ * @see #COMMAND_CODE_PLAYBACK_PAUSE
+ * @see #COMMAND_CODE_PLAYBACK_STOP
+ * @see #COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM
+ * @see #COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM
+ * @see #COMMAND_CODE_PLAYBACK_PREPARE
+ * @see #COMMAND_CODE_PLAYBACK_FAST_FORWARD
+ * @see #COMMAND_CODE_PLAYBACK_REWIND
+ * @see #COMMAND_CODE_PLAYBACK_SEEK_TO
+ * @see #COMMAND_CODE_PLAYBACK_SET_CURRENT_PLAYLIST_ITEM
+ * @see #COMMAND_CODE_PLAYBACK_SET_PLAYLIST_PARAMS
+ * @see #COMMAND_CODE_PLAYLIST_ADD
+ * @see #COMMAND_CODE_PLAYLIST_REMOVE
+ * @see #COMMAND_CODE_PLAYLIST_GET
+ * @see #COMMAND_CODE_SET_VOLUME
*/
- // TODO(jaewan): Add more documentations (or make it clear) which commands can be filtered
- // with this.
public boolean onCommandRequest(@NonNull ControllerInfo controller,
@NonNull Command command) {
return true;
}
/**
- * Called when a controller set rating on the currently playing contents.
+ * Called when a controller set rating on the currently playing contents by
+ * {@link MediaController2#setRating(Rating2)}.
*
* @param controller controller information
* @param rating new rating from the controller
@@ -297,7 +460,8 @@
public void onSetRating(@NonNull ControllerInfo controller, @NonNull Rating2 rating) { }
/**
- * Called when a controller sent a custom command.
+ * Called when a controller sent a custom command through
+ * {@link MediaController2#sendCustomCommand(Command, Bundle, ResultReceiver)}.
*
* @param controller controller information
* @param customCommand custom command.
@@ -309,7 +473,48 @@
@Nullable ResultReceiver cb) { }
/**
- * Override to handle requests to prepare for playing a specific mediaId.
+ * Called when a controller requested to play a specific mediaId through
+ * {@link MediaController2#playFromMediaId(String, Bundle)}.
+ *
+ * @param controller controller information
+ * @param mediaId media id
+ * @param extras optional extra bundle
+ * @see #COMMAND_CODE_PLAY_FROM_MEDIA_ID
+ */
+ public void onPlayFromMediaId(@NonNull ControllerInfo controller,
+ @NonNull String mediaId, @Nullable Bundle extras) { }
+
+ /**
+ * Called when a controller requested to begin playback from a search query through
+ * {@link MediaController2#playFromSearch(String, Bundle)}
+ * <p>
+ * An empty query indicates that the app may play any music. The implementation should
+ * attempt to make a smart choice about what to play.
+ *
+ * @param controller controller information
+ * @param query query string. Can be empty to indicate any suggested media
+ * @param extras optional extra bundle
+ * @see #COMMAND_CODE_PLAY_FROM_SEARCH
+ */
+ public void onPlayFromSearch(@NonNull ControllerInfo controller,
+ @NonNull String query, @Nullable Bundle extras) { }
+
+ /**
+ * Called when a controller requested to play a specific media item represented by a URI
+ * through {@link MediaController2#playFromUri(String, Bundle)}
+ *
+ * @param controller controller information
+ * @param uri uri
+ * @param extras optional extra bundle
+ * @see #COMMAND_CODE_PLAY_FROM_URI
+ */
+ public void onPlayFromUri(@NonNull ControllerInfo controller,
+ @NonNull String uri, @Nullable Bundle extras) { }
+
+ /**
+ * Called when a controller requested to prepare for playing a specific mediaId through
+ * {@link MediaController2#prepareFromMediaId(String, Bundle)}.
+ * <p>
* During the preparation, a session should not hold audio focus in order to allow other
* sessions play seamlessly. The state of playback should be updated to
* {@link PlaybackState#STATE_PAUSED} after the preparation is done.
@@ -319,28 +524,41 @@
* <p>
* Override {@link #onPlayFromMediaId} to handle requests for starting
* playback without preparation.
+ *
+ * @param controller controller information
+ * @param mediaId media id to prepare
+ * @param extras optional extra bundle
+ * @see #COMMAND_CODE_PREPARE_FROM_MEDIA_ID
*/
- public void onPlayFromMediaId(@NonNull ControllerInfo controller,
+ public void onPrepareFromMediaId(@NonNull ControllerInfo controller,
@NonNull String mediaId, @Nullable Bundle extras) { }
/**
- * Override to handle requests to prepare playback from a search query. An empty query
- * indicates that the app may prepare any music. The implementation should attempt to make a
- * smart choice about what to play. During the preparation, a session should not hold audio
- * focus in order to allow other sessions play seamlessly. The state of playback should be
- * updated to {@link PlaybackState#STATE_PAUSED} after the preparation is done.
+ * Called when a controller requested to prepare playback from a search query through
+ * {@link MediaController2#prepareFromSearch(String, Bundle)}.
* <p>
- * The playback of the prepared content should start in the later calls of
- * {@link MediaSession2#play()}.
+ * An empty query indicates that the app may prepare any music. The implementation should
+ * attempt to make a smart choice about what to play.
+ * <p>
+ * The state of playback should be updated to {@link PlaybackState#STATE_PAUSED} after the
+ * preparation is done. The playback of the prepared content should start in the later
+ * calls of {@link MediaSession2#play()}.
* <p>
* Override {@link #onPlayFromSearch} to handle requests for starting playback without
* preparation.
+ *
+ * @param controller controller information
+ * @param query query string. Can be empty to indicate any suggested media
+ * @param extras optional extra bundle
+ * @see #COMMAND_CODE_PREPARE_FROM_SEARCH
*/
- public void onPlayFromSearch(@NonNull ControllerInfo controller,
+ public void onPrepareFromSearch(@NonNull ControllerInfo controller,
@NonNull String query, @Nullable Bundle extras) { }
/**
- * Override to handle requests to prepare a specific media item represented by a URI.
+ * Called when a controller requested to prepare a specific media item represented by a URI
+ * through {@link MediaController2#prepareFromUri(Uri, Bundle)}.
+ * <p></p>
* During the preparation, a session should not hold audio focus in order to allow
* other sessions play seamlessly. The state of playback should be updated to
* {@link PlaybackState#STATE_PAUSED} after the preparation is done.
@@ -350,51 +568,14 @@
* <p>
* Override {@link #onPlayFromUri} to handle requests for starting playback without
* preparation.
- */
- public void onPlayFromUri(@NonNull ControllerInfo controller,
- @NonNull String uri, @Nullable Bundle extras) { }
-
- /**
- * Override to handle requests to play a specific mediaId.
- */
- public void onPrepareFromMediaId(@NonNull ControllerInfo controller,
- @NonNull String mediaId, @Nullable Bundle extras) { }
-
- /**
- * Override to handle requests to begin playback from a search query. An
- * empty query indicates that the app may play any music. The
- * implementation should attempt to make a smart choice about what to
- * play.
- */
- public void onPrepareFromSearch(@NonNull ControllerInfo controller,
- @NonNull String query, @Nullable Bundle extras) { }
-
- /**
- * Override to handle requests to play a specific media item represented by a URI.
+ *
+ * @param controller controller information
+ * @param uri uri
+ * @param extras optional extra bundle
+ * @see #COMMAND_CODE_PREPARE_FROM_URI
*/
public void onPrepareFromUri(@NonNull ControllerInfo controller,
@NonNull Uri uri, @Nullable Bundle extras) { }
-
- /**
- * Called when a controller wants to add a {@link MediaItem2} at the specified position
- * in the play queue.
- * <p>
- * The item from the media controller wouldn't have valid data source descriptor because
- * it would have been anonymized when it's sent to the remote process.
- *
- * @param item The media item to be inserted.
- * @param index The index at which the item is to be inserted.
- */
- public void onAddPlaylistItem(@NonNull ControllerInfo controller,
- @NonNull MediaItem2 item, int index) { }
-
- /**
- * Called when a controller wants to remove the {@link MediaItem2}
- *
- * @param item
- */
- // Can we do this automatically?
- public void onRemovePlaylistItem(@NonNull MediaItem2 item) { }
};
/**
@@ -403,36 +584,11 @@
* @hide
*/
static abstract class BuilderBase
- <T extends MediaSession2.BuilderBase<T, C>, C extends SessionCallback> {
- final Context mContext;
- final MediaPlayerInterface mPlayer;
- String mId;
- Executor mCallbackExecutor;
- C mCallback;
- VolumeProvider mVolumeProvider;
- int mRatingType;
- PendingIntent mSessionActivity;
+ <T extends MediaSession2, U extends BuilderBase<T, U, C>, C extends SessionCallback> {
+ private final BuilderBaseProvider<T, C> mProvider;
- /**
- * Constructor.
- *
- * @param context a context
- * @param player a player to handle incoming command from any controller.
- * @throws IllegalArgumentException if any parameter is null, or the player is a
- * {@link MediaSession2} or {@link MediaController2}.
- */
- // TODO(jaewan): Also need executor
- public BuilderBase(@NonNull Context context, @NonNull MediaPlayerInterface player) {
- if (context == null) {
- throw new IllegalArgumentException("context shouldn't be null");
- }
- if (player == null) {
- throw new IllegalArgumentException("player shouldn't be null");
- }
- mContext = context;
- mPlayer = player;
- // Ensure non-null
- mId = "";
+ BuilderBase(ProviderCreator<BuilderBase<T, U, C>, BuilderBaseProvider<T, C>> creator) {
+ mProvider = creator.createProvider(this);
}
/**
@@ -444,9 +600,9 @@
*
* @param volumeProvider The provider that will handle volume changes. Can be {@code null}
*/
- public T setVolumeProvider(@Nullable VolumeProvider volumeProvider) {
- mVolumeProvider = volumeProvider;
- return (T) this;
+ public U setVolumeProvider(@Nullable VolumeProvider volumeProvider) {
+ mProvider.setVolumeProvider_impl(volumeProvider);
+ return (U) this;
}
/**
@@ -462,9 +618,9 @@
* <li>{@link Rating2#RATING_THUMB_UP_DOWN}</li>
* </ul>
*/
- public T setRatingType(@Rating2.Style int type) {
- mRatingType = type;
- return (T) this;
+ public U setRatingType(@Rating2.Style int type) {
+ mProvider.setRatingType_impl(type);
+ return (U) this;
}
/**
@@ -474,9 +630,9 @@
*
* @param pi The intent to launch to show UI for this session.
*/
- public T setSessionActivity(@Nullable PendingIntent pi) {
- mSessionActivity = pi;
- return (T) this;
+ public U setSessionActivity(@Nullable PendingIntent pi) {
+ mProvider.setSessionActivity_impl(pi);
+ return (U) this;
}
/**
@@ -489,12 +645,9 @@
* @throws IllegalArgumentException if id is {@code null}
* @return
*/
- public T setId(@NonNull String id) {
- if (id == null) {
- throw new IllegalArgumentException("id shouldn't be null");
- }
- mId = id;
- return (T) this;
+ public U setId(@NonNull String id) {
+ mProvider.setId_impl(id);
+ return (U) this;
}
/**
@@ -504,17 +657,10 @@
* @param callback session callback.
* @return
*/
- public T setSessionCallback(@NonNull @CallbackExecutor Executor executor,
+ public U setSessionCallback(@NonNull @CallbackExecutor Executor executor,
@NonNull C callback) {
- if (executor == null) {
- throw new IllegalArgumentException("executor shouldn't be null");
- }
- if (callback == null) {
- throw new IllegalArgumentException("callback shouldn't be null");
- }
- mCallbackExecutor = executor;
- mCallback = callback;
- return (T) this;
+ mProvider.setSessionCallback_impl(executor, callback);
+ return (U) this;
}
/**
@@ -524,7 +670,9 @@
* @throws IllegalStateException if the session with the same id is already exists for the
* package.
*/
- public abstract MediaSession2 build();
+ public T build() {
+ return mProvider.build_impl();
+ }
}
/**
@@ -533,24 +681,12 @@
* Any incoming event from the {@link MediaController2} will be handled on the thread
* that created session with the {@link Builder#build()}.
*/
- // TODO(jaewan): Move this to updatable
// TODO(jaewan): Add setRatingType()
// TODO(jaewan): Add setSessionActivity()
- public static final class Builder extends BuilderBase<Builder, SessionCallback> {
+ public static final class Builder extends BuilderBase<MediaSession2, Builder, SessionCallback> {
public Builder(Context context, @NonNull MediaPlayerInterface player) {
- super(context, player);
- }
-
- @Override
- public MediaSession2 build() {
- if (mCallbackExecutor == null) {
- mCallbackExecutor = mContext.getMainExecutor();
- }
- if (mCallback == null) {
- mCallback = new SessionCallback(mContext);
- }
- return new MediaSession2(mContext, mPlayer, mId, mVolumeProvider, mRatingType,
- mSessionActivity, mCallbackExecutor, mCallback);
+ super((instance) -> ApiLoader.getProvider(context).createMediaSession2Builder(
+ context, (Builder) instance, player));
}
}
@@ -568,7 +704,7 @@
public ControllerInfo(Context context, int uid, int pid, String packageName,
IInterface callback) {
mProvider = ApiLoader.getProvider(context)
- .createMediaSession2ControllerInfoProvider(
+ .createMediaSession2ControllerInfo(
context, this, uid, pid, packageName, callback);
}
@@ -597,6 +733,9 @@
return mProvider.isTrusted_impl();
}
+ /**
+ * @hide
+ */
@SystemApi
public ControllerInfoProvider getProvider() {
return mProvider;
@@ -795,7 +934,7 @@
/**
* Parameter for the playlist.
*/
- public static class PlaylistParams {
+ public final static class PlaylistParams {
/**
* @hide
*/
@@ -850,79 +989,71 @@
*/
public static final int SHUFFLE_MODE_GROUP = 2;
+
+ private final MediaSession2Provider.PlaylistParamsProvider mProvider;
+
/**
- * Keys used for converting a PlaylistParams object to a bundle object and vice versa.
+ * Instantiate {@link PlaylistParams}
+ *
+ * @param context context
+ * @param repeatMode repeat mode
+ * @param shuffleMode shuffle mode
+ * @param playlistMetadata metadata for the list
*/
- private static final String KEY_REPEAT_MODE =
- "android.media.session2.playlistparams2.repeat_mode";
- private static final String KEY_SHUFFLE_MODE =
- "android.media.session2.playlistparams2.shuffle_mode";
- private static final String KEY_MEDIA_METADATA2_BUNDLE =
- "android.media.session2.playlistparams2.metadata2_bundle";
-
- private @RepeatMode int mRepeatMode;
- private @ShuffleMode int mShuffleMode;
-
- private MediaMetadata2 mPlaylistMetadata;
-
- public PlaylistParams(@RepeatMode int repeatMode, @ShuffleMode int shuffleMode,
- @Nullable MediaMetadata2 playlistMetadata) {
- mRepeatMode = repeatMode;
- mShuffleMode = shuffleMode;
- mPlaylistMetadata = playlistMetadata;
+ public PlaylistParams(@NonNull Context context, @RepeatMode int repeatMode,
+ @ShuffleMode int shuffleMode, @Nullable MediaMetadata2 playlistMetadata) {
+ mProvider = ApiLoader.getProvider(context).createMediaSession2PlaylistParams(
+ context, this, repeatMode, shuffleMode, playlistMetadata);
}
+ /**
+ * Create a new bundle for this object.
+ *
+ * @return
+ */
+ public @NonNull Bundle toBundle() {
+ return mProvider.toBundle_impl();
+ }
+
+ /**
+ * Create a new playlist params from the bundle that was previously returned by
+ * {@link #toBundle}.
+ *
+ * @param context context
+ * @return a new playlist params. Can be {@code null} for error.
+ */
+ public static @Nullable PlaylistParams fromBundle(
+ @NonNull Context context, @Nullable Bundle bundle) {
+ return ApiLoader.getProvider(context).fromBundle_PlaylistParams(context, bundle);
+ }
+
+ /**
+ * Get repeat mode
+ *
+ * @return repeat mode
+ * @see #REPEAT_MODE_NONE, #REPEAT_MODE_ONE, #REPEAT_MODE_ALL, #REPEAT_MODE_GROUP
+ */
public @RepeatMode int getRepeatMode() {
- return mRepeatMode;
+ return mProvider.getRepeatMode_impl();
}
+ /**
+ * Get shuffle mode
+ *
+ * @return shuffle mode
+ * @see #SHUFFLE_MODE_NONE, #SHUFFLE_MODE_ALL, #SHUFFLE_MODE_GROUP
+ */
public @ShuffleMode int getShuffleMode() {
- return mShuffleMode;
- }
-
- public MediaMetadata2 getPlaylistMetadata() {
- return mPlaylistMetadata;
+ return mProvider.getShuffleMode_impl();
}
/**
- * Returns this object as a bundle to share between processes.
+ * Get metadata for the playlist
*
- * @hide
+ * @return metadata. Can be {@code null}
*/
- public Bundle toBundle() {
- Bundle bundle = new Bundle();
- bundle.putInt(KEY_REPEAT_MODE, mRepeatMode);
- bundle.putInt(KEY_SHUFFLE_MODE, mShuffleMode);
- if (mPlaylistMetadata != null) {
- bundle.putBundle(KEY_MEDIA_METADATA2_BUNDLE, mPlaylistMetadata.getBundle());
- }
- return bundle;
- }
-
- /**
- * Creates an instance from a bundle which is previously created by {@link #toBundle()}.
- *
- * @param bundle A bundle created by {@link #toBundle()}.
- * @return A new {@link PlaylistParams} instance. Returns {@code null} if the given
- * {@param bundle} is null, or if the {@param bundle} has no playlist parameters.
- * @hide
- */
- public static PlaylistParams fromBundle(Bundle bundle) {
- if (bundle == null) {
- return null;
- }
- if (!bundle.containsKey(KEY_REPEAT_MODE) || !bundle.containsKey(KEY_SHUFFLE_MODE)) {
- return null;
- }
-
- Bundle metadataBundle = bundle.getBundle(KEY_MEDIA_METADATA2_BUNDLE);
- MediaMetadata2 metadata =
- metadataBundle == null ? null : new MediaMetadata2(metadataBundle);
-
- return new PlaylistParams(
- bundle.getInt(KEY_REPEAT_MODE),
- bundle.getInt(KEY_SHUFFLE_MODE),
- metadata);
+ public @Nullable MediaMetadata2 getPlaylistMetadata() {
+ return mProvider.getPlaylistMetadata_impl();
}
}
@@ -940,23 +1071,15 @@
* framework had to add heuristics to figure out if an app is
* @hide
*/
- MediaSession2(Context context, MediaPlayerInterface player, String id,
- VolumeProvider volumeProvider, int ratingType, PendingIntent sessionActivity,
- Executor callbackExecutor, SessionCallback callback) {
+ @SystemApi
+ public MediaSession2(MediaSession2Provider provider) {
super();
- mProvider = createProvider(context, player, id, volumeProvider, ratingType, sessionActivity,
- callbackExecutor, callback);
- mProvider.initialize();
+ mProvider = provider;
}
- MediaSession2Provider createProvider(Context context, MediaPlayerInterface player, String id,
- VolumeProvider volumeProvider, int ratingType, PendingIntent sessionActivity,
- Executor callbackExecutor, SessionCallback callback) {
- return ApiLoader.getProvider(context)
- .createMediaSession2(context, this, player, id, volumeProvider, ratingType,
- sessionActivity, callbackExecutor, callback);
- }
-
+ /**
+ * @hide
+ */
@SystemApi
public MediaSession2Provider getProvider() {
return mProvider;
diff --git a/media/java/android/media/PlaybackState2.java b/media/java/android/media/PlaybackState2.java
index da776eb..627974a 100644
--- a/media/java/android/media/PlaybackState2.java
+++ b/media/java/android/media/PlaybackState2.java
@@ -17,6 +17,11 @@
package android.media;
import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.media.update.ApiLoader;
+import android.media.update.PlaybackState2Provider;
import android.os.Bundle;
import java.lang.annotation.Retention;
@@ -29,14 +34,51 @@
* @hide
*/
public final class PlaybackState2 {
- private static final String TAG = "PlaybackState2";
-
+ // Similar to the PlaybackState2 with following changes
+ // - Not implement Parcelable and added from/toBundle()
+ // - Removed playback state that doesn't match with the MediaPlayer2
+ // Full list should be finalized when the MediaPlayer2 has getter for the playback state.
+ // Here's table for the MP2 state and PlaybackState2.State.
+ // +----------------------------------------+----------------------------------------+
+ // | MediaPlayer2 state | Matching PlaybackState2.State |
+ // | (Names are from MP2' Javadoc) | |
+ // +----------------------------------------+----------------------------------------+
+ // | Idle: Just finished creating MP2 | STATE_NONE |
+ // | or reset() is called | |
+ // +----------------------------------------+----------------------------------------+
+ // | Initialized: setDataSource/Playlist | N/A (Session/Controller don't |
+ // | | differentiate with Prepared) |
+ // +----------------------------------------+----------------------------------------+
+ // | Prepared: Prepared after initialized | STATE_PAUSED |
+ // +----------------------------------------+----------------------------------------+
+ // | Started: Started playback | STATE_PLAYING |
+ // +----------------------------------------+----------------------------------------+
+ // | Paused: Paused playback | STATE_PAUSED |
+ // +----------------------------------------+----------------------------------------+
+ // | PlaybackCompleted: Playback is done | STATE_PAUSED |
+ // +----------------------------------------+----------------------------------------+
+ // | Stopped: MP2.stop() is called. | STATE_STOPPED |
+ // | prepare() is needed to play again | |
+ // | (Seemingly the same as initialized | |
+ // | because cannot set data source | |
+ // | after this) | |
+ // +----------------------------------------+----------------------------------------+
+ // | Error: an API is called in a state | STATE_ERROR |
+ // | that the API isn't supported | |
+ // +----------------------------------------+----------------------------------------+
+ // | End: MP2.close() is called to release | N/A (MediaSession will be gone) |
+ // | MP2. Cannot be reused anymore | |
+ // +----------------------------------------+----------------------------------------+
+ // | Started, but | STATE_BUFFERING |
+ // | EventCallback.onBufferingUpdate() | |
+ // +----------------------------------------+----------------------------------------+
+ // - Removed actions and custom actions.
+ // - Repeat mode / shuffle mode is now in the PlaylistParams
// TODO(jaewan): Replace states from MediaPlayer2
/**
* @hide
*/
- @IntDef({STATE_NONE, STATE_STOPPED, STATE_PREPARED, STATE_PAUSED, STATE_PLAYING,
- STATE_FINISH, STATE_BUFFERING, STATE_ERROR})
+ @IntDef({STATE_NONE, STATE_STOPPED, STATE_PAUSED, STATE_PLAYING, STATE_BUFFERING, STATE_ERROR})
@Retention(RetentionPolicy.SOURCE)
public @interface State {}
@@ -52,88 +94,46 @@
public final static int STATE_STOPPED = 1;
/**
- * State indicating this item is currently prepared
- */
- public final static int STATE_PREPARED = 2;
-
- /**
* State indicating this item is currently paused.
*/
- public final static int STATE_PAUSED = 3;
+ public final static int STATE_PAUSED = 2;
/**
* State indicating this item is currently playing.
*/
- public final static int STATE_PLAYING = 4;
-
- /**
- * State indicating the playback reaches the end of the item.
- */
- public final static int STATE_FINISH = 5;
+ public final static int STATE_PLAYING = 3;
/**
* State indicating this item is currently buffering and will begin playing
* when enough data has buffered.
*/
- public final static int STATE_BUFFERING = 6;
+ public final static int STATE_BUFFERING = 4;
/**
* State indicating this item is currently in an error state. The error
* message should also be set when entering this state.
*/
- public final static int STATE_ERROR = 7;
+ public final static int STATE_ERROR = 5;
/**
* Use this value for the position to indicate the position is not known.
*/
public final static long PLAYBACK_POSITION_UNKNOWN = -1;
- /**
- * Keys used for converting a PlaybackState2 to a bundle object and vice versa.
- */
- private static final String KEY_STATE = "android.media.playbackstate2.state";
- private static final String KEY_POSITION = "android.media.playbackstate2.position";
- private static final String KEY_BUFFERED_POSITION =
- "android.media.playbackstate2.buffered_position";
- private static final String KEY_SPEED = "android.media.playbackstate2.speed";
- private static final String KEY_ERROR_MESSAGE = "android.media.playbackstate2.error_message";
- private static final String KEY_UPDATE_TIME = "android.media.playbackstate2.update_time";
- private static final String KEY_ACTIVE_ITEM_ID = "android.media.playbackstate2.active_item_id";
-
- private final int mState;
- private final long mPosition;
- private final long mUpdateTime;
- private final float mSpeed;
- private final long mBufferedPosition;
- private final long mActiveItemId;
- private final CharSequence mErrorMessage;
+ private final PlaybackState2Provider mProvider;
// TODO(jaewan): Better error handling?
// E.g. media item at #2 has issue, but continue playing #3
// login error. fire intent xxx to login
- public PlaybackState2(int state, long position, long updateTime, float speed,
- long bufferedPosition, long activeItemId, CharSequence error) {
- mState = state;
- mPosition = position;
- mSpeed = speed;
- mUpdateTime = updateTime;
- mBufferedPosition = bufferedPosition;
- mActiveItemId = activeItemId;
- mErrorMessage = error;
+ public PlaybackState2(@NonNull Context context, int state, long position, long updateTime,
+ float speed, long bufferedPosition, long activeItemId, CharSequence error) {
+ mProvider = ApiLoader.getProvider(context).createPlaybackState2(context, this, state,
+ position, updateTime, speed, bufferedPosition, activeItemId, error);
}
@Override
public String toString() {
- StringBuilder bob = new StringBuilder("PlaybackState {");
- bob.append("state=").append(mState);
- bob.append(", position=").append(mPosition);
- bob.append(", buffered position=").append(mBufferedPosition);
- bob.append(", speed=").append(mSpeed);
- bob.append(", updated=").append(mUpdateTime);
- bob.append(", active item id=").append(mActiveItemId);
- bob.append(", error=").append(mErrorMessage);
- bob.append("}");
- return bob.toString();
+ return mProvider.toString_impl();
}
/**
@@ -141,22 +141,24 @@
* <ul>
* <li> {@link PlaybackState2#STATE_NONE}</li>
* <li> {@link PlaybackState2#STATE_STOPPED}</li>
- * <li> {@link PlaybackState2#STATE_PLAYING}</li>
+ * <li> {@link PlaybackState2#STATE_PREPARED}</li>
* <li> {@link PlaybackState2#STATE_PAUSED}</li>
+ * <li> {@link PlaybackState2#STATE_PLAYING}</li>
+ * <li> {@link PlaybackState2#STATE_FINISH}</li>
* <li> {@link PlaybackState2#STATE_BUFFERING}</li>
* <li> {@link PlaybackState2#STATE_ERROR}</li>
* </ul>
*/
@State
public int getState() {
- return mState;
+ return mProvider.getState_impl();
}
/**
* Get the current playback position in ms.
*/
public long getPosition() {
- return mPosition;
+ return mProvider.getPosition_impl();
}
/**
@@ -165,7 +167,7 @@
* content.
*/
public long getBufferedPosition() {
- return mBufferedPosition;
+ return mProvider.getBufferedPosition_impl();
}
/**
@@ -176,7 +178,7 @@
* @return The current speed of playback.
*/
public float getPlaybackSpeed() {
- return mSpeed;
+ return mProvider.getPlaybackSpeed_impl();
}
/**
@@ -184,7 +186,7 @@
* {@link PlaybackState2#STATE_ERROR}.
*/
public CharSequence getErrorMessage() {
- return mErrorMessage;
+ return mProvider.getErrorMessage_impl();
}
/**
@@ -194,7 +196,7 @@
* @return The last time the position was updated.
*/
public long getLastPositionUpdateTime() {
- return mUpdateTime;
+ return mProvider.getLastPositionUpdateTime_impl();
}
/**
@@ -203,53 +205,26 @@
* @return The id of the currently active item in the queue
*/
public long getCurrentPlaylistItemIndex() {
- return mActiveItemId;
+ return mProvider.getCurrentPlaylistItemIndex_impl();
}
/**
* Returns this object as a bundle to share between processes.
*/
- public Bundle toBundle() {
- Bundle bundle = new Bundle();
- bundle.putInt(KEY_STATE, mState);
- bundle.putLong(KEY_POSITION, mPosition);
- bundle.putLong(KEY_UPDATE_TIME, mUpdateTime);
- bundle.putFloat(KEY_SPEED, mSpeed);
- bundle.putLong(KEY_BUFFERED_POSITION, mBufferedPosition);
- bundle.putLong(KEY_ACTIVE_ITEM_ID, mActiveItemId);
- bundle.putCharSequence(KEY_ERROR_MESSAGE, mErrorMessage);
- return bundle;
+ public @NonNull Bundle toBundle() {
+ return mProvider.toBundle_impl();
}
/**
* Creates an instance from a bundle which is previously created by {@link #toBundle()}.
*
+ * @param context context
* @param bundle A bundle created by {@link #toBundle()}.
* @return A new {@link PlaybackState2} instance. Returns {@code null} if the given
* {@param bundle} is null, or if the {@param bundle} has no playback state parameters.
*/
- public static PlaybackState2 fromBundle(Bundle bundle) {
- if (bundle == null) {
- return null;
- }
-
- if (!bundle.containsKey(KEY_STATE)
- || !bundle.containsKey(KEY_POSITION)
- || !bundle.containsKey(KEY_UPDATE_TIME)
- || !bundle.containsKey(KEY_SPEED)
- || !bundle.containsKey(KEY_BUFFERED_POSITION)
- || !bundle.containsKey(KEY_ACTIVE_ITEM_ID)
- || !bundle.containsKey(KEY_ERROR_MESSAGE)) {
- return null;
- }
-
- return new PlaybackState2(
- bundle.getInt(KEY_STATE),
- bundle.getLong(KEY_POSITION),
- bundle.getLong(KEY_UPDATE_TIME),
- bundle.getFloat(KEY_SPEED),
- bundle.getLong(KEY_BUFFERED_POSITION),
- bundle.getLong(KEY_ACTIVE_ITEM_ID),
- bundle.getCharSequence(KEY_ERROR_MESSAGE));
+ public @Nullable static PlaybackState2 fromBundle(@NonNull Context context,
+ @Nullable Bundle bundle) {
+ return ApiLoader.getProvider(context).fromBundle_PlaybackState2(context, bundle);
}
}
\ No newline at end of file
diff --git a/media/java/android/media/Rating2.java b/media/java/android/media/Rating2.java
index 93aea6f..4f77ecd 100644
--- a/media/java/android/media/Rating2.java
+++ b/media/java/android/media/Rating2.java
@@ -16,7 +16,13 @@
package android.media;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.IntDef;
+import android.annotation.SystemApi;
+import android.content.Context;
+import android.media.update.ApiLoader;
+import android.media.update.Rating2Provider;
import android.os.Bundle;
import android.util.Log;
@@ -33,10 +39,8 @@
* @hide
*/
public final class Rating2 {
- private static final String TAG = "Rating2";
-
- private static final String KEY_STYLE = "android.media.rating2.style";
- private static final String KEY_VALUE = "android.media.rating2.value";
+ // Mostly same as the android.media.Rating, but it's no longer implements Parcelable for
+ // updatable support.
/**
* @hide
@@ -91,31 +95,48 @@
*/
public final static int RATING_PERCENTAGE = 6;
- private final static float RATING_NOT_RATED = -1.0f;
+ private final Rating2Provider mProvider;
- private final int mRatingStyle;
-
- private final float mRatingValue;
-
- private Rating2(@Style int ratingStyle, float rating) {
- mRatingStyle = ratingStyle;
- mRatingValue = rating;
+ /**
+ * @hide
+ */
+ @SystemApi
+ public Rating2(@NonNull Rating2Provider provider) {
+ mProvider = provider;
}
@Override
public String toString() {
- return "Rating2:style=" + mRatingStyle + " rating="
- + (mRatingValue < 0.0f ? "unrated" : String.valueOf(mRatingValue));
+ return mProvider.toString_impl();
+ }
+
+ /**
+ * @hide
+ */
+ @SystemApi
+ public Rating2Provider getProvider() {
+ return mProvider;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return mProvider.equals_impl(obj);
+ }
+
+ @Override
+ public int hashCode() {
+ return mProvider.hashCode_impl();
}
/**
* Create an instance from bundle object, previoulsy created by {@link #toBundle()}
*
+ * @param context context
* @param bundle bundle
- * @return new Rating2 instance
+ * @return new Rating2 instance or {@code null} for error
*/
- public static Rating2 fromBundle(Bundle bundle) {
- return new Rating2(bundle.getInt(KEY_STYLE), bundle.getFloat(KEY_VALUE));
+ public static Rating2 fromBundle(@NonNull Context context, @Nullable Bundle bundle) {
+ return ApiLoader.getProvider(context).fromBundle_Rating2(context, bundle);
}
/**
@@ -123,55 +144,45 @@
* @return bundle of this object
*/
public Bundle toBundle() {
- Bundle bundle = new Bundle();
- bundle.putInt(KEY_STYLE, mRatingStyle);
- bundle.putFloat(KEY_VALUE, mRatingValue);
- return bundle;
+ return mProvider.toBundle_impl();
}
/**
* Return a Rating2 instance with no rating.
* Create and return a new Rating2 instance with no rating known for the given
* rating style.
+ * @param context context
* @param ratingStyle one of {@link #RATING_HEART}, {@link #RATING_THUMB_UP_DOWN},
* {@link #RATING_3_STARS}, {@link #RATING_4_STARS}, {@link #RATING_5_STARS},
* or {@link #RATING_PERCENTAGE}.
* @return null if an invalid rating style is passed, a new Rating2 instance otherwise.
*/
- public static Rating2 newUnratedRating(@Style int ratingStyle) {
- switch(ratingStyle) {
- case RATING_HEART:
- case RATING_THUMB_UP_DOWN:
- case RATING_3_STARS:
- case RATING_4_STARS:
- case RATING_5_STARS:
- case RATING_PERCENTAGE:
- return new Rating2(ratingStyle, RATING_NOT_RATED);
- default:
- return null;
- }
+ public static @Nullable Rating2 newUnratedRating(@NonNull Context context, @Style int ratingStyle) {
+ return ApiLoader.getProvider(context).newUnratedRating_Rating2(context, ratingStyle);
}
/**
* Return a Rating2 instance with a heart-based rating.
* Create and return a new Rating2 instance with a rating style of {@link #RATING_HEART},
* and a heart-based rating.
+ * @param context context
* @param hasHeart true for a "heart selected" rating, false for "heart unselected".
* @return a new Rating2 instance.
*/
- public static Rating2 newHeartRating(boolean hasHeart) {
- return new Rating2(RATING_HEART, hasHeart ? 1.0f : 0.0f);
+ public static @Nullable Rating2 newHeartRating(@NonNull Context context, boolean hasHeart) {
+ return ApiLoader.getProvider(context).newHeartRating_Rating2(context, hasHeart);
}
/**
* Return a Rating2 instance with a thumb-based rating.
* Create and return a new Rating2 instance with a {@link #RATING_THUMB_UP_DOWN}
* rating style, and a "thumb up" or "thumb down" rating.
+ * @param context context
* @param thumbIsUp true for a "thumb up" rating, false for "thumb down".
* @return a new Rating2 instance.
*/
- public static Rating2 newThumbRating(boolean thumbIsUp) {
- return new Rating2(RATING_THUMB_UP_DOWN, thumbIsUp ? 1.0f : 0.0f);
+ public static @Nullable Rating2 newThumbRating(@NonNull Context context, boolean thumbIsUp) {
+ return ApiLoader.getProvider(context).newThumbRating_Rating2(context, thumbIsUp);
}
/**
@@ -179,6 +190,7 @@
* Create and return a new Rating2 instance with one of the star-base rating styles
* and the given integer or fractional number of stars. Non integer values can for instance
* be used to represent an average rating value, which might not be an integer number of stars.
+ * @param context context
* @param starRatingStyle one of {@link #RATING_3_STARS}, {@link #RATING_4_STARS},
* {@link #RATING_5_STARS}.
* @param starRating a number ranging from 0.0f to 3.0f, 4.0f or 5.0f according to
@@ -186,51 +198,30 @@
* @return null if the rating style is invalid, or the rating is out of range,
* a new Rating2 instance otherwise.
*/
- public static Rating2 newStarRating(@StarStyle int starRatingStyle, float starRating) {
- float maxRating = -1.0f;
- switch(starRatingStyle) {
- case RATING_3_STARS:
- maxRating = 3.0f;
- break;
- case RATING_4_STARS:
- maxRating = 4.0f;
- break;
- case RATING_5_STARS:
- maxRating = 5.0f;
- break;
- default:
- Log.e(TAG, "Invalid rating style (" + starRatingStyle + ") for a star rating");
- return null;
- }
- if ((starRating < 0.0f) || (starRating > maxRating)) {
- Log.e(TAG, "Trying to set out of range star-based rating");
- return null;
- }
- return new Rating2(starRatingStyle, starRating);
+ public static @Nullable Rating2 newStarRating(@NonNull Context context,
+ @StarStyle int starRatingStyle, float starRating) {
+ return ApiLoader.getProvider(context).newStarRating_Rating2(
+ context, starRatingStyle, starRating);
}
/**
* Return a Rating2 instance with a percentage-based rating.
* Create and return a new Rating2 instance with a {@link #RATING_PERCENTAGE}
* rating style, and a rating of the given percentage.
+ * @param context context
* @param percent the value of the rating
* @return null if the rating is out of range, a new Rating2 instance otherwise.
*/
- public static Rating2 newPercentageRating(float percent) {
- if ((percent < 0.0f) || (percent > 100.0f)) {
- Log.e(TAG, "Invalid percentage-based rating value");
- return null;
- } else {
- return new Rating2(RATING_PERCENTAGE, percent);
- }
+ public static @Nullable Rating2 newPercentageRating(@NonNull Context context, float percent) {
+ return ApiLoader.getProvider(context).newPercentageRating_Rating2(context, percent);
}
/**
* Return whether there is a rating value available.
- * @return true if the instance was not created with {@link #newUnratedRating(int)}.
+ * @return true if the instance was not created with {@link #newUnratedRating(Context, int)}.
*/
public boolean isRated() {
- return mRatingValue >= 0.0f;
+ return mProvider.isRated_impl();
}
/**
@@ -241,7 +232,7 @@
*/
@Style
public int getRatingStyle() {
- return mRatingStyle;
+ return mProvider.getRatingStyle_impl();
}
/**
@@ -250,11 +241,7 @@
* if the rating style is not {@link #RATING_HEART} or if it is unrated.
*/
public boolean hasHeart() {
- if (mRatingStyle != RATING_HEART) {
- return false;
- } else {
- return (mRatingValue == 1.0f);
- }
+ return mProvider.hasHeart_impl();
}
/**
@@ -263,11 +250,7 @@
* if the rating style is not {@link #RATING_THUMB_UP_DOWN} or if it is unrated.
*/
public boolean isThumbUp() {
- if (mRatingStyle != RATING_THUMB_UP_DOWN) {
- return false;
- } else {
- return (mRatingValue == 1.0f);
- }
+ return mProvider.isThumbUp_impl();
}
/**
@@ -276,16 +259,7 @@
* not star-based, or if it is unrated.
*/
public float getStarRating() {
- switch (mRatingStyle) {
- case RATING_3_STARS:
- case RATING_4_STARS:
- case RATING_5_STARS:
- if (isRated()) {
- return mRatingValue;
- }
- default:
- return -1.0f;
- }
+ return mProvider.getStarRating_impl();
}
/**
@@ -294,10 +268,6 @@
* not percentage-based, or if it is unrated.
*/
public float getPercentRating() {
- if ((mRatingStyle != RATING_PERCENTAGE) || !isRated()) {
- return -1.0f;
- } else {
- return mRatingValue;
- }
+ return mProvider.getPercentRating_impl();
}
}
diff --git a/media/java/android/media/VolumeProvider2.java b/media/java/android/media/VolumeProvider2.java
new file mode 100644
index 0000000..00746e2
--- /dev/null
+++ b/media/java/android/media/VolumeProvider2.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.content.Context;
+import android.media.update.ApiLoader;
+import android.media.update.VolumeProvider2Provider;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Handles requests to adjust or set the volume on a session. This is also used
+ * to push volume updates back to the session. The provider must call
+ * {@link #setCurrentVolume(int)} each time the volume being provided changes.
+ * <p>
+ * You can set a volume provider on a session by calling
+ * {@link MediaSession2#setPlayer(MediaPlayerInterface, VolumeProvider)}.
+ *
+ * @hide
+ */
+public abstract class VolumeProvider2 {
+
+ /**
+ * @hide
+ */
+ @IntDef({VOLUME_CONTROL_FIXED, VOLUME_CONTROL_RELATIVE, VOLUME_CONTROL_ABSOLUTE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ControlType {}
+
+ /**
+ * The volume is fixed and can not be modified. Requests to change volume
+ * should be ignored.
+ */
+ public static final int VOLUME_CONTROL_FIXED = 0;
+
+ /**
+ * The volume control uses relative adjustment via
+ * {@link #onAdjustVolume(int)}. Attempts to set the volume to a specific
+ * value should be ignored.
+ */
+ public static final int VOLUME_CONTROL_RELATIVE = 1;
+
+ /**
+ * The volume control uses an absolute value. It may be adjusted using
+ * {@link #onAdjustVolume(int)} or set directly using
+ * {@link #onSetVolumeTo(int)}.
+ */
+ public static final int VOLUME_CONTROL_ABSOLUTE = 2;
+
+ private final VolumeProvider2Provider mProvider;
+
+ /**
+ * Create a new volume provider for handling volume events. You must specify
+ * the type of volume control, the maximum volume that can be used, and the
+ * current volume on the output.
+ *
+ * @param controlType The method for controlling volume that is used by this provider.
+ * @param maxVolume The maximum allowed volume.
+ * @param currentVolume The current volume on the output.
+ */
+ public VolumeProvider2(@NonNull Context context, @ControlType int controlType,
+ int maxVolume, int currentVolume) {
+ mProvider = ApiLoader.getProvider(context).createVolumeProvider2(
+ context, this, controlType, maxVolume, currentVolume);
+ }
+
+ /**
+ * @hide
+ */
+ @SystemApi
+ public VolumeProvider2Provider getProvider() {
+ return mProvider;
+ }
+
+ /**
+ * Get the volume control type that this volume provider uses.
+ *
+ * @return The volume control type for this volume provider
+ */
+ @ControlType
+ public final int getControlType() {
+ return mProvider.getControlType_impl();
+ }
+
+ /**
+ * Get the maximum volume this provider allows.
+ *
+ * @return The max allowed volume.
+ */
+ public final int getMaxVolume() {
+ return mProvider.getMaxVolume_impl();
+ }
+
+ /**
+ * Gets the current volume. This will be the last value set by
+ * {@link #setCurrentVolume(int)}.
+ *
+ * @return The current volume.
+ */
+ public final int getCurrentVolume() {
+ return mProvider.getCurrentVolume_impl();
+ }
+
+ /**
+ * Notify the system that the current volume has been changed. This must be
+ * called every time the volume changes to ensure it is displayed properly.
+ *
+ * @param currentVolume The current volume on the output.
+ */
+ public final void setCurrentVolume(int currentVolume) {
+ mProvider.setCurrentVolume_impl(currentVolume);
+ }
+
+ /**
+ * Override to handle requests to set the volume of the current output.
+ * After the volume has been modified {@link #setCurrentVolume} must be
+ * called to notify the system.
+ *
+ * @param volume The volume to set the output to.
+ */
+ public void onSetVolumeTo(int volume) { }
+
+ /**
+ * Override to handle requests to adjust the volume of the current output.
+ * Direction will be one of {@link AudioManager#ADJUST_LOWER},
+ * {@link AudioManager#ADJUST_RAISE}, {@link AudioManager#ADJUST_SAME}.
+ * After the volume has been modified {@link #setCurrentVolume} must be
+ * called to notify the system.
+ *
+ * @param direction The direction to change the volume in.
+ */
+ public void onAdjustVolume(int direction) { }
+}
diff --git a/media/java/android/media/update/FrameLayoutHelper.java b/media/java/android/media/update/FrameLayoutHelper.java
new file mode 100644
index 0000000..983dc70
--- /dev/null
+++ b/media/java/android/media/update/FrameLayoutHelper.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.update;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.widget.FrameLayout;
+
+/**
+ * Helper class for connecting the public API to an updatable implementation.
+ *
+ * @see ViewProvider
+ *
+ * @hide
+ */
+public abstract class FrameLayoutHelper<T extends ViewProvider> extends FrameLayout {
+ /** @hide */
+ final public T mProvider;
+
+ /** @hide */
+ public FrameLayoutHelper(ProviderCreator<T> creator,
+ Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ mProvider = creator.createProvider(this, new SuperProvider());
+ }
+
+ /** @hide */
+ // TODO @SystemApi
+ public T getProvider() {
+ return mProvider;
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return mProvider.getAccessibilityClassName_impl();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ return mProvider.onTouchEvent_impl(ev);
+ }
+
+ @Override
+ public boolean onTrackballEvent(MotionEvent ev) {
+ return mProvider.onTrackballEvent_impl(ev);
+ }
+
+ @Override
+ public void onFinishInflate() {
+ mProvider.onFinishInflate_impl();
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ mProvider.setEnabled_impl(enabled);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ mProvider.onAttachedToWindow_impl();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ mProvider.onDetachedFromWindow_impl();
+ }
+
+ /** @hide */
+ public class SuperProvider implements ViewProvider {
+ @Override
+ public CharSequence getAccessibilityClassName_impl() {
+ return FrameLayoutHelper.super.getAccessibilityClassName();
+ }
+
+ @Override
+ public boolean onTouchEvent_impl(MotionEvent ev) {
+ return FrameLayoutHelper.super.onTouchEvent(ev);
+ }
+
+ @Override
+ public boolean onTrackballEvent_impl(MotionEvent ev) {
+ return FrameLayoutHelper.super.onTrackballEvent(ev);
+ }
+
+ @Override
+ public void onFinishInflate_impl() {
+ FrameLayoutHelper.super.onFinishInflate();
+ }
+
+ @Override
+ public void setEnabled_impl(boolean enabled) {
+ FrameLayoutHelper.super.setEnabled(enabled);
+ }
+
+ @Override
+ public void onAttachedToWindow_impl() {
+ FrameLayoutHelper.super.onAttachedToWindow();
+ }
+
+ @Override
+ public void onDetachedFromWindow_impl() {
+ FrameLayoutHelper.super.onDetachedFromWindow();
+ }
+ }
+
+ /** @hide */
+ @FunctionalInterface
+ public interface ProviderCreator<U extends ViewProvider> {
+ U createProvider(FrameLayoutHelper<U> instance, ViewProvider superProvider);
+ }
+}
diff --git a/media/java/android/media/update/MediaLibraryService2Provider.java b/media/java/android/media/update/MediaLibraryService2Provider.java
index a568839..87f509a 100644
--- a/media/java/android/media/update/MediaLibraryService2Provider.java
+++ b/media/java/android/media/update/MediaLibraryService2Provider.java
@@ -17,12 +17,15 @@
package android.media.update;
import android.annotation.SystemApi;
+import android.media.MediaLibraryService2.MediaLibrarySession;
+import android.media.MediaLibraryService2.MediaLibrarySessionCallback;
import android.media.MediaSession2.ControllerInfo;
import android.os.Bundle;
/**
* @hide
*/
+// TODO: @SystemApi
public interface MediaLibraryService2Provider extends MediaSessionService2Provider {
// Nothing new for now
diff --git a/media/java/android/media/update/MediaMetadata2Provider.java b/media/java/android/media/update/MediaMetadata2Provider.java
new file mode 100644
index 0000000..55ac43d
--- /dev/null
+++ b/media/java/android/media/update/MediaMetadata2Provider.java
@@ -0,0 +1,37 @@
+package android.media.update;
+
+import android.graphics.Bitmap;
+import android.media.MediaMetadata2;
+import android.media.MediaMetadata2.Builder;
+import android.media.Rating2;
+import android.os.Bundle;
+
+import java.util.Set;
+
+/**
+ * @hide
+ */
+// TODO(jaewan): SystemApi
+public interface MediaMetadata2Provider {
+ boolean containsKey_impl(String key);
+ CharSequence getText_impl(String key);
+ String getMediaId_impl();
+ String getString_impl(String key);
+ long getLong_impl(String key);
+ Rating2 getRating_impl(String key);
+ Bundle toBundle_impl();
+ Set<String> keySet_impl();
+ int size_impl();
+ Bitmap getBitmap_impl(String key);
+ Bundle getExtra_impl();
+
+ interface BuilderProvider {
+ Builder putText_impl(String key, CharSequence value);
+ Builder putString_impl(String key, String value);
+ Builder putLong_impl(String key, long value);
+ Builder putRating_impl(String key, Rating2 value);
+ Builder putBitmap_impl(String key, Bitmap value);
+ Builder setExtra_impl(Bundle bundle);
+ MediaMetadata2 build_impl();
+ }
+}
diff --git a/media/java/android/media/update/MediaSession2Provider.java b/media/java/android/media/update/MediaSession2Provider.java
index f9e29d9..da4d0c7 100644
--- a/media/java/android/media/update/MediaSession2Provider.java
+++ b/media/java/android/media/update/MediaSession2Provider.java
@@ -16,14 +16,18 @@
package android.media.update;
+import android.app.PendingIntent;
import android.media.MediaItem2;
+import android.media.MediaMetadata2;
import android.media.MediaPlayerInterface;
import android.media.MediaPlayerInterface.PlaybackListener;
+import android.media.MediaSession2;
import android.media.MediaSession2.Command;
import android.media.MediaSession2.CommandButton;
import android.media.MediaSession2.CommandGroup;
import android.media.MediaSession2.ControllerInfo;
import android.media.MediaSession2.PlaylistParams;
+import android.media.MediaSession2.SessionCallback;
import android.media.SessionToken2;
import android.media.VolumeProvider;
import android.os.Bundle;
@@ -37,8 +41,6 @@
*/
// TODO: @SystemApi
public interface MediaSession2Provider extends TransportControlProvider {
- void initialize();
-
void close_impl();
void setPlayer_impl(MediaPlayerInterface player);
void setPlayer_impl(MediaPlayerInterface player, VolumeProvider volumeProvider);
@@ -87,4 +89,20 @@
int hashCode_impl();
boolean equals_impl(ControllerInfoProvider obj);
}
+
+ interface PlaylistParamsProvider {
+ int getRepeatMode_impl();
+ int getShuffleMode_impl();
+ MediaMetadata2 getPlaylistMetadata_impl();
+ Bundle toBundle_impl();
+ }
+
+ interface BuilderBaseProvider<T extends MediaSession2, C extends SessionCallback> {
+ void setVolumeProvider_impl(VolumeProvider volumeProvider);
+ void setRatingType_impl(int type);
+ void setSessionActivity_impl(PendingIntent pi);
+ void setId_impl(String id);
+ void setSessionCallback_impl(Executor executor, C callback);
+ T build_impl();
+ }
}
diff --git a/media/java/android/media/update/PlaybackInfoProvider.java b/media/java/android/media/update/PlaybackInfoProvider.java
new file mode 100644
index 0000000..36eb58a
--- /dev/null
+++ b/media/java/android/media/update/PlaybackInfoProvider.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.update;
+
+import android.media.AudioAttributes;
+
+/**
+ * @hide
+ */
+// TODO(jaewan): @SystemApi
+public interface PlaybackInfoProvider {
+ int getPlaybackType_impl();
+ AudioAttributes getAudioAttributes_impl();
+ int getControlType_impl();
+ int getMaxVolume_impl();
+ int getCurrentVolume_impl();
+}
diff --git a/media/java/android/media/update/PlaybackState2Provider.java b/media/java/android/media/update/PlaybackState2Provider.java
new file mode 100644
index 0000000..2875e98
--- /dev/null
+++ b/media/java/android/media/update/PlaybackState2Provider.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.update;
+
+import android.os.Bundle;
+
+/**
+ * @hide
+ */
+// TODO(jaewan): @SystemApi
+public interface PlaybackState2Provider {
+ String toString_impl();
+
+ int getState_impl();
+
+ long getPosition_impl();
+
+ long getBufferedPosition_impl();
+
+ float getPlaybackSpeed_impl();
+
+ CharSequence getErrorMessage_impl();
+
+ long getLastPositionUpdateTime_impl();
+
+ long getCurrentPlaylistItemIndex_impl();
+
+ Bundle toBundle_impl();
+}
diff --git a/media/java/android/media/update/ProviderCreator.java b/media/java/android/media/update/ProviderCreator.java
new file mode 100644
index 0000000..f5f3e47
--- /dev/null
+++ b/media/java/android/media/update/ProviderCreator.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.update;
+
+/** @hide */
+@FunctionalInterface
+public interface ProviderCreator<T, U> {
+ U createProvider(T instance);
+}
diff --git a/media/java/android/media/update/Rating2Provider.java b/media/java/android/media/update/Rating2Provider.java
new file mode 100644
index 0000000..8966196
--- /dev/null
+++ b/media/java/android/media/update/Rating2Provider.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.update;
+
+import android.annotation.SystemApi;
+import android.os.Bundle;
+
+/**
+ * @hide
+ */
+// TODO(jaewan): @SystemApi
+public interface Rating2Provider {
+ String toString_impl();
+ boolean equals_impl(Object obj);
+ int hashCode_impl();
+ Bundle toBundle_impl();
+ boolean isRated_impl();
+ int getRatingStyle_impl();
+ boolean hasHeart_impl();
+ boolean isThumbUp_impl();
+ float getStarRating_impl();
+ float getPercentRating_impl();
+}
\ No newline at end of file
diff --git a/media/java/android/media/update/StaticProvider.java b/media/java/android/media/update/StaticProvider.java
index 9648b27..922b452 100644
--- a/media/java/android/media/update/StaticProvider.java
+++ b/media/java/android/media/update/StaticProvider.java
@@ -17,7 +17,6 @@
package android.media.update;
import android.annotation.Nullable;
-import android.app.PendingIntent;
import android.content.Context;
import android.media.DataSourceDesc;
import android.media.MediaBrowser2;
@@ -27,19 +26,24 @@
import android.media.MediaItem2;
import android.media.MediaLibraryService2;
import android.media.MediaLibraryService2.MediaLibrarySession;
+import android.media.MediaLibraryService2.MediaLibrarySessionBuilder;
import android.media.MediaLibraryService2.MediaLibrarySessionCallback;
import android.media.MediaMetadata2;
import android.media.MediaPlayerInterface;
import android.media.MediaSession2;
+import android.media.MediaSession2.PlaylistParams;
import android.media.MediaSession2.SessionCallback;
import android.media.MediaSessionService2;
+import android.media.PlaybackState2;
+import android.media.Rating2;
import android.media.SessionPlayer2;
import android.media.SessionToken2;
-import android.media.VolumeProvider;
-import android.media.update.MediaLibraryService2Provider.MediaLibrarySessionProvider;
+import android.media.VolumeProvider2;
+import android.media.update.MediaSession2Provider.BuilderBaseProvider;
import android.media.update.MediaSession2Provider.CommandGroupProvider;
import android.media.update.MediaSession2Provider.CommandProvider;
import android.media.update.MediaSession2Provider.ControllerInfoProvider;
+import android.media.update.MediaSession2Provider.PlaylistParamsProvider;
import android.os.Bundle;
import android.os.IInterface;
import android.util.AttributeSet;
@@ -62,19 +66,21 @@
VideoView2 instance, ViewProvider superProvider,
@Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes);
- MediaSession2Provider createMediaSession2(Context context, MediaSession2 instance,
- MediaPlayerInterface player, String id, VolumeProvider volumeProvider, int ratingType,
- PendingIntent sessionActivity, Executor executor, SessionCallback callback);
CommandProvider createMediaSession2Command(MediaSession2.Command instance,
int commandCode, String action, Bundle extra);
MediaSession2.Command fromBundle_MediaSession2Command(Context context, Bundle bundle);
CommandGroupProvider createMediaSession2CommandGroup(Context context,
MediaSession2.CommandGroup instance, MediaSession2.CommandGroup others);
MediaSession2.CommandGroup fromBundle_MediaSession2CommandGroup(Context context, Bundle bundle);
- ControllerInfoProvider createMediaSession2ControllerInfoProvider(Context context,
+ ControllerInfoProvider createMediaSession2ControllerInfo(Context context,
MediaSession2.ControllerInfo instance, int uid, int pid,
String packageName, IInterface callback);
-
+ PlaylistParamsProvider createMediaSession2PlaylistParams(Context context,
+ PlaylistParams playlistParams, int repeatMode, int shuffleMode,
+ MediaMetadata2 playlistMetadata);
+ PlaylistParams fromBundle_PlaylistParams(Context context, Bundle bundle);
+ BuilderBaseProvider<MediaSession2, SessionCallback> createMediaSession2Builder(
+ Context context, MediaSession2.Builder instance, MediaPlayerInterface player);
MediaController2Provider createMediaController2(Context context, MediaController2 instance,
SessionToken2 token, Executor executor, ControllerCallback callback);
@@ -83,12 +89,12 @@
SessionToken2 token, Executor executor, BrowserCallback callback);
MediaSessionService2Provider createMediaSessionService2(MediaSessionService2 instance);
- MediaSessionService2Provider createMediaLibraryService2(MediaLibraryService2 instance);
- MediaLibrarySessionProvider createMediaLibraryService2MediaLibrarySession(Context context,
- MediaLibrarySession instance, MediaPlayerInterface player, String id,
- VolumeProvider volumeProvider, int ratingType, PendingIntent sessionActivity,
- Executor executor, MediaLibrarySessionCallback callback);
+ MediaSessionService2Provider createMediaLibraryService2(MediaLibraryService2 instance);
+ BuilderBaseProvider<MediaLibrarySession, MediaLibrarySessionCallback>
+ createMediaLibraryService2Builder(
+ Context context, MediaLibrarySessionBuilder instance, MediaPlayerInterface player,
+ Executor callbackExecutor, MediaLibrarySessionCallback callback);
SessionToken2Provider createSessionToken2(Context context, SessionToken2 instance,
String packageName, String serviceName, int uid);
@@ -96,7 +102,28 @@
SessionPlayer2Provider createSessionPlayer2(Context context, SessionPlayer2 instance);
- MediaItem2Provider createMediaItem2Provider(Context context, MediaItem2 mediaItem2,
+ MediaItem2Provider createMediaItem2(Context context, MediaItem2 mediaItem2,
String mediaId, DataSourceDesc dsd, MediaMetadata2 metadata, int flags);
MediaItem2 fromBundle_MediaItem2(Context context, Bundle bundle);
+
+ VolumeProvider2Provider createVolumeProvider2(Context context, VolumeProvider2 instance,
+ int controlType, int maxVolume, int currentVolume);
+
+ MediaMetadata2 fromBundle_MediaMetadata2(Context context, Bundle bundle);
+ MediaMetadata2Provider.BuilderProvider createMediaMetadata2Builder(
+ Context context, MediaMetadata2.Builder builder);
+ MediaMetadata2Provider.BuilderProvider createMediaMetadata2Builder(
+ Context context, MediaMetadata2.Builder builder, MediaMetadata2 source);
+
+ Rating2 newUnratedRating_Rating2(Context context, int ratingStyle);
+ Rating2 fromBundle_Rating2(Context context, Bundle bundle);
+ Rating2 newHeartRating_Rating2(Context context, boolean hasHeart);
+ Rating2 newThumbRating_Rating2(Context context, boolean thumbIsUp);
+ Rating2 newStarRating_Rating2(Context context, int starRatingStyle, float starRating);
+ Rating2 newPercentageRating_Rating2(Context context, float percent);
+
+ PlaybackState2Provider createPlaybackState2(Context context, PlaybackState2 instance, int state,
+ long position, long updateTime, float speed, long bufferedPosition, long activeItemId,
+ CharSequence error);
+ PlaybackState2 fromBundle_PlaybackState2(Context context, Bundle bundle);
}
diff --git a/media/java/android/media/update/VolumeProvider2Provider.java b/media/java/android/media/update/VolumeProvider2Provider.java
new file mode 100644
index 0000000..5657af6
--- /dev/null
+++ b/media/java/android/media/update/VolumeProvider2Provider.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.media.update;
+
+/**
+ * @hide
+ */
+// TODO(jaewan): @SystemApi
+public interface VolumeProvider2Provider {
+ int getControlType_impl();
+ int getMaxVolume_impl();
+ int getCurrentVolume_impl();
+ void setCurrentVolume_impl(int currentVolume);
+}
diff --git a/media/jni/Android.bp b/media/jni/Android.bp
index 051c802..7e5f581 100644
--- a/media/jni/Android.bp
+++ b/media/jni/Android.bp
@@ -34,6 +34,7 @@
"libutils",
"libbinder",
"libmedia",
+ "libmediaextractor",
"libmedia_omx",
"libmediametrics",
"libmediadrm",
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/StaticMetadata.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/StaticMetadata.java
index 5680d9f..b3f443b 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/StaticMetadata.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/StaticMetadata.java
@@ -1860,9 +1860,6 @@
return new ArrayList<Integer>();
}
- checkArrayValuesInRange(key, availableCaps,
- CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE,
- CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO);
capList = Arrays.asList(CameraTestUtils.toObject(availableCaps));
return capList;
}
diff --git a/packages/SystemUI/res-keyguard/values/strings.xml b/packages/SystemUI/res-keyguard/values/strings.xml
index 3a41681..8a48e7b 100644
--- a/packages/SystemUI/res-keyguard/values/strings.xml
+++ b/packages/SystemUI/res-keyguard/values/strings.xml
@@ -124,6 +124,10 @@
<string name="keyboardview_keycode_delete">Delete</string>
<!-- Description of the button used to disable current carrier when the device supported embedded SIM. [CHAR LIMIT=30] -->
<string name="disable_carrier_button_text">Disable eSIM</string>
+ <!-- Title of Error message when disabling current carrier failed for the device supported embedded SIM. [CHAR LIMIT=80] -->
+ <string name="error_disable_esim_title">Can\u2019t disable eSIM</string>
+ <!-- Description of Error message when disabling current carrier failed for the device supported embedded SIM. [CHAR LIMIT=80] -->
+ <string name="error_disable_esim_msg">The eSIM can\u2019t be disabled due to an error.</string>
<!-- Description of the Enter button in a KeyboardView. [CHAR LIMIT=NONE] -->
<string name="keyboardview_keycode_enter">Enter</string>
@@ -146,8 +150,8 @@
<string name="kg_sim_pin_instructions">Enter SIM PIN.</string>
<!-- Instructions for using the SIM PIN unlock screen when there's more than one SIM -->
<string name="kg_sim_pin_instructions_multi">Enter SIM PIN for \"<xliff:g id="carrier" example="CARD 1">%1$s</xliff:g>\".</string>
- <!-- Instructions for disabling eSIM carrier to unlock the phone with embedded SIM -->
- <string name="kg_sim_lock_instructions_esim">Disable eSIM to use device without mobile service.</string>
+ <!-- Instructions for disabling eSIM carrier to unlock the phone with embedded SIM. This message follows the original SIM PIN/PUK message of device without embedded SIM. -->
+ <string name="kg_sim_lock_esim_instructions"><xliff:g id="previous_msg" example="Enter SIM PIN.">%1$s</xliff:g> Disable eSIM to use device without mobile service.</string>
<!-- Instructions for using the PIN unlock screen -->
<string name="kg_pin_instructions">Enter PIN</string>
<!-- Instructions for using the password unlock screen -->
diff --git a/packages/SystemUI/res/layout/rounded_corners.xml b/packages/SystemUI/res/layout/rounded_corners.xml
index d1ef5d6..734c877 100644
--- a/packages/SystemUI/res/layout/rounded_corners.xml
+++ b/packages/SystemUI/res/layout/rounded_corners.xml
@@ -22,7 +22,7 @@
android:id="@+id/left"
android:layout_width="12dp"
android:layout_height="12dp"
- android:layout_gravity="left"
+ android:layout_gravity="left|top"
android:tint="#ff000000"
android:src="@drawable/rounded" />
<ImageView
@@ -30,6 +30,6 @@
android:layout_width="12dp"
android:layout_height="12dp"
android:tint="#ff000000"
- android:layout_gravity="right"
+ android:layout_gravity="right|bottom"
android:src="@drawable/rounded" />
</FrameLayout>
diff --git a/packages/SystemUI/res/layout/volume_dialog_row.xml b/packages/SystemUI/res/layout/volume_dialog_row.xml
index 1d1f95b..3e80085 100644
--- a/packages/SystemUI/res/layout/volume_dialog_row.xml
+++ b/packages/SystemUI/res/layout/volume_dialog_row.xml
@@ -61,6 +61,7 @@
android:layout_width="24dp"
android:layout_height="24dp"
android:background="?android:selectableItemBackgroundBorderless"
+ android:contentDescription="@string/accessibility_output_chooser"
style="@style/VolumeButtons"
android:layout_centerVertical="true"
android:src="@drawable/ic_swap"
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 6768470..1cc1cc8 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -348,8 +348,7 @@
<item>com.android.systemui.util.leak.GarbageMonitor$Service</item>
<item>com.android.systemui.LatencyTester</item>
<item>com.android.systemui.globalactions.GlobalActionsComponent</item>
- <item>com.android.systemui.RoundedCorners</item>
- <item>com.android.systemui.EmulatedDisplayCutout</item>
+ <item>com.android.systemui.ScreenDecorations</item>
<item>com.android.systemui.fingerprint.FingerprintDialogImpl</item>
</string-array>
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index edda613..f9aa821 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -96,5 +96,7 @@
<!-- For StatusBarIconContainer to tag its icon views -->
<item type="id" name="status_bar_view_state_tag" />
+
+ <item type="id" name="display_cutout" />
</resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 9b6af43..72615ce 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1272,6 +1272,9 @@
<!-- Content description for accessibility (not shown on the screen): volume dialog collapse button. [CHAR LIMIT=NONE] -->
<string name="accessibility_volume_collapse">Collapse</string>
+ <!-- content description for audio output chooser [CHAR LIMIT=NONE]-->
+ <string name="accessibility_output_chooser">Switch output device</string>
+
<!-- Screen pinning dialog title. -->
<string name="screen_pinning_title">Screen is pinned</string>
<!-- Screen pinning dialog description. -->
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardEsimArea.java b/packages/SystemUI/src/com/android/keyguard/KeyguardEsimArea.java
index cb5afec..b8a07cd 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardEsimArea.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardEsimArea.java
@@ -16,14 +16,18 @@
package com.android.keyguard;
+import android.app.AlertDialog;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.HandlerThread;
import android.os.UserHandle;
import android.util.AttributeSet;
import android.view.View;
+import android.view.WindowManager;
import android.widget.Button;
import android.telephony.SubscriptionManager;
import android.telephony.SubscriptionInfo;
@@ -50,8 +54,17 @@
if (ACTION_DISABLE_ESIM.equals(intent.getAction())) {
int resultCode = getResultCode();
if (resultCode != EuiccManager.EMBEDDED_SUBSCRIPTION_RESULT_OK) {
- // TODO (b/62680294): Surface more info. to the end users for this failure.
Log.e(TAG, "Error disabling esim, result code = " + resultCode);
+ AlertDialog.Builder builder =
+ new AlertDialog.Builder(mContext)
+ .setMessage(R.string.error_disable_esim_msg)
+ .setTitle(R.string.error_disable_esim_title)
+ .setCancelable(false /* cancelable */)
+ .setNeutralButton(R.string.ok, null /* listener */);
+ AlertDialog alertDialog = builder.create();
+ alertDialog.getWindow().setType(
+ WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
+ alertDialog.show();
}
}
}
@@ -101,14 +114,13 @@
@Override
public void onClick(View v) {
- Intent intent = new Intent(mContext, KeyguardEsimArea.class);
- intent.setAction(ACTION_DISABLE_ESIM);
+ Intent intent = new Intent(ACTION_DISABLE_ESIM);
intent.setPackage(mContext.getPackageName());
- PendingIntent callbackIntent = PendingIntent.getBroadcast(
+ PendingIntent callbackIntent = PendingIntent.getBroadcastAsUser(
mContext,
0 /* requestCode */,
intent,
- PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent.FLAG_UPDATE_CURRENT, UserHandle.SYSTEM);
mEuiccManager
.switchToSubscription(SubscriptionManager.INVALID_SUBSCRIPTION_ID, callbackIntent);
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinView.java
index e7432ba..703b205 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinView.java
@@ -131,7 +131,7 @@
}
if (isEsimLocked) {
- msg = msg + " " + rez.getString(R.string.kg_sim_lock_instructions_esim);
+ msg = rez.getString(R.string.kg_sim_lock_esim_instructions, msg);
}
mSecurityMessageDisplay.setMessage(msg);
@@ -187,6 +187,10 @@
msgId = isDefault ? R.string.kg_sim_pin_instructions : R.string.kg_password_pin_failed;
displayMessage = getContext().getString(msgId);
}
+ if (KeyguardEsimArea.isEsimLocked(mContext, mSubId)) {
+ displayMessage = getResources()
+ .getString(R.string.kg_sim_lock_esim_instructions, displayMessage);
+ }
if (DEBUG) Log.d(LOG_TAG, "getPinPasswordErrorMessage:"
+ " attemptsRemaining=" + attemptsRemaining + " displayMessage=" + displayMessage);
return displayMessage;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukView.java
index afee8ec..347c979 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukView.java
@@ -181,7 +181,7 @@
}
}
if (isEsimLocked) {
- msg = msg + " " + rez.getString(R.string.kg_sim_lock_instructions_esim);
+ msg = rez.getString(R.string.kg_sim_lock_esim_instructions, msg);
}
mSecurityMessageDisplay.setMessage(msg);
mSimImageView.setImageTintList(ColorStateList.valueOf(color));
@@ -231,6 +231,10 @@
R.string.kg_password_puk_failed;
displayMessage = getContext().getString(msgId);
}
+ if (KeyguardEsimArea.isEsimLocked(mContext, mSubId)) {
+ displayMessage = getResources()
+ .getString(R.string.kg_sim_lock_esim_instructions, displayMessage);
+ }
if (DEBUG) Log.d(LOG_TAG, "getPukPasswordErrorMessage:"
+ " attemptsRemaining=" + attemptsRemaining + " displayMessage=" + displayMessage);
return displayMessage;
diff --git a/packages/SystemUI/src/com/android/systemui/EmulatedDisplayCutout.java b/packages/SystemUI/src/com/android/systemui/EmulatedDisplayCutout.java
deleted file mode 100644
index 5d2e4d0..0000000
--- a/packages/SystemUI/src/com/android/systemui/EmulatedDisplayCutout.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui;
-
-import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Path;
-import android.graphics.PixelFormat;
-import android.view.DisplayCutout;
-import android.view.Gravity;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewGroup.LayoutParams;
-import android.view.WindowInsets;
-import android.view.WindowManager;
-
-import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
-
-/**
- * Emulates a display cutout by drawing its shape in an overlay as supplied by
- * {@link DisplayCutout}.
- */
-public class EmulatedDisplayCutout extends SystemUI implements ConfigurationListener {
- private View mOverlay;
- private boolean mAttached;
- private WindowManager mWindowManager;
-
- @Override
- public void start() {
- Dependency.get(ConfigurationController.class).addCallback(this);
-
- mWindowManager = mContext.getSystemService(WindowManager.class);
- updateAttached();
- }
-
- @Override
- public void onOverlayChanged() {
- updateAttached();
- }
-
- private void updateAttached() {
- boolean shouldAttach = mContext.getResources().getBoolean(
- com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout);
- setAttached(shouldAttach);
- }
-
- private void setAttached(boolean attached) {
- if (attached && !mAttached) {
- if (mOverlay == null) {
- mOverlay = new CutoutView(mContext);
- mOverlay.setLayoutParams(getLayoutParams());
- }
- mWindowManager.addView(mOverlay, mOverlay.getLayoutParams());
- mAttached = true;
- } else if (!attached && mAttached) {
- mWindowManager.removeView(mOverlay);
- mAttached = false;
- }
- }
-
- private WindowManager.LayoutParams getLayoutParams() {
- final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- LayoutParams.MATCH_PARENT,
- WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
- WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
- | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
- | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
- | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
- | WindowManager.LayoutParams.FLAG_SLIPPERY
- | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
- | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR,
- PixelFormat.TRANSLUCENT);
- lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS
- | WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY;
- lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
- lp.setTitle("EmulatedDisplayCutout");
- lp.gravity = Gravity.TOP;
- return lp;
- }
-
- private static class CutoutView extends View {
- private final Paint mPaint = new Paint();
- private final Path mBounds = new Path();
-
- CutoutView(Context context) {
- super(context);
- }
-
- @Override
- public WindowInsets onApplyWindowInsets(WindowInsets insets) {
- mBounds.reset();
- if (insets.getDisplayCutout() != null) {
- insets.getDisplayCutout().getBounds().getBoundaryPath(mBounds);
- }
- invalidate();
- return insets.consumeDisplayCutout();
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- if (!mBounds.isEmpty()) {
- mPaint.setColor(Color.BLACK);
- mPaint.setStyle(Paint.Style.FILL);
-
- canvas.drawPath(mBounds, mPaint);
- }
- }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/RoundedCorners.java b/packages/SystemUI/src/com/android/systemui/RoundedCorners.java
deleted file mode 100644
index c960fa1..0000000
--- a/packages/SystemUI/src/com/android/systemui/RoundedCorners.java
+++ /dev/null
@@ -1,221 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
- * except in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the
- * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-package com.android.systemui;
-
-import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
-
-import static com.android.systemui.tuner.TunablePadding.FLAG_START;
-import static com.android.systemui.tuner.TunablePadding.FLAG_END;
-
-import android.app.Fragment;
-import android.content.res.ColorStateList;
-import android.graphics.Color;
-import android.graphics.PixelFormat;
-import android.provider.Settings.Secure;
-import android.support.annotation.VisibleForTesting;
-import android.util.DisplayMetrics;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.View.OnLayoutChangeListener;
-import android.view.ViewGroup;
-import android.view.ViewGroup.LayoutParams;
-import android.view.WindowManager;
-import android.widget.ImageView;
-
-import com.android.systemui.R.id;
-import com.android.systemui.fragments.FragmentHostManager;
-import com.android.systemui.fragments.FragmentHostManager.FragmentListener;
-import com.android.systemui.plugins.qs.QS;
-import com.android.systemui.qs.SecureSetting;
-import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment;
-import com.android.systemui.statusbar.phone.NavigationBarFragment;
-import com.android.systemui.statusbar.phone.StatusBar;
-import com.android.systemui.tuner.TunablePadding;
-import com.android.systemui.tuner.TunerService;
-import com.android.systemui.tuner.TunerService.Tunable;
-
-public class RoundedCorners extends SystemUI implements Tunable {
- public static final String SIZE = "sysui_rounded_size";
- public static final String PADDING = "sysui_rounded_content_padding";
-
- private int mRoundedDefault;
- private View mOverlay;
- private View mBottomOverlay;
- private float mDensity;
- private TunablePadding mQsPadding;
- private TunablePadding mStatusBarPadding;
- private TunablePadding mNavBarPadding;
-
- @Override
- public void start() {
- mRoundedDefault = mContext.getResources().getDimensionPixelSize(
- R.dimen.rounded_corner_radius);
- if (mRoundedDefault != 0) {
- setupRounding();
- }
- int padding = mContext.getResources().getDimensionPixelSize(
- R.dimen.rounded_corner_content_padding);
- if (padding != 0) {
- setupPadding(padding);
- }
- }
-
- private void setupRounding() {
- mOverlay = LayoutInflater.from(mContext)
- .inflate(R.layout.rounded_corners, null);
- mOverlay.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
- mOverlay.setAlpha(0);
- mOverlay.findViewById(R.id.right).setRotation(90);
-
- mContext.getSystemService(WindowManager.class)
- .addView(mOverlay, getWindowLayoutParams());
- mBottomOverlay = LayoutInflater.from(mContext)
- .inflate(R.layout.rounded_corners, null);
- mBottomOverlay.setAlpha(0);
- mBottomOverlay.findViewById(R.id.right).setRotation(180);
- mBottomOverlay.findViewById(R.id.left).setRotation(270);
- WindowManager.LayoutParams layoutParams = getWindowLayoutParams();
- layoutParams.gravity = Gravity.BOTTOM;
- mContext.getSystemService(WindowManager.class)
- .addView(mBottomOverlay, layoutParams);
-
- DisplayMetrics metrics = new DisplayMetrics();
- mContext.getSystemService(WindowManager.class)
- .getDefaultDisplay().getMetrics(metrics);
- mDensity = metrics.density;
-
- Dependency.get(TunerService.class).addTunable(this, SIZE);
-
- // Watch color inversion and invert the overlay as needed.
- SecureSetting setting = new SecureSetting(mContext, Dependency.get(Dependency.MAIN_HANDLER),
- Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED) {
- @Override
- protected void handleValueChanged(int value, boolean observedChange) {
- int tint = value != 0 ? Color.WHITE : Color.BLACK;
- ColorStateList tintList = ColorStateList.valueOf(tint);
- ((ImageView) mOverlay.findViewById(id.left)).setImageTintList(tintList);
- ((ImageView) mOverlay.findViewById(id.right)).setImageTintList(tintList);
- ((ImageView) mBottomOverlay.findViewById(id.left)).setImageTintList(tintList);
- ((ImageView) mBottomOverlay.findViewById(id.right)).setImageTintList(tintList);
- }
- };
- setting.setListening(true);
- setting.onChange(false);
-
- mOverlay.addOnLayoutChangeListener(new OnLayoutChangeListener() {
- @Override
- public void onLayoutChange(View v, int left, int top, int right, int bottom,
- int oldLeft,
- int oldTop, int oldRight, int oldBottom) {
- mOverlay.removeOnLayoutChangeListener(this);
- mOverlay.animate()
- .alpha(1)
- .setDuration(1000)
- .start();
- mBottomOverlay.animate()
- .alpha(1)
- .setDuration(1000)
- .start();
- }
- });
- }
-
- private void setupPadding(int padding) {
- // Add some padding to all the content near the edge of the screen.
- StatusBar sb = getComponent(StatusBar.class);
- View statusBar = (sb != null ? sb.getStatusBarWindow() : null);
- if (statusBar != null) {
- TunablePadding.addTunablePadding(statusBar.findViewById(R.id.keyguard_header), PADDING,
- padding, FLAG_END);
-
- FragmentHostManager fragmentHostManager = FragmentHostManager.get(statusBar);
- fragmentHostManager.addTagListener(CollapsedStatusBarFragment.TAG,
- new TunablePaddingTagListener(padding, R.id.status_bar));
- fragmentHostManager.addTagListener(QS.TAG,
- new TunablePaddingTagListener(padding, R.id.header));
- }
- }
-
- private WindowManager.LayoutParams getWindowLayoutParams() {
- final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- LayoutParams.WRAP_CONTENT,
- WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
- WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
- | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
- | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
- | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
- | WindowManager.LayoutParams.FLAG_SLIPPERY
- | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
- PixelFormat.TRANSLUCENT);
- lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS
- | WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY;
- lp.setTitle("RoundedOverlay");
- lp.gravity = Gravity.TOP;
- lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
- return lp;
- }
-
-
- @Override
- public void onTuningChanged(String key, String newValue) {
- if (mOverlay == null) return;
- if (SIZE.equals(key)) {
- int size = mRoundedDefault;
- try {
- size = (int) (Integer.parseInt(newValue) * mDensity);
- } catch (Exception e) {
- }
- setSize(mOverlay.findViewById(R.id.left), size);
- setSize(mOverlay.findViewById(R.id.right), size);
- setSize(mBottomOverlay.findViewById(R.id.left), size);
- setSize(mBottomOverlay.findViewById(R.id.right), size);
- }
- }
-
- private void setSize(View view, int pixelSize) {
- LayoutParams params = view.getLayoutParams();
- params.width = pixelSize;
- params.height = pixelSize;
- view.setLayoutParams(params);
- }
-
- @VisibleForTesting
- static class TunablePaddingTagListener implements FragmentListener {
-
- private final int mPadding;
- private final int mId;
- private TunablePadding mTunablePadding;
-
- public TunablePaddingTagListener(int padding, int id) {
- mPadding = padding;
- mId = id;
- }
-
- @Override
- public void onFragmentViewCreated(String tag, Fragment fragment) {
- if (mTunablePadding != null) {
- mTunablePadding.destroy();
- }
- View view = fragment.getView();
- if (mId != 0) {
- view = view.findViewById(mId);
- }
- mTunablePadding = TunablePadding.addTunablePadding(view, PADDING, mPadding,
- FLAG_START | FLAG_END);
- }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
new file mode 100644
index 0000000..0b3e9e5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
@@ -0,0 +1,415 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui;
+
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+
+import static com.android.systemui.tuner.TunablePadding.FLAG_START;
+import static com.android.systemui.tuner.TunablePadding.FLAG_END;
+
+import android.app.Fragment;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Configuration;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.hardware.display.DisplayManager;
+import android.provider.Settings.Secure;
+import android.support.annotation.VisibleForTesting;
+import android.util.DisplayMetrics;
+import android.view.DisplayCutout;
+import android.view.DisplayInfo;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnLayoutChangeListener;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.fragments.FragmentHostManager.FragmentListener;
+import com.android.systemui.plugins.qs.QS;
+import com.android.systemui.qs.SecureSetting;
+import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment;
+import com.android.systemui.statusbar.phone.StatusBar;
+import com.android.systemui.tuner.TunablePadding;
+import com.android.systemui.tuner.TunerService;
+import com.android.systemui.tuner.TunerService.Tunable;
+
+/**
+ * An overlay that draws screen decorations in software (e.g for rounded corners or display cutout)
+ * for antialiasing and emulation purposes.
+ */
+public class ScreenDecorations extends SystemUI implements Tunable {
+ public static final String SIZE = "sysui_rounded_size";
+ public static final String PADDING = "sysui_rounded_content_padding";
+
+ private int mRoundedDefault;
+ private View mOverlay;
+ private View mBottomOverlay;
+ private float mDensity;
+ private WindowManager mWindowManager;
+ private boolean mLandscape;
+
+ @Override
+ public void start() {
+ mWindowManager = mContext.getSystemService(WindowManager.class);
+ mRoundedDefault = mContext.getResources().getDimensionPixelSize(
+ R.dimen.rounded_corner_radius);
+ if (mRoundedDefault != 0 || shouldDrawCutout()) {
+ setupDecorations();
+ }
+ int padding = mContext.getResources().getDimensionPixelSize(
+ R.dimen.rounded_corner_content_padding);
+ if (padding != 0) {
+ setupPadding(padding);
+ }
+ }
+
+ private void setupDecorations() {
+ mOverlay = LayoutInflater.from(mContext)
+ .inflate(R.layout.rounded_corners, null);
+ ((ViewGroup)mOverlay).addView(new DisplayCutoutView(mContext, true,
+ this::updateWindowVisibilities));
+ mBottomOverlay = LayoutInflater.from(mContext)
+ .inflate(R.layout.rounded_corners, null);
+ ((ViewGroup)mBottomOverlay).addView(new DisplayCutoutView(mContext, false,
+ this::updateWindowVisibilities));
+
+ mOverlay.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+ mOverlay.setAlpha(0);
+
+ mBottomOverlay.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+ mBottomOverlay.setAlpha(0);
+
+ updateViews();
+
+ mWindowManager.addView(mOverlay, getWindowLayoutParams());
+ mWindowManager.addView(mBottomOverlay, getBottomLayoutParams());
+
+ DisplayMetrics metrics = new DisplayMetrics();
+ mWindowManager.getDefaultDisplay().getMetrics(metrics);
+ mDensity = metrics.density;
+
+ Dependency.get(TunerService.class).addTunable(this, SIZE);
+
+ // Watch color inversion and invert the overlay as needed.
+ SecureSetting setting = new SecureSetting(mContext, Dependency.get(Dependency.MAIN_HANDLER),
+ Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED) {
+ @Override
+ protected void handleValueChanged(int value, boolean observedChange) {
+ int tint = value != 0 ? Color.WHITE : Color.BLACK;
+ ColorStateList tintList = ColorStateList.valueOf(tint);
+ ((ImageView) mOverlay.findViewById(R.id.left)).setImageTintList(tintList);
+ ((ImageView) mOverlay.findViewById(R.id.right)).setImageTintList(tintList);
+ ((ImageView) mBottomOverlay.findViewById(R.id.left)).setImageTintList(tintList);
+ ((ImageView) mBottomOverlay.findViewById(R.id.right)).setImageTintList(tintList);
+ }
+ };
+ setting.setListening(true);
+ setting.onChange(false);
+
+ mOverlay.addOnLayoutChangeListener(new OnLayoutChangeListener() {
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right, int bottom,
+ int oldLeft,
+ int oldTop, int oldRight, int oldBottom) {
+ mOverlay.removeOnLayoutChangeListener(this);
+ mOverlay.animate()
+ .alpha(1)
+ .setDuration(1000)
+ .start();
+ mBottomOverlay.animate()
+ .alpha(1)
+ .setDuration(1000)
+ .start();
+ }
+ });
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ boolean newLanscape = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE;
+ if (newLanscape != mLandscape) {
+ mLandscape = newLanscape;
+
+ if (mOverlay != null) {
+ updateLayoutParams();
+ updateViews();
+ }
+ }
+ if (shouldDrawCutout() && mOverlay == null) {
+ setupDecorations();
+ }
+ }
+
+ private void updateViews() {
+ View topLeft = mOverlay.findViewById(R.id.left);
+ View topRight = mOverlay.findViewById(R.id.right);
+ View bottomLeft = mBottomOverlay.findViewById(R.id.left);
+ View bottomRight = mBottomOverlay.findViewById(R.id.right);
+ if (mLandscape) {
+ // Flip corners
+ View tmp = topRight;
+ topRight = bottomLeft;
+ bottomLeft = tmp;
+ }
+ updateView(topLeft, Gravity.TOP | Gravity.LEFT, 0);
+ updateView(topRight, Gravity.TOP | Gravity.RIGHT, 90);
+ updateView(bottomLeft, Gravity.BOTTOM | Gravity.LEFT, 270);
+ updateView(bottomRight, Gravity.BOTTOM | Gravity.RIGHT, 180);
+
+ updateWindowVisibilities();
+ }
+
+ private void updateView(View v, int gravity, int rotation) {
+ ((FrameLayout.LayoutParams)v.getLayoutParams()).gravity = gravity;
+ v.setRotation(rotation);
+ }
+
+ private void updateWindowVisibilities() {
+ updateWindowVisibility(mOverlay);
+ updateWindowVisibility(mBottomOverlay);
+ }
+
+ private void updateWindowVisibility(View overlay) {
+ boolean visibleForCutout = shouldDrawCutout()
+ && overlay.findViewById(R.id.display_cutout).getVisibility() == View.VISIBLE;
+ boolean visibleForRoundedCorners = mRoundedDefault > 0;
+ overlay.setVisibility(visibleForCutout || visibleForRoundedCorners
+ ? View.VISIBLE : View.GONE);
+ }
+
+ private boolean shouldDrawCutout() {
+ return mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout);
+ }
+
+ private void setupPadding(int padding) {
+ // Add some padding to all the content near the edge of the screen.
+ StatusBar sb = getComponent(StatusBar.class);
+ View statusBar = (sb != null ? sb.getStatusBarWindow() : null);
+ if (statusBar != null) {
+ TunablePadding.addTunablePadding(statusBar.findViewById(R.id.keyguard_header), PADDING,
+ padding, FLAG_END);
+
+ FragmentHostManager fragmentHostManager = FragmentHostManager.get(statusBar);
+ fragmentHostManager.addTagListener(CollapsedStatusBarFragment.TAG,
+ new TunablePaddingTagListener(padding, R.id.status_bar));
+ fragmentHostManager.addTagListener(QS.TAG,
+ new TunablePaddingTagListener(padding, R.id.header));
+ }
+ }
+
+ @VisibleForTesting
+ WindowManager.LayoutParams getWindowLayoutParams() {
+ final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ LayoutParams.WRAP_CONTENT,
+ WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
+ WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
+ | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+ | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
+ | WindowManager.LayoutParams.FLAG_SLIPPERY
+ | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
+ PixelFormat.TRANSLUCENT);
+ lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS
+ | WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY;
+ lp.setTitle("ScreenDecorOverlay");
+ lp.gravity = Gravity.TOP | Gravity.LEFT;
+ lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+ if (mLandscape) {
+ lp.width = WRAP_CONTENT;
+ lp.height = MATCH_PARENT;
+ }
+ return lp;
+ }
+
+ private WindowManager.LayoutParams getBottomLayoutParams() {
+ WindowManager.LayoutParams lp = getWindowLayoutParams();
+ lp.setTitle("ScreenDecorOverlayBottom");
+ lp.gravity = Gravity.BOTTOM | Gravity.RIGHT;
+ return lp;
+ }
+
+ private void updateLayoutParams() {
+ mWindowManager.updateViewLayout(mOverlay, getWindowLayoutParams());
+ mWindowManager.updateViewLayout(mBottomOverlay, getBottomLayoutParams());
+ }
+
+ @Override
+ public void onTuningChanged(String key, String newValue) {
+ if (mOverlay == null) return;
+ if (SIZE.equals(key)) {
+ int size = mRoundedDefault;
+ try {
+ size = (int) (Integer.parseInt(newValue) * mDensity);
+ } catch (Exception e) {
+ }
+ setSize(mOverlay.findViewById(R.id.left), size);
+ setSize(mOverlay.findViewById(R.id.right), size);
+ setSize(mBottomOverlay.findViewById(R.id.left), size);
+ setSize(mBottomOverlay.findViewById(R.id.right), size);
+ }
+ }
+
+ private void setSize(View view, int pixelSize) {
+ LayoutParams params = view.getLayoutParams();
+ params.width = pixelSize;
+ params.height = pixelSize;
+ view.setLayoutParams(params);
+ }
+
+ @VisibleForTesting
+ static class TunablePaddingTagListener implements FragmentListener {
+
+ private final int mPadding;
+ private final int mId;
+ private TunablePadding mTunablePadding;
+
+ public TunablePaddingTagListener(int padding, int id) {
+ mPadding = padding;
+ mId = id;
+ }
+
+ @Override
+ public void onFragmentViewCreated(String tag, Fragment fragment) {
+ if (mTunablePadding != null) {
+ mTunablePadding.destroy();
+ }
+ View view = fragment.getView();
+ if (mId != 0) {
+ view = view.findViewById(mId);
+ }
+ mTunablePadding = TunablePadding.addTunablePadding(view, PADDING, mPadding,
+ FLAG_START | FLAG_END);
+ }
+ }
+
+ public static class DisplayCutoutView extends View implements DisplayManager.DisplayListener {
+
+ private final DisplayInfo mInfo = new DisplayInfo();
+ private final Paint mPaint = new Paint();
+ private final Rect mBoundingRect = new Rect();
+ private final Path mBoundingPath = new Path();
+ private final int[] mLocation = new int[2];
+ private final boolean mStart;
+ private final Runnable mVisibilityChangedListener;
+
+ public DisplayCutoutView(Context context, boolean start,
+ Runnable visibilityChangedListener) {
+ super(context);
+ mStart = start;
+ mVisibilityChangedListener = visibilityChangedListener;
+ setId(R.id.display_cutout);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mContext.getSystemService(DisplayManager.class).registerDisplayListener(this,
+ getHandler());
+ update();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mContext.getSystemService(DisplayManager.class).unregisterDisplayListener(this);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ getLocationOnScreen(mLocation);
+ canvas.translate(-mLocation[0], -mLocation[1]);
+ if (!mBoundingPath.isEmpty()) {
+ mPaint.setColor(Color.BLACK);
+ mPaint.setStyle(Paint.Style.FILL);
+ canvas.drawPath(mBoundingPath, mPaint);
+ }
+ }
+
+ @Override
+ public void onDisplayAdded(int displayId) {
+ }
+
+ @Override
+ public void onDisplayRemoved(int displayId) {
+ }
+
+ @Override
+ public void onDisplayChanged(int displayId) {
+ if (displayId == getDisplay().getDisplayId()) {
+ update();
+ }
+ }
+
+ private void update() {
+ requestLayout();
+ getDisplay().getDisplayInfo(mInfo);
+ mBoundingRect.setEmpty();
+ mBoundingPath.reset();
+ int newVisible;
+ if (hasCutout()) {
+ mBoundingRect.set(mInfo.displayCutout.getBoundingRect());
+ mInfo.displayCutout.getBounds().getBoundaryPath(mBoundingPath);
+ newVisible = VISIBLE;
+ } else {
+ newVisible = GONE;
+ }
+ if (newVisible != getVisibility()) {
+ setVisibility(newVisible);
+ mVisibilityChangedListener.run();
+ }
+ }
+
+ private boolean hasCutout() {
+ if (mInfo.displayCutout == null) {
+ return false;
+ }
+ DisplayCutout displayCutout = mInfo.displayCutout.calculateRelativeTo(
+ new Rect(0, 0, mInfo.logicalWidth, mInfo.logicalHeight));
+ if (mStart) {
+ return displayCutout.getSafeInsetLeft() > 0
+ || displayCutout.getSafeInsetTop() > 0;
+ } else {
+ return displayCutout.getSafeInsetRight() > 0
+ || displayCutout.getSafeInsetBottom() > 0;
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ if (mBoundingRect.isEmpty()) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ return;
+ }
+ setMeasuredDimension(
+ resolveSizeAndState(mBoundingRect.width(), widthMeasureSpec, 0),
+ resolveSizeAndState(mBoundingRect.height(), heightMeasureSpec, 0));
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
index 238ab29..c28b7ee 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
@@ -147,6 +147,7 @@
private boolean mHasTelephony;
private boolean mHasVibrator;
private boolean mHasLogoutButton;
+ private boolean mHasLockdownButton;
private final boolean mShowSilentToggle;
private final EmergencyAffordanceManager mEmergencyAffordanceManager;
private final ScreenshotHelper mScreenshotHelper;
@@ -311,6 +312,7 @@
ArraySet<String> addedKeys = new ArraySet<String>();
mHasLogoutButton = false;
+ mHasLockdownButton = false;
for (int i = 0; i < defaultActions.length; i++) {
String actionKey = defaultActions[i];
if (addedKeys.contains(actionKey)) {
@@ -341,6 +343,7 @@
Settings.Secure.LOCKDOWN_IN_POWER_MENU, 0) != 0
&& shouldDisplayLockdown()) {
mItems.add(getLockdownAction());
+ mHasLockdownButton = true;
}
} else if (GLOBAL_ACTION_KEY_VOICEASSIST.equals(actionKey)) {
mItems.add(getVoiceAssistAction());
@@ -871,10 +874,9 @@
public View getView(int position, View convertView, ViewGroup parent) {
Action action = getItem(position);
View view = action.create(mContext, convertView, parent, LayoutInflater.from(mContext));
- // When there is no logout button, only power off and restart should be in white
- // background, thus setting division view at third item; with logout button being the
- // third item, set the division view at fourth item instead.
- if (position == (mHasLogoutButton ? 3 : 2)) {
+ // Power off, restart, logout (if present) and lockdown (if present) should be in white
+ // background. Set the division based on which buttons are currently being displayed.
+ if (position == 2 + (mHasLogoutButton ? 1 : 0) + (mHasLockdownButton ? 1 : 0)) {
HardwareUiLayout.get(parent).setDivisionView(view);
}
return view;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
index 3ebeb4d..3dfb913 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
@@ -379,7 +379,7 @@
// Because space is usually constrained in the auto use-case, there should not be a
// pinned notification when the shade has been expanded. Ensure this by removing all heads-
// up notifications.
- mHeadsUpManager.removeAllHeadsUpEntries();
+ mHeadsUpManager.releaseAllImmediately();
super.animateExpandNotificationsPanel();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
new file mode 100644
index 0000000..c45c538
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
@@ -0,0 +1,455 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Resources;
+import android.support.v4.util.ArraySet;
+import android.util.Log;
+import android.util.Pools;
+import android.view.View;
+import android.view.ViewTreeObserver;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.Dumpable;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.HashSet;
+import java.util.Stack;
+
+/**
+ * A implementation of HeadsUpManager for phone and car.
+ */
+public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable,
+ ViewTreeObserver.OnComputeInternalInsetsListener, VisualStabilityManager.Callback,
+ OnHeadsUpChangedListener {
+ private static final String TAG = "HeadsUpManagerPhone";
+ private static final boolean DEBUG = false;
+
+ private final View mStatusBarWindowView;
+ private final int mStatusBarHeight;
+ private final NotificationGroupManager mGroupManager;
+ private final StatusBar mBar;
+ private final VisualStabilityManager mVisualStabilityManager;
+
+ private boolean mReleaseOnExpandFinish;
+ private boolean mTrackingHeadsUp;
+ private HashSet<String> mSwipedOutKeys = new HashSet<>();
+ private HashSet<NotificationData.Entry> mEntriesToRemoveAfterExpand = new HashSet<>();
+ private ArraySet<NotificationData.Entry> mEntriesToRemoveWhenReorderingAllowed
+ = new ArraySet<>();
+ private boolean mIsExpanded;
+ private int[] mTmpTwoArray = new int[2];
+ private boolean mHeadsUpGoingAway;
+ private boolean mWaitingOnCollapseWhenGoingAway;
+ private boolean mIsObserving;
+ private int mStatusBarState;
+
+ private final Pools.Pool<HeadsUpEntryPhone> mEntryPool = new Pools.Pool<HeadsUpEntryPhone>() {
+ private Stack<HeadsUpEntryPhone> mPoolObjects = new Stack<>();
+
+ @Override
+ public HeadsUpEntryPhone acquire() {
+ if (!mPoolObjects.isEmpty()) {
+ return mPoolObjects.pop();
+ }
+ return new HeadsUpEntryPhone();
+ }
+
+ @Override
+ public boolean release(@NonNull HeadsUpEntryPhone instance) {
+ instance.reset();
+ mPoolObjects.push(instance);
+ return true;
+ }
+ };
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Constructor:
+
+ public HeadsUpManagerPhone(@NonNull final Context context, @NonNull View statusBarWindowView,
+ @NonNull NotificationGroupManager groupManager, @NonNull StatusBar bar,
+ @NonNull VisualStabilityManager visualStabilityManager) {
+ super(context);
+
+ mStatusBarWindowView = statusBarWindowView;
+ mGroupManager = groupManager;
+ mBar = bar;
+ mVisualStabilityManager = visualStabilityManager;
+
+ Resources resources = mContext.getResources();
+ mStatusBarHeight = resources.getDimensionPixelSize(
+ com.android.internal.R.dimen.status_bar_height);
+
+ addListener(new OnHeadsUpChangedListener() {
+ @Override
+ public void onHeadsUpPinnedModeChanged(boolean hasPinnedNotification) {
+ if (DEBUG) Log.w(TAG, "onHeadsUpPinnedModeChanged");
+ updateTouchableRegionListener();
+ }
+ });
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Public methods:
+
+ /**
+ * Decides whether a click is invalid for a notification, i.e it has not been shown long enough
+ * that a user might have consciously clicked on it.
+ *
+ * @param key the key of the touched notification
+ * @return whether the touch is invalid and should be discarded
+ */
+ public boolean shouldSwallowClick(@NonNull String key) {
+ HeadsUpManager.HeadsUpEntry entry = getHeadsUpEntry(key);
+ if (entry != null && mClock.currentTimeMillis() < entry.postTime) {
+ return true;
+ }
+ return false;
+ }
+
+ public void onExpandingFinished() {
+ if (mReleaseOnExpandFinish) {
+ releaseAllImmediately();
+ mReleaseOnExpandFinish = false;
+ } else {
+ for (NotificationData.Entry entry : mEntriesToRemoveAfterExpand) {
+ if (isHeadsUp(entry.key)) {
+ // Maybe the heads-up was removed already
+ removeHeadsUpEntry(entry);
+ }
+ }
+ }
+ mEntriesToRemoveAfterExpand.clear();
+ }
+
+ /**
+ * Sets the tracking-heads-up flag. If the flag is true, HeadsUpManager doesn't remove the entry
+ * from the list even after a Heads Up Notification is gone.
+ */
+ public void setTrackingHeadsUp(boolean trackingHeadsUp) {
+ mTrackingHeadsUp = trackingHeadsUp;
+ }
+
+ /**
+ * Notify that the status bar panel gets expanded or collapsed.
+ *
+ * @param isExpanded True to notify expanded, false to notify collapsed.
+ */
+ public void setIsPanelExpanded(boolean isExpanded) {
+ if (isExpanded != mIsExpanded) {
+ mIsExpanded = isExpanded;
+ if (isExpanded) {
+ // make sure our state is sane
+ mWaitingOnCollapseWhenGoingAway = false;
+ mHeadsUpGoingAway = false;
+ updateTouchableRegionListener();
+ }
+ }
+ }
+
+ /**
+ * Set the current state of the statusbar.
+ */
+ public void setStatusBarState(int statusBarState) {
+ mStatusBarState = statusBarState;
+ }
+
+ /**
+ * Set that we are exiting the headsUp pinned mode, but some notifications might still be
+ * animating out. This is used to keep the touchable regions in a sane state.
+ */
+ public void setHeadsUpGoingAway(boolean headsUpGoingAway) {
+ if (headsUpGoingAway != mHeadsUpGoingAway) {
+ mHeadsUpGoingAway = headsUpGoingAway;
+ if (!headsUpGoingAway) {
+ waitForStatusBarLayout();
+ }
+ updateTouchableRegionListener();
+ }
+ }
+
+ /**
+ * Notifies that a remote input textbox in notification gets active or inactive.
+ * @param entry The entry of the target notification.
+ * @param remoteInputActive True to notify active, False to notify inactive.
+ */
+ public void setRemoteInputActive(
+ @NonNull NotificationData.Entry entry, boolean remoteInputActive) {
+ HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(entry.key);
+ if (headsUpEntry != null && headsUpEntry.remoteInputActive != remoteInputActive) {
+ headsUpEntry.remoteInputActive = remoteInputActive;
+ if (remoteInputActive) {
+ headsUpEntry.removeAutoRemovalCallbacks();
+ } else {
+ headsUpEntry.updateEntry(false /* updatePostTime */);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public void removeMinimumDisplayTimeForTesting() {
+ mMinimumDisplayTime = 1;
+ mHeadsUpNotificationDecay = 1;
+ mTouchAcceptanceDelay = 1;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // HeadsUpManager public methods overrides:
+
+ @Override
+ public boolean isTrackingHeadsUp() {
+ return mTrackingHeadsUp;
+ }
+
+ @Override
+ public void snooze() {
+ super.snooze();
+ mReleaseOnExpandFinish = true;
+ }
+
+ /**
+ * React to the removal of the notification in the heads up.
+ *
+ * @return true if the notification was removed and false if it still needs to be kept around
+ * for a bit since it wasn't shown long enough
+ */
+ @Override
+ public boolean removeNotification(@NonNull String key, boolean ignoreEarliestRemovalTime) {
+ if (wasShownLongEnough(key) || ignoreEarliestRemovalTime) {
+ return super.removeNotification(key, ignoreEarliestRemovalTime);
+ } else {
+ HeadsUpEntryPhone entry = getHeadsUpEntryPhone(key);
+ entry.removeAsSoonAsPossible();
+ return false;
+ }
+ }
+
+ public void addSwipedOutNotification(@NonNull String key) {
+ mSwipedOutKeys.add(key);
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Dumpable overrides:
+
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("HeadsUpManagerPhone state:");
+ dumpInternal(fd, pw, args);
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ViewTreeObserver.OnComputeInternalInsetsListener overrides:
+
+ /**
+ * Overridden from TreeObserver.
+ */
+ @Override
+ public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) {
+ if (mIsExpanded || mBar.isBouncerShowing()) {
+ // The touchable region is always the full area when expanded
+ return;
+ }
+ if (hasPinnedHeadsUp()) {
+ ExpandableNotificationRow topEntry = getTopEntry().row;
+ if (topEntry.isChildInGroup()) {
+ final ExpandableNotificationRow groupSummary
+ = mGroupManager.getGroupSummary(topEntry.getStatusBarNotification());
+ if (groupSummary != null) {
+ topEntry = groupSummary;
+ }
+ }
+ topEntry.getLocationOnScreen(mTmpTwoArray);
+ int minX = mTmpTwoArray[0];
+ int maxX = mTmpTwoArray[0] + topEntry.getWidth();
+ int maxY = topEntry.getIntrinsicHeight();
+
+ info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+ info.touchableRegion.set(minX, 0, maxX, maxY);
+ } else if (mHeadsUpGoingAway || mWaitingOnCollapseWhenGoingAway) {
+ info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+ info.touchableRegion.set(0, 0, mStatusBarWindowView.getWidth(), mStatusBarHeight);
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // VisualStabilityManager.Callback overrides:
+
+ @Override
+ public void onReorderingAllowed() {
+ mBar.getNotificationScrollLayout().setHeadsUpGoingAwayAnimationsAllowed(false);
+ for (NotificationData.Entry entry : mEntriesToRemoveWhenReorderingAllowed) {
+ if (isHeadsUp(entry.key)) {
+ // Maybe the heads-up was removed already
+ removeHeadsUpEntry(entry);
+ }
+ }
+ mEntriesToRemoveWhenReorderingAllowed.clear();
+ mBar.getNotificationScrollLayout().setHeadsUpGoingAwayAnimationsAllowed(true);
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // HeadsUpManager utility (protected) methods overrides:
+
+ @Override
+ protected HeadsUpEntry createHeadsUpEntry() {
+ return mEntryPool.acquire();
+ }
+
+ @Override
+ protected void releaseHeadsUpEntry(HeadsUpEntry entry) {
+ mEntryPool.release((HeadsUpEntryPhone) entry);
+ }
+
+ @Override
+ protected boolean shouldHeadsUpBecomePinned(NotificationData.Entry entry) {
+ return mStatusBarState != StatusBarState.KEYGUARD && !mIsExpanded
+ || super.shouldHeadsUpBecomePinned(entry);
+ }
+
+ @Override
+ protected void dumpInternal(FileDescriptor fd, PrintWriter pw, String[] args) {
+ super.dumpInternal(fd, pw, args);
+ pw.print(" mStatusBarState="); pw.println(mStatusBarState);
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Protected utility methods:
+
+ @Nullable
+ protected HeadsUpEntryPhone getHeadsUpEntryPhone(@NonNull String key) {
+ return (HeadsUpEntryPhone) getHeadsUpEntry(key);
+ }
+
+ @Nullable
+ protected HeadsUpEntryPhone getTopHeadsUpEntryPhone() {
+ return (HeadsUpEntryPhone) getTopHeadsUpEntry();
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Private utility methods:
+
+ private boolean wasShownLongEnough(@NonNull String key) {
+ if (mSwipedOutKeys.contains(key)) {
+ // We always instantly dismiss views being manually swiped out.
+ mSwipedOutKeys.remove(key);
+ return true;
+ }
+
+ HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(key);
+ HeadsUpEntryPhone topEntry = getTopHeadsUpEntryPhone();
+ if (headsUpEntry != topEntry) {
+ return true;
+ }
+ return headsUpEntry.wasShownLongEnough();
+ }
+
+ /**
+ * We need to wait on the whole panel to collapse, before we can remove the touchable region
+ * listener.
+ */
+ private void waitForStatusBarLayout() {
+ mWaitingOnCollapseWhenGoingAway = true;
+ mStatusBarWindowView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right, int bottom,
+ int oldLeft,
+ int oldTop, int oldRight, int oldBottom) {
+ if (mStatusBarWindowView.getHeight() <= mStatusBarHeight) {
+ mStatusBarWindowView.removeOnLayoutChangeListener(this);
+ mWaitingOnCollapseWhenGoingAway = false;
+ updateTouchableRegionListener();
+ }
+ }
+ });
+ }
+
+ private void updateTouchableRegionListener() {
+ boolean shouldObserve = hasPinnedHeadsUp() || mHeadsUpGoingAway
+ || mWaitingOnCollapseWhenGoingAway;
+ if (shouldObserve == mIsObserving) {
+ return;
+ }
+ if (shouldObserve) {
+ mStatusBarWindowView.getViewTreeObserver().addOnComputeInternalInsetsListener(this);
+ mStatusBarWindowView.requestLayout();
+ } else {
+ mStatusBarWindowView.getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
+ }
+ mIsObserving = shouldObserve;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // HeadsUpEntryPhone:
+
+ protected class HeadsUpEntryPhone extends HeadsUpManager.HeadsUpEntry {
+ public void setEntry(@NonNull final NotificationData.Entry entry) {
+ Runnable removeHeadsUpRunnable = () -> {
+ if (!mVisualStabilityManager.isReorderingAllowed()) {
+ mEntriesToRemoveWhenReorderingAllowed.add(entry);
+ mVisualStabilityManager.addReorderingAllowedCallback(
+ HeadsUpManagerPhone.this);
+ } else if (!mTrackingHeadsUp) {
+ removeHeadsUpEntry(entry);
+ } else {
+ mEntriesToRemoveAfterExpand.add(entry);
+ }
+ };
+
+ super.setEntry(entry, removeHeadsUpRunnable);
+ }
+
+ public boolean wasShownLongEnough() {
+ return earliestRemovaltime < mClock.currentTimeMillis();
+ }
+
+ @Override
+ public void updateEntry(boolean updatePostTime) {
+ super.updateEntry(updatePostTime);
+
+ if (mEntriesToRemoveAfterExpand.contains(entry)) {
+ mEntriesToRemoveAfterExpand.remove(entry);
+ }
+ if (mEntriesToRemoveWhenReorderingAllowed.contains(entry)) {
+ mEntriesToRemoveWhenReorderingAllowed.remove(entry);
+ }
+ }
+
+ @Override
+ public void expanded(boolean expanded) {
+ if (this.expanded == expanded) {
+ return;
+ }
+
+ this.expanded = expanded;
+ if (expanded) {
+ removeAutoRemovalCallbacks();
+ } else {
+ updateEntry(false /* updatePostTime */);
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java
index c85571c..2bfdefe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java
@@ -23,7 +23,7 @@
import com.android.systemui.Gefingerpoken;
import com.android.systemui.statusbar.ExpandableNotificationRow;
import com.android.systemui.statusbar.ExpandableView;
-import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
/**
@@ -31,7 +31,7 @@
*/
public class HeadsUpTouchHelper implements Gefingerpoken {
- private HeadsUpManager mHeadsUpManager;
+ private HeadsUpManagerPhone mHeadsUpManager;
private NotificationStackScrollLayout mStackScroller;
private int mTrackingPointer;
private float mTouchSlop;
@@ -43,7 +43,7 @@
private NotificationPanelView mPanel;
private ExpandableNotificationRow mPickedChild;
- public HeadsUpTouchHelper(HeadsUpManager headsUpManager,
+ public HeadsUpTouchHelper(HeadsUpManagerPhone headsUpManager,
NotificationStackScrollLayout stackScroller,
NotificationPanelView notificationPanelView) {
mHeadsUpManager = headsUpManager;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
index b8b309b..9d20e4e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
@@ -220,6 +220,11 @@
newLayout = getDefaultLayout();
}
String[] sets = newLayout.split(GRAVITY_SEPARATOR, 3);
+ if (sets.length != 3) {
+ Log.d(TAG, "Invalid layout.");
+ newLayout = getDefaultLayout();
+ sets = newLayout.split(GRAVITY_SEPARATOR, 3);
+ }
String[] start = sets[0].split(BUTTON_SEPARATOR);
String[] center = sets[1].split(BUTTON_SEPARATOR);
String[] end = sets[2].split(BUTTON_SEPARATOR);
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 31b8159..2111d2e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -68,7 +68,7 @@
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.notification.ActivityLaunchAnimator;
import com.android.systemui.statusbar.notification.NotificationUtils;
-import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
import com.android.systemui.statusbar.policy.KeyguardUserSwitcher;
import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
@@ -1571,7 +1571,7 @@
private void updatePanelExpanded() {
boolean isExpanded = !isFullyCollapsed();
if (mPanelExpanded != isExpanded) {
- mHeadsUpManager.setIsExpanded(isExpanded);
+ mHeadsUpManager.setIsPanelExpanded(isExpanded);
mStatusBar.setPanelExpanded(isExpanded);
mPanelExpanded = isExpanded;
}
@@ -2338,7 +2338,7 @@
}
@Override
- public void setHeadsUpManager(HeadsUpManager headsUpManager) {
+ public void setHeadsUpManager(HeadsUpManagerPhone headsUpManager) {
super.setHeadsUpManager(headsUpManager);
mHeadsUpTouchHelper = new HeadsUpTouchHelper(headsUpManager, mNotificationStackScroller,
this);
@@ -2630,8 +2630,8 @@
}
}
- public void setPulsing(Collection<HeadsUpManager.HeadsUpEntry> pulsing) {
- mKeyguardStatusView.setPulsing(pulsing != null);
+ public void setPulsing(boolean pulsing) {
+ mKeyguardStatusView.setPulsing(pulsing);
positionClockAndNotifications();
mNotificationStackScroller.setPulsing(pulsing, mKeyguardStatusView.getLocationOnScreen()[1]
+ mKeyguardStatusView.getClockBottom());
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java
index 2b7e474..6daabed 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java
@@ -50,7 +50,7 @@
import com.android.systemui.doze.DozeLog;
import com.android.systemui.statusbar.FlingAnimationUtils;
import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -75,7 +75,7 @@
}
protected StatusBar mStatusBar;
- protected HeadsUpManager mHeadsUpManager;
+ protected HeadsUpManagerPhone mHeadsUpManager;
private float mPeekHeight;
private float mHintDistance;
@@ -1252,7 +1252,7 @@
*/
protected abstract int getClearAllHeight();
- public void setHeadsUpManager(HeadsUpManager headsUpManager) {
+ public void setHeadsUpManager(HeadsUpManagerPhone headsUpManager) {
mHeadsUpManager = headsUpManager;
}
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 426268b..116e3f9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -208,6 +208,7 @@
import com.android.systemui.statusbar.notification.AboveShelfObserver;
import com.android.systemui.statusbar.notification.ActivityLaunchAnimator;
import com.android.systemui.statusbar.notification.VisualStabilityManager;
+import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
import com.android.systemui.statusbar.phone.UnlockMethodCache.OnUnlockMethodChangedListener;
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
@@ -219,6 +220,7 @@
import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
import com.android.systemui.statusbar.policy.ExtensionController;
import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.policy.HeadsUpUtil;
import com.android.systemui.statusbar.policy.KeyguardMonitor;
import com.android.systemui.statusbar.policy.KeyguardMonitorImpl;
import com.android.systemui.statusbar.policy.KeyguardUserSwitcher;
@@ -802,15 +804,14 @@
.commit();
mIconController = Dependency.get(StatusBarIconController.class);
- mHeadsUpManager = new HeadsUpManager(context, mStatusBarWindow, mGroupManager);
- mHeadsUpManager.setBar(this);
+ mHeadsUpManager = new HeadsUpManagerPhone(context, mStatusBarWindow, mGroupManager, this,
+ mVisualStabilityManager);
mHeadsUpManager.addListener(this);
mHeadsUpManager.addListener(mNotificationPanel);
mHeadsUpManager.addListener(mGroupManager);
mHeadsUpManager.addListener(mVisualStabilityManager);
mNotificationPanel.setHeadsUpManager(mHeadsUpManager);
mGroupManager.setHeadsUpManager(mHeadsUpManager);
- mHeadsUpManager.setVisualStabilityManager(mVisualStabilityManager);
putComponent(HeadsUpManager.class, mHeadsUpManager);
mEntryManager.setUpWithPresenter(this, mStackScroller, this, mHeadsUpManager);
@@ -1341,7 +1342,8 @@
@Override
public void onPerformRemoveNotification(StatusBarNotification n) {
- if (mStackScroller.hasPulsingNotifications() && mHeadsUpManager.getAllEntries().isEmpty()) {
+ if (mStackScroller.hasPulsingNotifications() &&
+ !mHeadsUpManager.hasHeadsUpNotifications()) {
// We were showing a pulse for a notification, but no notifications are pulsing anymore.
// Finish the pulse.
mDozeScrimController.pulseOutNow();
@@ -2090,9 +2092,8 @@
}
public void maybeEscalateHeadsUp() {
- Collection<HeadsUpManager.HeadsUpEntry> entries = mHeadsUpManager.getAllEntries();
- for (HeadsUpManager.HeadsUpEntry entry : entries) {
- final StatusBarNotification sbn = entry.entry.notification;
+ mHeadsUpManager.getAllEntries().forEach(entry -> {
+ final StatusBarNotification sbn = entry.notification;
final Notification notification = sbn.getNotification();
if (notification.fullScreenIntent != null) {
if (DEBUG) {
@@ -2102,11 +2103,11 @@
EventLog.writeEvent(EventLogTags.SYSUI_HEADS_UP_ESCALATION,
sbn.getKey());
notification.fullScreenIntent.send();
- entry.entry.notifyFullScreenIntentLaunched();
+ entry.notifyFullScreenIntentLaunched();
} catch (PendingIntent.CanceledException e) {
}
}
- }
+ });
mHeadsUpManager.releaseAllImmediately();
}
@@ -4636,24 +4637,22 @@
@Override
public void onPulseStarted() {
callback.onPulseStarted();
- Collection<HeadsUpManager.HeadsUpEntry> pulsingEntries =
- mHeadsUpManager.getAllEntries();
- if (!pulsingEntries.isEmpty()) {
+ if (mHeadsUpManager.hasHeadsUpNotifications()) {
// Only pulse the stack scroller if there's actually something to show.
// Otherwise just show the always-on screen.
- setPulsing(pulsingEntries);
+ setPulsing(true);
}
}
@Override
public void onPulseFinished() {
callback.onPulseFinished();
- setPulsing(null);
+ setPulsing(false);
}
- private void setPulsing(Collection<HeadsUpManager.HeadsUpEntry> pulsing) {
+ private void setPulsing(boolean pulsing) {
mNotificationPanel.setPulsing(pulsing);
- mVisualStabilityManager.setPulsing(pulsing != null);
+ mVisualStabilityManager.setPulsing(pulsing);
mIgnoreTouchWhilePulsing = false;
}
}, reason);
@@ -4801,7 +4800,7 @@
// for heads up notifications
- protected HeadsUpManager mHeadsUpManager;
+ protected HeadsUpManagerPhone mHeadsUpManager;
private AboveShelfObserver mAboveShelfObserver;
@@ -4904,7 +4903,7 @@
// Release the HUN notification to the shade.
if (isPresenterFullyCollapsed()) {
- HeadsUpManager.setIsClickedNotification(row, true);
+ HeadsUpUtil.setIsClickedHeadsUpNotification(row, true);
}
//
// In most cases, when FLAG_AUTO_CANCEL is set, the notification will
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessibilityManagerWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessibilityManagerWrapper.java
index 6a573f5..d85e18c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessibilityManagerWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessibilityManagerWrapper.java
@@ -14,10 +14,14 @@
package com.android.systemui.statusbar.policy;
+import android.accessibilityservice.AccessibilityServiceInfo;
import android.content.Context;
+import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityManager.AccessibilityServicesStateChangeListener;
+import java.util.List;
+
/**
* For mocking because AccessibilityManager is final for some reason...
*/
@@ -39,4 +43,27 @@
public void removeCallback(AccessibilityServicesStateChangeListener listener) {
mAccessibilityManager.removeAccessibilityServicesStateChangeListener(listener);
}
+
+ public void addAccessibilityStateChangeListener(
+ AccessibilityManager.AccessibilityStateChangeListener listener) {
+ mAccessibilityManager.addAccessibilityStateChangeListener(listener);
+ }
+
+ public void removeAccessibilityStateChangeListener(
+ AccessibilityManager.AccessibilityStateChangeListener listener) {
+ mAccessibilityManager.removeAccessibilityStateChangeListener(listener);
+ }
+
+ public boolean isEnabled() {
+ return mAccessibilityManager.isEnabled();
+ }
+
+ public void sendAccessibilityEvent(AccessibilityEvent event) {
+ mAccessibilityManager.sendAccessibilityEvent(event);
+ }
+
+ public List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList(
+ int feedbackTypeFlags) {
+ return mAccessibilityManager.getEnabledAccessibilityServiceList(feedbackTypeFlags);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java
index 53dfb24..a2b896d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java
@@ -16,118 +16,68 @@
package com.android.systemui.statusbar.policy;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.Context;
import android.content.res.Resources;
import android.database.ContentObserver;
+import android.os.SystemClock;
import android.os.Handler;
import android.os.Looper;
-import android.os.SystemClock;
-import android.provider.Settings;
-import android.support.v4.util.ArraySet;
import android.util.ArrayMap;
+import android.provider.Settings;
import android.util.Log;
-import android.util.Pools;
-import android.view.View;
-import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityEvent;
import com.android.internal.logging.MetricsLogger;
import com.android.systemui.R;
import com.android.systemui.statusbar.ExpandableNotificationRow;
import com.android.systemui.statusbar.NotificationData;
-import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.notification.VisualStabilityManager;
-import com.android.systemui.statusbar.phone.NotificationGroupManager;
-import com.android.systemui.statusbar.phone.StatusBar;
import java.io.FileDescriptor;
import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.Collection;
+import java.util.Iterator;
+import java.util.stream.Stream;
import java.util.HashMap;
import java.util.HashSet;
-import java.util.Stack;
/**
* A manager which handles heads up notifications which is a special mode where
* they simply peek from the top of the screen.
*/
-public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsListener,
- VisualStabilityManager.Callback {
+public class HeadsUpManager {
private static final String TAG = "HeadsUpManager";
private static final boolean DEBUG = false;
private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms";
- private static final int TAG_CLICKED_NOTIFICATION = R.id.is_clicked_heads_up_tag;
- private final int mHeadsUpNotificationDecay;
- private final int mMinimumDisplayTime;
+ protected final Clock mClock = new Clock();
+ protected final Context mContext;
+ protected final HashSet<OnHeadsUpChangedListener> mListeners = new HashSet<>();
+ protected final Handler mHandler = new Handler(Looper.getMainLooper());
- private final int mTouchAcceptanceDelay;
+ protected int mHeadsUpNotificationDecay;
+ protected int mMinimumDisplayTime;
+ protected int mTouchAcceptanceDelay;
+ protected int mSnoozeLengthMs;
+ protected boolean mHasPinnedNotification;
+ protected int mUser;
+
+ private final HashMap<String, HeadsUpEntry> mHeadsUpEntries = new HashMap<>();
private final ArrayMap<String, Long> mSnoozedPackages;
- private final HashSet<OnHeadsUpChangedListener> mListeners = new HashSet<>();
- private final int mDefaultSnoozeLengthMs;
- private final Handler mHandler = new Handler(Looper.getMainLooper());
- private final Pools.Pool<HeadsUpEntry> mEntryPool = new Pools.Pool<HeadsUpEntry>() {
+ private final ContentObserver mSettingsObserver;
- private Stack<HeadsUpEntry> mPoolObjects = new Stack<>();
-
- @Override
- public HeadsUpEntry acquire() {
- if (!mPoolObjects.isEmpty()) {
- return mPoolObjects.pop();
- }
- return new HeadsUpEntry();
- }
-
- @Override
- public boolean release(HeadsUpEntry instance) {
- instance.reset();
- mPoolObjects.push(instance);
- return true;
- }
- };
-
- private final View mStatusBarWindowView;
- private final int mStatusBarHeight;
- private final Context mContext;
- private final NotificationGroupManager mGroupManager;
- private StatusBar mBar;
- private int mSnoozeLengthMs;
- private ContentObserver mSettingsObserver;
- private HashMap<String, HeadsUpEntry> mHeadsUpEntries = new HashMap<>();
- private HashSet<String> mSwipedOutKeys = new HashSet<>();
- private int mUser;
- private Clock mClock;
- private boolean mReleaseOnExpandFinish;
- private boolean mTrackingHeadsUp;
- private HashSet<NotificationData.Entry> mEntriesToRemoveAfterExpand = new HashSet<>();
- private ArraySet<NotificationData.Entry> mEntriesToRemoveWhenReorderingAllowed
- = new ArraySet<>();
- private boolean mIsExpanded;
- private boolean mHasPinnedNotification;
- private int[] mTmpTwoArray = new int[2];
- private boolean mHeadsUpGoingAway;
- private boolean mWaitingOnCollapseWhenGoingAway;
- private boolean mIsObserving;
- private boolean mRemoteInputActive;
- private float mExpandedHeight;
- private VisualStabilityManager mVisualStabilityManager;
- private int mStatusBarState;
-
- public HeadsUpManager(final Context context, View statusBarWindowView,
- NotificationGroupManager groupManager) {
+ public HeadsUpManager(@NonNull final Context context) {
mContext = context;
- Resources resources = mContext.getResources();
- mTouchAcceptanceDelay = resources.getInteger(R.integer.touch_acceptance_delay);
- mSnoozedPackages = new ArrayMap<>();
- mDefaultSnoozeLengthMs = resources.getInteger(R.integer.heads_up_default_snooze_length_ms);
- mSnoozeLengthMs = mDefaultSnoozeLengthMs;
+ Resources resources = context.getResources();
mMinimumDisplayTime = resources.getInteger(R.integer.heads_up_notification_minimum_time);
mHeadsUpNotificationDecay = resources.getInteger(R.integer.heads_up_notification_decay);
- mClock = new Clock();
+ mTouchAcceptanceDelay = resources.getInteger(R.integer.touch_acceptance_delay);
+ mSnoozedPackages = new ArrayMap<>();
+ int defaultSnoozeLengthMs =
+ resources.getInteger(R.integer.heads_up_default_snooze_length_ms);
mSnoozeLengthMs = Settings.Global.getInt(context.getContentResolver(),
- SETTING_HEADS_UP_SNOOZE_LENGTH_MS, mDefaultSnoozeLengthMs);
+ SETTING_HEADS_UP_SNOOZE_LENGTH_MS, defaultSnoozeLengthMs);
mSettingsObserver = new ContentObserver(mHandler) {
@Override
public void onChange(boolean selfChange) {
@@ -142,47 +92,26 @@
context.getContentResolver().registerContentObserver(
Settings.Global.getUriFor(SETTING_HEADS_UP_SNOOZE_LENGTH_MS), false,
mSettingsObserver);
- mStatusBarWindowView = statusBarWindowView;
- mGroupManager = groupManager;
- mStatusBarHeight = resources.getDimensionPixelSize(
- com.android.internal.R.dimen.status_bar_height);
}
- private void updateTouchableRegionListener() {
- boolean shouldObserve = mHasPinnedNotification || mHeadsUpGoingAway
- || mWaitingOnCollapseWhenGoingAway;
- if (shouldObserve == mIsObserving) {
- return;
- }
- if (shouldObserve) {
- mStatusBarWindowView.getViewTreeObserver().addOnComputeInternalInsetsListener(this);
- mStatusBarWindowView.requestLayout();
- } else {
- mStatusBarWindowView.getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
- }
- mIsObserving = shouldObserve;
- }
-
- public void setBar(StatusBar bar) {
- mBar = bar;
- }
-
- public void addListener(OnHeadsUpChangedListener listener) {
+ /**
+ * Adds an OnHeadUpChangedListener to observe events.
+ */
+ public void addListener(@NonNull OnHeadsUpChangedListener listener) {
mListeners.add(listener);
}
- public void removeListener(OnHeadsUpChangedListener listener) {
+ /**
+ * Removes the OnHeadUpChangedListener from the observer list.
+ */
+ public void removeListener(@NonNull OnHeadsUpChangedListener listener) {
mListeners.remove(listener);
}
- public StatusBar getBar() {
- return mBar;
- }
-
/**
* Called when posting a new notification to the heads up.
*/
- public void showNotification(NotificationData.Entry headsUp) {
+ public void showNotification(@NonNull NotificationData.Entry headsUp) {
if (DEBUG) Log.v(TAG, "showNotification");
addHeadsUpEntry(headsUp);
updateNotification(headsUp, true);
@@ -192,7 +121,7 @@
/**
* Called when updating or posting a notification to the heads up.
*/
- public void updateNotification(NotificationData.Entry headsUp, boolean alert) {
+ public void updateNotification(@NonNull NotificationData.Entry headsUp, boolean alert) {
if (DEBUG) Log.v(TAG, "updateNotification");
headsUp.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
@@ -204,14 +133,13 @@
// with the groupmanager
return;
}
- headsUpEntry.updateEntry();
+ headsUpEntry.updateEntry(true /* updatePostTime */);
setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(headsUp));
}
}
- private void addHeadsUpEntry(NotificationData.Entry entry) {
- HeadsUpEntry headsUpEntry = mEntryPool.acquire();
-
+ private void addHeadsUpEntry(@NonNull NotificationData.Entry entry) {
+ HeadsUpEntry headsUpEntry = createHeadsUpEntry();
// This will also add the entry to the sortedList
headsUpEntry.setEntry(entry);
mHeadsUpEntries.put(entry.key, headsUpEntry);
@@ -223,16 +151,17 @@
entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
}
- private boolean shouldHeadsUpBecomePinned(NotificationData.Entry entry) {
- return mStatusBarState != StatusBarState.KEYGUARD
- && !mIsExpanded || hasFullScreenIntent(entry);
+ protected boolean shouldHeadsUpBecomePinned(@NonNull NotificationData.Entry entry) {
+ return hasFullScreenIntent(entry);
}
- private boolean hasFullScreenIntent(NotificationData.Entry entry) {
+ protected boolean hasFullScreenIntent(@NonNull NotificationData.Entry entry) {
return entry.notification.getNotification().fullScreenIntent != null;
}
- private void setEntryPinned(HeadsUpEntry headsUpEntry, boolean isPinned) {
+ protected void setEntryPinned(
+ @NonNull HeadsUpManager.HeadsUpEntry headsUpEntry, boolean isPinned) {
+ if (DEBUG) Log.v(TAG, "setEntryPinned: " + isPinned);
ExpandableNotificationRow row = headsUpEntry.entry.row;
if (row.isPinned() != isPinned) {
row.setPinned(isPinned);
@@ -247,33 +176,35 @@
}
}
- private void removeHeadsUpEntry(NotificationData.Entry entry) {
+ protected void removeHeadsUpEntry(NotificationData.Entry entry) {
HeadsUpEntry remove = mHeadsUpEntries.remove(entry.key);
+ onHeadsUpEntryRemoved(remove);
+ releaseHeadsUpEntry(remove);
+ }
+
+ protected void onHeadsUpEntryRemoved(HeadsUpEntry remove) {
+ NotificationData.Entry entry = remove.entry;
entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
entry.row.setHeadsUp(false);
setEntryPinned(remove, false /* isPinned */);
for (OnHeadsUpChangedListener listener : mListeners) {
listener.onHeadsUpStateChanged(entry, false);
}
- mEntryPool.release(remove);
}
- public void removeAllHeadsUpEntries() {
- for (String key : mHeadsUpEntries.keySet()) {
- removeHeadsUpEntry(mHeadsUpEntries.get(key).entry);
- }
- }
-
- private void updatePinnedMode() {
+ protected void updatePinnedMode() {
boolean hasPinnedNotification = hasPinnedNotificationInternal();
if (hasPinnedNotification == mHasPinnedNotification) {
return;
}
+ if (DEBUG) {
+ Log.v(TAG, "Pinned mode changed: " + mHasPinnedNotification + " -> " +
+ hasPinnedNotification);
+ }
mHasPinnedNotification = hasPinnedNotification;
if (mHasPinnedNotification) {
MetricsLogger.count(mContext, "note_peek", 1);
}
- updateTouchableRegionListener();
for (OnHeadsUpChangedListener listener : mListeners) {
listener.onHeadsUpPinnedModeChanged(hasPinnedNotification);
}
@@ -285,47 +216,36 @@
* @return true if the notification was removed and false if it still needs to be kept around
* for a bit since it wasn't shown long enough
*/
- public boolean removeNotification(String key, boolean ignoreEarliestRemovalTime) {
- if (DEBUG) Log.v(TAG, "remove");
- if (wasShownLongEnough(key) || ignoreEarliestRemovalTime) {
- releaseImmediately(key);
- return true;
- } else {
- getHeadsUpEntry(key).removeAsSoonAsPossible();
- return false;
- }
+ public boolean removeNotification(@NonNull String key, boolean ignoreEarliestRemovalTime) {
+ if (DEBUG) Log.v(TAG, "removeNotification");
+ releaseImmediately(key);
+ return true;
}
- private boolean wasShownLongEnough(String key) {
- HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
- HeadsUpEntry topEntry = getTopEntry();
- if (mSwipedOutKeys.contains(key)) {
- // We always instantly dismiss views being manually swiped out.
- mSwipedOutKeys.remove(key);
- return true;
- }
- if (headsUpEntry != topEntry) {
- return true;
- }
- return headsUpEntry.wasShownLongEnough();
- }
-
+ /**
+ * Returns if the given notification is in the Heads Up Notification list or not.
+ */
public boolean isHeadsUp(String key) {
return mHeadsUpEntries.containsKey(key);
}
/**
- * Push any current Heads Up notification down into the shade.
+ * Pushes any current Heads Up notification down into the shade.
*/
public void releaseAllImmediately() {
if (DEBUG) Log.v(TAG, "releaseAllImmediately");
- ArrayList<String> keys = new ArrayList<>(mHeadsUpEntries.keySet());
- for (String key : keys) {
- releaseImmediately(key);
+ Iterator<HeadsUpEntry> iterator = mHeadsUpEntries.values().iterator();
+ while (iterator.hasNext()) {
+ HeadsUpEntry entry = iterator.next();
+ iterator.remove();
+ onHeadsUpEntryRemoved(entry);
}
}
- public void releaseImmediately(String key) {
+ /**
+ * Pushes the given Heads Up notification down into the shade.
+ */
+ public void releaseImmediately(@NonNull String key) {
HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
if (headsUpEntry == null) {
return;
@@ -334,11 +254,14 @@
removeHeadsUpEntry(shadeEntry);
}
- public boolean isSnoozed(String packageName) {
+ /**
+ * Returns if the given notification is snoozed or not.
+ */
+ public boolean isSnoozed(@NonNull String packageName) {
final String key = snoozeKey(packageName, mUser);
Long snoozedUntil = mSnoozedPackages.get(key);
if (snoozedUntil != null) {
- if (snoozedUntil > SystemClock.elapsedRealtime()) {
+ if (snoozedUntil > mClock.currentTimeMillis()) {
if (DEBUG) Log.v(TAG, key + " snoozed");
return true;
}
@@ -347,33 +270,61 @@
return false;
}
+ /**
+ * Snoozes all current Heads Up Notifications.
+ */
public void snooze() {
for (String key : mHeadsUpEntries.keySet()) {
HeadsUpEntry entry = mHeadsUpEntries.get(key);
String packageName = entry.entry.notification.getPackageName();
mSnoozedPackages.put(snoozeKey(packageName, mUser),
- SystemClock.elapsedRealtime() + mSnoozeLengthMs);
+ mClock.currentTimeMillis() + mSnoozeLengthMs);
}
- mReleaseOnExpandFinish = true;
}
- private static String snoozeKey(String packageName, int user) {
+ private static String snoozeKey(@NonNull String packageName, int user) {
return user + "," + packageName;
}
- private HeadsUpEntry getHeadsUpEntry(String key) {
+ protected HeadsUpEntry getHeadsUpEntry(@NonNull String key) {
return mHeadsUpEntries.get(key);
}
- public NotificationData.Entry getEntry(String key) {
- return mHeadsUpEntries.get(key).entry;
+ /**
+ * Returns the entry of given Heads Up Notification.
+ *
+ * @param key Key of heads up notification
+ */
+ public NotificationData.Entry getEntry(@NonNull String key) {
+ HeadsUpEntry entry = mHeadsUpEntries.get(key);
+ return entry != null ? entry.entry : null;
}
- public Collection<HeadsUpEntry> getAllEntries() {
- return mHeadsUpEntries.values();
+ /**
+ * Returns the stream of all current Heads Up Notifications.
+ */
+ @NonNull
+ public Stream<NotificationData.Entry> getAllEntries() {
+ return mHeadsUpEntries.values().stream().map(headsUpEntry -> headsUpEntry.entry);
}
- public HeadsUpEntry getTopEntry() {
+ /**
+ * Returns the top Heads Up Notification, which appeares to show at first.
+ */
+ @Nullable
+ public NotificationData.Entry getTopEntry() {
+ HeadsUpEntry topEntry = getTopHeadsUpEntry();
+ return (topEntry != null) ? topEntry.entry : null;
+ }
+
+ /**
+ * Returns if any heads up notification is available or not.
+ */
+ public boolean hasHeadsUpNotifications() {
+ return !mHeadsUpEntries.isEmpty();
+ }
+
+ protected HeadsUpEntry getTopHeadsUpEntry() {
if (mHeadsUpEntries.isEmpty()) {
return null;
}
@@ -387,56 +338,21 @@
}
/**
- * Decides whether a click is invalid for a notification, i.e it has not been shown long enough
- * that a user might have consciously clicked on it.
- *
- * @param key the key of the touched notification
- * @return whether the touch is invalid and should be discarded
+ * Sets the current user.
*/
- public boolean shouldSwallowClick(String key) {
- HeadsUpEntry entry = mHeadsUpEntries.get(key);
- if (entry != null && mClock.currentTimeMillis() < entry.postTime) {
- return true;
- }
- return false;
- }
-
- public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) {
- if (mIsExpanded || mBar.isBouncerShowing()) {
- // The touchable region is always the full area when expanded
- return;
- }
- if (mHasPinnedNotification) {
- ExpandableNotificationRow topEntry = getTopEntry().entry.row;
- if (topEntry.isChildInGroup()) {
- final ExpandableNotificationRow groupSummary
- = mGroupManager.getGroupSummary(topEntry.getStatusBarNotification());
- if (groupSummary != null) {
- topEntry = groupSummary;
- }
- }
- topEntry.getLocationOnScreen(mTmpTwoArray);
- int minX = mTmpTwoArray[0];
- int maxX = mTmpTwoArray[0] + topEntry.getWidth();
- int maxY = topEntry.getIntrinsicHeight();
-
- info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
- info.touchableRegion.set(minX, 0, maxX, maxY);
- } else if (mHeadsUpGoingAway || mWaitingOnCollapseWhenGoingAway) {
- info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
- info.touchableRegion.set(0, 0, mStatusBarWindowView.getWidth(), mStatusBarHeight);
- }
- }
-
public void setUser(int user) {
mUser = user;
}
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println("HeadsUpManager state:");
+ dumpInternal(fd, pw, args);
+ }
+
+ protected void dumpInternal(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.print(" mTouchAcceptanceDelay="); pw.println(mTouchAcceptanceDelay);
pw.print(" mSnoozeLengthMs="); pw.println(mSnoozeLengthMs);
- pw.print(" now="); pw.println(SystemClock.elapsedRealtime());
+ pw.print(" now="); pw.println(mClock.currentTimeMillis());
pw.print(" mUser="); pw.println(mUser);
for (HeadsUpEntry entry: mHeadsUpEntries.values()) {
pw.print(" HeadsUpEntry="); pw.println(entry.entry);
@@ -449,6 +365,9 @@
}
}
+ /**
+ * Returns if there are any pinned Heads Up Notifications or not.
+ */
public boolean hasPinnedHeadsUp() {
return mHasPinnedNotification;
}
@@ -464,14 +383,8 @@
}
/**
- * Notifies that a notification was swiped out and will be removed.
- *
- * @param key the notification key
+ * Unpins all pinned Heads Up Notifications.
*/
- public void addSwipedOutNotification(String key) {
- mSwipedOutKeys.add(key);
- }
-
public void unpinAll() {
for (String key : mHeadsUpEntries.keySet()) {
HeadsUpEntry entry = mHeadsUpEntries.get(key);
@@ -481,60 +394,13 @@
}
}
- public void onExpandingFinished() {
- if (mReleaseOnExpandFinish) {
- releaseAllImmediately();
- mReleaseOnExpandFinish = false;
- } else {
- for (NotificationData.Entry entry : mEntriesToRemoveAfterExpand) {
- if (isHeadsUp(entry.key)) {
- // Maybe the heads-up was removed already
- removeHeadsUpEntry(entry);
- }
- }
- }
- mEntriesToRemoveAfterExpand.clear();
- }
-
- public void setTrackingHeadsUp(boolean trackingHeadsUp) {
- mTrackingHeadsUp = trackingHeadsUp;
- }
-
- public boolean isTrackingHeadsUp() {
- return mTrackingHeadsUp;
- }
-
- public void setIsExpanded(boolean isExpanded) {
- if (isExpanded != mIsExpanded) {
- mIsExpanded = isExpanded;
- if (isExpanded) {
- // make sure our state is sane
- mWaitingOnCollapseWhenGoingAway = false;
- mHeadsUpGoingAway = false;
- updateTouchableRegionListener();
- }
- }
- }
-
/**
- * @return the height of the top heads up notification when pinned. This is different from the
- * intrinsic height, which also includes whether the notification is system expanded and
- * is mainly used when dragging down from a heads up notification.
+ * Returns the value of the tracking-heads-up flag. See the doc of {@code setTrackingHeadsUp} as
+ * well.
*/
- public int getTopHeadsUpPinnedHeight() {
- HeadsUpEntry topEntry = getTopEntry();
- if (topEntry == null || topEntry.entry == null) {
- return 0;
- }
- ExpandableNotificationRow row = topEntry.entry.row;
- if (row.isChildInGroup()) {
- final ExpandableNotificationRow groupSummary
- = mGroupManager.getGroupSummary(row.getStatusBarNotification());
- if (groupSummary != null) {
- row = groupSummary;
- }
- }
- return row.getPinnedHeadsUpHeight();
+ public boolean isTrackingHeadsUp() {
+ // Might be implemented in subclass.
+ return false;
}
/**
@@ -553,147 +419,67 @@
}
/**
- * Set that we are exiting the headsUp pinned mode, but some notifications might still be
- * animating out. This is used to keep the touchable regions in a sane state.
+ * Sets an entry to be expanded and therefore stick in the heads up area if it's pinned
+ * until it's collapsed again.
*/
- public void setHeadsUpGoingAway(boolean headsUpGoingAway) {
- if (headsUpGoingAway != mHeadsUpGoingAway) {
- mHeadsUpGoingAway = headsUpGoingAway;
- if (!headsUpGoingAway) {
- waitForStatusBarLayout();
- }
- updateTouchableRegionListener();
- }
- }
-
- /**
- * We need to wait on the whole panel to collapse, before we can remove the touchable region
- * listener.
- */
- private void waitForStatusBarLayout() {
- mWaitingOnCollapseWhenGoingAway = true;
- mStatusBarWindowView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
- @Override
- public void onLayoutChange(View v, int left, int top, int right, int bottom,
- int oldLeft,
- int oldTop, int oldRight, int oldBottom) {
- if (mStatusBarWindowView.getHeight() <= mStatusBarHeight) {
- mStatusBarWindowView.removeOnLayoutChangeListener(this);
- mWaitingOnCollapseWhenGoingAway = false;
- updateTouchableRegionListener();
- }
- }
- });
- }
-
- public static void setIsClickedNotification(View child, boolean clicked) {
- child.setTag(TAG_CLICKED_NOTIFICATION, clicked ? true : null);
- }
-
- public static boolean isClickedHeadsUpNotification(View child) {
- Boolean clicked = (Boolean) child.getTag(TAG_CLICKED_NOTIFICATION);
- return clicked != null && clicked;
- }
-
- public void setRemoteInputActive(NotificationData.Entry entry, boolean remoteInputActive) {
- HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(entry.key);
- if (headsUpEntry != null && headsUpEntry.remoteInputActive != remoteInputActive) {
- headsUpEntry.remoteInputActive = remoteInputActive;
- if (remoteInputActive) {
- headsUpEntry.removeAutoRemovalCallbacks();
- } else {
- headsUpEntry.updateEntry(false /* updatePostTime */);
- }
- }
- }
/**
* Set an entry to be expanded and therefore stick in the heads up area if it's pinned
* until it's collapsed again.
*/
- public void setExpanded(NotificationData.Entry entry, boolean expanded) {
- HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(entry.key);
- if (headsUpEntry != null && headsUpEntry.expanded != expanded && entry.row.isPinned()) {
- headsUpEntry.expanded = expanded;
- if (expanded) {
- headsUpEntry.removeAutoRemovalCallbacks();
- } else {
- headsUpEntry.updateEntry(false /* updatePostTime */);
- }
+ public void setExpanded(@NonNull NotificationData.Entry entry, boolean expanded) {
+ HeadsUpManager.HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(entry.key);
+ if (headsUpEntry != null && entry.row.isPinned()) {
+ headsUpEntry.expanded(expanded);
}
}
- @Override
- public void onReorderingAllowed() {
- mBar.getNotificationScrollLayout().setHeadsUpGoingAwayAnimationsAllowed(false);
- for (NotificationData.Entry entry : mEntriesToRemoveWhenReorderingAllowed) {
- if (isHeadsUp(entry.key)) {
- // Maybe the heads-up was removed already
- removeHeadsUpEntry(entry);
- }
- }
- mEntriesToRemoveWhenReorderingAllowed.clear();
- mBar.getNotificationScrollLayout().setHeadsUpGoingAwayAnimationsAllowed(true);
+ @NonNull
+ protected HeadsUpEntry createHeadsUpEntry() {
+ return new HeadsUpEntry();
}
- public void setVisualStabilityManager(VisualStabilityManager visualStabilityManager) {
- mVisualStabilityManager = visualStabilityManager;
- }
-
- public void setStatusBarState(int statusBarState) {
- mStatusBarState = statusBarState;
+ protected void releaseHeadsUpEntry(@NonNull HeadsUpEntry entry) {
+ // Do nothing for HeadsUpEntry.
}
/**
* This represents a notification and how long it is in a heads up mode. It also manages its
* lifecycle automatically when created.
*/
- public class HeadsUpEntry implements Comparable<HeadsUpEntry> {
- public NotificationData.Entry entry;
+ protected class HeadsUpEntry implements Comparable<HeadsUpEntry> {
+ @Nullable public NotificationData.Entry entry;
public long postTime;
- public long earliestRemovaltime;
- private Runnable mRemoveHeadsUpRunnable;
public boolean remoteInputActive;
+ public long earliestRemovaltime;
public boolean expanded;
- public void setEntry(final NotificationData.Entry entry) {
+ private Runnable mRemoveHeadsUpRunnable;
+
+ public void setEntry(@Nullable final NotificationData.Entry entry) {
+ setEntry(entry, null);
+ }
+
+ public void setEntry(@Nullable final NotificationData.Entry entry,
+ @Nullable Runnable removeHeadsUpRunnable) {
this.entry = entry;
+ this.mRemoveHeadsUpRunnable = removeHeadsUpRunnable;
// The actual post time will be just after the heads-up really slided in
postTime = mClock.currentTimeMillis() + mTouchAcceptanceDelay;
- mRemoveHeadsUpRunnable = new Runnable() {
- @Override
- public void run() {
- if (!mVisualStabilityManager.isReorderingAllowed()) {
- mEntriesToRemoveWhenReorderingAllowed.add(entry);
- mVisualStabilityManager.addReorderingAllowedCallback(HeadsUpManager.this);
- } else if (!mTrackingHeadsUp) {
- removeHeadsUpEntry(entry);
- } else {
- mEntriesToRemoveAfterExpand.add(entry);
- }
- }
- };
- updateEntry();
- }
-
- public void updateEntry() {
- updateEntry(true);
+ updateEntry(true /* updatePostTime */);
}
public void updateEntry(boolean updatePostTime) {
+ if (DEBUG) Log.v(TAG, "updateEntry");
+
long currentTime = mClock.currentTimeMillis();
earliestRemovaltime = currentTime + mMinimumDisplayTime;
if (updatePostTime) {
postTime = Math.max(postTime, currentTime);
}
removeAutoRemovalCallbacks();
- if (mEntriesToRemoveAfterExpand.contains(entry)) {
- mEntriesToRemoveAfterExpand.remove(entry);
- }
- if (mEntriesToRemoveWhenReorderingAllowed.contains(entry)) {
- mEntriesToRemoveWhenReorderingAllowed.remove(entry);
- }
+
if (!isSticky()) {
long finishTime = postTime + mHeadsUpNotificationDecay;
long removeDelay = Math.max(finishTime - currentTime, mMinimumDisplayTime);
@@ -707,7 +493,7 @@
}
@Override
- public int compareTo(HeadsUpEntry o) {
+ public int compareTo(@NonNull HeadsUpEntry o) {
boolean isPinned = entry.row.isPinned();
boolean otherPinned = o.entry.row.isPinned();
if (isPinned && !otherPinned) {
@@ -734,26 +520,29 @@
: -1;
}
- public void removeAutoRemovalCallbacks() {
- mHandler.removeCallbacks(mRemoveHeadsUpRunnable);
- }
-
- public boolean wasShownLongEnough() {
- return earliestRemovaltime < mClock.currentTimeMillis();
- }
-
- public void removeAsSoonAsPossible() {
- removeAutoRemovalCallbacks();
- mHandler.postDelayed(mRemoveHeadsUpRunnable,
- earliestRemovaltime - mClock.currentTimeMillis());
+ public void expanded(boolean expanded) {
+ this.expanded = expanded;
}
public void reset() {
- removeAutoRemovalCallbacks();
entry = null;
- mRemoveHeadsUpRunnable = null;
expanded = false;
remoteInputActive = false;
+ removeAutoRemovalCallbacks();
+ mRemoveHeadsUpRunnable = null;
+ }
+
+ public void removeAutoRemovalCallbacks() {
+ if (mRemoveHeadsUpRunnable != null)
+ mHandler.removeCallbacks(mRemoveHeadsUpRunnable);
+ }
+
+ public void removeAsSoonAsPossible() {
+ if (mRemoveHeadsUpRunnable != null) {
+ removeAutoRemovalCallbacks();
+ mHandler.postDelayed(mRemoveHeadsUpRunnable,
+ earliestRemovaltime - mClock.currentTimeMillis());
+ }
}
}
@@ -762,5 +551,4 @@
return SystemClock.elapsedRealtime();
}
}
-
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpUtil.java
new file mode 100644
index 0000000..1e3c123c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpUtil.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy;
+
+import android.view.View;
+
+import com.android.systemui.R;
+
+/**
+ * A class of utility static methods for heads up notifications.
+ */
+public final class HeadsUpUtil {
+ private static final int TAG_CLICKED_NOTIFICATION = R.id.is_clicked_heads_up_tag;
+
+ /**
+ * Set the given view as clicked or not-clicked.
+ * @param view The view to be set the flag to.
+ * @param clicked True to set as clicked. False to not-clicked.
+ */
+ public static void setIsClickedHeadsUpNotification(View view, boolean clicked) {
+ view.setTag(TAG_CLICKED_NOTIFICATION, clicked ? true : null);
+ }
+
+ /**
+ * Check if the given view has the flag of "clicked notification"
+ * @param view The view to be checked.
+ * @return True if the view has clicked. False othrewise.
+ */
+ public static boolean isClickedHeadsUpNotification(View view) {
+ Boolean clicked = (Boolean) view.getTag(TAG_CLICKED_NOTIFICATION);
+ return clicked != null && clicked;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java
index 424858a..d7a810e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java
@@ -64,7 +64,7 @@
private boolean mPanelTracking;
private boolean mExpansionChanging;
private boolean mPanelFullWidth;
- private Collection<HeadsUpManager.HeadsUpEntry> mPulsing;
+ private boolean mPulsing;
private boolean mUnlockHintRunning;
private boolean mQsCustomizerShowing;
private int mIntrinsicPadding;
@@ -315,23 +315,18 @@
}
public boolean hasPulsingNotifications() {
- return mPulsing != null;
+ return mPulsing;
}
- public void setPulsing(Collection<HeadsUpManager.HeadsUpEntry> hasPulsing) {
+ public void setPulsing(boolean hasPulsing) {
mPulsing = hasPulsing;
}
public boolean isPulsing(NotificationData.Entry entry) {
- if (mPulsing == null) {
+ if (!mPulsing || mHeadsUpManager == null) {
return false;
}
- for (HeadsUpManager.HeadsUpEntry e : mPulsing) {
- if (e.entry == entry) {
- return true;
- }
- }
- return false;
+ return mHeadsUpManager.getAllEntries().anyMatch(e -> (e == entry));
}
public boolean isPanelTracking() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
index ad8a0eb..b28e1a9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
@@ -92,10 +92,11 @@
import com.android.systemui.statusbar.notification.FakeShadowView;
import com.android.systemui.statusbar.notification.NotificationUtils;
import com.android.systemui.statusbar.notification.VisibilityLocationProvider;
+import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.phone.StatusBar;
import com.android.systemui.statusbar.phone.ScrimController;
-import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.policy.HeadsUpUtil;
import com.android.systemui.statusbar.policy.ScrollAdapter;
import android.support.v4.graphics.ColorUtils;
@@ -288,7 +289,7 @@
private HashSet<View> mClearOverlayViewsWhenFinished = new HashSet<>();
private HashSet<Pair<ExpandableNotificationRow, Boolean>> mHeadsUpChangeAnimations
= new HashSet<>();
- private HeadsUpManager mHeadsUpManager;
+ private HeadsUpManagerPhone mHeadsUpManager;
private boolean mTrackingHeadsUp;
private ScrimController mScrimController;
private boolean mForceNoOverlappingRendering;
@@ -358,14 +359,14 @@
}
};
private PorterDuffXfermode mSrcMode = new PorterDuffXfermode(PorterDuff.Mode.SRC);
- private Collection<HeadsUpManager.HeadsUpEntry> mPulsing;
+ private boolean mPulsing;
private boolean mDrawBackgroundAsSrc;
private boolean mFadingOut;
private boolean mParentNotFullyVisible;
private boolean mGroupExpandedForMeasure;
private boolean mScrollable;
private View mForcedScroll;
- private float mDarkAmount = 1.0f;
+ private float mDarkAmount = 0f;
private static final Property<NotificationStackScrollLayout, Float> DARK_AMOUNT =
new FloatProperty<NotificationStackScrollLayout>("darkAmount") {
@Override
@@ -523,9 +524,9 @@
setClipBounds(null);
} else {
float animProgress = Interpolators.FAST_OUT_SLOW_IN
- .getInterpolation(mDarkAmount);
+ .getInterpolation(1f - mDarkAmount);
float sidePaddingsProgress = Interpolators.FAST_OUT_SLOW_IN
- .getInterpolation(mDarkAmount * 2);
+ .getInterpolation((1f - mDarkAmount) * 2);
mTmpRect.set((int) MathUtils.lerp(darkLeft, lockScreenLeft, sidePaddingsProgress),
(int) MathUtils.lerp(darkTop, lockScreenTop, animProgress),
(int) MathUtils.lerp(darkRight, lockScreenRight, sidePaddingsProgress),
@@ -548,7 +549,7 @@
} else {
float alpha =
BACKGROUND_ALPHA_DIMMED + (1 - BACKGROUND_ALPHA_DIMMED) * (1.0f - mDimAmount);
- alpha *= mDarkAmount;
+ alpha *= 1f - mDarkAmount;
// We need to manually blend in the background color
int scrimColor = mScrimController.getBackgroundColor();
color = ColorUtils.blendARGB(scrimColor, mBgColor, alpha);
@@ -689,7 +690,7 @@
}
private void updateAlgorithmHeightAndPadding() {
- if (mPulsing != null) {
+ if (mPulsing) {
mTopPadding = mClockBottom;
} else {
mTopPadding = mAmbientState.isDark() ? mDarkTopPadding : mRegularTopPadding;
@@ -919,6 +920,27 @@
}
/**
+ * @return the height of the top heads up notification when pinned. This is different from the
+ * intrinsic height, which also includes whether the notification is system expanded and
+ * is mainly used when dragging down from a heads up notification.
+ */
+ private int getTopHeadsUpPinnedHeight() {
+ NotificationData.Entry topEntry = mHeadsUpManager.getTopEntry();
+ if (topEntry == null) {
+ return 0;
+ }
+ ExpandableNotificationRow row = topEntry.row;
+ if (row.isChildInGroup()) {
+ final ExpandableNotificationRow groupSummary
+ = mGroupManager.getGroupSummary(row.getStatusBarNotification());
+ if (groupSummary != null) {
+ row = groupSummary;
+ }
+ }
+ return row.getPinnedHeadsUpHeight();
+ }
+
+ /**
* @return the position from where the appear transition ends when expanding.
* Measured in absolute height.
*/
@@ -929,7 +951,7 @@
int minNotificationsForShelf = 1;
if (mTrackingHeadsUp
|| (mHeadsUpManager.hasPinnedHeadsUp() && !mAmbientState.isDark())) {
- appearPosition = mHeadsUpManager.getTopHeadsUpPinnedHeight();
+ appearPosition = getTopHeadsUpPinnedHeight();
minNotificationsForShelf = 2;
} else {
appearPosition = 0;
@@ -1197,9 +1219,9 @@
if (slidingChild instanceof ExpandableNotificationRow) {
ExpandableNotificationRow row = (ExpandableNotificationRow) slidingChild;
if (!mIsExpanded && row.isHeadsUp() && row.isPinned()
- && mHeadsUpManager.getTopEntry().entry.row != row
+ && mHeadsUpManager.getTopEntry().row != row
&& mGroupManager.getGroupSummary(
- mHeadsUpManager.getTopEntry().entry.row.getStatusBarNotification())
+ mHeadsUpManager.getTopEntry().row.getStatusBarNotification())
!= row) {
continue;
}
@@ -2119,7 +2141,7 @@
@Override
public boolean hasPulsingNotifications() {
- return mPulsing != null;
+ return mPulsing;
}
private void updateScrollability() {
@@ -2304,8 +2326,9 @@
return;
}
+ final boolean awake = mDarkAmount != 0 || mAmbientState.isDark();
mScrimController.setExcludedBackgroundArea(
- mFadingOut || mParentNotFullyVisible || mDarkAmount != 1 || mIsClipped ? null
+ mFadingOut || mParentNotFullyVisible || awake || mIsClipped ? null
: mCurrentBounds);
invalidate();
}
@@ -2751,7 +2774,7 @@
}
private boolean isClickedHeadsUp(View child) {
- return HeadsUpManager.isClickedHeadsUpNotification(child);
+ return HeadsUpUtil.isClickedHeadsUpNotification(child);
}
/**
@@ -3858,17 +3881,12 @@
mDarkNeedsAnimation = true;
mDarkAnimationOriginIndex = findDarkAnimationOriginIndex(touchWakeUpScreenLocation);
mNeedsAnimation = true;
- setDarkAmount(0.0f);
- } else if (!dark) {
- setDarkAmount(1.0f);
- }
- requestChildrenUpdate();
- if (dark) {
- mScrimController.setExcludedBackgroundArea(null);
} else {
+ setDarkAmount(dark ? 1f : 0f);
updateBackground();
}
-
+ requestChildrenUpdate();
+ applyCurrentBackgroundBounds();
updateWillNotDraw();
updateContentHeight();
notifyHeightChangeListener(mShelf);
@@ -3894,7 +3912,7 @@
}
private void startBackgroundFadeIn() {
- ObjectAnimator fadeAnimator = ObjectAnimator.ofFloat(this, DARK_AMOUNT, 0f, 1f);
+ ObjectAnimator fadeAnimator = ObjectAnimator.ofFloat(this, DARK_AMOUNT, mDarkAmount, 0f);
fadeAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_WAKEUP);
fadeAnimator.setInterpolator(Interpolators.ALPHA_IN);
fadeAnimator.start();
@@ -4256,7 +4274,7 @@
mAnimationFinishedRunnables.add(runnable);
}
- public void setHeadsUpManager(HeadsUpManager headsUpManager) {
+ public void setHeadsUpManager(HeadsUpManagerPhone headsUpManager) {
mHeadsUpManager = headsUpManager;
mAmbientState.setHeadsUpManager(headsUpManager);
}
@@ -4324,8 +4342,8 @@
return mIsExpanded;
}
- public void setPulsing(Collection<HeadsUpManager.HeadsUpEntry> pulsing, int clockBottom) {
- if (mPulsing == null && pulsing == null) {
+ public void setPulsing(boolean pulsing, int clockBottom) {
+ if (!mPulsing && !pulsing) {
return;
}
mPulsing = pulsing;
@@ -4463,7 +4481,7 @@
pw.println(String.format("[%s: pulsing=%s qsCustomizerShowing=%s visibility=%s"
+ " alpha:%f scrollY:%d]",
this.getClass().getSimpleName(),
- mPulsing != null ?"T":"f",
+ mPulsing ? "T":"f",
mAmbientState.isQsCustomizerShowing() ? "T":"f",
getVisibility() == View.VISIBLE ? "visible"
: getVisibility() == View.GONE ? "gone"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/ViewState.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/ViewState.java
index 682b849..04a7bd7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/ViewState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/ViewState.java
@@ -30,7 +30,7 @@
import com.android.systemui.statusbar.ExpandableView;
import com.android.systemui.statusbar.notification.AnimatableProperty;
import com.android.systemui.statusbar.notification.PropertyAnimator;
-import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.policy.HeadsUpUtil;
/**
* A state of a view. This can be used to apply a set of view properties to a view with
@@ -582,7 +582,7 @@
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
- HeadsUpManager.setIsClickedNotification(child, false);
+ HeadsUpUtil.setIsClickedHeadsUpNotification(child, false);
child.setTag(TAG_ANIMATOR_TRANSLATION_Y, null);
child.setTag(TAG_START_TRANSLATION_Y, null);
child.setTag(TAG_END_TRANSLATION_Y, null);
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
index a166db5..9aee00e 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
@@ -59,7 +59,9 @@
import java.io.FileDescriptor;
import java.io.PrintWriter;
+import java.util.ArrayList;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -76,7 +78,7 @@
private static final int DYNAMIC_STREAM_START_INDEX = 100;
private static final int VIBRATE_HINT_DURATION = 50;
- private static final ArrayMap<Integer, Integer> STREAMS = new ArrayMap<>();
+ static final ArrayMap<Integer, Integer> STREAMS = new ArrayMap<>();
static {
STREAMS.put(AudioSystem.STREAM_ALARM, R.string.stream_alarm);
STREAMS.put(AudioSystem.STREAM_BLUETOOTH_SCO, R.string.stream_bluetooth_sco);
@@ -109,8 +111,10 @@
private boolean mShowVolumeDialog;
private boolean mShowSafetyWarning;
private DeviceCallback mDeviceCallback = new DeviceCallback();
- private AudioDeviceInfo mConnectedDevice;
private final NotificationManager mNotificationManager;
+ @GuardedBy("mLock")
+ private List<AudioDeviceInfo> mConnectedDevices = new ArrayList<>();
+ private Object mLock = new Object();
private boolean mDestroyed;
private VolumePolicy mVolumePolicy;
@@ -1055,26 +1059,25 @@
protected final class DeviceCallback extends AudioDeviceCallback {
public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
- for (AudioDeviceInfo info : addedDevices) {
- if (info.isSink()
- && (info.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP
- || info.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_SCO)) {
- mConnectedDevice = info;
- mCallbacks.onConnectedDeviceChanged(info.getProductName().toString());
+ synchronized (mLock) {
+ for (AudioDeviceInfo info : addedDevices) {
+ if (info.isSink()
+ && (info.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP
+ || info.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_SCO)) {
+ mConnectedDevices.add(info);
+ mCallbacks.onConnectedDeviceChanged(info.getProductName().toString());
+ }
}
}
}
public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
- if (mConnectedDevice == null) {
- mCallbacks.onConnectedDeviceChanged(null);
- return;
- }
- for (AudioDeviceInfo info : removedDevices) {
- if (info.isSink() == mConnectedDevice.isSink()
- && Objects.equals(info.getProductName(), mConnectedDevice.getProductName())
- && info.getType() == mConnectedDevice.getType()) {
- mConnectedDevice = null;
+ synchronized (mLock) {
+ for (AudioDeviceInfo info : removedDevices) {
+ mConnectedDevices.remove(info);
+ }
+
+ if (mConnectedDevices.size() == 0) {
mCallbacks.onConnectedDeviceChanged(null);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 8cfdeeb..001a582 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -63,7 +63,6 @@
import android.view.Window;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
-import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
import android.view.animation.DecelerateInterpolator;
import android.widget.ImageButton;
@@ -79,6 +78,7 @@
import com.android.systemui.plugins.VolumeDialogController;
import com.android.systemui.plugins.VolumeDialogController.State;
import com.android.systemui.plugins.VolumeDialogController.StreamState;
+import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -111,7 +111,7 @@
private ConfigurableTexts mConfigurableTexts;
private final SparseBooleanArray mDynamic = new SparseBooleanArray();
private final KeyguardManager mKeyguard;
- private final AccessibilityManager mAccessibilityMgr;
+ private final AccessibilityManagerWrapper mAccessibilityMgr;
private final Object mSafetyWarningLock = new Object();
private final Object mOutputChooserLock = new Object();
private final Accessibility mAccessibility = new Accessibility();
@@ -134,8 +134,7 @@
mContext = new ContextThemeWrapper(context, com.android.systemui.R.style.qs_theme);
mController = Dependency.get(VolumeDialogController.class);
mKeyguard = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
- mAccessibilityMgr =
- (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
+ mAccessibilityMgr = Dependency.get(AccessibilityManagerWrapper.class);
mActiveSliderTint = ColorStateList.valueOf(Utils.getColorAccent(mContext));
mInactiveSliderTint = loadColorStateList(R.color.volume_slider_inactive);
}
@@ -231,6 +230,10 @@
initRingerH();
}
+ protected ViewGroup getDialogView() {
+ return mDialogView;
+ }
+
private ColorStateList loadColorStateList(int colorResId) {
return ColorStateList.valueOf(mContext.getColor(colorResId));
}
@@ -258,6 +261,7 @@
private void addRow(int stream, int iconRes, int iconMuteRes, boolean important,
boolean defaultStream, boolean dynamic) {
+ if (D.BUG) Slog.d(TAG, "Adding row for stream " + stream);
VolumeRow row = new VolumeRow();
initRow(row, stream, iconRes, iconMuteRes, important, defaultStream);
int rowSize;
@@ -621,7 +625,7 @@
}
}
- private void onStateChangedH(State state) {
+ protected void onStateChangedH(State state) {
mState = state;
mDynamic.clear();
// add any new dynamic rows
@@ -894,7 +898,7 @@
return ss.remoteLabel;
}
try {
- return mContext.getString(ss.name);
+ return mContext.getResources().getString(ss.name);
} catch (Resources.NotFoundException e) {
Slog.e(TAG, "Can't find translation for stream " + ss);
return "";
diff --git a/packages/SystemUI/tests/src/com/android/systemui/RoundedCornersTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
similarity index 66%
rename from packages/SystemUI/tests/src/com/android/systemui/RoundedCornersTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
index 2a44771..2f05b06 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/RoundedCornersTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
@@ -14,9 +14,13 @@
package com.android.systemui;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY;
+
import static com.android.systemui.tuner.TunablePadding.FLAG_END;
import static com.android.systemui.tuner.TunablePadding.FLAG_START;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
@@ -29,6 +33,7 @@
import static org.mockito.Mockito.when;
import android.app.Fragment;
+import android.content.res.Configuration;
import android.support.test.filters.SmallTest;
import android.testing.AndroidTestingRunner;
import android.view.Display;
@@ -36,7 +41,7 @@
import android.view.WindowManager;
import com.android.systemui.R.dimen;
-import com.android.systemui.RoundedCorners.TunablePaddingTagListener;
+import com.android.systemui.ScreenDecorations.TunablePaddingTagListener;
import com.android.systemui.fragments.FragmentHostManager;
import com.android.systemui.fragments.FragmentService;
import com.android.systemui.statusbar.phone.StatusBar;
@@ -51,9 +56,9 @@
@RunWith(AndroidTestingRunner.class)
@SmallTest
-public class RoundedCornersTest extends SysuiTestCase {
+public class ScreenDecorationsTest extends SysuiTestCase {
- private RoundedCorners mRoundedCorners;
+ private ScreenDecorations mScreenDecorations;
private StatusBar mStatusBar;
private WindowManager mWindowManager;
private FragmentService mFragmentService;
@@ -81,20 +86,22 @@
mTunerService = mDependency.injectMockDependency(TunerService.class);
- mRoundedCorners = new RoundedCorners();
- mRoundedCorners.mContext = mContext;
- mRoundedCorners.mComponents = mContext.getComponents();
+ mScreenDecorations = new ScreenDecorations();
+ mScreenDecorations.mContext = mContext;
+ mScreenDecorations.mComponents = mContext.getComponents();
mTunablePaddingService = mDependency.injectMockDependency(TunablePaddingService.class);
}
@Test
- public void testNoRounding() {
+ public void testNoRounding_NoCutout() {
+ mContext.getOrCreateTestableResources().addOverride(
+ com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout, false);
mContext.getOrCreateTestableResources().addOverride(dimen.rounded_corner_radius, 0);
mContext.getOrCreateTestableResources()
.addOverride(dimen.rounded_corner_content_padding, 0);
- mRoundedCorners.start();
+ mScreenDecorations.start();
// No views added.
verify(mWindowManager, never()).addView(any(), any());
// No Fragments watched.
@@ -105,11 +112,13 @@
@Test
public void testRounding() {
+ mContext.getOrCreateTestableResources().addOverride(
+ com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout, false);
mContext.getOrCreateTestableResources().addOverride(dimen.rounded_corner_radius, 20);
mContext.getOrCreateTestableResources()
.addOverride(dimen.rounded_corner_content_padding, 20);
- mRoundedCorners.start();
+ mScreenDecorations.start();
// Add 2 windows for rounded corners (top and bottom).
verify(mWindowManager, times(2)).addView(any(), any());
@@ -122,6 +131,44 @@
}
@Test
+ public void testCutout() {
+ mContext.getOrCreateTestableResources().addOverride(
+ com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout, true);
+ mContext.getOrCreateTestableResources().addOverride(dimen.rounded_corner_radius, 0);
+ mContext.getOrCreateTestableResources()
+ .addOverride(dimen.rounded_corner_content_padding, 0);
+
+ mScreenDecorations.start();
+ // Add 2 windows for rounded corners (top and bottom).
+ verify(mWindowManager, times(2)).addView(any(), any());
+ }
+
+ @Test
+ public void testDelayedCutout() {
+ mContext.getOrCreateTestableResources().addOverride(
+ com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout, false);
+ mContext.getOrCreateTestableResources().addOverride(dimen.rounded_corner_radius, 0);
+ mContext.getOrCreateTestableResources()
+ .addOverride(dimen.rounded_corner_content_padding, 0);
+
+ mScreenDecorations.start();
+
+ mContext.getOrCreateTestableResources().addOverride(
+ com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout, true);
+ mScreenDecorations.onConfigurationChanged(new Configuration());
+
+ // Add 2 windows for rounded corners (top and bottom).
+ verify(mWindowManager, times(2)).addView(any(), any());
+ }
+
+ @Test
+ public void hasRoundedCornerOverlayFlagSet() {
+ assertThat(mScreenDecorations.getWindowLayoutParams().privateFlags
+ & PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY,
+ is(PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY));
+ }
+
+ @Test
public void testPaddingTagListener() {
TunablePaddingTagListener tagListener = new TunablePaddingTagListener(14, 5);
View v = mock(View.class);
@@ -136,7 +183,7 @@
// Trigger callback and verify we get a TunablePadding created.
tagListener.onFragmentViewCreated(null, f);
- verify(mTunablePaddingService).add(eq(child), eq(RoundedCorners.PADDING), eq(14),
+ verify(mTunablePaddingService).add(eq(child), eq(ScreenDecorations.PADDING), eq(14),
eq(FLAG_START | FLAG_END));
// Call again and verify destroy is called.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationTestHelper.java
index 6e7477f..f3c1171 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationTestHelper.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationTestHelper.java
@@ -32,6 +32,7 @@
import com.android.systemui.statusbar.notification.AboveShelfObserver;
import com.android.systemui.statusbar.notification.InflationException;
import com.android.systemui.statusbar.notification.NotificationInflaterTest;
+import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.policy.HeadsUpManager;
@@ -51,7 +52,7 @@
public NotificationTestHelper(Context context) {
mContext = context;
mInstrumentation = InstrumentationRegistry.getInstrumentation();
- mHeadsUpManager = new HeadsUpManager(mContext, null, mGroupManager);
+ mHeadsUpManager = new HeadsUpManagerPhone(mContext, null, mGroupManager, null, null);
}
public ExpandableNotificationRow createRow() throws Exception {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java
new file mode 100644
index 0000000..28f9417
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone;
+
+import android.app.ActivityManager;
+import android.app.Notification;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.view.View;
+import android.service.notification.StatusBarNotification;
+import android.support.test.filters.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.StatusBarIconView;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
+import static junit.framework.Assert.assertFalse;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class HeadsUpManagerPhoneTest extends SysuiTestCase {
+ @Rule public MockitoRule rule = MockitoJUnit.rule();
+
+ private static final String TEST_PACKAGE_NAME = "test";
+ private static final int TEST_UID = 0;
+
+ private HeadsUpManagerPhone mHeadsUpManager;
+
+ private NotificationData.Entry mEntry;
+ private StatusBarNotification mSbn;
+
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+
+ @Mock private NotificationGroupManager mGroupManager;
+ @Mock private View mStatusBarWindowView;
+ @Mock private StatusBar mBar;
+ @Mock private ExpandableNotificationRow mRow;
+ @Mock private VisualStabilityManager mVSManager;
+
+ @Before
+ public void setUp() {
+ when(mVSManager.isReorderingAllowed()).thenReturn(true);
+
+ mHeadsUpManager = new HeadsUpManagerPhone(mContext, mStatusBarWindowView, mGroupManager, mBar, mVSManager);
+
+ Notification.Builder n = new Notification.Builder(mContext, "")
+ .setSmallIcon(R.drawable.ic_person)
+ .setContentTitle("Title")
+ .setContentText("Text");
+ mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID,
+ 0, n.build(), new UserHandle(ActivityManager.getCurrentUser()), null, 0);
+
+ mEntry = new NotificationData.Entry(mSbn);
+ mEntry.row = mRow;
+ mEntry.expandedIcon = mock(StatusBarIconView.class);
+ }
+
+ @Test
+ public void testBasicOperations() {
+ // Check the initial state.
+ assertNull(mHeadsUpManager.getEntry(mEntry.key));
+ assertNull(mHeadsUpManager.getTopEntry());
+ assertEquals(0, mHeadsUpManager.getAllEntries().count());
+ assertFalse(mHeadsUpManager.hasHeadsUpNotifications());
+
+ // Add a notification.
+ mHeadsUpManager.showNotification(mEntry);
+
+ assertEquals(mEntry, mHeadsUpManager.getEntry(mEntry.key));
+ assertEquals(mEntry, mHeadsUpManager.getTopEntry());
+ assertEquals(1, mHeadsUpManager.getAllEntries().count());
+ assertTrue(mHeadsUpManager.hasHeadsUpNotifications());
+
+ // Update the notification.
+ mHeadsUpManager.updateNotification(mEntry, false);
+
+ assertEquals(mEntry, mHeadsUpManager.getEntry(mEntry.key));
+ assertEquals(mEntry, mHeadsUpManager.getTopEntry());
+ assertEquals(1, mHeadsUpManager.getAllEntries().count());
+ assertTrue(mHeadsUpManager.hasHeadsUpNotifications());
+
+ // Remove but defer, since the notification is visible on display.
+ mHeadsUpManager.removeNotification(mEntry.key, false);
+
+ assertEquals(mEntry, mHeadsUpManager.getEntry(mEntry.key));
+ assertEquals(mEntry, mHeadsUpManager.getTopEntry());
+ assertEquals(1, mHeadsUpManager.getAllEntries().count());
+ assertTrue(mHeadsUpManager.hasHeadsUpNotifications());
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
index bdf9b1f..31442af 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
@@ -86,8 +86,8 @@
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.notification.ActivityLaunchAnimator;
import com.android.systemui.statusbar.notification.VisualStabilityManager;
+import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
-import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.KeyguardMonitor;
import com.android.systemui.statusbar.policy.KeyguardMonitorImpl;
import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
@@ -110,7 +110,7 @@
@Mock private UnlockMethodCache mUnlockMethodCache;
@Mock private KeyguardIndicationController mKeyguardIndicationController;
@Mock private NotificationStackScrollLayout mStackScroller;
- @Mock private HeadsUpManager mHeadsUpManager;
+ @Mock private HeadsUpManagerPhone mHeadsUpManager;
@Mock private SystemServicesProxy mSystemServicesProxy;
@Mock private NotificationPanelView mNotificationPanelView;
@Mock private IStatusBarService mBarService;
@@ -588,7 +588,7 @@
static class TestableStatusBar extends StatusBar {
public TestableStatusBar(StatusBarKeyguardViewManager man,
UnlockMethodCache unlock, KeyguardIndicationController key,
- NotificationStackScrollLayout stack, HeadsUpManager hum,
+ NotificationStackScrollLayout stack, HeadsUpManagerPhone hum,
PowerManager pm, NotificationPanelView panelView,
IStatusBarService barService, NotificationListener notificationListener,
NotificationLogger notificationLogger,
@@ -650,7 +650,7 @@
public void setUpForTest(NotificationPresenter presenter,
NotificationListContainer listContainer,
Callback callback,
- HeadsUpManager headsUpManager,
+ HeadsUpManagerPhone headsUpManager,
NotificationData notificationData) {
super.setUpWithPresenter(presenter, listContainer, callback, headsUpManager);
mNotificationData = notificationData;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
new file mode 100644
index 0000000..2d28c9f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume;
+
+import static com.android.systemui.volume.Events.DISMISS_REASON_UNKNOWN;
+import static com.android.systemui.volume.Events.SHOW_REASON_UNKNOWN;
+import static com.android.systemui.volume.VolumeDialogControllerImpl.STREAMS;
+
+import static junit.framework.Assert.assertTrue;
+
+import android.app.KeyguardManager;
+import android.media.AudioManager;
+import android.support.test.filters.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.VolumeDialogController;
+import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.function.Predicate;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class VolumeDialogImplTest extends SysuiTestCase {
+
+ VolumeDialogImpl mDialog;
+
+ @Mock
+ VolumeDialogController mController;
+
+ @Mock
+ KeyguardManager mKeyguard;
+
+ @Mock
+ AccessibilityManagerWrapper mAccessibilityMgr;
+
+ @Before
+ public void setup() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mController = mDependency.injectMockDependency(VolumeDialogController.class);
+ mAccessibilityMgr = mDependency.injectMockDependency(AccessibilityManagerWrapper.class);
+ getContext().addMockSystemService(KeyguardManager.class, mKeyguard);
+
+ mDialog = new VolumeDialogImpl(getContext());
+ mDialog.init(0, null);
+ VolumeDialogController.State state = new VolumeDialogController.State();
+ for (int i = AudioManager.STREAM_VOICE_CALL; i <= AudioManager.STREAM_ACCESSIBILITY; i++) {
+ VolumeDialogController.StreamState ss = new VolumeDialogController.StreamState();
+ ss.name = STREAMS.get(i);
+ state.states.append(i, ss);
+ }
+ mDialog.onStateChangedH(state);
+ }
+
+ private void navigateViews(View view, Predicate<View> condition) {
+ if (view instanceof ViewGroup) {
+ ViewGroup viewGroup = (ViewGroup) view;
+ for (int i = 0; i < viewGroup.getChildCount(); i++) {
+ navigateViews(viewGroup.getChildAt(i), condition);
+ }
+ } else {
+ String resourceName = null;
+ try {
+ resourceName = getContext().getResources().getResourceName(view.getId());
+ } catch (Exception e) {}
+ assertTrue("View " + resourceName != null ? resourceName : view.getId()
+ + " failed test", condition.test(view));
+ }
+ }
+
+ @Test
+ public void testContentDescriptions() {
+ mDialog.show(SHOW_REASON_UNKNOWN);
+ ViewGroup dialog = mDialog.getDialogView();
+
+ navigateViews(dialog, view -> {
+ if (view instanceof ImageView) {
+ return !TextUtils.isEmpty(view.getContentDescription());
+ } else {
+ return true;
+ }
+ });
+
+ mDialog.dismiss(DISMISS_REASON_UNKNOWN);
+ }
+
+}
diff --git a/proto/src/gnss.proto b/proto/src/gnss.proto
index c54ddad..0168392 100644
--- a/proto/src/gnss.proto
+++ b/proto/src/gnss.proto
@@ -42,4 +42,20 @@
// Standard deviation of top 4 average CN0 (dB-Hz)
optional double standard_deviation_top_four_average_cn0_db_hz = 11;
-}
\ No newline at end of file
+
+ // Power metrics
+ optional PowerMetrics power_metrics = 12;
+}
+
+// Power metrics
+message PowerMetrics {
+
+ // Duration of power log (ms)
+ optional int64 logging_duration_ms = 1;
+
+ // Energy consumed (mAh)
+ optional double energy_consumed_mah = 2;
+
+ // Time spent in signal quality level (ms)
+ repeated int64 time_in_signal_quality_level_ms = 3;
+}
diff --git a/proto/src/metrics_constants.proto b/proto/src/metrics_constants.proto
index 03dfd46..7539d88 100644
--- a/proto/src/metrics_constants.proto
+++ b/proto/src/metrics_constants.proto
@@ -5168,6 +5168,11 @@
// OS: P
ROTATION_SUGGESTION_SHOWN = 1288;
+ // An autofill service was bound using an unofficial(but still supported) permission.
+ // Package: Package of the autofill service
+ // OS: P
+ AUTOFILL_INVALID_PERMISSION = 1289;
+
// ---- End P Constants, all P constants go above this line ----
// Add new aosp constants above this line.
// END OF AOSP CONSTANTS
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
index 0e2ca14..2b73b33 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
@@ -617,6 +617,23 @@
}
@Override
+ public String getUserDataId() throws RemoteException {
+ final int userId = UserHandle.getCallingUserId();
+
+ synchronized (mLock) {
+ final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId);
+ if (service != null) {
+ final UserData userData = service.getUserData(getCallingUid());
+ return userData == null ? null : userData.getId();
+ } else if (sVerbose) {
+ Slog.v(TAG, "getUserDataId(): no service for " + userId);
+ }
+ }
+
+ return null;
+ }
+
+ @Override
public void setUserData(UserData userData) throws RemoteException {
final int userId = UserHandle.getCallingUserId();
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 6b44fa5..6108afa 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -111,7 +111,7 @@
* until the user authenticates or it times out.
*/
final class Session implements RemoteFillService.FillServiceCallbacks, ViewState.Listener,
- AutoFillUI.AutoFillUiCallback {
+ AutoFillUI.AutoFillUiCallback, ValueFinder {
private static final String TAG = "AutofillSession";
private static final String EXTRA_REQUEST_ID = "android.service.autofill.extra.REQUEST_ID";
@@ -310,43 +310,56 @@
return ids;
}
- /**
- * Gets the value of a field, using either the {@code viewStates} or the {@code mContexts}, or
- * {@code null} when not found on either of them.
- */
+ @Override
@Nullable
- private String getValueAsString(@NonNull AutofillId id) {
- AutofillValue value = null;
+ public String findByAutofillId(@NonNull AutofillId id) {
synchronized (mLock) {
- final ViewState state = mViewStates.get(id);
- if (state == null) {
- if (sDebug) Slog.d(TAG, "getValue(): no view state for " + id);
- return null;
- }
- value = state.getCurrentValue();
- if (value == null) {
- if (sDebug) Slog.d(TAG, "getValue(): no current value for " + id);
- value = getValueFromContextsLocked(id);
- }
- }
- if (value != null) {
- if (value.isText()) {
- return value.getTextValue().toString();
- }
- if (value.isList()) {
- final CharSequence[] options = getAutofillOptionsFromContextsLocked(id);
- if (options != null) {
- final int index = value.getListValue();
- final CharSequence option = options[index];
- return option != null ? option.toString() : null;
- } else {
- Slog.w(TAG, "getValueAsString(): no autofill options for id " + id);
+ AutofillValue value = findValueLocked(id);
+ if (value != null) {
+ if (value.isText()) {
+ return value.getTextValue().toString();
+ }
+
+ if (value.isList()) {
+ final CharSequence[] options = getAutofillOptionsFromContextsLocked(id);
+ if (options != null) {
+ final int index = value.getListValue();
+ final CharSequence option = options[index];
+ return option != null ? option.toString() : null;
+ } else {
+ Slog.w(TAG, "findByAutofillId(): no autofill options for id " + id);
+ }
}
}
}
return null;
}
+ @Override
+ public AutofillValue findRawValueByAutofillId(AutofillId id) {
+ synchronized (mLock) {
+ return findValueLocked(id);
+ }
+ }
+
+ /**
+ * <p>Gets the value of a field, using either the {@code viewStates} or the {@code mContexts},
+ * or {@code null} when not found on either of them.
+ */
+ private AutofillValue findValueLocked(@NonNull AutofillId id) {
+ final ViewState state = mViewStates.get(id);
+ if (state == null) {
+ if (sDebug) Slog.d(TAG, "findValueLocked(): no view state for " + id);
+ return null;
+ }
+ AutofillValue value = state.getCurrentValue();
+ if (value == null) {
+ if (sDebug) Slog.d(TAG, "findValueLocked(): no current value for " + id);
+ value = getValueFromContextsLocked(id);
+ }
+ return value;
+ }
+
/**
* Updates values of the nodes in the context's structure so that:
* - proper node is focused
@@ -1355,14 +1368,12 @@
if (sDebug) {
Slog.d(TAG, "at least one field changed, validate fields for save UI");
}
- final ValueFinder valueFinder = (id) -> {return getValueAsString(id);};
-
final InternalValidator validator = saveInfo.getValidator();
if (validator != null) {
final LogMaker log = newLogMaker(MetricsEvent.AUTOFILL_SAVE_VALIDATION);
boolean isValid;
try {
- isValid = validator.isValid(valueFinder);
+ isValid = validator.isValid(this);
if (sDebug) Slog.d(TAG, validator + " returned " + isValid);
log.setType(isValid
? MetricsEvent.TYPE_SUCCESS
@@ -1404,10 +1415,13 @@
}
final AutofillValue datasetValue = datasetValues.get(id);
if (!currentValue.equals(datasetValue)) {
- if (sDebug) Slog.d(TAG, "found a change on id " + id);
+ if (sDebug) {
+ Slog.d(TAG, "found a dataset change on id " + id + ": from "
+ + datasetValue + " to " + currentValue);
+ }
continue datasets_loop;
}
- if (sVerbose) Slog.v(TAG, "no changes for id " + id);
+ if (sVerbose) Slog.v(TAG, "no dataset changes for id " + id);
}
if (sDebug) {
Slog.d(TAG, "ignoring Save UI because all fields match contents of "
@@ -1425,7 +1439,7 @@
final IAutoFillManagerClient client = getClient();
mPendingSaveUi = new PendingUi(mActivityToken, id, client);
getUiForShowing().showSaveUi(mService.getServiceLabel(), mService.getServiceIcon(),
- mService.getServicePackageName(), saveInfo, valueFinder,
+ mService.getServicePackageName(), saveInfo, this,
mComponentName.getPackageName(), this,
mPendingSaveUi);
if (client != null) {
diff --git a/services/core/java/com/android/server/AlarmManagerService.java b/services/core/java/com/android/server/AlarmManagerService.java
index 342b48e..30dfee8 100644
--- a/services/core/java/com/android/server/AlarmManagerService.java
+++ b/services/core/java/com/android/server/AlarmManagerService.java
@@ -16,6 +16,7 @@
package com.android.server;
+import android.annotation.UserIdInt;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.AlarmManager;
@@ -26,6 +27,8 @@
import android.app.IAlarmManager;
import android.app.IUidObserver;
import android.app.PendingIntent;
+import android.app.usage.UsageStatsManager;
+import android.app.usage.UsageStatsManagerInternal;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
@@ -57,6 +60,7 @@
import android.util.ArrayMap;
import android.util.KeyValueListParser;
import android.util.Log;
+import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
@@ -75,6 +79,7 @@
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.LinkedList;
import java.util.Locale;
import java.util.Random;
@@ -120,6 +125,7 @@
static final boolean DEBUG_LISTENER_CALLBACK = localLOGV || false;
static final boolean DEBUG_WAKELOCK = localLOGV || false;
static final boolean DEBUG_BG_LIMIT = localLOGV || false;
+ static final boolean DEBUG_STANDBY = localLOGV || false;
static final boolean RECORD_ALARMS_IN_HISTORY = true;
static final boolean RECORD_DEVICE_IDLE_ALARMS = false;
static final int ALARM_EVENT = 1;
@@ -140,6 +146,7 @@
AppOpsManager mAppOps;
DeviceIdleController.LocalService mLocalDeviceIdleController;
+ private UsageStatsManagerInternal mUsageStatsManagerInternal;
final Object mLock = new Object();
@@ -215,6 +222,14 @@
}
final ArrayList<IdleDispatchEntry> mAllowWhileIdleDispatches = new ArrayList();
+ interface Stats {
+ int REBATCH_ALL_ALARMS = 0;
+ }
+
+ private final StatLogger mStatLogger = new StatLogger(new String[] {
+ "REBATCH_ALL_ALARMS",
+ });
+
/**
* Broadcast options to use for FLAG_ALLOW_WHILE_IDLE.
*/
@@ -233,6 +248,8 @@
new SparseArray<>();
private final ForceAppStandbyTracker mForceAppStandbyTracker;
+ private boolean mAppStandbyParole;
+ private ArrayMap<Pair<String, Integer>, Long> mLastAlarmDeliveredForPackage = new ArrayMap<>();
/**
* All times are in milliseconds. These constants are kept synchronized with the system
@@ -249,13 +266,28 @@
= "allow_while_idle_whitelist_duration";
private static final String KEY_LISTENER_TIMEOUT = "listener_timeout";
+ // Keys for specifying throttling delay based on app standby bucketing
+ private final String[] KEYS_APP_STANDBY_DELAY = {
+ "standby_active_delay",
+ "standby_working_delay",
+ "standby_frequent_delay",
+ "standby_rare_delay",
+ "standby_never_delay",
+ };
+
private static final long DEFAULT_MIN_FUTURITY = 5 * 1000;
private static final long DEFAULT_MIN_INTERVAL = 60 * 1000;
private static final long DEFAULT_ALLOW_WHILE_IDLE_SHORT_TIME = DEFAULT_MIN_FUTURITY;
private static final long DEFAULT_ALLOW_WHILE_IDLE_LONG_TIME = 9*60*1000;
private static final long DEFAULT_ALLOW_WHILE_IDLE_WHITELIST_DURATION = 10*1000;
-
private static final long DEFAULT_LISTENER_TIMEOUT = 5 * 1000;
+ private final long[] DEFAULT_APP_STANDBY_DELAYS = {
+ 0, // Active
+ 6 * 60_000, // Working
+ 30 * 60_000, // Frequent
+ 2 * 60 * 60_000, // Rare
+ 10 * 24 * 60 * 60_000 // Never
+ };
// Minimum futurity of a new alarm
public long MIN_FUTURITY = DEFAULT_MIN_FUTURITY;
@@ -276,6 +308,8 @@
// Direct alarm listener callback timeout
public long LISTENER_TIMEOUT = DEFAULT_LISTENER_TIMEOUT;
+ public long[] APP_STANDBY_MIN_DELAYS = new long[DEFAULT_APP_STANDBY_DELAYS.length];
+
private ContentResolver mResolver;
private final KeyValueListParser mParser = new KeyValueListParser(',');
private long mLastAllowWhileIdleWhitelistDuration = -1;
@@ -328,7 +362,12 @@
DEFAULT_ALLOW_WHILE_IDLE_WHITELIST_DURATION);
LISTENER_TIMEOUT = mParser.getLong(KEY_LISTENER_TIMEOUT,
DEFAULT_LISTENER_TIMEOUT);
-
+ APP_STANDBY_MIN_DELAYS[0] = mParser.getDurationMillis(KEYS_APP_STANDBY_DELAY[0],
+ DEFAULT_APP_STANDBY_DELAYS[0]);
+ for (int i = 1; i < KEYS_APP_STANDBY_DELAY.length; i++) {
+ APP_STANDBY_MIN_DELAYS[i] = mParser.getDurationMillis(KEYS_APP_STANDBY_DELAY[i],
+ Math.max(APP_STANDBY_MIN_DELAYS[i-1], DEFAULT_APP_STANDBY_DELAYS[i]));
+ }
updateAllowWhileIdleWhitelistDurationLocked();
}
}
@@ -359,6 +398,12 @@
pw.print(" "); pw.print(KEY_ALLOW_WHILE_IDLE_WHITELIST_DURATION); pw.print("=");
TimeUtils.formatDuration(ALLOW_WHILE_IDLE_WHITELIST_DURATION, pw);
pw.println();
+
+ for (int i = 0; i < KEYS_APP_STANDBY_DELAY.length; i++) {
+ pw.print(" "); pw.print(KEYS_APP_STANDBY_DELAY[i]); pw.print("=");
+ TimeUtils.formatDuration(APP_STANDBY_MIN_DELAYS[i], pw);
+ pw.println();
+ }
}
void dumpProto(ProtoOutputStream proto, long fieldId) {
@@ -618,9 +663,7 @@
}
PriorityClass packagePrio = a.priorityClass;
- String alarmPackage = (a.operation != null)
- ? a.operation.getCreatorPackage()
- : a.packageName;
+ String alarmPackage = a.sourcePackage;
if (packagePrio == null) packagePrio = mPriorities.get(alarmPackage);
if (packagePrio == null) {
packagePrio = a.priorityClass = new PriorityClass(); // lowest prio & stale sequence
@@ -751,6 +794,7 @@
}
void rebatchAllAlarmsLocked(boolean doValidate) {
+ long start = mStatLogger.getTime();
final int oldCount =
getAlarmCount(mAlarmBatches) + ArrayUtils.size(mPendingWhileIdleAlarms);
final boolean oldHasTick = haveBatchesTimeTickAlarm(mAlarmBatches)
@@ -790,6 +834,7 @@
rescheduleKernelAlarmsLocked();
updateNextAlarmClockLocked();
+ mStatLogger.logDurationStat(Stats.REBATCH_ALL_ALARMS, start);
}
void reAddAlarmLocked(Alarm a, long nowElapsed, boolean doValidate) {
@@ -905,7 +950,7 @@
// Recurring alarms may have passed several alarm intervals while the
// alarm was kept pending. Send the appropriate trigger count.
if (alarm.repeatInterval > 0) {
- alarm.count += (nowELAPSED - alarm.whenElapsed) / alarm.repeatInterval;
+ alarm.count += (nowELAPSED - alarm.requestedWhenElapsed) / alarm.repeatInterval;
// Also schedule its next recurrence
final long delta = alarm.count * alarm.repeatInterval;
final long nextElapsed = alarm.whenElapsed + delta;
@@ -1228,6 +1273,8 @@
mAppOps = (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE);
mLocalDeviceIdleController
= LocalServices.getService(DeviceIdleController.LocalService.class);
+ mUsageStatsManagerInternal = LocalServices.getService(UsageStatsManagerInternal.class);
+ mUsageStatsManagerInternal.addAppIdleStateChangeListener(new AppStandbyTracker());
}
}
@@ -1377,6 +1424,51 @@
setImplLocked(a, false, doValidate);
}
+ private long getMinDelayForBucketLocked(int bucket) {
+ // Return the minimum time that should elapse before an app in the specified bucket
+ // can receive alarms again
+ if (bucket == UsageStatsManager.STANDBY_BUCKET_NEVER) {
+ return mConstants.APP_STANDBY_MIN_DELAYS[4];
+ }
+ else if (bucket >= UsageStatsManager.STANDBY_BUCKET_RARE) {
+ return mConstants.APP_STANDBY_MIN_DELAYS[3];
+ }
+ else if (bucket >= UsageStatsManager.STANDBY_BUCKET_FREQUENT) {
+ return mConstants.APP_STANDBY_MIN_DELAYS[2];
+ }
+ else if (bucket >= UsageStatsManager.STANDBY_BUCKET_WORKING_SET) {
+ return mConstants.APP_STANDBY_MIN_DELAYS[1];
+ }
+ else return mConstants.APP_STANDBY_MIN_DELAYS[0];
+ }
+
+ private void adjustDeliveryTimeBasedOnStandbyBucketLocked(Alarm alarm) {
+ if (alarm.alarmClock != null || UserHandle.isCore(alarm.creatorUid)) {
+ return;
+ }
+ if (mAppStandbyParole) {
+ if (alarm.whenElapsed > alarm.requestedWhenElapsed) {
+ // We did throttle this alarm earlier, restore original requirements
+ alarm.whenElapsed = alarm.requestedWhenElapsed;
+ alarm.maxWhenElapsed = alarm.requestedMaxWhenElapsed;
+ }
+ return;
+ }
+ final String sourcePackage = alarm.sourcePackage;
+ final int sourceUserId = UserHandle.getUserId(alarm.creatorUid);
+ final int standbyBucket = mUsageStatsManagerInternal.getAppStandbyBucket(
+ sourcePackage, sourceUserId, SystemClock.elapsedRealtime());
+
+ final Pair<String, Integer> packageUser = Pair.create(sourcePackage, sourceUserId);
+ final long lastElapsed = mLastAlarmDeliveredForPackage.getOrDefault(packageUser, 0L);
+ if (lastElapsed > 0) {
+ final long minElapsed = lastElapsed + getMinDelayForBucketLocked(standbyBucket);
+ if (alarm.requestedWhenElapsed < minElapsed) {
+ alarm.whenElapsed = alarm.maxWhenElapsed = minElapsed;
+ }
+ }
+ }
+
private void setImplLocked(Alarm a, boolean rebatching, boolean doValidate) {
if ((a.flags&AlarmManager.FLAG_IDLE_UNTIL) != 0) {
// This is a special alarm that will put the system into idle until it goes off.
@@ -1428,6 +1520,7 @@
mAllowWhileIdleDispatches.add(ent);
}
}
+ adjustDeliveryTimeBasedOnStandbyBucketLocked(a);
int whichBatch = ((a.flags&AlarmManager.FLAG_STANDALONE) != 0)
? -1 : attemptCoalesceLocked(a.whenElapsed, a.maxWhenElapsed);
@@ -1655,6 +1748,9 @@
mForceAppStandbyTracker.dump(pw, " ");
pw.println();
+ pw.println(" App Standby Parole: " + mAppStandbyParole);
+ pw.println();
+
final long nowRTC = System.currentTimeMillis();
final long nowELAPSED = SystemClock.elapsedRealtime();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
@@ -1753,6 +1849,15 @@
}
pw.println("]");
+ pw.println(" mLastAlarmDeliveredForPackage:");
+ for (int i = 0; i < mLastAlarmDeliveredForPackage.size(); i++) {
+ Pair<String, Integer> packageUser = mLastAlarmDeliveredForPackage.keyAt(i);
+ pw.print(" Package " + packageUser.first + ", User " + packageUser.second + ":");
+ TimeUtils.formatDuration(mLastAlarmDeliveredForPackage.valueAt(i), nowELAPSED, pw);
+ pw.println();
+ }
+ pw.println();
+
if (mPendingIdleUntil != null || mPendingWhileIdleAlarms.size() > 0) {
pw.println();
pw.println(" Idle mode state:");
@@ -1913,6 +2018,8 @@
}
}
}
+ pw.println();
+ mStatLogger.dump(pw, " ");
if (RECORD_DEVICE_IDLE_ALARMS) {
pw.println();
@@ -2746,8 +2853,7 @@
// Don't block starting foreground components
return false;
}
- final String sourcePackage =
- (alarm.operation != null) ? alarm.operation.getCreatorPackage() : alarm.packageName;
+ final String sourcePackage = alarm.sourcePackage;
final int sourceUid = alarm.creatorUid;
return mForceAppStandbyTracker.areAlarmsRestricted(sourceUid, sourcePackage,
allowWhileIdle);
@@ -2856,7 +2962,7 @@
if (alarm.repeatInterval > 0) {
// this adjustment will be zero if we're late by
// less than one full repeat interval
- alarm.count += (nowELAPSED - alarm.whenElapsed) / alarm.repeatInterval;
+ alarm.count += (nowELAPSED - alarm.requestedWhenElapsed) / alarm.repeatInterval;
// Also schedule its next recurrence
final long delta = alarm.count * alarm.repeatInterval;
@@ -2925,11 +3031,14 @@
public final int uid;
public final int creatorUid;
public final String packageName;
+ public final String sourcePackage;
public int count;
public long when;
public long windowLength;
public long whenElapsed; // 'when' in the elapsed time base
public long maxWhenElapsed; // also in the elapsed time base
+ public final long requestedWhenElapsed; // original expiry time requested by the app
+ public final long requestedMaxWhenElapsed;
public long repeatInterval;
public PriorityClass priorityClass;
@@ -2943,8 +3052,10 @@
|| _type == AlarmManager.RTC_WAKEUP;
when = _when;
whenElapsed = _whenElapsed;
+ requestedWhenElapsed = _whenElapsed;
windowLength = _windowLength;
maxWhenElapsed = _maxWhen;
+ requestedMaxWhenElapsed = _maxWhen;
repeatInterval = _interval;
operation = _op;
listener = _rec;
@@ -2955,7 +3066,7 @@
alarmClock = _info;
uid = _uid;
packageName = _pkgName;
-
+ sourcePackage = (operation != null) ? operation.getCreatorPackage() : packageName;
creatorUid = (operation != null) ? operation.getCreatorUid() : uid;
}
@@ -2980,9 +3091,7 @@
}
public boolean matches(String packageName) {
- return (operation != null)
- ? packageName.equals(operation.getTargetPackage())
- : packageName.equals(this.packageName);
+ return packageName.equals(sourcePackage);
}
@Override
@@ -2995,11 +3104,7 @@
sb.append(" when ");
sb.append(when);
sb.append(" ");
- if (operation != null) {
- sb.append(operation.getTargetPackage());
- } else {
- sb.append(packageName);
- }
+ sb.append(sourcePackage);
sb.append('}');
return sb.toString();
}
@@ -3009,6 +3114,8 @@
final boolean isRtc = (type == RTC || type == RTC_WAKEUP);
pw.print(prefix); pw.print("tag="); pw.println(statsTag);
pw.print(prefix); pw.print("type="); pw.print(type);
+ pw.print(" requestedWhenELapsed="); TimeUtils.formatDuration(
+ requestedWhenElapsed, nowELAPSED, pw);
pw.print(" whenElapsed="); TimeUtils.formatDuration(whenElapsed,
nowELAPSED, pw);
pw.print(" when=");
@@ -3249,8 +3356,6 @@
// alarms, we need to merge them in to the list. note we don't
// just deliver them first because we generally want non-wakeup
// alarms delivered after wakeup alarms.
- rescheduleKernelAlarmsLocked();
- updateNextAlarmClockLocked();
if (mPendingNonWakeupAlarms.size() > 0) {
calculateDeliveryPriorities(mPendingNonWakeupAlarms);
triggerList.addAll(mPendingNonWakeupAlarms);
@@ -3262,6 +3367,27 @@
}
mPendingNonWakeupAlarms.clear();
}
+ boolean needRebatch = false;
+ final HashSet<String> triggerPackages = new HashSet<>();
+ for (int i = triggerList.size() - 1; i >= 0; i--) {
+ triggerPackages.add(triggerList.get(i).sourcePackage);
+ }
+ outer:
+ for (int i = 0; i < mAlarmBatches.size(); i++) {
+ final Batch batch = mAlarmBatches.get(i);
+ for (int j = 0; j < batch.size(); j++) {
+ if (triggerPackages.contains(batch.get(j))) {
+ needRebatch = true;
+ break outer;
+ }
+ }
+ }
+ if (needRebatch) {
+ rebatchAllAlarmsLocked(false);
+ } else {
+ rescheduleKernelAlarmsLocked();
+ updateNextAlarmClockLocked();
+ }
deliverAlarmsLocked(triggerList, nowELAPSED);
}
}
@@ -3318,6 +3444,8 @@
public static final int SEND_NEXT_ALARM_CLOCK_CHANGED = 2;
public static final int LISTENER_TIMEOUT = 3;
public static final int REPORT_ALARMS_ACTIVE = 4;
+ public static final int APP_STANDBY_BUCKET_CHANGED = 5;
+ public static final int APP_STANDBY_PAROLE_CHANGED = 6;
public AlarmHandler() {
}
@@ -3363,6 +3491,19 @@
}
break;
+ case APP_STANDBY_PAROLE_CHANGED:
+ synchronized (mLock) {
+ mAppStandbyParole = (Boolean) msg.obj;
+ rebatchAllAlarmsLocked(false);
+ }
+ break;
+
+ case APP_STANDBY_BUCKET_CHANGED:
+ synchronized (mLock) {
+ rebatchAllAlarmsLocked(false);
+ }
+ break;
+
default:
// nope, just ignore it
break;
@@ -3489,6 +3630,13 @@
int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
if (userHandle >= 0) {
removeUserLocked(userHandle);
+ for (int i = mLastAlarmDeliveredForPackage.size() - 1; i >= 0; i--) {
+ final Pair<String, Integer> packageUser =
+ mLastAlarmDeliveredForPackage.keyAt(i);
+ if (packageUser.second == userHandle) {
+ mLastAlarmDeliveredForPackage.removeAt(i);
+ }
+ }
}
} else if (Intent.ACTION_UID_REMOVED.equals(action)) {
if (uid >= 0) {
@@ -3509,6 +3657,13 @@
}
}
if (pkgList != null && (pkgList.length > 0)) {
+ for (int i = mLastAlarmDeliveredForPackage.size() - 1; i >= 0; i--) {
+ Pair<String, Integer> packageUser = mLastAlarmDeliveredForPackage.keyAt(i);
+ if (ArrayUtils.contains(pkgList, packageUser.first)
+ && packageUser.second == UserHandle.getUserId(uid)) {
+ mLastAlarmDeliveredForPackage.removeAt(i);
+ }
+ }
for (String pkg : pkgList) {
if (uid >= 0) {
// package-removed case
@@ -3563,6 +3718,33 @@
}
};
+ /**
+ * Tracking of app assignments to standby buckets
+ */
+ final class AppStandbyTracker extends UsageStatsManagerInternal.AppIdleStateChangeListener {
+ @Override
+ public void onAppIdleStateChanged(final String packageName, final @UserIdInt int userId,
+ boolean idle, int bucket) {
+ if (DEBUG_STANDBY) {
+ Slog.d(TAG, "Package " + packageName + " for user " + userId + " now in bucket " +
+ bucket);
+ }
+ mHandler.removeMessages(AlarmHandler.APP_STANDBY_BUCKET_CHANGED);
+ mHandler.sendEmptyMessage(AlarmHandler.APP_STANDBY_BUCKET_CHANGED);
+ }
+
+ @Override
+ public void onParoleStateChanged(boolean isParoleOn) {
+ if (DEBUG_STANDBY) {
+ Slog.d(TAG, "Global parole state now " + (isParoleOn ? "ON" : "OFF"));
+ }
+ mHandler.removeMessages(AlarmHandler.APP_STANDBY_BUCKET_CHANGED);
+ mHandler.removeMessages(AlarmHandler.APP_STANDBY_PAROLE_CHANGED);
+ mHandler.obtainMessage(AlarmHandler.APP_STANDBY_PAROLE_CHANGED,
+ Boolean.valueOf(isParoleOn)).sendToTarget();
+ }
+ };
+
private final Listener mForceAppStandbyListener = new Listener() {
@Override
public void unblockAllUnrestrictedAlarms() {
@@ -3841,7 +4023,6 @@
alarm.packageName, alarm.type, alarm.statsTag, nowELAPSED);
mInFlight.add(inflight);
mBroadcastRefCount++;
-
if (allowWhileIdle) {
// Record the last time this uid handled an ALLOW_WHILE_IDLE alarm.
mLastAllowWhileIdleDispatch.put(alarm.creatorUid, nowELAPSED);
@@ -3860,6 +4041,11 @@
mAllowWhileIdleDispatches.add(ent);
}
}
+ if (!UserHandle.isCore(alarm.creatorUid)) {
+ final Pair<String, Integer> packageUser = Pair.create(alarm.sourcePackage,
+ UserHandle.getUserId(alarm.creatorUid));
+ mLastAlarmDeliveredForPackage.put(packageUser, nowELAPSED);
+ }
final BroadcastStats bs = inflight.mBroadcastStats;
bs.count++;
diff --git a/services/core/java/com/android/server/DeviceIdleController.java b/services/core/java/com/android/server/DeviceIdleController.java
index a12c85a..44974ff 100644
--- a/services/core/java/com/android/server/DeviceIdleController.java
+++ b/services/core/java/com/android/server/DeviceIdleController.java
@@ -141,6 +141,8 @@
private boolean mHasNetworkLocation;
private Location mLastGenericLocation;
private Location mLastGpsLocation;
+ // Current locked state of the screen
+ private boolean mScreenLocked;
/** Device is currently active. */
private static final int STATE_ACTIVE = 0;
@@ -156,6 +158,7 @@
private static final int STATE_IDLE = 5;
/** Device is in the idle state, but temporarily out of idle to do regular maintenance. */
private static final int STATE_IDLE_MAINTENANCE = 6;
+
private static String stateToString(int state) {
switch (state) {
case STATE_ACTIVE: return "ACTIVE";
@@ -547,6 +550,11 @@
"sms_temp_app_whitelist_duration";
private static final String KEY_NOTIFICATION_WHITELIST_DURATION =
"notification_whitelist_duration";
+ /**
+ * Whether to wait for the user to unlock the device before causing screen-on to
+ * exit doze. Default = true
+ */
+ private static final String KEY_WAIT_FOR_UNLOCK = "wait_for_unlock";
/**
* This is the time, after becoming inactive, that we go in to the first
@@ -765,6 +773,8 @@
*/
public long NOTIFICATION_WHITELIST_DURATION;
+ public boolean WAIT_FOR_UNLOCK;
+
private final ContentResolver mResolver;
private final boolean mSmallBatteryDevice;
private final KeyValueListParser mParser = new KeyValueListParser(',');
@@ -855,6 +865,7 @@
KEY_SMS_TEMP_APP_WHITELIST_DURATION, 20 * 1000L);
NOTIFICATION_WHITELIST_DURATION = mParser.getDurationMillis(
KEY_NOTIFICATION_WHITELIST_DURATION, 30 * 1000L);
+ WAIT_FOR_UNLOCK = mParser.getBoolean(KEY_WAIT_FOR_UNLOCK, false);
}
}
@@ -962,6 +973,9 @@
pw.print(" "); pw.print(KEY_NOTIFICATION_WHITELIST_DURATION); pw.print("=");
TimeUtils.formatDuration(NOTIFICATION_WHITELIST_DURATION, pw);
pw.println();
+
+ pw.print(" "); pw.print(KEY_WAIT_FOR_UNLOCK); pw.print("=");
+ pw.println(WAIT_FOR_UNLOCK);
}
}
@@ -1340,6 +1354,19 @@
}
}
+ private ActivityManagerInternal.ScreenObserver mScreenObserver =
+ new ActivityManagerInternal.ScreenObserver() {
+ @Override
+ public void onAwakeStateChanged(boolean isAwake) { }
+
+ @Override
+ public void onKeyguardStateChanged(boolean isShowing) {
+ synchronized (DeviceIdleController.this) {
+ DeviceIdleController.this.keyguardShowingLocked(isShowing);
+ }
+ }
+ };
+
public DeviceIdleController(Context context) {
super(context);
mConfigFile = new AtomicFile(new File(getSystemDir(), "deviceidle.xml"));
@@ -1406,6 +1433,7 @@
mNetworkConnected = true;
mScreenOn = true;
+ mScreenLocked = false;
// Start out assuming we are charging. If we aren't, we will at least get
// a battery update the next time the level drops.
mCharging = true;
@@ -1501,6 +1529,8 @@
mLocalActivityManager.setDeviceIdleWhitelist(mPowerSaveWhitelistAllAppIdArray);
mLocalPowerManager.setDeviceIdleWhitelist(mPowerSaveWhitelistAllAppIdArray);
+ mLocalActivityManager.registerScreenObserver(mScreenObserver);
+
passWhiteListToForceAppStandbyTrackerLocked();
updateInteractivityLocked();
}
@@ -1976,7 +2006,7 @@
}
} else if (screenOn) {
mScreenOn = true;
- if (!mForceIdle) {
+ if (!mForceIdle && (!mScreenLocked || !mConstants.WAIT_FOR_UNLOCK)) {
becomeActiveLocked("screen", Process.myUid());
}
}
@@ -1997,6 +2027,16 @@
}
}
+ void keyguardShowingLocked(boolean showing) {
+ if (DEBUG) Slog.i(TAG, "keyguardShowing=" + showing);
+ if (mScreenLocked != showing) {
+ mScreenLocked = showing;
+ if (mScreenOn && !mForceIdle && !mScreenLocked) {
+ becomeActiveLocked("unlocked", Process.myUid());
+ }
+ }
+ }
+
void scheduleReportActiveLocked(String activeReason, int activeUid) {
Message msg = mHandler.obtainMessage(MSG_REPORT_ACTIVE, activeUid, 0, activeReason);
mHandler.sendMessage(msg);
@@ -3308,6 +3348,7 @@
pw.print(" mForceIdle="); pw.println(mForceIdle);
pw.print(" mMotionSensor="); pw.println(mMotionSensor);
pw.print(" mScreenOn="); pw.println(mScreenOn);
+ pw.print(" mScreenLocked="); pw.println(mScreenLocked);
pw.print(" mNetworkConnected="); pw.println(mNetworkConnected);
pw.print(" mCharging="); pw.println(mCharging);
pw.print(" mMotionActive="); pw.println(mMotionListener.active);
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index b7e5d37..5b8b691 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -25424,6 +25424,7 @@
public void notifyAppTransitionFinished() {
synchronized (ActivityManagerService.this) {
mStackSupervisor.notifyAppTransitionDone();
+ mKeyguardController.notifyAppTransitionDone();
}
}
diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index bf38825..6beafcb 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -1251,14 +1251,10 @@
synchronized (mService) {
try {
Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "resolveIntent");
- int modifiedFlags = flags
- | PackageManager.MATCH_DEFAULT_ONLY | ActivityManagerService.STOCK_PM_FLAGS;
- if (intent.isBrowsableWebIntent()
- || (intent.getFlags() & Intent.FLAG_ACTIVITY_MATCH_EXTERNAL) != 0) {
- modifiedFlags |= PackageManager.MATCH_INSTANT;
- }
return mService.getPackageManagerInternalLocked().resolveIntent(
- intent, resolvedType, modifiedFlags, userId, true);
+ intent, resolvedType, PackageManager.MATCH_INSTANT
+ | PackageManager.MATCH_DEFAULT_ONLY | flags
+ | ActivityManagerService.STOCK_PM_FLAGS, userId, true);
} finally {
Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
diff --git a/services/core/java/com/android/server/am/ActivityStarter.java b/services/core/java/com/android/server/am/ActivityStarter.java
index eab88aa..8fd754a 100644
--- a/services/core/java/com/android/server/am/ActivityStarter.java
+++ b/services/core/java/com/android/server/am/ActivityStarter.java
@@ -31,7 +31,6 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-import static android.content.Intent.ACTION_INSTALL_INSTANT_APP_PACKAGE;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.content.Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT;
@@ -790,7 +789,7 @@
// Instead, launch the ephemeral installer. Once the installer is finished, it
// starts either the intent we resolved here [on install error] or the ephemeral
// app [on install success].
- if (rInfo != null && rInfo.isInstantAppAvailable) {
+ if (rInfo != null && rInfo.auxiliaryInfo != null) {
intent = createLaunchIntent(rInfo.auxiliaryInfo, ephemeralIntent,
callingPackage, verificationBundle, resolvedType, userId);
resolvedType = null;
@@ -850,27 +849,22 @@
/**
* Creates a launch intent for the given auxiliary resolution data.
*/
- private @NonNull Intent createLaunchIntent(@Nullable AuxiliaryResolveInfo auxiliaryResponse,
+ private @NonNull Intent createLaunchIntent(@NonNull AuxiliaryResolveInfo auxiliaryResponse,
Intent originalIntent, String callingPackage, Bundle verificationBundle,
String resolvedType, int userId) {
- if (auxiliaryResponse != null && auxiliaryResponse.needsPhaseTwo) {
+ if (auxiliaryResponse.needsPhaseTwo) {
// request phase two resolution
mService.getPackageManagerInternalLocked().requestInstantAppResolutionPhaseTwo(
auxiliaryResponse, originalIntent, resolvedType, callingPackage,
verificationBundle, userId);
}
return InstantAppResolver.buildEphemeralInstallerIntent(
- originalIntent,
- InstantAppResolver.sanitizeIntent(originalIntent),
- auxiliaryResponse == null ? null : auxiliaryResponse.failureIntent,
- callingPackage,
- verificationBundle,
- resolvedType,
- userId,
- auxiliaryResponse == null ? null : auxiliaryResponse.installFailureActivity,
- auxiliaryResponse == null ? null : auxiliaryResponse.token,
- auxiliaryResponse != null && auxiliaryResponse.needsPhaseTwo,
- auxiliaryResponse == null ? null : auxiliaryResponse.filters);
+ Intent.ACTION_INSTALL_INSTANT_APP_PACKAGE, originalIntent,
+ auxiliaryResponse.failureIntent, callingPackage, verificationBundle,
+ resolvedType, userId, auxiliaryResponse.packageName, auxiliaryResponse.splitName,
+ auxiliaryResponse.installFailureActivity, auxiliaryResponse.versionCode,
+ auxiliaryResponse.token, auxiliaryResponse.resolveInfo.getExtras(),
+ auxiliaryResponse.needsPhaseTwo);
}
void postStartActivityProcessing(ActivityRecord r, int result, ActivityStack targetStack) {
@@ -930,12 +924,12 @@
// Don't modify the client's object!
intent = new Intent(intent);
if (componentSpecified
- && !Intent.ACTION_INSTALL_INSTANT_APP_PACKAGE.equals(intent.getAction())
- && !Intent.ACTION_RESOLVE_INSTANT_APP_PACKAGE.equals(intent.getAction())
+ && intent.getData() != null
+ && Intent.ACTION_VIEW.equals(intent.getAction())
&& mService.getPackageManagerInternalLocked()
.isInstantAppInstallerComponent(intent.getComponent())) {
// intercept intents targeted directly to the ephemeral installer the
- // ephemeral installer should never be started with a raw Intent; instead
+ // ephemeral installer should never be started with a raw URL; instead
// adjust the intent so it looks like a "normal" instant app launch
intent.setComponent(null /*component*/);
componentSpecified = false;
diff --git a/services/core/java/com/android/server/am/KeyguardController.java b/services/core/java/com/android/server/am/KeyguardController.java
index 79f3fe3..05305f3 100644
--- a/services/core/java/com/android/server/am/KeyguardController.java
+++ b/services/core/java/com/android/server/am/KeyguardController.java
@@ -384,4 +384,8 @@
proto.write(KEYGUARD_OCCLUDED, mOccluded);
proto.end(token);
}
+
+ public void notifyAppTransitionDone() {
+ setKeyguardGoingAway(false);
+ }
}
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index f247de7..1825db8 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -2292,7 +2292,9 @@
// only mute for the current user
if (getCurrentUserId() == userId) {
final boolean currentMute = AudioSystem.isMicrophoneMuted();
+ final long identity = Binder.clearCallingIdentity();
AudioSystem.muteMicrophone(on);
+ Binder.restoreCallingIdentity(identity);
if (on != currentMute) {
mContext.sendBroadcast(new Intent(AudioManager.ACTION_MICROPHONE_MUTE_CHANGED)
.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY));
@@ -2666,7 +2668,9 @@
}
if (actualMode != mMode) {
+ final long identity = Binder.clearCallingIdentity();
status = AudioSystem.setPhoneState(actualMode);
+ Binder.restoreCallingIdentity(identity);
if (status == AudioSystem.AUDIO_STATUS_OK) {
if (DEBUG_MODE) { Log.v(TAG, " mode successfully set to " + actualMode); }
mMode = actualMode;
@@ -7275,11 +7279,15 @@
if (mHasFocusListener) {
mMediaFocusControl.removeFocusFollower(mPolicyCallback);
}
+ final long identity = Binder.clearCallingIdentity();
AudioSystem.registerPolicyMixes(mMixes, false);
+ Binder.restoreCallingIdentity(identity);
}
void connectMixes() {
+ final long identity = Binder.clearCallingIdentity();
AudioSystem.registerPolicyMixes(mMixes, true);
+ Binder.restoreCallingIdentity(identity);
}
};
diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java
index 8fa3318..e0baeee 100644
--- a/services/core/java/com/android/server/job/JobSchedulerService.java
+++ b/services/core/java/com/android/server/job/JobSchedulerService.java
@@ -2206,12 +2206,7 @@
Slog.i(TAG, "Moving uid " + uid + " to bucketIndex " + bucketIndex);
}
synchronized (mLock) {
- // TODO: update to be more efficient once we can slice by source UID
- mJobs.forEachJob((JobStatus job) -> {
- if (job.getSourceUid() == uid) {
- job.setStandbyBucket(bucketIndex);
- }
- });
+ mJobs.forEachJobForSourceUid(uid, job -> job.setStandbyBucket(bucketIndex));
onControllerStateChanged();
}
});
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/PersistentKeyChainSnapshot.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/PersistentKeyChainSnapshot.java
new file mode 100644
index 0000000..52381b8
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/PersistentKeyChainSnapshot.java
@@ -0,0 +1,298 @@
+/*
+ * 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.locksettings.recoverablekeystore.storage;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.security.keystore.recovery.KeyChainProtectionParams;
+import android.security.keystore.recovery.KeyChainSnapshot;
+import android.security.keystore.recovery.KeyDerivationParams;
+import android.security.keystore.recovery.WrappedApplicationKey;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class provides helper methods serialize and deserialize {@link KeyChainSnapshot}.
+ *
+ * <p> It is necessary since {@link android.os.Parcelable} is not designed for persistent storage.
+ *
+ * <p> For every list, length is stored before the elements.
+ *
+ */
+public class PersistentKeyChainSnapshot {
+ private static final int VERSION = 1;
+ private static final int NULL_LIST_LENGTH = -1;
+
+ private DataInputStream mInput;
+ private DataOutputStream mOut;
+ private ByteArrayOutputStream mOutStream;
+
+ @VisibleForTesting
+ PersistentKeyChainSnapshot() {
+ }
+
+ @VisibleForTesting
+ void initReader(byte[] input) {
+ mInput = new DataInputStream(new ByteArrayInputStream(input));
+ }
+
+ @VisibleForTesting
+ void initWriter() {
+ mOutStream = new ByteArrayOutputStream();
+ mOut = new DataOutputStream(mOutStream);
+ }
+
+ @VisibleForTesting
+ byte[] getOutput() {
+ return mOutStream.toByteArray();
+ }
+
+ /**
+ * Converts {@link KeyChainSnapshot} to its binary representation.
+ *
+ * @param snapshot The snapshot.
+ *
+ * @throws IOException if serialization failed.
+ */
+ public static byte[] serialize(@NonNull KeyChainSnapshot snapshot) throws IOException {
+ PersistentKeyChainSnapshot writer = new PersistentKeyChainSnapshot();
+ writer.initWriter();
+ writer.writeInt(VERSION);
+ writer.writeKeyChainSnapshot(snapshot);
+ return writer.getOutput();
+ }
+
+ /**
+ * deserializes {@link KeyChainSnapshot}.
+ *
+ * @input input - byte array produced by {@link serialize} method.
+ * @throws IOException if parsing failed.
+ */
+ public static @NonNull KeyChainSnapshot deserialize(@NonNull byte[] input)
+ throws IOException {
+ PersistentKeyChainSnapshot reader = new PersistentKeyChainSnapshot();
+ reader.initReader(input);
+ try {
+ int version = reader.readInt();
+ if (version != VERSION) {
+ throw new IOException("Unsupported version " + version);
+ }
+ return reader.readKeyChainSnapshot();
+ } catch (IOException e) {
+ throw new IOException("Malformed KeyChainSnapshot", e);
+ }
+ }
+
+ /**
+ * Must be in sync with {@link KeyChainSnapshot.writeToParcel}
+ */
+ @VisibleForTesting
+ void writeKeyChainSnapshot(KeyChainSnapshot snapshot) throws IOException {
+ writeInt(snapshot.getSnapshotVersion());
+ writeProtectionParamsList(snapshot.getKeyChainProtectionParams());
+ writeBytes(snapshot.getEncryptedRecoveryKeyBlob());
+ writeKeysList(snapshot.getWrappedApplicationKeys());
+
+ writeInt(snapshot.getMaxAttempts());
+ writeLong(snapshot.getCounterId());
+ writeBytes(snapshot.getServerParams());
+ writeBytes(snapshot.getTrustedHardwarePublicKey());
+ }
+
+ @VisibleForTesting
+ KeyChainSnapshot readKeyChainSnapshot() throws IOException {
+ int snapshotVersion = readInt();
+ List<KeyChainProtectionParams> protectionParams = readProtectionParamsList();
+ byte[] encryptedRecoveryKey = readBytes();
+ List<WrappedApplicationKey> keysList = readKeysList();
+
+ int maxAttempts = readInt();
+ long conterId = readLong();
+ byte[] serverParams = readBytes();
+ byte[] trustedHardwarePublicKey = readBytes();
+
+ return new KeyChainSnapshot.Builder()
+ .setSnapshotVersion(snapshotVersion)
+ .setKeyChainProtectionParams(protectionParams)
+ .setEncryptedRecoveryKeyBlob(encryptedRecoveryKey)
+ .setWrappedApplicationKeys(keysList)
+ .setMaxAttempts(maxAttempts)
+ .setCounterId(conterId)
+ .setServerParams(serverParams)
+ .setTrustedHardwarePublicKey(trustedHardwarePublicKey)
+ .build();
+ }
+
+ @VisibleForTesting
+ void writeProtectionParamsList(
+ @NonNull List<KeyChainProtectionParams> ProtectionParamsList) throws IOException {
+ writeInt(ProtectionParamsList.size());
+ for (KeyChainProtectionParams protectionParams : ProtectionParamsList) {
+ writeProtectionParams(protectionParams);
+ }
+ }
+
+ @VisibleForTesting
+ List<KeyChainProtectionParams> readProtectionParamsList() throws IOException {
+ int length = readInt();
+ List<KeyChainProtectionParams> result = new ArrayList<>(length);
+ for (int i = 0; i < length; i++) {
+ result.add(readProtectionParams());
+ }
+ return result;
+ }
+
+ /**
+ * Must be in sync with {@link KeyChainProtectionParams.writeToParcel}
+ */
+ @VisibleForTesting
+ void writeProtectionParams(@NonNull KeyChainProtectionParams protectionParams)
+ throws IOException {
+ if (!ArrayUtils.isEmpty(protectionParams.getSecret())) {
+ // Extra security check.
+ throw new RuntimeException("User generated secret should not be stored");
+ }
+ writeInt(protectionParams.getUserSecretType());
+ writeInt(protectionParams.getLockScreenUiFormat());
+ writeKeyDerivationParams(protectionParams.getKeyDerivationParams());
+ writeBytes(protectionParams.getSecret());
+ }
+
+ @VisibleForTesting
+ KeyChainProtectionParams readProtectionParams() throws IOException {
+ int userSecretType = readInt();
+ int lockScreenUiFormat = readInt();
+ KeyDerivationParams derivationParams = readKeyDerivationParams();
+ byte[] secret = readBytes();
+ return new KeyChainProtectionParams.Builder()
+ .setUserSecretType(userSecretType)
+ .setLockScreenUiFormat(lockScreenUiFormat)
+ .setKeyDerivationParams(derivationParams)
+ .setSecret(secret)
+ .build();
+ }
+
+ /**
+ * Must be in sync with {@link KeyDerivationParams.writeToParcel}
+ */
+ @VisibleForTesting
+ void writeKeyDerivationParams(@NonNull KeyDerivationParams Params) throws IOException {
+ writeInt(Params.getAlgorithm());
+ writeBytes(Params.getSalt());
+ }
+
+ @VisibleForTesting
+ KeyDerivationParams readKeyDerivationParams() throws IOException {
+ int algorithm = readInt();
+ byte[] salt = readBytes();
+ return KeyDerivationParams.createSha256Params(salt);
+ }
+
+ @VisibleForTesting
+ void writeKeysList(@NonNull List<WrappedApplicationKey> applicationKeys) throws IOException {
+ writeInt(applicationKeys.size());
+ for (WrappedApplicationKey keyEntry : applicationKeys) {
+ writeKeyEntry(keyEntry);
+ }
+ }
+
+ @VisibleForTesting
+ List<WrappedApplicationKey> readKeysList() throws IOException {
+ int length = readInt();
+ List<WrappedApplicationKey> result = new ArrayList<>(length);
+ for (int i = 0; i < length; i++) {
+ result.add(readKeyEntry());
+ }
+ return result;
+ }
+
+ /**
+ * Must be in sync with {@link WrappedApplicationKey.writeToParcel}
+ */
+ @VisibleForTesting
+ void writeKeyEntry(@NonNull WrappedApplicationKey keyEntry) throws IOException {
+ mOut.writeUTF(keyEntry.getAlias());
+ writeBytes(keyEntry.getEncryptedKeyMaterial());
+ writeBytes(keyEntry.getAccount());
+ }
+
+ @VisibleForTesting
+ WrappedApplicationKey readKeyEntry() throws IOException {
+ String alias = mInput.readUTF();
+ byte[] keyMaterial = readBytes();
+ byte[] account = readBytes();
+ return new WrappedApplicationKey.Builder()
+ .setAlias(alias)
+ .setEncryptedKeyMaterial(keyMaterial)
+ .setAccount(account)
+ .build();
+ }
+
+ @VisibleForTesting
+ void writeInt(int value) throws IOException {
+ mOut.writeInt(value);
+ }
+
+ @VisibleForTesting
+ int readInt() throws IOException {
+ return mInput.readInt();
+ }
+
+ @VisibleForTesting
+ void writeLong(long value) throws IOException {
+ mOut.writeLong(value);
+ }
+
+ @VisibleForTesting
+ long readLong() throws IOException {
+ return mInput.readLong();
+ }
+
+ @VisibleForTesting
+ void writeBytes(@Nullable byte[] value) throws IOException {
+ if (value == null) {
+ writeInt(NULL_LIST_LENGTH);
+ return;
+ }
+ writeInt(value.length);
+ mOut.write(value, 0, value.length);
+ }
+
+ /**
+ * Reads @code{byte[]} from current position. Converts {@code null} to an empty array.
+ */
+ @VisibleForTesting
+ @NonNull byte[] readBytes() throws IOException {
+ int length = readInt();
+ if (length == NULL_LIST_LENGTH) {
+ return new byte[]{};
+ }
+ byte[] result = new byte[length];
+ mInput.read(result, 0, result.length);
+ return result;
+ }
+}
+
diff --git a/services/core/java/com/android/server/net/watchlist/NetworkWatchlistService.java b/services/core/java/com/android/server/net/watchlist/NetworkWatchlistService.java
index 7165e60..5f4e471 100644
--- a/services/core/java/com/android/server/net/watchlist/NetworkWatchlistService.java
+++ b/services/core/java/com/android/server/net/watchlist/NetworkWatchlistService.java
@@ -23,8 +23,10 @@
import android.net.metrics.IpConnectivityLog;
import android.os.Binder;
import android.os.Process;
+import android.os.ResultReceiver;
import android.os.RemoteException;
import android.os.ServiceManager;
+import android.os.ShellCallback;
import android.os.SystemProperties;
import android.provider.Settings;
import android.text.TextUtils;
@@ -80,6 +82,7 @@
return;
}
try {
+ mService.init();
mService.initIpConnectivityMetrics();
mService.startWatchlistLogging();
} catch (RemoteException e) {
@@ -127,6 +130,10 @@
mIpConnectivityMetrics = ipConnectivityMetrics;
}
+ private void init() {
+ mConfig.removeTestModeConfig();
+ }
+
private void initIpConnectivityMetrics() {
mIpConnectivityMetrics = (IIpConnectivityMetrics) IIpConnectivityMetrics.Stub.asInterface(
ServiceManager.getService(IpConnectivityLog.SERVICE_NAME));
@@ -151,6 +158,22 @@
}
};
+ private boolean isCallerShell() {
+ final int callingUid = Binder.getCallingUid();
+ return callingUid == Process.SHELL_UID || callingUid == Process.ROOT_UID;
+ }
+
+ @Override
+ public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
+ String[] args, ShellCallback callback, ResultReceiver resultReceiver) {
+ if (!isCallerShell()) {
+ Slog.w(TAG, "Only shell is allowed to call network watchlist shell commands");
+ return;
+ }
+ (new NetworkWatchlistShellCommand(mContext)).exec(this, in, out, err, args, callback,
+ resultReceiver);
+ }
+
@VisibleForTesting
protected boolean startWatchlistLoggingImpl() throws RemoteException {
if (DEBUG) {
diff --git a/services/core/java/com/android/server/net/watchlist/NetworkWatchlistShellCommand.java b/services/core/java/com/android/server/net/watchlist/NetworkWatchlistShellCommand.java
new file mode 100644
index 0000000..9533823
--- /dev/null
+++ b/services/core/java/com/android/server/net/watchlist/NetworkWatchlistShellCommand.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2018 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.net.watchlist;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.NetworkWatchlistManager;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.ShellCommand;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+
+/**
+ * Network watchlist shell commands class, to provide a way to set temporary watchlist config for
+ * testing in shell, so CTS / GTS can use it to verify if watchlist feature is working properly.
+ */
+class NetworkWatchlistShellCommand extends ShellCommand {
+
+ final NetworkWatchlistManager mNetworkWatchlistManager;
+
+ NetworkWatchlistShellCommand(Context context) {
+ mNetworkWatchlistManager = new NetworkWatchlistManager(context);
+ }
+
+ @Override
+ public int onCommand(String cmd) {
+ if (cmd == null) {
+ return handleDefaultCommands(cmd);
+ }
+
+ final PrintWriter pw = getOutPrintWriter();
+ try {
+ switch(cmd) {
+ case "set-test-config":
+ return runSetTestConfig();
+ default:
+ return handleDefaultCommands(cmd);
+ }
+ } catch (RemoteException e) {
+ pw.println("Remote exception: " + e);
+ }
+ return -1;
+ }
+
+ /**
+ * Method to get fd from input xml path, and set it as temporary watchlist config.
+ */
+ private int runSetTestConfig() throws RemoteException {
+ final PrintWriter pw = getOutPrintWriter();
+ try {
+ final String configXmlPath = getNextArgRequired();
+ final ParcelFileDescriptor pfd = openFileForSystem(configXmlPath, "r");
+ if (pfd != null) {
+ final InputStream fileStream = new FileInputStream(pfd.getFileDescriptor());
+ WatchlistConfig.getInstance().setTestMode(fileStream);
+ }
+ pw.println("Success!");
+ } catch (RuntimeException | IOException ex) {
+ pw.println("Error: " + ex.toString());
+ return -1;
+ }
+ return 0;
+ }
+
+ @Override
+ public void onHelp() {
+ final PrintWriter pw = getOutPrintWriter();
+ pw.println("Network watchlist manager commands:");
+ pw.println(" help");
+ pw.println(" Print this help text.");
+ pw.println("");
+ pw.println(" set-test-config your_watchlist_config.xml");
+ pw.println();
+ Intent.printIntentArgsHelp(pw , "");
+ }
+}
diff --git a/services/core/java/com/android/server/net/watchlist/WatchlistConfig.java b/services/core/java/com/android/server/net/watchlist/WatchlistConfig.java
index 7387ad4..2714d5e 100644
--- a/services/core/java/com/android/server/net/watchlist/WatchlistConfig.java
+++ b/services/core/java/com/android/server/net/watchlist/WatchlistConfig.java
@@ -16,6 +16,7 @@
package com.android.server.net.watchlist;
+import android.os.FileUtils;
import android.util.AtomicFile;
import android.util.Log;
import android.util.Slog;
@@ -32,6 +33,7 @@
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
@@ -50,6 +52,8 @@
// Watchlist config that pushed by ConfigUpdater.
private static final String NETWORK_WATCHLIST_DB_PATH =
"/data/misc/network_watchlist/network_watchlist.xml";
+ private static final String NETWORK_WATCHLIST_DB_FOR_TEST_PATH =
+ "/data/misc/network_watchlist/network_watchlist_for_test.xml";
// Hash for null / unknown config, a 32 byte array filled with content 0x00
private static final byte[] UNKNOWN_CONFIG_HASH = new byte[32];
@@ -80,7 +84,7 @@
private boolean mIsSecureConfig = true;
private final static WatchlistConfig sInstance = new WatchlistConfig();
- private final File mXmlFile;
+ private File mXmlFile;
private volatile CrcShaDigests mDomainDigests;
private volatile CrcShaDigests mIpDigests;
@@ -232,7 +236,38 @@
return UNKNOWN_CONFIG_HASH;
}
+ /**
+ * This method will copy temporary test config and temporary override network watchlist config
+ * in memory. When device is rebooted, temporary test config will be removed, and system will
+ * use back the original watchlist config.
+ * Also, as temporary network watchlist config is not secure, we will mark it as insecure
+ * config and will be applied to testOnly applications only.
+ */
+ public void setTestMode(InputStream testConfigInputStream) throws IOException {
+ Log.i(TAG, "Setting watchlist testing config");
+ // Copy test config
+ FileUtils.copyToFileOrThrow(testConfigInputStream,
+ new File(NETWORK_WATCHLIST_DB_FOR_TEST_PATH));
+ // Mark config as insecure, so it will be applied to testOnly applications only
+ mIsSecureConfig = false;
+ // Reload watchlist config using test config file
+ mXmlFile = new File(NETWORK_WATCHLIST_DB_FOR_TEST_PATH);
+ reloadConfig();
+ }
+
+ public void removeTestModeConfig() {
+ try {
+ final File f = new File(NETWORK_WATCHLIST_DB_FOR_TEST_PATH);
+ if (f.exists()) {
+ f.delete();
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Unable to delete test config");
+ }
+ }
+
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("Watchlist config hash: " + HexDump.toHexString(getWatchlistConfigHash()));
pw.println("Domain CRC32 digest list:");
if (mDomainDigests != null) {
mDomainDigests.crc32Digests.dump(fd, pw, args);
diff --git a/services/core/java/com/android/server/net/watchlist/WatchlistLoggingHandler.java b/services/core/java/com/android/server/net/watchlist/WatchlistLoggingHandler.java
index 3b6d59e..c4de4ac 100644
--- a/services/core/java/com/android/server/net/watchlist/WatchlistLoggingHandler.java
+++ b/services/core/java/com/android/server/net/watchlist/WatchlistLoggingHandler.java
@@ -118,6 +118,25 @@
}
/**
+ * Return if a given package has testOnly is true.
+ */
+ private boolean isPackageTestOnly(int uid) {
+ final ApplicationInfo ai;
+ try {
+ final String[] packageNames = mPm.getPackagesForUid(uid);
+ if (packageNames == null || packageNames.length == 0) {
+ Slog.e(TAG, "Couldn't find package: " + packageNames);
+ return false;
+ }
+ ai = mPm.getApplicationInfo(packageNames[0],0);
+ } catch (NameNotFoundException e) {
+ // Should not happen.
+ return false;
+ }
+ return (ai.flags & ApplicationInfo.FLAG_TEST_ONLY) != 0;
+ }
+
+ /**
* Report network watchlist records if we collected enough data.
*/
public void reportWatchlistIfNecessary() {
@@ -146,16 +165,21 @@
}
final String cncDomain = searchAllSubDomainsInWatchlist(hostname);
if (cncDomain != null) {
- insertRecord(getDigestFromUid(uid), cncDomain, timestamp);
+ insertRecord(uid, cncDomain, timestamp);
} else {
final String cncIp = searchIpInWatchlist(ipAddresses);
if (cncIp != null) {
- insertRecord(getDigestFromUid(uid), cncIp, timestamp);
+ insertRecord(uid, cncIp, timestamp);
}
}
}
- private boolean insertRecord(byte[] digest, String cncHost, long timestamp) {
+ private boolean insertRecord(int uid, String cncHost, long timestamp) {
+ if (!mConfig.isConfigSecure() && !isPackageTestOnly(uid)) {
+ // Skip package if config is not secure and package is not TestOnly app.
+ return true;
+ }
+ final byte[] digest = getDigestFromUid(uid);
final boolean result = mDbHelper.insertNewRecord(digest, cncHost, timestamp);
tryAggregateRecords();
return result;
diff --git a/services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java b/services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java
index c73b0cf..4b577bb 100644
--- a/services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java
+++ b/services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java
@@ -141,20 +141,19 @@
}
/**
- * Aggregate the records in database, and return a rappor encoded result.
+ * Aggregate all records before most recent local midnight in database, and return a
+ * rappor encoded result.
*/
public AggregatedResult getAggregatedRecords() {
- final long twoDaysBefore = getTwoDaysBeforeTimestamp();
- final long yesterday = getYesterdayTimestamp();
- final String selectStatement = WhiteListReportContract.TIMESTAMP + " >= ? AND " +
- WhiteListReportContract.TIMESTAMP + " <= ?";
+ final long lastMidnightTime = getLastMidnightTime();
+ final String selectStatement = WhiteListReportContract.TIMESTAMP + " < ?";
final SQLiteDatabase db = getReadableDatabase();
Cursor c = null;
try {
c = db.query(true /* distinct */,
WhiteListReportContract.TABLE, DIGEST_DOMAIN_PROJECTION, selectStatement,
- new String[]{"" + twoDaysBefore, "" + yesterday}, null, null,
+ new String[]{"" + lastMidnightTime}, null, null,
null, null);
if (c == null) {
return null;
@@ -182,23 +181,19 @@
}
/**
- * Remove all the records before yesterday.
+ * Remove all the records before most recent local midnight.
*
* @return True if success.
*/
public boolean cleanup() {
final SQLiteDatabase db = getWritableDatabase();
- final long twoDaysBefore = getTwoDaysBeforeTimestamp();
- final String clause = WhiteListReportContract.TIMESTAMP + "< " + twoDaysBefore;
+ final long midnightTime = getLastMidnightTime();
+ final String clause = WhiteListReportContract.TIMESTAMP + "< " + midnightTime;
return db.delete(WhiteListReportContract.TABLE, clause, null) != 0;
}
- static long getTwoDaysBeforeTimestamp() {
- return getMidnightTimestamp(2);
- }
-
- static long getYesterdayTimestamp() {
- return getMidnightTimestamp(1);
+ static long getLastMidnightTime() {
+ return getMidnightTimestamp(0);
}
static long getMidnightTimestamp(int daysBefore) {
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 9865e35..727e7ee 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -111,6 +111,7 @@
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
+import android.os.IDeviceIdleController;
import android.os.IInterface;
import android.os.Looper;
import android.os.Message;
@@ -286,6 +287,7 @@
private AlarmManager mAlarmManager;
private ICompanionDeviceManager mCompanionManager;
private AccessibilityManager mAccessibilityManager;
+ private IDeviceIdleController mDeviceIdleController;
final IBinder mForegroundToken = new Binder();
private WorkerHandler mHandler;
@@ -659,6 +661,7 @@
@Override
public void onNotificationClick(int callingUid, int callingPid, String key) {
+ exitIdle();
synchronized (mNotificationLock) {
NotificationRecord r = mNotificationsByKey.get(key);
if (r == null) {
@@ -683,6 +686,7 @@
@Override
public void onNotificationActionClick(int callingUid, int callingPid, String key,
int actionIndex) {
+ exitIdle();
synchronized (mNotificationLock) {
NotificationRecord r = mNotificationsByKey.get(key);
if (r == null) {
@@ -812,6 +816,7 @@
@Override
public void onNotificationDirectReplied(String key) {
+ exitIdle();
synchronized (mNotificationLock) {
NotificationRecord r = mNotificationsByKey.get(key);
if (r != null) {
@@ -1280,6 +1285,8 @@
mAlarmManager = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE);
mCompanionManager = companionManager;
mActivityManager = activityManager;
+ mDeviceIdleController = IDeviceIdleController.Stub.asInterface(
+ ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER));
mHandler = new WorkerHandler(looper);
mRankingThread.start();
@@ -1533,6 +1540,15 @@
sendRegisteredOnlyBroadcast(NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED);
}
+ private void exitIdle() {
+ try {
+ if (mDeviceIdleController != null) {
+ mDeviceIdleController.exitIdle("notification interaction");
+ }
+ } catch (RemoteException e) {
+ }
+ }
+
private void updateNotificationChannelInt(String pkg, int uid, NotificationChannel channel,
boolean fromListener) {
if (channel.getImportance() == NotificationManager.IMPORTANCE_NONE) {
diff --git a/services/core/java/com/android/server/pm/EphemeralResolverConnection.java b/services/core/java/com/android/server/pm/EphemeralResolverConnection.java
index 6d6c960..b5ddf8c 100644
--- a/services/core/java/com/android/server/pm/EphemeralResolverConnection.java
+++ b/services/core/java/com/android/server/pm/EphemeralResolverConnection.java
@@ -43,7 +43,6 @@
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.TimeoutException;
@@ -85,8 +84,8 @@
mIntent = new Intent(action).setComponent(componentName);
}
- public final List<InstantAppResolveInfo> getInstantAppResolveInfoList(Intent sanitizedIntent,
- int hashPrefix[], String token) throws ConnectionException {
+ public final List<InstantAppResolveInfo> getInstantAppResolveInfoList(int hashPrefix[],
+ String token) throws ConnectionException {
throwIfCalledOnMainThread();
IInstantAppResolver target = null;
try {
@@ -99,7 +98,7 @@
}
try {
return mGetEphemeralResolveInfoCaller
- .getEphemeralResolveInfoList(target, sanitizedIntent, hashPrefix, token);
+ .getEphemeralResolveInfoList(target, hashPrefix, token);
} catch (TimeoutException e) {
throw new ConnectionException(ConnectionException.FAILURE_CALL);
} catch (RemoteException ignore) {
@@ -112,22 +111,26 @@
return null;
}
- public final void getInstantAppIntentFilterList(Intent sanitizedIntent, int hashPrefix[],
- String token, PhaseTwoCallback callback, Handler callbackHandler, final long startTime)
- throws ConnectionException {
+ public final void getInstantAppIntentFilterList(int hashPrefix[], String token,
+ String hostName, PhaseTwoCallback callback, Handler callbackHandler,
+ final long startTime) throws ConnectionException {
final IRemoteCallback remoteCallback = new IRemoteCallback.Stub() {
@Override
public void sendResult(Bundle data) throws RemoteException {
final ArrayList<InstantAppResolveInfo> resolveList =
data.getParcelableArrayList(
InstantAppResolverService.EXTRA_RESOLVE_INFO);
- callbackHandler.post(() -> callback.onPhaseTwoResolved(resolveList, startTime));
+ callbackHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ callback.onPhaseTwoResolved(resolveList, startTime);
+ }
+ });
}
};
try {
getRemoteInstanceLazy(token)
- .getInstantAppIntentFilterList(sanitizedIntent, hashPrefix, token,
- remoteCallback);
+ .getInstantAppIntentFilterList(hashPrefix, token, hostName, remoteCallback);
} catch (TimeoutException e) {
throw new ConnectionException(ConnectionException.FAILURE_BIND);
} catch (InterruptedException e) {
@@ -334,11 +337,10 @@
}
public List<InstantAppResolveInfo> getEphemeralResolveInfoList(
- IInstantAppResolver target, Intent sanitizedIntent, int hashPrefix[], String token)
+ IInstantAppResolver target, int hashPrefix[], String token)
throws RemoteException, TimeoutException {
final int sequence = onBeforeRemoteCall();
- target.getInstantAppResolveInfoList(sanitizedIntent, hashPrefix, token, sequence,
- mCallback);
+ target.getInstantAppResolveInfoList(hashPrefix, token, sequence, mCallback);
return getResultTimed(sequence);
}
}
diff --git a/services/core/java/com/android/server/pm/InstantAppRegistry.java b/services/core/java/com/android/server/pm/InstantAppRegistry.java
index af20cd7..30088dd 100644
--- a/services/core/java/com/android/server/pm/InstantAppRegistry.java
+++ b/services/core/java/com/android/server/pm/InstantAppRegistry.java
@@ -44,6 +44,7 @@
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.util.Xml;
+
import com.android.internal.annotations.GuardedBy;
import com.android.internal.os.BackgroundThread;
import com.android.internal.os.SomeArgs;
@@ -51,6 +52,7 @@
import com.android.internal.util.XmlUtils;
import libcore.io.IoUtils;
+
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
@@ -295,25 +297,26 @@
continue;
}
+ String cookieName = currentCookieFile.getName();
+ String currentCookieSha256 =
+ cookieName.substring(INSTANT_APP_COOKIE_FILE_PREFIX.length(),
+ cookieName.length() - INSTANT_APP_COOKIE_FILE_SIFFIX.length());
+
// Before we used only the first signature to compute the SHA 256 but some
// apps could be singed by multiple certs and the cert order is undefined.
// We prefer the modern computation procedure where all certs are taken
// into account but also allow the value from the old computation to avoid
// data loss.
- final String[] signaturesSha256Digests = PackageUtils.computeSignaturesSha256Digests(
- pkg.mSigningDetails.signatures);
- final String signaturesSha256Digest = PackageUtils.computeSignaturesSha256Digest(
- signaturesSha256Digests);
-
- // We prefer a match based on all signatures
- if (currentCookieFile.equals(computeInstantCookieFile(pkg.packageName,
- signaturesSha256Digest, userId))) {
+ if (pkg.mSigningDetails.checkCapability(currentCookieSha256,
+ PackageParser.SigningDetails.CertCapabilities.INSTALLED_DATA)) {
return;
}
- // For backwards compatibility we accept match based on first signature
- if (pkg.mSigningDetails.signatures.length > 1 && currentCookieFile.equals(computeInstantCookieFile(
- pkg.packageName, signaturesSha256Digests[0], userId))) {
+ // For backwards compatibility we accept match based on first signature only in the case
+ // of multiply-signed packagse
+ final String[] signaturesSha256Digests =
+ PackageUtils.computeSignaturesSha256Digests(pkg.mSigningDetails.signatures);
+ if (signaturesSha256Digests[0].equals(currentCookieSha256)) {
return;
}
diff --git a/services/core/java/com/android/server/pm/InstantAppResolver.java b/services/core/java/com/android/server/pm/InstantAppResolver.java
index 55212cc..30072d4 100644
--- a/services/core/java/com/android/server/pm/InstantAppResolver.java
+++ b/services/core/java/com/android/server/pm/InstantAppResolver.java
@@ -40,14 +40,11 @@
import android.content.pm.InstantAppResolveInfo;
import android.content.pm.InstantAppResolveInfo.InstantAppDigest;
import android.metrics.LogMaker;
-import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteException;
-import android.text.TextUtils;
import android.util.Log;
-import android.util.Slog;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto;
@@ -56,12 +53,8 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
-import java.util.Iterator;
import java.util.List;
-import java.util.Set;
import java.util.UUID;
/** @hide */
@@ -86,7 +79,6 @@
public @interface ResolutionStatus {}
private static MetricsLogger sMetricsLogger;
-
private static MetricsLogger getLogger() {
if (sMetricsLogger == null) {
sMetricsLogger = new MetricsLogger();
@@ -94,49 +86,26 @@
return sMetricsLogger;
}
- /**
- * Returns an intent with potential PII removed from the original intent. Fields removed
- * include extras and the host + path of the data, if defined.
- */
- public static Intent sanitizeIntent(Intent origIntent) {
- final Intent sanitizedIntent;
- sanitizedIntent = new Intent(origIntent.getAction());
- Set<String> categories = origIntent.getCategories();
- if (categories != null) {
- for (String category : categories) {
- sanitizedIntent.addCategory(category);
- }
- }
- Uri sanitizedUri = origIntent.getData() == null
- ? null
- : Uri.fromParts(origIntent.getScheme(), "", "");
- sanitizedIntent.setDataAndType(sanitizedUri, origIntent.getType());
- sanitizedIntent.addFlags(origIntent.getFlags());
- sanitizedIntent.setPackage(origIntent.getPackage());
- return sanitizedIntent;
- }
-
- public static AuxiliaryResolveInfo doInstantAppResolutionPhaseOne(
+ public static AuxiliaryResolveInfo doInstantAppResolutionPhaseOne(Context context,
EphemeralResolverConnection connection, InstantAppRequest requestObj) {
final long startTime = System.currentTimeMillis();
final String token = UUID.randomUUID().toString();
if (DEBUG_EPHEMERAL) {
Log.d(TAG, "[" + token + "] Phase1; resolving");
}
- final Intent origIntent = requestObj.origIntent;
- final Intent sanitizedIntent = sanitizeIntent(origIntent);
-
- final InstantAppDigest digest = getInstantAppDigest(origIntent);
+ final Intent intent = requestObj.origIntent;
+ final InstantAppDigest digest =
+ new InstantAppDigest(intent.getData().getHost(), 5 /*maxDigests*/);
final int[] shaPrefix = digest.getDigestPrefix();
AuxiliaryResolveInfo resolveInfo = null;
@ResolutionStatus int resolutionStatus = RESOLUTION_SUCCESS;
try {
final List<InstantAppResolveInfo> instantAppResolveInfoList =
- connection.getInstantAppResolveInfoList(sanitizedIntent, shaPrefix, token);
+ connection.getInstantAppResolveInfoList(shaPrefix, token);
if (instantAppResolveInfoList != null && instantAppResolveInfoList.size() > 0) {
resolveInfo = InstantAppResolver.filterInstantAppIntent(
- instantAppResolveInfoList, origIntent, requestObj.resolvedType,
- requestObj.userId, origIntent.getPackage(), digest, token);
+ instantAppResolveInfoList, intent, requestObj.resolvedType,
+ requestObj.userId, intent.getPackage(), digest, token);
}
} catch (ConnectionException e) {
if (e.failure == ConnectionException.FAILURE_BIND) {
@@ -166,12 +135,6 @@
return resolveInfo;
}
- private static InstantAppDigest getInstantAppDigest(Intent origIntent) {
- return origIntent.getData() != null && !TextUtils.isEmpty(origIntent.getData().getHost())
- ? new InstantAppDigest(origIntent.getData().getHost(), 5 /*maxDigests*/)
- : InstantAppDigest.UNDEFINED;
- }
-
public static void doInstantAppResolutionPhaseTwo(Context context,
EphemeralResolverConnection connection, InstantAppRequest requestObj,
ActivityInfo instantAppInstaller, Handler callbackHandler) {
@@ -180,53 +143,73 @@
if (DEBUG_EPHEMERAL) {
Log.d(TAG, "[" + token + "] Phase2; resolving");
}
- final Intent origIntent = requestObj.origIntent;
- final Intent sanitizedIntent = sanitizeIntent(origIntent);
- final InstantAppDigest digest = getInstantAppDigest(origIntent);
+ final Intent intent = requestObj.origIntent;
+ final String hostName = intent.getData().getHost();
+ final InstantAppDigest digest = new InstantAppDigest(hostName, 5 /*maxDigests*/);
final int[] shaPrefix = digest.getDigestPrefix();
final PhaseTwoCallback callback = new PhaseTwoCallback() {
@Override
void onPhaseTwoResolved(List<InstantAppResolveInfo> instantAppResolveInfoList,
long startTime) {
+ final String packageName;
+ final String splitName;
+ final long versionCode;
final Intent failureIntent;
+ final Bundle extras;
if (instantAppResolveInfoList != null && instantAppResolveInfoList.size() > 0) {
final AuxiliaryResolveInfo instantAppIntentInfo =
InstantAppResolver.filterInstantAppIntent(
- instantAppResolveInfoList, origIntent, null /*resolvedType*/,
- 0 /*userId*/, origIntent.getPackage(), digest, token);
- if (instantAppIntentInfo != null) {
+ instantAppResolveInfoList, intent, null /*resolvedType*/,
+ 0 /*userId*/, intent.getPackage(), digest, token);
+ if (instantAppIntentInfo != null
+ && instantAppIntentInfo.resolveInfo != null) {
+ packageName = instantAppIntentInfo.resolveInfo.getPackageName();
+ splitName = instantAppIntentInfo.splitName;
+ versionCode = instantAppIntentInfo.resolveInfo.getVersionCode();
failureIntent = instantAppIntentInfo.failureIntent;
+ extras = instantAppIntentInfo.resolveInfo.getExtras();
} else {
+ packageName = null;
+ splitName = null;
+ versionCode = -1;
failureIntent = null;
+ extras = null;
}
} else {
+ packageName = null;
+ splitName = null;
+ versionCode = -1;
failureIntent = null;
+ extras = null;
}
final Intent installerIntent = buildEphemeralInstallerIntent(
+ Intent.ACTION_RESOLVE_INSTANT_APP_PACKAGE,
requestObj.origIntent,
- sanitizedIntent,
failureIntent,
requestObj.callingPackage,
requestObj.verificationBundle,
requestObj.resolvedType,
requestObj.userId,
+ packageName,
+ splitName,
requestObj.responseObj.installFailureActivity,
+ versionCode,
token,
- false /*needsPhaseTwo*/,
- requestObj.responseObj.filters);
+ extras,
+ false /*needsPhaseTwo*/);
installerIntent.setComponent(new ComponentName(
instantAppInstaller.packageName, instantAppInstaller.name));
logMetrics(ACTION_INSTANT_APP_RESOLUTION_PHASE_TWO, startTime, token,
- requestObj.responseObj.filters != null ? RESOLUTION_SUCCESS : RESOLUTION_FAILURE);
+ packageName != null ? RESOLUTION_SUCCESS : RESOLUTION_FAILURE);
context.startActivity(installerIntent);
}
};
try {
- connection.getInstantAppIntentFilterList(sanitizedIntent, shaPrefix, token, callback,
- callbackHandler, startTime);
+ connection.getInstantAppIntentFilterList(
+ shaPrefix, token, hostName, callback, callbackHandler, startTime);
} catch (ConnectionException e) {
@ResolutionStatus int resolutionStatus = RESOLUTION_FAILURE;
if (e.failure == ConnectionException.FAILURE_BIND) {
@@ -248,20 +231,23 @@
* Builds and returns an intent to launch the instant installer.
*/
public static Intent buildEphemeralInstallerIntent(
+ @NonNull String action,
@NonNull Intent origIntent,
- @NonNull Intent sanitizedIntent,
- @Nullable Intent failureIntent,
+ @NonNull Intent failureIntent,
@NonNull String callingPackage,
@Nullable Bundle verificationBundle,
@NonNull String resolvedType,
int userId,
+ @NonNull String instantAppPackageName,
+ @Nullable String instantAppSplitName,
@Nullable ComponentName installFailureActivity,
+ long versionCode,
@Nullable String token,
- boolean needsPhaseTwo,
- List<AuxiliaryResolveInfo.AuxiliaryFilter> filters) {
+ @Nullable Bundle extras,
+ boolean needsPhaseTwo) {
// Construct the intent that launches the instant installer
int flags = origIntent.getFlags();
- final Intent intent = new Intent();
+ final Intent intent = new Intent(action);
intent.setFlags(flags
| Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TASK
@@ -274,23 +260,20 @@
intent.putExtra(Intent.EXTRA_EPHEMERAL_HOSTNAME, origIntent.getData().getHost());
}
intent.putExtra(Intent.EXTRA_INSTANT_APP_ACTION, origIntent.getAction());
- intent.putExtra(Intent.EXTRA_INTENT, sanitizedIntent);
+ if (extras != null) {
+ intent.putExtra(Intent.EXTRA_INSTANT_APP_EXTRAS, extras);
+ }
- if (needsPhaseTwo) {
- intent.setAction(Intent.ACTION_RESOLVE_INSTANT_APP_PACKAGE);
- } else {
- // We have all of the data we need; just start the installer without a second phase
+ // We have all of the data we need; just start the installer without a second phase
+ if (!needsPhaseTwo) {
+ // Intent that is launched if the package couldn't be installed for any reason.
if (failureIntent != null || installFailureActivity != null) {
- // Intent that is launched if the package couldn't be installed for any reason.
try {
final Intent onFailureIntent;
if (installFailureActivity != null) {
onFailureIntent = new Intent();
onFailureIntent.setComponent(installFailureActivity);
- if (filters != null && filters.size() == 1) {
- onFailureIntent.putExtra(Intent.EXTRA_SPLIT_NAME,
- filters.get(0).splitName);
- }
+ onFailureIntent.putExtra(Intent.EXTRA_SPLIT_NAME, instantAppSplitName);
onFailureIntent.putExtra(Intent.EXTRA_INTENT, origIntent);
} else {
onFailureIntent = failureIntent;
@@ -326,35 +309,17 @@
intent.putExtra(Intent.EXTRA_EPHEMERAL_SUCCESS,
new IntentSender(successIntentTarget));
} catch (RemoteException ignore) { /* ignore; same process */ }
+
+ intent.putExtra(Intent.EXTRA_PACKAGE_NAME, instantAppPackageName);
+ intent.putExtra(Intent.EXTRA_SPLIT_NAME, instantAppSplitName);
+ intent.putExtra(Intent.EXTRA_VERSION_CODE, (int) (versionCode & 0x7fffffff));
+ intent.putExtra(Intent.EXTRA_LONG_VERSION_CODE, versionCode);
+ intent.putExtra(Intent.EXTRA_CALLING_PACKAGE, callingPackage);
if (verificationBundle != null) {
intent.putExtra(Intent.EXTRA_VERIFICATION_BUNDLE, verificationBundle);
}
- intent.putExtra(Intent.EXTRA_CALLING_PACKAGE, callingPackage);
-
- if (filters != null) {
- Bundle resolvableFilters[] = new Bundle[filters.size()];
- for (int i = 0, max = filters.size(); i < max; i++) {
- Bundle resolvableFilter = new Bundle();
- AuxiliaryResolveInfo.AuxiliaryFilter filter = filters.get(i);
- resolvableFilter.putBoolean(Intent.EXTRA_UNKNOWN_INSTANT_APP,
- filter.resolveInfo != null
- && filter.resolveInfo.shouldLetInstallerDecide());
- resolvableFilter.putString(Intent.EXTRA_PACKAGE_NAME, filter.packageName);
- resolvableFilter.putString(Intent.EXTRA_SPLIT_NAME, filter.splitName);
- resolvableFilter.putLong(Intent.EXTRA_LONG_VERSION_CODE, filter.versionCode);
- resolvableFilter.putBundle(Intent.EXTRA_INSTANT_APP_EXTRAS, filter.extras);
- resolvableFilters[i] = resolvableFilter;
- if (i == 0) {
- // for backwards compat, always set the first result on the intent and add
- // the int version code
- intent.putExtras(resolvableFilter);
- intent.putExtra(Intent.EXTRA_VERSION_CODE, (int) filter.versionCode);
- }
- }
- intent.putExtra(Intent.EXTRA_INSTANT_APP_BUNDLES, resolvableFilters);
- }
- intent.setAction(Intent.ACTION_INSTALL_INSTANT_APP_PACKAGE);
}
+
return intent;
}
@@ -365,134 +330,69 @@
final int[] shaPrefix = digest.getDigestPrefix();
final byte[][] digestBytes = digest.getDigestBytes();
final Intent failureIntent = new Intent(origIntent);
- boolean requiresSecondPhase = false;
failureIntent.setFlags(failureIntent.getFlags() | Intent.FLAG_IGNORE_EPHEMERAL);
failureIntent.setLaunchToken(token);
- ArrayList<AuxiliaryResolveInfo.AuxiliaryFilter> filters = null;
- boolean isWebIntent = origIntent.isBrowsableWebIntent();
- for (InstantAppResolveInfo instantAppResolveInfo : instantAppResolveInfoList) {
- if (shaPrefix.length > 0 && instantAppResolveInfo.shouldLetInstallerDecide()) {
- Slog.e(TAG, "InstantAppResolveInfo with mShouldLetInstallerDecide=true when digest"
- + " provided; ignoring");
- continue;
- }
- byte[] filterDigestBytes = instantAppResolveInfo.getDigestBytes();
- // Only include matching digests if we have a prefix and we're either dealing with a
- // web intent or the resolveInfo specifies digest details.
- if (shaPrefix.length > 0 && (isWebIntent || filterDigestBytes.length > 0)) {
- boolean matchFound = false;
- // Go in reverse order so we match the narrowest scope first.
- for (int i = shaPrefix.length - 1; i >= 0; --i) {
- if (Arrays.equals(digestBytes[i], filterDigestBytes)) {
- matchFound = true;
- break;
+ // Go in reverse order so we match the narrowest scope first.
+ for (int i = shaPrefix.length - 1; i >= 0 ; --i) {
+ for (InstantAppResolveInfo instantAppInfo : instantAppResolveInfoList) {
+ if (!Arrays.equals(digestBytes[i], instantAppInfo.getDigestBytes())) {
+ continue;
+ }
+ if (packageName != null
+ && !packageName.equals(instantAppInfo.getPackageName())) {
+ continue;
+ }
+ final List<InstantAppIntentFilter> instantAppFilters =
+ instantAppInfo.getIntentFilters();
+ // No filters; we need to start phase two
+ if (instantAppFilters == null || instantAppFilters.isEmpty()) {
+ if (DEBUG_EPHEMERAL) {
+ Log.d(TAG, "No app filters; go to phase 2");
+ }
+ return new AuxiliaryResolveInfo(instantAppInfo,
+ new IntentFilter(Intent.ACTION_VIEW) /*intentFilter*/,
+ null /*splitName*/, token, true /*needsPhase2*/,
+ null /*failureIntent*/);
+ }
+ // We have a domain match; resolve the filters to see if anything matches.
+ final PackageManagerService.EphemeralIntentResolver instantAppResolver =
+ new PackageManagerService.EphemeralIntentResolver();
+ for (int j = instantAppFilters.size() - 1; j >= 0; --j) {
+ final InstantAppIntentFilter instantAppFilter = instantAppFilters.get(j);
+ final List<IntentFilter> splitFilters = instantAppFilter.getFilters();
+ if (splitFilters == null || splitFilters.isEmpty()) {
+ continue;
+ }
+ for (int k = splitFilters.size() - 1; k >= 0; --k) {
+ final AuxiliaryResolveInfo intentInfo =
+ new AuxiliaryResolveInfo(instantAppInfo,
+ splitFilters.get(k), instantAppFilter.getSplitName(),
+ token, false /*needsPhase2*/, failureIntent);
+ instantAppResolver.addFilter(intentInfo);
}
}
- if (!matchFound) {
- continue;
+ List<AuxiliaryResolveInfo> matchedResolveInfoList = instantAppResolver.queryIntent(
+ origIntent, resolvedType, false /*defaultOnly*/, userId);
+ if (!matchedResolveInfoList.isEmpty()) {
+ if (DEBUG_EPHEMERAL) {
+ final AuxiliaryResolveInfo info = matchedResolveInfoList.get(0);
+ Log.d(TAG, "[" + token + "] Found match;"
+ + " package: " + info.packageName
+ + ", split: " + info.splitName
+ + ", versionCode: " + info.versionCode);
+ }
+ return matchedResolveInfoList.get(0);
+ } else if (DEBUG_EPHEMERAL) {
+ Log.d(TAG, "[" + token + "] No matches found"
+ + " package: " + instantAppInfo.getPackageName()
+ + ", versionCode: " + instantAppInfo.getVersionCode());
}
}
- // We matched a resolve info; resolve the filters to see if anything matches completely.
- List<AuxiliaryResolveInfo.AuxiliaryFilter> matchFilters = computeResolveFilters(
- origIntent, resolvedType, userId, packageName, token, instantAppResolveInfo);
- if (matchFilters != null) {
- if (matchFilters.isEmpty()) {
- requiresSecondPhase = true;
- }
- if (filters == null) {
- filters = new ArrayList<>(matchFilters);
- } else {
- filters.addAll(matchFilters);
- }
- }
- }
- if (filters != null && !filters.isEmpty()) {
- return new AuxiliaryResolveInfo(token, requiresSecondPhase, failureIntent, filters);
}
// Hash or filter mis-match; no instant apps for this domain.
return null;
}
- /**
- * Returns one of three states: <p/>
- * <ul>
- * <li>{@code null} if there are no matches will not be; resolution is unnecessary.</li>
- * <li>An empty list signifying that a 2nd phase of resolution is required.</li>
- * <li>A populated list meaning that matches were found and should be sent directly to the
- * installer</li>
- * </ul>
- *
- */
- private static List<AuxiliaryResolveInfo.AuxiliaryFilter> computeResolveFilters(
- Intent origIntent, String resolvedType, int userId, String packageName, String token,
- InstantAppResolveInfo instantAppInfo) {
- if (instantAppInfo.shouldLetInstallerDecide()) {
- return Collections.singletonList(
- new AuxiliaryResolveInfo.AuxiliaryFilter(
- instantAppInfo, null /* splitName */,
- instantAppInfo.getExtras()));
- }
- if (packageName != null
- && !packageName.equals(instantAppInfo.getPackageName())) {
- return null;
- }
- final List<InstantAppIntentFilter> instantAppFilters =
- instantAppInfo.getIntentFilters();
- if (instantAppFilters == null || instantAppFilters.isEmpty()) {
- // No filters on web intent; no matches, 2nd phase unnecessary.
- if (origIntent.isBrowsableWebIntent()) {
- return null;
- }
- // No filters; we need to start phase two
- if (DEBUG_EPHEMERAL) {
- Log.d(TAG, "No app filters; go to phase 2");
- }
- return Collections.emptyList();
- }
- final PackageManagerService.EphemeralIntentResolver instantAppResolver =
- new PackageManagerService.EphemeralIntentResolver();
- for (int j = instantAppFilters.size() - 1; j >= 0; --j) {
- final InstantAppIntentFilter instantAppFilter = instantAppFilters.get(j);
- final List<IntentFilter> splitFilters = instantAppFilter.getFilters();
- if (splitFilters == null || splitFilters.isEmpty()) {
- continue;
- }
- for (int k = splitFilters.size() - 1; k >= 0; --k) {
- IntentFilter filter = splitFilters.get(k);
- Iterator<IntentFilter.AuthorityEntry> authorities =
- filter.authoritiesIterator();
- // ignore http/s-only filters.
- if ((authorities == null || !authorities.hasNext())
- && (filter.hasDataScheme("http") || filter.hasDataScheme("https"))
- && filter.hasAction(Intent.ACTION_VIEW)
- && filter.hasCategory(Intent.CATEGORY_BROWSABLE)) {
- continue;
- }
- instantAppResolver.addFilter(
- new AuxiliaryResolveInfo.AuxiliaryFilter(
- filter,
- instantAppInfo,
- instantAppFilter.getSplitName(),
- instantAppInfo.getExtras()
- ));
- }
- }
- List<AuxiliaryResolveInfo.AuxiliaryFilter> matchedResolveInfoList =
- instantAppResolver.queryIntent(
- origIntent, resolvedType, false /*defaultOnly*/, userId);
- if (!matchedResolveInfoList.isEmpty()) {
- if (DEBUG_EPHEMERAL) {
- Log.d(TAG, "[" + token + "] Found match(es); " + matchedResolveInfoList);
- }
- return matchedResolveInfoList;
- } else if (DEBUG_EPHEMERAL) {
- Log.d(TAG, "[" + token + "] No matches found"
- + " package: " + instantAppInfo.getPackageName()
- + ", versionCode: " + instantAppInfo.getVersionCode());
- }
- return null;
- }
-
private static void logMetrics(int action, long startTime, String token,
@ResolutionStatus int status) {
final LogMaker logMaker = new LogMaker(action)
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index fdb157e..01c44af 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -108,8 +108,6 @@
import static com.android.server.pm.PackageManagerServiceUtils.getCompressedFiles;
import static com.android.server.pm.PackageManagerServiceUtils.getLastModifiedTime;
import static com.android.server.pm.PackageManagerServiceUtils.logCriticalInfo;
-import static com.android.server.pm.PackageManagerServiceUtils.signingDetailsHasCertificate;
-import static com.android.server.pm.PackageManagerServiceUtils.signingDetailsHasSha256Certificate;
import static com.android.server.pm.PackageManagerServiceUtils.verifySignatures;
import static com.android.server.pm.permission.PermissionsState.PERMISSION_OPERATION_FAILURE;
import static com.android.server.pm.permission.PermissionsState.PERMISSION_OPERATION_SUCCESS;
@@ -246,6 +244,7 @@
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Base64;
+import android.util.ByteStringUtils;
import android.util.DisplayMetrics;
import android.util.EventLog;
import android.util.ExceptionUtils;
@@ -3595,35 +3594,24 @@
}
private @Nullable ActivityInfo getInstantAppInstallerLPr() {
- String[] orderedActions = Build.IS_ENG
- ? new String[]{
- Intent.ACTION_INSTALL_INSTANT_APP_PACKAGE + "_TEST",
- Intent.ACTION_INSTALL_INSTANT_APP_PACKAGE,
- Intent.ACTION_INSTALL_EPHEMERAL_PACKAGE}
- : new String[]{
- Intent.ACTION_INSTALL_INSTANT_APP_PACKAGE,
- Intent.ACTION_INSTALL_EPHEMERAL_PACKAGE};
+ final Intent intent = new Intent(Intent.ACTION_INSTALL_INSTANT_APP_PACKAGE);
+ intent.addCategory(Intent.CATEGORY_DEFAULT);
+ intent.setDataAndType(Uri.fromFile(new File("foo.apk")), PACKAGE_MIME_TYPE);
final int resolveFlags =
MATCH_DIRECT_BOOT_AWARE
- | MATCH_DIRECT_BOOT_UNAWARE
- | Intent.FLAG_IGNORE_EPHEMERAL
- | (!Build.IS_ENG ? MATCH_SYSTEM_ONLY : 0);
- final Intent intent = new Intent();
- intent.addCategory(Intent.CATEGORY_DEFAULT);
- intent.setDataAndType(Uri.fromFile(new File("foo.apk")), PACKAGE_MIME_TYPE);
- List<ResolveInfo> matches = null;
- for (String action : orderedActions) {
- intent.setAction(action);
+ | MATCH_DIRECT_BOOT_UNAWARE
+ | (!Build.IS_DEBUGGABLE ? MATCH_SYSTEM_ONLY : 0);
+ List<ResolveInfo> matches = queryIntentActivitiesInternal(intent, PACKAGE_MIME_TYPE,
+ resolveFlags, UserHandle.USER_SYSTEM);
+ // temporarily look for the old action
+ if (matches.isEmpty()) {
+ if (DEBUG_EPHEMERAL) {
+ Slog.d(TAG, "Ephemeral installer not found with new action; try old one");
+ }
+ intent.setAction(Intent.ACTION_INSTALL_EPHEMERAL_PACKAGE);
matches = queryIntentActivitiesInternal(intent, PACKAGE_MIME_TYPE,
resolveFlags, UserHandle.USER_SYSTEM);
- if (matches.isEmpty()) {
- if (DEBUG_EPHEMERAL) {
- Slog.d(TAG, "Instant App installer not found with " + action);
- }
- } else {
- break;
- }
}
Iterator<ResolveInfo> iter = matches.iterator();
while (iter.hasNext()) {
@@ -3631,8 +3619,7 @@
final PackageSetting ps = mSettings.mPackages.get(rInfo.activityInfo.packageName);
if (ps != null) {
final PermissionsState permissionsState = ps.getPermissionsState();
- if (permissionsState.hasPermission(Manifest.permission.INSTALL_PACKAGES, 0)
- || Build.IS_ENG) {
+ if (permissionsState.hasPermission(Manifest.permission.INSTALL_PACKAGES, 0)) {
continue;
}
}
@@ -4791,7 +4778,10 @@
flags |= PackageManager.MATCH_INSTANT;
} else {
final boolean wantMatchInstant = (flags & PackageManager.MATCH_INSTANT) != 0;
- final boolean allowMatchInstant = wantInstantApps
+ final boolean allowMatchInstant =
+ (wantInstantApps
+ && Intent.ACTION_VIEW.equals(intent.getAction())
+ && hasWebURI(intent))
|| (wantMatchInstant && canViewInstantApps(callingUid, userId));
flags &= ~(PackageManager.MATCH_VISIBLE_TO_INSTANT_APP_ONLY
| PackageManager.MATCH_EXPLICITLY_VISIBLE_ONLY);
@@ -5546,9 +5536,9 @@
}
switch (type) {
case CERT_INPUT_RAW_X509:
- return signingDetailsHasCertificate(certificate, p.mSigningDetails);
+ return p.mSigningDetails.hasCertificate(certificate);
case CERT_INPUT_SHA256:
- return signingDetailsHasSha256Certificate(certificate, p.mSigningDetails);
+ return p.mSigningDetails.hasSha256Certificate(certificate);
default:
return false;
}
@@ -5587,9 +5577,9 @@
}
switch (type) {
case CERT_INPUT_RAW_X509:
- return signingDetailsHasCertificate(certificate, signingDetails);
+ return signingDetails.hasCertificate(certificate);
case CERT_INPUT_SHA256:
- return signingDetailsHasSha256Certificate(certificate, signingDetails);
+ return signingDetails.hasSha256Certificate(certificate);
default:
return false;
}
@@ -5981,14 +5971,8 @@
if (!skipPackageCheck && intent.getPackage() != null) {
return false;
}
- if (!intent.isBrowsableWebIntent()) {
- // for non web intents, we should not resolve externally if an app already exists to
- // handle it or if the caller didn't explicitly request it.
- if ((resolvedActivities != null && resolvedActivities.size() != 0)
- || (intent.getFlags() & Intent.FLAG_ACTIVITY_MATCH_EXTERNAL) == 0) {
- return false;
- }
- } else if (intent.getData() == null) {
+ final boolean isWebUri = hasWebURI(intent);
+ if (!isWebUri || intent.getData().getHost() == null) {
return false;
}
// Deny ephemeral apps if the user chose _ALWAYS or _ALWAYS_ASK for intent resolution.
@@ -6386,7 +6370,7 @@
if (matches.get(i).getTargetUserId() == targetUserId) return true;
}
}
- if (intent.hasWebURI()) {
+ if (hasWebURI(intent)) {
// cross-profile app linking works only towards the parent.
final int callingUid = Binder.getCallingUid();
final UserInfo parent = getProfileParent(sourceUserId);
@@ -6561,7 +6545,7 @@
sortResult = true;
}
}
- if (intent.hasWebURI()) {
+ if (hasWebURI(intent)) {
CrossProfileDomainInfo xpDomainInfo = null;
final UserInfo parent = getProfileParent(userId);
if (parent != null) {
@@ -6647,6 +6631,7 @@
if (ps.getInstantApp(userId)) {
final long packedStatus = getDomainVerificationStatusLPr(ps, userId);
final int status = (int)(packedStatus >> 32);
+ final int linkGeneration = (int)(packedStatus & 0xFFFFFFFF);
if (status == INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER) {
// there's a local instant application installed, but, the user has
// chosen to never use it; skip resolution and don't acknowledge
@@ -6678,8 +6663,9 @@
null /*responseObj*/, intent /*origIntent*/, resolvedType,
null /*callingPackage*/, userId, null /*verificationBundle*/,
resolveForStart);
- auxiliaryResponse = InstantAppResolver.doInstantAppResolutionPhaseOne(
- mInstantAppResolverConnection, requestObject);
+ auxiliaryResponse =
+ InstantAppResolver.doInstantAppResolutionPhaseOne(
+ mContext, mInstantAppResolverConnection, requestObject);
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
} else {
// we have an instant application locally, but, we can't admit that since
@@ -6688,40 +6674,35 @@
// instant application available externally. when it comes time to start
// the instant application, we'll do the right thing.
final ApplicationInfo ai = localInstantApp.activityInfo.applicationInfo;
- auxiliaryResponse = new AuxiliaryResolveInfo(null /* failureActivity */,
- ai.packageName, ai.versionCode, null /* splitName */);
+ auxiliaryResponse = new AuxiliaryResolveInfo(
+ ai.packageName, null /*splitName*/, null /*failureActivity*/,
+ ai.versionCode, null /*failureIntent*/);
}
}
- if (intent.isBrowsableWebIntent() && auxiliaryResponse == null) {
- return result;
+ if (auxiliaryResponse != null) {
+ if (DEBUG_EPHEMERAL) {
+ Slog.v(TAG, "Adding ephemeral installer to the ResolveInfo list");
+ }
+ final ResolveInfo ephemeralInstaller = new ResolveInfo(mInstantAppInstallerInfo);
+ final PackageSetting ps =
+ mSettings.mPackages.get(mInstantAppInstallerActivity.packageName);
+ if (ps != null) {
+ ephemeralInstaller.activityInfo = PackageParser.generateActivityInfo(
+ mInstantAppInstallerActivity, 0, ps.readUserState(userId), userId);
+ ephemeralInstaller.activityInfo.launchToken = auxiliaryResponse.token;
+ ephemeralInstaller.auxiliaryInfo = auxiliaryResponse;
+ // make sure this resolver is the default
+ ephemeralInstaller.isDefault = true;
+ ephemeralInstaller.match = IntentFilter.MATCH_CATEGORY_SCHEME_SPECIFIC_PART
+ | IntentFilter.MATCH_ADJUSTMENT_NORMAL;
+ // add a non-generic filter
+ ephemeralInstaller.filter = new IntentFilter(intent.getAction());
+ ephemeralInstaller.filter.addDataPath(
+ intent.getData().getPath(), PatternMatcher.PATTERN_LITERAL);
+ ephemeralInstaller.isInstantAppAvailable = true;
+ result.add(ephemeralInstaller);
+ }
}
- final PackageSetting ps = mSettings.mPackages.get(mInstantAppInstallerActivity.packageName);
- if (ps == null) {
- return result;
- }
- final ResolveInfo ephemeralInstaller = new ResolveInfo(mInstantAppInstallerInfo);
- ephemeralInstaller.activityInfo = PackageParser.generateActivityInfo(
- mInstantAppInstallerActivity, 0, ps.readUserState(userId), userId);
- ephemeralInstaller.match = IntentFilter.MATCH_CATEGORY_SCHEME_SPECIFIC_PART
- | IntentFilter.MATCH_ADJUSTMENT_NORMAL;
- // add a non-generic filter
- ephemeralInstaller.filter = new IntentFilter();
- if (intent.getAction() != null) {
- ephemeralInstaller.filter.addAction(intent.getAction());
- }
- if (intent.getData() != null && intent.getData().getPath() != null) {
- ephemeralInstaller.filter.addDataPath(
- intent.getData().getPath(), PatternMatcher.PATTERN_LITERAL);
- }
- ephemeralInstaller.isInstantAppAvailable = true;
- // make sure this resolver is the default
- ephemeralInstaller.isDefault = true;
- ephemeralInstaller.auxiliaryInfo = auxiliaryResponse;
- if (DEBUG_EPHEMERAL) {
- Slog.v(TAG, "Adding ephemeral installer to the ResolveInfo list");
- }
-
- result.add(ephemeralInstaller);
return result;
}
@@ -6836,11 +6817,10 @@
final ResolveInfo info = resolveInfos.get(i);
// allow activities that are defined in the provided package
if (allowDynamicSplits
- && info.activityInfo != null
&& info.activityInfo.splitName != null
&& !ArrayUtils.contains(info.activityInfo.applicationInfo.splitNames,
info.activityInfo.splitName)) {
- if (mInstantAppInstallerActivity == null) {
+ if (mInstantAppInstallerInfo == null) {
if (DEBUG_INSTALL) {
Slog.v(TAG, "No installer - not adding it to the ResolveInfo list");
}
@@ -6852,15 +6832,14 @@
if (DEBUG_INSTALL) {
Slog.v(TAG, "Adding installer to the ResolveInfo list");
}
- final ResolveInfo installerInfo = new ResolveInfo(
- mInstantAppInstallerInfo);
+ final ResolveInfo installerInfo = new ResolveInfo(mInstantAppInstallerInfo);
final ComponentName installFailureActivity = findInstallFailureActivity(
info.activityInfo.packageName, filterCallingUid, userId);
installerInfo.auxiliaryInfo = new AuxiliaryResolveInfo(
+ info.activityInfo.packageName, info.activityInfo.splitName,
installFailureActivity,
- info.activityInfo.packageName,
info.activityInfo.applicationInfo.versionCode,
- info.activityInfo.splitName);
+ null /*failureIntent*/);
installerInfo.match = IntentFilter.MATCH_CATEGORY_SCHEME_SPECIFIC_PART
| IntentFilter.MATCH_ADJUSTMENT_NORMAL;
// add a non-generic filter
@@ -6877,7 +6856,6 @@
installerInfo.priority = info.priority;
installerInfo.preferredOrder = info.preferredOrder;
installerInfo.isDefault = info.isDefault;
- installerInfo.isInstantAppAvailable = true;
resolveInfos.set(i, installerInfo);
continue;
}
@@ -6935,6 +6913,17 @@
return resolveInfos.size() > 0 && resolveInfos.get(0).priority >= 0;
}
+ private static boolean hasWebURI(Intent intent) {
+ if (intent.getData() == null) {
+ return false;
+ }
+ final String scheme = intent.getScheme();
+ if (TextUtils.isEmpty(scheme)) {
+ return false;
+ }
+ return scheme.equals(IntentFilter.SCHEME_HTTP) || scheme.equals(IntentFilter.SCHEME_HTTPS);
+ }
+
private List<ResolveInfo> filterCandidatesWithDomainPreferredActivitiesLPr(Intent intent,
int matchFlags, List<ResolveInfo> candidates, CrossProfileDomainInfo xpDomainInfo,
int userId) {
@@ -7607,13 +7596,11 @@
if (DEBUG_EPHEMERAL) {
Slog.v(TAG, "Adding ephemeral installer to the ResolveInfo list");
}
- final ResolveInfo installerInfo = new ResolveInfo(
- mInstantAppInstallerInfo);
+ final ResolveInfo installerInfo = new ResolveInfo(mInstantAppInstallerInfo);
installerInfo.auxiliaryInfo = new AuxiliaryResolveInfo(
- null /* installFailureActivity */,
- info.serviceInfo.packageName,
- info.serviceInfo.applicationInfo.versionCode,
- info.serviceInfo.splitName);
+ info.serviceInfo.packageName, info.serviceInfo.splitName,
+ null /*failureActivity*/, info.serviceInfo.applicationInfo.versionCode,
+ null /*failureIntent*/);
// make sure this resolver is the default
installerInfo.isDefault = true;
installerInfo.match = IntentFilter.MATCH_CATEGORY_SCHEME_SPECIFIC_PART
@@ -7729,13 +7716,11 @@
if (DEBUG_EPHEMERAL) {
Slog.v(TAG, "Adding ephemeral installer to the ResolveInfo list");
}
- final ResolveInfo installerInfo = new ResolveInfo(
- mInstantAppInstallerInfo);
+ final ResolveInfo installerInfo = new ResolveInfo(mInstantAppInstallerInfo);
installerInfo.auxiliaryInfo = new AuxiliaryResolveInfo(
- null /*failureActivity*/,
- info.providerInfo.packageName,
- info.providerInfo.applicationInfo.versionCode,
- info.providerInfo.splitName);
+ info.providerInfo.packageName, info.providerInfo.splitName,
+ null /*failureActivity*/, info.providerInfo.applicationInfo.versionCode,
+ null /*failureIntent*/);
// make sure this resolver is the default
installerInfo.isDefault = true;
installerInfo.match = IntentFilter.MATCH_CATEGORY_SCHEME_SPECIFIC_PART
@@ -8367,13 +8352,11 @@
}
private void collectCertificatesLI(PackageSetting ps, PackageParser.Package pkg,
- boolean forceCollect) throws PackageManagerException {
+ boolean forceCollect, boolean skipVerify) throws PackageManagerException {
// When upgrading from pre-N MR1, verify the package time stamp using the package
// directory and not the APK file.
final long lastModifiedTime = mIsPreNMR1Upgrade
? new File(pkg.codePath).lastModified() : getLastModifiedTime(pkg);
- // Note that currently skipVerify skips verification on both base and splits for simplicity.
- final boolean skipVerify = forceCollect && canSkipFullPackageVerification(pkg);
if (ps != null && !forceCollect
&& ps.codePathString.equals(pkg.codePath)
&& ps.timeStamp == lastModifiedTime
@@ -8746,21 +8729,24 @@
}
// Verify certificates against what was last scanned. If it is an updated priv app, we will
- // force re-collecting certificate. Full apk verification will happen unless apk verity is
- // set up for the file. In that case, only small part of the apk is verified upfront.
+ // force re-collecting certificate.
final boolean forceCollect = PackageManagerServiceUtils.isApkVerificationForced(
disabledPkgSetting);
- collectCertificatesLI(pkgSetting, pkg, forceCollect);
+ // Full APK verification can be skipped during certificate collection, only if the file is
+ // in verified partition, or can be verified on access (when apk verity is enabled). In both
+ // cases, only data in Signing Block is verified instead of the whole file.
+ final boolean skipVerify = ((parseFlags & PackageParser.PARSE_IS_SYSTEM_DIR) != 0) ||
+ (forceCollect && canSkipFullPackageVerification(pkg));
+ collectCertificatesLI(pkgSetting, pkg, forceCollect, skipVerify);
boolean shouldHideSystemApp = false;
// A new application appeared on /system, but, we already have a copy of
// the application installed on /data.
if (scanSystemPartition && !isSystemPkgUpdated && pkgAlreadyExists
&& !pkgSetting.isSystem()) {
- // if the signatures don't match, wipe the installed application and its data
- if (compareSignatures(pkgSetting.signatures.mSigningDetails.signatures,
- pkg.mSigningDetails.signatures)
- != PackageManager.SIGNATURE_MATCH) {
+
+ if (!pkg.mSigningDetails.checkCapability(pkgSetting.signatures.mSigningDetails,
+ PackageParser.SigningDetails.CertCapabilities.INSTALLED_DATA)) {
logCriticalInfo(Log.WARN,
"System package signature mismatch;"
+ " name: " + pkgSetting.name);
@@ -9725,34 +9711,51 @@
}
final String[] expectedCertDigests = requiredCertDigests[i];
- // For apps targeting O MR1 we require explicit enumeration of all certs.
- final String[] libCertDigests = (targetSdk > Build.VERSION_CODES.O)
- ? PackageUtils.computeSignaturesSha256Digests(
- libPkg.mSigningDetails.signatures)
- : PackageUtils.computeSignaturesSha256Digests(
- new Signature[]{libPkg.mSigningDetails.signatures[0]});
- // Take a shortcut if sizes don't match. Note that if an app doesn't
- // target O we don't parse the "additional-certificate" tags similarly
- // how we only consider all certs only for apps targeting O (see above).
- // Therefore, the size check is safe to make.
- if (expectedCertDigests.length != libCertDigests.length) {
- throw new PackageManagerException(INSTALL_FAILED_MISSING_SHARED_LIBRARY,
- "Package " + packageName + " requires differently signed" +
- " static shared library; failing!");
- }
- // Use a predictable order as signature order may vary
- Arrays.sort(libCertDigests);
- Arrays.sort(expectedCertDigests);
+ if (expectedCertDigests.length > 1) {
- final int certCount = libCertDigests.length;
- for (int j = 0; j < certCount; j++) {
- if (!libCertDigests[j].equalsIgnoreCase(expectedCertDigests[j])) {
+ // For apps targeting O MR1 we require explicit enumeration of all certs.
+ final String[] libCertDigests = (targetSdk > Build.VERSION_CODES.O)
+ ? PackageUtils.computeSignaturesSha256Digests(
+ libPkg.mSigningDetails.signatures)
+ : PackageUtils.computeSignaturesSha256Digests(
+ new Signature[]{libPkg.mSigningDetails.signatures[0]});
+
+ // Take a shortcut if sizes don't match. Note that if an app doesn't
+ // target O we don't parse the "additional-certificate" tags similarly
+ // how we only consider all certs only for apps targeting O (see above).
+ // Therefore, the size check is safe to make.
+ if (expectedCertDigests.length != libCertDigests.length) {
throw new PackageManagerException(INSTALL_FAILED_MISSING_SHARED_LIBRARY,
"Package " + packageName + " requires differently signed" +
" static shared library; failing!");
}
+
+ // Use a predictable order as signature order may vary
+ Arrays.sort(libCertDigests);
+ Arrays.sort(expectedCertDigests);
+
+ final int certCount = libCertDigests.length;
+ for (int j = 0; j < certCount; j++) {
+ if (!libCertDigests[j].equalsIgnoreCase(expectedCertDigests[j])) {
+ throw new PackageManagerException(
+ INSTALL_FAILED_MISSING_SHARED_LIBRARY,
+ "Package " + packageName + " requires differently signed" +
+ " static shared library; failing!");
+ }
+ }
+ } else {
+
+ // lib signing cert could have rotated beyond the one expected, check to see
+ // if the new one has been blessed by the old
+ if (!libPkg.mSigningDetails.hasSha256Certificate(
+ ByteStringUtils.fromHexToByteArray(expectedCertDigests[0]))) {
+ throw new PackageManagerException(
+ INSTALL_FAILED_MISSING_SHARED_LIBRARY,
+ "Package " + packageName + " requires differently signed" +
+ " static shared library; failing!");
+ }
}
}
@@ -10169,6 +10172,15 @@
// We just determined the app is signed correctly, so bring
// over the latest parsed certs.
pkgSetting.signatures.mSigningDetails = pkg.mSigningDetails;
+
+
+ // if this is is a sharedUser, check to see if the new package is signed by a newer
+ // signing certificate than the existing one, and if so, copy over the new details
+ if (signatureCheckPs.sharedUser != null
+ && pkg.mSigningDetails.hasAncestor(
+ signatureCheckPs.sharedUser.signatures.mSigningDetails)) {
+ signatureCheckPs.sharedUser.signatures.mSigningDetails = pkg.mSigningDetails;
+ }
} catch (PackageManagerException e) {
if ((parseFlags & PackageParser.PARSE_IS_SYSTEM_DIR) == 0) {
throw e;
@@ -10195,6 +10207,13 @@
String msg = "System package " + pkg.packageName
+ " signature changed; retaining data.";
reportSettingsProblem(Log.WARN, msg);
+ } catch (IllegalArgumentException e) {
+
+ // should never happen: certs matched when checking, but not when comparing
+ // old to new for sharedUser
+ throw new PackageManagerException(INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES,
+ "Signing certificates comparison made on incomparable signing details"
+ + " but somehow passed verifySignatures!");
}
}
@@ -11775,7 +11794,7 @@
mInstantAppInstallerActivity.exported = true;
mInstantAppInstallerActivity.enabled = true;
mInstantAppInstallerInfo.activityInfo = mInstantAppInstallerActivity;
- mInstantAppInstallerInfo.priority = 1;
+ mInstantAppInstallerInfo.priority = 0;
mInstantAppInstallerInfo.preferredOrder = 1;
mInstantAppInstallerInfo.isDefault = true;
mInstantAppInstallerInfo.match = IntentFilter.MATCH_CATEGORY_SCHEME_SPECIFIC_PART
@@ -13225,8 +13244,7 @@
}
static final class EphemeralIntentResolver
- extends IntentResolver<AuxiliaryResolveInfo.AuxiliaryFilter,
- AuxiliaryResolveInfo.AuxiliaryFilter> {
+ extends IntentResolver<AuxiliaryResolveInfo, AuxiliaryResolveInfo> {
/**
* The result that has the highest defined order. Ordering applies on a
* per-package basis. Mapping is from package name to Pair of order and
@@ -13241,19 +13259,18 @@
final ArrayMap<String, Pair<Integer, InstantAppResolveInfo>> mOrderResult = new ArrayMap<>();
@Override
- protected AuxiliaryResolveInfo.AuxiliaryFilter[] newArray(int size) {
- return new AuxiliaryResolveInfo.AuxiliaryFilter[size];
+ protected AuxiliaryResolveInfo[] newArray(int size) {
+ return new AuxiliaryResolveInfo[size];
}
@Override
- protected boolean isPackageForFilter(String packageName,
- AuxiliaryResolveInfo.AuxiliaryFilter responseObj) {
+ protected boolean isPackageForFilter(String packageName, AuxiliaryResolveInfo responseObj) {
return true;
}
@Override
- protected AuxiliaryResolveInfo.AuxiliaryFilter newResult(
- AuxiliaryResolveInfo.AuxiliaryFilter responseObj, int match, int userId) {
+ protected AuxiliaryResolveInfo newResult(AuxiliaryResolveInfo responseObj, int match,
+ int userId) {
if (!sUserManager.exists(userId)) {
return null;
}
@@ -13274,7 +13291,7 @@
}
@Override
- protected void filterResults(List<AuxiliaryResolveInfo.AuxiliaryFilter> results) {
+ protected void filterResults(List<AuxiliaryResolveInfo> results) {
// only do work if ordering is enabled [most of the time it won't be]
if (mOrderResult.size() == 0) {
return;
@@ -16061,10 +16078,10 @@
return;
}
} else {
+
// default to original signature matching
- if (compareSignatures(oldPackage.mSigningDetails.signatures,
- pkg.mSigningDetails.signatures)
- != PackageManager.SIGNATURE_MATCH) {
+ if (!pkg.mSigningDetails.checkCapability(oldPackage.mSigningDetails,
+ PackageParser.SigningDetails.CertCapabilities.INSTALLED_DATA)) {
res.setError(INSTALL_FAILED_UPDATE_INCOMPATIBLE,
"New package has a different signature: " + pkgName);
return;
@@ -17058,9 +17075,25 @@
sourcePackageSetting, scanFlags))) {
sigsOk = ksms.checkUpgradeKeySetLocked(sourcePackageSetting, pkg);
} else {
- sigsOk = compareSignatures(
- sourcePackageSetting.signatures.mSigningDetails.signatures,
- pkg.mSigningDetails.signatures) == PackageManager.SIGNATURE_MATCH;
+
+ // in the event of signing certificate rotation, we need to see if the
+ // package's certificate has rotated from the current one, or if it is an
+ // older certificate with which the current is ok with sharing permissions
+ if (sourcePackageSetting.signatures.mSigningDetails.checkCapability(
+ pkg.mSigningDetails,
+ PackageParser.SigningDetails.CertCapabilities.PERMISSION)) {
+ sigsOk = true;
+ } else if (pkg.mSigningDetails.checkCapability(
+ sourcePackageSetting.signatures.mSigningDetails,
+ PackageParser.SigningDetails.CertCapabilities.PERMISSION)) {
+
+ // the scanned package checks out, has signing certificate rotation
+ // history, and is newer; bring it over
+ sourcePackageSetting.signatures.mSigningDetails = pkg.mSigningDetails;
+ sigsOk = true;
+ } else {
+ sigsOk = false;
+ }
}
if (!sigsOk) {
// If the owning package is the system itself, we log but allow
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
index 76c199b..c02331d 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
@@ -531,18 +531,29 @@
// migrate the old signatures to the new scheme
packageSignatures.mSigningDetails = parsedSignatures;
return true;
+ } else if (parsedSignatures.hasPastSigningCertificates()) {
+
+ // well this sucks: the parsed package has probably rotated signing certificates, but
+ // we don't have enough information to determine if the new signing certificate was
+ // blessed by the old one
+ logCriticalInfo(Log.INFO, "Existing package " + packageName + " has flattened signing "
+ + "certificate chain. Unable to install newer version with rotated signing "
+ + "certificate.");
}
return false;
}
- private static boolean matchSignaturesRecover(String packageName,
- Signature[] existingSignatures, Signature[] parsedSignatures) {
+ private static boolean matchSignaturesRecover(
+ String packageName,
+ PackageParser.SigningDetails existingSignatures,
+ PackageParser.SigningDetails parsedSignatures,
+ @PackageParser.SigningDetails.CertCapabilities int flags) {
String msg = null;
try {
- if (Signature.areEffectiveMatch(existingSignatures, parsedSignatures)) {
- logCriticalInfo(Log.INFO,
- "Recovered effectively matching certificates for " + packageName);
- return true;
+ if (parsedSignatures.checkCapabilityRecover(existingSignatures, flags)) {
+ logCriticalInfo(Log.INFO, "Recovered effectively matching certificates for "
+ + packageName);
+ return true;
}
} catch (CertificateException e) {
msg = e.getMessage();
@@ -563,9 +574,11 @@
PackageSetting disabledPkgSetting) {
try {
PackageParser.collectCertificates(disabledPkgSetting.pkg, true /* skipVerify */);
- if (compareSignatures(pkgSetting.signatures.mSigningDetails.signatures,
- disabledPkgSetting.signatures.mSigningDetails.signatures)
- != PackageManager.SIGNATURE_MATCH) {
+ if (pkgSetting.signatures.mSigningDetails.checkCapability(
+ disabledPkgSetting.signatures.mSigningDetails,
+ PackageParser.SigningDetails.CertCapabilities.INSTALLED_DATA)) {
+ return true;
+ } else {
logCriticalInfo(Log.ERROR, "Updated system app mismatches cert on /system: " +
pkgSetting.name);
return false;
@@ -575,69 +588,6 @@
e.getMessage());
return false;
}
- return true;
- }
-
- /**
- * Checks the signing certificates to see if the provided certificate is a member. Invalid for
- * {@code SigningDetails} with multiple signing certificates.
- * @param certificate certificate to check for membership
- * @param signingDetails signing certificates record whose members are to be searched
- * @return true if {@code certificate} is in {@code signingDetails}
- */
- public static boolean signingDetailsHasCertificate(
- byte[] certificate, PackageParser.SigningDetails signingDetails) {
- if (signingDetails == PackageParser.SigningDetails.UNKNOWN) {
- return false;
- }
- Signature signature = new Signature(certificate);
- if (signingDetails.hasPastSigningCertificates()) {
- for (int i = 0; i < signingDetails.pastSigningCertificates.length; i++) {
- if (signingDetails.pastSigningCertificates[i].equals(signature)) {
- return true;
- }
- }
- } else {
- // no signing history, just check the current signer
- if (signingDetails.signatures.length == 1
- && signingDetails.signatures[0].equals(signature)) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Checks the signing certificates to see if the provided certificate is a member. Invalid for
- * {@code SigningDetails} with multiple signing certificaes.
- * @param sha256Certificate certificate to check for membership
- * @param signingDetails signing certificates record whose members are to be searched
- * @return true if {@code certificate} is in {@code signingDetails}
- */
- public static boolean signingDetailsHasSha256Certificate(
- byte[] sha256Certificate, PackageParser.SigningDetails signingDetails ) {
- if (signingDetails == PackageParser.SigningDetails.UNKNOWN) {
- return false;
- }
- if (signingDetails.hasPastSigningCertificates()) {
- for (int i = 0; i < signingDetails.pastSigningCertificates.length; i++) {
- byte[] digest = PackageUtils.computeSha256DigestBytes(
- signingDetails.pastSigningCertificates[i].toByteArray());
- if (Arrays.equals(sha256Certificate, digest)) {
- return true;
- }
- }
- } else {
- // no signing history, just check the current signer
- if (signingDetails.signatures.length == 1) {
- byte[] digest = PackageUtils.computeSha256DigestBytes(
- signingDetails.signatures[0].toByteArray());
- if (Arrays.equals(sha256Certificate, digest)) {
- return true;
- }
- }
- }
- return false;
}
/** Returns true if APK Verity is enabled. */
@@ -662,10 +612,11 @@
final String packageName = pkgSetting.name;
boolean compatMatch = false;
if (pkgSetting.signatures.mSigningDetails.signatures != null) {
+
// Already existing package. Make sure signatures match
- boolean match = compareSignatures(pkgSetting.signatures.mSigningDetails.signatures,
- parsedSignatures.signatures)
- == PackageManager.SIGNATURE_MATCH;
+ boolean match = parsedSignatures.checkCapability(
+ pkgSetting.signatures.mSigningDetails,
+ PackageParser.SigningDetails.CertCapabilities.INSTALLED_DATA);
if (!match && compareCompat) {
match = matchSignaturesCompat(packageName, pkgSetting.signatures,
parsedSignatures);
@@ -673,8 +624,10 @@
}
if (!match && compareRecover) {
match = matchSignaturesRecover(
- packageName, pkgSetting.signatures.mSigningDetails.signatures,
- parsedSignatures.signatures);
+ packageName,
+ pkgSetting.signatures.mSigningDetails,
+ parsedSignatures,
+ PackageParser.SigningDetails.CertCapabilities.INSTALLED_DATA);
}
if (!match && isApkVerificationForced(disabledPkgSetting)) {
@@ -689,20 +642,35 @@
}
// Check for shared user signatures
if (pkgSetting.sharedUser != null
- && pkgSetting.sharedUser.signatures.mSigningDetails.signatures != null) {
- // Already existing package. Make sure signatures match
+ && pkgSetting.sharedUser.signatures.mSigningDetails
+ != PackageParser.SigningDetails.UNKNOWN) {
+
+ // Already existing package. Make sure signatures match. In case of signing certificate
+ // rotation, the packages with newer certs need to be ok with being sharedUserId with
+ // the older ones. We check to see if either the new package is signed by an older cert
+ // with which the current sharedUser is ok, or if it is signed by a newer one, and is ok
+ // with being sharedUser with the existing signing cert.
boolean match =
- compareSignatures(
- pkgSetting.sharedUser.signatures.mSigningDetails.signatures,
- parsedSignatures.signatures) == PackageManager.SIGNATURE_MATCH;
+ parsedSignatures.checkCapability(
+ pkgSetting.sharedUser.signatures.mSigningDetails,
+ PackageParser.SigningDetails.CertCapabilities.SHARED_USER_ID)
+ || pkgSetting.sharedUser.signatures.mSigningDetails.checkCapability(
+ parsedSignatures,
+ PackageParser.SigningDetails.CertCapabilities.SHARED_USER_ID);
if (!match && compareCompat) {
match = matchSignaturesCompat(
packageName, pkgSetting.sharedUser.signatures, parsedSignatures);
}
if (!match && compareRecover) {
- match = matchSignaturesRecover(packageName,
- pkgSetting.sharedUser.signatures.mSigningDetails.signatures,
- parsedSignatures.signatures);
+ match =
+ matchSignaturesRecover(packageName,
+ pkgSetting.sharedUser.signatures.mSigningDetails,
+ parsedSignatures,
+ PackageParser.SigningDetails.CertCapabilities.SHARED_USER_ID)
+ || matchSignaturesRecover(packageName,
+ parsedSignatures,
+ pkgSetting.sharedUser.signatures.mSigningDetails,
+ PackageParser.SigningDetails.CertCapabilities.SHARED_USER_ID);
compatMatch |= match;
}
if (!match) {
diff --git a/services/core/java/com/android/server/pm/PackageSettingBase.java b/services/core/java/com/android/server/pm/PackageSettingBase.java
index 18356c5..f14a684 100644
--- a/services/core/java/com/android/server/pm/PackageSettingBase.java
+++ b/services/core/java/com/android/server/pm/PackageSettingBase.java
@@ -23,6 +23,7 @@
import android.content.pm.ApplicationInfo;
import android.content.pm.IntentFilterVerificationInfo;
import android.content.pm.PackageManager;
+import android.content.pm.PackageParser;
import android.content.pm.PackageUserState;
import android.content.pm.Signature;
import android.service.pm.PackageProto;
@@ -236,6 +237,10 @@
return signatures.mSigningDetails.signatures;
}
+ public PackageParser.SigningDetails getSigningDetails() {
+ return signatures.mSigningDetails;
+ }
+
/**
* Makes a shallow copy of the given package settings.
*
diff --git a/services/core/java/com/android/server/pm/PackageSignatures.java b/services/core/java/com/android/server/pm/PackageSignatures.java
index 0229a37..95f490e 100644
--- a/services/core/java/com/android/server/pm/PackageSignatures.java
+++ b/services/core/java/com/android/server/pm/PackageSignatures.java
@@ -291,6 +291,7 @@
PackageManagerService.reportSettingsProblem(Log.WARN,
"<pastSigs> encountered multiple times under the same <sigs> at "
+ parser.getPositionDescription());
+ XmlUtils.skipCurrentTag(parser);
}
} else {
PackageManagerService.reportSettingsProblem(Log.WARN,
diff --git a/services/core/java/com/android/server/pm/SELinuxMMAC.java b/services/core/java/com/android/server/pm/SELinuxMMAC.java
index 7c6b309..fe5b3d4 100644
--- a/services/core/java/com/android/server/pm/SELinuxMMAC.java
+++ b/services/core/java/com/android/server/pm/SELinuxMMAC.java
@@ -482,7 +482,11 @@
Signature[] certs = mCerts.toArray(new Signature[0]);
if (pkg.mSigningDetails != SigningDetails.UNKNOWN
&& !Signature.areExactMatch(certs, pkg.mSigningDetails.signatures)) {
- return null;
+
+ // certs aren't exact match, but the package may have rotated from the known system cert
+ if (certs.length > 1 || !pkg.mSigningDetails.hasCertificate(certs[0])) {
+ return null;
+ }
}
// Check for inner package name matches given that the
diff --git a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
index e2123c2..6308766 100644
--- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
+++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
@@ -1166,9 +1166,9 @@
final String systemPackageName = mServiceInternal.getKnownPackageName(
PackageManagerInternal.PACKAGE_SYSTEM, UserHandle.USER_SYSTEM);
final PackageParser.Package systemPackage = getPackage(systemPackageName);
- return compareSignatures(systemPackage.mSigningDetails.signatures,
- pkg.mSigningDetails.signatures)
- == PackageManager.SIGNATURE_MATCH;
+ return pkg.mSigningDetails.hasAncestorOrSelf(systemPackage.mSigningDetails)
+ || systemPackage.mSigningDetails.checkCapability(pkg.mSigningDetails,
+ PackageParser.SigningDetails.CertCapabilities.PERMISSION);
}
private void grantDefaultPermissionExceptions(int userId) {
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
index cb3b107..08f8bbd 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
@@ -1022,12 +1022,24 @@
PackageManagerInternal.PACKAGE_SYSTEM, UserHandle.USER_SYSTEM);
final PackageParser.Package systemPackage =
mPackageManagerInt.getPackage(systemPackageName);
- boolean allowed = (PackageManagerServiceUtils.compareSignatures(
- bp.getSourceSignatures(), pkg.mSigningDetails.signatures)
- == PackageManager.SIGNATURE_MATCH)
- || (PackageManagerServiceUtils.compareSignatures(
- systemPackage.mSigningDetails.signatures, pkg.mSigningDetails.signatures)
- == PackageManager.SIGNATURE_MATCH);
+
+ // check if the package is allow to use this signature permission. A package is allowed to
+ // use a signature permission if:
+ // - it has the same set of signing certificates as the source package
+ // - or its signing certificate was rotated from the source package's certificate
+ // - or its signing certificate is a previous signing certificate of the defining
+ // package, and the defining package still trusts the old certificate for permissions
+ // - or it shares the above relationships with the system package
+ boolean allowed =
+ pkg.mSigningDetails.hasAncestorOrSelf(
+ bp.getSourcePackageSetting().getSigningDetails())
+ || bp.getSourcePackageSetting().getSigningDetails().checkCapability(
+ pkg.mSigningDetails,
+ PackageParser.SigningDetails.CertCapabilities.PERMISSION)
+ || pkg.mSigningDetails.hasAncestorOrSelf(systemPackage.mSigningDetails)
+ || systemPackage.mSigningDetails.checkCapability(
+ pkg.mSigningDetails,
+ PackageParser.SigningDetails.CertCapabilities.PERMISSION);
if (!allowed && (privilegedPermission || oemPermission)) {
if (pkg.isSystem()) {
// For updated system applications, a privileged/oem permission
diff --git a/services/core/java/com/android/server/power/ShutdownThread.java b/services/core/java/com/android/server/power/ShutdownThread.java
index eed3102..bd4aa1c 100644
--- a/services/core/java/com/android/server/power/ShutdownThread.java
+++ b/services/core/java/com/android/server/power/ShutdownThread.java
@@ -114,6 +114,7 @@
private static String METRIC_PM = "shutdown_package_manager";
private static String METRIC_RADIOS = "shutdown_radios";
private static String METRIC_RADIO = "shutdown_radio";
+ private static String METRIC_SHUTDOWN_TIME_START = "begin_shutdown";
private final Object mActionDoneSync = new Object();
private boolean mActionDone;
@@ -415,6 +416,7 @@
public void run() {
TimingsTraceLog shutdownTimingLog = newTimingsLog();
shutdownTimingLog.traceBegin("SystemServerShutdown");
+ metricShutdownStart();
metricStarted(METRIC_SYSTEM_SERVER);
BroadcastReceiver br = new BroadcastReceiver() {
@@ -530,7 +532,7 @@
shutdownTimingLog.traceEnd(); // SystemServerShutdown
metricEnded(METRIC_SYSTEM_SERVER);
- saveMetrics(mReboot);
+ saveMetrics(mReboot, mReason);
// Remaining work will be done by init, including vold shutdown
rebootOrShutdown(mContext, mReboot, mReason);
}
@@ -552,6 +554,12 @@
}
}
+ private static void metricShutdownStart() {
+ synchronized (TRON_METRICS) {
+ TRON_METRICS.put(METRIC_SHUTDOWN_TIME_START, System.currentTimeMillis());
+ }
+ }
+
private void setRebootProgress(final int progress, final CharSequence message) {
mHandler.post(new Runnable() {
@Override
@@ -673,10 +681,11 @@
PowerManagerService.lowLevelShutdown(reason);
}
- private static void saveMetrics(boolean reboot) {
+ private static void saveMetrics(boolean reboot, String reason) {
StringBuilder metricValue = new StringBuilder();
metricValue.append("reboot:");
metricValue.append(reboot ? "y" : "n");
+ metricValue.append(",").append("reason:").append(reason);
final int metricsSize = TRON_METRICS.size();
for (int i = 0; i < metricsSize; i++) {
final String name = TRON_METRICS.keyAt(i);
diff --git a/services/core/java/com/android/server/power/batterysaver/BatterySavingStats.java b/services/core/java/com/android/server/power/batterysaver/BatterySavingStats.java
index df4f8ec..9466350 100644
--- a/services/core/java/com/android/server/power/batterysaver/BatterySavingStats.java
+++ b/services/core/java/com/android/server/power/batterysaver/BatterySavingStats.java
@@ -22,6 +22,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.MetricsLogger;
import com.android.server.EventLogTags;
import com.android.server.LocalServices;
import com.android.server.power.BatterySaverPolicy;
@@ -116,9 +117,16 @@
}
}
+ @VisibleForTesting
+ static final String COUNTER_POWER_MILLIAMPS_PREFIX = "battery_saver_stats_milliamps_";
+
+ @VisibleForTesting
+ static final String COUNTER_TIME_SECONDS_PREFIX = "battery_saver_stats_seconds_";
+
private static BatterySavingStats sInstance;
private BatteryManagerInternal mBatteryManagerInternal;
+ private final MetricsLogger mMetricsLogger;
private static final int STATE_NOT_INITIALIZED = -1;
private static final int STATE_CHARGING = -2;
@@ -136,17 +144,21 @@
@GuardedBy("mLock")
final ArrayMap<Integer, Stat> mStats = new ArrayMap<>();
+ private final MetricsLoggerHelper mMetricsLoggerHelper = new MetricsLoggerHelper();
+
/**
* Don't call it directly -- use {@link #getInstance()}. Not private for testing.
+ * @param metricsLogger
*/
@VisibleForTesting
- BatterySavingStats() {
+ BatterySavingStats(MetricsLogger metricsLogger) {
mBatteryManagerInternal = LocalServices.getService(BatteryManagerInternal.class);
+ mMetricsLogger = metricsLogger;
}
public static synchronized BatterySavingStats getInstance() {
if (sInstance == null) {
- sInstance = new BatterySavingStats();
+ sInstance = new BatterySavingStats(new MetricsLogger());
}
return sInstance;
}
@@ -208,10 +220,12 @@
return getStat(statesToIndex(batterySaverState, interactiveState, dozeState));
}
+ @VisibleForTesting
long injectCurrentTime() {
return SystemClock.elapsedRealtime();
}
+ @VisibleForTesting
int injectBatteryLevel() {
final BatteryManagerInternal bmi = getBatteryManagerInternal();
if (bmi == null) {
@@ -227,15 +241,9 @@
*/
public void transitionState(int batterySaverState, int interactiveState, int dozeState) {
synchronized (mLock) {
-
final int newState = statesToIndex(
batterySaverState, interactiveState, dozeState);
- if (mCurrentState == newState) {
- return;
- }
-
- endLastStateLocked();
- startNewStateLocked(newState);
+ transitionStateLocked(newState);
}
}
@@ -244,23 +252,30 @@
*/
public void startCharging() {
synchronized (mLock) {
- if (mCurrentState < 0) {
- return;
- }
-
- endLastStateLocked();
- startNewStateLocked(STATE_CHARGING);
+ transitionStateLocked(STATE_CHARGING);
}
}
- private void endLastStateLocked() {
+ private void transitionStateLocked(int newState) {
+ if (mCurrentState == newState) {
+ return;
+ }
+ final long now = injectCurrentTime();
+ final int batteryLevel = injectBatteryLevel();
+
+ endLastStateLocked(now, batteryLevel);
+ startNewStateLocked(newState, now, batteryLevel);
+ mMetricsLoggerHelper.transitionState(newState, now, batteryLevel);
+ }
+
+ private void endLastStateLocked(long now, int batteryLevel) {
if (mCurrentState < 0) {
return;
}
final Stat stat = getStat(mCurrentState);
- stat.endBatteryLevel = injectBatteryLevel();
- stat.endTime = injectCurrentTime();
+ stat.endBatteryLevel = batteryLevel;
+ stat.endTime = now;
final long deltaTime = stat.endTime - stat.startTime;
final int deltaDrain = stat.startBatteryLevel - stat.endBatteryLevel;
@@ -283,9 +298,10 @@
deltaDrain,
stat.totalTimeMillis,
stat.totalBatteryDrain);
+
}
- private void startNewStateLocked(int newState) {
+ private void startNewStateLocked(int newState, long now, int batteryLevel) {
if (DEBUG) {
Slog.d(TAG, "New state: " + stateToString(newState));
}
@@ -296,8 +312,8 @@
}
final Stat stat = getStat(mCurrentState);
- stat.startBatteryLevel = injectBatteryLevel();
- stat.startTime = injectCurrentTime();
+ stat.startBatteryLevel = batteryLevel;
+ stat.startTime = now;
stat.endTime = 0;
}
@@ -349,5 +365,50 @@
onStat.totalBatteryDrain / 1000,
onStat.drainPerHour() / 1000.0));
}
-}
+ @VisibleForTesting
+ class MetricsLoggerHelper {
+ private int mLastState = STATE_NOT_INITIALIZED;
+ private long mStartTime;
+ private int mStartBatteryLevel;
+
+ private static final int STATE_CHANGE_DETECT_MASK =
+ (BatterySaverState.MASK << BatterySaverState.SHIFT) |
+ (InteractiveState.MASK << InteractiveState.SHIFT);
+
+ public void transitionState(int newState, long now, int batteryLevel) {
+ final boolean stateChanging =
+ ((mLastState >= 0) ^ (newState >= 0)) ||
+ (((mLastState ^ newState) & STATE_CHANGE_DETECT_MASK) != 0);
+ if (stateChanging) {
+ if (mLastState >= 0) {
+ final long deltaTime = now - mStartTime;
+ final int deltaBattery = mStartBatteryLevel - batteryLevel;
+
+ report(mLastState, deltaTime, deltaBattery);
+ }
+ mStartTime = now;
+ mStartBatteryLevel = batteryLevel;
+ }
+ mLastState = newState;
+ }
+
+ String getCounterSuffix(int state) {
+ final boolean batterySaver =
+ BatterySaverState.fromIndex(state) != BatterySaverState.OFF;
+ final boolean interactive =
+ InteractiveState.fromIndex(state) != InteractiveState.NON_INTERACTIVE;
+ if (batterySaver) {
+ return interactive ? "11" : "10";
+ } else {
+ return interactive ? "01" : "00";
+ }
+ }
+
+ void report(int state, long deltaTimeMs, int deltaBatteryUa) {
+ final String suffix = getCounterSuffix(state);
+ mMetricsLogger.count(COUNTER_POWER_MILLIAMPS_PREFIX + suffix, deltaBatteryUa / 1000);
+ mMetricsLogger.count(COUNTER_TIME_SECONDS_PREFIX + suffix, (int) (deltaTimeMs / 1000));
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/slice/SliceFullAccessList.java b/services/core/java/com/android/server/slice/SliceFullAccessList.java
new file mode 100644
index 0000000..591e809
--- /dev/null
+++ b/services/core/java/com/android/server/slice/SliceFullAccessList.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2018 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.slice;
+
+import android.content.Context;
+import android.content.pm.UserInfo;
+import android.os.UserManager;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.util.XmlUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.util.List;
+
+public class SliceFullAccessList {
+
+ static final int DB_VERSION = 1;
+ private static final String TAG = "SliceFullAccessList";
+
+ private static final String TAG_LIST = "slice-access-list";
+ private static final String TAG_PKG = "pkg";
+ private static final String TAG_USER = "user";
+
+ private final String ATT_USER_ID = "user";
+ private final String ATT_VERSION = "version";
+
+ private final SparseArray<ArraySet<String>> mFullAccessPkgs = new SparseArray<>();
+ private final Context mContext;
+
+ public SliceFullAccessList(Context context) {
+ mContext = context;
+ }
+
+ public boolean hasFullAccess(String pkg, int userId) {
+ ArraySet<String> pkgs = mFullAccessPkgs.get(userId, null);
+ return pkgs != null && pkgs.contains(pkg);
+ }
+
+ public void grantFullAccess(String pkg, int userId) {
+ ArraySet<String> pkgs = mFullAccessPkgs.get(userId, null);
+ if (pkgs == null) {
+ pkgs = new ArraySet<>();
+ mFullAccessPkgs.put(userId, pkgs);
+ }
+ pkgs.add(pkg);
+ }
+
+ public void writeXml(XmlSerializer out) throws IOException {
+ out.startTag(null, TAG_LIST);
+ out.attribute(null, ATT_VERSION, String.valueOf(DB_VERSION));
+
+ final int N = mFullAccessPkgs.size();
+ for (int i = 0 ; i < N; i++) {
+ final int userId = mFullAccessPkgs.keyAt(i);
+ final ArraySet<String> pkgs = mFullAccessPkgs.valueAt(i);
+ out.startTag(null, TAG_USER);
+ out.attribute(null, ATT_USER_ID, Integer.toString(userId));
+ if (pkgs != null) {
+ final int M = pkgs.size();
+ for (int j = 0; j < M; j++) {
+ out.startTag(null, TAG_PKG);
+ out.text(pkgs.valueAt(j));
+ out.endTag(null, TAG_PKG);
+
+ }
+ }
+ out.endTag(null, TAG_USER);
+ }
+ out.endTag(null, TAG_LIST);
+ }
+
+ public void readXml(XmlPullParser parser) throws XmlPullParserException, IOException {
+ // upgrade xml
+ int xmlVersion = XmlUtils.readIntAttribute(parser, ATT_VERSION, 0);
+ final List<UserInfo> activeUsers = UserManager.get(mContext).getUsers(true);
+ for (UserInfo userInfo : activeUsers) {
+ upgradeXml(xmlVersion, userInfo.getUserHandle().getIdentifier());
+ }
+
+ mFullAccessPkgs.clear();
+ // read grants
+ int type;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
+ String tag = parser.getName();
+ if (type == XmlPullParser.END_TAG
+ && TAG_LIST.equals(tag)) {
+ break;
+ }
+ if (type == XmlPullParser.START_TAG) {
+ if (TAG_USER.equals(tag)) {
+ final int userId = XmlUtils.readIntAttribute(parser, ATT_USER_ID, 0);
+ ArraySet<String> pkgs = new ArraySet<>();
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
+ String userTag = parser.getName();
+ if (type == XmlPullParser.END_TAG
+ && TAG_USER.equals(userTag)) {
+ break;
+ }
+ if (type == XmlPullParser.START_TAG) {
+ if (TAG_PKG.equals(userTag)) {
+ final String pkg = parser.nextText();
+ pkgs.add(pkg);
+ }
+ }
+ }
+ mFullAccessPkgs.put(userId, pkgs);
+ }
+ }
+ }
+ }
+
+ protected void upgradeXml(final int xmlVersion, final int userId) {}
+}
diff --git a/services/core/java/com/android/server/slice/SliceManagerService.java b/services/core/java/com/android/server/slice/SliceManagerService.java
index 8e7daee..5db0fc0 100644
--- a/services/core/java/com/android/server/slice/SliceManagerService.java
+++ b/services/core/java/com/android/server/slice/SliceManagerService.java
@@ -39,6 +39,7 @@
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Binder;
+import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
@@ -47,7 +48,10 @@
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.ArraySet;
+import android.util.AtomicFile;
import android.util.Log;
+import android.util.Slog;
+import android.util.Xml.Encoding;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
@@ -57,6 +61,16 @@
import com.android.server.ServiceThread;
import com.android.server.SystemService;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@@ -77,6 +91,8 @@
private final ArraySet<SliceGrant> mUserGrants = new ArraySet<>();
private final Handler mHandler;
private final ContentObserver mObserver;
+ private final AtomicFile mSliceAccessFile;
+ private final SliceFullAccessList mAccessList;
public SliceManagerService(Context context) {
this(context, createHandler().getLooper());
@@ -101,6 +117,21 @@
}
}
};
+ final File systemDir = new File(Environment.getDataDirectory(), "system");
+ mSliceAccessFile = new AtomicFile(new File(systemDir, "slice_access.xml"));
+ mAccessList = new SliceFullAccessList(mContext);
+
+ synchronized (mSliceAccessFile) {
+ if (!mSliceAccessFile.exists()) return;
+ try {
+ InputStream input = mSliceAccessFile.openRead();
+ XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+ parser.setInput(input, Encoding.UTF_8.name());
+ mAccessList.readXml(parser);
+ } catch (IOException | XmlPullParserException e) {
+ Slog.d(TAG, "Can't read slice access file", e);
+ }
+ }
}
/// ----- Lifecycle stuff -----
@@ -175,11 +206,11 @@
== PERMISSION_GRANTED) {
return SliceManager.PERMISSION_GRANTED;
}
- if (hasFullSliceAccess(pkg, uid)) {
+ if (hasFullSliceAccess(pkg, UserHandle.getUserId(uid))) {
return SliceManager.PERMISSION_GRANTED;
}
synchronized (mLock) {
- if (mUserGrants.contains(new SliceGrant(uri, pkg))) {
+ if (mUserGrants.contains(new SliceGrant(uri, pkg, UserHandle.getUserId(uid)))) {
return SliceManager.PERMISSION_USER_GRANTED;
}
}
@@ -192,17 +223,19 @@
getContext().enforceCallingOrSelfPermission(permission.MANAGE_SLICE_PERMISSIONS,
"Slice granting requires MANAGE_SLICE_PERMISSIONS");
if (allSlices) {
- // TODO: Manage full access grants.
+ mAccessList.grantFullAccess(pkg, Binder.getCallingUserHandle().getIdentifier());
+ mHandler.post(mSaveAccessList);
} else {
synchronized (mLock) {
- mUserGrants.add(new SliceGrant(uri, pkg));
+ mUserGrants.add(new SliceGrant(uri, pkg,
+ Binder.getCallingUserHandle().getIdentifier()));
}
- long ident = Binder.clearCallingIdentity();
- try {
- mContext.getContentResolver().notifyChange(uri, null);
- } finally {
- Binder.restoreCallingIdentity(ident);
- }
+ }
+ long ident = Binder.clearCallingIdentity();
+ try {
+ mContext.getContentResolver().notifyChange(uri, null);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
}
synchronized (mLock) {
for (PinnedSliceState p : mPinnedSlicesByUri.values()) {
@@ -260,7 +293,7 @@
protected int checkAccess(String pkg, Uri uri, int uid, int pid) {
int user = UserHandle.getUserId(uid);
// Check for default launcher/assistant.
- if (!hasFullSliceAccess(pkg, uid)) {
+ if (!hasFullSliceAccess(pkg, user)) {
// Also allow things with uri access.
if (getContext().checkUriPermission(uri, pid, uid,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != PERMISSION_GRANTED) {
@@ -411,8 +444,7 @@
}
private boolean isGrantedFullAccess(String pkg, int userId) {
- // TODO: This will be user granted access, if we allow this through a prompt.
- return false;
+ return mAccessList.hasFullAccess(pkg, userId);
}
private static ServiceThread createHandler() {
@@ -422,6 +454,32 @@
return handlerThread;
}
+ private final Runnable mSaveAccessList = new Runnable() {
+ @Override
+ public void run() {
+ synchronized (mSliceAccessFile) {
+ final FileOutputStream stream;
+ try {
+ stream = mSliceAccessFile.startWrite();
+ } catch (IOException e) {
+ Slog.w(TAG, "Failed to save access file", e);
+ return;
+ }
+
+ try {
+ XmlSerializer out = XmlPullParserFactory.newInstance().newSerializer();
+ out.setOutput(stream, Encoding.UTF_8.name());
+ mAccessList.writeXml(out);
+ out.flush();
+ mSliceAccessFile.finishWrite(stream);
+ } catch (IOException | XmlPullParserException e) {
+ Slog.w(TAG, "Failed to save access file, restoring backup", e);
+ mSliceAccessFile.failWrite(stream);
+ }
+ }
+ }
+ };
+
public static class Lifecycle extends SystemService {
private SliceManagerService mService;
@@ -456,10 +514,12 @@
private class SliceGrant {
private final Uri mUri;
private final String mPkg;
+ private final int mUserId;
- public SliceGrant(Uri uri, String pkg) {
+ public SliceGrant(Uri uri, String pkg, int userId) {
mUri = uri;
mPkg = pkg;
+ mUserId = userId;
}
@Override
@@ -471,7 +531,8 @@
public boolean equals(Object obj) {
if (!(obj instanceof SliceGrant)) return false;
SliceGrant other = (SliceGrant) obj;
- return Objects.equals(other.mUri, mUri) && Objects.equals(other.mPkg, mPkg);
+ return Objects.equals(other.mUri, mUri) && Objects.equals(other.mPkg, mPkg)
+ && (other.mUserId == mUserId);
}
}
}
diff --git a/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java b/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java
new file mode 100644
index 0000000..853c7eb
--- /dev/null
+++ b/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java
@@ -0,0 +1,395 @@
+/*
+ * Copyright (C) 2018 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.textclassifier;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Slog;
+import android.service.textclassifier.ITextClassifierService;
+import android.service.textclassifier.ITextClassificationCallback;
+import android.service.textclassifier.ITextLinksCallback;
+import android.service.textclassifier.ITextSelectionCallback;
+import android.service.textclassifier.TextClassifierService;
+import android.view.textclassifier.TextClassification;
+import android.view.textclassifier.TextClassifier;
+import android.view.textclassifier.TextLinks;
+import android.view.textclassifier.TextSelection;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+import com.android.server.SystemService;
+
+import java.util.LinkedList;
+import java.util.Queue;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A manager for TextClassifier services.
+ * Apps bind to the TextClassificationManagerService for text classification. This service
+ * reroutes calls to it to a {@link TextClassifierService} that it manages.
+ */
+public final class TextClassificationManagerService extends ITextClassifierService.Stub {
+
+ private static final String LOG_TAG = "TextClassificationManagerService";
+
+ // How long after the last interaction with the service we would unbind
+ private static final long TIMEOUT_IDLE_BIND_MILLIS = TimeUnit.MINUTES.toMillis(1);
+
+ public static final class Lifecycle extends SystemService {
+
+ private final TextClassificationManagerService mManagerService;
+
+ public Lifecycle(Context context) {
+ super(context);
+ mManagerService = new TextClassificationManagerService(context);
+ }
+
+ @Override
+ public void onStart() {
+ try {
+ publishBinderService(Context.TEXT_CLASSIFICATION_SERVICE, mManagerService);
+ } catch (Throwable t) {
+ // Starting this service is not critical to the running of this device and should
+ // therefore not crash the device. If it fails, log the error and continue.
+ Slog.e(LOG_TAG, "Could not start the TextClassificationManagerService.", t);
+ }
+ }
+ }
+
+ private final Context mContext;
+ private final Handler mHandler;
+ private final Intent mServiceIntent;
+ private final ServiceConnection mConnection;
+ private final Runnable mUnbind;
+ private final Object mLock;
+ @GuardedBy("mLock")
+ private final Queue<PendingRequest> mPendingRequests;
+
+ @GuardedBy("mLock")
+ private ITextClassifierService mService;
+ @GuardedBy("mLock")
+ private boolean mBinding;
+
+ private TextClassificationManagerService(Context context) {
+ mContext = Preconditions.checkNotNull(context);
+ mHandler = new Handler();
+ mServiceIntent = new Intent(TextClassifierService.SERVICE_INTERFACE)
+ .setComponent(TextClassifierService.getServiceComponentName(mContext));
+ mConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ synchronized (mLock) {
+ mService = ITextClassifierService.Stub.asInterface(service);
+ setBindingLocked(false);
+ handlePendingRequestsLocked();
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ cleanupService();
+ }
+
+ @Override
+ public void onBindingDied(ComponentName name) {
+ cleanupService();
+ }
+
+ @Override
+ public void onNullBinding(ComponentName name) {
+ cleanupService();
+ }
+
+ private void cleanupService() {
+ synchronized (mLock) {
+ mService = null;
+ setBindingLocked(false);
+ handlePendingRequestsLocked();
+ }
+ }
+ };
+ mPendingRequests = new LinkedList<>();
+ mUnbind = this::unbind;
+ mLock = new Object();
+ }
+
+ @Override
+ public void onSuggestSelection(
+ CharSequence text, int selectionStartIndex, int selectionEndIndex,
+ TextSelection.Options options, ITextSelectionCallback callback)
+ throws RemoteException {
+ // TODO(b/72481438): All remote calls need to take userId.
+ validateInput(text, selectionStartIndex, selectionEndIndex, callback);
+
+ if (!bind()) {
+ callback.onFailure();
+ return;
+ }
+
+ synchronized (mLock) {
+ if (isBoundLocked()) {
+ mService.onSuggestSelection(
+ text, selectionStartIndex, selectionEndIndex, options, callback);
+ scheduleUnbindLocked();
+ } else {
+ final Callable<Void> request = () -> {
+ onSuggestSelection(
+ text, selectionStartIndex, selectionEndIndex,
+ options, callback);
+ return null;
+ };
+ final Callable<Void> onServiceFailure = () -> {
+ callback.onFailure();
+ return null;
+ };
+ enqueueRequestLocked(request, onServiceFailure, callback.asBinder());
+ }
+ }
+ }
+
+ @Override
+ public void onClassifyText(
+ CharSequence text, int startIndex, int endIndex,
+ TextClassification.Options options, ITextClassificationCallback callback)
+ throws RemoteException {
+ validateInput(text, startIndex, endIndex, callback);
+
+ if (!bind()) {
+ callback.onFailure();
+ return;
+ }
+
+ synchronized (mLock) {
+ if (isBoundLocked()) {
+ mService.onClassifyText(text, startIndex, endIndex, options, callback);
+ scheduleUnbindLocked();
+ } else {
+ final Callable<Void> request = () -> {
+ onClassifyText(text, startIndex, endIndex, options, callback);
+ return null;
+ };
+ final Callable<Void> onServiceFailure = () -> {
+ callback.onFailure();
+ return null;
+ };
+ enqueueRequestLocked(request, onServiceFailure, callback.asBinder());
+ }
+ }
+ }
+
+ @Override
+ public void onGenerateLinks(
+ CharSequence text, TextLinks.Options options, ITextLinksCallback callback)
+ throws RemoteException {
+ validateInput(text, callback);
+
+ if (!bind()) {
+ callback.onFailure();
+ return;
+ }
+
+ synchronized (mLock) {
+ if (isBoundLocked()) {
+ mService.onGenerateLinks(text, options, callback);
+ scheduleUnbindLocked();
+ } else {
+ final Callable<Void> request = () -> {
+ onGenerateLinks(text, options, callback);
+ return null;
+ };
+ final Callable<Void> onServiceFailure = () -> {
+ callback.onFailure();
+ return null;
+ };
+ enqueueRequestLocked(request, onServiceFailure, callback.asBinder());
+ }
+ }
+ }
+
+ /**
+ * @return true if the service is bound or in the process of being bound.
+ * Returns false otherwise.
+ */
+ private boolean bind() {
+ synchronized (mLock) {
+ if (isBoundLocked() || isBindingLocked()) {
+ return true;
+ }
+
+ // TODO: Handle bind timeout.
+ final boolean willBind;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ Slog.d(LOG_TAG, "Binding to " + mServiceIntent.getComponent());
+ willBind = mContext.bindServiceAsUser(
+ mServiceIntent, mConnection,
+ Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
+ Binder.getCallingUserHandle());
+ setBindingLocked(willBind);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ return willBind;
+ }
+ }
+
+ @GuardedBy("mLock")
+ private boolean isBoundLocked() {
+ return mService != null;
+ }
+
+ @GuardedBy("mLock")
+ private boolean isBindingLocked() {
+ return mBinding;
+ }
+
+ @GuardedBy("mLock")
+ private void setBindingLocked(boolean binding) {
+ mBinding = binding;
+ }
+
+ private void unbind() {
+ synchronized (mLock) {
+ if (!isBoundLocked()) {
+ return;
+ }
+
+ Slog.d(LOG_TAG, "Unbinding from " + mServiceIntent.getComponent());
+ mContext.unbindService(mConnection);
+
+ synchronized (mLock) {
+ mService = null;
+ }
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void scheduleUnbindLocked() {
+ mHandler.removeCallbacks(mUnbind);
+ mHandler.postDelayed(mUnbind, TIMEOUT_IDLE_BIND_MILLIS);
+ }
+
+ @GuardedBy("mLock")
+ private void enqueueRequestLocked(
+ Callable<Void> request, Callable<Void> onServiceFailure, IBinder binder) {
+ mPendingRequests.add(new PendingRequest(request, onServiceFailure, binder));
+ }
+
+ @GuardedBy("mLock")
+ private void handlePendingRequestsLocked() {
+ // TODO(b/72481146): Implement PendingRequest similar to that in RemoteFillService.
+ final PendingRequest[] pendingRequests =
+ mPendingRequests.toArray(new PendingRequest[mPendingRequests.size()]);
+ for (PendingRequest pendingRequest : pendingRequests) {
+ if (isBoundLocked()) {
+ pendingRequest.executeLocked();
+ } else {
+ pendingRequest.notifyServiceFailureLocked();
+ }
+ }
+ }
+
+ private final class PendingRequest implements IBinder.DeathRecipient {
+
+ private final Callable<Void> mRequest;
+ private final Callable<Void> mOnServiceFailure;
+ private final IBinder mBinder;
+
+ /**
+ * Initializes a new pending request.
+ *
+ * @param request action to perform when the service is bound
+ * @param onServiceFailure action to perform when the service dies or disconnects
+ * @param binder binder to the process that made this pending request
+ */
+ PendingRequest(
+ @NonNull Callable<Void> request, @NonNull Callable<Void> onServiceFailure,
+ @NonNull IBinder binder) {
+ mRequest = Preconditions.checkNotNull(request);
+ mOnServiceFailure = Preconditions.checkNotNull(onServiceFailure);
+ mBinder = Preconditions.checkNotNull(binder);
+ try {
+ mBinder.linkToDeath(this, 0);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @GuardedBy("mLock")
+ void executeLocked() {
+ removeLocked();
+ try {
+ mRequest.call();
+ } catch (Exception e) {
+ Slog.d(LOG_TAG, "Error handling pending request: " + e.getMessage());
+ }
+ }
+
+ @GuardedBy("mLock")
+ void notifyServiceFailureLocked() {
+ removeLocked();
+ try {
+ mOnServiceFailure.call();
+ } catch (Exception e) {
+ Slog.d(LOG_TAG, "Error notifying callback of service failure: "
+ + e.getMessage());
+ }
+ }
+
+ @Override
+ public void binderDied() {
+ synchronized (mLock) {
+ // No need to handle this pending request anymore. Remove.
+ removeLocked();
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void removeLocked() {
+ mPendingRequests.remove(this);
+ mBinder.unlinkToDeath(this, 0);
+ }
+ }
+
+ private static void validateInput(
+ CharSequence text, int startIndex, int endIndex, Object callback)
+ throws RemoteException {
+ try {
+ TextClassifier.Utils.validate(text, startIndex, endIndex, true /* allowInMainThread */);
+ Preconditions.checkNotNull(callback);
+ } catch (IllegalArgumentException | NullPointerException e) {
+ throw new RemoteException(e.getMessage());
+ }
+ }
+
+ private static void validateInput(CharSequence text, Object callback) throws RemoteException {
+ try {
+ TextClassifier.Utils.validate(text, true /* allowInMainThread */);
+ Preconditions.checkNotNull(callback);
+ } catch (IllegalArgumentException | NullPointerException e) {
+ throw new RemoteException(e.getMessage());
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/updates/SmartSelectionInstallReceiver.java b/services/core/java/com/android/server/updates/SmartSelectionInstallReceiver.java
index 1457366..eff9a9a 100644
--- a/services/core/java/com/android/server/updates/SmartSelectionInstallReceiver.java
+++ b/services/core/java/com/android/server/updates/SmartSelectionInstallReceiver.java
@@ -21,8 +21,8 @@
public SmartSelectionInstallReceiver() {
super(
"/data/misc/textclassifier/",
- "textclassifier.smartselection.model",
- "metadata/smartselection",
+ "textclassifier.model",
+ "metadata/classification",
"version");
}
diff --git a/services/core/java/com/android/server/wm/TaskStack.java b/services/core/java/com/android/server/wm/TaskStack.java
index cf54b67..ae5341b 100644
--- a/services/core/java/com/android/server/wm/TaskStack.java
+++ b/services/core/java/com/android/server/wm/TaskStack.java
@@ -736,15 +736,11 @@
}
private void updateSurfaceBounds() {
- updateSurfaceBounds(getPendingTransaction());
+ updateSurfaceSize(getPendingTransaction());
+ updateSurfacePosition();
scheduleAnimation();
}
- void updateSurfaceBounds(SurfaceControl.Transaction transaction) {
- updateSurfaceSize(transaction);
- updateSurfacePosition(transaction);
- }
-
private void updateSurfaceSize(SurfaceControl.Transaction transaction) {
if (mSurfaceControl == null) {
return;
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 0c0ce0e..6bd7f22 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -131,7 +131,7 @@
@Override
public void onConfigurationChanged(Configuration newParentConfig) {
super.onConfigurationChanged(newParentConfig);
- updateSurfacePosition(getPendingTransaction());
+ updateSurfacePosition();
scheduleAnimation();
}
@@ -1204,7 +1204,7 @@
}
}
- void updateSurfacePosition(SurfaceControl.Transaction transaction) {
+ void updateSurfacePosition() {
if (mSurfaceControl == null) {
return;
}
@@ -1214,12 +1214,8 @@
return;
}
- transaction.setPosition(mSurfaceControl, mTmpPos.x, mTmpPos.y);
+ getPendingTransaction().setPosition(mSurfaceControl, mTmpPos.x, mTmpPos.y);
mLastSurfacePosition.set(mTmpPos.x, mTmpPos.y);
-
- for (int i = mChildren.size() - 1; i >= 0; i--) {
- mChildren.get(i).updateSurfacePosition(transaction);
- }
}
void getRelativePosition(Point outPos) {
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 55c982c..53a8d82 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -4519,7 +4519,7 @@
if (dimmer != null) {
applyDims(dimmer);
}
- updateSurfacePosition(mPendingTransaction);
+ updateSurfacePosition();
mWinAnimator.prepareSurfaceLocked(true);
super.prepareSurfaces();
@@ -4541,7 +4541,11 @@
}
@Override
- void updateSurfacePosition(Transaction t) {
+ void updateSurfacePosition() {
+ updateSurfacePosition(getPendingTransaction());
+ }
+
+ private void updateSurfacePosition(Transaction t) {
if (mSurfaceControl == null) {
return;
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java b/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java
index d1cc5de..9fcf3ee 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java
@@ -19,12 +19,10 @@
import android.app.admin.IDevicePolicyManager;
import android.content.ComponentName;
import android.os.PersistableBundle;
-import android.os.UserHandle;
import android.security.keymaster.KeymasterCertificateChain;
import android.security.keystore.ParcelableKeyGenParameterSpec;
import android.telephony.data.ApnSetting;
-import com.android.internal.R;
import com.android.server.SystemService;
import java.util.ArrayList;
@@ -107,11 +105,6 @@
}
@Override
- public boolean startUserInBackground(ComponentName who, UserHandle userHandle) {
- return false;
- }
-
- @Override
public void setStartUserSessionMessage(
ComponentName admin, CharSequence startUserSessionMessage) {}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index dae7605..4c57f7f 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -19,7 +19,6 @@
import static android.Manifest.permission.BIND_DEVICE_ADMIN;
import static android.Manifest.permission.MANAGE_CA_CERTIFICATES;
import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
-import static android.app.ActivityManager.USER_OP_SUCCESS;
import static android.app.admin.DeviceAdminReceiver.EXTRA_TRANSFER_OWNERSHIP_ADMIN_EXTRAS_BUNDLE;
import static android.app.admin.DevicePolicyManager.ACTION_PROVISION_MANAGED_USER;
import static android.app.admin.DevicePolicyManager.CODE_ACCOUNTS_NOT_EMPTY;
@@ -68,9 +67,6 @@
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker
.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW;
-import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.PROVISIONING_ENTRY_POINT_ADB;
-import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW;
-
import static com.android.server.devicepolicy.TransferOwnershipMetadataManager.ADMIN_TYPE_DEVICE_OWNER;
import static com.android.server.devicepolicy.TransferOwnershipMetadataManager.ADMIN_TYPE_PROFILE_OWNER;
@@ -249,7 +245,6 @@
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
-import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -9030,7 +9025,7 @@
}
@Override
- public boolean startUserInBackground(ComponentName who, UserHandle userHandle) {
+ public int startUserInBackground(ComponentName who, UserHandle userHandle) {
Preconditions.checkNotNull(who, "ComponentName is null");
Preconditions.checkNotNull(userHandle, "UserHandle is null");
@@ -9041,27 +9036,31 @@
final int userId = userHandle.getIdentifier();
if (isManagedProfile(userId)) {
Log.w(LOG_TAG, "Managed profile cannot be started in background");
- return false;
+ return DevicePolicyManager.USER_OPERATION_ERROR_MANAGED_PROFILE;
}
final long id = mInjector.binderClearCallingIdentity();
try {
if (!mInjector.getActivityManagerInternal().canStartMoreUsers()) {
Log.w(LOG_TAG, "Cannot start more users in background");
- return false;
+ return DevicePolicyManager.USER_OPERATION_ERROR_MAX_RUNNING_USERS;
}
- return mInjector.getIActivityManager().startUserInBackground(userId);
+ if (mInjector.getIActivityManager().startUserInBackground(userId)) {
+ return DevicePolicyManager.USER_OPERATION_SUCCESS;
+ } else {
+ return DevicePolicyManager.USER_OPERATION_ERROR_UNKNOWN;
+ }
} catch (RemoteException e) {
// Same process, should not happen.
- return false;
+ return DevicePolicyManager.USER_OPERATION_ERROR_UNKNOWN;
} finally {
mInjector.binderRestoreCallingIdentity(id);
}
}
@Override
- public boolean stopUser(ComponentName who, UserHandle userHandle) {
+ public int stopUser(ComponentName who, UserHandle userHandle) {
Preconditions.checkNotNull(who, "ComponentName is null");
Preconditions.checkNotNull(userHandle, "UserHandle is null");
@@ -9072,23 +9071,14 @@
final int userId = userHandle.getIdentifier();
if (isManagedProfile(userId)) {
Log.w(LOG_TAG, "Managed profile cannot be stopped");
- return false;
+ return DevicePolicyManager.USER_OPERATION_ERROR_MANAGED_PROFILE;
}
- final long id = mInjector.binderClearCallingIdentity();
- try {
- return mInjector.getIActivityManager().stopUser(userId, true /*force*/, null)
- == USER_OP_SUCCESS;
- } catch (RemoteException e) {
- // Same process, should not happen.
- return false;
- } finally {
- mInjector.binderRestoreCallingIdentity(id);
- }
+ return stopUserUnchecked(userId);
}
@Override
- public boolean logoutUser(ComponentName who) {
+ public int logoutUser(ComponentName who) {
Preconditions.checkNotNull(who, "ComponentName is null");
final int callingUserId = mInjector.userHandleGetCallingUserId();
@@ -9102,20 +9092,40 @@
if (isManagedProfile(callingUserId)) {
Log.w(LOG_TAG, "Managed profile cannot be logout");
- return false;
+ return DevicePolicyManager.USER_OPERATION_ERROR_MANAGED_PROFILE;
}
final long id = mInjector.binderClearCallingIdentity();
try {
if (!mInjector.getIActivityManager().switchUser(UserHandle.USER_SYSTEM)) {
Log.w(LOG_TAG, "Failed to switch to primary user");
- return false;
+ // This should never happen as target user is UserHandle.USER_SYSTEM
+ return DevicePolicyManager.USER_OPERATION_ERROR_UNKNOWN;
}
- return mInjector.getIActivityManager().stopUser(callingUserId, true /*force*/, null)
- == USER_OP_SUCCESS;
} catch (RemoteException e) {
// Same process, should not happen.
- return false;
+ return DevicePolicyManager.USER_OPERATION_ERROR_UNKNOWN;
+ } finally {
+ mInjector.binderRestoreCallingIdentity(id);
+ }
+
+ return stopUserUnchecked(callingUserId);
+ }
+
+ private int stopUserUnchecked(int userId) {
+ final long id = mInjector.binderClearCallingIdentity();
+ try {
+ switch (mInjector.getIActivityManager().stopUser(userId, true /*force*/, null)) {
+ case ActivityManager.USER_OP_SUCCESS:
+ return DevicePolicyManager.USER_OPERATION_SUCCESS;
+ case ActivityManager.USER_OP_IS_CURRENT:
+ return DevicePolicyManager.USER_OPERATION_ERROR_CURRENT_USER;
+ default:
+ return DevicePolicyManager.USER_OPERATION_ERROR_UNKNOWN;
+ }
+ } catch (RemoteException e) {
+ // Same process, should not happen.
+ return DevicePolicyManager.USER_OPERATION_ERROR_UNKNOWN;
} finally {
mInjector.binderRestoreCallingIdentity(id);
}
@@ -11437,16 +11447,11 @@
}
private Set<String> getMeteredDisabledPackagesLocked(int userId) {
- final DevicePolicyData policy = getUserData(userId);
+ final ComponentName who = getOwnerComponent(userId);
final Set<String> restrictedPkgs = new ArraySet<>();
- for (int i = policy.mAdminList.size() - 1; i >= 0; --i) {
- final ActiveAdmin admin = policy.mAdminList.get(i);
- if (!isActiveAdminWithPolicyForUserLocked(admin,
- DeviceAdminInfo.USES_POLICY_PROFILE_OWNER, userId)) {
- // Not a profile or device owner, ignore
- continue;
- }
- if (admin.meteredDisabledPackages != null) {
+ if (who != null) {
+ final ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userId);
+ if (admin != null && admin.meteredDisabledPackages != null) {
restrictedPkgs.addAll(admin.meteredDisabledPackages);
}
}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index f95c6f0..210fd47 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -111,6 +111,7 @@
import com.android.server.statusbar.StatusBarManagerService;
import com.android.server.storage.DeviceStorageMonitorService;
import com.android.server.telecom.TelecomLoaderService;
+import com.android.server.textclassifier.TextClassificationManagerService;
import com.android.server.trust.TrustManagerService;
import com.android.server.tv.TvInputManagerService;
import com.android.server.tv.TvRemoteService;
@@ -733,6 +734,8 @@
false);
boolean disableTextServices = SystemProperties.getBoolean("config.disable_textservices",
false);
+ boolean disableSystemTextClassifier = SystemProperties.getBoolean(
+ "config.disable_systemtextclassifier", false);
boolean disableConsumerIr = SystemProperties.getBoolean("config.disable_consumerir", false);
boolean disableVrManager = SystemProperties.getBoolean("config.disable_vrmanager", false);
boolean disableCameraService = SystemProperties.getBoolean("config.disable_cameraservice",
@@ -1066,6 +1069,12 @@
traceEnd();
}
+ if (!disableSystemTextClassifier) {
+ traceBeginAndSlog("StartTextClassificationManagerService");
+ mSystemServiceManager.startService(TextClassificationManagerService.Lifecycle.class);
+ traceEnd();
+ }
+
traceBeginAndSlog("StartNetworkScoreService");
try {
networkScore = new NetworkScoreService(context);
diff --git a/services/robotests/src/com/android/server/backup/BackupManagerConstantsTest.java b/services/robotests/src/com/android/server/backup/BackupManagerConstantsTest.java
index c397f23..0752537 100644
--- a/services/robotests/src/com/android/server/backup/BackupManagerConstantsTest.java
+++ b/services/robotests/src/com/android/server/backup/BackupManagerConstantsTest.java
@@ -26,6 +26,7 @@
import com.android.server.testing.FrameworkRobolectricTestRunner;
import com.android.server.testing.SystemLoaderClasses;
+import com.android.server.testing.SystemLoaderPackages;
import org.junit.Before;
import org.junit.Test;
@@ -36,7 +37,7 @@
@RunWith(FrameworkRobolectricTestRunner.class)
@Config(manifest = Config.NONE, sdk = 26)
-@SystemLoaderClasses({BackupManagerConstants.class})
+@SystemLoaderPackages({"com.android.server.backup"})
@Presubmit
public class BackupManagerConstantsTest {
private static final String PACKAGE_NAME = "some.package.name";
diff --git a/services/robotests/src/com/android/server/backup/BackupManagerServiceTest.java b/services/robotests/src/com/android/server/backup/BackupManagerServiceTest.java
index b60ad4b..df09780 100644
--- a/services/robotests/src/com/android/server/backup/BackupManagerServiceTest.java
+++ b/services/robotests/src/com/android/server/backup/BackupManagerServiceTest.java
@@ -48,7 +48,8 @@
import com.android.server.backup.testing.TransportTestUtils.TransportMock;
import com.android.server.backup.transport.TransportNotRegisteredException;
import com.android.server.testing.FrameworkRobolectricTestRunner;
-import com.android.server.testing.SystemLoaderClasses;
+import com.android.server.testing.SystemLoaderPackages;
+
import java.io.File;
import java.util.HashMap;
import java.util.List;
@@ -74,11 +75,7 @@
sdk = 26,
shadows = {ShadowAppBackupUtils.class, ShadowBackupPolicyEnforcer.class}
)
-@SystemLoaderClasses({
- BackupManagerService.class,
- TransportManager.class,
- PackageManagerBackupAgent.class
-})
+@SystemLoaderPackages({"com.android.server.backup"})
@Presubmit
public class BackupManagerServiceTest {
private static final String TAG = "BMSTest";
diff --git a/services/robotests/src/com/android/server/backup/PerformBackupTaskTest.java b/services/robotests/src/com/android/server/backup/PerformBackupTaskTest.java
index 1360828..e103464 100644
--- a/services/robotests/src/com/android/server/backup/PerformBackupTaskTest.java
+++ b/services/robotests/src/com/android/server/backup/PerformBackupTaskTest.java
@@ -69,6 +69,7 @@
import com.android.server.backup.transport.TransportClient;
import com.android.server.testing.FrameworkRobolectricTestRunner;
import com.android.server.testing.SystemLoaderClasses;
+import com.android.server.testing.SystemLoaderPackages;
import com.android.server.testing.shadows.FrameworkShadowPackageManager;
import com.android.server.testing.shadows.ShadowBackupDataInput;
import com.android.server.testing.shadows.ShadowBackupDataOutput;
@@ -102,12 +103,10 @@
ShadowQueuedWork.class
}
)
+@SystemLoaderPackages({"com.android.server.backup"})
@SystemLoaderClasses({
- BackupManagerService.class,
- PerformBackupTask.class,
BackupDataOutput.class,
FullBackupDataOutput.class,
- TransportManager.class,
BackupAgent.class,
IBackupTransport.class,
IBackupAgent.class,
diff --git a/services/robotests/src/com/android/server/backup/TransportManagerTest.java b/services/robotests/src/com/android/server/backup/TransportManagerTest.java
index 068fe81..44ac803 100644
--- a/services/robotests/src/com/android/server/backup/TransportManagerTest.java
+++ b/services/robotests/src/com/android/server/backup/TransportManagerTest.java
@@ -51,7 +51,7 @@
import com.android.server.backup.transport.TransportClientManager;
import com.android.server.backup.transport.TransportNotRegisteredException;
import com.android.server.testing.FrameworkRobolectricTestRunner;
-import com.android.server.testing.SystemLoaderClasses;
+import com.android.server.testing.SystemLoaderPackages;
import com.android.server.testing.shadows.FrameworkShadowContextImpl;
import com.android.server.testing.shadows.FrameworkShadowPackageManager;
import java.util.ArrayList;
@@ -75,7 +75,7 @@
sdk = 26,
shadows = {FrameworkShadowPackageManager.class, FrameworkShadowContextImpl.class}
)
-@SystemLoaderClasses({TransportManager.class})
+@SystemLoaderPackages({"com.android.server.backup"})
@Presubmit
public class TransportManagerTest {
private static final String PACKAGE_A = "some.package.a";
diff --git a/services/robotests/src/com/android/server/backup/internal/PerformInitializeTaskTest.java b/services/robotests/src/com/android/server/backup/internal/PerformInitializeTaskTest.java
index 55fb460..5810c30 100644
--- a/services/robotests/src/com/android/server/backup/internal/PerformInitializeTaskTest.java
+++ b/services/robotests/src/com/android/server/backup/internal/PerformInitializeTaskTest.java
@@ -49,7 +49,7 @@
import com.android.server.backup.testing.TransportTestUtils.TransportMock;
import com.android.server.backup.transport.TransportClient;
import com.android.server.testing.FrameworkRobolectricTestRunner;
-import com.android.server.testing.SystemLoaderClasses;
+import com.android.server.testing.SystemLoaderPackages;
import org.junit.Before;
import org.junit.Test;
@@ -67,11 +67,7 @@
@RunWith(FrameworkRobolectricTestRunner.class)
@Config(manifest = Config.NONE, sdk = 26)
-@SystemLoaderClasses({
- BackupManagerService.class,
- PerformInitializeTaskTest.class,
- TransportManager.class
-})
+@SystemLoaderPackages({"com.android.server.backup"})
@Presubmit
public class PerformInitializeTaskTest {
@Mock private BackupManagerService mBackupManagerService;
diff --git a/services/robotests/src/com/android/server/backup/transport/TransportClientTest.java b/services/robotests/src/com/android/server/backup/transport/TransportClientTest.java
index db6e62f..ff1644c 100644
--- a/services/robotests/src/com/android/server/backup/transport/TransportClientTest.java
+++ b/services/robotests/src/com/android/server/backup/transport/TransportClientTest.java
@@ -46,6 +46,7 @@
import com.android.server.backup.TransportManager;
import com.android.server.testing.FrameworkRobolectricTestRunner;
import com.android.server.testing.SystemLoaderClasses;
+import com.android.server.testing.SystemLoaderPackages;
import com.android.server.testing.shadows.ShadowCloseGuard;
import com.android.server.testing.shadows.ShadowEventLog;
import com.android.server.testing.shadows.ShadowSlog;
@@ -66,7 +67,7 @@
sdk = 26,
shadows = {ShadowEventLog.class, ShadowCloseGuard.class, ShadowSlog.class}
)
-@SystemLoaderClasses({TransportManager.class, TransportClient.class})
+@SystemLoaderPackages({"com.android.server.backup"})
@Presubmit
public class TransportClientTest {
private static final String PACKAGE_NAME = "some.package.name";
diff --git a/services/robotests/src/com/android/server/testing/FrameworkRobolectricTestRunner.java b/services/robotests/src/com/android/server/testing/FrameworkRobolectricTestRunner.java
index c94d598..d2a4d06 100644
--- a/services/robotests/src/com/android/server/testing/FrameworkRobolectricTestRunner.java
+++ b/services/robotests/src/com/android/server/testing/FrameworkRobolectricTestRunner.java
@@ -16,8 +16,7 @@
package com.android.server.testing;
-import com.android.server.backup.PerformBackupTaskTest;
-import com.android.server.backup.internal.PerformBackupTask;
+import static java.util.Arrays.asList;
import com.google.common.collect.ImmutableSet;
@@ -33,10 +32,11 @@
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
-import java.nio.file.Files;
-import java.nio.file.Paths;
+import java.util.Arrays;
import java.util.Enumeration;
+import java.util.HashSet;
import java.util.Set;
+import java.util.stream.Stream;
import javax.annotation.Nonnull;
@@ -51,9 +51,9 @@
* against the actual classes that are in the tree, not a past version of them. Ideally we would
* have a locally built jar referenced by Robolectric, but until that happens one can use this
* class.
- * This class reads the {@link SystemLoaderClasses} annotation on test classes and for each class
- * in that annotation value it will bypass the android jar and load it from the system class loader.
- * Allowing the test to test the actual class in the tree.
+ * This class reads the {@link SystemLoaderClasses} or {@link SystemLoaderPackages} annotations on
+ * test classes, for classes that match the annotations it will bypass the android jar and load it
+ * from the system class loader. Allowing the test to test the actual class in the tree.
*
* Implementation note: One could think about overriding
* {@link RobolectricTestRunner#createClassLoaderConfig(FrameworkMethod)} method and putting the
@@ -72,11 +72,21 @@
public FrameworkRobolectricTestRunner(Class<?> testClass) throws InitializationError {
super(testClass);
- SystemLoaderClasses annotation = testClass.getAnnotation(SystemLoaderClasses.class);
- Class<?>[] systemLoaderClasses =
- (annotation != null) ? annotation.value() : new Class<?>[0];
- Set<String> systemLoaderClassNames = classesToClassNames(systemLoaderClasses);
- mSandboxFactory = new FrameworkSandboxFactory(systemLoaderClassNames);
+ Set<String> classPrefixes = getSystemLoaderClassPrefixes(testClass);
+ mSandboxFactory = new FrameworkSandboxFactory(classPrefixes);
+ }
+
+ private Set<String> getSystemLoaderClassPrefixes(Class<?> testClass) {
+ Set<String> classPrefixes = new HashSet<>();
+ SystemLoaderClasses byClass = testClass.getAnnotation(SystemLoaderClasses.class);
+ if (byClass != null) {
+ Stream.of(byClass.value()).map(Class::getName).forEach(classPrefixes::add);
+ }
+ SystemLoaderPackages byPackage = testClass.getAnnotation(SystemLoaderPackages.class);
+ if (byPackage != null) {
+ classPrefixes.addAll(asList(byPackage.value()));
+ }
+ return classPrefixes;
}
@Nonnull
@@ -92,15 +102,15 @@
}
private static class FrameworkClassLoader extends SandboxClassLoader {
- private final Set<String> mSystemLoaderClasses;
+ private final Set<String> mSystemLoaderClassPrefixes;
private FrameworkClassLoader(
- Set<String> systemLoaderClasses,
+ Set<String> systemLoaderClassPrefixes,
ClassLoader systemClassLoader,
InstrumentationConfiguration instrumentationConfig,
URL... urls) {
super(systemClassLoader, instrumentationConfig, urls);
- mSystemLoaderClasses = systemLoaderClasses;
+ mSystemLoaderClassPrefixes = systemLoaderClassPrefixes;
}
@Override
@@ -146,8 +156,8 @@
* loader, so we test if the classes in the annotation are prefixes of the class to load.
*/
private boolean shouldLoadFromSystemLoader(String className) {
- for (String classNamePrefix : mSystemLoaderClasses) {
- if (className.startsWith(classNamePrefix)) {
+ for (String classPrefix : mSystemLoaderClassPrefixes) {
+ if (className.startsWith(classPrefix)) {
return true;
}
}
@@ -156,10 +166,10 @@
}
private static class FrameworkSandboxFactory extends SandboxFactory {
- private final Set<String> mSystemLoaderClasses;
+ private final Set<String> mSystemLoaderClassPrefixes;
- private FrameworkSandboxFactory(Set<String> systemLoaderClasses) {
- mSystemLoaderClasses = systemLoaderClasses;
+ private FrameworkSandboxFactory(Set<String> systemLoaderClassPrefixes) {
+ mSystemLoaderClassPrefixes = systemLoaderClassPrefixes;
}
@Nonnull
@@ -167,18 +177,10 @@
public ClassLoader createClassLoader(
InstrumentationConfiguration instrumentationConfig, URL... urls) {
return new FrameworkClassLoader(
- mSystemLoaderClasses,
+ mSystemLoaderClassPrefixes,
ClassLoader.getSystemClassLoader(),
instrumentationConfig,
urls);
}
}
-
- private static Set<String> classesToClassNames(Class<?>[] classes) {
- ImmutableSet.Builder<String> builder = ImmutableSet.builder();
- for (Class<?> classObject : classes) {
- builder.add(classObject.getName());
- }
- return builder.build();
- }
}
diff --git a/services/robotests/src/com/android/server/testing/SystemLoaderPackages.java b/services/robotests/src/com/android/server/testing/SystemLoaderPackages.java
new file mode 100644
index 0000000..e01c0a4
--- /dev/null
+++ b/services/robotests/src/com/android/server/testing/SystemLoaderPackages.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2018 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.testing;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to be used in test classes that run with {@link FrameworkRobolectricTestRunner}.
+ * This will make the classes under the specified packages be loaded from the system class loader,
+ * NOT from the Robolectric android jar.
+ *
+ * @see FrameworkRobolectricTestRunner
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface SystemLoaderPackages {
+ String[] value() default {};
+}
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/PersistentKeyChainSnapshotTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/PersistentKeyChainSnapshotTest.java
new file mode 100644
index 0000000..aad5295
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/PersistentKeyChainSnapshotTest.java
@@ -0,0 +1,335 @@
+/*
+ * 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.locksettings.recoverablekeystore.storage;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.testng.Assert.assertThrows;
+
+import android.security.keystore.recovery.KeyDerivationParams;
+import android.security.keystore.recovery.WrappedApplicationKey;
+import android.security.keystore.recovery.KeyChainSnapshot;
+import android.security.keystore.recovery.KeyChainProtectionParams;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PersistentKeyChainSnapshotTest {
+
+ private static final String ALIAS = "some_key";
+ private static final String ALIAS2 = "another_key";
+ private static final byte[] RECOVERY_KEY_MATERIAL = "recovery_key_data"
+ .getBytes(StandardCharsets.UTF_8);
+ private static final byte[] KEY_MATERIAL = "app_key_data".getBytes(StandardCharsets.UTF_8);
+ private static final byte[] PUBLIC_KEY = "public_key_data".getBytes(StandardCharsets.UTF_8);
+ private static final byte[] ACCOUNT = "test_account".getBytes(StandardCharsets.UTF_8);
+ private static final byte[] SALT = "salt".getBytes(StandardCharsets.UTF_8);
+ private static final int SNAPSHOT_VERSION = 2;
+ private static final int MAX_ATTEMPTS = 10;
+ private static final long COUNTER_ID = 123456789L;
+ private static final byte[] SERVER_PARAMS = "server_params".getBytes(StandardCharsets.UTF_8);
+ private static final byte[] ZERO_BYTES = new byte[0];
+ private static final byte[] ONE_BYTE = new byte[]{(byte) 11};
+ private static final byte[] TWO_BYTES = new byte[]{(byte) 222,(byte) 222};
+
+ @Test
+ public void testWriteInt() throws Exception {
+ PersistentKeyChainSnapshot writer = new PersistentKeyChainSnapshot();
+ writer.initWriter();
+ writer.writeInt(Integer.MIN_VALUE);
+ writer.writeInt(Integer.MAX_VALUE);
+ byte[] result = writer.getOutput();
+
+ PersistentKeyChainSnapshot reader = new PersistentKeyChainSnapshot();
+ reader.initReader(result);
+ assertThat(reader.readInt()).isEqualTo(Integer.MIN_VALUE);
+ assertThat(reader.readInt()).isEqualTo(Integer.MAX_VALUE);
+
+ assertThrows(
+ IOException.class,
+ () -> reader.readInt());
+ }
+
+ @Test
+ public void testWriteLong() throws Exception {
+ PersistentKeyChainSnapshot writer = new PersistentKeyChainSnapshot();
+ writer.initWriter();
+ writer.writeLong(Long.MIN_VALUE);
+ writer.writeLong(Long.MAX_VALUE);
+ byte[] result = writer.getOutput();
+
+ PersistentKeyChainSnapshot reader = new PersistentKeyChainSnapshot();
+ reader.initReader(result);
+ assertThat(reader.readLong()).isEqualTo(Long.MIN_VALUE);
+ assertThat(reader.readLong()).isEqualTo(Long.MAX_VALUE);
+
+ assertThrows(
+ IOException.class,
+ () -> reader.readLong());
+ }
+
+ @Test
+ public void testWriteBytes() throws Exception {
+ PersistentKeyChainSnapshot writer = new PersistentKeyChainSnapshot();
+ writer.initWriter();
+ writer.writeBytes(ZERO_BYTES);
+ writer.writeBytes(ONE_BYTE);
+ writer.writeBytes(TWO_BYTES);
+ byte[] result = writer.getOutput();
+
+ PersistentKeyChainSnapshot reader = new PersistentKeyChainSnapshot();
+ reader.initReader(result);
+ assertThat(reader.readBytes()).isEqualTo(ZERO_BYTES);
+ assertThat(reader.readBytes()).isEqualTo(ONE_BYTE);
+ assertThat(reader.readBytes()).isEqualTo(TWO_BYTES);
+
+ assertThrows(
+ IOException.class,
+ () -> reader.readBytes());
+ }
+
+ @Test
+ public void testReadBytes_returnsNullArrayAsEmpty() throws Exception {
+ PersistentKeyChainSnapshot writer = new PersistentKeyChainSnapshot();
+ writer.initWriter();
+ writer.writeBytes(null);
+ byte[] result = writer.getOutput();
+
+ PersistentKeyChainSnapshot reader = new PersistentKeyChainSnapshot();
+ reader.initReader(result);
+ assertThat(reader.readBytes()).isEqualTo(new byte[]{}); // null -> empty array
+ }
+
+ @Test
+ public void testWriteKeyEntry() throws Exception {
+ PersistentKeyChainSnapshot writer = new PersistentKeyChainSnapshot();
+ writer.initWriter();
+ WrappedApplicationKey entry = new WrappedApplicationKey.Builder()
+ .setAlias(ALIAS)
+ .setEncryptedKeyMaterial(KEY_MATERIAL)
+ .setAccount(ACCOUNT)
+ .build();
+ writer.writeKeyEntry(entry);
+
+ byte[] result = writer.getOutput();
+
+ PersistentKeyChainSnapshot reader = new PersistentKeyChainSnapshot();
+ reader.initReader(result);
+
+ WrappedApplicationKey copy = reader.readKeyEntry();
+ assertThat(copy.getAlias()).isEqualTo(ALIAS);
+ assertThat(copy.getEncryptedKeyMaterial()).isEqualTo(KEY_MATERIAL);
+ assertThat(copy.getAccount()).isEqualTo(ACCOUNT);
+
+ assertThrows(
+ IOException.class,
+ () -> reader.readKeyEntry());
+ }
+
+ public void testWriteProtectionParams() throws Exception {
+ PersistentKeyChainSnapshot writer = new PersistentKeyChainSnapshot();
+ writer.initWriter();
+ KeyDerivationParams derivationParams = KeyDerivationParams.createSha256Params(SALT);
+ KeyChainProtectionParams protectionParams = new KeyChainProtectionParams.Builder()
+ .setUserSecretType(1)
+ .setLockScreenUiFormat(2)
+ .setKeyDerivationParams(derivationParams)
+ .build();
+ writer.writeProtectionParams(protectionParams);
+
+ byte[] result = writer.getOutput();
+
+ PersistentKeyChainSnapshot reader = new PersistentKeyChainSnapshot();
+ reader.initReader(result);
+
+ KeyChainProtectionParams copy = reader.readProtectionParams();
+ assertThat(copy.getUserSecretType()).isEqualTo(1);
+ assertThat(copy.getLockScreenUiFormat()).isEqualTo(2);
+ assertThat(copy.getKeyDerivationParams().getSalt()).isEqualTo(SALT);
+
+ assertThrows(
+ IOException.class,
+ () -> reader.readProtectionParams());
+ }
+
+ public void testKeyChainSnapshot() throws Exception {
+ PersistentKeyChainSnapshot writer = new PersistentKeyChainSnapshot();
+ writer.initWriter();
+
+ KeyDerivationParams derivationParams = KeyDerivationParams.createSha256Params(SALT);
+
+ ArrayList<KeyChainProtectionParams> protectionParamsList = new ArrayList<>();
+ protectionParamsList.add(new KeyChainProtectionParams.Builder()
+ .setUserSecretType(1)
+ .setLockScreenUiFormat(2)
+ .setKeyDerivationParams(derivationParams)
+ .build());
+
+ ArrayList<WrappedApplicationKey> appKeysList = new ArrayList<>();
+ appKeysList.add(new WrappedApplicationKey.Builder()
+ .setAlias(ALIAS)
+ .setEncryptedKeyMaterial(KEY_MATERIAL)
+ .setAccount(ACCOUNT)
+ .build());
+
+ KeyChainSnapshot snapshot = new KeyChainSnapshot.Builder()
+ .setSnapshotVersion(SNAPSHOT_VERSION)
+ .setKeyChainProtectionParams(protectionParamsList)
+ .setEncryptedRecoveryKeyBlob(KEY_MATERIAL)
+ .setWrappedApplicationKeys(appKeysList)
+ .setMaxAttempts(MAX_ATTEMPTS)
+ .setCounterId(COUNTER_ID)
+ .setServerParams(SERVER_PARAMS)
+ .setTrustedHardwarePublicKey(PUBLIC_KEY)
+ .build();
+
+ writer.writeKeyChainSnapshot(snapshot);
+
+ byte[] result = writer.getOutput();
+
+ PersistentKeyChainSnapshot reader = new PersistentKeyChainSnapshot();
+ reader.initReader(result);
+
+ KeyChainSnapshot copy = reader.readKeyChainSnapshot();
+ assertThat(copy.getSnapshotVersion()).isEqualTo(SNAPSHOT_VERSION);
+ assertThat(copy.getKeyChainProtectionParams()).hasSize(2);
+ assertThat(copy.getKeyChainProtectionParams().get(0).getUserSecretType()).isEqualTo(1);
+ assertThat(copy.getKeyChainProtectionParams().get(1).getUserSecretType()).isEqualTo(2);
+ assertThat(copy.getEncryptedRecoveryKeyBlob()).isEqualTo(RECOVERY_KEY_MATERIAL);
+ assertThat(copy.getWrappedApplicationKeys()).hasSize(2);
+ assertThat(copy.getWrappedApplicationKeys().get(0).getAlias()).isEqualTo(ALIAS);
+ assertThat(copy.getWrappedApplicationKeys().get(1).getAlias()).isEqualTo(ALIAS2);
+ assertThat(copy.getMaxAttempts()).isEqualTo(MAX_ATTEMPTS);
+ assertThat(copy.getCounterId()).isEqualTo(COUNTER_ID);
+ assertThat(copy.getServerParams()).isEqualTo(SERVER_PARAMS);
+ assertThat(copy.getTrustedHardwarePublicKey()).isEqualTo(PUBLIC_KEY);
+
+ assertThrows(
+ IOException.class,
+ () -> reader.readKeyChainSnapshot());
+
+ verifyDeserialize(snapshot);
+ }
+
+ public void testKeyChainSnapshot_withManyKeysAndProtectionParams() throws Exception {
+ PersistentKeyChainSnapshot writer = new PersistentKeyChainSnapshot();
+ writer.initWriter();
+
+ KeyDerivationParams derivationParams = KeyDerivationParams.createSha256Params(SALT);
+
+ ArrayList<KeyChainProtectionParams> protectionParamsList = new ArrayList<>();
+ protectionParamsList.add(new KeyChainProtectionParams.Builder()
+ .setUserSecretType(1)
+ .setLockScreenUiFormat(2)
+ .setKeyDerivationParams(derivationParams)
+ .build());
+ protectionParamsList.add(new KeyChainProtectionParams.Builder()
+ .setUserSecretType(2)
+ .setLockScreenUiFormat(3)
+ .setKeyDerivationParams(derivationParams)
+ .build());
+ ArrayList<WrappedApplicationKey> appKeysList = new ArrayList<>();
+ appKeysList.add(new WrappedApplicationKey.Builder()
+ .setAlias(ALIAS)
+ .setEncryptedKeyMaterial(KEY_MATERIAL)
+ .setAccount(ACCOUNT)
+ .build());
+ appKeysList.add(new WrappedApplicationKey.Builder()
+ .setAlias(ALIAS2)
+ .setEncryptedKeyMaterial(KEY_MATERIAL)
+ .setAccount(ACCOUNT)
+ .build());
+
+
+ KeyChainSnapshot snapshot = new KeyChainSnapshot.Builder()
+ .setSnapshotVersion(SNAPSHOT_VERSION)
+ .setKeyChainProtectionParams(protectionParamsList)
+ .setEncryptedRecoveryKeyBlob(KEY_MATERIAL)
+ .setWrappedApplicationKeys(appKeysList)
+ .setMaxAttempts(MAX_ATTEMPTS)
+ .setCounterId(COUNTER_ID)
+ .setServerParams(SERVER_PARAMS)
+ .setTrustedHardwarePublicKey(PUBLIC_KEY)
+ .build();
+
+ writer.writeKeyChainSnapshot(snapshot);
+
+ byte[] result = writer.getOutput();
+
+ PersistentKeyChainSnapshot reader = new PersistentKeyChainSnapshot();
+ reader.initReader(result);
+
+ KeyChainSnapshot copy = reader.readKeyChainSnapshot();
+ assertThat(copy.getSnapshotVersion()).isEqualTo(SNAPSHOT_VERSION);
+ assertThat(copy.getKeyChainProtectionParams().get(0).getUserSecretType()).isEqualTo(1);
+ assertThat(copy.getEncryptedRecoveryKeyBlob()).isEqualTo(RECOVERY_KEY_MATERIAL);
+ assertThat(copy.getWrappedApplicationKeys().get(0).getAlias()).isEqualTo(ALIAS);
+ assertThat(copy.getMaxAttempts()).isEqualTo(MAX_ATTEMPTS);
+ assertThat(copy.getCounterId()).isEqualTo(COUNTER_ID);
+ assertThat(copy.getServerParams()).isEqualTo(SERVER_PARAMS);
+ assertThat(copy.getTrustedHardwarePublicKey()).isEqualTo(PUBLIC_KEY);
+
+ assertThrows(
+ IOException.class,
+ () -> reader.readKeyChainSnapshot());
+
+ verifyDeserialize(snapshot);
+ }
+
+ private void verifyDeserialize(KeyChainSnapshot snapshot) throws Exception {
+ byte[] serialized = PersistentKeyChainSnapshot.serialize(snapshot);
+ KeyChainSnapshot copy = PersistentKeyChainSnapshot.deserialize(serialized);
+ assertThat(copy.getSnapshotVersion())
+ .isEqualTo(snapshot.getSnapshotVersion());
+ assertThat(copy.getKeyChainProtectionParams().size())
+ .isEqualTo(copy.getKeyChainProtectionParams().size());
+ assertThat(copy.getEncryptedRecoveryKeyBlob())
+ .isEqualTo(snapshot.getEncryptedRecoveryKeyBlob());
+ assertThat(copy.getWrappedApplicationKeys().size())
+ .isEqualTo(snapshot.getWrappedApplicationKeys().size());
+ assertThat(copy.getMaxAttempts()).isEqualTo(snapshot.getMaxAttempts());
+ assertThat(copy.getCounterId()).isEqualTo(snapshot.getCounterId());
+ assertThat(copy.getServerParams()).isEqualTo(snapshot.getServerParams());
+ assertThat(copy.getTrustedHardwarePublicKey())
+ .isEqualTo(snapshot.getTrustedHardwarePublicKey());
+ }
+
+
+ public void testDeserialize_failsForNewerVersion() throws Exception {
+ byte[] newVersion = new byte[]{(byte) 2, (byte) 0, (byte) 0, (byte) 0};
+ assertThrows(
+ IOException.class,
+ () -> PersistentKeyChainSnapshot.deserialize(newVersion));
+ }
+
+ public void testDeserialize_failsForEmptyData() throws Exception {
+ byte[] empty = new byte[]{};
+ assertThrows(
+ IOException.class,
+ () -> PersistentKeyChainSnapshot.deserialize(empty));
+ }
+
+}
+
diff --git a/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySavingStatsTest.java b/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySavingStatsTest.java
index f788cac..c3714c8 100644
--- a/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySavingStatsTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySavingStatsTest.java
@@ -16,10 +16,18 @@
package com.android.server.power.batterysaver;
import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
+import com.android.internal.logging.MetricsLogger;
import com.android.server.power.batterysaver.BatterySavingStats.BatterySaverState;
import com.android.server.power.batterysaver.BatterySavingStats.DozeState;
import com.android.server.power.batterysaver.BatterySavingStats.InteractiveState;
@@ -39,7 +47,11 @@
private class BatterySavingStatsTestable extends BatterySavingStats {
private long mTime = 1_000_000; // Some random starting time.
- private int mBatteryLevel = 100;
+ private int mBatteryLevel = 1_000_000_000;
+
+ private BatterySavingStatsTestable() {
+ super(mMetricsLogger);
+ }
@Override
long injectCurrentTime() {
@@ -60,8 +72,8 @@
mTime += 60_000 * minutes;
}
- void drainBattery(int percent) {
- mBatteryLevel -= percent;
+ void drainBattery(int value) {
+ mBatteryLevel -= value;
if (mBatteryLevel < 0) {
mBatteryLevel = 0;
}
@@ -81,6 +93,8 @@
}
}
+ public MetricsLogger mMetricsLogger = mock(MetricsLogger.class);
+
@Test
public void testAll() {
final BatterySavingStatsTestable target = new BatterySavingStatsTestable();
@@ -202,4 +216,95 @@
"BS=1,I=1,D=2:{0m,0,0.00}",
target.toDebugString());
}
+
+ private void assertMetricsLog(String counter, int value) {
+ verify(mMetricsLogger, times(1)).count(eq(counter), eq(value));
+ }
+
+ @Test
+ public void testMetricsLogger() {
+ final BatterySavingStatsTestable target = new BatterySavingStatsTestable();
+
+ target.advanceClock(1);
+ target.drainBattery(1000);
+
+ target.transitionState(
+ BatterySaverState.OFF,
+ InteractiveState.INTERACTIVE,
+ DozeState.NOT_DOZING);
+
+ verify(mMetricsLogger, times(0)).count(anyString(), anyInt());
+
+ target.advanceClock(1);
+ target.drainBattery(2000);
+
+ reset(mMetricsLogger);
+ target.transitionState(
+ BatterySaverState.OFF,
+ InteractiveState.NON_INTERACTIVE,
+ DozeState.NOT_DOZING);
+
+ assertMetricsLog(BatterySavingStats.COUNTER_POWER_MILLIAMPS_PREFIX + "01", 2);
+ assertMetricsLog(BatterySavingStats.COUNTER_TIME_SECONDS_PREFIX + "01", 60);
+
+ target.advanceClock(1);
+ target.drainBattery(2000);
+
+ reset(mMetricsLogger);
+ target.transitionState(
+ BatterySaverState.OFF,
+ InteractiveState.NON_INTERACTIVE,
+ DozeState.DEEP);
+
+ target.advanceClock(1);
+ target.drainBattery(2000);
+
+ verify(mMetricsLogger, times(0)).count(anyString(), anyInt());
+
+ target.transitionState(
+ BatterySaverState.OFF,
+ InteractiveState.NON_INTERACTIVE,
+ DozeState.LIGHT);
+
+ target.advanceClock(1);
+ target.drainBattery(2000);
+
+ verify(mMetricsLogger, times(0)).count(anyString(), anyInt());
+
+ target.transitionState(
+ BatterySaverState.ON,
+ InteractiveState.INTERACTIVE,
+ DozeState.NOT_DOZING);
+
+ assertMetricsLog(BatterySavingStats.COUNTER_POWER_MILLIAMPS_PREFIX + "00", 2 * 3);
+ assertMetricsLog(BatterySavingStats.COUNTER_TIME_SECONDS_PREFIX + "00", 60 * 3);
+
+ target.advanceClock(10);
+ target.drainBattery(10_000);
+
+ reset(mMetricsLogger);
+ target.startCharging();
+
+ assertMetricsLog(BatterySavingStats.COUNTER_POWER_MILLIAMPS_PREFIX + "11", 10);
+ assertMetricsLog(BatterySavingStats.COUNTER_TIME_SECONDS_PREFIX + "11", 60 * 10);
+
+ target.advanceClock(1);
+ target.drainBattery(2000);
+
+ reset(mMetricsLogger);
+ target.transitionState(
+ BatterySaverState.ON,
+ InteractiveState.NON_INTERACTIVE,
+ DozeState.NOT_DOZING);
+
+ verify(mMetricsLogger, times(0)).count(anyString(), anyInt());
+
+ target.advanceClock(1);
+ target.drainBattery(2000);
+
+ target.startCharging();
+
+ assertMetricsLog(BatterySavingStats.COUNTER_POWER_MILLIAMPS_PREFIX + "10", 2);
+ assertMetricsLog(BatterySavingStats.COUNTER_TIME_SECONDS_PREFIX + "10", 60);
+ }
}
diff --git a/services/tests/uiservicestests/AndroidManifest.xml b/services/tests/uiservicestests/AndroidManifest.xml
index 3475572..aabf9ea 100644
--- a/services/tests/uiservicestests/AndroidManifest.xml
+++ b/services/tests/uiservicestests/AndroidManifest.xml
@@ -26,6 +26,7 @@
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.STATUS_BAR_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_VOICE_INTERACTION_SERVICE" />
+ <uses-permission android:name="android.permission.DEVICE_POWER" />
<application>
<uses-library android:name="android.test.runner" />
diff --git a/services/tests/uiservicestests/src/com/android/server/slice/SliceFullAccessListTest.java b/services/tests/uiservicestests/src/com/android/server/slice/SliceFullAccessListTest.java
new file mode 100644
index 0000000..7c14d08
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/slice/SliceFullAccessListTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2018 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.slice;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.support.test.filters.SmallTest;
+import android.util.Xml.Encoding;
+
+import com.android.server.UiServiceTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+@SmallTest
+public class SliceFullAccessListTest extends UiServiceTestCase {
+
+ private static final String TEST_XML = "<slice-access-list version=\"1\"><user "
+ + "user=\"0\"><pkg>pkg</pkg><pkg>pkg1</pkg></user><user "
+ + "user=\"1\"><pkg>pkg</pkg></user><user "
+ + "user=\"3\"><pkg>pkg2</pkg></user></slice-access-list>";
+
+ private SliceFullAccessList mAccessList;
+
+ @Before
+ public void setup() {
+ mAccessList = new SliceFullAccessList(mContext);
+ }
+
+ @Test
+ public void testNoDefaultAccess() {
+ assertFalse(mAccessList.hasFullAccess("pkg", 0));
+ }
+
+ @Test
+ public void testGrantAccess() {
+ mAccessList.grantFullAccess("pkg", 0);
+ assertTrue(mAccessList.hasFullAccess("pkg", 0));
+ }
+
+ @Test
+ public void testUserSeparation() {
+ mAccessList.grantFullAccess("pkg", 1);
+ assertFalse(mAccessList.hasFullAccess("pkg", 0));
+ }
+
+ @Test
+ public void testSerialization() throws XmlPullParserException, IOException {
+ mAccessList.grantFullAccess("pkg", 0);
+ mAccessList.grantFullAccess("pkg1", 0);
+ mAccessList.grantFullAccess("pkg", 1);
+ mAccessList.grantFullAccess("pkg2", 3);
+
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ XmlSerializer out = XmlPullParserFactory.newInstance().newSerializer();
+ out.setOutput(output, Encoding.UTF_8.name());
+ mAccessList.writeXml(out);
+ out.flush();
+
+ assertEquals(TEST_XML, output.toString(Encoding.UTF_8.name()));
+ }
+
+ @Test
+ public void testDeSerialization() throws XmlPullParserException, IOException {
+ ByteArrayInputStream input = new ByteArrayInputStream(TEST_XML.getBytes());
+ XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+ parser.setInput(input, Encoding.UTF_8.name());
+
+ mAccessList.readXml(parser);
+
+ assertTrue(mAccessList.hasFullAccess("pkg", 0));
+ assertTrue(mAccessList.hasFullAccess("pkg1", 0));
+ assertTrue(mAccessList.hasFullAccess("pkg", 1));
+ assertTrue(mAccessList.hasFullAccess("pkg2", 3));
+
+ assertFalse(mAccessList.hasFullAccess("pkg3", 0));
+ assertFalse(mAccessList.hasFullAccess("pkg1", 1));
+ assertFalse(mAccessList.hasFullAccess("pkg", 3));
+ assertFalse(mAccessList.hasFullAccess("pkg", 2));
+ }
+}
\ No newline at end of file
diff --git a/telephony/java/android/telephony/CellSignalStrengthCdma.java b/telephony/java/android/telephony/CellSignalStrengthCdma.java
index 0474362..dfaaab9 100644
--- a/telephony/java/android/telephony/CellSignalStrengthCdma.java
+++ b/telephony/java/android/telephony/CellSignalStrengthCdma.java
@@ -20,6 +20,8 @@
import android.os.Parcelable;
import android.telephony.Rlog;
+import java.util.Objects;
+
/**
* Signal strength related information.
*/
@@ -293,9 +295,7 @@
@Override
public int hashCode() {
- int primeNum = 31;
- return ((mCdmaDbm * primeNum) + (mCdmaEcio * primeNum)
- + (mEvdoDbm * primeNum) + (mEvdoEcio * primeNum) + (mEvdoSnr * primeNum));
+ return Objects.hash(mCdmaDbm, mCdmaEcio, mEvdoDbm, mEvdoEcio, mEvdoSnr);
}
@Override
diff --git a/telephony/java/android/telephony/CellSignalStrengthGsm.java b/telephony/java/android/telephony/CellSignalStrengthGsm.java
index 4137853..f68d2ca 100644
--- a/telephony/java/android/telephony/CellSignalStrengthGsm.java
+++ b/telephony/java/android/telephony/CellSignalStrengthGsm.java
@@ -20,6 +20,8 @@
import android.os.Parcelable;
import android.telephony.Rlog;
+import java.util.Objects;
+
/**
* GSM signal strength related information.
*/
@@ -185,8 +187,7 @@
@Override
public int hashCode() {
- int primeNum = 31;
- return (mSignalStrength * primeNum) + (mBitErrorRate * primeNum);
+ return Objects.hash(mSignalStrength, mBitErrorRate, mTimingAdvance);
}
@Override
diff --git a/telephony/java/android/telephony/CellSignalStrengthLte.java b/telephony/java/android/telephony/CellSignalStrengthLte.java
index 0d07a40..6ffc8b6 100644
--- a/telephony/java/android/telephony/CellSignalStrengthLte.java
+++ b/telephony/java/android/telephony/CellSignalStrengthLte.java
@@ -20,6 +20,8 @@
import android.os.Parcelable;
import android.telephony.Rlog;
+import java.util.Objects;
+
/**
* LTE signal strength related information.
*/
@@ -231,10 +233,7 @@
@Override
public int hashCode() {
- int primeNum = 31;
- return (mSignalStrength * primeNum) + (mRsrp * primeNum)
- + (mRsrq * primeNum) + (mRssnr * primeNum) + (mCqi * primeNum)
- + (mTimingAdvance * primeNum);
+ return Objects.hash(mSignalStrength, mRsrp, mRsrq, mRssnr, mCqi, mTimingAdvance);
}
@Override
diff --git a/telephony/java/android/telephony/CellSignalStrengthWcdma.java b/telephony/java/android/telephony/CellSignalStrengthWcdma.java
index b94b01d..2cd56b8 100644
--- a/telephony/java/android/telephony/CellSignalStrengthWcdma.java
+++ b/telephony/java/android/telephony/CellSignalStrengthWcdma.java
@@ -20,6 +20,8 @@
import android.os.Parcelable;
import android.telephony.Rlog;
+import java.util.Objects;
+
/**
* Wcdma signal strength related information.
*/
@@ -156,8 +158,7 @@
@Override
public int hashCode() {
- int primeNum = 31;
- return (mSignalStrength * primeNum) + (mBitErrorRate * primeNum);
+ return Objects.hash(mSignalStrength, mBitErrorRate);
}
@Override
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 0239fcf..5c290da 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -4463,7 +4463,7 @@
try {
ITelephony telephony = getITelephony();
if (telephony != null)
- return telephony.iccTransmitApduBasicChannel(subId, cla,
+ return telephony.iccTransmitApduBasicChannel(subId, getOpPackageName(), cla,
instruction, p1, p2, p3, data);
} catch (RemoteException ex) {
} catch (NullPointerException ex) {
@@ -4952,28 +4952,6 @@
}
}
- /**
- * Returns the response of ISIM Authetification through RIL.
- * Returns null if the Authentification hasn't been successed or isn't present iphonesubinfo.
- * @return the response of ISIM Authetification, or null if not available
- * @hide
- * @deprecated
- * @see getIccAuthentication with appType=PhoneConstants.APPTYPE_ISIM
- */
- public String getIsimChallengeResponse(String nonce){
- try {
- IPhoneSubInfo info = getSubscriberInfo();
- if (info == null)
- return null;
- return info.getIsimChallengeResponse(nonce);
- } catch (RemoteException ex) {
- return null;
- } catch (NullPointerException ex) {
- // This could happen before phone restarts due to crashing
- return null;
- }
- }
-
// ICC SIM Application Types
/** UICC application type is SIM */
public static final int APPTYPE_SIM = PhoneConstants.APPTYPE_SIM;
diff --git a/telephony/java/android/telephony/data/DataCallResponse.java b/telephony/java/android/telephony/data/DataCallResponse.java
index ef3a183..25f5133 100644
--- a/telephony/java/android/telephony/data/DataCallResponse.java
+++ b/telephony/java/android/telephony/data/DataCallResponse.java
@@ -27,6 +27,7 @@
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
/**
* Description of the response of a setup data call connection request.
@@ -220,17 +221,8 @@
@Override
public int hashCode() {
- return mStatus * 31
- + mSuggestedRetryTime * 37
- + mCid * 41
- + mActive * 43
- + mType.hashCode() * 47
- + mIfname.hashCode() * 53
- + mAddresses.hashCode() * 59
- + mDnses.hashCode() * 61
- + mGateways.hashCode() * 67
- + mPcscfs.hashCode() * 71
- + mMtu * 73;
+ return Objects.hash(mStatus, mSuggestedRetryTime, mCid, mActive, mType, mIfname, mAddresses,
+ mDnses, mGateways, mPcscfs, mMtu);
}
@Override
diff --git a/telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl b/telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl
index f8a040d..0ed0820 100644
--- a/telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl
+++ b/telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl
@@ -202,14 +202,6 @@
String[] getIsimPcscf(int subId);
/**
- * TODO: Deprecate and remove this interface. Superceded by getIccsimChallengeResponse.
- * Returns the response of ISIM Authetification through RIL.
- * @return the response of ISIM Authetification, or null if
- * the Authentification hasn't been successed or isn't present iphonesubinfo.
- */
- String getIsimChallengeResponse(String nonce);
-
- /**
* Returns the response of the SIM application on the UICC to authentication
* challenge/response algorithm. The data string and challenge response are
* Base64 encoded Strings.
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 24c497c..2b4c059 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -681,6 +681,7 @@
* Input parameters equivalent to TS 27.007 AT+CSIM command.
*
* @param subId The subscription to use.
+ * @param callingPackage the name of the package making the call.
* @param cla Class of the APDU command.
* @param instruction Instruction of the APDU command.
* @param p1 P1 value of the APDU command.
@@ -691,7 +692,7 @@
* @return The APDU response from the ICC card with the status appended at
* the end.
*/
- String iccTransmitApduBasicChannel(int subId, int cla, int instruction,
+ String iccTransmitApduBasicChannel(int subId, String callingPackage, int cla, int instruction,
int p1, int p2, int p3, String data);
/**
diff --git a/test-base/Android.bp b/test-base/Android.bp
index ccf57b0..62fed61 100644
--- a/test-base/Android.bp
+++ b/test-base/Android.bp
@@ -83,28 +83,3 @@
"junit",
],
}
-
-// Build the legacy-android-test library
-// =====================================
-// This contains the android.test classes that were in Android API level 25,
-// including those from android.test.runner.
-// Also contains the com.android.internal.util.Predicate[s] classes.
-java_library_static {
- name: "legacy-android-test",
-
- srcs: [
- "src/android/**/*.java",
- "src/com/**/*.java",
- ],
-
- static_libs: [
- "android.test.runner-minus-junit",
- "android.test.mock",
- ],
-
- no_framework_libs: true,
- libs: [
- "framework",
- "junit",
- ],
-}
diff --git a/test-legacy/Android.bp b/test-legacy/Android.bp
new file mode 100644
index 0000000..d2af8a9
--- /dev/null
+++ b/test-legacy/Android.bp
@@ -0,0 +1,36 @@
+//
+// Copyright (C) 2018 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.
+//
+
+// Build the legacy-android-test library
+// =====================================
+// This contains the android.test classes that were in Android API level 25,
+// including those from android.test.runner.
+// Also contains the com.android.internal.util.Predicate[s] classes.
+java_library_static {
+ name: "legacy-android-test",
+
+ static_libs: [
+ "android.test.base-minus-junit",
+ "android.test.runner-minus-junit",
+ "android.test.mock",
+ ],
+
+ no_framework_libs: true,
+ libs: [
+ "framework",
+ "junit",
+ ],
+}
diff --git a/test-legacy/Android.mk b/test-legacy/Android.mk
new file mode 100644
index 0000000..b8c5326
--- /dev/null
+++ b/test-legacy/Android.mk
@@ -0,0 +1,40 @@
+#
+# Copyright (C) 2018 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)
+
+# For unbundled build we'll use the prebuilt jar from prebuilts/sdk.
+ifeq (,$(TARGET_BUILD_APPS)$(filter true,$(TARGET_BUILD_PDK)))
+
+# Build the android.test.legacy library
+# =====================================
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := android.test.legacy
+
+LOCAL_SDK_VERSION := current
+
+LOCAL_JAVA_LIBRARIES := junit
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ android.test.base-minus-junit \
+ android.test.runner-minus-junit \
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+# Archive a copy of the classes.jar in SDK build.
+$(call dist-for-goals,sdk win_sdk,$(full_classes_jar):android.test.legacy.jar)
+
+endif # not TARGET_BUILD_APPS not TARGET_BUILD_PDK=true
diff --git a/test-runner/Android.mk b/test-runner/Android.mk
index 229a6ac..706f636 100644
--- a/test-runner/Android.mk
+++ b/test-runner/Android.mk
@@ -117,23 +117,5 @@
endif # not TARGET_BUILD_APPS not TARGET_BUILD_PDK=true
-# Build the android.test.legacy library
-# =====================================
-include $(CLEAR_VARS)
-
-LOCAL_MODULE := android.test.legacy
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src/android)
-
-LOCAL_SDK_VERSION := current
-
-LOCAL_JAVA_LIBRARIES := android.test.mock.stubs junit
-LOCAL_STATIC_JAVA_LIBRARIES := android.test.base-minus-junit
-
-include $(BUILD_STATIC_JAVA_LIBRARY)
-
-# Archive a copy of the classes.jar in SDK build.
-$(call dist-for-goals,sdk win_sdk,$(full_classes_jar):android.test.legacy.jar)
-
# additionally, build unit tests in a separate .apk
include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/tools/aapt2/Resources.proto b/tools/aapt2/Resources.proto
index 069360e..df483b2 100644
--- a/tools/aapt2/Resources.proto
+++ b/tools/aapt2/Resources.proto
@@ -318,7 +318,7 @@
float dimension_value = 4;
float fraction_value = 5;
int32 int_decimal_value = 6;
- uint32 int_hexidecimal_value = 7;
+ uint32 int_hexadecimal_value = 7;
bool boolean_value = 8;
uint32 color_argb8_value = 9;
uint32 color_rgb8_value = 10;
diff --git a/tools/aapt2/format/proto/ProtoDeserialize.cpp b/tools/aapt2/format/proto/ProtoDeserialize.cpp
index 81bc2c8..f1eb952 100644
--- a/tools/aapt2/format/proto/ProtoDeserialize.cpp
+++ b/tools/aapt2/format/proto/ProtoDeserialize.cpp
@@ -792,9 +792,9 @@
val.dataType = android::Res_value::TYPE_INT_DEC;
val.data = static_cast<uint32_t>(pb_prim.int_decimal_value());
} break;
- case pb::Primitive::kIntHexidecimalValue: {
+ case pb::Primitive::kIntHexadecimalValue: {
val.dataType = android::Res_value::TYPE_INT_HEX;
- val.data = pb_prim.int_hexidecimal_value();
+ val.data = pb_prim.int_hexadecimal_value();
} break;
case pb::Primitive::kBooleanValue: {
val.dataType = android::Res_value::TYPE_INT_BOOLEAN;
diff --git a/tools/aapt2/format/proto/ProtoSerialize.cpp b/tools/aapt2/format/proto/ProtoSerialize.cpp
index e9622f5..1d00852 100644
--- a/tools/aapt2/format/proto/ProtoSerialize.cpp
+++ b/tools/aapt2/format/proto/ProtoSerialize.cpp
@@ -460,7 +460,7 @@
pb_prim->set_int_decimal_value(static_cast<int32_t>(val.data));
} break;
case android::Res_value::TYPE_INT_HEX: {
- pb_prim->set_int_hexidecimal_value(val.data);
+ pb_prim->set_int_hexadecimal_value(val.data);
} break;
case android::Res_value::TYPE_INT_BOOLEAN: {
pb_prim->set_boolean_value(static_cast<bool>(val.data));
diff --git a/wifi/java/android/net/wifi/aware/DiscoverySession.java b/wifi/java/android/net/wifi/aware/DiscoverySession.java
index 9f73622..699f54c 100644
--- a/wifi/java/android/net/wifi/aware/DiscoverySession.java
+++ b/wifi/java/android/net/wifi/aware/DiscoverySession.java
@@ -22,6 +22,8 @@
import android.net.NetworkSpecifier;
import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
+
import dalvik.system.CloseGuard;
import java.lang.ref.WeakReference;
@@ -142,6 +144,34 @@
}
/**
+ * Access the client ID of the Aware session.
+ *
+ * Note: internal visibility for testing.
+ *
+ * @return The internal client ID.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ public int getClientId() {
+ return mClientId;
+ }
+
+ /**
+ * Access the discovery session ID of the Aware session.
+ *
+ * Note: internal visibility for testing.
+ *
+ * @return The internal discovery session ID.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ public int getSessionId() {
+ return mSessionId;
+ }
+
+ /**
* Sends a message to the specified destination. Aware messages are transmitted in the context
* of a discovery session - executed subsequent to a publish/subscribe
* {@link DiscoverySessionCallback#onServiceDiscovered(PeerHandle,
@@ -246,8 +276,7 @@
* or
* {@link DiscoverySessionCallback#onMessageReceived(PeerHandle, byte[])}.
* On a RESPONDER this value is used to gate the acceptance of a connection
- * request from only that peer. A RESPONDER may specify a {@code null} -
- * indicating that it will accept connection requests from any device.
+ * request from only that peer.
*
* @return A {@link NetworkSpecifier} to be used to construct
* {@link android.net.NetworkRequest.Builder#setNetworkSpecifier(NetworkSpecifier)} to pass to
@@ -255,7 +284,7 @@
* android.net.ConnectivityManager.NetworkCallback)}
* [or other varieties of that API].
*/
- public NetworkSpecifier createNetworkSpecifierOpen(@Nullable PeerHandle peerHandle) {
+ public NetworkSpecifier createNetworkSpecifierOpen(@NonNull PeerHandle peerHandle) {
if (mTerminated) {
Log.w(TAG, "createNetworkSpecifierOpen: called on terminated session");
return null;
@@ -295,8 +324,7 @@
* byte[], java.util.List)} or
* {@link DiscoverySessionCallback#onMessageReceived(PeerHandle,
* byte[])}. On a RESPONDER this value is used to gate the acceptance of a connection request
- * from only that peer. A RESPONDER may specify a {@code null} - indicating
- * that it will accept connection requests from any device.
+ * from only that peer.
* @param passphrase The passphrase to be used to encrypt the link. The PMK is generated from
* the passphrase. Use the
* {@link #createNetworkSpecifierOpen(PeerHandle)} API to
@@ -309,7 +337,7 @@
* [or other varieties of that API].
*/
public NetworkSpecifier createNetworkSpecifierPassphrase(
- @Nullable PeerHandle peerHandle, @NonNull String passphrase) {
+ @NonNull PeerHandle peerHandle, @NonNull String passphrase) {
if (!WifiAwareUtils.validatePassphrase(passphrase)) {
throw new IllegalArgumentException("Passphrase must meet length requirements");
}
@@ -354,8 +382,7 @@
* byte[], java.util.List)} or
* {@link DiscoverySessionCallback#onMessageReceived(PeerHandle,
* byte[])}. On a RESPONDER this value is used to gate the acceptance of a connection request
- * from only that peer. A RESPONDER may specify a null - indicating that
- * it will accept connection requests from any device.
+ * from only that peer.
* @param pmk A PMK (pairwise master key, see IEEE 802.11i) specifying the key to use for
* encrypting the data-path. Use the
* {@link #createNetworkSpecifierPassphrase(PeerHandle, String)} to specify a
@@ -371,7 +398,7 @@
* @hide
*/
@SystemApi
- public NetworkSpecifier createNetworkSpecifierPmk(@Nullable PeerHandle peerHandle,
+ public NetworkSpecifier createNetworkSpecifierPmk(@NonNull PeerHandle peerHandle,
@NonNull byte[] pmk) {
if (!WifiAwareUtils.validatePmk(pmk)) {
throw new IllegalArgumentException("PMK must 32 bytes");
diff --git a/wifi/java/android/net/wifi/aware/WifiAwareManager.java b/wifi/java/android/net/wifi/aware/WifiAwareManager.java
index 2f0c316..06a5c2e 100644
--- a/wifi/java/android/net/wifi/aware/WifiAwareManager.java
+++ b/wifi/java/android/net/wifi/aware/WifiAwareManager.java
@@ -406,7 +406,7 @@
/** @hide */
public NetworkSpecifier createNetworkSpecifier(int clientId, int role, int sessionId,
- PeerHandle peerHandle, @Nullable byte[] pmk, @Nullable String passphrase) {
+ @NonNull PeerHandle peerHandle, @Nullable byte[] pmk, @Nullable String passphrase) {
if (VDBG) {
Log.v(TAG, "createNetworkSpecifier: role=" + role + ", sessionId=" + sessionId
+ ", peerHandle=" + ((peerHandle == null) ? peerHandle : peerHandle.peerId)
@@ -420,12 +420,9 @@
"createNetworkSpecifier: Invalid 'role' argument when creating a network "
+ "specifier");
}
- if (role == WIFI_AWARE_DATA_PATH_ROLE_INITIATOR) {
- if (peerHandle == null) {
- throw new IllegalArgumentException(
- "createNetworkSpecifier: Invalid peer handle (value of null) - not "
- + "permitted on INITIATOR");
- }
+ if (peerHandle == null) {
+ throw new IllegalArgumentException(
+ "createNetworkSpecifier: Invalid peer handle - cannot be null");
}
return new WifiAwareNetworkSpecifier(
@@ -443,7 +440,7 @@
/** @hide */
public NetworkSpecifier createNetworkSpecifier(int clientId, @DataPathRole int role,
- @Nullable byte[] peer, @Nullable byte[] pmk, @Nullable String passphrase) {
+ @NonNull byte[] peer, @Nullable byte[] pmk, @Nullable String passphrase) {
if (VDBG) {
Log.v(TAG, "createNetworkSpecifier: role=" + role
+ ", pmk=" + ((pmk == null) ? "null" : "non-null")
@@ -456,11 +453,9 @@
"createNetworkSpecifier: Invalid 'role' argument when creating a network "
+ "specifier");
}
- if (role == WIFI_AWARE_DATA_PATH_ROLE_INITIATOR) {
- if (peer == null) {
- throw new IllegalArgumentException("createNetworkSpecifier: Invalid peer MAC "
- + "address - null not permitted on INITIATOR");
- }
+ if (peer == null) {
+ throw new IllegalArgumentException(
+ "createNetworkSpecifier: Invalid peer MAC - cannot be null");
}
if (peer != null && peer.length != 6) {
throw new IllegalArgumentException("createNetworkSpecifier: Invalid peer MAC address");
diff --git a/wifi/java/android/net/wifi/aware/WifiAwareSession.java b/wifi/java/android/net/wifi/aware/WifiAwareSession.java
index f26b9f5..3219653 100644
--- a/wifi/java/android/net/wifi/aware/WifiAwareSession.java
+++ b/wifi/java/android/net/wifi/aware/WifiAwareSession.java
@@ -25,6 +25,8 @@
import android.os.Looper;
import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
+
import dalvik.system.CloseGuard;
import java.lang.ref.WeakReference;
@@ -97,6 +99,20 @@
}
/**
+ * Access the client ID of the Aware session.
+ *
+ * Note: internal visibility for testing.
+ *
+ * @return The internal client ID.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ public int getClientId() {
+ return mClientId;
+ }
+
+ /**
* Issue a request to the Aware service to create a new Aware publish discovery session, using
* the specified {@code publishConfig} configuration. The results of the publish operation
* are routed to the callbacks of {@link DiscoverySessionCallback}:
@@ -207,8 +223,7 @@
* {@link WifiAwareManager#WIFI_AWARE_DATA_PATH_ROLE_RESPONDER}
* @param peer The MAC address of the peer's Aware discovery interface. On a RESPONDER this
* value is used to gate the acceptance of a connection request from only that
- * peer. A RESPONDER may specify a {@code null} - indicating that it will accept
- * connection requests from any device.
+ * peer.
*
* @return A {@link NetworkSpecifier} to be used to construct
* {@link android.net.NetworkRequest.Builder#setNetworkSpecifier(NetworkSpecifier)} to pass to
@@ -217,7 +232,7 @@
* [or other varieties of that API].
*/
public NetworkSpecifier createNetworkSpecifierOpen(
- @WifiAwareManager.DataPathRole int role, @Nullable byte[] peer) {
+ @WifiAwareManager.DataPathRole int role, @NonNull byte[] peer) {
WifiAwareManager mgr = mMgr.get();
if (mgr == null) {
Log.e(TAG, "createNetworkSpecifierOpen: called post GC on WifiAwareManager");
@@ -246,8 +261,7 @@
* {@link WifiAwareManager#WIFI_AWARE_DATA_PATH_ROLE_RESPONDER}
* @param peer The MAC address of the peer's Aware discovery interface. On a RESPONDER this
* value is used to gate the acceptance of a connection request from only that
- * peer. A RESPONDER may specify a {@code null} - indicating that it will accept
- * connection requests from any device.
+ * peer.
* @param passphrase The passphrase to be used to encrypt the link. The PMK is generated from
* the passphrase. Use {@link #createNetworkSpecifierOpen(int, byte[])} to
* specify an open (unencrypted) link.
@@ -259,7 +273,7 @@
* [or other varieties of that API].
*/
public NetworkSpecifier createNetworkSpecifierPassphrase(
- @WifiAwareManager.DataPathRole int role, @Nullable byte[] peer,
+ @WifiAwareManager.DataPathRole int role, @NonNull byte[] peer,
@NonNull String passphrase) {
WifiAwareManager mgr = mMgr.get();
if (mgr == null) {
@@ -293,8 +307,7 @@
* {@link WifiAwareManager#WIFI_AWARE_DATA_PATH_ROLE_RESPONDER}
* @param peer The MAC address of the peer's Aware discovery interface. On a RESPONDER this
* value is used to gate the acceptance of a connection request from only that
- * peer. A RESPONDER may specify a null - indicating that it will accept
- * connection requests from any device.
+ * peer.
* @param pmk A PMK (pairwise master key, see IEEE 802.11i) specifying the key to use for
* encrypting the data-path. Use the
* {@link #createNetworkSpecifierPassphrase(int, byte[], String)} to specify a
@@ -311,7 +324,7 @@
*/
@SystemApi
public NetworkSpecifier createNetworkSpecifierPmk(
- @WifiAwareManager.DataPathRole int role, @Nullable byte[] peer, @NonNull byte[] pmk) {
+ @WifiAwareManager.DataPathRole int role, @NonNull byte[] peer, @NonNull byte[] pmk) {
WifiAwareManager mgr = mMgr.get();
if (mgr == null) {
Log.e(TAG, "createNetworkSpecifierPmk: called post GC on WifiAwareManager");
diff --git a/wifi/tests/src/android/net/wifi/aware/WifiAwareManagerTest.java b/wifi/tests/src/android/net/wifi/aware/WifiAwareManagerTest.java
index b0a048d..84e3ed9 100644
--- a/wifi/tests/src/android/net/wifi/aware/WifiAwareManagerTest.java
+++ b/wifi/tests/src/android/net/wifi/aware/WifiAwareManagerTest.java
@@ -1022,7 +1022,7 @@
*/
@Test(expected = IllegalArgumentException.class)
public void testNetworkSpecifierWithClientNullPmk() throws Exception {
- executeNetworkSpecifierWithClient(true, null, null);
+ executeNetworkSpecifierWithClient(new PeerHandle(123412), true, null, null);
}
/**
@@ -1030,7 +1030,7 @@
*/
@Test(expected = IllegalArgumentException.class)
public void testNetworkSpecifierWithClientIncorrectLengthPmk() throws Exception {
- executeNetworkSpecifierWithClient(true, "012".getBytes(), null);
+ executeNetworkSpecifierWithClient(new PeerHandle(123412), true, "012".getBytes(), null);
}
/**
@@ -1038,7 +1038,7 @@
*/
@Test(expected = IllegalArgumentException.class)
public void testNetworkSpecifierWithClientNullPassphrase() throws Exception {
- executeNetworkSpecifierWithClient(false, null, null);
+ executeNetworkSpecifierWithClient(new PeerHandle(123412), false, null, null);
}
/**
@@ -1046,7 +1046,7 @@
*/
@Test(expected = IllegalArgumentException.class)
public void testNetworkSpecifierWithClientShortPassphrase() throws Exception {
- executeNetworkSpecifierWithClient(false, null, "012");
+ executeNetworkSpecifierWithClient(new PeerHandle(123412), false, null, "012");
}
/**
@@ -1054,15 +1054,23 @@
*/
@Test(expected = IllegalArgumentException.class)
public void testNetworkSpecifierWithClientLongPassphrase() throws Exception {
- executeNetworkSpecifierWithClient(false, null,
+ executeNetworkSpecifierWithClient(new PeerHandle(123412), false, null,
"0123456789012345678901234567890123456789012345678901234567890123456789");
}
- private void executeNetworkSpecifierWithClient(boolean doPmk, byte[] pmk, String passphrase)
- throws Exception {
+ /**
+ * Validate that a null PeerHandle triggers an exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testNetworkSpecifierWithClientNullPeer() throws Exception {
+ executeNetworkSpecifierWithClient(null, false, null,
+ "0123456789012345678901234567890123456789012345678901234567890123456789");
+ }
+
+ private void executeNetworkSpecifierWithClient(PeerHandle peerHandle, boolean doPmk, byte[] pmk,
+ String passphrase) throws Exception {
final int clientId = 4565;
final int sessionId = 123;
- final PeerHandle peerHandle = new PeerHandle(123412);
final ConfigRequest configRequest = new ConfigRequest.Builder().build();
final PublishConfig publishConfig = new PublishConfig.Builder().build();
@@ -1108,7 +1116,8 @@
*/
@Test(expected = IllegalArgumentException.class)
public void testNetworkSpecifierDirectNullPmk() throws Exception {
- executeNetworkSpecifierDirect(true, null, null);
+ executeNetworkSpecifierDirect(HexEncoding.decode("000102030405".toCharArray(), false), true,
+ null, null);
}
/**
@@ -1116,7 +1125,8 @@
*/
@Test(expected = IllegalArgumentException.class)
public void testNetworkSpecifierDirectIncorrectLengthPmk() throws Exception {
- executeNetworkSpecifierDirect(true, "012".getBytes(), null);
+ executeNetworkSpecifierDirect(HexEncoding.decode("000102030405".toCharArray(), false), true,
+ "012".getBytes(), null);
}
/**
@@ -1124,7 +1134,8 @@
*/
@Test(expected = IllegalArgumentException.class)
public void testNetworkSpecifierDirectNullPassphrase() throws Exception {
- executeNetworkSpecifierDirect(false, null, null);
+ executeNetworkSpecifierDirect(HexEncoding.decode("000102030405".toCharArray(), false),
+ false, null, null);
}
/**
@@ -1132,7 +1143,8 @@
*/
@Test(expected = IllegalArgumentException.class)
public void testNetworkSpecifierDirectShortPassphrase() throws Exception {
- executeNetworkSpecifierDirect(false, null, "012");
+ executeNetworkSpecifierDirect(HexEncoding.decode("000102030405".toCharArray(), false),
+ false, null, "012");
}
/**
@@ -1140,14 +1152,22 @@
*/
@Test(expected = IllegalArgumentException.class)
public void testNetworkSpecifierDirectLongPassphrase() throws Exception {
- executeNetworkSpecifierDirect(false, null,
+ executeNetworkSpecifierDirect(HexEncoding.decode("000102030405".toCharArray(), false),
+ false, null,
"0123456789012345678901234567890123456789012345678901234567890123456789");
}
- private void executeNetworkSpecifierDirect(boolean doPmk, byte[] pmk, String passphrase)
- throws Exception {
+ /**
+ * Validate that a null peer MAC triggers an exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testNetworkSpecifierDirectNullPeer() throws Exception {
+ executeNetworkSpecifierDirect(null, false, null, null);
+ }
+
+ private void executeNetworkSpecifierDirect(byte[] someMac, boolean doPmk, byte[] pmk,
+ String passphrase) throws Exception {
final int clientId = 134;
- final byte[] someMac = HexEncoding.decode("000102030405".toCharArray(), false);
final int role = WifiAwareManager.WIFI_AWARE_DATA_PATH_ROLE_INITIATOR;
final ConfigRequest configRequest = new ConfigRequest.Builder().build();