diff options
362 files changed, 14507 insertions, 4706 deletions
diff --git a/Android.bp b/Android.bp index db0f0eae6658..694bf26d6fba 100644 --- a/Android.bp +++ b/Android.bp @@ -27,6 +27,7 @@ java_library { name: "framework", + installable: true, srcs: [ // From build/make/core/pathmap.mk FRAMEWORK_BASE_SUBDIRS @@ -706,6 +707,7 @@ java_library { // specified on the build command line. java_library { name: "framework-oahl-backward-compatibility", + installable: true, srcs: [ "core/java/android/content/pm/OrgApacheHttpLegacyUpdater.java", ], @@ -761,6 +763,7 @@ gensrcs { // ============================================================ java_library { name: "ext", + installable: true, no_framework_libs: true, static_libs: [ "libphonenumber-platform", @@ -1192,6 +1195,24 @@ droiddoc { " -showAnnotation android.annotation.TestApi", } +droiddoc { + name: "hiddenapi-mappings", + defaults: ["framework-docs-default"], + arg_files: [ + "core/res/AndroidManifest.xml", + ":api-version-xml", + "core/java/overview.html", + ":current-support-api", + ], + dex_mapping_filename: "dex-mapping.txt", + args: framework_docs_args + + " -referenceonly" + + " -nodocs" + + " -showUnannotated" + + " -showAnnotation android.annotation.SystemApi" + + " -showAnnotation android.annotation.TestApi", +} + filegroup { name: "apache-http-stubs-sources", srcs: [ diff --git a/apct-tests/perftests/core/src/android/text/BoringLayoutCreateDrawPerfTest.java b/apct-tests/perftests/core/src/android/text/BoringLayoutCreateDrawPerfTest.java index 586c3852325a..64f2800ee112 100644 --- a/apct-tests/perftests/core/src/android/text/BoringLayoutCreateDrawPerfTest.java +++ b/apct-tests/perftests/core/src/android/text/BoringLayoutCreateDrawPerfTest.java @@ -46,7 +46,7 @@ public class BoringLayoutCreateDrawPerfTest { private static final float SPACING_ADD = 10f; private static final float SPACING_MULT = 1.5f; - @Parameterized.Parameters(name = "cached={3},{1}chars,{0}") + @Parameterized.Parameters(name = "cached {3} {1}chars {0}") public static Collection cases() { final List<Object[]> params = new ArrayList<>(); for (int length : new int[]{128}) { diff --git a/apct-tests/perftests/core/src/android/text/BoringLayoutIsBoringPerfTest.java b/apct-tests/perftests/core/src/android/text/BoringLayoutIsBoringPerfTest.java index 9d11f29557d2..194a88c36bba 100644 --- a/apct-tests/perftests/core/src/android/text/BoringLayoutIsBoringPerfTest.java +++ b/apct-tests/perftests/core/src/android/text/BoringLayoutIsBoringPerfTest.java @@ -40,7 +40,7 @@ public class BoringLayoutIsBoringPerfTest { private static final boolean[] BOOLEANS = new boolean[]{false, true}; - @Parameterized.Parameters(name = "cached={4},{1}chars,{0}") + @Parameterized.Parameters(name = "cached {4} {1}chars {0}") public static Collection cases() { final List<Object[]> params = new ArrayList<>(); for (int length : new int[]{128}) { diff --git a/apct-tests/perftests/core/src/android/text/PaintMeasureDrawPerfTest.java b/apct-tests/perftests/core/src/android/text/PaintMeasureDrawPerfTest.java index 676879857491..ad5a34e44997 100644 --- a/apct-tests/perftests/core/src/android/text/PaintMeasureDrawPerfTest.java +++ b/apct-tests/perftests/core/src/android/text/PaintMeasureDrawPerfTest.java @@ -42,7 +42,7 @@ public class PaintMeasureDrawPerfTest { private static final boolean[] BOOLEANS = new boolean[]{false, true}; - @Parameterized.Parameters(name = "cached={1},{0}chars") + @Parameterized.Parameters(name = "cached {1} {0}chars") public static Collection cases() { final List<Object[]> params = new ArrayList<>(); for (int length : new int[]{128}) { diff --git a/apct-tests/perftests/core/src/android/text/StaticLayoutCreateDrawPerfTest.java b/apct-tests/perftests/core/src/android/text/StaticLayoutCreateDrawPerfTest.java index bfdb7589bdff..deb2b0a74aaa 100644 --- a/apct-tests/perftests/core/src/android/text/StaticLayoutCreateDrawPerfTest.java +++ b/apct-tests/perftests/core/src/android/text/StaticLayoutCreateDrawPerfTest.java @@ -50,7 +50,7 @@ public class StaticLayoutCreateDrawPerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); - @Parameterized.Parameters(name = "cached={3},{1}chars,{0}") + @Parameterized.Parameters(name = "cached {3} {1}chars {0}") public static Collection cases() { final List<Object[]> params = new ArrayList<>(); for (int length : new int[]{128}) { diff --git a/apct-tests/perftests/core/src/android/text/TextViewSetTextMeasurePerfTest.java b/apct-tests/perftests/core/src/android/text/TextViewSetTextMeasurePerfTest.java index ff2d57edb11b..c2898fa05c81 100644 --- a/apct-tests/perftests/core/src/android/text/TextViewSetTextMeasurePerfTest.java +++ b/apct-tests/perftests/core/src/android/text/TextViewSetTextMeasurePerfTest.java @@ -51,7 +51,7 @@ public class TextViewSetTextMeasurePerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); - @Parameterized.Parameters(name = "cached={3},{1}chars,{0}") + @Parameterized.Parameters(name = "cached {3} {1}chars {0}") public static Collection cases() { final List<Object[]> params = new ArrayList<>(); for (int length : new int[]{128}) { diff --git a/api/current.txt b/api/current.txt index bad0e79cbc54..e471086488fe 100644 --- a/api/current.txt +++ b/api/current.txt @@ -9137,6 +9137,7 @@ package android.content { method public android.os.Bundle call(java.lang.String, java.lang.String, android.os.Bundle) throws android.os.RemoteException; method public final android.net.Uri canonicalize(android.net.Uri) throws android.os.RemoteException; method public void close(); + method public static void closeQuietly(android.content.ContentProviderClient); method public int delete(android.net.Uri, java.lang.String, java.lang.String[]) throws android.os.RemoteException; method public android.content.ContentProvider getLocalContentProvider(); method public java.lang.String[] getStreamTypes(android.net.Uri, java.lang.String) throws android.os.RemoteException; @@ -32547,6 +32548,21 @@ package android.os { ctor public FileUriExposedException(java.lang.String); } + public class FileUtils { + method public static void closeQuietly(java.lang.AutoCloseable); + method public static void closeQuietly(java.io.FileDescriptor); + method public static long copy(java.io.File, java.io.File) throws java.io.IOException; + method public static long copy(java.io.File, java.io.File, android.os.CancellationSignal, java.util.concurrent.Executor, android.os.FileUtils.ProgressListener) throws java.io.IOException; + method public static long copy(java.io.InputStream, java.io.OutputStream) throws java.io.IOException; + method public static long copy(java.io.InputStream, java.io.OutputStream, android.os.CancellationSignal, java.util.concurrent.Executor, android.os.FileUtils.ProgressListener) throws java.io.IOException; + method public static long copy(java.io.FileDescriptor, java.io.FileDescriptor) throws java.io.IOException; + method public static long copy(java.io.FileDescriptor, java.io.FileDescriptor, android.os.CancellationSignal, java.util.concurrent.Executor, android.os.FileUtils.ProgressListener) throws java.io.IOException; + } + + public static abstract interface FileUtils.ProgressListener { + method public abstract void onProgress(long); + } + public class Handler { ctor public Handler(); ctor public Handler(android.os.Handler.Callback); @@ -42414,8 +42430,10 @@ package android.telephony { method public java.lang.CharSequence getDisplayName(); method public java.lang.String getIccId(); method public int getIconTint(); - method public int getMcc(); - method public int getMnc(); + method public deprecated int getMcc(); + method public java.lang.String getMccString(); + method public deprecated int getMnc(); + method public java.lang.String getMncString(); method public java.lang.String getNumber(); method public int getSimSlotIndex(); method public int getSubscriptionId(); @@ -42508,13 +42526,11 @@ package android.telephony { method public java.lang.String getIccAuthentication(int, int, java.lang.String); method public java.lang.String getImei(); method public java.lang.String getImei(int); - method public java.lang.String getTypeAllocationCode(); - method public java.lang.String getTypeAllocationCode(int); method public java.lang.String getLine1Number(); - method public java.lang.String getMeid(); - method public java.lang.String getMeid(int); method public java.lang.String getManufacturerCode(); method public java.lang.String getManufacturerCode(int); + method public java.lang.String getMeid(); + method public java.lang.String getMeid(int); method public java.lang.String getMmsUAProfUrl(); method public java.lang.String getMmsUserAgent(); method public java.lang.String getNai(); @@ -42537,6 +42553,8 @@ package android.telephony { method public int getSimState(); method public int getSimState(int); method public java.lang.String getSubscriberId(); + method public java.lang.String getTypeAllocationCode(); + method public java.lang.String getTypeAllocationCode(int); method public java.lang.String getVisualVoicemailPackageName(); method public java.lang.String getVoiceMailAlphaTag(); method public java.lang.String getVoiceMailNumber(); @@ -44749,6 +44767,7 @@ package android.text.util { public class Linkify { ctor public Linkify(); method public static final boolean addLinks(android.text.Spannable, int); + method public static final boolean addLinks(android.text.Spannable, int, android.text.util.Linkify.UrlSpanFactory); method public static final boolean addLinks(android.widget.TextView, int); method public static final void addLinks(android.widget.TextView, java.util.regex.Pattern, java.lang.String); method public static final void addLinks(android.widget.TextView, java.util.regex.Pattern, java.lang.String, android.text.util.Linkify.MatchFilter, android.text.util.Linkify.TransformFilter); @@ -44756,6 +44775,7 @@ package android.text.util { 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 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, android.text.util.Linkify.UrlSpanFactory); field public static final int ALL = 15; // 0xf field public static final int EMAIL_ADDRESSES = 2; // 0x2 field public static final deprecated int MAP_ADDRESSES = 8; // 0x8 @@ -44774,6 +44794,11 @@ package android.text.util { method public abstract java.lang.String transformUrl(java.util.regex.Matcher, java.lang.String); } + public static class Linkify.UrlSpanFactory { + ctor public Linkify.UrlSpanFactory(); + method public android.text.style.URLSpan create(java.lang.String); + } + public class Rfc822Token { ctor public Rfc822Token(java.lang.String, java.lang.String, java.lang.String); method public java.lang.String getAddress(); @@ -49395,18 +49420,18 @@ package android.view.accessibility { method public void setCheckable(boolean); method public void setChecked(boolean); method public void setClassName(java.lang.CharSequence); - method public void setClickable(boolean); + method public deprecated void setClickable(boolean); method public void setCollectionInfo(android.view.accessibility.AccessibilityNodeInfo.CollectionInfo); method public void setCollectionItemInfo(android.view.accessibility.AccessibilityNodeInfo.CollectionItemInfo); method public void setContentDescription(java.lang.CharSequence); method public void setContentInvalid(boolean); - method public void setContextClickable(boolean); - method public void setDismissable(boolean); + method public deprecated void setContextClickable(boolean); + method public deprecated void setDismissable(boolean); method public void setDrawingOrder(int); method public void setEditable(boolean); method public void setEnabled(boolean); method public void setError(java.lang.CharSequence); - method public void setFocusable(boolean); + method public deprecated void setFocusable(boolean); method public void setFocused(boolean); method public void setHeading(boolean); method public void setHintText(java.lang.CharSequence); @@ -49417,7 +49442,7 @@ package android.view.accessibility { method public void setLabeledBy(android.view.View); method public void setLabeledBy(android.view.View, int); method public void setLiveRegion(int); - method public void setLongClickable(boolean); + method public deprecated void setLongClickable(boolean); method public void setMaxTextLength(int); method public void setMovementGranularities(int); method public void setMultiLine(boolean); @@ -49428,7 +49453,7 @@ package android.view.accessibility { method public void setPassword(boolean); method public void setRangeInfo(android.view.accessibility.AccessibilityNodeInfo.RangeInfo); method public void setScreenReaderFocusable(boolean); - method public void setScrollable(boolean); + method public deprecated void setScrollable(boolean); method public void setSelected(boolean); method public void setShowingHintText(boolean); method public void setSource(android.view.View); diff --git a/api/system-current.txt b/api/system-current.txt index 049a9d2bf9ed..7c0d95859275 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -4664,6 +4664,7 @@ package android.service.notification { method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator<android.service.notification.Adjustment> CREATOR; field public static final java.lang.String KEY_PEOPLE = "key_people"; + field public static final java.lang.String KEY_SMART_ACTIONS = "key_smart_actions"; field public static final java.lang.String KEY_SNOOZE_CRITERIA = "key_snooze_criteria"; field public static final java.lang.String KEY_USER_SENTIMENT = "key_user_sentiment"; } diff --git a/api/test-current.txt b/api/test-current.txt index e5061ed32a9e..b8acfdb7a7e4 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -1043,6 +1043,7 @@ package android.service.notification { method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator<android.service.notification.Adjustment> CREATOR; field public static final java.lang.String KEY_PEOPLE = "key_people"; + field public static final java.lang.String KEY_SMART_ACTIONS = "key_smart_actions"; field public static final java.lang.String KEY_SNOOZE_CRITERIA = "key_snooze_criteria"; field public static final java.lang.String KEY_USER_SENTIMENT = "key_user_sentiment"; } diff --git a/cmds/content/src/com/android/commands/content/Content.java b/cmds/content/src/com/android/commands/content/Content.java index 6e0bd3a81d84..36e51b9703c9 100644 --- a/cmds/content/src/com/android/commands/content/Content.java +++ b/cmds/content/src/com/android/commands/content/Content.java @@ -462,7 +462,7 @@ public class Content { IBinder token = new Binder(); try { ContentProviderHolder holder = activityManager.getContentProviderExternal( - providerName, mUserId, token); + providerName, mUserId, token, "*cmd*"); if (holder == null) { throw new IllegalStateException("Could not find provider: " + providerName); } diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto index cdb72ab7f174..e23676f96151 100644 --- a/cmds/statsd/src/atoms.proto +++ b/cmds/statsd/src/atoms.proto @@ -126,7 +126,7 @@ message Atom { } // Pulled events will start at field 10000. - // Next: 10022 + // Next: 10023 oneof pulled { WifiBytesTransfer wifi_bytes_transfer = 10000; WifiBytesTransferByFgBg wifi_bytes_transfer_by_fg_bg = 10001; @@ -150,6 +150,7 @@ message Atom { RemainingBatteryCapacity remaining_battery_capacity = 10019; FullBatteryCapacity full_battery_capacity = 10020; Temperature temperature = 10021; + BinderCalls binder_calls = 10022; } // DO NOT USE field numbers above 100,000 in AOSP. Field numbers above @@ -1975,3 +1976,39 @@ message Temperature { // Temperature in tenths of a degree C. optional int32 temperature_dC = 3; } + +/** + * Pulls the statistics of calls to Binder. + * + * Binder stats are cumulative from boot unless somebody reset the data using + * > adb shell dumpsys binder_calls_stats --reset + */ +message BinderCalls { + // TODO(gaillard): figure out if binder call stats includes data from isolated uids, if a uid + // gets recycled and we have isolated uids, we might attribute the data incorrectly. + // TODO(gaillard): there is a high dimensions cardinality, figure out if we should drop the less + // commonly used APIs. + optional int32 uid = 1 [(is_uid) = true]; + // Fully qualified class name of the API call. + optional string service_class_name = 2; + // Method name of the API call. It can also be a transaction code if we cannot resolve it to a + // name. See Binder#getTransactionName. + optional string service_method_name = 3; + // Total number of API calls. + optional int64 call_count = 4; + // Number of exceptions thrown by the API. + optional int64 exception_count = 5; + // Total latency of all API calls. + // Average can be computed using total_latency_micros / call_count. + optional int64 total_latency_micros = 6; + // Maximum latency of one API call. + optional int64 max_latency_micros = 7; + // Total CPU usage of all API calls. + optional int64 total_cpu_micros = 8; + // Maximum CPU usage of one API call. + optional int64 max_cpu_micros = 9; + // Maximum parcel reply size of one API call. + optional int64 max_reply_size_bytes = 10; + // Maximum parcel request size of one API call. + optional int64 max_request_size_bytes = 11; +} diff --git a/cmds/statsd/src/external/StatsPullerManager.cpp b/cmds/statsd/src/external/StatsPullerManager.cpp index 06edff9e498c..160d6e86c55f 100644 --- a/cmds/statsd/src/external/StatsPullerManager.cpp +++ b/cmds/statsd/src/external/StatsPullerManager.cpp @@ -171,7 +171,14 @@ const std::map<int, PullAtomInfo> StatsPullerManager::kAllPullAtomInfo = { 1 * NS_PER_SEC, new StatsCompanionServicePuller(android::util::PROCESS_MEMORY_STATE)}}, // temperature - {android::util::TEMPERATURE, {{}, {}, 1, new ResourceThermalManagerPuller()}}}; + {android::util::TEMPERATURE, {{}, {}, 1, new ResourceThermalManagerPuller()}}, + // binder_calls + {android::util::BINDER_CALLS, + {{4, 5, 6, 8}, + {2, 3, 7, 9, 10, 11}, + 1 * NS_PER_SEC, + new StatsCompanionServicePuller(android::util::BINDER_CALLS)}} + }; StatsPullerManager::StatsPullerManager() : mNextPullTimeNs(NO_ALARM_UPDATE) { } diff --git a/cmds/statsd/src/guardrail/StatsdStats.cpp b/cmds/statsd/src/guardrail/StatsdStats.cpp index 038cb954f578..a955511e198c 100644 --- a/cmds/statsd/src/guardrail/StatsdStats.cpp +++ b/cmds/statsd/src/guardrail/StatsdStats.cpp @@ -100,6 +100,7 @@ const int FIELD_ID_UID_MAP_DROPPED_CHANGES = 3; const int FIELD_ID_UID_MAP_DELETED_APPS = 4; const std::map<int, std::pair<size_t, size_t>> StatsdStats::kAtomDimensionKeySizeLimitMap = { + {android::util::BINDER_CALLS, {6000, 10000}}, {android::util::CPU_TIME_PER_UID_FREQ, {6000, 10000}}, }; diff --git a/cmds/uiautomator/library/testrunner-src/com/android/uiautomator/core/ShellUiAutomatorBridge.java b/cmds/uiautomator/library/testrunner-src/com/android/uiautomator/core/ShellUiAutomatorBridge.java index 653851546d01..950a258d123d 100644 --- a/cmds/uiautomator/library/testrunner-src/com/android/uiautomator/core/ShellUiAutomatorBridge.java +++ b/cmds/uiautomator/library/testrunner-src/com/android/uiautomator/core/ShellUiAutomatorBridge.java @@ -62,7 +62,7 @@ public class ShellUiAutomatorBridge extends UiAutomatorBridge { IBinder token = new Binder(); try { ContentProviderHolder holder = activityManager.getContentProviderExternal( - providerName, UserHandle.USER_SYSTEM, token); + providerName, UserHandle.USER_SYSTEM, token, "*uiautomator*"); if (holder == null) { throw new IllegalStateException("Could not find provider: " + providerName); } diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 6bac52d58c1d..6638dd94c6e4 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -1086,7 +1086,7 @@ public class Activity extends ContextThemeWrapper * * @param savedInstanceState contains the saved state */ - final void performRestoreInstanceState(Bundle savedInstanceState) { + final void performRestoreInstanceState(@NonNull Bundle savedInstanceState) { onRestoreInstanceState(savedInstanceState); restoreManagedDialogs(savedInstanceState); } @@ -1100,8 +1100,8 @@ public class Activity extends ContextThemeWrapper * @param savedInstanceState contains the saved state * @param persistentState contains the persistable saved state */ - final void performRestoreInstanceState(Bundle savedInstanceState, - PersistableBundle persistentState) { + final void performRestoreInstanceState(@Nullable Bundle savedInstanceState, + @Nullable PersistableBundle persistentState) { onRestoreInstanceState(savedInstanceState, persistentState); if (savedInstanceState != null) { restoreManagedDialogs(savedInstanceState); @@ -1128,7 +1128,7 @@ public class Activity extends ContextThemeWrapper * @see #onResume * @see #onSaveInstanceState */ - protected void onRestoreInstanceState(Bundle savedInstanceState) { + protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { if (mWindow != null) { Bundle windowState = savedInstanceState.getBundle(WINDOW_HIERARCHY_TAG); if (windowState != null) { @@ -1149,8 +1149,12 @@ public class Activity extends ContextThemeWrapper * * <p>If this method is called {@link #onRestoreInstanceState(Bundle)} will not be called. * - * @param savedInstanceState the data most recently supplied in {@link #onSaveInstanceState}. - * @param persistentState the data most recently supplied in {@link #onSaveInstanceState}. + * <p>At least one of {@code savedInstanceState} or {@code persistentState} will not be null. + * + * @param savedInstanceState the data most recently supplied in {@link #onSaveInstanceState} + * or null. + * @param persistentState the data most recently supplied in {@link #onSaveInstanceState} + * or null. * * @see #onRestoreInstanceState(Bundle) * @see #onCreate @@ -1158,8 +1162,8 @@ public class Activity extends ContextThemeWrapper * @see #onResume * @see #onSaveInstanceState */ - public void onRestoreInstanceState(Bundle savedInstanceState, - PersistableBundle persistentState) { + public void onRestoreInstanceState(@Nullable Bundle savedInstanceState, + @Nullable PersistableBundle persistentState) { if (savedInstanceState != null) { onRestoreInstanceState(savedInstanceState); } @@ -1545,7 +1549,7 @@ public class Activity extends ContextThemeWrapper * * @param outState The bundle to save the state to. */ - final void performSaveInstanceState(Bundle outState) { + final void performSaveInstanceState(@NonNull Bundle outState) { onSaveInstanceState(outState); saveManagedDialogs(outState); mActivityTransitionState.saveState(outState); @@ -1562,7 +1566,8 @@ public class Activity extends ContextThemeWrapper * @param outState The bundle to save the state to. * @param outPersistentState The bundle to save persistent state to. */ - final void performSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) { + final void performSaveInstanceState(@NonNull Bundle outState, + @NonNull PersistableBundle outPersistentState) { onSaveInstanceState(outState, outPersistentState); saveManagedDialogs(outState); storeHasCurrentPermissionRequest(outState); @@ -1618,7 +1623,7 @@ public class Activity extends ContextThemeWrapper * @see #onRestoreInstanceState * @see #onPause */ - protected void onSaveInstanceState(Bundle outState) { + protected void onSaveInstanceState(@NonNull Bundle outState) { outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState()); outState.putInt(LAST_AUTOFILL_ID, mLastAutofillId); @@ -1648,7 +1653,8 @@ public class Activity extends ContextThemeWrapper * @see #onRestoreInstanceState(Bundle, PersistableBundle) * @see #onPause */ - public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) { + public void onSaveInstanceState(@NonNull Bundle outState, + @NonNull PersistableBundle outPersistentState) { onSaveInstanceState(outState); } diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 3f579bc442b7..f27b286f9efd 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -3080,32 +3080,16 @@ public class ActivityManager { */ public int processState; - /** - * Whether the app is focused in multi-window environment. - * @hide - */ - public boolean isFocused; - - /** - * Copy of {@link com.android.server.am.ProcessRecord#lastActivityTime} of the process. - * @hide - */ - public long lastActivityTime; - public RunningAppProcessInfo() { importance = IMPORTANCE_FOREGROUND; importanceReasonCode = REASON_UNKNOWN; processState = PROCESS_STATE_IMPORTANT_FOREGROUND; - isFocused = false; - lastActivityTime = 0; } public RunningAppProcessInfo(String pProcessName, int pPid, String pArr[]) { processName = pProcessName; pid = pPid; pkgList = pArr; - isFocused = false; - lastActivityTime = 0; } public int describeContents() { @@ -3126,8 +3110,6 @@ public class ActivityManager { ComponentName.writeToParcel(importanceReasonComponent, dest); dest.writeInt(importanceReasonImportance); dest.writeInt(processState); - dest.writeInt(isFocused ? 1 : 0); - dest.writeLong(lastActivityTime); } public void readFromParcel(Parcel source) { @@ -3144,8 +3126,6 @@ public class ActivityManager { importanceReasonComponent = ComponentName.readFromParcel(source); importanceReasonImportance = source.readInt(); processState = source.readInt(); - isFocused = source.readInt() != 0; - lastActivityTime = source.readLong(); } public static final Creator<RunningAppProcessInfo> CREATOR = diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index f3c67310c05d..2daa5779bde7 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -35,6 +35,7 @@ import android.app.servertransaction.ActivityLifecycleItem.LifecycleState; import android.app.servertransaction.ActivityRelaunchItem; import android.app.servertransaction.ActivityResultItem; import android.app.servertransaction.ClientTransaction; +import android.app.servertransaction.ClientTransactionItem; import android.app.servertransaction.PendingTransactionActions; import android.app.servertransaction.PendingTransactionActions.StopInfo; import android.app.servertransaction.TransactionExecutor; @@ -176,6 +177,7 @@ import java.net.InetAddress; import java.text.DateFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; @@ -257,6 +259,8 @@ public final class ActivityThread extends ClientTransactionHandler { final H mH = new H(); final Executor mExecutor = new HandlerExecutor(mH); final ArrayMap<IBinder, ActivityClientRecord> mActivities = new ArrayMap<>(); + final Map<IBinder, ClientTransactionItem> mActivitiesToBeDestroyed = + Collections.synchronizedMap(new ArrayMap<IBinder, ClientTransactionItem>()); // List of new activities (via ActivityRecord.nextIdle) that should // be reported when next we idle. ActivityClientRecord mNewActivities = null; @@ -4474,6 +4478,11 @@ public final class ActivityThread extends ClientTransactionHandler { } @Override + public Map<IBinder, ClientTransactionItem> getActivitiesToBeDestroyed() { + return mActivitiesToBeDestroyed; + } + + @Override public void handleDestroyActivity(IBinder token, boolean finishing, int configChanges, boolean getNonConfigInstance, String reason) { ActivityClientRecord r = performDestroyActivity(token, finishing, diff --git a/core/java/android/app/Application.java b/core/java/android/app/Application.java index 4531f53bd86f..6a58d9b0de36 100644 --- a/core/java/android/app/Application.java +++ b/core/java/android/app/Application.java @@ -17,6 +17,8 @@ package android.app; import android.annotation.CallSuper; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.ComponentCallbacks; import android.content.ComponentCallbacks2; import android.content.Context; @@ -58,13 +60,13 @@ public class Application extends ContextWrapper implements ComponentCallbacks2 { public LoadedApk mLoadedApk; public interface ActivityLifecycleCallbacks { - void onActivityCreated(Activity activity, Bundle savedInstanceState); - void onActivityStarted(Activity activity); - void onActivityResumed(Activity activity); - void onActivityPaused(Activity activity); - void onActivityStopped(Activity activity); - void onActivitySaveInstanceState(Activity activity, Bundle outState); - void onActivityDestroyed(Activity activity); + void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState); + void onActivityStarted(@NonNull Activity activity); + void onActivityResumed(@NonNull Activity activity); + void onActivityPaused(@NonNull Activity activity); + void onActivityStopped(@NonNull Activity activity); + void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState); + void onActivityDestroyed(@NonNull Activity activity); } /** @@ -213,7 +215,8 @@ public class Application extends ContextWrapper implements ComponentCallbacks2 { mLoadedApk = ContextImpl.getImpl(context).mPackageInfo; } - /* package */ void dispatchActivityCreated(Activity activity, Bundle savedInstanceState) { + /* package */ void dispatchActivityCreated(@NonNull Activity activity, + @Nullable Bundle savedInstanceState) { Object[] callbacks = collectActivityLifecycleCallbacks(); if (callbacks != null) { for (int i=0; i<callbacks.length; i++) { @@ -223,7 +226,7 @@ public class Application extends ContextWrapper implements ComponentCallbacks2 { } } - /* package */ void dispatchActivityStarted(Activity activity) { + /* package */ void dispatchActivityStarted(@NonNull Activity activity) { Object[] callbacks = collectActivityLifecycleCallbacks(); if (callbacks != null) { for (int i=0; i<callbacks.length; i++) { @@ -232,7 +235,7 @@ public class Application extends ContextWrapper implements ComponentCallbacks2 { } } - /* package */ void dispatchActivityResumed(Activity activity) { + /* package */ void dispatchActivityResumed(@NonNull Activity activity) { Object[] callbacks = collectActivityLifecycleCallbacks(); if (callbacks != null) { for (int i=0; i<callbacks.length; i++) { @@ -241,7 +244,7 @@ public class Application extends ContextWrapper implements ComponentCallbacks2 { } } - /* package */ void dispatchActivityPaused(Activity activity) { + /* package */ void dispatchActivityPaused(@NonNull Activity activity) { Object[] callbacks = collectActivityLifecycleCallbacks(); if (callbacks != null) { for (int i=0; i<callbacks.length; i++) { @@ -250,7 +253,7 @@ public class Application extends ContextWrapper implements ComponentCallbacks2 { } } - /* package */ void dispatchActivityStopped(Activity activity) { + /* package */ void dispatchActivityStopped(@NonNull Activity activity) { Object[] callbacks = collectActivityLifecycleCallbacks(); if (callbacks != null) { for (int i=0; i<callbacks.length; i++) { @@ -259,7 +262,8 @@ public class Application extends ContextWrapper implements ComponentCallbacks2 { } } - /* package */ void dispatchActivitySaveInstanceState(Activity activity, Bundle outState) { + /* package */ void dispatchActivitySaveInstanceState(@NonNull Activity activity, + @NonNull Bundle outState) { Object[] callbacks = collectActivityLifecycleCallbacks(); if (callbacks != null) { for (int i=0; i<callbacks.length; i++) { @@ -269,7 +273,7 @@ public class Application extends ContextWrapper implements ComponentCallbacks2 { } } - /* package */ void dispatchActivityDestroyed(Activity activity) { + /* package */ void dispatchActivityDestroyed(@NonNull Activity activity) { Object[] callbacks = collectActivityLifecycleCallbacks(); if (callbacks != null) { for (int i=0; i<callbacks.length; i++) { diff --git a/core/java/android/app/ClientTransactionHandler.java b/core/java/android/app/ClientTransactionHandler.java index d9c7cf3ccc74..193f933df782 100644 --- a/core/java/android/app/ClientTransactionHandler.java +++ b/core/java/android/app/ClientTransactionHandler.java @@ -16,6 +16,7 @@ package android.app; import android.app.servertransaction.ClientTransaction; +import android.app.servertransaction.ClientTransactionItem; import android.app.servertransaction.PendingTransactionActions; import android.app.servertransaction.TransactionExecutor; import android.content.Intent; @@ -29,6 +30,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.content.ReferrerIntent; import java.util.List; +import java.util.Map; /** * Defines operations that a {@link android.app.servertransaction.ClientTransaction} or its items @@ -78,6 +80,9 @@ public abstract class ClientTransactionHandler { // Execute phase related logic and handlers. Methods here execute actual lifecycle transactions // and deliver callbacks. + /** Get activity and its corresponding transaction item which are going to destroy. */ + public abstract Map<IBinder, ClientTransactionItem> getActivitiesToBeDestroyed(); + /** Destroy the activity. */ public abstract void handleDestroyActivity(IBinder token, boolean finishing, int configChanges, boolean getNonConfigInstance, String reason); diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index f9c39165b2c8..19d7c83818f6 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -268,7 +268,7 @@ interface IActivityManager { void showBootMessage(in CharSequence msg, boolean always); void killAllBackgroundProcesses(); ContentProviderHolder getContentProviderExternal(in String name, int userId, - in IBinder token); + in IBinder token, String tag); void removeContentProviderExternal(in String name, in IBinder token); // Get memory information about the calling process. void getMyMemoryState(out ActivityManager.RunningAppProcessInfo outInfo); diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index 34be41b63233..d9969a77b88a 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -1313,7 +1313,8 @@ public class Instrumentation { * @param activity The activity being restored. * @param savedInstanceState The previously saved state being restored. */ - public void callActivityOnRestoreInstanceState(Activity activity, Bundle savedInstanceState) { + public void callActivityOnRestoreInstanceState(@NonNull Activity activity, + @NonNull Bundle savedInstanceState) { activity.performRestoreInstanceState(savedInstanceState); } @@ -1322,11 +1323,12 @@ public class Instrumentation { * method. The default implementation simply calls through to that method. * * @param activity The activity being restored. - * @param savedInstanceState The previously saved state being restored. + * @param savedInstanceState The previously saved state being restored (or null). * @param persistentState The previously persisted state (or null) */ - public void callActivityOnRestoreInstanceState(Activity activity, Bundle savedInstanceState, - PersistableBundle persistentState) { + public void callActivityOnRestoreInstanceState(@NonNull Activity activity, + @Nullable Bundle savedInstanceState, + @Nullable PersistableBundle persistentState) { activity.performRestoreInstanceState(savedInstanceState, persistentState); } @@ -1335,11 +1337,12 @@ public class Instrumentation { * The default implementation simply calls through to that method. * * @param activity The activity being created. - * @param icicle The previously frozen state (or null) to pass through to + * @param savedInstanceState The previously saved state (or null) to pass through to * onPostCreate(). */ - public void callActivityOnPostCreate(Activity activity, Bundle icicle) { - activity.onPostCreate(icicle); + public void callActivityOnPostCreate(@NonNull Activity activity, + @Nullable Bundle savedInstanceState) { + activity.onPostCreate(savedInstanceState); } /** @@ -1347,12 +1350,14 @@ public class Instrumentation { * The default implementation simply calls through to that method. * * @param activity The activity being created. - * @param icicle The previously frozen state (or null) to pass through to + * @param savedInstanceState The previously frozen state (or null) to pass through to * onPostCreate(). + * @param persistentState The previously persisted state (or null) */ - public void callActivityOnPostCreate(Activity activity, Bundle icicle, - PersistableBundle persistentState) { - activity.onPostCreate(icicle, persistentState); + public void callActivityOnPostCreate(@NonNull Activity activity, + @Nullable Bundle savedInstanceState, + @Nullable PersistableBundle persistentState) { + activity.onPostCreate(savedInstanceState, persistentState); } /** @@ -1439,7 +1444,8 @@ public class Instrumentation { * @param activity The activity being saved. * @param outState The bundle to pass to the call. */ - public void callActivityOnSaveInstanceState(Activity activity, Bundle outState) { + public void callActivityOnSaveInstanceState(@NonNull Activity activity, + @NonNull Bundle outState) { activity.performSaveInstanceState(outState); } @@ -1450,8 +1456,8 @@ public class Instrumentation { * @param outState The bundle to pass to the call. * @param outPersistentState The persistent bundle to pass to the call. */ - public void callActivityOnSaveInstanceState(Activity activity, Bundle outState, - PersistableBundle outPersistentState) { + public void callActivityOnSaveInstanceState(@NonNull Activity activity, + @NonNull Bundle outState, @NonNull PersistableBundle outPersistentState) { activity.performSaveInstanceState(outState, outPersistentState); } diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 05bf6bfa3a8f..74d3c0df87ad 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -202,6 +202,11 @@ public class Notification implements Parcelable */ private static final int MAX_REPLY_HISTORY = 5; + /** + * Maximum numbers of action buttons in a notification. + * @hide + */ + public static final int MAX_ACTION_BUTTONS = 3; /** * If the notification contained an unsent draft for a RemoteInput when the user clicked on it, @@ -3151,8 +3156,6 @@ public class Notification implements Parcelable public static final String EXTRA_REBUILD_HEADS_UP_CONTENT_VIEW_ACTION_COUNT = "android.rebuild.hudViewActionCount"; - private static final int MAX_ACTION_BUTTONS = 3; - private static final boolean USE_ONLY_TITLE_IN_LOW_PRIORITY_SUMMARY = SystemProperties.getBoolean("notifications.only_title", true); @@ -4473,9 +4476,9 @@ public class Notification implements Parcelable mTextColorsAreForBackground = backgroundColor; if (!hasForegroundColor() || !isColorized()) { mPrimaryTextColor = ContrastColorUtil.resolvePrimaryColor(mContext, - backgroundColor); + backgroundColor, mInNightMode); mSecondaryTextColor = ContrastColorUtil.resolveSecondaryColor(mContext, - backgroundColor); + backgroundColor, mInNightMode); if (backgroundColor != COLOR_DEFAULT && isColorized()) { mPrimaryTextColor = ContrastColorUtil.findAlphaToMeetContrast( mPrimaryTextColor, backgroundColor, 4.5); @@ -5260,7 +5263,7 @@ public class Notification implements Parcelable // background color background = outResultColor[0].getDefaultColor(); int textColor = ContrastColorUtil.resolvePrimaryColor(mContext, - background); + background, mInNightMode); button.setTextColor(R.id.action0, textColor); rippleColor = textColor; } else if (mN.color != COLOR_DEFAULT && !isColorized() && mTintActionButtons) { @@ -5440,7 +5443,7 @@ public class Notification implements Parcelable com.android.internal.R.color.notification_material_background_color); if (mN.color == COLOR_DEFAULT) { ensureColors(); - color = ContrastColorUtil.resolveDefaultColor(mContext, background); + color = ContrastColorUtil.resolveDefaultColor(mContext, background, mInNightMode); } else { color = ContrastColorUtil.resolveContrastColor(mContext, mN.color, background, mInNightMode); @@ -5459,7 +5462,8 @@ public class Notification implements Parcelable } int background = mContext.getColor( com.android.internal.R.color.notification_material_background_color); - mNeutralColor = ContrastColorUtil.resolveDefaultColor(mContext, background); + mNeutralColor = ContrastColorUtil.resolveDefaultColor(mContext, background, + mInNightMode); if (Color.alpha(mNeutralColor) < 255) { // alpha doesn't go well for color filters, so let's blend it manually mNeutralColor = ContrastColorUtil.compositeColors(mNeutralColor, background); @@ -7162,8 +7166,8 @@ public class Notification implements Parcelable } public static final class Message { - - static final String KEY_TEXT = "text"; + /** @hide */ + public static final String KEY_TEXT = "text"; static final String KEY_TIMESTAMP = "time"; static final String KEY_SENDER = "sender"; static final String KEY_SENDER_PERSON = "sender_person"; @@ -7830,10 +7834,13 @@ public class Notification implements Parcelable // If the action buttons should not be tinted, then just use the default // notification color. Otherwise, just use the passed-in color. + Configuration currentConfig = mBuilder.mContext.getResources().getConfiguration(); + boolean inNightMode = (currentConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK) + == Configuration.UI_MODE_NIGHT_YES; int tintColor = mBuilder.shouldTintActionButtons() || mBuilder.isColorized() ? color : ContrastColorUtil.resolveColor(mBuilder.mContext, - Notification.COLOR_DEFAULT); + Notification.COLOR_DEFAULT, inNightMode); button.setDrawableTint(R.id.action0, false, tintColor, PorterDuff.Mode.SRC_ATOP); diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java index 1ad305441539..03fd139e12ff 100644 --- a/core/java/android/app/NotificationChannel.java +++ b/core/java/android/app/NotificationChannel.java @@ -38,6 +38,7 @@ import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlSerializer; import java.io.IOException; +import java.io.PrintWriter; import java.util.Arrays; /** @@ -942,6 +943,32 @@ public final class NotificationChannel implements Parcelable { return result; } + /** @hide */ + public void dump(PrintWriter pw, String prefix, boolean redacted) { + String redactedName = redacted ? TextUtils.trimToLengthWithEllipsis(mName, 3) : mName; + String output = "NotificationChannel{" + + "mId='" + mId + '\'' + + ", mName=" + redactedName + + ", mDescription=" + (!TextUtils.isEmpty(mDesc) ? "hasDescription " : "") + + ", mImportance=" + mImportance + + ", mBypassDnd=" + mBypassDnd + + ", mLockscreenVisibility=" + mLockscreenVisibility + + ", mSound=" + mSound + + ", mLights=" + mLights + + ", mLightColor=" + mLightColor + + ", mVibration=" + Arrays.toString(mVibration) + + ", mUserLockedFields=" + Integer.toHexString(mUserLockedFields) + + ", mFgServiceShown=" + mFgServiceShown + + ", mVibrationEnabled=" + mVibrationEnabled + + ", mShowBadge=" + mShowBadge + + ", mDeleted=" + mDeleted + + ", mGroup='" + mGroup + '\'' + + ", mAudioAttributes=" + mAudioAttributes + + ", mBlockableSystem=" + mBlockableSystem + + '}'; + pw.println(prefix + output); + } + @Override public String toString() { return "NotificationChannel{" diff --git a/core/java/android/app/servertransaction/ClientTransaction.java b/core/java/android/app/servertransaction/ClientTransaction.java index 08ad2f055774..2c1e59bfc066 100644 --- a/core/java/android/app/servertransaction/ClientTransaction.java +++ b/core/java/android/app/servertransaction/ClientTransaction.java @@ -164,6 +164,31 @@ public class ClientTransaction implements Parcelable, ObjectPoolItem { ObjectPool.recycle(this); } + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(64); + sb.append("ClientTransaction{"); + if (mActivityToken != null) { + sb.append(" a:").append(Integer.toHexString(System.identityHashCode(mActivityToken))); + } + if (mActivityCallbacks != null && !mActivityCallbacks.isEmpty()) { + sb.append(" c:"); + final int size = mActivityCallbacks.size(); + for (int i = 0; i < size; i++) { + sb.append(mActivityCallbacks.get(i).getClass().getSimpleName()); + if (i < size - 1) { + sb.append(","); + } + } + } + if (mLifecycleStateRequest != null) { + sb.append(" s:"); + sb.append(mLifecycleStateRequest.getClass().getSimpleName()); + } + sb.append(" }"); + return sb.toString(); + } + // Parcelable implementation diff --git a/core/java/android/app/servertransaction/DestroyActivityItem.java b/core/java/android/app/servertransaction/DestroyActivityItem.java index b443166d151c..5941486c099d 100644 --- a/core/java/android/app/servertransaction/DestroyActivityItem.java +++ b/core/java/android/app/servertransaction/DestroyActivityItem.java @@ -33,6 +33,11 @@ public class DestroyActivityItem extends ActivityLifecycleItem { private int mConfigChanges; @Override + public void preExecute(ClientTransactionHandler client, IBinder token) { + client.getActivitiesToBeDestroyed().put(token, this); + } + + @Override public void execute(ClientTransactionHandler client, IBinder token, PendingTransactionActions pendingActions) { Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityDestroy"); diff --git a/core/java/android/app/servertransaction/TransactionExecutor.java b/core/java/android/app/servertransaction/TransactionExecutor.java index 43a2b4cc43f8..503e18b62cae 100644 --- a/core/java/android/app/servertransaction/TransactionExecutor.java +++ b/core/java/android/app/servertransaction/TransactionExecutor.java @@ -35,6 +35,7 @@ import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; import java.util.List; +import java.util.Map; /** * Class that manages transaction execution in the correct order. @@ -63,6 +64,24 @@ public class TransactionExecutor { */ public void execute(ClientTransaction transaction) { final IBinder token = transaction.getActivityToken(); + if (token != null) { + final Map<IBinder, ClientTransactionItem> activitiesToBeDestroyed = + mTransactionHandler.getActivitiesToBeDestroyed(); + final ClientTransactionItem destroyItem = activitiesToBeDestroyed.get(token); + if (destroyItem != null) { + if (transaction.getLifecycleStateRequest() == destroyItem) { + // It is going to execute the transaction that will destroy activity with the + // token, so the corresponding to-be-destroyed record can be removed. + activitiesToBeDestroyed.remove(token); + } + if (mTransactionHandler.getActivityClient(token) == null) { + // The activity has not been created but has been requested to destroy, so all + // transactions for the token are just like being cancelled. + Slog.w(TAG, "Skip pre-destroyed " + transaction); + return; + } + } + } log("Start resolving transaction for client: " + mTransactionHandler + ", token: " + token); executeCallbacks(transaction); @@ -76,7 +95,7 @@ public class TransactionExecutor { @VisibleForTesting public void executeCallbacks(ClientTransaction transaction) { final List<ClientTransactionItem> callbacks = transaction.getCallbacks(); - if (callbacks == null) { + if (callbacks == null || callbacks.isEmpty()) { // No callbacks to execute, return early. return; } diff --git a/core/java/android/content/ContentProviderClient.java b/core/java/android/content/ContentProviderClient.java index 2d490a03bd76..9d8c318f8092 100644 --- a/core/java/android/content/ContentProviderClient.java +++ b/core/java/android/content/ContentProviderClient.java @@ -39,6 +39,8 @@ import com.android.internal.util.Preconditions; import dalvik.system.CloseGuard; +import libcore.io.IoUtils; + import java.io.FileNotFoundException; import java.util.ArrayList; import java.util.concurrent.atomic.AtomicBoolean; @@ -560,14 +562,17 @@ public class ContentProviderClient implements AutoCloseable { return ContentProvider.coerceToLocalContentProvider(mContentProvider); } + /** + * Closes the given object quietly, ignoring any checked exceptions. Does + * nothing if the given object is {@code null}. + */ + public static void closeQuietly(ContentProviderClient client) { + IoUtils.closeQuietly(client); + } + /** {@hide} */ public static void releaseQuietly(ContentProviderClient client) { - if (client != null) { - try { - client.release(); - } catch (Exception ignored) { - } - } + IoUtils.closeQuietly(client); } private class NotRespondingRunnable implements Runnable { diff --git a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java index 4baf2638d888..86bd30c19ca3 100644 --- a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java +++ b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java @@ -747,6 +747,9 @@ public class CameraMetadataNative implements Parcelable { if (faceDetectMode == null) { Log.w(TAG, "Face detect mode metadata is null, assuming the mode is SIMPLE"); faceDetectMode = CaptureResult.STATISTICS_FACE_DETECT_MODE_SIMPLE; + } else if (faceDetectMode > CaptureResult.STATISTICS_FACE_DETECT_MODE_FULL) { + // Face detect mode is larger than FULL, assuming the mode is FULL + faceDetectMode = CaptureResult.STATISTICS_FACE_DETECT_MODE_FULL; } else { if (faceDetectMode == CaptureResult.STATISTICS_FACE_DETECT_MODE_OFF) { return new Face[0]; diff --git a/core/java/android/hardware/face/FaceManager.java b/core/java/android/hardware/face/FaceManager.java index 0a66e914c9b6..3de9de3ed627 100644 --- a/core/java/android/hardware/face/FaceManager.java +++ b/core/java/android/hardware/face/FaceManager.java @@ -836,10 +836,8 @@ public class FaceManager implements BiometricFaceConstants { * {@link EnrollmentCallback#onEnrollmentError(int, CharSequence)} * * @param remaining The number of remaining steps - * @param vendorMsg Vendor feedback about the current enroll attempt. Use it to customize - * the GUI according to vendor's requirements. */ - public void onEnrollmentProgress(int remaining, long vendorMsg) { + public void onEnrollmentProgress(int remaining) { } } @@ -920,7 +918,7 @@ public class FaceManager implements BiometricFaceConstants { public void handleMessage(android.os.Message msg) { switch (msg.what) { case MSG_ENROLL_RESULT: - sendEnrollResult((EnrollResultMsg) msg.obj); + sendEnrollResult((Face) msg.obj, msg.arg1 /* remaining */); break; case MSG_ACQUIRED: sendAcquiredResult((Long) msg.obj /* deviceId */, msg.arg1 /* acquire info */, @@ -951,8 +949,6 @@ public class FaceManager implements BiometricFaceConstants { Log.e(TAG, "Received MSG_REMOVED, but face is null"); return; } - - mRemovalCallback.onRemovalSucceeded(face, remaining); } @@ -972,11 +968,9 @@ public class FaceManager implements BiometricFaceConstants { } } - private void sendEnrollResult(EnrollResultMsg faceWrapper) { + private void sendEnrollResult(Face face, int remaining) { if (mEnrollmentCallback != null) { - int remaining = faceWrapper.getRemaining(); - long vendorMsg = faceWrapper.getVendorMsg(); - mEnrollmentCallback.onEnrollmentProgress(remaining, vendorMsg); + mEnrollmentCallback.onEnrollmentProgress(remaining); } } @@ -1010,28 +1004,4 @@ public class FaceManager implements BiometricFaceConstants { mAuthenticationCallback.onAuthenticationHelp(clientInfo, msg); } } - - private class EnrollResultMsg { - private final Face mFace; - private final int mRemaining; - private final long mVendorMsg; - - EnrollResultMsg(Face face, int remaining, long vendorMsg) { - mFace = face; - mRemaining = remaining; - mVendorMsg = vendorMsg; - } - - Face getFace() { - return mFace; - } - - long getVendorMsg() { - return mVendorMsg; - } - - int getRemaining() { - return mRemaining; - } - } } diff --git a/core/java/android/hardware/location/NanoAppFilter.java b/core/java/android/hardware/location/NanoAppFilter.java index 4d8e7344cd75..562065e20815 100644 --- a/core/java/android/hardware/location/NanoAppFilter.java +++ b/core/java/android/hardware/location/NanoAppFilter.java @@ -85,7 +85,7 @@ public class NanoAppFilter implements Parcelable { mAppId = in.readLong(); mAppVersion = in.readInt(); mVersionRestrictionMask = in.readInt(); - mAppIdVendorMask = in.readInt(); + mAppIdVendorMask = in.readLong(); } public int describeContents() { @@ -93,7 +93,6 @@ public class NanoAppFilter implements Parcelable { } public void writeToParcel(Parcel out, int flags) { - out.writeLong(mAppId); out.writeInt(mAppVersion); out.writeInt(mVersionRestrictionMask); diff --git a/core/java/android/hardware/radio/RadioManager.aidl b/core/java/android/hardware/radio/RadioManager.aidl index 8a39388482fd..34c05d89d715 100644 --- a/core/java/android/hardware/radio/RadioManager.aidl +++ b/core/java/android/hardware/radio/RadioManager.aidl @@ -20,6 +20,9 @@ package android.hardware.radio; parcelable RadioManager.BandConfig; /** @hide */ +parcelable RadioManager.BandDescriptor; + +/** @hide */ parcelable RadioManager.ModuleProperties; /** @hide */ diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java index f31130f8645d..ab2cf86fab29 100644 --- a/core/java/android/os/Binder.java +++ b/core/java/android/os/Binder.java @@ -552,6 +552,20 @@ public class Binder implements IBinder { } /** + * Resolves a transaction code to a human readable name. + * + * <p>Default implementation is a stub that returns null. + * <p>AIDL generated code will return the original method name. + * + * @param transactionCode The code to resolve. + * @return A human readable name. + * @hide + */ + public @Nullable String getTransactionName(int transactionCode) { + return null; + } + + /** * Implemented to call the more convenient version * {@link #dump(FileDescriptor, PrintWriter, String[])}. */ diff --git a/core/java/android/os/FileUtils.java b/core/java/android/os/FileUtils.java index 88d6e847b644..9fccd1ec7b43 100644 --- a/core/java/android/os/FileUtils.java +++ b/core/java/android/os/FileUtils.java @@ -53,32 +53,35 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Comparator; import java.util.Objects; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; import java.util.zip.CRC32; import java.util.zip.CheckedInputStream; /** - * Tools for managing files. Not for public consumption. - * @hide + * Utility methods useful for working with files. */ public class FileUtils { private static final String TAG = "FileUtils"; - public static final int S_IRWXU = 00700; - public static final int S_IRUSR = 00400; - public static final int S_IWUSR = 00200; - public static final int S_IXUSR = 00100; + /** {@hide} */ public static final int S_IRWXU = 00700; + /** {@hide} */ public static final int S_IRUSR = 00400; + /** {@hide} */ public static final int S_IWUSR = 00200; + /** {@hide} */ public static final int S_IXUSR = 00100; - public static final int S_IRWXG = 00070; - public static final int S_IRGRP = 00040; - public static final int S_IWGRP = 00020; - public static final int S_IXGRP = 00010; + /** {@hide} */ public static final int S_IRWXG = 00070; + /** {@hide} */ public static final int S_IRGRP = 00040; + /** {@hide} */ public static final int S_IWGRP = 00020; + /** {@hide} */ public static final int S_IXGRP = 00010; - public static final int S_IRWXO = 00007; - public static final int S_IROTH = 00004; - public static final int S_IWOTH = 00002; - public static final int S_IXOTH = 00001; + /** {@hide} */ public static final int S_IRWXO = 00007; + /** {@hide} */ public static final int S_IROTH = 00004; + /** {@hide} */ public static final int S_IWOTH = 00002; + /** {@hide} */ public static final int S_IXOTH = 00001; + + private FileUtils() { + } /** Regular expression for safe filenames: no spaces or metacharacters. * @@ -94,6 +97,9 @@ public class FileUtils { private static final long COPY_CHECKPOINT_BYTES = 524288; + /** + * Listener that is called periodically as progress is made. + */ public interface ProgressListener { public void onProgress(long progress); } @@ -105,6 +111,7 @@ public class FileUtils { * @param uid to apply through {@code chown}, or -1 to leave unchanged * @param gid to apply through {@code chown}, or -1 to leave unchanged * @return 0 on success, otherwise errno. + * @hide */ public static int setPermissions(File path, int mode, int uid, int gid) { return setPermissions(path.getAbsolutePath(), mode, uid, gid); @@ -117,6 +124,7 @@ public class FileUtils { * @param uid to apply through {@code chown}, or -1 to leave unchanged * @param gid to apply through {@code chown}, or -1 to leave unchanged * @return 0 on success, otherwise errno. + * @hide */ public static int setPermissions(String path, int mode, int uid, int gid) { try { @@ -145,6 +153,7 @@ public class FileUtils { * @param uid to apply through {@code chown}, or -1 to leave unchanged * @param gid to apply through {@code chown}, or -1 to leave unchanged * @return 0 on success, otherwise errno. + * @hide */ public static int setPermissions(FileDescriptor fd, int mode, int uid, int gid) { try { @@ -166,7 +175,14 @@ public class FileUtils { return 0; } - public static void copyPermissions(File from, File to) throws IOException { + /** + * Copy the owner UID, owner GID, and mode bits from one file to another. + * + * @param from File where attributes should be copied from. + * @param to File where attributes should be copied to. + * @hide + */ + public static void copyPermissions(@NonNull File from, @NonNull File to) throws IOException { try { final StructStat stat = Os.stat(from.getAbsolutePath()); Os.chmod(to.getAbsolutePath(), stat.st_mode); @@ -177,8 +193,10 @@ public class FileUtils { } /** - * Return owning UID of given path, otherwise -1. + * @deprecated use {@link Os#stat(String)} instead. + * @hide */ + @Deprecated public static int getUid(String path) { try { return Os.stat(path).st_uid; @@ -190,6 +208,8 @@ public class FileUtils { /** * Perform an fsync on the given FileOutputStream. The stream at this * point must be flushed but not yet closed. + * + * @hide */ public static boolean sync(FileOutputStream stream) { try { @@ -204,6 +224,7 @@ public class FileUtils { /** * @deprecated use {@link #copy(File, File)} instead. + * @hide */ @Deprecated public static boolean copyFile(File srcFile, File destFile) { @@ -217,6 +238,7 @@ public class FileUtils { /** * @deprecated use {@link #copy(File, File)} instead. + * @hide */ @Deprecated public static void copyFileOrThrow(File srcFile, File destFile) throws IOException { @@ -227,6 +249,7 @@ public class FileUtils { /** * @deprecated use {@link #copy(InputStream, OutputStream)} instead. + * @hide */ @Deprecated public static boolean copyToFile(InputStream inputStream, File destFile) { @@ -240,6 +263,7 @@ public class FileUtils { /** * @deprecated use {@link #copy(InputStream, OutputStream)} instead. + * @hide */ @Deprecated public static void copyToFileOrThrow(InputStream in, File destFile) throws IOException { @@ -265,7 +289,7 @@ public class FileUtils { * @return number of bytes copied. */ public static long copy(@NonNull File from, @NonNull File to) throws IOException { - return copy(from, to, null, null); + return copy(from, to, null, null, null); } /** @@ -274,16 +298,17 @@ public class FileUtils { * Attempts to use several optimization strategies to copy the data in the * kernel before falling back to a userspace copy as a last resort. * - * @param listener to be periodically notified as the copy progresses. * @param signal to signal if the copy should be cancelled early. + * @param executor that listener events should be delivered via. + * @param listener to be periodically notified as the copy progresses. * @return number of bytes copied. */ public static long copy(@NonNull File from, @NonNull File to, - @Nullable ProgressListener listener, @Nullable CancellationSignal signal) - throws IOException { + @Nullable CancellationSignal signal, @Nullable Executor executor, + @Nullable ProgressListener listener) throws IOException { try (FileInputStream in = new FileInputStream(from); FileOutputStream out = new FileOutputStream(to)) { - return copy(in, out, listener, signal); + return copy(in, out, signal, executor, listener); } } @@ -296,7 +321,7 @@ public class FileUtils { * @return number of bytes copied. */ public static long copy(@NonNull InputStream in, @NonNull OutputStream out) throws IOException { - return copy(in, out, null, null); + return copy(in, out, null, null, null); } /** @@ -305,22 +330,23 @@ public class FileUtils { * Attempts to use several optimization strategies to copy the data in the * kernel before falling back to a userspace copy as a last resort. * - * @param listener to be periodically notified as the copy progresses. * @param signal to signal if the copy should be cancelled early. + * @param executor that listener events should be delivered via. + * @param listener to be periodically notified as the copy progresses. * @return number of bytes copied. */ public static long copy(@NonNull InputStream in, @NonNull OutputStream out, - @Nullable ProgressListener listener, @Nullable CancellationSignal signal) - throws IOException { + @Nullable CancellationSignal signal, @Nullable Executor executor, + @Nullable ProgressListener listener) throws IOException { if (ENABLE_COPY_OPTIMIZATIONS) { if (in instanceof FileInputStream && out instanceof FileOutputStream) { return copy(((FileInputStream) in).getFD(), ((FileOutputStream) out).getFD(), - listener, signal); + signal, executor, listener); } } // Worse case fallback to userspace - return copyInternalUserspace(in, out, listener, signal); + return copyInternalUserspace(in, out, signal, executor, listener); } /** @@ -333,7 +359,7 @@ public class FileUtils { */ public static long copy(@NonNull FileDescriptor in, @NonNull FileDescriptor out) throws IOException { - return copy(in, out, null, null); + return copy(in, out, null, null, null); } /** @@ -342,14 +368,15 @@ public class FileUtils { * Attempts to use several optimization strategies to copy the data in the * kernel before falling back to a userspace copy as a last resort. * - * @param listener to be periodically notified as the copy progresses. * @param signal to signal if the copy should be cancelled early. + * @param executor that listener events should be delivered via. + * @param listener to be periodically notified as the copy progresses. * @return number of bytes copied. */ public static long copy(@NonNull FileDescriptor in, @NonNull FileDescriptor out, - @Nullable ProgressListener listener, @Nullable CancellationSignal signal) - throws IOException { - return copy(in, out, listener, signal, Long.MAX_VALUE); + @Nullable CancellationSignal signal, @Nullable Executor executor, + @Nullable ProgressListener listener) throws IOException { + return copy(in, out, Long.MAX_VALUE, signal, executor, listener); } /** @@ -358,22 +385,24 @@ public class FileUtils { * Attempts to use several optimization strategies to copy the data in the * kernel before falling back to a userspace copy as a last resort. * - * @param listener to be periodically notified as the copy progresses. - * @param signal to signal if the copy should be cancelled early. * @param count the number of bytes to copy. + * @param signal to signal if the copy should be cancelled early. + * @param executor that listener events should be delivered via. + * @param listener to be periodically notified as the copy progresses. * @return number of bytes copied. + * @hide */ - public static long copy(@NonNull FileDescriptor in, @NonNull FileDescriptor out, - @Nullable ProgressListener listener, @Nullable CancellationSignal signal, long count) - throws IOException { + public static long copy(@NonNull FileDescriptor in, @NonNull FileDescriptor out, long count, + @Nullable CancellationSignal signal, @Nullable Executor executor, + @Nullable ProgressListener listener) throws IOException { if (ENABLE_COPY_OPTIMIZATIONS) { try { final StructStat st_in = Os.fstat(in); final StructStat st_out = Os.fstat(out); if (S_ISREG(st_in.st_mode) && S_ISREG(st_out.st_mode)) { - return copyInternalSendfile(in, out, listener, signal, count); + return copyInternalSendfile(in, out, count, signal, executor, listener); } else if (S_ISFIFO(st_in.st_mode) || S_ISFIFO(st_out.st_mode)) { - return copyInternalSplice(in, out, listener, signal, count); + return copyInternalSplice(in, out, count, signal, executor, listener); } } catch (ErrnoException e) { throw e.rethrowAsIOException(); @@ -381,15 +410,17 @@ public class FileUtils { } // Worse case fallback to userspace - return copyInternalUserspace(in, out, listener, signal, count); + return copyInternalUserspace(in, out, count, signal, executor, listener); } /** * Requires one of input or output to be a pipe. + * + * @hide */ @VisibleForTesting - public static long copyInternalSplice(FileDescriptor in, FileDescriptor out, - ProgressListener listener, CancellationSignal signal, long count) + public static long copyInternalSplice(FileDescriptor in, FileDescriptor out, long count, + CancellationSignal signal, Executor executor, ProgressListener listener) throws ErrnoException { long progress = 0; long checkpoint = 0; @@ -405,24 +436,32 @@ public class FileUtils { if (signal != null) { signal.throwIfCanceled(); } - if (listener != null) { - listener.onProgress(progress); + if (executor != null && listener != null) { + final long progressSnapshot = progress; + executor.execute(() -> { + listener.onProgress(progressSnapshot); + }); } checkpoint = 0; } } - if (listener != null) { - listener.onProgress(progress); + if (executor != null && listener != null) { + final long progressSnapshot = progress; + executor.execute(() -> { + listener.onProgress(progressSnapshot); + }); } return progress; } /** * Requires both input and output to be a regular file. + * + * @hide */ @VisibleForTesting - public static long copyInternalSendfile(FileDescriptor in, FileDescriptor out, - ProgressListener listener, CancellationSignal signal, long count) + public static long copyInternalSendfile(FileDescriptor in, FileDescriptor out, long count, + CancellationSignal signal, Executor executor, ProgressListener listener) throws ErrnoException { long progress = 0; long checkpoint = 0; @@ -437,33 +476,52 @@ public class FileUtils { if (signal != null) { signal.throwIfCanceled(); } - if (listener != null) { - listener.onProgress(progress); + if (executor != null && listener != null) { + final long progressSnapshot = progress; + executor.execute(() -> { + listener.onProgress(progressSnapshot); + }); } checkpoint = 0; } } - if (listener != null) { - listener.onProgress(progress); + if (executor != null && listener != null) { + final long progressSnapshot = progress; + executor.execute(() -> { + listener.onProgress(progressSnapshot); + }); } return progress; } + /** {@hide} */ + @Deprecated @VisibleForTesting public static long copyInternalUserspace(FileDescriptor in, FileDescriptor out, - ProgressListener listener, CancellationSignal signal, long count) throws IOException { + ProgressListener listener, CancellationSignal signal, long count) + throws IOException { + return copyInternalUserspace(in, out, count, signal, Runnable::run, listener); + } + + /** {@hide} */ + @VisibleForTesting + public static long copyInternalUserspace(FileDescriptor in, FileDescriptor out, long count, + CancellationSignal signal, Executor executor, ProgressListener listener) + throws IOException { if (count != Long.MAX_VALUE) { return copyInternalUserspace(new SizedInputStream(new FileInputStream(in), count), - new FileOutputStream(out), listener, signal); + new FileOutputStream(out), signal, executor, listener); } else { return copyInternalUserspace(new FileInputStream(in), - new FileOutputStream(out), listener, signal); + new FileOutputStream(out), signal, executor, listener); } } + /** {@hide} */ @VisibleForTesting public static long copyInternalUserspace(InputStream in, OutputStream out, - ProgressListener listener, CancellationSignal signal) throws IOException { + CancellationSignal signal, Executor executor, ProgressListener listener) + throws IOException { long progress = 0; long checkpoint = 0; byte[] buffer = new byte[8192]; @@ -479,14 +537,20 @@ public class FileUtils { if (signal != null) { signal.throwIfCanceled(); } - if (listener != null) { - listener.onProgress(progress); + if (executor != null && listener != null) { + final long progressSnapshot = progress; + executor.execute(() -> { + listener.onProgress(progressSnapshot); + }); } checkpoint = 0; } } - if (listener != null) { - listener.onProgress(progress); + if (executor != null && listener != null) { + final long progressSnapshot = progress; + executor.execute(() -> { + listener.onProgress(progressSnapshot); + }); } return progress; } @@ -494,6 +558,7 @@ public class FileUtils { /** * Check if a filename is "safe" (no metacharacters or spaces). * @param file The file to check + * @hide */ public static boolean isFilenameSafe(File file) { // Note, we check whether it matches what's known to be safe, @@ -509,6 +574,7 @@ public class FileUtils { * @param ellipsis to add of the file was truncated (can be null) * @return the contents of the file, possibly truncated * @throws IOException if something goes wrong reading the file + * @hide */ public static String readTextFile(File file, int max, String ellipsis) throws IOException { InputStream input = new FileInputStream(file); @@ -563,13 +629,16 @@ public class FileUtils { } } + /** {@hide} */ public static void stringToFile(File file, String string) throws IOException { stringToFile(file.getAbsolutePath(), string); } - /* + /** * Writes the bytes given in {@code content} to the file whose absolute path * is {@code filename}. + * + * @hide */ public static void bytesToFile(String filename, byte[] content) throws IOException { if (filename.startsWith("/proc/")) { @@ -592,18 +661,23 @@ public class FileUtils { * @param filename * @param string * @throws IOException + * @hide */ public static void stringToFile(String filename, String string) throws IOException { bytesToFile(filename, string.getBytes(StandardCharsets.UTF_8)); } /** - * Computes the checksum of a file using the CRC32 checksum routine. - * The value of the checksum is returned. + * Computes the checksum of a file using the CRC32 checksum routine. The + * value of the checksum is returned. * - * @param file the file to checksum, must not be null + * @param file the file to checksum, must not be null * @return the checksum value or an exception is thrown. + * @deprecated this is a weak hashing algorithm, and should not be used due + * to its potential for collision. + * @hide */ + @Deprecated public static long checksumCrc32(File file) throws FileNotFoundException, IOException { CRC32 checkSummer = new CRC32(); CheckedInputStream cis = null; @@ -632,6 +706,7 @@ public class FileUtils { * @param minCount Always keep at least this many files. * @param minAgeMs Always keep files younger than this age, in milliseconds. * @return if any files were deleted. + * @hide */ public static boolean deleteOlderFiles(File dir, int minCount, long minAgeMs) { if (minCount < 0 || minAgeMs < 0) { @@ -673,6 +748,8 @@ public class FileUtils { * Both files <em>must</em> have been resolved using * {@link File#getCanonicalFile()} to avoid symlink or path traversal * attacks. + * + * @hide */ public static boolean contains(File[] dirs, File file) { for (File dir : dirs) { @@ -690,12 +767,15 @@ public class FileUtils { * Both files <em>must</em> have been resolved using * {@link File#getCanonicalFile()} to avoid symlink or path traversal * attacks. + * + * @hide */ public static boolean contains(File dir, File file) { if (dir == null || file == null) return false; return contains(dir.getAbsolutePath(), file.getAbsolutePath()); } + /** {@hide} */ public static boolean contains(String dirPath, String filePath) { if (dirPath.equals(filePath)) { return true; @@ -706,6 +786,7 @@ public class FileUtils { return filePath.startsWith(dirPath); } + /** {@hide} */ public static boolean deleteContentsAndDir(File dir) { if (deleteContents(dir)) { return dir.delete(); @@ -714,6 +795,7 @@ public class FileUtils { } } + /** {@hide} */ public static boolean deleteContents(File dir) { File[] files = dir.listFiles(); boolean success = true; @@ -743,6 +825,8 @@ public class FileUtils { /** * Check if given filename is valid for an ext4 filesystem. + * + * @hide */ public static boolean isValidExtFilename(String name) { return (name != null) && name.equals(buildValidExtFilename(name)); @@ -751,6 +835,8 @@ public class FileUtils { /** * Mutate the given filename to make it valid for an ext4 filesystem, * replacing any invalid characters with "_". + * + * @hide */ public static String buildValidExtFilename(String name) { if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) { @@ -792,6 +878,8 @@ public class FileUtils { /** * Check if given filename is valid for a FAT filesystem. + * + * @hide */ public static boolean isValidFatFilename(String name) { return (name != null) && name.equals(buildValidFatFilename(name)); @@ -800,6 +888,8 @@ public class FileUtils { /** * Mutate the given filename to make it valid for a FAT filesystem, * replacing any invalid characters with "_". + * + * @hide */ public static String buildValidFatFilename(String name) { if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) { @@ -820,6 +910,7 @@ public class FileUtils { return res.toString(); } + /** {@hide} */ @VisibleForTesting public static String trimFilename(String str, int maxBytes) { final StringBuilder res = new StringBuilder(str); @@ -827,6 +918,7 @@ public class FileUtils { return res.toString(); } + /** {@hide} */ private static void trimFilename(StringBuilder res, int maxBytes) { byte[] raw = res.toString().getBytes(StandardCharsets.UTF_8); if (raw.length > maxBytes) { @@ -839,12 +931,14 @@ public class FileUtils { } } + /** {@hide} */ public static String rewriteAfterRename(File beforeDir, File afterDir, String path) { if (path == null) return null; final File result = rewriteAfterRename(beforeDir, afterDir, new File(path)); return (result != null) ? result.getAbsolutePath() : null; } + /** {@hide} */ public static String[] rewriteAfterRename(File beforeDir, File afterDir, String[] paths) { if (paths == null) return null; final String[] result = new String[paths.length]; @@ -858,6 +952,8 @@ public class FileUtils { * Given a path under the "before" directory, rewrite it to live under the * "after" directory. For example, {@code /before/foo/bar.txt} would become * {@code /after/foo/bar.txt}. + * + * @hide */ public static File rewriteAfterRename(File beforeDir, File afterDir, File file) { if (file == null || beforeDir == null || afterDir == null) return null; @@ -869,6 +965,7 @@ public class FileUtils { return null; } + /** {@hide} */ private static File buildUniqueFileWithExtension(File parent, String name, String ext) throws FileNotFoundException { File file = buildFile(parent, name, ext); @@ -895,6 +992,7 @@ public class FileUtils { * 'example.txt' or 'example (1).txt', etc. * * @throws FileNotFoundException + * @hide */ public static File buildUniqueFile(File parent, String mimeType, String displayName) throws FileNotFoundException { @@ -905,6 +1003,8 @@ public class FileUtils { /** * Generates a unique file name under the given parent directory, keeping * any extension intact. + * + * @hide */ public static File buildUniqueFile(File parent, String displayName) throws FileNotFoundException { @@ -929,6 +1029,8 @@ public class FileUtils { * If the display name doesn't have an extension that matches the requested MIME type, the * extension is regarded as a part of filename and default extension for that MIME type is * appended. + * + * @hide */ public static String[] splitFileName(String mimeType, String displayName) { String name; @@ -975,6 +1077,7 @@ public class FileUtils { return new String[] { name, ext }; } + /** {@hide} */ private static File buildFile(File parent, String name, String ext) { if (TextUtils.isEmpty(ext)) { return new File(parent, name); @@ -983,6 +1086,7 @@ public class FileUtils { } } + /** {@hide} */ public static @NonNull String[] listOrEmpty(@Nullable File dir) { if (dir == null) return EmptyArray.STRING; final String[] res = dir.list(); @@ -993,6 +1097,7 @@ public class FileUtils { } } + /** {@hide} */ public static @NonNull File[] listFilesOrEmpty(@Nullable File dir) { if (dir == null) return EMPTY; final File[] res = dir.listFiles(); @@ -1003,6 +1108,7 @@ public class FileUtils { } } + /** {@hide} */ public static @NonNull File[] listFilesOrEmpty(@Nullable File dir, FilenameFilter filter) { if (dir == null) return EMPTY; final File[] res = dir.listFiles(filter); @@ -1013,6 +1119,7 @@ public class FileUtils { } } + /** {@hide} */ public static @Nullable File newFileOrNull(@Nullable String path) { return (path != null) ? new File(path) : null; } @@ -1021,6 +1128,8 @@ public class FileUtils { * Creates a directory with name {@code name} under an existing directory {@code baseDir}. * Returns a {@code File} object representing the directory on success, {@code null} on * failure. + * + * @hide */ public static @Nullable File createDir(File baseDir, String name) { final File dir = new File(baseDir, name); @@ -1036,6 +1145,8 @@ public class FileUtils { * Round the given size of a storage device to a nice round power-of-two * value, such as 256MB or 32GB. This avoids showing weird values like * "29.5GB" in UI. + * + * @hide */ public static long roundStorageSize(long size) { long val = 1; @@ -1050,6 +1161,23 @@ public class FileUtils { return val * pow; } + /** + * Closes the given object quietly, ignoring any checked exceptions. Does + * nothing if the given object is {@code null}. + */ + public static void closeQuietly(@Nullable AutoCloseable closeable) { + IoUtils.closeQuietly(closeable); + } + + /** + * Closes the given object quietly, ignoring any checked exceptions. Does + * nothing if the given object is {@code null}. + */ + public static void closeQuietly(@Nullable FileDescriptor fd) { + IoUtils.closeQuietly(fd); + } + + /** {@hide} */ @VisibleForTesting public static class MemoryPipe extends Thread implements AutoCloseable { private final FileDescriptor[] pipe; diff --git a/core/java/android/print/PrintFileDocumentAdapter.java b/core/java/android/print/PrintFileDocumentAdapter.java index a5f93050e307..eb4b31510880 100644 --- a/core/java/android/print/PrintFileDocumentAdapter.java +++ b/core/java/android/print/PrintFileDocumentAdapter.java @@ -118,7 +118,7 @@ public class PrintFileDocumentAdapter extends PrintDocumentAdapter { protected Void doInBackground(Void... params) { try (InputStream in = new FileInputStream(mFile); OutputStream out = new FileOutputStream(mDestination.getFileDescriptor())) { - FileUtils.copy(in, out, null, mCancellationSignal); + FileUtils.copy(in, out, mCancellationSignal, null, null); } catch (OperationCanceledException e) { // Ignored; already handled below } catch (IOException e) { diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java index a80dcedf9915..f97c64c96a7a 100644 --- a/core/java/android/provider/DocumentsContract.java +++ b/core/java/android/provider/DocumentsContract.java @@ -100,7 +100,8 @@ public final class DocumentsContract { public static final String PROVIDER_INTERFACE = "android.content.action.DOCUMENTS_PROVIDER"; /** {@hide} */ - public static final String EXTRA_PACKAGE_NAME = "android.content.extra.PACKAGE_NAME"; + @Deprecated + public static final String EXTRA_PACKAGE_NAME = Intent.EXTRA_PACKAGE_NAME; /** {@hide} */ public static final String EXTRA_SHOW_ADVANCED = "android.content.extra.SHOW_ADVANCED"; diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 82b66d7b7925..a6a6f35d5ea9 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -4161,7 +4161,8 @@ public final class Settings { NOTIFICATION_VIBRATION_INTENSITY, HAPTIC_FEEDBACK_INTENSITY, DISPLAY_COLOR_MODE, - ALARM_ALERT + ALARM_ALERT, + NOTIFICATION_LIGHT_PULSE, }; /** @@ -4364,6 +4365,7 @@ public final class Settings { VALIDATORS.put(WIFI_STATIC_DNS1, WIFI_STATIC_DNS1_VALIDATOR); VALIDATORS.put(WIFI_STATIC_DNS2, WIFI_STATIC_DNS2_VALIDATOR); VALIDATORS.put(SHOW_BATTERY_PERCENT, SHOW_BATTERY_PERCENT_VALIDATOR); + VALIDATORS.put(NOTIFICATION_LIGHT_PULSE, BOOLEAN_VALIDATOR); } /** @@ -8019,6 +8021,8 @@ public final class Settings { MANUAL_RINGER_TOGGLE_COUNT, HUSH_GESTURE_USED, IN_CALL_NOTIFICATION_ENABLED, + LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, + LOCK_SCREEN_SHOW_NOTIFICATIONS, }; /** @@ -8161,6 +8165,8 @@ public final class Settings { VALIDATORS.put(HUSH_GESTURE_USED, HUSH_GESTURE_USED_VALIDATOR); VALIDATORS.put(MANUAL_RINGER_TOGGLE_COUNT, MANUAL_RINGER_TOGGLE_COUNT_VALIDATOR); VALIDATORS.put(IN_CALL_NOTIFICATION_ENABLED, IN_CALL_NOTIFICATION_ENABLED_VALIDATOR); + VALIDATORS.put(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, BOOLEAN_VALIDATOR); + VALIDATORS.put(LOCK_SCREEN_SHOW_NOTIFICATIONS, BOOLEAN_VALIDATOR); } /** diff --git a/core/java/android/service/notification/Adjustment.java b/core/java/android/service/notification/Adjustment.java index 7348cf6848f9..0d94af4bc828 100644 --- a/core/java/android/service/notification/Adjustment.java +++ b/core/java/android/service/notification/Adjustment.java @@ -65,6 +65,12 @@ public final class Adjustment implements Parcelable { public static final String KEY_USER_SENTIMENT = "key_user_sentiment"; /** + * Data type: ArrayList of {@link android.app.Notification.Action}. + * Used to suggest extra actions for a notification. + */ + public static final String KEY_SMART_ACTIONS = "key_smart_actions"; + + /** * Create a notification adjustment. * * @param pkg The package of the notification. diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java index a7d70d01b04a..09425a9c5d28 100644 --- a/core/java/android/service/notification/NotificationListenerService.java +++ b/core/java/android/service/notification/NotificationListenerService.java @@ -1426,6 +1426,7 @@ public abstract class NotificationListenerService extends Service { private boolean mShowBadge; private @UserSentiment int mUserSentiment = USER_SENTIMENT_NEUTRAL; private boolean mHidden; + private ArrayList<Notification.Action> mSmartActions; public Ranking() {} @@ -1556,6 +1557,13 @@ public abstract class NotificationListenerService extends Service { } /** + * @hide + */ + public List<Notification.Action> getSmartActions() { + return mSmartActions; + } + + /** * Returns whether this notification can be displayed as a badge. * * @return true if the notification can be displayed as a badge, false otherwise. @@ -1583,7 +1591,7 @@ public abstract class NotificationListenerService extends Service { CharSequence explanation, String overrideGroupKey, NotificationChannel channel, ArrayList<String> overridePeople, ArrayList<SnoozeCriterion> snoozeCriteria, boolean showBadge, - int userSentiment, boolean hidden) { + int userSentiment, boolean hidden, ArrayList<Notification.Action> smartActions) { mKey = key; mRank = rank; mIsAmbient = importance < NotificationManager.IMPORTANCE_LOW; @@ -1599,6 +1607,7 @@ public abstract class NotificationListenerService extends Service { mShowBadge = showBadge; mUserSentiment = userSentiment; mHidden = hidden; + mSmartActions = smartActions; } /** @@ -1648,6 +1657,7 @@ public abstract class NotificationListenerService extends Service { private ArrayMap<String, Boolean> mShowBadge; private ArrayMap<String, Integer> mUserSentiment; private ArrayMap<String, Boolean> mHidden; + private ArrayMap<String, ArrayList<Notification.Action>> mSmartActions; private RankingMap(NotificationRankingUpdate rankingUpdate) { mRankingUpdate = rankingUpdate; @@ -1676,7 +1686,7 @@ public abstract class NotificationListenerService extends Service { getVisibilityOverride(key), getSuppressedVisualEffects(key), getImportance(key), getImportanceExplanation(key), getOverrideGroupKey(key), getChannel(key), getOverridePeople(key), getSnoozeCriteria(key), - getShowBadge(key), getUserSentiment(key), getHidden(key)); + getShowBadge(key), getUserSentiment(key), getHidden(key), getSmartActions(key)); return rank >= 0; } @@ -1814,6 +1824,15 @@ public abstract class NotificationListenerService extends Service { return hidden == null ? false : hidden.booleanValue(); } + private ArrayList<Notification.Action> getSmartActions(String key) { + synchronized (this) { + if (mSmartActions == null) { + buildSmartActions(); + } + } + return mSmartActions.get(key); + } + // Locked by 'this' private void buildRanksLocked() { String[] orderedKeys = mRankingUpdate.getOrderedKeys(); @@ -1931,6 +1950,15 @@ public abstract class NotificationListenerService extends Service { } } + // Locked by 'this' + private void buildSmartActions() { + Bundle smartActions = mRankingUpdate.getSmartActions(); + mSmartActions = new ArrayMap<>(smartActions.size()); + for (String key : smartActions.keySet()) { + mSmartActions.put(key, smartActions.getParcelableArrayList(key)); + } + } + // ----------- Parcelable @Override diff --git a/core/java/android/service/notification/NotificationRankingUpdate.java b/core/java/android/service/notification/NotificationRankingUpdate.java index 00c47ec0ee89..bed221494d4e 100644 --- a/core/java/android/service/notification/NotificationRankingUpdate.java +++ b/core/java/android/service/notification/NotificationRankingUpdate.java @@ -37,12 +37,13 @@ public class NotificationRankingUpdate implements Parcelable { private final Bundle mShowBadge; private final Bundle mUserSentiment; private final Bundle mHidden; + private final Bundle mSmartActions; public NotificationRankingUpdate(String[] keys, String[] interceptedKeys, Bundle visibilityOverrides, Bundle suppressedVisualEffects, int[] importance, Bundle explanation, Bundle overrideGroupKeys, Bundle channels, Bundle overridePeople, Bundle snoozeCriteria, - Bundle showBadge, Bundle userSentiment, Bundle hidden) { + Bundle showBadge, Bundle userSentiment, Bundle hidden, Bundle smartActions) { mKeys = keys; mInterceptedKeys = interceptedKeys; mVisibilityOverrides = visibilityOverrides; @@ -56,6 +57,7 @@ public class NotificationRankingUpdate implements Parcelable { mShowBadge = showBadge; mUserSentiment = userSentiment; mHidden = hidden; + mSmartActions = smartActions; } public NotificationRankingUpdate(Parcel in) { @@ -73,6 +75,7 @@ public class NotificationRankingUpdate implements Parcelable { mShowBadge = in.readBundle(); mUserSentiment = in.readBundle(); mHidden = in.readBundle(); + mSmartActions = in.readBundle(); } @Override @@ -95,6 +98,7 @@ public class NotificationRankingUpdate implements Parcelable { out.writeBundle(mShowBadge); out.writeBundle(mUserSentiment); out.writeBundle(mHidden); + out.writeBundle(mSmartActions); } public static final Parcelable.Creator<NotificationRankingUpdate> CREATOR @@ -159,4 +163,8 @@ public class NotificationRankingUpdate implements Parcelable { public Bundle getHidden() { return mHidden; } + + public Bundle getSmartActions() { + return mSmartActions; + } } diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index 5546e803342b..df88e642a970 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -1007,6 +1007,24 @@ public class ZenModeConfig implements Parcelable { return true; } + /** + * Returns whether the conditionId is a valid ScheduleCondition. + * If allowNever is true, this will return true even if the ScheduleCondition never occurs. + */ + public static boolean isValidScheduleConditionId(Uri conditionId, boolean allowNever) { + ScheduleInfo info; + try { + info = tryParseScheduleConditionId(conditionId); + } catch (NullPointerException | ArrayIndexOutOfBoundsException e) { + return false; + } + + if (info == null || (!allowNever && (info.days == null || info.days.length == 0))) { + return false; + } + return true; + } + public static ScheduleInfo tryParseScheduleConditionId(Uri conditionId) { final boolean isSchedule = conditionId != null && conditionId.getScheme().equals(Condition.SCHEME) diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java index 6b2f80241df3..dde4c1d2801f 100644 --- a/core/java/android/text/TextUtils.java +++ b/core/java/android/text/TextUtils.java @@ -2091,6 +2091,25 @@ public class TextUtils { return (T) text.subSequence(0, size); } + /** + * Trims the {@code text} to the first {@code size} characters and adds an ellipsis if the + * resulting string is shorter than the input. This will result in an output string which is + * longer than {@code size} for most inputs. + * + * @param size length of the result, should be greater than 0 + * + * @hide + */ + @Nullable + public static <T extends CharSequence> T trimToLengthWithEllipsis(@Nullable T text, + @IntRange(from = 1) int size) { + T trimmed = trimToSize(text, size); + if (trimmed.length() < text.length()) { + trimmed = (T) (trimmed.toString() + "..."); + } + return trimmed; + } + private static Object sLock = new Object(); private static char[] sTemp = null; diff --git a/core/java/android/text/util/Linkify.java b/core/java/android/text/util/Linkify.java index f3d39de18507..08cbbe628eeb 100644 --- a/core/java/android/text/util/Linkify.java +++ b/core/java/android/text/util/Linkify.java @@ -35,6 +35,7 @@ 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.annotations.GuardedBy; import libcore.util.EmptyArray; @@ -63,6 +64,10 @@ import java.util.regex.Pattern; * does not have a URL scheme prefix, the supplied scheme will be prepended to * create <code>http://example.com</code> when the clickable URL link is * created. + * + * @see MatchFilter + * @see TransformFilter + * @see UrlSpanFactory */ public class Linkify { @@ -218,6 +223,44 @@ public class Linkify { } /** + * Factory class to create {@link URLSpan}s. While adding spans to a {@link Spannable}, + * {@link Linkify} will call {@link UrlSpanFactory#create(String)} function to create a + * {@link URLSpan}. + * + * @see #addLinks(Spannable, int, UrlSpanFactory) + * @see #addLinks(Spannable, Pattern, String, String[], MatchFilter, TransformFilter, + * UrlSpanFactory) + */ + public static class UrlSpanFactory { + private static final Object sInstanceLock = new Object(); + + @GuardedBy("sInstanceLock") + private static volatile UrlSpanFactory sInstance = null; + + private static synchronized UrlSpanFactory getInstance() { + if (sInstance == null) { + synchronized (sInstanceLock) { + if (sInstance == null) { + sInstance = new UrlSpanFactory(); + } + } + } + return sInstance; + } + + /** + * Factory function that will called by {@link Linkify} in order to create a + * {@link URLSpan}. + * + * @param url URL found + * @return a URLSpan instance + */ + public URLSpan create(final String url) { + return new URLSpan(url); + } + } + + /** * Scans the text of the provided Spannable and turns all occurrences * of the link types indicated in the mask into clickable links. * If the mask is nonzero, it also removes any existing URLSpans @@ -228,24 +271,55 @@ public class Linkify { * @param mask Mask to define which kinds of links will be searched. * * @return True if at least one link is found and applied. + * + * @see #addLinks(Spannable, int, UrlSpanFactory) */ public static final boolean addLinks(@NonNull Spannable text, @LinkifyMask int mask) { - return addLinks(text, mask, null); + return addLinks(text, mask, null, null); } + /** + * Scans the text of the provided Spannable and turns all occurrences + * of the link types indicated in the mask into clickable links. + * If the mask is nonzero, it also removes any existing URLSpans + * attached to the Spannable, to avoid problems if you call it + * repeatedly on the same text. + * + * @param text Spannable whose text is to be marked-up with links + * @param mask mask to define which kinds of links will be searched + * @param urlSpanFactory factory class used to create {@link URLSpan}s + * @return True if at least one link is found and applied. + */ + public static final boolean addLinks(@NonNull Spannable text, @LinkifyMask int mask, + @Nullable UrlSpanFactory urlSpanFactory) { + return addLinks(text, mask, null, urlSpanFactory); + } + + /** + * Scans the text of the provided Spannable and turns all occurrences of the link types + * indicated in the mask into clickable links. If the mask is nonzero, it also removes any + * existing URLSpans attached to the Spannable, to avoid problems if you call it repeatedly + * on the same text. + * + * @param text Spannable whose text is to be marked-up with links + * @param mask mask to define which kinds of links will be searched + * @param context Context to be used while identifying phone numbers + * @param urlSpanFactory factory class used to create {@link URLSpan}s + * @return true if at least one link is found and applied. + */ private static boolean addLinks(@NonNull Spannable text, @LinkifyMask int mask, - @Nullable Context context) { + @Nullable Context context, @Nullable UrlSpanFactory urlSpanFactory) { if (mask == 0) { return false; } - URLSpan[] old = text.getSpans(0, text.length(), URLSpan.class); + final URLSpan[] old = text.getSpans(0, text.length(), URLSpan.class); for (int i = old.length - 1; i >= 0; i--) { text.removeSpan(old[i]); } - ArrayList<LinkSpec> links = new ArrayList<LinkSpec>(); + final ArrayList<LinkSpec> links = new ArrayList<LinkSpec>(); if ((mask & WEB_URLS) != 0) { gatherLinks(links, text, Patterns.AUTOLINK_WEB_URL, @@ -274,7 +348,7 @@ public class Linkify { } for (LinkSpec link: links) { - applyLink(link.url, link.start, link.end, text); + applyLink(link.url, link.start, link.end, text, urlSpanFactory); } return true; @@ -290,6 +364,8 @@ public class Linkify { * @param mask Mask to define which kinds of links will be searched. * * @return True if at least one link is found and applied. + * + * @see #addLinks(Spannable, int, UrlSpanFactory) */ public static final boolean addLinks(@NonNull TextView text, @LinkifyMask int mask) { if (mask == 0) { @@ -299,7 +375,7 @@ public class Linkify { final Context context = text.getContext(); final CharSequence t = text.getText(); if (t instanceof Spannable) { - if (addLinks((Spannable) t, mask, context)) { + if (addLinks((Spannable) t, mask, context, null)) { addLinkMovementMethod(text); return true; } @@ -308,7 +384,7 @@ public class Linkify { } else { SpannableString s = SpannableString.valueOf(t); - if (addLinks(s, mask, context)) { + if (addLinks(s, mask, context, null)) { addLinkMovementMethod(text); text.setText(s); @@ -403,6 +479,8 @@ public class Linkify { * @param pattern Regex pattern to be used for finding links * @param scheme URL scheme string (eg <code>http://</code>) to be * prepended to the links that do not start with this scheme. + * @see #addLinks(Spannable, Pattern, String, String[], MatchFilter, TransformFilter, + * UrlSpanFactory) */ public static final boolean addLinks(@NonNull Spannable text, @NonNull Pattern pattern, @Nullable String scheme) { @@ -423,6 +501,8 @@ public class Linkify { * @param transformFilter Filter to allow the client code to update the link found. * * @return True if at least one link is found and applied. + * @see #addLinks(Spannable, Pattern, String, String[], MatchFilter, TransformFilter, + * UrlSpanFactory) */ public static final boolean addLinks(@NonNull Spannable spannable, @NonNull Pattern pattern, @Nullable String scheme, @Nullable MatchFilter matchFilter, @@ -446,10 +526,39 @@ public class Linkify { * @param transformFilter Filter to allow the client code to update the link found. * * @return True if at least one link is found and applied. + * + * @see #addLinks(Spannable, Pattern, String, String[], MatchFilter, TransformFilter, + * UrlSpanFactory) */ public static final boolean addLinks(@NonNull Spannable spannable, @NonNull Pattern pattern, - @Nullable String defaultScheme, @Nullable String[] schemes, + @Nullable String defaultScheme, @Nullable String[] schemes, @Nullable MatchFilter matchFilter, @Nullable TransformFilter transformFilter) { + return addLinks(spannable, pattern, defaultScheme, schemes, matchFilter, transformFilter, + null); + } + + /** + * Applies a regex to a Spannable turning the matches into links. + * + * @param spannable spannable whose text is to be marked-up with links. + * @param pattern regex pattern to be used for finding links. + * @param defaultScheme the default scheme to be prepended to links if the link does not + * start with one of the <code>schemes</code> given. + * @param schemes array of schemes (eg <code>http://</code>) to check if the link found + * contains a scheme. Passing a null or empty value means prepend + * defaultScheme + * to all links. + * @param matchFilter the filter that is used to allow the client code additional control + * over which pattern matches are to be converted into links. + * @param transformFilter filter to allow the client code to update the link found. + * @param urlSpanFactory factory class used to create {@link URLSpan}s + * + * @return True if at least one link is found and applied. + */ + public static final boolean addLinks(@NonNull Spannable spannable, @NonNull Pattern pattern, + @Nullable String defaultScheme, @Nullable String[] schemes, + @Nullable MatchFilter matchFilter, @Nullable TransformFilter transformFilter, + @Nullable UrlSpanFactory urlSpanFactory) { final String[] schemesCopy; if (defaultScheme == null) defaultScheme = ""; if (schemes == null || schemes.length < 1) { @@ -478,7 +587,7 @@ public class Linkify { if (allowed) { String url = makeUrl(m.group(0), schemesCopy, m, transformFilter); - applyLink(url, start, end, spannable); + applyLink(url, start, end, spannable, urlSpanFactory); hasMatches = true; } } @@ -486,9 +595,12 @@ public class Linkify { return hasMatches; } - private static final void applyLink(String url, int start, int end, Spannable text) { - URLSpan span = new URLSpan(url); - + private static void applyLink(String url, int start, int end, Spannable text, + @Nullable UrlSpanFactory urlSpanFactory) { + if (urlSpanFactory == null) { + urlSpanFactory = UrlSpanFactory.getInstance(); + } + final URLSpan span = urlSpanFactory.create(url); text.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } diff --git a/core/java/android/util/MathUtils.java b/core/java/android/util/MathUtils.java index b2e24c357e3c..72865ccdc71a 100644 --- a/core/java/android/util/MathUtils.java +++ b/core/java/android/util/MathUtils.java @@ -187,6 +187,21 @@ public final class MathUtils { } /** + * Perform Hermite interpolation between two values. + * Eg: + * smoothStep(0, 0.5f, 0.5f) = 1f + * smoothStep(0, 0.5f, 0.25f) = 0.5f + * + * @param start Left edge. + * @param end Right edge. + * @param x A value between {@code start} and {@code end}. + * @return A number between 0 and 1 representing where {@code x} is in the interpolation. + */ + public static float smoothStep(float start, float end, float x) { + return constrain((x - start) / (end - start), 0f, 1f); + } + + /** * Returns the sum of the two parameters, or throws an exception if the resulting sum would * cause an overflow or underflow. * @throws IllegalArgumentException when overflow or underflow would occur. diff --git a/core/java/android/util/Xml.java b/core/java/android/util/Xml.java index 041e8a85aafe..e3b8fec3559e 100644 --- a/core/java/android/util/Xml.java +++ b/core/java/android/util/Xml.java @@ -16,27 +16,27 @@ package android.util; -import java.io.IOException; -import java.io.InputStream; -import java.io.Reader; -import java.io.StringReader; -import java.io.UnsupportedEncodingException; -import org.apache.harmony.xml.ExpatReader; -import org.kxml2.io.KXmlParser; +import libcore.util.XmlObjectFactory; + import org.xml.sax.ContentHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlPullParserFactory; import org.xmlpull.v1.XmlSerializer; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; +import java.io.UnsupportedEncodingException; + /** * XML utility methods. */ public class Xml { - /** @hide */ public Xml() {} + private Xml() {} /** * {@link org.xmlpull.v1.XmlPullParser} "relaxed" feature name. @@ -52,7 +52,7 @@ public class Xml { public static void parse(String xml, ContentHandler contentHandler) throws SAXException { try { - XMLReader reader = new ExpatReader(); + XMLReader reader = XmlObjectFactory.newXMLReader(); reader.setContentHandler(contentHandler); reader.parse(new InputSource(new StringReader(xml))); } catch (IOException e) { @@ -66,7 +66,7 @@ public class Xml { */ public static void parse(Reader in, ContentHandler contentHandler) throws IOException, SAXException { - XMLReader reader = new ExpatReader(); + XMLReader reader = XmlObjectFactory.newXMLReader(); reader.setContentHandler(contentHandler); reader.parse(new InputSource(in)); } @@ -77,7 +77,7 @@ public class Xml { */ public static void parse(InputStream in, Encoding encoding, ContentHandler contentHandler) throws IOException, SAXException { - XMLReader reader = new ExpatReader(); + XMLReader reader = XmlObjectFactory.newXMLReader(); reader.setContentHandler(contentHandler); InputSource source = new InputSource(in); source.setEncoding(encoding.expatName); @@ -89,7 +89,7 @@ public class Xml { */ public static XmlPullParser newPullParser() { try { - KXmlParser parser = new KXmlParser(); + XmlPullParser parser = XmlObjectFactory.newXmlPullParser(); parser.setFeature(XmlPullParser.FEATURE_PROCESS_DOCDECL, true); parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); return parser; @@ -102,25 +102,7 @@ public class Xml { * Creates a new xml serializer. */ public static XmlSerializer newSerializer() { - try { - return XmlSerializerFactory.instance.newSerializer(); - } catch (XmlPullParserException e) { - throw new AssertionError(e); - } - } - - /** Factory for xml serializers. Initialized on demand. */ - static class XmlSerializerFactory { - static final String TYPE - = "org.kxml2.io.KXmlParser,org.kxml2.io.KXmlSerializer"; - static final XmlPullParserFactory instance; - static { - try { - instance = XmlPullParserFactory.newInstance(TYPE, null); - } catch (XmlPullParserException e) { - throw new AssertionError(e); - } - } + return XmlObjectFactory.newXmlSerializer(); } /** diff --git a/core/java/android/view/RenderNode.java b/core/java/android/view/RenderNode.java index e10eeb0457be..e0df59f57b31 100644 --- a/core/java/android/view/RenderNode.java +++ b/core/java/android/view/RenderNode.java @@ -16,6 +16,7 @@ package android.view; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Matrix; @@ -29,6 +30,9 @@ import dalvik.annotation.optimization.FastNative; import libcore.util.NativeAllocationRegistry; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * <p>A display list records a series of graphics related operations and can replay * them later. Display lists are usually built by recording operations on a @@ -449,6 +453,25 @@ public class RenderNode { return nSetHasOverlappingRendering(mNativeRenderNode, hasOverlappingRendering); } + /** @hide */ + @IntDef({USAGE_BACKGROUND}) + @Retention(RetentionPolicy.SOURCE) + public @interface UsageHint {} + + /** The default usage hint */ + public static final int USAGE_UNKNOWN = 0; + + /** Usage is background content */ + public static final int USAGE_BACKGROUND = 1; + + /** + * Provides a hint on what this RenderNode's display list content contains. This hint is used + * for automatic content transforms to improve accessibility or similar. + */ + public void setUsageHint(@UsageHint int usageHint) { + nSetUsageHint(mNativeRenderNode, usageHint); + } + /** * Indicates whether the content of this display list overlaps. * @@ -948,6 +971,8 @@ public class RenderNode { private static native boolean nSetHasOverlappingRendering(long renderNode, boolean hasOverlappingRendering); @CriticalNative + private static native void nSetUsageHint(long renderNode, int usageHint); + @CriticalNative private static native boolean nSetElevation(long renderNode, float lift); @CriticalNative private static native boolean nSetTranslationX(long renderNode, float translationX); diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 546ea87e23a2..e2e2b000d2fd 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -7547,7 +7547,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) { if ((event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) - && !TextUtils.isEmpty(getAccessibilityPaneTitle())) { + && isAccessibilityPane()) { event.getText().add(getAccessibilityPaneTitle()); } } @@ -12963,7 +12963,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } } } - if (!TextUtils.isEmpty(getAccessibilityPaneTitle())) { + if (isAccessibilityPane()) { if (isVisible != oldVisible) { notifyViewAccessibilityStateChangedIfNeeded(isVisible ? AccessibilityEvent.CONTENT_CHANGE_TYPE_PANE_APPEARED @@ -20442,6 +20442,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private RenderNode getDrawableRenderNode(Drawable drawable, RenderNode renderNode) { if (renderNode == null) { renderNode = RenderNode.create(drawable.getClass().getName(), this); + renderNode.setUsageHint(RenderNode.USAGE_BACKGROUND); } final Rect bounds = drawable.getBounds(); diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java index e8e653759a97..eee3630c2266 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -601,22 +601,14 @@ public class AccessibilityNodeInfo implements Parcelable { private static final int BOOLEAN_PROPERTY_CHECKED = 0x00000002; - private static final int BOOLEAN_PROPERTY_FOCUSABLE = 0x00000004; - private static final int BOOLEAN_PROPERTY_FOCUSED = 0x00000008; private static final int BOOLEAN_PROPERTY_SELECTED = 0x00000010; - private static final int BOOLEAN_PROPERTY_CLICKABLE = 0x00000020; - - private static final int BOOLEAN_PROPERTY_LONG_CLICKABLE = 0x00000040; - private static final int BOOLEAN_PROPERTY_ENABLED = 0x00000080; private static final int BOOLEAN_PROPERTY_PASSWORD = 0x00000100; - private static final int BOOLEAN_PROPERTY_SCROLLABLE = 0x00000200; - private static final int BOOLEAN_PROPERTY_ACCESSIBILITY_FOCUSED = 0x00000400; private static final int BOOLEAN_PROPERTY_VISIBLE_TO_USER = 0x00000800; @@ -631,8 +623,6 @@ public class AccessibilityNodeInfo implements Parcelable { private static final int BOOLEAN_PROPERTY_CONTENT_INVALID = 0x00010000; - private static final int BOOLEAN_PROPERTY_CONTEXT_CLICKABLE = 0x00020000; - private static final int BOOLEAN_PROPERTY_IMPORTANCE = 0x0040000; private static final int BOOLEAN_PROPERTY_SCREEN_READER_FOCUSABLE = 0x0080000; @@ -1168,6 +1158,10 @@ public class AccessibilityNodeInfo implements Parcelable { mActions.add(action); } + private boolean hasActionWithId(int actionId) { + return getActionList().stream().anyMatch(action -> action.getId() == actionId); + } + /** * Adds an action that can be performed on the node. * <p> @@ -1767,7 +1761,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @return True if the node is focusable. */ public boolean isFocusable() { - return getBooleanProperty(BOOLEAN_PROPERTY_FOCUSABLE); + return hasActionWithId(ACTION_FOCUS); } /** @@ -1781,10 +1775,11 @@ public class AccessibilityNodeInfo implements Parcelable { * @param focusable True if the node is focusable. * * @throws IllegalStateException If called from an AccessibilityService. + * @deprecated Use {@link #addAction(AccessibilityAction)} + * with {@link AccessibilityAction#ACTION_FOCUS} */ - public void setFocusable(boolean focusable) { - setBooleanProperty(BOOLEAN_PROPERTY_FOCUSABLE, focusable); - } + @Deprecated + public void setFocusable(boolean focusable) { } /** * Gets whether this node is focused. @@ -1892,7 +1887,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @return True if the node is clickable. */ public boolean isClickable() { - return getBooleanProperty(BOOLEAN_PROPERTY_CLICKABLE); + return hasActionWithId(ACTION_CLICK); } /** @@ -1906,10 +1901,11 @@ public class AccessibilityNodeInfo implements Parcelable { * @param clickable True if the node is clickable. * * @throws IllegalStateException If called from an AccessibilityService. + * @deprecated Use {@link #addAction(AccessibilityAction)} + * with {@link AccessibilityAction#ACTION_CLICK} */ - public void setClickable(boolean clickable) { - setBooleanProperty(BOOLEAN_PROPERTY_CLICKABLE, clickable); - } + @Deprecated + public void setClickable(boolean clickable) { } /** * Gets whether this node is long clickable. @@ -1917,7 +1913,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @return True if the node is long clickable. */ public boolean isLongClickable() { - return getBooleanProperty(BOOLEAN_PROPERTY_LONG_CLICKABLE); + return hasActionWithId(ACTION_LONG_CLICK); } /** @@ -1931,10 +1927,11 @@ public class AccessibilityNodeInfo implements Parcelable { * @param longClickable True if the node is long clickable. * * @throws IllegalStateException If called from an AccessibilityService. + * @deprecated Use {@link #addAction(AccessibilityAction)} + * with {@link AccessibilityAction#ACTION_LONG_CLICK} */ - public void setLongClickable(boolean longClickable) { - setBooleanProperty(BOOLEAN_PROPERTY_LONG_CLICKABLE, longClickable); - } + @Deprecated + public void setLongClickable(boolean longClickable) { } /** * Gets whether this node is enabled. @@ -1992,7 +1989,13 @@ public class AccessibilityNodeInfo implements Parcelable { * @return True if the node is scrollable, false otherwise. */ public boolean isScrollable() { - return getBooleanProperty(BOOLEAN_PROPERTY_SCROLLABLE); + return hasActionWithId(ACTION_SCROLL_BACKWARD) + || hasActionWithId(ACTION_SCROLL_FORWARD) + || hasActionWithId(R.id.accessibilityActionScrollToPosition) + || hasActionWithId(R.id.accessibilityActionScrollUp) + || hasActionWithId(R.id.accessibilityActionScrollDown) + || hasActionWithId(R.id.accessibilityActionScrollLeft) + || hasActionWithId(R.id.accessibilityActionScrollRight); } /** @@ -2006,9 +2009,11 @@ public class AccessibilityNodeInfo implements Parcelable { * @param scrollable True if the node is scrollable, false otherwise. * * @throws IllegalStateException If called from an AccessibilityService. + * @deprecated Use {@link #addAction(AccessibilityAction)} */ + @Deprecated + public void setScrollable(boolean scrollable) { - setBooleanProperty(BOOLEAN_PROPERTY_SCROLLABLE, scrollable); } /** @@ -2199,7 +2204,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @return True if the node is context clickable. */ public boolean isContextClickable() { - return getBooleanProperty(BOOLEAN_PROPERTY_CONTEXT_CLICKABLE); + return hasActionWithId(R.id.accessibilityActionContextClick); } /** @@ -2212,10 +2217,11 @@ public class AccessibilityNodeInfo implements Parcelable { * * @param contextClickable True if the node is context clickable. * @throws IllegalStateException If called from an AccessibilityService. + * @deprecated Use {@link #addAction(AccessibilityAction)} + * with {@link AccessibilityAction#ACTION_CONTEXT_CLICK} */ - public void setContextClickable(boolean contextClickable) { - setBooleanProperty(BOOLEAN_PROPERTY_CONTEXT_CLICKABLE, contextClickable); - } + @Deprecated + public void setContextClickable(boolean contextClickable) { } /** * Gets the node's live region mode. @@ -2309,7 +2315,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @return If the node can be dismissed. */ public boolean isDismissable() { - return getBooleanProperty(BOOLEAN_PROPERTY_DISMISSABLE); + return hasActionWithId(ACTION_DISMISS); } /** @@ -2321,10 +2327,11 @@ public class AccessibilityNodeInfo implements Parcelable { * </p> * * @param dismissable If the node can be dismissed. + * @deprecated Use {@link #addAction(AccessibilityAction)} + * with {@link AccessibilityAction#ACTION_DISMISS} */ - public void setDismissable(boolean dismissable) { - setBooleanProperty(BOOLEAN_PROPERTY_DISMISSABLE, dismissable); - } + @Deprecated + public void setDismissable(boolean dismissable) { } /** * Returns whether the node originates from a view considered important for accessibility. diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java index 8f28102016d7..41daf9e11370 100644 --- a/core/java/android/view/autofill/AutofillManager.java +++ b/core/java/android/view/autofill/AutofillManager.java @@ -57,6 +57,7 @@ import android.view.accessibility.AccessibilityWindowInfo; import com.android.internal.annotations.GuardedBy; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.internal.os.IResultReceiver; import com.android.internal.util.ArrayUtils; import com.android.internal.util.Preconditions; @@ -72,6 +73,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; //TODO: use java.lang.ref.Cleaner once Android supports Java 9 import sun.misc.Cleaner; @@ -572,10 +575,11 @@ public final class AutofillManager { final AutofillClient client = getClient(); if (client != null) { + final SyncResultReceiver receiver = new SyncResultReceiver(); try { - final boolean sessionWasRestored = mService.restoreSession(mSessionId, - client.autofillClientGetActivityToken(), - mServiceClient.asBinder()); + mService.restoreSession(mSessionId, client.autofillClientGetActivityToken(), + mServiceClient.asBinder(), receiver); + final boolean sessionWasRestored = receiver.getIntResult() == 1; if (!sessionWasRestored) { Log.w(TAG, "Session " + mSessionId + " could not be restored"); @@ -691,7 +695,9 @@ public final class AutofillManager { */ @Nullable public FillEventHistory getFillEventHistory() { try { - return mService.getFillEventHistory(); + final SyncResultReceiver receiver = new SyncResultReceiver(); + mService.getFillEventHistory(receiver); + return receiver.getObjectResult(SyncResultReceiver.TYPE_PARCELABLE); } catch (RemoteException e) { e.rethrowFromSystemServer(); return null; @@ -1242,8 +1248,10 @@ public final class AutofillManager { public boolean hasEnabledAutofillServices() { if (mService == null) return false; + final SyncResultReceiver receiver = new SyncResultReceiver(); try { - return mService.isServiceEnabled(mContext.getUserId(), mContext.getPackageName()); + mService.isServiceEnabled(mContext.getUserId(), mContext.getPackageName(), receiver); + return receiver.getIntResult() == 1; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1257,8 +1265,10 @@ public final class AutofillManager { public ComponentName getAutofillServiceComponentName() { if (mService == null) return null; + final SyncResultReceiver receiver = new SyncResultReceiver(); try { - return mService.getAutofillServiceComponentName(); + mService.getAutofillServiceComponentName(receiver); + return receiver.getObjectResult(SyncResultReceiver.TYPE_PARCELABLE); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1281,7 +1291,9 @@ public final class AutofillManager { */ @Nullable public String getUserDataId() { try { - return mService.getUserDataId(); + final SyncResultReceiver receiver = new SyncResultReceiver(); + mService.getUserDataId(receiver); + return receiver.getObjectResult(SyncResultReceiver.TYPE_STRING); } catch (RemoteException e) { e.rethrowFromSystemServer(); return null; @@ -1301,7 +1313,9 @@ public final class AutofillManager { */ @Nullable public UserData getUserData() { try { - return mService.getUserData(); + final SyncResultReceiver receiver = new SyncResultReceiver(); + mService.getUserData(receiver); + return receiver.getObjectResult(SyncResultReceiver.TYPE_PARCELABLE); } catch (RemoteException e) { e.rethrowFromSystemServer(); return null; @@ -1337,8 +1351,10 @@ public final class AutofillManager { * the user. */ public boolean isFieldClassificationEnabled() { + final SyncResultReceiver receiver = new SyncResultReceiver(); try { - return mService.isFieldClassificationEnabled(); + mService.isFieldClassificationEnabled(receiver); + return receiver.getIntResult() == 1; } catch (RemoteException e) { e.rethrowFromSystemServer(); return false; @@ -1358,8 +1374,10 @@ public final class AutofillManager { */ @Nullable public String getDefaultFieldClassificationAlgorithm() { + final SyncResultReceiver receiver = new SyncResultReceiver(); try { - return mService.getDefaultFieldClassificationAlgorithm(); + mService.getDefaultFieldClassificationAlgorithm(receiver); + return receiver.getObjectResult(SyncResultReceiver.TYPE_STRING); } catch (RemoteException e) { e.rethrowFromSystemServer(); return null; @@ -1376,9 +1394,10 @@ public final class AutofillManager { */ @NonNull public List<String> getAvailableFieldClassificationAlgorithms() { - final String[] algorithms; + final SyncResultReceiver receiver = new SyncResultReceiver(); try { - algorithms = mService.getAvailableFieldClassificationAlgorithms(); + mService.getAvailableFieldClassificationAlgorithms(receiver); + final String[] algorithms = receiver.getObjectResult(SyncResultReceiver.TYPE_STRING); return algorithms != null ? Arrays.asList(algorithms) : Collections.emptyList(); } catch (RemoteException e) { e.rethrowFromSystemServer(); @@ -1399,8 +1418,10 @@ public final class AutofillManager { public boolean isAutofillSupported() { if (mService == null) return false; + final SyncResultReceiver receiver = new SyncResultReceiver(); try { - return mService.isServiceSupported(mContext.getUserId()); + mService.isServiceSupported(mContext.getUserId(), receiver); + return receiver.getIntResult() == 1; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1521,10 +1542,12 @@ public final class AutofillManager { final AutofillClient client = getClient(); if (client == null) return; // NOTE: getClient() already logged it.. - mSessionId = mService.startSession(client.autofillClientGetActivityToken(), + final SyncResultReceiver receiver = new SyncResultReceiver(); + mService.startSession(client.autofillClientGetActivityToken(), mServiceClient.asBinder(), id, bounds, value, mContext.getUserId(), mCallback != null, flags, client.autofillClientGetComponentName(), - isCompatibilityModeEnabledLocked()); + isCompatibilityModeEnabledLocked(), receiver); + mSessionId = receiver.getIntResult(); if (mSessionId != NO_SESSION) { mState = STATE_ACTIVE; } @@ -1602,7 +1625,9 @@ public final class AutofillManager { mServiceClient = new AutofillManagerClient(this); try { final int userId = mContext.getUserId(); - final int flags = mService.addClient(mServiceClient, userId); + final SyncResultReceiver receiver = new SyncResultReceiver(); + mService.addClient(mServiceClient, userId, receiver); + final int flags = receiver.getIntResult(); mEnabled = (flags & FLAG_ADD_CLIENT_ENABLED) != 0; sDebug = (flags & FLAG_ADD_CLIENT_DEBUG) != 0; sVerbose = (flags & FLAG_ADD_CLIENT_VERBOSE) != 0; @@ -1923,7 +1948,7 @@ public final class AutofillManager { mFillableIds.add(id); } if (sVerbose) { - Log.v(TAG, "setTrackedViews(): fillableIds=" + fillableIds + Log.v(TAG, "setTrackedViews(): fillableIds=" + Arrays.toString(fillableIds) + ", mFillableIds" + mFillableIds); } } @@ -2818,4 +2843,104 @@ public final class AutofillManager { } } } + + /** + * @hide + */ + public static final class SyncResultReceiver extends IResultReceiver.Stub { + + private static final String EXTRA = "EXTRA"; + + /** + * How long to block waiting for {@link IResultReceiver} callbacks when calling server. + */ + private static final long BINDER_TIMEOUT_MS = 5000; + + private static final int TYPE_STRING = 0; + private static final int TYPE_STRING_ARRAY = 1; + private static final int TYPE_PARCELABLE = 2; + + private final CountDownLatch mLatch = new CountDownLatch(1); + private int mResult; + private Bundle mBundle; + + private void waitResult() { + try { + if (!mLatch.await(BINDER_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { + throw new IllegalStateException("Not called in " + BINDER_TIMEOUT_MS + "ms"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + /** + * Gets the result from an operation that returns an {@code int}. + */ + int getIntResult() { + waitResult(); + return mResult; + } + + /** + * Gets the result from an operation that returns an {@code Object}. + * + * @param type type of expected object. + */ + @Nullable + @SuppressWarnings("unchecked") + <T> T getObjectResult(int type) { + waitResult(); + if (mBundle == null) { + return null; + } + switch (type) { + case TYPE_STRING: + return (T) mBundle.getString(EXTRA); + case TYPE_STRING_ARRAY: + return (T) mBundle.getString(EXTRA); + case TYPE_PARCELABLE: + return (T) mBundle.getParcelable(EXTRA); + default: + throw new IllegalArgumentException("unsupported type: " + type); + } + } + + @Override + public void send(int resultCode, Bundle resultData) { + mResult = resultCode; + mBundle = resultData; + mLatch.countDown(); + } + + /** + * Creates a bundle for a {@code String} value. + */ + @NonNull + public static Bundle bundleFor(@Nullable String value) { + final Bundle bundle = new Bundle(); + bundle.putString(EXTRA, value); + return bundle; + } + + /** + * Creates a bundle for a {@code String[]} value. + */ + @NonNull + public static Bundle bundleFor(@Nullable String[] value) { + final Bundle bundle = new Bundle(); + bundle.putStringArray(EXTRA, value); + return bundle; + } + + /** + * Creates a bundle for a {@code Parcelable} value. + */ + @NonNull + public static Bundle bundleFor(@Nullable Parcelable value) { + final Bundle bundle = new Bundle(); + bundle.putParcelable(EXTRA, value); + return bundle; + } + } } diff --git a/core/java/android/view/autofill/IAutoFillManager.aidl b/core/java/android/view/autofill/IAutoFillManager.aidl index 6b26f233f58c..26aeba5cfdfa 100644 --- a/core/java/android/view/autofill/IAutoFillManager.aidl +++ b/core/java/android/view/autofill/IAutoFillManager.aidl @@ -28,41 +28,39 @@ import android.service.autofill.UserData; import android.view.autofill.AutofillId; import android.view.autofill.AutofillValue; import android.view.autofill.IAutoFillManagerClient; +import com.android.internal.os.IResultReceiver; /** * Mediator between apps being auto-filled and auto-fill service implementations. * * {@hide} */ - // TODO(b/73536867) STOPSHIP : this whole interface should be either oneway or not, and we're - // gradually converting the methods (as some of them return a value form the server and must be - // refactored). -interface IAutoFillManager { +oneway interface IAutoFillManager { // Returns flags: FLAG_ADD_CLIENT_ENABLED | FLAG_ADD_CLIENT_DEBUG | FLAG_ADD_CLIENT_VERBOSE - int addClient(in IAutoFillManagerClient client, int userId); + void addClient(in IAutoFillManagerClient client, int userId, in IResultReceiver result); void removeClient(in IAutoFillManagerClient client, int userId); - int startSession(IBinder activityToken, in IBinder appCallback, in AutofillId autoFillId, - in Rect bounds, in AutofillValue value, int userId, boolean hasCallback, int flags, - in ComponentName componentName, boolean compatMode); - FillEventHistory getFillEventHistory(); - boolean restoreSession(int sessionId, in IBinder activityToken, in IBinder appCallback); - oneway void updateSession(int sessionId, in AutofillId id, in Rect bounds, - in AutofillValue value, int action, int flags, int userId); - oneway void setAutofillFailure(int sessionId, in List<AutofillId> ids, int userId); - oneway void finishSession(int sessionId, int userId); - oneway void cancelSession(int sessionId, int userId); - oneway void setAuthenticationResult(in Bundle data, int sessionId, int authenticationId, - int userId); - oneway void setHasCallback(int sessionId, int userId, boolean hasIt); + void startSession(IBinder activityToken, in IBinder appCallback, in AutofillId autoFillId, + in Rect bounds, in AutofillValue value, int userId, boolean hasCallback, int flags, + in ComponentName componentName, boolean compatMode, in IResultReceiver result); + void getFillEventHistory(in IResultReceiver result); + void restoreSession(int sessionId, in IBinder activityToken, in IBinder appCallback, + in IResultReceiver result); + void updateSession(int sessionId, in AutofillId id, in Rect bounds, + in AutofillValue value, int action, int flags, int userId); + void setAutofillFailure(int sessionId, in List<AutofillId> ids, int userId); + void finishSession(int sessionId, int userId); + void cancelSession(int sessionId, int userId); + void setAuthenticationResult(in Bundle data, int sessionId, int authenticationId, int userId); + void setHasCallback(int sessionId, int userId, boolean hasIt); void disableOwnedAutofillServices(int userId); - boolean isServiceSupported(int userId); - boolean isServiceEnabled(int userId, String packageName); + void isServiceSupported(int userId, in IResultReceiver result); + void isServiceEnabled(int userId, String packageName, in IResultReceiver result); void onPendingSaveUi(int operation, IBinder token); - UserData getUserData(); - String getUserDataId(); + void getUserData(in IResultReceiver result); + void getUserDataId(in IResultReceiver result); void setUserData(in UserData userData); - boolean isFieldClassificationEnabled(); - ComponentName getAutofillServiceComponentName(); - String[] getAvailableFieldClassificationAlgorithms(); - String getDefaultFieldClassificationAlgorithm(); + void isFieldClassificationEnabled(in IResultReceiver result); + void getAutofillServiceComponentName(in IResultReceiver result); + void getAvailableFieldClassificationAlgorithms(in IResultReceiver result); + void getDefaultFieldClassificationAlgorithm(in IResultReceiver result); } diff --git a/core/java/com/android/internal/app/procstats/AssociationState.java b/core/java/com/android/internal/app/procstats/AssociationState.java index e73ec1c40d19..e5d6556e1218 100644 --- a/core/java/com/android/internal/app/procstats/AssociationState.java +++ b/core/java/com/android/internal/app/procstats/AssociationState.java @@ -21,37 +21,143 @@ import android.os.Parcel; import android.os.SystemClock; import android.os.UserHandle; import android.util.ArrayMap; +import android.util.Slog; import android.util.TimeUtils; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.Objects; public final class AssociationState { private static final String TAG = "ProcessStats"; private static final boolean DEBUG = false; - private final String mPackage; + private final ProcessStats mProcessStats; + private final ProcessStats.PackageState mPackageState; private final String mProcessName; private final String mName; private final DurationsTable mDurations; public final class SourceState { + final SourceKey mKey; + int mProcStateSeq = -1; + int mProcState = ProcessStats.STATE_NOTHING; + boolean mInTrackingList; + int mNesting; + int mCount; + long mStartUptime; + long mDuration; + long mTrackingUptime; + int mActiveCount; + long mActiveStartUptime; + long mActiveDuration; + + SourceState(SourceKey key) { + mKey = key; + } + + public AssociationState getAssociationState() { + return AssociationState.this; + } + + public String getProcessName() { + return mKey.mProcess; + } + + public int getUid() { + return mKey.mUid; + } + + public void trackProcState(int procState, int seq, long now) { + procState = ProcessState.PROCESS_STATE_TO_STATE[procState]; + if (seq != mProcStateSeq) { + mProcStateSeq = seq; + mProcState = procState; + } else if (procState < mProcState) { + mProcState = procState; + } + if (procState < ProcessStats.STATE_HOME) { + if (!mInTrackingList) { + mInTrackingList = true; + mTrackingUptime = now; + mProcessStats.mTrackingAssociations.add(this); + } + } else { + stopTracking(now); + } + } + public void stop() { mNesting--; if (mNesting == 0) { - mDuration += SystemClock.uptimeMillis() - mStartTime; + mDuration += SystemClock.uptimeMillis() - mStartUptime; mNumActive--; + stopTracking(SystemClock.uptimeMillis()); } } - int mNesting; - int mCount; - long mStartTime; - long mDuration; + void startActive(long now) { + if (mInTrackingList) { + if (mActiveStartUptime == 0) { + mActiveStartUptime = now; + mActiveCount++; + } + } else { + Slog.wtf(TAG, "startActive while not tracking: " + this); + } + } + + void stopActive(long now) { + if (mActiveStartUptime != 0) { + if (!mInTrackingList) { + Slog.wtf(TAG, "stopActive while not tracking: " + this); + } + mActiveDuration += now - mActiveStartUptime; + mActiveStartUptime = 0; + } + } + + void stopTracking(long now) { + stopActive(now); + if (mInTrackingList) { + mInTrackingList = false; + // Do a manual search for where to remove, since these objects will typically + // be towards the end of the array. + final ArrayList<SourceState> list = mProcessStats.mTrackingAssociations; + for (int i = list.size() - 1; i >= 0; i--) { + if (list.get(i) == this) { + list.remove(i); + return; + } + } + Slog.wtf(TAG, "Stop tracking didn't find in tracking list: " + this); + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(64); + sb.append("SourceState{").append(Integer.toHexString(System.identityHashCode(this))) + .append(" ").append(mKey.mProcess).append("/").append(mKey.mUid); + if (mProcState != ProcessStats.STATE_NOTHING) { + sb.append(" ").append(DumpUtils.STATE_NAMES[mProcState]).append(" #") + .append(mProcStateSeq); + } + sb.append("}"); + return sb.toString(); + } } - final static class SourceKey { + private final static class SourceKey { + /** + * UID, consider this final. Not final just to avoid a temporary object during lookup. + */ int mUid; + + /** + * Process name, consider this final. Not final just to avoid a temporary object during + * lookup. + */ String mProcess; SourceKey(int uid, String process) { @@ -82,7 +188,6 @@ public final class AssociationState { sb.append('}'); return sb.toString(); } - } /** @@ -92,18 +197,26 @@ public final class AssociationState { private final SourceKey mTmpSourceKey = new SourceKey(0, null); + private ProcessState mProc; + private int mNumActive; - public AssociationState(ProcessStats processStats, String pkg, String name, - String processName) { - mPackage = pkg; + public AssociationState(ProcessStats processStats, ProcessStats.PackageState packageState, + String name, String processName, ProcessState proc) { + mProcessStats = processStats; + mPackageState = packageState; mName = name; mProcessName = processName; mDurations = new DurationsTable(processStats.mTableData); + mProc = proc; + } + + public int getUid() { + return mPackageState.mUid; } public String getPackage() { - return mPackage; + return mPackageState.mPackageName; } public String getProcessName() { @@ -114,18 +227,27 @@ public final class AssociationState { return mName; } + public ProcessState getProcess() { + return mProc; + } + + public void setProcess(ProcessState proc) { + mProc = proc; + } + public SourceState startSource(int uid, String processName) { mTmpSourceKey.mUid = uid; mTmpSourceKey.mProcess = processName; SourceState src = mSources.get(mTmpSourceKey); if (src == null) { - src = new SourceState(); - mSources.put(new SourceKey(uid, processName), src); + SourceKey key = new SourceKey(uid, processName); + src = new SourceState(key); + mSources.put(key, src); } src.mNesting++; if (src.mNesting == 1) { src.mCount++; - src.mStartTime = SystemClock.uptimeMillis(); + src.mStartUptime = SystemClock.uptimeMillis(); mNumActive++; } return src; @@ -138,11 +260,13 @@ public final class AssociationState { final SourceState otherSrc = other.mSources.valueAt(isrc); SourceState mySrc = mSources.get(key); if (mySrc == null) { - mySrc = new SourceState(); + mySrc = new SourceState(key); mSources.put(key, mySrc); } mySrc.mCount += otherSrc.mCount; mySrc.mDuration += otherSrc.mDuration; + mySrc.mActiveCount += otherSrc.mActiveCount; + mySrc.mActiveDuration += otherSrc.mActiveDuration; } } @@ -160,8 +284,15 @@ public final class AssociationState { SourceState src = mSources.valueAt(isrc); if (src.mNesting > 0) { src.mCount = 1; - src.mStartTime = now; + src.mStartUptime = now; src.mDuration = 0; + if (src.mActiveStartUptime > 0) { + src.mActiveCount = 1; + src.mActiveStartUptime = now; + } else { + src.mActiveCount = 0; + } + src.mActiveDuration = 0; } else { mSources.removeAt(isrc); } @@ -169,7 +300,7 @@ public final class AssociationState { } } - public void writeToParcel(ProcessStats stats, Parcel out, long now) { + public void writeToParcel(ProcessStats stats, Parcel out, long nowUptime) { mDurations.writeToParcel(out); final int NSRC = mSources.size(); out.writeInt(NSRC); @@ -180,9 +311,15 @@ public final class AssociationState { stats.writeCommonString(out, key.mProcess); out.writeInt(src.mCount); out.writeLong(src.mDuration); + out.writeInt(src.mActiveCount); + out.writeLong(src.mActiveDuration); } } + /** + * Returns non-null if all else fine, else a String that describes the error that + * caused it to fail. + */ public String readFromParcel(ProcessStats stats, Parcel in, int parcelVersion) { if (!mDurations.readFromParcel(in)) { return "Duration table corrupt"; @@ -195,21 +332,27 @@ public final class AssociationState { final int uid = in.readInt(); final String procName = stats.readCommonString(in, parcelVersion); final SourceKey key = new SourceKey(uid, procName); - final SourceState src = new SourceState(); + final SourceState src = new SourceState(key); src.mCount = in.readInt(); src.mDuration = in.readLong(); + src.mActiveCount = in.readInt(); + src.mActiveDuration = in.readLong(); mSources.put(key, src); } return null; } - public void commitStateTime(long now) { + public void commitStateTime(long nowUptime) { if (isInUse()) { for (int isrc = mSources.size() - 1; isrc >= 0; isrc--) { SourceState src = mSources.valueAt(isrc); if (src.mNesting > 0) { - src.mDuration += now - src.mStartTime; - src.mStartTime = now; + src.mDuration += nowUptime - src.mStartUptime; + src.mStartUptime = nowUptime; + } + if (src.mActiveStartUptime > 0) { + src.mActiveDuration += nowUptime - src.mActiveStartUptime; + src.mActiveStartUptime = nowUptime; } } } @@ -237,7 +380,7 @@ public final class AssociationState { pw.print(src.mCount); long duration = src.mDuration; if (src.mNesting > 0) { - duration += now - src.mStartTime; + duration += now - src.mStartUptime; } if (dumpAll) { pw.print(" / Duration "); @@ -248,9 +391,37 @@ public final class AssociationState { } DumpUtils.printPercent(pw, (double)duration/(double)totalTime); if (src.mNesting > 0) { - pw.print(" (running)"); + pw.print(" (running"); + if (src.mProcState != ProcessStats.STATE_NOTHING) { + pw.print(" / "); + pw.print(DumpUtils.STATE_NAMES[src.mProcState]); + pw.print(" #"); + pw.print(src.mProcStateSeq); + } + pw.print(")"); } pw.println(); + if (src.mActiveCount > 0) { + pw.print(prefixInner); + pw.print(" Active count "); + pw.print(src.mActiveCount); + duration = src.mActiveDuration; + if (src.mActiveStartUptime > 0) { + duration += now - src.mActiveStartUptime; + } + if (dumpAll) { + pw.print(" / Duration "); + TimeUtils.formatDuration(duration, pw); + pw.print(" / "); + } else { + pw.print(" / time "); + } + DumpUtils.printPercent(pw, (double)duration/(double)totalTime); + if (src.mActiveStartUptime > 0) { + pw.print(" (running)"); + } + pw.println(); + } } } @@ -277,7 +448,15 @@ public final class AssociationState { pw.print(src.mCount); long duration = src.mDuration; if (src.mNesting > 0) { - duration += now - src.mStartTime; + duration += now - src.mStartUptime; + } + pw.print(","); + pw.print(duration); + pw.print(","); + pw.print(src.mActiveCount); + duration = src.mActiveDuration; + if (src.mActiveStartUptime > 0) { + duration += now - src.mActiveStartUptime; } pw.print(","); pw.print(duration); @@ -287,7 +466,7 @@ public final class AssociationState { public String toString() { return "AssociationState{" + Integer.toHexString(System.identityHashCode(this)) - + " " + mName + " pkg=" + mPackage + " proc=" - + Integer.toHexString(System.identityHashCode(this)) + "}"; + + " " + mName + " pkg=" + mPackageState.mPackageName + " proc=" + + Integer.toHexString(System.identityHashCode(mProc)) + "}"; } } diff --git a/core/java/com/android/internal/app/procstats/ProcessState.java b/core/java/com/android/internal/app/procstats/ProcessState.java index 5a08f62f218b..ad42288c25bb 100644 --- a/core/java/com/android/internal/app/procstats/ProcessState.java +++ b/core/java/com/android/internal/app/procstats/ProcessState.java @@ -71,7 +71,7 @@ public final class ProcessState { private static final boolean DEBUG_PARCEL = false; // Map from process states to the states we track. - private static final int[] PROCESS_STATE_TO_STATE = new int[] { + static final int[] PROCESS_STATE_TO_STATE = new int[] { STATE_PERSISTENT, // ActivityManager.PROCESS_STATE_PERSISTENT STATE_PERSISTENT, // ActivityManager.PROCESS_STATE_PERSISTENT_UI STATE_TOP, // ActivityManager.PROCESS_STATE_TOP @@ -129,7 +129,7 @@ public final class ProcessState { private final PssTable mPssTable; private ProcessState mCommonProcess; - private int mCurState = STATE_NOTHING; + private int mCurCombinedState = STATE_NOTHING; private long mStartTime; private int mLastPssState = STATE_NOTHING; @@ -180,7 +180,7 @@ public final class ProcessState { mPackage = pkg; mUid = uid; mVersion = vers; - mCurState = commonProcess.mCurState; + mCurCombinedState = commonProcess.mCurCombinedState; mStartTime = now; mDurations = new DurationsTable(commonProcess.mStats.mTableData); mPssTable = new PssTable(commonProcess.mStats.mTableData); @@ -324,7 +324,7 @@ public final class ProcessState { public boolean isInUse() { return mActive || mNumActiveServices > 0 || mNumStartedServices > 0 - || mCurState != STATE_NOTHING; + || mCurCombinedState != STATE_NOTHING; } public boolean isActive() { @@ -333,7 +333,7 @@ public final class ProcessState { public boolean hasAnyData() { return !(mDurations.getKeyCount() == 0 - && mCurState == STATE_NOTHING + && mCurCombinedState == STATE_NOTHING && mPssTable.getKeyCount() == 0); } @@ -355,7 +355,7 @@ public final class ProcessState { } // First update the common process. - mCommonProcess.setState(state, now); + mCommonProcess.setCombinedState(state, now); // If the common process is not multi-package, there is nothing else to do. if (!mCommonProcess.mMultiPackage) { @@ -364,25 +364,29 @@ public final class ProcessState { if (pkgList != null) { for (int ip=pkgList.size()-1; ip>=0; ip--) { - pullFixedProc(pkgList, ip).setState(state, now); + pullFixedProc(pkgList, ip).setCombinedState(state, now); } } } - public void setState(int state, long now) { + public void setCombinedState(int state, long now) { ensureNotDead(); - if (!mDead && (mCurState != state)) { + if (!mDead && (mCurCombinedState != state)) { //Slog.i(TAG, "Setting state in " + mName + "/" + mPackage + ": " + state); commitStateTime(now); - mCurState = state; + mCurCombinedState = state; } } + public int getCombinedState() { + return mCurCombinedState; + } + public void commitStateTime(long now) { - if (mCurState != STATE_NOTHING) { + if (mCurCombinedState != STATE_NOTHING) { long dur = now - mStartTime; if (dur > 0) { - mDurations.addDuration(mCurState, dur); + mDurations.addDuration(mCurCombinedState, dur); } } mStartTime = now; @@ -430,8 +434,8 @@ public final class ProcessState { mCommonProcess.incStartedServices(memFactor, now, serviceName); } mNumStartedServices++; - if (mNumStartedServices == 1 && mCurState == STATE_NOTHING) { - setState(STATE_SERVICE_RESTARTING + (memFactor*STATE_COUNT), now); + if (mNumStartedServices == 1 && mCurCombinedState == STATE_NOTHING) { + setCombinedState(STATE_SERVICE_RESTARTING + (memFactor*STATE_COUNT), now); } } @@ -446,8 +450,8 @@ public final class ProcessState { mCommonProcess.decStartedServices(memFactor, now, serviceName); } mNumStartedServices--; - if (mNumStartedServices == 0 && (mCurState%STATE_COUNT) == STATE_SERVICE_RESTARTING) { - setState(STATE_NOTHING, now); + if (mNumStartedServices == 0 && (mCurCombinedState %STATE_COUNT) == STATE_SERVICE_RESTARTING) { + setCombinedState(STATE_NOTHING, now); } else if (mNumStartedServices < 0) { Slog.wtfStack(TAG, "Proc started services underrun: pkg=" + mPackage + " uid=" + mUid + " name=" + mName); @@ -481,16 +485,16 @@ public final class ProcessState { break; } if (!always) { - if (mLastPssState == mCurState && SystemClock.uptimeMillis() + if (mLastPssState == mCurCombinedState && SystemClock.uptimeMillis() < (mLastPssTime+(30*1000))) { return; } } - mLastPssState = mCurState; + mLastPssState = mCurCombinedState; mLastPssTime = SystemClock.uptimeMillis(); - if (mCurState != STATE_NOTHING) { + if (mCurCombinedState != STATE_NOTHING) { // First update the common process. - mCommonProcess.mPssTable.mergeStats(mCurState, 1, pss, pss, pss, uss, uss, uss, + mCommonProcess.mPssTable.mergeStats(mCurCombinedState, 1, pss, pss, pss, uss, uss, uss, rss, rss, rss); // If the common process is not multi-package, there is nothing else to do. @@ -500,7 +504,7 @@ public final class ProcessState { if (pkgList != null) { for (int ip=pkgList.size()-1; ip>=0; ip--) { - pullFixedProc(pkgList, ip).mPssTable.mergeStats(mCurState, 1, + pullFixedProc(pkgList, ip).mPssTable.mergeStats(mCurCombinedState, 1, pss, pss, pss, uss, uss, uss, rss, rss, rss); } } @@ -619,7 +623,7 @@ public final class ProcessState { public long getDuration(int state, long now) { long time = mDurations.getValueForId((byte)state); - if (mCurState == state) { + if (mCurCombinedState == state) { time += now - mStartTime; } return time; @@ -724,7 +728,7 @@ public final class ProcessState { final int key = mDurations.getKeyAt(i); final int type = SparseMappingTable.getIdFromKey(key); long time = mDurations.getValue(key); - if (mCurState == type) { + if (mCurCombinedState == type) { time += now - mStartTime; } final int procState = type % STATE_COUNT; @@ -827,7 +831,7 @@ public final class ProcessState { final int bucket = ((iscreen + imem) * STATE_COUNT) + procStates[ip]; long time = mDurations.getValueForId((byte)bucket); String running = ""; - if (mCurState == bucket) { + if (mCurCombinedState == bucket) { running = " (running)"; } if (time != 0) { @@ -1177,14 +1181,14 @@ public final class ProcessState { final int key = mDurations.getKeyAt(i); final int type = SparseMappingTable.getIdFromKey(key); long time = mDurations.getValue(key); - if (mCurState == type) { + if (mCurCombinedState == type) { didCurState = true; time += now - mStartTime; } DumpUtils.printProcStateTagAndValue(pw, type, time); } - if (!didCurState && mCurState != STATE_NOTHING) { - DumpUtils.printProcStateTagAndValue(pw, mCurState, now - mStartTime); + if (!didCurState && mCurCombinedState != STATE_NOTHING) { + DumpUtils.printProcStateTagAndValue(pw, mCurCombinedState, now - mStartTime); } } @@ -1251,14 +1255,14 @@ public final class ProcessState { final int key = mDurations.getKeyAt(i); final int type = SparseMappingTable.getIdFromKey(key); long time = mDurations.getValue(key); - if (mCurState == type) { + if (mCurCombinedState == type) { didCurState = true; time += now - mStartTime; } durationByState.put(type, time); } - if (!didCurState && mCurState != STATE_NOTHING) { - durationByState.put(mCurState, now - mStartTime); + if (!didCurState && mCurCombinedState != STATE_NOTHING) { + durationByState.put(mCurCombinedState, now - mStartTime); } for (int i=0; i<mPssTable.getKeyCount(); i++) { diff --git a/core/java/com/android/internal/app/procstats/ProcessStats.java b/core/java/com/android/internal/app/procstats/ProcessStats.java index 1f871d9f92b8..15f140e2ea2d 100644 --- a/core/java/com/android/internal/app/procstats/ProcessStats.java +++ b/core/java/com/android/internal/app/procstats/ProcessStats.java @@ -16,6 +16,7 @@ package com.android.internal.app.procstats; +import android.content.ComponentName; import android.os.Debug; import android.os.Parcel; import android.os.Parcelable; @@ -157,7 +158,7 @@ public final class ProcessStats implements Parcelable { }; // Current version of the parcel format. - private static final int PARCEL_VERSION = 31; + private static final int PARCEL_VERSION = 32; // In-memory Parcel magic number, used to detect attempts to unmarshall bad data private static final int MAGIC = 0x50535454; @@ -168,6 +169,8 @@ public final class ProcessStats implements Parcelable { public final ProcessMap<LongSparseArray<PackageState>> mPackages = new ProcessMap<>(); public final ProcessMap<ProcessState> mProcesses = new ProcessMap<>(); + public final ArrayList<AssociationState.SourceState> mTrackingAssociations = new ArrayList<>(); + public final long[] mMemFactorDurations = new long[ADJ_COUNT]; public int mMemFactor = STATE_NOTHING; public long mStartTime; @@ -1203,8 +1206,8 @@ public final class ProcessStats implements Parcelable { AssociationState asc = hadData ? pkgState.mAssociations.get(associationName) : null; if (asc == null) { - asc = new AssociationState(this, pkgName, associationName, - processName); + asc = new AssociationState(this, pkgState, associationName, + processName, null); } String errorMsg = asc.readFromParcel(this, in, version); if (errorMsg != null) { @@ -1308,6 +1311,17 @@ public final class ProcessStats implements Parcelable { Slog.d(TAG, "GETPROC leaving proc of " + ss); } } + // Also update active associations. + for (int i=commonPkgState.mAssociations.size()-1; i>=0; i--) { + AssociationState as = commonPkgState.mAssociations.valueAt(i); + if (as.getProcess() == commonProc) { + if (DEBUG) Slog.d(TAG, "GETPROC switching association to cloned: " + + as); + as.setProcess(cloned); + } else if (DEBUG) { + Slog.d(TAG, "GETPROC leaving proc of " + as); + } + } } else { Slog.w(TAG, "Cloning proc state: no package state " + commonProc.getPackage() + "/" + pkgState.mUid + " for proc " + commonProc.getName()); @@ -1356,12 +1370,42 @@ public final class ProcessStats implements Parcelable { } final ProcessState procs = processName != null ? getProcessStateLocked(packageName, uid, vers, processName) : null; - as = new AssociationState(this, packageName, className, processName); + as = new AssociationState(this, pkgs, className, processName, procs); pkgs.mAssociations.put(className, as); if (DEBUG) Slog.d(TAG, "GETASC: creating " + as + " in " + procs); return as; } + public void updateTrackingAssociationsLocked(int curSeq, long now) { + final int NUM = mTrackingAssociations.size(); + for (int i = NUM - 1; i >= 0; i--) { + final AssociationState.SourceState act = mTrackingAssociations.get(i); + if (act.mProcStateSeq != curSeq) { + act.mInTrackingList = false; + act.mProcState = STATE_NOTHING; + mTrackingAssociations.remove(i); + } else { + final ProcessState proc = act.getAssociationState().getProcess(); + if (proc != null) { + final int procState = proc.getCombinedState() % STATE_COUNT; + if (act.mProcState == procState) { + act.startActive(now); + } else { + act.stopActive(now); + if (act.mProcState < procState) { + Slog.w(TAG, "Tracking association " + act + " whose proc state " + + act.mProcState + " is better than process " + proc + + " proc state " + procState); + } + } + } else { + Slog.wtf(TAG, "Tracking association without process: " + act + + " in " + act.getAssociationState()); + } + } + } + } + public void dumpLocked(PrintWriter pw, String reqPackage, long now, boolean dumpSummary, boolean dumpAll, boolean activeOnly) { long totalTime = DumpUtils.dumpSingleTime(null, null, mMemFactorDurations, mMemFactor, @@ -1543,10 +1587,70 @@ public final class ProcessStats implements Parcelable { proc.dumpInternalLocked(pw, " ", dumpAll); } } + if (dumpAll) { - pw.println(); + if (sepNeeded) { + pw.println(); + } + sepNeeded = true; pw.print(" Total procs: "); pw.print(numShownProcs); pw.print(" shown of "); pw.print(numTotalProcs); pw.println(" total"); + if (mTrackingAssociations.size() > 0) { + pw.println(); + pw.println("Tracking associations:"); + for (int i = 0; i < mTrackingAssociations.size(); i++) { + final AssociationState.SourceState src = mTrackingAssociations.get(i); + final AssociationState asc = src.getAssociationState(); + pw.print(" #"); + pw.print(i); + pw.print(": "); + pw.print(asc.getProcessName()); + pw.print("/"); + UserHandle.formatUid(pw, asc.getUid()); + pw.print(" <- "); + pw.print(src.getProcessName()); + pw.print("/"); + UserHandle.formatUid(pw, src.getUid()); + pw.println(":"); + pw.print(" Tracking for: "); + TimeUtils.formatDuration(now - src.mTrackingUptime, pw); + pw.println(); + pw.print(" Component: "); + pw.print(new ComponentName(asc.getPackage(), asc.getName()) + .flattenToShortString()); + pw.println(); + pw.print(" Proc state: "); + if (src.mProcState != ProcessStats.STATE_NOTHING) { + pw.print(DumpUtils.STATE_NAMES[src.mProcState]); + } else { + pw.print("--"); + } + pw.print(" #"); + pw.println(src.mProcStateSeq); + pw.print(" Process: "); + pw.println(asc.getProcess()); + if (src.mActiveCount > 0) { + pw.print(" Active count "); + pw.print(src.mActiveCount); + long duration = src.mActiveDuration; + if (src.mActiveStartUptime > 0) { + duration += now - src.mActiveStartUptime; + } + if (dumpAll) { + pw.print(" / Duration "); + TimeUtils.formatDuration(duration, pw); + pw.print(" / "); + } else { + pw.print(" / time "); + } + DumpUtils.printPercent(pw, (double)duration/(double)totalTime); + if (src.mActiveStartUptime > 0) { + pw.print(" (running)"); + } + pw.println(); + } + } + } } if (sepNeeded) { @@ -1985,15 +2089,19 @@ public final class ProcessStats implements Parcelable { mVersionCode = versionCode; } - public AssociationState getAssociationStateLocked(String processName, String className) { + public AssociationState getAssociationStateLocked(ProcessState proc, String className) { AssociationState as = mAssociations.get(className); if (as != null) { if (DEBUG) Slog.d(TAG, "GETASC: returning existing " + as); + if (proc != null) { + as.setProcess(proc); + } return as; } - as = new AssociationState(mProcessStats, mPackageName, className, processName); + as = new AssociationState(mProcessStats, this, className, proc.getName(), + proc); mAssociations.put(className, as); - if (DEBUG) Slog.d(TAG, "GETASC: creating " + as + " in " + processName); + if (DEBUG) Slog.d(TAG, "GETASC: creating " + as + " in " + proc.getName()); return as; } } diff --git a/core/java/com/android/internal/app/procstats/ServiceState.java b/core/java/com/android/internal/app/procstats/ServiceState.java index 53ed5c0128d8..04e61e067f53 100644 --- a/core/java/com/android/internal/app/procstats/ServiceState.java +++ b/core/java/com/android/internal/app/procstats/ServiceState.java @@ -554,6 +554,6 @@ public final class ServiceState { public String toString() { return "ServiceState{" + Integer.toHexString(System.identityHashCode(this)) + " " + mName + " pkg=" + mPackage + " proc=" - + Integer.toHexString(System.identityHashCode(this)) + "}"; + + Integer.toHexString(System.identityHashCode(mProc)) + "}"; } } diff --git a/core/java/com/android/internal/os/BinderCallsStats.java b/core/java/com/android/internal/os/BinderCallsStats.java index 20eab92d018b..f87c081fd04f 100644 --- a/core/java/com/android/internal/os/BinderCallsStats.java +++ b/core/java/com/android/internal/os/BinderCallsStats.java @@ -16,6 +16,7 @@ package com.android.internal.os; +import android.annotation.Nullable; import android.os.Binder; import android.os.SystemClock; import android.os.UserHandle; @@ -23,6 +24,7 @@ import android.text.format.DateFormat; import android.util.ArrayMap; import android.util.Log; import android.util.Pair; +import android.util.Slog; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; @@ -75,10 +77,10 @@ public class BinderCallsStats { } public CallSession callStarted(Binder binder, int code) { - return callStarted(binder.getClass().getName(), code); + return callStarted(binder.getClass().getName(), code, binder.getTransactionName(code)); } - private CallSession callStarted(String className, int code) { + private CallSession callStarted(String className, int code, @Nullable String methodName) { if (!mEnabled) { return NOT_ENABLED; } @@ -90,6 +92,7 @@ public class BinderCallsStats { s.callStat.className = className; s.callStat.msg = code; + s.callStat.methodName = methodName; s.exceptionThrown = false; s.cpuTimeStarted = -1; s.timeStarted = -1; @@ -169,6 +172,7 @@ public class BinderCallsStats { callStat = s.sampledCallStat; } callStat.callCount++; + callStat.methodName = s.callStat.methodName; if (s.cpuTimeStarted >= 0) { callStat.cpuTimeMicros += duration; callStat.maxCpuTimeMicros = Math.max(callStat.maxCpuTimeMicros, duration); @@ -209,6 +213,39 @@ public class BinderCallsStats { } } + public ArrayList<ExportedCallStat> getExportedCallStats() { + // We do not collect all the data if detailed tracking is off. + if (!mDetailedTracking) { + return new ArrayList<ExportedCallStat>(); + } + + ArrayList<ExportedCallStat> resultCallStats = new ArrayList<>(); + synchronized (mLock) { + int uidEntriesSize = mUidEntries.size(); + for (int entryIdx = 0; entryIdx < uidEntriesSize; entryIdx++){ + UidEntry entry = mUidEntries.valueAt(entryIdx); + for (CallStat stat : entry.getCallStatsList()) { + ExportedCallStat exported = new ExportedCallStat(); + exported.uid = entry.uid; + exported.className = stat.className; + exported.methodName = stat.methodName == null + ? String.valueOf(stat.msg) : stat.methodName; + exported.cpuTimeMicros = stat.cpuTimeMicros; + exported.maxCpuTimeMicros = stat.maxCpuTimeMicros; + exported.latencyMicros = stat.latencyMicros; + exported.maxLatencyMicros = stat.maxLatencyMicros; + exported.callCount = stat.callCount; + exported.maxRequestSizeBytes = stat.maxRequestSizeBytes; + exported.maxReplySizeBytes = stat.maxReplySizeBytes; + exported.exceptionCount = stat.exceptionCount; + resultCallStats.add(exported); + } + } + } + + return resultCallStats; + } + public void dump(PrintWriter pw, Map<Integer,String> appIdToPkgNameMap, boolean verbose) { synchronized (mLock) { dumpLocked(pw, appIdToPkgNameMap, verbose); @@ -250,6 +287,7 @@ public class BinderCallsStats { sb.setLength(0); sb.append(" ") .append(uidToString(uidEntry.uid, appIdToPkgNameMap)) + .append(",").append(e) .append(',').append(e.cpuTimeMicros) .append(',').append(e.maxCpuTimeMicros) .append(',').append(e.latencyMicros) @@ -368,10 +406,30 @@ public class BinderCallsStats { } } + /** + * Aggregated data by uid/class/method to be sent through WestWorld. + */ + public static class ExportedCallStat { + public int uid; + public String className; + public String methodName; + public long cpuTimeMicros; + public long maxCpuTimeMicros; + public long latencyMicros; + public long maxLatencyMicros; + public long callCount; + public long maxRequestSizeBytes; + public long maxReplySizeBytes; + public long exceptionCount; + } + @VisibleForTesting public static class CallStat { public String className; public int msg; + // Method name might be null when we cannot resolve the transaction code. For instance, if + // the binder was not generated by AIDL. + public @Nullable String methodName; public long cpuTimeMicros; public long maxCpuTimeMicros; public long latencyMicros; @@ -410,7 +468,7 @@ public class BinderCallsStats { @Override public String toString() { - return className + "/" + msg; + return className + "#" + (methodName == null ? msg : methodName); } } diff --git a/core/java/com/android/internal/util/ContrastColorUtil.java b/core/java/com/android/internal/util/ContrastColorUtil.java index 60dd86b1fc10..16ca4fcdbe34 100644 --- a/core/java/com/android/internal/util/ContrastColorUtil.java +++ b/core/java/com/android/internal/util/ContrastColorUtil.java @@ -454,9 +454,12 @@ public class ContrastColorUtil { /** * Resolves {@param color} to an actual color if it is {@link Notification#COLOR_DEFAULT} */ - public static int resolveColor(Context context, int color) { + public static int resolveColor(Context context, int color, boolean defaultBackgroundIsDark) { if (color == Notification.COLOR_DEFAULT) { - return context.getColor(com.android.internal.R.color.notification_default_color_light); + int res = defaultBackgroundIsDark + ? com.android.internal.R.color.notification_default_color_dark + : com.android.internal.R.color.notification_default_color_light; + return context.getColor(res); } return color; } @@ -486,7 +489,7 @@ public class ContrastColorUtil { */ public static int resolveContrastColor(Context context, int notificationColor, int backgroundColor, boolean isDark) { - final int resolvedColor = resolveColor(context, notificationColor); + final int resolvedColor = resolveColor(context, notificationColor, isDark); int color = resolvedColor; color = ContrastColorUtil.ensureTextContrast(color, backgroundColor, isDark); @@ -520,7 +523,8 @@ public class ContrastColorUtil { } public static int resolveAmbientColor(Context context, int notificationColor) { - final int resolvedColor = resolveColor(context, notificationColor); + final int resolvedColor = resolveColor(context, notificationColor, + true /* defaultBackgroundIsDark */); int color = resolvedColor; color = ContrastColorUtil.ensureTextContrastOnBlack(color); @@ -538,8 +542,9 @@ public class ContrastColorUtil { return color; } - public static int resolvePrimaryColor(Context context, int backgroundColor) { - boolean useDark = shouldUseDark(backgroundColor); + public static int resolvePrimaryColor(Context context, int backgroundColor, + boolean defaultBackgroundIsDark) { + boolean useDark = shouldUseDark(backgroundColor, defaultBackgroundIsDark); if (useDark) { return context.getColor( com.android.internal.R.color.notification_primary_text_color_light); @@ -549,8 +554,9 @@ public class ContrastColorUtil { } } - public static int resolveSecondaryColor(Context context, int backgroundColor) { - boolean useDark = shouldUseDark(backgroundColor); + public static int resolveSecondaryColor(Context context, int backgroundColor, + boolean defaultBackgroundIsDark) { + boolean useDark = shouldUseDark(backgroundColor, defaultBackgroundIsDark); if (useDark) { return context.getColor( com.android.internal.R.color.notification_secondary_text_color_light); @@ -560,8 +566,9 @@ public class ContrastColorUtil { } } - public static int resolveDefaultColor(Context context, int backgroundColor) { - boolean useDark = shouldUseDark(backgroundColor); + public static int resolveDefaultColor(Context context, int backgroundColor, + boolean defaultBackgroundIsDark) { + boolean useDark = shouldUseDark(backgroundColor, defaultBackgroundIsDark); if (useDark) { return context.getColor( com.android.internal.R.color.notification_default_color_light); @@ -591,12 +598,11 @@ public class ContrastColorUtil { return ColorUtilsFromCompat.LABToColor(result[0], result[1], result[2]); } - private static boolean shouldUseDark(int backgroundColor) { - boolean useDark = backgroundColor == Notification.COLOR_DEFAULT; - if (!useDark) { - useDark = ColorUtilsFromCompat.calculateLuminance(backgroundColor) > 0.5; + private static boolean shouldUseDark(int backgroundColor, boolean defaultBackgroundIsDark) { + if (backgroundColor == Notification.COLOR_DEFAULT) { + return !defaultBackgroundIsDark; } - return useDark; + return ColorUtilsFromCompat.calculateLuminance(backgroundColor) > 0.5; } public static double calculateLuminance(int backgroundColor) { diff --git a/core/jni/Android.bp b/core/jni/Android.bp index d4903d80c7e2..b675698a461b 100644 --- a/core/jni/Android.bp +++ b/core/jni/Android.bp @@ -126,7 +126,6 @@ cc_library_shared { "android/graphics/Camera.cpp", "android/graphics/CanvasProperty.cpp", "android/graphics/ColorFilter.cpp", - "android/graphics/DrawFilter.cpp", "android/graphics/FontFamily.cpp", "android/graphics/FontUtils.cpp", "android/graphics/CreateJavaOutputStreamAdaptor.cpp", @@ -143,6 +142,7 @@ cc_library_shared { "android/graphics/NinePatch.cpp", "android/graphics/NinePatchPeeker.cpp", "android/graphics/Paint.cpp", + "android/graphics/PaintFilter.cpp", "android/graphics/Path.cpp", "android/graphics/PathMeasure.cpp", "android/graphics/PathEffect.cpp", @@ -220,7 +220,6 @@ cc_library_shared { "external/skia/src/image", "external/skia/src/images", "frameworks/base/media/jni", - "libcore/include", "system/media/camera/include", "system/media/private/camera/include", ], diff --git a/core/jni/android/graphics/DrawFilter.cpp b/core/jni/android/graphics/PaintFilter.cpp index c1dc0dd025b6..182b22b3c917 100644 --- a/core/jni/android/graphics/DrawFilter.cpp +++ b/core/jni/android/graphics/PaintFilter.cpp @@ -15,36 +15,43 @@ ** limitations under the License. */ -// This file was generated from the C++ include file: SkColorFilter.h -// Any changes made to this file will be discarded by the build. -// To change this file, either edit the include, or device/tools/gluemaker/main.cpp, -// or one of the auxilary file specifications in device/tools/gluemaker. - #include "jni.h" #include "GraphicsJNI.h" #include <android_runtime/AndroidRuntime.h> #include "core_jni_helpers.h" -#include "SkDrawFilter.h" -#include "SkPaintFlagsDrawFilter.h" +#include "hwui/PaintFilter.h" #include "SkPaint.h" namespace android { -// Custom version of SkPaintFlagsDrawFilter that also calls setFilterQuality. -class CompatFlagsDrawFilter : public SkPaintFlagsDrawFilter { +class PaintFlagsFilter : public PaintFilter { public: - CompatFlagsDrawFilter(uint32_t clearFlags, uint32_t setFlags, - SkFilterQuality desiredQuality) - : SkPaintFlagsDrawFilter(clearFlags, setFlags) + PaintFlagsFilter(uint32_t clearFlags, uint32_t setFlags) { + fClearFlags = static_cast<uint16_t>(clearFlags & SkPaint::kAllFlags); + fSetFlags = static_cast<uint16_t>(setFlags & SkPaint::kAllFlags); + } + void filter(SkPaint* paint) override { + paint->setFlags((paint->getFlags() & ~fClearFlags) | fSetFlags); + } + +private: + uint16_t fClearFlags; + uint16_t fSetFlags; +}; + +// Custom version of PaintFlagsDrawFilter that also calls setFilterQuality. +class CompatPaintFlagsFilter : public PaintFlagsFilter { +public: + CompatPaintFlagsFilter(uint32_t clearFlags, uint32_t setFlags, SkFilterQuality desiredQuality) + : PaintFlagsFilter(clearFlags, setFlags) , fDesiredQuality(desiredQuality) { } - virtual bool filter(SkPaint* paint, Type type) { - SkPaintFlagsDrawFilter::filter(paint, type); + virtual void filter(SkPaint* paint) { + PaintFlagsFilter::filter(paint); paint->setFilterQuality(fDesiredQuality); - return true; } private: @@ -61,16 +68,16 @@ static inline bool hadFiltering(jint& flags) { return result; } -class SkDrawFilterGlue { +class PaintFilterGlue { public: static void finalizer(JNIEnv* env, jobject clazz, jlong objHandle) { - SkDrawFilter* obj = reinterpret_cast<SkDrawFilter*>(objHandle); + PaintFilter* obj = reinterpret_cast<PaintFilter*>(objHandle); SkSafeUnref(obj); } - static jlong CreatePaintFlagsDF(JNIEnv* env, jobject clazz, - jint clearFlags, jint setFlags) { + static jlong CreatePaintFlagsFilter(JNIEnv* env, jobject clazz, + jint clearFlags, jint setFlags) { if (clearFlags | setFlags) { // Mask both groups of flags to remove FILTER_BITMAP_FLAG, which no // longer has a Skia equivalent flag (instead it corresponds to @@ -79,16 +86,16 @@ public: const bool turnFilteringOn = hadFiltering(setFlags); const bool turnFilteringOff = hadFiltering(clearFlags); - SkDrawFilter* filter; + PaintFilter* filter; if (turnFilteringOn) { // Turning filtering on overrides turning it off. - filter = new CompatFlagsDrawFilter(clearFlags, setFlags, + filter = new CompatPaintFlagsFilter(clearFlags, setFlags, kLow_SkFilterQuality); } else if (turnFilteringOff) { - filter = new CompatFlagsDrawFilter(clearFlags, setFlags, + filter = new CompatPaintFlagsFilter(clearFlags, setFlags, kNone_SkFilterQuality); } else { - filter = new SkPaintFlagsDrawFilter(clearFlags, setFlags); + filter = new PaintFlagsFilter(clearFlags, setFlags); } return reinterpret_cast<jlong>(filter); } else { @@ -98,11 +105,11 @@ public: }; static const JNINativeMethod drawfilter_methods[] = { - {"nativeDestructor", "(J)V", (void*) SkDrawFilterGlue::finalizer} + {"nativeDestructor", "(J)V", (void*) PaintFilterGlue::finalizer} }; static const JNINativeMethod paintflags_methods[] = { - {"nativeConstructor","(II)J", (void*) SkDrawFilterGlue::CreatePaintFlagsDF} + {"nativeConstructor","(II)J", (void*) PaintFilterGlue::CreatePaintFlagsFilter} }; int register_android_graphics_DrawFilter(JNIEnv* env) { @@ -110,7 +117,7 @@ int register_android_graphics_DrawFilter(JNIEnv* env) { NELEM(drawfilter_methods)); result |= RegisterMethodsOrDie(env, "android/graphics/PaintFlagsDrawFilter", paintflags_methods, NELEM(paintflags_methods)); - + return 0; } diff --git a/core/jni/android_graphics_Canvas.cpp b/core/jni/android_graphics_Canvas.cpp index 484b33f1ca9e..3b024b442060 100644 --- a/core/jni/android_graphics_Canvas.cpp +++ b/core/jni/android_graphics_Canvas.cpp @@ -22,13 +22,13 @@ #include <androidfw/ResourceTypes.h> #include <hwui/Canvas.h> #include <hwui/Paint.h> +#include <hwui/PaintFilter.h> #include <hwui/Typeface.h> #include <minikin/Layout.h> #include <nativehelper/ScopedPrimitiveArray.h> #include <nativehelper/ScopedStringChars.h> #include "Bitmap.h" -#include "SkDrawFilter.h" #include "SkGraphics.h" #include "SkRegion.h" #include "SkVertices.h" @@ -582,8 +582,9 @@ static void drawTextOnPathString(JNIEnv* env, jobject, jlong canvasHandle, jstri env->ReleaseStringChars(text, jchars); } -static void setDrawFilter(jlong canvasHandle, jlong filterHandle) { - get_canvas(canvasHandle)->setDrawFilter(reinterpret_cast<SkDrawFilter*>(filterHandle)); +static void setPaintFilter(jlong canvasHandle, jlong filterHandle) { + PaintFilter* paintFilter = reinterpret_cast<PaintFilter*>(filterHandle); + get_canvas(canvasHandle)->setPaintFilter(sk_ref_sp(paintFilter)); } static void freeCaches(JNIEnv* env, jobject) { @@ -633,7 +634,7 @@ static const JNINativeMethod gMethods[] = { {"nQuickReject","(JFFFF)Z", (void*)CanvasJNI::quickRejectRect}, {"nClipRect","(JFFFFI)Z", (void*) CanvasJNI::clipRect}, {"nClipPath","(JJI)Z", (void*) CanvasJNI::clipPath}, - {"nSetDrawFilter", "(JJ)V", (void*) CanvasJNI::setDrawFilter}, + {"nSetDrawFilter", "(JJ)V", (void*) CanvasJNI::setPaintFilter}, }; // If called from Canvas these are regular JNI diff --git a/core/jni/android_media_MediaMetricsJNI.h b/core/jni/android_media_MediaMetricsJNI.h index 16081b4af1cb..b3cb4d293399 100644 --- a/core/jni/android_media_MediaMetricsJNI.h +++ b/core/jni/android_media_MediaMetricsJNI.h @@ -17,7 +17,6 @@ #ifndef _ANDROID_MEDIA_MEDIAMETRICSJNI_H_ #define _ANDROID_MEDIA_MEDIAMETRICSJNI_H_ -#include <android_runtime/AndroidRuntime.h> #include <jni.h> #include <nativehelper/JNIHelp.h> #include <media/MediaAnalyticsItem.h> diff --git a/core/jni/android_text_MeasuredParagraph.cpp b/core/jni/android_text_MeasuredParagraph.cpp index 41a81accf72c..9eb6f8d4189a 100644 --- a/core/jni/android_text_MeasuredParagraph.cpp +++ b/core/jni/android_text_MeasuredParagraph.cpp @@ -17,7 +17,6 @@ #define LOG_TAG "MeasuredParagraph" #include "GraphicsJNI.h" -#include "ScopedIcuLocale.h" #include "unicode/locid.h" #include "unicode/brkiter.h" #include "utils/misc.h" diff --git a/core/jni/android_text_StaticLayout.cpp b/core/jni/android_text_StaticLayout.cpp index ad84858bb8db..fec5b6995646 100644 --- a/core/jni/android_text_StaticLayout.cpp +++ b/core/jni/android_text_StaticLayout.cpp @@ -16,7 +16,6 @@ #define LOG_TAG "StaticLayout" -#include "ScopedIcuLocale.h" #include "unicode/locid.h" #include "unicode/brkiter.h" #include "utils/misc.h" diff --git a/core/jni/android_view_RenderNode.cpp b/core/jni/android_view_RenderNode.cpp index 22bbc3c48119..46b19bdf660f 100644 --- a/core/jni/android_view_RenderNode.cpp +++ b/core/jni/android_view_RenderNode.cpp @@ -222,6 +222,11 @@ static jboolean android_view_RenderNode_setHasOverlappingRendering(jlong renderN RenderNode::GENERIC); } +static void android_view_RenderNode_setUsageHint(jlong renderNodePtr, jint usageHint) { + RenderNode* renderNode = reinterpret_cast<RenderNode*>(renderNodePtr); + renderNode->setUsageHint(static_cast<UsageHint>(usageHint)); +} + static jboolean android_view_RenderNode_setElevation(jlong renderNodePtr, float elevation) { return SET_AND_DIRTY(setElevation, elevation, RenderNode::Z); } @@ -614,6 +619,7 @@ static const JNINativeMethod gMethods[] = { { "nSetAlpha", "(JF)Z", (void*) android_view_RenderNode_setAlpha }, { "nSetHasOverlappingRendering", "(JZ)Z", (void*) android_view_RenderNode_setHasOverlappingRendering }, + { "nSetUsageHint", "(JI)V", (void*) android_view_RenderNode_setUsageHint }, { "nSetElevation", "(JF)Z", (void*) android_view_RenderNode_setElevation }, { "nSetTranslationX", "(JF)Z", (void*) android_view_RenderNode_setTranslationX }, { "nSetTranslationY", "(JF)Z", (void*) android_view_RenderNode_setTranslationY }, diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index e2a5c57fecd6..472df1a4ab70 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -2437,7 +2437,8 @@ <permission android:name="android.permission.ASEC_RENAME" android:protectionLevel="signature" /> - <!-- @SystemApi Allows applications to write the apn settings. + <!-- @SystemApi Allows applications to write the apn settings and read sensitive fields of + an existing apn settings like user and password. <p>Not for use by third-party applications. --> <permission android:name="android.permission.WRITE_APN_SETTINGS" android:protectionLevel="signature|privileged" /> diff --git a/core/res/res/drawable-hdpi/ic_grayedout_printer.png b/core/res/res/drawable-hdpi/ic_grayedout_printer.png Binary files differdeleted file mode 100644 index 5e54970d6b3f..000000000000 --- a/core/res/res/drawable-hdpi/ic_grayedout_printer.png +++ /dev/null diff --git a/core/res/res/drawable-mdpi/ic_grayedout_printer.png b/core/res/res/drawable-mdpi/ic_grayedout_printer.png Binary files differdeleted file mode 100644 index 5e54970d6b3f..000000000000 --- a/core/res/res/drawable-mdpi/ic_grayedout_printer.png +++ /dev/null diff --git a/core/res/res/drawable-xhdpi/ic_grayedout_printer.png b/core/res/res/drawable-xhdpi/ic_grayedout_printer.png Binary files differdeleted file mode 100644 index 5e54970d6b3f..000000000000 --- a/core/res/res/drawable-xhdpi/ic_grayedout_printer.png +++ /dev/null diff --git a/core/res/res/values-land/dimens_package_installer.xml b/core/res/res/values-land/dimens_package_installer.xml new file mode 100644 index 000000000000..72f4ec22c36f --- /dev/null +++ b/core/res/res/values-land/dimens_package_installer.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. + --> + +<!-- Landscape dimensions for the permission grant dialog. --> +<resources> + <!-- 37:20 == 65% width --> + <dimen name="permissionGrantDialogWidth">37</dimen> +</resources> diff --git a/packages/PrintSpooler/res/drawable/print_warning.xml b/core/res/res/values-mcc334-mnc03/config.xml index 35f0fed78a7f..c0d2b3598f82 100644 --- a/packages/PrintSpooler/res/drawable/print_warning.xml +++ b/core/res/res/values-mcc334-mnc03/config.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2016 The Android Open Source Project +<!-- 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. @@ -14,12 +14,7 @@ limitations under the License. --> -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="96dp" - android:height="96dp" - android:viewportWidth="96.0" - android:viewportHeight="96.0"> - <path - android:fillColor="#C8CCCE" - android:pathData="M4,84H92L48,8 4,84zM52,72h-8v-8h8v8zM52,56H44V40h8v16z"/> -</vector> +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <!-- Show area update info settings in CellBroadcastReceiver and information in SIM status in Settings app --> + <bool name="config_showAreaUpdateInfoSettings">true</bool> +</resources> diff --git a/core/res/res/values-mcc334-mnc030/config.xml b/core/res/res/values-mcc334-mnc030/config.xml new file mode 100644 index 000000000000..c0d2b3598f82 --- /dev/null +++ b/core/res/res/values-mcc334-mnc030/config.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <!-- Show area update info settings in CellBroadcastReceiver and information in SIM status in Settings app --> + <bool name="config_showAreaUpdateInfoSettings">true</bool> +</resources> diff --git a/core/res/res/values-mcc704-mnc03/config.xml b/core/res/res/values-mcc704-mnc03/config.xml new file mode 100644 index 000000000000..c0d2b3598f82 --- /dev/null +++ b/core/res/res/values-mcc704-mnc03/config.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <!-- Show area update info settings in CellBroadcastReceiver and information in SIM status in Settings app --> + <bool name="config_showAreaUpdateInfoSettings">true</bool> +</resources> diff --git a/core/res/res/values-mcc706-mnc04/config.xml b/core/res/res/values-mcc706-mnc04/config.xml new file mode 100644 index 000000000000..c0d2b3598f82 --- /dev/null +++ b/core/res/res/values-mcc706-mnc04/config.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <!-- Show area update info settings in CellBroadcastReceiver and information in SIM status in Settings app --> + <bool name="config_showAreaUpdateInfoSettings">true</bool> +</resources> diff --git a/core/res/res/values-mcc712-mnc04/config.xml b/core/res/res/values-mcc712-mnc04/config.xml new file mode 100644 index 000000000000..c0d2b3598f82 --- /dev/null +++ b/core/res/res/values-mcc712-mnc04/config.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <!-- Show area update info settings in CellBroadcastReceiver and information in SIM status in Settings app --> + <bool name="config_showAreaUpdateInfoSettings">true</bool> +</resources> diff --git a/core/res/res/values-mcc716-mnc06/config.xml b/core/res/res/values-mcc716-mnc06/config.xml new file mode 100644 index 000000000000..c0d2b3598f82 --- /dev/null +++ b/core/res/res/values-mcc716-mnc06/config.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <!-- Show area update info settings in CellBroadcastReceiver and information in SIM status in Settings app --> + <bool name="config_showAreaUpdateInfoSettings">true</bool> +</resources> diff --git a/core/res/res/values-mcc716-mnc10/config.xml b/core/res/res/values-mcc716-mnc10/config.xml new file mode 100644 index 000000000000..c0d2b3598f82 --- /dev/null +++ b/core/res/res/values-mcc716-mnc10/config.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <!-- Show area update info settings in CellBroadcastReceiver and information in SIM status in Settings app --> + <bool name="config_showAreaUpdateInfoSettings">true</bool> +</resources> diff --git a/core/res/res/values-mcc716-mnc17/config.xml b/core/res/res/values-mcc716-mnc17/config.xml new file mode 100644 index 000000000000..c0d2b3598f82 --- /dev/null +++ b/core/res/res/values-mcc716-mnc17/config.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <!-- Show area update info settings in CellBroadcastReceiver and information in SIM status in Settings app --> + <bool name="config_showAreaUpdateInfoSettings">true</bool> +</resources> diff --git a/core/res/res/values-mcc722-mnc07/config.xml b/core/res/res/values-mcc722-mnc07/config.xml new file mode 100644 index 000000000000..c0d2b3598f82 --- /dev/null +++ b/core/res/res/values-mcc722-mnc07/config.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <!-- Show area update info settings in CellBroadcastReceiver and information in SIM status in Settings app --> + <bool name="config_showAreaUpdateInfoSettings">true</bool> +</resources> diff --git a/core/res/res/values-mcc732-mnc123/config.xml b/core/res/res/values-mcc732-mnc123/config.xml new file mode 100644 index 000000000000..c0d2b3598f82 --- /dev/null +++ b/core/res/res/values-mcc732-mnc123/config.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <!-- Show area update info settings in CellBroadcastReceiver and information in SIM status in Settings app --> + <bool name="config_showAreaUpdateInfoSettings">true</bool> +</resources> diff --git a/core/res/res/values-mcc740-mnc00/config.xml b/core/res/res/values-mcc740-mnc00/config.xml new file mode 100644 index 000000000000..c0d2b3598f82 --- /dev/null +++ b/core/res/res/values-mcc740-mnc00/config.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <!-- Show area update info settings in CellBroadcastReceiver and information in SIM status in Settings app --> + <bool name="config_showAreaUpdateInfoSettings">true</bool> +</resources> diff --git a/core/res/res/values-night/colors.xml b/core/res/res/values-night/colors.xml new file mode 100644 index 000000000000..688040ba0cc1 --- /dev/null +++ b/core/res/res/values-night/colors.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + + NOTE: You might also want to edit: packages/SystemUI/res/values-night/colors.xml + --> +<resources> + <!-- The primary text color if the text is on top of a dark background. + This is also affects colorized notifications with dark backgrounds. --> + <color name="notification_primary_text_color_dark">#dadada</color> + + <!-- The secondary text color if the text is on top of a dark background. --> + <color name="notification_secondary_text_color_dark">#dadada</color> + + <color name="notification_default_color_dark">#dadada</color> + + <!-- The background color of a notification card. --> + <color name="notification_material_background_color">@*android:color/material_grey_900</color> +</resources>
\ No newline at end of file diff --git a/core/res/res/values-night/values.xml b/core/res/res/values-night/values.xml index 23b0a24fff98..4eb2ff3b5970 100644 --- a/core/res/res/values-night/values.xml +++ b/core/res/res/values-night/values.xml @@ -31,4 +31,9 @@ <!-- volume background --> <item name="panelColorBackground">@color/material_grey_800</item> </style> + + <style name="TextAppearance.Material.Notification"> + <item name="textColor">@color/notification_secondary_text_color_dark</item> + <item name="textSize">@dimen/notification_text_size</item> + </style> </resources>
\ No newline at end of file diff --git a/core/res/res/values-port/dimens_package_installer.xml b/core/res/res/values-port/dimens_package_installer.xml new file mode 100644 index 000000000000..67cafe752409 --- /dev/null +++ b/core/res/res/values-port/dimens_package_installer.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. + --> + +<!-- portrait dimensions for the permission grant dialog. --> +<resources> + <!-- 380:20 == 95% width --> + <dimen name="permissionGrantDialogWidth">380</dimen> +</resources> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 18624370ccfa..e4ce2b1e91ca 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -3414,7 +3414,7 @@ <string-array translatable="false" name="config_batteryPackageTypeService"/> <!-- Flag indicating whether or not to enable night mode detection. --> - <bool name="config_enableNightMode">false</bool> + <bool name="config_enableNightMode">true</bool> <!-- Flag indicating that the actions buttons for a notification should be tinted with by the color supplied by the Notification.Builder if present. --> diff --git a/core/res/res/values/styles_package_installer.xml b/core/res/res/values/styles_package_installer.xml new file mode 100644 index 000000000000..8bfcc8dcde76 --- /dev/null +++ b/core/res/res/values/styles_package_installer.xml @@ -0,0 +1,97 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. + --> + +<!-- styles for the permission grant dialog. --> +<resources> + <style name="PermissionGrantDialog"> + <item name="background">?attr/windowBackground</item> + <item name="elevation">?attr/windowElevation</item> + <item name="layout_weight">@dimen/permissionGrantDialogWidth</item> + </style> + + <style name="PermissionGrantTitleIcon"> + <item name="layout_width">36dp</item> + <item name="layout_height">36dp</item> + <item name="tint">?attr/colorAccent</item> + <item name="scaleType">fitCenter</item> + </style> + + <style name="PermissionGrantTitleMessage" + parent="@style/TextAppearance.DeviceDefault"> + <item name="paddingStart">22dp</item> + <item name="textSize">20sp</item> + <item name="textColor">?attr/textColorPrimary</item> + </style> + + <style name="PermissionGrantIndex" + parent="@style/TextAppearance.DeviceDefault"> + <item name="paddingEnd">12dp</item> + <item name="singleLine">true</item> + <item name="textColor">?attr/textColorSecondary</item> + </style> + + <style name="PermissionGrantDescription"> + <item name="layout_marginTop">20dp</item> + <item name="layout_marginStart">24dp</item> + <item name="layout_marginBottom">16dp</item> + <item name="layout_marginEnd">24dp</item> + </style> + + <style name="PermissionGrantContent"> + <item name="layout_marginStart">16dp</item> + <item name="layout_marginEnd">24dp</item> + </style> + + <style name="PermissionGrantRadioGroup"> + <item name="layout_marginStart">8dp</item> + <item name="layout_marginTop">-4dp</item> + </style> + + <style name="PermissionGrantRadioButton" + parent="@style/Widget.DeviceDefault.CompoundButton.RadioButton"> + <item name="paddingStart">16dp</item> + <item name="paddingTop">8dp</item> + <item name="paddingBottom">8dp</item> + <item name="textSize">16sp</item> + </style> + + <style name="PermissionGrantDetailMessage" + parent="@style/TextAppearance.DeviceDefault"> + <item name="layout_marginStart">8dp</item> + <item name="layout_marginBottom">4dp</item> + <item name="textColor">?attr/textColorPrimary</item> + <item name="textSize">16sp</item> + </style> + + <style name="PermissionGrantDetailMessageSpace"> + <item name="layout_height">8dp</item> + </style> + + <style name="PermissionGrantCheckbox" + parent="@style/Widget.DeviceDefault.CompoundButton.CheckBox"> + <item name="paddingStart">16dp</item> + <item name="textColor">?attr/textColorSecondary</item> + <item name="buttonTint">?attr/textColorSecondary</item> + <item name="textSize">16sp</item> + </style> + + <style name="PermissionGrantButtonBar"> + <item name="layout_marginStart">24dp</item> + <item name="layout_marginEnd">16dp</item> + <item name="layout_marginBottom">4dp</item> + </style> +</resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 99001e3a9209..6a58b670b666 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -1339,7 +1339,6 @@ <java-symbol type="drawable" name="ic_text_dot" /> <java-symbol type="drawable" name="ic_print" /> <java-symbol type="drawable" name="ic_print_error" /> - <java-symbol type="drawable" name="ic_grayedout_printer" /> <java-symbol type="drawable" name="jog_dial_arrow_long_left_green" /> <java-symbol type="drawable" name="jog_dial_arrow_long_right_red" /> <java-symbol type="drawable" name="jog_dial_arrow_short_left_and_right" /> diff --git a/core/res/res/values/themes_package_installer.xml b/core/res/res/values/themes_package_installer.xml new file mode 100644 index 000000000000..a6341dc664ab --- /dev/null +++ b/core/res/res/values/themes_package_installer.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. + --> + +<!-- themes for the permission grant dialog. --> +<resources> + <style name="Theme.DeviceDefault.Light.Panel.PermissionGrantApp" + parent="@style/Theme.DeviceDefault.Light.Panel"> + <item name="windowIsFloating">false</item> + <item name="windowTranslucentStatus">true</item> + <item name="backgroundDimEnabled">true</item> + <item name="windowAnimationStyle">@style/Animation.Material.Dialog</item> + </style> + + <style name="Theme.DeviceDefault.Light.Dialog.PermissionGrant" + parent="@style/Theme.DeviceDefault.Light.Dialog"> + <item name="titleTextStyle">@style/PermissionGrantTitleMessage</item> + <item name="radioButtonStyle">@style/PermissionGrantRadioButton</item> + <item name="checkboxStyle">@style/PermissionGrantCheckbox</item> + <item name="buttonBarStyle">@style/PermissionGrantButtonBar</item> + </style> +</resources> diff --git a/core/tests/benchmarks/src/android/os/FileUtilsBenchmark.java b/core/tests/benchmarks/src/android/os/FileUtilsBenchmark.java index 5989da7af655..c70fc3cce43c 100644 --- a/core/tests/benchmarks/src/android/os/FileUtilsBenchmark.java +++ b/core/tests/benchmarks/src/android/os/FileUtilsBenchmark.java @@ -54,7 +54,7 @@ public class FileUtilsBenchmark { for (int i = 0; i < reps; i++) { try (FileInputStream in = new FileInputStream(mSrc); FileOutputStream out = new FileOutputStream(mDest)) { - copyInternalUserspace(in.getFD(), out.getFD(), null, null, Long.MAX_VALUE); + copyInternalUserspace(in.getFD(), out.getFD(), Long.MAX_VALUE, null, null, null); } } } @@ -63,7 +63,7 @@ public class FileUtilsBenchmark { for (int i = 0; i < reps; i++) { try (FileInputStream in = new FileInputStream(mSrc); FileOutputStream out = new FileOutputStream(mDest)) { - copyInternalSendfile(in.getFD(), out.getFD(), null, null, Long.MAX_VALUE); + copyInternalSendfile(in.getFD(), out.getFD(), Long.MAX_VALUE, null, null, null); } } } @@ -72,7 +72,7 @@ public class FileUtilsBenchmark { for (int i = 0; i < reps; i++) { try (MemoryPipe in = MemoryPipe.createSource(mData); FileOutputStream out = new FileOutputStream(mDest)) { - copyInternalUserspace(in.getFD(), out.getFD(), null, null, Long.MAX_VALUE); + copyInternalUserspace(in.getFD(), out.getFD(), Long.MAX_VALUE, null, null, null); } } } @@ -81,7 +81,7 @@ public class FileUtilsBenchmark { for (int i = 0; i < reps; i++) { try (MemoryPipe in = MemoryPipe.createSource(mData); FileOutputStream out = new FileOutputStream(mDest)) { - copyInternalSplice(in.getFD(), out.getFD(), null, null, Long.MAX_VALUE); + copyInternalSplice(in.getFD(), out.getFD(), Long.MAX_VALUE, null, null, null); } } } @@ -90,7 +90,7 @@ public class FileUtilsBenchmark { for (int i = 0; i < reps; i++) { try (FileInputStream in = new FileInputStream(mSrc); MemoryPipe out = MemoryPipe.createSink(mData)) { - copyInternalUserspace(in.getFD(), out.getFD(), null, null, Long.MAX_VALUE); + copyInternalUserspace(in.getFD(), out.getFD(), Long.MAX_VALUE, null, null, null); } } } @@ -99,7 +99,7 @@ public class FileUtilsBenchmark { for (int i = 0; i < reps; i++) { try (FileInputStream in = new FileInputStream(mSrc); MemoryPipe out = MemoryPipe.createSink(mData)) { - copyInternalSplice(in.getFD(), out.getFD(), null, null, Long.MAX_VALUE); + copyInternalSplice(in.getFD(), out.getFD(), Long.MAX_VALUE, null, null, null); } } } diff --git a/core/tests/bluetoothtests/src/android/bluetooth/BluetoothTestUtils.java b/core/tests/bluetoothtests/src/android/bluetooth/BluetoothTestUtils.java index ada03666b7ba..b906d84adf52 100644 --- a/core/tests/bluetoothtests/src/android/bluetooth/BluetoothTestUtils.java +++ b/core/tests/bluetoothtests/src/android/bluetooth/BluetoothTestUtils.java @@ -446,7 +446,10 @@ public class BluetoothTestUtils extends Assert { final IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); mContext.registerReceiver(receiver, filter); - assertTrue(adapter.enable()); + // Note: for Wear Local Edition builds, which have Permission Review Mode enabled to + // obey China CMIIT, BluetoothAdapter may not startup immediately on methods enable/disable. + // So no assertion applied here. + adapter.enable(); boolean success = false; try { success = completionSemaphore.tryAcquire(ENABLE_DISABLE_TIMEOUT, TimeUnit.MILLISECONDS); @@ -489,7 +492,10 @@ public class BluetoothTestUtils extends Assert { final IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); mContext.registerReceiver(receiver, filter); - assertTrue(adapter.disable()); + // Note: for Wear Local Edition builds, which have Permission Review Mode enabled to + // obey China CMIIT, BluetoothAdapter may not startup immediately on methods enable/disable. + // So no assertion applied here. + adapter.disable(); boolean success = false; try { success = completionSemaphore.tryAcquire(ENABLE_DISABLE_TIMEOUT, TimeUnit.MILLISECONDS); diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java index fe58116002f2..3d114f4b7c6c 100644 --- a/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java +++ b/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java @@ -47,6 +47,7 @@ import android.os.Parcelable; import android.platform.test.annotations.Presubmit; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; +import android.util.ArrayMap; import org.junit.Before; import org.junit.Test; @@ -56,6 +57,7 @@ import org.mockito.InOrder; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; /** Test {@link TransactionExecutor} logic. */ @@ -232,6 +234,44 @@ public class TransactionExecutorTests { } @Test + public void testDoNotLaunchDestroyedActivity() { + final Map<IBinder, ClientTransactionItem> activitiesToBeDestroyed = new ArrayMap<>(); + when(mTransactionHandler.getActivitiesToBeDestroyed()).thenReturn(activitiesToBeDestroyed); + // Assume launch transaction is still in queue, so there is no client record. + when(mTransactionHandler.getActivityClient(any())).thenReturn(null); + + // An incoming destroy transaction enters binder thread (preExecute). + final IBinder token = mock(IBinder.class); + final ClientTransaction destroyTransaction = ClientTransaction.obtain(null /* client */, + token /* activityToken */); + destroyTransaction.setLifecycleStateRequest( + DestroyActivityItem.obtain(false /* finished */, 0 /* configChanges */)); + destroyTransaction.preExecute(mTransactionHandler); + // The activity should be added to to-be-destroyed container. + assertEquals(1, mTransactionHandler.getActivitiesToBeDestroyed().size()); + + // A previous queued launch transaction runs on main thread (execute). + final ClientTransaction launchTransaction = ClientTransaction.obtain(null /* client */, + token /* activityToken */); + final LaunchActivityItem launchItem = spy(LaunchActivityItem.obtain( + null /* intent */, 0 /* ident */, null /* info */, null /* curConfig */, + null, /* overrideConfig */ null /* compatInfo */, null /* referrer */ , + null /* voiceInteractor */, 0 /* procState */, null /* state */, + null /* persistentState */, null /* pendingResults */, + null /* pendingNewIntents */, false /* isForward */, null /* profilerInfo */)); + launchTransaction.addCallback(launchItem); + mExecutor.execute(launchTransaction); + + // The launch transaction should not be executed because its token is in the + // to-be-destroyed container. + verify(launchItem, times(0)).execute(any(), any(), any()); + + // After the destroy transaction has been executed, the token should be removed. + mExecutor.execute(destroyTransaction); + assertEquals(0, mTransactionHandler.getActivitiesToBeDestroyed().size()); + } + + @Test public void testActivityResultRequiredStateResolution() { PostExecItem postExecItem = new PostExecItem(ON_RESUME); diff --git a/core/tests/coretests/src/android/os/FileUtilsTest.java b/core/tests/coretests/src/android/os/FileUtilsTest.java index 0bc3a2d879ab..9c9f11b76ab4 100644 --- a/core/tests/coretests/src/android/os/FileUtilsTest.java +++ b/core/tests/coretests/src/android/os/FileUtilsTest.java @@ -193,7 +193,7 @@ public class FileUtilsTest { try (MemoryPipe in = MemoryPipe.createSource(source); FileOutputStream out = new FileOutputStream(dest)) { - FileUtils.copy(in.getFD(), out.getFD(), null, null, size); + FileUtils.copy(in.getFD(), out.getFD(), size, null, null, null); } actual = readFile(dest); diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java index f91d14988d29..60e512cb2c1c 100644 --- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java +++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java @@ -66,7 +66,6 @@ public class SettingsBackupTest { Settings.System.LOCKSCREEN_DISABLED, // ? Settings.System.MEDIA_BUTTON_RECEIVER, // candidate for backup? Settings.System.MUTE_STREAMS_AFFECTED, // candidate for backup? - Settings.System.NOTIFICATION_LIGHT_PULSE, // candidate for backup? Settings.System.NOTIFICATION_SOUND_CACHE, // internal cache Settings.System.POINTER_LOCATION, // backup candidate? Settings.System.DEBUG_ENABLE_ENHANCED_CALL_BLOCKING, // used for testing only @@ -560,10 +559,8 @@ public class SettingsBackupTest { Settings.Secure.LAST_SETUP_SHOWN, Settings.Secure.LOCATION_CHANGER, Settings.Secure.LOCATION_MODE, - Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, // Candidate? Settings.Secure.LOCK_SCREEN_ALLOW_REMOTE_INPUT, // Candidate? Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT, - Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, // Candidate? Settings.Secure.LOCK_TO_APP_EXIT_LOCKED, Settings.Secure.MANAGED_PROFILE_CONTACT_REMOTE_SEARCH, Settings.Secure.MULTI_PRESS_TIMEOUT, diff --git a/core/tests/coretests/src/android/text/TextUtilsTest.java b/core/tests/coretests/src/android/text/TextUtilsTest.java index 870d6b2b7370..72290bf10f9d 100644 --- a/core/tests/coretests/src/android/text/TextUtilsTest.java +++ b/core/tests/coretests/src/android/text/TextUtilsTest.java @@ -785,4 +785,11 @@ public class TextUtilsTest { assertEquals(2, TextUtils.length(" ")); assertEquals(6, TextUtils.length("Hello!")); } + + @Test + public void testTrimToLengthWithEllipsis() { + assertEquals("ABC...", TextUtils.trimToLengthWithEllipsis("ABCDEF", 3)); + assertEquals("ABC", TextUtils.trimToLengthWithEllipsis("ABC", 3)); + assertEquals("", TextUtils.trimToLengthWithEllipsis("", 3)); + } } diff --git a/core/tests/coretests/src/com/android/internal/os/BinderCallsStatsTest.java b/core/tests/coretests/src/com/android/internal/os/BinderCallsStatsTest.java index 914fb7409c74..d46c1543e0f0 100644 --- a/core/tests/coretests/src/com/android/internal/os/BinderCallsStatsTest.java +++ b/core/tests/coretests/src/com/android/internal/os/BinderCallsStatsTest.java @@ -226,6 +226,26 @@ public class BinderCallsStatsTest { } @Test + public void testTransactionCodeResolved() { + TestBinderCallsStats bcs = new TestBinderCallsStats(); + bcs.setDetailedTracking(true); + Binder binder = new Binder() { + @Override + public String getTransactionName(int code) { + return "resolved"; + } + }; + BinderCallsStats.CallSession callSession = bcs.callStarted(binder, 1); + bcs.time += 10; + bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE); + + List<BinderCallsStats.CallStat> callStatsList = + bcs.getUidEntries().get(TEST_UID).getCallStatsList(); + assertEquals(1, callStatsList.get(0).msg); + assertEquals("resolved", callStatsList.get(0).methodName); + } + + @Test public void testParcelSize() { TestBinderCallsStats bcs = new TestBinderCallsStats(); bcs.setDetailedTracking(true); @@ -323,6 +343,42 @@ public class BinderCallsStatsTest { bcs.dump(pw, new HashMap<>(), true); } + @Test + public void testGetExportedStatsWhenDetailedTrackingDisabled() { + TestBinderCallsStats bcs = new TestBinderCallsStats(); + bcs.setDetailedTracking(false); + Binder binder = new Binder(); + BinderCallsStats.CallSession callSession = bcs.callStarted(binder, 1); + bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE); + + assertEquals(0, bcs.getExportedCallStats().size()); + } + + @Test + public void testGetExportedStatsWhenDetailedTrackingEnabled() { + TestBinderCallsStats bcs = new TestBinderCallsStats(); + bcs.setDetailedTracking(true); + Binder binder = new Binder(); + BinderCallsStats.CallSession callSession = bcs.callStarted(binder, 1); + bcs.time += 10; + bcs.elapsedTime += 20; + bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE); + + assertEquals(1, bcs.getExportedCallStats().size()); + BinderCallsStats.ExportedCallStat stat = bcs.getExportedCallStats().get(0); + assertEquals(TEST_UID, stat.uid); + assertEquals("android.os.Binder", stat.className); + assertEquals("1", stat.methodName); + assertEquals(10, stat.cpuTimeMicros); + assertEquals(10, stat.maxCpuTimeMicros); + assertEquals(20, stat.latencyMicros); + assertEquals(20, stat.maxLatencyMicros); + assertEquals(1, stat.callCount); + assertEquals(REQUEST_SIZE, stat.maxRequestSizeBytes); + assertEquals(REPLY_SIZE, stat.maxReplySizeBytes); + assertEquals(0, stat.exceptionCount); + } + static class TestBinderCallsStats extends BinderCallsStats { int callingUid = TEST_UID; long time = 1234; diff --git a/data/sounds/AllAudio.mk b/data/sounds/AllAudio.mk index bf8067ccbd5c..bb8add1187e7 100644 --- a/data/sounds/AllAudio.mk +++ b/data/sounds/AllAudio.mk @@ -234,3 +234,8 @@ PRODUCT_COPY_FILES += \ $(LOCAL_PATH)/effects/ogg/camera_focus.ogg:system/media/audio/ui/camera_focus.ogg \ $(LOCAL_PATH)/effects/ogg/ChargingStarted.ogg:system/media/audio/ui/ChargingStarted.ogg \ $(LOCAL_PATH)/effects/ogg/InCallNotification.ogg:system/media/audio/ui/InCallNotification.ogg \ + $(LOCAL_PATH)/effects/ogg/NFCFailure.ogg:system/media/audio/ui/NFCFailure.ogg \ + $(LOCAL_PATH)/effects/ogg/NFCInitiated.ogg:system/media/audio/ui/NFCInitiated.ogg \ + $(LOCAL_PATH)/effects/ogg/NFCSuccess.ogg:system/media/audio/ui/NFCSuccess.ogg \ + $(LOCAL_PATH)/effects/ogg/NFCTransferComplete.ogg:system/media/audio/ui/NFCTransferComplete.ogg \ + $(LOCAL_PATH)/effects/ogg/NFCTransferInitiated.ogg:system/media/audio/ui/NFCTransferInitiated.ogg \ diff --git a/data/sounds/AudioPackage14.mk b/data/sounds/AudioPackage14.mk new file mode 100644 index 000000000000..c903a2b0eb97 --- /dev/null +++ b/data/sounds/AudioPackage14.mk @@ -0,0 +1,32 @@ +# +# Audio Package 14 - P +# +# Include this file in a product makefile to include these audio files +# +# + +LOCAL_PATH := frameworks/base/data/sounds + +# Simple files that do not require renaming +ALARM_FILES := Argon Carbon Helium Krypton Neon Oxygen Osmium Platinum Timer +NOTIFICATION_FILES := Ariel Ceres Carme Elara Europa Iapetus Io Rhea Salacia Titan Tethys +RINGTONE_FILES := Atria Callisto Dione Ganymede Luna Oberon Phobos Pyxis Sedna Titania Triton \ + Umbriel +EFFECT_FILES := Effect_Tick KeypressReturn KeypressInvalid KeypressDelete KeypressSpacebar KeypressStandard \ + camera_focus Dock Undock Lock Unlock Trusted ChargingStarted InCallNotification \ + NFCFailure NFCInitiated NFCSuccess NFCTransferComplete NFCTransferInitiated +MATERIAL_EFFECT_FILES := camera_click VideoRecord WirelessChargingStarted LowBattery VideoStop + +PRODUCT_COPY_FILES += $(foreach fn,$(ALARM_FILES),\ + $(LOCAL_PATH)/alarms/material/ogg/$(fn).ogg:system/media/audio/alarms/$(fn).ogg) + +PRODUCT_COPY_FILES += $(foreach fn,$(NOTIFICATION_FILES),\ + $(LOCAL_PATH)/notifications/material/ogg/$(fn).ogg:system/media/audio/notifications/$(fn).ogg) + +PRODUCT_COPY_FILES += $(foreach fn,$(RINGTONE_FILES),\ + $(LOCAL_PATH)/ringtones/material/ogg/$(fn).ogg:system/media/audio/ringtones/$(fn).ogg) + +PRODUCT_COPY_FILES += $(foreach fn,$(EFFECT_FILES),\ + $(LOCAL_PATH)/effects/ogg/$(fn).ogg:system/media/audio/ui/$(fn).ogg) +PRODUCT_COPY_FILES += $(foreach fn,$(MATERIAL_EFFECT_FILES),\ + $(LOCAL_PATH)/effects/material/ogg/$(fn).ogg:system/media/audio/ui/$(fn).ogg) diff --git a/data/sounds/README.txt b/data/sounds/README.txt index 193fd7179f5f..db20319f6bfc 100644 --- a/data/sounds/README.txt +++ b/data/sounds/README.txt @@ -31,3 +31,13 @@ Video recording ./effects/ogg/VideoStop_48k.ogg unused +NFC +--- + +./effects/ogg/NFCFailure.ogg +./effects/ogg/NFCInitiated.ogg +./effects/ogg/NFCSuccess.ogg +./effects/ogg/NFCTransferComplete.ogg +./effects/ogg/NFCTransferInitiated.ogg + +referenced in AudioPackage14.mk (= AudioPackage13.mk + NFC sounds). diff --git a/data/sounds/effects/ogg/NFCFailure.ogg b/data/sounds/effects/ogg/NFCFailure.ogg Binary files differnew file mode 100644 index 000000000000..e9ee6624b16c --- /dev/null +++ b/data/sounds/effects/ogg/NFCFailure.ogg diff --git a/data/sounds/effects/ogg/NFCInitiated.ogg b/data/sounds/effects/ogg/NFCInitiated.ogg Binary files differnew file mode 100644 index 000000000000..a86319faaac7 --- /dev/null +++ b/data/sounds/effects/ogg/NFCInitiated.ogg diff --git a/data/sounds/effects/ogg/NFCSuccess.ogg b/data/sounds/effects/ogg/NFCSuccess.ogg Binary files differnew file mode 100644 index 000000000000..39dfd1f9d863 --- /dev/null +++ b/data/sounds/effects/ogg/NFCSuccess.ogg diff --git a/data/sounds/effects/ogg/NFCTransferComplete.ogg b/data/sounds/effects/ogg/NFCTransferComplete.ogg Binary files differnew file mode 100644 index 000000000000..f00cd9876eff --- /dev/null +++ b/data/sounds/effects/ogg/NFCTransferComplete.ogg diff --git a/data/sounds/effects/ogg/NFCTransferInitiated.ogg b/data/sounds/effects/ogg/NFCTransferInitiated.ogg Binary files differnew file mode 100644 index 000000000000..7be1bcbe6375 --- /dev/null +++ b/data/sounds/effects/ogg/NFCTransferInitiated.ogg diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index 0db779906545..59760ab0a20f 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -48,6 +48,11 @@ cc_defaults { device_uses_hwc2: { cflags: ["-DUSE_HWC2"], }, + eng: { + lto: { + never: true, + }, + }, }, } @@ -198,6 +203,7 @@ cc_defaults { "AnimatorManager.cpp", "Caches.cpp", "CanvasState.cpp", + "CanvasTransform.cpp", "ClipArea.cpp", "DamageAccumulator.cpp", "DeferredLayerUpdater.cpp", diff --git a/libs/hwui/CanvasTransform.cpp b/libs/hwui/CanvasTransform.cpp new file mode 100644 index 000000000000..bac7a4d17a49 --- /dev/null +++ b/libs/hwui/CanvasTransform.cpp @@ -0,0 +1,113 @@ +/* + * 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. + */ + +#include "CanvasTransform.h" +#include "Properties.h" + +#include <SkColorFilter.h> +#include <SkPaint.h> +#include <log/log.h> + +namespace android::uirenderer { + +static SkColor makeLight(SkColor color) { + SkScalar hsv[3]; + SkColorToHSV(color, hsv); + if (hsv[1] > .2f) return color; + // hsv[1] *= .85f; + // hsv[2] = std::min(1.0f, std::max(hsv[2], 1 - hsv[2]) * 1.3f); + hsv[2] = std::max(hsv[2], 1.1f - hsv[2]); + return SkHSVToColor(SkColorGetA(color), hsv); +} + +static SkColor makeDark(SkColor color) { + SkScalar hsv[3]; + SkColorToHSV(color, hsv); + if (hsv[1] > .2f) return color; + // hsv[1] *= .85f; + // hsv[2] = std::max(0.0f, std::min(hsv[2], 1 - hsv[2]) * .7f); + hsv[2] = std::min(hsv[2], 1.1f - hsv[2]); + return SkHSVToColor(SkColorGetA(color), hsv); +} + +static SkColor transformColor(ColorTransform transform, SkColor color) { + switch (transform) { + case ColorTransform::Light: + return makeLight(color); + case ColorTransform::Dark: + return makeDark(color); + default: + return color; + } +} + +static void applyColorTransform(ColorTransform transform, SkPaint& paint) { + if (transform == ColorTransform::None) return; + + SkColor newColor = transformColor(transform, paint.getColor()); + paint.setColor(newColor); + + if (paint.getColorFilter()) { + SkBlendMode mode; + SkColor color; + // TODO: LRU this or something to avoid spamming new color mode filters + if (paint.getColorFilter()->asColorMode(&color, &mode)) { + color = transformColor(transform, color); + paint.setColorFilter(SkColorFilter::MakeModeFilter(color, mode)); + } + } +} + +class ColorFilterCanvas : public SkPaintFilterCanvas { +public: + ColorFilterCanvas(ColorTransform transform, SkCanvas* canvas) + : SkPaintFilterCanvas(canvas), mTransform(transform) {} + + bool onFilter(SkTCopyOnFirstWrite<SkPaint>* paint, Type type) const override { + if (*paint) { + applyColorTransform(mTransform, *(paint->writable())); + } + return true; + } + +private: + ColorTransform mTransform; +}; + +std::unique_ptr<SkCanvas> makeTransformCanvas(SkCanvas* inCanvas, ColorTransform transform) { + switch (transform) { + case ColorTransform::Light: + return std::make_unique<ColorFilterCanvas>(ColorTransform::Light, inCanvas); + case ColorTransform::Dark: + return std::make_unique<ColorFilterCanvas>(ColorTransform::Dark, inCanvas); + default: + return nullptr; + } +} + +std::unique_ptr<SkCanvas> makeTransformCanvas(SkCanvas* inCanvas, UsageHint usageHint) { + if (Properties::forceDarkMode) { + switch (usageHint) { + case UsageHint::Unknown: + return makeTransformCanvas(inCanvas, ColorTransform::Light); + case UsageHint::Background: + return makeTransformCanvas(inCanvas, ColorTransform::Dark); + } + } + return nullptr; +} + +}; // namespace android::uirenderer
\ No newline at end of file diff --git a/libs/hwui/CanvasTransform.h b/libs/hwui/CanvasTransform.h new file mode 100644 index 000000000000..f71fdfaf3fba --- /dev/null +++ b/libs/hwui/CanvasTransform.h @@ -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. + */ + +#pragma once + +#include <SkCanvas.h> +#include <SkPaintFilterCanvas.h> + +#include <memory> + +namespace android::uirenderer { + +enum class UsageHint { + Unknown = 0, + Background = 1, +}; + +enum class ColorTransform { + None, + Light, + Dark, +}; + +std::unique_ptr<SkCanvas> makeTransformCanvas(SkCanvas* inCanvas, ColorTransform transform); +std::unique_ptr<SkCanvas> makeTransformCanvas(SkCanvas* inCanvas, UsageHint usageHint); + +} // namespace android::uirenderer;
\ No newline at end of file diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp index 0338a3aba97d..17bec1934490 100644 --- a/libs/hwui/Properties.cpp +++ b/libs/hwui/Properties.cpp @@ -60,6 +60,7 @@ bool Properties::forceDrawFrame = false; bool Properties::filterOutTestOverhead = false; bool Properties::disableVsync = false; bool Properties::skpCaptureEnabled = false; +bool Properties::forceDarkMode = false; bool Properties::enableRTAnimations = true; bool Properties::runningInEmulator = false; @@ -146,6 +147,8 @@ bool Properties::load() { runningInEmulator = property_get_bool(PROPERTY_QEMU_KERNEL, false); + forceDarkMode = property_get_bool(PROPERTY_FORCE_DARK, false); + return (prevDebugLayersUpdates != debugLayersUpdates) || (prevDebugOverdraw != debugOverdraw) || (prevDebugStencilClip != debugStencilClip); } diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h index d640c749d829..ea017a72cd74 100644 --- a/libs/hwui/Properties.h +++ b/libs/hwui/Properties.h @@ -190,6 +190,8 @@ enum DebugLevel { */ #define PROPERTY_QEMU_KERNEL "ro.kernel.qemu" +#define PROPERTY_FORCE_DARK "debug.hwui.force_dark" + /////////////////////////////////////////////////////////////////////////////// // Misc /////////////////////////////////////////////////////////////////////////////// @@ -263,6 +265,7 @@ public: static bool disableVsync; static bool skpCaptureEnabled; + static bool forceDarkMode; // For experimentation b/68769804 ANDROID_API static bool enableRTAnimations; diff --git a/libs/hwui/RenderNode.h b/libs/hwui/RenderNode.h index dc962f307903..8393288d2ffd 100644 --- a/libs/hwui/RenderNode.h +++ b/libs/hwui/RenderNode.h @@ -28,6 +28,7 @@ #include <androidfw/ResourceTypes.h> #include "AnimatorManager.h" +#include "CanvasTransform.h" #include "Debug.h" #include "DisplayList.h" #include "Matrix.h" @@ -208,6 +209,14 @@ public: void output(std::ostream& output, uint32_t level); + void setUsageHint(UsageHint usageHint) { + mUsageHint = usageHint; + } + + UsageHint usageHint() const { + return mUsageHint; + } + private: void computeOrderingImpl(RenderNodeOp* opState, std::vector<RenderNodeOp*>* compositedChildrenOfProjectionSurface, @@ -263,6 +272,8 @@ private: sp<PositionListener> mPositionListener; + UsageHint mUsageHint = UsageHint::Unknown; + // METHODS & FIELDS ONLY USED BY THE SKIA RENDERER public: /** diff --git a/libs/hwui/RenderProperties.cpp b/libs/hwui/RenderProperties.cpp index e49574462e9e..ff9cf45cdc73 100644 --- a/libs/hwui/RenderProperties.cpp +++ b/libs/hwui/RenderProperties.cpp @@ -44,8 +44,8 @@ void LayerProperties::reset() { } bool LayerProperties::setColorFilter(SkColorFilter* filter) { - if (mColorFilter == filter) return false; - SkRefCnt_SafeAssign(mColorFilter, filter); + if (mColorFilter.get() == filter) return false; + mColorFilter = sk_ref_sp(filter); return true; } @@ -62,7 +62,7 @@ LayerProperties& LayerProperties::operator=(const LayerProperties& other) { setOpaque(other.opaque()); setAlpha(other.alpha()); setXferMode(other.xferMode()); - setColorFilter(other.colorFilter()); + setColorFilter(other.getColorFilter()); return *this; } diff --git a/libs/hwui/RenderProperties.h b/libs/hwui/RenderProperties.h index c024373efb9e..0766e3b7ed28 100644 --- a/libs/hwui/RenderProperties.h +++ b/libs/hwui/RenderProperties.h @@ -89,9 +89,7 @@ public: SkBlendMode xferMode() const { return mMode; } - bool setColorFilter(SkColorFilter* filter); - - SkColorFilter* colorFilter() const { return mColorFilter; } + SkColorFilter* getColorFilter() const { return mColorFilter.get(); } // Sets alpha, xfermode, and colorfilter from an SkPaint // paint may be NULL, in which case defaults will be set @@ -105,6 +103,7 @@ private: LayerProperties(); ~LayerProperties(); void reset(); + bool setColorFilter(SkColorFilter* filter); // Private since external users should go through properties().effectiveLayerType() LayerType type() const { return mType; } @@ -116,7 +115,7 @@ private: bool mOpaque; uint8_t mAlpha; SkBlendMode mMode; - SkColorFilter* mColorFilter = nullptr; + sk_sp<SkColorFilter> mColorFilter; }; /* diff --git a/libs/hwui/SkiaCanvas.cpp b/libs/hwui/SkiaCanvas.cpp index 7b41f89605d2..17f1a3b4db3c 100644 --- a/libs/hwui/SkiaCanvas.cpp +++ b/libs/hwui/SkiaCanvas.cpp @@ -21,6 +21,7 @@ #include "VectorDrawable.h" #include "hwui/Bitmap.h" #include "hwui/MinikinUtils.h" +#include "hwui/PaintFilter.h" #include "pipeline/skia/AnimatedDrawables.h" #include <SkAnimatedImage.h> @@ -28,7 +29,6 @@ #include <SkColorFilter.h> #include <SkColorSpaceXformCanvas.h> #include <SkDeque.h> -#include <SkDrawFilter.h> #include <SkDrawable.h> #include <SkGraphics.h> #include <SkImage.h> @@ -40,6 +40,8 @@ #include <SkTextBlob.h> #include <memory> +#include <optional> +#include <utility> namespace android { @@ -211,7 +213,7 @@ public: Clip(const SkRRect& rrect, SkClipOp op, const SkMatrix& m) : mType(Type::RRect), mOp(op), mMatrix(m), mRRect(rrect) {} Clip(const SkPath& path, SkClipOp op, const SkMatrix& m) - : mType(Type::Path), mOp(op), mMatrix(m), mPath(&path) {} + : mType(Type::Path), mOp(op), mMatrix(m), mPath(std::in_place, path) {} void apply(SkCanvas* canvas) const { canvas->setMatrix(mMatrix); @@ -223,7 +225,7 @@ public: canvas->clipRRect(mRRect, mOp); break; case Type::Path: - canvas->clipPath(*mPath.get(), mOp); + canvas->clipPath(mPath.value(), mOp); break; } } @@ -240,7 +242,7 @@ private: SkMatrix mMatrix; // These are logically a union (tracked separately due to non-POD path). - SkTLazy<SkPath> mPath; + std::optional<SkPath> mPath; SkRRect mRRect; }; @@ -400,12 +402,12 @@ bool SkiaCanvas::clipPath(const SkPath* path, SkClipOp op) { // Canvas state operations: Filters // ---------------------------------------------------------------------------- -SkDrawFilter* SkiaCanvas::getDrawFilter() { - return mCanvas->getDrawFilter(); +PaintFilter* SkiaCanvas::getPaintFilter() { + return mPaintFilter.get(); } -void SkiaCanvas::setDrawFilter(SkDrawFilter* drawFilter) { - mCanvas->setDrawFilter(drawFilter); +void SkiaCanvas::setPaintFilter(sk_sp<PaintFilter> paintFilter) { + mPaintFilter = std::move(paintFilter); } // ---------------------------------------------------------------------------- @@ -439,8 +441,15 @@ void SkiaCanvas::drawColor(int color, SkBlendMode mode) { mCanvas->drawColor(color, mode); } +SkiaCanvas::PaintCoW&& SkiaCanvas::filterPaint(PaintCoW&& paint) const { + if (mPaintFilter) { + mPaintFilter->filter(&paint.writeable()); + } + return std::move(paint); +} + void SkiaCanvas::drawPaint(const SkPaint& paint) { - mCanvas->drawPaint(paint); + mCanvas->drawPaint(*filterPaint(paint)); } // ---------------------------------------------------------------------------- @@ -457,53 +466,53 @@ void SkiaCanvas::drawPoints(const float* points, int count, const SkPaint& paint pts[i].set(points[0], points[1]); points += 2; } - mCanvas->drawPoints(mode, count, pts.get(), paint); + mCanvas->drawPoints(mode, count, pts.get(), *filterPaint(paint)); } void SkiaCanvas::drawPoint(float x, float y, const SkPaint& paint) { - mCanvas->drawPoint(x, y, paint); + mCanvas->drawPoint(x, y, *filterPaint(paint)); } void SkiaCanvas::drawPoints(const float* points, int count, const SkPaint& paint) { - this->drawPoints(points, count, paint, SkCanvas::kPoints_PointMode); + this->drawPoints(points, count, *filterPaint(paint), SkCanvas::kPoints_PointMode); } void SkiaCanvas::drawLine(float startX, float startY, float stopX, float stopY, const SkPaint& paint) { - mCanvas->drawLine(startX, startY, stopX, stopY, paint); + mCanvas->drawLine(startX, startY, stopX, stopY, *filterPaint(paint)); } void SkiaCanvas::drawLines(const float* points, int count, const SkPaint& paint) { if (CC_UNLIKELY(count < 4 || paint.nothingToDraw())) return; - this->drawPoints(points, count, paint, SkCanvas::kLines_PointMode); + this->drawPoints(points, count, *filterPaint(paint), SkCanvas::kLines_PointMode); } void SkiaCanvas::drawRect(float left, float top, float right, float bottom, const SkPaint& paint) { if (CC_UNLIKELY(paint.nothingToDraw())) return; - mCanvas->drawRect({left, top, right, bottom}, paint); + mCanvas->drawRect({left, top, right, bottom}, *filterPaint(paint)); } void SkiaCanvas::drawRegion(const SkRegion& region, const SkPaint& paint) { if (CC_UNLIKELY(paint.nothingToDraw())) return; - mCanvas->drawRegion(region, paint); + mCanvas->drawRegion(region, *filterPaint(paint)); } void SkiaCanvas::drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, const SkPaint& paint) { if (CC_UNLIKELY(paint.nothingToDraw())) return; SkRect rect = SkRect::MakeLTRB(left, top, right, bottom); - mCanvas->drawRoundRect(rect, rx, ry, paint); + mCanvas->drawRoundRect(rect, rx, ry, *filterPaint(paint)); } void SkiaCanvas::drawCircle(float x, float y, float radius, const SkPaint& paint) { if (CC_UNLIKELY(radius <= 0 || paint.nothingToDraw())) return; - mCanvas->drawCircle(x, y, radius, paint); + mCanvas->drawCircle(x, y, radius, *filterPaint(paint)); } void SkiaCanvas::drawOval(float left, float top, float right, float bottom, const SkPaint& paint) { if (CC_UNLIKELY(paint.nothingToDraw())) return; SkRect oval = SkRect::MakeLTRB(left, top, right, bottom); - mCanvas->drawOval(oval, paint); + mCanvas->drawOval(oval, *filterPaint(paint)); } void SkiaCanvas::drawArc(float left, float top, float right, float bottom, float startAngle, @@ -511,9 +520,9 @@ void SkiaCanvas::drawArc(float left, float top, float right, float bottom, float if (CC_UNLIKELY(paint.nothingToDraw())) return; SkRect arc = SkRect::MakeLTRB(left, top, right, bottom); if (fabs(sweepAngle) >= 360.0f) { - mCanvas->drawOval(arc, paint); + mCanvas->drawOval(arc, *filterPaint(paint)); } else { - mCanvas->drawArc(arc, startAngle, sweepAngle, useCenter, paint); + mCanvas->drawArc(arc, startAngle, sweepAngle, useCenter, *filterPaint(paint)); } } @@ -522,19 +531,19 @@ void SkiaCanvas::drawPath(const SkPath& path, const SkPaint& paint) { if (CC_UNLIKELY(path.isEmpty() && (!path.isInverseFillType()))) { return; } - mCanvas->drawPath(path, paint); + mCanvas->drawPath(path, *filterPaint(paint)); } void SkiaCanvas::drawVertices(const SkVertices* vertices, SkBlendMode mode, const SkPaint& paint) { - mCanvas->drawVertices(vertices, mode, paint); + mCanvas->drawVertices(vertices, mode, *filterPaint(paint)); } // ---------------------------------------------------------------------------- // Canvas draw operations: Bitmaps // ---------------------------------------------------------------------------- -const SkPaint* SkiaCanvas::addFilter(const SkPaint* origPaint, SkPaint* tmpPaint, - sk_sp<SkColorFilter> colorSpaceFilter) { +SkiaCanvas::PaintCoW&& SkiaCanvas::filterBitmap(PaintCoW&& paint, + sk_sp<SkColorFilter> colorSpaceFilter) const { /* We don't apply the colorSpace filter if this canvas is already wrapped with * a SkColorSpaceXformCanvas since it already takes care of converting the * contents of the bitmap into the appropriate colorspace. The mCanvasWrapper @@ -542,39 +551,31 @@ const SkPaint* SkiaCanvas::addFilter(const SkPaint* origPaint, SkPaint* tmpPaint * to have a non-sRGB colorspace. */ if (!mCanvasWrapper && colorSpaceFilter) { - if (origPaint) { - *tmpPaint = *origPaint; - } - - if (tmpPaint->getColorFilter()) { - tmpPaint->setColorFilter( - SkColorFilter::MakeComposeFilter(tmpPaint->refColorFilter(), colorSpaceFilter)); - LOG_ALWAYS_FATAL_IF(!tmpPaint->getColorFilter()); + SkPaint& tmpPaint = paint.writeable(); + if (tmpPaint.getColorFilter()) { + tmpPaint.setColorFilter(SkColorFilter::MakeComposeFilter(tmpPaint.refColorFilter(), + std::move(colorSpaceFilter))); + LOG_ALWAYS_FATAL_IF(!tmpPaint.getColorFilter()); } else { - tmpPaint->setColorFilter(colorSpaceFilter); + tmpPaint.setColorFilter(std::move(colorSpaceFilter)); } - - return tmpPaint; - } else { - return origPaint; } + return filterPaint(std::move(paint)); } void SkiaCanvas::drawBitmap(Bitmap& bitmap, float left, float top, const SkPaint* paint) { - SkPaint tmpPaint; sk_sp<SkColorFilter> colorFilter; sk_sp<SkImage> image = bitmap.makeImage(&colorFilter); - mCanvas->drawImage(image, left, top, addFilter(paint, &tmpPaint, colorFilter)); + mCanvas->drawImage(image, left, top, filterBitmap(paint, std::move(colorFilter))); } void SkiaCanvas::drawBitmap(Bitmap& bitmap, const SkMatrix& matrix, const SkPaint* paint) { SkAutoCanvasRestore acr(mCanvas, true); mCanvas->concat(matrix); - SkPaint tmpPaint; sk_sp<SkColorFilter> colorFilter; sk_sp<SkImage> image = bitmap.makeImage(&colorFilter); - mCanvas->drawImage(image, 0, 0, addFilter(paint, &tmpPaint, colorFilter)); + mCanvas->drawImage(image, 0, 0, filterBitmap(paint, std::move(colorFilter))); } void SkiaCanvas::drawBitmap(Bitmap& bitmap, float srcLeft, float srcTop, float srcRight, @@ -583,10 +584,9 @@ void SkiaCanvas::drawBitmap(Bitmap& bitmap, float srcLeft, float srcTop, float s SkRect srcRect = SkRect::MakeLTRB(srcLeft, srcTop, srcRight, srcBottom); SkRect dstRect = SkRect::MakeLTRB(dstLeft, dstTop, dstRight, dstBottom); - SkPaint tmpPaint; sk_sp<SkColorFilter> colorFilter; sk_sp<SkImage> image = bitmap.makeImage(&colorFilter); - mCanvas->drawImageRect(image, srcRect, dstRect, addFilter(paint, &tmpPaint, colorFilter), + mCanvas->drawImageRect(image, srcRect, dstRect, filterBitmap(paint, std::move(colorFilter)), SkCanvas::kFast_SrcRectConstraint); } @@ -665,21 +665,20 @@ void SkiaCanvas::drawBitmapMesh(Bitmap& bitmap, int meshWidth, int meshHeight, #endif // cons-up a shader for the bitmap - SkPaint tmpPaint; - if (paint) { - tmpPaint = *paint; - } + PaintCoW paintCoW(paint); + SkPaint& tmpPaint = paintCoW.writeable(); sk_sp<SkColorFilter> colorFilter; sk_sp<SkImage> image = bitmap.makeImage(&colorFilter); sk_sp<SkShader> shader = image->makeShader(SkShader::kClamp_TileMode, SkShader::kClamp_TileMode); if (colorFilter) { - shader = shader->makeWithColorFilter(colorFilter); + shader = shader->makeWithColorFilter(std::move(colorFilter)); } - tmpPaint.setShader(shader); + tmpPaint.setShader(std::move(shader)); - mCanvas->drawVertices(builder.detach(), SkBlendMode::kModulate, tmpPaint); + mCanvas->drawVertices(builder.detach(), SkBlendMode::kModulate, + *filterPaint(std::move(paintCoW))); } void SkiaCanvas::drawNinePatch(Bitmap& bitmap, const Res_png_9patch& chunk, float dstLeft, @@ -706,10 +705,10 @@ void SkiaCanvas::drawNinePatch(Bitmap& bitmap, const Res_png_9patch& chunk, floa lattice.fBounds = nullptr; SkRect dst = SkRect::MakeLTRB(dstLeft, dstTop, dstRight, dstBottom); - SkPaint tmpPaint; sk_sp<SkColorFilter> colorFilter; sk_sp<SkImage> image = bitmap.makeImage(&colorFilter); - mCanvas->drawImageLattice(image.get(), lattice, dst, addFilter(paint, &tmpPaint, colorFilter)); + mCanvas->drawImageLattice(image.get(), lattice, dst, + filterBitmap(paint, std::move(colorFilter))); } double SkiaCanvas::drawAnimatedImage(AnimatedImageDrawable* imgDrawable) { @@ -732,6 +731,9 @@ void SkiaCanvas::drawGlyphs(ReadGlyphFunc glyphFunc, int count, const SkPaint& p // glyphs centered or right-aligned; the offset above takes // care of all alignment. SkPaint paintCopy(paint); + if (mPaintFilter) { + mPaintFilter->filter(&paintCopy); + } paintCopy.setTextAlign(SkPaint::kLeft_Align); SkASSERT(paintCopy.getTextEncoding() == SkPaint::kGlyphID_TextEncoding); // Stroke with a hairline is drawn on HW with a fill style for compatibility with Android O and @@ -760,6 +762,9 @@ void SkiaCanvas::drawLayoutOnPath(const minikin::Layout& layout, float hOffset, // glyphs centered or right-aligned; the offsets take care of // that portion of the alignment. SkPaint paintCopy(paint); + if (mPaintFilter) { + mPaintFilter->filter(&paintCopy); + } paintCopy.setTextAlign(SkPaint::kLeft_Align); SkASSERT(paintCopy.getTextEncoding() == SkPaint::kGlyphID_TextEncoding); diff --git a/libs/hwui/SkiaCanvas.h b/libs/hwui/SkiaCanvas.h index 3efc22a03cdf..24b7ec6d5c7b 100644 --- a/libs/hwui/SkiaCanvas.h +++ b/libs/hwui/SkiaCanvas.h @@ -22,7 +22,9 @@ #include "hwui/Canvas.h" #include <SkCanvas.h> -#include <SkTLazy.h> + +#include <cassert> +#include <optional> namespace android { @@ -87,8 +89,8 @@ public: virtual bool clipRect(float left, float top, float right, float bottom, SkClipOp op) override; virtual bool clipPath(const SkPath* path, SkClipOp op) override; - virtual SkDrawFilter* getDrawFilter() override; - virtual void setDrawFilter(SkDrawFilter* drawFilter) override; + virtual PaintFilter* getPaintFilter() override; + virtual void setPaintFilter(sk_sp<PaintFilter> paintFilter) override; virtual SkCanvasState* captureCanvasState() const override; @@ -158,6 +160,46 @@ protected: const SkPaint& paint, const SkPath& path, size_t start, size_t end) override; + /** This class acts as a copy on write SkPaint. + * + * Initially this will be the SkPaint passed to the contructor. + * The first time writable() is called this will become a copy of the + * initial SkPaint (or a default SkPaint if nullptr). + */ + struct PaintCoW { + PaintCoW(const SkPaint& that) : mPtr(&that) {} + PaintCoW(const SkPaint* ptr) : mPtr(ptr) {} + PaintCoW(const PaintCoW&) = delete; + PaintCoW(PaintCoW&&) = delete; + PaintCoW& operator=(const PaintCoW&) = delete; + PaintCoW& operator=(PaintCoW&&) = delete; + SkPaint& writeable() { + if (!mStorage) { + if (!mPtr) { + mStorage.emplace(); + } else { + mStorage.emplace(*mPtr); + } + mPtr = &*mStorage; + } + return *mStorage; + } + operator const SkPaint*() const { return mPtr; } + const SkPaint* operator->() const { assert(mPtr); return mPtr; } + const SkPaint& operator*() const { assert(mPtr); return *mPtr; } + explicit operator bool() { return mPtr != nullptr; } + private: + const SkPaint* mPtr; + std::optional<SkPaint> mStorage; + }; + + /** Filters the paint using the current paint filter. + * + * @param paint the paint to filter. Will be initialized with the default + * SkPaint before filtering if filtering is required. + */ + PaintCoW&& filterPaint(PaintCoW&& paint) const; + private: struct SaveRec { int saveCount; @@ -174,8 +216,15 @@ private: void drawPoints(const float* points, int count, const SkPaint& paint, SkCanvas::PointMode mode); - const SkPaint* addFilter(const SkPaint* origPaint, SkPaint* tmpPaint, - sk_sp<SkColorFilter> colorSpaceFilter); + /** Filters the paint for bitmap drawing. + * + * After filtering the paint for bitmap drawing, + * also calls filterPaint on the paint. + * + * @param paint the paint to filter. Will be initialized with the default + * SkPaint before filtering if filtering is required. + */ + PaintCoW&& filterBitmap(PaintCoW&& paint, sk_sp<SkColorFilter> colorSpaceFilter) const; class Clip; @@ -185,6 +234,7 @@ private: // unless it is the same as mCanvasOwned.get() std::unique_ptr<SkDeque> mSaveStack; // lazily allocated, tracks partial saves. std::vector<Clip> mClipStack; // tracks persistent clips. + sk_sp<PaintFilter> mPaintFilter; }; } // namespace android diff --git a/libs/hwui/VectorDrawable.h b/libs/hwui/VectorDrawable.h index e84b9acffca7..af0ae05a74b3 100644 --- a/libs/hwui/VectorDrawable.h +++ b/libs/hwui/VectorDrawable.h @@ -59,12 +59,6 @@ namespace VectorDrawable { onPropertyChanged(); \ retVal; \ }) -#define UPDATE_SKPROP(field, value) \ - ({ \ - bool retVal = ((field) != (value)); \ - if ((field) != (value)) SkRefCnt_SafeAssign((field), (value)); \ - retVal; \ - }) /* A VectorDrawable is composed of a tree of nodes. * Each node can be a group node, or a path. @@ -223,29 +217,28 @@ public: int fillType = 0; /* non-zero or kWinding_FillType in Skia */ }; explicit FullPathProperties(Node* mNode) : Properties(mNode), mTrimDirty(false) {} - ~FullPathProperties() { - SkSafeUnref(fillGradient); - SkSafeUnref(strokeGradient); - } + ~FullPathProperties() {} void syncProperties(const FullPathProperties& prop) { mPrimitiveFields = prop.mPrimitiveFields; mTrimDirty = true; - UPDATE_SKPROP(fillGradient, prop.fillGradient); - UPDATE_SKPROP(strokeGradient, prop.strokeGradient); + fillGradient = prop.fillGradient; + strokeGradient = prop.strokeGradient; onPropertyChanged(); } void setFillGradient(SkShader* gradient) { - if (UPDATE_SKPROP(fillGradient, gradient)) { + if (fillGradient.get() != gradient) { + fillGradient = sk_ref_sp(gradient); onPropertyChanged(); } } void setStrokeGradient(SkShader* gradient) { - if (UPDATE_SKPROP(strokeGradient, gradient)) { + if (strokeGradient.get() != gradient) { + strokeGradient = sk_ref_sp(gradient); onPropertyChanged(); } } - SkShader* getFillGradient() const { return fillGradient; } - SkShader* getStrokeGradient() const { return strokeGradient; } + SkShader* getFillGradient() const { return fillGradient.get(); } + SkShader* getStrokeGradient() const { return strokeGradient.get(); } float getStrokeWidth() const { return mPrimitiveFields.strokeWidth; } void setStrokeWidth(float strokeWidth) { VD_SET_PRIMITIVE_FIELD_AND_NOTIFY(strokeWidth, strokeWidth); @@ -325,8 +318,8 @@ public: count, }; PrimitiveFields mPrimitiveFields; - SkShader* fillGradient = nullptr; - SkShader* strokeGradient = nullptr; + sk_sp<SkShader> fillGradient; + sk_sp<SkShader> strokeGradient; }; // Called from UI thread @@ -550,8 +543,7 @@ public: SkRect bounds; int scaledWidth = 0; int scaledHeight = 0; - SkColorFilter* colorFilter = nullptr; - ~NonAnimatableProperties() { SkSafeUnref(colorFilter); } + sk_sp<SkColorFilter> colorFilter; } mNonAnimatableProperties; bool mNonAnimatablePropertiesDirty = true; @@ -561,8 +553,7 @@ public: void syncNonAnimatableProperties(const TreeProperties& prop) { // Copy over the data that can only be changed in UI thread if (mNonAnimatableProperties.colorFilter != prop.mNonAnimatableProperties.colorFilter) { - SkRefCnt_SafeAssign(mNonAnimatableProperties.colorFilter, - prop.mNonAnimatableProperties.colorFilter); + mNonAnimatableProperties.colorFilter = prop.mNonAnimatableProperties.colorFilter; } mNonAnimatableProperties = prop.mNonAnimatableProperties; } @@ -599,12 +590,13 @@ public: } } void setColorFilter(SkColorFilter* filter) { - if (UPDATE_SKPROP(mNonAnimatableProperties.colorFilter, filter)) { + if (mNonAnimatableProperties.colorFilter.get() != filter) { + mNonAnimatableProperties.colorFilter = sk_ref_sp(filter); mNonAnimatablePropertiesDirty = true; mTree->onPropertyChanged(this); } } - SkColorFilter* getColorFilter() const { return mNonAnimatableProperties.colorFilter; } + SkColorFilter* getColorFilter() const { return mNonAnimatableProperties.colorFilter.get(); } float getViewportWidth() const { return mNonAnimatableProperties.viewportWidth; } float getViewportHeight() const { return mNonAnimatableProperties.viewportHeight; } diff --git a/libs/hwui/hwui/Canvas.cpp b/libs/hwui/hwui/Canvas.cpp index fb6bd6f0821a..af7f013e27b1 100644 --- a/libs/hwui/hwui/Canvas.cpp +++ b/libs/hwui/hwui/Canvas.cpp @@ -23,7 +23,7 @@ #include "Typeface.h" #include "pipeline/skia/SkiaRecordingCanvas.h" -#include <SkDrawFilter.h> +#include "hwui/PaintFilter.h" namespace android { @@ -40,10 +40,10 @@ static inline void drawStroke(SkScalar left, SkScalar right, SkScalar top, SkSca void Canvas::drawTextDecorations(float x, float y, float length, const SkPaint& paint) { uint32_t flags; - SkDrawFilter* drawFilter = getDrawFilter(); - if (drawFilter) { + PaintFilter* paintFilter = getPaintFilter(); + if (paintFilter) { SkPaint paintCopy(paint); - drawFilter->filter(&paintCopy, SkDrawFilter::kText_Type); + paintFilter->filter(&paintCopy); flags = paintCopy.getFlags(); } else { flags = paint.getFlags(); diff --git a/libs/hwui/hwui/Canvas.h b/libs/hwui/hwui/Canvas.h index 88b12b258419..5d380a68631f 100644 --- a/libs/hwui/hwui/Canvas.h +++ b/libs/hwui/hwui/Canvas.h @@ -39,6 +39,7 @@ enum class Bidi : uint8_t; } namespace android { +class PaintFilter; namespace uirenderer { class CanvasPropertyPaint; @@ -213,8 +214,8 @@ public: virtual bool clipPath(const SkPath* path, SkClipOp op) = 0; // filters - virtual SkDrawFilter* getDrawFilter() = 0; - virtual void setDrawFilter(SkDrawFilter* drawFilter) = 0; + virtual PaintFilter* getPaintFilter() = 0; + virtual void setPaintFilter(sk_sp<PaintFilter> paintFilter) = 0; // WebView only virtual SkCanvasState* captureCanvasState() const { return nullptr; } diff --git a/libs/hwui/hwui/PaintFilter.h b/libs/hwui/hwui/PaintFilter.h new file mode 100644 index 000000000000..bf5627eac229 --- /dev/null +++ b/libs/hwui/hwui/PaintFilter.h @@ -0,0 +1,19 @@ +#ifndef ANDROID_GRAPHICS_PAINT_FILTER_H_ +#define ANDROID_GRAPHICS_PAINT_FILTER_H_ + +class SkPaint; + +namespace android { + +class PaintFilter : public SkRefCnt { +public: + /** + * Called with the paint that will be used to draw. + * The implementation may modify the paint as they wish. + */ + virtual void filter(SkPaint*) = 0; +}; + +} // namespace android + +#endif diff --git a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp index c195a8eee870..85fdc103b1e3 100644 --- a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp +++ b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp @@ -156,10 +156,10 @@ static bool layerNeedsPaint(const LayerProperties& properties, float alphaMultip SkPaint* paint) { paint->setFilterQuality(kLow_SkFilterQuality); if (alphaMultiplier < 1.0f || properties.alpha() < 255 || - properties.xferMode() != SkBlendMode::kSrcOver || properties.colorFilter() != nullptr) { + properties.xferMode() != SkBlendMode::kSrcOver || properties.getColorFilter() != nullptr) { paint->setAlpha(properties.alpha() * alphaMultiplier); paint->setBlendMode(properties.xferMode()); - paint->setColorFilter(sk_ref_sp(properties.colorFilter())); + paint->setColorFilter(sk_ref_sp(properties.getColorFilter())); return true; } return false; diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp index f0da660f17b0..46e39aa58c6f 100644 --- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp +++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp @@ -17,6 +17,7 @@ #include "SkiaRecordingCanvas.h" #include <SkImagePriv.h> +#include "CanvasTransform.h" #include "Layer.h" #include "LayerDrawable.h" #include "NinePatchUtils.h" @@ -44,13 +45,21 @@ void SkiaRecordingCanvas::initDisplayList(uirenderer::RenderNode* renderNode, in } mDisplayList->attachRecorder(&mRecorder, SkIRect::MakeWH(width, height)); - SkiaCanvas::reset(&mRecorder); + SkCanvas* canvas = &mRecorder; + if (renderNode) { + mWrappedCanvas = makeTransformCanvas(&mRecorder, renderNode->usageHint()); + if (mWrappedCanvas) { + canvas = mWrappedCanvas.get(); + } + } + SkiaCanvas::reset(canvas); } uirenderer::DisplayList* SkiaRecordingCanvas::finishRecording() { // close any existing chunks if necessary insertReorderBarrier(false); mRecorder.restoreToCount(1); + mWrappedCanvas = nullptr; return mDisplayList.release(); } @@ -148,12 +157,45 @@ void SkiaRecordingCanvas::drawVectorDrawable(VectorDrawableRoot* tree) { // Recording Canvas draw operations: Bitmaps // ---------------------------------------------------------------------------- +SkiaCanvas::PaintCoW&& SkiaRecordingCanvas::filterBitmap(PaintCoW&& paint, + sk_sp<SkColorFilter> colorSpaceFilter) { + bool fixBlending = false; + bool fixAA = false; + if (paint) { + // kClear blend mode is drawn as kDstOut on HW for compatibility with Android O and + // older. + fixBlending = sApiLevel <= 27 && paint->getBlendMode() == SkBlendMode::kClear; + fixAA = paint->isAntiAlias(); + } + + if (fixBlending || fixAA || colorSpaceFilter) { + SkPaint& tmpPaint = paint.writeable(); + + if (fixBlending) { + tmpPaint.setBlendMode(SkBlendMode::kDstOut); + } + + if (colorSpaceFilter) { + if (tmpPaint.getColorFilter()) { + tmpPaint.setColorFilter(SkColorFilter::MakeComposeFilter( + tmpPaint.refColorFilter(), std::move(colorSpaceFilter))); + } else { + tmpPaint.setColorFilter(std::move(colorSpaceFilter)); + } + LOG_ALWAYS_FATAL_IF(!tmpPaint.getColorFilter()); + } + + // disabling AA on bitmap draws matches legacy HWUI behavior + tmpPaint.setAntiAlias(false); + } + + return filterPaint(std::move(paint)); +} void SkiaRecordingCanvas::drawBitmap(Bitmap& bitmap, float left, float top, const SkPaint* paint) { - SkPaint tmpPaint; sk_sp<SkColorFilter> colorFilter; sk_sp<SkImage> image = bitmap.makeImage(&colorFilter); - mRecorder.drawImage(image, left, top, bitmapPaint(paint, &tmpPaint, colorFilter)); + mRecorder.drawImage(image, left, top, filterBitmap(paint, std::move(colorFilter))); // if image->unique() is true, then mRecorder.drawImage failed for some reason. It also means // it is not safe to store a raw SkImage pointer, because the image object will be destroyed // when this function ends. @@ -166,10 +208,9 @@ void SkiaRecordingCanvas::drawBitmap(Bitmap& bitmap, const SkMatrix& matrix, con SkAutoCanvasRestore acr(&mRecorder, true); concat(matrix); - SkPaint tmpPaint; sk_sp<SkColorFilter> colorFilter; sk_sp<SkImage> image = bitmap.makeImage(&colorFilter); - mRecorder.drawImage(image, 0, 0, bitmapPaint(paint, &tmpPaint, colorFilter)); + mRecorder.drawImage(image, 0, 0, filterBitmap(paint, std::move(colorFilter))); if (!bitmap.isImmutable() && image.get() && !image->unique()) { mDisplayList->mMutableImages.push_back(image.get()); } @@ -181,10 +222,9 @@ void SkiaRecordingCanvas::drawBitmap(Bitmap& bitmap, float srcLeft, float srcTop SkRect srcRect = SkRect::MakeLTRB(srcLeft, srcTop, srcRight, srcBottom); SkRect dstRect = SkRect::MakeLTRB(dstLeft, dstTop, dstRight, dstBottom); - SkPaint tmpPaint; sk_sp<SkColorFilter> colorFilter; sk_sp<SkImage> image = bitmap.makeImage(&colorFilter); - mRecorder.drawImageRect(image, srcRect, dstRect, bitmapPaint(paint, &tmpPaint, colorFilter), + mRecorder.drawImageRect(image, srcRect, dstRect, filterBitmap(paint, std::move(colorFilter)), SkCanvas::kFast_SrcRectConstraint); if (!bitmap.isImmutable() && image.get() && !image->unique() && !srcRect.isEmpty() && !dstRect.isEmpty()) { @@ -216,23 +256,16 @@ void SkiaRecordingCanvas::drawNinePatch(Bitmap& bitmap, const Res_png_9patch& ch lattice.fBounds = nullptr; SkRect dst = SkRect::MakeLTRB(dstLeft, dstTop, dstRight, dstBottom); - SkPaint tmpPaint; - sk_sp<SkColorFilter> colorFilter; - sk_sp<SkImage> image = bitmap.makeImage(&colorFilter); - const SkPaint* filteredPaint = bitmapPaint(paint, &tmpPaint, colorFilter); + PaintCoW filteredPaint(paint); // Besides kNone, the other three SkFilterQualities are treated the same. And Android's // Java API only supports kLow and kNone anyway. if (!filteredPaint || filteredPaint->getFilterQuality() == kNone_SkFilterQuality) { - if (filteredPaint != &tmpPaint) { - if (paint) { - tmpPaint = *paint; - } - filteredPaint = &tmpPaint; - } - tmpPaint.setFilterQuality(kLow_SkFilterQuality); + filteredPaint.writeable().setFilterQuality(kLow_SkFilterQuality); } - - mRecorder.drawImageLattice(image.get(), lattice, dst, filteredPaint); + sk_sp<SkColorFilter> colorFilter; + sk_sp<SkImage> image = bitmap.makeImage(&colorFilter); + mRecorder.drawImageLattice(image.get(), lattice, dst, + filterBitmap(std::move(filteredPaint), std::move(colorFilter))); if (!bitmap.isImmutable() && image.get() && !image->unique() && !dst.isEmpty()) { mDisplayList->mMutableImages.push_back(image.get()); } diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h index 93807a5476e6..50d84d6cf101 100644 --- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h +++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h @@ -78,6 +78,7 @@ public: private: SkLiteRecorder mRecorder; std::unique_ptr<SkiaDisplayList> mDisplayList; + std::unique_ptr<SkCanvas> mWrappedCanvas; StartReorderBarrierDrawable* mCurrentBarrier; /** @@ -89,44 +90,7 @@ private: */ void initDisplayList(uirenderer::RenderNode* renderNode, int width, int height); - inline static const SkPaint* bitmapPaint(const SkPaint* origPaint, SkPaint* tmpPaint, - sk_sp<SkColorFilter> colorSpaceFilter) { - bool fixBlending = false; - bool fixAA = false; - if (origPaint) { - // kClear blend mode is drawn as kDstOut on HW for compatibility with Android O and - // older. - fixBlending = sApiLevel <= 27 && origPaint->getBlendMode() == SkBlendMode::kClear; - fixAA = origPaint->isAntiAlias(); - } - - if (fixBlending || fixAA || colorSpaceFilter) { - if (origPaint) { - *tmpPaint = *origPaint; - } - - if (fixBlending) { - tmpPaint->setBlendMode(SkBlendMode::kDstOut); - } - - if (colorSpaceFilter) { - if (tmpPaint->getColorFilter()) { - tmpPaint->setColorFilter(SkColorFilter::MakeComposeFilter( - tmpPaint->refColorFilter(), colorSpaceFilter)); - } else { - tmpPaint->setColorFilter(colorSpaceFilter); - } - LOG_ALWAYS_FATAL_IF(!tmpPaint->getColorFilter()); - } - - // disabling AA on bitmap draws matches legacy HWUI behavior - tmpPaint->setAntiAlias(false); - return tmpPaint; - } else { - return origPaint; - } - } - + PaintCoW&& filterBitmap(PaintCoW&& paint, sk_sp<SkColorFilter> colorSpaceFilter); }; }; // namespace skiapipeline diff --git a/libs/hwui/tests/macrobench/main.cpp b/libs/hwui/tests/macrobench/main.cpp index b74d3595e964..e3c97ce686d9 100644 --- a/libs/hwui/tests/macrobench/main.cpp +++ b/libs/hwui/tests/macrobench/main.cpp @@ -136,8 +136,6 @@ static bool setBenchmarkFormat(const char* format) { gBenchmarkReporter.reset(new benchmark::ConsoleReporter()); } else if (!strcmp(format, "json")) { gBenchmarkReporter.reset(new benchmark::JSONReporter()); - } else if (!strcmp(format, "csv")) { - gBenchmarkReporter.reset(new benchmark::CSVReporter()); } else { fprintf(stderr, "Unknown format '%s'", format); return false; diff --git a/media/java/android/media/MediaPlayer2.java b/media/java/android/media/MediaPlayer2.java index b747291d1d28..61c412dac185 100644 --- a/media/java/android/media/MediaPlayer2.java +++ b/media/java/android/media/MediaPlayer2.java @@ -2137,11 +2137,17 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener */ public static final int CALL_STATUS_ERROR_IO = 4; + /** Status code represents that the call has been skipped. For example, a {@link #seekTo} + * request may be skipped if it is followed by another {@link #seekTo} request. + * @see EventCallback#onCallCompleted + */ + public static final int CALL_STATUS_SKIPPED = 5; + /** Status code represents that DRM operation is called before preparing a DRM scheme through * {@link #prepareDrm}. * @see android.media.MediaPlayer2.EventCallback#onCallCompleted */ - public static final int CALL_STATUS_NO_DRM_SCHEME = 5; + public static final int CALL_STATUS_NO_DRM_SCHEME = 6; /** * @hide @@ -2153,6 +2159,7 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener CALL_STATUS_BAD_VALUE, CALL_STATUS_PERMISSION_DENIED, CALL_STATUS_ERROR_IO, + CALL_STATUS_SKIPPED, CALL_STATUS_NO_DRM_SCHEME}) @Retention(RetentionPolicy.SOURCE) public @interface CallStatus {} diff --git a/media/java/android/media/MediaPlayer2Impl.java b/media/java/android/media/MediaPlayer2Impl.java index 5a71afd4625e..dccfd7a87ba7 100644 --- a/media/java/android/media/MediaPlayer2Impl.java +++ b/media/java/android/media/MediaPlayer2Impl.java @@ -203,8 +203,6 @@ public final class MediaPlayer2Impl extends MediaPlayer2 { * playback will continue from where it was paused. If playback had * been stopped, or never started before, playback will start at the * beginning. - * - * @throws IllegalStateException if it is called in an invalid state */ @Override public void play() { @@ -226,8 +224,6 @@ public final class MediaPlayer2Impl extends MediaPlayer2 { * call prepare(). For streams, you should call prepare(), * which returns immediately, rather than blocking until enough data has been * buffered. - * - * @throws IllegalStateException if it is called in an invalid state */ @Override public void prepare() { @@ -243,9 +239,6 @@ public final class MediaPlayer2Impl extends MediaPlayer2 { /** * Pauses playback. Call play() to resume. - * - * @throws IllegalStateException if the internal player engine has not been - * initialized. */ @Override public void pause() { @@ -351,23 +344,22 @@ public final class MediaPlayer2Impl extends MediaPlayer2 { * Sets the data source as described by a DataSourceDesc. * * @param dsd the descriptor of data source you want to play - * @throws IllegalStateException if it is called in an invalid state - * @throws NullPointerException if dsd is null */ @Override public void setDataSource(@NonNull DataSourceDesc dsd) { addTask(new Task(CALL_COMPLETED_SET_DATA_SOURCE, false) { @Override - void process() { - Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null"); - // TODO: setDataSource could update exist data source + void process() throws IOException { + Preconditions.checkArgument(dsd != null, "the DataSourceDesc cannot be null"); + int state = getState(); + if (state != PLAYER_STATE_ERROR && state != PLAYER_STATE_IDLE) { + throw new IllegalStateException("called in wrong state " + state); + } + synchronized (mSrcLock) { mCurrentDSD = dsd; mCurrentSrcId = mSrcIdGenerator++; - try { - handleDataSource(true /* isCurrent */, dsd, mCurrentSrcId); - } catch (IOException e) { - } + handleDataSource(true /* isCurrent */, dsd, mCurrentSrcId); } } }); @@ -386,7 +378,7 @@ public final class MediaPlayer2Impl extends MediaPlayer2 { addTask(new Task(CALL_COMPLETED_SET_NEXT_DATA_SOURCE, false) { @Override void process() { - Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null"); + Preconditions.checkArgument(dsd != null, "the DataSourceDesc cannot be null"); synchronized (mSrcLock) { mNextDSDs = new ArrayList<DataSourceDesc>(1); mNextDSDs.add(dsd); @@ -1282,7 +1274,7 @@ public final class MediaPlayer2Impl extends MediaPlayer2 { addTask(new Task(CALL_COMPLETED_SET_BUFFERING_PARAMS, false) { @Override void process() { - Preconditions.checkNotNull(params, "the BufferingParams cannot be null"); + Preconditions.checkArgument(params != null, "the BufferingParams cannot be null"); _setBufferingParams(params); } }); @@ -1346,7 +1338,7 @@ public final class MediaPlayer2Impl extends MediaPlayer2 { addTask(new Task(CALL_COMPLETED_SET_PLAYBACK_PARAMS, false) { @Override void process() { - Preconditions.checkNotNull(params, "the PlaybackParams cannot be null"); + Preconditions.checkArgument(params != null, "the PlaybackParams cannot be null"); _setPlaybackParams(params); } }); @@ -1379,7 +1371,7 @@ public final class MediaPlayer2Impl extends MediaPlayer2 { addTask(new Task(CALL_COMPLETED_SET_SYNC_PARAMS, false) { @Override void process() { - Preconditions.checkNotNull(params, "the SyncParams cannot be null"); + Preconditions.checkArgument(params != null, "the SyncParams cannot be null"); _setSyncParams(params); } }); @@ -4627,7 +4619,12 @@ public final class MediaPlayer2Impl extends MediaPlayer2 { public void run() { int status = CALL_STATUS_NO_ERROR; try { - process(); + if (mMediaCallType != CALL_COMPLETED_NOTIFY_WHEN_COMMAND_LABEL_REACHED + && getState() == PLAYER_STATE_ERROR) { + status = CALL_STATUS_INVALID_OPERATION; + } else { + process(); + } } catch (IllegalStateException e) { status = CALL_STATUS_INVALID_OPERATION; } catch (IllegalArgumentException e) { @@ -4638,6 +4635,8 @@ public final class MediaPlayer2Impl extends MediaPlayer2 { status = CALL_STATUS_ERROR_IO; } catch (NoDrmSchemeException e) { status = CALL_STATUS_NO_DRM_SCHEME; + } catch (CommandSkippedException e) { + status = CALL_STATUS_SKIPPED; } catch (Exception e) { status = CALL_STATUS_ERROR_UNKNOWN; } @@ -4671,4 +4670,10 @@ public final class MediaPlayer2Impl extends MediaPlayer2 { } } }; + + private final class CommandSkippedException extends RuntimeException { + public CommandSkippedException(String detailMessage) { + super(detailMessage); + } + }; } diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java index 143182f83ace..ec2d9bab5107 100644 --- a/media/java/android/media/tv/TvInputManager.java +++ b/media/java/android/media/tv/TvInputManager.java @@ -888,6 +888,8 @@ public final class TvInputManager { if (token != null) { session = new Session(token, channel, mService, mUserId, seq, mSessionCallbackRecordMap); + } else { + mSessionCallbackRecordMap.delete(seq); } record.postSessionCreated(session); } @@ -2487,7 +2489,7 @@ public final class TvInputManager { } } synchronized (mSessionCallbackRecordMap) { - mSessionCallbackRecordMap.remove(mSeq); + mSessionCallbackRecordMap.delete(mSeq); } } diff --git a/media/jni/android_media_Media2DataSource.cpp b/media/jni/android_media_Media2DataSource.cpp index bc3f6bd80cd8..b3434e9ab8ea 100644 --- a/media/jni/android_media_Media2DataSource.cpp +++ b/media/jni/android_media_Media2DataSource.cpp @@ -20,12 +20,12 @@ #include "android_media_Media2DataSource.h" -#include "android_runtime/AndroidRuntime.h" -#include "android_runtime/Log.h" +#include "log/log.h" #include "jni.h" #include <nativehelper/JNIHelp.h> #include <drm/drm_framework_common.h> +#include <mediaplayer2/JavaVMHelper.h> #include <media/stagefright/foundation/ADebug.h> #include <nativehelper/ScopedLocalRef.h> @@ -56,7 +56,7 @@ JMedia2DataSource::JMedia2DataSource(JNIEnv* env, jobject source) } JMedia2DataSource::~JMedia2DataSource() { - JNIEnv* env = AndroidRuntime::getJNIEnv(); + JNIEnv* env = JavaVMHelper::getJNIEnv(); env->DeleteGlobalRef(mMedia2DataSourceObj); env->DeleteGlobalRef(mByteArrayObj); } @@ -75,12 +75,12 @@ ssize_t JMedia2DataSource::readAt(off64_t offset, void *data, size_t size) { size = kBufferSize; } - JNIEnv* env = AndroidRuntime::getJNIEnv(); + JNIEnv* env = JavaVMHelper::getJNIEnv(); jint numread = env->CallIntMethod(mMedia2DataSourceObj, mReadAtMethod, (jlong)offset, mByteArrayObj, (jint)0, (jint)size); if (env->ExceptionCheck()) { ALOGW("An exception occurred in readAt()"); - LOGW_EX(env); + jniLogException(env, ANDROID_LOG_WARN, LOG_TAG); env->ExceptionClear(); mJavaObjStatus = UNKNOWN_ERROR; return -1; @@ -117,11 +117,11 @@ status_t JMedia2DataSource::getSize(off64_t* size) { return OK; } - JNIEnv* env = AndroidRuntime::getJNIEnv(); + JNIEnv* env = JavaVMHelper::getJNIEnv(); *size = env->CallLongMethod(mMedia2DataSourceObj, mGetSizeMethod); if (env->ExceptionCheck()) { ALOGW("An exception occurred in getSize()"); - LOGW_EX(env); + jniLogException(env, ANDROID_LOG_WARN, LOG_TAG); env->ExceptionClear(); // After returning an error, size shouldn't be used by callers. *size = UNKNOWN_ERROR; @@ -142,7 +142,7 @@ status_t JMedia2DataSource::getSize(off64_t* size) { void JMedia2DataSource::close() { Mutex::Autolock lock(mLock); - JNIEnv* env = AndroidRuntime::getJNIEnv(); + JNIEnv* env = JavaVMHelper::getJNIEnv(); env->CallVoidMethod(mMedia2DataSourceObj, mCloseMethod); // The closed state is effectively the same as an error state. mJavaObjStatus = UNKNOWN_ERROR; diff --git a/media/jni/android_media_Media2HTTPConnection.cpp b/media/jni/android_media_Media2HTTPConnection.cpp index 60176e3d238e..d02ee06dba49 100644 --- a/media/jni/android_media_Media2HTTPConnection.cpp +++ b/media/jni/android_media_Media2HTTPConnection.cpp @@ -18,14 +18,14 @@ #define LOG_TAG "Media2HTTPConnection-JNI" #include <utils/Log.h> +#include <mediaplayer2/JavaVMHelper.h> #include <media/stagefright/foundation/ADebug.h> #include <nativehelper/ScopedLocalRef.h> #include "android_media_Media2HTTPConnection.h" #include "android_util_Binder.h" -#include "android_runtime/AndroidRuntime.h" -#include "android_runtime/Log.h" +#include "log/log.h" #include "jni.h" #include <nativehelper/JNIHelp.h> @@ -84,7 +84,7 @@ JMedia2HTTPConnection::JMedia2HTTPConnection(JNIEnv *env, jobject thiz) { } JMedia2HTTPConnection::~JMedia2HTTPConnection() { - JNIEnv *env = AndroidRuntime::getJNIEnv(); + JNIEnv *env = JavaVMHelper::getJNIEnv(); env->DeleteGlobalRef(mMedia2HTTPConnectionObj); env->DeleteGlobalRef(mByteArrayObj); } @@ -101,7 +101,7 @@ bool JMedia2HTTPConnection::connect( } } - JNIEnv* env = AndroidRuntime::getJNIEnv(); + JNIEnv* env = JavaVMHelper::getJNIEnv(); jstring juri = env->NewStringUTF(uri); jstring jheaders = env->NewStringUTF(tmp.string()); @@ -115,12 +115,12 @@ bool JMedia2HTTPConnection::connect( } void JMedia2HTTPConnection::disconnect() { - JNIEnv* env = AndroidRuntime::getJNIEnv(); + JNIEnv* env = JavaVMHelper::getJNIEnv(); env->CallVoidMethod(mMedia2HTTPConnectionObj, mDisconnectMethod); } ssize_t JMedia2HTTPConnection::readAt(off64_t offset, void *data, size_t size) { - JNIEnv* env = AndroidRuntime::getJNIEnv(); + JNIEnv* env = JavaVMHelper::getJNIEnv(); if (size > kBufferSize) { size = kBufferSize; @@ -141,12 +141,12 @@ ssize_t JMedia2HTTPConnection::readAt(off64_t offset, void *data, size_t size) { } off64_t JMedia2HTTPConnection::getSize() { - JNIEnv* env = AndroidRuntime::getJNIEnv(); + JNIEnv* env = JavaVMHelper::getJNIEnv(); return (off64_t)(env->CallLongMethod(mMedia2HTTPConnectionObj, mGetSizeMethod)); } status_t JMedia2HTTPConnection::getMIMEType(String8 *mimeType) { - JNIEnv* env = AndroidRuntime::getJNIEnv(); + JNIEnv* env = JavaVMHelper::getJNIEnv(); jstring jmime = (jstring)env->CallObjectMethod(mMedia2HTTPConnectionObj, mGetMIMETypeMethod); jboolean flag = env->ExceptionCheck(); if (flag) { @@ -165,7 +165,7 @@ status_t JMedia2HTTPConnection::getMIMEType(String8 *mimeType) { } status_t JMedia2HTTPConnection::getUri(String8 *uri) { - JNIEnv* env = AndroidRuntime::getJNIEnv(); + JNIEnv* env = JavaVMHelper::getJNIEnv(); jstring juri = (jstring)env->CallObjectMethod(mMedia2HTTPConnectionObj, mGetUriMethod); jboolean flag = env->ExceptionCheck(); if (flag) { diff --git a/media/jni/android_media_Media2HTTPService.cpp b/media/jni/android_media_Media2HTTPService.cpp index 382f099b7932..1c63889e2939 100644 --- a/media/jni/android_media_Media2HTTPService.cpp +++ b/media/jni/android_media_Media2HTTPService.cpp @@ -21,11 +21,11 @@ #include "android_media_Media2HTTPConnection.h" #include "android_media_Media2HTTPService.h" -#include "android_runtime/AndroidRuntime.h" -#include "android_runtime/Log.h" +#include "log/log.h" #include "jni.h" #include <nativehelper/JNIHelp.h> +#include <mediaplayer2/JavaVMHelper.h> #include <media/stagefright/foundation/ADebug.h> #include <nativehelper/ScopedLocalRef.h> @@ -46,12 +46,12 @@ JMedia2HTTPService::JMedia2HTTPService(JNIEnv *env, jobject thiz) { } JMedia2HTTPService::~JMedia2HTTPService() { - JNIEnv *env = AndroidRuntime::getJNIEnv(); + JNIEnv *env = JavaVMHelper::getJNIEnv(); env->DeleteGlobalRef(mMedia2HTTPServiceObj); } sp<MediaHTTPConnection> JMedia2HTTPService::makeHTTPConnection() { - JNIEnv* env = AndroidRuntime::getJNIEnv(); + JNIEnv* env = JavaVMHelper::getJNIEnv(); jobject media2HTTPConnectionObj = env->CallObjectMethod(mMedia2HTTPServiceObj, mMakeHTTPConnectionMethod); diff --git a/media/jni/android_media_MediaPlayer2.cpp b/media/jni/android_media_MediaPlayer2.cpp index abf053408633..426598727c89 100644 --- a/media/jni/android_media_MediaPlayer2.cpp +++ b/media/jni/android_media_MediaPlayer2.cpp @@ -29,6 +29,7 @@ #include <media/stagefright/Utils.h> #include <media/stagefright/foundation/ByteUtils.h> // for FOURCC definition #include <mediaplayer2/JAudioTrack.h> +#include <mediaplayer2/JavaVMHelper.h> #include <mediaplayer2/mediaplayer2.h> #include <stdio.h> #include <assert.h> @@ -39,7 +40,7 @@ #include "jni.h" #include <nativehelper/JNIHelp.h> #include "android/native_window_jni.h" -#include "android_runtime/Log.h" +#include "log/log.h" #include "utils/Errors.h" // for status_t #include "utils/KeyedVector.h" #include "utils/String8.h" @@ -184,14 +185,14 @@ JNIMediaPlayer2Listener::JNIMediaPlayer2Listener(JNIEnv* env, jobject thiz, jobj JNIMediaPlayer2Listener::~JNIMediaPlayer2Listener() { // remove global references - JNIEnv *env = AndroidRuntime::getJNIEnv(); + JNIEnv *env = JavaVMHelper::getJNIEnv(); env->DeleteGlobalRef(mObject); env->DeleteGlobalRef(mClass); } void JNIMediaPlayer2Listener::notify(int64_t srcId, int msg, int ext1, int ext2, const Parcel *obj) { - JNIEnv *env = AndroidRuntime::getJNIEnv(); + JNIEnv *env = JavaVMHelper::getJNIEnv(); if (obj && obj->dataSize() > 0) { jobject jParcel = createJavaParcelObject(env); if (jParcel != NULL) { @@ -207,7 +208,7 @@ void JNIMediaPlayer2Listener::notify(int64_t srcId, int msg, int ext1, int ext2, } if (env->ExceptionCheck()) { ALOGW("An exception occurred while notifying an event."); - LOGW_EX(env); + jniLogException(env, ANDROID_LOG_WARN, LOG_TAG); env->ExceptionClear(); } } @@ -1539,8 +1540,7 @@ static const JNINativeMethod gMethods[] = { // This function only registers the native methods static int register_android_media_MediaPlayer2Impl(JNIEnv *env) { - return AndroidRuntime::registerNativeMethods(env, - "android/media/MediaPlayer2Impl", gMethods, NELEM(gMethods)); + return jniRegisterNativeMethods(env, "android/media/MediaPlayer2Impl", gMethods, NELEM(gMethods)); } jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) @@ -1559,6 +1559,8 @@ jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) goto bail; } + JavaVMHelper::setJavaVM(vm); + /* success -- return valid version number */ result = JNI_VERSION_1_4; diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/CameraTestUtils.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/CameraTestUtils.java index b2be46430831..5ab50925a268 100644 --- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/CameraTestUtils.java +++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/CameraTestUtils.java @@ -43,6 +43,8 @@ import android.hardware.camera2.CaptureResult; import android.hardware.camera2.TotalCaptureResult; import android.hardware.camera2.params.InputConfiguration; import android.hardware.camera2.params.MeteringRectangle; +import android.hardware.camera2.params.OutputConfiguration; +import android.hardware.camera2.params.SessionConfiguration; import android.hardware.camera2.params.StreamConfigurationMap; import android.location.Location; import android.location.LocationManager; @@ -75,6 +77,7 @@ import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.Executor; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; @@ -694,6 +697,38 @@ public class CameraTestUtils extends Assert { } /** + * Configure a new camera session with output surfaces and initial session parameters. + * + * @param camera The CameraDevice to be configured. + * @param outputSurfaces The surface list that used for camera output. + * @param listener The callback CameraDevice will notify when session is available. + * @param handler The handler used to notify callbacks. + * @param initialRequest Initial request settings to use as session parameters. + */ + public static CameraCaptureSession configureCameraSessionWithParameters(CameraDevice camera, + List<Surface> outputSurfaces, BlockingSessionCallback listener, + Handler handler, CaptureRequest initialRequest) throws CameraAccessException { + List<OutputConfiguration> outConfigurations = new ArrayList<>(outputSurfaces.size()); + for (Surface surface : outputSurfaces) { + outConfigurations.add(new OutputConfiguration(surface)); + } + SessionConfiguration sessionConfig = new SessionConfiguration( + SessionConfiguration.SESSION_REGULAR, outConfigurations, + new HandlerExecutor(handler), listener); + sessionConfig.setSessionParameters(initialRequest); + camera.createCaptureSession(sessionConfig); + + CameraCaptureSession session = listener.waitAndGetSession(SESSION_CONFIGURE_TIMEOUT_MS); + assertFalse("Camera session should not be a reprocessable session", + session.isReprocessable()); + assertFalse("Capture session type must be regular", + CameraConstrainedHighSpeedCaptureSession.class.isAssignableFrom( + session.getClass())); + + return session; + } + + /** * Configure a new camera session with output surfaces and type. * * @param camera The CameraDevice to be configured. @@ -1334,6 +1369,20 @@ public class CameraTestUtils extends Assert { } } + public static class HandlerExecutor implements Executor { + private final Handler mHandler; + + public HandlerExecutor(Handler handler) { + assertNotNull("handler must be valid", handler); + mHandler = handler; + } + + @Override + public void execute(Runnable runCmd) { + mHandler.post(runCmd); + } + } + /** * Provide a mock for {@link CameraDevice.StateCallback}. * diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/stress/Camera2RecordingTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/stress/Camera2RecordingTest.java index ddb05f017cc8..8f27fefca0d0 100644 --- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/stress/Camera2RecordingTest.java +++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/stress/Camera2RecordingTest.java @@ -58,6 +58,7 @@ import static com.android.mediaframeworktest.helpers.CameraTestUtils.SIZE_BOUND_ import static com.android.mediaframeworktest.helpers.CameraTestUtils.SimpleCaptureCallback; import static com.android.mediaframeworktest.helpers.CameraTestUtils.SimpleImageReaderListener; import static com.android.mediaframeworktest.helpers.CameraTestUtils.configureCameraSession; +import static com.android.mediaframeworktest.helpers.CameraTestUtils.configureCameraSessionWithParameters; import static com.android.mediaframeworktest.helpers.CameraTestUtils.getSupportedVideoSizes; import static com.android.mediaframeworktest.helpers.CameraTestUtils.getValueNotNull; @@ -872,7 +873,6 @@ public class Camera2RecordingTest extends Camera2SurfaceViewTestCase { outputSurfaces.add(mReaderSurface); } mSessionListener = new BlockingSessionCallback(); - mSession = configureCameraSession(mCamera, outputSurfaces, mSessionListener, mHandler); CaptureRequest.Builder recordingRequestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_RECORD); @@ -885,7 +885,10 @@ public class Camera2RecordingTest extends Camera2SurfaceViewTestCase { } recordingRequestBuilder.addTarget(mRecordingSurface); recordingRequestBuilder.addTarget(mPreviewSurface); - mSession.setRepeatingRequest(recordingRequestBuilder.build(), listener, mHandler); + CaptureRequest recordingRequest = recordingRequestBuilder.build(); + mSession = configureCameraSessionWithParameters(mCamera, outputSurfaces, mSessionListener, + mHandler, recordingRequest); + mSession.setRepeatingRequest(recordingRequest, listener, mHandler); if (useMediaRecorder) { mMediaRecorder.start(); diff --git a/packages/CarSystemUI/Android.mk b/packages/CarSystemUI/Android.mk new file mode 100644 index 000000000000..0d40b7f22c5b --- /dev/null +++ b/packages/CarSystemUI/Android.mk @@ -0,0 +1,86 @@ +# LOCAL_PATH is not the current directory of this file. +# If you need the local path, use CAR_SYSUI_PATH. +# This tweak ensures that the resource overlay that is device-specific still works +# which requires that LOCAL_PATH match the original path (which must be frameworks/base/packages/SystemUI). +# +# For clarity, we also define SYSTEM_UI_AOSP_PATH to frameworks/base/packages/SystemUI and refer to that +SYSTEM_UI_AOSP_PATH := frameworks/base/packages/SystemUI +SYSTEM_UI_CAR_PATH := frameworks/base/packages/CarSystemUI +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_USE_AAPT2 := true + +LOCAL_MODULE_TAGS := optional + +# The same as SYSTEM_UI_AOSP_PATH but based on the value of LOCAL_PATH which is +# frameworks/base/packages/CarSystemUI. +RELATIVE_SYSTEM_UI_AOSP_PATH := ../../../../$(SYSTEM_UI_AOSP_PATH) + + +LOCAL_SRC_FILES := \ + $(call all-java-files-under, src) \ + $(call all-Iaidl-files-under, src) \ + $(call all-java-files-under, $(RELATIVE_SYSTEM_UI_AOSP_PATH)/src) \ + $(call all-Iaidl-files-under, $(RELATIVE_SYSTEM_UI_AOSP_PATH)/src) + +LOCAL_STATIC_ANDROID_LIBRARIES := \ + SystemUIPluginLib \ + SystemUISharedLib \ + androidx.car_car \ + androidx.legacy_legacy-support-v4 \ + androidx.recyclerview_recyclerview \ + androidx.preference_preference \ + androidx.appcompat_appcompat \ + androidx.mediarouter_mediarouter \ + androidx.palette_palette \ + androidx.legacy_legacy-preference-v14 \ + androidx.leanback_leanback \ + androidx.slice_slice-core \ + androidx.slice_slice-view \ + androidx.slice_slice-builders \ + androidx.arch.core_core-runtime \ + androidx.lifecycle_lifecycle-extensions \ + +LOCAL_STATIC_JAVA_LIBRARIES := \ + SystemUI-tags \ + SystemUI-proto + +LOCAL_JAVA_LIBRARIES := telephony-common \ + android.car + +LOCAL_FULL_LIBS_MANIFEST_FILES := $(SYSTEM_UI_AOSP_PATH)/AndroidManifest.xml +LOCAL_MANIFEST_FILE := AndroidManifest.xml + +LOCAL_MODULE_OWNER := google +LOCAL_PACKAGE_NAME := CarSystemUI +LOCAL_PRIVATE_PLATFORM_APIS := true +LOCAL_CERTIFICATE := platform +LOCAL_PRIVILEGED_MODULE := true + +LOCAL_PROGUARD_FLAG_FILES := $(RELATIVE_SYSTEM_UI_AOSP_PATH)/proguard.flags \ + proguard.flags + +LOCAL_RESOURCE_DIR := \ + $(LOCAL_PATH)/res \ + $(SYSTEM_UI_AOSP_PATH)/res-keyguard \ + $(SYSTEM_UI_AOSP_PATH)/res + +ifneq ($(INCREMENTAL_BUILDS),) + LOCAL_PROGUARD_ENABLED := disabled + LOCAL_JACK_ENABLED := incremental +endif + +LOCAL_DX_FLAGS := --multi-dex +LOCAL_JACK_FLAGS := --multi-dex native + +include frameworks/base/packages/SettingsLib/common.mk + +LOCAL_OVERRIDES_PACKAGES := SystemUI + +LOCAL_AAPT_FLAGS := --extra-packages com.android.keyguard + +include $(BUILD_PACKAGE) + +include $(call all-makefiles-under, $(SYSTEM_UI_CAR_PATH)) diff --git a/packages/CarSystemUI/AndroidManifest.xml b/packages/CarSystemUI/AndroidManifest.xml new file mode 100644 index 000000000000..4e8a3a3885a7 --- /dev/null +++ b/packages/CarSystemUI/AndroidManifest.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. + --> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + package="com.android.systemui" + android:sharedUserId="android.uid.systemui" + coreApp="true"> + + +</manifest> diff --git a/packages/CarSystemUI/proguard.flags b/packages/CarSystemUI/proguard.flags new file mode 100644 index 000000000000..ceb037cdb70f --- /dev/null +++ b/packages/CarSystemUI/proguard.flags @@ -0,0 +1 @@ +-keep class com.android.systemui.CarSystemUIFactory diff --git a/packages/CarSystemUI/res/drawable/car_ic_apps.xml b/packages/CarSystemUI/res/drawable/car_ic_apps.xml new file mode 100644 index 000000000000..13b543b2ee60 --- /dev/null +++ b/packages/CarSystemUI/res/drawable/car_ic_apps.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="33dp" + android:height="33dp" + android:viewportHeight="33.0" + android:viewportWidth="33.0"> + <path + android:fillColor="@color/car_nav_icon_fill_color" + android:fillType="evenOdd" + android:pathData="M1,20L12,20C12.55,20 13,20.45 13,21L13,32C13,32.55 12.55,33 12,33L1,33C0.45,33 0,32.55 0,32L0,21C0,20.45 0.45,20 1,20ZM21,20L32,20C32.55,20 33,20.45 33,21L33,32C33,32.55 32.55,33 32,33L21,33C20.45,33 20,32.55 20,32L20,21C20,20.45 20.45,20 21,20ZM1,0L12,0C12.55,0 13,0.45 13,1L13,12C13,12.55 12.55,13 12,13L1,13C0.45,13 0,12.55 0,12L0,1C0,0.45 0.45,0 1,0ZM21,0L32,0C32.55,0 33,0.45 33,1L33,12C33,12.55 32.55,13 32,13L21,13C20.45,13 20,12.55 20,12L20,1C20,0.45 20.45,0 21,0Z" + android:strokeColor="@color/car_nav_icon_fill_color" + android:strokeWidth="1"/> +</vector> diff --git a/packages/CarSystemUI/res/drawable/car_ic_apps_black.xml b/packages/CarSystemUI/res/drawable/car_ic_apps_black.xml new file mode 100644 index 000000000000..498d36621766 --- /dev/null +++ b/packages/CarSystemUI/res/drawable/car_ic_apps_black.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="33dp" + android:height="33dp" + android:viewportHeight="33.0" + android:viewportWidth="33.0"> + <path + android:fillColor="#FF000000" + android:fillType="evenOdd" + android:pathData="M1,20L12,20C12.55,20 13,20.45 13,21L13,32C13,32.55 12.55,33 12,33L1,33C0.45,33 0,32.55 0,32L0,21C0,20.45 0.45,20 1,20ZM21,20L32,20C32.55,20 33,20.45 33,21L33,32C33,32.55 32.55,33 32,33L21,33C20.45,33 20,32.55 20,32L20,21C20,20.45 20.45,20 21,20ZM1,0L12,0C12.55,0 13,0.45 13,1L13,12C13,12.55 12.55,13 12,13L1,13C0.45,13 0,12.55 0,12L0,1C0,0.45 0.45,0 1,0ZM21,0L32,0C32.55,0 33,0.45 33,1L33,12C33,12.55 32.55,13 32,13L21,13C20.45,13 20,12.55 20,12L20,1C20,0.45 20.45,0 21,0Z" + android:strokeColor="#00000000" + android:strokeWidth="1"/> +</vector> diff --git a/packages/CarSystemUI/res/drawable/car_ic_apps_selected.xml b/packages/CarSystemUI/res/drawable/car_ic_apps_selected.xml new file mode 100644 index 000000000000..250bfe8668fb --- /dev/null +++ b/packages/CarSystemUI/res/drawable/car_ic_apps_selected.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:drawable="@drawable/car_ic_selection_bg" android:gravity="center"/> + <item android:drawable="@drawable/car_ic_apps_black" android:gravity="center"/> +</layer-list> diff --git a/packages/CarSystemUI/res/drawable/car_ic_music.xml b/packages/CarSystemUI/res/drawable/car_ic_music.xml new file mode 100644 index 000000000000..8c1549470a0e --- /dev/null +++ b/packages/CarSystemUI/res/drawable/car_ic_music.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="34dp" + android:height="39dp" + android:viewportHeight="39.0" + android:viewportWidth="34.0"> + <path + android:fillColor="@color/car_nav_icon_fill_color" + android:fillType="evenOdd" + android:pathData="M23,25L31,23L31,9L12,12L12,38C12,38.45 11.45,39 11,39L1,39C0.55,39 0.01,38.87 0,38.3C0,38.3 0,35.87 0,31C0,30.42 0.62,30.08 1,30L9,28L9,4.85C9,4.36 9.36,3.94 9.84,3.87L32.84,0.19C33.39,0.1 33.9,0.47 33.99,1.01C34,1.07 34,1.12 34,1.17L34,33.06C34,33.51 33.45,34 33,34L23,34C22.55,34 22,33.51 22,33.06L22,26C22,25.61 22.16,25.18 23,25Z" + android:strokeColor="@color/car_nav_icon_fill_color" + android:strokeWidth="1"/> + <path + android:fillColor="@color/car_nav_icon_fill_color" + android:fillType="evenOdd" + android:pathData="M23.57,26.48L23.57,32.42L32.42,32.42L32.42,24.27L23.57,26.48ZM32.42,7.18L32.42,1.85L10.57,5.34L10.57,10.63L32.42,7.18ZM10.42,29.27L9.38,29.53L1.58,31.48C1.58,34.35 1.58,34.45 1.57,36.48C1.58,36.89 1.58,37.2 1.58,37.43L10.42,37.43L10.42,29.27Z" + android:strokeColor="@color/car_nav_icon_fill_color" + android:strokeWidth="3.15"/> +</vector> diff --git a/packages/CarSystemUI/res/drawable/car_ic_music_black.xml b/packages/CarSystemUI/res/drawable/car_ic_music_black.xml new file mode 100644 index 000000000000..64287a549314 --- /dev/null +++ b/packages/CarSystemUI/res/drawable/car_ic_music_black.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="34dp" + android:height="39dp" + android:viewportHeight="39.0" + android:viewportWidth="34.0"> + <path + android:fillColor="#FF000000" + android:fillType="evenOdd" + android:pathData="M23,25L31,23L31,9L12,12L12,38C12,38.45 11.45,39 11,39L1,39C0.55,39 0.01,38.87 0,38.3C0,38.3 0,35.87 0,31C0,30.42 0.62,30.08 1,30L9,28L9,4.85C9,4.36 9.36,3.94 9.84,3.87L32.84,0.19C33.39,0.1 33.9,0.47 33.99,1.01C34,1.07 34,1.12 34,1.17L34,33.06C34,33.51 33.45,34 33,34L23,34C22.55,34 22,33.51 22,33.06L22,26C22,25.61 22.16,25.18 23,25Z" + android:strokeColor="#FF000000" + android:strokeWidth="1"/> + <path + android:fillColor="#FF000000" + android:fillType="evenOdd" + android:pathData="M23.57,26.48L23.57,32.42L32.42,32.42L32.42,24.27L23.57,26.48ZM32.42,7.18L32.42,1.85L10.57,5.34L10.57,10.63L32.42,7.18ZM10.42,29.27L9.38,29.53L1.58,31.48C1.58,34.35 1.58,34.45 1.57,36.48C1.58,36.89 1.58,37.2 1.58,37.43L10.42,37.43L10.42,29.27Z" + android:strokeColor="#FF000000" + android:strokeWidth="3.15"/> +</vector> diff --git a/packages/CarSystemUI/res/drawable/car_ic_music_selected.xml b/packages/CarSystemUI/res/drawable/car_ic_music_selected.xml new file mode 100644 index 000000000000..9703a8c57174 --- /dev/null +++ b/packages/CarSystemUI/res/drawable/car_ic_music_selected.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:drawable="@drawable/car_ic_selection_bg" android:gravity="center"/> + <item android:drawable="@drawable/car_ic_music_black" android:gravity="center"/> +</layer-list> diff --git a/packages/CarSystemUI/res/drawable/car_ic_navigation.xml b/packages/CarSystemUI/res/drawable/car_ic_navigation.xml new file mode 100644 index 000000000000..58667beb98d9 --- /dev/null +++ b/packages/CarSystemUI/res/drawable/car_ic_navigation.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="32dp" + android:height="37dp" + android:viewportHeight="37.0" + android:viewportWidth="32.0"> + <path + android:fillColor="@color/car_nav_icon_fill_color" + android:fillType="evenOdd" + android:pathData="M16.62,0.61L31.33,35.21C31.55,35.72 31.31,36.3 30.8,36.52C30.48,36.66 30.12,36.62 29.83,36.42L15.7,26.44L1.58,36.42C1.13,36.73 0.5,36.63 0.18,36.18C-0.02,35.89 -0.06,35.53 0.08,35.21L14.78,0.61C15,0.1 15.59,-0.14 16.1,0.08C16.33,0.18 16.52,0.37 16.62,0.61Z" + android:strokeColor="@color/car_nav_icon_fill_color" + android:strokeWidth="1"/> +</vector> diff --git a/packages/CarSystemUI/res/drawable/car_ic_navigation_black.xml b/packages/CarSystemUI/res/drawable/car_ic_navigation_black.xml new file mode 100644 index 000000000000..145d4c912ff0 --- /dev/null +++ b/packages/CarSystemUI/res/drawable/car_ic_navigation_black.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="32dp" + android:height="37dp" + android:viewportHeight="37.0" + android:viewportWidth="32.0"> + <path + android:fillColor="#FF000000" + android:fillType="evenOdd" + android:pathData="M16.62,0.61L31.33,35.21C31.55,35.72 31.31,36.3 30.8,36.52C30.48,36.66 30.12,36.62 29.83,36.42L15.7,26.44L1.58,36.42C1.13,36.73 0.5,36.63 0.18,36.18C-0.02,35.89 -0.06,35.53 0.08,35.21L14.78,0.61C15,0.1 15.59,-0.14 16.1,0.08C16.33,0.18 16.52,0.37 16.62,0.61Z" + android:strokeColor="#00000000" + android:strokeWidth="1"/> +</vector> diff --git a/packages/CarSystemUI/res/drawable/car_ic_navigation_selected.xml b/packages/CarSystemUI/res/drawable/car_ic_navigation_selected.xml new file mode 100644 index 000000000000..f0174db25312 --- /dev/null +++ b/packages/CarSystemUI/res/drawable/car_ic_navigation_selected.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> + +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:drawable="@drawable/car_ic_selection_bg" android:gravity="center"/> + <item android:drawable="@drawable/car_ic_navigation_black" android:gravity="center"/> +</layer-list>
\ No newline at end of file diff --git a/packages/CarSystemUI/res/drawable/car_ic_notification.xml b/packages/CarSystemUI/res/drawable/car_ic_notification.xml new file mode 100644 index 000000000000..f96b05038edb --- /dev/null +++ b/packages/CarSystemUI/res/drawable/car_ic_notification.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="32dp" + android:height="38dp" + android:viewportHeight="38.0" + android:viewportWidth="32.0"> + <path + android:fillColor="@color/car_nav_icon_fill_color" + android:fillType="evenOdd" + android:pathData="M20.62,3.98C25.51,5.85 28.98,10.61 28.98,16.18L28.98,24.95L32,24.95L32,31.22L0,31.22L0,24.95L3.02,24.95L3.02,16.18C3.02,10.61 6.49,5.85 11.38,3.98C11.72,1.73 13.66,0 16,0C18.34,0 20.28,1.73 20.62,3.98ZM11.33,33.3L20.67,33.3C20.67,35.9 18.58,38 16,38C13.42,38 11.33,35.9 11.33,33.3Z" + android:strokeColor="@color/car_nav_icon_fill_color" + android:strokeWidth="1"/> +</vector> diff --git a/packages/CarSystemUI/res/drawable/car_ic_notification_black.xml b/packages/CarSystemUI/res/drawable/car_ic_notification_black.xml new file mode 100644 index 000000000000..5f8df8829b40 --- /dev/null +++ b/packages/CarSystemUI/res/drawable/car_ic_notification_black.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="32dp" + android:height="38dp" + android:viewportHeight="38.0" + android:viewportWidth="32.0"> + <path + android:fillColor="#000000" + android:fillType="evenOdd" + android:pathData="M20.62,3.98C25.51,5.85 28.98,10.61 28.98,16.18L28.98,24.95L32,24.95L32,31.22L0,31.22L0,24.95L3.02,24.95L3.02,16.18C3.02,10.61 6.49,5.85 11.38,3.98C11.72,1.73 13.66,0 16,0C18.34,0 20.28,1.73 20.62,3.98ZM11.33,33.3L20.67,33.3C20.67,35.9 18.58,38 16,38C13.42,38 11.33,35.9 11.33,33.3Z" + android:strokeColor="#00000000" + android:strokeWidth="1"/> +</vector> diff --git a/packages/CarSystemUI/res/drawable/car_ic_notification_selected.xml b/packages/CarSystemUI/res/drawable/car_ic_notification_selected.xml new file mode 100644 index 000000000000..02eec9351208 --- /dev/null +++ b/packages/CarSystemUI/res/drawable/car_ic_notification_selected.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> + +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:drawable="@drawable/car_ic_selection_bg" android:gravity="center"/> + <item android:drawable="@drawable/car_ic_notification_black" android:gravity="center"/> +</layer-list>
\ No newline at end of file diff --git a/packages/CarSystemUI/res/drawable/car_ic_overview.xml b/packages/CarSystemUI/res/drawable/car_ic_overview.xml new file mode 100644 index 000000000000..590109a0735e --- /dev/null +++ b/packages/CarSystemUI/res/drawable/car_ic_overview.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="39dp" + android:height="40dp" + android:viewportHeight="40.0" + android:viewportWidth="39.0"> + <path + android:fillColor="@color/car_nav_icon_fill_color" + android:fillType="evenOdd" + android:pathData="M33.4,22.74L33.4,37.74C33.4,38.84 32.51,39.74 31.4,39.74L24.4,39.74L24.4,24.74L14.4,24.74L14.4,39.74L7.4,39.74C6.3,39.74 5.4,38.84 5.4,37.74L5.4,22.74L0.5,22.74C0.22,22.74 0,22.51 0,22.24C0,22.12 0.04,22 0.12,21.91L19.03,0.17C19.21,-0.04 19.52,-0.06 19.73,0.12C19.75,0.14 19.76,0.15 19.78,0.17L38.68,21.91C38.86,22.12 38.84,22.43 38.63,22.62C38.54,22.69 38.43,22.74 38.31,22.74L33.4,22.74Z" + android:strokeColor="@color/car_nav_icon_fill_color" + android:strokeWidth="1"/> +</vector> diff --git a/packages/CarSystemUI/res/drawable/car_ic_overview_black.xml b/packages/CarSystemUI/res/drawable/car_ic_overview_black.xml new file mode 100644 index 000000000000..48cff978ec79 --- /dev/null +++ b/packages/CarSystemUI/res/drawable/car_ic_overview_black.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="39dp" + android:height="40dp" + android:viewportHeight="40.0" + android:viewportWidth="39.0"> + <path + android:fillColor="#FF000000" + android:fillType="evenOdd" + android:pathData="M33.4,22.74L33.4,37.74C33.4,38.84 32.51,39.74 31.4,39.74L24.4,39.74L24.4,24.74L14.4,24.74L14.4,39.74L7.4,39.74C6.3,39.74 5.4,38.84 5.4,37.74L5.4,22.74L0.5,22.74C0.22,22.74 0,22.51 0,22.24C0,22.12 0.04,22 0.12,21.91L19.03,0.17C19.21,-0.04 19.52,-0.06 19.73,0.12C19.75,0.14 19.76,0.15 19.78,0.17L38.68,21.91C38.86,22.12 38.84,22.43 38.63,22.62C38.54,22.69 38.43,22.74 38.31,22.74L33.4,22.74Z" + android:strokeColor="#00000000" + android:strokeWidth="1"/> +</vector> diff --git a/packages/CarSystemUI/res/drawable/car_ic_overview_selected.xml b/packages/CarSystemUI/res/drawable/car_ic_overview_selected.xml new file mode 100644 index 000000000000..18ebf0c6a4f8 --- /dev/null +++ b/packages/CarSystemUI/res/drawable/car_ic_overview_selected.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:drawable="@drawable/car_ic_selection_bg" android:gravity="center"/> + <item android:drawable="@drawable/car_ic_overview_black" android:gravity="center"/> +</layer-list> diff --git a/packages/CarSystemUI/res/drawable/car_ic_phone.xml b/packages/CarSystemUI/res/drawable/car_ic_phone.xml new file mode 100644 index 000000000000..70916703f882 --- /dev/null +++ b/packages/CarSystemUI/res/drawable/car_ic_phone.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="40dp" + android:height="40dp" + android:viewportHeight="40.0" + android:viewportWidth="40.0"> + <path + android:fillColor="@color/car_nav_icon_fill_color" + android:fillType="nonZero" + android:pathData="M21.31,35.75L21.3,35.76L3.46,17.92L0.03,6.25C-0.16,5.32 0.53,3.88 1.41,3.51C6.36,1.25 8.88,0.1 8.95,0.08C10.02,-0.23 11.39,0.39 11.7,1.45L13.76,10.37C13.97,11.1 13.62,11.91 13.07,12.43L8.24,17.25L22.31,31.32L27.13,26.49C27.65,25.94 28.47,25.6 29.19,25.81L38.11,27.87C39.18,28.17 39.79,29.55 39.49,30.61C39.46,30.69 38.32,33.2 36.05,38.16C35.68,39.04 34.25,39.73 33.31,39.53L21.65,36.1C21.51,35.96 21.4,35.84 21.31,35.75Z" + android:strokeColor="@color/car_nav_icon_fill_color" + android:strokeWidth="1"/> +</vector> diff --git a/packages/CarSystemUI/res/drawable/car_ic_phone_black.xml b/packages/CarSystemUI/res/drawable/car_ic_phone_black.xml new file mode 100644 index 000000000000..087eebb2bd0a --- /dev/null +++ b/packages/CarSystemUI/res/drawable/car_ic_phone_black.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="40dp" + android:height="40dp" + android:viewportHeight="40.0" + android:viewportWidth="40.0"> + <path + android:fillColor="#FF000000" + android:fillType="nonZero" + android:pathData="M21.31,35.75L21.3,35.76L3.46,17.92L0.03,6.25C-0.16,5.32 0.53,3.88 1.41,3.51C6.36,1.25 8.88,0.1 8.95,0.08C10.02,-0.23 11.39,0.39 11.7,1.45L13.76,10.37C13.97,11.1 13.62,11.91 13.07,12.43L8.24,17.25L22.31,31.32L27.13,26.49C27.65,25.94 28.47,25.6 29.19,25.81L38.11,27.87C39.18,28.17 39.79,29.55 39.49,30.61C39.46,30.69 38.32,33.2 36.05,38.16C35.68,39.04 34.25,39.73 33.31,39.53L21.65,36.1C21.51,35.96 21.4,35.84 21.31,35.75Z" + android:strokeColor="#00000000" + android:strokeWidth="1"/> +</vector> diff --git a/packages/CarSystemUI/res/drawable/car_ic_phone_selected.xml b/packages/CarSystemUI/res/drawable/car_ic_phone_selected.xml new file mode 100644 index 000000000000..197eac0204e5 --- /dev/null +++ b/packages/CarSystemUI/res/drawable/car_ic_phone_selected.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:drawable="@drawable/car_ic_selection_bg" android:gravity="center"/> + <item android:drawable="@drawable/car_ic_phone_black" android:gravity="center"/> +</layer-list> diff --git a/packages/CarSystemUI/res/drawable/car_ic_selection_bg.xml b/packages/CarSystemUI/res/drawable/car_ic_selection_bg.xml new file mode 100644 index 000000000000..781beaf96264 --- /dev/null +++ b/packages/CarSystemUI/res/drawable/car_ic_selection_bg.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="70dp" + android:height="70dp" + android:viewportHeight="70.0" + android:viewportWidth="70.0"> + <path + android:fillColor="@color/car_accent" + android:fillType="evenOdd" + android:pathData="M4,0L66,0A4,4 0,0 1,70 4L70,66A4,4 0,0 1,66 70L4,70A4,4 0,0 1,0 66L0,4A4,4 0,0 1,4 0z" + android:strokeColor="#00000000" + android:strokeWidth="1"/> +</vector> diff --git a/packages/CarSystemUI/res/drawable/car_seekbar_thumb.xml b/packages/CarSystemUI/res/drawable/car_seekbar_thumb.xml new file mode 100644 index 000000000000..1a9b8a521d78 --- /dev/null +++ b/packages/CarSystemUI/res/drawable/car_seekbar_thumb.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> + +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item> + <shape android:shape="oval"> + <padding + android:bottom="@dimen/car_padding_1" + android:left="@dimen/car_padding_1" + android:right="@dimen/car_padding_1" + android:top="@dimen/car_padding_1"/> + <solid android:color="@android:color/black"/> + </shape> + </item> + <item> + <shape android:shape="oval"> + <solid android:color="@color/car_accent"/> + <size + android:width="@dimen/car_seekbar_thumb_size" + android:height="@dimen/car_seekbar_thumb_size"/> + </shape> + </item> +</layer-list> diff --git a/packages/CarSystemUI/res/drawable/nav_button_background.xml b/packages/CarSystemUI/res/drawable/nav_button_background.xml new file mode 100644 index 000000000000..376347cdf4a9 --- /dev/null +++ b/packages/CarSystemUI/res/drawable/nav_button_background.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> + +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="@color/nav_bar_ripple_background_color"> + <item android:id="@android:id/mask"> + <shape android:shape="rectangle"> + <solid android:color="?android:colorAccent"/> + <corners android:radius="6dp"/> + </shape> + </item> +</ripple> diff --git a/packages/CarSystemUI/res/drawable/notification_material_bg.xml b/packages/CarSystemUI/res/drawable/notification_material_bg.xml new file mode 100644 index 000000000000..03746c8bfe4d --- /dev/null +++ b/packages/CarSystemUI/res/drawable/notification_material_bg.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="@color/notification_ripple_untinted_color"> + <item> + <shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="@color/notification_material_background_color"/> + <corners + android:radius="@dimen/notification_shadow_radius"/> + </shape> + </item> +</ripple> diff --git a/packages/CarSystemUI/res/drawable/notification_material_bg_dim.xml b/packages/CarSystemUI/res/drawable/notification_material_bg_dim.xml new file mode 100644 index 000000000000..03746c8bfe4d --- /dev/null +++ b/packages/CarSystemUI/res/drawable/notification_material_bg_dim.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="@color/notification_ripple_untinted_color"> + <item> + <shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="@color/notification_material_background_color"/> + <corners + android:radius="@dimen/notification_shadow_radius"/> + </shape> + </item> +</ripple> diff --git a/packages/CarSystemUI/res/drawable/volume_dialog_background.xml b/packages/CarSystemUI/res/drawable/volume_dialog_background.xml new file mode 100644 index 000000000000..fa3ca8f27fc9 --- /dev/null +++ b/packages/CarSystemUI/res/drawable/volume_dialog_background.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="?android:attr/colorBackgroundFloating"/> + <padding + android:bottom="5dp" + android:left="5dp" + android:right="5dp" + android:top="5dp"/> + <corners android:bottomLeftRadius="20dp" + android:bottomRightRadius="20dp"/> +</shape> diff --git a/packages/CarSystemUI/res/layout/car_navigation_bar.xml b/packages/CarSystemUI/res/layout/car_navigation_bar.xml new file mode 100644 index 000000000000..c7dfa9b9934c --- /dev/null +++ b/packages/CarSystemUI/res/layout/car_navigation_bar.xml @@ -0,0 +1,129 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> + +<com.android.systemui.statusbar.car.CarNavigationBarView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:systemui="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@android:color/black" + android:orientation="vertical"> + <LinearLayout + android:id="@id/nav_buttons" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:paddingStart="20dp" + android:paddingEnd="20dp" + android:gravity="center"> + + <com.android.systemui.statusbar.car.CarFacetButton + android:id="@+id/home" + style="@style/NavigationBarButton" + systemui:componentNames="com.android.car.carlauncher/.CarLauncher" + systemui:icon="@drawable/car_ic_overview" + systemui:intent="intent:#Intent;action=android.intent.action.MAIN;category=android.intent.category.HOME;launchFlags=0x14000000;end" + systemui:longIntent="intent:#Intent;action=com.google.android.demandspace.START;end" + systemui:selectedIcon="@drawable/car_ic_overview_selected" + systemui:useMoreIcon="false" + /> + + <Space + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1"/> + + <com.android.systemui.statusbar.car.CarFacetButton + android:id="@+id/maps_nav" + style="@style/NavigationBarButton" + systemui:categories="android.intent.category.APP_MAPS" + systemui:icon="@drawable/car_ic_navigation" + systemui:intent="intent:#Intent;component=com.android.car.carlauncher/.CarLauncher;category=android.intent.category.APP_MAPS;launchFlags=0x24000000;end" + systemui:selectedIcon="@drawable/car_ic_navigation_selected" + systemui:useMoreIcon="false" + /> + + <Space + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1"/> + + <com.android.systemui.statusbar.car.CarFacetButton + android:id="@+id/music_nav" + style="@style/NavigationBarButton" + systemui:icon="@drawable/car_ic_music" + systemui:intent="intent:#Intent;component=com.android.car.media/.MediaActivity;launchFlags=0x14000000;end" + systemui:packages="com.android.car.media" + systemui:selectedIcon="@drawable/car_ic_music_selected" + systemui:useMoreIcon="false" + /> + + <Space + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1"/> + + <com.android.systemui.statusbar.car.CarFacetButton + android:id="@+id/phone_nav" + style="@style/NavigationBarButton" + systemui:componentNames="com.android.car.dialer/.TelecomActivity" + systemui:icon="@drawable/car_ic_phone" + systemui:intent="intent:#Intent;component=com.android.car.dialer/.TelecomActivity;launchFlags=0x14000000;end" + systemui:selectedIcon="@drawable/car_ic_phone_selected" + systemui:useMoreIcon="false" + /> + + <Space + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1"/> + + <com.android.systemui.statusbar.car.CarFacetButton + android:id="@+id/grid_nav" + style="@style/NavigationBarButton" + systemui:componentNames="com.android.car.carlauncher/.AppGridActivity" + systemui:icon="@drawable/car_ic_apps" + systemui:intent="intent:#Intent;component=com.android.car.carlauncher/.AppGridActivity;launchFlags=0x24000000;end" + systemui:selectedIcon="@drawable/car_ic_apps_selected" + systemui:useMoreIcon="false" + /> + + <Space + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1"/> + + <com.android.keyguard.AlphaOptimizedImageButton + android:id="@+id/notifications" + style="@style/NavigationBarButton" + android:background="?android:attr/selectableItemBackground" + android:src="@drawable/car_ic_notification" + /> + </LinearLayout> + + <LinearLayout + android:id="@+id/lock_screen_nav_buttons" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:paddingStart="@dimen/car_keyline_1" + android:paddingEnd="@dimen/car_keyline_1" + android:gravity="center" + android:visibility="gone"> + </LinearLayout> + +</com.android.systemui.statusbar.car.CarNavigationBarView> diff --git a/packages/CarSystemUI/res/layout/car_navigation_bar_unprovisioned.xml b/packages/CarSystemUI/res/layout/car_navigation_bar_unprovisioned.xml new file mode 100644 index 000000000000..46e60db0ba4b --- /dev/null +++ b/packages/CarSystemUI/res/layout/car_navigation_bar_unprovisioned.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> + +<com.android.systemui.statusbar.car.CarNavigationBarView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:systemui="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@android:color/black" + android:orientation="vertical"> + + <LinearLayout + android:id="@+id/nav_buttons" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:paddingStart="@dimen/car_padding_5" + android:paddingEnd="@dimen/car_padding_5"> + + <com.android.systemui.statusbar.car.CarNavigationButton + android:id="@+id/home" + android:layout_width="@dimen/car_touch_target_size" + android:layout_height="match_parent" + android:background="?android:attr/selectableItemBackground" + android:src="@drawable/car_ic_overview" + systemui:intent="intent:#Intent;action=android.intent.action.MAIN;category=android.intent.category.HOME;launchFlags=0x14000000;end" + /> + </LinearLayout> +</com.android.systemui.statusbar.car.CarNavigationBarView> + diff --git a/packages/CarSystemUI/res/layout/car_status_bar_header.xml b/packages/CarSystemUI/res/layout/car_status_bar_header.xml new file mode 100644 index 000000000000..e4460721890c --- /dev/null +++ b/packages/CarSystemUI/res/layout/car_status_bar_header.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<!-- Extends LinearLayout --> +<com.android.systemui.qs.car.CarStatusBarHeader + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/header" + android:layout_width="match_parent" + android:layout_height="@dimen/status_bar_height"> + + <include layout="@layout/car_top_navigation_bar" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_weight="1" + /> +</com.android.systemui.qs.car.CarStatusBarHeader> diff --git a/packages/CarSystemUI/res/layout/car_top_navigation_bar.xml b/packages/CarSystemUI/res/layout/car_top_navigation_bar.xml new file mode 100644 index 000000000000..55d50a0545fe --- /dev/null +++ b/packages/CarSystemUI/res/layout/car_top_navigation_bar.xml @@ -0,0 +1,151 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> + +<com.android.systemui.statusbar.car.CarNavigationBarView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:systemui="http://schemas.android.com/apk/res-auto" + android:id="@+id/car_top_bar" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@android:color/black" + android:orientation="vertical"> + + <RelativeLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="1"> + + <FrameLayout + android:id="@+id/left_hvac_container" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_alignParentStart="true" + > + + <com.android.systemui.statusbar.car.CarNavigationButton + android:id="@+id/hvacleft" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@null" + systemui:broadcast="true" + systemui:intent="intent:#Intent;action=android.car.intent.action.TOGGLE_HVAC_CONTROLS;end" + /> + + <com.android.systemui.statusbar.hvac.AnimatedTemperatureView + android:id="@+id/lefttext" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:paddingStart="@dimen/car_padding_4" + android:paddingEnd="16dp" + android:gravity="center_vertical|start" + android:minEms="4" + android:textAppearance="@style/TextAppearance.Car.Status" + systemui:hvacAreaId="1" + systemui:hvacMaxText="@string/hvac_max_text" + systemui:hvacMaxValue="@dimen/hvac_max_value" + systemui:hvacMinText="@string/hvac_min_text" + systemui:hvacMinValue="@dimen/hvac_min_value" + systemui:hvacPivotOffset="60dp" + systemui:hvacPropertyId="358614275" + systemui:hvacTempFormat="%.0f\u00B0" + /> + </FrameLayout> + + <FrameLayout + android:id="@+id/clock_container" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_centerInParent="true" + > + <com.android.systemui.statusbar.car.CarNavigationButton + android:id="@+id/qs" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@null" + systemui:intent="intent:#Intent;component=com.android.car.settings/.common.CarSettingActivity;launchFlags=0x14008000;end" + /> + <com.android.systemui.statusbar.policy.Clock + android:id="@+id/clock" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:elevation="5dp" + android:singleLine="true" + android:textAppearance="@style/TextAppearance.StatusBar.Clock" + /> + </FrameLayout> + + <LinearLayout + android:id="@+id/system_icon_area" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_centerHorizontal="true" + android:layout_centerVertical="true" + android:layout_toEndOf="@+id/clock_container" + android:paddingStart="@dimen/car_padding_1" + android:gravity="center_vertical" + android:orientation="horizontal" + > + + <include + layout="@layout/system_icons" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:paddingStart="4dp" + android:gravity="center_vertical" + /> + </LinearLayout> + + <FrameLayout + android:id="@+id/right_hvac_container" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_alignParentEnd="true" + > + + <com.android.systemui.statusbar.car.CarNavigationButton + android:id="@+id/hvacright" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@null" + systemui:broadcast="true" + systemui:intent="intent:#Intent;action=android.car.intent.action.TOGGLE_HVAC_CONTROLS;end" + /> + + <com.android.systemui.statusbar.hvac.AnimatedTemperatureView + android:id="@+id/righttext" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:paddingStart="16dp" + android:paddingEnd="@dimen/car_padding_4" + android:gravity="center_vertical|end" + android:minEms="4" + android:textAppearance="@style/TextAppearance.Car.Status" + systemui:hvacAreaId="4" + systemui:hvacMaxText="@string/hvac_max_text" + systemui:hvacMaxValue="@dimen/hvac_max_value" + systemui:hvacMinText="@string/hvac_min_text" + systemui:hvacMinValue="@dimen/hvac_min_value" + systemui:hvacPivotOffset="60dp" + systemui:hvacPropertyId="358614275" + systemui:hvacTempFormat="%.0f\u00B0" + /> + </FrameLayout> + </RelativeLayout> + +</com.android.systemui.statusbar.car.CarNavigationBarView> diff --git a/packages/CarSystemUI/res/layout/car_volume_dialog.xml b/packages/CarSystemUI/res/layout/car_volume_dialog.xml new file mode 100644 index 000000000000..c98740e42701 --- /dev/null +++ b/packages/CarSystemUI/res/layout/car_volume_dialog.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<androidx.car.widget.PagedListView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/volume_list" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="@android:color/black" + android:minWidth="@dimen/volume_dialog_panel_width" + android:theme="@style/Theme.Car.DialogListView" + app:dividerEndMargin="@dimen/car_keyline_1" + app:dividerStartMargin="@dimen/car_keyline_1" + app:gutter="none" + app:scrollBarEnabled="false" + app:showPagedListViewDivider="true"/> diff --git a/packages/CarSystemUI/res/layout/status_bar_wifi_group.xml b/packages/CarSystemUI/res/layout/status_bar_wifi_group.xml new file mode 100644 index 000000000000..2793e562a574 --- /dev/null +++ b/packages/CarSystemUI/res/layout/status_bar_wifi_group.xml @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<com.android.systemui.statusbar.StatusBarWifiView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:systemui="http://schemas.android.com/apk/res-auto" + android:id="@+id/wifi_combo" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:gravity="center_vertical"> + + <com.android.keyguard.AlphaOptimizedLinearLayout + android:id="@+id/wifi_group" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:paddingStart="2dp" + android:gravity="center_vertical" + > + <FrameLayout + android:id="@+id/inout_container" + android:layout_width="wrap_content" + android:layout_height="17dp" + android:gravity="center_vertical"> + <ImageView + android:id="@+id/wifi_in" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingEnd="2dp" + android:src="@drawable/ic_activity_down" + android:visibility="gone" + /> + <ImageView + android:id="@+id/wifi_out" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingEnd="2dp" + android:src="@drawable/ic_activity_up" + android:visibility="gone" + /> + </FrameLayout> + <FrameLayout + android:id="@+id/wifi_combo" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center_vertical"> + <com.android.systemui.statusbar.AlphaOptimizedImageView + android:id="@+id/wifi_signal" + android:layout_width="@dimen/status_bar_icon_size" + android:layout_height="@dimen/status_bar_icon_size" + android:theme="?attr/lightIconTheme"/> + </FrameLayout> + + <View + android:id="@+id/wifi_signal_spacer" + android:layout_width="@dimen/status_bar_wifi_signal_spacer_width" + android:layout_height="4dp" + android:visibility="gone"/> + + <ViewStub + android:id="@+id/connected_device_signals_stub" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout="@layout/connected_device_signal"/> + + <View + android:id="@+id/wifi_airplane_spacer" + android:layout_width="@dimen/status_bar_airplane_spacer_width" + android:layout_height="4dp" + android:visibility="gone" + /> + </com.android.keyguard.AlphaOptimizedLinearLayout> +</com.android.systemui.statusbar.StatusBarWifiView> diff --git a/packages/CarSystemUI/res/layout/super_status_bar.xml b/packages/CarSystemUI/res/layout/super_status_bar.xml new file mode 100644 index 000000000000..0594dce2ba22 --- /dev/null +++ b/packages/CarSystemUI/res/layout/super_status_bar.xml @@ -0,0 +1,91 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> + +<!-- This is the combined status bar / notification panel window. --> +<com.android.systemui.statusbar.phone.StatusBarWindowView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:sysui="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:fitsSystemWindows="true"> + + <com.android.systemui.statusbar.BackDropView + android:id="@+id/backdrop" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:visibility="gone" + sysui:ignoreRightInset="true" + > + <ImageView android:id="@+id/backdrop_back" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scaleType="centerCrop"/> + <ImageView android:id="@+id/backdrop_front" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scaleType="centerCrop" + android:visibility="invisible"/> + </com.android.systemui.statusbar.BackDropView> + + <com.android.systemui.statusbar.ScrimView + android:id="@+id/scrim_behind" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:importantForAccessibility="no" + sysui:ignoreRightInset="true" + /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="@dimen/status_bar_height" + android:orientation="vertical" + > + <FrameLayout + android:id="@+id/status_bar_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:visibility="gone" + /> + + <include layout="@layout/car_top_navigation_bar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + /> + </LinearLayout> + + <include layout="@layout/brightness_mirror"/> + + <ViewStub android:id="@+id/fullscreen_user_switcher_stub" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout="@layout/car_fullscreen_user_switcher"/> + + <include layout="@layout/status_bar_expanded" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:visibility="invisible"/> + + <com.android.systemui.statusbar.ScrimView + android:id="@+id/scrim_in_front" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:importantForAccessibility="no" + sysui:ignoreRightInset="true" + /> + +</com.android.systemui.statusbar.phone.StatusBarWindowView> diff --git a/packages/CarSystemUI/res/layout/system_icons.xml b/packages/CarSystemUI/res/layout/system_icons.xml new file mode 100644 index 000000000000..a7dd65eab550 --- /dev/null +++ b/packages/CarSystemUI/res/layout/system_icons.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/system_icons" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:gravity="center_vertical"> + + <com.android.systemui.statusbar.phone.StatusIconContainer + android:id="@+id/statusIcons" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:paddingEnd="4dp" + android:gravity="center_vertical" + android:orientation="horizontal" + /> + + <com.android.systemui.BatteryMeterView + android:id="@+id/battery" + android:layout_width="0dp" + android:layout_height="match_parent" + /> +</LinearLayout>
\ No newline at end of file diff --git a/packages/CarSystemUI/res/values-night/colors.xml b/packages/CarSystemUI/res/values-night/colors.xml new file mode 100644 index 000000000000..dad94a894603 --- /dev/null +++ b/packages/CarSystemUI/res/values-night/colors.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + <color name="car_accent">#356FE5</color> + <color name="status_bar_background_color">#ff000000</color> + <color name="system_bar_background_opaque">#ff0c1013</color> + + <!-- The color of the ripples on the untinted notifications --> + <color name="notification_ripple_untinted_color">@color/ripple_material_dark</color> +</resources> diff --git a/packages/CarSystemUI/res/values/attrs.xml b/packages/CarSystemUI/res/values/attrs.xml new file mode 100644 index 000000000000..617873865289 --- /dev/null +++ b/packages/CarSystemUI/res/values/attrs.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<resources> + + <!-- Custom attributes to configure hvac values --> + <declare-styleable name="AnimatedTemperatureView"> + <attr name="hvacAreaId" format="integer"/> + <attr name="hvacPropertyId" format="integer"/> + <attr name="hvacTempFormat" format="string"/> + <!-- how far away the animations should center around --> + <attr name="hvacPivotOffset" format="dimension"/> + <attr name="hvacMinValue" format="float"/> + <attr name="hvacMaxValue" format="float"/> + <attr name="hvacMinText" format="string|reference"/> + <attr name="hvacMaxText" format="string|reference"/> + <attr name="android:gravity"/> + <attr name="android:minEms"/> + <attr name="android:textAppearance"/> + </declare-styleable> +</resources> diff --git a/packages/CarSystemUI/res/values/colors.xml b/packages/CarSystemUI/res/values/colors.xml new file mode 100644 index 000000000000..e9ddbaf42b8c --- /dev/null +++ b/packages/CarSystemUI/res/values/colors.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + <color name="nav_bar_ripple_background_color">#40ffffff</color> + <color name="car_accent">#356FE5</color> + <!-- colors for user switcher --> + <color name="car_user_switcher_background_color">#000000</color> + <color name="car_user_switcher_name_text_color">@color/car_title2_light</color> + <color name="car_user_switcher_add_user_background_color">#131313</color> + <color name="car_nav_icon_fill_color">@color/car_grey_50</color> + <!-- colors for seekbar --> + <color name="car_seekbar_track_background">#131315</color> + <color name="car_seekbar_track_secondary_progress">@color/car_accent</color> + <!-- colors for volume dialog tint --> + <color name="car_volume_dialog_tint">@color/car_tint_light</color> + + <!-- System ui can't depend on car libs so redefine. --> + <color name="car_grey_50">#fffafafa</color> + + <color name="docked_divider_background">@color/car_grey_50</color> + <color name="system_bar_background_opaque">#ff172026</color> + + <color name="status_bar_background_color">#33000000</color> + <drawable name="system_bar_background">@color/status_bar_background_color</drawable> + + <!-- The scrim color for the background of the notifications shade. --> + <color name="scrim_behind_color">#172026</color> + + <!-- The color of the dividing line between grouped notifications. --> + <color name="notification_divider_color">@*android:color/notification_action_list</color> + + <!-- The color of the ripples on the untinted notifications --> + <color name="notification_ripple_untinted_color">@color/ripple_material_light</color> + + <color name="car_teal_700">#ff00796b</color> + <color name="car_grey_300">#ffe0e0e0</color> + <color name="car_grey_900">#ff212121</color> + + <color name="keyguard_button_text_color">@android:color/black</color> +</resources> diff --git a/packages/CarSystemUI/res/values/config.xml b/packages/CarSystemUI/res/values/config.xml new file mode 100644 index 000000000000..452d61df5322 --- /dev/null +++ b/packages/CarSystemUI/res/values/config.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<resources> + <string name="config_statusBarComponent" translatable="false"> + com.android.systemui.statusbar.car.CarStatusBar + </string> + <string name="config_systemUIFactoryComponent" translatable="false"> + com.android.systemui.CarSystemUIFactory + </string> + <bool name="config_enableFullscreenUserSwitcher">true</bool> + + <!-- configure which system ui bars should be displayed --> + <bool name="config_enableLeftNavigationBar">false</bool> + <bool name="config_enableRightNavigationBar">false</bool> + <bool name="config_enableBottomNavigationBar">true</bool> + +</resources> diff --git a/packages/CarSystemUI/res/values/dimens.xml b/packages/CarSystemUI/res/values/dimens.xml new file mode 100644 index 000000000000..3829aa3685b6 --- /dev/null +++ b/packages/CarSystemUI/res/values/dimens.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<resources> + <!-- + Note: status bar height and navigation bar heights are defined + in frameworks/base/core package and thus will have no effect if + set here. See car_product overlay for car specific defaults--> + + <dimen name="status_bar_icon_drawing_size_dark">36dp</dimen> + <dimen name="status_bar_icon_drawing_size">36dp</dimen> + <dimen name="car_qs_header_system_icons_area_height">96dp</dimen> + <!-- The amount by which to scale up the status bar icons. --> + <item name="status_bar_icon_scale_factor" format="float" type="dimen">1.75</item> + + <dimen name="car_primary_icon_size">36dp</dimen> + + <!-- dimensions for the car user switcher --> + <dimen name="car_user_switcher_name_text_size">@dimen/car_title2_size</dimen> + <dimen name="car_user_switcher_vertical_spacing_between_users">124dp</dimen> + + <!--These values represent MIN and MAX for hvac--> + <item name="hvac_min_value" format="float" type="dimen">0</item> + <item name="hvac_max_value" format="float" type="dimen">126</item> + + <!-- Largest size an avatar might need to be drawn in the user picker, status bar, or + quick settings header --> + <dimen name="max_avatar_size">128dp</dimen> + + <!-- Standard image button size for volume dialog buttons --> + <dimen name="volume_button_size">84dp</dimen> + <!-- The maximum width allowed for the volume dialog. For auto, we allow this to span a good + deal of the screen. This value accounts for the side margins. --> + <dimen name="volume_dialog_panel_width">1920dp</dimen> + <dimen name="volume_dialog_side_margin">@dimen/side_margin</dimen> + + <dimen name="volume_dialog_elevation">6dp</dimen> + + <dimen name="volume_dialog_row_margin_end">@dimen/car_keyline_3</dimen> + + <dimen name="volume_dialog_row_padding_end">0dp</dimen> + + <dimen name="line_item_height">128dp</dimen> + <dimen name="volume_icon_size">96dp</dimen> + <dimen name="side_margin">148dp</dimen> + <dimen name="car_keyline_1">24dp</dimen> + <dimen name="car_keyline_2">96dp</dimen> + <dimen name="car_keyline_3">128dp</dimen> +</resources> diff --git a/packages/CarSystemUI/res/values/integers.xml b/packages/CarSystemUI/res/values/integers.xml new file mode 100644 index 000000000000..8b87c740425f --- /dev/null +++ b/packages/CarSystemUI/res/values/integers.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. +--> + +<resources> + <integer name="user_fullscreen_switcher_num_col">2</integer> +</resources> diff --git a/packages/CarSystemUI/res/values/strings.xml b/packages/CarSystemUI/res/values/strings.xml new file mode 100644 index 000000000000..0368e61978fd --- /dev/null +++ b/packages/CarSystemUI/res/values/strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<resources> + <!-- String to represent lowest setting of an HVAC system [CHAR LIMIT=5]--> + <string name="hvac_min_text">Min</string> + <!-- String to represent largest setting of an HVAC system [CHAR LIMIT=5]--> + <string name="hvac_max_text">Max</string> +</resources> diff --git a/packages/CarSystemUI/res/values/styles.xml b/packages/CarSystemUI/res/values/styles.xml new file mode 100644 index 000000000000..22a699c3c776 --- /dev/null +++ b/packages/CarSystemUI/res/values/styles.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + + <!-- The style for the volume icons in the volume dialog. This style makes the icon scale to + fit its container since auto wants the icon to be larger. The padding is added to make it + so the icon does not press along the edges of the dialog. --> + <style name="VolumeButtons" parent="@android:style/Widget.Material.Button.Borderless"> + <item name="android:background">@drawable/btn_borderless_rect</item> + <item name="android:scaleType">fitCenter</item> + <item name="android:padding">22dp</item> + </style> + + <style name="TextAppearance.StatusBar.Clock" + parent="@*android:style/TextAppearance.StatusBar.Icon"> + <item name="android:textSize">42sp</item> + <item name="android:fontFamily">sans-serif-regular</item> + <item name="android:textColor">@color/car_grey_50</item> + </style> + + <style name="TextAppearance.Car.Status"> + <item name="android:textSize">@dimen/car_body2_size</item> + <item name="android:textColor">@color/car_grey_50</item> + </style> + + <style name="CarNavigationBarButtonTheme"> + <item name="android:colorControlHighlight">@color/nav_bar_ripple_background_color</item> + </style> + + <style name="Theme.Car.DialogListView" parent="@style/Theme.Car.NoActionBar"> + <item name="listItemBackgroundColor">@android:color/black</item> + </style> + + <style name="NavigationBarButton"> + <item name="android:layout_height">96dp</item> + <item name="android:layout_width">96dp</item> + <item name="android:background">@drawable/nav_button_background</item> + </style> + +</resources> diff --git a/packages/CarSystemUI/res/xml/car_volume_items.xml b/packages/CarSystemUI/res/xml/car_volume_items.xml new file mode 100644 index 000000000000..8715946aac26 --- /dev/null +++ b/packages/CarSystemUI/res/xml/car_volume_items.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<!-- + Defines all possible items on car volume settings UI, keyed by usage. +--> +<carVolumeItems xmlns:car="http://schemas.android.com/apk/res-auto"> + <item car:usage="unknown" + car:icon="@drawable/car_ic_music"/> + <item car:usage="media" + car:icon="@drawable/car_ic_music"/> + <item car:usage="voice_communication" + car:icon="@*android:drawable/ic_audio_ring_notif"/> + <item car:usage="voice_communication_signalling" + car:icon="@*android:drawable/ic_audio_ring_notif"/> + <item car:usage="alarm" + car:icon="@drawable/ic_volume_alarm"/> + <item car:usage="notification" + car:icon="@drawable/car_ic_notification"/> + <item car:usage="notification_ringtone" + car:icon="@drawable/car_ic_notification"/> + <item car:usage="notification_communication_request" + car:icon="@drawable/car_ic_notification"/> + <item car:usage="notification_communication_instant" + car:icon="@drawable/car_ic_notification"/> + <item car:usage="notification_communication_delayed" + car:icon="@drawable/car_ic_notification"/> + <item car:usage="notification_event" + car:icon="@drawable/car_ic_notification"/> + <item car:usage="assistance_accessibility" + car:icon="@drawable/car_ic_notification"/> + <item car:usage="assistance_navigation_guidance" + car:icon="@drawable/car_ic_navigation"/> + <item car:usage="assistance_sonification" + car:icon="@drawable/car_ic_notification"/> + <item car:usage="game" + car:icon="@drawable/car_ic_music"/> + <item car:usage="assistant" + car:icon="@drawable/car_ic_music"/> +</carVolumeItems> + diff --git a/packages/CarSystemUI/src/com/android/systemui/CarSystemUIFactory.java b/packages/CarSystemUI/src/com/android/systemui/CarSystemUIFactory.java new file mode 100644 index 000000000000..1d39f72b12dc --- /dev/null +++ b/packages/CarSystemUI/src/com/android/systemui/CarSystemUIFactory.java @@ -0,0 +1,51 @@ +/* + * 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; + +import android.content.Context; +import android.util.ArrayMap; + +import com.android.internal.widget.LockPatternUtils; +import com.android.keyguard.ViewMediatorCallback; +import com.android.systemui.Dependency.DependencyProvider; +import com.android.systemui.car.CarNotificationEntryManager; +import com.android.systemui.statusbar.NotificationEntryManager; +import com.android.systemui.statusbar.car.CarFacetButtonController; +import com.android.systemui.statusbar.car.CarStatusBarKeyguardViewManager; +import com.android.systemui.statusbar.car.hvac.HvacController; +import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; + +/** + * Class factory to provide car specific SystemUI components. + */ +public class CarSystemUIFactory extends SystemUIFactory { + + public StatusBarKeyguardViewManager createStatusBarKeyguardViewManager(Context context, + ViewMediatorCallback viewMediatorCallback, LockPatternUtils lockPatternUtils) { + return new CarStatusBarKeyguardViewManager(context, viewMediatorCallback, lockPatternUtils); + } + + @Override + public void injectDependencies(ArrayMap<Object, DependencyProvider> providers, + Context context) { + super.injectDependencies(providers, context); + providers.put(NotificationEntryManager.class, + () -> new CarNotificationEntryManager(context)); + providers.put(CarFacetButtonController.class, () -> new CarFacetButtonController(context)); + providers.put(HvacController.class, () -> new HvacController(context)); + } +} diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/AnimatedTemperatureView.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/AnimatedTemperatureView.java new file mode 100644 index 000000000000..27d31064955d --- /dev/null +++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/AnimatedTemperatureView.java @@ -0,0 +1,276 @@ +/* + * 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.hvac; + +import android.animation.ObjectAnimator; +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.Rect; +import android.graphics.drawable.ColorDrawable; +import com.android.systemui.statusbar.car.hvac.TemperatureView; +import com.android.systemui.statusbar.car.hvac.HvacController; +import android.util.AttributeSet; +import android.util.Property; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextSwitcher; +import android.widget.TextView; + +import com.android.systemui.Dependency; +import com.android.systemui.R; + +/** + * Simple text display of HVAC properties, It is designed to show mTemperature and is configured in + * the XML. + * XML properties: + * hvacPropertyId - Example: CarHvacManager.ID_ZONED_TEMP_SETPOINT (16385) + * hvacAreaId - Example: VehicleSeat.SEAT_ROW_1_LEFT (1) + * hvacTempFormat - Example: "%.1f\u00B0" (1 decimal and the degree symbol) + * hvacOrientaion = Example: left + * <p> + * Note: It registers itself with {@link HvacController} + */ +public class AnimatedTemperatureView extends FrameLayout implements TemperatureView { + + private static final float TEMPERATURE_EQUIVALENT_DELTA = .01f; + private static final Property<ColorDrawable, Integer> COLOR_PROPERTY = + new Property<ColorDrawable, Integer>(Integer.class, "color") { + + @Override + public Integer get(ColorDrawable object) { + return object.getColor(); + } + + @Override + public void set(ColorDrawable object, Integer value) { + object.setColor(value); + } + }; + + static boolean isHorizontal(int gravity) { + return Gravity.isHorizontal(gravity) + && (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.CENTER_HORIZONTAL; + } + + @SuppressLint("RtlHardcoded") + static boolean isLeft(int gravity, int layoutDirection) { + return Gravity + .getAbsoluteGravity(gravity & Gravity.HORIZONTAL_GRAVITY_MASK, layoutDirection) + == Gravity.LEFT; + } + + static boolean isVertical(int gravity) { + return Gravity.isVertical(gravity) + && (gravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.CENTER_VERTICAL; + } + + static boolean isTop(int gravity) { + return (gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.TOP; + } + + private final int mAreaId; + private final int mPropertyId; + private final int mPivotOffset; + private final int mGravity; + private final int mTextAppearanceRes; + private final int mMinEms; + private final Rect mPaddingRect; + private final float mMinValue; + private final float mMaxValue; + + private final ColorDrawable mBackgroundColor; + + private final TemperatureColorStore mColorStore = new TemperatureColorStore(); + private final TemperatureBackgroundAnimator mBackgroundAnimator; + private final TemperatureTextAnimator mTextAnimator; + + public AnimatedTemperatureView(Context context, AttributeSet attrs) { + super(context, attrs); + TypedArray typedArray = context.obtainStyledAttributes(attrs, + R.styleable.AnimatedTemperatureView); + mAreaId = typedArray.getInt(R.styleable.AnimatedTemperatureView_hvacAreaId, -1); + mPropertyId = typedArray.getInt(R.styleable.AnimatedTemperatureView_hvacPropertyId, -1); + mPivotOffset = + typedArray.getDimensionPixelOffset( + R.styleable.AnimatedTemperatureView_hvacPivotOffset, 0); + mGravity = typedArray.getInt(R.styleable.AnimatedTemperatureView_android_gravity, + Gravity.START); + mTextAppearanceRes = + typedArray.getResourceId(R.styleable.AnimatedTemperatureView_android_textAppearance, + 0); + mMinEms = typedArray.getInteger(R.styleable.AnimatedTemperatureView_android_minEms, 0); + mMinValue = typedArray.getFloat(R.styleable.AnimatedTemperatureView_hvacMinValue, + Float.NaN); + mMaxValue = typedArray.getFloat(R.styleable.AnimatedTemperatureView_hvacMaxValue, + Float.NaN); + + + mPaddingRect = + new Rect(getPaddingLeft(), getPaddingTop(), getPaddingRight(), getPaddingBottom()); + setPadding(0, 0, 0, 0); + + setClipChildren(false); + setClipToPadding(false); + + // init Views + TextSwitcher textSwitcher = new TextSwitcher(context); + textSwitcher.setFactory(this::generateTextView); + ImageView background = new ImageView(context); + mBackgroundColor = new ColorDrawable(Color.TRANSPARENT); + background.setImageDrawable(mBackgroundColor); + background.setVisibility(View.GONE); + + mBackgroundAnimator = new TemperatureBackgroundAnimator(this, background); + + + String format = typedArray.getString(R.styleable.AnimatedTemperatureView_hvacTempFormat); + format = (format == null) ? "%.1f\u00B0" : format; + CharSequence minText = typedArray.getString( + R.styleable.AnimatedTemperatureView_hvacMinText); + CharSequence maxText = typedArray.getString( + R.styleable.AnimatedTemperatureView_hvacMaxText); + mTextAnimator = new TemperatureTextAnimator(this, textSwitcher, format, mPivotOffset, + minText, maxText); + + addView(background, ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); + addView(textSwitcher, ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); + + typedArray.recycle(); + + // register with controller + HvacController hvacController = Dependency.get(HvacController.class); + hvacController.addHvacTextView(this); + } + + private TextView generateTextView() { + TextView textView = new TextView(getContext()); + textView.setTextAppearance(mTextAppearanceRes); + textView.setAllCaps(true); + textView.setMinEms(mMinEms); + textView.setGravity(mGravity); + textView.setPadding(mPaddingRect.left, mPaddingRect.top, mPaddingRect.right, + mPaddingRect.bottom); + textView.getViewTreeObserver() + .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + if (isHorizontal(mGravity)) { + if (isLeft(mGravity, getLayoutDirection())) { + textView.setPivotX(-mPivotOffset); + } else { + textView.setPivotX(textView.getWidth() + mPivotOffset); + } + } + textView.getViewTreeObserver().removeOnPreDrawListener(this); + return false; + } + }); + textView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + + return textView; + } + + /** + * Formats the float for display + * + * @param temp - The current temp or NaN + */ + @Override + public void setTemp(float temp) { + mTextAnimator.setTemp(temp); + if (Float.isNaN(temp)) { + mBackgroundAnimator.hideCircle(); + return; + } + int color; + if (isMinValue(temp)) { + color = mColorStore.getMinColor(); + } else if (isMaxValue(temp)) { + color = mColorStore.getMaxColor(); + } else { + color = mColorStore.getColorForTemperature(temp); + } + if (mBackgroundAnimator.isOpen()) { + ObjectAnimator colorAnimator = + ObjectAnimator.ofInt(mBackgroundColor, COLOR_PROPERTY, color); + colorAnimator.setEvaluator((fraction, startValue, endValue) -> mColorStore + .lerpColor(fraction, (int) startValue, (int) endValue)); + colorAnimator.start(); + } else { + mBackgroundColor.setColor(color); + } + + mBackgroundAnimator.animateOpen(); + } + + boolean isMinValue(float temp) { + return !Float.isNaN(mMinValue) && isApproxEqual(temp, mMinValue); + } + + boolean isMaxValue(float temp) { + return !Float.isNaN(mMaxValue) && isApproxEqual(temp, mMaxValue); + } + + private boolean isApproxEqual(float left, float right) { + return Math.abs(left - right) <= TEMPERATURE_EQUIVALENT_DELTA; + } + + int getGravity() { + return mGravity; + } + + int getPivotOffset() { + return mPivotOffset; + } + + Rect getPaddingRect() { + return mPaddingRect; + } + + /** + * @return propertiyId Example: CarHvacManager.ID_ZONED_TEMP_SETPOINT (358614275) + */ + @Override + public int getPropertyId() { + return mPropertyId; + } + + /** + * @return hvac AreaId - Example: VehicleSeat.SEAT_ROW_1_LEFT (1) + */ + @Override + public int getAreaId() { + return mAreaId; + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mBackgroundAnimator.stopAnimations(); + } + +} + diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/TemperatureBackgroundAnimator.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/TemperatureBackgroundAnimator.java new file mode 100644 index 000000000000..0bc94b500387 --- /dev/null +++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/TemperatureBackgroundAnimator.java @@ -0,0 +1,338 @@ +/* + * 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.hvac; + +import static com.android.systemui.statusbar.hvac.AnimatedTemperatureView.isHorizontal; +import static com.android.systemui.statusbar.hvac.AnimatedTemperatureView.isLeft; +import static com.android.systemui.statusbar.hvac.AnimatedTemperatureView.isTop; +import static com.android.systemui.statusbar.hvac.AnimatedTemperatureView.isVertical; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.annotation.IntDef; +import android.graphics.Rect; +import android.view.View; +import android.view.ViewAnimationUtils; +import android.view.animation.AnticipateInterpolator; +import android.widget.ImageView; + +import java.util.ArrayList; +import java.util.List; + +/** + * Controls circular reveal animation of temperature background + */ +class TemperatureBackgroundAnimator { + + private static final AnticipateInterpolator ANTICIPATE_INTERPOLATOR = + new AnticipateInterpolator(); + private static final float MAX_OPACITY = .6f; + + private final View mAnimatedView; + + private int mPivotX; + private int mPivotY; + private int mGoneRadius; + private int mOvershootRadius; + private int mRestingRadius; + private int mBumpRadius; + + @CircleState + private int mCircleState; + + private Animator mCircularReveal; + private boolean mAnimationsReady; + + @IntDef({CircleState.GONE, CircleState.ENTERING, CircleState.OVERSHOT, CircleState.RESTING, + CircleState.RESTED, CircleState.BUMPING, CircleState.BUMPED, CircleState.EXITING}) + private @interface CircleState { + int GONE = 0; + int ENTERING = 1; + int OVERSHOT = 2; + int RESTING = 3; + int RESTED = 4; + int BUMPING = 5; + int BUMPED = 6; + int EXITING = 7; + } + + TemperatureBackgroundAnimator( + AnimatedTemperatureView parent, + ImageView animatedView) { + mAnimatedView = animatedView; + mAnimatedView.setAlpha(0); + + parent.addOnLayoutChangeListener( + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> + setupAnimations(parent.getGravity(), parent.getPivotOffset(), + parent.getPaddingRect(), parent.getWidth(), parent.getHeight())); + } + + private void setupAnimations(int gravity, int pivotOffset, Rect paddingRect, + int width, int height) { + int padding; + if (isHorizontal(gravity)) { + mGoneRadius = pivotOffset; + if (isLeft(gravity, mAnimatedView.getLayoutDirection())) { + mPivotX = -pivotOffset; + padding = paddingRect.right; + } else { + mPivotX = width + pivotOffset; + padding = paddingRect.left; + } + mPivotY = height / 2; + mOvershootRadius = pivotOffset + width; + } else if (isVertical(gravity)) { + mGoneRadius = pivotOffset; + if (isTop(gravity)) { + mPivotY = -pivotOffset; + padding = paddingRect.bottom; + } else { + mPivotY = height + pivotOffset; + padding = paddingRect.top; + } + mPivotX = width / 2; + mOvershootRadius = pivotOffset + height; + } else { + mPivotX = width / 2; + mPivotY = height / 2; + mGoneRadius = 0; + if (width > height) { + mOvershootRadius = height; + padding = Math.max(paddingRect.top, paddingRect.bottom); + } else { + mOvershootRadius = width; + padding = Math.max(paddingRect.left, paddingRect.right); + } + } + mRestingRadius = mOvershootRadius - padding; + mBumpRadius = mOvershootRadius - padding / 3; + mAnimationsReady = true; + } + + boolean isOpen() { + return mCircleState != CircleState.GONE; + } + + void animateOpen() { + if (!mAnimationsReady || mCircleState == CircleState.ENTERING) { + return; + } + + AnimatorSet set = new AnimatorSet(); + List<Animator> animators = new ArrayList<>(); + switch (mCircleState) { + case CircleState.ENTERING: + throw new AssertionError("Should not be able to reach this statement"); + case CircleState.GONE: { + Animator startCircle = createEnterAnimator(); + markState(startCircle, CircleState.ENTERING); + animators.add(startCircle); + Animator holdOvershoot = ViewAnimationUtils + .createCircularReveal(mAnimatedView, mPivotX, mPivotY, mOvershootRadius, + mOvershootRadius); + holdOvershoot.setDuration(50); + markState(holdOvershoot, CircleState.OVERSHOT); + animators.add(holdOvershoot); + Animator rest = ViewAnimationUtils + .createCircularReveal(mAnimatedView, mPivotX, mPivotY, mOvershootRadius, + mRestingRadius); + markState(rest, CircleState.RESTING); + animators.add(rest); + Animator holdRest = ViewAnimationUtils + .createCircularReveal(mAnimatedView, mPivotX, mPivotY, mRestingRadius, + mRestingRadius); + markState(holdRest, CircleState.RESTED); + holdRest.setDuration(1000); + animators.add(holdRest); + Animator exit = createExitAnimator(mRestingRadius); + markState(exit, CircleState.EXITING); + animators.add(exit); + } + break; + case CircleState.RESTED: + case CircleState.RESTING: + case CircleState.EXITING: + case CircleState.OVERSHOT: + int startRadius = + mCircleState == CircleState.OVERSHOT ? mOvershootRadius : mRestingRadius; + Animator bump = ViewAnimationUtils + .createCircularReveal(mAnimatedView, mPivotX, mPivotY, startRadius, + mBumpRadius); + bump.setDuration(50); + markState(bump, CircleState.BUMPING); + animators.add(bump); + // fallthrough intentional + case CircleState.BUMPED: + case CircleState.BUMPING: + Animator holdBump = ViewAnimationUtils + .createCircularReveal(mAnimatedView, mPivotX, mPivotY, mBumpRadius, + mBumpRadius); + holdBump.setDuration(100); + markState(holdBump, CircleState.BUMPED); + animators.add(holdBump); + Animator rest = ViewAnimationUtils + .createCircularReveal(mAnimatedView, mPivotX, mPivotY, mBumpRadius, + mRestingRadius); + markState(rest, CircleState.RESTING); + animators.add(rest); + Animator holdRest = ViewAnimationUtils + .createCircularReveal(mAnimatedView, mPivotX, mPivotY, mRestingRadius, + mRestingRadius); + holdRest.setDuration(1000); + markState(holdRest, CircleState.RESTED); + animators.add(holdRest); + Animator exit = createExitAnimator(mRestingRadius); + markState(exit, CircleState.EXITING); + animators.add(exit); + break; + } + set.playSequentially(animators); + set.addListener(new AnimatorListenerAdapter() { + private boolean mCanceled = false; + + @Override + public void onAnimationStart(Animator animation) { + if (mCircularReveal != null) { + mCircularReveal.cancel(); + } + mCircularReveal = animation; + mAnimatedView.setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationCancel(Animator animation) { + mCanceled = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + if (mCanceled) { + return; + } + mCircularReveal = null; + mCircleState = CircleState.GONE; + mAnimatedView.setVisibility(View.GONE); + } + }); + + set.start(); + } + + private Animator createEnterAnimator() { + AnimatorSet animatorSet = new AnimatorSet(); + Animator circularReveal = ViewAnimationUtils + .createCircularReveal(mAnimatedView, mPivotX, mPivotY, mGoneRadius, + mOvershootRadius); + Animator fade = ObjectAnimator.ofFloat(mAnimatedView, View.ALPHA, MAX_OPACITY); + animatorSet.playTogether(circularReveal, fade); + return animatorSet; + } + + private Animator createExitAnimator(int startRadius) { + AnimatorSet animatorSet = new AnimatorSet(); + Animator circularHide = ViewAnimationUtils + .createCircularReveal(mAnimatedView, mPivotX, mPivotY, startRadius, + (mGoneRadius + startRadius) / 2); + circularHide.setInterpolator(ANTICIPATE_INTERPOLATOR); + Animator fade = ObjectAnimator.ofFloat(mAnimatedView, View.ALPHA, 0); + fade.setStartDelay(50); + animatorSet.playTogether(circularHide, fade); + return animatorSet; + } + + void hideCircle() { + if (!mAnimationsReady || mCircleState == CircleState.GONE + || mCircleState == CircleState.EXITING) { + return; + } + + int startRadius; + switch (mCircleState) { + // Unreachable, but here to exhaust switch cases + //noinspection ConstantConditions + case CircleState.EXITING: + //noinspection ConstantConditions + case CircleState.GONE: + throw new AssertionError("Should not be able to reach this statement"); + case CircleState.BUMPED: + case CircleState.BUMPING: + startRadius = mBumpRadius; + break; + case CircleState.OVERSHOT: + startRadius = mOvershootRadius; + break; + case CircleState.ENTERING: + case CircleState.RESTED: + case CircleState.RESTING: + startRadius = mRestingRadius; + break; + default: + throw new IllegalStateException("Unknown CircleState: " + mCircleState); + } + + Animator hideAnimation = createExitAnimator(startRadius); + if (startRadius == mRestingRadius) { + hideAnimation.setInterpolator(ANTICIPATE_INTERPOLATOR); + } + hideAnimation.addListener(new AnimatorListenerAdapter() { + private boolean mCanceled = false; + + @Override + public void onAnimationStart(Animator animation) { + mCircleState = CircleState.EXITING; + if (mCircularReveal != null) { + mCircularReveal.cancel(); + } + mCircularReveal = animation; + } + + @Override + public void onAnimationCancel(Animator animation) { + mCanceled = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + if (mCanceled) { + return; + } + mCircularReveal = null; + mCircleState = CircleState.GONE; + mAnimatedView.setVisibility(View.GONE); + } + }); + hideAnimation.start(); + } + + void stopAnimations() { + if (mCircularReveal != null) { + mCircularReveal.end(); + } + } + + private void markState(Animator animator, @CircleState int startState) { + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + mCircleState = startState; + } + }); + } +} diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/TemperatureColorStore.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/TemperatureColorStore.java new file mode 100644 index 000000000000..a40ffaf850c5 --- /dev/null +++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/TemperatureColorStore.java @@ -0,0 +1,202 @@ +/* + * 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.hvac; + +import android.graphics.Color; + +/** + * Contains the logic for mapping colors to temperatures + */ +class TemperatureColorStore { + + private static class TemperatureColorValue { + final float mTemperature; + final int mColor; + + private TemperatureColorValue(float temperature, int color) { + this.mTemperature = temperature; + this.mColor = color; + } + + float getTemperature() { + return mTemperature; + } + + int getColor() { + return mColor; + } + } + + private static TemperatureColorValue tempToColor(float temperature, int color) { + return new TemperatureColorValue(temperature, color); + } + + private static final int COLOR_COLDEST = 0xFF406DFF; + private static final int COLOR_COLD = 0xFF4094FF; + private static final int COLOR_NEUTRAL = 0xFFF4F4F4; + private static final int COLOR_WARM = 0xFFFF550F; + private static final int COLOR_WARMEST = 0xFFFF0000; + // must be sorted by temperature + private static final TemperatureColorValue[] sTemperatureColorValues = + { + // Celsius + tempToColor(19, COLOR_COLDEST), + tempToColor(21, COLOR_COLD), + tempToColor(23, COLOR_NEUTRAL), + tempToColor(25, COLOR_WARM), + tempToColor(27, COLOR_WARMEST), + + // Switch over + tempToColor(45, COLOR_WARMEST), + tempToColor(45.00001f, COLOR_COLDEST), + + // Farenheight + tempToColor(66, COLOR_COLDEST), + tempToColor(70, COLOR_COLD), + tempToColor(74, COLOR_NEUTRAL), + tempToColor(76, COLOR_WARM), + tempToColor(80, COLOR_WARMEST) + }; + + private static final int COLOR_UNSET = Color.BLACK; + + private final float[] mTempHsv1 = new float[3]; + private final float[] mTempHsv2 = new float[3]; + private final float[] mTempHsv3 = new float[3]; + + int getMinColor() { + return COLOR_COLDEST; + } + + int getMaxColor() { + return COLOR_WARMEST; + } + + int getColorForTemperature(float temperature) { + if (Float.isNaN(temperature)) { + return COLOR_UNSET; + } + TemperatureColorValue bottomValue = sTemperatureColorValues[0]; + if (temperature <= bottomValue.getTemperature()) { + return bottomValue.getColor(); + } + TemperatureColorValue topValue = + sTemperatureColorValues[sTemperatureColorValues.length - 1]; + if (temperature >= topValue.getTemperature()) { + return topValue.getColor(); + } + + int index = binarySearch(temperature); + if (index >= 0) { + return sTemperatureColorValues[index].getColor(); + } + + index = -index - 1; // move to the insertion point + + TemperatureColorValue startValue = sTemperatureColorValues[index - 1]; + TemperatureColorValue endValue = sTemperatureColorValues[index]; + float fraction = (temperature - startValue.getTemperature()) / (endValue.getTemperature() + - startValue.getTemperature()); + return lerpColor(fraction, startValue.getColor(), endValue.getColor()); + } + + int lerpColor(float fraction, int startColor, int endColor) { + float[] startHsv = mTempHsv1; + Color.colorToHSV(startColor, startHsv); + float[] endHsv = mTempHsv2; + Color.colorToHSV(endColor, endHsv); + + // If a target color is white/gray, it should use the same hue as the other target + if (startHsv[1] == 0) { + startHsv[0] = endHsv[0]; + } + if (endHsv[1] == 0) { + endHsv[0] = startHsv[0]; + } + + float[] outColor = mTempHsv3; + outColor[0] = hueLerp(fraction, startHsv[0], endHsv[0]); + outColor[1] = lerp(fraction, startHsv[1], endHsv[1]); + outColor[2] = lerp(fraction, startHsv[2], endHsv[2]); + + return Color.HSVToColor(outColor); + } + + private float hueLerp(float fraction, float start, float end) { + // If in flat part of curve, no interpolation necessary + if (start == end) { + return start; + } + + // If the hues are more than 180 degrees apart, go the other way around the color wheel + // by moving the smaller value above 360 + if (Math.abs(start - end) > 180f) { + if (start < end) { + start += 360f; + } else { + end += 360f; + } + } + // Lerp and ensure the final output is within [0, 360) + return lerp(fraction, start, end) % 360f; + + } + + private float lerp(float fraction, float start, float end) { + // If in flat part of curve, no interpolation necessary + if (start == end) { + return start; + } + + // If outside bounds, use boundary value + if (fraction >= 1) { + return end; + } + if (fraction <= 0) { + return start; + } + + return (end - start) * fraction + start; + } + + private int binarySearch(float temperature) { + int low = 0; + int high = sTemperatureColorValues.length; + + while (low <= high) { + int mid = (low + high) >>> 1; + float midVal = sTemperatureColorValues[mid].getTemperature(); + + if (midVal < temperature) { + low = mid + 1; // Neither val is NaN, thisVal is smaller + } else if (midVal > temperature) { + high = mid - 1; // Neither val is NaN, thisVal is larger + } else { + int midBits = Float.floatToIntBits(midVal); + int keyBits = Float.floatToIntBits(temperature); + if (midBits == keyBits) { // Values are equal + return mid; // Key found + } else if (midBits < keyBits) { // (-0.0, 0.0) or (!NaN, NaN) + low = mid + 1; + } else { /* (0.0, -0.0) or (NaN, !NaN)*/ + high = mid - 1; + } + } + } + return -(low + 1); // key not found. + } +} diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/TemperatureTextAnimator.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/TemperatureTextAnimator.java new file mode 100644 index 000000000000..8ee5ef6badc3 --- /dev/null +++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/TemperatureTextAnimator.java @@ -0,0 +1,164 @@ +/* + * 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.hvac; + +import static com.android.systemui.statusbar.hvac.AnimatedTemperatureView.isHorizontal; +import static com.android.systemui.statusbar.hvac.AnimatedTemperatureView.isLeft; + +import android.annotation.NonNull; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.view.animation.AnimationSet; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.RotateAnimation; +import android.view.animation.TranslateAnimation; +import android.widget.TextSwitcher; + +/** + * Controls animating TemperatureView's text + */ +class TemperatureTextAnimator { + + private static final DecelerateInterpolator DECELERATE_INTERPOLATOR = + new DecelerateInterpolator(); + private static final AccelerateDecelerateInterpolator ACCELERATE_DECELERATE_INTERPOLATOR = + new AccelerateDecelerateInterpolator(); + + private static final int ROTATION_DEGREES = 15; + private static final int DURATION_MILLIS = 200; + + private AnimatedTemperatureView mParent; + private final TextSwitcher mTextSwitcher; + private final String mTempFormat; + private final int mPivotOffset; + private final CharSequence mMinText; + private final CharSequence mMaxText; + + private Animation mTextInAnimationUp; + private Animation mTextOutAnimationUp; + private Animation mTextInAnimationDown; + private Animation mTextOutAnimationDown; + private Animation mTextFadeInAnimation; + private Animation mTextFadeOutAnimation; + + private float mLastTemp = Float.NaN; + + TemperatureTextAnimator(AnimatedTemperatureView parent, TextSwitcher textSwitcher, + String tempFormat, int pivotOffset, + CharSequence minText, CharSequence maxText) { + mParent = parent; + mTextSwitcher = textSwitcher; + mTempFormat = tempFormat; + mPivotOffset = pivotOffset; + mMinText = minText; + mMaxText = maxText; + + mParent.addOnLayoutChangeListener( + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> + setupAnimations(mParent.getGravity())); + } + + void setTemp(float temp) { + if (Float.isNaN(temp)) { + mTextSwitcher.setInAnimation(mTextFadeInAnimation); + mTextSwitcher.setOutAnimation(mTextFadeOutAnimation); + mTextSwitcher.setText("--"); + mLastTemp = temp; + return; + } + boolean isMinValue = mParent.isMinValue(temp); + boolean isMaxValue = mParent.isMaxValue(temp); + if (Float.isNaN(mLastTemp)) { + mTextSwitcher.setInAnimation(mTextFadeInAnimation); + mTextSwitcher.setOutAnimation(mTextFadeOutAnimation); + } else if (!isMinValue && (isMaxValue || temp > mLastTemp)) { + mTextSwitcher.setInAnimation(mTextInAnimationUp); + mTextSwitcher.setOutAnimation(mTextOutAnimationUp); + } else { + mTextSwitcher.setInAnimation(mTextInAnimationDown); + mTextSwitcher.setOutAnimation(mTextOutAnimationDown); + } + CharSequence text; + if (isMinValue) { + text = mMinText; + } else if (isMaxValue) { + text = mMaxText; + } else { + text = String.format(mTempFormat, temp); + } + mTextSwitcher.setText(text); + mLastTemp = temp; + } + + private void setupAnimations(int gravity) { + mTextFadeInAnimation = createFadeAnimation(true); + mTextFadeOutAnimation = createFadeAnimation(false); + if (!isHorizontal(gravity)) { + mTextInAnimationUp = createTranslateFadeAnimation(true, true); + mTextOutAnimationUp = createTranslateFadeAnimation(false, true); + mTextInAnimationDown = createTranslateFadeAnimation(true, false); + mTextOutAnimationDown = createTranslateFadeAnimation(false, false); + } else { + boolean isLeft = isLeft(gravity, mTextSwitcher.getLayoutDirection()); + mTextInAnimationUp = createRotateFadeAnimation(true, isLeft, true); + mTextOutAnimationUp = createRotateFadeAnimation(false, isLeft, true); + mTextInAnimationDown = createRotateFadeAnimation(true, isLeft, false); + mTextOutAnimationDown = createRotateFadeAnimation(false, isLeft, false); + } + } + + @NonNull + private Animation createFadeAnimation(boolean in) { + AnimationSet set = new AnimationSet(true); + AlphaAnimation alphaAnimation = new AlphaAnimation(in ? 0 : 1, in ? 1 : 0); + alphaAnimation.setDuration(DURATION_MILLIS); + set.addAnimation(new RotateAnimation(0, 0)); // Undo any previous rotation + set.addAnimation(alphaAnimation); + return set; + } + + @NonNull + private Animation createTranslateFadeAnimation(boolean in, boolean up) { + AnimationSet set = new AnimationSet(true); + set.setInterpolator(ACCELERATE_DECELERATE_INTERPOLATOR); + set.setDuration(DURATION_MILLIS); + int fromYDelta = in ? (up ? 1 : -1) : 0; + int toYDelta = in ? 0 : (up ? -1 : 1); + set.addAnimation( + new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 0, + Animation.RELATIVE_TO_SELF, fromYDelta, Animation.RELATIVE_TO_SELF, + toYDelta)); + set.addAnimation(new AlphaAnimation(in ? 0 : 1, in ? 1 : 0)); + return set; + } + + @NonNull + private Animation createRotateFadeAnimation(boolean in, boolean isLeft, boolean up) { + AnimationSet set = new AnimationSet(true); + set.setInterpolator(DECELERATE_INTERPOLATOR); + set.setDuration(DURATION_MILLIS); + + float degrees = isLeft == up ? -ROTATION_DEGREES : ROTATION_DEGREES; + int pivotX = isLeft ? -mPivotOffset : mParent.getWidth() + mPivotOffset; + set.addAnimation( + new RotateAnimation(in ? -degrees : 0f, in ? 0f : degrees, Animation.ABSOLUTE, + pivotX, Animation.ABSOLUTE, 0f)); + set.addAnimation(new AlphaAnimation(in ? 0 : 1, in ? 1 : 0)); + return set; + } +} diff --git a/packages/ExtServices/src/android/ext/services/notification/Assistant.java b/packages/ExtServices/src/android/ext/services/notification/Assistant.java index fdd6a9caefa3..a539b1f2ba60 100644 --- a/packages/ExtServices/src/android/ext/services/notification/Assistant.java +++ b/packages/ExtServices/src/android/ext/services/notification/Assistant.java @@ -19,7 +19,9 @@ package android.ext.services.notification; import static android.app.NotificationManager.IMPORTANCE_MIN; import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE; +import android.annotation.NonNull; import android.app.INotificationManager; +import android.app.Notification; import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; @@ -80,6 +82,7 @@ public class Assistant extends NotificationAssistantService { private float mDismissToViewRatioLimit; private int mStreakLimit; + private SmartActionsHelper mSmartActionsHelper; // key : impressions tracker // TODO: prune deleted channels and apps @@ -99,6 +102,7 @@ public class Assistant extends NotificationAssistantService { // Contexts are correctly hooked up by the creation step, which is required for the observer // to be hooked up/initialized. new SettingsObserver(mHandler); + mSmartActionsHelper = new SmartActionsHelper(); } private void loadFile() { @@ -187,7 +191,26 @@ public class Assistant extends NotificationAssistantService { @Override public Adjustment onNotificationEnqueued(StatusBarNotification sbn) { if (DEBUG) Log.i(TAG, "ENQUEUED " + sbn.getKey()); - return null; + ArrayList<Notification.Action> actions = + mSmartActionsHelper.suggestActions(this, sbn); + if (actions.isEmpty()) { + return null; + } + return createEnqueuedNotificationAdjustment(sbn, actions); + } + + /** A convenience helper for creating an adjustment for an SBN. */ + private Adjustment createEnqueuedNotificationAdjustment( + @NonNull StatusBarNotification statusBarNotification, + @NonNull ArrayList<Notification.Action> smartActions) { + Bundle signals = new Bundle(); + signals.putParcelableArrayList(Adjustment.KEY_SMART_ACTIONS, smartActions); + return new Adjustment( + statusBarNotification.getPackageName(), + statusBarNotification.getKey(), + signals, + "smart action" /* explanation */, + statusBarNotification.getUserId()); } @Override @@ -378,4 +401,4 @@ public class Assistant extends NotificationAssistantService { } } } -}
\ No newline at end of file +} diff --git a/packages/ExtServices/src/android/ext/services/notification/SmartActionsHelper.java b/packages/ExtServices/src/android/ext/services/notification/SmartActionsHelper.java new file mode 100644 index 000000000000..1754461b844d --- /dev/null +++ b/packages/ExtServices/src/android/ext/services/notification/SmartActionsHelper.java @@ -0,0 +1,202 @@ +/** + * 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.ext.services.notification; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.Notification; +import android.app.RemoteAction; +import android.content.Context; +import android.os.Bundle; +import android.os.Parcelable; +import android.service.notification.StatusBarNotification; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.view.textclassifier.TextClassification; +import android.view.textclassifier.TextClassificationManager; +import android.view.textclassifier.TextClassifier; +import android.view.textclassifier.TextLinks; + +import com.android.internal.util.Preconditions; + +import java.util.ArrayList; +import java.util.Collections; + +public class SmartActionsHelper { + private static final ArrayList<Notification.Action> EMPTY_LIST = new ArrayList<>(); + + // If a notification has any of these flags set, it's inelgibile for actions being added. + private static final int FLAG_MASK_INELGIBILE_FOR_ACTIONS = + Notification.FLAG_ONGOING_EVENT + | Notification.FLAG_FOREGROUND_SERVICE + | Notification.FLAG_GROUP_SUMMARY + | Notification.FLAG_NO_CLEAR; + private static final int MAX_ACTION_EXTRACTION_TEXT_LENGTH = 400; + private static final int MAX_ACTIONS_PER_LINK = 1; + private static final int MAX_SMART_ACTIONS = Notification.MAX_ACTION_BUTTONS; + + SmartActionsHelper() {} + + /** + * Adds action adjustments based on the notification contents. + * + * TODO: Once we have a API in {@link TextClassificationManager} to predict smart actions + * from notification text / message, we can replace most of the code here by consuming that API. + */ + @NonNull + ArrayList<Notification.Action> suggestActions( + @Nullable Context context, @NonNull StatusBarNotification sbn) { + if (!isEligibleForActionAdjustment(sbn)) { + return EMPTY_LIST; + } + if (context == null) { + return EMPTY_LIST; + } + TextClassificationManager tcm = context.getSystemService(TextClassificationManager.class); + if (tcm == null) { + return EMPTY_LIST; + } + Notification.Action[] actions = sbn.getNotification().actions; + int numOfExistingActions = actions == null ? 0: actions.length; + int maxSmartActions = MAX_SMART_ACTIONS - numOfExistingActions; + return suggestActionsFromText( + tcm, + getMostSalientActionText(sbn.getNotification()), maxSmartActions); + } + + /** + * Returns whether a notification is eligible for action adjustments. + * + * <p>We exclude system notifications, those that get refreshed frequently, or ones that relate + * to fundamental phone functionality where any error would result in a very negative user + * experience. + */ + private boolean isEligibleForActionAdjustment(@NonNull StatusBarNotification sbn) { + Notification notification = sbn.getNotification(); + String pkg = sbn.getPackageName(); + if (notification.actions != null + && notification.actions.length >= Notification.MAX_ACTION_BUTTONS) { + return false; + } + if (0 != (notification.flags & FLAG_MASK_INELGIBILE_FOR_ACTIONS)) { + return false; + } + if (TextUtils.isEmpty(pkg) || pkg.equals("android")) { + return false; + } + // For now, we are only interested in messages. + return Notification.CATEGORY_MESSAGE.equals(notification.category) + || Notification.MessagingStyle.class.equals(notification.getNotificationStyle()); + } + + /** Returns the text most salient for action extraction in a notification. */ + @Nullable + private CharSequence getMostSalientActionText(@NonNull Notification notification) { + /* If it's messaging style, use the most recent message. */ + Parcelable[] messages = notification.extras.getParcelableArray(Notification.EXTRA_MESSAGES); + if (messages != null && messages.length != 0) { + Bundle lastMessage = (Bundle) messages[messages.length - 1]; + CharSequence lastMessageText = + lastMessage.getCharSequence(Notification.MessagingStyle.Message.KEY_TEXT); + if (!TextUtils.isEmpty(lastMessageText)) { + return lastMessageText; + } + } + + // Fall back to using the normal text. + return notification.extras.getCharSequence(Notification.EXTRA_TEXT); + } + + /** Returns a list of actions to act on entities in a given piece of text. */ + @NonNull + private ArrayList<Notification.Action> suggestActionsFromText( + @NonNull TextClassificationManager tcm, @Nullable CharSequence text, + int maxSmartActions) { + if (TextUtils.isEmpty(text)) { + return EMPTY_LIST; + } + TextClassifier textClassifier = tcm.getTextClassifier(); + + // We want to process only text visible to the user to avoid confusing suggestions, so we + // truncate the text to a reasonable length. This is particularly important for e.g. + // email apps that sometimes include the text for the entire thread. + text = text.subSequence(0, Math.min(text.length(), MAX_ACTION_EXTRACTION_TEXT_LENGTH)); + + // Extract all entities. + TextLinks.Request textLinksRequest = new TextLinks.Request.Builder(text) + .setEntityConfig( + TextClassifier.EntityConfig.createWithHints( + Collections.singletonList( + TextClassifier.HINT_TEXT_IS_NOT_EDITABLE))) + .build(); + TextLinks links = textClassifier.generateLinks(textLinksRequest); + ArrayMap<String, Integer> entityTypeFrequency = getEntityTypeFrequency(links); + + ArrayList<Notification.Action> actions = new ArrayList<>(); + for (TextLinks.TextLink link : links.getLinks()) { + // Ignore any entity type for which we have too many entities. This is to handle the + // case where a notification contains e.g. a list of phone numbers. In such cases, the + // user likely wants to act on the whole list rather than an individual entity. + if (link.getEntityCount() == 0 + || entityTypeFrequency.get(link.getEntity(0)) != 1) { + continue; + } + + // Generate the actions, and add the most prominent ones to the action bar. + TextClassification classification = + textClassifier.classifyText( + new TextClassification.Request.Builder( + text, link.getStart(), link.getEnd()).build()); + int numOfActions = Math.min( + MAX_ACTIONS_PER_LINK, classification.getActions().size()); + for (int i = 0; i < numOfActions; ++i) { + RemoteAction action = classification.getActions().get(i); + actions.add( + new Notification.Action.Builder( + action.getIcon(), + action.getTitle(), + action.getActionIntent()) + .build()); + // We have enough smart actions. + if (actions.size() >= maxSmartActions) { + return actions; + } + } + } + return actions; + } + + /** + * Given the links extracted from a piece of text, returns the frequency of each entity + * type. + */ + @NonNull + private ArrayMap<String, Integer> getEntityTypeFrequency(@NonNull TextLinks links) { + ArrayMap<String, Integer> entityTypeCount = new ArrayMap<>(); + for (TextLinks.TextLink link : links.getLinks()) { + if (link.getEntityCount() == 0) { + continue; + } + String entityType = link.getEntity(0); + if (entityTypeCount.containsKey(entityType)) { + entityTypeCount.put(entityType, entityTypeCount.get(entityType) + 1); + } else { + entityTypeCount.put(entityType, 1); + } + } + return entityTypeCount; + } +} diff --git a/packages/PrintSpooler/res/drawable-hdpi/ic_expand_less.png b/packages/PrintSpooler/res/drawable-hdpi/ic_expand_less.png Binary files differdeleted file mode 100644 index b6a5eb5fa48c..000000000000 --- a/packages/PrintSpooler/res/drawable-hdpi/ic_expand_less.png +++ /dev/null diff --git a/packages/PrintSpooler/res/drawable-hdpi/ic_expand_more.png b/packages/PrintSpooler/res/drawable-hdpi/ic_expand_more.png Binary files differdeleted file mode 100644 index 4e36bd29db9e..000000000000 --- a/packages/PrintSpooler/res/drawable-hdpi/ic_expand_more.png +++ /dev/null diff --git a/packages/PrintSpooler/res/drawable-hdpi/ic_grayedout_printer.png b/packages/PrintSpooler/res/drawable-hdpi/ic_grayedout_printer.png Binary files differdeleted file mode 100644 index 5e54970d6b3f..000000000000 --- a/packages/PrintSpooler/res/drawable-hdpi/ic_grayedout_printer.png +++ /dev/null diff --git a/packages/PrintSpooler/res/drawable-hdpi/ic_restart.png b/packages/PrintSpooler/res/drawable-hdpi/ic_restart.png Binary files differdeleted file mode 100644 index bb9d855f7769..000000000000 --- a/packages/PrintSpooler/res/drawable-hdpi/ic_restart.png +++ /dev/null diff --git a/packages/PrintSpooler/res/drawable-hdpi/stat_notify_cancelling.png b/packages/PrintSpooler/res/drawable-hdpi/stat_notify_cancelling.png Binary files differdeleted file mode 100644 index 2757db05d657..000000000000 --- a/packages/PrintSpooler/res/drawable-hdpi/stat_notify_cancelling.png +++ /dev/null diff --git a/packages/PrintSpooler/res/drawable-mdpi/ic_expand_less.png b/packages/PrintSpooler/res/drawable-mdpi/ic_expand_less.png Binary files differdeleted file mode 100644 index 428a94663194..000000000000 --- a/packages/PrintSpooler/res/drawable-mdpi/ic_expand_less.png +++ /dev/null diff --git a/packages/PrintSpooler/res/drawable-mdpi/ic_expand_less_24dp.png b/packages/PrintSpooler/res/drawable-mdpi/ic_expand_less_24dp.png Binary files differdeleted file mode 100644 index 3220eeafa859..000000000000 --- a/packages/PrintSpooler/res/drawable-mdpi/ic_expand_less_24dp.png +++ /dev/null diff --git a/packages/PrintSpooler/res/drawable-mdpi/ic_expand_more.png b/packages/PrintSpooler/res/drawable-mdpi/ic_expand_more.png Binary files differdeleted file mode 100644 index fbbd094bdc53..000000000000 --- a/packages/PrintSpooler/res/drawable-mdpi/ic_expand_more.png +++ /dev/null diff --git a/packages/PrintSpooler/res/drawable-mdpi/ic_expand_more_24dp.png b/packages/PrintSpooler/res/drawable-mdpi/ic_expand_more_24dp.png Binary files differdeleted file mode 100644 index 5530f52e6b0c..000000000000 --- a/packages/PrintSpooler/res/drawable-mdpi/ic_expand_more_24dp.png +++ /dev/null diff --git a/packages/PrintSpooler/res/drawable-mdpi/ic_grayedout_printer.png b/packages/PrintSpooler/res/drawable-mdpi/ic_grayedout_printer.png Binary files differdeleted file mode 100644 index 5e54970d6b3f..000000000000 --- a/packages/PrintSpooler/res/drawable-mdpi/ic_grayedout_printer.png +++ /dev/null diff --git a/packages/PrintSpooler/res/drawable-mdpi/ic_restart.png b/packages/PrintSpooler/res/drawable-mdpi/ic_restart.png Binary files differdeleted file mode 100644 index bd611e8e24d2..000000000000 --- a/packages/PrintSpooler/res/drawable-mdpi/ic_restart.png +++ /dev/null diff --git a/packages/PrintSpooler/res/drawable-mdpi/stat_notify_cancelling.png b/packages/PrintSpooler/res/drawable-mdpi/stat_notify_cancelling.png Binary files differdeleted file mode 100644 index c1b380a7778f..000000000000 --- a/packages/PrintSpooler/res/drawable-mdpi/stat_notify_cancelling.png +++ /dev/null diff --git a/packages/PrintSpooler/res/drawable-xhdpi/ic_expand_less.png b/packages/PrintSpooler/res/drawable-xhdpi/ic_expand_less.png Binary files differdeleted file mode 100644 index 6161c209482d..000000000000 --- a/packages/PrintSpooler/res/drawable-xhdpi/ic_expand_less.png +++ /dev/null diff --git a/packages/PrintSpooler/res/drawable-xhdpi/ic_expand_more.png b/packages/PrintSpooler/res/drawable-xhdpi/ic_expand_more.png Binary files differdeleted file mode 100644 index 3a89805d503a..000000000000 --- a/packages/PrintSpooler/res/drawable-xhdpi/ic_expand_more.png +++ /dev/null diff --git a/packages/PrintSpooler/res/drawable-xhdpi/ic_grayedout_printer.png b/packages/PrintSpooler/res/drawable-xhdpi/ic_grayedout_printer.png Binary files differdeleted file mode 100644 index 5e54970d6b3f..000000000000 --- a/packages/PrintSpooler/res/drawable-xhdpi/ic_grayedout_printer.png +++ /dev/null diff --git a/packages/PrintSpooler/res/drawable-xhdpi/ic_restart.png b/packages/PrintSpooler/res/drawable-xhdpi/ic_restart.png Binary files differdeleted file mode 100644 index a7fdc0dfcb1d..000000000000 --- a/packages/PrintSpooler/res/drawable-xhdpi/ic_restart.png +++ /dev/null diff --git a/packages/PrintSpooler/res/drawable-xhdpi/stat_notify_cancelling.png b/packages/PrintSpooler/res/drawable-xhdpi/stat_notify_cancelling.png Binary files differdeleted file mode 100644 index fedc00e22e2a..000000000000 --- a/packages/PrintSpooler/res/drawable-xhdpi/stat_notify_cancelling.png +++ /dev/null diff --git a/packages/PrintSpooler/res/drawable-xxhdpi/ic_expand_less.png b/packages/PrintSpooler/res/drawable-xxhdpi/ic_expand_less.png Binary files differdeleted file mode 100644 index 52a52d9fa7a6..000000000000 --- a/packages/PrintSpooler/res/drawable-xxhdpi/ic_expand_less.png +++ /dev/null diff --git a/packages/PrintSpooler/res/drawable-xxhdpi/ic_expand_more.png b/packages/PrintSpooler/res/drawable-xxhdpi/ic_expand_more.png Binary files differdeleted file mode 100644 index 15e6abd447fd..000000000000 --- a/packages/PrintSpooler/res/drawable-xxhdpi/ic_expand_more.png +++ /dev/null diff --git a/packages/PrintSpooler/res/drawable-xxxhdpi/ic_expand_less.png b/packages/PrintSpooler/res/drawable-xxxhdpi/ic_expand_less.png Binary files differdeleted file mode 100644 index 46811a1269ad..000000000000 --- a/packages/PrintSpooler/res/drawable-xxxhdpi/ic_expand_less.png +++ /dev/null diff --git a/packages/PrintSpooler/res/drawable-xxxhdpi/ic_expand_more.png b/packages/PrintSpooler/res/drawable-xxxhdpi/ic_expand_more.png Binary files differdeleted file mode 100644 index 141f28b26264..000000000000 --- a/packages/PrintSpooler/res/drawable-xxxhdpi/ic_expand_more.png +++ /dev/null diff --git a/packages/PrintSpooler/res/drawable/ic_clear.xml b/packages/PrintSpooler/res/drawable/ic_clear.xml new file mode 100644 index 000000000000..076e8ef37ad0 --- /dev/null +++ b/packages/PrintSpooler/res/drawable/ic_clear.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" + android:fillColor="?android:colorForeground"/> +</vector> diff --git a/packages/PrintSpooler/res/drawable/ic_expand_less.xml b/packages/PrintSpooler/res/drawable/ic_expand_less.xml index 6f1ece17d393..c3e87dc9c4c3 100644 --- a/packages/PrintSpooler/res/drawable/ic_expand_less.xml +++ b/packages/PrintSpooler/res/drawable/ic_expand_less.xml @@ -1,43 +1,26 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- 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. ---> - -<selector xmlns:android="http://schemas.android.com/apk/res/android" - android:autoMirrored="true"> - - <item - android:state_checked="true"> - <bitmap - android:src="@drawable/ic_expand_less" - android:tint="?android:attr/colorControlActivated"> - </bitmap> - </item> - - <item - android:state_pressed="true"> - <bitmap - android:src="@drawable/ic_expand_less" - android:tint="?android:attr/colorControlActivated"> - </bitmap> - </item> - - <item> - <bitmap - android:src="@drawable/ic_expand_less" - android:tint="?android:attr/colorControlNormal"> - </bitmap> - </item> - -</selector> +<!-- + 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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:pathData="M7.41,15.41L12,10.83l4.59,4.58L18,14l-6,-6 -6,6z" + android:fillColor="?android:colorForeground" /> +</vector>
\ No newline at end of file diff --git a/packages/PrintSpooler/res/drawable/ic_expand_more.xml b/packages/PrintSpooler/res/drawable/ic_expand_more.xml index 8d7145278988..3895144a5c1c 100644 --- a/packages/PrintSpooler/res/drawable/ic_expand_more.xml +++ b/packages/PrintSpooler/res/drawable/ic_expand_more.xml @@ -1,43 +1,26 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- 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. ---> - -<selector xmlns:android="http://schemas.android.com/apk/res/android" - android:autoMirrored="true"> - - <item - android:state_checked="true"> - <bitmap - android:src="@drawable/ic_expand_more" - android:tint="?android:attr/colorControlActivated"> - </bitmap> - </item> - - <item - android:state_pressed="true"> - <bitmap - android:src="@drawable/ic_expand_more" - android:tint="?android:attr/colorControlActivated"> - </bitmap> - </item> - - <item> - <bitmap - android:src="@drawable/ic_expand_more" - android:tint="?android:attr/colorControlNormal"> - </bitmap> - </item> - -</selector> +<!-- + 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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:pathData="M7.41,8.59L12,13.17l4.59,-4.58L18,10l-6,6l-6,-6L7.41,8.59z" + android:fillColor="?android:colorForeground" /> +</vector>
\ No newline at end of file diff --git a/packages/PrintSpooler/res/layout/preview_page.xml b/packages/PrintSpooler/res/layout/preview_page.xml index aafdd8fc035c..8db347e3db4f 100644 --- a/packages/PrintSpooler/res/layout/preview_page.xml +++ b/packages/PrintSpooler/res/layout/preview_page.xml @@ -44,7 +44,7 @@ android:layout_height="wrap_content" android:layout_centerInParent="true" android:textAppearance="?android:attr/textAppearanceSmall" - android:textColor="?android:attr/textColorPrimary"> + android:textColor="@android:color/white"> </TextView> <ImageView diff --git a/packages/PrintSpooler/res/layout/preview_page_error.xml b/packages/PrintSpooler/res/layout/preview_page_error.xml index 4e9fb7787010..99ab99d07c40 100644 --- a/packages/PrintSpooler/res/layout/preview_page_error.xml +++ b/packages/PrintSpooler/res/layout/preview_page_error.xml @@ -21,11 +21,14 @@ android:gravity="center"> <ImageView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginBottom="12dip" - android:src="@drawable/print_warning" - android:contentDescription="@null" /> + android:layout_width="120dp" + android:layout_height="110dp" + android:layout_marginBottom="12dip" + android:src="@*android:drawable/ic_print_error" + android:scaleType="fitEnd" + android:alpha="0.1" + android:tint="@android:color/black" + android:importantForAccessibility="no" /> <TextView android:layout_width="wrap_content" diff --git a/packages/PrintSpooler/res/layout/preview_page_loading.xml b/packages/PrintSpooler/res/layout/preview_page_loading.xml index 1af3a17ca227..918edd987a79 100644 --- a/packages/PrintSpooler/res/layout/preview_page_loading.xml +++ b/packages/PrintSpooler/res/layout/preview_page_loading.xml @@ -19,14 +19,13 @@ android:layout_height="fill_parent"> <ImageView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_margin="36dip" + android:layout_width="120dp" + android:layout_height="110dp" android:layout_gravity="center" - android:src="@drawable/ic_grayedout_printer" - android:contentDescription="@null" - android:scaleType="centerInside" - android:adjustViewBounds="true"> - </ImageView> + android:src="@*android:drawable/ic_print" + android:scaleType="fitCenter" + android:alpha="0.1" + android:tint="@android:color/black" + android:importantForAccessibility="no" /> </FrameLayout> diff --git a/packages/PrintSpooler/res/layout/preview_page_selected.xml b/packages/PrintSpooler/res/layout/preview_page_selected.xml index 77f4727434e6..6727142dfc8b 100644 --- a/packages/PrintSpooler/res/layout/preview_page_selected.xml +++ b/packages/PrintSpooler/res/layout/preview_page_selected.xml @@ -42,7 +42,7 @@ android:layout_height="wrap_content" android:layout_centerInParent="true" android:textAppearance="?android:attr/textAppearanceSmall" - android:textColor="?android:attr/textColorPrimary"> + android:textColor="@android:color/white"> </TextView> <ImageView diff --git a/packages/PrintSpooler/res/layout/print_activity.xml b/packages/PrintSpooler/res/layout/print_activity.xml index 774f320d6fd2..212f398aa163 100644 --- a/packages/PrintSpooler/res/layout/print_activity.xml +++ b/packages/PrintSpooler/res/layout/print_activity.xml @@ -29,7 +29,7 @@ android:paddingStart="8dip" android:layout_marginEnd="16dp" android:elevation="@dimen/preview_controls_elevation" - android:background="?android:attr/colorPrimary"> + style="?android:actionBarStyle"> <com.android.printspooler.widget.ClickInterceptSpinner android:id="@+id/destination_spinner" @@ -55,7 +55,7 @@ android:paddingBottom="8dip" android:orientation="horizontal" android:elevation="@dimen/preview_controls_elevation" - android:background="?android:attr/colorPrimary"> + style="?android:actionBarStyle"> <TextView android:layout_width="wrap_content" @@ -121,7 +121,6 @@ android:layout_width="fill_parent" android:layout_height="fill_parent" android:animateLayoutChanges="true" - android:background="@color/print_preview_background_color" android:gravity="center"> <!-- Error message added here --> diff --git a/packages/PrintSpooler/res/layout/print_activity_controls.xml b/packages/PrintSpooler/res/layout/print_activity_controls.xml index 69d4f914a453..3aafc99da454 100644 --- a/packages/PrintSpooler/res/layout/print_activity_controls.xml +++ b/packages/PrintSpooler/res/layout/print_activity_controls.xml @@ -22,7 +22,7 @@ android:layout_height="wrap_content" android:orientation="vertical" android:elevation="@dimen/preview_controls_elevation" - android:background="?android:attr/colorPrimary"> + style="?android:actionBarStyle"> <LinearLayout android:id="@+id/draggable_content" diff --git a/packages/PrintSpooler/res/layout/print_error_fragment.xml b/packages/PrintSpooler/res/layout/print_error_fragment.xml index 3ea2abdb5b5a..9d9dd0100481 100644 --- a/packages/PrintSpooler/res/layout/print_error_fragment.xml +++ b/packages/PrintSpooler/res/layout/print_error_fragment.xml @@ -16,35 +16,39 @@ --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="fill_parent" - android:layout_height="fill_parent" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginStart="16dip" + android:layout_marginEnd="16dip" android:gravity="center" android:orientation="vertical"> <ImageView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginBottom="12dip" - android:src="@drawable/ic_grayedout_printer" - android:contentDescription="@null"> - </ImageView> + android:layout_width="120dp" + android:layout_height="110dp" + android:layout_marginBottom="12dp" + android:src="@*android:drawable/ic_print_error" + android:scaleType="fitEnd" + android:alpha="0.1" + android:tint="?android:colorForeground" + android:importantForAccessibility="no" /> <TextView android:id="@+id/message" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="16dip" - android:layout_marginEnd="16dip" - android:gravity="center_horizontal" - android:text="@string/print_error_default_message" - android:textAppearance="?android:attr/textAppearanceLargeInverse"> - </TextView> + android:layout_marginBottom="16dp" + android:gravity="center" + android:textAppearance="?android:attr/textAppearanceLarge" + android:textColor="?android:attr/textColorSecondary" + android:text="@string/print_error_default_message" /> <Button android:id="@+id/action_button" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@string/print_error_retry"> - </Button> + style="?android:attr/borderlessButtonStyle" + android:textColor="?android:attr/textColorSecondary" + android:text="@string/print_add_printer" /> </LinearLayout> diff --git a/packages/PrintSpooler/res/layout/print_progress_fragment.xml b/packages/PrintSpooler/res/layout/print_progress_fragment.xml index 3b010f813e06..890716051d55 100644 --- a/packages/PrintSpooler/res/layout/print_progress_fragment.xml +++ b/packages/PrintSpooler/res/layout/print_progress_fragment.xml @@ -15,34 +15,39 @@ --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" android:gravity="center" android:orientation="vertical"> <ImageView + android:layout_width="120dp" + android:layout_height="110dp" + android:layout_marginBottom="12dp" + android:src="@*android:drawable/ic_print" + android:scaleType="fitEnd" + android:alpha="0.1" + android:tint="?android:colorForeground" + android:importantForAccessibility="no" /> + + <TextView + android:id="@+id/message" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginBottom="12dip" - android:src="@drawable/ic_grayedout_printer" - android:contentDescription="@null"> - </ImageView> + android:layout_marginBottom="16dp" + android:gravity="center" + android:textAppearance="?android:attr/textAppearanceLarge" + android:textColor="?android:attr/textColorSecondary" + android:text="@string/print_preparing_preview" /> <ProgressBar - android:layout_width="fill_parent" + android:layout_width="300dp" android:layout_height="wrap_content" android:indeterminate="true" - style="?android:attr/progressBarStyleHorizontal"> - </ProgressBar> - - <TextView - android:id="@+id/message" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textAppearance="?android:attr/textAppearanceLargeInverse" - android:text="@string/print_preparing_preview"> - </TextView> + android:importantForAccessibility="no" + style="?android:attr/progressBarStyleHorizontal" /> </LinearLayout> diff --git a/packages/PrintSpooler/res/layout/select_printer_activity.xml b/packages/PrintSpooler/res/layout/select_printer_activity.xml index 91beff6993d2..681924b334d0 100644 --- a/packages/PrintSpooler/res/layout/select_printer_activity.xml +++ b/packages/PrintSpooler/res/layout/select_printer_activity.xml @@ -31,45 +31,48 @@ android:visibility="gone"> <LinearLayout - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" android:gravity="center" android:orientation="vertical"> <ImageView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginBottom="12dip" - android:src="@*android:drawable/ic_grayedout_printer" - android:importantForAccessibility="no"> - </ImageView> + android:layout_width="120dp" + android:layout_height="110dp" + android:layout_marginBottom="12dp" + android:src="@*android:drawable/ic_print" + android:scaleType="fitEnd" + android:alpha="0.1" + android:tint="?android:colorForeground" + android:importantForAccessibility="no" /> <TextView android:id="@+id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginBottom="16dp" + android:gravity="center" android:textAppearance="?android:attr/textAppearanceLarge" android:textColor="?android:attr/textColorSecondary" - android:text="@string/print_searching_for_printers"> - </TextView> + android:text="@string/print_searching_for_printers" /> <ProgressBar android:id="@+id/progress_bar" - android:layout_width="fill_parent" + android:layout_width="300dp" android:layout_height="wrap_content" android:indeterminate="true" - style="?android:attr/progressBarStyleHorizontal"> - </ProgressBar> + android:importantForAccessibility="no" + style="?android:attr/progressBarStyleHorizontal" /> <Button - android:id="@+id/button" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - style="?android:attr/buttonBarButtonStyle" - android:textAppearance="?android:attr/textAppearanceSmall" - android:text="@string/print_add_printer" - android:textAllCaps="true" /> + android:id="@+id/button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + style="?android:attr/borderlessButtonStyle" + android:textColor="?android:attr/textColorSecondary" + android:text="@string/print_add_printer" /> </LinearLayout> diff --git a/packages/PrintSpooler/res/values/colors.xml b/packages/PrintSpooler/res/values/colors.xml index 68bc6f277ad2..cb9e8866a396 100644 --- a/packages/PrintSpooler/res/values/colors.xml +++ b/packages/PrintSpooler/res/values/colors.xml @@ -18,8 +18,6 @@ <color name="print_preview_scrim_color">#99000000</color> - <color name="print_preview_background_color">#F2F1F2</color> - <color name="unselected_page_background_color">#C0C0C0</color> <color name="material_grey_500">#ffa3a3a3</color> diff --git a/packages/PrintSpooler/res/values/themes.xml b/packages/PrintSpooler/res/values/themes.xml index a968ffa9ed1d..844e9c9f0030 100644 --- a/packages/PrintSpooler/res/values/themes.xml +++ b/packages/PrintSpooler/res/values/themes.xml @@ -21,17 +21,14 @@ </style> <style name="Theme.SelectPrinterActivity" - parent="android:style/Theme.DeviceDefault.Light.DarkActionBar"> + parent="android:style/Theme.DeviceDefault.Light"> <item name="android:textAppearanceListItemSecondary">@style/ListItemSecondary</item> </style> - <style name="Theme.PrintActivity" parent="@android:style/Theme.DeviceDefault"> + <style name="Theme.PrintActivity" parent="@android:style/Theme.DeviceDefault.Light"> <item name="android:windowIsTranslucent">true</item> - <item name="android:windowBackground">@android:color/transparent</item> - <item name="android:windowContentOverlay">@null</item> <item name="android:windowActionBar">false</item> <item name="android:windowNoTitle">true</item> - <item name="android:backgroundDimEnabled">false</item> </style> </resources> diff --git a/packages/PrintSpooler/src/com/android/printspooler/model/NotificationController.java b/packages/PrintSpooler/src/com/android/printspooler/model/NotificationController.java index 9d737e0cd9d5..abdfad5bb27a 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/model/NotificationController.java +++ b/packages/PrintSpooler/src/com/android/printspooler/model/NotificationController.java @@ -166,7 +166,7 @@ final class NotificationController { */ private Action createCancelAction(PrintJobInfo printJob) { return new Action.Builder( - Icon.createWithResource(mContext, R.drawable.stat_notify_cancelling), + Icon.createWithResource(mContext, R.drawable.ic_clear), mContext.getString(R.string.cancel), createCancelIntent(printJob)).build(); } @@ -225,7 +225,7 @@ final class NotificationController { private void createFailedNotification(PrintJobInfo printJob) { Action.Builder restartActionBuilder = new Action.Builder( - Icon.createWithResource(mContext, R.drawable.ic_restart), + Icon.createWithResource(mContext, com.android.internal.R.drawable.ic_restart), mContext.getString(R.string.restart), createRestartIntent(printJob.getId())); createNotification(printJob, createCancelAction(printJob), restartActionBuilder.build()); @@ -317,7 +317,7 @@ final class NotificationController { if (!printJob.isCancelling()) { return com.android.internal.R.drawable.ic_print; } else { - return R.drawable.stat_notify_cancelling; + return R.drawable.ic_clear; } } } diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java index 06fbf9f6e1c1..59f272ff70f6 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java +++ b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java @@ -297,7 +297,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat + " cannot be null"); } - mCallingPackageName = extras.getString(DocumentsContract.EXTRA_PACKAGE_NAME); + mCallingPackageName = extras.getString(Intent.EXTRA_PACKAGE_NAME); if (savedInstanceState == null) { MetricsLogger.action(this, MetricsEvent.PRINT_PREVIEW, mCallingPackageName); @@ -715,7 +715,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); intent.setType("application/pdf"); intent.putExtra(Intent.EXTRA_TITLE, info.getName()); - intent.putExtra(DocumentsContract.EXTRA_PACKAGE_NAME, mCallingPackageName); + intent.putExtra(Intent.EXTRA_PACKAGE_NAME, mCallingPackageName); try { startActivityForResult(intent, ACTIVITY_REQUEST_CREATE_FILE); diff --git a/packages/SettingsLib/res/values-ca/strings.xml b/packages/SettingsLib/res/values-ca/strings.xml index 2488f55ad6bc..50d9c17bec23 100644 --- a/packages/SettingsLib/res/values-ca/strings.xml +++ b/packages/SettingsLib/res/values-ca/strings.xml @@ -398,8 +398,8 @@ <string name="enabled_by_admin" msgid="5302986023578399263">"Activada per l\'administrador"</string> <string name="disabled_by_admin" msgid="8505398946020816620">"Desactivada per l\'administrador"</string> <string name="disabled" msgid="9206776641295849915">"Desactivat"</string> - <string name="external_source_trusted" msgid="2707996266575928037">"Permeses"</string> - <string name="external_source_untrusted" msgid="2677442511837596726">"No permeses"</string> + <string name="external_source_trusted" msgid="2707996266575928037">"Amb permís"</string> + <string name="external_source_untrusted" msgid="2677442511837596726">"Sense permís"</string> <string name="install_other_apps" msgid="6986686991775883017">"Instal·lar aplicacions desconegudes"</string> <string name="home" msgid="3256884684164448244">"Pàgina d\'inici de configuració"</string> <string-array name="battery_labels"> diff --git a/packages/SettingsLib/res/values-ne/strings.xml b/packages/SettingsLib/res/values-ne/strings.xml index 3e2afed2d11f..ca6fe8829406 100644 --- a/packages/SettingsLib/res/values-ne/strings.xml +++ b/packages/SettingsLib/res/values-ne/strings.xml @@ -320,7 +320,7 @@ <string name="show_notification_channel_warnings_summary" msgid="5536803251863694895">"अनुप्रयोगले कुनै मान्य च्यानल बिना सूचना पोस्ट गर्दा स्क्रिनमा चेतावनी देखाउँछ"</string> <string name="force_allow_on_external" msgid="3215759785081916381">"बाह्यमा बल प्रयोगको अनुमति प्राप्त अनुप्रयोगहरू"</string> <string name="force_allow_on_external_summary" msgid="3640752408258034689">"म्यानिफेेस्टका मानहरूको ख्याल नगरी कुनै पनि अनुप्रयोगलाई बाह्य भण्डारणमा लेख्न सकिने खाले बनाउँछ"</string> - <string name="force_resizable_activities" msgid="8615764378147824985">"गतिविधिहरू रिसाइज गर्नको लागि बाध्य गर्नुहोस्"</string> + <string name="force_resizable_activities" msgid="8615764378147824985">"आकार बदल्न योग्य हुने बनाउन गतिविधिहरूलाई बाध्यात्मक बनाउनुहोस्।"</string> <string name="force_resizable_activities_summary" msgid="6667493494706124459">"म्यानिफेेस्ट मानहरूको ख्याल नगरी, बहु-विन्डोको लागि सबै रिसाइज गर्न सकिने गतिविधिहरू बनाउनुहोस्।"</string> <string name="enable_freeform_support" msgid="1461893351278940416">"फ्रिफर्म विन्डोहरू सक्रिय गर्नुहोस्"</string> <string name="enable_freeform_support_summary" msgid="8247310463288834487">"प्रयोगात्मक फ्रिफर्म विन्डोहरूका लागि समर्थन सक्रिय गर्नुहोस्।"</string> diff --git a/packages/SettingsLib/res/values-ta/arrays.xml b/packages/SettingsLib/res/values-ta/arrays.xml index 7b69c7859802..54adf826076c 100644 --- a/packages/SettingsLib/res/values-ta/arrays.xml +++ b/packages/SettingsLib/res/values-ta/arrays.xml @@ -22,7 +22,7 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string-array name="wifi_status"> <item msgid="1922181315419294640"></item> - <item msgid="8934131797783724664">"ஸ்கேன் செய்கிறது…"</item> + <item msgid="8934131797783724664">"தேடுகிறது..."</item> <item msgid="8513729475867537913">"இணைக்கிறது..."</item> <item msgid="515055375277271756">"அங்கீகரிக்கிறது..."</item> <item msgid="1943354004029184381">"IP முகவரியைப் பெறுகிறது…"</item> @@ -36,7 +36,7 @@ </string-array> <string-array name="wifi_status_with_ssid"> <item msgid="7714855332363650812"></item> - <item msgid="8878186979715711006">"ஸ்கேன் செய்கிறது…"</item> + <item msgid="8878186979715711006">"தேடுகிறது..."</item> <item msgid="355508996603873860">"<xliff:g id="NETWORK_NAME">%1$s</xliff:g> இல் இணைக்கிறது…"</item> <item msgid="554971459996405634">"<xliff:g id="NETWORK_NAME">%1$s</xliff:g> உடன் அங்கீகரிக்கிறது…"</item> <item msgid="7928343808033020343">"<xliff:g id="NETWORK_NAME">%1$s</xliff:g> இலிருந்து IP முகவரியைப் பெறுகிறது…"</item> @@ -180,30 +180,30 @@ </string-array> <string-array name="window_animation_scale_entries"> <item msgid="8134156599370824081">"அனிமேஷனை முடக்கு"</item> - <item msgid="6624864048416710414">"அனிமேஷன் அளவு .5x"</item> - <item msgid="2219332261255416635">"அனிமேஷன் அளவு 1x"</item> - <item msgid="3544428804137048509">"அனிமேஷன் அளவு 1.5x"</item> - <item msgid="3110710404225974514">"அனிமேஷன் அளவு 2x"</item> - <item msgid="4402738611528318731">"அனிமேஷன் அளவு 5x"</item> - <item msgid="6189539267968330656">"அனிமேஷன் அளவு 10x"</item> + <item msgid="6624864048416710414">"அனிமேஷன் வேகம் .5x"</item> + <item msgid="2219332261255416635">"அனிமேஷன் வேகம் 1x"</item> + <item msgid="3544428804137048509">"அனிமேஷன் வேகம் 1.5x"</item> + <item msgid="3110710404225974514">"அனிமேஷன் வேகம் 2x"</item> + <item msgid="4402738611528318731">"அனிமேஷன் வேகம் 5x"</item> + <item msgid="6189539267968330656">"அனிமேஷன் வேகம் 10x"</item> </string-array> <string-array name="transition_animation_scale_entries"> <item msgid="8464255836173039442">"அனிமேஷனை முடக்கு"</item> - <item msgid="3375781541913316411">"அனிமேஷன் அளவு .5x"</item> - <item msgid="1991041427801869945">"அனிமேஷன் அளவு 1x"</item> - <item msgid="4012689927622382874">"அனிமேஷன் அளவு 1.5x"</item> - <item msgid="3289156759925947169">"அனிமேஷன் அளவு 2x"</item> - <item msgid="7705857441213621835">"அனிமேஷன் அளவு 5x"</item> - <item msgid="6660750935954853365">"அனிமேஷன் அளவு 10x"</item> + <item msgid="3375781541913316411">"அனிமேஷன் வேகம் .5x"</item> + <item msgid="1991041427801869945">"அனிமேஷன் வேகம் 1x"</item> + <item msgid="4012689927622382874">"அனிமேஷன் வேகம் 1.5x"</item> + <item msgid="3289156759925947169">"அனிமேஷன் வேகம் 2x"</item> + <item msgid="7705857441213621835">"அனிமேஷன் வேகம் 5x"</item> + <item msgid="6660750935954853365">"அனிமேஷன் வேகம் 10x"</item> </string-array> <string-array name="animator_duration_scale_entries"> <item msgid="6039901060648228241">"அனிமேஷனை முடக்கு"</item> - <item msgid="1138649021950863198">"அனிமேஷன் அளவு .5x"</item> - <item msgid="4394388961370833040">"அனிமேஷன் அளவு 1x"</item> - <item msgid="8125427921655194973">"அனிமேஷன் அளவு 1.5x"</item> - <item msgid="3334024790739189573">"அனிமேஷன் அளவு 2x"</item> - <item msgid="3170120558236848008">"அனிமேஷன் அளவு 5x"</item> - <item msgid="1069584980746680398">"அனிமேஷன் அளவு 10x"</item> + <item msgid="1138649021950863198">"அனிமேஷன் வேகம் .5x"</item> + <item msgid="4394388961370833040">"அனிமேஷன் வேகம் 1x"</item> + <item msgid="8125427921655194973">"அனிமேஷன் வேகம் 1.5x"</item> + <item msgid="3334024790739189573">"அனிமேஷன் வேகம் 2x"</item> + <item msgid="3170120558236848008">"அனிமேஷன் வேகம் 5x"</item> + <item msgid="1069584980746680398">"அனிமேஷன் வேகம் 10x"</item> </string-array> <string-array name="overlay_display_devices_entries"> <item msgid="1606809880904982133">"ஏதுமில்லை"</item> diff --git a/packages/SettingsLib/res/values-ta/strings.xml b/packages/SettingsLib/res/values-ta/strings.xml index 828e5427ec37..5f63fee33b43 100644 --- a/packages/SettingsLib/res/values-ta/strings.xml +++ b/packages/SettingsLib/res/values-ta/strings.xml @@ -306,8 +306,8 @@ <string name="track_frame_time" msgid="6094365083096851167">"சுயவிவர HWUI ரெண்டரிங்"</string> <string name="enable_gpu_debug_layers" msgid="3848838293793255097">"GPU பிழைத்திருத்த லேயர்களை இயக்கு"</string> <string name="enable_gpu_debug_layers_summary" msgid="8009136940671194940">"பிழைத்திருத்த ஆப்ஸிற்கு, GPU பிழைத்திருத்த லேயர்களை ஏற்றுவதற்கு அனுமதி"</string> - <string name="window_animation_scale_title" msgid="6162587588166114700">"சாளர அனிமேஷன் அளவு"</string> - <string name="transition_animation_scale_title" msgid="387527540523595875">"அனிமேஷன் மாற்றத்தின் அளவு"</string> + <string name="window_animation_scale_title" msgid="6162587588166114700">"சாளர அனிமேஷன் வேகம்"</string> + <string name="transition_animation_scale_title" msgid="387527540523595875">"அனிமேஷன் மாற்றத்தின் வேகம்"</string> <string name="animator_duration_scale_title" msgid="3406722410819934083">"அனிமேட்டர் கால அளவு"</string> <string name="overlay_display_devices_title" msgid="5364176287998398539">"இரண்டாம்நிலைக் காட்சிகளை உருவகப்படுத்து"</string> <string name="debug_applications_category" msgid="4206913653849771549">"ஆப்ஸ்"</string> @@ -358,7 +358,7 @@ <string name="picture_color_mode" msgid="4560755008730283695">"படத்தின் வண்ணப் பயன்முறை"</string> <string name="picture_color_mode_desc" msgid="1141891467675548590">"sRGBஐப் பயன்படுத்தும்"</string> <string name="daltonizer_mode_disabled" msgid="7482661936053801862">"முடக்கப்பட்டது"</string> - <string name="daltonizer_mode_monochromacy" msgid="8485709880666106721">"மோனோகுரோமசி"</string> + <string name="daltonizer_mode_monochromacy" msgid="8485709880666106721">"ஒற்றை நிறத் தன்மை"</string> <string name="daltonizer_mode_deuteranomaly" msgid="5475532989673586329">"நிறம் அடையாளங்காண முடியாமை (சிவப்பு-பச்சை)"</string> <string name="daltonizer_mode_protanomaly" msgid="8424148009038666065">"நிறம் அடையாளங்காண முடியாமை (சிவப்பு-பச்சை)"</string> <string name="daltonizer_mode_tritanomaly" msgid="481725854987912389">"நிறம் அடையாளங்காண முடியாமை (நீலம்-மஞ்சள்)"</string> diff --git a/packages/SettingsLib/src/com/android/settingslib/CustomDialogPreferenceCompat.java b/packages/SettingsLib/src/com/android/settingslib/CustomDialogPreferenceCompat.java new file mode 100644 index 000000000000..6ac9d4e21f40 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/CustomDialogPreferenceCompat.java @@ -0,0 +1,129 @@ +/* + * 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.settingslib; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.util.AttributeSet; +import android.view.View; + +import androidx.appcompat.app.AlertDialog; +import androidx.preference.DialogPreference; +import androidx.preference.PreferenceDialogFragmentCompat; + +public class CustomDialogPreferenceCompat extends DialogPreference { + + private CustomPreferenceDialogFragment mFragment; + private DialogInterface.OnShowListener mOnShowListener; + + public CustomDialogPreferenceCompat(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public CustomDialogPreferenceCompat(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public CustomDialogPreferenceCompat(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CustomDialogPreferenceCompat(Context context) { + super(context); + } + + public boolean isDialogOpen() { + return getDialog() != null && getDialog().isShowing(); + } + + public Dialog getDialog() { + return mFragment != null ? mFragment.getDialog() : null; + } + + public void setOnShowListener(DialogInterface.OnShowListener listner) { + mOnShowListener = listner; + } + + protected void onPrepareDialogBuilder(AlertDialog.Builder builder, + DialogInterface.OnClickListener listener) { + } + + protected void onDialogClosed(boolean positiveResult) { + } + + protected void onClick(DialogInterface dialog, int which) { + } + + protected void onBindDialogView(View view) { + } + + private void setFragment(CustomPreferenceDialogFragment fragment) { + mFragment = fragment; + } + + private DialogInterface.OnShowListener getOnShowListener() { + return mOnShowListener; + } + + public static class CustomPreferenceDialogFragment extends PreferenceDialogFragmentCompat { + + public static CustomPreferenceDialogFragment newInstance(String key) { + final CustomPreferenceDialogFragment fragment = new CustomPreferenceDialogFragment(); + final Bundle b = new Bundle(1); + b.putString(ARG_KEY, key); + fragment.setArguments(b); + return fragment; + } + + private CustomDialogPreferenceCompat getCustomizablePreference() { + return (CustomDialogPreferenceCompat) getPreference(); + } + + @Override + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + super.onPrepareDialogBuilder(builder); + getCustomizablePreference().setFragment(this); + getCustomizablePreference().onPrepareDialogBuilder(builder, this); + } + + @Override + public void onDialogClosed(boolean positiveResult) { + getCustomizablePreference().onDialogClosed(positiveResult); + } + + @Override + protected void onBindDialogView(View view) { + super.onBindDialogView(view); + getCustomizablePreference().onBindDialogView(view); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Dialog dialog = super.onCreateDialog(savedInstanceState); + dialog.setOnShowListener(getCustomizablePreference().getOnShowListener()); + return dialog; + } + + @Override + public void onClick(DialogInterface dialog, int which) { + super.onClick(dialog, which); + getCustomizablePreference().onClick(dialog, which); + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/CustomEditTextPreferenceCompat.java b/packages/SettingsLib/src/com/android/settingslib/CustomEditTextPreferenceCompat.java new file mode 100644 index 000000000000..6ddc89af03ad --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/CustomEditTextPreferenceCompat.java @@ -0,0 +1,136 @@ +/* + * 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.settingslib; + +import static android.text.InputType.TYPE_CLASS_TEXT; +import static android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.util.AttributeSet; +import android.view.View; +import android.widget.EditText; + +import androidx.annotation.CallSuper; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.EditTextPreference; +import androidx.preference.EditTextPreferenceDialogFragmentCompat; + +public class CustomEditTextPreferenceCompat extends EditTextPreference { + + private CustomPreferenceDialogFragment mFragment; + + public CustomEditTextPreferenceCompat(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public CustomEditTextPreferenceCompat(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public CustomEditTextPreferenceCompat(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CustomEditTextPreferenceCompat(Context context) { + super(context); + } + + public EditText getEditText() { + if (mFragment != null) { + final Dialog dialog = mFragment.getDialog(); + if (dialog != null) { + return (EditText) dialog.findViewById(android.R.id.edit); + } + } + return null; + } + + public boolean isDialogOpen() { + return getDialog() != null && getDialog().isShowing(); + } + + public Dialog getDialog() { + return mFragment != null ? mFragment.getDialog() : null; + } + + protected void onPrepareDialogBuilder(AlertDialog.Builder builder, + DialogInterface.OnClickListener listener) { + } + + protected void onDialogClosed(boolean positiveResult) { + } + + protected void onClick(DialogInterface dialog, int which) { + } + + @CallSuper + protected void onBindDialogView(View view) { + final EditText editText = view.findViewById(android.R.id.edit); + if (editText != null) { + editText.setInputType(TYPE_CLASS_TEXT | TYPE_TEXT_FLAG_CAP_SENTENCES); + editText.requestFocus(); + } + } + + private void setFragment(CustomPreferenceDialogFragment fragment) { + mFragment = fragment; + } + + public static class CustomPreferenceDialogFragment extends + EditTextPreferenceDialogFragmentCompat { + + public static CustomPreferenceDialogFragment newInstance(String key) { + final CustomPreferenceDialogFragment fragment = new CustomPreferenceDialogFragment(); + final Bundle b = new Bundle(1); + b.putString(ARG_KEY, key); + fragment.setArguments(b); + return fragment; + } + + private CustomEditTextPreferenceCompat getCustomizablePreference() { + return (CustomEditTextPreferenceCompat) getPreference(); + } + + @Override + protected void onBindDialogView(View view) { + super.onBindDialogView(view); + getCustomizablePreference().onBindDialogView(view); + } + + @Override + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + super.onPrepareDialogBuilder(builder); + getCustomizablePreference().setFragment(this); + getCustomizablePreference().onPrepareDialogBuilder(builder, this); + } + + @Override + public void onDialogClosed(boolean positiveResult) { + super.onDialogClosed(positiveResult); + getCustomizablePreference().onDialogClosed(positiveResult); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + super.onClick(dialog, which); + getCustomizablePreference().onClick(dialog, which); + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableActivity.java b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableActivity.java index f9aa062b64da..2bd0b27063ea 100644 --- a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableActivity.java +++ b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableActivity.java @@ -24,20 +24,22 @@ import static androidx.lifecycle.Lifecycle.Event.ON_STOP; import android.annotation.Nullable; import android.app.Activity; -import androidx.lifecycle.LifecycleOwner; import android.os.Bundle; import android.os.PersistableBundle; import android.view.Menu; import android.view.MenuItem; +import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.LifecycleOwner; + /** * {@link Activity} that has hooks to observe activity lifecycle events. */ -public class ObservableActivity extends Activity implements LifecycleOwner { +public class ObservableActivity extends FragmentActivity implements LifecycleOwner { private final Lifecycle mLifecycle = new Lifecycle(this); - public Lifecycle getLifecycle() { + public Lifecycle getSettingsLifecycle() { return mLifecycle; } diff --git a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableDialogFragment.java b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableDialogFragment.java index 972e062bb396..869f54f7d13a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableDialogFragment.java +++ b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableDialogFragment.java @@ -22,14 +22,15 @@ import static androidx.lifecycle.Lifecycle.Event.ON_RESUME; import static androidx.lifecycle.Lifecycle.Event.ON_START; import static androidx.lifecycle.Lifecycle.Event.ON_STOP; -import android.app.DialogFragment; -import androidx.lifecycle.LifecycleOwner; import android.content.Context; import android.os.Bundle; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.LifecycleOwner; + /** * {@link DialogFragment} that has hooks to observe fragment lifecycle events. */ @@ -37,6 +38,10 @@ public class ObservableDialogFragment extends DialogFragment implements Lifecycl protected final Lifecycle mLifecycle = new Lifecycle(this); + public Lifecycle getSettingsLifecycle() { + return mLifecycle; + } + @Override public void onAttach(Context context) { super.onAttach(context); @@ -100,9 +105,4 @@ public class ObservableDialogFragment extends DialogFragment implements Lifecycl } return lifecycleHandled; } - - @Override - public Lifecycle getLifecycle() { - return mLifecycle; - } } diff --git a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableFragment.java b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableFragment.java index 55597cc1247a..6ba930dcac96 100644 --- a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableFragment.java +++ b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableFragment.java @@ -24,19 +24,20 @@ import static androidx.lifecycle.Lifecycle.Event.ON_START; import static androidx.lifecycle.Lifecycle.Event.ON_STOP; import android.annotation.CallSuper; -import android.app.Fragment; -import androidx.lifecycle.LifecycleOwner; import android.content.Context; import android.os.Bundle; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.LifecycleOwner; + public class ObservableFragment extends Fragment implements LifecycleOwner { private final Lifecycle mLifecycle = new Lifecycle(this); - public Lifecycle getLifecycle() { + public Lifecycle getSettingsLifecycle() { return mLifecycle; } diff --git a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservablePreferenceFragment.java b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservablePreferenceFragment.java index 904681c4aa3c..bd1e5a588968 100644 --- a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservablePreferenceFragment.java +++ b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservablePreferenceFragment.java @@ -24,24 +24,25 @@ import static androidx.lifecycle.Lifecycle.Event.ON_START; import static androidx.lifecycle.Lifecycle.Event.ON_STOP; import android.annotation.CallSuper; -import androidx.lifecycle.LifecycleOwner; import android.content.Context; import android.os.Bundle; -import androidx.preference.PreferenceFragment; -import androidx.preference.PreferenceScreen; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import androidx.lifecycle.LifecycleOwner; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; + /** - * {@link PreferenceFragment} that has hooks to observe fragment lifecycle events. + * {@link PreferenceFragmentCompat} that has hooks to observe fragment lifecycle events. */ -public abstract class ObservablePreferenceFragment extends PreferenceFragment +public abstract class ObservablePreferenceFragment extends PreferenceFragmentCompat implements LifecycleOwner { private final Lifecycle mLifecycle = new Lifecycle(this); - public Lifecycle getLifecycle() { + public Lifecycle getSettingsLifecycle() { return mLifecycle; } diff --git a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java index e5d97c9b4a7b..3c0f6fe8ccbb 100644 --- a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java +++ b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java @@ -256,11 +256,12 @@ public class DreamBackend { } } - public void launchSettings(DreamInfo dreamInfo) { + public void launchSettings(Context uiContext, DreamInfo dreamInfo) { logd("launchSettings(%s)", dreamInfo); - if (dreamInfo == null || dreamInfo.settingsComponentName == null) + if (dreamInfo == null || dreamInfo.settingsComponentName == null) { return; - mContext.startActivity(new Intent().setComponent(dreamInfo.settingsComponentName)); + } + uiContext.startActivity(new Intent().setComponent(dreamInfo.settingsComponentName)); } public void preview(DreamInfo dreamInfo) { diff --git a/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeEnablerManagerCompat.java b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeEnablerManagerCompat.java new file mode 100644 index 000000000000..ad1368c7731d --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeEnablerManagerCompat.java @@ -0,0 +1,265 @@ +/* + * 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.settingslib.inputmethod; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.text.TextUtils; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.InputMethodSubtype; + +import com.android.settingslib.R; + +import java.text.Collator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; +import androidx.preference.TwoStatePreference; + +public class InputMethodAndSubtypeEnablerManagerCompat implements + Preference.OnPreferenceChangeListener { + + private final PreferenceFragmentCompat mFragment; + + private boolean mHaveHardKeyboard; + private final HashMap<String, List<Preference>> mInputMethodAndSubtypePrefsMap = + new HashMap<>(); + private final HashMap<String, TwoStatePreference> mAutoSelectionPrefsMap = new HashMap<>(); + private InputMethodManager mImm; + // TODO: Change mInputMethodInfoList to Map + private List<InputMethodInfo> mInputMethodInfoList; + private final Collator mCollator = Collator.getInstance(); + + public InputMethodAndSubtypeEnablerManagerCompat(PreferenceFragmentCompat fragment) { + mFragment = fragment; + mImm = fragment.getContext().getSystemService(InputMethodManager.class); + + mInputMethodInfoList = mImm.getInputMethodList(); + } + + public void init(PreferenceFragmentCompat fragment, String targetImi, PreferenceScreen root) { + final Configuration config = fragment.getResources().getConfiguration(); + mHaveHardKeyboard = (config.keyboard == Configuration.KEYBOARD_QWERTY); + + for (final InputMethodInfo imi : mInputMethodInfoList) { + // Add subtype preferences of this IME when it is specified or no IME is specified. + if (imi.getId().equals(targetImi) || TextUtils.isEmpty(targetImi)) { + addInputMethodSubtypePreferences(fragment, imi, root); + } + } + } + + public void refresh(Context context, PreferenceFragmentCompat fragment) { + // Refresh internal states in mInputMethodSettingValues to keep the latest + // "InputMethodInfo"s and "InputMethodSubtype"s + InputMethodSettingValuesWrapper + .getInstance(context).refreshAllInputMethodAndSubtypes(); + InputMethodAndSubtypeUtilCompat.loadInputMethodSubtypeList(fragment, + context.getContentResolver(), mInputMethodInfoList, mInputMethodAndSubtypePrefsMap); + updateAutoSelectionPreferences(); + } + + public void save(Context context, PreferenceFragmentCompat fragment) { + InputMethodAndSubtypeUtilCompat.saveInputMethodSubtypeList(fragment, + context.getContentResolver(), mInputMethodInfoList, mHaveHardKeyboard); + } + + @Override + public boolean onPreferenceChange(final Preference pref, final Object newValue) { + if (!(newValue instanceof Boolean)) { + return true; // Invoke default behavior. + } + final boolean isChecking = (Boolean) newValue; + for (final String imiId : mAutoSelectionPrefsMap.keySet()) { + // An auto select subtype preference is changing. + if (mAutoSelectionPrefsMap.get(imiId) == pref) { + final TwoStatePreference autoSelectionPref = (TwoStatePreference) pref; + autoSelectionPref.setChecked(isChecking); + // Enable or disable subtypes depending on the auto selection preference. + setAutoSelectionSubtypesEnabled(imiId, autoSelectionPref.isChecked()); + return false; + } + } + // A subtype preference is changing. + if (pref instanceof InputMethodSubtypePreference) { + final InputMethodSubtypePreference subtypePref = (InputMethodSubtypePreference) pref; + subtypePref.setChecked(isChecking); + if (!subtypePref.isChecked()) { + // It takes care of the case where no subtypes are explicitly enabled then the auto + // selection preference is going to be checked. + updateAutoSelectionPreferences(); + } + return false; + } + return true; // Invoke default behavior. + } + + private void addInputMethodSubtypePreferences(PreferenceFragmentCompat fragment, + InputMethodInfo imi, final PreferenceScreen root) { + Context prefContext = fragment.getPreferenceManager().getContext(); + + final int subtypeCount = imi.getSubtypeCount(); + if (subtypeCount <= 1) { + return; + } + final String imiId = imi.getId(); + final PreferenceCategory keyboardSettingsCategory = + new PreferenceCategory(prefContext); + root.addPreference(keyboardSettingsCategory); + final PackageManager pm = prefContext.getPackageManager(); + final CharSequence label = imi.loadLabel(pm); + + keyboardSettingsCategory.setTitle(label); + keyboardSettingsCategory.setKey(imiId); + // TODO: Use toggle Preference if images are ready. + final TwoStatePreference autoSelectionPref = + new SwitchWithNoTextPreference(prefContext); + mAutoSelectionPrefsMap.put(imiId, autoSelectionPref); + keyboardSettingsCategory.addPreference(autoSelectionPref); + autoSelectionPref.setOnPreferenceChangeListener(this); + + final PreferenceCategory activeInputMethodsCategory = + new PreferenceCategory(prefContext); + activeInputMethodsCategory.setTitle(R.string.active_input_method_subtypes); + root.addPreference(activeInputMethodsCategory); + + CharSequence autoSubtypeLabel = null; + final ArrayList<Preference> subtypePreferences = new ArrayList<>(); + for (int index = 0; index < subtypeCount; ++index) { + final InputMethodSubtype subtype = imi.getSubtypeAt(index); + if (subtype.overridesImplicitlyEnabledSubtype()) { + if (autoSubtypeLabel == null) { + autoSubtypeLabel = InputMethodAndSubtypeUtil.getSubtypeLocaleNameAsSentence( + subtype, prefContext, imi); + } + } else { + final Preference subtypePref = new InputMethodSubtypePreference( + prefContext, subtype, imi); + subtypePreferences.add(subtypePref); + } + } + subtypePreferences.sort((lhs, rhs) -> { + if (lhs instanceof InputMethodSubtypePreference) { + return ((InputMethodSubtypePreference) lhs).compareTo(rhs, mCollator); + } + return lhs.compareTo(rhs); + }); + for (final Preference pref : subtypePreferences) { + activeInputMethodsCategory.addPreference(pref); + pref.setOnPreferenceChangeListener(this); + InputMethodAndSubtypeUtil.removeUnnecessaryNonPersistentPreference(pref); + } + mInputMethodAndSubtypePrefsMap.put(imiId, subtypePreferences); + if (TextUtils.isEmpty(autoSubtypeLabel)) { + autoSelectionPref.setTitle( + R.string.use_system_language_to_select_input_method_subtypes); + } else { + autoSelectionPref.setTitle(autoSubtypeLabel); + } + } + + private boolean isNoSubtypesExplicitlySelected(final String imiId) { + final List<Preference> subtypePrefs = mInputMethodAndSubtypePrefsMap.get(imiId); + for (final Preference pref : subtypePrefs) { + if (pref instanceof TwoStatePreference && ((TwoStatePreference) pref).isChecked()) { + return false; + } + } + return true; + } + + private void setAutoSelectionSubtypesEnabled(final String imiId, + final boolean autoSelectionEnabled) { + final TwoStatePreference autoSelectionPref = mAutoSelectionPrefsMap.get(imiId); + if (autoSelectionPref == null) { + return; + } + autoSelectionPref.setChecked(autoSelectionEnabled); + final List<Preference> subtypePrefs = mInputMethodAndSubtypePrefsMap.get(imiId); + for (final Preference pref : subtypePrefs) { + if (pref instanceof TwoStatePreference) { + // When autoSelectionEnabled is true, all subtype prefs need to be disabled with + // implicitly checked subtypes. In case of false, all subtype prefs need to be + // enabled. + pref.setEnabled(!autoSelectionEnabled); + if (autoSelectionEnabled) { + ((TwoStatePreference) pref).setChecked(false); + } + } + } + if (autoSelectionEnabled) { + InputMethodAndSubtypeUtilCompat.saveInputMethodSubtypeList( + mFragment, mFragment.getContext().getContentResolver(), + mInputMethodInfoList, mHaveHardKeyboard); + updateImplicitlyEnabledSubtypes(imiId); + } + } + + private void updateImplicitlyEnabledSubtypes(final String targetImiId) { + // When targetImiId is null, apply to all subtypes of all IMEs + for (final InputMethodInfo imi : mInputMethodInfoList) { + final String imiId = imi.getId(); + final TwoStatePreference autoSelectionPref = mAutoSelectionPrefsMap.get(imiId); + // No need to update implicitly enabled subtypes when the user has unchecked the + // "subtype auto selection". + if (autoSelectionPref == null || !autoSelectionPref.isChecked()) { + continue; + } + if (imiId.equals(targetImiId) || targetImiId == null) { + updateImplicitlyEnabledSubtypesOf(imi); + } + } + } + + private void updateImplicitlyEnabledSubtypesOf(final InputMethodInfo imi) { + final String imiId = imi.getId(); + final List<Preference> subtypePrefs = mInputMethodAndSubtypePrefsMap.get(imiId); + final List<InputMethodSubtype> implicitlyEnabledSubtypes = + mImm.getEnabledInputMethodSubtypeList(imi, true); + if (subtypePrefs == null || implicitlyEnabledSubtypes == null) { + return; + } + for (final Preference pref : subtypePrefs) { + if (!(pref instanceof TwoStatePreference)) { + continue; + } + final TwoStatePreference subtypePref = (TwoStatePreference) pref; + subtypePref.setChecked(false); + for (final InputMethodSubtype subtype : implicitlyEnabledSubtypes) { + final String implicitlyEnabledSubtypePrefKey = imiId + subtype.hashCode(); + if (subtypePref.getKey().equals(implicitlyEnabledSubtypePrefKey)) { + subtypePref.setChecked(true); + break; + } + } + } + } + + private void updateAutoSelectionPreferences() { + for (final String imiId : mInputMethodAndSubtypePrefsMap.keySet()) { + setAutoSelectionSubtypesEnabled(imiId, isNoSubtypesExplicitlySelected(imiId)); + } + updateImplicitlyEnabledSubtypes(null /* targetImiId */ /* check */); + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeUtilCompat.java b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeUtilCompat.java new file mode 100644 index 000000000000..9ad2adbcc432 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeUtilCompat.java @@ -0,0 +1,436 @@ +/* + * 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.settingslib.inputmethod; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ContentResolver; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.icu.text.ListFormatter; +import android.provider.Settings; +import android.provider.Settings.SettingNotFoundException; +import android.text.TextUtils; +import android.util.Log; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodSubtype; + +import com.android.internal.app.LocaleHelper; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; +import androidx.preference.TwoStatePreference; + +// TODO: Consolidate this with {@link InputMethodSettingValuesWrapper}. +public class InputMethodAndSubtypeUtilCompat { + + private static final boolean DEBUG = false; + private static final String TAG = "InputMethdAndSubtypeUtlCompat"; + + private static final String SUBTYPE_MODE_KEYBOARD = "keyboard"; + private static final char INPUT_METHOD_SEPARATER = ':'; + private static final char INPUT_METHOD_SUBTYPE_SEPARATER = ';'; + private static final int NOT_A_SUBTYPE_ID = -1; + + private static final TextUtils.SimpleStringSplitter sStringInputMethodSplitter + = new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATER); + + private static final TextUtils.SimpleStringSplitter sStringInputMethodSubtypeSplitter + = new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATER); + + // InputMethods and subtypes are saved in the settings as follows: + // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1 + public static String buildInputMethodsAndSubtypesString( + final HashMap<String, HashSet<String>> imeToSubtypesMap) { + final StringBuilder builder = new StringBuilder(); + for (final String imi : imeToSubtypesMap.keySet()) { + if (builder.length() > 0) { + builder.append(INPUT_METHOD_SEPARATER); + } + final HashSet<String> subtypeIdSet = imeToSubtypesMap.get(imi); + builder.append(imi); + for (final String subtypeId : subtypeIdSet) { + builder.append(INPUT_METHOD_SUBTYPE_SEPARATER).append(subtypeId); + } + } + return builder.toString(); + } + + private static String buildInputMethodsString(final HashSet<String> imiList) { + final StringBuilder builder = new StringBuilder(); + for (final String imi : imiList) { + if (builder.length() > 0) { + builder.append(INPUT_METHOD_SEPARATER); + } + builder.append(imi); + } + return builder.toString(); + } + + private static int getInputMethodSubtypeSelected(ContentResolver resolver) { + try { + return Settings.Secure.getInt(resolver, + Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE); + } catch (SettingNotFoundException e) { + return NOT_A_SUBTYPE_ID; + } + } + + private static boolean isInputMethodSubtypeSelected(ContentResolver resolver) { + return getInputMethodSubtypeSelected(resolver) != NOT_A_SUBTYPE_ID; + } + + private static void putSelectedInputMethodSubtype(ContentResolver resolver, int hashCode) { + Settings.Secure.putInt(resolver, Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, hashCode); + } + + // Needs to modify InputMethodManageService if you want to change the format of saved string. + static HashMap<String, HashSet<String>> getEnabledInputMethodsAndSubtypeList( + ContentResolver resolver) { + final String enabledInputMethodsStr = Settings.Secure.getString( + resolver, Settings.Secure.ENABLED_INPUT_METHODS); + if (DEBUG) { + Log.d(TAG, "--- Load enabled input methods: " + enabledInputMethodsStr); + } + return parseInputMethodsAndSubtypesString(enabledInputMethodsStr); + } + + public static HashMap<String, HashSet<String>> parseInputMethodsAndSubtypesString( + final String inputMethodsAndSubtypesString) { + final HashMap<String, HashSet<String>> subtypesMap = new HashMap<>(); + if (TextUtils.isEmpty(inputMethodsAndSubtypesString)) { + return subtypesMap; + } + sStringInputMethodSplitter.setString(inputMethodsAndSubtypesString); + while (sStringInputMethodSplitter.hasNext()) { + final String nextImsStr = sStringInputMethodSplitter.next(); + sStringInputMethodSubtypeSplitter.setString(nextImsStr); + if (sStringInputMethodSubtypeSplitter.hasNext()) { + final HashSet<String> subtypeIdSet = new HashSet<>(); + // The first element is {@link InputMethodInfoId}. + final String imiId = sStringInputMethodSubtypeSplitter.next(); + while (sStringInputMethodSubtypeSplitter.hasNext()) { + subtypeIdSet.add(sStringInputMethodSubtypeSplitter.next()); + } + subtypesMap.put(imiId, subtypeIdSet); + } + } + return subtypesMap; + } + + private static HashSet<String> getDisabledSystemIMEs(ContentResolver resolver) { + HashSet<String> set = new HashSet<>(); + String disabledIMEsStr = Settings.Secure.getString( + resolver, Settings.Secure.DISABLED_SYSTEM_INPUT_METHODS); + if (TextUtils.isEmpty(disabledIMEsStr)) { + return set; + } + sStringInputMethodSplitter.setString(disabledIMEsStr); + while(sStringInputMethodSplitter.hasNext()) { + set.add(sStringInputMethodSplitter.next()); + } + return set; + } + + public static void saveInputMethodSubtypeList(PreferenceFragmentCompat context, + ContentResolver resolver, List<InputMethodInfo> inputMethodInfos, + boolean hasHardKeyboard) { + String currentInputMethodId = Settings.Secure.getString(resolver, + Settings.Secure.DEFAULT_INPUT_METHOD); + final int selectedInputMethodSubtype = getInputMethodSubtypeSelected(resolver); + final HashMap<String, HashSet<String>> enabledIMEsAndSubtypesMap = + getEnabledInputMethodsAndSubtypeList(resolver); + final HashSet<String> disabledSystemIMEs = getDisabledSystemIMEs(resolver); + + boolean needsToResetSelectedSubtype = false; + for (final InputMethodInfo imi : inputMethodInfos) { + final String imiId = imi.getId(); + final Preference pref = context.findPreference(imiId); + if (pref == null) { + continue; + } + // In the choose input method screen or in the subtype enabler screen, + // <code>pref</code> is an instance of TwoStatePreference. + final boolean isImeChecked = (pref instanceof TwoStatePreference) ? + ((TwoStatePreference) pref).isChecked() + : enabledIMEsAndSubtypesMap.containsKey(imiId); + final boolean isCurrentInputMethod = imiId.equals(currentInputMethodId); + final boolean systemIme = imi.isSystem(); + if ((!hasHardKeyboard && InputMethodSettingValuesWrapper.getInstance( + context.getActivity()).isAlwaysCheckedIme(imi)) + || isImeChecked) { + if (!enabledIMEsAndSubtypesMap.containsKey(imiId)) { + // imiId has just been enabled + enabledIMEsAndSubtypesMap.put(imiId, new HashSet<>()); + } + final HashSet<String> subtypesSet = enabledIMEsAndSubtypesMap.get(imiId); + + boolean subtypePrefFound = false; + final int subtypeCount = imi.getSubtypeCount(); + for (int i = 0; i < subtypeCount; ++i) { + final InputMethodSubtype subtype = imi.getSubtypeAt(i); + final String subtypeHashCodeStr = String.valueOf(subtype.hashCode()); + final TwoStatePreference subtypePref = (TwoStatePreference) context + .findPreference(imiId + subtypeHashCodeStr); + // In the Configure input method screen which does not have subtype preferences. + if (subtypePref == null) { + continue; + } + if (!subtypePrefFound) { + // Once subtype preference is found, subtypeSet needs to be cleared. + // Because of system change, hashCode value could have been changed. + subtypesSet.clear(); + // If selected subtype preference is disabled, needs to reset. + needsToResetSelectedSubtype = true; + subtypePrefFound = true; + } + // Checking <code>subtypePref.isEnabled()</code> is insufficient to determine + // whether the user manually enabled this subtype or not. Implicitly-enabled + // subtypes are also checked just as an indicator to users. We also need to + // check <code>subtypePref.isEnabled()</code> so that only manually enabled + // subtypes can be saved here. + if (subtypePref.isEnabled() && subtypePref.isChecked()) { + subtypesSet.add(subtypeHashCodeStr); + if (isCurrentInputMethod) { + if (selectedInputMethodSubtype == subtype.hashCode()) { + // Selected subtype is still enabled, there is no need to reset + // selected subtype. + needsToResetSelectedSubtype = false; + } + } + } else { + subtypesSet.remove(subtypeHashCodeStr); + } + } + } else { + enabledIMEsAndSubtypesMap.remove(imiId); + if (isCurrentInputMethod) { + // We are processing the current input method, but found that it's not enabled. + // This means that the current input method has been uninstalled. + // If currentInputMethod is already uninstalled, InputMethodManagerService will + // find the applicable IME from the history and the system locale. + if (DEBUG) { + Log.d(TAG, "Current IME was uninstalled or disabled."); + } + currentInputMethodId = null; + } + } + // If it's a disabled system ime, add it to the disabled list so that it + // doesn't get enabled automatically on any changes to the package list + if (systemIme && hasHardKeyboard) { + if (disabledSystemIMEs.contains(imiId)) { + if (isImeChecked) { + disabledSystemIMEs.remove(imiId); + } + } else { + if (!isImeChecked) { + disabledSystemIMEs.add(imiId); + } + } + } + } + + final String enabledIMEsAndSubtypesString = buildInputMethodsAndSubtypesString( + enabledIMEsAndSubtypesMap); + final String disabledSystemIMEsString = buildInputMethodsString(disabledSystemIMEs); + if (DEBUG) { + Log.d(TAG, "--- Save enabled inputmethod settings. :" + enabledIMEsAndSubtypesString); + Log.d(TAG, "--- Save disabled system inputmethod settings. :" + + disabledSystemIMEsString); + Log.d(TAG, "--- Save default inputmethod settings. :" + currentInputMethodId); + Log.d(TAG, "--- Needs to reset the selected subtype :" + needsToResetSelectedSubtype); + Log.d(TAG, "--- Subtype is selected :" + isInputMethodSubtypeSelected(resolver)); + } + + // Redefines SelectedSubtype when all subtypes are unchecked or there is no subtype + // selected. And if the selected subtype of the current input method was disabled, + // We should reset the selected input method's subtype. + if (needsToResetSelectedSubtype || !isInputMethodSubtypeSelected(resolver)) { + if (DEBUG) { + Log.d(TAG, "--- Reset inputmethod subtype because it's not defined."); + } + putSelectedInputMethodSubtype(resolver, NOT_A_SUBTYPE_ID); + } + + Settings.Secure.putString(resolver, + Settings.Secure.ENABLED_INPUT_METHODS, enabledIMEsAndSubtypesString); + if (disabledSystemIMEsString.length() > 0) { + Settings.Secure.putString(resolver, Settings.Secure.DISABLED_SYSTEM_INPUT_METHODS, + disabledSystemIMEsString); + } + // If the current input method is unset, InputMethodManagerService will find the applicable + // IME from the history and the system locale. + Settings.Secure.putString(resolver, Settings.Secure.DEFAULT_INPUT_METHOD, + currentInputMethodId != null ? currentInputMethodId : ""); + } + + public static void loadInputMethodSubtypeList(final PreferenceFragmentCompat context, + final ContentResolver resolver, final List<InputMethodInfo> inputMethodInfos, + final Map<String, List<Preference>> inputMethodPrefsMap) { + final HashMap<String, HashSet<String>> enabledSubtypes = + getEnabledInputMethodsAndSubtypeList(resolver); + + for (final InputMethodInfo imi : inputMethodInfos) { + final String imiId = imi.getId(); + final Preference pref = context.findPreference(imiId); + if (pref instanceof TwoStatePreference) { + final TwoStatePreference subtypePref = (TwoStatePreference) pref; + final boolean isEnabled = enabledSubtypes.containsKey(imiId); + subtypePref.setChecked(isEnabled); + if (inputMethodPrefsMap != null) { + for (final Preference childPref: inputMethodPrefsMap.get(imiId)) { + childPref.setEnabled(isEnabled); + } + } + setSubtypesPreferenceEnabled(context, inputMethodInfos, imiId, isEnabled); + } + } + updateSubtypesPreferenceChecked(context, inputMethodInfos, enabledSubtypes); + } + + private static void setSubtypesPreferenceEnabled(final PreferenceFragmentCompat context, + final List<InputMethodInfo> inputMethodProperties, final String id, + final boolean enabled) { + final PreferenceScreen preferenceScreen = context.getPreferenceScreen(); + for (final InputMethodInfo imi : inputMethodProperties) { + if (id.equals(imi.getId())) { + final int subtypeCount = imi.getSubtypeCount(); + for (int i = 0; i < subtypeCount; ++i) { + final InputMethodSubtype subtype = imi.getSubtypeAt(i); + final TwoStatePreference pref = (TwoStatePreference) preferenceScreen + .findPreference(id + subtype.hashCode()); + if (pref != null) { + pref.setEnabled(enabled); + } + } + } + } + } + + private static void updateSubtypesPreferenceChecked(final PreferenceFragmentCompat context, + final List<InputMethodInfo> inputMethodProperties, + final HashMap<String, HashSet<String>> enabledSubtypes) { + final PreferenceScreen preferenceScreen = context.getPreferenceScreen(); + for (final InputMethodInfo imi : inputMethodProperties) { + final String id = imi.getId(); + if (!enabledSubtypes.containsKey(id)) { + // There is no need to enable/disable subtypes of disabled IMEs. + continue; + } + final HashSet<String> enabledSubtypesSet = enabledSubtypes.get(id); + final int subtypeCount = imi.getSubtypeCount(); + for (int i = 0; i < subtypeCount; ++i) { + final InputMethodSubtype subtype = imi.getSubtypeAt(i); + final String hashCode = String.valueOf(subtype.hashCode()); + if (DEBUG) { + Log.d(TAG, "--- Set checked state: " + "id" + ", " + hashCode + ", " + + enabledSubtypesSet.contains(hashCode)); + } + final TwoStatePreference pref = (TwoStatePreference) preferenceScreen + .findPreference(id + hashCode); + if (pref != null) { + pref.setChecked(enabledSubtypesSet.contains(hashCode)); + } + } + } + } + + public static void removeUnnecessaryNonPersistentPreference(final Preference pref) { + final String key = pref.getKey(); + if (pref.isPersistent() || key == null) { + return; + } + final SharedPreferences prefs = pref.getSharedPreferences(); + if (prefs != null && prefs.contains(key)) { + prefs.edit().remove(key).apply(); + } + } + + @NonNull + public static String getSubtypeLocaleNameAsSentence(@Nullable InputMethodSubtype subtype, + @NonNull final Context context, @NonNull final InputMethodInfo inputMethodInfo) { + if (subtype == null) { + return ""; + } + final Locale locale = getDisplayLocale(context); + final CharSequence subtypeName = subtype.getDisplayName(context, + inputMethodInfo.getPackageName(), inputMethodInfo.getServiceInfo() + .applicationInfo); + return LocaleHelper.toSentenceCase(subtypeName.toString(), locale); + } + + @NonNull + public static String getSubtypeLocaleNameListAsSentence( + @NonNull final List<InputMethodSubtype> subtypes, @NonNull final Context context, + @NonNull final InputMethodInfo inputMethodInfo) { + if (subtypes.isEmpty()) { + return ""; + } + final Locale locale = getDisplayLocale(context); + final int subtypeCount = subtypes.size(); + final CharSequence[] subtypeNames = new CharSequence[subtypeCount]; + for (int i = 0; i < subtypeCount; i++) { + subtypeNames[i] = subtypes.get(i).getDisplayName(context, + inputMethodInfo.getPackageName(), inputMethodInfo.getServiceInfo() + .applicationInfo); + } + return LocaleHelper.toSentenceCase( + ListFormatter.getInstance(locale).format((Object[]) subtypeNames), locale); + } + + @NonNull + private static Locale getDisplayLocale(@Nullable final Context context) { + if (context == null) { + return Locale.getDefault(); + } + if (context.getResources() == null) { + return Locale.getDefault(); + } + final Configuration configuration = context.getResources().getConfiguration(); + if (configuration == null) { + return Locale.getDefault(); + } + final Locale configurationLocale = configuration.getLocales().get(0); + if (configurationLocale == null) { + return Locale.getDefault(); + } + return configurationLocale; + } + + public static boolean isValidSystemNonAuxAsciiCapableIme(InputMethodInfo imi) { + if (imi.isAuxiliaryIme() || !imi.isSystem()) { + return false; + } + final int subtypeCount = imi.getSubtypeCount(); + for (int i = 0; i < subtypeCount; ++i) { + final InputMethodSubtype subtype = imi.getSubtypeAt(i); + if (SUBTYPE_MODE_KEYBOARD.equalsIgnoreCase(subtype.getMode()) + && subtype.isAsciiCapable()) { + return true; + } + } + return false; + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/license/LicenseHtmlLoaderCompat.java b/packages/SettingsLib/src/com/android/settingslib/license/LicenseHtmlLoaderCompat.java new file mode 100644 index 000000000000..d7c14ad66baa --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/license/LicenseHtmlLoaderCompat.java @@ -0,0 +1,111 @@ +/* + * 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.settingslib.license; + +import android.content.Context; +import android.util.Log; + +import com.android.settingslib.utils.AsyncLoaderCompat; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.VisibleForTesting; + +/** + * LicenseHtmlLoader is a loader which loads a license html file from default license xml files. + */ +public class LicenseHtmlLoaderCompat extends AsyncLoaderCompat<File> { + private static final String TAG = "LicenseHtmlLoaderCompat"; + + private static final String[] DEFAULT_LICENSE_XML_PATHS = { + "/system/etc/NOTICE.xml.gz", + "/vendor/etc/NOTICE.xml.gz", + "/odm/etc/NOTICE.xml.gz", + "/oem/etc/NOTICE.xml.gz"}; + private static final String NOTICE_HTML_FILE_NAME = "NOTICE.html"; + + private Context mContext; + + public LicenseHtmlLoaderCompat(Context context) { + super(context); + mContext = context; + } + + @Override + public File loadInBackground() { + return generateHtmlFromDefaultXmlFiles(); + } + + @Override + protected void onDiscardResult(File f) { + } + + private File generateHtmlFromDefaultXmlFiles() { + final List<File> xmlFiles = getVaildXmlFiles(); + if (xmlFiles.isEmpty()) { + Log.e(TAG, "No notice file exists."); + return null; + } + + File cachedHtmlFile = getCachedHtmlFile(); + if (!isCachedHtmlFileOutdated(xmlFiles, cachedHtmlFile) + || generateHtmlFile(xmlFiles, cachedHtmlFile)) { + return cachedHtmlFile; + } + + return null; + } + + @VisibleForTesting + List<File> getVaildXmlFiles() { + final List<File> xmlFiles = new ArrayList(); + for (final String xmlPath : DEFAULT_LICENSE_XML_PATHS) { + File file = new File(xmlPath); + if (file.exists() && file.length() != 0) { + xmlFiles.add(file); + } + } + return xmlFiles; + } + + @VisibleForTesting + File getCachedHtmlFile() { + return new File(mContext.getCacheDir(), NOTICE_HTML_FILE_NAME); + } + + @VisibleForTesting + boolean isCachedHtmlFileOutdated(List<File> xmlFiles, File cachedHtmlFile) { + boolean outdated = true; + if (cachedHtmlFile.exists() && cachedHtmlFile.length() != 0) { + outdated = false; + for (File file : xmlFiles) { + if (cachedHtmlFile.lastModified() < file.lastModified()) { + outdated = true; + break; + } + } + } + return outdated; + } + + @VisibleForTesting + boolean generateHtmlFile(List<File> xmlFiles, File htmlFile) { + return LicenseHtmlGeneratorFromXml.generateHtml(xmlFiles, htmlFile); + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/net/ChartDataLoaderCompat.java b/packages/SettingsLib/src/com/android/settingslib/net/ChartDataLoaderCompat.java new file mode 100644 index 000000000000..3adbd4d01ca0 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/net/ChartDataLoaderCompat.java @@ -0,0 +1,146 @@ +/* + * 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.settingslib.net; + +import static android.net.NetworkStats.SET_DEFAULT; +import static android.net.NetworkStats.SET_FOREGROUND; +import static android.net.NetworkStats.TAG_NONE; +import static android.net.NetworkStatsHistory.FIELD_RX_BYTES; +import static android.net.NetworkStatsHistory.FIELD_TX_BYTES; +import static android.text.format.DateUtils.HOUR_IN_MILLIS; + +import android.content.Context; +import android.net.INetworkStatsSession; +import android.net.NetworkStatsHistory; +import android.net.NetworkTemplate; +import android.os.Bundle; +import android.os.RemoteException; + +import com.android.settingslib.AppItem; + +import androidx.loader.content.AsyncTaskLoader; + +/** + * Loader for historical chart data for both network and UID details. + */ +public class ChartDataLoaderCompat extends AsyncTaskLoader<ChartData> { + private static final String KEY_TEMPLATE = "template"; + private static final String KEY_APP = "app"; + private static final String KEY_FIELDS = "fields"; + + private final INetworkStatsSession mSession; + private final Bundle mArgs; + + public static Bundle buildArgs(NetworkTemplate template, AppItem app) { + return buildArgs(template, app, FIELD_RX_BYTES | FIELD_TX_BYTES); + } + + public static Bundle buildArgs(NetworkTemplate template, AppItem app, int fields) { + final Bundle args = new Bundle(); + args.putParcelable(KEY_TEMPLATE, template); + args.putParcelable(KEY_APP, app); + args.putInt(KEY_FIELDS, fields); + return args; + } + + public ChartDataLoaderCompat(Context context, INetworkStatsSession session, Bundle args) { + super(context); + mSession = session; + mArgs = args; + } + + @Override + protected void onStartLoading() { + super.onStartLoading(); + forceLoad(); + } + + @Override + public ChartData loadInBackground() { + final NetworkTemplate template = mArgs.getParcelable(KEY_TEMPLATE); + final AppItem app = mArgs.getParcelable(KEY_APP); + final int fields = mArgs.getInt(KEY_FIELDS); + + try { + return loadInBackground(template, app, fields); + } catch (RemoteException e) { + // since we can't do much without history, and we don't want to + // leave with half-baked UI, we bail hard. + throw new RuntimeException("problem reading network stats", e); + } + } + + private ChartData loadInBackground(NetworkTemplate template, AppItem app, int fields) + throws RemoteException { + final ChartData data = new ChartData(); + data.network = mSession.getHistoryForNetwork(template, fields); + + if (app != null) { + // load stats for current uid and template + final int size = app.uids.size(); + for (int i = 0; i < size; i++) { + final int uid = app.uids.keyAt(i); + data.detailDefault = collectHistoryForUid( + template, uid, SET_DEFAULT, data.detailDefault); + data.detailForeground = collectHistoryForUid( + template, uid, SET_FOREGROUND, data.detailForeground); + } + + if (size > 0) { + data.detail = new NetworkStatsHistory(data.detailForeground.getBucketDuration()); + data.detail.recordEntireHistory(data.detailDefault); + data.detail.recordEntireHistory(data.detailForeground); + } else { + data.detailDefault = new NetworkStatsHistory(HOUR_IN_MILLIS); + data.detailForeground = new NetworkStatsHistory(HOUR_IN_MILLIS); + data.detail = new NetworkStatsHistory(HOUR_IN_MILLIS); + } + } + + return data; + } + + @Override + protected void onStopLoading() { + super.onStopLoading(); + cancelLoad(); + } + + @Override + protected void onReset() { + super.onReset(); + cancelLoad(); + } + + /** + * Collect {@link NetworkStatsHistory} for the requested UID, combining with + * an existing {@link NetworkStatsHistory} if provided. + */ + private NetworkStatsHistory collectHistoryForUid( + NetworkTemplate template, int uid, int set, NetworkStatsHistory existing) + throws RemoteException { + final NetworkStatsHistory history = mSession.getHistoryForUid( + template, uid, set, TAG_NONE, FIELD_RX_BYTES | FIELD_TX_BYTES); + + if (existing != null) { + existing.recordEntireHistory(history); + return existing; + } else { + return history; + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/net/SummaryForAllUidLoaderCompat.java b/packages/SettingsLib/src/com/android/settingslib/net/SummaryForAllUidLoaderCompat.java new file mode 100644 index 000000000000..c311337d6cf5 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/net/SummaryForAllUidLoaderCompat.java @@ -0,0 +1,81 @@ +/* + * 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.settingslib.net; + +import android.content.Context; +import android.net.INetworkStatsSession; +import android.net.NetworkStats; +import android.net.NetworkTemplate; +import android.os.Bundle; +import android.os.RemoteException; + +import androidx.loader.content.AsyncTaskLoader; + +public class SummaryForAllUidLoaderCompat extends AsyncTaskLoader<NetworkStats> { + private static final String KEY_TEMPLATE = "template"; + private static final String KEY_START = "start"; + private static final String KEY_END = "end"; + + private final INetworkStatsSession mSession; + private final Bundle mArgs; + + public static Bundle buildArgs(NetworkTemplate template, long start, long end) { + final Bundle args = new Bundle(); + args.putParcelable(KEY_TEMPLATE, template); + args.putLong(KEY_START, start); + args.putLong(KEY_END, end); + return args; + } + + public SummaryForAllUidLoaderCompat(Context context, INetworkStatsSession session, + Bundle args) { + super(context); + mSession = session; + mArgs = args; + } + + @Override + protected void onStartLoading() { + super.onStartLoading(); + forceLoad(); + } + + @Override + public NetworkStats loadInBackground() { + final NetworkTemplate template = mArgs.getParcelable(KEY_TEMPLATE); + final long start = mArgs.getLong(KEY_START); + final long end = mArgs.getLong(KEY_END); + + try { + return mSession.getSummaryForAllUid(template, start, end, false); + } catch (RemoteException e) { + return null; + } + } + + @Override + protected void onStopLoading() { + super.onStopLoading(); + cancelLoad(); + } + + @Override + protected void onReset() { + super.onReset(); + cancelLoad(); + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionControllerMixinCompat.java b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionControllerMixinCompat.java new file mode 100644 index 000000000000..179121739896 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionControllerMixinCompat.java @@ -0,0 +1,143 @@ +/* + * 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.settingslib.suggestions; + + +import android.content.ComponentName; +import android.content.Context; +import android.os.Bundle; +import android.service.settings.suggestions.Suggestion; +import android.util.Log; + +import com.android.settingslib.core.lifecycle.Lifecycle; + +import java.util.List; + +import androidx.annotation.Nullable; +import androidx.lifecycle.OnLifecycleEvent; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.Loader; + +/** + * Manages IPC communication to SettingsIntelligence for suggestion related services. + */ +public class SuggestionControllerMixinCompat implements + SuggestionController.ServiceConnectionListener, androidx.lifecycle.LifecycleObserver, + LoaderManager.LoaderCallbacks<List<Suggestion>> { + + public interface SuggestionControllerHost { + /** + * Called when suggestion data fetching is ready. + */ + void onSuggestionReady(List<Suggestion> data); + + /** + * Returns {@link LoaderManager} associated with the host. If host is not attached to + * activity then return null. + */ + @Nullable + LoaderManager getLoaderManager(); + } + + private static final String TAG = "SuggestionCtrlMixin"; + private static final boolean DEBUG = false; + + private final Context mContext; + private final SuggestionController mSuggestionController; + private final SuggestionControllerHost mHost; + + private boolean mSuggestionLoaded; + + public SuggestionControllerMixinCompat(Context context, SuggestionControllerHost host, + Lifecycle lifecycle, ComponentName componentName) { + mContext = context.getApplicationContext(); + mHost = host; + mSuggestionController = new SuggestionController(mContext, componentName, + this /* serviceConnectionListener */); + if (lifecycle != null) { + lifecycle.addObserver(this); + } + } + + @OnLifecycleEvent(Lifecycle.Event.ON_START) + public void onStart() { + if (DEBUG) { + Log.d(TAG, "SuggestionController started"); + } + mSuggestionController.start(); + } + + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) + public void onStop() { + if (DEBUG) { + Log.d(TAG, "SuggestionController stopped."); + } + mSuggestionController.stop(); + } + + @Override + public void onServiceConnected() { + final LoaderManager loaderManager = mHost.getLoaderManager(); + if (loaderManager != null) { + loaderManager.restartLoader(SuggestionLoader.LOADER_ID_SUGGESTIONS, + null /* args */, this /* callback */); + } + } + + @Override + public void onServiceDisconnected() { + if (DEBUG) { + Log.d(TAG, "SuggestionService disconnected"); + } + final LoaderManager loaderManager = mHost.getLoaderManager(); + if (loaderManager != null) { + loaderManager.destroyLoader(SuggestionLoader.LOADER_ID_SUGGESTIONS); + } + } + + @Override + public Loader<List<Suggestion>> onCreateLoader(int id, Bundle args) { + if (id == SuggestionLoader.LOADER_ID_SUGGESTIONS) { + mSuggestionLoaded = false; + return new SuggestionLoaderCompat(mContext, mSuggestionController); + } + throw new IllegalArgumentException("This loader id is not supported " + id); + } + + @Override + public void onLoadFinished(Loader<List<Suggestion>> loader, List<Suggestion> data) { + mSuggestionLoaded = true; + mHost.onSuggestionReady(data); + } + + @Override + public void onLoaderReset(Loader<List<Suggestion>> loader) { + mSuggestionLoaded = false; + } + + public boolean isSuggestionLoaded() { + return mSuggestionLoaded; + } + + public void dismissSuggestion(Suggestion suggestion) { + mSuggestionController.dismissSuggestions(suggestion); + } + + public void launchSuggestion(Suggestion suggestion) { + mSuggestionController.launchSuggestion(suggestion); + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionLoaderCompat.java b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionLoaderCompat.java new file mode 100644 index 000000000000..066de19172de --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionLoaderCompat.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.suggestions; + +import android.content.Context; +import android.service.settings.suggestions.Suggestion; +import android.util.Log; + +import com.android.settingslib.utils.AsyncLoaderCompat; + +import java.util.List; + +public class SuggestionLoaderCompat extends AsyncLoaderCompat<List<Suggestion>> { + + public static final int LOADER_ID_SUGGESTIONS = 42; + private static final String TAG = "SuggestionLoader"; + + private final SuggestionController mSuggestionController; + + public SuggestionLoaderCompat(Context context, SuggestionController controller) { + super(context); + mSuggestionController = controller; + } + + @Override + protected void onDiscardResult(List<Suggestion> result) { + + } + + @Override + public List<Suggestion> loadInBackground() { + final List<Suggestion> data = mSuggestionController.getSuggestions(); + if (data == null) { + Log.d(TAG, "data is null"); + } else { + Log.d(TAG, "data size " + data.size()); + } + return data; + } +}
\ No newline at end of file diff --git a/packages/SettingsLib/src/com/android/settingslib/utils/AsyncLoaderCompat.java b/packages/SettingsLib/src/com/android/settingslib/utils/AsyncLoaderCompat.java new file mode 100644 index 000000000000..916d7e312631 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/utils/AsyncLoaderCompat.java @@ -0,0 +1,111 @@ +/* + * 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.settingslib.utils; + +import android.content.Context; + +import androidx.loader.content.AsyncTaskLoader; + +/** + * This class fills in some boilerplate for AsyncTaskLoader to actually load things. + * + * Subclasses need to implement {@link AsyncLoaderCompat#loadInBackground()} to perform the actual + * background task, and {@link AsyncLoaderCompat#onDiscardResult(T)} to clean up previously loaded + * results. + * + * This loader is based on the MailAsyncTaskLoader from the AOSP EmailUnified repo. + * + * @param <T> the data type to be loaded. + */ +public abstract class AsyncLoaderCompat<T> extends AsyncTaskLoader<T> { + private T mResult; + + public AsyncLoaderCompat(final Context context) { + super(context); + } + + @Override + protected void onStartLoading() { + if (mResult != null) { + deliverResult(mResult); + } + + if (takeContentChanged() || mResult == null) { + forceLoad(); + } + } + + @Override + protected void onStopLoading() { + cancelLoad(); + } + + @Override + public void deliverResult(final T data) { + if (isReset()) { + if (data != null) { + onDiscardResult(data); + } + return; + } + + final T oldResult = mResult; + mResult = data; + + if (isStarted()) { + super.deliverResult(data); + } + + if (oldResult != null && oldResult != mResult) { + onDiscardResult(oldResult); + } + } + + @Override + protected void onReset() { + super.onReset(); + + onStopLoading(); + + if (mResult != null) { + onDiscardResult(mResult); + } + mResult = null; + } + + @Override + public void onCanceled(final T data) { + super.onCanceled(data); + + if (data != null) { + onDiscardResult(data); + } + } + + /** + * Called when discarding the load results so subclasses can take care of clean-up or + * recycling tasks. This is not called if the same result (by way of pointer equality) is + * returned again by a subsequent call to loadInBackground, or if result is null. + * + * Note that this may be called concurrently with loadInBackground(), and in some circumstances + * may be called more than once for a given object. + * + * @param result The value returned from {@link AsyncLoaderCompat#loadInBackground()} which + * is to be discarded. + */ + protected abstract void onDiscardResult(T result); +} diff --git a/packages/SettingsLib/src/com/android/settingslib/widget/FooterPreferenceMixinCompat.java b/packages/SettingsLib/src/com/android/settingslib/widget/FooterPreferenceMixinCompat.java new file mode 100644 index 000000000000..260ac838cb32 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/widget/FooterPreferenceMixinCompat.java @@ -0,0 +1,72 @@ +/* + * 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.settingslib.widget; + +import android.content.Context; + +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.SetPreferenceScreen; + +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; + +public class FooterPreferenceMixinCompat implements LifecycleObserver, SetPreferenceScreen { + + private final PreferenceFragmentCompat mFragment; + private FooterPreference mFooterPreference; + + public FooterPreferenceMixinCompat(PreferenceFragmentCompat fragment, Lifecycle lifecycle) { + mFragment = fragment; + lifecycle.addObserver(this); + } + + @Override + public void setPreferenceScreen(PreferenceScreen preferenceScreen) { + if (mFooterPreference != null) { + preferenceScreen.addPreference(mFooterPreference); + } + } + + /** + * Creates a new {@link FooterPreference}. + */ + public FooterPreference createFooterPreference() { + final PreferenceScreen screen = mFragment.getPreferenceScreen(); + if (mFooterPreference != null && screen != null) { + screen.removePreference(mFooterPreference); + } + mFooterPreference = new FooterPreference(getPrefContext()); + + if (screen != null) { + screen.addPreference(mFooterPreference); + } + return mFooterPreference; + } + + /** + * Returns an UI context with theme properly set for new Preference objects. + */ + private Context getPrefContext() { + return mFragment.getPreferenceManager().getContext(); + } + + public boolean hasFooter() { + return mFooterPreference != null; + } +} + diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/CustomEditTextPreferenceComaptTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/CustomEditTextPreferenceComaptTest.java new file mode 100644 index 000000000000..9ba996752f49 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/CustomEditTextPreferenceComaptTest.java @@ -0,0 +1,77 @@ +/* + * 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.settingslib; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.view.View; +import android.widget.EditText; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.util.ReflectionHelpers; + +@RunWith(SettingsLibRobolectricTestRunner.class) +public class CustomEditTextPreferenceComaptTest { + + @Mock + private View mView; + + private TestPreference mPreference; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mPreference = new TestPreference(RuntimeEnvironment.application); + } + + @Test + public void bindDialogView_shouldRequestFocus() { + final String testText = ""; + final EditText editText = spy(new EditText(RuntimeEnvironment.application)); + editText.setText(testText); + when(mView.findViewById(android.R.id.edit)).thenReturn(editText); + + mPreference.onBindDialogView(mView); + + verify(editText).requestFocus(); + } + + @Test + public void getEditText_noDialog_shouldNotCrash() { + ReflectionHelpers.setField(mPreference, "mFragment", + mock(CustomEditTextPreferenceCompat.CustomPreferenceDialogFragment.class)); + + mPreference.getEditText(); + + // no crash + } + + private static class TestPreference extends CustomEditTextPreferenceCompat { + public TestPreference(Context context) { + super(context); + } + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/lifecycle/LifecycleTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/lifecycle/LifecycleTest.java index 52068e9842e5..28828a00cce6 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/lifecycle/LifecycleTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/lifecycle/LifecycleTest.java @@ -18,11 +18,12 @@ package com.android.settingslib.core.lifecycle; import static androidx.lifecycle.Lifecycle.Event.ON_START; import static com.google.common.truth.Truth.assertThat; -import androidx.lifecycle.LifecycleOwner; import android.content.Context; +import android.os.Bundle; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import android.widget.LinearLayout; import com.android.settingslib.SettingsLibRobolectricTestRunner; import com.android.settingslib.core.lifecycle.events.OnAttach; @@ -34,13 +35,15 @@ import com.android.settingslib.core.lifecycle.events.OnPrepareOptionsMenu; import com.android.settingslib.core.lifecycle.events.OnResume; import com.android.settingslib.core.lifecycle.events.OnStart; import com.android.settingslib.core.lifecycle.events.OnStop; +import com.android.settingslib.testutils.FragmentTestUtils; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.Robolectric; import org.robolectric.android.controller.ActivityController; -import org.robolectric.android.controller.FragmentController; + +import androidx.lifecycle.LifecycleOwner; @RunWith(SettingsLibRobolectricTestRunner.class) public class LifecycleTest { @@ -64,7 +67,7 @@ public class LifecycleTest { public TestFragment() { mFragObserver = new TestObserver(); - getLifecycle().addObserver(mFragObserver); + getSettingsLifecycle().addObserver(mFragObserver); } } @@ -74,9 +77,17 @@ public class LifecycleTest { public TestActivity() { mActObserver = new TestObserver(); - getLifecycle().addObserver(mActObserver); + getSettingsLifecycle().addObserver(mActObserver); } + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + LinearLayout view = new LinearLayout(this); + view.setId(1); + + setContentView(view); + } } public static class TestObserver implements LifecycleObserver, OnAttach, OnStart, OnResume, @@ -151,11 +162,9 @@ public class LifecycleTest { @Test public void runThroughActivityLifecycles_shouldObserveEverything() { ActivityController<TestActivity> ac = Robolectric.buildActivity(TestActivity.class); - TestActivity activity = ac.get(); + TestActivity activity = ac.setup().get(); - ac.start(); assertThat(activity.mActObserver.mOnStartObserved).isTrue(); - ac.resume(); assertThat(activity.mActObserver.mOnResumeObserved).isTrue(); activity.onCreateOptionsMenu(null); assertThat(activity.mActObserver.mOnCreateOptionsMenuObserved).isTrue(); @@ -173,50 +182,50 @@ public class LifecycleTest { @Test public void runThroughDialogFragmentLifecycles_shouldObserveEverything() { - FragmentController<TestDialogFragment> fragmentController = - Robolectric.buildFragment(TestDialogFragment.class); - TestDialogFragment fragment = fragmentController.get(); + final TestDialogFragment fragment = new TestDialogFragment(); + FragmentTestUtils.startFragment(fragment); - fragmentController.create().start().resume(); fragment.onCreateOptionsMenu(null, null); fragment.onPrepareOptionsMenu(null); fragment.onOptionsItemSelected(null); - fragmentController.pause().stop().destroy(); + assertThat(fragment.mFragObserver.mOnCreateOptionsMenuObserved).isTrue(); + assertThat(fragment.mFragObserver.mOnPrepareOptionsMenuObserved).isTrue(); + assertThat(fragment.mFragObserver.mOnOptionsItemSelectedObserved).isTrue(); assertThat(fragment.mFragObserver.mOnAttachObserved).isTrue(); assertThat(fragment.mFragObserver.mOnAttachHasContext).isTrue(); assertThat(fragment.mFragObserver.mOnStartObserved).isTrue(); assertThat(fragment.mFragObserver.mOnResumeObserved).isTrue(); + fragment.onPause(); assertThat(fragment.mFragObserver.mOnPauseObserved).isTrue(); + fragment.onStop(); assertThat(fragment.mFragObserver.mOnStopObserved).isTrue(); + fragment.onDestroy(); assertThat(fragment.mFragObserver.mOnDestroyObserved).isTrue(); - assertThat(fragment.mFragObserver.mOnCreateOptionsMenuObserved).isTrue(); - assertThat(fragment.mFragObserver.mOnPrepareOptionsMenuObserved).isTrue(); - assertThat(fragment.mFragObserver.mOnOptionsItemSelectedObserved).isTrue(); } @Test public void runThroughFragmentLifecycles_shouldObserveEverything() { - FragmentController<TestFragment> fragmentController = - Robolectric.buildFragment(TestFragment.class); - TestFragment fragment = fragmentController.get(); + final TestFragment fragment = new TestFragment(); + FragmentTestUtils.startFragment(fragment); - fragmentController.create().start().resume(); fragment.onCreateOptionsMenu(null, null); fragment.onPrepareOptionsMenu(null); fragment.onOptionsItemSelected(null); - fragmentController.pause().stop().destroy(); + assertThat(fragment.mFragObserver.mOnCreateOptionsMenuObserved).isTrue(); + assertThat(fragment.mFragObserver.mOnPrepareOptionsMenuObserved).isTrue(); + assertThat(fragment.mFragObserver.mOnOptionsItemSelectedObserved).isTrue(); assertThat(fragment.mFragObserver.mOnAttachObserved).isTrue(); assertThat(fragment.mFragObserver.mOnAttachHasContext).isTrue(); assertThat(fragment.mFragObserver.mOnStartObserved).isTrue(); assertThat(fragment.mFragObserver.mOnResumeObserved).isTrue(); + fragment.onPause(); assertThat(fragment.mFragObserver.mOnPauseObserved).isTrue(); + fragment.onStop(); assertThat(fragment.mFragObserver.mOnStopObserved).isTrue(); + fragment.onDestroy(); assertThat(fragment.mFragObserver.mOnDestroyObserved).isTrue(); - assertThat(fragment.mFragObserver.mOnCreateOptionsMenuObserved).isTrue(); - assertThat(fragment.mFragObserver.mOnPrepareOptionsMenuObserved).isTrue(); - assertThat(fragment.mFragObserver.mOnOptionsItemSelectedObserved).isTrue(); } @Test @@ -237,17 +246,16 @@ public class LifecycleTest { @Test public void onOptionItemSelectedShortCircuitsIfAnObserverHandlesTheMenuItem() { - FragmentController<TestFragment> fragmentController = - Robolectric.buildFragment(TestFragment.class); - TestFragment fragment = fragmentController.get(); - OptionItemAccepter accepter = new OptionItemAccepter(); + final TestFragment fragment = new TestFragment(); + FragmentTestUtils.startFragment(fragment); + + final OptionItemAccepter accepter = new OptionItemAccepter(); fragment.getLifecycle().addObserver(accepter); - fragmentController.create().start().resume(); + fragment.onCreateOptionsMenu(null, null); fragment.onPrepareOptionsMenu(null); fragment.onOptionsItemSelected(null); - fragmentController.pause().stop().destroy(); assertThat(accepter.wasCalled).isFalse(); } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeUtilCompatTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeUtilCompatTest.java new file mode 100644 index 000000000000..ddadac105c18 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeUtilCompatTest.java @@ -0,0 +1,271 @@ +/* + * 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.settingslib.inputmethod; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.pm.ApplicationInfo; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodSubtype; +import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; + +@RunWith(RobolectricTestRunner.class) +public class InputMethodAndSubtypeUtilCompatTest { + + private static final HashSet<String> EMPTY_STRING_SET = new HashSet<>(); + + private static HashSet<String> asHashSet(String... strings) { + HashSet<String> hashSet = new HashSet<>(); + for (String s : strings) { + hashSet.add(s); + } + return hashSet; + } + + @Test + public void parseInputMethodsAndSubtypesString_EmptyString() { + assertThat(InputMethodAndSubtypeUtilCompat. + parseInputMethodsAndSubtypesString("")).isEmpty(); + assertThat(InputMethodAndSubtypeUtilCompat. + parseInputMethodsAndSubtypesString(null)).isEmpty(); + } + + @Test + public void parseInputMethodsAndSubtypesString_SingleImeNoSubtype() { + HashMap<String, HashSet<String>> r = + InputMethodAndSubtypeUtilCompat.parseInputMethodsAndSubtypesString("ime0"); + assertThat(r).containsExactly("ime0", EMPTY_STRING_SET); + } + + @Test + public void parseInputMethodsAndSubtypesString_MultipleImesNoSubtype() { + HashMap<String, HashSet<String>> r = + InputMethodAndSubtypeUtilCompat.parseInputMethodsAndSubtypesString("ime0:ime1"); + assertThat(r).containsExactly("ime0", EMPTY_STRING_SET, "ime1", EMPTY_STRING_SET); + } + + @Test + public void parseInputMethodsAndSubtypesString_SingleImeSingleSubtype() { + HashMap<String, HashSet<String>> r = + InputMethodAndSubtypeUtilCompat.parseInputMethodsAndSubtypesString("ime0;subtype0"); + assertThat(r).containsExactly("ime0", asHashSet("subtype0")); + } + + @Test + public void parseInputMethodsAndSubtypesString_SingleImeDuplicateSameSubtypes() { + HashMap<String, HashSet<String>> r = + InputMethodAndSubtypeUtilCompat.parseInputMethodsAndSubtypesString( + "ime0;subtype0;subtype0"); + assertThat(r).containsExactly("ime0", asHashSet("subtype0")); + } + + @Test + public void parseInputMethodsAndSubtypesString_SingleImeMultipleSubtypes() { + HashMap<String, HashSet<String>> r = + InputMethodAndSubtypeUtilCompat.parseInputMethodsAndSubtypesString( + "ime0;subtype0;subtype1"); + assertThat(r).containsExactly("ime0", asHashSet("subtype0", "subtype1")); + } + + @Test + public void parseInputMethodsAndSubtypesString_MultiplePairsOfImeSubtype() { + assertThat(InputMethodAndSubtypeUtilCompat.parseInputMethodsAndSubtypesString( + "ime0;subtype0:ime1;subtype1")) + .containsExactly("ime0", asHashSet("subtype0"), "ime1", asHashSet("subtype1")); + assertThat(InputMethodAndSubtypeUtilCompat.parseInputMethodsAndSubtypesString( + "ime0;subtype0;subtype1:ime1;subtype2")) + .containsExactly("ime0", asHashSet("subtype0", "subtype1"), + "ime1", asHashSet("subtype2")); + assertThat(InputMethodAndSubtypeUtilCompat.parseInputMethodsAndSubtypesString( + "ime0;subtype0;subtype1:ime1;subtype1;subtype2")) + .containsExactly("ime0", asHashSet("subtype0", "subtype1"), + "ime1", asHashSet("subtype1", "subtype2")); + + } + + @Test + public void parseInputMethodsAndSubtypesString_MixedImeSubtypePairsAndImeNoSubtype() { + HashMap<String, HashSet<String>> r = + InputMethodAndSubtypeUtilCompat.parseInputMethodsAndSubtypesString( + "ime0;subtype0;subtype1:ime1;subtype1;subtype2:ime2"); + assertThat(r).containsExactly("ime0", asHashSet("subtype0", "subtype1"), + "ime1", asHashSet("subtype1", "subtype2"), + "ime2", EMPTY_STRING_SET); + } + + @Test + public void buildInputMethodsAndSubtypesString_EmptyInput() { + HashMap<String, HashSet<String>> map = new HashMap<>(); + assertThat(map).isEmpty(); + } + + @Test + public void buildInputMethodsAndSubtypesString_SingleIme() { + HashMap<String, HashSet<String>> map = new HashMap<>(); + map.put("ime0", new HashSet<>()); + String result = InputMethodAndSubtypeUtilCompat.buildInputMethodsAndSubtypesString(map); + assertThat(result).isEqualTo("ime0"); + } + + @Test + public void buildInputMethodsAndSubtypesString_SingleImeSingleSubtype() { + HashMap<String, HashSet<String>> map = new HashMap<>(); + map.put("ime0", asHashSet("subtype0")); + String result = InputMethodAndSubtypeUtilCompat.buildInputMethodsAndSubtypesString(map); + assertThat(result).isEqualTo("ime0;subtype0"); + } + + @Test + public void buildInputMethodsAndSubtypesString_SingleImeMultipleSubtypes() { + HashMap<String, HashSet<String>> map = new HashMap<>(); + map.put("ime0", asHashSet("subtype0", "subtype1")); + String result = InputMethodAndSubtypeUtilCompat.buildInputMethodsAndSubtypesString(map); + + // We do not expect what order will be used to concatenate items in + // InputMethodAndSubtypeUtil.buildInputMethodsAndSubtypesString() hence accept all possible + // permutations here. + assertThat(result).matches("ime0;subtype0;subtype1|ime0;subtype1;subtype0"); + } + + @Test + public void buildInputMethodsAndSubtypesString_MultipleImesNoSubtypes() { + HashMap<String, HashSet<String>> map = new HashMap<>(); + map.put("ime0", EMPTY_STRING_SET); + map.put("ime1", EMPTY_STRING_SET); + String result = InputMethodAndSubtypeUtilCompat.buildInputMethodsAndSubtypesString(map); + + // We do not expect what order will be used to concatenate items in + // InputMethodAndSubtypeUtil.buildInputMethodsAndSubtypesString() hence accept all possible + // permutations here. + assertThat(result).matches("ime0:ime1|ime1:ime0"); + } + + @Test + public void buildInputMethodsAndSubtypesString_MultipleImesWithAndWithoutSubtypes() { + HashMap<String, HashSet<String>> map = new HashMap<>(); + map.put("ime0", asHashSet("subtype0", "subtype1")); + map.put("ime1", EMPTY_STRING_SET); + String result = InputMethodAndSubtypeUtilCompat.buildInputMethodsAndSubtypesString(map); + + // We do not expect what order will be used to concatenate items in + // InputMethodAndSubtypeUtil.buildInputMethodsAndSubtypesString() hence accept all possible + // permutations here. + assertThat(result).matches("ime0;subtype0;subtype1:ime1|ime0;subtype1;subtype0:ime1" + + "|ime1:ime0;subtype0;subtype1|ime1:ime0;subtype1;subtype0"); + } + + @Test + public void buildInputMethodsAndSubtypesString_MultipleImesWithSubtypes() { + HashMap<String, HashSet<String>> map = new HashMap<>(); + map.put("ime0", asHashSet("subtype0", "subtype1")); + map.put("ime1", asHashSet("subtype2", "subtype3")); + String result = InputMethodAndSubtypeUtilCompat.buildInputMethodsAndSubtypesString(map); + + // We do not expect what order will be used to concatenate items in + // InputMethodAndSubtypeUtil.buildInputMethodsAndSubtypesString() hence accept all possible + // permutations here. + assertThat(result).matches("ime0;subtype0;subtype1:ime1;subtype2;subtype3" + + "|ime0;subtype1;subtype0:ime1;subtype2;subtype3" + + "|ime0;subtype0;subtype1:ime1;subtype3;subtype2" + + "|ime0;subtype1;subtype0:ime1;subtype3;subtype2" + + "|ime1;subtype2;subtype3:ime0;subtype0;subtype1" + + "|ime2;subtype3;subtype2:ime0;subtype0;subtype1" + + "|ime3;subtype2;subtype3:ime0;subtype1;subtype0" + + "|ime4;subtype3;subtype2:ime0;subtype1;subtype0"); + } + + @Test + public void isValidSystemNonAuxAsciiCapableIme() { + // System IME w/ no subtype + assertThat(InputMethodAndSubtypeUtilCompat.isValidSystemNonAuxAsciiCapableIme( + createDummyIme(true, false))) + .isFalse(); + + // System IME w/ non-Aux and non-ASCII-capable "keyboard" subtype + assertThat(InputMethodAndSubtypeUtilCompat.isValidSystemNonAuxAsciiCapableIme( + createDummyIme(true, false, createDummySubtype("keyboard", false, false)))) + .isFalse(); + + // System IME w/ non-Aux and ASCII-capable "keyboard" subtype + assertThat(InputMethodAndSubtypeUtilCompat.isValidSystemNonAuxAsciiCapableIme( + createDummyIme(true, false, createDummySubtype("keyboard", false, true)))) + .isTrue(); + + // System IME w/ Aux and ASCII-capable "keyboard" subtype + assertThat(InputMethodAndSubtypeUtilCompat.isValidSystemNonAuxAsciiCapableIme( + createDummyIme(true, true, createDummySubtype("keyboard", true, true)))) + .isFalse(); + + // System IME w/ non-Aux and ASCII-capable "voice" subtype + assertThat(InputMethodAndSubtypeUtilCompat.isValidSystemNonAuxAsciiCapableIme( + createDummyIme(true, false, createDummySubtype("voice", false, true)))) + .isFalse(); + + // System IME w/ non-Aux and non-ASCII-capable subtype + Non-Aux and ASCII-capable subtype + assertThat(InputMethodAndSubtypeUtilCompat.isValidSystemNonAuxAsciiCapableIme( + createDummyIme(true, false, + createDummySubtype("keyboard", false, true), + createDummySubtype("keyboard", false, false)))) + .isTrue(); + + // Non-system IME w/ non-Aux and ASCII-capable "keyboard" subtype + assertThat(InputMethodAndSubtypeUtilCompat.isValidSystemNonAuxAsciiCapableIme( + createDummyIme(false, false, createDummySubtype("keyboard", false, true)))) + .isFalse(); + } + + private static InputMethodInfo createDummyIme(boolean isSystem, boolean isAuxIme, + InputMethodSubtype... subtypes) { + final ResolveInfo ri = new ResolveInfo(); + final ServiceInfo si = new ServiceInfo(); + final ApplicationInfo ai = new ApplicationInfo(); + ai.packageName = "com.example.android.dummyime"; + ai.enabled = true; + ai.flags |= (isSystem ? ApplicationInfo.FLAG_SYSTEM : 0); + si.applicationInfo = ai; + si.enabled = true; + si.packageName = "com.example.android.dummyime"; + si.name = "Dummy IME"; + si.exported = true; + si.nonLocalizedLabel = "Dummy IME"; + ri.serviceInfo = si; + return new InputMethodInfo(ri, isAuxIme, "", Arrays.asList(subtypes), 1, false); + } + + private static InputMethodSubtype createDummySubtype( + String mode, boolean isAuxiliary, boolean isAsciiCapable) { + return new InputMethodSubtypeBuilder() + .setSubtypeNameResId(0) + .setSubtypeIconResId(0) + .setSubtypeLocale("en_US") + .setLanguageTag("en-US") + .setSubtypeMode(mode) + .setIsAuxiliary(isAuxiliary) + .setIsAsciiCapable(isAsciiCapable) + .build(); + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/license/LicenseHtmlLoaderCompatTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/license/LicenseHtmlLoaderCompatTest.java new file mode 100644 index 000000000000..f981f365ec2b --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/license/LicenseHtmlLoaderCompatTest.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.license; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.content.Context; + +import com.android.settingslib.SettingsLibRobolectricTestRunner; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.File; +import java.util.ArrayList; + +@RunWith(SettingsLibRobolectricTestRunner.class) +public class LicenseHtmlLoaderCompatTest { + @Mock + private Context mContext; + + LicenseHtmlLoaderCompat newLicenseHtmlLoader(ArrayList<File> xmlFiles, + File cachedHtmlFile, boolean isCachedHtmlFileOutdated, + boolean generateHtmlFileSucceeded) { + LicenseHtmlLoaderCompat loader = spy(new LicenseHtmlLoaderCompat(mContext)); + doReturn(xmlFiles).when(loader).getVaildXmlFiles(); + doReturn(cachedHtmlFile).when(loader).getCachedHtmlFile(); + doReturn(isCachedHtmlFileOutdated).when(loader).isCachedHtmlFileOutdated(any(), any()); + doReturn(generateHtmlFileSucceeded).when(loader).generateHtmlFile(any(), any()); + return loader; + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testLoadInBackground() { + ArrayList<File> xmlFiles = new ArrayList(); + xmlFiles.add(new File("test.xml")); + File cachedHtmlFile = new File("test.html"); + + LicenseHtmlLoaderCompat loader = newLicenseHtmlLoader(xmlFiles, cachedHtmlFile, true, true); + + assertThat(loader.loadInBackground()).isEqualTo(cachedHtmlFile); + verify(loader).generateHtmlFile(any(), any()); + } + + @Test + public void testLoadInBackgroundWithNoVaildXmlFiles() { + ArrayList<File> xmlFiles = new ArrayList(); + File cachedHtmlFile = new File("test.html"); + + LicenseHtmlLoaderCompat loader = newLicenseHtmlLoader(xmlFiles, cachedHtmlFile, true, true); + + assertThat(loader.loadInBackground()).isNull(); + verify(loader, never()).generateHtmlFile(any(), any()); + } + + @Test + public void testLoadInBackgroundWithNonOutdatedCachedHtmlFile() { + ArrayList<File> xmlFiles = new ArrayList(); + xmlFiles.add(new File("test.xml")); + File cachedHtmlFile = new File("test.html"); + + LicenseHtmlLoaderCompat loader = newLicenseHtmlLoader(xmlFiles, cachedHtmlFile, false, + true); + + assertThat(loader.loadInBackground()).isEqualTo(cachedHtmlFile); + verify(loader, never()).generateHtmlFile(any(), any()); + } + + @Test + public void testLoadInBackgroundWithGenerateHtmlFileFailed() { + ArrayList<File> xmlFiles = new ArrayList(); + xmlFiles.add(new File("test.xml")); + File cachedHtmlFile = new File("test.html"); + + LicenseHtmlLoaderCompat loader = newLicenseHtmlLoader(xmlFiles, cachedHtmlFile, true, + false); + + assertThat(loader.loadInBackground()).isNull(); + verify(loader).generateHtmlFile(any(), any()); + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/suggestions/SuggestionControllerMixinCompatTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/suggestions/SuggestionControllerMixinCompatTest.java new file mode 100644 index 000000000000..1ee3afa9c1ce --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/suggestions/SuggestionControllerMixinCompatTest.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 com.android.settingslib.suggestions; + +import static androidx.lifecycle.Lifecycle.Event.ON_START; +import static androidx.lifecycle.Lifecycle.Event.ON_STOP; +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.ComponentName; +import android.content.Context; + +import com.android.settingslib.SettingsLibRobolectricTestRunner; +import com.android.settingslib.core.lifecycle.Lifecycle; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import androidx.lifecycle.LifecycleOwner; +import androidx.loader.app.LoaderManager; + +@RunWith(SettingsLibRobolectricTestRunner.class) +@Config(shadows = ShadowSuggestionController.class) +public class SuggestionControllerMixinCompatTest { + + @Mock + private SuggestionControllerMixinCompat.SuggestionControllerHost mHost; + + private Context mContext; + private LifecycleOwner mLifecycleOwner; + private Lifecycle mLifecycle; + private SuggestionControllerMixinCompat mMixin; + private ComponentName mComponentName; + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + mLifecycleOwner = () -> mLifecycle; + mLifecycle = new Lifecycle(mLifecycleOwner); + mComponentName = new ComponentName( + "com.android.settings.intelligence", + "com.android.settings.intelligence.suggestions.SuggestionService"); + } + + @After + public void tearDown() { + ShadowSuggestionController.reset(); + } + + @Test + public void goThroughLifecycle_onStartStop_shouldStartStopController() { + mMixin = new SuggestionControllerMixinCompat(mContext, mHost, mLifecycle, mComponentName); + + mLifecycle.handleLifecycleEvent(ON_START); + assertThat(ShadowSuggestionController.sStartCalled).isTrue(); + + mLifecycle.handleLifecycleEvent(ON_STOP); + assertThat(ShadowSuggestionController.sStopCalled).isTrue(); + } + + @Test + public void onServiceConnected_shouldGetSuggestion() { + final LoaderManager loaderManager = mock(LoaderManager.class); + when(mHost.getLoaderManager()).thenReturn(loaderManager); + + mMixin = new SuggestionControllerMixinCompat(mContext, mHost, mLifecycle, mComponentName); + mMixin.onServiceConnected(); + + verify(loaderManager).restartLoader(SuggestionLoader.LOADER_ID_SUGGESTIONS, + null /* args */, mMixin /* callback */); + } + + @Test + public void onServiceConnected_hostNotAttached_shouldDoNothing() { + when(mHost.getLoaderManager()).thenReturn(null); + + mMixin = new SuggestionControllerMixinCompat(mContext, mHost, mLifecycle, mComponentName); + mMixin.onServiceConnected(); + + verify(mHost).getLoaderManager(); + } + + @Test + public void onServiceDisconnected_hostNotAttached_shouldDoNothing() { + when(mHost.getLoaderManager()).thenReturn(null); + + mMixin = new SuggestionControllerMixinCompat(mContext, mHost, mLifecycle, mComponentName); + mMixin.onServiceDisconnected(); + + verify(mHost).getLoaderManager(); + } + + @Test + public void doneLoadingg_shouldSetSuggestionLoaded() { + mMixin = new SuggestionControllerMixinCompat(mContext, mHost, mLifecycle, mComponentName); + + mMixin.onLoadFinished(mock(SuggestionLoaderCompat.class), null); + + assertThat(mMixin.isSuggestionLoaded()).isTrue(); + + mMixin.onLoaderReset(mock(SuggestionLoaderCompat.class)); + + assertThat(mMixin.isSuggestionLoaded()).isFalse(); + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/testutils/FragmentTestUtils.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/testutils/FragmentTestUtils.java new file mode 100644 index 000000000000..8ba8606b8e08 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/testutils/FragmentTestUtils.java @@ -0,0 +1,74 @@ +/* + * 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.settingslib.testutils; + +import android.os.Bundle; +import android.widget.LinearLayout; + +import org.robolectric.Robolectric; + +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; + +/** + * Utilities for creating Fragments for testing. + * <p> + * TODO(b/111195449) - Duplicated from org.robolectric.shadows.support.v4.SupportFragmentTestUtil + */ +@Deprecated +public class FragmentTestUtils { + + public static void startFragment(Fragment fragment) { + buildFragmentManager(FragmentUtilActivity.class) + .beginTransaction().add(fragment, null).commit(); + } + + public static void startFragment(Fragment fragment, + Class<? extends FragmentActivity> activityClass) { + buildFragmentManager(activityClass) + .beginTransaction().add(fragment, null).commit(); + } + + public static void startVisibleFragment(Fragment fragment) { + buildFragmentManager(FragmentUtilActivity.class) + .beginTransaction().add(1, fragment, null).commit(); + } + + public static void startVisibleFragment(Fragment fragment, + Class<? extends FragmentActivity> activityClass, int containerViewId) { + buildFragmentManager(activityClass) + .beginTransaction().add(containerViewId, fragment, null).commit(); + } + + private static FragmentManager buildFragmentManager( + Class<? extends FragmentActivity> activityClass) { + FragmentActivity activity = Robolectric.setupActivity(activityClass); + return activity.getSupportFragmentManager(); + } + + private static class FragmentUtilActivity extends FragmentActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + LinearLayout view = new LinearLayout(this); + view.setId(1); + + setContentView(view); + } + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/FooterPreferenceMixinCompatTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/FooterPreferenceMixinCompatTest.java new file mode 100644 index 000000000000..1abbaba441ee --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/FooterPreferenceMixinCompatTest.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.widget; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.android.settingslib.SettingsLibRobolectricTestRunner; +import com.android.settingslib.core.lifecycle.Lifecycle; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.shadows.ShadowApplication; + +import androidx.lifecycle.LifecycleOwner; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; + +@RunWith(SettingsLibRobolectricTestRunner.class) +public class FooterPreferenceMixinCompatTest { + + @Mock + private PreferenceFragmentCompat mFragment; + @Mock + private PreferenceScreen mScreen; + + private LifecycleOwner mLifecycleOwner; + private Lifecycle mLifecycle; + private FooterPreferenceMixinCompat mMixin; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mLifecycleOwner = () -> mLifecycle; + mLifecycle = new Lifecycle(mLifecycleOwner); + when(mFragment.getPreferenceManager()).thenReturn(mock(PreferenceManager.class)); + when(mFragment.getPreferenceManager().getContext()) + .thenReturn(ShadowApplication.getInstance().getApplicationContext()); + mMixin = new FooterPreferenceMixinCompat(mFragment, mLifecycle); + } + + @Test + public void createFooter_screenNotAvailable_noCrash() { + assertThat(mMixin.createFooterPreference()).isNotNull(); + } + + @Test + public void createFooter_screenAvailable_canAttachToScreen() { + when(mFragment.getPreferenceScreen()).thenReturn(mScreen); + + final FooterPreference preference = mMixin.createFooterPreference(); + + assertThat(preference).isNotNull(); + verify(mScreen).addPreference(preference); + } + + @Test + public void createFooter_screenAvailableDelayed_canAttachToScreen() { + final FooterPreference preference = mMixin.createFooterPreference(); + + mLifecycle.setPreferenceScreen(mScreen); + + assertThat(preference).isNotNull(); + verify(mScreen).addPreference(preference); + } + + @Test + public void createFooterTwice_screenAvailable_replaceOldFooter() { + when(mFragment.getPreferenceScreen()).thenReturn(mScreen); + + mMixin.createFooterPreference(); + mMixin.createFooterPreference(); + + verify(mScreen).removePreference(any(FooterPreference.class)); + verify(mScreen, times(2)).addPreference(any(FooterPreference.class)); + } + +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/FooterPreferenceMixinTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/FooterPreferenceMixinTest.java index 78b7616716f5..8604d186f2dd 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/FooterPreferenceMixinTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/FooterPreferenceMixinTest.java @@ -23,11 +23,6 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import androidx.lifecycle.LifecycleOwner; -import androidx.preference.PreferenceFragment; -import androidx.preference.PreferenceManager; -import androidx.preference.PreferenceScreen; - import com.android.settingslib.SettingsLibRobolectricTestRunner; import com.android.settingslib.core.lifecycle.Lifecycle; @@ -38,6 +33,11 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.shadows.ShadowApplication; +import androidx.lifecycle.LifecycleOwner; +import androidx.preference.PreferenceFragment; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; + @RunWith(SettingsLibRobolectricTestRunner.class) public class FooterPreferenceMixinTest { diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index 375fef8ac3f9..9592b63c6d1e 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -2935,7 +2935,7 @@ public class SettingsProvider extends ContentProvider { } private final class UpgradeController { - private static final int SETTINGS_VERSION = 169; + private static final int SETTINGS_VERSION = 170; private final int mUserId; @@ -3810,6 +3810,37 @@ public class SettingsProvider extends ContentProvider { currentVersion = 169; } + if (currentVersion == 169) { + // Version 169: by default, add STREAM_VOICE_CALL to list of streams that can + // be muted. + final SettingsState systemSettings = getSystemSettingsLocked(userId); + final Setting currentSetting = systemSettings.getSettingLocked( + Settings.System.MUTE_STREAMS_AFFECTED); + if (!currentSetting.isNull()) { + try { + int currentSettingIntegerValue = Integer.parseInt( + currentSetting.getValue()); + if ((currentSettingIntegerValue + & (1 << AudioManager.STREAM_VOICE_CALL)) == 0) { + systemSettings.insertSettingLocked( + Settings.System.MUTE_STREAMS_AFFECTED, + Integer.toString( + currentSettingIntegerValue + | (1 << AudioManager.STREAM_VOICE_CALL)), + null, true, SettingsState.SYSTEM_PACKAGE_NAME); + } + } catch (NumberFormatException e) { + // remove the setting in case it is not a valid integer + Slog.w("Failed to parse integer value of MUTE_STREAMS_AFFECTED" + + "setting, removing setting", e); + systemSettings.deleteSettingLocked( + Settings.System.MUTE_STREAMS_AFFECTED); + } + + } + currentVersion = 170; + } + // vXXX: Add new settings above this point. if (currentVersion != newVersion) { diff --git a/packages/SystemUI/res/color/notification_guts_buttons.xml b/packages/SystemUI/res/color/notification_guts_buttons.xml index 3b8d59bae744..412e0be65cf7 100644 --- a/packages/SystemUI/res/color/notification_guts_buttons.xml +++ b/packages/SystemUI/res/color/notification_guts_buttons.xml @@ -2,6 +2,6 @@ <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_checked="true" android:color="?android:attr/colorAccent" /> - <item android:color="@android:color/black" + <item android:color="@color/notification_primary_text_color" android:alpha=".54" /> </selector>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/notification_snooze.xml b/packages/SystemUI/res/layout/notification_snooze.xml index ea6ef4cc0012..55e7a8526e1a 100644 --- a/packages/SystemUI/res/layout/notification_snooze.xml +++ b/packages/SystemUI/res/layout/notification_snooze.xml @@ -36,7 +36,7 @@ android:layout_alignParentStart="true" android:layout_centerVertical="true" android:paddingStart="@*android:dimen/notification_content_margin_start" - android:textColor="#DD000000" + android:textColor="@color/notification_primary_text_color" android:paddingEnd="4dp"/> <ImageView diff --git a/packages/SystemUI/res/values-night/colors.xml b/packages/SystemUI/res/values-night/colors.xml new file mode 100644 index 000000000000..45d218548d1a --- /dev/null +++ b/packages/SystemUI/res/values-night/colors.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + + NOTE: You might also want to edit: core/res/res/values-night/*.xml + --> +<resources> + <!-- The color of the material notification background --> + <color name="notification_material_background_color">@*android:color/notification_material_background_color</color> + + <!-- The color of the legacy notifications with customs backgrounds (gingerbread and lollipop.) + It's fine to override this color since at that point the shade was dark. --> + <color name="notification_legacy_background_color">@*android:color/notification_material_background_color</color> + + <!-- The color of the material notification background when dimmed --> + <color name="notification_material_background_dimmed_color">#aa212121</color> + + <!-- The color of the dividing line between grouped notifications while . --> + <color name="notification_divider_color">#000</color> + + <!-- The background color of the notification shade --> + <color name="notification_shade_background_color">#181818</color> + + <!-- The color of the ripples on the untinted notifications --> + <color name="notification_ripple_untinted_color">#30ffffff</color> + + <!-- The "inside" of a notification, reached via longpress --> + <color name="notification_guts_bg_color">@*android:color/notification_material_background_color</color> + + <!-- The color of the text inside a notification --> + <color name="notification_primary_text_color">@*android:color/notification_primary_text_color_dark</color> +</resources>
\ No newline at end of file diff --git a/packages/SystemUI/res/values-night/dimens.xml b/packages/SystemUI/res/values-night/dimens.xml new file mode 100644 index 000000000000..481483991de9 --- /dev/null +++ b/packages/SystemUI/res/values-night/dimens.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> + +<resources> + <!-- The height of the divider between the individual notifications. --> + <dimen name="notification_divider_height">1dp</dimen> +</resources>
\ No newline at end of file diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 07f1ee084183..4920fb2ea407 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -94,6 +94,9 @@ <!-- The color of the gear shown behind a notification --> <color name="notification_gear_color">#ff757575</color> + <!-- The color of the text inside a notification --> + <color name="notification_primary_text_color">@*android:color/notification_primary_text_color_light</color> + <!-- The "inside" of a notification, reached via longpress --> <color name="notification_guts_bg_color">#f8f9fa</color> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index f62249a74cdb..a9d995c889c7 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -252,6 +252,9 @@ --> <dimen name="qs_header_system_icons_area_height">48dp</dimen> + <!-- How far the quick-quick settings panel extends below the status bar --> + <dimen name="qs_quick_header_panel_height">128dp</dimen> + <!-- The height of the container that holds the system icons in the quick settings header in the car setting. --> <dimen name="car_qs_header_system_icons_area_height">54dp</dimen> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index f8670485caf7..5a03c4a37cad 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -452,23 +452,20 @@ <style name="TextAppearance.NotificationInfo"> <item name="android:fontFamily">sans-serif</item> - <item name="android:textColor">@android:color/black</item> + <item name="android:textColor">@color/notification_primary_text_color</item> </style> <style name="TextAppearance.NotificationInfo.Primary"> - <item name="android:textColor">?android:attr/textColorPrimary</item> <item name="android:textSize">16sp</item> <item name="android:alpha">0.87</item> </style> <style name="TextAppearance.NotificationInfo.Confirmation"> - <item name="android:textColor">?android:attr/textColorPrimary</item> <item name="android:textSize">14sp</item> <item name="android:alpha">0.87</item> </style> <style name="TextAppearance.NotificationInfo.Secondary"> - <item name="android:textColor">?android:attr/textColorPrimary</item> <item name="android:textSize">14sp</item> <item name="android:alpha">0.54</item> </style> diff --git a/packages/SystemUI/src/com/android/systemui/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/OverviewProxyService.java index 70ca055677e7..d21465c53bdf 100644 --- a/packages/SystemUI/src/com/android/systemui/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/OverviewProxyService.java @@ -30,6 +30,7 @@ import android.os.Looper; import android.os.PatternMatcher; import android.os.RemoteException; import android.os.UserHandle; +import android.provider.Settings; import android.util.Log; import android.view.SurfaceControl; @@ -391,14 +392,22 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println(TAG_OPS + " state:"); - pw.print(" mConnectionBackoffAttempts="); pw.println(mConnectionBackoffAttempts); + pw.print(" recentsComponentName="); pw.println(mRecentsComponentName); + pw.print(" isConnected="); pw.println(mOverviewProxy != null); pw.print(" isCurrentUserSetup="); pw.println(mDeviceProvisionedController .isCurrentUserSetup()); - pw.print(" isConnected="); pw.println(mOverviewProxy != null); - pw.print(" mRecentsComponentName="); pw.println(mRecentsComponentName); - pw.print(" mIsEnabled="); pw.println(isEnabled()); - pw.print(" mInteractionFlags="); pw.println(mInteractionFlags); - pw.print(" mQuickStepIntent="); pw.println(mQuickStepIntent); + pw.print(" connectionBackoffAttempts="); pw.println(mConnectionBackoffAttempts); + pw.print(" interactionFlags="); pw.println(mInteractionFlags); + + pw.print(" quickStepIntent="); pw.println(mQuickStepIntent); + pw.print(" quickStepIntentResolved="); pw.println(isEnabled()); + + final int swipeUpDefaultValue = mContext.getResources() + .getBoolean(com.android.internal.R.bool.config_swipe_up_gesture_default) ? 1 : 0; + final int swipeUpEnabled = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.SWIPE_UP_TO_SWITCH_APPS_ENABLED, swipeUpDefaultValue); + pw.print(" swipeUpSetting="); pw.println(swipeUpEnabled != 0); + pw.print(" swipeUpSettingDefault="); pw.println(swipeUpDefaultValue != 0); } public interface OverviewProxyListener { diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java index 6801e6917d3d..9a648d17c4ff 100644 --- a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java +++ b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java @@ -77,6 +77,7 @@ public class PowerUI extends SystemUI { private int mPlugType = 0; private int mInvalidCharger = 0; private EnhancedEstimates mEnhancedEstimates; + private Estimate mLastEstimate; private boolean mLowWarningShownThisChargeCycle; private boolean mSevereWarningShownThisChargeCycle; @@ -247,7 +248,8 @@ public class PowerUI extends SystemUI { // Show the correct version of low battery warning if needed ThreadUtils.postOnBackgroundThread(() -> { - maybeShowBatteryWarning(plugged, oldPlugged, oldBucket, bucket); + maybeShowBatteryWarning( + oldBatteryLevel, plugged, oldPlugged, oldBucket, bucket); }); } else if (Intent.ACTION_SCREEN_OFF.equals(action)) { @@ -262,14 +264,18 @@ public class PowerUI extends SystemUI { } } - protected void maybeShowBatteryWarning(boolean plugged, boolean oldPlugged, int oldBucket, - int bucket) { + protected void maybeShowBatteryWarning(int oldBatteryLevel, boolean plugged, boolean oldPlugged, + int oldBucket, int bucket) { boolean isPowerSaver = mPowerManager.isPowerSaveMode(); // only play SFX when the dialog comes up or the bucket changes final boolean playSound = bucket != oldBucket || oldPlugged; final boolean hybridEnabled = mEnhancedEstimates.isHybridNotificationEnabled(); if (hybridEnabled) { - final Estimate estimate = mEnhancedEstimates.getEstimate(); + Estimate estimate = mLastEstimate; + if (estimate == null || mBatteryLevel != oldBatteryLevel) { + estimate = mEnhancedEstimates.getEstimate(); + mLastEstimate = estimate; + } // Turbo is not always booted once SysUI is running so we have ot make sure we actually // get data back if (estimate != null) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java index a21886891e3d..35d2f90748c3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java @@ -33,6 +33,7 @@ import android.media.AudioManager; import android.os.Handler; import android.provider.AlarmClock; import android.service.notification.ZenModeConfig; +import android.widget.FrameLayout; import androidx.annotation.VisibleForTesting; import android.text.format.DateUtils; import android.util.AttributeSet; @@ -270,8 +271,22 @@ public class QuickStatusBarHeader extends RelativeLayout implements updateResources(); } + /** + * The height of QQS should always be the status bar height + 128dp. This is normally easy, but + * when there is a notch involved the status bar can remain a fixed pixel size. + */ + private void updateMinimumHeight() { + int sbHeight = mContext.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.status_bar_height); + int qqsHeight = mContext.getResources().getDimensionPixelSize( + R.dimen.qs_quick_header_panel_height); + + setMinimumHeight(sbHeight + qqsHeight); + } + private void updateResources() { Resources resources = mContext.getResources(); + updateMinimumHeight(); // Update height for a few views, especially due to landscape mode restricting space. mHeaderTextContainerView.getLayoutParams().height = @@ -282,10 +297,17 @@ public class QuickStatusBarHeader extends RelativeLayout implements com.android.internal.R.dimen.quick_qs_offset_height); mSystemIconsView.setLayoutParams(mSystemIconsView.getLayoutParams()); - getLayoutParams().height = resources.getDimensionPixelSize(mQsDisabled - ? com.android.internal.R.dimen.quick_qs_offset_height - : com.android.internal.R.dimen.quick_qs_total_height); - setLayoutParams(getLayoutParams()); + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); + if (mQsDisabled) { + lp.height = resources.getDimensionPixelSize( + com.android.internal.R.dimen.quick_qs_offset_height); + } else { + lp.height = Math.max(getMinimumHeight(), + resources.getDimensionPixelSize( + com.android.internal.R.dimen.quick_qs_offset_height)); + } + + setLayoutParams(lp); updateStatusIconAlphaAnimator(); updateHeaderTextContainerAlphaAnimator(); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java index 0ea941ff4f3e..5eaee5452ad5 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java @@ -301,11 +301,11 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); // Create a share action for the notification - PendingIntent shareAction = PendingIntent.getBroadcast(context, 0, + PendingIntent shareAction = PendingIntent.getBroadcastAsUser(context, 0, new Intent(context, GlobalScreenshot.ActionProxyReceiver.class) .putExtra(EXTRA_ACTION_INTENT, sharingChooserIntent) .putExtra(EXTRA_DISALLOW_ENTER_PIP, true), - PendingIntent.FLAG_CANCEL_CURRENT); + PendingIntent.FLAG_CANCEL_CURRENT, UserHandle.SYSTEM); Notification.Action.Builder shareActionBuilder = new Notification.Action.Builder( R.drawable.ic_screenshot_share, r.getString(com.android.internal.R.string.share), shareAction); @@ -324,11 +324,11 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { editIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); // Create a edit action - PendingIntent editAction = PendingIntent.getBroadcast(context, 1, + PendingIntent editAction = PendingIntent.getBroadcastAsUser(context, 1, new Intent(context, GlobalScreenshot.ActionProxyReceiver.class) .putExtra(EXTRA_ACTION_INTENT, editIntent) .putExtra(EXTRA_CANCEL_NOTIFICATION, editIntent.getComponent() != null), - PendingIntent.FLAG_CANCEL_CURRENT); + PendingIntent.FLAG_CANCEL_CURRENT, UserHandle.SYSTEM); Notification.Action.Builder editActionBuilder = new Notification.Action.Builder( R.drawable.ic_screenshot_edit, r.getString(com.android.internal.R.string.screenshot_edit), editAction); @@ -910,7 +910,7 @@ class GlobalScreenshot { ActivityOptions opts = ActivityOptions.makeBasic(); opts.setDisallowEnterPictureInPictureWhileLaunching( intent.getBooleanExtra(EXTRA_DISALLOW_ENTER_PIP, false)); - context.startActivityAsUser(actionIntent, opts.toBundle(),UserHandle.CURRENT); + context.startActivityAsUser(actionIntent, opts.toBundle(), UserHandle.CURRENT); }; StatusBar statusBar = SysUiServiceProvider.getComponent(context, StatusBar.class); statusBar.executeRunnableDismissingKeyguard(startActivityRunnable, null, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java index 6a387971500e..acdfa293ee6c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java @@ -98,8 +98,8 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView = new PathInterpolator(0.6f, 0, 0.5f, 1); private static final Interpolator ACTIVATE_INVERSE_ALPHA_INTERPOLATOR = new PathInterpolator(0, 0, 0.5f, 1); - private final int mTintedRippleColor; - protected final int mNormalRippleColor; + private int mTintedRippleColor; + protected int mNormalRippleColor; private final AccessibilityManager mAccessibilityManager; private final DoubleTapHelper mDoubleTapHelper; @@ -132,7 +132,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView private ValueAnimator mBackgroundColorAnimator; private float mAppearAnimationFraction = -1.0f; private float mAppearAnimationTranslation; - private final int mNormalColor; + private int mNormalColor; private boolean mIsBelowSpeedBump; private FalsingManager mFalsingManager; @@ -188,11 +188,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView mSlowOutLinearInInterpolator = new PathInterpolator(0.8f, 0.0f, 1.0f, 1.0f); setClipChildren(false); setClipToPadding(false); - mNormalColor = context.getColor(R.color.notification_material_background_color); - mTintedRippleColor = context.getColor( - R.color.notification_ripple_tinted_color); - mNormalRippleColor = context.getColor( - R.color.notification_ripple_untinted_color); + updateColors(); mFalsingManager = FalsingManager.getInstance(context); mAccessibilityManager = AccessibilityManager.getInstance(mContext); @@ -206,6 +202,16 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView initDimens(); } + private void updateColors() { + mNormalColor = mContext.getColor(R.color.notification_material_background_color); + mTintedRippleColor = mContext.getColor( + R.color.notification_ripple_tinted_color); + mNormalRippleColor = mContext.getColor( + R.color.notification_ripple_untinted_color); + mDimmedAlpha = Color.alpha(mContext.getColor( + R.color.notification_material_background_dimmed_color)); + } + private void initDimens() { mHeadsUpAddStartLocation = getResources().getDimensionPixelSize( com.android.internal.R.dimen.notification_content_margin_start); @@ -217,6 +223,12 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView initDimens(); } + public void onUiModeChanged() { + updateColors(); + initBackground(); + updateBackgroundTint(); + } + @Override protected void onFinishInflate() { super.onFinishInflate(); @@ -224,8 +236,6 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView mFakeShadow = findViewById(R.id.fake_shadow); mShadowHidden = mFakeShadow.getVisibility() != VISIBLE; mBackgroundDimmed = findViewById(R.id.backgroundDimmed); - mDimmedAlpha = Color.alpha(mContext.getColor( - R.color.notification_material_background_dimmed_color)); initBackground(); updateBackground(); updateBackgroundTint(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java index 9393d5b9be68..1d3f40894399 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java @@ -24,6 +24,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; import android.annotation.Nullable; +import android.app.Notification; import android.app.NotificationChannel; import android.content.Context; import android.content.pm.PackageInfo; @@ -1053,6 +1054,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView super.onDensityOrFontScaleChanged(); initDimens(); initBackground(); + reInflateViews(); + } + + private void reInflateViews() { // Let's update our childrencontainer. This is intentionally not guarded with // mIsSummaryWithChildren since we might have had children but not anymore. if (mChildrenContainer != null) { @@ -1079,7 +1084,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView l.initView(); l.reInflateViews(); } - mNotificationInflater.onDensityOrFontScaleChanged(); + mNotificationInflater.clearCachesAndReInflate(); onNotificationUpdated(); } @@ -1090,6 +1095,17 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } } + @Override + public void onUiModeChanged() { + super.onUiModeChanged(); + reInflateViews(); + if (mChildrenContainer != null) { + for (ExpandableNotificationRow child : mChildrenContainer.getNotificationChildren()) { + child.onUiModeChanged(); + } + } + } + public void setContentBackground(int customBackgroundColor, boolean animate, NotificationContentView notificationContentView) { if (getShowingLayout() == notificationContentView) { @@ -1448,6 +1464,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mNotificationInflater.setUsesIncreasedHeight(use); } + public void setSmartActions(List<Notification.Action> smartActions) { + mNotificationInflater.setSmartActions(smartActions); + } + public void setUseIncreasedHeadsUpHeight(boolean use) { mUseIncreasedHeadsUpHeight = use; mNotificationInflater.setUsesIncreasedHeadsUpHeight(use); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java index a58752c20973..93433da9574b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java @@ -28,6 +28,7 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR; import android.Manifest; +import android.annotation.NonNull; import android.app.AppGlobals; import android.app.Notification; import android.app.NotificationChannel; @@ -51,6 +52,8 @@ import android.view.View; import android.widget.ImageView; import android.widget.RemoteViews; +import androidx.annotation.Nullable; + import com.android.internal.annotations.VisibleForTesting; import com.android.internal.statusbar.StatusBarIcon; import com.android.internal.util.ArrayUtils; @@ -105,6 +108,8 @@ public class NotificationData { public CharSequence remoteInputText; public List<SnoozeCriterion> snoozeCriteria; public int userSentiment = Ranking.USER_SENTIMENT_NEUTRAL; + @NonNull + public List<Notification.Action> smartActions = Collections.emptyList(); private int mCachedContrastColor = COLOR_INVALID; private int mCachedContrastColorIsFor = COLOR_INVALID; @@ -131,8 +136,23 @@ public class NotificationData { private boolean hasSentReply; public Entry(StatusBarNotification n) { + this(n, null); + } + + public Entry(StatusBarNotification n, @Nullable Ranking ranking) { this.key = n.getKey(); this.notification = n; + if (ranking != null) { + populateFromRanking(ranking); + } + } + + public void populateFromRanking(@NonNull Ranking ranking) { + channel = ranking.getChannel(); + snoozeCriteria = ranking.getSnoozeCriteria(); + userSentiment = ranking.getUserSentiment(); + smartActions = ranking.getSmartActions() == null + ? Collections.emptyList() : ranking.getSmartActions(); } public void setInterruption() { @@ -232,6 +252,7 @@ public class NotificationData { /** * Update the notification icons. + * * @param context the context to create the icons with. * @param sbn the notification to read the icon from. * @throws InflationException @@ -291,7 +312,7 @@ public class NotificationData { } public void onInflationTaskFinished() { - mRunningTask = null; + mRunningTask = null; } @VisibleForTesting @@ -607,7 +628,7 @@ public class NotificationData { getRanking(key, mTmpRanking); return mTmpRanking.getOverrideGroupKey(); } - return null; + return null; } public List<SnoozeCriterion> getSnoozeCriteria(String key) { @@ -658,9 +679,7 @@ public class NotificationData { entry.notification.setOverrideGroupKey(overrideGroupKey); mGroupManager.onEntryUpdated(entry, oldSbn); } - entry.channel = getChannel(entry.key); - entry.snoozeCriteria = getSnoozeCriteria(entry.key); - entry.userSentiment = mTmpRanking.getUserSentiment(); + entry.populateFromRanking(mTmpRanking); } } } @@ -833,6 +852,7 @@ public class NotificationData { public boolean isNotificationForCurrentProfiles(StatusBarNotification sbn); public String getCurrentMediaNotificationKey(); public NotificationGroupManager getGroupManager(); + /** * @return true iff the device is dozing */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationEntryManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationEntryManager.java index 06f26c9cbc7c..bf0792923a22 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationEntryManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationEntryManager.java @@ -19,6 +19,7 @@ import static com.android.systemui.statusbar.NotificationRemoteInputManager.ENAB import static com.android.systemui.statusbar.NotificationRemoteInputManager .FORCE_REMOTE_INPUT_HISTORY; +import android.annotation.Nullable; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; @@ -37,6 +38,7 @@ import android.service.notification.NotificationListenerService; import android.service.notification.NotificationStats; import android.service.notification.StatusBarNotification; import android.text.TextUtils; +import android.util.ArrayMap; import android.util.ArraySet; import android.util.EventLog; import android.util.Log; @@ -726,10 +728,10 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater. && !mPresenter.isPresenterFullyCollapsed(); row.setUseIncreasedCollapsedHeight(useIncreasedCollapsedHeight); row.setUseIncreasedHeadsUpHeight(useIncreasedHeadsUp); + row.setSmartActions(entry.smartActions); row.updateNotification(entry); } - protected void addNotificationViews(NotificationData.Entry entry) { if (entry == null) { return; @@ -740,12 +742,13 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater. updateNotifications(); } - protected NotificationData.Entry createNotificationViews(StatusBarNotification sbn) + protected NotificationData.Entry createNotificationViews( + StatusBarNotification sbn, NotificationListenerService.Ranking ranking) throws InflationException { if (DEBUG) { - Log.d(TAG, "createNotificationViews(notification=" + sbn); + Log.d(TAG, "createNotificationViews(notification=" + sbn + " " + ranking); } - NotificationData.Entry entry = new NotificationData.Entry(sbn); + NotificationData.Entry entry = new NotificationData.Entry(sbn, ranking); Dependency.get(LeakDetector.class).trackInstance(entry); entry.createIcons(mContext, sbn); // Construct the expanded view. @@ -754,12 +757,14 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater. } private void addNotificationInternal(StatusBarNotification notification, - NotificationListenerService.RankingMap ranking) throws InflationException { + NotificationListenerService.RankingMap rankingMap) throws InflationException { String key = notification.getKey(); if (DEBUG) Log.d(TAG, "addNotification key=" + key); - mNotificationData.updateRanking(ranking); - NotificationData.Entry shadeEntry = createNotificationViews(notification); + mNotificationData.updateRanking(rankingMap); + NotificationListenerService.Ranking ranking = new NotificationListenerService.Ranking(); + rankingMap.getRanking(key, ranking); + NotificationData.Entry shadeEntry = createNotificationViews(notification, ranking); boolean isHeadsUped = shouldPeek(shadeEntry); if (!isHeadsUped && notification.getNotification().fullScreenIntent != null) { if (shouldSuppressFullScreenIntent(shadeEntry)) { @@ -905,11 +910,57 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater. mPresenter.updateNotificationViews(); } - public void updateNotificationRanking(NotificationListenerService.RankingMap ranking) { - mNotificationData.updateRanking(ranking); + public void updateNotificationRanking(NotificationListenerService.RankingMap rankingMap) { + List<NotificationData.Entry> entries = new ArrayList<>(); + entries.addAll(mNotificationData.getActiveNotifications()); + entries.addAll(mPendingNotifications.values()); + + // Has a copy of the current UI adjustments. + ArrayMap<String, NotificationUiAdjustment> oldAdjustments = new ArrayMap<>(); + for (NotificationData.Entry entry : entries) { + NotificationUiAdjustment adjustment = + NotificationUiAdjustment.extractFromNotificationEntry(entry); + oldAdjustments.put(entry.key, adjustment); + } + + // Populate notification entries from the new rankings. + mNotificationData.updateRanking(rankingMap); + updateRankingOfPendingNotifications(rankingMap); + + // By comparing the old and new UI adjustments, reinflate the view accordingly. + for (NotificationData.Entry entry : entries) { + NotificationUiAdjustment newAdjustment = + NotificationUiAdjustment.extractFromNotificationEntry(entry); + + if (NotificationUiAdjustment.needReinflate( + oldAdjustments.get(entry.key), newAdjustment)) { + if (entry.row != null) { + entry.reset(); + PackageManager pmUser = StatusBar.getPackageManagerForUser(mContext, + entry.notification.getUser().getIdentifier()); + updateNotification(entry, pmUser, entry.notification, entry.row); + } else { + // Once the RowInflaterTask is done, it will pick up the updated entry, so + // no-op here. + } + } + } + updateNotifications(); } + private void updateRankingOfPendingNotifications( + @Nullable NotificationListenerService.RankingMap rankingMap) { + if (rankingMap == null) { + return; + } + NotificationListenerService.Ranking tmpRanking = new NotificationListenerService.Ranking(); + for (NotificationData.Entry pendingNotification : mPendingNotifications.values()) { + rankingMap.getRanking(pendingNotification.key, tmpRanking); + pendingNotification.populateFromRanking(tmpRanking); + } + } + protected boolean shouldPeek(NotificationData.Entry entry) { return shouldPeek(entry, entry.notification); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationHeaderUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationHeaderUtil.java index 886d6f17fedb..e013ae723c22 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationHeaderUtil.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationHeaderUtil.java @@ -17,6 +17,7 @@ package com.android.systemui.statusbar; import android.app.Notification; +import android.content.res.Configuration; import android.graphics.PorterDuff; import android.graphics.drawable.Icon; import android.text.TextUtils; @@ -25,6 +26,8 @@ import android.view.View; import android.widget.ImageView; import android.widget.TextView; +import com.android.internal.util.ContrastColorUtil; + import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -74,8 +77,11 @@ public class NotificationHeaderUtil { imageView.getDrawable().mutate(); if (shouldApply) { // lets gray it out - int grey = view.getContext().getColor( - com.android.internal.R.color.notification_default_color_light); + Configuration config = view.getContext().getResources().getConfiguration(); + boolean inNightMode = (config.uiMode & Configuration.UI_MODE_NIGHT_MASK) + == Configuration.UI_MODE_NIGHT_YES; + int grey = ContrastColorUtil.resolveColor(view.getContext(), + Notification.COLOR_DEFAULT, inNightMode); imageView.getDrawable().setColorFilter(grey, PorterDuff.Mode.SRC_ATOP); } else { // lets reset it diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationUiAdjustment.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationUiAdjustment.java new file mode 100644 index 000000000000..e6bdb2631912 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationUiAdjustment.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 com.android.systemui.statusbar; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.Notification; +import android.app.RemoteInput; +import android.graphics.drawable.Icon; +import android.text.TextUtils; + +import androidx.annotation.VisibleForTesting; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * By diffing two entries, determines is view reinflation needed. + */ +public class NotificationUiAdjustment { + + public final String key; + public final List<Notification.Action> smartActions; + + @VisibleForTesting + NotificationUiAdjustment(String key, List<Notification.Action> smartActions) { + this.key = key; + this.smartActions = smartActions == null + ? Collections.emptyList() + : new ArrayList<>(smartActions); + } + + public static NotificationUiAdjustment extractFromNotificationEntry( + NotificationData.Entry entry) { + return new NotificationUiAdjustment(entry.key, entry.smartActions); + } + + public static boolean needReinflate( + @NonNull NotificationUiAdjustment oldAdjustment, + @NonNull NotificationUiAdjustment newAdjustment) { + if (oldAdjustment == newAdjustment) { + return false; + } + return areDifferent(oldAdjustment.smartActions, newAdjustment.smartActions); + } + + public static boolean areDifferent( + @NonNull List<Notification.Action> first, @NonNull List<Notification.Action> second) { + if (first == second) { + return false; + } + if (first == null || second == null) { + return true; + } + if (first.size() != second.size()) { + return true; + } + for (int i = 0; i < first.size(); i++) { + Notification.Action firstAction = first.get(i); + Notification.Action secondAction = second.get(i); + + if (!TextUtils.equals(firstAction.title, secondAction.title)) { + return true; + } + + if (areDifferent(firstAction.getIcon(), secondAction.getIcon())) { + return true; + } + + if (!Objects.equals(firstAction.actionIntent, secondAction.actionIntent)) { + return true; + } + + if (areDifferent(firstAction.getRemoteInputs(), secondAction.getRemoteInputs())) { + return true; + } + } + return false; + } + + private static boolean areDifferent(@Nullable Icon first, @Nullable Icon second) { + if (first == second) { + return false; + } + if (first == null || second == null) { + return true; + } + return !first.sameAs(second); + } + + private static boolean areDifferent( + @Nullable RemoteInput[] first, @Nullable RemoteInput[] second) { + if (first == second) { + return false; + } + if (first == null || second == null) { + return true; + } + if (first.length != second.length) { + return true; + } + for (int i = 0; i < first.length; i++) { + RemoteInput firstRemoteInput = first[i]; + RemoteInput secondRemoteInput = second[i]; + + if (!TextUtils.equals(firstRemoteInput.getLabel(), secondRemoteInput.getLabel())) { + return true; + } + if (areDifferent(firstRemoteInput.getChoices(), secondRemoteInput.getChoices())) { + return true; + } + } + return false; + } + + private static boolean areDifferent( + @Nullable CharSequence[] first, @Nullable CharSequence[] second) { + if (first == second) { + return false; + } + if (first == null || second == null) { + return true; + } + if (first.length != second.length) { + return true; + } + for (int i = 0; i < first.length; i++) { + CharSequence firstCharSequence = first[i]; + CharSequence secondCharSequence = second[i]; + if (!TextUtils.equals(firstCharSequence, secondCharSequence)) { + return true; + } + } + return false; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java index c820e2b19366..cc5fbe53bba4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java @@ -661,8 +661,9 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi if (hsl[1] < 0.2f) { contrastedColor = Notification.COLOR_DEFAULT; } + boolean isDark = !ContrastColorUtil.isColorLight(mCachedContrastBackgroundColor); contrastedColor = ContrastColorUtil.resolveContrastColor(mContext, - contrastedColor, mCachedContrastBackgroundColor); + contrastedColor, mCachedContrastBackgroundColor, isDark); } mContrastedDrawableColor = contrastedColor; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInflater.java index 130305715409..b9740d370782 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInflater.java @@ -27,15 +27,17 @@ import android.view.View; import android.widget.RemoteViews; import com.android.internal.annotations.VisibleForTesting; -import com.android.systemui.R; -import com.android.systemui.statusbar.InflationTask; import com.android.systemui.statusbar.ExpandableNotificationRow; +import com.android.systemui.statusbar.InflationTask; import com.android.systemui.statusbar.NotificationContentView; import com.android.systemui.statusbar.NotificationData; import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.util.Assert; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; @@ -67,6 +69,7 @@ public class NotificationInflater { private boolean mIsChildInGroup; private InflationCallback mCallback; private boolean mRedactAmbient; + private List<Notification.Action> mSmartActions; public NotificationInflater(ExpandableNotificationRow row) { mRow = row; @@ -95,6 +98,10 @@ public class NotificationInflater { mUsesIncreasedHeight = usesIncreasedHeight; } + public void setSmartActions(List<Notification.Action> smartActions) { + mSmartActions = smartActions; + } + public void setUsesIncreasedHeadsUpHeight(boolean usesIncreasedHeight) { mUsesIncreasedHeadsUpHeight = usesIncreasedHeight; } @@ -140,7 +147,7 @@ public class NotificationInflater { AsyncInflationTask task = new AsyncInflationTask(sbn, reInflateFlags, mRow, mIsLowPriority, mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, mRedactAmbient, - mCallback, mRemoteViewClickHandler); + mCallback, mRemoteViewClickHandler, mSmartActions); if (mCallback != null && mCallback.doInflateSynchronous()) { task.onPostExecute(task.doInBackground()); } else { @@ -554,7 +561,7 @@ public class NotificationInflater { } } - public void onDensityOrFontScaleChanged() { + public void clearCachesAndReInflate() { NotificationData.Entry entry = mRow.getEntry(); entry.cachedAmbientContentView = null; entry.cachedBigContentView = null; @@ -586,13 +593,15 @@ public class NotificationInflater { private Exception mError; private RemoteViews.OnClickHandler mRemoteViewClickHandler; private CancellationSignal mCancellationSignal; + private List<Notification.Action> mSmartActions; private AsyncInflationTask(StatusBarNotification notification, int reInflateFlags, ExpandableNotificationRow row, boolean isLowPriority, boolean isChildInGroup, boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, boolean redactAmbient, InflationCallback callback, - RemoteViews.OnClickHandler remoteViewClickHandler) { + RemoteViews.OnClickHandler remoteViewClickHandler, + List<Notification.Action> smartActions) { mRow = row; mSbn = notification; mReInflateFlags = reInflateFlags; @@ -604,6 +613,9 @@ public class NotificationInflater { mRedactAmbient = redactAmbient; mRemoteViewClickHandler = remoteViewClickHandler; mCallback = callback; + mSmartActions = smartActions == null + ? Collections.emptyList() + : new ArrayList<>(smartActions); NotificationData.Entry entry = row.getEntry(); entry.setInflationTask(this); } @@ -619,6 +631,9 @@ public class NotificationInflater { final Notification.Builder recoveredBuilder = Notification.Builder.recoverBuilder(mContext, mSbn.getNotification()); + + applyChanges(recoveredBuilder); + Context packageContext = mSbn.getPackageContext(mContext); Notification notification = mSbn.getNotification(); if (notification.isMediaNotification()) { @@ -646,6 +661,18 @@ public class NotificationInflater { } } + /** + * Apply changes to the given notification builder, like adding smart actions suggested by + * a {@link android.service.notification.NotificationAssistantService}. + */ + private void applyChanges(Notification.Builder builder) { + if (mSmartActions != null) { + for (Notification.Action smartAction : mSmartActions) { + builder.addAction(smartAction); + } + } + } + private void handleError(Exception e) { mRow.getEntry().onInflationTaskFinished(); StatusBarNotification sbn = mRow.getStatusBarNotification(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java index ea70ebbe7e4b..a781be69c93e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java @@ -189,6 +189,14 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue state |= DISABLE_SYSTEM_INFO; state |= DISABLE_CLOCK; } + + // In landscape, the heads up show but shouldHideNotificationIcons() return false + // because the visual icon is in notification icon area rather than heads up's space. + // whether the notification icon show or not, clock should hide when heads up show. + if (mStatusBarComponent.isHeadsUpShouldBeVisible()) { + state |= DISABLE_CLOCK; + } + if (mNetworkController != null && EncryptionHelper.IS_DATA_ENCRYPTED) { if (mNetworkController.hasEmergencyCryptKeeperText()) { state |= DISABLE_NOTIFICATION_ICONS; 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 5a07dbd1eda5..240d467936b3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java @@ -2533,7 +2533,8 @@ public class NotificationPanelView extends PanelView implements } private void updateStatusBarIcons() { - boolean showIconsWhenExpanded = isFullWidth() && getExpandedHeight() < getOpeningHeight(); + boolean showIconsWhenExpanded = (isPanelVisibleBecauseOfHeadsUp() || isFullWidth()) + && getExpandedHeight() < getOpeningHeight(); if (showIconsWhenExpanded && mNoVisibleNotifications && isOnKeyguard()) { showIconsWhenExpanded = false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index f5736423c6d2..5b42d5e31c0a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -113,6 +113,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; private final DozeParameters mDozeParameters; private final AlarmTimeout mTimeTicker; + private final KeyguardVisibilityCallback mKeyguardVisibilityCallback; private final SysuiColorExtractor mColorExtractor; private GradientColors mLockColors; @@ -171,6 +172,8 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo mUnlockMethodCache = UnlockMethodCache.getInstance(mContext); mDarkenWhileDragging = !mUnlockMethodCache.canSkipBouncer(); mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(mContext); + mKeyguardVisibilityCallback = new KeyguardVisibilityCallback(); + mKeyguardUpdateMonitor.registerCallback(mKeyguardVisibilityCallback); mScrimBehindAlphaResValue = mContext.getResources().getFloat(R.dimen.scrim_behind_alpha); mTimeTicker = new AlarmTimeout(alarmManager, this::onHideWallpaperTimeout, "hide_aod_wallpaper", new Handler()); @@ -892,6 +895,16 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo for (ScrimState state : ScrimState.values()) { state.setHasBackdrop(hasBackdrop); } + + // Backdrop event may arrive after state was already applied, + // in this case, back-scrim needs to be re-evaluated + if (mState == ScrimState.AOD || mState == ScrimState.PULSING) { + float newBehindAlpha = mState.getBehindAlpha(mNotificationDensity); + if (mCurrentBehindAlpha != newBehindAlpha) { + mCurrentBehindAlpha = newBehindAlpha; + updateScrims(); + } + } } public void setLaunchingAffordanceWithPreview(boolean launchingAffordanceWithPreview) { @@ -910,4 +923,16 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo default void onCancelled() { } } + + /** + * Simple keyguard callback that updates scrims when keyguard visibility changes. + */ + private class KeyguardVisibilityCallback extends KeyguardUpdateMonitorCallback { + + @Override + public void onKeyguardVisibilityChanged(boolean showing) { + mNeedsDrawableColorUpdate = true; + scheduleUpdate(); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java index 19015fcdacf7..081ebfac5f1b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java @@ -105,7 +105,6 @@ public enum ScrimState { public void prepare(ScrimState previousState) { final boolean alwaysOnEnabled = mDozeParameters.getAlwaysOn(); mBlankScreen = mDisplayRequiresBlanking; - mCurrentBehindAlpha = mWallpaperSupportsAmbientMode && !mHasBackdrop ? 0f : 1f; mCurrentInFrontAlpha = alwaysOnEnabled ? mAodFrontScrimAlpha : 1f; mCurrentInFrontTint = Color.BLACK; mCurrentBehindTint = Color.BLACK; @@ -116,6 +115,11 @@ public enum ScrimState { } @Override + public float getBehindAlpha(float busyness) { + return mWallpaperSupportsAmbientMode && !mHasBackdrop ? 0f : 1f; + } + + @Override public boolean isLowPowerState() { return true; } @@ -129,10 +133,14 @@ public enum ScrimState { public void prepare(ScrimState previousState) { mCurrentInFrontAlpha = 0; mCurrentInFrontTint = Color.BLACK; - mCurrentBehindAlpha = mWallpaperSupportsAmbientMode && !mHasBackdrop ? 0f : 1f; mCurrentBehindTint = Color.BLACK; mBlankScreen = mDisplayRequiresBlanking; } + + @Override + public float getBehindAlpha(float busyness) { + return mWallpaperSupportsAmbientMode && !mHasBackdrop ? 0f : 1f; + } }, /** 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 7cf2c3376e89..8d077f3afcbd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -1198,6 +1198,9 @@ public class StatusBar extends SystemUI implements DemoMode, if (mBrightnessMirrorController != null) { mBrightnessMirrorController.onUiModeChanged(); } + if (mStackScroller != null) { + mStackScroller.onUiModeChanged(); + } } private void inflateEmptyShadeView() { @@ -2155,6 +2158,10 @@ public class StatusBar extends SystemUI implements DemoMode, } } + public boolean isHeadsUpShouldBeVisible() { + return mHeadsUpAppearanceController.shouldBeVisible(); + } + /** * All changes to the status bar and notifications funnel through here and are batched. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java index 25261c0ad2ca..f729120f98bc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java @@ -976,7 +976,7 @@ public class NetworkControllerImpl extends BroadcastReceiver private SubscriptionInfo addSignalController(int id, int simSlotIndex) { SubscriptionInfo info = new SubscriptionInfo(id, "", simSlotIndex, "", "", 0, 0, "", 0, - null, 0, 0, ""); + null, null, null, ""); MobileSignalController controller = new MobileSignalController(mContext, mConfig, mHasMobileDataFeature, mPhone, mCallbackHandler, this, info, mSubDefaults, mReceiverHandler.getLooper()); 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 6fc491119aa6..eb3289b8e8e8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java @@ -514,6 +514,20 @@ public class NotificationStackScrollLayout extends ViewGroup mSwipeHelper.onMenuShown(row); } + public void onUiModeChanged() { + mBgColor = mContext.getColor(R.color.notification_shade_background_color); + updateBackgroundDimming(); + + // Re-inflate all notification views + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + if (child instanceof ActivatableNotificationView) { + ((ActivatableNotificationView) child).onUiModeChanged(); + } + } + } + protected void onDraw(Canvas canvas) { if (mShouldDrawNotificationBackground && (mCurrentBounds.top < mCurrentBounds.bottom || mAmbientState.isDark())) { @@ -588,8 +602,8 @@ public class NotificationStackScrollLayout extends ViewGroup // Interpolate between semi-transparent notification panel background color // and white AOD separator. - float colorInterpolation = Interpolators.DECELERATE_QUINT.getInterpolation( - mInterpolatedDarkAmount); + float colorInterpolation = MathUtils.smoothStep(0.4f /* start */, 1f /* end */, + mLinearDarkAmount); int color = ColorUtils.blendARGB(awakeColor, Color.WHITE, colorInterpolation); if (mCachedBackgroundColor != color) { @@ -1425,7 +1439,8 @@ public class NotificationStackScrollLayout extends ViewGroup */ private int targetScrollForView(ExpandableView v, int positionInLinearLayout) { return positionInLinearLayout + v.getIntrinsicHeight() + - getImeInset() - getHeight() + getTopPadding(); + getImeInset() - getHeight() + + ((!isExpanded() && isPinnedHeadsUp(v)) ? mHeadsUpInset : getTopPadding()); } @Override @@ -2056,9 +2071,15 @@ public class NotificationStackScrollLayout extends ViewGroup } private int getScrollRange() { - int scrollRange = Math.max(0, mContentHeight - mMaxLayoutHeight); + // In current design, it only use the top HUN to treat all of HUNs + // although there are more than one HUNs + int contentHeight = mContentHeight; + if (!isExpanded() && mHeadsUpManager.hasPinnedHeadsUp()) { + contentHeight = mHeadsUpInset + getTopHeadsUpPinnedHeight(); + } + int scrollRange = Math.max(0, contentHeight - mMaxLayoutHeight); int imeInset = getImeInset(); - scrollRange += Math.min(imeInset, Math.max(0, mContentHeight - (getHeight() - imeInset))); + scrollRange += Math.min(imeInset, Math.max(0, contentHeight - (getHeight() - imeInset))); return scrollRange; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java index ee006d3e267d..886bd5e3dd8d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java @@ -473,6 +473,15 @@ public class StackScrollAlgorithm { childState.yTranslation = topState.yTranslation + topState.height - childState.height; } + + // heads up notification show and this row is the top entry of heads up + // notifications. i.e. this row should be the only one row that has input field + // To check if the row need to do translation according to scroll Y + // heads up show full of row's content and any scroll y indicate that the + // translationY need to move up the HUN. + if (!mIsExpanded && isTopEntry && ambientState.getScrollY() > 0) { + childState.yTranslation -= ambientState.getScrollY(); + } } if (row.isHeadsUpAnimatingAway()) { childState.hidden = false; diff --git a/packages/SystemUI/src/com/android/systemui/volume/CarVolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/CarVolumeDialogImpl.java index 4a9856b1c840..2cbb78a4a8f5 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/CarVolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/CarVolumeDialogImpl.java @@ -351,11 +351,11 @@ public class CarVolumeDialogImpl implements VolumeDialog { listItem.setOnSeekBarChangeListener( new CarVolumeDialogImpl.VolumeSeekBarChangeListener(volumeGroupId, mCarAudioManager)); Drawable primaryIcon = mContext.getResources().getDrawable(volumeItem.icon); - primaryIcon.setTint(color); + primaryIcon.mutate().setTint(color); listItem.setPrimaryActionIcon(primaryIcon); if (supplementalIconId != 0) { Drawable supplementalIcon = mContext.getResources().getDrawable(supplementalIconId); - supplementalIcon.setTint(color); + supplementalIcon.mutate().setTint(color); listItem.setSupplementalIcon(supplementalIcon, true, supplementalIconOnClickListener); } else { diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java b/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java index d19715d5c5cc..5ecf0c04aeb1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java @@ -24,6 +24,7 @@ import static junit.framework.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -66,6 +67,8 @@ public class PowerUITest extends SysuiTestCase { public static final long BELOW_HYBRID_THRESHOLD = TimeUnit.HOURS.toMillis(2); public static final long ABOVE_HYBRID_THRESHOLD = TimeUnit.HOURS.toMillis(4); private static final long ABOVE_CHARGE_CYCLE_THRESHOLD = Duration.ofHours(8).toMillis(); + private static final int OLD_BATTERY_LEVEL_NINE = 9; + private static final int OLD_BATTERY_LEVEL_10 = 10; private HardwarePropertiesManager mHardProps; private WarningsUI mMockWarnings; private PowerUI mPowerUI; @@ -307,8 +310,8 @@ public class PowerUITest extends SysuiTestCase { .thenReturn(new Estimate(BELOW_HYBRID_THRESHOLD, true)); mPowerUI.mBatteryStatus = BatteryManager.BATTERY_HEALTH_GOOD; - mPowerUI.maybeShowBatteryWarning(UNPLUGGED, UNPLUGGED, ABOVE_WARNING_BUCKET, - ABOVE_WARNING_BUCKET); + mPowerUI.maybeShowBatteryWarning(OLD_BATTERY_LEVEL_NINE, UNPLUGGED, UNPLUGGED, + ABOVE_WARNING_BUCKET, ABOVE_WARNING_BUCKET); // reduce battery level to handle time based trigger -> level trigger interactions mPowerUI.mBatteryLevel = 10; @@ -449,6 +452,33 @@ public class PowerUITest extends SysuiTestCase { verify(mMockWarnings, never()).dismissLowBatteryWarning(); } + @Test + public void testMaybeShowBatteryWarning_onlyQueriesEstimateOnBatteryLevelChangeOrNull() { + mPowerUI.start(); + Estimate estimate = new Estimate(BELOW_HYBRID_THRESHOLD, true); + when(mEnhancedEstimates.isHybridNotificationEnabled()).thenReturn(true); + when(mEnhancedEstimates.getLowWarningThreshold()).thenReturn(PowerUI.THREE_HOURS_IN_MILLIS); + when(mEnhancedEstimates.getSevereWarningThreshold()).thenReturn(ONE_HOUR_MILLIS); + when(mEnhancedEstimates.getEstimate()).thenReturn(estimate); + mPowerUI.mBatteryStatus = BatteryManager.BATTERY_HEALTH_GOOD; + + // we expect that the first time it will query even if the level is the same + mPowerUI.mBatteryLevel = 9; + mPowerUI.maybeShowBatteryWarning(OLD_BATTERY_LEVEL_NINE, UNPLUGGED, UNPLUGGED, + ABOVE_WARNING_BUCKET, ABOVE_WARNING_BUCKET); + verify(mEnhancedEstimates, times(1)).getEstimate(); + + // We should NOT query again if the battery level hasn't changed + mPowerUI.maybeShowBatteryWarning(OLD_BATTERY_LEVEL_NINE, UNPLUGGED, UNPLUGGED, + ABOVE_WARNING_BUCKET, ABOVE_WARNING_BUCKET); + verify(mEnhancedEstimates, times(1)).getEstimate(); + + // Battery level has changed, so we should query again + mPowerUI.maybeShowBatteryWarning(OLD_BATTERY_LEVEL_10, UNPLUGGED, UNPLUGGED, + ABOVE_WARNING_BUCKET, ABOVE_WARNING_BUCKET); + verify(mEnhancedEstimates, times(2)).getEstimate(); + } + private void setCurrentTemp(float temp) { when(mHardProps.getDeviceTemperatures(DEVICE_TEMPERATURE_SKIN, TEMPERATURE_CURRENT)) .thenReturn(new float[] { temp }); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationDataTest.java index 77522e44b183..f2f58938009b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationDataTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationDataTest.java @@ -38,11 +38,16 @@ import static org.mockito.Mockito.when; import android.Manifest; import android.app.Notification; import android.app.NotificationChannel; +import android.app.PendingIntent; +import android.content.Intent; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; +import android.graphics.drawable.Icon; import android.media.session.MediaSession; import android.os.Bundle; import android.service.notification.NotificationListenerService; +import android.service.notification.NotificationListenerService.Ranking; +import android.service.notification.SnoozeCriterion; import android.service.notification.StatusBarNotification; import android.support.test.annotation.UiThreadTest; import android.support.test.filters.SmallTest; @@ -72,6 +77,8 @@ public class NotificationDataTest extends SysuiTestCase { private static final int UID_ALLOW_DURING_SETUP = 456; private static final String TEST_HIDDEN_NOTIFICATION_KEY = "testHiddenNotificationKey"; private static final String TEST_EXEMPT_DND_VISUAL_SUPPRESSION_KEY = "exempt"; + private static final NotificationChannel NOTIFICATION_CHANNEL = + new NotificationChannel("id", "name", NotificationChannel.USER_LOCKED_IMPORTANCE); private final StatusBarNotification mMockStatusBarNotification = mock(StatusBarNotification.class); @@ -145,11 +152,9 @@ public class NotificationDataTest extends SysuiTestCase { @Test public void testChannelSetWhenAdded() { mNotificationData.add(mRow.getEntry()); - Assert.assertTrue(mRow.getEntry().channel != null); + assertEquals(NOTIFICATION_CHANNEL, mRow.getEntry().channel); } - - @Test public void testAllRelevantNotisTaggedWithAppOps() throws Exception { mNotificationData.add(mRow.getEntry()); @@ -373,6 +378,32 @@ public class NotificationDataTest extends SysuiTestCase { assertFalse(mNotificationData.isExemptFromDndVisualSuppression(entry)); } + @Test + public void testCreateNotificationDataEntry_RankingUpdate() { + Ranking ranking = mock(Ranking.class); + + ArrayList<Notification.Action> smartActions = new ArrayList<>(); + smartActions.add(createAction()); + when(ranking.getSmartActions()).thenReturn(smartActions); + + when(ranking.getChannel()).thenReturn(NOTIFICATION_CHANNEL); + + when(ranking.getUserSentiment()).thenReturn(Ranking.USER_SENTIMENT_NEGATIVE); + + SnoozeCriterion snoozeCriterion = new SnoozeCriterion("id", "explanation", "confirmation"); + ArrayList<SnoozeCriterion> snoozeCriterions = new ArrayList<>(); + snoozeCriterions.add(snoozeCriterion); + when(ranking.getSnoozeCriteria()).thenReturn(snoozeCriterions); + + NotificationData.Entry entry = + new NotificationData.Entry(mMockStatusBarNotification, ranking); + + assertEquals(smartActions, entry.smartActions); + assertEquals(NOTIFICATION_CHANNEL, entry.channel); + assertEquals(Ranking.USER_SENTIMENT_NEGATIVE, entry.userSentiment); + assertEquals(snoozeCriterions, entry.snoozeCriteria); + } + private void initStatusBarNotification(boolean allowDuringSetup) { Bundle bundle = new Bundle(); bundle.putBoolean(Notification.EXTRA_ALLOW_DURING_SETUP, allowDuringSetup); @@ -388,12 +419,7 @@ public class NotificationDataTest extends SysuiTestCase { } @Override - public NotificationChannel getChannel(String key) { - return new NotificationChannel(null, null, 0); - } - - @Override - protected boolean getRanking(String key, NotificationListenerService.Ranking outRanking) { + protected boolean getRanking(String key, Ranking outRanking) { super.getRanking(key, outRanking); if (key.equals(TEST_HIDDEN_NOTIFICATION_KEY)) { outRanking.populate(key, outRanking.getRank(), @@ -401,23 +427,31 @@ public class NotificationDataTest extends SysuiTestCase { outRanking.getVisibilityOverride(), outRanking.getSuppressedVisualEffects(), outRanking.getImportance(), outRanking.getImportanceExplanation(), outRanking.getOverrideGroupKey(), outRanking.getChannel(), null, null, - outRanking.canShowBadge(), outRanking.getUserSentiment(), true); + outRanking.canShowBadge(), outRanking.getUserSentiment(), true, + null); } else if (key.equals(TEST_EXEMPT_DND_VISUAL_SUPPRESSION_KEY)) { outRanking.populate(key, outRanking.getRank(), outRanking.matchesInterruptionFilter(), outRanking.getVisibilityOverride(), 255, outRanking.getImportance(), outRanking.getImportanceExplanation(), outRanking.getOverrideGroupKey(), outRanking.getChannel(), null, null, - outRanking.canShowBadge(), outRanking.getUserSentiment(), true); + outRanking.canShowBadge(), outRanking.getUserSentiment(), true, null); } else { outRanking.populate(key, outRanking.getRank(), outRanking.matchesInterruptionFilter(), outRanking.getVisibilityOverride(), outRanking.getSuppressedVisualEffects(), outRanking.getImportance(), outRanking.getImportanceExplanation(), - outRanking.getOverrideGroupKey(), outRanking.getChannel(), null, null, - outRanking.canShowBadge(), outRanking.getUserSentiment(), false); + outRanking.getOverrideGroupKey(), NOTIFICATION_CHANNEL, null, null, + outRanking.canShowBadge(), outRanking.getUserSentiment(), false, null); } return true; } } + + private Notification.Action createAction() { + return new Notification.Action.Builder( + Icon.createWithResource(getContext(), android.R.drawable.sym_def_app_icon), + "action", + PendingIntent.getBroadcast(getContext(), 0, new Intent("Action"), 0)).build(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationEntryManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationEntryManagerTest.java index afe16cf13b76..e75e5786b463 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationEntryManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationEntryManagerTest.java @@ -37,7 +37,10 @@ import android.app.ActivityManager; import android.app.AppOpsManager; import android.app.Notification; import android.app.NotificationManager; +import android.app.PendingIntent; import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Icon; import android.os.Handler; import android.os.Looper; import android.os.UserHandle; @@ -54,6 +57,8 @@ import com.android.internal.statusbar.IStatusBarService; import com.android.systemui.ForegroundServiceController; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; +import com.android.systemui.statusbar.notification.NotificationInflater; +import com.android.systemui.statusbar.notification.RowInflaterTask; import com.android.systemui.statusbar.notification.VisualStabilityManager; import com.android.systemui.statusbar.phone.NotificationGroupManager; import com.android.systemui.statusbar.policy.DeviceProvisionedController; @@ -68,6 +73,9 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -99,6 +107,7 @@ public class NotificationEntryManagerTest extends SysuiTestCase { @Mock private VisualStabilityManager mVisualStabilityManager; @Mock private MetricsLogger mMetricsLogger; @Mock private SmartReplyController mSmartReplyController; + @Mock private RowInflaterTask mAsyncInflationTask; private NotificationData.Entry mEntry; private StatusBarNotification mSbn; @@ -139,7 +148,26 @@ public class NotificationEntryManagerTest extends SysuiTestCase { 0, NotificationManager.IMPORTANCE_DEFAULT, null, null, - null, null, null, true, sentiment, false); + null, null, null, true, sentiment, false, null); + return true; + }).when(mRankingMap).getRanking(eq(key), any(NotificationListenerService.Ranking.class)); + } + + private void setSmartActions(String key, ArrayList<Notification.Action> smartActions) { + doAnswer(invocationOnMock -> { + NotificationListenerService.Ranking ranking = (NotificationListenerService.Ranking) + invocationOnMock.getArguments()[1]; + ranking.populate( + key, + 0, + false, + 0, + 0, + NotificationManager.IMPORTANCE_DEFAULT, + null, null, + null, null, null, true, + NotificationListenerService.Ranking.USER_SENTIMENT_NEUTRAL, false, + smartActions); return true; }).when(mRankingMap).getRanking(eq(key), any(NotificationListenerService.Ranking.class)); } @@ -427,4 +455,71 @@ public class NotificationEntryManagerTest extends SysuiTestCase { Assert.assertTrue(newSbn.getNotification().extras .getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false)); } + + @Test + public void testUpdateNotificationRanking() { + when(mPresenter.isDeviceProvisioned()).thenReturn(true); + when(mPresenter.isNotificationForCurrentProfiles(any())).thenReturn(true); + + mEntry.row = mRow; + mEntry.setInflationTask(mAsyncInflationTask); + mEntryManager.getNotificationData().add(mEntry); + setSmartActions(mEntry.key, new ArrayList<>(Arrays.asList(createAction()))); + + mEntryManager.updateNotificationRanking(mRankingMap); + verify(mRow).updateNotification(eq(mEntry)); + assertEquals(1, mEntry.smartActions.size()); + assertEquals("action", mEntry.smartActions.get(0).title); + } + + @Test + public void testUpdateNotificationRanking_noChange() { + when(mPresenter.isDeviceProvisioned()).thenReturn(true); + when(mPresenter.isNotificationForCurrentProfiles(any())).thenReturn(true); + + mEntry.row = mRow; + mEntryManager.getNotificationData().add(mEntry); + setSmartActions(mEntry.key, null); + + mEntryManager.updateNotificationRanking(mRankingMap); + verify(mRow, never()).updateNotification(eq(mEntry)); + assertEquals(0, mEntry.smartActions.size()); + } + + @Test + public void testUpdateNotificationRanking_rowNotInflatedYet() { + when(mPresenter.isDeviceProvisioned()).thenReturn(true); + when(mPresenter.isNotificationForCurrentProfiles(any())).thenReturn(true); + + mEntry.row = null; + mEntryManager.getNotificationData().add(mEntry); + setSmartActions(mEntry.key, new ArrayList<>(Arrays.asList(createAction()))); + + mEntryManager.updateNotificationRanking(mRankingMap); + verify(mRow, never()).updateNotification(eq(mEntry)); + assertEquals(1, mEntry.smartActions.size()); + assertEquals("action", mEntry.smartActions.get(0).title); + } + + @Test + public void testUpdateNotificationRanking_pendingNotification() { + when(mPresenter.isDeviceProvisioned()).thenReturn(true); + when(mPresenter.isNotificationForCurrentProfiles(any())).thenReturn(true); + + mEntry.row = null; + mEntryManager.mPendingNotifications.put(mEntry.key, mEntry); + setSmartActions(mEntry.key, new ArrayList<>(Arrays.asList(createAction()))); + + mEntryManager.updateNotificationRanking(mRankingMap); + verify(mRow, never()).updateNotification(eq(mEntry)); + assertEquals(1, mEntry.smartActions.size()); + assertEquals("action", mEntry.smartActions.get(0).title); + } + + private Notification.Action createAction() { + return new Notification.Action.Builder( + Icon.createWithResource(getContext(), android.R.drawable.sym_def_app_icon), + "action", + PendingIntent.getBroadcast(getContext(), 0, new Intent("Action"), 0)).build(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationUiAdjustmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationUiAdjustmentTest.java new file mode 100644 index 000000000000..ce47e60f3e1c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationUiAdjustmentTest.java @@ -0,0 +1,180 @@ +/* + * 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 com.android.systemui.statusbar; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.Notification; +import android.app.PendingIntent; +import android.app.RemoteInput; +import android.content.Intent; +import android.graphics.drawable.Icon; +import android.support.test.filters.SmallTest; + +import com.android.internal.R; +import com.android.systemui.SysuiTestCase; + +import org.junit.Test; + +import java.util.Collections; + +@SmallTest +public class NotificationUiAdjustmentTest extends SysuiTestCase { + + @Test + public void needReinflate_differentLength() { + PendingIntent pendingIntent = + PendingIntent.getActivity(mContext, 0, new Intent(), 0); + Notification.Action action = + createActionBuilder("first", R.drawable.ic_corp_icon, pendingIntent).build(); + assertThat(NotificationUiAdjustment.needReinflate( + new NotificationUiAdjustment("first", Collections.emptyList()), + new NotificationUiAdjustment("second", Collections.singletonList(action)))) + .isTrue(); + } + + @Test + public void needReinflate_differentLabels() { + PendingIntent pendingIntent = + PendingIntent.getActivity(mContext, 0, new Intent(), 0); + Notification.Action firstAction = + createActionBuilder("first", R.drawable.ic_corp_icon, pendingIntent).build(); + Notification.Action secondAction = + createActionBuilder("second", R.drawable.ic_corp_icon, pendingIntent).build(); + + assertThat(NotificationUiAdjustment.needReinflate( + new NotificationUiAdjustment("first", Collections.singletonList(firstAction)), + new NotificationUiAdjustment("second", Collections.singletonList(secondAction)))) + .isTrue(); + } + + @Test + public void needReinflate_differentIcons() { + PendingIntent pendingIntent = + PendingIntent.getActivity(mContext, 0, new Intent(), 0); + Notification.Action firstAction = + createActionBuilder("same", R.drawable.ic_corp_icon, pendingIntent).build(); + Notification.Action secondAction = + createActionBuilder("same", R.drawable.ic_account_circle, pendingIntent) + .build(); + + assertThat(NotificationUiAdjustment.needReinflate( + new NotificationUiAdjustment("first", Collections.singletonList(firstAction)), + new NotificationUiAdjustment("second", Collections.singletonList(secondAction)))) + .isTrue(); + } + + @Test + public void needReinflate_differentPendingIntent() { + PendingIntent firstPendingIntent = + PendingIntent.getActivity(mContext, 0, new Intent(Intent.ACTION_VIEW), 0); + PendingIntent secondPendingIntent = + PendingIntent.getActivity(mContext, 0, new Intent(Intent.ACTION_PROCESS_TEXT), 0); + Notification.Action firstAction = + createActionBuilder("same", R.drawable.ic_corp_icon, firstPendingIntent) + .build(); + Notification.Action secondAction = + createActionBuilder("same", R.drawable.ic_corp_icon, secondPendingIntent) + .build(); + + assertThat(NotificationUiAdjustment.needReinflate( + new NotificationUiAdjustment("first", Collections.singletonList(firstAction)), + new NotificationUiAdjustment("second", Collections.singletonList(secondAction)))) + .isTrue(); + } + + @Test + public void needReinflate_differentChoices() { + PendingIntent pendingIntent = + PendingIntent.getActivity(mContext, 0, new Intent(), 0); + + RemoteInput firstRemoteInput = + createRemoteInput("same", "same", new CharSequence[] {"first"}); + RemoteInput secondRemoteInput = + createRemoteInput("same", "same", new CharSequence[] {"second"}); + + Notification.Action firstAction = + createActionBuilder("same", R.drawable.ic_corp_icon, pendingIntent) + .addRemoteInput(firstRemoteInput) + .build(); + Notification.Action secondAction = + createActionBuilder("same", R.drawable.ic_corp_icon, pendingIntent) + .addRemoteInput(secondRemoteInput) + .build(); + + assertThat(NotificationUiAdjustment.needReinflate( + new NotificationUiAdjustment("first", Collections.singletonList(firstAction)), + new NotificationUiAdjustment("second", Collections.singletonList(secondAction)))) + .isTrue(); + } + + @Test + public void needReinflate_differentRemoteInputLabel() { + PendingIntent pendingIntent = + PendingIntent.getActivity(mContext, 0, new Intent(), 0); + + RemoteInput firstRemoteInput = + createRemoteInput("same", "first", new CharSequence[] {"same"}); + RemoteInput secondRemoteInput = + createRemoteInput("same", "second", new CharSequence[] {"same"}); + + Notification.Action firstAction = + createActionBuilder("same", R.drawable.ic_corp_icon, pendingIntent) + .addRemoteInput(firstRemoteInput) + .build(); + Notification.Action secondAction = + createActionBuilder("same", R.drawable.ic_corp_icon, pendingIntent) + .addRemoteInput(secondRemoteInput) + .build(); + + assertThat(NotificationUiAdjustment.needReinflate( + new NotificationUiAdjustment("first", Collections.singletonList(firstAction)), + new NotificationUiAdjustment("second", Collections.singletonList(secondAction)))) + .isTrue(); + } + + @Test + public void needReinflate_negative() { + PendingIntent pendingIntent = + PendingIntent.getActivity(mContext, 0, new Intent(), 0); + RemoteInput firstRemoteInput = + createRemoteInput("same", "same", new CharSequence[] {"same"}); + RemoteInput secondRemoteInput = + createRemoteInput("same", "same", new CharSequence[] {"same"}); + + Notification.Action firstAction = + createActionBuilder("same", R.drawable.ic_corp_icon, pendingIntent) + .addRemoteInput(firstRemoteInput).build(); + Notification.Action secondAction = + createActionBuilder("same", R.drawable.ic_corp_icon, pendingIntent) + .addRemoteInput(secondRemoteInput).build(); + + assertThat(NotificationUiAdjustment.needReinflate( + new NotificationUiAdjustment("first", Collections.singletonList(firstAction)), + new NotificationUiAdjustment("second", Collections.singletonList(secondAction)))) + .isFalse(); + } + + private Notification.Action.Builder createActionBuilder( + String title, int drawableRes, PendingIntent pendingIntent) { + return new Notification.Action.Builder( + Icon.createWithResource(mContext, drawableRes), title, pendingIntent); + } + + private RemoteInput createRemoteInput(String resultKey, String label, CharSequence[] choices) { + return new RemoteInput.Builder(resultKey).setLabel(label).setChoices(choices).build(); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java index 89d562a2099c..9c558748697a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java @@ -152,6 +152,20 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + public void setHasBackdrop_withAodWallpaperAndAlbumArt() { + mScrimController.setWallpaperSupportsAmbientMode(true); + mScrimController.transitionTo(ScrimState.AOD); + mScrimController.finishAnimationsImmediately(); + mScrimController.setHasBackdrop(true); + mScrimController.finishAnimationsImmediately(); + // Front scrim should be transparent + // Back scrim should be visible with tint + assertScrimVisibility(VISIBILITY_FULLY_TRANSPARENT, VISIBILITY_FULLY_OPAQUE); + assertScrimTint(mScrimBehind, true /* tinted */); + assertScrimTint(mScrimInFront, true /* tinted */); + } + + @Test public void transitionToAod_withFrontAlphaUpdates() { // Assert that setting the AOD front scrim alpha doesn't take effect in a non-AOD state. mScrimController.transitionTo(ScrimState.KEYGUARD); diff --git a/proto/src/metrics_constants.proto b/proto/src/metrics_constants.proto index dc6d16be6e30..b6a5a9c562eb 100644 --- a/proto/src/metrics_constants.proto +++ b/proto/src/metrics_constants.proto @@ -6160,6 +6160,14 @@ message MetricsEvent { // CATEGORY: SETTINGS // OS: Q FACE_ENROLL_FINISHED = 1508; + + // OPEN: Face Enroll sidecar + // CATEGORY: SETTINGS + // OS: Q + FACE_ENROLL_SIDECAR = 1509; + + // OPEN: Settings > Add face > Error dialog + DIALOG_FACE_ERROR = 5510; // ---- End Q Constants, all Q constants go above this line ---- // Add new aosp constants above this line. diff --git a/sax/tests/saxtests/src/android/sax/ExpatPerformanceTest.java b/sax/tests/saxtests/src/android/sax/ExpatPerformanceTest.java deleted file mode 100644 index 892c49052533..000000000000 --- a/sax/tests/saxtests/src/android/sax/ExpatPerformanceTest.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (C) 2007 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.sax; - -import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.LargeTest; -import android.util.Log; -import android.util.Xml; -import org.kxml2.io.KXmlParser; -import org.xml.sax.SAXException; -import org.xml.sax.helpers.DefaultHandler; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; - -import com.android.frameworks.saxtests.R; - -public class ExpatPerformanceTest extends AndroidTestCase { - - private static final String TAG = ExpatPerformanceTest.class.getSimpleName(); - - private byte[] mXmlBytes; - - @Override - public void setUp() throws Exception { - super.setUp(); - InputStream in = mContext.getResources().openRawResource(R.raw.youtube); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - int length; - while ((length = in.read(buffer)) != -1) { - out.write(buffer, 0, length); - } - mXmlBytes = out.toByteArray(); - - Log.i("***", "File size: " + (mXmlBytes.length / 1024) + "k"); - } - - @LargeTest - public void testPerformance() throws Exception { -// try { -// Debug.startMethodTracing("expat3"); -// for (int i = 0; i < 1; i++) { - runJavaPullParser(); - runSax(); - runExpatPullParser(); -// } -// } finally { -// Debug.stopMethodTracing(); -// } - } - - private InputStream newInputStream() { - return new ByteArrayInputStream(mXmlBytes); - } - - private void runSax() throws IOException, SAXException { - long start = System.currentTimeMillis(); - Xml.parse(newInputStream(), Xml.Encoding.UTF_8, new DefaultHandler()); - long elapsed = System.currentTimeMillis() - start; - Log.i(TAG, "expat SAX: " + elapsed + "ms"); - } - - private void runExpatPullParser() throws XmlPullParserException, IOException { - long start = System.currentTimeMillis(); - XmlPullParser pullParser = Xml.newPullParser(); - pullParser.setInput(newInputStream(), "UTF-8"); - withPullParser(pullParser); - long elapsed = System.currentTimeMillis() - start; - Log.i(TAG, "expat pull: " + elapsed + "ms"); - } - - private void runJavaPullParser() throws XmlPullParserException, IOException { - XmlPullParser pullParser; - long start = System.currentTimeMillis(); - pullParser = new KXmlParser(); - pullParser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); - pullParser.setInput(newInputStream(), "UTF-8"); - withPullParser(pullParser); - long elapsed = System.currentTimeMillis() - start; - Log.i(TAG, "java pull parser: " + elapsed + "ms"); - } - - private static void withPullParser(XmlPullParser pullParser) - throws IOException, XmlPullParserException { - int eventType = pullParser.next(); - while (eventType != XmlPullParser.END_DOCUMENT) { - switch (eventType) { - case XmlPullParser.START_TAG: - pullParser.getName(); -// int nattrs = pullParser.getAttributeCount(); -// for (int i = 0; i < nattrs; ++i) { -// pullParser.getAttributeName(i); -// pullParser.getAttributeValue(i); -// } - break; - case XmlPullParser.END_TAG: - pullParser.getName(); - break; - case XmlPullParser.TEXT: - pullParser.getText(); - break; - } - eventType = pullParser.next(); - } - } -} diff --git a/services/Android.bp b/services/Android.bp index d125adc5fa7d..bea51be321c9 100644 --- a/services/Android.bp +++ b/services/Android.bp @@ -2,6 +2,7 @@ // ============================================================ java_library { name: "services", + installable: true, dex_preopt: { app_image: true, diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java index 41e9d2bf49de..021fdcd84971 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java @@ -46,6 +46,7 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; +import android.os.Parcelable; import android.os.RemoteCallback; import android.os.RemoteException; import android.os.ResultReceiver; @@ -622,6 +623,38 @@ public final class AutofillManagerService extends SystemService { return getWhitelistedCompatModePackages(getWhitelistedCompatModePackagesFromSettings()); } + private void send(@NonNull IResultReceiver receiver, int value) { + try { + receiver.send(value, null); + } catch (RemoteException e) { + Slog.w(TAG, "Error async reporting result to client: " + e); + } + } + + private void send(@NonNull IResultReceiver receiver, @NonNull Bundle value) { + try { + receiver.send(0, value); + } catch (RemoteException e) { + Slog.w(TAG, "Error async reporting result to client: " + e); + } + } + + private void send(@NonNull IResultReceiver receiver, @Nullable String value) { + send(receiver, AutofillManager.SyncResultReceiver.bundleFor(value)); + } + + private void send(@NonNull IResultReceiver receiver, @Nullable String[] value) { + send(receiver, AutofillManager.SyncResultReceiver.bundleFor(value)); + } + + private void send(@NonNull IResultReceiver receiver, @Nullable Parcelable value) { + send(receiver, AutofillManager.SyncResultReceiver.bundleFor(value)); + } + + private void send(@NonNull IResultReceiver receiver, boolean value) { + send(receiver, value ? 1 : 0); + } + @Nullable @VisibleForTesting static Map<String, String[]> getWhitelistedCompatModePackages(String setting) { @@ -827,9 +860,10 @@ public final class AutofillManagerService extends SystemService { final class AutoFillManagerServiceStub extends IAutoFillManager.Stub { @Override - public int addClient(IAutoFillManagerClient client, int userId) { + public void addClient(IAutoFillManagerClient client, int userId, + @NonNull IResultReceiver receiver) { + int flags = 0; synchronized (mLock) { - int flags = 0; if (getServiceForUserLocked(userId).addClientLocked(client)) { flags |= AutofillManager.FLAG_ADD_CLIENT_ENABLED; } @@ -839,8 +873,8 @@ public final class AutofillManagerService extends SystemService { if (sVerbose) { flags |= AutofillManager.FLAG_ADD_CLIENT_VERBOSE; } - return flags; } + send(receiver, flags); } @Override @@ -874,9 +908,9 @@ public final class AutofillManagerService extends SystemService { } @Override - public int startSession(IBinder activityToken, IBinder appCallback, AutofillId autofillId, + public void startSession(IBinder activityToken, IBinder appCallback, AutofillId autofillId, Rect bounds, AutofillValue value, int userId, boolean hasCallback, int flags, - ComponentName componentName, boolean compatMode) { + ComponentName componentName, boolean compatMode, IResultReceiver receiver) { activityToken = Preconditions.checkNotNull(activityToken, "activityToken"); appCallback = Preconditions.checkNotNull(appCallback, "appCallback"); @@ -892,61 +926,63 @@ public final class AutofillManagerService extends SystemService { throw new IllegalArgumentException(packageName + " is not a valid package", e); } + final int sessionId; synchronized (mLock) { final AutofillManagerServiceImpl service = getServiceForUserLocked(userId); - return service.startSessionLocked(activityToken, getCallingUid(), appCallback, + sessionId = service.startSessionLocked(activityToken, getCallingUid(), appCallback, autofillId, bounds, value, hasCallback, componentName, compatMode, mAllowInstantService, flags); } + send(receiver, sessionId); } @Override - public FillEventHistory getFillEventHistory() throws RemoteException { + public void getFillEventHistory(@NonNull IResultReceiver receiver) throws RemoteException { final int userId = UserHandle.getCallingUserId(); + FillEventHistory fillEventHistory = null; synchronized (mLock) { final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId); if (service != null) { - return service.getFillEventHistory(getCallingUid()); + fillEventHistory = service.getFillEventHistory(getCallingUid()); } else if (sVerbose) { Slog.v(TAG, "getFillEventHistory(): no service for " + userId); } } - - return null; + send(receiver, fillEventHistory); } @Override - public UserData getUserData() throws RemoteException { + public void getUserData(@NonNull IResultReceiver receiver) throws RemoteException { final int userId = UserHandle.getCallingUserId(); + UserData userData = null; synchronized (mLock) { final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId); if (service != null) { - return service.getUserData(getCallingUid()); + userData = service.getUserData(getCallingUid()); } else if (sVerbose) { Slog.v(TAG, "getUserData(): no service for " + userId); } } - - return null; + send(receiver, userData); } @Override - public String getUserDataId() throws RemoteException { + public void getUserDataId(@NonNull IResultReceiver receiver) throws RemoteException { final int userId = UserHandle.getCallingUserId(); + UserData userData = null; synchronized (mLock) { final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId); if (service != null) { - final UserData userData = service.getUserData(getCallingUid()); - return userData == null ? null : userData.getId(); + userData = service.getUserData(getCallingUid()); } else if (sVerbose) { Slog.v(TAG, "getUserDataId(): no service for " + userId); } } - - return null; + final String userDataId = userData == null ? null : userData.getId(); + send(receiver, userDataId); } @Override @@ -964,89 +1000,96 @@ public final class AutofillManagerService extends SystemService { } @Override - public boolean isFieldClassificationEnabled() throws RemoteException { + public void isFieldClassificationEnabled(@NonNull IResultReceiver receiver) + throws RemoteException { final int userId = UserHandle.getCallingUserId(); + boolean enabled = false; synchronized (mLock) { final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId); if (service != null) { - return service.isFieldClassificationEnabled(getCallingUid()); + enabled = service.isFieldClassificationEnabled(getCallingUid()); } else if (sVerbose) { Slog.v(TAG, "isFieldClassificationEnabled(): no service for " + userId); } } - - return false; + send(receiver, enabled); } @Override - public String getDefaultFieldClassificationAlgorithm() throws RemoteException { + public void getDefaultFieldClassificationAlgorithm(@NonNull IResultReceiver receiver) + throws RemoteException { final int userId = UserHandle.getCallingUserId(); + String algorithm = null; synchronized (mLock) { final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId); if (service != null) { - return service.getDefaultFieldClassificationAlgorithm(getCallingUid()); + algorithm = service.getDefaultFieldClassificationAlgorithm(getCallingUid()); } else { if (sVerbose) { Slog.v(TAG, "getDefaultFcAlgorithm(): no service for " + userId); } - return null; } } + send(receiver, algorithm); } @Override - public String[] getAvailableFieldClassificationAlgorithms() throws RemoteException { + public void getAvailableFieldClassificationAlgorithms(@NonNull IResultReceiver receiver) + throws RemoteException { final int userId = UserHandle.getCallingUserId(); + String[] algorithms = null; synchronized (mLock) { final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId); if (service != null) { - return service.getAvailableFieldClassificationAlgorithms(getCallingUid()); + algorithms = service.getAvailableFieldClassificationAlgorithms(getCallingUid()); } else { if (sVerbose) { Slog.v(TAG, "getAvailableFcAlgorithms(): no service for " + userId); } - return null; } } + send(receiver, algorithms); } @Override - public ComponentName getAutofillServiceComponentName() throws RemoteException { + public void getAutofillServiceComponentName(@NonNull IResultReceiver receiver) + throws RemoteException { final int userId = UserHandle.getCallingUserId(); + ComponentName componentName = null; synchronized (mLock) { final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId); if (service != null) { - return service.getServiceComponentName(); + componentName = service.getServiceComponentName(); } else if (sVerbose) { Slog.v(TAG, "getAutofillServiceComponentName(): no service for " + userId); } } - - return null; + send(receiver, componentName); } @Override - public boolean restoreSession(int sessionId, IBinder activityToken, IBinder appCallback) + public void restoreSession(int sessionId, @NonNull IBinder activityToken, + @NonNull IBinder appCallback, @NonNull IResultReceiver receiver) throws RemoteException { final int userId = UserHandle.getCallingUserId(); activityToken = Preconditions.checkNotNull(activityToken, "activityToken"); appCallback = Preconditions.checkNotNull(appCallback, "appCallback"); + boolean restored = false; synchronized (mLock) { final AutofillManagerServiceImpl service = mServicesCache.get(userId); if (service != null) { - return service.restoreSession(sessionId, getCallingUid(), activityToken, + restored = service.restoreSession(sessionId, getCallingUid(), activityToken, appCallback); } else if (sVerbose) { Slog.v(TAG, "restoreSession(): no service for " + userId); } } - - return false; + send(receiver, restored); } @Override @@ -1112,23 +1155,27 @@ public final class AutofillManagerService extends SystemService { } @Override - public boolean isServiceSupported(int userId) { + public void isServiceSupported(int userId, @NonNull IResultReceiver receiver) { + boolean supported = false; synchronized (mLock) { - return !mDisabledUsers.get(userId); + supported = !mDisabledUsers.get(userId); } + send(receiver, supported); } @Override - public boolean isServiceEnabled(int userId, String packageName) { + public void isServiceEnabled(int userId, @NonNull String packageName, + @NonNull IResultReceiver receiver) { + boolean enabled = false; synchronized (mLock) { final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId); if (service != null) { - return Objects.equals(packageName, service.getServicePackageName()); + enabled = Objects.equals(packageName, service.getServicePackageName()); } else if (sVerbose) { Slog.v(TAG, "isServiceEnabled(): no service for " + userId); } - return false; } + send(receiver, enabled); } @Override diff --git a/services/core/java/com/android/server/LocationManagerService.java b/services/core/java/com/android/server/LocationManagerService.java index de02e81a00c4..776cf47a1e56 100644 --- a/services/core/java/com/android/server/LocationManagerService.java +++ b/services/core/java/com/android/server/LocationManagerService.java @@ -1549,6 +1549,22 @@ public class LocationManagerService extends ILocationManager.Stub { return -1; } + private static String resolutionLevelToOpStr(int allowedResolutionLevel) { + switch(allowedResolutionLevel) { + case RESOLUTION_LEVEL_COARSE: + return AppOpsManager.OPSTR_COARSE_LOCATION; + case RESOLUTION_LEVEL_FINE: + return AppOpsManager.OPSTR_FINE_LOCATION; + case RESOLUTION_LEVEL_NONE: + // The client is not allowed to get any location, so both FINE and COARSE ops will + // be denied. Pick the most restrictive one to be safe. + return AppOpsManager.OPSTR_FINE_LOCATION; + default: + // Use the most restrictive ops if not sure. + return AppOpsManager.OPSTR_FINE_LOCATION; + } + } + boolean reportLocationAccessNoThrow( int pid, int uid, String packageName, int allowedResolutionLevel) { int op = resolutionLevelToOp(allowedResolutionLevel); @@ -2295,7 +2311,7 @@ public class LocationManagerService extends ILocationManager.Stub { } // Don't return stale location to apps with foreground-only location permission. - String op = getResolutionPermission(allowedResolutionLevel); + String op = resolutionLevelToOpStr(allowedResolutionLevel); long locationAgeMs = SystemClock.elapsedRealtime() - location.getElapsedRealtimeNanos() / NANOS_PER_MILLI; if ((locationAgeMs > mLastLocationMaxAgeMs) diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 5028fd59124f..14eeb7853263 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -86,7 +86,6 @@ import static android.os.Process.setThreadPriority; import static android.os.Process.setThreadScheduler; import static android.os.Process.startWebView; import static android.os.Process.zygoteProcess; -import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER; import static android.provider.Settings.Global.ALWAYS_FINISH_ACTIVITIES; import static android.provider.Settings.Global.DEBUG_APP; import static android.provider.Settings.Global.NETWORK_ACCESS_TIMEOUT_MS; @@ -164,7 +163,6 @@ import android.app.ActivityManagerInternal; import android.app.ActivityManagerProto; import android.app.ActivityOptions; import android.app.ActivityThread; -import android.app.AlertDialog; import android.app.AppGlobals; import android.app.AppOpsManager; import android.app.AppOpsManagerInternal.CheckOpsDelegate; @@ -206,7 +204,6 @@ import android.content.ComponentName; import android.content.ContentProvider; import android.content.ContentResolver; import android.content.Context; -import android.content.DialogInterface; import android.content.IContentProvider; import android.content.IIntentReceiver; import android.content.IIntentSender; @@ -262,7 +259,6 @@ import android.os.Looper; import android.os.Message; import android.os.Parcel; import android.os.ParcelFileDescriptor; -import android.os.PowerManager; import android.os.PowerManager.ServiceType; import android.os.PowerManagerInternal; import android.os.Process; @@ -285,7 +281,6 @@ import android.os.storage.StorageManager; import android.os.storage.StorageManagerInternal; import android.provider.Downloads; import android.provider.Settings; -import android.service.voice.IVoiceInteractionSession; import android.text.TextUtils; import android.text.format.DateUtils; import android.text.style.SuggestionSpan; @@ -323,10 +318,8 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.DumpHeapActivity; import com.android.internal.app.IAppOpsCallback; import com.android.internal.app.IAppOpsService; -import com.android.internal.app.IVoiceInteractor; import com.android.internal.app.ProcessMap; import com.android.internal.app.SystemUserHomeActivity; -import com.android.internal.app.procstats.AssociationState; import com.android.internal.app.procstats.ProcessStats; import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; import com.android.internal.notification.SystemNotificationChannels; @@ -364,21 +357,9 @@ import com.android.server.SystemService; import com.android.server.SystemServiceManager; import com.android.server.ThreadPriorityBooster; import com.android.server.Watchdog; -import com.android.server.am.ActivityManagerServiceDumpActivitiesProto; -import com.android.server.am.ActivityManagerServiceDumpBroadcastsProto; -import com.android.server.am.ActivityManagerServiceDumpProcessesProto; import com.android.server.am.ActivityManagerServiceDumpProcessesProto.UidObserverRegistrationProto; -import com.android.server.am.ActivityManagerServiceDumpServicesProto; -import com.android.server.am.ActivityManagerServiceProto; import com.android.server.am.ActivityStack.ActivityState; -import com.android.server.am.GrantUriProto; -import com.android.server.am.ImportanceTokenProto; -import com.android.server.am.MemInfoDumpProto; import com.android.server.am.MemoryStatUtil.MemoryStat; -import com.android.server.am.NeededUriGrantsProto; -import com.android.server.am.ProcessOomProto; -import com.android.server.am.ProcessToGcProto; -import com.android.server.am.StickyBroadcastProto; import com.android.server.firewall.IntentFirewall; import com.android.server.job.JobSchedulerInternal; import com.android.server.pm.Installer; @@ -387,7 +368,6 @@ import com.android.server.pm.dex.DexManager; import com.android.server.utils.PriorityDump; import com.android.server.vr.VrManagerInternal; import com.android.server.wm.ActivityTaskManagerInternal; -import com.android.server.wm.ActivityTaskManagerInternal.SleepToken; import com.android.server.wm.WindowManagerService; import org.xmlpull.v1.XmlPullParser; @@ -540,6 +520,9 @@ public class ActivityManagerService extends IActivityManager.Stub // Used to indicate that an app transition should be animated. static final boolean ANIMATE = true; + // If set, we will push process association information in to procstats. + static final boolean TRACK_PROCSTATS_ASSOCIATIONS = true; + /** * Default value for {@link Settings.Global#NETWORK_ACCESS_TIMEOUT_MS}. */ @@ -583,6 +566,8 @@ public class ActivityManagerService extends IActivityManager.Stub public final IntentFirewall mIntentFirewall; + public OomAdjProfiler mOomAdjProfiler = new OomAdjProfiler(); + // Whether we should use SCHED_FIFO for UI and RenderThreads. private boolean mUseFifoUiScheduling = false; @@ -2648,6 +2633,7 @@ public class ActivityManagerService extends IActivityManager.Stub mOnBattery = DEBUG_POWER ? true : mBatteryStatsService.getActiveStatistics().getIsOnBattery(); mBatteryStatsService.getActiveStatistics().setCallback(this); + mOomAdjProfiler.batteryPowerChanged(mOnBattery); mProcessStats = new ProcessStatsService(this, new File(systemDir, "procstats")); @@ -2936,12 +2922,14 @@ public class ActivityManagerService extends IActivityManager.Stub synchronized(mPidsSelfLocked) { mOnBattery = DEBUG_POWER ? true : onBattery; } + mOomAdjProfiler.batteryPowerChanged(onBattery); } } @Override public void batteryStatsReset() { BinderCallsStatsService.reset(); + mOomAdjProfiler.reset(); } @Override @@ -9020,7 +9008,8 @@ public class ActivityManagerService extends IActivityManager.Stub } ContentProviderConnection incProviderCountLocked(ProcessRecord r, - final ContentProviderRecord cpr, IBinder externalProcessToken, boolean stable) { + final ContentProviderRecord cpr, IBinder externalProcessToken, int callingUid, + String callingTag, boolean stable) { if (r != null) { for (int i=0; i<r.conProviders.size(); i++) { ContentProviderConnection conn = r.conProviders.get(i); @@ -9055,7 +9044,7 @@ public class ActivityManagerService extends IActivityManager.Stub cpr.uid, cpr.appInfo.longVersionCode, cpr.name, cpr.info.processName); return conn; } - cpr.addExternalProcessHandleLocked(externalProcessToken); + cpr.addExternalProcessHandleLocked(externalProcessToken, callingUid, callingTag); return null; } @@ -9132,7 +9121,8 @@ public class ActivityManagerService extends IActivityManager.Stub } private ContentProviderHolder getContentProviderImpl(IApplicationThread caller, - String name, IBinder token, boolean stable, int userId) { + String name, IBinder token, int callingUid, String callingTag, boolean stable, + int userId) { ContentProviderRecord cpr; ContentProviderConnection conn = null; ProviderInfo cpi = null; @@ -9225,7 +9215,7 @@ public class ActivityManagerService extends IActivityManager.Stub // In this case the provider instance already exists, so we can // return it right away. - conn = incProviderCountLocked(r, cpr, token, stable); + conn = incProviderCountLocked(r, cpr, token, callingUid, callingTag, stable); if (conn != null && (conn.stableCount+conn.unstableCount) == 1) { if (cpr.proc != null && r.setAdj <= ProcessList.PERCEPTIBLE_APP_ADJ) { // If this is a perceptible app accessing the provider, @@ -9469,7 +9459,7 @@ public class ActivityManagerService extends IActivityManager.Stub } mProviderMap.putProviderByName(name, cpr); - conn = incProviderCountLocked(r, cpr, token, stable); + conn = incProviderCountLocked(r, cpr, token, callingUid, callingTag, stable); if (conn != null) { conn.waiting = true; } @@ -9580,21 +9570,23 @@ public class ActivityManagerService extends IActivityManager.Stub } // The incoming user check is now handled in checkContentProviderPermissionLocked() to deal // with cross-user grant. - return getContentProviderImpl(caller, name, null, stable, userId); + return getContentProviderImpl(caller, name, null, Binder.getCallingUid(), null, stable, + userId); } public ContentProviderHolder getContentProviderExternal( - String name, int userId, IBinder token) { + String name, int userId, IBinder token, String tag) { enforceCallingPermission(android.Manifest.permission.ACCESS_CONTENT_PROVIDERS_EXTERNALLY, "Do not have permission in call getContentProviderExternal()"); userId = mUserController.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(), userId, false, ALLOW_FULL_ONLY, "getContentProvider", null); - return getContentProviderExternalUnchecked(name, token, userId); + return getContentProviderExternalUnchecked(name, token, Binder.getCallingUid(), + tag != null ? tag : "*external*", userId); } private ContentProviderHolder getContentProviderExternalUnchecked(String name, - IBinder token, int userId) { - return getContentProviderImpl(null, name, token, true, userId); + IBinder token, int callingUid, String callingTag, int userId) { + return getContentProviderImpl(null, name, token, callingUid, callingTag, true, userId); } /** @@ -9986,7 +9978,8 @@ public class ActivityManagerService extends IActivityManager.Stub } ContentProviderHolder holder = null; try { - holder = getContentProviderExternalUnchecked(name, null, userId); + holder = getContentProviderExternalUnchecked(name, null, callingUid, + "*getmimetype*", userId); if (holder != null) { return holder.provider.getType(uri); } @@ -10201,7 +10194,8 @@ public class ActivityManagerService extends IActivityManager.Stub final int userId = UserHandle.getCallingUserId(); final Uri uri = Uri.parse(uriString); String name = uri.getAuthority(); - ContentProviderHolder cph = getContentProviderExternalUnchecked(name, null, userId); + ContentProviderHolder cph = getContentProviderExternalUnchecked(name, null, + Binder.getCallingUid(), "*opencontent*", userId); ParcelFileDescriptor pfd = null; if (cph != null) { // We record the binder invoker's uid in thread-local storage before @@ -10257,6 +10251,7 @@ public class ActivityManagerService extends IActivityManager.Stub mServices.updateScreenStateLocked(isAwake); reportCurWakefulnessUsageEventLocked(); mActivityTaskManager.onScreenAwakeChanged(isAwake); + mOomAdjProfiler.onWakefulnessChanged(wakefulness); } updateOomAdjLocked(); } @@ -12127,7 +12122,7 @@ public class ActivityManagerService extends IActivityManager.Stub return imp; } - private void fillInProcMemInfoLocked(ProcessRecord app, + private void fillInProcMemInfo(ProcessRecord app, ActivityManager.RunningAppProcessInfo outInfo, int clientTargetSdk) { outInfo.pid = app.pid; @@ -12147,8 +12142,6 @@ public class ActivityManagerService extends IActivityManager.Stub outInfo.importance = procStateToImportance(procState, adj, outInfo, clientTargetSdk); outInfo.importanceReasonCode = app.adjTypeCode; outInfo.processState = app.curProcState; - outInfo.isFocused = (app == getTopAppLocked()); - outInfo.lastActivityTime = app.lastActivityTime; } @Override @@ -12179,7 +12172,7 @@ public class ActivityManagerService extends IActivityManager.Stub ActivityManager.RunningAppProcessInfo currApp = new ActivityManager.RunningAppProcessInfo(app.processName, app.pid, app.getPackageList()); - fillInProcMemInfoLocked(app, currApp, clientTargetSdk); + fillInProcMemInfo(app, currApp, clientTargetSdk); if (app.adjSource instanceof ProcessRecord) { currApp.importanceReasonPid = ((ProcessRecord)app.adjSource).pid; currApp.importanceReasonImportance = @@ -12248,7 +12241,7 @@ public class ActivityManagerService extends IActivityManager.Stub proc = mPidsSelfLocked.get(Binder.getCallingPid()); } if (proc != null) { - fillInProcMemInfoLocked(proc, outState, clientTargetSdk); + fillInProcMemInfo(proc, outState, clientTargetSdk); } } } @@ -12746,6 +12739,11 @@ public class ActivityManagerService extends IActivityManager.Stub pw.println("-------------------------------------------------------------------------------"); } dumpProcessesLocked(fd, pw, args, opti, dumpAll, dumpPackage, dumpAppId); + pw.println(); + if (dumpAll) { + pw.println("-------------------------------------------------------------------------------"); + } + mOomAdjProfiler.dump(pw); } } Binder.restoreCallingIdentity(origId); @@ -16031,6 +16029,7 @@ public class ActivityManagerService extends IActivityManager.Stub } updateCpuStatsNow(); long[] memtrackTmp = new long[1]; + long[] swaptrackTmp = new long[2]; final List<ProcessCpuTracker.Stats> stats; // Get a list of Stats that have vsize > 0 synchronized (mProcessCpuTracker) { @@ -16041,12 +16040,13 @@ public class ActivityManagerService extends IActivityManager.Stub final int statsCount = stats.size(); for (int i = 0; i < statsCount; i++) { ProcessCpuTracker.Stats st = stats.get(i); - long pss = Debug.getPss(st.pid, null, memtrackTmp); + long pss = Debug.getPss(st.pid, swaptrackTmp, memtrackTmp); if (pss > 0) { if (infoMap.indexOfKey(st.pid) < 0) { ProcessMemInfo mi = new ProcessMemInfo(st.name, st.pid, ProcessList.NATIVE_ADJ, -1, "native", null); mi.pss = pss; + mi.swapPss = swaptrackTmp[1]; mi.memtrack = memtrackTmp[0]; memInfos.add(mi); } @@ -16054,14 +16054,17 @@ public class ActivityManagerService extends IActivityManager.Stub } long totalPss = 0; + long totalSwapPss = 0; long totalMemtrack = 0; for (int i=0, N=memInfos.size(); i<N; i++) { ProcessMemInfo mi = memInfos.get(i); if (mi.pss == 0) { - mi.pss = Debug.getPss(mi.pid, null, memtrackTmp); + mi.pss = Debug.getPss(mi.pid, swaptrackTmp, memtrackTmp); + mi.swapPss = swaptrackTmp[1]; mi.memtrack = memtrackTmp[0]; } totalPss += mi.pss; + totalSwapPss += mi.swapPss; totalMemtrack += mi.memtrack; } Collections.sort(memInfos, new Comparator<ProcessMemInfo>() { @@ -16223,7 +16226,7 @@ public class ActivityManagerService extends IActivityManager.Stub memInfoBuilder.append("\n"); memInfoBuilder.append(" Lost RAM: "); memInfoBuilder.append(stringifyKBSize(memInfo.getTotalSizeKb() - - totalPss - memInfo.getFreeSizeKb() - memInfo.getCachedSizeKb() + - (totalPss - totalSwapPss) - memInfo.getFreeSizeKb() - memInfo.getCachedSizeKb() - memInfo.getKernelUsedSizeKb() - memInfo.getZramTotalSizeKb())); memInfoBuilder.append("\n"); Slog.i(TAG, "Low on memory:"); @@ -18889,6 +18892,7 @@ public class ActivityManagerService extends IActivityManager.Stub // The process is being computed, so there is a cycle. We cannot // rely on this process's state. app.containsCycle = true; + return false; } } @@ -18913,6 +18917,7 @@ public class ActivityManagerService extends IActivityManager.Stub final int logUid = mCurOomAdjUid; int prevAppAdj = app.curAdj; + int prevProcState = app.curProcState; if (app.maxAdj <= ProcessList.FOREGROUND_APP_ADJ) { // The max adjustment doesn't allow this app to be anything @@ -19281,19 +19286,25 @@ public class ActivityManagerService extends IActivityManager.Stub // all connected clients. ConnectionRecord cr = clist.get(i); if (cr.binding.client == app) { - // Binding to ourself is not interesting. + // Binding to oneself is not interesting. continue; } + boolean trackedProcState = false; if ((cr.flags&Context.BIND_WAIVE_PRIORITY) == 0) { ProcessRecord client = cr.binding.client; computeOomAdjLocked(client, cachedAdj, TOP_APP, doingAll, now); if (client.containsCycle) { - // We've detected a cycle. We should ignore this connection and allow - // this process to retry computeOomAdjLocked later in case a later-checked - // connection from a client would raise its priority legitimately. + // We've detected a cycle. We should retry computeOomAdjLocked later in + // case a later-checked connection from a client would raise its + // priority legitimately. app.containsCycle = true; - continue; + // If the client has not been completely evaluated, skip using its + // priority. Else use the conservative value for now and look for a + // better state in the next iteration. + if (client.completedAdjSeq < mAdjSeq) { + continue; + } } int clientAdj = client.curRawAdj; int clientProcState = client.curProcState; @@ -19356,6 +19367,8 @@ public class ActivityManagerService extends IActivityManager.Stub newAdj = ProcessList.PERSISTENT_SERVICE_ADJ; schedGroup = ProcessList.SCHED_GROUP_DEFAULT; procState = ActivityManager.PROCESS_STATE_PERSISTENT; + cr.trackProcState(procState, mAdjSeq, now); + trackedProcState = true; } } else if ((cr.flags&Context.BIND_NOT_VISIBLE) != 0 && clientAdj < ProcessList.PERCEPTIBLE_APP_ADJ @@ -19441,6 +19454,9 @@ public class ActivityManagerService extends IActivityManager.Stub ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND; } } + if (!trackedProcState) { + cr.trackProcState(clientProcState, mAdjSeq, now); + } if (procState > clientProcState) { procState = clientProcState; if (adjType == null) { @@ -19517,11 +19533,16 @@ public class ActivityManagerService extends IActivityManager.Stub } computeOomAdjLocked(client, cachedAdj, TOP_APP, doingAll, now); if (client.containsCycle) { - // We've detected a cycle. We should ignore this connection and allow - // this process to retry computeOomAdjLocked later in case a later-checked - // connection from a client would raise its priority legitimately. + // We've detected a cycle. We should retry computeOomAdjLocked later in + // case a later-checked connection from a client would raise its + // priority legitimately. app.containsCycle = true; - continue; + // If the client has not been completely evaluated, skip using its + // priority. Else use the conservative value for now and look for a + // better state in the next iteration. + if (client.completedAdjSeq < mAdjSeq) { + continue; + } } int clientAdj = client.curRawAdj; int clientProcState = client.curProcState; @@ -19570,6 +19591,7 @@ public class ActivityManagerService extends IActivityManager.Stub } } } + conn.trackProcState(clientProcState, mAdjSeq, now); if (procState > clientProcState) { procState = clientProcState; } @@ -19753,8 +19775,8 @@ public class ActivityManagerService extends IActivityManager.Stub app.foregroundActivities = foregroundActivities; app.completedAdjSeq = mAdjSeq; - // if curAdj is less than prevAppAdj, then this process was promoted - return app.curAdj < prevAppAdj; + // if curAdj or curProcState improved, then this process was promoted + return app.curAdj < prevAppAdj || app.curProcState < prevProcState; } /** @@ -20668,23 +20690,22 @@ public class ActivityManagerService extends IActivityManager.Stub } @GuardedBy("this") - ProcessRecord getTopAppLocked() { - final ActivityRecord TOP_ACT = resumedAppLocked(); - if (TOP_ACT != null && TOP_ACT.hasProcess()) { - return (ProcessRecord) TOP_ACT.app.mOwner; - } else { - return null; - } - } - - @GuardedBy("this") final void updateOomAdjLocked() { - final ProcessRecord TOP_APP = getTopAppLocked(); + mOomAdjProfiler.oomAdjStarted(); + final ActivityRecord TOP_ACT = resumedAppLocked(); + final ProcessRecord TOP_APP = TOP_ACT != null && TOP_ACT.hasProcess() + ? (ProcessRecord) TOP_ACT.app.mOwner : null; final long now = SystemClock.uptimeMillis(); final long nowElapsed = SystemClock.elapsedRealtime(); final long oldTime = now - ProcessList.MAX_EMPTY_TIME; final int N = mLruProcesses.size(); + if (false) { + RuntimeException e = new RuntimeException(); + e.fillInStackTrace(); + Slog.i(TAG, "updateOomAdj: top=" + TOP_ACT, e); + } + // Reset state in all uid records. for (int i=mActiveUids.size()-1; i>=0; i--) { final UidRecord uidRec = mActiveUids.valueAt(i); @@ -20818,7 +20839,7 @@ public class ActivityManagerService extends IActivityManager.Stub // - Continue retrying until no process was promoted. // - Iterate from least important to most important. int cycleCount = 0; - while (retryCycles) { + while (retryCycles && cycleCount < 10) { cycleCount++; retryCycles = false; @@ -20833,12 +20854,14 @@ public class ActivityManagerService extends IActivityManager.Stub for (int i=0; i<N; i++) { ProcessRecord app = mLruProcesses.get(i); if (!app.killedByAm && app.thread != null && app.containsCycle == true) { + if (computeOomAdjLocked(app, ProcessList.UNKNOWN_ADJ, TOP_APP, true, now)) { retryCycles = true; } } } } + for (int i=N-1; i>=0; i--) { ProcessRecord app = mLruProcesses.get(i); if (!app.killedByAm && app.thread != null) { @@ -20902,6 +20925,8 @@ public class ActivityManagerService extends IActivityManager.Stub } } + mProcessStats.updateTrackingAssociationsLocked(mAdjSeq, now); + incrementProcStateSeqAndNotifyAppsLocked(); mNumServiceProcs = mNewNumServiceProcs; @@ -21178,6 +21203,7 @@ public class ActivityManagerService extends IActivityManager.Stub Slog.d(TAG_OOM_ADJ, "Did OOM ADJ in " + duration + "ms"); } } + mOomAdjProfiler.oomAdjEnded(); } @Override diff --git a/services/core/java/com/android/server/am/AppTaskImpl.java b/services/core/java/com/android/server/am/AppTaskImpl.java index c22dedc4073f..a1f1ff9cc3ec 100644 --- a/services/core/java/com/android/server/am/AppTaskImpl.java +++ b/services/core/java/com/android/server/am/AppTaskImpl.java @@ -97,7 +97,7 @@ class AppTaskImpl extends IAppTask.Stub { final int callingUid = Binder.getCallingUid(); final long origId = Binder.clearCallingIdentity(); try { - synchronized (this) { + synchronized (mService.mGlobalLock) { mService.mStackSupervisor.startActivityFromRecents(callingPid, callingUid, mTaskId, null); } diff --git a/services/core/java/com/android/server/am/ConnectionRecord.java b/services/core/java/com/android/server/am/ConnectionRecord.java index c348aeee8b86..fa8e6c431770 100644 --- a/services/core/java/com/android/server/am/ConnectionRecord.java +++ b/services/core/java/com/android/server/am/ConnectionRecord.java @@ -106,9 +106,14 @@ final class ConnectionRecord { } public void startAssociationIfNeeded() { - if (association == null) { - ProcessStats.ProcessStateHolder holder = binding.service.app != null - ? binding.service.app.pkgList.get(binding.service.name.getPackageName()) : null; + // If we don't already have an active association, create one... but only if this + // is an association between two different processes. + if (ActivityManagerService.TRACK_PROCSTATS_ASSOCIATIONS + && association == null && binding.service.app != null + && (binding.service.appInfo.uid != clientUid + || !binding.service.processName.equals(clientProcessName))) { + ProcessStats.ProcessStateHolder holder = binding.service.app.pkgList.get( + binding.service.name.getPackageName()); if (holder == null) { Slog.wtf(TAG_AM, "No package in referenced service " + binding.service.name.toShortString() + ": proc=" + binding.service.app); @@ -116,7 +121,7 @@ final class ConnectionRecord { Slog.wtf(TAG_AM, "Inactive holder in referenced service " + binding.service.name.toShortString() + ": proc=" + binding.service.app); } else { - association = holder.pkg.getAssociationStateLocked(binding.service.processName, + association = holder.pkg.getAssociationStateLocked(holder.state, binding.service.name.getClassName()).startSource(clientUid, clientProcessName); @@ -124,6 +129,12 @@ final class ConnectionRecord { } } + public void trackProcState(int procState, int seq, long now) { + if (association != null) { + association.trackProcState(procState, seq, now); + } + } + public void stopAssociation() { if (association != null) { association.stop(); diff --git a/services/core/java/com/android/server/am/ContentProviderConnection.java b/services/core/java/com/android/server/am/ContentProviderConnection.java index 0320b10b9a7a..f2d4f739c8bf 100644 --- a/services/core/java/com/android/server/am/ContentProviderConnection.java +++ b/services/core/java/com/android/server/am/ContentProviderConnection.java @@ -53,9 +53,14 @@ public final class ContentProviderConnection extends Binder { } public void startAssociationIfNeeded() { - if (association == null) { - ProcessStats.ProcessStateHolder holder = provider.proc != null - ? provider.proc.pkgList.get(provider.name.getPackageName()) : null; + // If we don't already have an active association, create one... but only if this + // is an association between two different processes. + if (ActivityManagerService.TRACK_PROCSTATS_ASSOCIATIONS + && association == null && provider.proc != null + && (provider.appInfo.uid != client.uid + || !provider.info.processName.equals(client.processName))) { + ProcessStats.ProcessStateHolder holder = provider.proc.pkgList.get( + provider.name.getPackageName()); if (holder == null) { Slog.wtf(TAG_AM, "No package in referenced provider " + provider.name.toShortString() + ": proc=" + provider.proc); @@ -63,13 +68,19 @@ public final class ContentProviderConnection extends Binder { Slog.wtf(TAG_AM, "Inactive holder in referenced provider " + provider.name.toShortString() + ": proc=" + provider.proc); } else { - association = holder.pkg.getAssociationStateLocked(provider.info.processName, + association = holder.pkg.getAssociationStateLocked(holder.state, provider.name.getClassName()).startSource(client.uid, client.processName); } } } + public void trackProcState(int procState, int seq, long now) { + if (association != null) { + association.trackProcState(procState, seq, now); + } + } + public void stopAssociation() { if (association != null) { association.stop(); diff --git a/services/core/java/com/android/server/am/ContentProviderRecord.java b/services/core/java/com/android/server/am/ContentProviderRecord.java index 07ae1aedc686..c22966d631f6 100644 --- a/services/core/java/com/android/server/am/ContentProviderRecord.java +++ b/services/core/java/com/android/server/am/ContentProviderRecord.java @@ -16,6 +16,8 @@ package com.android.server.am; +import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM; + import android.app.ContentProviderHolder; import android.content.ComponentName; import android.content.IContentProvider; @@ -26,11 +28,14 @@ import android.os.IBinder.DeathRecipient; import android.os.Process; import android.os.RemoteException; import android.os.UserHandle; +import android.util.ArrayMap; import android.util.Slog; +import com.android.internal.app.procstats.AssociationState; +import com.android.internal.app.procstats.ProcessStats; + import java.io.PrintWriter; import java.util.ArrayList; -import java.util.HashMap; final class ContentProviderRecord implements ComponentName.WithComponentName { final ActivityManagerService service; @@ -46,7 +51,7 @@ final class ContentProviderRecord implements ComponentName.WithComponentName { = new ArrayList<ContentProviderConnection>(); //final HashSet<ProcessRecord> clients = new HashSet<ProcessRecord>(); // Handles for non-framework processes supported by this provider - HashMap<IBinder, ExternalProcessHandle> externalProcessTokenToHandle; + ArrayMap<IBinder, ExternalProcessHandle> externalProcessTokenToHandle; // Count for external process for which we have no handles. int externalProcessNoHandleCount; ProcessRecord proc; // if non-null, hosting process. @@ -85,12 +90,24 @@ final class ContentProviderRecord implements ComponentName.WithComponentName { public void setProcess(ProcessRecord proc) { this.proc = proc; - for (int iconn = connections.size() - 1; iconn >= 0; iconn--) { - final ContentProviderConnection conn = connections.get(iconn); - if (proc != null) { - conn.startAssociationIfNeeded(); - } else { - conn.stopAssociation(); + if (ActivityManagerService.TRACK_PROCSTATS_ASSOCIATIONS) { + for (int iconn = connections.size() - 1; iconn >= 0; iconn--) { + final ContentProviderConnection conn = connections.get(iconn); + if (proc != null) { + conn.startAssociationIfNeeded(); + } else { + conn.stopAssociation(); + } + } + if (externalProcessTokenToHandle != null) { + for (int iext = externalProcessTokenToHandle.size() - 1; iext >= 0; iext--) { + final ExternalProcessHandle handle = externalProcessTokenToHandle.valueAt(iext); + if (proc != null) { + handle.startAssociationIfNeeded(this); + } else { + handle.stopAssociation(); + } + } } } } @@ -100,17 +117,18 @@ final class ContentProviderRecord implements ComponentName.WithComponentName { && uid == app.info.uid; } - public void addExternalProcessHandleLocked(IBinder token) { + public void addExternalProcessHandleLocked(IBinder token, int callingUid, String callingTag) { if (token == null) { externalProcessNoHandleCount++; } else { if (externalProcessTokenToHandle == null) { - externalProcessTokenToHandle = new HashMap<IBinder, ExternalProcessHandle>(); + externalProcessTokenToHandle = new ArrayMap<>(); } ExternalProcessHandle handle = externalProcessTokenToHandle.get(token); if (handle == null) { - handle = new ExternalProcessHandle(token); + handle = new ExternalProcessHandle(token, callingUid, callingTag); externalProcessTokenToHandle.put(token, handle); + handle.startAssociationIfNeeded(this); } handle.mAcquisitionCount++; } @@ -141,6 +159,7 @@ final class ContentProviderRecord implements ComponentName.WithComponentName { private void removeExternalProcessHandleInternalLocked(IBinder token) { ExternalProcessHandle handle = externalProcessTokenToHandle.get(token); handle.unlinkFromOwnDeathLocked(); + handle.stopAssociation(); externalProcessTokenToHandle.remove(token); if (externalProcessTokenToHandle.size() == 0) { externalProcessTokenToHandle = null; @@ -246,11 +265,16 @@ final class ContentProviderRecord implements ComponentName.WithComponentName { private class ExternalProcessHandle implements DeathRecipient { private static final String LOG_TAG = "ExternalProcessHanldle"; - private final IBinder mToken; - private int mAcquisitionCount; + final IBinder mToken; + final int mOwningUid; + final String mOwningProcessName; + int mAcquisitionCount; + AssociationState.SourceState mAssociation; - public ExternalProcessHandle(IBinder token) { + public ExternalProcessHandle(IBinder token, int owningUid, String owningProcessName) { mToken = token; + mOwningUid = owningUid; + mOwningProcessName = owningProcessName; try { token.linkToDeath(this, 0); } catch (RemoteException re) { @@ -262,6 +286,37 @@ final class ContentProviderRecord implements ComponentName.WithComponentName { mToken.unlinkToDeath(this, 0); } + public void startAssociationIfNeeded(ContentProviderRecord provider) { + // If we don't already have an active association, create one... but only if this + // is an association between two different processes. + if (ActivityManagerService.TRACK_PROCSTATS_ASSOCIATIONS + && mAssociation == null && provider.proc != null + && (provider.appInfo.uid != mOwningUid + || !provider.info.processName.equals(mOwningProcessName))) { + ProcessStats.ProcessStateHolder holder = provider.proc.pkgList.get( + provider.name.getPackageName()); + if (holder == null) { + Slog.wtf(TAG_AM, "No package in referenced provider " + + provider.name.toShortString() + ": proc=" + provider.proc); + } else if (holder.pkg == null) { + Slog.wtf(TAG_AM, "Inactive holder in referenced provider " + + provider.name.toShortString() + ": proc=" + provider.proc); + } else { + mAssociation = holder.pkg.getAssociationStateLocked(holder.state, + provider.name.getClassName()).startSource(mOwningUid, + mOwningProcessName); + + } + } + } + + public void stopAssociation() { + if (mAssociation != null) { + mAssociation.stop(); + mAssociation = null; + } + } + @Override public void binderDied() { synchronized (service) { diff --git a/services/core/java/com/android/server/am/OomAdjProfiler.java b/services/core/java/com/android/server/am/OomAdjProfiler.java new file mode 100644 index 000000000000..6230e0dfaa55 --- /dev/null +++ b/services/core/java/com/android/server/am/OomAdjProfiler.java @@ -0,0 +1,185 @@ +/* + * 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.am; + +import android.os.PowerManagerInternal; +import android.os.Process; +import android.os.SystemClock; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.os.BackgroundThread; +import com.android.internal.os.ProcessCpuTracker; +import com.android.internal.util.RingBuffer; +import com.android.internal.util.function.pooled.PooledLambda; + +import java.io.PrintWriter; + +public class OomAdjProfiler { + @GuardedBy("this") + private boolean mOnBattery; + @GuardedBy("this") + private boolean mScreenOff; + + @GuardedBy("this") + private long mOomAdjStartTimeMs; + @GuardedBy("this") + private boolean mOomAdjStarted; + + @GuardedBy("this") + private CpuTimes mOomAdjRunTime = new CpuTimes(); + @GuardedBy("this") + private CpuTimes mSystemServerCpuTime = new CpuTimes(); + + @GuardedBy("this") + private long mLastSystemServerCpuTimeMs; + @GuardedBy("this") + private boolean mSystemServerCpuTimeUpdateScheduled; + private final ProcessCpuTracker mProcessCpuTracker = new ProcessCpuTracker(false); + + @GuardedBy("this") + final RingBuffer<CpuTimes> mOomAdjRunTimesHist = new RingBuffer<>(CpuTimes.class, 10); + @GuardedBy("this") + final RingBuffer<CpuTimes> mSystemServerCpuTimesHist = new RingBuffer<>(CpuTimes.class, 10); + + void batteryPowerChanged(boolean onBattery) { + synchronized (this) { + scheduleSystemServerCpuTimeUpdate(); + mOnBattery = onBattery; + } + } + + void onWakefulnessChanged(int wakefulness) { + synchronized (this) { + scheduleSystemServerCpuTimeUpdate(); + mScreenOff = wakefulness != PowerManagerInternal.WAKEFULNESS_AWAKE; + } + } + + void oomAdjStarted() { + synchronized (this) { + mOomAdjStartTimeMs = SystemClock.currentThreadTimeMillis(); + mOomAdjStarted = true; + } + } + + void oomAdjEnded() { + synchronized (this) { + if (!mOomAdjStarted) { + return; + } + mOomAdjRunTime.addCpuTimeMs(SystemClock.currentThreadTimeMillis() - mOomAdjStartTimeMs); + } + } + + private void scheduleSystemServerCpuTimeUpdate() { + synchronized (this) { + if (mSystemServerCpuTimeUpdateScheduled) { + return; + } + mSystemServerCpuTimeUpdateScheduled = true; + BackgroundThread.getHandler().post(PooledLambda.obtainRunnable( + OomAdjProfiler::updateSystemServerCpuTime, + this, mOnBattery, mScreenOff).recycleOnUse()); + } + } + + private void updateSystemServerCpuTime(boolean onBattery, boolean screenOff) { + final long cpuTimeMs = mProcessCpuTracker.getCpuTimeForPid(Process.myPid()); + synchronized (this) { + mSystemServerCpuTime.addCpuTimeMs( + cpuTimeMs - mLastSystemServerCpuTimeMs, onBattery, screenOff); + mLastSystemServerCpuTimeMs = cpuTimeMs; + mSystemServerCpuTimeUpdateScheduled = false; + notifyAll(); + } + } + + void reset() { + synchronized (this) { + if (mSystemServerCpuTime.isEmpty()) { + return; + } + mOomAdjRunTimesHist.append(mOomAdjRunTime); + mSystemServerCpuTimesHist.append(mSystemServerCpuTime); + mOomAdjRunTime = new CpuTimes(); + mSystemServerCpuTime = new CpuTimes(); + } + } + + void dump(PrintWriter pw) { + synchronized (this) { + if (mSystemServerCpuTimeUpdateScheduled) { + while (mSystemServerCpuTimeUpdateScheduled) { + try { + wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } else { + updateSystemServerCpuTime(mOnBattery, mScreenOff); + } + + pw.println("System server and oomAdj runtimes (ms) in recent battery sessions " + + "(most recent first):"); + if (!mSystemServerCpuTime.isEmpty()) { + pw.print(" "); + pw.print("system_server="); + pw.print(mSystemServerCpuTime); + pw.print(" "); + pw.print("oom_adj="); + pw.println(mOomAdjRunTime); + } + final CpuTimes[] systemServerCpuTimes = mSystemServerCpuTimesHist.toArray(); + final CpuTimes[] oomAdjRunTimes = mOomAdjRunTimesHist.toArray(); + for (int i = oomAdjRunTimes.length - 1; i >= 0; --i) { + pw.print(" "); + pw.print("system_server="); + pw.print(systemServerCpuTimes[i]); + pw.print(" "); + pw.print("oom_adj="); + pw.println(oomAdjRunTimes[i]); + } + } + } + + private class CpuTimes { + private long mOnBatteryTimeMs; + private long mOnBatteryScreenOffTimeMs; + + public void addCpuTimeMs(long cpuTimeMs) { + addCpuTimeMs(cpuTimeMs, mOnBattery, mScreenOff); + } + + public void addCpuTimeMs(long cpuTimeMs, boolean onBattery, boolean screenOff) { + if (onBattery) { + mOnBatteryTimeMs += cpuTimeMs; + if (screenOff) { + mOnBatteryScreenOffTimeMs += cpuTimeMs; + } + } + } + + public boolean isEmpty() { + return mOnBatteryTimeMs == 0 && mOnBatteryScreenOffTimeMs == 0; + } + + public String toString() { + return "[" + mOnBatteryTimeMs + "," + mOnBatteryScreenOffTimeMs + "]"; + } + } +} diff --git a/services/core/java/com/android/server/am/ProcessMemInfo.java b/services/core/java/com/android/server/am/ProcessMemInfo.java index 83d29e2bba68..6c10a2af9fe6 100644 --- a/services/core/java/com/android/server/am/ProcessMemInfo.java +++ b/services/core/java/com/android/server/am/ProcessMemInfo.java @@ -24,6 +24,7 @@ public class ProcessMemInfo { final String adjType; final String adjReason; long pss; + long swapPss; long memtrack; public ProcessMemInfo(String _name, int _pid, int _oomAdj, int _procState, diff --git a/services/core/java/com/android/server/am/ProcessStatsService.java b/services/core/java/com/android/server/am/ProcessStatsService.java index e1bc1bccdb14..f0bd8fa31478 100644 --- a/services/core/java/com/android/server/am/ProcessStatsService.java +++ b/services/core/java/com/android/server/am/ProcessStatsService.java @@ -32,7 +32,6 @@ import android.util.TimeUtils; import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.GuardedBy; -import com.android.internal.app.procstats.AssociationState; import com.android.internal.app.procstats.DumpUtils; import com.android.internal.app.procstats.IProcessStats; import com.android.internal.app.procstats.ProcessState; @@ -194,6 +193,11 @@ public final class ProcessStatsService extends IProcessStats.Stub { } @GuardedBy("mAm") + public void updateTrackingAssociationsLocked(int curSeq, long now) { + mProcessStats.updateTrackingAssociationsLocked(curSeq, now); + } + + @GuardedBy("mAm") public boolean shouldWriteNowLocked(long now) { if (now > (mLastWriteTime+WRITE_PERIOD)) { if (SystemClock.elapsedRealtime() diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java index f72b8010429b..8f436206f480 100644 --- a/services/core/java/com/android/server/am/ServiceRecord.java +++ b/services/core/java/com/android/server/am/ServiceRecord.java @@ -502,14 +502,16 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN public void setProcess(ProcessRecord _proc) { app = _proc; - for (int conni=connections.size()-1; conni>=0; conni--) { - ArrayList<ConnectionRecord> cr = connections.valueAt(conni); - for (int i=0; i<cr.size(); i++) { - final ConnectionRecord conn = cr.get(i); - if (_proc != null) { - conn.startAssociationIfNeeded(); - } else { - conn.stopAssociation(); + if (ActivityManagerService.TRACK_PROCSTATS_ASSOCIATIONS) { + for (int conni = connections.size() - 1; conni >= 0; conni--) { + ArrayList<ConnectionRecord> cr = connections.valueAt(conni); + for (int i = 0; i < cr.size(); i++) { + final ConnectionRecord conn = cr.get(i); + if (_proc != null) { + conn.startAssociationIfNeeded(); + } else { + conn.stopAssociation(); + } } } } diff --git a/services/core/java/com/android/server/am/TEST_MAPPING b/services/core/java/com/android/server/am/TEST_MAPPING new file mode 100644 index 000000000000..9e11eb0fa603 --- /dev/null +++ b/services/core/java/com/android/server/am/TEST_MAPPING @@ -0,0 +1,70 @@ +{ + "presubmit": [ + { + "name": "CtsActivityManagerDeviceTestCases", + "options": [ + { + "include-annotation": "android.platform.test.annotations.Presubmit" + }, + { + "exclude-annotation": "android.support.test.filters.FlakyTest" + } + ] + }, + { + "name": "CtsActivityManagerDeviceSdk25TestCases", + "options": [ + { + "include-annotation": "android.platform.test.annotations.Presubmit" + }, + { + "exclude-annotation": "android.support.test.filters.FlakyTest" + } + ] + }, + { + "name": "CtsAppTestCases", + "options": [ + { + "include-filter": "android.app.cts.TaskDescriptionTest" + }, + { + "include-annotation": "android.platform.test.annotations.Presubmit" + }, + { + "exclude-annotation": "android.support.test.filters.FlakyTest" + } + ] + }, + { + "name": "FrameworksServicesTests", + "options": [ + { + "include-filter": "com.android.server.am." + }, + { + "include-annotation": "android.platform.test.annotations.Presubmit" + }, + { + "exclude-annotation": "android.support.test.filters.FlakyTest" + } + ] + } + ], + "postsubmit": [ + { + "name": "CtsActivityManagerDeviceTestCases" + }, + { + "name": "CtsActivityManagerDeviceSdk25TestCases" + }, + { + "name": "FrameworksServicesTests", + "options": [ + { + "include-filter": "com.android.server.am." + } + ] + } + ] +} diff --git a/services/core/java/com/android/server/content/SyncJobService.java b/services/core/java/com/android/server/content/SyncJobService.java index 089632dbe01c..bfcc629541f8 100644 --- a/services/core/java/com/android/server/content/SyncJobService.java +++ b/services/core/java/com/android/server/content/SyncJobService.java @@ -16,12 +16,10 @@ package com.android.server.content; +import android.annotation.Nullable; import android.app.job.JobParameters; import android.app.job.JobService; -import android.content.Intent; import android.os.Message; -import android.os.Messenger; -import android.os.RemoteException; import android.os.SystemClock; import android.util.Log; import android.util.Slog; @@ -34,78 +32,86 @@ import com.android.internal.annotations.GuardedBy; public class SyncJobService extends JobService { private static final String TAG = "SyncManager"; - public static final String EXTRA_MESSENGER = "messenger"; + private static final Object sLock = new Object(); - private Messenger mMessenger; + @GuardedBy("sLock") + private static SyncJobService sInstance; - private final Object mLock = new Object(); + @GuardedBy("sLock") + private static final SparseArray<JobParameters> sJobParamsMap = new SparseArray<>(); - @GuardedBy("mLock") - private final SparseArray<JobParameters> mJobParamsMap = new SparseArray<>(); + @GuardedBy("sLock") + private static final SparseBooleanArray sStartedSyncs = new SparseBooleanArray(); - @GuardedBy("mLock") - private final SparseBooleanArray mStartedSyncs = new SparseBooleanArray(); + @GuardedBy("sLock") + private static final SparseLongArray sJobStartUptimes = new SparseLongArray(); - @GuardedBy("mLock") - private final SparseLongArray mJobStartUptimes = new SparseLongArray(); + private static final SyncLogger sLogger = SyncLogger.getInstance(); - private final SyncLogger mLogger = SyncLogger.getInstance(); - - /** - * This service is started by the SyncManager which passes a messenger object to - * communicate back with it. It never stops while the device is running. - */ - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - mMessenger = intent.getParcelableExtra(EXTRA_MESSENGER); - Message m = Message.obtain(); - m.what = SyncManager.SyncHandler.MESSAGE_JOBSERVICE_OBJECT; - m.obj = this; - sendMessage(m); - - return START_NOT_STICKY; + private void updateInstance() { + synchronized (SyncJobService.class) { + sInstance = this; + } } - private void sendMessage(Message message) { - if (mMessenger == null) { - Slog.e(TAG, "Messenger not initialized."); - return; + @Nullable + private static SyncJobService getInstance() { + synchronized (sLock) { + if (sInstance == null) { + Slog.wtf(TAG, "sInstance == null"); + } + return sInstance; } - try { - mMessenger.send(message); - } catch (RemoteException e) { - Slog.e(TAG, e.toString()); + } + + public static boolean isReady() { + synchronized (sLock) { + return sInstance != null; } } @Override public boolean onStartJob(JobParameters params) { + updateInstance(); + + sLogger.purgeOldLogs(); - mLogger.purgeOldLogs(); + SyncOperation op = SyncOperation.maybeCreateFromJobExtras(params.getExtras()); + + if (op == null) { + Slog.wtf(TAG, "Got invalid job " + params.getJobId()); + return false; + } + + final boolean readyToSync = SyncManager.readyToSync(op.target.userId); + + sLogger.log("onStartJob() jobid=", params.getJobId(), " op=", op, + " readyToSync", readyToSync); + + if (!readyToSync) { + // If the user isn't unlocked or the device has been provisioned yet, just stop the job + // at this point. If it's a non-periodic sync, ask the job scheduler to reschedule it. + // If it's a periodic sync, then just wait until the next cycle. + final boolean wantsReschedule = !op.isPeriodic; + jobFinished(params, wantsReschedule); + return true; + } boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE); - synchronized (mLock) { + synchronized (sLock) { final int jobId = params.getJobId(); - mJobParamsMap.put(jobId, params); + sJobParamsMap.put(jobId, params); - mStartedSyncs.delete(jobId); - mJobStartUptimes.put(jobId, SystemClock.uptimeMillis()); + sStartedSyncs.delete(jobId); + sJobStartUptimes.put(jobId, SystemClock.uptimeMillis()); } Message m = Message.obtain(); m.what = SyncManager.SyncHandler.MESSAGE_START_SYNC; - SyncOperation op = SyncOperation.maybeCreateFromJobExtras(params.getExtras()); - - mLogger.log("onStartJob() jobid=", params.getJobId(), " op=", op); - - if (op == null) { - Slog.e(TAG, "Got invalid job " + params.getJobId()); - return false; - } if (isLoggable) { Slog.v(TAG, "Got start job message " + op.target); } m.obj = op; - sendMessage(m); + SyncManager.sendMessage(m); return true; } @@ -115,15 +121,22 @@ public class SyncJobService extends JobService { Slog.v(TAG, "onStopJob called " + params.getJobId() + ", reason: " + params.getStopReason()); } - final boolean readyToSync = SyncManager.readyToSync(); + final SyncOperation op = SyncOperation.maybeCreateFromJobExtras(params.getExtras()); + if (op == null) { + Slog.wtf(TAG, "Got invalid job " + params.getJobId()); + return false; + } - mLogger.log("onStopJob() ", mLogger.jobParametersToString(params), + final boolean readyToSync = SyncManager.readyToSync(op.target.userId); + + sLogger.log("onStopJob() ", sLogger.jobParametersToString(params), " readyToSync=", readyToSync); - synchronized (mLock) { + + synchronized (sLock) { final int jobId = params.getJobId(); - mJobParamsMap.remove(jobId); + sJobParamsMap.remove(jobId); - final long startUptime = mJobStartUptimes.get(jobId); + final long startUptime = sJobStartUptimes.get(jobId); final long nowUptime = SystemClock.uptimeMillis(); final long runtime = nowUptime - startUptime; @@ -135,61 +148,57 @@ public class SyncJobService extends JobService { // WTF if startSyncH() hasn't happened, *unless* onStopJob() was called too soon. // (1 minute threshold.) // Also don't wtf when it's not ready to sync. - if (readyToSync && !mStartedSyncs.get(jobId)) { + if (readyToSync && !sStartedSyncs.get(jobId)) { wtf("Job " + jobId + " didn't start: " + " startUptime=" + startUptime + " nowUptime=" + nowUptime + " params=" + jobParametersToString(params)); } - } else if (runtime < 10 * 1000) { - // This happens too in a normal case too, and it's rather too often. - // Disable it for now. -// // Job stopped too soon. WTF. -// wtf("Job " + jobId + " stopped in " + runtime + " ms: " -// + " startUptime=" + startUptime -// + " nowUptime=" + nowUptime -// + " params=" + jobParametersToString(params)); } - mStartedSyncs.delete(jobId); - mJobStartUptimes.delete(jobId); + sStartedSyncs.delete(jobId); + sJobStartUptimes.delete(jobId); } Message m = Message.obtain(); m.what = SyncManager.SyncHandler.MESSAGE_STOP_SYNC; - m.obj = SyncOperation.maybeCreateFromJobExtras(params.getExtras()); - if (m.obj == null) { - return false; - } + m.obj = op; // Reschedule if this job was NOT explicitly canceled. m.arg1 = params.getStopReason() != JobParameters.REASON_CANCELED ? 1 : 0; // Apply backoff only if stop is called due to timeout. m.arg2 = params.getStopReason() == JobParameters.REASON_TIMEOUT ? 1 : 0; - sendMessage(m); + SyncManager.sendMessage(m); return false; } - public void callJobFinished(int jobId, boolean needsReschedule, String why) { - synchronized (mLock) { - JobParameters params = mJobParamsMap.get(jobId); - mLogger.log("callJobFinished()", + public static void callJobFinished(int jobId, boolean needsReschedule, String why) { + final SyncJobService instance = getInstance(); + if (instance != null) { + instance.callJobFinishedInner(jobId, needsReschedule, why); + } + } + + public void callJobFinishedInner(int jobId, boolean needsReschedule, String why) { + synchronized (sLock) { + JobParameters params = sJobParamsMap.get(jobId); + sLogger.log("callJobFinished()", " jobid=", jobId, " needsReschedule=", needsReschedule, - " ", mLogger.jobParametersToString(params), + " ", sLogger.jobParametersToString(params), " why=", why); if (params != null) { jobFinished(params, needsReschedule); - mJobParamsMap.remove(jobId); + sJobParamsMap.remove(jobId); } else { Slog.e(TAG, "Job params not found for " + String.valueOf(jobId)); } } } - public void markSyncStarted(int jobId) { - synchronized (mLock) { - mStartedSyncs.put(jobId, true); + public static void markSyncStarted(int jobId) { + synchronized (sLock) { + sStartedSyncs.put(jobId, true); } } @@ -203,8 +212,8 @@ public class SyncJobService extends JobService { } } - private void wtf(String message) { - mLogger.log(message); + private static void wtf(String message) { + sLogger.log(message); Slog.wtf(TAG, message); } } diff --git a/services/core/java/com/android/server/content/SyncManager.java b/services/core/java/com/android/server/content/SyncManager.java index 0a640b8a76c6..d06e78514a5c 100644 --- a/services/core/java/com/android/server/content/SyncManager.java +++ b/services/core/java/com/android/server/content/SyncManager.java @@ -21,6 +21,7 @@ import android.accounts.AccountAndUser; import android.accounts.AccountManager; import android.accounts.AccountManagerInternal; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.AppGlobals; @@ -72,7 +73,6 @@ import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.Message; -import android.os.Messenger; import android.os.PowerManager; import android.os.Process; import android.os.RemoteCallback; @@ -89,6 +89,7 @@ import android.util.EventLog; import android.util.Log; import android.util.Pair; import android.util.Slog; +import android.util.SparseBooleanArray; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; @@ -227,7 +228,6 @@ public class SyncManager { // TODO: add better locking around mRunningAccounts private volatile AccountAndUser[] mRunningAccounts = INITIAL_ACCOUNTS_ARRAY; - volatile private PowerManager.WakeLock mHandleAlarmWakeLock; volatile private PowerManager.WakeLock mSyncManagerWakeLock; volatile private boolean mDataConnectionIsConnected = false; volatile private boolean mStorageIsLow = false; @@ -238,7 +238,6 @@ public class SyncManager { private final IBatteryStats mBatteryStats; private JobScheduler mJobScheduler; private JobSchedulerInternal mJobSchedulerInternal; - private SyncJobService mSyncJobService; private SyncStorageEngine mSyncStorageEngine; @@ -318,16 +317,6 @@ public class SyncManager { } }; - private final BroadcastReceiver mBootCompletedReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - mBootCompleted = true; - // Called because it gets all pending jobs and stores them in mScheduledSyncs cache. - verifyJobScheduler(); - mSyncHandler.onBootCompleted(); - } - }; - private final BroadcastReceiver mAccountsUpdatedReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -371,14 +360,14 @@ public class SyncManager { m.sendToTarget(); } - private void doDatabaseCleanup() { + private void removeStaleAccounts() { for (UserInfo user : mUserManager.getUsers(true)) { // Skip any partially created/removed users if (user.partial) continue; Account[] accountsForUser = AccountManagerService.getSingleton().getAccounts( user.id, mContext.getOpPackageName()); - mSyncStorageEngine.doDatabaseCleanup(accountsForUser, user.id); + mSyncStorageEngine.removeStaleAccounts(accountsForUser, user.id); } } @@ -464,8 +453,8 @@ public class SyncManager { private final SyncHandler mSyncHandler; private final SyncManagerConstants mConstants; - private volatile boolean mBootCompleted = false; - private volatile boolean mJobServiceReady = false; + @GuardedBy("mUnlockedUsers") + private final SparseBooleanArray mUnlockedUsers = new SparseBooleanArray(); private ConnectivityManager getConnectivityManager() { synchronized (this) { @@ -641,12 +630,6 @@ public class SyncManager { IntentFilter intentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); context.registerReceiver(mConnectivityIntentReceiver, intentFilter); - if (!factoryTest) { - intentFilter = new IntentFilter(Intent.ACTION_BOOT_COMPLETED); - intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); - context.registerReceiver(mBootCompletedReceiver, intentFilter); - } - intentFilter = new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW); intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); context.registerReceiver(mStorageIntentReceiver, intentFilter); @@ -690,14 +673,6 @@ public class SyncManager { mBatteryStats = IBatteryStats.Stub.asInterface(ServiceManager.getService( BatteryStats.SERVICE_NAME)); - // This WakeLock is used to ensure that we stay awake between the time that we receive - // a sync alarm notification and when we finish processing it. We need to do this - // because we don't do the work in the alarm handler, rather we do it in a message - // handler. - mHandleAlarmWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, - HANDLE_SYNC_ALARM_WAKE_LOCK); - mHandleAlarmWakeLock.setReferenceCounted(false); - // This WakeLock is used to ensure that we stay awake while running the sync loop // message handler. Normally we will hold a sync adapter wake lock while it is being // synced but during the execution of the sync loop it might finish a sync for @@ -715,7 +690,6 @@ public class SyncManager { public void onChange(boolean selfChange) { mProvisioned |= isDeviceProvisioned(); if (mProvisioned) { - mSyncHandler.onDeviceProvisioned(); resolver.unregisterContentObserver(this); } } @@ -744,19 +718,6 @@ public class SyncManager { null, null); } - // Set up the communication channel between the scheduled job and the sync manager. - // This is posted to the *main* looper intentionally, to defer calling startService() - // until after the lengthy primary boot sequence completes on that thread, to avoid - // spurious ANR triggering. - final Intent startServiceIntent = new Intent(mContext, SyncJobService.class); - startServiceIntent.putExtra(SyncJobService.EXTRA_MESSENGER, new Messenger(mSyncHandler)); - new Handler(mContext.getMainLooper()).post(new Runnable() { - @Override - public void run() { - mContext.startService(startServiceIntent); - } - }); - // Sync adapters were able to access the synced account without the accounts // permission which circumvents our permission model. Therefore, we require // sync adapters that don't have access to the account to get user consent. @@ -768,16 +729,31 @@ public class SyncManager { mLogger.log("Sync manager initialized: " + Build.FINGERPRINT); } - public void onStartUser(int userHandle) { - mSyncHandler.post(() -> mLogger.log("onStartUser: user=", userHandle)); + public void onStartUser(int userId) { + // Log on the handler to avoid slowing down device boot. + mSyncHandler.post(() -> mLogger.log("onStartUser: user=", userId)); } - public void onUnlockUser(int userHandle) { - mSyncHandler.post(() -> mLogger.log("onUnlockUser: user=", userHandle)); + public void onUnlockUser(int userId) { + synchronized (mUnlockedUsers) { + mUnlockedUsers.put(userId, true); + } + // Log on the handler to avoid slowing down device boot. + mSyncHandler.post(() -> mLogger.log("onUnlockUser: user=", userId)); + } + + public void onStopUser(int userId) { + synchronized (mUnlockedUsers) { + mUnlockedUsers.put(userId, false); + } + // Log on the handler to avoid slowing down user switch. + mSyncHandler.post(() -> mLogger.log("onStopUser: user=", userId)); } - public void onStopUser(int userHandle) { - mSyncHandler.post(() -> mLogger.log("onStopUser: user=", userHandle)); + private boolean isUserUnlocked(int userId) { + synchronized (mUnlockedUsers) { + return mUnlockedUsers.get(userId); + } } public void onBootPhase(int phase) { @@ -1820,7 +1796,7 @@ public class SyncManager { updateRunningAccounts(null /* Don't sync any target */); // Clean up the storage engine database - mSyncStorageEngine.doDatabaseCleanup(new Account[0], userId); + mSyncStorageEngine.removeStaleAccounts(null, userId); List<SyncOperation> ops = getAllPendingSyncs(); for (SyncOperation op: ops) { if (op.target.userId == userId) { @@ -2235,8 +2211,13 @@ public class SyncManager { mSyncStorageEngine.resetTodayStats(/* force=*/ false); for (AccountAndUser account : accounts) { - pw.printf("Account %s u%d %s\n", - account.account.name, account.userId, account.account.type); + final boolean unlocked; + synchronized (mUnlockedUsers) { + unlocked = mUnlockedUsers.get(account.userId); + } + pw.printf("Account %s u%d %s%s\n", + account.account.name, account.userId, account.account.type, + (unlocked ? "" : " (locked)")); pw.println("======================================================================="); final PrintTable table = new PrintTable(16); @@ -2872,13 +2853,29 @@ public class SyncManager { } } + @Nullable + private static SyncManager getInstance() { + synchronized (SyncManager.class) { + if (sInstance == null) { + Slog.wtf(TAG, "sInstance == null"); // Maybe called too early? + } + return sInstance; + } + } + /** - * @return whether the device is ready to run sync jobs. + * @return whether the device is ready to run sync jobs for a given user. */ - public static boolean readyToSync() { - synchronized (SyncManager.class) { - return sInstance != null && sInstance.mProvisioned && sInstance.mBootCompleted - && sInstance.mJobServiceReady; + public static boolean readyToSync(int userId) { + final SyncManager instance = getInstance(); + return (instance != null) && SyncJobService.isReady() + && instance.mProvisioned && instance.isUserUnlocked(userId); + } + + public static void sendMessage(Message message) { + final SyncManager instance = getInstance(); + if (instance != null) { + instance.mSyncHandler.sendMessage(message); } } @@ -2889,11 +2886,9 @@ public class SyncManager { class SyncHandler extends Handler { // Messages that can be sent on mHandler. private static final int MESSAGE_SYNC_FINISHED = 1; - private static final int MESSAGE_RELEASE_MESSAGES_FROM_QUEUE = 2; private static final int MESSAGE_SERVICE_CONNECTED = 4; private static final int MESSAGE_SERVICE_DISCONNECTED = 5; private static final int MESSAGE_CANCEL = 6; - static final int MESSAGE_JOBSERVICE_OBJECT = 7; static final int MESSAGE_START_SYNC = 10; static final int MESSAGE_STOP_SYNC = 11; static final int MESSAGE_SCHEDULE_SYNC = 12; @@ -2910,86 +2905,17 @@ public class SyncManager { public final SyncTimeTracker mSyncTimeTracker = new SyncTimeTracker(); private final HashMap<String, PowerManager.WakeLock> mWakeLocks = Maps.newHashMap(); - private List<Message> mUnreadyQueue = new ArrayList<Message>(); - - void onBootCompleted() { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Slog.v(TAG, "Boot completed."); - } - checkIfDeviceReady(); - } - - void onDeviceProvisioned() { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "mProvisioned=" + mProvisioned); - } - checkIfDeviceReady(); - } - - void checkIfDeviceReady() { - if (mProvisioned && mBootCompleted && mJobServiceReady) { - synchronized(this) { - mSyncStorageEngine.restoreAllPeriodicSyncs(); - // Dispatch any stashed messages. - obtainMessage(MESSAGE_RELEASE_MESSAGES_FROM_QUEUE).sendToTarget(); - } - } - } - - /** - * Stash any messages that come to the handler before boot is complete or before the device - * is properly provisioned (i.e. out of set-up wizard). - * {@link #onBootCompleted()} and {@link SyncHandler#onDeviceProvisioned} both - * need to come in before we start syncing. - * @param msg Message to dispatch at a later point. - * @return true if a message was enqueued, false otherwise. This is to avoid losing the - * message if we manage to acquire the lock but by the time we do boot has completed. - */ - private boolean tryEnqueueMessageUntilReadyToRun(Message msg) { - synchronized (this) { - if (!mBootCompleted || !mProvisioned || !mJobServiceReady) { - // Need to copy the message bc looper will recycle it. - Message m = Message.obtain(msg); - mUnreadyQueue.add(m); - return true; - } else { - return false; - } - } - } - public SyncHandler(Looper looper) { super(looper); } public void handleMessage(Message msg) { + // TODO Do we really need this wake lock?? If we actually needed it, this is probably + // not the best place to acquire the lock -- it's probably too late, because the device + // could have gone to sleep before we reach here. + mSyncManagerWakeLock.acquire(); try { - mSyncManagerWakeLock.acquire(); - // We only want to enqueue sync related messages until device is ready. - // Other messages are handled without enqueuing. - if (msg.what == MESSAGE_JOBSERVICE_OBJECT) { - Slog.i(TAG, "Got SyncJobService instance."); - mSyncJobService = (SyncJobService) msg.obj; - mJobServiceReady = true; - checkIfDeviceReady(); - } else if (msg.what == SyncHandler.MESSAGE_ACCOUNTS_UPDATED) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Slog.v(TAG, "handleSyncHandlerMessage: MESSAGE_ACCOUNTS_UPDATED"); - } - EndPoint targets = (EndPoint) msg.obj; - updateRunningAccountsH(targets); - } else if (msg.what == MESSAGE_RELEASE_MESSAGES_FROM_QUEUE) { - if (mUnreadyQueue != null) { - for (Message m : mUnreadyQueue) { - handleSyncMessage(m); - } - mUnreadyQueue = null; - } - } else if (tryEnqueueMessageUntilReadyToRun(msg)) { - // No work to be done. - } else { - handleSyncMessage(msg); - } + handleSyncMessage(msg); } finally { mSyncManagerWakeLock.release(); } @@ -3001,6 +2927,13 @@ public class SyncManager { try { mDataConnectionIsConnected = readDataConnectionState(); switch (msg.what) { + case MESSAGE_ACCOUNTS_UPDATED: + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Slog.v(TAG, "handleSyncHandlerMessage: MESSAGE_ACCOUNTS_UPDATED"); + } + EndPoint targets = (EndPoint) msg.obj; + updateRunningAccountsH(targets); + break; case MESSAGE_SCHEDULE_SYNC: ScheduleSyncMessagePayload syncPayload = (ScheduleSyncMessagePayload) msg.obj; @@ -3069,7 +3002,7 @@ public class SyncManager { if (isLoggable) { Slog.v(TAG, "syncFinished" + payload.activeSyncContext.mSyncOperation); } - mSyncJobService.callJobFinished( + SyncJobService.callJobFinished( payload.activeSyncContext.mSyncOperation.jobId, false, "sync finished"); runSyncFinishedOrCanceledH(payload.syncResult, @@ -3119,7 +3052,7 @@ public class SyncManager { // which is a soft error. SyncResult syncResult = new SyncResult(); syncResult.stats.numIoExceptions++; - mSyncJobService.callJobFinished( + SyncJobService.callJobFinished( currentSyncContext.mSyncOperation.jobId, false, "service disconnected"); runSyncFinishedOrCanceledH(syncResult, currentSyncContext); @@ -3138,7 +3071,7 @@ public class SyncManager { Log.w(TAG, String.format( "Detected sync making no progress for %s. cancelling.", monitoredSyncContext)); - mSyncJobService.callJobFinished( + SyncJobService.callJobFinished( monitoredSyncContext.mSyncOperation.jobId, false, "no network activity"); runSyncFinishedOrCanceledH( @@ -3175,7 +3108,7 @@ public class SyncManager { private void deferSyncH(SyncOperation op, long delay, String why) { mLogger.log("deferSyncH() ", (op.isPeriodic ? "periodic " : ""), "sync. op=", op, " delay=", delay, " why=", why); - mSyncJobService.callJobFinished(op.jobId, false, why); + SyncJobService.callJobFinished(op.jobId, false, why); if (op.isPeriodic) { scheduleSyncOperationH(op.createOneTimeSyncOperation(), delay); } else { @@ -3213,7 +3146,7 @@ public class SyncManager { // assume the clock is correct. mSyncStorageEngine.setClockValid(); - mSyncJobService.markSyncStarted(op.jobId); + SyncJobService.markSyncStarted(op.jobId); if (mStorageIsLow) { deferSyncH(op, SYNC_DELAY_ON_LOW_STORAGE, "storage low"); @@ -3226,7 +3159,7 @@ public class SyncManager { List<SyncOperation> ops = getAllPendingSyncs(); for (SyncOperation syncOperation: ops) { if (syncOperation.sourcePeriodicId == op.jobId) { - mSyncJobService.callJobFinished(op.jobId, false, + SyncJobService.callJobFinished(op.jobId, false, "periodic sync, pending"); return; } @@ -3235,7 +3168,7 @@ public class SyncManager { // executing according to some backoff criteria. for (ActiveSyncContext asc: mActiveSyncContexts) { if (asc.mSyncOperation.sourcePeriodicId == op.jobId) { - mSyncJobService.callJobFinished(op.jobId, false, + SyncJobService.callJobFinished(op.jobId, false, "periodic sync, already running"); return; } @@ -3272,13 +3205,13 @@ public class SyncManager { switch (syncOpState) { case SYNC_OP_STATE_INVALID_NO_ACCOUNT_ACCESS: case SYNC_OP_STATE_INVALID: { - mSyncJobService.callJobFinished(op.jobId, false, + SyncJobService.callJobFinished(op.jobId, false, "invalid op state: " + syncOpState); } return; } if (!dispatchSyncOperation(op)) { - mSyncJobService.callJobFinished(op.jobId, false, "dispatchSyncOperation() failed"); + SyncJobService.callJobFinished(op.jobId, false, "dispatchSyncOperation() failed"); } setAuthorityPendingState(op.target); @@ -3306,9 +3239,7 @@ public class SyncManager { if (mLogger.enabled()) { mLogger.log("updateRunningAccountsH: ", Arrays.toString(mRunningAccounts)); } - if (mBootCompleted) { - doDatabaseCleanup(); - } + removeStaleAccounts(); AccountAndUser[] accounts = mRunningAccounts; for (ActiveSyncContext currentSyncContext : mActiveSyncContexts) { @@ -3453,7 +3384,7 @@ public class SyncManager { if (op.sourcePeriodicId == syncOperation.jobId || op.jobId == syncOperation.jobId) { ActiveSyncContext asc = findActiveSyncContextH(syncOperation.jobId); if (asc != null) { - mSyncJobService.callJobFinished(syncOperation.jobId, false, + SyncJobService.callJobFinished(syncOperation.jobId, false, "removePeriodicSyncInternalH"); runSyncFinishedOrCanceledH(null, asc); } @@ -3662,7 +3593,7 @@ public class SyncManager { false /* no config settings */)) { continue; } - mSyncJobService.callJobFinished(activeSyncContext.mSyncOperation.jobId, false, + SyncJobService.callJobFinished(activeSyncContext.mSyncOperation.jobId, false, why); runSyncFinishedOrCanceledH(null /* cancel => no result */, activeSyncContext); } diff --git a/services/core/java/com/android/server/content/SyncStorageEngine.java b/services/core/java/com/android/server/content/SyncStorageEngine.java index 811dc755af6b..391e3b01da08 100644 --- a/services/core/java/com/android/server/content/SyncStorageEngine.java +++ b/services/core/java/com/android/server/content/SyncStorageEngine.java @@ -19,6 +19,7 @@ package com.android.server.content; import android.accounts.Account; import android.accounts.AccountAndUser; import android.accounts.AccountManager; +import android.annotation.Nullable; import android.app.backup.BackupManager; import android.content.ComponentName; import android.content.ContentResolver; @@ -1005,7 +1006,7 @@ public class SyncStorageEngine { * Called when the set of account has changed, given the new array of * active accounts. */ - public void doDatabaseCleanup(Account[] accounts, int userId) { + public void removeStaleAccounts(@Nullable Account[] accounts, int userId) { synchronized (mAuthorities) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Slog.v(TAG, "Updating for new accounts..."); @@ -1014,8 +1015,9 @@ public class SyncStorageEngine { Iterator<AccountInfo> accIt = mAccounts.values().iterator(); while (accIt.hasNext()) { AccountInfo acc = accIt.next(); - if (!ArrayUtils.contains(accounts, acc.accountAndUser.account) - && acc.accountAndUser.userId == userId) { + if ((accounts == null) || ( + (acc.accountAndUser.userId == userId) + && !ArrayUtils.contains(accounts, acc.accountAndUser.account))) { // This account no longer exists... if (Log.isLoggable(TAG, Log.VERBOSE)) { Slog.v(TAG, "Account removed: " + acc.accountAndUser); diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java index 1c3342a9af42..b97e90487123 100644 --- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java +++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java @@ -400,6 +400,7 @@ class AutomaticBrightnessController { } else if (mLightSensorEnabled) { mLightSensorEnabled = false; mAmbientLuxValid = !mResetAmbientLuxAfterWarmUpConfig; + mScreenAutoBrightness = -1; mRecentLightSamples = 0; mAmbientLightRingBuffer.clear(); mCurrentLightSensorRate = -1; diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index 99412c56b274..b124ac78a8ef 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -791,9 +791,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call && mAutomaticBrightnessController != null; final boolean userSetBrightnessChanged = updateUserSetScreenBrightness(); - if (userSetBrightnessChanged) { - mTemporaryScreenBrightness = -1; - } // Use the temporary screen brightness if there isn't an override, either from // WindowManager or based on the display state. @@ -1514,11 +1511,13 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call } if (mCurrentScreenBrightnessSetting == mPendingScreenBrightnessSetting) { mPendingScreenBrightnessSetting = -1; + mTemporaryScreenBrightness = -1; return false; } mCurrentScreenBrightnessSetting = mPendingScreenBrightnessSetting; mLastUserSetScreenBrightness = mPendingScreenBrightnessSetting; mPendingScreenBrightnessSetting = -1; + mTemporaryScreenBrightness = -1; return true; } diff --git a/services/core/java/com/android/server/hdmi/Constants.java b/services/core/java/com/android/server/hdmi/Constants.java index a2a55e532ef4..0eb18a8f8cd5 100644 --- a/services/core/java/com/android/server/hdmi/Constants.java +++ b/services/core/java/com/android/server/hdmi/Constants.java @@ -200,6 +200,8 @@ final class Constants { static final int UNKNOWN_VOLUME = -1; + static final String PROPERTY_PREFERRED_ADDRESS_AUDIO_SYSTEM = + "persist.sys.hdmi.addr.audiosystem"; static final String PROPERTY_PREFERRED_ADDRESS_PLAYBACK = "persist.sys.hdmi.addr.playback"; static final String PROPERTY_PREFERRED_ADDRESS_TV = "persist.sys.hdmi.addr.tv"; diff --git a/services/core/java/com/android/server/hdmi/DelayedMessageBuffer.java b/services/core/java/com/android/server/hdmi/DelayedMessageBuffer.java index 2c1a7d5d18e6..15ec486f989b 100644 --- a/services/core/java/com/android/server/hdmi/DelayedMessageBuffer.java +++ b/services/core/java/com/android/server/hdmi/DelayedMessageBuffer.java @@ -85,7 +85,7 @@ final class DelayedMessageBuffer { void processAllMessages() { // Use the copied buffer. - ArrayList<HdmiCecMessage> copiedBuffer = new ArrayList<HdmiCecMessage>(mBuffer); + ArrayList<HdmiCecMessage> copiedBuffer = new ArrayList<>(mBuffer); mBuffer.clear(); for (HdmiCecMessage message : copiedBuffer) { mDevice.onMessage(message); @@ -104,7 +104,7 @@ final class DelayedMessageBuffer { * are associated with */ void processMessagesForDevice(int address) { - ArrayList<HdmiCecMessage> copiedBuffer = new ArrayList<HdmiCecMessage>(mBuffer); + ArrayList<HdmiCecMessage> copiedBuffer = new ArrayList<>(mBuffer); mBuffer.clear(); HdmiLogger.debug("Checking message for address:" + address); for (HdmiCecMessage message : copiedBuffer) { @@ -134,7 +134,7 @@ final class DelayedMessageBuffer { * @param address logical address of the device to be the active source */ void processActiveSource(int address) { - ArrayList<HdmiCecMessage> copiedBuffer = new ArrayList<HdmiCecMessage>(mBuffer); + ArrayList<HdmiCecMessage> copiedBuffer = new ArrayList<>(mBuffer); mBuffer.clear(); for (HdmiCecMessage message : copiedBuffer) { if (message.getOpcode() == Constants.MESSAGE_ACTIVE_SOURCE diff --git a/services/core/java/com/android/server/hdmi/DeviceDiscoveryAction.java b/services/core/java/com/android/server/hdmi/DeviceDiscoveryAction.java index db8dedbf12cc..b75e75feabb3 100755 --- a/services/core/java/com/android/server/hdmi/DeviceDiscoveryAction.java +++ b/services/core/java/com/android/server/hdmi/DeviceDiscoveryAction.java @@ -386,6 +386,7 @@ final class DeviceDiscoveryAction extends HdmiCecFeatureAction { return; case STATE_WAITING_FOR_VENDOR_ID: queryVendorId(address); + return; default: return; } diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java index 2949b92f82df..1dd10f550985 100755 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java @@ -169,6 +169,8 @@ abstract class HdmiCecLocalDevice { return new HdmiCecLocalDeviceTv(service); case HdmiDeviceInfo.DEVICE_PLAYBACK: return new HdmiCecLocalDevicePlayback(service); + case HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM: + return new HdmiCecLocalDeviceAudioSystem(service); default: return null; } @@ -264,14 +266,28 @@ abstract class HdmiCecLocalDevice { return handleRoutingChange(message); case Constants.MESSAGE_ROUTING_INFORMATION: return handleRoutingInformation(message); + case Constants.MESSAGE_REQUEST_ARC_INITIATION: + return handleRequestArcInitiate(message); + case Constants.MESSAGE_REQUEST_ARC_TERMINATION: + return handleRequestArcTermination(message); case Constants.MESSAGE_INITIATE_ARC: return handleInitiateArc(message); case Constants.MESSAGE_TERMINATE_ARC: return handleTerminateArc(message); + case Constants.MESSAGE_REPORT_ARC_INITIATED: + return handleReportArcInitiate(message); + case Constants.MESSAGE_REPORT_ARC_TERMINATED: + return handleReportArcTermination(message); + case Constants.MESSAGE_SYSTEM_AUDIO_MODE_REQUEST: + return handleSystemAudioModeRequest(message); case Constants.MESSAGE_SET_SYSTEM_AUDIO_MODE: return handleSetSystemAudioMode(message); case Constants.MESSAGE_SYSTEM_AUDIO_MODE_STATUS: return handleSystemAudioModeStatus(message); + case Constants.MESSAGE_GIVE_SYSTEM_AUDIO_MODE_STATUS: + return handleGiveSystemAudioModeStatus(message); + case Constants.MESSAGE_GIVE_AUDIO_STATUS: + return handleGiveAudioStatus(message); case Constants.MESSAGE_REPORT_AUDIO_STATUS: return handleReportAudioStatus(message); case Constants.MESSAGE_STANDBY: @@ -419,10 +435,18 @@ abstract class HdmiCecLocalDevice { return false; } + protected boolean handleGiveSystemAudioModeStatus(HdmiCecMessage message) { + return false; + } + protected boolean handleSetSystemAudioMode(HdmiCecMessage message) { return false; } + protected boolean handleSystemAudioModeRequest(HdmiCecMessage message) { + return false; + } + protected boolean handleTerminateArc(HdmiCecMessage message) { return false; } @@ -431,10 +455,30 @@ abstract class HdmiCecLocalDevice { return false; } + protected boolean handleRequestArcInitiate(HdmiCecMessage message) { + return false; + } + + protected boolean handleRequestArcTermination(HdmiCecMessage message) { + return false; + } + + protected boolean handleReportArcInitiate(HdmiCecMessage message) { + return false; + } + + protected boolean handleReportArcTermination(HdmiCecMessage message) { + return false; + } + protected boolean handleReportAudioStatus(HdmiCecMessage message) { return false; } + protected boolean handleGiveAudioStatus(HdmiCecMessage message) { + return false; + } + @ServiceThreadOnly protected boolean handleStandby(HdmiCecMessage message) { assertRunOnServiceThread(); diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java new file mode 100644 index 000000000000..655166ee1a4b --- /dev/null +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java @@ -0,0 +1,133 @@ +/* + * 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.hdmi; + +import android.hardware.hdmi.HdmiDeviceInfo; +import android.media.AudioManager; +import android.os.SystemProperties; +import com.android.internal.annotations.GuardedBy; +import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly; + +/** + * Represent a logical device of type {@link HdmiDeviceInfo#DEVICE_AUDIO_SYSTEM} residing in + * Android system. + */ +public class HdmiCecLocalDeviceAudioSystem extends HdmiCecLocalDevice { + + private static final String TAG = "HdmiCecLocalDeviceAudioSystem"; + + // Whether System audio mode is activated or not. + // This becomes true only when all system audio sequences are finished. + @GuardedBy("mLock") + private boolean mSystemAudioActivated; + + protected HdmiCecLocalDeviceAudioSystem(HdmiControlService service) { + super(service, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM); + } + + @Override + @ServiceThreadOnly + protected void onAddressAllocated(int logicalAddress, int reason) { + assertRunOnServiceThread(); + mService.sendCecCommand(HdmiCecMessageBuilder.buildReportPhysicalAddressCommand( + mAddress, mService.getPhysicalAddress(), mDeviceType)); + mService.sendCecCommand(HdmiCecMessageBuilder.buildDeviceVendorIdCommand( + mAddress, mService.getVendorId())); + startQueuedActions(); + } + + @Override + @ServiceThreadOnly + protected int getPreferredAddress() { + assertRunOnServiceThread(); + return SystemProperties.getInt(Constants.PROPERTY_PREFERRED_ADDRESS_AUDIO_SYSTEM, + Constants.ADDR_UNREGISTERED); + } + + @Override + @ServiceThreadOnly + protected void setPreferredAddress(int addr) { + assertRunOnServiceThread(); + SystemProperties.set(Constants.PROPERTY_PREFERRED_ADDRESS_AUDIO_SYSTEM, + String.valueOf(addr)); + } + + @Override + @ServiceThreadOnly + protected boolean handleReportAudioStatus(HdmiCecMessage message) { + assertRunOnServiceThread(); + // TODO(amyjojo): implement report audio status handler + HdmiLogger.debug(TAG + "Stub handleReportAudioStatus"); + return true; + } + + @Override + @ServiceThreadOnly + protected boolean handleInitiateArc(HdmiCecMessage message) { + assertRunOnServiceThread(); + // TODO(amyjojo): implement initiate arc handler + HdmiLogger.debug(TAG + "Stub handleInitiateArc"); + return true; + } + + @Override + @ServiceThreadOnly + protected boolean handleReportArcInitiate(HdmiCecMessage message) { + assertRunOnServiceThread(); + // TODO(amyjojo): implement report arc initiate handler + HdmiLogger.debug(TAG + "Stub handleReportArcInitiate"); + return true; + } + + @Override + @ServiceThreadOnly + protected boolean handleReportArcTermination(HdmiCecMessage message) { + assertRunOnServiceThread(); + // TODO(amyjojo): implement report arc terminate handler + HdmiLogger.debug(TAG + "Stub handleReportArcTermination"); + return true; + } + + @Override + @ServiceThreadOnly + protected boolean handleGiveAudioStatus(HdmiCecMessage message) { + assertRunOnServiceThread(); + + reportAudioStatus(message); + return true; + } + + @Override + @ServiceThreadOnly + protected boolean handleGiveSystemAudioModeStatus(HdmiCecMessage message) { + assertRunOnServiceThread(); + mService.sendCecCommand(HdmiCecMessageBuilder + .buildReportSystemAudioMode(mAddress, message.getSource(), mSystemAudioActivated)); + return true; + } + + private void reportAudioStatus(HdmiCecMessage message) { + assertRunOnServiceThread(); + + int volume = mService.getAudioManager().getStreamVolume(AudioManager.STREAM_MUSIC); + boolean mute = mService.getAudioManager().isStreamMute(AudioManager.STREAM_MUSIC); + int maxVolume = mService.getAudioManager().getStreamMaxVolume(AudioManager.STREAM_MUSIC); + int scaledVolume = VolumeControlAction.scaleToCecVolume(volume, maxVolume); + + mService.sendCecCommand(HdmiCecMessageBuilder + .buildReportAudioStatus(mAddress, message.getSource(), scaledVolume, mute)); + } +} diff --git a/services/core/java/com/android/server/hdmi/HdmiCecMessage.java b/services/core/java/com/android/server/hdmi/HdmiCecMessage.java index fd0ff9d22bf8..c0056158a9c5 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecMessage.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecMessage.java @@ -16,9 +16,11 @@ package com.android.server.hdmi; +import android.annotation.Nullable; import libcore.util.EmptyArray; import java.util.Arrays; +import java.util.Objects; /** * A class to encapsulate HDMI-CEC message used for the devices connected via @@ -44,6 +46,27 @@ public final class HdmiCecMessage { mParams = Arrays.copyOf(params, params.length); } + @Override + public boolean equals(@Nullable Object message) { + if (message instanceof HdmiCecMessage) { + HdmiCecMessage that = (HdmiCecMessage) message; + return this.mSource == that.getSource() && + this.mDestination == that.getDestination() && + this.mOpcode == that.getOpcode() && + Arrays.equals(this.mParams, that.getParams()); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash( + mSource, + mDestination, + mOpcode, + Arrays.hashCode(mParams)); + } + /** * Return the source address field of the message. It is the logical address * of the device which generated the message. diff --git a/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java b/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java index 9a51e3c4e234..37f96142089a 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java @@ -211,6 +211,28 @@ public class HdmiCecMessageBuilder { } /** + * Build <Initiate Arc> + * + * @param src source address of command + * @param dest destination address of command + * @return newly created {@link HdmiCecMessage} + */ + static HdmiCecMessage buildInitiateArc(int src, int dest) { + return buildCommand(src, dest, Constants.MESSAGE_INITIATE_ARC); + } + + /** + * Build <Terminate Arc> + * + * @param src source address of command + * @param dest destination address of command + * @return newly created {@link HdmiCecMessage} + */ + static HdmiCecMessage buildTerminateArc(int src, int dest) { + return buildCommand(src, dest, Constants.MESSAGE_TERMINATE_ARC); + } + + /** * Build <Request Arc Termination> * * @param src source address of command @@ -372,6 +394,34 @@ public class HdmiCecMessageBuilder { } /** + * Build <Set System Audio Mode> command. + * + * @param src source address of command + * @param des destination address of command + * @param systemAudioStatus whether to set System Audio Mode on or off + * @return newly created {@link HdmiCecMessage} + */ + static HdmiCecMessage buildSetSystemAudioMode(int src, int des, boolean systemAudioStatus) { + return buildCommandWithBooleanParam(src, des, Constants.MESSAGE_SET_SYSTEM_AUDIO_MODE, + systemAudioStatus + ); + } + + /** + * Build <Report System Audio Mode> command. + * + * @param src source address of command + * @param des destination address of command + * @param systemAudioStatus whether System Audio Mode is on or off + * @return newly created {@link HdmiCecMessage} + */ + static HdmiCecMessage buildReportSystemAudioMode(int src, int des, boolean systemAudioStatus) { + return buildCommandWithBooleanParam(src, des, Constants.MESSAGE_SYSTEM_AUDIO_MODE_STATUS, + systemAudioStatus + ); + } + + /** * Build <Give Audio Status> command. * * @param src source address of command @@ -383,6 +433,21 @@ public class HdmiCecMessageBuilder { } /** + * Build <Report Audio Status> command. + * + * @param src source address of command + * @param dest destination address of command + * @param volume volume level of current device in param + * @param mute mute status of current device in param + * @return newly created {@link HdmiCecMessage} + */ + static HdmiCecMessage buildReportAudioStatus(int src, int dest, int volume, boolean mute) { + byte status = (byte) ((byte) (mute ? 1 << 7 : 0) | ((byte) volume & 0x7F)); + byte[] params = new byte[] { status }; + return buildCommand(src, dest, Constants.MESSAGE_REPORT_AUDIO_STATUS, params); + } + + /** * Build <User Control Pressed> command. * * @param src source address of command @@ -592,6 +657,23 @@ public class HdmiCecMessageBuilder { return new HdmiCecMessage(src, dest, opcode, params); } + /** + * Build a {@link HdmiCecMessage} with a boolean param and other given values. + * + * @param src source address of command + * @param des destination address of command + * @param opcode opcode for a message + * @param param boolean param for building the command + * @return newly created {@link HdmiCecMessage} + */ + private static HdmiCecMessage buildCommandWithBooleanParam(int src, int des, + int opcode, boolean param) { + byte[] params = new byte[]{ + param ? (byte) 0b1 : 0b0 + }; + return buildCommand(src, des, opcode, params); + } + private static byte[] physicalAddressToParam(int physicalAddress) { return new byte[] { (byte) ((physicalAddress >> 8) & 0xFF), diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index a1753e58c4ea..8a146397fa3e 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -96,7 +96,7 @@ public class HdmiControlService extends SystemService { static final String PERMISSION = "android.permission.HDMI_CEC"; - // The reason code to initiate intializeCec(). + // The reason code to initiate initializeCec(). static final int INITIATED_BY_ENABLE_CEC = 0; static final int INITIATED_BY_BOOT_UP = 1; static final int INITIATED_BY_SCREEN_ON = 2; diff --git a/services/core/java/com/android/server/hdmi/HdmiLogger.java b/services/core/java/com/android/server/hdmi/HdmiLogger.java index ebe52c0dfbf0..2309293dcbd7 100644 --- a/services/core/java/com/android/server/hdmi/HdmiLogger.java +++ b/services/core/java/com/android/server/hdmi/HdmiLogger.java @@ -17,7 +17,6 @@ package com.android.server.hdmi; import android.annotation.Nullable; -import android.os.Build; import android.os.SystemClock; import android.util.Pair; import android.util.Slog; @@ -41,7 +40,7 @@ import java.util.HashMap; final class HdmiLogger { private static final String TAG = "HDMI"; // Logging duration for same error message. - private static final long ERROR_LOG_DURATTION_MILLIS = 20 * 1000; // 20s + private static final long ERROR_LOG_DURATION_MILLIS = 20 * 1000; // 20s private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); @@ -134,6 +133,6 @@ final class HdmiLogger { } private static boolean shouldLogNow(@Nullable Pair<Long, Integer> timing, long curTime) { - return timing == null || curTime - timing.first > ERROR_LOG_DURATTION_MILLIS; + return timing == null || curTime - timing.first > ERROR_LOG_DURATION_MILLIS; } } diff --git a/services/core/java/com/android/server/hdmi/HdmiUtils.java b/services/core/java/com/android/server/hdmi/HdmiUtils.java index 4ac3bba73e25..2a8117fac68d 100644 --- a/services/core/java/com/android/server/hdmi/HdmiUtils.java +++ b/services/core/java/com/android/server/hdmi/HdmiUtils.java @@ -114,7 +114,7 @@ final class HdmiUtils { * * @param logicalAddress the logical address to verify * @param deviceType the device type to check - * @throw IllegalArgumentException + * @throws IllegalArgumentException */ static void verifyAddressType(int logicalAddress, int deviceType) { int actualDeviceType = getTypeFromAddress(logicalAddress); diff --git a/services/core/java/com/android/server/hdmi/PowerStatusMonitorAction.java b/services/core/java/com/android/server/hdmi/PowerStatusMonitorAction.java index 6893012342f9..a62d0b63221c 100644 --- a/services/core/java/com/android/server/hdmi/PowerStatusMonitorAction.java +++ b/services/core/java/com/android/server/hdmi/PowerStatusMonitorAction.java @@ -38,7 +38,7 @@ public class PowerStatusMonitorAction extends HdmiCecFeatureAction { private static final int INVALID_POWER_STATUS = POWER_STATUS_UNKNOWN - 1; // Monitoring interval (60s) - private static final int MONITIROING_INTERNAL_MS = 60000; + private static final int MONITORING_INTERNAL_MS = 60000; // Timeout once sending <Give Device Power Status> private static final int REPORT_POWER_STATUS_TIMEOUT_MS = 5000; @@ -132,7 +132,7 @@ public class PowerStatusMonitorAction extends HdmiCecFeatureAction { mState = STATE_WAIT_FOR_REPORT_POWER_STATUS; // Add both timers, monitoring and timeout. - addTimer(STATE_WAIT_FOR_NEXT_MONITORING, MONITIROING_INTERNAL_MS); + addTimer(STATE_WAIT_FOR_NEXT_MONITORING, MONITORING_INTERNAL_MS); addTimer(STATE_WAIT_FOR_REPORT_POWER_STATUS, REPORT_POWER_STATUS_TIMEOUT_MS); } diff --git a/services/core/java/com/android/server/hdmi/RequestArcAction.java b/services/core/java/com/android/server/hdmi/RequestArcAction.java index 75a79cb0213c..c70101c43d79 100644 --- a/services/core/java/com/android/server/hdmi/RequestArcAction.java +++ b/services/core/java/com/android/server/hdmi/RequestArcAction.java @@ -35,7 +35,7 @@ abstract class RequestArcAction extends HdmiCecFeatureAction { * * @param source {@link HdmiCecLocalDevice} instance * @param avrAddress address of AV receiver. It should be AUDIO_SYSTEM type - * @throw IllegalArugmentException if device type of sourceAddress and avrAddress + * @throws IllegalArgumentException if device type of sourceAddress and avrAddress * is invalid */ RequestArcAction(HdmiCecLocalDevice source, int avrAddress) { diff --git a/services/core/java/com/android/server/hdmi/SystemAudioAction.java b/services/core/java/com/android/server/hdmi/SystemAudioAction.java index 449b2085715c..a5477e865c40 100644 --- a/services/core/java/com/android/server/hdmi/SystemAudioAction.java +++ b/services/core/java/com/android/server/hdmi/SystemAudioAction.java @@ -60,7 +60,7 @@ abstract class SystemAudioAction extends HdmiCecFeatureAction { * @param avrAddress logical address of AVR device * @param targetStatus Whether to enable the system audio mode or not * @param callback callback interface to be notified when it's done - * @throw IllegalArugmentException if device type of sourceAddress and avrAddress is invalid + * @throws IllegalArgumentException if device type of sourceAddress and avrAddress is invalid */ SystemAudioAction(HdmiCecLocalDevice source, int avrAddress, boolean targetStatus, IHdmiControlCallback callback) { diff --git a/services/core/java/com/android/server/hdmi/SystemAudioActionFromAvr.java b/services/core/java/com/android/server/hdmi/SystemAudioActionFromAvr.java index eb5119b8a233..6ddff91a70f7 100644 --- a/services/core/java/com/android/server/hdmi/SystemAudioActionFromAvr.java +++ b/services/core/java/com/android/server/hdmi/SystemAudioActionFromAvr.java @@ -32,7 +32,7 @@ final class SystemAudioActionFromAvr extends SystemAudioAction { * @param avrAddress logical address of AVR device * @param targetStatus Whether to enable the system audio mode or not * @param callback callback interface to be notified when it's done - * @throw IllegalArugmentException if device type of tvAddress and avrAddress is invalid + * @throws IllegalArgumentException if device type of tvAddress and avrAddress is invalid */ SystemAudioActionFromAvr(HdmiCecLocalDevice source, int avrAddress, boolean targetStatus, IHdmiControlCallback callback) { diff --git a/services/core/java/com/android/server/hdmi/SystemAudioActionFromTv.java b/services/core/java/com/android/server/hdmi/SystemAudioActionFromTv.java index 02ecd1392bc7..5c0c272f59e0 100644 --- a/services/core/java/com/android/server/hdmi/SystemAudioActionFromTv.java +++ b/services/core/java/com/android/server/hdmi/SystemAudioActionFromTv.java @@ -32,7 +32,7 @@ final class SystemAudioActionFromTv extends SystemAudioAction { * @param avrAddress logical address of AVR device * @param targetStatus Whether to enable the system audio mode or not * @param callback callback interface to be notified when it's done - * @throw IllegalArugmentException if device type of tvAddress is invalid + * @throws IllegalArgumentException if device type of tvAddress is invalid */ SystemAudioActionFromTv(HdmiCecLocalDevice sourceAddress, int avrAddress, boolean targetStatus, IHdmiControlCallback callback) { diff --git a/services/core/java/com/android/server/hdmi/SystemAudioStatusAction.java b/services/core/java/com/android/server/hdmi/SystemAudioStatusAction.java index d41a36ca031f..13f0f4ae4a92 100644 --- a/services/core/java/com/android/server/hdmi/SystemAudioStatusAction.java +++ b/services/core/java/com/android/server/hdmi/SystemAudioStatusAction.java @@ -64,7 +64,7 @@ final class SystemAudioStatusAction extends HdmiCecFeatureAction { } private void handleSendGiveAudioStatusFailure() { - // Inform to all application that the audio status (volumn, mute) of + // Inform to all application that the audio status (volume, mute) of // the audio amplifier is unknown. tv().setAudioStatus(false, Constants.UNKNOWN_VOLUME); diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java index 442354bbb6b9..a49cf44dc775 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecord.java +++ b/services/core/java/com/android/server/media/MediaSessionRecord.java @@ -481,7 +481,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { mAudioManagerInternal.adjustStreamVolumeForUid(stream, direction, flags, packageName, uid); } - } catch (IllegalArgumentException e) { + } catch (IllegalArgumentException | SecurityException e) { Log.e(TAG, "Cannot adjust volume: direction=" + direction + ", stream=" + stream + ", flags=" + flags + ", packageName=" + packageName + ", uid=" + uid + ", useSuggested=" + useSuggested diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 71c7c881619e..dcdc2031d761 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -386,6 +386,7 @@ public class NotificationManagerService extends SystemService { private static final String ATTR_VERSION = "version"; private RankingHelper mRankingHelper; + private PreferencesHelper mPreferencesHelper; private final UserProfiles mUserProfiles = new UserProfiles(); private NotificationListeners mListeners; @@ -525,8 +526,8 @@ public class NotificationManagerService extends SystemService { while (XmlUtils.nextElementWithin(parser, outerDepth)) { if (ZenModeConfig.ZEN_TAG.equals(parser.getName())) { mZenModeHelper.readXml(parser, forRestore); - } else if (RankingHelper.TAG_RANKING.equals(parser.getName())){ - mRankingHelper.readXml(parser, forRestore); + } else if (PreferencesHelper.TAG_RANKING.equals(parser.getName())){ + mPreferencesHelper.readXml(parser, forRestore); } if (mListeners.getConfig().xmlTag.equals(parser.getName())) { mListeners.readXml(parser, mAllowedManagedServicePackages); @@ -608,7 +609,7 @@ public class NotificationManagerService extends SystemService { out.startTag(null, TAG_NOTIFICATION_POLICY); out.attribute(null, ATTR_VERSION, Integer.toString(DB_VERSION)); mZenModeHelper.writeXml(out, forBackup, null); - mRankingHelper.writeXml(out, forBackup); + mPreferencesHelper.writeXml(out, forBackup); mListeners.writeXml(out, forBackup); mAssistants.writeXml(out, forBackup); mConditionProviders.writeXml(out, forBackup); @@ -949,7 +950,7 @@ public class NotificationManagerService extends SystemService { // update system notification channels SystemNotificationChannels.createAll(context); mZenModeHelper.updateDefaultZenRules(); - mRankingHelper.onLocaleChanged(context, ActivityManager.getCurrentUser()); + mPreferencesHelper.onLocaleChanged(context, ActivityManager.getCurrentUser()); } } }; @@ -1092,7 +1093,8 @@ public class NotificationManagerService extends SystemService { mListeners.onPackagesChanged(removingPackage, pkgList, uidList); mAssistants.onPackagesChanged(removingPackage, pkgList, uidList); mConditionProviders.onPackagesChanged(removingPackage, pkgList, uidList); - mRankingHelper.onPackagesChanged(removingPackage, changeUserId, pkgList, uidList); + mPreferencesHelper.onPackagesChanged( + removingPackage, changeUserId, pkgList, uidList); savePolicyFile(); } } @@ -1152,7 +1154,7 @@ public class NotificationManagerService extends SystemService { final int user = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL); mUserProfiles.updateCache(context); mZenModeHelper.onUserRemoved(user); - mRankingHelper.onUserRemoved(user); + mPreferencesHelper.onUserRemoved(user); mListeners.onUserRemoved(user); mConditionProviders.onUserRemoved(user); mAssistants.onUserRemoved(user); @@ -1210,7 +1212,7 @@ public class NotificationManagerService extends SystemService { Settings.Global.MAX_NOTIFICATION_ENQUEUE_RATE, mMaxPackageEnqueueRate); } if (uri == null || NOTIFICATION_BADGING_URI.equals(uri)) { - mRankingHelper.updateBadgingEnabled(); + mPreferencesHelper.updateBadgingEnabled(); } } } @@ -1332,6 +1334,9 @@ public class NotificationManagerService extends SystemService { } @VisibleForTesting + void setPreferencesHelper(PreferencesHelper prefHelper) { mPreferencesHelper = prefHelper; } + + @VisibleForTesting void setRankingHandler(RankingHandler rankingHandler) { mRankingHandler = rankingHandler; } @@ -1419,9 +1424,13 @@ public class NotificationManagerService extends SystemService { mRankingHandler.requestSort(); } }); - mRankingHelper = new RankingHelper(getContext(), + mPreferencesHelper = new PreferencesHelper(getContext(), mPackageManagerClient, mRankingHandler, + mZenModeHelper); + mRankingHelper = new RankingHelper(getContext(), + mRankingHandler, + mPreferencesHelper, mZenModeHelper, mUsageStats, extractorNames); @@ -1676,14 +1685,14 @@ public class NotificationManagerService extends SystemService { } } final NotificationChannel preUpdate = - mRankingHelper.getNotificationChannel(pkg, uid, channel.getId(), true); + mPreferencesHelper.getNotificationChannel(pkg, uid, channel.getId(), true); - mRankingHelper.updateNotificationChannel(pkg, uid, channel, true); + mPreferencesHelper.updateNotificationChannel(pkg, uid, channel, true); maybeNotifyChannelOwner(pkg, uid, preUpdate, channel); if (!fromListener) { final NotificationChannel modifiedChannel = - mRankingHelper.getNotificationChannel(pkg, uid, channel.getId(), false); + mPreferencesHelper.getNotificationChannel(pkg, uid, channel.getId(), false); mListeners.notifyNotificationChannelChanged( pkg, UserHandle.getUserHandleForUid(uid), modifiedChannel, NOTIFICATION_CHANNEL_OR_GROUP_UPDATED); @@ -1720,8 +1729,8 @@ public class NotificationManagerService extends SystemService { Preconditions.checkNotNull(pkg); final NotificationChannelGroup preUpdate = - mRankingHelper.getNotificationChannelGroup(group.getId(), pkg, uid); - mRankingHelper.createNotificationChannelGroup(pkg, uid, group, + mPreferencesHelper.getNotificationChannelGroup(group.getId(), pkg, uid); + mPreferencesHelper.createNotificationChannelGroup(pkg, uid, group, fromApp); if (!fromApp) { maybeNotifyChannelGroupOwner(pkg, uid, preUpdate, group); @@ -2124,7 +2133,7 @@ public class NotificationManagerService extends SystemService { public void setNotificationsEnabledForPackage(String pkg, int uid, boolean enabled) { enforceSystemOrSystemUI("setNotificationsEnabledForPackage"); - mRankingHelper.setEnabled(pkg, uid, enabled); + mPreferencesHelper.setEnabled(pkg, uid, enabled); // Now, cancel any outstanding notifications that are part of a just-disabled app if (!enabled) { cancelAllNotificationsInt(MY_UID, MY_PID, pkg, null, 0, 0, true, @@ -2162,7 +2171,7 @@ public class NotificationManagerService extends SystemService { String pkg, int uid, boolean enabled) { setNotificationsEnabledForPackage(pkg, uid, enabled); - mRankingHelper.setAppImportanceLocked(pkg, uid); + mPreferencesHelper.setAppImportanceLocked(pkg, uid); } /** @@ -2180,25 +2189,25 @@ public class NotificationManagerService extends SystemService { public boolean areNotificationsEnabledForPackage(String pkg, int uid) { checkCallerIsSystemOrSameApp(pkg); - return mRankingHelper.getImportance(pkg, uid) != IMPORTANCE_NONE; + return mPreferencesHelper.getImportance(pkg, uid) != IMPORTANCE_NONE; } @Override public int getPackageImportance(String pkg) { checkCallerIsSystemOrSameApp(pkg); - return mRankingHelper.getImportance(pkg, Binder.getCallingUid()); + return mPreferencesHelper.getImportance(pkg, Binder.getCallingUid()); } @Override public boolean canShowBadge(String pkg, int uid) { checkCallerIsSystem(); - return mRankingHelper.canShowBadge(pkg, uid); + return mPreferencesHelper.canShowBadge(pkg, uid); } @Override public void setShowBadge(String pkg, int uid, boolean showBadge) { checkCallerIsSystem(); - mRankingHelper.setShowBadge(pkg, uid, showBadge); + mPreferencesHelper.setShowBadge(pkg, uid, showBadge); savePolicyFile(); } @@ -2230,12 +2239,12 @@ public class NotificationManagerService extends SystemService { for (int i = 0; i < channelsSize; i++) { final NotificationChannel channel = channels.get(i); Preconditions.checkNotNull(channel, "channel in list is null"); - mRankingHelper.createNotificationChannel(pkg, uid, channel, + mPreferencesHelper.createNotificationChannel(pkg, uid, channel, true /* fromTargetApp */, mConditionProviders.isPackageOrComponentAllowed( pkg, UserHandle.getUserId(uid))); mListeners.notifyNotificationChannelChanged(pkg, UserHandle.getUserHandleForUid(uid), - mRankingHelper.getNotificationChannel(pkg, uid, channel.getId(), false), + mPreferencesHelper.getNotificationChannel(pkg, uid, channel.getId(), false), NOTIFICATION_CHANNEL_OR_GROUP_ADDED); } savePolicyFile(); @@ -2258,7 +2267,7 @@ public class NotificationManagerService extends SystemService { @Override public NotificationChannel getNotificationChannel(String pkg, String channelId) { checkCallerIsSystemOrSameApp(pkg); - return mRankingHelper.getNotificationChannel( + return mPreferencesHelper.getNotificationChannel( pkg, Binder.getCallingUid(), channelId, false /* includeDeleted */); } @@ -2266,7 +2275,7 @@ public class NotificationManagerService extends SystemService { public NotificationChannel getNotificationChannelForPackage(String pkg, int uid, String channelId, boolean includeDeleted) { checkCallerIsSystem(); - return mRankingHelper.getNotificationChannel(pkg, uid, channelId, includeDeleted); + return mPreferencesHelper.getNotificationChannel(pkg, uid, channelId, includeDeleted); } @Override @@ -2278,10 +2287,10 @@ public class NotificationManagerService extends SystemService { } cancelAllNotificationsInt(MY_UID, MY_PID, pkg, channelId, 0, 0, true, UserHandle.getUserId(callingUid), REASON_CHANNEL_BANNED, null); - mRankingHelper.deleteNotificationChannel(pkg, callingUid, channelId); + mPreferencesHelper.deleteNotificationChannel(pkg, callingUid, channelId); mListeners.notifyNotificationChannelChanged(pkg, UserHandle.getUserHandleForUid(callingUid), - mRankingHelper.getNotificationChannel(pkg, callingUid, channelId, true), + mPreferencesHelper.getNotificationChannel(pkg, callingUid, channelId, true), NOTIFICATION_CHANNEL_OR_GROUP_DELETED); savePolicyFile(); } @@ -2289,7 +2298,7 @@ public class NotificationManagerService extends SystemService { @Override public NotificationChannelGroup getNotificationChannelGroup(String pkg, String groupId) { checkCallerIsSystemOrSameApp(pkg); - return mRankingHelper.getNotificationChannelGroupWithChannels( + return mPreferencesHelper.getNotificationChannelGroupWithChannels( pkg, Binder.getCallingUid(), groupId, false); } @@ -2297,7 +2306,7 @@ public class NotificationManagerService extends SystemService { public ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroups( String pkg) { checkCallerIsSystemOrSameApp(pkg); - return mRankingHelper.getNotificationChannelGroups( + return mPreferencesHelper.getNotificationChannelGroups( pkg, Binder.getCallingUid(), false, false); } @@ -2307,10 +2316,10 @@ public class NotificationManagerService extends SystemService { final int callingUid = Binder.getCallingUid(); NotificationChannelGroup groupToDelete = - mRankingHelper.getNotificationChannelGroup(groupId, pkg, callingUid); + mPreferencesHelper.getNotificationChannelGroup(groupId, pkg, callingUid); if (groupToDelete != null) { List<NotificationChannel> deletedChannels = - mRankingHelper.deleteNotificationChannelGroup(pkg, callingUid, groupId); + mPreferencesHelper.deleteNotificationChannelGroup(pkg, callingUid, groupId); for (int i = 0; i < deletedChannels.size(); i++) { final NotificationChannel deletedChannel = deletedChannels.get(i); cancelAllNotificationsInt(MY_UID, MY_PID, pkg, deletedChannel.getId(), 0, 0, @@ -2341,47 +2350,47 @@ public class NotificationManagerService extends SystemService { public ParceledListSlice<NotificationChannel> getNotificationChannelsForPackage(String pkg, int uid, boolean includeDeleted) { enforceSystemOrSystemUI("getNotificationChannelsForPackage"); - return mRankingHelper.getNotificationChannels(pkg, uid, includeDeleted); + return mPreferencesHelper.getNotificationChannels(pkg, uid, includeDeleted); } @Override public int getNumNotificationChannelsForPackage(String pkg, int uid, boolean includeDeleted) { enforceSystemOrSystemUI("getNumNotificationChannelsForPackage"); - return mRankingHelper.getNotificationChannels(pkg, uid, includeDeleted) + return mPreferencesHelper.getNotificationChannels(pkg, uid, includeDeleted) .getList().size(); } @Override public boolean onlyHasDefaultChannel(String pkg, int uid) { enforceSystemOrSystemUI("onlyHasDefaultChannel"); - return mRankingHelper.onlyHasDefaultChannel(pkg, uid); + return mPreferencesHelper.onlyHasDefaultChannel(pkg, uid); } @Override public int getDeletedChannelCount(String pkg, int uid) { enforceSystemOrSystemUI("getDeletedChannelCount"); - return mRankingHelper.getDeletedChannelCount(pkg, uid); + return mPreferencesHelper.getDeletedChannelCount(pkg, uid); } @Override public int getBlockedChannelCount(String pkg, int uid) { enforceSystemOrSystemUI("getBlockedChannelCount"); - return mRankingHelper.getBlockedChannelCount(pkg, uid); + return mPreferencesHelper.getBlockedChannelCount(pkg, uid); } @Override public ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroupsForPackage( String pkg, int uid, boolean includeDeleted) { checkCallerIsSystem(); - return mRankingHelper.getNotificationChannelGroups(pkg, uid, includeDeleted, true); + return mPreferencesHelper.getNotificationChannelGroups(pkg, uid, includeDeleted, true); } @Override public NotificationChannelGroup getPopulatedNotificationChannelGroupForPackage( String pkg, int uid, String groupId, boolean includeDeleted) { enforceSystemOrSystemUI("getPopulatedNotificationChannelGroupForPackage"); - return mRankingHelper.getNotificationChannelGroupWithChannels( + return mPreferencesHelper.getNotificationChannelGroupWithChannels( pkg, uid, groupId, includeDeleted); } @@ -2389,13 +2398,13 @@ public class NotificationManagerService extends SystemService { public NotificationChannelGroup getNotificationChannelGroupForPackage( String groupId, String pkg, int uid) { enforceSystemOrSystemUI("getNotificationChannelGroupForPackage"); - return mRankingHelper.getNotificationChannelGroup(groupId, pkg, uid); + return mPreferencesHelper.getNotificationChannelGroup(groupId, pkg, uid); } @Override public ParceledListSlice<NotificationChannel> getNotificationChannels(String pkg) { checkCallerIsSystemOrSameApp(pkg); - return mRankingHelper.getNotificationChannels( + return mPreferencesHelper.getNotificationChannels( pkg, Binder.getCallingUid(), false /* includeDeleted */); } @@ -2412,12 +2421,12 @@ public class NotificationManagerService extends SystemService { @Override public int getBlockedAppCount(int userId) { checkCallerIsSystem(); - return mRankingHelper.getBlockedAppCount(userId); + return mPreferencesHelper.getBlockedAppCount(userId); } @Override public boolean areChannelsBypassingDnd() { - return mRankingHelper.areChannelsBypassingDnd(); + return mPreferencesHelper.areChannelsBypassingDnd(); } @Override @@ -2440,7 +2449,7 @@ public class NotificationManagerService extends SystemService { // Reset notification preferences if (!fromApp) { - mRankingHelper.onPackagesChanged( + mPreferencesHelper.onPackagesChanged( true, UserHandle.getCallingUserId(), packages, uids); } @@ -3512,7 +3521,7 @@ public class NotificationManagerService extends SystemService { Preconditions.checkNotNull(user); verifyPrivilegedListener(token, user); - return mRankingHelper.getNotificationChannels(pkg, getUidForPackageAndUser(pkg, user), + return mPreferencesHelper.getNotificationChannels(pkg, getUidForPackageAndUser(pkg, user), false /* includeDeleted */); } @@ -3525,7 +3534,7 @@ public class NotificationManagerService extends SystemService { verifyPrivilegedListener(token, user); List<NotificationChannelGroup> groups = new ArrayList<>(); - groups.addAll(mRankingHelper.getNotificationChannelGroups( + groups.addAll(mPreferencesHelper.getNotificationChannelGroups( pkg, getUidForPackageAndUser(pkg, user))); return new ParceledListSlice<>(groups); } @@ -3706,10 +3715,10 @@ public class NotificationManagerService extends SystemService { JSONObject dump = new JSONObject(); try { dump.put("service", "Notification Manager"); - dump.put("bans", mRankingHelper.dumpBansJson(filter)); - dump.put("ranking", mRankingHelper.dumpJson(filter)); + dump.put("bans", mPreferencesHelper.dumpBansJson(filter)); + dump.put("ranking", mPreferencesHelper.dumpJson(filter)); dump.put("stats", mUsageStats.dumpJson(filter)); - dump.put("channels", mRankingHelper.dumpChannelsJson(filter)); + dump.put("channels", mPreferencesHelper.dumpChannelsJson(filter)); } catch (JSONException e) { e.printStackTrace(); } @@ -3782,6 +3791,7 @@ public class NotificationManagerService extends SystemService { long rankingToken = proto.start(NotificationServiceDumpProto.RANKING_CONFIG); mRankingHelper.dump(proto, filter); + mPreferencesHelper.dump(proto, filter); proto.end(rankingToken); } @@ -3890,6 +3900,9 @@ public class NotificationManagerService extends SystemService { pw.println("\n Ranking Config:"); mRankingHelper.dump(pw, " ", filter); + pw.println("\n Notification Preferences:"); + mPreferencesHelper.dump(pw, " ", filter); + pw.println("\n Notification listeners:"); mListeners.dump(pw, filter); pw.print(" mListenerHints: "); pw.println(mListenerHints); @@ -3953,7 +3966,7 @@ public class NotificationManagerService extends SystemService { @Override public NotificationChannel getNotificationChannel(String pkg, int uid, String channelId) { - return mRankingHelper.getNotificationChannel(pkg, uid, channelId, false); + return mPreferencesHelper.getNotificationChannel(pkg, uid, channelId, false); } @Override @@ -4049,7 +4062,7 @@ public class NotificationManagerService extends SystemService { if (mIsTelevision && (new Notification.TvExtender(notification)).getChannelId() != null) { channelId = (new Notification.TvExtender(notification)).getChannelId(); } - final NotificationChannel channel = mRankingHelper.getNotificationChannel(pkg, + final NotificationChannel channel = mPreferencesHelper.getNotificationChannel(pkg, notificationUid, channelId, false /* includeDeleted */); if (channel == null) { final String noChannelStr = "No Channel found for " @@ -4064,7 +4077,7 @@ public class NotificationManagerService extends SystemService { + ", notificationUid=" + notificationUid + ", notification=" + notification; Log.e(TAG, noChannelStr); - boolean appNotificationsOff = mRankingHelper.getImportance(pkg, notificationUid) + boolean appNotificationsOff = mPreferencesHelper.getImportance(pkg, notificationUid) == NotificationManager.IMPORTANCE_NONE; if (!appNotificationsOff) { @@ -4079,7 +4092,7 @@ public class NotificationManagerService extends SystemService { pkg, opPkg, id, tag, notificationUid, callingPid, notification, user, null, System.currentTimeMillis()); final NotificationRecord r = new NotificationRecord(getContext(), n, channel); - r.setIsAppImportanceLocked(mRankingHelper.getIsAppImportanceLocked(pkg, callingUid)); + r.setIsAppImportanceLocked(mPreferencesHelper.getIsAppImportanceLocked(pkg, callingUid)); if ((notification.flags & Notification.FLAG_FOREGROUND_SERVICE) != 0) { final boolean fgServiceShown = channel.isFgServiceShown(); @@ -4098,7 +4111,7 @@ public class NotificationManagerService extends SystemService { channel.unlockFields(NotificationChannel.USER_LOCKED_IMPORTANCE); channel.setFgServiceShown(true); } - mRankingHelper.updateNotificationChannel(pkg, notificationUid, channel, false); + mPreferencesHelper.updateNotificationChannel(pkg, notificationUid, channel, false); r.updateNotificationChannel(channel); } } else if (!fgServiceShown && !TextUtils.isEmpty(channelId) @@ -4273,8 +4286,8 @@ public class NotificationManagerService extends SystemService { return isPackageSuspended; } final boolean isBlocked = - mRankingHelper.isGroupBlocked(pkg, callingUid, r.getChannel().getGroup()) - || mRankingHelper.getImportance(pkg, callingUid) + mPreferencesHelper.isGroupBlocked(pkg, callingUid, r.getChannel().getGroup()) + || mPreferencesHelper.getImportance(pkg, callingUid) == NotificationManager.IMPORTANCE_NONE || r.getChannel().getImportance() == NotificationManager.IMPORTANCE_NONE; if (isBlocked) { @@ -5247,6 +5260,7 @@ public class NotificationManagerService extends SystemService { ArrayList<ArrayList<SnoozeCriterion>> snoozeCriteriaBefore = new ArrayList<>(N); ArrayList<Integer> userSentimentBefore = new ArrayList<>(N); ArrayList<Integer> suppressVisuallyBefore = new ArrayList<>(N); + ArrayList<ArrayList<Notification.Action>> smartActionsBefore = new ArrayList<>(N); for (int i = 0; i < N; i++) { final NotificationRecord r = mNotificationList.get(i); orderBefore.add(r.getKey()); @@ -5258,6 +5272,7 @@ public class NotificationManagerService extends SystemService { snoozeCriteriaBefore.add(r.getSnoozeCriteria()); userSentimentBefore.add(r.getUserSentiment()); suppressVisuallyBefore.add(r.getSuppressedVisualEffects()); + smartActionsBefore.add(r.getSmartActions()); mRankingHelper.extractSignals(r); } mRankingHelper.sort(mNotificationList); @@ -5272,7 +5287,8 @@ public class NotificationManagerService extends SystemService { || !Objects.equals(snoozeCriteriaBefore.get(i), r.getSnoozeCriteria()) || !Objects.equals(userSentimentBefore.get(i), r.getUserSentiment()) || !Objects.equals(suppressVisuallyBefore.get(i), - r.getSuppressedVisualEffects())) { + r.getSuppressedVisualEffects()) + || !Objects.equals(smartActionsBefore.get(i), r.getSmartActions())) { mHandler.scheduleSendRankingUpdate(); return; } @@ -6255,6 +6271,7 @@ public class NotificationManagerService extends SystemService { Bundle showBadge = new Bundle(); Bundle userSentiment = new Bundle(); Bundle hidden = new Bundle(); + Bundle smartActions = new Bundle(); for (int i = 0; i < N; i++) { NotificationRecord record = mNotificationList.get(i); if (!isVisibleToListener(record.sbn, info)) { @@ -6282,6 +6299,7 @@ public class NotificationManagerService extends SystemService { showBadge.putBoolean(key, record.canShowBadge()); userSentiment.putInt(key, record.getUserSentiment()); hidden.putBoolean(key, record.isHidden()); + smartActions.putParcelableArrayList(key, record.getSmartActions()); } final int M = keys.size(); String[] keysAr = keys.toArray(new String[M]); @@ -6292,7 +6310,8 @@ public class NotificationManagerService extends SystemService { } return new NotificationRankingUpdate(keysAr, interceptedKeysAr, visibilityOverrides, suppressedVisualEffects, importanceAr, explanation, overrideGroupKeys, - channels, overridePeople, snoozeCriteria, showBadge, userSentiment, hidden); + channels, overridePeople, snoozeCriteria, showBadge, userSentiment, hidden, + smartActions); } boolean hasCompanionDevice(ManagedServiceInfo info) { diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index 1ab05ec94476..0154c72482c7 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -159,6 +159,7 @@ public final class NotificationRecord { private Light mLight; private String mGroupLogTag; private String mChannelIdLogTag; + private ArrayList<Notification.Action> mSmartActions; private final List<Adjustment> mAdjustments; private final NotificationStats mStats; @@ -630,6 +631,9 @@ public final class NotificationRecord { Adjustment.KEY_USER_SENTIMENT, USER_SENTIMENT_NEUTRAL)); } } + if (signals.containsKey(Adjustment.KEY_SMART_ACTIONS)) { + setSmartActions(signals.getParcelableArrayList(Adjustment.KEY_SMART_ACTIONS)); + } } } } @@ -1049,6 +1053,14 @@ public final class NotificationRecord { mHasSeenSmartReplies = hasSeenSmartReplies; } + public void setSmartActions(ArrayList<Notification.Action> smartActions) { + mSmartActions = smartActions; + } + + public ArrayList<Notification.Action> getSmartActions() { + return mSmartActions; + } + /** * @return all {@link Uri} that should have permission granted to whoever * will be rendering it. This list has already been vetted to only diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java new file mode 100644 index 000000000000..dfc61d98c604 --- /dev/null +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -0,0 +1,1373 @@ +/** + * 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.notification; + +import static android.app.NotificationManager.IMPORTANCE_NONE; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationChannelGroup; +import android.app.NotificationManager; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ParceledListSlice; +import android.metrics.LogMaker; +import android.os.Build; +import android.os.UserHandle; +import android.provider.Settings; +import android.service.notification.NotificationListenerService; +import android.service.notification.RankingHelperProto; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Slog; +import android.util.SparseBooleanArray; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.logging.MetricsLogger; +import com.android.internal.util.Preconditions; +import com.android.internal.util.XmlUtils; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +public class PreferencesHelper implements RankingConfig { + private static final String TAG = "NotificationPrefHelper"; + private static final int XML_VERSION = 1; + + @VisibleForTesting + static final String TAG_RANKING = "ranking"; + private static final String TAG_PACKAGE = "package"; + private static final String TAG_CHANNEL = "channel"; + private static final String TAG_GROUP = "channelGroup"; + + private static final String ATT_VERSION = "version"; + private static final String ATT_NAME = "name"; + private static final String ATT_UID = "uid"; + private static final String ATT_ID = "id"; + private static final String ATT_PRIORITY = "priority"; + private static final String ATT_VISIBILITY = "visibility"; + private static final String ATT_IMPORTANCE = "importance"; + private static final String ATT_SHOW_BADGE = "show_badge"; + private static final String ATT_APP_USER_LOCKED_FIELDS = "app_user_locked_fields"; + + private static final int DEFAULT_PRIORITY = Notification.PRIORITY_DEFAULT; + private static final int DEFAULT_VISIBILITY = NotificationManager.VISIBILITY_NO_OVERRIDE; + private static final int DEFAULT_IMPORTANCE = NotificationManager.IMPORTANCE_UNSPECIFIED; + private static final boolean DEFAULT_SHOW_BADGE = true; + /** + * Default value for what fields are user locked. See {@link LockableAppFields} for all lockable + * fields. + */ + private static final int DEFAULT_LOCKED_APP_FIELDS = 0; + + /** + * All user-lockable fields for a given application. + */ + @IntDef({LockableAppFields.USER_LOCKED_IMPORTANCE}) + public @interface LockableAppFields { + int USER_LOCKED_IMPORTANCE = 0x00000001; + } + + // pkg|uid => PackagePreferences + private final ArrayMap<String, PackagePreferences> mPackagePreferencess = new ArrayMap<>(); + // pkg => PackagePreferences + private final ArrayMap<String, PackagePreferences> mRestoredWithoutUids = new ArrayMap<>(); + + + private final Context mContext; + private final PackageManager mPm; + private final RankingHandler mRankingHandler; + private final ZenModeHelper mZenModeHelper; + + private SparseBooleanArray mBadgingEnabled; + private boolean mAreChannelsBypassingDnd; + + + public PreferencesHelper(Context context, PackageManager pm, RankingHandler rankingHandler, + ZenModeHelper zenHelper) { + mContext = context; + mZenModeHelper = zenHelper; + mRankingHandler = rankingHandler; + mPm = pm; + + updateBadgingEnabled(); + + mAreChannelsBypassingDnd = (mZenModeHelper.getNotificationPolicy().state & + NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND) == 1; + updateChannelsBypassingDnd(); + + } + + public void readXml(XmlPullParser parser, boolean forRestore) + throws XmlPullParserException, IOException { + int type = parser.getEventType(); + if (type != XmlPullParser.START_TAG) return; + String tag = parser.getName(); + if (!TAG_RANKING.equals(tag)) return; + // Clobber groups and channels with the xml, but don't delete other data that wasn't present + // at the time of serialization. + mRestoredWithoutUids.clear(); + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { + tag = parser.getName(); + if (type == XmlPullParser.END_TAG && TAG_RANKING.equals(tag)) { + return; + } + if (type == XmlPullParser.START_TAG) { + if (TAG_PACKAGE.equals(tag)) { + int uid = XmlUtils.readIntAttribute(parser, ATT_UID, + PackagePreferences.UNKNOWN_UID); + String name = parser.getAttributeValue(null, ATT_NAME); + if (!TextUtils.isEmpty(name)) { + if (forRestore) { + try { + //TODO: http://b/22388012 + uid = mPm.getPackageUidAsUser(name, + UserHandle.USER_SYSTEM); + } catch (PackageManager.NameNotFoundException e) { + // noop + } + } + + PackagePreferences r = getOrCreatePackagePreferences(name, uid, + XmlUtils.readIntAttribute( + parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE), + XmlUtils.readIntAttribute(parser, ATT_PRIORITY, DEFAULT_PRIORITY), + XmlUtils.readIntAttribute( + parser, ATT_VISIBILITY, DEFAULT_VISIBILITY), + XmlUtils.readBooleanAttribute( + parser, ATT_SHOW_BADGE, DEFAULT_SHOW_BADGE)); + r.importance = XmlUtils.readIntAttribute( + parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE); + r.priority = XmlUtils.readIntAttribute( + parser, ATT_PRIORITY, DEFAULT_PRIORITY); + r.visibility = XmlUtils.readIntAttribute( + parser, ATT_VISIBILITY, DEFAULT_VISIBILITY); + r.showBadge = XmlUtils.readBooleanAttribute( + parser, ATT_SHOW_BADGE, DEFAULT_SHOW_BADGE); + r.lockedAppFields = XmlUtils.readIntAttribute(parser, + ATT_APP_USER_LOCKED_FIELDS, DEFAULT_LOCKED_APP_FIELDS); + + final int innerDepth = parser.getDepth(); + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG + || parser.getDepth() > innerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + String tagName = parser.getName(); + // Channel groups + if (TAG_GROUP.equals(tagName)) { + String id = parser.getAttributeValue(null, ATT_ID); + CharSequence groupName = parser.getAttributeValue(null, ATT_NAME); + if (!TextUtils.isEmpty(id)) { + NotificationChannelGroup group + = new NotificationChannelGroup(id, groupName); + group.populateFromXml(parser); + r.groups.put(id, group); + } + } + // Channels + if (TAG_CHANNEL.equals(tagName)) { + String id = parser.getAttributeValue(null, ATT_ID); + String channelName = parser.getAttributeValue(null, ATT_NAME); + int channelImportance = XmlUtils.readIntAttribute( + parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE); + if (!TextUtils.isEmpty(id) && !TextUtils.isEmpty(channelName)) { + NotificationChannel channel = new NotificationChannel(id, + channelName, channelImportance); + if (forRestore) { + channel.populateFromXmlForRestore(parser, mContext); + } else { + channel.populateFromXml(parser); + } + r.channels.put(id, channel); + } + } + } + + try { + deleteDefaultChannelIfNeeded(r); + } catch (PackageManager.NameNotFoundException e) { + Slog.e(TAG, "deleteDefaultChannelIfNeeded - Exception: " + e); + } + } + } + } + } + throw new IllegalStateException("Failed to reach END_DOCUMENT"); + } + + private PackagePreferences getPackagePreferences(String pkg, int uid) { + final String key = packagePreferencesKey(pkg, uid); + synchronized (mPackagePreferencess) { + return mPackagePreferencess.get(key); + } + } + + private PackagePreferences getOrCreatePackagePreferences(String pkg, int uid) { + return getOrCreatePackagePreferences(pkg, uid, + DEFAULT_IMPORTANCE, DEFAULT_PRIORITY, DEFAULT_VISIBILITY, DEFAULT_SHOW_BADGE); + } + + private PackagePreferences getOrCreatePackagePreferences(String pkg, int uid, int importance, + int priority, int visibility, boolean showBadge) { + final String key = packagePreferencesKey(pkg, uid); + synchronized (mPackagePreferencess) { + PackagePreferences + r = (uid == PackagePreferences.UNKNOWN_UID) ? mRestoredWithoutUids.get(pkg) + : mPackagePreferencess.get(key); + if (r == null) { + r = new PackagePreferences(); + r.pkg = pkg; + r.uid = uid; + r.importance = importance; + r.priority = priority; + r.visibility = visibility; + r.showBadge = showBadge; + + try { + createDefaultChannelIfNeeded(r); + } catch (PackageManager.NameNotFoundException e) { + Slog.e(TAG, "createDefaultChannelIfNeeded - Exception: " + e); + } + + if (r.uid == PackagePreferences.UNKNOWN_UID) { + mRestoredWithoutUids.put(pkg, r); + } else { + mPackagePreferencess.put(key, r); + } + } + return r; + } + } + + private boolean shouldHaveDefaultChannel(PackagePreferences r) throws + PackageManager.NameNotFoundException { + final int userId = UserHandle.getUserId(r.uid); + final ApplicationInfo applicationInfo = + mPm.getApplicationInfoAsUser(r.pkg, 0, userId); + if (applicationInfo.targetSdkVersion >= Build.VERSION_CODES.O) { + // O apps should not have the default channel. + return false; + } + + // Otherwise, this app should have the default channel. + return true; + } + + private void deleteDefaultChannelIfNeeded(PackagePreferences r) throws + PackageManager.NameNotFoundException { + if (!r.channels.containsKey(NotificationChannel.DEFAULT_CHANNEL_ID)) { + // Not present + return; + } + + if (shouldHaveDefaultChannel(r)) { + // Keep the default channel until upgraded. + return; + } + + // Remove Default Channel. + r.channels.remove(NotificationChannel.DEFAULT_CHANNEL_ID); + } + + private void createDefaultChannelIfNeeded(PackagePreferences r) throws + PackageManager.NameNotFoundException { + if (r.channels.containsKey(NotificationChannel.DEFAULT_CHANNEL_ID)) { + r.channels.get(NotificationChannel.DEFAULT_CHANNEL_ID).setName(mContext.getString( + com.android.internal.R.string.default_notification_channel_label)); + return; + } + + if (!shouldHaveDefaultChannel(r)) { + // Keep the default channel until upgraded. + return; + } + + // Create Default Channel + NotificationChannel channel; + channel = new NotificationChannel( + NotificationChannel.DEFAULT_CHANNEL_ID, + mContext.getString(R.string.default_notification_channel_label), + r.importance); + channel.setBypassDnd(r.priority == Notification.PRIORITY_MAX); + channel.setLockscreenVisibility(r.visibility); + if (r.importance != NotificationManager.IMPORTANCE_UNSPECIFIED) { + channel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE); + } + if (r.priority != DEFAULT_PRIORITY) { + channel.lockFields(NotificationChannel.USER_LOCKED_PRIORITY); + } + if (r.visibility != DEFAULT_VISIBILITY) { + channel.lockFields(NotificationChannel.USER_LOCKED_VISIBILITY); + } + r.channels.put(channel.getId(), channel); + } + + public void writeXml(XmlSerializer out, boolean forBackup) throws IOException { + out.startTag(null, TAG_RANKING); + out.attribute(null, ATT_VERSION, Integer.toString(XML_VERSION)); + + synchronized (mPackagePreferencess) { + final int N = mPackagePreferencess.size(); + for (int i = 0; i < N; i++) { + final PackagePreferences r = mPackagePreferencess.valueAt(i); + //TODO: http://b/22388012 + if (forBackup && UserHandle.getUserId(r.uid) != UserHandle.USER_SYSTEM) { + continue; + } + final boolean hasNonDefaultSettings = + r.importance != DEFAULT_IMPORTANCE + || r.priority != DEFAULT_PRIORITY + || r.visibility != DEFAULT_VISIBILITY + || r.showBadge != DEFAULT_SHOW_BADGE + || r.lockedAppFields != DEFAULT_LOCKED_APP_FIELDS + || r.channels.size() > 0 + || r.groups.size() > 0; + if (hasNonDefaultSettings) { + out.startTag(null, TAG_PACKAGE); + out.attribute(null, ATT_NAME, r.pkg); + if (r.importance != DEFAULT_IMPORTANCE) { + out.attribute(null, ATT_IMPORTANCE, Integer.toString(r.importance)); + } + if (r.priority != DEFAULT_PRIORITY) { + out.attribute(null, ATT_PRIORITY, Integer.toString(r.priority)); + } + if (r.visibility != DEFAULT_VISIBILITY) { + out.attribute(null, ATT_VISIBILITY, Integer.toString(r.visibility)); + } + out.attribute(null, ATT_SHOW_BADGE, Boolean.toString(r.showBadge)); + out.attribute(null, ATT_APP_USER_LOCKED_FIELDS, + Integer.toString(r.lockedAppFields)); + + if (!forBackup) { + out.attribute(null, ATT_UID, Integer.toString(r.uid)); + } + + for (NotificationChannelGroup group : r.groups.values()) { + group.writeXml(out); + } + + for (NotificationChannel channel : r.channels.values()) { + if (forBackup) { + if (!channel.isDeleted()) { + channel.writeXmlForBackup(out, mContext); + } + } else { + channel.writeXml(out); + } + } + + out.endTag(null, TAG_PACKAGE); + } + } + } + out.endTag(null, TAG_RANKING); + } + + /** + * Gets importance. + */ + @Override + public int getImportance(String packageName, int uid) { + return getOrCreatePackagePreferences(packageName, uid).importance; + } + + + /** + * Returns whether the importance of the corresponding notification is user-locked and shouldn't + * be adjusted by an assistant (via means of a blocking helper, for example). For the channel + * locking field, see {@link NotificationChannel#USER_LOCKED_IMPORTANCE}. + */ + public boolean getIsAppImportanceLocked(String packageName, int uid) { + int userLockedFields = getOrCreatePackagePreferences(packageName, uid).lockedAppFields; + return (userLockedFields & LockableAppFields.USER_LOCKED_IMPORTANCE) != 0; + } + + @Override + public boolean canShowBadge(String packageName, int uid) { + return getOrCreatePackagePreferences(packageName, uid).showBadge; + } + + @Override + public void setShowBadge(String packageName, int uid, boolean showBadge) { + getOrCreatePackagePreferences(packageName, uid).showBadge = showBadge; + updateConfig(); + } + + @Override + public boolean isGroupBlocked(String packageName, int uid, String groupId) { + if (groupId == null) { + return false; + } + PackagePreferences r = getOrCreatePackagePreferences(packageName, uid); + NotificationChannelGroup group = r.groups.get(groupId); + if (group == null) { + return false; + } + return group.isBlocked(); + } + + int getPackagePriority(String pkg, int uid) { + return getOrCreatePackagePreferences(pkg, uid).priority; + } + + int getPackageVisibility(String pkg, int uid) { + return getOrCreatePackagePreferences(pkg, uid).visibility; + } + + @Override + public void createNotificationChannelGroup(String pkg, int uid, NotificationChannelGroup group, + boolean fromTargetApp) { + Preconditions.checkNotNull(pkg); + Preconditions.checkNotNull(group); + Preconditions.checkNotNull(group.getId()); + Preconditions.checkNotNull(!TextUtils.isEmpty(group.getName())); + PackagePreferences r = getOrCreatePackagePreferences(pkg, uid); + if (r == null) { + throw new IllegalArgumentException("Invalid package"); + } + final NotificationChannelGroup oldGroup = r.groups.get(group.getId()); + if (!group.equals(oldGroup)) { + // will log for new entries as well as name/description changes + MetricsLogger.action(getChannelGroupLog(group.getId(), pkg)); + } + if (oldGroup != null) { + group.setChannels(oldGroup.getChannels()); + + if (fromTargetApp) { + group.setBlocked(oldGroup.isBlocked()); + } + } + r.groups.put(group.getId(), group); + } + + @Override + public void createNotificationChannel(String pkg, int uid, NotificationChannel channel, + boolean fromTargetApp, boolean hasDndAccess) { + Preconditions.checkNotNull(pkg); + Preconditions.checkNotNull(channel); + Preconditions.checkNotNull(channel.getId()); + Preconditions.checkArgument(!TextUtils.isEmpty(channel.getName())); + PackagePreferences r = getOrCreatePackagePreferences(pkg, uid); + if (r == null) { + throw new IllegalArgumentException("Invalid package"); + } + if (channel.getGroup() != null && !r.groups.containsKey(channel.getGroup())) { + throw new IllegalArgumentException("NotificationChannelGroup doesn't exist"); + } + if (NotificationChannel.DEFAULT_CHANNEL_ID.equals(channel.getId())) { + throw new IllegalArgumentException("Reserved id"); + } + NotificationChannel existing = r.channels.get(channel.getId()); + // Keep most of the existing settings + if (existing != null && fromTargetApp) { + if (existing.isDeleted()) { + existing.setDeleted(false); + + // log a resurrected channel as if it's new again + MetricsLogger.action(getChannelLog(channel, pkg).setType( + com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_OPEN)); + } + + existing.setName(channel.getName().toString()); + existing.setDescription(channel.getDescription()); + existing.setBlockableSystem(channel.isBlockableSystem()); + if (existing.getGroup() == null) { + existing.setGroup(channel.getGroup()); + } + + // Apps are allowed to downgrade channel importance if the user has not changed any + // fields on this channel yet. + if (existing.getUserLockedFields() == 0 && + channel.getImportance() < existing.getImportance()) { + existing.setImportance(channel.getImportance()); + } + + // system apps and dnd access apps can bypass dnd if the user hasn't changed any + // fields on the channel yet + if (existing.getUserLockedFields() == 0 && hasDndAccess) { + boolean bypassDnd = channel.canBypassDnd(); + existing.setBypassDnd(bypassDnd); + + if (bypassDnd != mAreChannelsBypassingDnd) { + updateChannelsBypassingDnd(); + } + } + + updateConfig(); + return; + } + if (channel.getImportance() < IMPORTANCE_NONE + || channel.getImportance() > NotificationManager.IMPORTANCE_MAX) { + throw new IllegalArgumentException("Invalid importance level"); + } + + // Reset fields that apps aren't allowed to set. + if (fromTargetApp && !hasDndAccess) { + channel.setBypassDnd(r.priority == Notification.PRIORITY_MAX); + } + if (fromTargetApp) { + channel.setLockscreenVisibility(r.visibility); + } + clearLockedFields(channel); + if (channel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) { + channel.setLockscreenVisibility( + NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE); + } + if (!r.showBadge) { + channel.setShowBadge(false); + } + + r.channels.put(channel.getId(), channel); + if (channel.canBypassDnd() != mAreChannelsBypassingDnd) { + updateChannelsBypassingDnd(); + } + MetricsLogger.action(getChannelLog(channel, pkg).setType( + com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_OPEN)); + } + + void clearLockedFields(NotificationChannel channel) { + channel.unlockFields(channel.getUserLockedFields()); + } + + @Override + public void updateNotificationChannel(String pkg, int uid, NotificationChannel updatedChannel, + boolean fromUser) { + Preconditions.checkNotNull(updatedChannel); + Preconditions.checkNotNull(updatedChannel.getId()); + PackagePreferences r = getOrCreatePackagePreferences(pkg, uid); + if (r == null) { + throw new IllegalArgumentException("Invalid package"); + } + NotificationChannel channel = r.channels.get(updatedChannel.getId()); + if (channel == null || channel.isDeleted()) { + throw new IllegalArgumentException("Channel does not exist"); + } + if (updatedChannel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) { + updatedChannel.setLockscreenVisibility( + NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE); + } + if (!fromUser) { + updatedChannel.unlockFields(updatedChannel.getUserLockedFields()); + } + if (fromUser) { + updatedChannel.lockFields(channel.getUserLockedFields()); + lockFieldsForUpdate(channel, updatedChannel); + } + r.channels.put(updatedChannel.getId(), updatedChannel); + + if (NotificationChannel.DEFAULT_CHANNEL_ID.equals(updatedChannel.getId())) { + // copy settings to app level so they are inherited by new channels + // when the app migrates + r.importance = updatedChannel.getImportance(); + r.priority = updatedChannel.canBypassDnd() + ? Notification.PRIORITY_MAX : Notification.PRIORITY_DEFAULT; + r.visibility = updatedChannel.getLockscreenVisibility(); + r.showBadge = updatedChannel.canShowBadge(); + } + + if (!channel.equals(updatedChannel)) { + // only log if there are real changes + MetricsLogger.action(getChannelLog(updatedChannel, pkg)); + } + + if (updatedChannel.canBypassDnd() != mAreChannelsBypassingDnd) { + updateChannelsBypassingDnd(); + } + updateConfig(); + } + + @Override + public NotificationChannel getNotificationChannel(String pkg, int uid, String channelId, + boolean includeDeleted) { + Preconditions.checkNotNull(pkg); + PackagePreferences r = getOrCreatePackagePreferences(pkg, uid); + if (r == null) { + return null; + } + if (channelId == null) { + channelId = NotificationChannel.DEFAULT_CHANNEL_ID; + } + final NotificationChannel nc = r.channels.get(channelId); + if (nc != null && (includeDeleted || !nc.isDeleted())) { + return nc; + } + return null; + } + + @Override + public void deleteNotificationChannel(String pkg, int uid, String channelId) { + PackagePreferences r = getPackagePreferences(pkg, uid); + if (r == null) { + return; + } + NotificationChannel channel = r.channels.get(channelId); + if (channel != null) { + channel.setDeleted(true); + LogMaker lm = getChannelLog(channel, pkg); + lm.setType(com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_CLOSE); + MetricsLogger.action(lm); + + if (mAreChannelsBypassingDnd && channel.canBypassDnd()) { + updateChannelsBypassingDnd(); + } + } + } + + @Override + @VisibleForTesting + public void permanentlyDeleteNotificationChannel(String pkg, int uid, String channelId) { + Preconditions.checkNotNull(pkg); + Preconditions.checkNotNull(channelId); + PackagePreferences r = getPackagePreferences(pkg, uid); + if (r == null) { + return; + } + r.channels.remove(channelId); + } + + @Override + public void permanentlyDeleteNotificationChannels(String pkg, int uid) { + Preconditions.checkNotNull(pkg); + PackagePreferences r = getPackagePreferences(pkg, uid); + if (r == null) { + return; + } + int N = r.channels.size() - 1; + for (int i = N; i >= 0; i--) { + String key = r.channels.keyAt(i); + if (!NotificationChannel.DEFAULT_CHANNEL_ID.equals(key)) { + r.channels.remove(key); + } + } + } + + public NotificationChannelGroup getNotificationChannelGroupWithChannels(String pkg, + int uid, String groupId, boolean includeDeleted) { + Preconditions.checkNotNull(pkg); + PackagePreferences r = getPackagePreferences(pkg, uid); + if (r == null || groupId == null || !r.groups.containsKey(groupId)) { + return null; + } + NotificationChannelGroup group = r.groups.get(groupId).clone(); + group.setChannels(new ArrayList<>()); + int N = r.channels.size(); + for (int i = 0; i < N; i++) { + final NotificationChannel nc = r.channels.valueAt(i); + if (includeDeleted || !nc.isDeleted()) { + if (groupId.equals(nc.getGroup())) { + group.addChannel(nc); + } + } + } + return group; + } + + public NotificationChannelGroup getNotificationChannelGroup(String groupId, String pkg, + int uid) { + Preconditions.checkNotNull(pkg); + PackagePreferences r = getPackagePreferences(pkg, uid); + if (r == null) { + return null; + } + return r.groups.get(groupId); + } + + @Override + public ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroups(String pkg, + int uid, boolean includeDeleted, boolean includeNonGrouped) { + Preconditions.checkNotNull(pkg); + Map<String, NotificationChannelGroup> groups = new ArrayMap<>(); + PackagePreferences r = getPackagePreferences(pkg, uid); + if (r == null) { + return ParceledListSlice.emptyList(); + } + NotificationChannelGroup nonGrouped = new NotificationChannelGroup(null, null); + int N = r.channels.size(); + for (int i = 0; i < N; i++) { + final NotificationChannel nc = r.channels.valueAt(i); + if (includeDeleted || !nc.isDeleted()) { + if (nc.getGroup() != null) { + if (r.groups.get(nc.getGroup()) != null) { + NotificationChannelGroup ncg = groups.get(nc.getGroup()); + if (ncg == null) { + ncg = r.groups.get(nc.getGroup()).clone(); + ncg.setChannels(new ArrayList<>()); + groups.put(nc.getGroup(), ncg); + + } + ncg.addChannel(nc); + } + } else { + nonGrouped.addChannel(nc); + } + } + } + if (includeNonGrouped && nonGrouped.getChannels().size() > 0) { + groups.put(null, nonGrouped); + } + return new ParceledListSlice<>(new ArrayList<>(groups.values())); + } + + public List<NotificationChannel> deleteNotificationChannelGroup(String pkg, int uid, + String groupId) { + List<NotificationChannel> deletedChannels = new ArrayList<>(); + PackagePreferences r = getPackagePreferences(pkg, uid); + if (r == null || TextUtils.isEmpty(groupId)) { + return deletedChannels; + } + + r.groups.remove(groupId); + + int N = r.channels.size(); + for (int i = 0; i < N; i++) { + final NotificationChannel nc = r.channels.valueAt(i); + if (groupId.equals(nc.getGroup())) { + nc.setDeleted(true); + deletedChannels.add(nc); + } + } + return deletedChannels; + } + + @Override + public Collection<NotificationChannelGroup> getNotificationChannelGroups(String pkg, + int uid) { + PackagePreferences r = getPackagePreferences(pkg, uid); + if (r == null) { + return new ArrayList<>(); + } + return r.groups.values(); + } + + @Override + public ParceledListSlice<NotificationChannel> getNotificationChannels(String pkg, int uid, + boolean includeDeleted) { + Preconditions.checkNotNull(pkg); + List<NotificationChannel> channels = new ArrayList<>(); + PackagePreferences r = getPackagePreferences(pkg, uid); + if (r == null) { + return ParceledListSlice.emptyList(); + } + int N = r.channels.size(); + for (int i = 0; i < N; i++) { + final NotificationChannel nc = r.channels.valueAt(i); + if (includeDeleted || !nc.isDeleted()) { + channels.add(nc); + } + } + return new ParceledListSlice<>(channels); + } + + /** + * True for pre-O apps that only have the default channel, or pre O apps that have no + * channels yet. This method will create the default channel for pre-O apps that don't have it. + * Should never be true for O+ targeting apps, but that's enforced on boot/when an app + * upgrades. + */ + public boolean onlyHasDefaultChannel(String pkg, int uid) { + PackagePreferences r = getOrCreatePackagePreferences(pkg, uid); + if (r.channels.size() == 1 + && r.channels.containsKey(NotificationChannel.DEFAULT_CHANNEL_ID)) { + return true; + } + return false; + } + + public int getDeletedChannelCount(String pkg, int uid) { + Preconditions.checkNotNull(pkg); + int deletedCount = 0; + PackagePreferences r = getPackagePreferences(pkg, uid); + if (r == null) { + return deletedCount; + } + int N = r.channels.size(); + for (int i = 0; i < N; i++) { + final NotificationChannel nc = r.channels.valueAt(i); + if (nc.isDeleted()) { + deletedCount++; + } + } + return deletedCount; + } + + public int getBlockedChannelCount(String pkg, int uid) { + Preconditions.checkNotNull(pkg); + int blockedCount = 0; + PackagePreferences r = getPackagePreferences(pkg, uid); + if (r == null) { + return blockedCount; + } + int N = r.channels.size(); + for (int i = 0; i < N; i++) { + final NotificationChannel nc = r.channels.valueAt(i); + if (!nc.isDeleted() && IMPORTANCE_NONE == nc.getImportance()) { + blockedCount++; + } + } + return blockedCount; + } + + public int getBlockedAppCount(int userId) { + int count = 0; + synchronized (mPackagePreferencess) { + final int N = mPackagePreferencess.size(); + for (int i = 0; i < N; i++) { + final PackagePreferences r = mPackagePreferencess.valueAt(i); + if (userId == UserHandle.getUserId(r.uid) + && r.importance == IMPORTANCE_NONE) { + count++; + } + } + } + return count; + } + + public void updateChannelsBypassingDnd() { + synchronized (mPackagePreferencess) { + final int numPackagePreferencess = mPackagePreferencess.size(); + for (int PackagePreferencesIndex = 0; PackagePreferencesIndex < numPackagePreferencess; + PackagePreferencesIndex++) { + final PackagePreferences r = mPackagePreferencess.valueAt(PackagePreferencesIndex); + final int numChannels = r.channels.size(); + + for (int channelIndex = 0; channelIndex < numChannels; channelIndex++) { + NotificationChannel channel = r.channels.valueAt(channelIndex); + if (!channel.isDeleted() && channel.canBypassDnd()) { + // If any channel bypasses DND, synchronize state and return early. + if (!mAreChannelsBypassingDnd) { + mAreChannelsBypassingDnd = true; + updateZenPolicy(true); + } + return; + } + } + } + } + + // If no channels bypass DND, update the zen policy once to disable DND bypass. + if (mAreChannelsBypassingDnd) { + mAreChannelsBypassingDnd = false; + updateZenPolicy(false); + } + } + + public void updateZenPolicy(boolean areChannelsBypassingDnd) { + NotificationManager.Policy policy = mZenModeHelper.getNotificationPolicy(); + mZenModeHelper.setNotificationPolicy(new NotificationManager.Policy( + policy.priorityCategories, policy.priorityCallSenders, + policy.priorityMessageSenders, policy.suppressedVisualEffects, + (areChannelsBypassingDnd ? NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND + : 0))); + } + + public boolean areChannelsBypassingDnd() { + return mAreChannelsBypassingDnd; + } + + /** + * Sets importance. + */ + @Override + public void setImportance(String pkgName, int uid, int importance) { + getOrCreatePackagePreferences(pkgName, uid).importance = importance; + updateConfig(); + } + + public void setEnabled(String packageName, int uid, boolean enabled) { + boolean wasEnabled = getImportance(packageName, uid) != IMPORTANCE_NONE; + if (wasEnabled == enabled) { + return; + } + setImportance(packageName, uid, + enabled ? DEFAULT_IMPORTANCE : IMPORTANCE_NONE); + } + + /** + * Sets whether any notifications from the app, represented by the given {@code pkgName} and + * {@code uid}, have their importance locked by the user. Locked notifications don't get + * considered for sentiment adjustments (and thus never show a blocking helper). + */ + public void setAppImportanceLocked(String packageName, int uid) { + PackagePreferences PackagePreferences = getOrCreatePackagePreferences(packageName, uid); + if ((PackagePreferences.lockedAppFields & LockableAppFields.USER_LOCKED_IMPORTANCE) != 0) { + return; + } + + PackagePreferences.lockedAppFields = + PackagePreferences.lockedAppFields | LockableAppFields.USER_LOCKED_IMPORTANCE; + updateConfig(); + } + + @VisibleForTesting + void lockFieldsForUpdate(NotificationChannel original, NotificationChannel update) { + if (original.canBypassDnd() != update.canBypassDnd()) { + update.lockFields(NotificationChannel.USER_LOCKED_PRIORITY); + } + if (original.getLockscreenVisibility() != update.getLockscreenVisibility()) { + update.lockFields(NotificationChannel.USER_LOCKED_VISIBILITY); + } + if (original.getImportance() != update.getImportance()) { + update.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE); + } + if (original.shouldShowLights() != update.shouldShowLights() + || original.getLightColor() != update.getLightColor()) { + update.lockFields(NotificationChannel.USER_LOCKED_LIGHTS); + } + if (!Objects.equals(original.getSound(), update.getSound())) { + update.lockFields(NotificationChannel.USER_LOCKED_SOUND); + } + if (!Arrays.equals(original.getVibrationPattern(), update.getVibrationPattern()) + || original.shouldVibrate() != update.shouldVibrate()) { + update.lockFields(NotificationChannel.USER_LOCKED_VIBRATION); + } + if (original.canShowBadge() != update.canShowBadge()) { + update.lockFields(NotificationChannel.USER_LOCKED_SHOW_BADGE); + } + } + + public void dump(PrintWriter pw, String prefix, + @NonNull NotificationManagerService.DumpFilter filter) { + pw.print(prefix); + pw.println("per-package config:"); + + pw.println("PackagePreferencess:"); + synchronized (mPackagePreferencess) { + dumpPackagePreferencess(pw, prefix, filter, mPackagePreferencess); + } + pw.println("Restored without uid:"); + dumpPackagePreferencess(pw, prefix, filter, mRestoredWithoutUids); + } + + public void dump(ProtoOutputStream proto, + @NonNull NotificationManagerService.DumpFilter filter) { + synchronized (mPackagePreferencess) { + dumpPackagePreferencess(proto, RankingHelperProto.RECORDS, filter, + mPackagePreferencess); + } + dumpPackagePreferencess(proto, RankingHelperProto.RECORDS_RESTORED_WITHOUT_UID, filter, + mRestoredWithoutUids); + } + + private static void dumpPackagePreferencess(PrintWriter pw, String prefix, + @NonNull NotificationManagerService.DumpFilter filter, + ArrayMap<String, PackagePreferences> PackagePreferencess) { + final int N = PackagePreferencess.size(); + for (int i = 0; i < N; i++) { + final PackagePreferences r = PackagePreferencess.valueAt(i); + if (filter.matches(r.pkg)) { + pw.print(prefix); + pw.print(" AppSettings: "); + pw.print(r.pkg); + pw.print(" ("); + pw.print(r.uid == PackagePreferences.UNKNOWN_UID ? "UNKNOWN_UID" + : Integer.toString(r.uid)); + pw.print(')'); + if (r.importance != DEFAULT_IMPORTANCE) { + pw.print(" importance="); + pw.print(NotificationListenerService.Ranking.importanceToString(r.importance)); + } + if (r.priority != DEFAULT_PRIORITY) { + pw.print(" priority="); + pw.print(Notification.priorityToString(r.priority)); + } + if (r.visibility != DEFAULT_VISIBILITY) { + pw.print(" visibility="); + pw.print(Notification.visibilityToString(r.visibility)); + } + pw.print(" showBadge="); + pw.print(Boolean.toString(r.showBadge)); + pw.println(); + for (NotificationChannel channel : r.channels.values()) { + pw.print(prefix); + channel.dump(pw, " ", filter.redact); + } + for (NotificationChannelGroup group : r.groups.values()) { + pw.print(prefix); + pw.print(" "); + pw.print(" "); + pw.println(group); + } + } + } + } + + private static void dumpPackagePreferencess(ProtoOutputStream proto, long fieldId, + @NonNull NotificationManagerService.DumpFilter filter, + ArrayMap<String, PackagePreferences> PackagePreferencess) { + final int N = PackagePreferencess.size(); + long fToken; + for (int i = 0; i < N; i++) { + final PackagePreferences r = PackagePreferencess.valueAt(i); + if (filter.matches(r.pkg)) { + fToken = proto.start(fieldId); + + proto.write(RankingHelperProto.RecordProto.PACKAGE, r.pkg); + proto.write(RankingHelperProto.RecordProto.UID, r.uid); + proto.write(RankingHelperProto.RecordProto.IMPORTANCE, r.importance); + proto.write(RankingHelperProto.RecordProto.PRIORITY, r.priority); + proto.write(RankingHelperProto.RecordProto.VISIBILITY, r.visibility); + proto.write(RankingHelperProto.RecordProto.SHOW_BADGE, r.showBadge); + + for (NotificationChannel channel : r.channels.values()) { + channel.writeToProto(proto, RankingHelperProto.RecordProto.CHANNELS); + } + for (NotificationChannelGroup group : r.groups.values()) { + group.writeToProto(proto, RankingHelperProto.RecordProto.CHANNEL_GROUPS); + } + + proto.end(fToken); + } + } + } + + public JSONObject dumpJson(NotificationManagerService.DumpFilter filter) { + JSONObject ranking = new JSONObject(); + JSONArray PackagePreferencess = new JSONArray(); + try { + ranking.put("noUid", mRestoredWithoutUids.size()); + } catch (JSONException e) { + // pass + } + synchronized (mPackagePreferencess) { + final int N = mPackagePreferencess.size(); + for (int i = 0; i < N; i++) { + final PackagePreferences r = mPackagePreferencess.valueAt(i); + if (filter == null || filter.matches(r.pkg)) { + JSONObject PackagePreferences = new JSONObject(); + try { + PackagePreferences.put("userId", UserHandle.getUserId(r.uid)); + PackagePreferences.put("packageName", r.pkg); + if (r.importance != DEFAULT_IMPORTANCE) { + PackagePreferences.put("importance", + NotificationListenerService.Ranking.importanceToString( + r.importance)); + } + if (r.priority != DEFAULT_PRIORITY) { + PackagePreferences.put("priority", + Notification.priorityToString(r.priority)); + } + if (r.visibility != DEFAULT_VISIBILITY) { + PackagePreferences.put("visibility", + Notification.visibilityToString(r.visibility)); + } + if (r.showBadge != DEFAULT_SHOW_BADGE) { + PackagePreferences.put("showBadge", Boolean.valueOf(r.showBadge)); + } + JSONArray channels = new JSONArray(); + for (NotificationChannel channel : r.channels.values()) { + channels.put(channel.toJson()); + } + PackagePreferences.put("channels", channels); + JSONArray groups = new JSONArray(); + for (NotificationChannelGroup group : r.groups.values()) { + groups.put(group.toJson()); + } + PackagePreferences.put("groups", groups); + } catch (JSONException e) { + // pass + } + PackagePreferencess.put(PackagePreferences); + } + } + } + try { + ranking.put("PackagePreferencess", PackagePreferencess); + } catch (JSONException e) { + // pass + } + return ranking; + } + + /** + * Dump only the ban information as structured JSON for the stats collector. + * + * This is intentionally redundant with {#link dumpJson} because the old + * scraper will expect this format. + * + * @param filter + * @return + */ + public JSONArray dumpBansJson(NotificationManagerService.DumpFilter filter) { + JSONArray bans = new JSONArray(); + Map<Integer, String> packageBans = getPackageBans(); + for (Map.Entry<Integer, String> ban : packageBans.entrySet()) { + final int userId = UserHandle.getUserId(ban.getKey()); + final String packageName = ban.getValue(); + if (filter == null || filter.matches(packageName)) { + JSONObject banJson = new JSONObject(); + try { + banJson.put("userId", userId); + banJson.put("packageName", packageName); + } catch (JSONException e) { + e.printStackTrace(); + } + bans.put(banJson); + } + } + return bans; + } + + public Map<Integer, String> getPackageBans() { + synchronized (mPackagePreferencess) { + final int N = mPackagePreferencess.size(); + ArrayMap<Integer, String> packageBans = new ArrayMap<>(N); + for (int i = 0; i < N; i++) { + final PackagePreferences r = mPackagePreferencess.valueAt(i); + if (r.importance == IMPORTANCE_NONE) { + packageBans.put(r.uid, r.pkg); + } + } + + return packageBans; + } + } + + /** + * Dump only the channel information as structured JSON for the stats collector. + * + * This is intentionally redundant with {#link dumpJson} because the old + * scraper will expect this format. + * + * @param filter + * @return + */ + public JSONArray dumpChannelsJson(NotificationManagerService.DumpFilter filter) { + JSONArray channels = new JSONArray(); + Map<String, Integer> packageChannels = getPackageChannels(); + for (Map.Entry<String, Integer> channelCount : packageChannels.entrySet()) { + final String packageName = channelCount.getKey(); + if (filter == null || filter.matches(packageName)) { + JSONObject channelCountJson = new JSONObject(); + try { + channelCountJson.put("packageName", packageName); + channelCountJson.put("channelCount", channelCount.getValue()); + } catch (JSONException e) { + e.printStackTrace(); + } + channels.put(channelCountJson); + } + } + return channels; + } + + private Map<String, Integer> getPackageChannels() { + ArrayMap<String, Integer> packageChannels = new ArrayMap<>(); + synchronized (mPackagePreferencess) { + for (int i = 0; i < mPackagePreferencess.size(); i++) { + final PackagePreferences r = mPackagePreferencess.valueAt(i); + int channelCount = 0; + for (int j = 0; j < r.channels.size(); j++) { + if (!r.channels.valueAt(j).isDeleted()) { + channelCount++; + } + } + packageChannels.put(r.pkg, channelCount); + } + } + return packageChannels; + } + + public void onUserRemoved(int userId) { + synchronized (mPackagePreferencess) { + int N = mPackagePreferencess.size(); + for (int i = N - 1; i >= 0; i--) { + PackagePreferences PackagePreferences = mPackagePreferencess.valueAt(i); + if (UserHandle.getUserId(PackagePreferences.uid) == userId) { + mPackagePreferencess.removeAt(i); + } + } + } + } + + protected void onLocaleChanged(Context context, int userId) { + synchronized (mPackagePreferencess) { + int N = mPackagePreferencess.size(); + for (int i = 0; i < N; i++) { + PackagePreferences PackagePreferences = mPackagePreferencess.valueAt(i); + if (UserHandle.getUserId(PackagePreferences.uid) == userId) { + if (PackagePreferences.channels.containsKey( + NotificationChannel.DEFAULT_CHANNEL_ID)) { + PackagePreferences.channels.get( + NotificationChannel.DEFAULT_CHANNEL_ID).setName( + context.getResources().getString( + R.string.default_notification_channel_label)); + } + } + } + } + } + + public void onPackagesChanged(boolean removingPackage, int changeUserId, String[] pkgList, + int[] uidList) { + if (pkgList == null || pkgList.length == 0) { + return; // nothing to do + } + boolean updated = false; + if (removingPackage) { + // Remove notification settings for uninstalled package + int size = Math.min(pkgList.length, uidList.length); + for (int i = 0; i < size; i++) { + final String pkg = pkgList[i]; + final int uid = uidList[i]; + synchronized (mPackagePreferencess) { + mPackagePreferencess.remove(packagePreferencesKey(pkg, uid)); + } + mRestoredWithoutUids.remove(pkg); + updated = true; + } + } else { + for (String pkg : pkgList) { + // Package install + final PackagePreferences r = mRestoredWithoutUids.get(pkg); + if (r != null) { + try { + r.uid = mPm.getPackageUidAsUser(r.pkg, changeUserId); + mRestoredWithoutUids.remove(pkg); + synchronized (mPackagePreferencess) { + mPackagePreferencess.put(packagePreferencesKey(r.pkg, r.uid), r); + } + updated = true; + } catch (PackageManager.NameNotFoundException e) { + // noop + } + } + // Package upgrade + try { + PackagePreferences fullPackagePreferences = getPackagePreferences(pkg, + mPm.getPackageUidAsUser(pkg, changeUserId)); + if (fullPackagePreferences != null) { + createDefaultChannelIfNeeded(fullPackagePreferences); + deleteDefaultChannelIfNeeded(fullPackagePreferences); + } + } catch (PackageManager.NameNotFoundException e) { + } + } + } + + if (updated) { + updateConfig(); + } + } + + private LogMaker getChannelLog(NotificationChannel channel, String pkg) { + return new LogMaker( + com.android.internal.logging.nano.MetricsProto.MetricsEvent + .ACTION_NOTIFICATION_CHANNEL) + .setType(com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_UPDATE) + .setPackageName(pkg) + .addTaggedData( + com.android.internal.logging.nano.MetricsProto.MetricsEvent + .FIELD_NOTIFICATION_CHANNEL_ID, + channel.getId()) + .addTaggedData( + com.android.internal.logging.nano.MetricsProto.MetricsEvent + .FIELD_NOTIFICATION_CHANNEL_IMPORTANCE, + channel.getImportance()); + } + + private LogMaker getChannelGroupLog(String groupId, String pkg) { + return new LogMaker( + com.android.internal.logging.nano.MetricsProto.MetricsEvent + .ACTION_NOTIFICATION_CHANNEL_GROUP) + .setType(com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_UPDATE) + .addTaggedData( + com.android.internal.logging.nano.MetricsProto.MetricsEvent + .FIELD_NOTIFICATION_CHANNEL_GROUP_ID, + groupId) + .setPackageName(pkg); + } + + + public void updateBadgingEnabled() { + if (mBadgingEnabled == null) { + mBadgingEnabled = new SparseBooleanArray(); + } + boolean changed = false; + // update the cached values + for (int index = 0; index < mBadgingEnabled.size(); index++) { + int userId = mBadgingEnabled.keyAt(index); + final boolean oldValue = mBadgingEnabled.get(userId); + final boolean newValue = Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.NOTIFICATION_BADGING, + DEFAULT_SHOW_BADGE ? 1 : 0, userId) != 0; + mBadgingEnabled.put(userId, newValue); + changed |= oldValue != newValue; + } + if (changed) { + updateConfig(); + } + } + + public boolean badgingEnabled(UserHandle userHandle) { + int userId = userHandle.getIdentifier(); + if (userId == UserHandle.USER_ALL) { + return false; + } + if (mBadgingEnabled.indexOfKey(userId) < 0) { + mBadgingEnabled.put(userId, + Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.NOTIFICATION_BADGING, + DEFAULT_SHOW_BADGE ? 1 : 0, userId) != 0); + } + return mBadgingEnabled.get(userId, DEFAULT_SHOW_BADGE); + } + + private void updateConfig() { + mRankingHandler.requestSort(); + } + + private static String packagePreferencesKey(String pkg, int uid) { + return pkg + "|" + uid; + } + + private static class PackagePreferences { + static int UNKNOWN_UID = UserHandle.USER_NULL; + + String pkg; + int uid = UNKNOWN_UID; + int importance = DEFAULT_IMPORTANCE; + int priority = DEFAULT_PRIORITY; + int visibility = DEFAULT_VISIBILITY; + boolean showBadge = DEFAULT_SHOW_BADGE; + int lockedAppFields = DEFAULT_LOCKED_APP_FIELDS; + + ArrayMap<String, NotificationChannel> channels = new ArrayMap<>(); + Map<String, NotificationChannelGroup> groups = new ConcurrentHashMap<>(); + } +} diff --git a/services/core/java/com/android/server/notification/RankingHelper.java b/services/core/java/com/android/server/notification/RankingHelper.java index 61b5415ec7a3..f5e58ea9b27b 100644 --- a/services/core/java/com/android/server/notification/RankingHelper.java +++ b/services/core/java/com/android/server/notification/RankingHelper.java @@ -15,123 +15,37 @@ */ package com.android.server.notification; -import static android.app.NotificationManager.IMPORTANCE_NONE; - -import android.annotation.IntDef; import android.annotation.NonNull; -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationChannelGroup; import android.app.NotificationManager; import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.ParceledListSlice; -import android.metrics.LogMaker; -import android.os.Build; -import android.os.UserHandle; -import android.provider.Settings.Secure; -import android.service.notification.NotificationListenerService.Ranking; import android.service.notification.RankingHelperProto; -import android.service.notification.RankingHelperProto.RecordProto; -import android.text.TextUtils; import android.util.ArrayMap; import android.util.Slog; -import android.util.SparseBooleanArray; import android.util.proto.ProtoOutputStream; -import com.android.internal.R; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.MetricsLogger; -import com.android.internal.logging.nano.MetricsProto; -import com.android.internal.util.Preconditions; -import com.android.internal.util.XmlUtils; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlSerializer; - -import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; -public class RankingHelper implements RankingConfig { +public class RankingHelper { private static final String TAG = "RankingHelper"; - private static final int XML_VERSION = 1; - - static final String TAG_RANKING = "ranking"; - private static final String TAG_PACKAGE = "package"; - private static final String TAG_CHANNEL = "channel"; - private static final String TAG_GROUP = "channelGroup"; - - private static final String ATT_VERSION = "version"; - private static final String ATT_NAME = "name"; - private static final String ATT_UID = "uid"; - private static final String ATT_ID = "id"; - private static final String ATT_PRIORITY = "priority"; - private static final String ATT_VISIBILITY = "visibility"; - private static final String ATT_IMPORTANCE = "importance"; - private static final String ATT_SHOW_BADGE = "show_badge"; - private static final String ATT_APP_USER_LOCKED_FIELDS = "app_user_locked_fields"; - - private static final int DEFAULT_PRIORITY = Notification.PRIORITY_DEFAULT; - private static final int DEFAULT_VISIBILITY = NotificationManager.VISIBILITY_NO_OVERRIDE; - private static final int DEFAULT_IMPORTANCE = NotificationManager.IMPORTANCE_UNSPECIFIED; - private static final boolean DEFAULT_SHOW_BADGE = true; - /** - * Default value for what fields are user locked. See {@link LockableAppFields} for all lockable - * fields. - */ - private static final int DEFAULT_LOCKED_APP_FIELDS = 0; - - /** - * All user-lockable fields for a given application. - */ - @IntDef({LockableAppFields.USER_LOCKED_IMPORTANCE}) - public @interface LockableAppFields { - int USER_LOCKED_IMPORTANCE = 0x00000001; - } - private final NotificationSignalExtractor[] mSignalExtractors; private final NotificationComparator mPreliminaryComparator; private final GlobalSortKeyComparator mFinalComparator = new GlobalSortKeyComparator(); - private final ArrayMap<String, Record> mRecords = new ArrayMap<>(); // pkg|uid => Record private final ArrayMap<String, NotificationRecord> mProxyByGroupTmp = new ArrayMap<>(); - private final ArrayMap<String, Record> mRestoredWithoutUids = new ArrayMap<>(); // pkg => Record private final Context mContext; private final RankingHandler mRankingHandler; - private final PackageManager mPm; - private SparseBooleanArray mBadgingEnabled; - private boolean mAreChannelsBypassingDnd; - private ZenModeHelper mZenModeHelper; - public RankingHelper(Context context, PackageManager pm, RankingHandler rankingHandler, + public RankingHelper(Context context, RankingHandler rankingHandler, RankingConfig config, ZenModeHelper zenHelper, NotificationUsageStats usageStats, String[] extractorNames) { mContext = context; mRankingHandler = rankingHandler; - mPm = pm; - mZenModeHelper= zenHelper; - mPreliminaryComparator = new NotificationComparator(mContext); - updateBadgingEnabled(); - final int N = extractorNames.length; mSignalExtractors = new NotificationSignalExtractor[N]; for (int i = 0; i < N; i++) { @@ -140,7 +54,7 @@ public class RankingHelper implements RankingConfig { NotificationSignalExtractor extractor = (NotificationSignalExtractor) extractorClass.newInstance(); extractor.initialize(mContext, usageStats); - extractor.setConfig(this); + extractor.setConfig(config); extractor.setZenHelper(zenHelper); mSignalExtractors[i] = extractor; } catch (ClassNotFoundException e) { @@ -151,10 +65,6 @@ public class RankingHelper implements RankingConfig { Slog.w(TAG, "Problem accessing extractor " + extractorNames[i] + ".", e); } } - - mAreChannelsBypassingDnd = (mZenModeHelper.getNotificationPolicy().state & - NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND) == 1; - updateChannelsBypassingDnd(); } @SuppressWarnings("unchecked") @@ -184,279 +94,6 @@ public class RankingHelper implements RankingConfig { } } - public void readXml(XmlPullParser parser, boolean forRestore) - throws XmlPullParserException, IOException { - int type = parser.getEventType(); - if (type != XmlPullParser.START_TAG) return; - String tag = parser.getName(); - if (!TAG_RANKING.equals(tag)) return; - // Clobber groups and channels with the xml, but don't delete other data that wasn't present - // at the time of serialization. - mRestoredWithoutUids.clear(); - while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { - tag = parser.getName(); - if (type == XmlPullParser.END_TAG && TAG_RANKING.equals(tag)) { - return; - } - if (type == XmlPullParser.START_TAG) { - if (TAG_PACKAGE.equals(tag)) { - int uid = XmlUtils.readIntAttribute(parser, ATT_UID, Record.UNKNOWN_UID); - String name = parser.getAttributeValue(null, ATT_NAME); - if (!TextUtils.isEmpty(name)) { - if (forRestore) { - try { - //TODO: http://b/22388012 - uid = mPm.getPackageUidAsUser(name, UserHandle.USER_SYSTEM); - } catch (NameNotFoundException e) { - // noop - } - } - - Record r = getOrCreateRecord(name, uid, - XmlUtils.readIntAttribute( - parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE), - XmlUtils.readIntAttribute(parser, ATT_PRIORITY, DEFAULT_PRIORITY), - XmlUtils.readIntAttribute( - parser, ATT_VISIBILITY, DEFAULT_VISIBILITY), - XmlUtils.readBooleanAttribute( - parser, ATT_SHOW_BADGE, DEFAULT_SHOW_BADGE)); - r.importance = XmlUtils.readIntAttribute( - parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE); - r.priority = XmlUtils.readIntAttribute( - parser, ATT_PRIORITY, DEFAULT_PRIORITY); - r.visibility = XmlUtils.readIntAttribute( - parser, ATT_VISIBILITY, DEFAULT_VISIBILITY); - r.showBadge = XmlUtils.readBooleanAttribute( - parser, ATT_SHOW_BADGE, DEFAULT_SHOW_BADGE); - r.lockedAppFields = XmlUtils.readIntAttribute(parser, - ATT_APP_USER_LOCKED_FIELDS, DEFAULT_LOCKED_APP_FIELDS); - - final int innerDepth = parser.getDepth(); - while ((type = parser.next()) != XmlPullParser.END_DOCUMENT - && (type != XmlPullParser.END_TAG - || parser.getDepth() > innerDepth)) { - if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { - continue; - } - - String tagName = parser.getName(); - // Channel groups - if (TAG_GROUP.equals(tagName)) { - String id = parser.getAttributeValue(null, ATT_ID); - CharSequence groupName = parser.getAttributeValue(null, ATT_NAME); - if (!TextUtils.isEmpty(id)) { - NotificationChannelGroup group - = new NotificationChannelGroup(id, groupName); - group.populateFromXml(parser); - r.groups.put(id, group); - } - } - // Channels - if (TAG_CHANNEL.equals(tagName)) { - String id = parser.getAttributeValue(null, ATT_ID); - String channelName = parser.getAttributeValue(null, ATT_NAME); - int channelImportance = XmlUtils.readIntAttribute( - parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE); - if (!TextUtils.isEmpty(id) && !TextUtils.isEmpty(channelName)) { - NotificationChannel channel = new NotificationChannel(id, - channelName, channelImportance); - if (forRestore) { - channel.populateFromXmlForRestore(parser, mContext); - } else { - channel.populateFromXml(parser); - } - r.channels.put(id, channel); - } - } - } - - try { - deleteDefaultChannelIfNeeded(r); - } catch (NameNotFoundException e) { - Slog.e(TAG, "deleteDefaultChannelIfNeeded - Exception: " + e); - } - } - } - } - } - throw new IllegalStateException("Failed to reach END_DOCUMENT"); - } - - private static String recordKey(String pkg, int uid) { - return pkg + "|" + uid; - } - - private Record getRecord(String pkg, int uid) { - final String key = recordKey(pkg, uid); - synchronized (mRecords) { - return mRecords.get(key); - } - } - - private Record getOrCreateRecord(String pkg, int uid) { - return getOrCreateRecord(pkg, uid, - DEFAULT_IMPORTANCE, DEFAULT_PRIORITY, DEFAULT_VISIBILITY, DEFAULT_SHOW_BADGE); - } - - private Record getOrCreateRecord(String pkg, int uid, int importance, int priority, - int visibility, boolean showBadge) { - final String key = recordKey(pkg, uid); - synchronized (mRecords) { - Record r = (uid == Record.UNKNOWN_UID) ? mRestoredWithoutUids.get(pkg) : mRecords.get( - key); - if (r == null) { - r = new Record(); - r.pkg = pkg; - r.uid = uid; - r.importance = importance; - r.priority = priority; - r.visibility = visibility; - r.showBadge = showBadge; - - try { - createDefaultChannelIfNeeded(r); - } catch (NameNotFoundException e) { - Slog.e(TAG, "createDefaultChannelIfNeeded - Exception: " + e); - } - - if (r.uid == Record.UNKNOWN_UID) { - mRestoredWithoutUids.put(pkg, r); - } else { - mRecords.put(key, r); - } - } - return r; - } - } - - private boolean shouldHaveDefaultChannel(Record r) throws NameNotFoundException { - final int userId = UserHandle.getUserId(r.uid); - final ApplicationInfo applicationInfo = mPm.getApplicationInfoAsUser(r.pkg, 0, userId); - if (applicationInfo.targetSdkVersion >= Build.VERSION_CODES.O) { - // O apps should not have the default channel. - return false; - } - - // Otherwise, this app should have the default channel. - return true; - } - - private void deleteDefaultChannelIfNeeded(Record r) throws NameNotFoundException { - if (!r.channels.containsKey(NotificationChannel.DEFAULT_CHANNEL_ID)) { - // Not present - return; - } - - if (shouldHaveDefaultChannel(r)) { - // Keep the default channel until upgraded. - return; - } - - // Remove Default Channel. - r.channels.remove(NotificationChannel.DEFAULT_CHANNEL_ID); - } - - private void createDefaultChannelIfNeeded(Record r) throws NameNotFoundException { - if (r.channels.containsKey(NotificationChannel.DEFAULT_CHANNEL_ID)) { - r.channels.get(NotificationChannel.DEFAULT_CHANNEL_ID).setName( - mContext.getString(R.string.default_notification_channel_label)); - return; - } - - if (!shouldHaveDefaultChannel(r)) { - // Keep the default channel until upgraded. - return; - } - - // Create Default Channel - NotificationChannel channel; - channel = new NotificationChannel( - NotificationChannel.DEFAULT_CHANNEL_ID, - mContext.getString(R.string.default_notification_channel_label), - r.importance); - channel.setBypassDnd(r.priority == Notification.PRIORITY_MAX); - channel.setLockscreenVisibility(r.visibility); - if (r.importance != NotificationManager.IMPORTANCE_UNSPECIFIED) { - channel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE); - } - if (r.priority != DEFAULT_PRIORITY) { - channel.lockFields(NotificationChannel.USER_LOCKED_PRIORITY); - } - if (r.visibility != DEFAULT_VISIBILITY) { - channel.lockFields(NotificationChannel.USER_LOCKED_VISIBILITY); - } - r.channels.put(channel.getId(), channel); - } - - public void writeXml(XmlSerializer out, boolean forBackup) throws IOException { - out.startTag(null, TAG_RANKING); - out.attribute(null, ATT_VERSION, Integer.toString(XML_VERSION)); - - synchronized (mRecords) { - final int N = mRecords.size(); - for (int i = 0; i < N; i++) { - final Record r = mRecords.valueAt(i); - //TODO: http://b/22388012 - if (forBackup && UserHandle.getUserId(r.uid) != UserHandle.USER_SYSTEM) { - continue; - } - final boolean hasNonDefaultSettings = - r.importance != DEFAULT_IMPORTANCE - || r.priority != DEFAULT_PRIORITY - || r.visibility != DEFAULT_VISIBILITY - || r.showBadge != DEFAULT_SHOW_BADGE - || r.lockedAppFields != DEFAULT_LOCKED_APP_FIELDS - || r.channels.size() > 0 - || r.groups.size() > 0; - if (hasNonDefaultSettings) { - out.startTag(null, TAG_PACKAGE); - out.attribute(null, ATT_NAME, r.pkg); - if (r.importance != DEFAULT_IMPORTANCE) { - out.attribute(null, ATT_IMPORTANCE, Integer.toString(r.importance)); - } - if (r.priority != DEFAULT_PRIORITY) { - out.attribute(null, ATT_PRIORITY, Integer.toString(r.priority)); - } - if (r.visibility != DEFAULT_VISIBILITY) { - out.attribute(null, ATT_VISIBILITY, Integer.toString(r.visibility)); - } - out.attribute(null, ATT_SHOW_BADGE, Boolean.toString(r.showBadge)); - out.attribute(null, ATT_APP_USER_LOCKED_FIELDS, - Integer.toString(r.lockedAppFields)); - - if (!forBackup) { - out.attribute(null, ATT_UID, Integer.toString(r.uid)); - } - - for (NotificationChannelGroup group : r.groups.values()) { - group.writeXml(out); - } - - for (NotificationChannel channel : r.channels.values()) { - if (forBackup) { - if (!channel.isDeleted()) { - channel.writeXmlForBackup(out, mContext); - } - } else { - channel.writeXml(out); - } - } - - out.endTag(null, TAG_PACKAGE); - } - } - } - out.endTag(null, TAG_RANKING); - } - - private void updateConfig() { - final int N = mSignalExtractors.length; - for (int i = 0; i < N; i++) { - mSignalExtractors[i].setConfig(this); - } - mRankingHandler.requestSort(); - } - public void sort(ArrayList<NotificationRecord> notificationList) { final int N = notificationList.size(); // clear global sort keys @@ -521,562 +158,6 @@ public class RankingHelper implements RankingConfig { return Collections.binarySearch(notificationList, target, mFinalComparator); } - /** - * Gets importance. - */ - @Override - public int getImportance(String packageName, int uid) { - return getOrCreateRecord(packageName, uid).importance; - } - - - /** - * Returns whether the importance of the corresponding notification is user-locked and shouldn't - * be adjusted by an assistant (via means of a blocking helper, for example). For the channel - * locking field, see {@link NotificationChannel#USER_LOCKED_IMPORTANCE}. - */ - public boolean getIsAppImportanceLocked(String packageName, int uid) { - int userLockedFields = getOrCreateRecord(packageName, uid).lockedAppFields; - return (userLockedFields & LockableAppFields.USER_LOCKED_IMPORTANCE) != 0; - } - - @Override - public boolean canShowBadge(String packageName, int uid) { - return getOrCreateRecord(packageName, uid).showBadge; - } - - @Override - public void setShowBadge(String packageName, int uid, boolean showBadge) { - getOrCreateRecord(packageName, uid).showBadge = showBadge; - updateConfig(); - } - - @Override - public boolean isGroupBlocked(String packageName, int uid, String groupId) { - if (groupId == null) { - return false; - } - Record r = getOrCreateRecord(packageName, uid); - NotificationChannelGroup group = r.groups.get(groupId); - if (group == null) { - return false; - } - return group.isBlocked(); - } - - int getPackagePriority(String pkg, int uid) { - return getOrCreateRecord(pkg, uid).priority; - } - - int getPackageVisibility(String pkg, int uid) { - return getOrCreateRecord(pkg, uid).visibility; - } - - @Override - public void createNotificationChannelGroup(String pkg, int uid, NotificationChannelGroup group, - boolean fromTargetApp) { - Preconditions.checkNotNull(pkg); - Preconditions.checkNotNull(group); - Preconditions.checkNotNull(group.getId()); - Preconditions.checkNotNull(!TextUtils.isEmpty(group.getName())); - Record r = getOrCreateRecord(pkg, uid); - if (r == null) { - throw new IllegalArgumentException("Invalid package"); - } - final NotificationChannelGroup oldGroup = r.groups.get(group.getId()); - if (!group.equals(oldGroup)) { - // will log for new entries as well as name/description changes - MetricsLogger.action(getChannelGroupLog(group.getId(), pkg)); - } - if (oldGroup != null) { - group.setChannels(oldGroup.getChannels()); - - if (fromTargetApp) { - group.setBlocked(oldGroup.isBlocked()); - } - } - r.groups.put(group.getId(), group); - } - - @Override - public void createNotificationChannel(String pkg, int uid, NotificationChannel channel, - boolean fromTargetApp, boolean hasDndAccess) { - Preconditions.checkNotNull(pkg); - Preconditions.checkNotNull(channel); - Preconditions.checkNotNull(channel.getId()); - Preconditions.checkArgument(!TextUtils.isEmpty(channel.getName())); - Record r = getOrCreateRecord(pkg, uid); - if (r == null) { - throw new IllegalArgumentException("Invalid package"); - } - if (channel.getGroup() != null && !r.groups.containsKey(channel.getGroup())) { - throw new IllegalArgumentException("NotificationChannelGroup doesn't exist"); - } - if (NotificationChannel.DEFAULT_CHANNEL_ID.equals(channel.getId())) { - throw new IllegalArgumentException("Reserved id"); - } - NotificationChannel existing = r.channels.get(channel.getId()); - // Keep most of the existing settings - if (existing != null && fromTargetApp) { - if (existing.isDeleted()) { - existing.setDeleted(false); - - // log a resurrected channel as if it's new again - MetricsLogger.action(getChannelLog(channel, pkg).setType( - MetricsProto.MetricsEvent.TYPE_OPEN)); - } - - existing.setName(channel.getName().toString()); - existing.setDescription(channel.getDescription()); - existing.setBlockableSystem(channel.isBlockableSystem()); - if (existing.getGroup() == null) { - existing.setGroup(channel.getGroup()); - } - - // Apps are allowed to downgrade channel importance if the user has not changed any - // fields on this channel yet. - if (existing.getUserLockedFields() == 0 && - channel.getImportance() < existing.getImportance()) { - existing.setImportance(channel.getImportance()); - } - - // system apps and dnd access apps can bypass dnd if the user hasn't changed any - // fields on the channel yet - if (existing.getUserLockedFields() == 0 && hasDndAccess) { - boolean bypassDnd = channel.canBypassDnd(); - existing.setBypassDnd(bypassDnd); - - if (bypassDnd != mAreChannelsBypassingDnd) { - updateChannelsBypassingDnd(); - } - } - - updateConfig(); - return; - } - if (channel.getImportance() < IMPORTANCE_NONE - || channel.getImportance() > NotificationManager.IMPORTANCE_MAX) { - throw new IllegalArgumentException("Invalid importance level"); - } - - // Reset fields that apps aren't allowed to set. - if (fromTargetApp && !hasDndAccess) { - channel.setBypassDnd(r.priority == Notification.PRIORITY_MAX); - } - if (fromTargetApp) { - channel.setLockscreenVisibility(r.visibility); - } - clearLockedFields(channel); - if (channel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) { - channel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE); - } - if (!r.showBadge) { - channel.setShowBadge(false); - } - - r.channels.put(channel.getId(), channel); - if (channel.canBypassDnd() != mAreChannelsBypassingDnd) { - updateChannelsBypassingDnd(); - } - MetricsLogger.action(getChannelLog(channel, pkg).setType( - MetricsProto.MetricsEvent.TYPE_OPEN)); - } - - void clearLockedFields(NotificationChannel channel) { - channel.unlockFields(channel.getUserLockedFields()); - } - - @Override - public void updateNotificationChannel(String pkg, int uid, NotificationChannel updatedChannel, - boolean fromUser) { - Preconditions.checkNotNull(updatedChannel); - Preconditions.checkNotNull(updatedChannel.getId()); - Record r = getOrCreateRecord(pkg, uid); - if (r == null) { - throw new IllegalArgumentException("Invalid package"); - } - NotificationChannel channel = r.channels.get(updatedChannel.getId()); - if (channel == null || channel.isDeleted()) { - throw new IllegalArgumentException("Channel does not exist"); - } - if (updatedChannel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) { - updatedChannel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE); - } - if (!fromUser) { - updatedChannel.unlockFields(updatedChannel.getUserLockedFields()); - } - if (fromUser) { - updatedChannel.lockFields(channel.getUserLockedFields()); - lockFieldsForUpdate(channel, updatedChannel); - } - r.channels.put(updatedChannel.getId(), updatedChannel); - - if (NotificationChannel.DEFAULT_CHANNEL_ID.equals(updatedChannel.getId())) { - // copy settings to app level so they are inherited by new channels - // when the app migrates - r.importance = updatedChannel.getImportance(); - r.priority = updatedChannel.canBypassDnd() - ? Notification.PRIORITY_MAX : Notification.PRIORITY_DEFAULT; - r.visibility = updatedChannel.getLockscreenVisibility(); - r.showBadge = updatedChannel.canShowBadge(); - } - - if (!channel.equals(updatedChannel)) { - // only log if there are real changes - MetricsLogger.action(getChannelLog(updatedChannel, pkg)); - } - - if (updatedChannel.canBypassDnd() != mAreChannelsBypassingDnd) { - updateChannelsBypassingDnd(); - } - updateConfig(); - } - - @Override - public NotificationChannel getNotificationChannel(String pkg, int uid, String channelId, - boolean includeDeleted) { - Preconditions.checkNotNull(pkg); - Record r = getOrCreateRecord(pkg, uid); - if (r == null) { - return null; - } - if (channelId == null) { - channelId = NotificationChannel.DEFAULT_CHANNEL_ID; - } - final NotificationChannel nc = r.channels.get(channelId); - if (nc != null && (includeDeleted || !nc.isDeleted())) { - return nc; - } - return null; - } - - @Override - public void deleteNotificationChannel(String pkg, int uid, String channelId) { - Record r = getRecord(pkg, uid); - if (r == null) { - return; - } - NotificationChannel channel = r.channels.get(channelId); - if (channel != null) { - channel.setDeleted(true); - LogMaker lm = getChannelLog(channel, pkg); - lm.setType(MetricsProto.MetricsEvent.TYPE_CLOSE); - MetricsLogger.action(lm); - - if (mAreChannelsBypassingDnd && channel.canBypassDnd()) { - updateChannelsBypassingDnd(); - } - } - } - - @Override - @VisibleForTesting - public void permanentlyDeleteNotificationChannel(String pkg, int uid, String channelId) { - Preconditions.checkNotNull(pkg); - Preconditions.checkNotNull(channelId); - Record r = getRecord(pkg, uid); - if (r == null) { - return; - } - r.channels.remove(channelId); - } - - @Override - public void permanentlyDeleteNotificationChannels(String pkg, int uid) { - Preconditions.checkNotNull(pkg); - Record r = getRecord(pkg, uid); - if (r == null) { - return; - } - int N = r.channels.size() - 1; - for (int i = N; i >= 0; i--) { - String key = r.channels.keyAt(i); - if (!NotificationChannel.DEFAULT_CHANNEL_ID.equals(key)) { - r.channels.remove(key); - } - } - } - - public NotificationChannelGroup getNotificationChannelGroupWithChannels(String pkg, - int uid, String groupId, boolean includeDeleted) { - Preconditions.checkNotNull(pkg); - Record r = getRecord(pkg, uid); - if (r == null || groupId == null || !r.groups.containsKey(groupId)) { - return null; - } - NotificationChannelGroup group = r.groups.get(groupId).clone(); - group.setChannels(new ArrayList<>()); - int N = r.channels.size(); - for (int i = 0; i < N; i++) { - final NotificationChannel nc = r.channels.valueAt(i); - if (includeDeleted || !nc.isDeleted()) { - if (groupId.equals(nc.getGroup())) { - group.addChannel(nc); - } - } - } - return group; - } - - public NotificationChannelGroup getNotificationChannelGroup(String groupId, String pkg, - int uid) { - Preconditions.checkNotNull(pkg); - Record r = getRecord(pkg, uid); - if (r == null) { - return null; - } - return r.groups.get(groupId); - } - - @Override - public ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroups(String pkg, - int uid, boolean includeDeleted, boolean includeNonGrouped) { - Preconditions.checkNotNull(pkg); - Map<String, NotificationChannelGroup> groups = new ArrayMap<>(); - Record r = getRecord(pkg, uid); - if (r == null) { - return ParceledListSlice.emptyList(); - } - NotificationChannelGroup nonGrouped = new NotificationChannelGroup(null, null); - int N = r.channels.size(); - for (int i = 0; i < N; i++) { - final NotificationChannel nc = r.channels.valueAt(i); - if (includeDeleted || !nc.isDeleted()) { - if (nc.getGroup() != null) { - if (r.groups.get(nc.getGroup()) != null) { - NotificationChannelGroup ncg = groups.get(nc.getGroup()); - if (ncg == null) { - ncg = r.groups.get(nc.getGroup()).clone(); - ncg.setChannels(new ArrayList<>()); - groups.put(nc.getGroup(), ncg); - - } - ncg.addChannel(nc); - } - } else { - nonGrouped.addChannel(nc); - } - } - } - if (includeNonGrouped && nonGrouped.getChannels().size() > 0) { - groups.put(null, nonGrouped); - } - return new ParceledListSlice<>(new ArrayList<>(groups.values())); - } - - public List<NotificationChannel> deleteNotificationChannelGroup(String pkg, int uid, - String groupId) { - List<NotificationChannel> deletedChannels = new ArrayList<>(); - Record r = getRecord(pkg, uid); - if (r == null || TextUtils.isEmpty(groupId)) { - return deletedChannels; - } - - r.groups.remove(groupId); - - int N = r.channels.size(); - for (int i = 0; i < N; i++) { - final NotificationChannel nc = r.channels.valueAt(i); - if (groupId.equals(nc.getGroup())) { - nc.setDeleted(true); - deletedChannels.add(nc); - } - } - return deletedChannels; - } - - @Override - public Collection<NotificationChannelGroup> getNotificationChannelGroups(String pkg, - int uid) { - Record r = getRecord(pkg, uid); - if (r == null) { - return new ArrayList<>(); - } - return r.groups.values(); - } - - @Override - public ParceledListSlice<NotificationChannel> getNotificationChannels(String pkg, int uid, - boolean includeDeleted) { - Preconditions.checkNotNull(pkg); - List<NotificationChannel> channels = new ArrayList<>(); - Record r = getRecord(pkg, uid); - if (r == null) { - return ParceledListSlice.emptyList(); - } - int N = r.channels.size(); - for (int i = 0; i < N; i++) { - final NotificationChannel nc = r.channels.valueAt(i); - if (includeDeleted || !nc.isDeleted()) { - channels.add(nc); - } - } - return new ParceledListSlice<>(channels); - } - - /** - * True for pre-O apps that only have the default channel, or pre O apps that have no - * channels yet. This method will create the default channel for pre-O apps that don't have it. - * Should never be true for O+ targeting apps, but that's enforced on boot/when an app - * upgrades. - */ - public boolean onlyHasDefaultChannel(String pkg, int uid) { - Record r = getOrCreateRecord(pkg, uid); - if (r.channels.size() == 1 - && r.channels.containsKey(NotificationChannel.DEFAULT_CHANNEL_ID)) { - return true; - } - return false; - } - - public int getDeletedChannelCount(String pkg, int uid) { - Preconditions.checkNotNull(pkg); - int deletedCount = 0; - Record r = getRecord(pkg, uid); - if (r == null) { - return deletedCount; - } - int N = r.channels.size(); - for (int i = 0; i < N; i++) { - final NotificationChannel nc = r.channels.valueAt(i); - if (nc.isDeleted()) { - deletedCount++; - } - } - return deletedCount; - } - - public int getBlockedChannelCount(String pkg, int uid) { - Preconditions.checkNotNull(pkg); - int blockedCount = 0; - Record r = getRecord(pkg, uid); - if (r == null) { - return blockedCount; - } - int N = r.channels.size(); - for (int i = 0; i < N; i++) { - final NotificationChannel nc = r.channels.valueAt(i); - if (!nc.isDeleted() && IMPORTANCE_NONE == nc.getImportance()) { - blockedCount++; - } - } - return blockedCount; - } - - public int getBlockedAppCount(int userId) { - int count = 0; - synchronized (mRecords) { - final int N = mRecords.size(); - for (int i = 0; i < N; i++) { - final Record r = mRecords.valueAt(i); - if (userId == UserHandle.getUserId(r.uid) - && r.importance == IMPORTANCE_NONE) { - count++; - } - } - } - return count; - } - - public void updateChannelsBypassingDnd() { - synchronized (mRecords) { - final int numRecords = mRecords.size(); - for (int recordIndex = 0; recordIndex < numRecords; recordIndex++) { - final Record r = mRecords.valueAt(recordIndex); - final int numChannels = r.channels.size(); - - for (int channelIndex = 0; channelIndex < numChannels; channelIndex++) { - NotificationChannel channel = r.channels.valueAt(channelIndex); - if (!channel.isDeleted() && channel.canBypassDnd()) { - if (!mAreChannelsBypassingDnd) { - mAreChannelsBypassingDnd = true; - updateZenPolicy(true); - } - return; - } - } - } - } - - if (mAreChannelsBypassingDnd) { - mAreChannelsBypassingDnd = false; - updateZenPolicy(false); - } - } - - public void updateZenPolicy(boolean areChannelsBypassingDnd) { - NotificationManager.Policy policy = mZenModeHelper.getNotificationPolicy(); - mZenModeHelper.setNotificationPolicy(new NotificationManager.Policy( - policy.priorityCategories, policy.priorityCallSenders, - policy.priorityMessageSenders, policy.suppressedVisualEffects, - (areChannelsBypassingDnd ? NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND - : 0))); - } - - public boolean areChannelsBypassingDnd() { - return mAreChannelsBypassingDnd; - } - - /** - * Sets importance. - */ - @Override - public void setImportance(String pkgName, int uid, int importance) { - getOrCreateRecord(pkgName, uid).importance = importance; - updateConfig(); - } - - public void setEnabled(String packageName, int uid, boolean enabled) { - boolean wasEnabled = getImportance(packageName, uid) != IMPORTANCE_NONE; - if (wasEnabled == enabled) { - return; - } - setImportance(packageName, uid, - enabled ? DEFAULT_IMPORTANCE : IMPORTANCE_NONE); - } - - /** - * Sets whether any notifications from the app, represented by the given {@code pkgName} and - * {@code uid}, have their importance locked by the user. Locked notifications don't get - * considered for sentiment adjustments (and thus never show a blocking helper). - */ - public void setAppImportanceLocked(String packageName, int uid) { - Record record = getOrCreateRecord(packageName, uid); - if ((record.lockedAppFields & LockableAppFields.USER_LOCKED_IMPORTANCE) != 0) { - return; - } - - record.lockedAppFields = record.lockedAppFields | LockableAppFields.USER_LOCKED_IMPORTANCE; - updateConfig(); - } - - @VisibleForTesting - void lockFieldsForUpdate(NotificationChannel original, NotificationChannel update) { - if (original.canBypassDnd() != update.canBypassDnd()) { - update.lockFields(NotificationChannel.USER_LOCKED_PRIORITY); - } - if (original.getLockscreenVisibility() != update.getLockscreenVisibility()) { - update.lockFields(NotificationChannel.USER_LOCKED_VISIBILITY); - } - if (original.getImportance() != update.getImportance()) { - update.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE); - } - if (original.shouldShowLights() != update.shouldShowLights() - || original.getLightColor() != update.getLightColor()) { - update.lockFields(NotificationChannel.USER_LOCKED_LIGHTS); - } - if (!Objects.equals(original.getSound(), update.getSound())) { - update.lockFields(NotificationChannel.USER_LOCKED_SOUND); - } - if (!Arrays.equals(original.getVibrationPattern(), update.getVibrationPattern()) - || original.shouldVibrate() != update.shouldVibrate()) { - update.lockFields(NotificationChannel.USER_LOCKED_VIBRATION); - } - if (original.canShowBadge() != update.canShowBadge()) { - update.lockFields(NotificationChannel.USER_LOCKED_SHOW_BADGE); - } - } - public void dump(PrintWriter pw, String prefix, @NonNull NotificationManagerService.DumpFilter filter) { final int N = mSignalExtractors.length; @@ -1088,16 +169,6 @@ public class RankingHelper implements RankingConfig { pw.print(" "); pw.println(mSignalExtractors[i].getClass().getSimpleName()); } - - pw.print(prefix); - pw.println("per-package config:"); - - pw.println("Records:"); - synchronized (mRecords) { - dumpRecords(pw, prefix, filter, mRecords); - } - pw.println("Restored without uid:"); - dumpRecords(pw, prefix, filter, mRestoredWithoutUids); } public void dump(ProtoOutputStream proto, @@ -1105,375 +176,7 @@ public class RankingHelper implements RankingConfig { final int N = mSignalExtractors.length; for (int i = 0; i < N; i++) { proto.write(RankingHelperProto.NOTIFICATION_SIGNAL_EXTRACTORS, - mSignalExtractors[i].getClass().getSimpleName()); + mSignalExtractors[i].getClass().getSimpleName()); } - synchronized (mRecords) { - dumpRecords(proto, RankingHelperProto.RECORDS, filter, mRecords); - } - dumpRecords(proto, RankingHelperProto.RECORDS_RESTORED_WITHOUT_UID, filter, - mRestoredWithoutUids); } - - private static void dumpRecords(ProtoOutputStream proto, long fieldId, - @NonNull NotificationManagerService.DumpFilter filter, - ArrayMap<String, Record> records) { - final int N = records.size(); - long fToken; - for (int i = 0; i < N; i++) { - final Record r = records.valueAt(i); - if (filter.matches(r.pkg)) { - fToken = proto.start(fieldId); - - proto.write(RecordProto.PACKAGE, r.pkg); - proto.write(RecordProto.UID, r.uid); - proto.write(RecordProto.IMPORTANCE, r.importance); - proto.write(RecordProto.PRIORITY, r.priority); - proto.write(RecordProto.VISIBILITY, r.visibility); - proto.write(RecordProto.SHOW_BADGE, r.showBadge); - - for (NotificationChannel channel : r.channels.values()) { - channel.writeToProto(proto, RecordProto.CHANNELS); - } - for (NotificationChannelGroup group : r.groups.values()) { - group.writeToProto(proto, RecordProto.CHANNEL_GROUPS); - } - - proto.end(fToken); - } - } - } - - private static void dumpRecords(PrintWriter pw, String prefix, - @NonNull NotificationManagerService.DumpFilter filter, - ArrayMap<String, Record> records) { - final int N = records.size(); - for (int i = 0; i < N; i++) { - final Record r = records.valueAt(i); - if (filter.matches(r.pkg)) { - pw.print(prefix); - pw.print(" AppSettings: "); - pw.print(r.pkg); - pw.print(" ("); - pw.print(r.uid == Record.UNKNOWN_UID ? "UNKNOWN_UID" : Integer.toString(r.uid)); - pw.print(')'); - if (r.importance != DEFAULT_IMPORTANCE) { - pw.print(" importance="); - pw.print(Ranking.importanceToString(r.importance)); - } - if (r.priority != DEFAULT_PRIORITY) { - pw.print(" priority="); - pw.print(Notification.priorityToString(r.priority)); - } - if (r.visibility != DEFAULT_VISIBILITY) { - pw.print(" visibility="); - pw.print(Notification.visibilityToString(r.visibility)); - } - pw.print(" showBadge="); - pw.print(Boolean.toString(r.showBadge)); - pw.println(); - for (NotificationChannel channel : r.channels.values()) { - pw.print(prefix); - pw.print(" "); - pw.print(" "); - pw.println(channel); - } - for (NotificationChannelGroup group : r.groups.values()) { - pw.print(prefix); - pw.print(" "); - pw.print(" "); - pw.println(group); - } - } - } - } - - public JSONObject dumpJson(NotificationManagerService.DumpFilter filter) { - JSONObject ranking = new JSONObject(); - JSONArray records = new JSONArray(); - try { - ranking.put("noUid", mRestoredWithoutUids.size()); - } catch (JSONException e) { - // pass - } - synchronized (mRecords) { - final int N = mRecords.size(); - for (int i = 0; i < N; i++) { - final Record r = mRecords.valueAt(i); - if (filter == null || filter.matches(r.pkg)) { - JSONObject record = new JSONObject(); - try { - record.put("userId", UserHandle.getUserId(r.uid)); - record.put("packageName", r.pkg); - if (r.importance != DEFAULT_IMPORTANCE) { - record.put("importance", Ranking.importanceToString(r.importance)); - } - if (r.priority != DEFAULT_PRIORITY) { - record.put("priority", Notification.priorityToString(r.priority)); - } - if (r.visibility != DEFAULT_VISIBILITY) { - record.put("visibility", Notification.visibilityToString(r.visibility)); - } - if (r.showBadge != DEFAULT_SHOW_BADGE) { - record.put("showBadge", Boolean.valueOf(r.showBadge)); - } - JSONArray channels = new JSONArray(); - for (NotificationChannel channel : r.channels.values()) { - channels.put(channel.toJson()); - } - record.put("channels", channels); - JSONArray groups = new JSONArray(); - for (NotificationChannelGroup group : r.groups.values()) { - groups.put(group.toJson()); - } - record.put("groups", groups); - } catch (JSONException e) { - // pass - } - records.put(record); - } - } - } - try { - ranking.put("records", records); - } catch (JSONException e) { - // pass - } - return ranking; - } - - /** - * Dump only the ban information as structured JSON for the stats collector. - * - * This is intentionally redundant with {#link dumpJson} because the old - * scraper will expect this format. - * - * @param filter - * @return - */ - public JSONArray dumpBansJson(NotificationManagerService.DumpFilter filter) { - JSONArray bans = new JSONArray(); - Map<Integer, String> packageBans = getPackageBans(); - for(Entry<Integer, String> ban : packageBans.entrySet()) { - final int userId = UserHandle.getUserId(ban.getKey()); - final String packageName = ban.getValue(); - if (filter == null || filter.matches(packageName)) { - JSONObject banJson = new JSONObject(); - try { - banJson.put("userId", userId); - banJson.put("packageName", packageName); - } catch (JSONException e) { - e.printStackTrace(); - } - bans.put(banJson); - } - } - return bans; - } - - public Map<Integer, String> getPackageBans() { - synchronized (mRecords) { - final int N = mRecords.size(); - ArrayMap<Integer, String> packageBans = new ArrayMap<>(N); - for (int i = 0; i < N; i++) { - final Record r = mRecords.valueAt(i); - if (r.importance == IMPORTANCE_NONE) { - packageBans.put(r.uid, r.pkg); - } - } - - return packageBans; - } - } - - /** - * Dump only the channel information as structured JSON for the stats collector. - * - * This is intentionally redundant with {#link dumpJson} because the old - * scraper will expect this format. - * - * @param filter - * @return - */ - public JSONArray dumpChannelsJson(NotificationManagerService.DumpFilter filter) { - JSONArray channels = new JSONArray(); - Map<String, Integer> packageChannels = getPackageChannels(); - for(Entry<String, Integer> channelCount : packageChannels.entrySet()) { - final String packageName = channelCount.getKey(); - if (filter == null || filter.matches(packageName)) { - JSONObject channelCountJson = new JSONObject(); - try { - channelCountJson.put("packageName", packageName); - channelCountJson.put("channelCount", channelCount.getValue()); - } catch (JSONException e) { - e.printStackTrace(); - } - channels.put(channelCountJson); - } - } - return channels; - } - - private Map<String, Integer> getPackageChannels() { - ArrayMap<String, Integer> packageChannels = new ArrayMap<>(); - synchronized (mRecords) { - for (int i = 0; i < mRecords.size(); i++) { - final Record r = mRecords.valueAt(i); - int channelCount = 0; - for (int j = 0; j < r.channels.size(); j++) { - if (!r.channels.valueAt(j).isDeleted()) { - channelCount++; - } - } - packageChannels.put(r.pkg, channelCount); - } - } - return packageChannels; - } - - public void onUserRemoved(int userId) { - synchronized (mRecords) { - int N = mRecords.size(); - for (int i = N - 1; i >= 0 ; i--) { - Record record = mRecords.valueAt(i); - if (UserHandle.getUserId(record.uid) == userId) { - mRecords.removeAt(i); - } - } - } - } - - protected void onLocaleChanged(Context context, int userId) { - synchronized (mRecords) { - int N = mRecords.size(); - for (int i = 0; i < N; i++) { - Record record = mRecords.valueAt(i); - if (UserHandle.getUserId(record.uid) == userId) { - if (record.channels.containsKey(NotificationChannel.DEFAULT_CHANNEL_ID)) { - record.channels.get(NotificationChannel.DEFAULT_CHANNEL_ID).setName( - context.getResources().getString( - R.string.default_notification_channel_label)); - } - } - } - } - } - - public void onPackagesChanged(boolean removingPackage, int changeUserId, String[] pkgList, - int[] uidList) { - if (pkgList == null || pkgList.length == 0) { - return; // nothing to do - } - boolean updated = false; - if (removingPackage) { - // Remove notification settings for uninstalled package - int size = Math.min(pkgList.length, uidList.length); - for (int i = 0; i < size; i++) { - final String pkg = pkgList[i]; - final int uid = uidList[i]; - synchronized (mRecords) { - mRecords.remove(recordKey(pkg, uid)); - } - mRestoredWithoutUids.remove(pkg); - updated = true; - } - } else { - for (String pkg : pkgList) { - // Package install - final Record r = mRestoredWithoutUids.get(pkg); - if (r != null) { - try { - r.uid = mPm.getPackageUidAsUser(r.pkg, changeUserId); - mRestoredWithoutUids.remove(pkg); - synchronized (mRecords) { - mRecords.put(recordKey(r.pkg, r.uid), r); - } - updated = true; - } catch (NameNotFoundException e) { - // noop - } - } - // Package upgrade - try { - Record fullRecord = getRecord(pkg, - mPm.getPackageUidAsUser(pkg, changeUserId)); - if (fullRecord != null) { - createDefaultChannelIfNeeded(fullRecord); - deleteDefaultChannelIfNeeded(fullRecord); - } - } catch (NameNotFoundException e) {} - } - } - - if (updated) { - updateConfig(); - } - } - - private LogMaker getChannelLog(NotificationChannel channel, String pkg) { - return new LogMaker(MetricsProto.MetricsEvent.ACTION_NOTIFICATION_CHANNEL) - .setType(MetricsProto.MetricsEvent.TYPE_UPDATE) - .setPackageName(pkg) - .addTaggedData(MetricsProto.MetricsEvent.FIELD_NOTIFICATION_CHANNEL_ID, - channel.getId()) - .addTaggedData(MetricsProto.MetricsEvent.FIELD_NOTIFICATION_CHANNEL_IMPORTANCE, - channel.getImportance()); - } - - private LogMaker getChannelGroupLog(String groupId, String pkg) { - return new LogMaker(MetricsProto.MetricsEvent.ACTION_NOTIFICATION_CHANNEL_GROUP) - .setType(MetricsProto.MetricsEvent.TYPE_UPDATE) - .addTaggedData(MetricsProto.MetricsEvent.FIELD_NOTIFICATION_CHANNEL_GROUP_ID, - groupId) - .setPackageName(pkg); - } - - public void updateBadgingEnabled() { - if (mBadgingEnabled == null) { - mBadgingEnabled = new SparseBooleanArray(); - } - boolean changed = false; - // update the cached values - for (int index = 0; index < mBadgingEnabled.size(); index++) { - int userId = mBadgingEnabled.keyAt(index); - final boolean oldValue = mBadgingEnabled.get(userId); - final boolean newValue = Secure.getIntForUser(mContext.getContentResolver(), - Secure.NOTIFICATION_BADGING, - DEFAULT_SHOW_BADGE ? 1 : 0, userId) != 0; - mBadgingEnabled.put(userId, newValue); - changed |= oldValue != newValue; - } - if (changed) { - updateConfig(); - } - } - - public boolean badgingEnabled(UserHandle userHandle) { - int userId = userHandle.getIdentifier(); - if (userId == UserHandle.USER_ALL) { - return false; - } - if (mBadgingEnabled.indexOfKey(userId) < 0) { - mBadgingEnabled.put(userId, - Secure.getIntForUser(mContext.getContentResolver(), - Secure.NOTIFICATION_BADGING, - DEFAULT_SHOW_BADGE ? 1 : 0, userId) != 0); - } - return mBadgingEnabled.get(userId, DEFAULT_SHOW_BADGE); - } - - - private static class Record { - static int UNKNOWN_UID = UserHandle.USER_NULL; - - String pkg; - int uid = UNKNOWN_UID; - int importance = DEFAULT_IMPORTANCE; - int priority = DEFAULT_PRIORITY; - int visibility = DEFAULT_VISIBILITY; - boolean showBadge = DEFAULT_SHOW_BADGE; - int lockedAppFields = DEFAULT_LOCKED_APP_FIELDS; - - ArrayMap<String, NotificationChannel> channels = new ArrayMap<>(); - Map<String, NotificationChannelGroup> groups = new ConcurrentHashMap<>(); - } } diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index fa934fe23fff..b92d52cb870a 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -685,13 +685,14 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { // inserted above to hold the session active. try { final Int64Ref last = new Int64Ref(0); - FileUtils.copy(incomingFd.getFileDescriptor(), targetFd, (long progress) -> { - if (params.sizeBytes > 0) { - final long delta = progress - last.value; - last.value = progress; - addClientProgress((float) delta / (float) params.sizeBytes); - } - }, null, lengthBytes); + FileUtils.copy(incomingFd.getFileDescriptor(), targetFd, lengthBytes, null, + Runnable::run, (long progress) -> { + if (params.sizeBytes > 0) { + final long delta = progress - last.value; + last.value = progress; + addClientProgress((float) delta / (float) params.sizeBytes); + } + }); } finally { IoUtils.closeQuietly(targetFd); IoUtils.closeQuietly(incomingFd); @@ -930,6 +931,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @GuardedBy("mLock") private void commitLocked() throws PackageManagerException { + if (mRelinquished) { + Slog.d(TAG, "Ignoring commit after previous commit relinquished control"); + return; + } if (mDestroyed) { throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, "Session destroyed"); } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 734a872b0fcc..3d5d079fc8e1 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -2568,6 +2568,8 @@ public class PackageManagerService extends IPackageManager.Stub mIsPreNMR1Upgrade = mIsUpgrade && ver.sdkVersion < Build.VERSION_CODES.N_MR1; + int preUpgradeSdkVersion = ver.sdkVersion; + // save off the names of pre-existing system packages prior to scanning; we don't // want to automatically grant runtime permissions for new system apps if (mPromoteSystemApps) { @@ -3161,6 +3163,58 @@ public class PackageManagerService extends IPackageManager.Stub checkDefaultBrowser(); + // If a granted permission is split, all new permissions should be granted too + if (mIsUpgrade) { + final int callingUid = getCallingUid(); + + final int numSplitPerms = PackageParser.SPLIT_PERMISSIONS.length; + for (int splitPermNum = 0; splitPermNum < numSplitPerms; splitPermNum++) { + final PackageParser.SplitPermissionInfo splitPerm = + PackageParser.SPLIT_PERMISSIONS[splitPermNum]; + final String rootPerm = splitPerm.rootPerm; + + if (preUpgradeSdkVersion >= splitPerm.targetSdk) { + continue; + } + + final int numPackages = mPackages.size(); + for (int packageNum = 0; packageNum < numPackages; packageNum++) { + final PackageParser.Package pkg = mPackages.valueAt(packageNum); + + if (pkg.applicationInfo.targetSdkVersion >= splitPerm.targetSdk + || !pkg.requestedPermissions.contains(rootPerm)) { + continue; + } + + final int userId = UserHandle.getUserId(pkg.applicationInfo.uid); + final String pkgName = pkg.packageName; + + if (checkPermission(rootPerm, pkgName, userId) == PERMISSION_DENIED) { + continue; + } + + final String[] newPerms = splitPerm.newPerms; + + final int numNewPerms = newPerms.length; + for (int newPermNum = 0; newPermNum < numNewPerms; newPermNum++) { + final String newPerm = newPerms[newPermNum]; + if (checkPermission(newPerm, pkgName, userId) == PERMISSION_GRANTED) { + continue; + } + + if (DEBUG_PERMISSIONS) { + Slog.v(TAG, "Granting " + newPerm + " to " + pkgName + + " as the root permission " + rootPerm + + " is already granted"); + } + + mPermissionManager.grantRuntimePermission(newPerm, pkgName, true, + callingUid, userId, null); + } + } + } + } + // clear only after permissions and other defaults have been updated mExistingSystemPackages.clear(); mPromoteSystemApps = false; 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 1ae59cbea452..50e6f8d5b905 100644 --- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java +++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java @@ -1193,12 +1193,27 @@ public final class DefaultPermissionGrantPolicy { } } - private void grantRuntimePermissions(PackageParser.Package pkg, Set<String> permissions, - boolean systemFixed, boolean ignoreSystemPackage, int userId) { + private void grantRuntimePermissions(PackageParser.Package pkg, + Set<String> permissionsWithoutSplits, boolean systemFixed, boolean ignoreSystemPackage, + int userId) { if (pkg.requestedPermissions.isEmpty()) { return; } + final ArraySet<String> permissions = new ArraySet<>(permissionsWithoutSplits); + + // Automatically attempt to grant split permissions to older APKs + final int numSplitPerms = PackageParser.SPLIT_PERMISSIONS.length; + for (int splitPermNum = 0; splitPermNum < numSplitPerms; splitPermNum++) { + final PackageParser.SplitPermissionInfo splitPerm = + PackageParser.SPLIT_PERMISSIONS[splitPermNum]; + + if (pkg.applicationInfo.targetSdkVersion < splitPerm.targetSdk + && permissionsWithoutSplits.contains(splitPerm.rootPerm)) { + Collections.addAll(permissions, splitPerm.newPerms); + } + } + List<String> requestedPermissions = pkg.requestedPermissions; Set<String> grantablePermissions = null; diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 5bc35e7296c3..e6195b47a586 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -290,6 +290,7 @@ import com.android.server.wm.AppTransition; import com.android.server.wm.DisplayFrames; import com.android.server.wm.WindowManagerInternal; import com.android.server.wm.WindowManagerInternal.AppTransitionListener; +import com.android.server.wm.utils.InsetUtils; import java.io.File; import java.io.FileReader; @@ -4536,16 +4537,15 @@ public class PhoneWindowManager implements WindowManagerPolicy { @Override // TODO: Should probably be moved into DisplayFrames. - public boolean getLayoutHintLw(WindowManager.LayoutParams attrs, Rect taskBounds, - DisplayFrames displayFrames, Rect outFrame, Rect outContentInsets, Rect outStableInsets, + public boolean getLayoutHintLw(LayoutParams attrs, Rect taskBounds, + DisplayFrames displayFrames, boolean floatingStack, Rect outFrame, + Rect outContentInsets, Rect outStableInsets, Rect outOutsets, DisplayCutout.ParcelableWrapper outDisplayCutout) { final int fl = PolicyControl.getWindowFlags(null, attrs); final int pfl = attrs.privateFlags; final int requestedSysUiVis = PolicyControl.getSystemUiVisibility(null, attrs); final int sysUiVis = requestedSysUiVis | getImpliedSysUiFlagsForLayout(attrs); final int displayRotation = displayFrames.mRotation; - final int displayWidth = displayFrames.mDisplayWidth; - final int displayHeight = displayFrames.mDisplayHeight; final boolean useOutsets = outOutsets != null && shouldUseOutsets(attrs, fl); if (useOutsets) { @@ -4569,45 +4569,40 @@ public class PhoneWindowManager implements WindowManagerPolicy { final boolean screenDecor = (pfl & PRIVATE_FLAG_IS_SCREEN_DECOR) != 0; if (layoutInScreenAndInsetDecor && !screenDecor) { - int availRight, availBottom; if (canHideNavigationBar() && (sysUiVis & View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) != 0) { outFrame.set(displayFrames.mUnrestricted); - availRight = displayFrames.mUnrestricted.right; - availBottom = displayFrames.mUnrestricted.bottom; } else { outFrame.set(displayFrames.mRestricted); - availRight = displayFrames.mRestricted.right; - availBottom = displayFrames.mRestricted.bottom; } - outStableInsets.set(displayFrames.mStable.left, displayFrames.mStable.top, - availRight - displayFrames.mStable.right, - availBottom - displayFrames.mStable.bottom); - if ((sysUiVis & View.SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0) { + final Rect sf; + if (floatingStack) { + sf = null; + } else { + sf = displayFrames.mStable; + } + + final Rect cf; + if (floatingStack) { + cf = null; + } else if ((sysUiVis & View.SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0) { if ((fl & FLAG_FULLSCREEN) != 0) { - outContentInsets.set(displayFrames.mStableFullscreen.left, - displayFrames.mStableFullscreen.top, - availRight - displayFrames.mStableFullscreen.right, - availBottom - displayFrames.mStableFullscreen.bottom); + cf = displayFrames.mStableFullscreen; } else { - outContentInsets.set(outStableInsets); + cf = displayFrames.mStable; } } else if ((fl & FLAG_FULLSCREEN) != 0 || (fl & FLAG_LAYOUT_IN_OVERSCAN) != 0) { - outContentInsets.setEmpty(); + cf = displayFrames.mOverscan; } else { - outContentInsets.set(displayFrames.mCurrent.left, displayFrames.mCurrent.top, - availRight - displayFrames.mCurrent.right, - availBottom - displayFrames.mCurrent.bottom); + cf = displayFrames.mCurrent; } if (taskBounds != null) { - calculateRelevantTaskInsets(taskBounds, outContentInsets, - displayWidth, displayHeight); - calculateRelevantTaskInsets(taskBounds, outStableInsets, - displayWidth, displayHeight); outFrame.intersect(taskBounds); } + InsetUtils.insetsBetweenFrames(outFrame, cf, outContentInsets); + InsetUtils.insetsBetweenFrames(outFrame, sf, outStableInsets); outDisplayCutout.set(displayFrames.mDisplayCutout.calculateRelativeTo(outFrame) .getDisplayCutout()); return mForceShowSystemBars; @@ -4628,22 +4623,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } - /** - * For any given task bounds, the insets relevant for these bounds given the insets relevant - * for the entire display. - */ - private void calculateRelevantTaskInsets(Rect taskBounds, Rect inOutInsets, int displayWidth, - int displayHeight) { - mTmpRect.set(0, 0, displayWidth, displayHeight); - mTmpRect.inset(inOutInsets); - mTmpRect.intersect(taskBounds); - int leftInset = mTmpRect.left - taskBounds.left; - int topInset = mTmpRect.top - taskBounds.top; - int rightInset = taskBounds.right - mTmpRect.right; - int bottomInset = taskBounds.bottom - mTmpRect.bottom; - inOutInsets.set(leftInset, topInset, rightInset, bottomInset); - } - private boolean shouldUseOutsets(WindowManager.LayoutParams attrs, int fl) { return attrs.type == TYPE_WALLPAPER || (fl & (WindowManager.LayoutParams.FLAG_FULLSCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_IN_OVERSCAN)) != 0; diff --git a/services/core/java/com/android/server/policy/WindowManagerPolicy.java b/services/core/java/com/android/server/policy/WindowManagerPolicy.java index 9fbe4194049b..1ebbe3ac36fa 100644 --- a/services/core/java/com/android/server/policy/WindowManagerPolicy.java +++ b/services/core/java/com/android/server/policy/WindowManagerPolicy.java @@ -71,7 +71,6 @@ import android.content.Context; import android.content.pm.ActivityInfo; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; -import android.graphics.Point; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; @@ -1180,6 +1179,7 @@ public interface WindowManagerPolicy extends WindowManagerPolicyConstants { * @param taskBounds The bounds of the task this window is on or {@code null} if no task is * associated with the window. * @param displayFrames display frames. + * @param floatingStack Whether the window's stack is floating. * @param outFrame The frame of the window. * @param outContentInsets The areas covered by system windows, expressed as positive insets. * @param outStableInsets The areas covered by stable system windows irrespective of their @@ -1190,8 +1190,8 @@ public interface WindowManagerPolicy extends WindowManagerPolicyConstants { * See {@link #isNavBarForcedShownLw(WindowState)}. */ default boolean getLayoutHintLw(WindowManager.LayoutParams attrs, Rect taskBounds, - DisplayFrames displayFrames, Rect outFrame, Rect outContentInsets, - Rect outStableInsets, Rect outOutsets, + DisplayFrames displayFrames, boolean floatingStack, + Rect outFrame, Rect outContentInsets, Rect outStableInsets, Rect outOutsets, DisplayCutout.ParcelableWrapper outDisplayCutout) { return false; } diff --git a/services/core/java/com/android/server/stats/StatsCompanionService.java b/services/core/java/com/android/server/stats/StatsCompanionService.java index 4d6544034cef..c7c24a562696 100644 --- a/services/core/java/com/android/server/stats/StatsCompanionService.java +++ b/services/core/java/com/android/server/stats/StatsCompanionService.java @@ -57,11 +57,14 @@ import android.os.UserHandle; import android.os.UserManager; import android.telephony.ModemActivityInfo; import android.telephony.TelephonyManager; +import android.util.ArrayMap; import android.util.Slog; import android.util.StatsLog; import com.android.internal.annotations.GuardedBy; import com.android.internal.net.NetworkStatsFactory; +import com.android.internal.os.BinderCallsStats; +import com.android.internal.os.BinderCallsStats.ExportedCallStat; import com.android.internal.os.KernelCpuSpeedReader; import com.android.internal.os.KernelUidCpuTimeReader; import com.android.internal.os.KernelUidCpuClusterTimeReader; @@ -891,6 +894,26 @@ public class StatsCompanionService extends IStatsCompanionService.Stub { } } + private void pullBinderCallsStats(int tagId, List<StatsLogEventWrapper> pulledData) { + List<ExportedCallStat> callStats = BinderCallsStats.getInstance().getExportedCallStats(); + long elapsedNanos = SystemClock.elapsedRealtimeNanos(); + for (ExportedCallStat callStat : callStats) { + StatsLogEventWrapper e = new StatsLogEventWrapper(elapsedNanos, tagId, 11 /* fields */); + e.writeInt(callStat.uid); + e.writeString(callStat.className); + e.writeString(callStat.methodName); + e.writeLong(callStat.callCount); + e.writeLong(callStat.exceptionCount); + e.writeLong(callStat.latencyMicros); + e.writeLong(callStat.maxLatencyMicros); + e.writeLong(callStat.cpuTimeMicros); + e.writeLong(callStat.maxCpuTimeMicros); + e.writeLong(callStat.maxReplySizeBytes); + e.writeLong(callStat.maxRequestSizeBytes); + pulledData.add(e); + } + } + /** * Pulls various data. */ @@ -973,6 +996,10 @@ public class StatsCompanionService extends IStatsCompanionService.Stub { pullProcessMemoryState(tagId, ret); break; } + case StatsLog.BINDER_CALLS: { + pullBinderCallsStats(tagId, ret); + break; + } default: Slog.w(TAG, "No such tagId data as " + tagId); return null; diff --git a/services/core/java/com/android/server/wm/SurfaceAnimationRunner.java b/services/core/java/com/android/server/wm/SurfaceAnimationRunner.java index 2bfff269e457..cb504607420c 100644 --- a/services/core/java/com/android/server/wm/SurfaceAnimationRunner.java +++ b/services/core/java/com/android/server/wm/SurfaceAnimationRunner.java @@ -26,6 +26,8 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.Nullable; +import android.hardware.power.V1_0.PowerHint; +import android.os.PowerManagerInternal; import android.util.ArrayMap; import android.view.Choreographer; import android.view.SurfaceControl; @@ -57,6 +59,7 @@ class SurfaceAnimationRunner { private final AnimationHandler mAnimationHandler; private final Transaction mFrameTransaction; private final AnimatorFactory mAnimatorFactory; + private final PowerManagerInternal mPowerManagerInternal; private boolean mApplyScheduled; @GuardedBy("mLock") @@ -70,13 +73,15 @@ class SurfaceAnimationRunner { @GuardedBy("mLock") private boolean mAnimationStartDeferred; - SurfaceAnimationRunner() { - this(null /* callbackProvider */, null /* animatorFactory */, new Transaction()); + SurfaceAnimationRunner(PowerManagerInternal powerManagerInternal) { + this(null /* callbackProvider */, null /* animatorFactory */, new Transaction(), + powerManagerInternal); } @VisibleForTesting SurfaceAnimationRunner(@Nullable AnimationFrameCallbackProvider callbackProvider, - AnimatorFactory animatorFactory, Transaction frameTransaction) { + AnimatorFactory animatorFactory, Transaction frameTransaction, + PowerManagerInternal powerManagerInternal) { SurfaceAnimationThread.getHandler().runWithScissors(() -> mChoreographer = getSfInstance(), 0 /* timeout */); mFrameTransaction = frameTransaction; @@ -87,6 +92,7 @@ class SurfaceAnimationRunner { mAnimatorFactory = animatorFactory != null ? animatorFactory : SfValueAnimator::new; + mPowerManagerInternal = powerManagerInternal; } /** @@ -231,6 +237,7 @@ class SurfaceAnimationRunner { synchronized (mLock) { startPendingAnimationsLocked(); } + mPowerManagerInternal.powerHint(PowerHint.INTERACTION, 0); } private void scheduleApplyTransaction() { diff --git a/services/core/java/com/android/server/wm/TEST_MAPPING b/services/core/java/com/android/server/wm/TEST_MAPPING new file mode 100644 index 000000000000..e885afa8031d --- /dev/null +++ b/services/core/java/com/android/server/wm/TEST_MAPPING @@ -0,0 +1,42 @@ +{ + "presubmit": [ + { + "name": "CtsWindowManagerDeviceTestCases", + "options": [ + { + "include-annotation": "android.platform.test.annotations.Presubmit" + }, + { + "exclude-annotation": "android.support.test.filters.FlakyTest" + } + ] + }, + { + "name": "FrameworksServicesTests", + "options": [ + { + "include-filter": "com.android.server.wm." + }, + { + "include-annotation": "android.platform.test.annotations.Presubmit" + }, + { + "exclude-annotation": "android.support.test.filters.FlakyTest" + } + ] + } + ], + "postsubmit": [ + { + "name": "CtsWindowManagerDeviceTestCases" + }, + { + "name": "FrameworksServicesTests", + "options": [ + { + "include-filter": "com.android.server.wm." + } + ] + } + ] +} diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 54703b3c3c37..732a82829684 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -1069,7 +1069,7 @@ public class WindowManagerService extends IWindowManager.Stub PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, TAG_WM); mHoldingScreenWakeLock.setReferenceCounted(false); - mSurfaceAnimationRunner = new SurfaceAnimationRunner(); + mSurfaceAnimationRunner = new SurfaceAnimationRunner(mPowerManagerInternal); mAllowTheaterModeWakeFromLayout = context.getResources().getBoolean( com.android.internal.R.bool.config_allowTheaterModeWakeFromWindowLayout); @@ -1469,14 +1469,18 @@ public class WindowManagerService extends IWindowManager.Stub displayFrames.onDisplayInfoUpdated(displayInfo, displayContent.calculateDisplayCutoutForRotation(displayInfo.rotation)); final Rect taskBounds; + final boolean floatingStack; if (atoken != null && atoken.getTask() != null) { taskBounds = mTmpRect; atoken.getTask().getBounds(mTmpRect); + floatingStack = atoken.getTask().isFloating(); } else { taskBounds = null; + floatingStack = false; } - if (mPolicy.getLayoutHintLw(win.mAttrs, taskBounds, displayFrames, outFrame, - outContentInsets, outStableInsets, outOutsets, outDisplayCutout)) { + if (mPolicy.getLayoutHintLw(win.mAttrs, taskBounds, displayFrames, floatingStack, + outFrame, outContentInsets, outStableInsets, outOutsets, + outDisplayCutout)) { res |= WindowManagerGlobal.ADD_FLAG_ALWAYS_CONSUME_NAV_BAR; } diff --git a/services/core/java/com/android/server/wm/utils/InsetUtils.java b/services/core/java/com/android/server/wm/utils/InsetUtils.java index b4a998add374..c5b103fdb40b 100644 --- a/services/core/java/com/android/server/wm/utils/InsetUtils.java +++ b/services/core/java/com/android/server/wm/utils/InsetUtils.java @@ -16,8 +16,11 @@ package com.android.server.wm.utils; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.graphics.Rect; + /** * Utility methods to handle insets represented as rects. */ @@ -35,4 +38,30 @@ public class InsetUtils { inOutInsets.right += insetsToAdd.right; inOutInsets.bottom += insetsToAdd.bottom; } + + /** + * Calculates the insets from the {@code outerFrame} to the {@code innerFrame}. + * + * Note that if a side of the outer frame is not actually on the outside, the inset for that + * side will be clamped to zero. + * + * @param outerFrame the reference frame from which the insets are calculated + * @param innerFrame the inset frame, to which the insets are calculated, + * or null to clear the insets. + * @param outInsets is set to the result of the inset calculation. + */ + public static void insetsBetweenFrames(@NonNull Rect outerFrame, @Nullable Rect innerFrame, + @NonNull Rect outInsets) { + if (innerFrame == null) { + outInsets.setEmpty(); + return; + } + final int w = outerFrame.width(); + final int h = outerFrame.height(); + outInsets.set( + Math.min(w, Math.max(0, innerFrame.left - outerFrame.left)), + Math.min(h, Math.max(0, innerFrame.top - outerFrame.top)), + Math.min(w, Math.max(0, outerFrame.right - innerFrame.right)), + Math.min(h, Math.max(0, outerFrame.bottom - innerFrame.bottom))); + } } diff --git a/services/print/java/com/android/server/print/UserState.java b/services/print/java/com/android/server/print/UserState.java index 4fbc14c0097f..e8266a574bf5 100644 --- a/services/print/java/com/android/server/print/UserState.java +++ b/services/print/java/com/android/server/print/UserState.java @@ -243,7 +243,7 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks, intent.setData(Uri.fromParts("printjob", printJob.getId().flattenToString(), null)); intent.putExtra(PrintManager.EXTRA_PRINT_DOCUMENT_ADAPTER, adapter.asBinder()); intent.putExtra(PrintManager.EXTRA_PRINT_JOB, printJob); - intent.putExtra(DocumentsContract.EXTRA_PACKAGE_NAME, packageName); + intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName); IntentSender intentSender = PendingIntent.getActivityAsUser( mContext, 0, intent, PendingIntent.FLAG_ONE_SHOT diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java new file mode 100644 index 000000000000..865050fd6082 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java @@ -0,0 +1,121 @@ +/* + * 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.hdmi; + +import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM; +import static com.android.server.hdmi.Constants.ADDR_TV; +import static com.android.server.hdmi.Constants.ADDR_UNREGISTERED; +import static junit.framework.Assert.assertTrue; +import static junit.framework.Assert.assertEquals; + +import android.media.AudioManager; +import android.support.test.filters.SmallTest; +import junit.framework.Assert; +import java.util.Arrays; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@SmallTest +@RunWith(JUnit4.class) +/** + * Tests for {@link HdmiCecLocalDeviceAudioSystem} class. + */ +public class HdmiCecLocalDeviceAudioSystemTest { + + + private HdmiControlService mHdmiControlService; + private HdmiCecLocalDeviceAudioSystem mHdmiCecLocalDeviceAudioSystem; + private HdmiCecMessage mResultMessage; + private int mMusicVolume; + private int mMusicMaxVolume; + private boolean mMusicMute; + + @Before + public void SetUp() { + mHdmiControlService = new HdmiControlService(null) { + @Override + AudioManager getAudioManager() { + return new AudioManager() { + @Override + public int getStreamVolume(int streamType) { + switch (streamType) { + case STREAM_MUSIC: + return mMusicVolume; + default: + return 0; + } + } + + @Override + public boolean isStreamMute(int streamType) { + switch (streamType) { + case STREAM_MUSIC: + return mMusicMute; + default: + return false; + } + } + + @Override + public int getStreamMaxVolume(int streamType) { + switch (streamType) { + case STREAM_MUSIC: + return mMusicMaxVolume; + default: + return 100; + } + } + }; + } + + @Override + void sendCecCommand(HdmiCecMessage command) { + mResultMessage = command; + } + }; + mHdmiCecLocalDeviceAudioSystem = new HdmiCecLocalDeviceAudioSystem(mHdmiControlService); + } + + @Test + public void handleGiveAudioStatus_volume_10_mute_true() { + mMusicVolume = 10; + mMusicMute = true; + mMusicMaxVolume = 20; + int scaledVolume = VolumeControlAction.scaleToCecVolume(10, mMusicMaxVolume); + HdmiCecMessage expectMessage = HdmiCecMessageBuilder.buildReportAudioStatus( + ADDR_UNREGISTERED, ADDR_TV, scaledVolume, true); + + HdmiCecMessage message = HdmiCecMessageBuilder.buildGiveAudioStatus( + ADDR_TV, ADDR_AUDIO_SYSTEM); + assertTrue(mHdmiCecLocalDeviceAudioSystem.handleGiveAudioStatus(message)); + + assertTrue(mResultMessage.equals(expectMessage)); + } + + @Test + public void handleGiveSystemAudioModeStatus_off() { + HdmiCecMessage expectMessage = HdmiCecMessageBuilder + .buildReportSystemAudioMode(ADDR_UNREGISTERED, ADDR_TV, false); + + HdmiCecMessage message = HdmiCecMessageBuilder + .buildGiveSystemAudioModeStatus(ADDR_TV, ADDR_AUDIO_SYSTEM); + assertTrue(mHdmiCecLocalDeviceAudioSystem.handleGiveSystemAudioModeStatus(message)); + + assertTrue(mResultMessage.equals(expectMessage)); + } +} diff --git a/services/tests/servicestests/src/com/android/server/policy/PhoneWindowManagerLayoutTest.java b/services/tests/servicestests/src/com/android/server/policy/PhoneWindowManagerLayoutTest.java index 97a716f6bd99..cb94ec7caaca 100644 --- a/services/tests/servicestests/src/com/android/server/policy/PhoneWindowManagerLayoutTest.java +++ b/services/tests/servicestests/src/com/android/server/policy/PhoneWindowManagerLayoutTest.java @@ -22,7 +22,6 @@ import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN; import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; -import static android.view.WindowManager.LayoutParams.FLAG_FULLSCREEN; import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR; import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; @@ -309,8 +308,8 @@ public class PhoneWindowManagerLayoutTest extends PhoneWindowManagerTestBase { final Rect stable = new Rect(); final Rect outsets = new Rect(); final DisplayCutout.ParcelableWrapper cutout = new DisplayCutout.ParcelableWrapper(); - mPolicy.getLayoutHintLw(mAppWindow.attrs, null /* taskBounds */, mFrames, frame, content, - stable, outsets, cutout); + mPolicy.getLayoutHintLw(mAppWindow.attrs, null /* taskBounds */, mFrames, + false /* floatingStack */, frame, content, stable, outsets, cutout); assertThat(frame, equalTo(mFrames.mUnrestricted)); assertThat(content, equalTo(new Rect())); @@ -331,8 +330,8 @@ public class PhoneWindowManagerLayoutTest extends PhoneWindowManagerTestBase { final DisplayCutout.ParcelableWrapper outDisplayCutout = new DisplayCutout.ParcelableWrapper(); - mPolicy.getLayoutHintLw(mAppWindow.attrs, null, mFrames, outFrame, outContentInsets, - outStableInsets, outOutsets, outDisplayCutout); + mPolicy.getLayoutHintLw(mAppWindow.attrs, null, mFrames, false /* floatingStack */, + outFrame, outContentInsets, outStableInsets, outOutsets, outDisplayCutout); assertThat(outFrame, is(mFrames.mUnrestricted)); assertThat(outContentInsets, is(new Rect(0, STATUS_BAR_HEIGHT, 0, NAV_BAR_HEIGHT))); @@ -355,8 +354,35 @@ public class PhoneWindowManagerLayoutTest extends PhoneWindowManagerTestBase { final DisplayCutout.ParcelableWrapper outDisplayCutout = new DisplayCutout.ParcelableWrapper(); - mPolicy.getLayoutHintLw(mAppWindow.attrs, taskBounds, mFrames, outFrame, outContentInsets, - outStableInsets, outOutsets, outDisplayCutout); + mPolicy.getLayoutHintLw(mAppWindow.attrs, taskBounds, mFrames, false /* floatingStack */, + outFrame, outContentInsets, outStableInsets, outOutsets, outDisplayCutout); + + assertThat(outFrame, is(taskBounds)); + assertThat(outContentInsets, is(new Rect())); + assertThat(outStableInsets, is(new Rect())); + assertThat(outOutsets, is(new Rect())); + assertThat(outDisplayCutout, is(new DisplayCutout.ParcelableWrapper())); + } + + @Test + public void layoutHint_appWindowInTask_outsideContentFrame() { + // Initialize DisplayFrames + mPolicy.beginLayoutLw(mFrames, 0 /* UI mode */); + + // Task is in the nav bar area (usually does not happen, but this is similar enough to the + // possible overlap with the IME) + final Rect taskBounds = new Rect(100, mFrames.mContent.bottom + 1, + 200, mFrames.mContent.bottom + 10); + + final Rect outFrame = new Rect(); + final Rect outContentInsets = new Rect(); + final Rect outStableInsets = new Rect(); + final Rect outOutsets = new Rect(); + final DisplayCutout.ParcelableWrapper outDisplayCutout = + new DisplayCutout.ParcelableWrapper(); + + mPolicy.getLayoutHintLw(mAppWindow.attrs, taskBounds, mFrames, true /* floatingStack */, + outFrame, outContentInsets, outStableInsets, outOutsets, outDisplayCutout); assertThat(outFrame, is(taskBounds)); assertThat(outContentInsets, is(new Rect())); diff --git a/services/tests/servicestests/src/com/android/server/wm/SurfaceAnimationRunnerTest.java b/services/tests/servicestests/src/com/android/server/wm/SurfaceAnimationRunnerTest.java index edac8a5202d7..79e9bb4c769d 100644 --- a/services/tests/servicestests/src/com/android/server/wm/SurfaceAnimationRunnerTest.java +++ b/services/tests/servicestests/src/com/android/server/wm/SurfaceAnimationRunnerTest.java @@ -20,24 +20,23 @@ import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.any; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.animation.AnimationHandler; import android.animation.AnimationHandler.AnimationFrameCallbackProvider; import android.animation.ValueAnimator; import android.graphics.Matrix; import android.graphics.Point; +import android.os.PowerManagerInternal; import android.platform.test.annotations.Presubmit; import android.support.test.filters.FlakyTest; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; -import android.util.Log; import android.view.Choreographer; import android.view.Choreographer.FrameCallback; import android.view.SurfaceControl; @@ -46,7 +45,6 @@ import android.view.animation.Animation; import android.view.animation.TranslateAnimation; import com.android.server.wm.LocalAnimationAdapter.AnimationSpec; -import com.android.server.wm.SurfaceAnimationRunner.AnimatorFactory; import org.junit.Before; import org.junit.Rule; @@ -71,6 +69,7 @@ public class SurfaceAnimationRunnerTest extends WindowTestsBase { @Mock SurfaceControl mMockSurface; @Mock Transaction mMockTransaction; @Mock AnimationSpec mMockAnimationSpec; + @Mock PowerManagerInternal mMockPowerManager; @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule(); private SurfaceAnimationRunner mSurfaceAnimationRunner; @@ -81,7 +80,7 @@ public class SurfaceAnimationRunnerTest extends WindowTestsBase { super.setUp(); mFinishCallbackLatch = new CountDownLatch(1); mSurfaceAnimationRunner = new SurfaceAnimationRunner(null /* callbackProvider */, null, - mMockTransaction); + mMockTransaction, mMockPowerManager); } private void finishedCallback() { @@ -113,7 +112,7 @@ public class SurfaceAnimationRunnerTest extends WindowTestsBase { @Test public void testCancel_notStarted() throws Exception { mSurfaceAnimationRunner = new SurfaceAnimationRunner(new NoOpFrameCallbackProvider(), null, - mMockTransaction); + mMockTransaction, mMockPowerManager); mSurfaceAnimationRunner .startAnimation(createTranslateAnimation(), mMockSurface, mMockTransaction, this::finishedCallback); @@ -126,7 +125,7 @@ public class SurfaceAnimationRunnerTest extends WindowTestsBase { @Test public void testCancel_running() throws Exception { mSurfaceAnimationRunner = new SurfaceAnimationRunner(new NoOpFrameCallbackProvider(), null, - mMockTransaction); + mMockTransaction, mMockPowerManager); mSurfaceAnimationRunner.startAnimation(createTranslateAnimation(), mMockSurface, mMockTransaction, this::finishedCallback); waitUntilNextFrame(); @@ -156,7 +155,7 @@ public class SurfaceAnimationRunnerTest extends WindowTestsBase { listener.onAnimationUpdate(animation); }); } - }, mMockTransaction); + }, mMockTransaction, mMockPowerManager); when(mMockAnimationSpec.getDuration()).thenReturn(200L); mSurfaceAnimationRunner.startAnimation(mMockAnimationSpec, mMockSurface, mMockTransaction, this::finishedCallback); @@ -184,6 +183,19 @@ public class SurfaceAnimationRunnerTest extends WindowTestsBase { assertFinishCallbackCalled(); } + @Test + public void testPowerHint() throws Exception { + mSurfaceAnimationRunner = new SurfaceAnimationRunner(new NoOpFrameCallbackProvider(), null, + mMockTransaction, mMockPowerManager); + mSurfaceAnimationRunner.startAnimation(createTranslateAnimation(), mMockSurface, + mMockTransaction, this::finishedCallback); + waitUntilNextFrame(); + + // TODO: For some reason we don't have access to PowerHint definition from the tests. For + // now let's just verify that we got some kind of hint. + verify(mMockPowerManager).powerHint(anyInt(), anyInt()); + } + private void waitUntilNextFrame() throws Exception { final CountDownLatch latch = new CountDownLatch(1); mSurfaceAnimationRunner.mChoreographer.postCallback(Choreographer.CALLBACK_COMMIT, diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAdjustmentExtractorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAdjustmentExtractorTest.java index fd674f0c3858..f17a30ddb1b6 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAdjustmentExtractorTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAdjustmentExtractorTest.java @@ -25,6 +25,9 @@ import static junit.framework.Assert.assertTrue; import android.app.Notification; import android.app.NotificationChannel; +import android.app.PendingIntent; +import android.content.Intent; +import android.graphics.drawable.Icon; import android.os.Bundle; import android.os.UserHandle; import android.service.notification.Adjustment; @@ -54,6 +57,9 @@ public class NotificationAdjustmentExtractorTest extends UiServiceTestCase { ArrayList<String> people = new ArrayList<>(); people.add("you"); signals.putStringArrayList(Adjustment.KEY_PEOPLE, people); + ArrayList<Notification.Action> smartActions = new ArrayList<>(); + smartActions.add(createAction()); + signals.putParcelableArrayList(Adjustment.KEY_SMART_ACTIONS, smartActions); Adjustment adjustment = new Adjustment("pkg", r.getKey(), signals, "", 0); r.addAdjustment(adjustment); @@ -66,6 +72,7 @@ public class NotificationAdjustmentExtractorTest extends UiServiceTestCase { assertTrue(r.getGroupKey().contains(GroupHelper.AUTOGROUP_KEY)); assertEquals(people, r.getPeopleOverride()); assertEquals(snoozeCriteria, r.getSnoozeCriteria()); + assertEquals(smartActions, r.getSmartActions()); } @Test @@ -114,4 +121,11 @@ public class NotificationAdjustmentExtractorTest extends UiServiceTestCase { 0, n, UserHandle.ALL, null, System.currentTimeMillis()); return new NotificationRecord(getContext(), sbn, channel); } + + private Notification.Action createAction() { + return new Notification.Action.Builder( + Icon.createWithResource(getContext(), android.R.drawable.sym_def_app_icon), + "action", + PendingIntent.getBroadcast(getContext(), 0, new Intent("Action"), 0)).build(); + } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java index ef9ba78b8263..742ad65f159a 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java @@ -31,11 +31,14 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.app.INotificationManager; +import android.app.Notification; import android.app.NotificationChannel; +import android.app.PendingIntent; import android.content.Intent; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; +import android.os.Parcel; import android.service.notification.NotificationListenerService; import android.service.notification.NotificationListenerService.Ranking; import android.service.notification.NotificationRankingUpdate; @@ -91,6 +94,7 @@ public class NotificationListenerServiceTest extends UiServiceTestCase { assertEquals(getShowBadge(i), ranking.canShowBadge()); assertEquals(getUserSentiment(i), ranking.getUserSentiment()); assertEquals(getHidden(i), ranking.isSuspended()); + assertActionsEqual(getSmartActions(key, i), ranking.getSmartActions()); } } @@ -107,6 +111,7 @@ public class NotificationListenerServiceTest extends UiServiceTestCase { int[] importance = new int[mKeys.length]; Bundle userSentiment = new Bundle(); Bundle mHidden = new Bundle(); + Bundle smartActions = new Bundle(); for (int i = 0; i < mKeys.length; i++) { String key = mKeys[i]; @@ -124,11 +129,13 @@ public class NotificationListenerServiceTest extends UiServiceTestCase { showBadge.putBoolean(key, getShowBadge(i)); userSentiment.putInt(key, getUserSentiment(i)); mHidden.putBoolean(key, getHidden(i)); + smartActions.putParcelableArrayList(key, getSmartActions(key, i)); } NotificationRankingUpdate update = new NotificationRankingUpdate(mKeys, interceptedKeys.toArray(new String[0]), visibilityOverrides, suppressedVisualEffects, importance, explanation, overrideGroupKeys, - channels, overridePeople, snoozeCriteria, showBadge, userSentiment, mHidden); + channels, overridePeople, snoozeCriteria, showBadge, userSentiment, mHidden, + smartActions); return update; } @@ -196,6 +203,29 @@ public class NotificationListenerServiceTest extends UiServiceTestCase { return snooze; } + private ArrayList<Notification.Action> getSmartActions(String key, int index) { + ArrayList<Notification.Action> actions = new ArrayList<>(); + for (int i = 0; i < index; i++) { + PendingIntent intent = PendingIntent.getBroadcast( + getContext(), + index /*requestCode*/, + new Intent("ACTION_" + key), + 0 /*flags*/); + actions.add(new Notification.Action.Builder(null /*icon*/, key, intent).build()); + } + return actions; + } + + private void assertActionsEqual( + List<Notification.Action> expecteds, List<Notification.Action> actuals) { + assertEquals(expecteds.size(), actuals.size()); + for (int i = 0; i < expecteds.size(); i++) { + Notification.Action expected = expecteds.get(i); + Notification.Action actual = actuals.get(i); + assertEquals(expected.title, actual.title); + } + } + public static class TestListenerService extends NotificationListenerService { private final IBinder binder = new LocalBinder(); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 45a3c4183999..e7a8b581ecdb 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -158,6 +158,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { private TestableLooper mTestableLooper; @Mock private RankingHelper mRankingHelper; + @Mock private PreferencesHelper mPreferencesHelper; AtomicFile mPolicyFile; File mFile; @Mock @@ -600,8 +601,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testBlockedNotifications_blockedChannelGroup() throws Exception { when(mPackageManager.isPackageSuspendedForUser(anyString(), anyInt())).thenReturn(false); - mService.setRankingHelper(mRankingHelper); - when(mRankingHelper.isGroupBlocked(anyString(), anyInt(), anyString())).thenReturn(true); + mService.setPreferencesHelper(mPreferencesHelper); + when(mPreferencesHelper.isGroupBlocked(anyString(), anyInt(), anyString())).thenReturn(true); NotificationChannel channel = new NotificationChannel("id", "name", NotificationManager.IMPORTANCE_HIGH); @@ -1222,36 +1223,36 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testTvExtenderChannelOverride_onTv() throws Exception { mService.setIsTelevision(true); - mService.setRankingHelper(mRankingHelper); - when(mRankingHelper.getNotificationChannel( + mService.setPreferencesHelper(mPreferencesHelper); + when(mPreferencesHelper.getNotificationChannel( anyString(), anyInt(), eq("foo"), anyBoolean())).thenReturn( new NotificationChannel("foo", "foo", IMPORTANCE_HIGH)); Notification.TvExtender tv = new Notification.TvExtender().setChannelId("foo"); mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", 0, generateNotificationRecord(null, tv).getNotification(), 0); - verify(mRankingHelper, times(1)).getNotificationChannel( + verify(mPreferencesHelper, times(1)).getNotificationChannel( anyString(), anyInt(), eq("foo"), anyBoolean()); } @Test public void testTvExtenderChannelOverride_notOnTv() throws Exception { mService.setIsTelevision(false); - mService.setRankingHelper(mRankingHelper); - when(mRankingHelper.getNotificationChannel( + mService.setPreferencesHelper(mPreferencesHelper); + when(mPreferencesHelper.getNotificationChannel( anyString(), anyInt(), anyString(), anyBoolean())).thenReturn( mTestNotificationChannel); Notification.TvExtender tv = new Notification.TvExtender().setChannelId("foo"); mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", 0, generateNotificationRecord(null, tv).getNotification(), 0); - verify(mRankingHelper, times(1)).getNotificationChannel( + verify(mPreferencesHelper, times(1)).getNotificationChannel( anyString(), anyInt(), eq(mTestNotificationChannel.getId()), anyBoolean()); } @Test public void testUpdateAppNotifyCreatorBlock() throws Exception { - mService.setRankingHelper(mRankingHelper); + mService.setPreferencesHelper(mPreferencesHelper); mBinderService.setNotificationsEnabledForPackage(PKG, 0, false); ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class); @@ -1265,7 +1266,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testUpdateAppNotifyCreatorUnblock() throws Exception { - mService.setRankingHelper(mRankingHelper); + mService.setPreferencesHelper(mPreferencesHelper); mBinderService.setNotificationsEnabledForPackage(PKG, 0, true); ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class); @@ -1279,8 +1280,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testUpdateChannelNotifyCreatorBlock() throws Exception { - mService.setRankingHelper(mRankingHelper); - when(mRankingHelper.getNotificationChannel(eq(PKG), anyInt(), + mService.setPreferencesHelper(mPreferencesHelper); + when(mPreferencesHelper.getNotificationChannel(eq(PKG), anyInt(), eq(mTestNotificationChannel.getId()), anyBoolean())) .thenReturn(mTestNotificationChannel); @@ -1305,8 +1306,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { NotificationChannel existingChannel = new NotificationChannel(mTestNotificationChannel.getId(), mTestNotificationChannel.getName(), IMPORTANCE_NONE); - mService.setRankingHelper(mRankingHelper); - when(mRankingHelper.getNotificationChannel(eq(PKG), anyInt(), + mService.setPreferencesHelper(mPreferencesHelper); + when(mPreferencesHelper.getNotificationChannel(eq(PKG), anyInt(), eq(mTestNotificationChannel.getId()), anyBoolean())) .thenReturn(existingChannel); @@ -1327,8 +1328,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { NotificationChannel existingChannel = new NotificationChannel(mTestNotificationChannel.getId(), mTestNotificationChannel.getName(), IMPORTANCE_MAX); - mService.setRankingHelper(mRankingHelper); - when(mRankingHelper.getNotificationChannel(eq(PKG), anyInt(), + mService.setPreferencesHelper(mPreferencesHelper); + when(mPreferencesHelper.getNotificationChannel(eq(PKG), anyInt(), eq(mTestNotificationChannel.getId()), anyBoolean())) .thenReturn(existingChannel); @@ -1339,8 +1340,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testUpdateGroupNotifyCreatorBlock() throws Exception { NotificationChannelGroup existing = new NotificationChannelGroup("id", "name"); - mService.setRankingHelper(mRankingHelper); - when(mRankingHelper.getNotificationChannelGroup(eq(existing.getId()), eq(PKG), anyInt())) + mService.setPreferencesHelper(mPreferencesHelper); + when(mPreferencesHelper.getNotificationChannelGroup(eq(existing.getId()), eq(PKG), anyInt())) .thenReturn(existing); NotificationChannelGroup updated = new NotificationChannelGroup("id", "name"); @@ -1362,8 +1363,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testUpdateGroupNotifyCreatorUnblock() throws Exception { NotificationChannelGroup existing = new NotificationChannelGroup("id", "name"); existing.setBlocked(true); - mService.setRankingHelper(mRankingHelper); - when(mRankingHelper.getNotificationChannelGroup(eq(existing.getId()), eq(PKG), anyInt())) + mService.setPreferencesHelper(mPreferencesHelper); + when(mPreferencesHelper.getNotificationChannelGroup(eq(existing.getId()), eq(PKG), anyInt())) .thenReturn(existing); mBinderService.updateNotificationChannelGroupForPackage( @@ -1382,8 +1383,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testUpdateGroupNoNotifyCreatorOtherChanges() throws Exception { NotificationChannelGroup existing = new NotificationChannelGroup("id", "name"); - mService.setRankingHelper(mRankingHelper); - when(mRankingHelper.getNotificationChannelGroup(eq(existing.getId()), eq(PKG), anyInt())) + mService.setPreferencesHelper(mPreferencesHelper); + when(mPreferencesHelper.getNotificationChannelGroup(eq(existing.getId()), eq(PKG), anyInt())) .thenReturn(existing); mBinderService.updateNotificationChannelGroupForPackage( @@ -1396,12 +1397,12 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { List<String> associations = new ArrayList<>(); associations.add("a"); when(mCompanionMgr.getAssociations(PKG, mUid)).thenReturn(associations); - mService.setRankingHelper(mRankingHelper); - when(mRankingHelper.getNotificationChannel(eq(PKG), anyInt(), + mService.setPreferencesHelper(mPreferencesHelper); + when(mPreferencesHelper.getNotificationChannel(eq(PKG), anyInt(), eq(mTestNotificationChannel.getId()), anyBoolean())) .thenReturn(mTestNotificationChannel); NotificationChannel channel2 = new NotificationChannel("a", "b", IMPORTANCE_LOW); - when(mRankingHelper.getNotificationChannel(eq(PKG), anyInt(), + when(mPreferencesHelper.getNotificationChannel(eq(PKG), anyInt(), eq(channel2.getId()), anyBoolean())) .thenReturn(channel2); @@ -1421,7 +1422,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { List<String> associations = new ArrayList<>(); associations.add("a"); when(mCompanionMgr.getAssociations(PKG, mUid)).thenReturn(associations); - mService.setRankingHelper(mRankingHelper); + mService.setPreferencesHelper(mPreferencesHelper); NotificationChannelGroup group1 = new NotificationChannelGroup("a", "b"); NotificationChannelGroup group2 = new NotificationChannelGroup("n", "m"); @@ -1441,9 +1442,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { List<String> associations = new ArrayList<>(); associations.add("a"); when(mCompanionMgr.getAssociations(PKG, mUid)).thenReturn(associations); - mService.setRankingHelper(mRankingHelper); + mService.setPreferencesHelper(mPreferencesHelper); mTestNotificationChannel.setLightColor(Color.CYAN); - when(mRankingHelper.getNotificationChannel(eq(PKG), anyInt(), + when(mPreferencesHelper.getNotificationChannel(eq(PKG), anyInt(), eq(mTestNotificationChannel.getId()), anyBoolean())) .thenReturn(mTestNotificationChannel); @@ -1459,8 +1460,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { List<String> associations = new ArrayList<>(); associations.add("a"); when(mCompanionMgr.getAssociations(PKG, mUid)).thenReturn(associations); - mService.setRankingHelper(mRankingHelper); - when(mRankingHelper.getNotificationChannel(eq(PKG), anyInt(), + mService.setPreferencesHelper(mPreferencesHelper); + when(mPreferencesHelper.getNotificationChannel(eq(PKG), anyInt(), eq(mTestNotificationChannel.getId()), anyBoolean())) .thenReturn(mTestNotificationChannel); reset(mListeners); @@ -1476,8 +1477,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { associations.add("a"); when(mCompanionMgr.getAssociations(PKG, mUid)).thenReturn(associations); NotificationChannelGroup ncg = new NotificationChannelGroup("a", "b/c"); - mService.setRankingHelper(mRankingHelper); - when(mRankingHelper.getNotificationChannelGroup(eq(ncg.getId()), eq(PKG), anyInt())) + mService.setPreferencesHelper(mPreferencesHelper); + when(mPreferencesHelper.getNotificationChannelGroup(eq(ncg.getId()), eq(PKG), anyInt())) .thenReturn(ncg); reset(mListeners); mBinderService.deleteNotificationChannelGroup(PKG, ncg.getId()); @@ -1488,18 +1489,18 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testUpdateNotificationChannelFromPrivilegedListener_success() throws Exception { - mService.setRankingHelper(mRankingHelper); + mService.setPreferencesHelper(mPreferencesHelper); List<String> associations = new ArrayList<>(); associations.add("a"); when(mCompanionMgr.getAssociations(PKG, mUid)).thenReturn(associations); - when(mRankingHelper.getNotificationChannel(eq(PKG), anyInt(), + when(mPreferencesHelper.getNotificationChannel(eq(PKG), anyInt(), eq(mTestNotificationChannel.getId()), anyBoolean())) .thenReturn(mTestNotificationChannel); mBinderService.updateNotificationChannelFromPrivilegedListener( null, PKG, Process.myUserHandle(), mTestNotificationChannel); - verify(mRankingHelper, times(1)).updateNotificationChannel( + verify(mPreferencesHelper, times(1)).updateNotificationChannel( anyString(), anyInt(), any(), anyBoolean()); verify(mListeners, never()).notifyNotificationChannelChanged(eq(PKG), @@ -1509,7 +1510,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testUpdateNotificationChannelFromPrivilegedListener_noAccess() throws Exception { - mService.setRankingHelper(mRankingHelper); + mService.setPreferencesHelper(mPreferencesHelper); List<String> associations = new ArrayList<>(); when(mCompanionMgr.getAssociations(PKG, mUid)).thenReturn(associations); @@ -1521,7 +1522,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // pass } - verify(mRankingHelper, never()).updateNotificationChannel( + verify(mPreferencesHelper, never()).updateNotificationChannel( anyString(), anyInt(), any(), anyBoolean()); verify(mListeners, never()).notifyNotificationChannelChanged(eq(PKG), @@ -1531,7 +1532,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testUpdateNotificationChannelFromPrivilegedListener_badUser() throws Exception { - mService.setRankingHelper(mRankingHelper); + mService.setPreferencesHelper(mPreferencesHelper); List<String> associations = new ArrayList<>(); associations.add("a"); when(mCompanionMgr.getAssociations(PKG, mUid)).thenReturn(associations); @@ -1548,7 +1549,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // pass } - verify(mRankingHelper, never()).updateNotificationChannel( + verify(mPreferencesHelper, never()).updateNotificationChannel( anyString(), anyInt(), any(), anyBoolean()); verify(mListeners, never()).notifyNotificationChannelChanged(eq(PKG), @@ -1558,7 +1559,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testGetNotificationChannelFromPrivilegedListener_success() throws Exception { - mService.setRankingHelper(mRankingHelper); + mService.setPreferencesHelper(mPreferencesHelper); List<String> associations = new ArrayList<>(); associations.add("a"); when(mCompanionMgr.getAssociations(PKG, mUid)).thenReturn(associations); @@ -1566,13 +1567,13 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.getNotificationChannelsFromPrivilegedListener( null, PKG, Process.myUserHandle()); - verify(mRankingHelper, times(1)).getNotificationChannels( + verify(mPreferencesHelper, times(1)).getNotificationChannels( anyString(), anyInt(), anyBoolean()); } @Test public void testGetNotificationChannelFromPrivilegedListener_noAccess() throws Exception { - mService.setRankingHelper(mRankingHelper); + mService.setPreferencesHelper(mPreferencesHelper); List<String> associations = new ArrayList<>(); when(mCompanionMgr.getAssociations(PKG, mUid)).thenReturn(associations); @@ -1584,13 +1585,13 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // pass } - verify(mRankingHelper, never()).getNotificationChannels( + verify(mPreferencesHelper, never()).getNotificationChannels( anyString(), anyInt(), anyBoolean()); } @Test public void testGetNotificationChannelFromPrivilegedListener_badUser() throws Exception { - mService.setRankingHelper(mRankingHelper); + mService.setPreferencesHelper(mPreferencesHelper); List<String> associations = new ArrayList<>(); associations.add("a"); when(mCompanionMgr.getAssociations(PKG, mUid)).thenReturn(associations); @@ -1606,13 +1607,13 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // pass } - verify(mRankingHelper, never()).getNotificationChannels( + verify(mPreferencesHelper, never()).getNotificationChannels( anyString(), anyInt(), anyBoolean()); } @Test public void testGetNotificationChannelGroupsFromPrivilegedListener_success() throws Exception { - mService.setRankingHelper(mRankingHelper); + mService.setPreferencesHelper(mPreferencesHelper); List<String> associations = new ArrayList<>(); associations.add("a"); when(mCompanionMgr.getAssociations(PKG, mUid)).thenReturn(associations); @@ -1620,12 +1621,12 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.getNotificationChannelGroupsFromPrivilegedListener( null, PKG, Process.myUserHandle()); - verify(mRankingHelper, times(1)).getNotificationChannelGroups(anyString(), anyInt()); + verify(mPreferencesHelper, times(1)).getNotificationChannelGroups(anyString(), anyInt()); } @Test public void testGetNotificationChannelGroupsFromPrivilegedListener_noAccess() throws Exception { - mService.setRankingHelper(mRankingHelper); + mService.setPreferencesHelper(mPreferencesHelper); List<String> associations = new ArrayList<>(); when(mCompanionMgr.getAssociations(PKG, mUid)).thenReturn(associations); @@ -1637,12 +1638,12 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // pass } - verify(mRankingHelper, never()).getNotificationChannelGroups(anyString(), anyInt()); + verify(mPreferencesHelper, never()).getNotificationChannelGroups(anyString(), anyInt()); } @Test public void testGetNotificationChannelGroupsFromPrivilegedListener_badUser() throws Exception { - mService.setRankingHelper(mRankingHelper); + mService.setPreferencesHelper(mPreferencesHelper); List<String> associations = new ArrayList<>(); when(mCompanionMgr.getAssociations(PKG, mUid)).thenReturn(associations); mListener = mock(ManagedServices.ManagedServiceInfo.class); @@ -1657,7 +1658,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // pass } - verify(mRankingHelper, never()).getNotificationChannelGroups(anyString(), anyInt()); + verify(mPreferencesHelper, never()).getNotificationChannelGroups(anyString(), anyInt()); } @Test @@ -2182,7 +2183,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testHandleRankingSort_sendsUpdateOnSignalExtractorChange() throws Exception { - mService.setRankingHelper(mRankingHelper); + mService.setPreferencesHelper(mPreferencesHelper); NotificationManagerService.WorkerHandler handler = mock( NotificationManagerService.WorkerHandler.class); mService.setHandler(handler); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java index e28699113a3d..bd6416dd5d4d 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java @@ -34,8 +34,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.ActivityManager; @@ -48,6 +46,7 @@ import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.graphics.Color; +import android.graphics.drawable.Icon; import android.media.AudioAttributes; import android.metrics.LogMaker; import android.net.Uri; @@ -59,8 +58,8 @@ import android.service.notification.Adjustment; import android.service.notification.StatusBarNotification; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; -import android.util.Slog; +import com.android.internal.R; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.server.UiServiceTestCase; @@ -70,6 +69,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.ArrayList; import java.util.Objects; @SmallTest @@ -697,4 +697,20 @@ public class NotificationRecordTest extends UiServiceTestCase { record.calculateGrantableUris(); // should not throw } + + @Test + public void testSmartActions() { + StatusBarNotification sbn = getNotification(PKG_O, true /* noisy */, + true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */, + false /* lights */, false /* defaultLights */, groupId /* group */); + NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel); + assertNull(record.getSmartActions()); + + ArrayList<Notification.Action> smartActions = new ArrayList<>(); + smartActions.add(new Notification.Action.Builder( + Icon.createWithResource(getContext(), R.drawable.btn_default), + "text", null).build()); + record.setSmartActions(smartActions); + assertEquals(smartActions, record.getSmartActions()); + } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java new file mode 100644 index 000000000000..02d5869b46d0 --- /dev/null +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -0,0 +1,1742 @@ +/* + * 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.notification; + +import static android.app.NotificationManager.IMPORTANCE_DEFAULT; +import static android.app.NotificationManager.IMPORTANCE_HIGH; +import static android.app.NotificationManager.IMPORTANCE_LOW; +import static android.app.NotificationManager.IMPORTANCE_MAX; +import static android.app.NotificationManager.IMPORTANCE_NONE; +import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; + +import static junit.framework.Assert.assertNull; +import static junit.framework.Assert.fail; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationChannelGroup; +import android.app.NotificationManager; +import android.content.ContentProvider; +import android.content.Context; +import android.content.IContentProvider; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.Signature; +import android.content.res.Resources; +import android.graphics.Color; +import android.media.AudioAttributes; +import android.net.Uri; +import android.os.Build; +import android.os.UserHandle; +import android.provider.Settings; +import android.provider.Settings.Secure; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; +import android.test.suitebuilder.annotation.SmallTest; +import android.testing.TestableContentResolver; +import android.util.ArrayMap; +import android.util.Xml; + +import com.android.internal.util.FastXmlSerializer; +import com.android.server.UiServiceTestCase; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlSerializer; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ThreadLocalRandom; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class PreferencesHelperTest extends UiServiceTestCase { + private static final String PKG = "com.android.server.notification"; + private static final int UID = 0; + private static final UserHandle USER = UserHandle.of(0); + private static final String UPDATED_PKG = "updatedPkg"; + private static final int UID2 = 1111; + private static final String SYSTEM_PKG = "android"; + private static final int SYSTEM_UID= 1000; + private static final UserHandle USER2 = UserHandle.of(10); + private static final String TEST_CHANNEL_ID = "test_channel_id"; + private static final String TEST_AUTHORITY = "test"; + private static final Uri SOUND_URI = + Uri.parse("content://" + TEST_AUTHORITY + "/internal/audio/media/10"); + private static final Uri CANONICAL_SOUND_URI = + Uri.parse("content://" + TEST_AUTHORITY + + "/internal/audio/media/10?title=Test&canonical=1"); + + @Mock NotificationUsageStats mUsageStats; + @Mock RankingHandler mHandler; + @Mock PackageManager mPm; + @Mock IContentProvider mTestIContentProvider; + @Mock Context mContext; + @Mock ZenModeHelper mMockZenModeHelper; + + private NotificationManager.Policy mTestNotificationPolicy; + + private PreferencesHelper mHelper; + private AudioAttributes mAudioAttributes; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + UserHandle user = UserHandle.ALL; + + final ApplicationInfo legacy = new ApplicationInfo(); + legacy.targetSdkVersion = Build.VERSION_CODES.N_MR1; + final ApplicationInfo upgrade = new ApplicationInfo(); + upgrade.targetSdkVersion = Build.VERSION_CODES.O; + when(mPm.getApplicationInfoAsUser(eq(PKG), anyInt(), anyInt())).thenReturn(legacy); + when(mPm.getApplicationInfoAsUser(eq(UPDATED_PKG), anyInt(), anyInt())).thenReturn(upgrade); + when(mPm.getApplicationInfoAsUser(eq(SYSTEM_PKG), anyInt(), anyInt())).thenReturn(upgrade); + when(mPm.getPackageUidAsUser(eq(PKG), anyInt())).thenReturn(UID); + when(mPm.getPackageUidAsUser(eq(UPDATED_PKG), anyInt())).thenReturn(UID2); + when(mPm.getPackageUidAsUser(eq(SYSTEM_PKG), anyInt())).thenReturn(SYSTEM_UID); + PackageInfo info = mock(PackageInfo.class); + info.signatures = new Signature[] {mock(Signature.class)}; + when(mPm.getPackageInfoAsUser(eq(SYSTEM_PKG), anyInt(), anyInt())).thenReturn(info); + when(mPm.getPackageInfoAsUser(eq(PKG), anyInt(), anyInt())) + .thenReturn(mock(PackageInfo.class)); + when(mContext.getResources()).thenReturn( + InstrumentationRegistry.getContext().getResources()); + when(mContext.getContentResolver()).thenReturn( + InstrumentationRegistry.getContext().getContentResolver()); + when(mContext.getPackageManager()).thenReturn(mPm); + when(mContext.getApplicationInfo()).thenReturn(legacy); + // most tests assume badging is enabled + TestableContentResolver contentResolver = getContext().getContentResolver(); + contentResolver.setFallbackToExisting(false); + Secure.putIntForUser(contentResolver, + Secure.NOTIFICATION_BADGING, 1, UserHandle.getUserId(UID)); + + ContentProvider testContentProvider = mock(ContentProvider.class); + when(testContentProvider.getIContentProvider()).thenReturn(mTestIContentProvider); + contentResolver.addProvider(TEST_AUTHORITY, testContentProvider); + + when(mTestIContentProvider.canonicalize(any(), eq(SOUND_URI))) + .thenReturn(CANONICAL_SOUND_URI); + when(mTestIContentProvider.canonicalize(any(), eq(CANONICAL_SOUND_URI))) + .thenReturn(CANONICAL_SOUND_URI); + when(mTestIContentProvider.uncanonicalize(any(), eq(CANONICAL_SOUND_URI))) + .thenReturn(SOUND_URI); + + mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0, + NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND); + when(mMockZenModeHelper.getNotificationPolicy()).thenReturn(mTestNotificationPolicy); + mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper); + resetZenModeHelper(); + + mAudioAttributes = new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN) + .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) + .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED) + .build(); + } + + private NotificationChannel getDefaultChannel() { + return new NotificationChannel(NotificationChannel.DEFAULT_CHANNEL_ID, "name", + IMPORTANCE_LOW); + } + + private ByteArrayOutputStream writeXmlAndPurge(String pkg, int uid, boolean forBackup, + String... channelIds) + throws Exception { + XmlSerializer serializer = new FastXmlSerializer(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + serializer.setOutput(new BufferedOutputStream(baos), "utf-8"); + serializer.startDocument(null, true); + mHelper.writeXml(serializer, forBackup); + serializer.endDocument(); + serializer.flush(); + for (String channelId : channelIds) { + mHelper.permanentlyDeleteNotificationChannel(pkg, uid, channelId); + } + return baos; + } + + private void loadStreamXml(ByteArrayOutputStream stream, boolean forRestore) throws Exception { + loadByteArrayXml(stream.toByteArray(), forRestore); + } + + private void loadByteArrayXml(byte[] byteArray, boolean forRestore) throws Exception { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(new BufferedInputStream(new ByteArrayInputStream(byteArray)), null); + parser.nextTag(); + mHelper.readXml(parser, forRestore); + } + + private void compareChannels(NotificationChannel expected, NotificationChannel actual) { + assertEquals(expected.getId(), actual.getId()); + assertEquals(expected.getName(), actual.getName()); + assertEquals(expected.getDescription(), actual.getDescription()); + assertEquals(expected.shouldVibrate(), actual.shouldVibrate()); + assertEquals(expected.shouldShowLights(), actual.shouldShowLights()); + assertEquals(expected.getImportance(), actual.getImportance()); + assertEquals(expected.getLockscreenVisibility(), actual.getLockscreenVisibility()); + assertEquals(expected.getSound(), actual.getSound()); + assertEquals(expected.canBypassDnd(), actual.canBypassDnd()); + assertTrue(Arrays.equals(expected.getVibrationPattern(), actual.getVibrationPattern())); + assertEquals(expected.getGroup(), actual.getGroup()); + assertEquals(expected.getAudioAttributes(), actual.getAudioAttributes()); + assertEquals(expected.getLightColor(), actual.getLightColor()); + } + + private void compareGroups(NotificationChannelGroup expected, NotificationChannelGroup actual) { + assertEquals(expected.getId(), actual.getId()); + assertEquals(expected.getName(), actual.getName()); + assertEquals(expected.getDescription(), actual.getDescription()); + assertEquals(expected.isBlocked(), actual.isBlocked()); + } + + private NotificationChannel getChannel() { + return new NotificationChannel("id", "name", IMPORTANCE_LOW); + } + + private NotificationChannel findChannel(List<NotificationChannel> channels, String id) { + for (NotificationChannel channel : channels) { + if (channel.getId().equals(id)) { + return channel; + } + } + return null; + } + + private void resetZenModeHelper() { + reset(mMockZenModeHelper); + when(mMockZenModeHelper.getNotificationPolicy()).thenReturn(mTestNotificationPolicy); + } + + @Test + public void testChannelXml() throws Exception { + NotificationChannelGroup ncg = new NotificationChannelGroup("1", "bye"); + ncg.setBlocked(true); + ncg.setDescription("group desc"); + NotificationChannelGroup ncg2 = new NotificationChannelGroup("2", "hello"); + NotificationChannel channel1 = + new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); + NotificationChannel channel2 = + new NotificationChannel("id2", "name2", IMPORTANCE_LOW); + channel2.setDescription("descriptions for all"); + channel2.setSound(new Uri.Builder().scheme("test").build(), mAudioAttributes); + channel2.enableLights(true); + channel2.setBypassDnd(true); + channel2.setLockscreenVisibility(Notification.VISIBILITY_SECRET); + channel2.enableVibration(true); + channel2.setGroup(ncg.getId()); + channel2.setVibrationPattern(new long[]{100, 67, 145, 156}); + channel2.setLightColor(Color.BLUE); + + mHelper.createNotificationChannelGroup(PKG, UID, ncg, true); + mHelper.createNotificationChannelGroup(PKG, UID, ncg2, true); + mHelper.createNotificationChannel(PKG, UID, channel1, true, false); + mHelper.createNotificationChannel(PKG, UID, channel2, false, false); + + mHelper.setShowBadge(PKG, UID, true); + mHelper.setAppImportanceLocked(PKG, UID); + + ByteArrayOutputStream baos = writeXmlAndPurge(PKG, UID, false, channel1.getId(), + channel2.getId(), NotificationChannel.DEFAULT_CHANNEL_ID); + mHelper.onPackagesChanged(true, UserHandle.myUserId(), new String[]{PKG}, new int[]{UID}); + + loadStreamXml(baos, false); + + assertTrue(mHelper.canShowBadge(PKG, UID)); + assertTrue(mHelper.getIsAppImportanceLocked(PKG, UID)); + assertEquals(channel1, mHelper.getNotificationChannel(PKG, UID, channel1.getId(), false)); + compareChannels(channel2, + mHelper.getNotificationChannel(PKG, UID, channel2.getId(), false)); + + List<NotificationChannelGroup> actualGroups = + mHelper.getNotificationChannelGroups(PKG, UID, false, true).getList(); + boolean foundNcg = false; + for (NotificationChannelGroup actual : actualGroups) { + if (ncg.getId().equals(actual.getId())) { + foundNcg = true; + compareGroups(ncg, actual); + } else if (ncg2.getId().equals(actual.getId())) { + compareGroups(ncg2, actual); + } + } + assertTrue(foundNcg); + + boolean foundChannel2Group = false; + for (NotificationChannelGroup actual : actualGroups) { + if (channel2.getGroup().equals(actual.getChannels().get(0).getGroup())) { + foundChannel2Group = true; + break; + } + } + assertTrue(foundChannel2Group); + } + + @Test + public void testChannelXmlForBackup() throws Exception { + NotificationChannelGroup ncg = new NotificationChannelGroup("1", "bye"); + NotificationChannelGroup ncg2 = new NotificationChannelGroup("2", "hello"); + NotificationChannel channel1 = + new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); + NotificationChannel channel2 = + new NotificationChannel("id2", "name2", IMPORTANCE_LOW); + channel2.setDescription("descriptions for all"); + channel2.setSound(SOUND_URI, mAudioAttributes); + channel2.enableLights(true); + channel2.setBypassDnd(true); + channel2.setLockscreenVisibility(Notification.VISIBILITY_SECRET); + channel2.enableVibration(false); + channel2.setGroup(ncg.getId()); + channel2.setLightColor(Color.BLUE); + NotificationChannel channel3 = new NotificationChannel("id3", "NAM3", IMPORTANCE_HIGH); + channel3.enableVibration(true); + + mHelper.createNotificationChannelGroup(PKG, UID, ncg, true); + mHelper.createNotificationChannelGroup(PKG, UID, ncg2, true); + mHelper.createNotificationChannel(PKG, UID, channel1, true, false); + mHelper.createNotificationChannel(PKG, UID, channel2, false, false); + mHelper.createNotificationChannel(PKG, UID, channel3, false, false); + mHelper.createNotificationChannel(UPDATED_PKG, UID2, getChannel(), true, false); + + mHelper.setShowBadge(PKG, UID, true); + + mHelper.setImportance(UPDATED_PKG, UID2, IMPORTANCE_NONE); + + ByteArrayOutputStream baos = writeXmlAndPurge(PKG, UID, true, channel1.getId(), + channel2.getId(), channel3.getId(), NotificationChannel.DEFAULT_CHANNEL_ID); + mHelper.onPackagesChanged(true, UserHandle.myUserId(), new String[]{PKG, UPDATED_PKG}, + new int[]{UID, UID2}); + + mHelper.setShowBadge(UPDATED_PKG, UID2, true); + + loadStreamXml(baos, true); + + assertEquals(IMPORTANCE_NONE, mHelper.getImportance(UPDATED_PKG, UID2)); + assertTrue(mHelper.canShowBadge(PKG, UID)); + assertEquals(channel1, mHelper.getNotificationChannel(PKG, UID, channel1.getId(), false)); + compareChannels(channel2, + mHelper.getNotificationChannel(PKG, UID, channel2.getId(), false)); + compareChannels(channel3, + mHelper.getNotificationChannel(PKG, UID, channel3.getId(), false)); + + List<NotificationChannelGroup> actualGroups = + mHelper.getNotificationChannelGroups(PKG, UID, false, true).getList(); + boolean foundNcg = false; + for (NotificationChannelGroup actual : actualGroups) { + if (ncg.getId().equals(actual.getId())) { + foundNcg = true; + compareGroups(ncg, actual); + } else if (ncg2.getId().equals(actual.getId())) { + compareGroups(ncg2, actual); + } + } + assertTrue(foundNcg); + + boolean foundChannel2Group = false; + for (NotificationChannelGroup actual : actualGroups) { + if (channel2.getGroup().equals(actual.getChannels().get(0).getGroup())) { + foundChannel2Group = true; + break; + } + } + assertTrue(foundChannel2Group); + } + + @Test + public void testBackupXml_backupCanonicalizedSoundUri() throws Exception { + NotificationChannel channel = + new NotificationChannel("id", "name", IMPORTANCE_LOW); + channel.setSound(SOUND_URI, mAudioAttributes); + mHelper.createNotificationChannel(PKG, UID, channel, true, false); + + ByteArrayOutputStream baos = writeXmlAndPurge(PKG, UID, true, channel.getId()); + + // Testing that in restore we are given the canonical version + loadStreamXml(baos, true); + verify(mTestIContentProvider).uncanonicalize(any(), eq(CANONICAL_SOUND_URI)); + } + + @Test + public void testRestoreXml_withExistentCanonicalizedSoundUri() throws Exception { + Uri localUri = Uri.parse("content://" + TEST_AUTHORITY + "/local/url"); + Uri canonicalBasedOnLocal = localUri.buildUpon() + .appendQueryParameter("title", "Test") + .appendQueryParameter("canonical", "1") + .build(); + when(mTestIContentProvider.canonicalize(any(), eq(CANONICAL_SOUND_URI))) + .thenReturn(canonicalBasedOnLocal); + when(mTestIContentProvider.uncanonicalize(any(), eq(CANONICAL_SOUND_URI))) + .thenReturn(localUri); + when(mTestIContentProvider.uncanonicalize(any(), eq(canonicalBasedOnLocal))) + .thenReturn(localUri); + + NotificationChannel channel = + new NotificationChannel("id", "name", IMPORTANCE_LOW); + channel.setSound(SOUND_URI, mAudioAttributes); + mHelper.createNotificationChannel(PKG, UID, channel, true, false); + ByteArrayOutputStream baos = writeXmlAndPurge(PKG, UID, true, channel.getId()); + + loadStreamXml(baos, true); + + NotificationChannel actualChannel = mHelper.getNotificationChannel( + PKG, UID, channel.getId(), false); + assertEquals(localUri, actualChannel.getSound()); + } + + @Test + public void testRestoreXml_withNonExistentCanonicalizedSoundUri() throws Exception { + Thread.sleep(3000); + when(mTestIContentProvider.canonicalize(any(), eq(CANONICAL_SOUND_URI))) + .thenReturn(null); + when(mTestIContentProvider.uncanonicalize(any(), eq(CANONICAL_SOUND_URI))) + .thenReturn(null); + + NotificationChannel channel = + new NotificationChannel("id", "name", IMPORTANCE_LOW); + channel.setSound(SOUND_URI, mAudioAttributes); + mHelper.createNotificationChannel(PKG, UID, channel, true, false); + ByteArrayOutputStream baos = writeXmlAndPurge(PKG, UID, true, channel.getId()); + + loadStreamXml(baos, true); + + NotificationChannel actualChannel = mHelper.getNotificationChannel( + PKG, UID, channel.getId(), false); + assertEquals(Settings.System.DEFAULT_NOTIFICATION_URI, actualChannel.getSound()); + } + + + /** + * Although we don't make backups with uncanonicalized uris anymore, we used to, so we have to + * handle its restore properly. + */ + @Test + public void testRestoreXml_withUncanonicalizedNonLocalSoundUri() throws Exception { + // Not a local uncanonicalized uri, simulating that it fails to exist locally + when(mTestIContentProvider.canonicalize(any(), eq(SOUND_URI))).thenReturn(null); + String id = "id"; + String backupWithUncanonicalizedSoundUri = "<ranking version=\"1\">\n" + + "<package name=\"com.android.server.notification\" show_badge=\"true\">\n" + + "<channel id=\"" + id + "\" name=\"name\" importance=\"2\" " + + "sound=\"" + SOUND_URI + "\" " + + "usage=\"6\" content_type=\"0\" flags=\"1\" show_badge=\"true\" />\n" + + "<channel id=\"miscellaneous\" name=\"Uncategorized\" usage=\"5\" " + + "content_type=\"4\" flags=\"0\" show_badge=\"true\" />\n" + + "</package>\n" + + "</ranking>\n"; + + loadByteArrayXml(backupWithUncanonicalizedSoundUri.getBytes(), true); + + NotificationChannel actualChannel = mHelper.getNotificationChannel(PKG, UID, id, false); + assertEquals(Settings.System.DEFAULT_NOTIFICATION_URI, actualChannel.getSound()); + } + + @Test + public void testBackupRestoreXml_withNullSoundUri() throws Exception { + NotificationChannel channel = + new NotificationChannel("id", "name", IMPORTANCE_LOW); + channel.setSound(null, mAudioAttributes); + mHelper.createNotificationChannel(PKG, UID, channel, true, false); + ByteArrayOutputStream baos = writeXmlAndPurge(PKG, UID, true, channel.getId()); + + loadStreamXml(baos, true); + + NotificationChannel actualChannel = mHelper.getNotificationChannel( + PKG, UID, channel.getId(), false); + assertEquals(null, actualChannel.getSound()); + } + + @Test + public void testChannelXml_backup() throws Exception { + NotificationChannelGroup ncg = new NotificationChannelGroup("1", "bye"); + NotificationChannelGroup ncg2 = new NotificationChannelGroup("2", "hello"); + NotificationChannel channel1 = + new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); + NotificationChannel channel2 = + new NotificationChannel("id2", "name2", IMPORTANCE_LOW); + NotificationChannel channel3 = + new NotificationChannel("id3", "name3", IMPORTANCE_LOW); + channel3.setGroup(ncg.getId()); + + mHelper.createNotificationChannelGroup(PKG, UID, ncg, true); + mHelper.createNotificationChannelGroup(PKG, UID, ncg2, true); + mHelper.createNotificationChannel(PKG, UID, channel1, true, false); + mHelper.createNotificationChannel(PKG, UID, channel2, false, false); + mHelper.createNotificationChannel(PKG, UID, channel3, true, false); + + mHelper.deleteNotificationChannel(PKG, UID, channel1.getId()); + mHelper.deleteNotificationChannelGroup(PKG, UID, ncg.getId()); + assertEquals(channel2, mHelper.getNotificationChannel(PKG, UID, channel2.getId(), false)); + + ByteArrayOutputStream baos = writeXmlAndPurge(PKG, UID, true, channel1.getId(), + channel2.getId(), channel3.getId(), NotificationChannel.DEFAULT_CHANNEL_ID); + mHelper.onPackagesChanged(true, UserHandle.myUserId(), new String[]{PKG}, new int[]{UID}); + + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(new BufferedInputStream(new ByteArrayInputStream(baos.toByteArray())), + null); + parser.nextTag(); + mHelper.readXml(parser, true); + + assertNull(mHelper.getNotificationChannel(PKG, UID, channel1.getId(), false)); + assertNull(mHelper.getNotificationChannel(PKG, UID, channel3.getId(), false)); + assertNull(mHelper.getNotificationChannelGroup(ncg.getId(), PKG, UID)); + //assertEquals(ncg2, mHelper.getNotificationChannelGroup(ncg2.getId(), PKG, UID)); + assertEquals(channel2, mHelper.getNotificationChannel(PKG, UID, channel2.getId(), false)); + } + + @Test + public void testChannelXml_defaultChannelLegacyApp_noUserSettings() throws Exception { + ByteArrayOutputStream baos = writeXmlAndPurge(PKG, UID, false, + NotificationChannel.DEFAULT_CHANNEL_ID); + + loadStreamXml(baos, false); + + final NotificationChannel updated = mHelper.getNotificationChannel(PKG, UID, + NotificationChannel.DEFAULT_CHANNEL_ID, false); + assertEquals(NotificationManager.IMPORTANCE_UNSPECIFIED, updated.getImportance()); + assertFalse(updated.canBypassDnd()); + assertEquals(NotificationManager.VISIBILITY_NO_OVERRIDE, updated.getLockscreenVisibility()); + assertEquals(0, updated.getUserLockedFields()); + } + + @Test + public void testChannelXml_defaultChannelUpdatedApp_userSettings() throws Exception { + final NotificationChannel defaultChannel = mHelper.getNotificationChannel(PKG, UID, + NotificationChannel.DEFAULT_CHANNEL_ID, false); + defaultChannel.setImportance(NotificationManager.IMPORTANCE_LOW); + mHelper.updateNotificationChannel(PKG, UID, defaultChannel, true); + + ByteArrayOutputStream baos = writeXmlAndPurge(PKG, UID, false, + NotificationChannel.DEFAULT_CHANNEL_ID); + + loadStreamXml(baos, false); + + assertEquals(NotificationManager.IMPORTANCE_LOW, mHelper.getNotificationChannel( + PKG, UID, NotificationChannel.DEFAULT_CHANNEL_ID, false).getImportance()); + } + + @Test + public void testChannelXml_upgradeCreateDefaultChannel() throws Exception { + final String preupgradeXml = "<ranking version=\"1\">\n" + + "<package name=\"" + PKG + + "\" importance=\"" + NotificationManager.IMPORTANCE_HIGH + + "\" priority=\"" + Notification.PRIORITY_MAX + "\" visibility=\"" + + Notification.VISIBILITY_SECRET + "\"" +" uid=\"" + UID + "\" />\n" + + "<package name=\"" + UPDATED_PKG + "\" uid=\"" + UID2 + "\" visibility=\"" + + Notification.VISIBILITY_PRIVATE + "\" />\n" + + "</ranking>"; + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(new BufferedInputStream(new ByteArrayInputStream(preupgradeXml.getBytes())), + null); + parser.nextTag(); + mHelper.readXml(parser, false); + + final NotificationChannel updated1 = + mHelper.getNotificationChannel(PKG, UID, NotificationChannel.DEFAULT_CHANNEL_ID, false); + assertEquals(NotificationManager.IMPORTANCE_HIGH, updated1.getImportance()); + assertTrue(updated1.canBypassDnd()); + assertEquals(Notification.VISIBILITY_SECRET, updated1.getLockscreenVisibility()); + assertEquals(NotificationChannel.USER_LOCKED_IMPORTANCE + | NotificationChannel.USER_LOCKED_PRIORITY + | NotificationChannel.USER_LOCKED_VISIBILITY, + updated1.getUserLockedFields()); + + // No Default Channel created for updated packages + assertEquals(null, mHelper.getNotificationChannel(UPDATED_PKG, UID2, + NotificationChannel.DEFAULT_CHANNEL_ID, false)); + } + + @Test + public void testChannelXml_upgradeDeletesDefaultChannel() throws Exception { + final NotificationChannel defaultChannel = mHelper.getNotificationChannel( + PKG, UID, NotificationChannel.DEFAULT_CHANNEL_ID, false); + assertTrue(defaultChannel != null); + ByteArrayOutputStream baos = + writeXmlAndPurge(PKG, UID, false, NotificationChannel.DEFAULT_CHANNEL_ID); + // Load package at higher sdk. + final ApplicationInfo upgraded = new ApplicationInfo(); + upgraded.targetSdkVersion = Build.VERSION_CODES.N_MR1 + 1; + when(mPm.getApplicationInfoAsUser(eq(PKG), anyInt(), anyInt())).thenReturn(upgraded); + loadStreamXml(baos, false); + + // Default Channel should be gone. + assertEquals(null, mHelper.getNotificationChannel(PKG, UID, + NotificationChannel.DEFAULT_CHANNEL_ID, false)); + } + + @Test + public void testDeletesDefaultChannelAfterChannelIsCreated() throws Exception { + mHelper.createNotificationChannel(PKG, UID, + new NotificationChannel("bananas", "bananas", IMPORTANCE_LOW), true, false); + ByteArrayOutputStream baos = writeXmlAndPurge(PKG, UID, false, + NotificationChannel.DEFAULT_CHANNEL_ID, "bananas"); + + // Load package at higher sdk. + final ApplicationInfo upgraded = new ApplicationInfo(); + upgraded.targetSdkVersion = Build.VERSION_CODES.N_MR1 + 1; + when(mPm.getApplicationInfoAsUser(eq(PKG), anyInt(), anyInt())).thenReturn(upgraded); + loadStreamXml(baos, false); + + // Default Channel should be gone. + assertEquals(null, mHelper.getNotificationChannel(PKG, UID, + NotificationChannel.DEFAULT_CHANNEL_ID, false)); + } + + @Test + public void testLoadingOldChannelsDoesNotDeleteNewlyCreatedChannels() throws Exception { + ByteArrayOutputStream baos = writeXmlAndPurge(PKG, UID, false, + NotificationChannel.DEFAULT_CHANNEL_ID, "bananas"); + mHelper.createNotificationChannel(PKG, UID, + new NotificationChannel("bananas", "bananas", IMPORTANCE_LOW), true, false); + + loadStreamXml(baos, false); + + // Should still have the newly created channel that wasn't in the xml. + assertTrue(mHelper.getNotificationChannel(PKG, UID, "bananas", false) != null); + } + + @Test + public void testCreateChannel_blocked() throws Exception { + mHelper.setImportance(PKG, UID, IMPORTANCE_NONE); + + mHelper.createNotificationChannel(PKG, UID, + new NotificationChannel("bananas", "bananas", IMPORTANCE_LOW), true, false); + } + + @Test + public void testCreateChannel_badImportance() throws Exception { + try { + mHelper.createNotificationChannel(PKG, UID, + new NotificationChannel("bananas", "bananas", IMPORTANCE_NONE - 1), + true, false); + fail("Was allowed to create a channel with invalid importance"); + } catch (IllegalArgumentException e) { + // yay + } + try { + mHelper.createNotificationChannel(PKG, UID, + new NotificationChannel("bananas", "bananas", IMPORTANCE_UNSPECIFIED), + true, false); + fail("Was allowed to create a channel with invalid importance"); + } catch (IllegalArgumentException e) { + // yay + } + try { + mHelper.createNotificationChannel(PKG, UID, + new NotificationChannel("bananas", "bananas", IMPORTANCE_MAX + 1), + true, false); + fail("Was allowed to create a channel with invalid importance"); + } catch (IllegalArgumentException e) { + // yay + } + mHelper.createNotificationChannel(PKG, UID, + new NotificationChannel("bananas", "bananas", IMPORTANCE_NONE), true, false); + mHelper.createNotificationChannel(PKG, UID, + new NotificationChannel("bananas", "bananas", IMPORTANCE_MAX), true, false); + } + + + @Test + public void testUpdate() throws Exception { + // no fields locked by user + final NotificationChannel channel = + new NotificationChannel("id2", "name2", IMPORTANCE_LOW); + channel.setSound(new Uri.Builder().scheme("test").build(), mAudioAttributes); + channel.enableLights(true); + channel.setBypassDnd(true); + channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET); + + mHelper.createNotificationChannel(PKG, UID, channel, false, false); + + // same id, try to update all fields + final NotificationChannel channel2 = + new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_HIGH); + channel2.setSound(new Uri.Builder().scheme("test2").build(), mAudioAttributes); + channel2.enableLights(false); + channel2.setBypassDnd(false); + channel2.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); + + mHelper.updateNotificationChannel(PKG, UID, channel2, true); + + // all fields should be changed + assertEquals(channel2, mHelper.getNotificationChannel(PKG, UID, channel.getId(), false)); + + verify(mHandler, times(1)).requestSort(); + } + + @Test + public void testUpdate_preUpgrade_updatesAppFields() throws Exception { + mHelper.setImportance(PKG, UID, IMPORTANCE_UNSPECIFIED); + assertTrue(mHelper.canShowBadge(PKG, UID)); + assertEquals(Notification.PRIORITY_DEFAULT, mHelper.getPackagePriority(PKG, UID)); + assertEquals(NotificationManager.VISIBILITY_NO_OVERRIDE, + mHelper.getPackageVisibility(PKG, UID)); + assertFalse(mHelper.getIsAppImportanceLocked(PKG, UID)); + + NotificationChannel defaultChannel = mHelper.getNotificationChannel( + PKG, UID, NotificationChannel.DEFAULT_CHANNEL_ID, false); + + defaultChannel.setShowBadge(false); + defaultChannel.setImportance(IMPORTANCE_NONE); + defaultChannel.setBypassDnd(true); + defaultChannel.setLockscreenVisibility(Notification.VISIBILITY_SECRET); + + mHelper.setAppImportanceLocked(PKG, UID); + mHelper.updateNotificationChannel(PKG, UID, defaultChannel, true); + + // ensure app level fields are changed + assertFalse(mHelper.canShowBadge(PKG, UID)); + assertEquals(Notification.PRIORITY_MAX, mHelper.getPackagePriority(PKG, UID)); + assertEquals(Notification.VISIBILITY_SECRET, mHelper.getPackageVisibility(PKG, UID)); + assertEquals(IMPORTANCE_NONE, mHelper.getImportance(PKG, UID)); + assertTrue(mHelper.getIsAppImportanceLocked(PKG, UID)); + } + + @Test + public void testUpdate_postUpgrade_noUpdateAppFields() throws Exception { + final NotificationChannel channel = new NotificationChannel("id2", "name2", IMPORTANCE_LOW); + + mHelper.createNotificationChannel(PKG, UID, channel, false, false); + assertTrue(mHelper.canShowBadge(PKG, UID)); + assertEquals(Notification.PRIORITY_DEFAULT, mHelper.getPackagePriority(PKG, UID)); + assertEquals(NotificationManager.VISIBILITY_NO_OVERRIDE, + mHelper.getPackageVisibility(PKG, UID)); + + channel.setShowBadge(false); + channel.setImportance(IMPORTANCE_NONE); + channel.setBypassDnd(true); + channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET); + + mHelper.updateNotificationChannel(PKG, UID, channel, true); + + // ensure app level fields are not changed + assertTrue(mHelper.canShowBadge(PKG, UID)); + assertEquals(Notification.PRIORITY_DEFAULT, mHelper.getPackagePriority(PKG, UID)); + assertEquals(NotificationManager.VISIBILITY_NO_OVERRIDE, + mHelper.getPackageVisibility(PKG, UID)); + assertEquals(NotificationManager.IMPORTANCE_UNSPECIFIED, mHelper.getImportance(PKG, UID)); + } + + @Test + public void testGetNotificationChannel_ReturnsNullForUnknownChannel() throws Exception { + assertEquals(null, mHelper.getNotificationChannel(PKG, UID, "garbage", false)); + } + + @Test + public void testCreateChannel_CannotChangeHiddenFields() throws Exception { + final NotificationChannel channel = + new NotificationChannel("id2", "name2", IMPORTANCE_LOW); + channel.setSound(new Uri.Builder().scheme("test").build(), mAudioAttributes); + channel.enableLights(true); + channel.setBypassDnd(true); + channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET); + channel.setShowBadge(true); + int lockMask = 0; + for (int i = 0; i < NotificationChannel.LOCKABLE_FIELDS.length; i++) { + lockMask |= NotificationChannel.LOCKABLE_FIELDS[i]; + } + channel.lockFields(lockMask); + + mHelper.createNotificationChannel(PKG, UID, channel, true, false); + + NotificationChannel savedChannel = + mHelper.getNotificationChannel(PKG, UID, channel.getId(), false); + + assertEquals(channel.getName(), savedChannel.getName()); + assertEquals(channel.shouldShowLights(), savedChannel.shouldShowLights()); + assertFalse(savedChannel.canBypassDnd()); + assertFalse(Notification.VISIBILITY_SECRET == savedChannel.getLockscreenVisibility()); + assertEquals(channel.canShowBadge(), savedChannel.canShowBadge()); + + verify(mHandler, never()).requestSort(); + } + + @Test + public void testCreateChannel_CannotChangeHiddenFieldsAssistant() throws Exception { + final NotificationChannel channel = + new NotificationChannel("id2", "name2", IMPORTANCE_LOW); + channel.setSound(new Uri.Builder().scheme("test").build(), mAudioAttributes); + channel.enableLights(true); + channel.setBypassDnd(true); + channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET); + channel.setShowBadge(true); + int lockMask = 0; + for (int i = 0; i < NotificationChannel.LOCKABLE_FIELDS.length; i++) { + lockMask |= NotificationChannel.LOCKABLE_FIELDS[i]; + } + channel.lockFields(lockMask); + + mHelper.createNotificationChannel(PKG, UID, channel, true, false); + + NotificationChannel savedChannel = + mHelper.getNotificationChannel(PKG, UID, channel.getId(), false); + + assertEquals(channel.getName(), savedChannel.getName()); + assertEquals(channel.shouldShowLights(), savedChannel.shouldShowLights()); + assertFalse(savedChannel.canBypassDnd()); + assertFalse(Notification.VISIBILITY_SECRET == savedChannel.getLockscreenVisibility()); + assertEquals(channel.canShowBadge(), savedChannel.canShowBadge()); + } + + @Test + public void testClearLockedFields() throws Exception { + final NotificationChannel channel = getChannel(); + mHelper.clearLockedFields(channel); + assertEquals(0, channel.getUserLockedFields()); + + channel.lockFields(NotificationChannel.USER_LOCKED_PRIORITY + | NotificationChannel.USER_LOCKED_IMPORTANCE); + mHelper.clearLockedFields(channel); + assertEquals(0, channel.getUserLockedFields()); + } + + @Test + public void testLockFields_soundAndVibration() throws Exception { + mHelper.createNotificationChannel(PKG, UID, getChannel(), true, false); + + final NotificationChannel update1 = getChannel(); + update1.setSound(new Uri.Builder().scheme("test").build(), + new AudioAttributes.Builder().build()); + update1.lockFields(NotificationChannel.USER_LOCKED_PRIORITY); + mHelper.updateNotificationChannel(PKG, UID, update1, true); + assertEquals(NotificationChannel.USER_LOCKED_PRIORITY + | NotificationChannel.USER_LOCKED_SOUND, + mHelper.getNotificationChannel(PKG, UID, update1.getId(), false) + .getUserLockedFields()); + + NotificationChannel update2 = getChannel(); + update2.enableVibration(true); + mHelper.updateNotificationChannel(PKG, UID, update2, true); + assertEquals(NotificationChannel.USER_LOCKED_PRIORITY + | NotificationChannel.USER_LOCKED_SOUND + | NotificationChannel.USER_LOCKED_VIBRATION, + mHelper.getNotificationChannel(PKG, UID, update2.getId(), false) + .getUserLockedFields()); + } + + @Test + public void testLockFields_vibrationAndLights() throws Exception { + mHelper.createNotificationChannel(PKG, UID, getChannel(), true, false); + + final NotificationChannel update1 = getChannel(); + update1.setVibrationPattern(new long[]{7945, 46 ,246}); + mHelper.updateNotificationChannel(PKG, UID, update1, true); + assertEquals(NotificationChannel.USER_LOCKED_VIBRATION, + mHelper.getNotificationChannel(PKG, UID, update1.getId(), false) + .getUserLockedFields()); + + final NotificationChannel update2 = getChannel(); + update2.enableLights(true); + mHelper.updateNotificationChannel(PKG, UID, update2, true); + assertEquals(NotificationChannel.USER_LOCKED_VIBRATION + | NotificationChannel.USER_LOCKED_LIGHTS, + mHelper.getNotificationChannel(PKG, UID, update2.getId(), false) + .getUserLockedFields()); + } + + @Test + public void testLockFields_lightsAndImportance() throws Exception { + mHelper.createNotificationChannel(PKG, UID, getChannel(), true, false); + + final NotificationChannel update1 = getChannel(); + update1.setLightColor(Color.GREEN); + mHelper.updateNotificationChannel(PKG, UID, update1, true); + assertEquals(NotificationChannel.USER_LOCKED_LIGHTS, + mHelper.getNotificationChannel(PKG, UID, update1.getId(), false) + .getUserLockedFields()); + + final NotificationChannel update2 = getChannel(); + update2.setImportance(IMPORTANCE_DEFAULT); + mHelper.updateNotificationChannel(PKG, UID, update2, true); + assertEquals(NotificationChannel.USER_LOCKED_LIGHTS + | NotificationChannel.USER_LOCKED_IMPORTANCE, + mHelper.getNotificationChannel(PKG, UID, update2.getId(), false) + .getUserLockedFields()); + } + + @Test + public void testLockFields_visibilityAndDndAndBadge() throws Exception { + mHelper.createNotificationChannel(PKG, UID, getChannel(), true, false); + assertEquals(0, + mHelper.getNotificationChannel(PKG, UID, getChannel().getId(), false) + .getUserLockedFields()); + + final NotificationChannel update1 = getChannel(); + update1.setBypassDnd(true); + mHelper.updateNotificationChannel(PKG, UID, update1, true); + assertEquals(NotificationChannel.USER_LOCKED_PRIORITY, + mHelper.getNotificationChannel(PKG, UID, update1.getId(), false) + .getUserLockedFields()); + + final NotificationChannel update2 = getChannel(); + update2.setLockscreenVisibility(Notification.VISIBILITY_SECRET); + mHelper.updateNotificationChannel(PKG, UID, update2, true); + assertEquals(NotificationChannel.USER_LOCKED_PRIORITY + | NotificationChannel.USER_LOCKED_VISIBILITY, + mHelper.getNotificationChannel(PKG, UID, update2.getId(), false) + .getUserLockedFields()); + + final NotificationChannel update3 = getChannel(); + update3.setShowBadge(false); + mHelper.updateNotificationChannel(PKG, UID, update3, true); + assertEquals(NotificationChannel.USER_LOCKED_PRIORITY + | NotificationChannel.USER_LOCKED_VISIBILITY + | NotificationChannel.USER_LOCKED_SHOW_BADGE, + mHelper.getNotificationChannel(PKG, UID, update3.getId(), false) + .getUserLockedFields()); + } + + @Test + public void testDeleteNonExistentChannel() throws Exception { + mHelper.deleteNotificationChannelGroup(PKG, UID, "does not exist"); + } + + @Test + public void testGetDeletedChannel() throws Exception { + NotificationChannel channel = getChannel(); + channel.setSound(new Uri.Builder().scheme("test").build(), mAudioAttributes); + channel.enableLights(true); + channel.setBypassDnd(true); + channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET); + channel.enableVibration(true); + channel.setVibrationPattern(new long[]{100, 67, 145, 156}); + + mHelper.createNotificationChannel(PKG, UID, channel, true, false); + mHelper.deleteNotificationChannel(PKG, UID, channel.getId()); + + // Does not return deleted channel + NotificationChannel response = + mHelper.getNotificationChannel(PKG, UID, channel.getId(), false); + assertNull(response); + + // Returns deleted channel + response = mHelper.getNotificationChannel(PKG, UID, channel.getId(), true); + compareChannels(channel, response); + assertTrue(response.isDeleted()); + } + + @Test + public void testGetDeletedChannels() throws Exception { + Map<String, NotificationChannel> channelMap = new HashMap<>(); + NotificationChannel channel = + new NotificationChannel("id2", "name2", IMPORTANCE_LOW); + channel.setSound(new Uri.Builder().scheme("test").build(), mAudioAttributes); + channel.enableLights(true); + channel.setBypassDnd(true); + channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); + channel.enableVibration(true); + channel.setVibrationPattern(new long[]{100, 67, 145, 156}); + channelMap.put(channel.getId(), channel); + NotificationChannel channel2 = + new NotificationChannel("id4", "a", NotificationManager.IMPORTANCE_HIGH); + channelMap.put(channel2.getId(), channel2); + mHelper.createNotificationChannel(PKG, UID, channel, true, false); + mHelper.createNotificationChannel(PKG, UID, channel2, true, false); + + mHelper.deleteNotificationChannel(PKG, UID, channel.getId()); + + // Returns only non-deleted channels + List<NotificationChannel> channels = + mHelper.getNotificationChannels(PKG, UID, false).getList(); + assertEquals(2, channels.size()); // Default channel + non-deleted channel + for (NotificationChannel nc : channels) { + if (!NotificationChannel.DEFAULT_CHANNEL_ID.equals(nc.getId())) { + compareChannels(channel2, nc); + } + } + + // Returns deleted channels too + channels = mHelper.getNotificationChannels(PKG, UID, true).getList(); + assertEquals(3, channels.size()); // Includes default channel + for (NotificationChannel nc : channels) { + if (!NotificationChannel.DEFAULT_CHANNEL_ID.equals(nc.getId())) { + compareChannels(channelMap.get(nc.getId()), nc); + } + } + } + + @Test + public void testGetDeletedChannelCount() throws Exception { + NotificationChannel channel = + new NotificationChannel("id2", "name2", IMPORTANCE_LOW); + NotificationChannel channel2 = + new NotificationChannel("id4", "a", NotificationManager.IMPORTANCE_HIGH); + NotificationChannel channel3 = + new NotificationChannel("id5", "a", NotificationManager.IMPORTANCE_HIGH); + mHelper.createNotificationChannel(PKG, UID, channel, true, false); + mHelper.createNotificationChannel(PKG, UID, channel2, true, false); + mHelper.createNotificationChannel(PKG, UID, channel3, true, false); + + mHelper.deleteNotificationChannel(PKG, UID, channel.getId()); + mHelper.deleteNotificationChannel(PKG, UID, channel3.getId()); + + assertEquals(2, mHelper.getDeletedChannelCount(PKG, UID)); + assertEquals(0, mHelper.getDeletedChannelCount("pkg2", UID2)); + } + + @Test + public void testGetBlockedChannelCount() throws Exception { + NotificationChannel channel = + new NotificationChannel("id2", "name2", IMPORTANCE_LOW); + NotificationChannel channel2 = + new NotificationChannel("id4", "a", NotificationManager.IMPORTANCE_NONE); + NotificationChannel channel3 = + new NotificationChannel("id5", "a", NotificationManager.IMPORTANCE_NONE); + mHelper.createNotificationChannel(PKG, UID, channel, true, false); + mHelper.createNotificationChannel(PKG, UID, channel2, true, false); + mHelper.createNotificationChannel(PKG, UID, channel3, true, false); + + mHelper.deleteNotificationChannel(PKG, UID, channel3.getId()); + + assertEquals(1, mHelper.getBlockedChannelCount(PKG, UID)); + assertEquals(0, mHelper.getBlockedChannelCount("pkg2", UID2)); + } + + @Test + public void testCreateAndDeleteCanChannelsBypassDnd() throws Exception { + // create notification channel that can't bypass dnd + // expected result: areChannelsBypassingDnd = false + // setNotificationPolicy isn't called since areChannelsBypassingDnd was already false + NotificationChannel channel = new NotificationChannel("id1", "name1", IMPORTANCE_LOW); + mHelper.createNotificationChannel(PKG, UID, channel, true, false); + assertFalse(mHelper.areChannelsBypassingDnd()); + verify(mMockZenModeHelper, never()).setNotificationPolicy(any()); + resetZenModeHelper(); + + // create notification channel that can bypass dnd + // expected result: areChannelsBypassingDnd = true + NotificationChannel channel2 = new NotificationChannel("id2", "name2", IMPORTANCE_LOW); + channel2.setBypassDnd(true); + mHelper.createNotificationChannel(PKG, UID, channel2, true, true); + assertTrue(mHelper.areChannelsBypassingDnd()); + verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any()); + resetZenModeHelper(); + + // delete channels + mHelper.deleteNotificationChannel(PKG, UID, channel.getId()); + assertTrue(mHelper.areChannelsBypassingDnd()); // channel2 can still bypass DND + verify(mMockZenModeHelper, never()).setNotificationPolicy(any()); + resetZenModeHelper(); + + mHelper.deleteNotificationChannel(PKG, UID, channel2.getId()); + assertFalse(mHelper.areChannelsBypassingDnd()); + verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any()); + resetZenModeHelper(); + } + + @Test + public void testUpdateCanChannelsBypassDnd() throws Exception { + // create notification channel that can't bypass dnd + // expected result: areChannelsBypassingDnd = false + // setNotificationPolicy isn't called since areChannelsBypassingDnd was already false + NotificationChannel channel = new NotificationChannel("id1", "name1", IMPORTANCE_LOW); + mHelper.createNotificationChannel(PKG, UID, channel, true, false); + assertFalse(mHelper.areChannelsBypassingDnd()); + verify(mMockZenModeHelper, never()).setNotificationPolicy(any()); + resetZenModeHelper(); + + // update channel so it CAN bypass dnd: + // expected result: areChannelsBypassingDnd = true + channel.setBypassDnd(true); + mHelper.updateNotificationChannel(PKG, UID, channel, true); + assertTrue(mHelper.areChannelsBypassingDnd()); + verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any()); + resetZenModeHelper(); + + // update channel so it can't bypass dnd: + // expected result: areChannelsBypassingDnd = false + channel.setBypassDnd(false); + mHelper.updateNotificationChannel(PKG, UID, channel, true); + assertFalse(mHelper.areChannelsBypassingDnd()); + verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any()); + resetZenModeHelper(); + } + + @Test + public void testSetupNewZenModeHelper_canBypass() { + // start notification policy off with mAreChannelsBypassingDnd = true, but + // RankingHelper should change to false + mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0, + NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND); + when(mMockZenModeHelper.getNotificationPolicy()).thenReturn(mTestNotificationPolicy); + mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper); + assertFalse(mHelper.areChannelsBypassingDnd()); + verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any()); + resetZenModeHelper(); + } + + @Test + public void testSetupNewZenModeHelper_cannotBypass() { + // start notification policy off with mAreChannelsBypassingDnd = false + mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0, 0); + when(mMockZenModeHelper.getNotificationPolicy()).thenReturn(mTestNotificationPolicy); + mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper); + assertFalse(mHelper.areChannelsBypassingDnd()); + verify(mMockZenModeHelper, never()).setNotificationPolicy(any()); + resetZenModeHelper(); + } + + @Test + public void testCreateDeletedChannel() throws Exception { + long[] vibration = new long[]{100, 67, 145, 156}; + NotificationChannel channel = + new NotificationChannel("id2", "name2", IMPORTANCE_LOW); + channel.setVibrationPattern(vibration); + + mHelper.createNotificationChannel(PKG, UID, channel, true, false); + mHelper.deleteNotificationChannel(PKG, UID, channel.getId()); + + NotificationChannel newChannel = new NotificationChannel( + channel.getId(), channel.getName(), NotificationManager.IMPORTANCE_HIGH); + newChannel.setVibrationPattern(new long[]{100}); + + mHelper.createNotificationChannel(PKG, UID, newChannel, true, false); + + // No long deleted, using old settings + compareChannels(channel, + mHelper.getNotificationChannel(PKG, UID, newChannel.getId(), false)); + } + + @Test + public void testOnlyHasDefaultChannel() throws Exception { + assertTrue(mHelper.onlyHasDefaultChannel(PKG, UID)); + assertFalse(mHelper.onlyHasDefaultChannel(UPDATED_PKG, UID2)); + + mHelper.createNotificationChannel(PKG, UID, getChannel(), true, false); + assertFalse(mHelper.onlyHasDefaultChannel(PKG, UID)); + } + + @Test + public void testCreateChannel_defaultChannelId() throws Exception { + try { + mHelper.createNotificationChannel(PKG, UID, new NotificationChannel( + NotificationChannel.DEFAULT_CHANNEL_ID, "ha", IMPORTANCE_HIGH), true, false); + fail("Allowed to create default channel"); + } catch (IllegalArgumentException e) { + // pass + } + } + + @Test + public void testCreateChannel_alreadyExists() throws Exception { + long[] vibration = new long[]{100, 67, 145, 156}; + NotificationChannel channel = + new NotificationChannel("id2", "name2", IMPORTANCE_LOW); + channel.setVibrationPattern(vibration); + + mHelper.createNotificationChannel(PKG, UID, channel, true, false); + + NotificationChannel newChannel = new NotificationChannel( + channel.getId(), channel.getName(), NotificationManager.IMPORTANCE_HIGH); + newChannel.setVibrationPattern(new long[]{100}); + + mHelper.createNotificationChannel(PKG, UID, newChannel, true, false); + + // Old settings not overridden + compareChannels(channel, + mHelper.getNotificationChannel(PKG, UID, newChannel.getId(), false)); + } + + @Test + public void testCreateChannel_noOverrideSound() throws Exception { + Uri sound = new Uri.Builder().scheme("test").build(); + final NotificationChannel channel = new NotificationChannel("id2", "name2", + NotificationManager.IMPORTANCE_DEFAULT); + channel.setSound(sound, mAudioAttributes); + mHelper.createNotificationChannel(PKG, UID, channel, true, false); + assertEquals(sound, mHelper.getNotificationChannel( + PKG, UID, channel.getId(), false).getSound()); + } + + @Test + public void testPermanentlyDeleteChannels() throws Exception { + NotificationChannel channel1 = + new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); + NotificationChannel channel2 = + new NotificationChannel("id2", "name2", IMPORTANCE_LOW); + + mHelper.createNotificationChannel(PKG, UID, channel1, true, false); + mHelper.createNotificationChannel(PKG, UID, channel2, false, false); + + mHelper.permanentlyDeleteNotificationChannels(PKG, UID); + + // Only default channel remains + assertEquals(1, mHelper.getNotificationChannels(PKG, UID, true).getList().size()); + } + + @Test + public void testDeleteGroup() throws Exception { + NotificationChannelGroup notDeleted = new NotificationChannelGroup("not", "deleted"); + NotificationChannelGroup deleted = new NotificationChannelGroup("totally", "deleted"); + NotificationChannel nonGroupedNonDeletedChannel = + new NotificationChannel("no group", "so not deleted", IMPORTANCE_HIGH); + NotificationChannel groupedButNotDeleted = + new NotificationChannel("not deleted", "belongs to notDeleted", IMPORTANCE_DEFAULT); + groupedButNotDeleted.setGroup("not"); + NotificationChannel groupedAndDeleted = + new NotificationChannel("deleted", "belongs to deleted", IMPORTANCE_DEFAULT); + groupedAndDeleted.setGroup("totally"); + + mHelper.createNotificationChannelGroup(PKG, UID, notDeleted, true); + mHelper.createNotificationChannelGroup(PKG, UID, deleted, true); + mHelper.createNotificationChannel(PKG, UID, nonGroupedNonDeletedChannel, true, false); + mHelper.createNotificationChannel(PKG, UID, groupedAndDeleted, true, false); + mHelper.createNotificationChannel(PKG, UID, groupedButNotDeleted, true, false); + + mHelper.deleteNotificationChannelGroup(PKG, UID, deleted.getId()); + + assertNull(mHelper.getNotificationChannelGroup(deleted.getId(), PKG, UID)); + assertNotNull(mHelper.getNotificationChannelGroup(notDeleted.getId(), PKG, UID)); + + assertNull(mHelper.getNotificationChannel(PKG, UID, groupedAndDeleted.getId(), false)); + compareChannels(groupedAndDeleted, + mHelper.getNotificationChannel(PKG, UID, groupedAndDeleted.getId(), true)); + + compareChannels(groupedButNotDeleted, + mHelper.getNotificationChannel(PKG, UID, groupedButNotDeleted.getId(), false)); + compareChannels(nonGroupedNonDeletedChannel, mHelper.getNotificationChannel( + PKG, UID, nonGroupedNonDeletedChannel.getId(), false)); + + // notDeleted + assertEquals(1, mHelper.getNotificationChannelGroups(PKG, UID).size()); + + verify(mHandler, never()).requestSort(); + } + + @Test + public void testOnUserRemoved() throws Exception { + int[] user0Uids = {98, 235, 16, 3782}; + int[] user1Uids = new int[user0Uids.length]; + for (int i = 0; i < user0Uids.length; i++) { + user1Uids[i] = UserHandle.PER_USER_RANGE + user0Uids[i]; + + final ApplicationInfo legacy = new ApplicationInfo(); + legacy.targetSdkVersion = Build.VERSION_CODES.N_MR1; + when(mPm.getApplicationInfoAsUser(eq(PKG), anyInt(), anyInt())).thenReturn(legacy); + + // create records with the default channel for all user 0 and user 1 uids + mHelper.getImportance(PKG, user0Uids[i]); + mHelper.getImportance(PKG, user1Uids[i]); + } + + mHelper.onUserRemoved(1); + + // user 0 records remain + for (int i = 0; i < user0Uids.length; i++) { + assertEquals(1, + mHelper.getNotificationChannels(PKG, user0Uids[i], false).getList().size()); + } + // user 1 records are gone + for (int i = 0; i < user1Uids.length; i++) { + assertEquals(0, + mHelper.getNotificationChannels(PKG, user1Uids[i], false).getList().size()); + } + } + + @Test + public void testOnPackageChanged_packageRemoval() throws Exception { + // Deleted + NotificationChannel channel1 = + new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); + mHelper.createNotificationChannel(PKG, UID, channel1, true, false); + + mHelper.onPackagesChanged(true, UserHandle.USER_SYSTEM, new String[]{PKG}, new int[]{UID}); + + assertEquals(0, mHelper.getNotificationChannels(PKG, UID, true).getList().size()); + + // Not deleted + mHelper.createNotificationChannel(PKG, UID, channel1, true, false); + + mHelper.onPackagesChanged(false, UserHandle.USER_SYSTEM, new String[]{PKG}, new int[]{UID}); + assertEquals(2, mHelper.getNotificationChannels(PKG, UID, false).getList().size()); + } + + @Test + public void testOnPackageChanged_packageRemoval_importance() throws Exception { + mHelper.setImportance(PKG, UID, NotificationManager.IMPORTANCE_HIGH); + + mHelper.onPackagesChanged(true, UserHandle.USER_SYSTEM, new String[]{PKG}, new int[]{UID}); + + assertEquals(NotificationManager.IMPORTANCE_UNSPECIFIED, mHelper.getImportance(PKG, UID)); + } + + @Test + public void testOnPackageChanged_packageRemoval_groups() throws Exception { + NotificationChannelGroup ncg = new NotificationChannelGroup("group1", "name1"); + mHelper.createNotificationChannelGroup(PKG, UID, ncg, true); + NotificationChannelGroup ncg2 = new NotificationChannelGroup("group2", "name2"); + mHelper.createNotificationChannelGroup(PKG, UID, ncg2, true); + + mHelper.onPackagesChanged(true, UserHandle.USER_SYSTEM, new String[]{PKG}, new int[]{UID}); + + assertEquals(0, + mHelper.getNotificationChannelGroups(PKG, UID, true, true).getList().size()); + } + + @Test + public void testOnPackageChange_downgradeTargetSdk() throws Exception { + // create channel as api 26 + mHelper.createNotificationChannel(UPDATED_PKG, UID2, getChannel(), true, false); + + // install new app version targeting 25 + final ApplicationInfo legacy = new ApplicationInfo(); + legacy.targetSdkVersion = Build.VERSION_CODES.N_MR1; + when(mPm.getApplicationInfoAsUser(eq(UPDATED_PKG), anyInt(), anyInt())).thenReturn(legacy); + mHelper.onPackagesChanged( + false, UserHandle.USER_SYSTEM, new String[]{UPDATED_PKG}, new int[]{UID2}); + + // make sure the default channel was readded + //assertEquals(2, mHelper.getNotificationChannels(UPDATED_PKG, UID2, false).getList().size()); + assertNotNull(mHelper.getNotificationChannel( + UPDATED_PKG, UID2, NotificationChannel.DEFAULT_CHANNEL_ID, false)); + } + + @Test + public void testRecordDefaults() throws Exception { + assertEquals(NotificationManager.IMPORTANCE_UNSPECIFIED, mHelper.getImportance(PKG, UID)); + assertEquals(true, mHelper.canShowBadge(PKG, UID)); + assertEquals(1, mHelper.getNotificationChannels(PKG, UID, false).getList().size()); + } + + @Test + public void testCreateGroup() throws Exception { + NotificationChannelGroup ncg = new NotificationChannelGroup("group1", "name1"); + mHelper.createNotificationChannelGroup(PKG, UID, ncg, true); + assertEquals(ncg, mHelper.getNotificationChannelGroups(PKG, UID).iterator().next()); + verify(mHandler, never()).requestSort(); + } + + @Test + public void testCannotCreateChannel_badGroup() throws Exception { + NotificationChannel channel1 = + new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); + channel1.setGroup("garbage"); + try { + mHelper.createNotificationChannel(PKG, UID, channel1, true, false); + fail("Created a channel with a bad group"); + } catch (IllegalArgumentException e) { + } + } + + @Test + public void testCannotCreateChannel_goodGroup() throws Exception { + NotificationChannelGroup ncg = new NotificationChannelGroup("group1", "name1"); + mHelper.createNotificationChannelGroup(PKG, UID, ncg, true); + NotificationChannel channel1 = + new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); + channel1.setGroup(ncg.getId()); + mHelper.createNotificationChannel(PKG, UID, channel1, true, false); + + assertEquals(ncg.getId(), + mHelper.getNotificationChannel(PKG, UID, channel1.getId(), false).getGroup()); + } + + @Test + public void testGetChannelGroups() throws Exception { + NotificationChannelGroup unused = new NotificationChannelGroup("unused", "s"); + mHelper.createNotificationChannelGroup(PKG, UID, unused, true); + NotificationChannelGroup ncg = new NotificationChannelGroup("group1", "name1"); + mHelper.createNotificationChannelGroup(PKG, UID, ncg, true); + NotificationChannelGroup ncg2 = new NotificationChannelGroup("group2", "name2"); + mHelper.createNotificationChannelGroup(PKG, UID, ncg2, true); + + NotificationChannel channel1 = + new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); + channel1.setGroup(ncg.getId()); + mHelper.createNotificationChannel(PKG, UID, channel1, true, false); + NotificationChannel channel1a = + new NotificationChannel("id1a", "name1", NotificationManager.IMPORTANCE_HIGH); + channel1a.setGroup(ncg.getId()); + mHelper.createNotificationChannel(PKG, UID, channel1a, true, false); + + NotificationChannel channel2 = + new NotificationChannel("id2", "name1", NotificationManager.IMPORTANCE_HIGH); + channel2.setGroup(ncg2.getId()); + mHelper.createNotificationChannel(PKG, UID, channel2, true, false); + + NotificationChannel channel3 = + new NotificationChannel("id3", "name1", NotificationManager.IMPORTANCE_HIGH); + mHelper.createNotificationChannel(PKG, UID, channel3, true, false); + + List<NotificationChannelGroup> actual = + mHelper.getNotificationChannelGroups(PKG, UID, true, true).getList(); + assertEquals(3, actual.size()); + for (NotificationChannelGroup group : actual) { + if (group.getId() == null) { + assertEquals(2, group.getChannels().size()); // misc channel too + assertTrue(channel3.getId().equals(group.getChannels().get(0).getId()) + || channel3.getId().equals(group.getChannels().get(1).getId())); + } else if (group.getId().equals(ncg.getId())) { + assertEquals(2, group.getChannels().size()); + if (group.getChannels().get(0).getId().equals(channel1.getId())) { + assertTrue(group.getChannels().get(1).getId().equals(channel1a.getId())); + } else if (group.getChannels().get(0).getId().equals(channel1a.getId())) { + assertTrue(group.getChannels().get(1).getId().equals(channel1.getId())); + } else { + fail("expected channel not found"); + } + } else if (group.getId().equals(ncg2.getId())) { + assertEquals(1, group.getChannels().size()); + assertEquals(channel2.getId(), group.getChannels().get(0).getId()); + } + } + } + + @Test + public void testGetChannelGroups_noSideEffects() throws Exception { + NotificationChannelGroup ncg = new NotificationChannelGroup("group1", "name1"); + mHelper.createNotificationChannelGroup(PKG, UID, ncg, true); + + NotificationChannel channel1 = + new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); + channel1.setGroup(ncg.getId()); + mHelper.createNotificationChannel(PKG, UID, channel1, true, false); + mHelper.getNotificationChannelGroups(PKG, UID, true, true).getList(); + + channel1.setImportance(IMPORTANCE_LOW); + mHelper.updateNotificationChannel(PKG, UID, channel1, true); + + List<NotificationChannelGroup> actual = + mHelper.getNotificationChannelGroups(PKG, UID, true, true).getList(); + + assertEquals(2, actual.size()); + for (NotificationChannelGroup group : actual) { + if (Objects.equals(group.getId(), ncg.getId())) { + assertEquals(1, group.getChannels().size()); + } + } + } + + @Test + public void testCreateChannel_updateName() throws Exception { + NotificationChannel nc = new NotificationChannel("id", "hello", IMPORTANCE_DEFAULT); + mHelper.createNotificationChannel(PKG, UID, nc, true, false); + NotificationChannel actual = mHelper.getNotificationChannel(PKG, UID, "id", false); + assertEquals("hello", actual.getName()); + + nc = new NotificationChannel("id", "goodbye", IMPORTANCE_HIGH); + mHelper.createNotificationChannel(PKG, UID, nc, true, false); + + actual = mHelper.getNotificationChannel(PKG, UID, "id", false); + assertEquals("goodbye", actual.getName()); + assertEquals(IMPORTANCE_DEFAULT, actual.getImportance()); + + verify(mHandler, times(1)).requestSort(); + } + + @Test + public void testCreateChannel_addToGroup() throws Exception { + NotificationChannelGroup group = new NotificationChannelGroup("group", ""); + mHelper.createNotificationChannelGroup(PKG, UID, group, true); + NotificationChannel nc = new NotificationChannel("id", "hello", IMPORTANCE_DEFAULT); + mHelper.createNotificationChannel(PKG, UID, nc, true, false); + NotificationChannel actual = mHelper.getNotificationChannel(PKG, UID, "id", false); + assertNull(actual.getGroup()); + + nc = new NotificationChannel("id", "hello", IMPORTANCE_HIGH); + nc.setGroup(group.getId()); + mHelper.createNotificationChannel(PKG, UID, nc, true, false); + + actual = mHelper.getNotificationChannel(PKG, UID, "id", false); + assertNotNull(actual.getGroup()); + assertEquals(IMPORTANCE_DEFAULT, actual.getImportance()); + + verify(mHandler, times(1)).requestSort(); + } + + @Test + public void testDumpChannelsJson() throws Exception { + final ApplicationInfo upgrade = new ApplicationInfo(); + upgrade.targetSdkVersion = Build.VERSION_CODES.O; + try { + when(mPm.getApplicationInfoAsUser( + anyString(), anyInt(), anyInt())).thenReturn(upgrade); + } catch (PackageManager.NameNotFoundException e) { + } + ArrayMap<String, Integer> expectedChannels = new ArrayMap<>(); + int numPackages = ThreadLocalRandom.current().nextInt(1, 5); + for (int i = 0; i < numPackages; i++) { + String pkgName = "pkg" + i; + int numChannels = ThreadLocalRandom.current().nextInt(1, 10); + for (int j = 0; j < numChannels; j++) { + mHelper.createNotificationChannel(pkgName, UID, + new NotificationChannel("" + j, "a", IMPORTANCE_HIGH), true, false); + } + expectedChannels.put(pkgName, numChannels); + } + + // delete the first channel of the first package + String pkg = expectedChannels.keyAt(0); + mHelper.deleteNotificationChannel("pkg" + 0, UID, "0"); + // dump should not include deleted channels + int count = expectedChannels.get(pkg); + expectedChannels.put(pkg, count - 1); + + JSONArray actual = mHelper.dumpChannelsJson(new NotificationManagerService.DumpFilter()); + assertEquals(numPackages, actual.length()); + for (int i = 0; i < numPackages; i++) { + JSONObject object = actual.getJSONObject(i); + assertTrue(expectedChannels.containsKey(object.get("packageName"))); + assertEquals(expectedChannels.get(object.get("packageName")).intValue(), + object.getInt("channelCount")); + } + } + + @Test + public void testBadgingOverrideTrue() throws Exception { + Secure.putIntForUser(getContext().getContentResolver(), + Secure.NOTIFICATION_BADGING, 1, + USER.getIdentifier()); + mHelper.updateBadgingEnabled(); // would be called by settings observer + assertTrue(mHelper.badgingEnabled(USER)); + } + + @Test + public void testBadgingOverrideFalse() throws Exception { + Secure.putIntForUser(getContext().getContentResolver(), + Secure.NOTIFICATION_BADGING, 0, + USER.getIdentifier()); + mHelper.updateBadgingEnabled(); // would be called by settings observer + assertFalse(mHelper.badgingEnabled(USER)); + } + + @Test + public void testBadgingForUserAll() throws Exception { + try { + mHelper.badgingEnabled(UserHandle.ALL); + } catch (Exception e) { + fail("just don't throw"); + } + } + + @Test + public void testBadgingOverrideUserIsolation() throws Exception { + Secure.putIntForUser(getContext().getContentResolver(), + Secure.NOTIFICATION_BADGING, 0, + USER.getIdentifier()); + Secure.putIntForUser(getContext().getContentResolver(), + Secure.NOTIFICATION_BADGING, 1, + USER2.getIdentifier()); + mHelper.updateBadgingEnabled(); // would be called by settings observer + assertFalse(mHelper.badgingEnabled(USER)); + assertTrue(mHelper.badgingEnabled(USER2)); + } + + @Test + public void testOnLocaleChanged_updatesDefaultChannels() throws Exception { + String newLabel = "bananas!"; + final NotificationChannel defaultChannel = mHelper.getNotificationChannel(PKG, UID, + NotificationChannel.DEFAULT_CHANNEL_ID, false); + assertFalse(newLabel.equals(defaultChannel.getName())); + + Resources res = mock(Resources.class); + when(mContext.getResources()).thenReturn(res); + when(res.getString(com.android.internal.R.string.default_notification_channel_label)) + .thenReturn(newLabel); + + mHelper.onLocaleChanged(mContext, USER.getIdentifier()); + + assertEquals(newLabel, mHelper.getNotificationChannel(PKG, UID, + NotificationChannel.DEFAULT_CHANNEL_ID, false).getName()); + } + + @Test + public void testIsGroupBlocked_noGroup() throws Exception { + assertFalse(mHelper.isGroupBlocked(PKG, UID, null)); + + assertFalse(mHelper.isGroupBlocked(PKG, UID, "non existent group")); + } + + @Test + public void testIsGroupBlocked_notBlocked() throws Exception { + NotificationChannelGroup group = new NotificationChannelGroup("id", "name"); + mHelper.createNotificationChannelGroup(PKG, UID, group, true); + + assertFalse(mHelper.isGroupBlocked(PKG, UID, group.getId())); + } + + @Test + public void testIsGroupBlocked_blocked() throws Exception { + NotificationChannelGroup group = new NotificationChannelGroup("id", "name"); + mHelper.createNotificationChannelGroup(PKG, UID, group, true); + group.setBlocked(true); + mHelper.createNotificationChannelGroup(PKG, UID, group, false); + + assertTrue(mHelper.isGroupBlocked(PKG, UID, group.getId())); + } + + @Test + public void testIsGroup_appCannotResetBlock() throws Exception { + NotificationChannelGroup group = new NotificationChannelGroup("id", "name"); + mHelper.createNotificationChannelGroup(PKG, UID, group, true); + NotificationChannelGroup group2 = group.clone(); + group2.setBlocked(true); + mHelper.createNotificationChannelGroup(PKG, UID, group2, false); + assertTrue(mHelper.isGroupBlocked(PKG, UID, group.getId())); + + NotificationChannelGroup group3 = group.clone(); + group3.setBlocked(false); + mHelper.createNotificationChannelGroup(PKG, UID, group3, true); + assertTrue(mHelper.isGroupBlocked(PKG, UID, group.getId())); + } + + @Test + public void testGetNotificationChannelGroupWithChannels() throws Exception { + NotificationChannelGroup group = new NotificationChannelGroup("group", ""); + NotificationChannelGroup other = new NotificationChannelGroup("something else", ""); + mHelper.createNotificationChannelGroup(PKG, UID, group, true); + mHelper.createNotificationChannelGroup(PKG, UID, other, true); + + NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_DEFAULT); + a.setGroup(group.getId()); + NotificationChannel b = new NotificationChannel("b", "b", IMPORTANCE_DEFAULT); + b.setGroup(other.getId()); + NotificationChannel c = new NotificationChannel("c", "c", IMPORTANCE_DEFAULT); + c.setGroup(group.getId()); + NotificationChannel d = new NotificationChannel("d", "d", IMPORTANCE_DEFAULT); + + mHelper.createNotificationChannel(PKG, UID, a, true, false); + mHelper.createNotificationChannel(PKG, UID, b, true, false); + mHelper.createNotificationChannel(PKG, UID, c, true, false); + mHelper.createNotificationChannel(PKG, UID, d, true, false); + mHelper.deleteNotificationChannel(PKG, UID, c.getId()); + + NotificationChannelGroup retrieved = mHelper.getNotificationChannelGroupWithChannels( + PKG, UID, group.getId(), true); + assertEquals(2, retrieved.getChannels().size()); + compareChannels(a, findChannel(retrieved.getChannels(), a.getId())); + compareChannels(c, findChannel(retrieved.getChannels(), c.getId())); + + retrieved = mHelper.getNotificationChannelGroupWithChannels( + PKG, UID, group.getId(), false); + assertEquals(1, retrieved.getChannels().size()); + compareChannels(a, findChannel(retrieved.getChannels(), a.getId())); + } + + @Test + public void testAndroidPkgCannotBypassDnd_creation() { + NotificationChannel test = new NotificationChannel("A", "a", IMPORTANCE_LOW); + test.setBypassDnd(true); + + mHelper.createNotificationChannel(SYSTEM_PKG, SYSTEM_UID, test, true, false); + + assertFalse(mHelper.getNotificationChannel(SYSTEM_PKG, SYSTEM_UID, "A", false) + .canBypassDnd()); + } + + @Test + public void testDndPkgCanBypassDnd_creation() { + NotificationChannel test = new NotificationChannel("A", "a", IMPORTANCE_LOW); + test.setBypassDnd(true); + + mHelper.createNotificationChannel(PKG, UID, test, true, true); + + assertTrue(mHelper.getNotificationChannel(PKG, UID, "A", false).canBypassDnd()); + } + + @Test + public void testNormalPkgCannotBypassDnd_creation() { + NotificationChannel test = new NotificationChannel("A", "a", IMPORTANCE_LOW); + test.setBypassDnd(true); + + mHelper.createNotificationChannel(PKG, 1000, test, true, false); + + assertFalse(mHelper.getNotificationChannel(PKG, 1000, "A", false).canBypassDnd()); + } + + @Test + public void testAndroidPkgCannotBypassDnd_update() throws Exception { + NotificationChannel test = new NotificationChannel("A", "a", IMPORTANCE_LOW); + mHelper.createNotificationChannel(SYSTEM_PKG, SYSTEM_UID, test, true, false); + + NotificationChannel update = new NotificationChannel("A", "a", IMPORTANCE_LOW); + update.setBypassDnd(true); + mHelper.createNotificationChannel(SYSTEM_PKG, SYSTEM_UID, update, true, false); + + assertFalse(mHelper.getNotificationChannel(SYSTEM_PKG, SYSTEM_UID, "A", false) + .canBypassDnd()); + } + + @Test + public void testDndPkgCanBypassDnd_update() throws Exception { + NotificationChannel test = new NotificationChannel("A", "a", IMPORTANCE_LOW); + mHelper.createNotificationChannel(PKG, UID, test, true, true); + + NotificationChannel update = new NotificationChannel("A", "a", IMPORTANCE_LOW); + update.setBypassDnd(true); + mHelper.createNotificationChannel(PKG, UID, update, true, true); + + assertTrue(mHelper.getNotificationChannel(PKG, UID, "A", false).canBypassDnd()); + } + + @Test + public void testNormalPkgCannotBypassDnd_update() { + NotificationChannel test = new NotificationChannel("A", "a", IMPORTANCE_LOW); + mHelper.createNotificationChannel(PKG, 1000, test, true, false); + NotificationChannel update = new NotificationChannel("A", "a", IMPORTANCE_LOW); + update.setBypassDnd(true); + mHelper.createNotificationChannel(PKG, 1000, update, true, false); + assertFalse(mHelper.getNotificationChannel(PKG, 1000, "A", false).canBypassDnd()); + } + + @Test + public void testGetBlockedAppCount_noApps() { + assertEquals(0, mHelper.getBlockedAppCount(0)); + } + + @Test + public void testGetBlockedAppCount_noAppsForUserId() { + mHelper.setEnabled(PKG, 100, false); + assertEquals(0, mHelper.getBlockedAppCount(9)); + } + + @Test + public void testGetBlockedAppCount_appsForUserId() { + mHelper.setEnabled(PKG, 1020, false); + mHelper.setEnabled(PKG, 1030, false); + mHelper.setEnabled(PKG, 1060, false); + mHelper.setEnabled(PKG, 1000, true); + assertEquals(3, mHelper.getBlockedAppCount(0)); + } +} diff --git a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java index 98c6ec42207f..7e0fcc908015 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java @@ -15,35 +15,17 @@ */ package com.android.server.notification; -import static android.app.NotificationManager.IMPORTANCE_DEFAULT; -import static android.app.NotificationManager.IMPORTANCE_HIGH; import static android.app.NotificationManager.IMPORTANCE_LOW; -import static android.app.NotificationManager.IMPORTANCE_MAX; -import static android.app.NotificationManager.IMPORTANCE_NONE; -import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; -import static junit.framework.Assert.assertNull; -import static junit.framework.Assert.fail; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Matchers.anyInt; -import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Notification; import android.app.NotificationChannel; -import android.app.NotificationChannelGroup; import android.app.NotificationManager; import android.content.ContentProvider; import android.content.Context; @@ -52,46 +34,26 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.Signature; -import android.content.res.Resources; -import android.graphics.Color; import android.media.AudioAttributes; import android.net.Uri; import android.os.Build; import android.os.UserHandle; -import android.provider.Settings; import android.provider.Settings.Secure; import android.service.notification.StatusBarNotification; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; import android.testing.TestableContentResolver; -import android.util.ArrayMap; -import android.util.Xml; -import com.android.internal.util.FastXmlSerializer; import com.android.server.UiServiceTestCase; -import org.json.JSONArray; -import org.json.JSONObject; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlSerializer; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.ThreadLocalRandom; @SmallTest @RunWith(AndroidJUnit4.class) @@ -118,6 +80,7 @@ public class RankingHelperTest extends UiServiceTestCase { @Mock IContentProvider mTestIContentProvider; @Mock Context mContext; @Mock ZenModeHelper mMockZenModeHelper; + @Mock RankingConfig mConfig; private NotificationManager.Policy mTestNotificationPolicy; private Notification mNotiGroupGSortA; @@ -179,9 +142,8 @@ public class RankingHelperTest extends UiServiceTestCase { mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0, NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND); when(mMockZenModeHelper.getNotificationPolicy()).thenReturn(mTestNotificationPolicy); - mHelper = new RankingHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mHelper = new RankingHelper(getContext(), mHandler, mConfig, mMockZenModeHelper, mUsageStats, new String[] {ImportanceExtractor.class.getName()}); - resetZenModeHelper(); mNotiGroupGSortA = new Notification.Builder(mContext, TEST_CHANNEL_ID) .setContentTitle("A") @@ -240,74 +202,6 @@ public class RankingHelperTest extends UiServiceTestCase { IMPORTANCE_LOW); } - private ByteArrayOutputStream writeXmlAndPurge(String pkg, int uid, boolean forBackup, - String... channelIds) - throws Exception { - XmlSerializer serializer = new FastXmlSerializer(); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - serializer.setOutput(new BufferedOutputStream(baos), "utf-8"); - serializer.startDocument(null, true); - mHelper.writeXml(serializer, forBackup); - serializer.endDocument(); - serializer.flush(); - for (String channelId : channelIds) { - mHelper.permanentlyDeleteNotificationChannel(pkg, uid, channelId); - } - return baos; - } - - private void loadStreamXml(ByteArrayOutputStream stream, boolean forRestore) throws Exception { - loadByteArrayXml(stream.toByteArray(), forRestore); - } - - private void loadByteArrayXml(byte[] byteArray, boolean forRestore) throws Exception { - XmlPullParser parser = Xml.newPullParser(); - parser.setInput(new BufferedInputStream(new ByteArrayInputStream(byteArray)), null); - parser.nextTag(); - mHelper.readXml(parser, forRestore); - } - - private void compareChannels(NotificationChannel expected, NotificationChannel actual) { - assertEquals(expected.getId(), actual.getId()); - assertEquals(expected.getName(), actual.getName()); - assertEquals(expected.getDescription(), actual.getDescription()); - assertEquals(expected.shouldVibrate(), actual.shouldVibrate()); - assertEquals(expected.shouldShowLights(), actual.shouldShowLights()); - assertEquals(expected.getImportance(), actual.getImportance()); - assertEquals(expected.getLockscreenVisibility(), actual.getLockscreenVisibility()); - assertEquals(expected.getSound(), actual.getSound()); - assertEquals(expected.canBypassDnd(), actual.canBypassDnd()); - assertTrue(Arrays.equals(expected.getVibrationPattern(), actual.getVibrationPattern())); - assertEquals(expected.getGroup(), actual.getGroup()); - assertEquals(expected.getAudioAttributes(), actual.getAudioAttributes()); - assertEquals(expected.getLightColor(), actual.getLightColor()); - } - - private void compareGroups(NotificationChannelGroup expected, NotificationChannelGroup actual) { - assertEquals(expected.getId(), actual.getId()); - assertEquals(expected.getName(), actual.getName()); - assertEquals(expected.getDescription(), actual.getDescription()); - assertEquals(expected.isBlocked(), actual.isBlocked()); - } - - private NotificationChannel getChannel() { - return new NotificationChannel("id", "name", IMPORTANCE_LOW); - } - - private NotificationChannel findChannel(List<NotificationChannel> channels, String id) { - for (NotificationChannel channel : channels) { - if (channel.getId().equals(id)) { - return channel; - } - } - return null; - } - - private void resetZenModeHelper() { - reset(mMockZenModeHelper); - when(mMockZenModeHelper.getNotificationPolicy()).thenReturn(mTestNotificationPolicy); - } - @Test public void testFindAfterRankingWithASplitGroup() throws Exception { ArrayList<NotificationRecord> notificationList = new ArrayList<NotificationRecord>(3); @@ -357,1496 +251,4 @@ public class RankingHelperTest extends UiServiceTestCase { ArrayList<NotificationRecord> notificationList = new ArrayList<NotificationRecord>(); mHelper.sort(notificationList); } - - @Test - public void testChannelXml() throws Exception { - NotificationChannelGroup ncg = new NotificationChannelGroup("1", "bye"); - ncg.setBlocked(true); - ncg.setDescription("group desc"); - NotificationChannelGroup ncg2 = new NotificationChannelGroup("2", "hello"); - NotificationChannel channel1 = - new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); - NotificationChannel channel2 = - new NotificationChannel("id2", "name2", IMPORTANCE_LOW); - channel2.setDescription("descriptions for all"); - channel2.setSound(new Uri.Builder().scheme("test").build(), mAudioAttributes); - channel2.enableLights(true); - channel2.setBypassDnd(true); - channel2.setLockscreenVisibility(Notification.VISIBILITY_SECRET); - channel2.enableVibration(true); - channel2.setGroup(ncg.getId()); - channel2.setVibrationPattern(new long[]{100, 67, 145, 156}); - channel2.setLightColor(Color.BLUE); - - mHelper.createNotificationChannelGroup(PKG, UID, ncg, true); - mHelper.createNotificationChannelGroup(PKG, UID, ncg2, true); - mHelper.createNotificationChannel(PKG, UID, channel1, true, false); - mHelper.createNotificationChannel(PKG, UID, channel2, false, false); - - mHelper.setShowBadge(PKG, UID, true); - mHelper.setAppImportanceLocked(PKG, UID); - - ByteArrayOutputStream baos = writeXmlAndPurge(PKG, UID, false, channel1.getId(), - channel2.getId(), NotificationChannel.DEFAULT_CHANNEL_ID); - mHelper.onPackagesChanged(true, UserHandle.myUserId(), new String[]{PKG}, new int[]{UID}); - - loadStreamXml(baos, false); - - assertTrue(mHelper.canShowBadge(PKG, UID)); - assertTrue(mHelper.getIsAppImportanceLocked(PKG, UID)); - assertEquals(channel1, mHelper.getNotificationChannel(PKG, UID, channel1.getId(), false)); - compareChannels(channel2, - mHelper.getNotificationChannel(PKG, UID, channel2.getId(), false)); - - List<NotificationChannelGroup> actualGroups = - mHelper.getNotificationChannelGroups(PKG, UID, false, true).getList(); - boolean foundNcg = false; - for (NotificationChannelGroup actual : actualGroups) { - if (ncg.getId().equals(actual.getId())) { - foundNcg = true; - compareGroups(ncg, actual); - } else if (ncg2.getId().equals(actual.getId())) { - compareGroups(ncg2, actual); - } - } - assertTrue(foundNcg); - - boolean foundChannel2Group = false; - for (NotificationChannelGroup actual : actualGroups) { - if (channel2.getGroup().equals(actual.getChannels().get(0).getGroup())) { - foundChannel2Group = true; - break; - } - } - assertTrue(foundChannel2Group); - } - - @Test - public void testChannelXmlForBackup() throws Exception { - NotificationChannelGroup ncg = new NotificationChannelGroup("1", "bye"); - NotificationChannelGroup ncg2 = new NotificationChannelGroup("2", "hello"); - NotificationChannel channel1 = - new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); - NotificationChannel channel2 = - new NotificationChannel("id2", "name2", IMPORTANCE_LOW); - channel2.setDescription("descriptions for all"); - channel2.setSound(SOUND_URI, mAudioAttributes); - channel2.enableLights(true); - channel2.setBypassDnd(true); - channel2.setLockscreenVisibility(Notification.VISIBILITY_SECRET); - channel2.enableVibration(false); - channel2.setGroup(ncg.getId()); - channel2.setLightColor(Color.BLUE); - NotificationChannel channel3 = new NotificationChannel("id3", "NAM3", IMPORTANCE_HIGH); - channel3.enableVibration(true); - - mHelper.createNotificationChannelGroup(PKG, UID, ncg, true); - mHelper.createNotificationChannelGroup(PKG, UID, ncg2, true); - mHelper.createNotificationChannel(PKG, UID, channel1, true, false); - mHelper.createNotificationChannel(PKG, UID, channel2, false, false); - mHelper.createNotificationChannel(PKG, UID, channel3, false, false); - mHelper.createNotificationChannel(UPDATED_PKG, UID2, getChannel(), true, false); - - mHelper.setShowBadge(PKG, UID, true); - - mHelper.setImportance(UPDATED_PKG, UID2, IMPORTANCE_NONE); - - ByteArrayOutputStream baos = writeXmlAndPurge(PKG, UID, true, channel1.getId(), - channel2.getId(), channel3.getId(), NotificationChannel.DEFAULT_CHANNEL_ID); - mHelper.onPackagesChanged(true, UserHandle.myUserId(), new String[]{PKG, UPDATED_PKG}, - new int[]{UID, UID2}); - - mHelper.setShowBadge(UPDATED_PKG, UID2, true); - - loadStreamXml(baos, true); - - assertEquals(IMPORTANCE_NONE, mHelper.getImportance(UPDATED_PKG, UID2)); - assertTrue(mHelper.canShowBadge(PKG, UID)); - assertEquals(channel1, mHelper.getNotificationChannel(PKG, UID, channel1.getId(), false)); - compareChannels(channel2, - mHelper.getNotificationChannel(PKG, UID, channel2.getId(), false)); - compareChannels(channel3, - mHelper.getNotificationChannel(PKG, UID, channel3.getId(), false)); - - List<NotificationChannelGroup> actualGroups = - mHelper.getNotificationChannelGroups(PKG, UID, false, true).getList(); - boolean foundNcg = false; - for (NotificationChannelGroup actual : actualGroups) { - if (ncg.getId().equals(actual.getId())) { - foundNcg = true; - compareGroups(ncg, actual); - } else if (ncg2.getId().equals(actual.getId())) { - compareGroups(ncg2, actual); - } - } - assertTrue(foundNcg); - - boolean foundChannel2Group = false; - for (NotificationChannelGroup actual : actualGroups) { - if (channel2.getGroup().equals(actual.getChannels().get(0).getGroup())) { - foundChannel2Group = true; - break; - } - } - assertTrue(foundChannel2Group); - } - - @Test - public void testBackupXml_backupCanonicalizedSoundUri() throws Exception { - NotificationChannel channel = - new NotificationChannel("id", "name", IMPORTANCE_LOW); - channel.setSound(SOUND_URI, mAudioAttributes); - mHelper.createNotificationChannel(PKG, UID, channel, true, false); - - ByteArrayOutputStream baos = writeXmlAndPurge(PKG, UID, true, channel.getId()); - - // Testing that in restore we are given the canonical version - loadStreamXml(baos, true); - verify(mTestIContentProvider).uncanonicalize(any(), eq(CANONICAL_SOUND_URI)); - } - - @Test - public void testRestoreXml_withExistentCanonicalizedSoundUri() throws Exception { - Uri localUri = Uri.parse("content://" + TEST_AUTHORITY + "/local/url"); - Uri canonicalBasedOnLocal = localUri.buildUpon() - .appendQueryParameter("title", "Test") - .appendQueryParameter("canonical", "1") - .build(); - when(mTestIContentProvider.canonicalize(any(), eq(CANONICAL_SOUND_URI))) - .thenReturn(canonicalBasedOnLocal); - when(mTestIContentProvider.uncanonicalize(any(), eq(CANONICAL_SOUND_URI))) - .thenReturn(localUri); - when(mTestIContentProvider.uncanonicalize(any(), eq(canonicalBasedOnLocal))) - .thenReturn(localUri); - - NotificationChannel channel = - new NotificationChannel("id", "name", IMPORTANCE_LOW); - channel.setSound(SOUND_URI, mAudioAttributes); - mHelper.createNotificationChannel(PKG, UID, channel, true, false); - ByteArrayOutputStream baos = writeXmlAndPurge(PKG, UID, true, channel.getId()); - - loadStreamXml(baos, true); - - NotificationChannel actualChannel = mHelper.getNotificationChannel( - PKG, UID, channel.getId(), false); - assertEquals(localUri, actualChannel.getSound()); - } - - @Test - public void testRestoreXml_withNonExistentCanonicalizedSoundUri() throws Exception { - Thread.sleep(3000); - when(mTestIContentProvider.canonicalize(any(), eq(CANONICAL_SOUND_URI))) - .thenReturn(null); - when(mTestIContentProvider.uncanonicalize(any(), eq(CANONICAL_SOUND_URI))) - .thenReturn(null); - - NotificationChannel channel = - new NotificationChannel("id", "name", IMPORTANCE_LOW); - channel.setSound(SOUND_URI, mAudioAttributes); - mHelper.createNotificationChannel(PKG, UID, channel, true, false); - ByteArrayOutputStream baos = writeXmlAndPurge(PKG, UID, true, channel.getId()); - - loadStreamXml(baos, true); - - NotificationChannel actualChannel = mHelper.getNotificationChannel( - PKG, UID, channel.getId(), false); - assertEquals(Settings.System.DEFAULT_NOTIFICATION_URI, actualChannel.getSound()); - } - - - /** - * Although we don't make backups with uncanonicalized uris anymore, we used to, so we have to - * handle its restore properly. - */ - @Test - public void testRestoreXml_withUncanonicalizedNonLocalSoundUri() throws Exception { - // Not a local uncanonicalized uri, simulating that it fails to exist locally - when(mTestIContentProvider.canonicalize(any(), eq(SOUND_URI))).thenReturn(null); - String id = "id"; - String backupWithUncanonicalizedSoundUri = "<ranking version=\"1\">\n" - + "<package name=\"com.android.server.notification\" show_badge=\"true\">\n" - + "<channel id=\"" + id + "\" name=\"name\" importance=\"2\" " - + "sound=\"" + SOUND_URI + "\" " - + "usage=\"6\" content_type=\"0\" flags=\"1\" show_badge=\"true\" />\n" - + "<channel id=\"miscellaneous\" name=\"Uncategorized\" usage=\"5\" " - + "content_type=\"4\" flags=\"0\" show_badge=\"true\" />\n" - + "</package>\n" - + "</ranking>\n"; - - loadByteArrayXml(backupWithUncanonicalizedSoundUri.getBytes(), true); - - NotificationChannel actualChannel = mHelper.getNotificationChannel(PKG, UID, id, false); - assertEquals(Settings.System.DEFAULT_NOTIFICATION_URI, actualChannel.getSound()); - } - - @Test - public void testBackupRestoreXml_withNullSoundUri() throws Exception { - NotificationChannel channel = - new NotificationChannel("id", "name", IMPORTANCE_LOW); - channel.setSound(null, mAudioAttributes); - mHelper.createNotificationChannel(PKG, UID, channel, true, false); - ByteArrayOutputStream baos = writeXmlAndPurge(PKG, UID, true, channel.getId()); - - loadStreamXml(baos, true); - - NotificationChannel actualChannel = mHelper.getNotificationChannel( - PKG, UID, channel.getId(), false); - assertEquals(null, actualChannel.getSound()); - } - - @Test - public void testChannelXml_backup() throws Exception { - NotificationChannelGroup ncg = new NotificationChannelGroup("1", "bye"); - NotificationChannelGroup ncg2 = new NotificationChannelGroup("2", "hello"); - NotificationChannel channel1 = - new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); - NotificationChannel channel2 = - new NotificationChannel("id2", "name2", IMPORTANCE_LOW); - NotificationChannel channel3 = - new NotificationChannel("id3", "name3", IMPORTANCE_LOW); - channel3.setGroup(ncg.getId()); - - mHelper.createNotificationChannelGroup(PKG, UID, ncg, true); - mHelper.createNotificationChannelGroup(PKG, UID, ncg2, true); - mHelper.createNotificationChannel(PKG, UID, channel1, true, false); - mHelper.createNotificationChannel(PKG, UID, channel2, false, false); - mHelper.createNotificationChannel(PKG, UID, channel3, true, false); - - mHelper.deleteNotificationChannel(PKG, UID, channel1.getId()); - mHelper.deleteNotificationChannelGroup(PKG, UID, ncg.getId()); - assertEquals(channel2, mHelper.getNotificationChannel(PKG, UID, channel2.getId(), false)); - - ByteArrayOutputStream baos = writeXmlAndPurge(PKG, UID, true, channel1.getId(), - channel2.getId(), channel3.getId(), NotificationChannel.DEFAULT_CHANNEL_ID); - mHelper.onPackagesChanged(true, UserHandle.myUserId(), new String[]{PKG}, new int[]{UID}); - - XmlPullParser parser = Xml.newPullParser(); - parser.setInput(new BufferedInputStream(new ByteArrayInputStream(baos.toByteArray())), - null); - parser.nextTag(); - mHelper.readXml(parser, true); - - assertNull(mHelper.getNotificationChannel(PKG, UID, channel1.getId(), false)); - assertNull(mHelper.getNotificationChannel(PKG, UID, channel3.getId(), false)); - assertNull(mHelper.getNotificationChannelGroup(ncg.getId(), PKG, UID)); - //assertEquals(ncg2, mHelper.getNotificationChannelGroup(ncg2.getId(), PKG, UID)); - assertEquals(channel2, mHelper.getNotificationChannel(PKG, UID, channel2.getId(), false)); - } - - @Test - public void testChannelXml_defaultChannelLegacyApp_noUserSettings() throws Exception { - ByteArrayOutputStream baos = writeXmlAndPurge(PKG, UID, false, - NotificationChannel.DEFAULT_CHANNEL_ID); - - loadStreamXml(baos, false); - - final NotificationChannel updated = mHelper.getNotificationChannel(PKG, UID, - NotificationChannel.DEFAULT_CHANNEL_ID, false); - assertEquals(NotificationManager.IMPORTANCE_UNSPECIFIED, updated.getImportance()); - assertFalse(updated.canBypassDnd()); - assertEquals(NotificationManager.VISIBILITY_NO_OVERRIDE, updated.getLockscreenVisibility()); - assertEquals(0, updated.getUserLockedFields()); - } - - @Test - public void testChannelXml_defaultChannelUpdatedApp_userSettings() throws Exception { - final NotificationChannel defaultChannel = mHelper.getNotificationChannel(PKG, UID, - NotificationChannel.DEFAULT_CHANNEL_ID, false); - defaultChannel.setImportance(NotificationManager.IMPORTANCE_LOW); - mHelper.updateNotificationChannel(PKG, UID, defaultChannel, true); - - ByteArrayOutputStream baos = writeXmlAndPurge(PKG, UID, false, - NotificationChannel.DEFAULT_CHANNEL_ID); - - loadStreamXml(baos, false); - - assertEquals(NotificationManager.IMPORTANCE_LOW, mHelper.getNotificationChannel( - PKG, UID, NotificationChannel.DEFAULT_CHANNEL_ID, false).getImportance()); - } - - @Test - public void testChannelXml_upgradeCreateDefaultChannel() throws Exception { - final String preupgradeXml = "<ranking version=\"1\">\n" - + "<package name=\"" + PKG - + "\" importance=\"" + NotificationManager.IMPORTANCE_HIGH - + "\" priority=\"" + Notification.PRIORITY_MAX + "\" visibility=\"" - + Notification.VISIBILITY_SECRET + "\"" +" uid=\"" + UID + "\" />\n" - + "<package name=\"" + UPDATED_PKG + "\" uid=\"" + UID2 + "\" visibility=\"" - + Notification.VISIBILITY_PRIVATE + "\" />\n" - + "</ranking>"; - XmlPullParser parser = Xml.newPullParser(); - parser.setInput(new BufferedInputStream(new ByteArrayInputStream(preupgradeXml.getBytes())), - null); - parser.nextTag(); - mHelper.readXml(parser, false); - - final NotificationChannel updated1 = - mHelper.getNotificationChannel(PKG, UID, NotificationChannel.DEFAULT_CHANNEL_ID, false); - assertEquals(NotificationManager.IMPORTANCE_HIGH, updated1.getImportance()); - assertTrue(updated1.canBypassDnd()); - assertEquals(Notification.VISIBILITY_SECRET, updated1.getLockscreenVisibility()); - assertEquals(NotificationChannel.USER_LOCKED_IMPORTANCE - | NotificationChannel.USER_LOCKED_PRIORITY - | NotificationChannel.USER_LOCKED_VISIBILITY, - updated1.getUserLockedFields()); - - // No Default Channel created for updated packages - assertEquals(null, mHelper.getNotificationChannel(UPDATED_PKG, UID2, - NotificationChannel.DEFAULT_CHANNEL_ID, false)); - } - - @Test - public void testChannelXml_upgradeDeletesDefaultChannel() throws Exception { - final NotificationChannel defaultChannel = mHelper.getNotificationChannel( - PKG, UID, NotificationChannel.DEFAULT_CHANNEL_ID, false); - assertTrue(defaultChannel != null); - ByteArrayOutputStream baos = - writeXmlAndPurge(PKG, UID, false, NotificationChannel.DEFAULT_CHANNEL_ID); - // Load package at higher sdk. - final ApplicationInfo upgraded = new ApplicationInfo(); - upgraded.targetSdkVersion = Build.VERSION_CODES.N_MR1 + 1; - when(mPm.getApplicationInfoAsUser(eq(PKG), anyInt(), anyInt())).thenReturn(upgraded); - loadStreamXml(baos, false); - - // Default Channel should be gone. - assertEquals(null, mHelper.getNotificationChannel(PKG, UID, - NotificationChannel.DEFAULT_CHANNEL_ID, false)); - } - - @Test - public void testDeletesDefaultChannelAfterChannelIsCreated() throws Exception { - mHelper.createNotificationChannel(PKG, UID, - new NotificationChannel("bananas", "bananas", IMPORTANCE_LOW), true, false); - ByteArrayOutputStream baos = writeXmlAndPurge(PKG, UID, false, - NotificationChannel.DEFAULT_CHANNEL_ID, "bananas"); - - // Load package at higher sdk. - final ApplicationInfo upgraded = new ApplicationInfo(); - upgraded.targetSdkVersion = Build.VERSION_CODES.N_MR1 + 1; - when(mPm.getApplicationInfoAsUser(eq(PKG), anyInt(), anyInt())).thenReturn(upgraded); - loadStreamXml(baos, false); - - // Default Channel should be gone. - assertEquals(null, mHelper.getNotificationChannel(PKG, UID, - NotificationChannel.DEFAULT_CHANNEL_ID, false)); - } - - @Test - public void testLoadingOldChannelsDoesNotDeleteNewlyCreatedChannels() throws Exception { - ByteArrayOutputStream baos = writeXmlAndPurge(PKG, UID, false, - NotificationChannel.DEFAULT_CHANNEL_ID, "bananas"); - mHelper.createNotificationChannel(PKG, UID, - new NotificationChannel("bananas", "bananas", IMPORTANCE_LOW), true, false); - - loadStreamXml(baos, false); - - // Should still have the newly created channel that wasn't in the xml. - assertTrue(mHelper.getNotificationChannel(PKG, UID, "bananas", false) != null); - } - - @Test - public void testCreateChannel_blocked() throws Exception { - mHelper.setImportance(PKG, UID, IMPORTANCE_NONE); - - mHelper.createNotificationChannel(PKG, UID, - new NotificationChannel("bananas", "bananas", IMPORTANCE_LOW), true, false); - } - - @Test - public void testCreateChannel_badImportance() throws Exception { - try { - mHelper.createNotificationChannel(PKG, UID, - new NotificationChannel("bananas", "bananas", IMPORTANCE_NONE - 1), - true, false); - fail("Was allowed to create a channel with invalid importance"); - } catch (IllegalArgumentException e) { - // yay - } - try { - mHelper.createNotificationChannel(PKG, UID, - new NotificationChannel("bananas", "bananas", IMPORTANCE_UNSPECIFIED), - true, false); - fail("Was allowed to create a channel with invalid importance"); - } catch (IllegalArgumentException e) { - // yay - } - try { - mHelper.createNotificationChannel(PKG, UID, - new NotificationChannel("bananas", "bananas", IMPORTANCE_MAX + 1), - true, false); - fail("Was allowed to create a channel with invalid importance"); - } catch (IllegalArgumentException e) { - // yay - } - mHelper.createNotificationChannel(PKG, UID, - new NotificationChannel("bananas", "bananas", IMPORTANCE_NONE), true, false); - mHelper.createNotificationChannel(PKG, UID, - new NotificationChannel("bananas", "bananas", IMPORTANCE_MAX), true, false); - } - - - @Test - public void testUpdate() throws Exception { - // no fields locked by user - final NotificationChannel channel = - new NotificationChannel("id2", "name2", IMPORTANCE_LOW); - channel.setSound(new Uri.Builder().scheme("test").build(), mAudioAttributes); - channel.enableLights(true); - channel.setBypassDnd(true); - channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET); - - mHelper.createNotificationChannel(PKG, UID, channel, false, false); - - // same id, try to update all fields - final NotificationChannel channel2 = - new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_HIGH); - channel2.setSound(new Uri.Builder().scheme("test2").build(), mAudioAttributes); - channel2.enableLights(false); - channel2.setBypassDnd(false); - channel2.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); - - mHelper.updateNotificationChannel(PKG, UID, channel2, true); - - // all fields should be changed - assertEquals(channel2, mHelper.getNotificationChannel(PKG, UID, channel.getId(), false)); - - verify(mHandler, times(1)).requestSort(); - } - - @Test - public void testUpdate_preUpgrade_updatesAppFields() throws Exception { - mHelper.setImportance(PKG, UID, IMPORTANCE_UNSPECIFIED); - assertTrue(mHelper.canShowBadge(PKG, UID)); - assertEquals(Notification.PRIORITY_DEFAULT, mHelper.getPackagePriority(PKG, UID)); - assertEquals(NotificationManager.VISIBILITY_NO_OVERRIDE, - mHelper.getPackageVisibility(PKG, UID)); - assertFalse(mHelper.getIsAppImportanceLocked(PKG, UID)); - - NotificationChannel defaultChannel = mHelper.getNotificationChannel( - PKG, UID, NotificationChannel.DEFAULT_CHANNEL_ID, false); - - defaultChannel.setShowBadge(false); - defaultChannel.setImportance(IMPORTANCE_NONE); - defaultChannel.setBypassDnd(true); - defaultChannel.setLockscreenVisibility(Notification.VISIBILITY_SECRET); - - mHelper.setAppImportanceLocked(PKG, UID); - mHelper.updateNotificationChannel(PKG, UID, defaultChannel, true); - - // ensure app level fields are changed - assertFalse(mHelper.canShowBadge(PKG, UID)); - assertEquals(Notification.PRIORITY_MAX, mHelper.getPackagePriority(PKG, UID)); - assertEquals(Notification.VISIBILITY_SECRET, mHelper.getPackageVisibility(PKG, UID)); - assertEquals(IMPORTANCE_NONE, mHelper.getImportance(PKG, UID)); - assertTrue(mHelper.getIsAppImportanceLocked(PKG, UID)); - } - - @Test - public void testUpdate_postUpgrade_noUpdateAppFields() throws Exception { - final NotificationChannel channel = new NotificationChannel("id2", "name2", IMPORTANCE_LOW); - - mHelper.createNotificationChannel(PKG, UID, channel, false, false); - assertTrue(mHelper.canShowBadge(PKG, UID)); - assertEquals(Notification.PRIORITY_DEFAULT, mHelper.getPackagePriority(PKG, UID)); - assertEquals(NotificationManager.VISIBILITY_NO_OVERRIDE, - mHelper.getPackageVisibility(PKG, UID)); - - channel.setShowBadge(false); - channel.setImportance(IMPORTANCE_NONE); - channel.setBypassDnd(true); - channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET); - - mHelper.updateNotificationChannel(PKG, UID, channel, true); - - // ensure app level fields are not changed - assertTrue(mHelper.canShowBadge(PKG, UID)); - assertEquals(Notification.PRIORITY_DEFAULT, mHelper.getPackagePriority(PKG, UID)); - assertEquals(NotificationManager.VISIBILITY_NO_OVERRIDE, - mHelper.getPackageVisibility(PKG, UID)); - assertEquals(NotificationManager.IMPORTANCE_UNSPECIFIED, mHelper.getImportance(PKG, UID)); - } - - @Test - public void testGetNotificationChannel_ReturnsNullForUnknownChannel() throws Exception { - assertEquals(null, mHelper.getNotificationChannel(PKG, UID, "garbage", false)); - } - - @Test - public void testCreateChannel_CannotChangeHiddenFields() throws Exception { - final NotificationChannel channel = - new NotificationChannel("id2", "name2", IMPORTANCE_LOW); - channel.setSound(new Uri.Builder().scheme("test").build(), mAudioAttributes); - channel.enableLights(true); - channel.setBypassDnd(true); - channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET); - channel.setShowBadge(true); - int lockMask = 0; - for (int i = 0; i < NotificationChannel.LOCKABLE_FIELDS.length; i++) { - lockMask |= NotificationChannel.LOCKABLE_FIELDS[i]; - } - channel.lockFields(lockMask); - - mHelper.createNotificationChannel(PKG, UID, channel, true, false); - - NotificationChannel savedChannel = - mHelper.getNotificationChannel(PKG, UID, channel.getId(), false); - - assertEquals(channel.getName(), savedChannel.getName()); - assertEquals(channel.shouldShowLights(), savedChannel.shouldShowLights()); - assertFalse(savedChannel.canBypassDnd()); - assertFalse(Notification.VISIBILITY_SECRET == savedChannel.getLockscreenVisibility()); - assertEquals(channel.canShowBadge(), savedChannel.canShowBadge()); - - verify(mHandler, never()).requestSort(); - } - - @Test - public void testCreateChannel_CannotChangeHiddenFieldsAssistant() throws Exception { - final NotificationChannel channel = - new NotificationChannel("id2", "name2", IMPORTANCE_LOW); - channel.setSound(new Uri.Builder().scheme("test").build(), mAudioAttributes); - channel.enableLights(true); - channel.setBypassDnd(true); - channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET); - channel.setShowBadge(true); - int lockMask = 0; - for (int i = 0; i < NotificationChannel.LOCKABLE_FIELDS.length; i++) { - lockMask |= NotificationChannel.LOCKABLE_FIELDS[i]; - } - channel.lockFields(lockMask); - - mHelper.createNotificationChannel(PKG, UID, channel, true, false); - - NotificationChannel savedChannel = - mHelper.getNotificationChannel(PKG, UID, channel.getId(), false); - - assertEquals(channel.getName(), savedChannel.getName()); - assertEquals(channel.shouldShowLights(), savedChannel.shouldShowLights()); - assertFalse(savedChannel.canBypassDnd()); - assertFalse(Notification.VISIBILITY_SECRET == savedChannel.getLockscreenVisibility()); - assertEquals(channel.canShowBadge(), savedChannel.canShowBadge()); - } - - @Test - public void testClearLockedFields() throws Exception { - final NotificationChannel channel = getChannel(); - mHelper.clearLockedFields(channel); - assertEquals(0, channel.getUserLockedFields()); - - channel.lockFields(NotificationChannel.USER_LOCKED_PRIORITY - | NotificationChannel.USER_LOCKED_IMPORTANCE); - mHelper.clearLockedFields(channel); - assertEquals(0, channel.getUserLockedFields()); - } - - @Test - public void testLockFields_soundAndVibration() throws Exception { - mHelper.createNotificationChannel(PKG, UID, getChannel(), true, false); - - final NotificationChannel update1 = getChannel(); - update1.setSound(new Uri.Builder().scheme("test").build(), - new AudioAttributes.Builder().build()); - update1.lockFields(NotificationChannel.USER_LOCKED_PRIORITY); - mHelper.updateNotificationChannel(PKG, UID, update1, true); - assertEquals(NotificationChannel.USER_LOCKED_PRIORITY - | NotificationChannel.USER_LOCKED_SOUND, - mHelper.getNotificationChannel(PKG, UID, update1.getId(), false) - .getUserLockedFields()); - - NotificationChannel update2 = getChannel(); - update2.enableVibration(true); - mHelper.updateNotificationChannel(PKG, UID, update2, true); - assertEquals(NotificationChannel.USER_LOCKED_PRIORITY - | NotificationChannel.USER_LOCKED_SOUND - | NotificationChannel.USER_LOCKED_VIBRATION, - mHelper.getNotificationChannel(PKG, UID, update2.getId(), false) - .getUserLockedFields()); - } - - @Test - public void testLockFields_vibrationAndLights() throws Exception { - mHelper.createNotificationChannel(PKG, UID, getChannel(), true, false); - - final NotificationChannel update1 = getChannel(); - update1.setVibrationPattern(new long[]{7945, 46 ,246}); - mHelper.updateNotificationChannel(PKG, UID, update1, true); - assertEquals(NotificationChannel.USER_LOCKED_VIBRATION, - mHelper.getNotificationChannel(PKG, UID, update1.getId(), false) - .getUserLockedFields()); - - final NotificationChannel update2 = getChannel(); - update2.enableLights(true); - mHelper.updateNotificationChannel(PKG, UID, update2, true); - assertEquals(NotificationChannel.USER_LOCKED_VIBRATION - | NotificationChannel.USER_LOCKED_LIGHTS, - mHelper.getNotificationChannel(PKG, UID, update2.getId(), false) - .getUserLockedFields()); - } - - @Test - public void testLockFields_lightsAndImportance() throws Exception { - mHelper.createNotificationChannel(PKG, UID, getChannel(), true, false); - - final NotificationChannel update1 = getChannel(); - update1.setLightColor(Color.GREEN); - mHelper.updateNotificationChannel(PKG, UID, update1, true); - assertEquals(NotificationChannel.USER_LOCKED_LIGHTS, - mHelper.getNotificationChannel(PKG, UID, update1.getId(), false) - .getUserLockedFields()); - - final NotificationChannel update2 = getChannel(); - update2.setImportance(IMPORTANCE_DEFAULT); - mHelper.updateNotificationChannel(PKG, UID, update2, true); - assertEquals(NotificationChannel.USER_LOCKED_LIGHTS - | NotificationChannel.USER_LOCKED_IMPORTANCE, - mHelper.getNotificationChannel(PKG, UID, update2.getId(), false) - .getUserLockedFields()); - } - - @Test - public void testLockFields_visibilityAndDndAndBadge() throws Exception { - mHelper.createNotificationChannel(PKG, UID, getChannel(), true, false); - assertEquals(0, - mHelper.getNotificationChannel(PKG, UID, getChannel().getId(), false) - .getUserLockedFields()); - - final NotificationChannel update1 = getChannel(); - update1.setBypassDnd(true); - mHelper.updateNotificationChannel(PKG, UID, update1, true); - assertEquals(NotificationChannel.USER_LOCKED_PRIORITY, - mHelper.getNotificationChannel(PKG, UID, update1.getId(), false) - .getUserLockedFields()); - - final NotificationChannel update2 = getChannel(); - update2.setLockscreenVisibility(Notification.VISIBILITY_SECRET); - mHelper.updateNotificationChannel(PKG, UID, update2, true); - assertEquals(NotificationChannel.USER_LOCKED_PRIORITY - | NotificationChannel.USER_LOCKED_VISIBILITY, - mHelper.getNotificationChannel(PKG, UID, update2.getId(), false) - .getUserLockedFields()); - - final NotificationChannel update3 = getChannel(); - update3.setShowBadge(false); - mHelper.updateNotificationChannel(PKG, UID, update3, true); - assertEquals(NotificationChannel.USER_LOCKED_PRIORITY - | NotificationChannel.USER_LOCKED_VISIBILITY - | NotificationChannel.USER_LOCKED_SHOW_BADGE, - mHelper.getNotificationChannel(PKG, UID, update3.getId(), false) - .getUserLockedFields()); - } - - @Test - public void testDeleteNonExistentChannel() throws Exception { - mHelper.deleteNotificationChannelGroup(PKG, UID, "does not exist"); - } - - @Test - public void testGetDeletedChannel() throws Exception { - NotificationChannel channel = getChannel(); - channel.setSound(new Uri.Builder().scheme("test").build(), mAudioAttributes); - channel.enableLights(true); - channel.setBypassDnd(true); - channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET); - channel.enableVibration(true); - channel.setVibrationPattern(new long[]{100, 67, 145, 156}); - - mHelper.createNotificationChannel(PKG, UID, channel, true, false); - mHelper.deleteNotificationChannel(PKG, UID, channel.getId()); - - // Does not return deleted channel - NotificationChannel response = - mHelper.getNotificationChannel(PKG, UID, channel.getId(), false); - assertNull(response); - - // Returns deleted channel - response = mHelper.getNotificationChannel(PKG, UID, channel.getId(), true); - compareChannels(channel, response); - assertTrue(response.isDeleted()); - } - - @Test - public void testGetDeletedChannels() throws Exception { - Map<String, NotificationChannel> channelMap = new HashMap<>(); - NotificationChannel channel = - new NotificationChannel("id2", "name2", IMPORTANCE_LOW); - channel.setSound(new Uri.Builder().scheme("test").build(), mAudioAttributes); - channel.enableLights(true); - channel.setBypassDnd(true); - channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); - channel.enableVibration(true); - channel.setVibrationPattern(new long[]{100, 67, 145, 156}); - channelMap.put(channel.getId(), channel); - NotificationChannel channel2 = - new NotificationChannel("id4", "a", NotificationManager.IMPORTANCE_HIGH); - channelMap.put(channel2.getId(), channel2); - mHelper.createNotificationChannel(PKG, UID, channel, true, false); - mHelper.createNotificationChannel(PKG, UID, channel2, true, false); - - mHelper.deleteNotificationChannel(PKG, UID, channel.getId()); - - // Returns only non-deleted channels - List<NotificationChannel> channels = - mHelper.getNotificationChannels(PKG, UID, false).getList(); - assertEquals(2, channels.size()); // Default channel + non-deleted channel - for (NotificationChannel nc : channels) { - if (!NotificationChannel.DEFAULT_CHANNEL_ID.equals(nc.getId())) { - compareChannels(channel2, nc); - } - } - - // Returns deleted channels too - channels = mHelper.getNotificationChannels(PKG, UID, true).getList(); - assertEquals(3, channels.size()); // Includes default channel - for (NotificationChannel nc : channels) { - if (!NotificationChannel.DEFAULT_CHANNEL_ID.equals(nc.getId())) { - compareChannels(channelMap.get(nc.getId()), nc); - } - } - } - - @Test - public void testGetDeletedChannelCount() throws Exception { - NotificationChannel channel = - new NotificationChannel("id2", "name2", IMPORTANCE_LOW); - NotificationChannel channel2 = - new NotificationChannel("id4", "a", NotificationManager.IMPORTANCE_HIGH); - NotificationChannel channel3 = - new NotificationChannel("id5", "a", NotificationManager.IMPORTANCE_HIGH); - mHelper.createNotificationChannel(PKG, UID, channel, true, false); - mHelper.createNotificationChannel(PKG, UID, channel2, true, false); - mHelper.createNotificationChannel(PKG, UID, channel3, true, false); - - mHelper.deleteNotificationChannel(PKG, UID, channel.getId()); - mHelper.deleteNotificationChannel(PKG, UID, channel3.getId()); - - assertEquals(2, mHelper.getDeletedChannelCount(PKG, UID)); - assertEquals(0, mHelper.getDeletedChannelCount("pkg2", UID2)); - } - - @Test - public void testGetBlockedChannelCount() throws Exception { - NotificationChannel channel = - new NotificationChannel("id2", "name2", IMPORTANCE_LOW); - NotificationChannel channel2 = - new NotificationChannel("id4", "a", NotificationManager.IMPORTANCE_NONE); - NotificationChannel channel3 = - new NotificationChannel("id5", "a", NotificationManager.IMPORTANCE_NONE); - mHelper.createNotificationChannel(PKG, UID, channel, true, false); - mHelper.createNotificationChannel(PKG, UID, channel2, true, false); - mHelper.createNotificationChannel(PKG, UID, channel3, true, false); - - mHelper.deleteNotificationChannel(PKG, UID, channel3.getId()); - - assertEquals(1, mHelper.getBlockedChannelCount(PKG, UID)); - assertEquals(0, mHelper.getBlockedChannelCount("pkg2", UID2)); - } - - @Test - public void testCreateAndDeleteCanChannelsBypassDnd() throws Exception { - // create notification channel that can't bypass dnd - // expected result: areChannelsBypassingDnd = false - // setNotificationPolicy isn't called since areChannelsBypassingDnd was already false - NotificationChannel channel = new NotificationChannel("id1", "name1", IMPORTANCE_LOW); - mHelper.createNotificationChannel(PKG, UID, channel, true, false); - assertFalse(mHelper.areChannelsBypassingDnd()); - verify(mMockZenModeHelper, never()).setNotificationPolicy(any()); - resetZenModeHelper(); - - // create notification channel that can bypass dnd - // expected result: areChannelsBypassingDnd = true - NotificationChannel channel2 = new NotificationChannel("id2", "name2", IMPORTANCE_LOW); - channel2.setBypassDnd(true); - mHelper.createNotificationChannel(PKG, UID, channel2, true, true); - assertTrue(mHelper.areChannelsBypassingDnd()); - verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any()); - resetZenModeHelper(); - - // delete channels - mHelper.deleteNotificationChannel(PKG, UID, channel.getId()); - assertTrue(mHelper.areChannelsBypassingDnd()); // channel2 can still bypass DND - verify(mMockZenModeHelper, never()).setNotificationPolicy(any()); - resetZenModeHelper(); - - mHelper.deleteNotificationChannel(PKG, UID, channel2.getId()); - assertFalse(mHelper.areChannelsBypassingDnd()); - verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any()); - resetZenModeHelper(); - } - - @Test - public void testUpdateCanChannelsBypassDnd() throws Exception { - // create notification channel that can't bypass dnd - // expected result: areChannelsBypassingDnd = false - // setNotificationPolicy isn't called since areChannelsBypassingDnd was already false - NotificationChannel channel = new NotificationChannel("id1", "name1", IMPORTANCE_LOW); - mHelper.createNotificationChannel(PKG, UID, channel, true, false); - assertFalse(mHelper.areChannelsBypassingDnd()); - verify(mMockZenModeHelper, never()).setNotificationPolicy(any()); - resetZenModeHelper(); - - // update channel so it CAN bypass dnd: - // expected result: areChannelsBypassingDnd = true - channel.setBypassDnd(true); - mHelper.updateNotificationChannel(PKG, UID, channel, true); - assertTrue(mHelper.areChannelsBypassingDnd()); - verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any()); - resetZenModeHelper(); - - // update channel so it can't bypass dnd: - // expected result: areChannelsBypassingDnd = false - channel.setBypassDnd(false); - mHelper.updateNotificationChannel(PKG, UID, channel, true); - assertFalse(mHelper.areChannelsBypassingDnd()); - verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any()); - resetZenModeHelper(); - } - - @Test - public void testSetupNewZenModeHelper_canBypass() { - // start notification policy off with mAreChannelsBypassingDnd = true, but - // RankingHelper should change to false - mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0, - NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND); - when(mMockZenModeHelper.getNotificationPolicy()).thenReturn(mTestNotificationPolicy); - mHelper = new RankingHelper(getContext(), mPm, mHandler, mMockZenModeHelper, - mUsageStats, new String[] {ImportanceExtractor.class.getName()}); - assertFalse(mHelper.areChannelsBypassingDnd()); - verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any()); - resetZenModeHelper(); - } - - @Test - public void testSetupNewZenModeHelper_cannotBypass() { - // start notification policy off with mAreChannelsBypassingDnd = false - mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0, 0); - when(mMockZenModeHelper.getNotificationPolicy()).thenReturn(mTestNotificationPolicy); - mHelper = new RankingHelper(getContext(), mPm, mHandler, mMockZenModeHelper, - mUsageStats, new String[] {ImportanceExtractor.class.getName()}); - assertFalse(mHelper.areChannelsBypassingDnd()); - verify(mMockZenModeHelper, never()).setNotificationPolicy(any()); - resetZenModeHelper(); - } - - @Test - public void testCreateDeletedChannel() throws Exception { - long[] vibration = new long[]{100, 67, 145, 156}; - NotificationChannel channel = - new NotificationChannel("id2", "name2", IMPORTANCE_LOW); - channel.setVibrationPattern(vibration); - - mHelper.createNotificationChannel(PKG, UID, channel, true, false); - mHelper.deleteNotificationChannel(PKG, UID, channel.getId()); - - NotificationChannel newChannel = new NotificationChannel( - channel.getId(), channel.getName(), NotificationManager.IMPORTANCE_HIGH); - newChannel.setVibrationPattern(new long[]{100}); - - mHelper.createNotificationChannel(PKG, UID, newChannel, true, false); - - // No long deleted, using old settings - compareChannels(channel, - mHelper.getNotificationChannel(PKG, UID, newChannel.getId(), false)); - } - - @Test - public void testOnlyHasDefaultChannel() throws Exception { - assertTrue(mHelper.onlyHasDefaultChannel(PKG, UID)); - assertFalse(mHelper.onlyHasDefaultChannel(UPDATED_PKG, UID2)); - - mHelper.createNotificationChannel(PKG, UID, getChannel(), true, false); - assertFalse(mHelper.onlyHasDefaultChannel(PKG, UID)); - } - - @Test - public void testCreateChannel_defaultChannelId() throws Exception { - try { - mHelper.createNotificationChannel(PKG, UID, new NotificationChannel( - NotificationChannel.DEFAULT_CHANNEL_ID, "ha", IMPORTANCE_HIGH), true, false); - fail("Allowed to create default channel"); - } catch (IllegalArgumentException e) { - // pass - } - } - - @Test - public void testCreateChannel_alreadyExists() throws Exception { - long[] vibration = new long[]{100, 67, 145, 156}; - NotificationChannel channel = - new NotificationChannel("id2", "name2", IMPORTANCE_LOW); - channel.setVibrationPattern(vibration); - - mHelper.createNotificationChannel(PKG, UID, channel, true, false); - - NotificationChannel newChannel = new NotificationChannel( - channel.getId(), channel.getName(), NotificationManager.IMPORTANCE_HIGH); - newChannel.setVibrationPattern(new long[]{100}); - - mHelper.createNotificationChannel(PKG, UID, newChannel, true, false); - - // Old settings not overridden - compareChannels(channel, - mHelper.getNotificationChannel(PKG, UID, newChannel.getId(), false)); - } - - @Test - public void testCreateChannel_noOverrideSound() throws Exception { - Uri sound = new Uri.Builder().scheme("test").build(); - final NotificationChannel channel = new NotificationChannel("id2", "name2", - NotificationManager.IMPORTANCE_DEFAULT); - channel.setSound(sound, mAudioAttributes); - mHelper.createNotificationChannel(PKG, UID, channel, true, false); - assertEquals(sound, mHelper.getNotificationChannel( - PKG, UID, channel.getId(), false).getSound()); - } - - @Test - public void testPermanentlyDeleteChannels() throws Exception { - NotificationChannel channel1 = - new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); - NotificationChannel channel2 = - new NotificationChannel("id2", "name2", IMPORTANCE_LOW); - - mHelper.createNotificationChannel(PKG, UID, channel1, true, false); - mHelper.createNotificationChannel(PKG, UID, channel2, false, false); - - mHelper.permanentlyDeleteNotificationChannels(PKG, UID); - - // Only default channel remains - assertEquals(1, mHelper.getNotificationChannels(PKG, UID, true).getList().size()); - } - - @Test - public void testDeleteGroup() throws Exception { - NotificationChannelGroup notDeleted = new NotificationChannelGroup("not", "deleted"); - NotificationChannelGroup deleted = new NotificationChannelGroup("totally", "deleted"); - NotificationChannel nonGroupedNonDeletedChannel = - new NotificationChannel("no group", "so not deleted", IMPORTANCE_HIGH); - NotificationChannel groupedButNotDeleted = - new NotificationChannel("not deleted", "belongs to notDeleted", IMPORTANCE_DEFAULT); - groupedButNotDeleted.setGroup("not"); - NotificationChannel groupedAndDeleted = - new NotificationChannel("deleted", "belongs to deleted", IMPORTANCE_DEFAULT); - groupedAndDeleted.setGroup("totally"); - - mHelper.createNotificationChannelGroup(PKG, UID, notDeleted, true); - mHelper.createNotificationChannelGroup(PKG, UID, deleted, true); - mHelper.createNotificationChannel(PKG, UID, nonGroupedNonDeletedChannel, true, false); - mHelper.createNotificationChannel(PKG, UID, groupedAndDeleted, true, false); - mHelper.createNotificationChannel(PKG, UID, groupedButNotDeleted, true, false); - - mHelper.deleteNotificationChannelGroup(PKG, UID, deleted.getId()); - - assertNull(mHelper.getNotificationChannelGroup(deleted.getId(), PKG, UID)); - assertNotNull(mHelper.getNotificationChannelGroup(notDeleted.getId(), PKG, UID)); - - assertNull(mHelper.getNotificationChannel(PKG, UID, groupedAndDeleted.getId(), false)); - compareChannels(groupedAndDeleted, - mHelper.getNotificationChannel(PKG, UID, groupedAndDeleted.getId(), true)); - - compareChannels(groupedButNotDeleted, - mHelper.getNotificationChannel(PKG, UID, groupedButNotDeleted.getId(), false)); - compareChannels(nonGroupedNonDeletedChannel, mHelper.getNotificationChannel( - PKG, UID, nonGroupedNonDeletedChannel.getId(), false)); - - // notDeleted - assertEquals(1, mHelper.getNotificationChannelGroups(PKG, UID).size()); - - verify(mHandler, never()).requestSort(); - } - - @Test - public void testOnUserRemoved() throws Exception { - int[] user0Uids = {98, 235, 16, 3782}; - int[] user1Uids = new int[user0Uids.length]; - for (int i = 0; i < user0Uids.length; i++) { - user1Uids[i] = UserHandle.PER_USER_RANGE + user0Uids[i]; - - final ApplicationInfo legacy = new ApplicationInfo(); - legacy.targetSdkVersion = Build.VERSION_CODES.N_MR1; - when(mPm.getApplicationInfoAsUser(eq(PKG), anyInt(), anyInt())).thenReturn(legacy); - - // create records with the default channel for all user 0 and user 1 uids - mHelper.getImportance(PKG, user0Uids[i]); - mHelper.getImportance(PKG, user1Uids[i]); - } - - mHelper.onUserRemoved(1); - - // user 0 records remain - for (int i = 0; i < user0Uids.length; i++) { - assertEquals(1, - mHelper.getNotificationChannels(PKG, user0Uids[i], false).getList().size()); - } - // user 1 records are gone - for (int i = 0; i < user1Uids.length; i++) { - assertEquals(0, - mHelper.getNotificationChannels(PKG, user1Uids[i], false).getList().size()); - } - } - - @Test - public void testOnPackageChanged_packageRemoval() throws Exception { - // Deleted - NotificationChannel channel1 = - new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); - mHelper.createNotificationChannel(PKG, UID, channel1, true, false); - - mHelper.onPackagesChanged(true, UserHandle.USER_SYSTEM, new String[]{PKG}, new int[]{UID}); - - assertEquals(0, mHelper.getNotificationChannels(PKG, UID, true).getList().size()); - - // Not deleted - mHelper.createNotificationChannel(PKG, UID, channel1, true, false); - - mHelper.onPackagesChanged(false, UserHandle.USER_SYSTEM, new String[]{PKG}, new int[]{UID}); - assertEquals(2, mHelper.getNotificationChannels(PKG, UID, false).getList().size()); - } - - @Test - public void testOnPackageChanged_packageRemoval_importance() throws Exception { - mHelper.setImportance(PKG, UID, NotificationManager.IMPORTANCE_HIGH); - - mHelper.onPackagesChanged(true, UserHandle.USER_SYSTEM, new String[]{PKG}, new int[]{UID}); - - assertEquals(NotificationManager.IMPORTANCE_UNSPECIFIED, mHelper.getImportance(PKG, UID)); - } - - @Test - public void testOnPackageChanged_packageRemoval_groups() throws Exception { - NotificationChannelGroup ncg = new NotificationChannelGroup("group1", "name1"); - mHelper.createNotificationChannelGroup(PKG, UID, ncg, true); - NotificationChannelGroup ncg2 = new NotificationChannelGroup("group2", "name2"); - mHelper.createNotificationChannelGroup(PKG, UID, ncg2, true); - - mHelper.onPackagesChanged(true, UserHandle.USER_SYSTEM, new String[]{PKG}, new int[]{UID}); - - assertEquals(0, - mHelper.getNotificationChannelGroups(PKG, UID, true, true).getList().size()); - } - - @Test - public void testOnPackageChange_downgradeTargetSdk() throws Exception { - // create channel as api 26 - mHelper.createNotificationChannel(UPDATED_PKG, UID2, getChannel(), true, false); - - // install new app version targeting 25 - final ApplicationInfo legacy = new ApplicationInfo(); - legacy.targetSdkVersion = Build.VERSION_CODES.N_MR1; - when(mPm.getApplicationInfoAsUser(eq(UPDATED_PKG), anyInt(), anyInt())).thenReturn(legacy); - mHelper.onPackagesChanged( - false, UserHandle.USER_SYSTEM, new String[]{UPDATED_PKG}, new int[]{UID2}); - - // make sure the default channel was readded - //assertEquals(2, mHelper.getNotificationChannels(UPDATED_PKG, UID2, false).getList().size()); - assertNotNull(mHelper.getNotificationChannel( - UPDATED_PKG, UID2, NotificationChannel.DEFAULT_CHANNEL_ID, false)); - } - - @Test - public void testRecordDefaults() throws Exception { - assertEquals(NotificationManager.IMPORTANCE_UNSPECIFIED, mHelper.getImportance(PKG, UID)); - assertEquals(true, mHelper.canShowBadge(PKG, UID)); - assertEquals(1, mHelper.getNotificationChannels(PKG, UID, false).getList().size()); - } - - @Test - public void testCreateGroup() throws Exception { - NotificationChannelGroup ncg = new NotificationChannelGroup("group1", "name1"); - mHelper.createNotificationChannelGroup(PKG, UID, ncg, true); - assertEquals(ncg, mHelper.getNotificationChannelGroups(PKG, UID).iterator().next()); - verify(mHandler, never()).requestSort(); - } - - @Test - public void testCannotCreateChannel_badGroup() throws Exception { - NotificationChannel channel1 = - new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); - channel1.setGroup("garbage"); - try { - mHelper.createNotificationChannel(PKG, UID, channel1, true, false); - fail("Created a channel with a bad group"); - } catch (IllegalArgumentException e) { - } - } - - @Test - public void testCannotCreateChannel_goodGroup() throws Exception { - NotificationChannelGroup ncg = new NotificationChannelGroup("group1", "name1"); - mHelper.createNotificationChannelGroup(PKG, UID, ncg, true); - NotificationChannel channel1 = - new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); - channel1.setGroup(ncg.getId()); - mHelper.createNotificationChannel(PKG, UID, channel1, true, false); - - assertEquals(ncg.getId(), - mHelper.getNotificationChannel(PKG, UID, channel1.getId(), false).getGroup()); - } - - @Test - public void testGetChannelGroups() throws Exception { - NotificationChannelGroup unused = new NotificationChannelGroup("unused", "s"); - mHelper.createNotificationChannelGroup(PKG, UID, unused, true); - NotificationChannelGroup ncg = new NotificationChannelGroup("group1", "name1"); - mHelper.createNotificationChannelGroup(PKG, UID, ncg, true); - NotificationChannelGroup ncg2 = new NotificationChannelGroup("group2", "name2"); - mHelper.createNotificationChannelGroup(PKG, UID, ncg2, true); - - NotificationChannel channel1 = - new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); - channel1.setGroup(ncg.getId()); - mHelper.createNotificationChannel(PKG, UID, channel1, true, false); - NotificationChannel channel1a = - new NotificationChannel("id1a", "name1", NotificationManager.IMPORTANCE_HIGH); - channel1a.setGroup(ncg.getId()); - mHelper.createNotificationChannel(PKG, UID, channel1a, true, false); - - NotificationChannel channel2 = - new NotificationChannel("id2", "name1", NotificationManager.IMPORTANCE_HIGH); - channel2.setGroup(ncg2.getId()); - mHelper.createNotificationChannel(PKG, UID, channel2, true, false); - - NotificationChannel channel3 = - new NotificationChannel("id3", "name1", NotificationManager.IMPORTANCE_HIGH); - mHelper.createNotificationChannel(PKG, UID, channel3, true, false); - - List<NotificationChannelGroup> actual = - mHelper.getNotificationChannelGroups(PKG, UID, true, true).getList(); - assertEquals(3, actual.size()); - for (NotificationChannelGroup group : actual) { - if (group.getId() == null) { - assertEquals(2, group.getChannels().size()); // misc channel too - assertTrue(channel3.getId().equals(group.getChannels().get(0).getId()) - || channel3.getId().equals(group.getChannels().get(1).getId())); - } else if (group.getId().equals(ncg.getId())) { - assertEquals(2, group.getChannels().size()); - if (group.getChannels().get(0).getId().equals(channel1.getId())) { - assertTrue(group.getChannels().get(1).getId().equals(channel1a.getId())); - } else if (group.getChannels().get(0).getId().equals(channel1a.getId())) { - assertTrue(group.getChannels().get(1).getId().equals(channel1.getId())); - } else { - fail("expected channel not found"); - } - } else if (group.getId().equals(ncg2.getId())) { - assertEquals(1, group.getChannels().size()); - assertEquals(channel2.getId(), group.getChannels().get(0).getId()); - } - } - } - - @Test - public void testGetChannelGroups_noSideEffects() throws Exception { - NotificationChannelGroup ncg = new NotificationChannelGroup("group1", "name1"); - mHelper.createNotificationChannelGroup(PKG, UID, ncg, true); - - NotificationChannel channel1 = - new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); - channel1.setGroup(ncg.getId()); - mHelper.createNotificationChannel(PKG, UID, channel1, true, false); - mHelper.getNotificationChannelGroups(PKG, UID, true, true).getList(); - - channel1.setImportance(IMPORTANCE_LOW); - mHelper.updateNotificationChannel(PKG, UID, channel1, true); - - List<NotificationChannelGroup> actual = - mHelper.getNotificationChannelGroups(PKG, UID, true, true).getList(); - - assertEquals(2, actual.size()); - for (NotificationChannelGroup group : actual) { - if (Objects.equals(group.getId(), ncg.getId())) { - assertEquals(1, group.getChannels().size()); - } - } - } - - @Test - public void testCreateChannel_updateName() throws Exception { - NotificationChannel nc = new NotificationChannel("id", "hello", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG, UID, nc, true, false); - NotificationChannel actual = mHelper.getNotificationChannel(PKG, UID, "id", false); - assertEquals("hello", actual.getName()); - - nc = new NotificationChannel("id", "goodbye", IMPORTANCE_HIGH); - mHelper.createNotificationChannel(PKG, UID, nc, true, false); - - actual = mHelper.getNotificationChannel(PKG, UID, "id", false); - assertEquals("goodbye", actual.getName()); - assertEquals(IMPORTANCE_DEFAULT, actual.getImportance()); - - verify(mHandler, times(1)).requestSort(); - } - - @Test - public void testCreateChannel_addToGroup() throws Exception { - NotificationChannelGroup group = new NotificationChannelGroup("group", ""); - mHelper.createNotificationChannelGroup(PKG, UID, group, true); - NotificationChannel nc = new NotificationChannel("id", "hello", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG, UID, nc, true, false); - NotificationChannel actual = mHelper.getNotificationChannel(PKG, UID, "id", false); - assertNull(actual.getGroup()); - - nc = new NotificationChannel("id", "hello", IMPORTANCE_HIGH); - nc.setGroup(group.getId()); - mHelper.createNotificationChannel(PKG, UID, nc, true, false); - - actual = mHelper.getNotificationChannel(PKG, UID, "id", false); - assertNotNull(actual.getGroup()); - assertEquals(IMPORTANCE_DEFAULT, actual.getImportance()); - - verify(mHandler, times(1)).requestSort(); - } - - @Test - public void testDumpChannelsJson() throws Exception { - final ApplicationInfo upgrade = new ApplicationInfo(); - upgrade.targetSdkVersion = Build.VERSION_CODES.O; - try { - when(mPm.getApplicationInfoAsUser( - anyString(), anyInt(), anyInt())).thenReturn(upgrade); - } catch (PackageManager.NameNotFoundException e) { - } - ArrayMap<String, Integer> expectedChannels = new ArrayMap<>(); - int numPackages = ThreadLocalRandom.current().nextInt(1, 5); - for (int i = 0; i < numPackages; i++) { - String pkgName = "pkg" + i; - int numChannels = ThreadLocalRandom.current().nextInt(1, 10); - for (int j = 0; j < numChannels; j++) { - mHelper.createNotificationChannel(pkgName, UID, - new NotificationChannel("" + j, "a", IMPORTANCE_HIGH), true, false); - } - expectedChannels.put(pkgName, numChannels); - } - - // delete the first channel of the first package - String pkg = expectedChannels.keyAt(0); - mHelper.deleteNotificationChannel("pkg" + 0, UID, "0"); - // dump should not include deleted channels - int count = expectedChannels.get(pkg); - expectedChannels.put(pkg, count - 1); - - JSONArray actual = mHelper.dumpChannelsJson(new NotificationManagerService.DumpFilter()); - assertEquals(numPackages, actual.length()); - for (int i = 0; i < numPackages; i++) { - JSONObject object = actual.getJSONObject(i); - assertTrue(expectedChannels.containsKey(object.get("packageName"))); - assertEquals(expectedChannels.get(object.get("packageName")).intValue(), - object.getInt("channelCount")); - } - } - - @Test - public void testBadgingOverrideTrue() throws Exception { - Secure.putIntForUser(getContext().getContentResolver(), - Secure.NOTIFICATION_BADGING, 1, - USER.getIdentifier()); - mHelper.updateBadgingEnabled(); // would be called by settings observer - assertTrue(mHelper.badgingEnabled(USER)); - } - - @Test - public void testBadgingOverrideFalse() throws Exception { - Secure.putIntForUser(getContext().getContentResolver(), - Secure.NOTIFICATION_BADGING, 0, - USER.getIdentifier()); - mHelper.updateBadgingEnabled(); // would be called by settings observer - assertFalse(mHelper.badgingEnabled(USER)); - } - - @Test - public void testBadgingForUserAll() throws Exception { - try { - mHelper.badgingEnabled(UserHandle.ALL); - } catch (Exception e) { - fail("just don't throw"); - } - } - - @Test - public void testBadgingOverrideUserIsolation() throws Exception { - Secure.putIntForUser(getContext().getContentResolver(), - Secure.NOTIFICATION_BADGING, 0, - USER.getIdentifier()); - Secure.putIntForUser(getContext().getContentResolver(), - Secure.NOTIFICATION_BADGING, 1, - USER2.getIdentifier()); - mHelper.updateBadgingEnabled(); // would be called by settings observer - assertFalse(mHelper.badgingEnabled(USER)); - assertTrue(mHelper.badgingEnabled(USER2)); - } - - @Test - public void testOnLocaleChanged_updatesDefaultChannels() throws Exception { - String newLabel = "bananas!"; - final NotificationChannel defaultChannel = mHelper.getNotificationChannel(PKG, UID, - NotificationChannel.DEFAULT_CHANNEL_ID, false); - assertFalse(newLabel.equals(defaultChannel.getName())); - - Resources res = mock(Resources.class); - when(mContext.getResources()).thenReturn(res); - when(res.getString(com.android.internal.R.string.default_notification_channel_label)) - .thenReturn(newLabel); - - mHelper.onLocaleChanged(mContext, USER.getIdentifier()); - - assertEquals(newLabel, mHelper.getNotificationChannel(PKG, UID, - NotificationChannel.DEFAULT_CHANNEL_ID, false).getName()); - } - - @Test - public void testIsGroupBlocked_noGroup() throws Exception { - assertFalse(mHelper.isGroupBlocked(PKG, UID, null)); - - assertFalse(mHelper.isGroupBlocked(PKG, UID, "non existent group")); - } - - @Test - public void testIsGroupBlocked_notBlocked() throws Exception { - NotificationChannelGroup group = new NotificationChannelGroup("id", "name"); - mHelper.createNotificationChannelGroup(PKG, UID, group, true); - - assertFalse(mHelper.isGroupBlocked(PKG, UID, group.getId())); - } - - @Test - public void testIsGroupBlocked_blocked() throws Exception { - NotificationChannelGroup group = new NotificationChannelGroup("id", "name"); - mHelper.createNotificationChannelGroup(PKG, UID, group, true); - group.setBlocked(true); - mHelper.createNotificationChannelGroup(PKG, UID, group, false); - - assertTrue(mHelper.isGroupBlocked(PKG, UID, group.getId())); - } - - @Test - public void testIsGroup_appCannotResetBlock() throws Exception { - NotificationChannelGroup group = new NotificationChannelGroup("id", "name"); - mHelper.createNotificationChannelGroup(PKG, UID, group, true); - NotificationChannelGroup group2 = group.clone(); - group2.setBlocked(true); - mHelper.createNotificationChannelGroup(PKG, UID, group2, false); - assertTrue(mHelper.isGroupBlocked(PKG, UID, group.getId())); - - NotificationChannelGroup group3 = group.clone(); - group3.setBlocked(false); - mHelper.createNotificationChannelGroup(PKG, UID, group3, true); - assertTrue(mHelper.isGroupBlocked(PKG, UID, group.getId())); - } - - @Test - public void testGetNotificationChannelGroupWithChannels() throws Exception { - NotificationChannelGroup group = new NotificationChannelGroup("group", ""); - NotificationChannelGroup other = new NotificationChannelGroup("something else", ""); - mHelper.createNotificationChannelGroup(PKG, UID, group, true); - mHelper.createNotificationChannelGroup(PKG, UID, other, true); - - NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_DEFAULT); - a.setGroup(group.getId()); - NotificationChannel b = new NotificationChannel("b", "b", IMPORTANCE_DEFAULT); - b.setGroup(other.getId()); - NotificationChannel c = new NotificationChannel("c", "c", IMPORTANCE_DEFAULT); - c.setGroup(group.getId()); - NotificationChannel d = new NotificationChannel("d", "d", IMPORTANCE_DEFAULT); - - mHelper.createNotificationChannel(PKG, UID, a, true, false); - mHelper.createNotificationChannel(PKG, UID, b, true, false); - mHelper.createNotificationChannel(PKG, UID, c, true, false); - mHelper.createNotificationChannel(PKG, UID, d, true, false); - mHelper.deleteNotificationChannel(PKG, UID, c.getId()); - - NotificationChannelGroup retrieved = mHelper.getNotificationChannelGroupWithChannels( - PKG, UID, group.getId(), true); - assertEquals(2, retrieved.getChannels().size()); - compareChannels(a, findChannel(retrieved.getChannels(), a.getId())); - compareChannels(c, findChannel(retrieved.getChannels(), c.getId())); - - retrieved = mHelper.getNotificationChannelGroupWithChannels( - PKG, UID, group.getId(), false); - assertEquals(1, retrieved.getChannels().size()); - compareChannels(a, findChannel(retrieved.getChannels(), a.getId())); - } - - @Test - public void testAndroidPkgCannotBypassDnd_creation() { - NotificationChannel test = new NotificationChannel("A", "a", IMPORTANCE_LOW); - test.setBypassDnd(true); - - mHelper.createNotificationChannel(SYSTEM_PKG, SYSTEM_UID, test, true, false); - - assertFalse(mHelper.getNotificationChannel(SYSTEM_PKG, SYSTEM_UID, "A", false) - .canBypassDnd()); - } - - @Test - public void testDndPkgCanBypassDnd_creation() { - NotificationChannel test = new NotificationChannel("A", "a", IMPORTANCE_LOW); - test.setBypassDnd(true); - - mHelper.createNotificationChannel(PKG, UID, test, true, true); - - assertTrue(mHelper.getNotificationChannel(PKG, UID, "A", false).canBypassDnd()); - } - - @Test - public void testNormalPkgCannotBypassDnd_creation() { - NotificationChannel test = new NotificationChannel("A", "a", IMPORTANCE_LOW); - test.setBypassDnd(true); - - mHelper.createNotificationChannel(PKG, 1000, test, true, false); - - assertFalse(mHelper.getNotificationChannel(PKG, 1000, "A", false).canBypassDnd()); - } - - @Test - public void testAndroidPkgCannotBypassDnd_update() throws Exception { - NotificationChannel test = new NotificationChannel("A", "a", IMPORTANCE_LOW); - mHelper.createNotificationChannel(SYSTEM_PKG, SYSTEM_UID, test, true, false); - - NotificationChannel update = new NotificationChannel("A", "a", IMPORTANCE_LOW); - update.setBypassDnd(true); - mHelper.createNotificationChannel(SYSTEM_PKG, SYSTEM_UID, update, true, false); - - assertFalse(mHelper.getNotificationChannel(SYSTEM_PKG, SYSTEM_UID, "A", false) - .canBypassDnd()); - } - - @Test - public void testDndPkgCanBypassDnd_update() throws Exception { - NotificationChannel test = new NotificationChannel("A", "a", IMPORTANCE_LOW); - mHelper.createNotificationChannel(PKG, UID, test, true, true); - - NotificationChannel update = new NotificationChannel("A", "a", IMPORTANCE_LOW); - update.setBypassDnd(true); - mHelper.createNotificationChannel(PKG, UID, update, true, true); - - assertTrue(mHelper.getNotificationChannel(PKG, UID, "A", false).canBypassDnd()); - } - - @Test - public void testNormalPkgCannotBypassDnd_update() { - NotificationChannel test = new NotificationChannel("A", "a", IMPORTANCE_LOW); - mHelper.createNotificationChannel(PKG, 1000, test, true, false); - NotificationChannel update = new NotificationChannel("A", "a", IMPORTANCE_LOW); - update.setBypassDnd(true); - mHelper.createNotificationChannel(PKG, 1000, update, true, false); - assertFalse(mHelper.getNotificationChannel(PKG, 1000, "A", false).canBypassDnd()); - } - - @Test - public void testGetBlockedAppCount_noApps() { - assertEquals(0, mHelper.getBlockedAppCount(0)); - } - - @Test - public void testGetBlockedAppCount_noAppsForUserId() { - mHelper.setEnabled(PKG, 100, false); - assertEquals(0, mHelper.getBlockedAppCount(9)); - } - - @Test - public void testGetBlockedAppCount_appsForUserId() { - mHelper.setEnabled(PKG, 1020, false); - mHelper.setEnabled(PKG, 1030, false); - mHelper.setEnabled(PKG, 1060, false); - mHelper.setEnabled(PKG, 1000, true); - assertEquals(3, mHelper.getBlockedAppCount(0)); - } } diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java index 5239fe5ef6f4..35f64a183d96 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UsageStatsService.java @@ -974,7 +974,8 @@ public class UsageStatsService extends SystemService implements final long token = Binder.clearCallingIdentity(); try { final int packageUid = mPackageManagerInternal.getPackageUid(packageName, - PackageManager.MATCH_ANY_USER, userId); + PackageManager.MATCH_ANY_USER | PackageManager.MATCH_DIRECT_BOOT_UNAWARE + | PackageManager.MATCH_DIRECT_BOOT_AWARE, userId); // Caller cannot set their own standby state if (packageUid == callingUid) { throw new IllegalArgumentException("Cannot set your own standby bucket"); diff --git a/telecomm/java/android/telecom/Connection.java b/telecomm/java/android/telecom/Connection.java index 3e97c8f40bf8..468c8fa9e30c 100644 --- a/telecomm/java/android/telecom/Connection.java +++ b/telecomm/java/android/telecom/Connection.java @@ -854,6 +854,8 @@ public abstract class Connection extends Conferenceable { private final OutputStreamWriter mPipeToInCall; private final ParcelFileDescriptor mFdFromInCall; private final ParcelFileDescriptor mFdToInCall; + + private final FileInputStream mFromInCallFileInputStream; private char[] mReadBuffer = new char[READ_BUFFER_SIZE]; /** @@ -862,11 +864,11 @@ public abstract class Connection extends Conferenceable { public RttTextStream(ParcelFileDescriptor toInCall, ParcelFileDescriptor fromInCall) { mFdFromInCall = fromInCall; mFdToInCall = toInCall; + mFromInCallFileInputStream = new FileInputStream(fromInCall.getFileDescriptor()); // Wrap the FileInputStream in a Channel so that it's interruptible. mPipeFromInCall = new InputStreamReader( - Channels.newInputStream(Channels.newChannel( - new FileInputStream(fromInCall.getFileDescriptor())))); + Channels.newInputStream(Channels.newChannel(mFromInCallFileInputStream))); mPipeToInCall = new OutputStreamWriter( new FileOutputStream(toInCall.getFileDescriptor())); } @@ -914,7 +916,7 @@ public abstract class Connection extends Conferenceable { * not entered any new text yet. */ public String readImmediately() throws IOException { - if (mPipeFromInCall.ready()) { + if (mFromInCallFileInputStream.available() > 0) { return read(); } else { return null; diff --git a/telephony/java/android/telephony/CellIdentity.java b/telephony/java/android/telephony/CellIdentity.java index 890a6ea7c88e..2a41829dcc2f 100644 --- a/telephony/java/android/telephony/CellIdentity.java +++ b/telephony/java/android/telephony/CellIdentity.java @@ -175,7 +175,10 @@ public abstract class CellIdentity implements Parcelable { } CellIdentity o = (CellIdentity) other; - return TextUtils.equals(mAlphaLong, o.mAlphaLong) + return mType == o.mType + && TextUtils.equals(mMccStr, o.mMccStr) + && TextUtils.equals(mMncStr, o.mMncStr) + && TextUtils.equals(mAlphaLong, o.mAlphaLong) && TextUtils.equals(mAlphaShort, o.mAlphaShort); } @@ -233,4 +236,4 @@ public abstract class CellIdentity implements Parcelable { protected void log(String s) { Rlog.w(mTag, s); } -}
\ No newline at end of file +} diff --git a/telephony/java/android/telephony/CellIdentityTdscdma.java b/telephony/java/android/telephony/CellIdentityTdscdma.java index 18ab6d4d2bd6..b99fe466ec46 100644 --- a/telephony/java/android/telephony/CellIdentityTdscdma.java +++ b/telephony/java/android/telephony/CellIdentityTdscdma.java @@ -16,9 +16,7 @@ package android.telephony; -import android.annotation.Nullable; import android.os.Parcel; -import android.text.TextUtils; import java.util.Objects; @@ -35,6 +33,8 @@ public final class CellIdentityTdscdma extends CellIdentity { private final int mCid; // 8-bit Cell Parameters ID described in TS 25.331, 0..127, INT_MAX if unknown. private final int mCpid; + // 16-bit UMTS Absolute RF Channel Number described in TS 25.101 sec. 5.4.3 + private final int mUarfcn; /** * @hide @@ -44,6 +44,7 @@ public final class CellIdentityTdscdma extends CellIdentity { mLac = Integer.MAX_VALUE; mCid = Integer.MAX_VALUE; mCpid = Integer.MAX_VALUE; + mUarfcn = Integer.MAX_VALUE; } /** @@ -52,28 +53,12 @@ public final class CellIdentityTdscdma extends CellIdentity { * @param lac 16-bit Location Area Code, 0..65535, INT_MAX if unknown * @param cid 28-bit UMTS Cell Identity described in TS 25.331, 0..268435455, INT_MAX if unknown * @param cpid 8-bit Cell Parameters ID described in TS 25.331, 0..127, INT_MAX if unknown + * @param uarfcn 16-bit UMTS Absolute RF Channel Number described in TS 25.101 sec. 5.4.3 * * @hide */ - public CellIdentityTdscdma(int mcc, int mnc, int lac, int cid, int cpid) { - this(String.valueOf(mcc), String.valueOf(mnc), lac, cid, cpid, null, null); - } - - /** - * @param mcc 3-digit Mobile Country Code in string format - * @param mnc 2 or 3-digit Mobile Network Code in string format - * @param lac 16-bit Location Area Code, 0..65535, INT_MAX if unknown - * @param cid 28-bit UMTS Cell Identity described in TS 25.331, 0..268435455, INT_MAX if unknown - * @param cpid 8-bit Cell Parameters ID described in TS 25.331, 0..127, INT_MAX if unknown - * - * FIXME: This is a temporary constructor to facilitate migration. - * @hide - */ - public CellIdentityTdscdma(String mcc, String mnc, int lac, int cid, int cpid) { - super(TAG, TYPE_TDSCDMA, mcc, mnc, null, null); - mLac = lac; - mCid = cid; - mCpid = cpid; + public CellIdentityTdscdma(int mcc, int mnc, int lac, int cid, int cpid, int uarfcn) { + this(String.valueOf(mcc), String.valueOf(mnc), lac, cid, cpid, uarfcn, null, null); } /** @@ -82,22 +67,24 @@ public final class CellIdentityTdscdma extends CellIdentity { * @param lac 16-bit Location Area Code, 0..65535, INT_MAX if unknown * @param cid 28-bit UMTS Cell Identity described in TS 25.331, 0..268435455, INT_MAX if unknown * @param cpid 8-bit Cell Parameters ID described in TS 25.331, 0..127, INT_MAX if unknown + * @param uarfcn 16-bit UMTS Absolute RF Channel Number described in TS 25.101 sec. 5.4.3 * @param alphal long alpha Operator Name String or Enhanced Operator Name String * @param alphas short alpha Operator Name String or Enhanced Operator Name String * * @hide */ - public CellIdentityTdscdma(String mcc, String mnc, int lac, int cid, int cpid, + public CellIdentityTdscdma(String mcc, String mnc, int lac, int cid, int cpid, int uarfcn, String alphal, String alphas) { super(TAG, TYPE_TDSCDMA, mcc, mnc, alphal, alphas); mLac = lac; mCid = cid; mCpid = cpid; + mUarfcn = uarfcn; } private CellIdentityTdscdma(CellIdentityTdscdma cid) { this(cid.mMccStr, cid.mMncStr, cid.mLac, cid.mCid, - cid.mCpid, cid.mAlphaLong, cid.mAlphaShort); + cid.mCpid, cid.mUarfcn, cid.mAlphaLong, cid.mAlphaShort); } CellIdentityTdscdma copy() { @@ -141,9 +128,10 @@ public final class CellIdentityTdscdma extends CellIdentity { return mCpid; } + /** @hide */ @Override - public int hashCode() { - return Objects.hash(mLac, mCid, mCpid, super.hashCode()); + public int getChannelNumber() { + return mUarfcn; } @Override @@ -157,24 +145,29 @@ public final class CellIdentityTdscdma extends CellIdentity { } CellIdentityTdscdma o = (CellIdentityTdscdma) other; - return TextUtils.equals(mMccStr, o.mMccStr) - && TextUtils.equals(mMncStr, o.mMncStr) - && mLac == o.mLac + return mLac == o.mLac && mCid == o.mCid && mCpid == o.mCpid + && mUarfcn == o.mUarfcn && super.equals(other); } @Override + public int hashCode() { + return Objects.hash(mLac, mCid, mCpid, mUarfcn, super.hashCode()); + } + + @Override public String toString() { return new StringBuilder(TAG) .append(":{ mMcc=").append(mMccStr) .append(" mMnc=").append(mMncStr) + .append(" mAlphaLong=").append(mAlphaLong) + .append(" mAlphaShort=").append(mAlphaShort) .append(" mLac=").append(mLac) .append(" mCid=").append(mCid) .append(" mCpid=").append(mCpid) - .append(" mAlphaLong=").append(mAlphaLong) - .append(" mAlphaShort=").append(mAlphaShort) + .append(" mUarfcn=").append(mUarfcn) .append("}").toString(); } @@ -186,6 +179,7 @@ public final class CellIdentityTdscdma extends CellIdentity { dest.writeInt(mLac); dest.writeInt(mCid); dest.writeInt(mCpid); + dest.writeInt(mUarfcn); } /** Construct from Parcel, type has already been processed */ @@ -194,7 +188,7 @@ public final class CellIdentityTdscdma extends CellIdentity { mLac = in.readInt(); mCid = in.readInt(); mCpid = in.readInt(); - + mUarfcn = in.readInt(); if (DBG) log(toString()); } diff --git a/telephony/java/android/telephony/CellIdentityWcdma.java b/telephony/java/android/telephony/CellIdentityWcdma.java index 984483edd8da..43f9406be79e 100644 --- a/telephony/java/android/telephony/CellIdentityWcdma.java +++ b/telephony/java/android/telephony/CellIdentityWcdma.java @@ -35,7 +35,7 @@ public final class CellIdentityWcdma extends CellIdentity { private final int mCid; // 9-bit UMTS Primary Scrambling Code described in TS 25.331, 0..511 private final int mPsc; - // 16-bit UMTS Absolute RF Channel Number + // 16-bit UMTS Absolute RF Channel Number described in TS 25.101 sec. 5.4.4 private final int mUarfcn; /** @@ -70,7 +70,7 @@ public final class CellIdentityWcdma extends CellIdentity { * @param lac 16-bit Location Area Code, 0..65535 * @param cid 28-bit UMTS Cell Identity * @param psc 9-bit UMTS Primary Scrambling Code - * @param uarfcn 16-bit UMTS Absolute RF Channel Number + * @param uarfcn 16-bit UMTS Absolute RF Channel Number described in TS 25.101 sec. 5.4.3 * * @hide */ @@ -83,7 +83,7 @@ public final class CellIdentityWcdma extends CellIdentity { * @param lac 16-bit Location Area Code, 0..65535 * @param cid 28-bit UMTS Cell Identity * @param psc 9-bit UMTS Primary Scrambling Code - * @param uarfcn 16-bit UMTS Absolute RF Channel Number + * @param uarfcn 16-bit UMTS Absolute RF Channel Number described in TS 25.101 sec. 5.4.3 * @param mccStr 3-digit Mobile Country Code in string format * @param mncStr 2 or 3-digit Mobile Network Code in string format * @param alphal long alpha Operator Name String or Enhanced Operator Name String diff --git a/telephony/java/android/telephony/CellInfo.java b/telephony/java/android/telephony/CellInfo.java index 9232ed7167cc..3aab3fc9e199 100644 --- a/telephony/java/android/telephony/CellInfo.java +++ b/telephony/java/android/telephony/CellInfo.java @@ -19,6 +19,7 @@ package android.telephony; import android.annotation.IntDef; import android.os.Parcel; import android.os.Parcelable; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -36,6 +37,8 @@ public abstract class CellInfo implements Parcelable { protected static final int TYPE_LTE = 3; /** @hide */ protected static final int TYPE_WCDMA = 4; + /** @hide */ + protected static final int TYPE_TDCDMA = 5; // Type to distinguish where time stamp gets recorded. @@ -260,6 +263,7 @@ public abstract class CellInfo implements Parcelable { case TYPE_CDMA: return CellInfoCdma.createFromParcelBody(in); case TYPE_LTE: return CellInfoLte.createFromParcelBody(in); case TYPE_WCDMA: return CellInfoWcdma.createFromParcelBody(in); + case TYPE_TDCDMA: return CellInfoTdscdma.createFromParcelBody(in); default: throw new RuntimeException("Bad CellInfo Parcel"); } } diff --git a/telephony/java/android/telephony/CellInfoCdma.java b/telephony/java/android/telephony/CellInfoCdma.java index 6f2f1f677859..6403bc5a16a1 100644 --- a/telephony/java/android/telephony/CellInfoCdma.java +++ b/telephony/java/android/telephony/CellInfoCdma.java @@ -21,7 +21,7 @@ import android.os.Parcelable; import android.telephony.Rlog; /** - * Immutable cell information from a point in time. + * A {@link CellInfo} representing a CDMA cell that provides identity and measurement info. */ public final class CellInfoCdma extends CellInfo implements Parcelable { diff --git a/telephony/java/android/telephony/CellInfoGsm.java b/telephony/java/android/telephony/CellInfoGsm.java index 1bedddb6b794..a3a9b315241e 100644 --- a/telephony/java/android/telephony/CellInfoGsm.java +++ b/telephony/java/android/telephony/CellInfoGsm.java @@ -21,7 +21,7 @@ import android.os.Parcelable; import android.telephony.Rlog; /** - * Immutable cell information from a point in time. + * A {@link CellInfo} representing a GSM cell that provides identity and measurement info. */ public final class CellInfoGsm extends CellInfo implements Parcelable { diff --git a/telephony/java/android/telephony/CellInfoLte.java b/telephony/java/android/telephony/CellInfoLte.java index 287c9f044a07..b892e89a8517 100644 --- a/telephony/java/android/telephony/CellInfoLte.java +++ b/telephony/java/android/telephony/CellInfoLte.java @@ -21,7 +21,7 @@ import android.os.Parcelable; import android.telephony.Rlog; /** - * Immutable cell information from a point in time. + * A {@link CellInfo} representing an LTE cell that provides identity and measurement info. */ public final class CellInfoLte extends CellInfo implements Parcelable { diff --git a/telephony/java/android/telephony/CellInfoTdscdma.java b/telephony/java/android/telephony/CellInfoTdscdma.java new file mode 100644 index 000000000000..7084c51f1b8a --- /dev/null +++ b/telephony/java/android/telephony/CellInfoTdscdma.java @@ -0,0 +1,151 @@ +/* + * 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.telephony; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +/** + * A {@link CellInfo} representing a TD-SCDMA cell that provides identity and measurement info. + * + * @hide + */ +public final class CellInfoTdscdma extends CellInfo implements Parcelable { + + private static final String LOG_TAG = "CellInfoTdscdma"; + private static final boolean DBG = false; + + private CellIdentityTdscdma mCellIdentityTdscdma; + private CellSignalStrengthTdscdma mCellSignalStrengthTdscdma; + + /** @hide */ + public CellInfoTdscdma() { + super(); + mCellIdentityTdscdma = new CellIdentityTdscdma(); + mCellSignalStrengthTdscdma = new CellSignalStrengthTdscdma(); + } + + /** @hide */ + public CellInfoTdscdma(CellInfoTdscdma ci) { + super(ci); + this.mCellIdentityTdscdma = ci.mCellIdentityTdscdma.copy(); + this.mCellSignalStrengthTdscdma = ci.mCellSignalStrengthTdscdma.copy(); + } + + public CellIdentityTdscdma getCellIdentity() { + return mCellIdentityTdscdma; + } + /** @hide */ + public void setCellIdentity(CellIdentityTdscdma cid) { + mCellIdentityTdscdma = cid; + } + + public CellSignalStrengthTdscdma getCellSignalStrength() { + return mCellSignalStrengthTdscdma; + } + /** @hide */ + public void setCellSignalStrength(CellSignalStrengthTdscdma css) { + mCellSignalStrengthTdscdma = css; + } + + /** + * @return hash code + */ + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), mCellIdentityTdscdma, mCellSignalStrengthTdscdma); + } + + @Override + public boolean equals(Object other) { + if (!super.equals(other)) { + return false; + } + try { + CellInfoTdscdma o = (CellInfoTdscdma) other; + return mCellIdentityTdscdma.equals(o.mCellIdentityTdscdma) + && mCellSignalStrengthTdscdma.equals(o.mCellSignalStrengthTdscdma); + } catch (ClassCastException e) { + return false; + } + } + + @Override + public String toString() { + StringBuffer sb = new StringBuffer(); + + sb.append("CellInfoTdscdma:{"); + sb.append(super.toString()); + sb.append(" ").append(mCellIdentityTdscdma); + sb.append(" ").append(mCellSignalStrengthTdscdma); + sb.append("}"); + + return sb.toString(); + } + + /** Implement the Parcelable interface */ + @Override + public int describeContents() { + return 0; + } + + /** Implement the Parcelable interface */ + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags, TYPE_TDCDMA); + mCellIdentityTdscdma.writeToParcel(dest, flags); + mCellSignalStrengthTdscdma.writeToParcel(dest, flags); + } + + /** + * Construct a CellInfoTdscdma object from the given parcel + * where the token is already been processed. + */ + private CellInfoTdscdma(Parcel in) { + super(in); + mCellIdentityTdscdma = CellIdentityTdscdma.CREATOR.createFromParcel(in); + mCellSignalStrengthTdscdma = CellSignalStrengthTdscdma.CREATOR.createFromParcel(in); + } + + /** Implement the Parcelable interface */ + public static final Creator<CellInfoTdscdma> CREATOR = new Creator<CellInfoTdscdma>() { + @Override + public CellInfoTdscdma createFromParcel(Parcel in) { + in.readInt(); // Skip past token, we know what it is + return createFromParcelBody(in); + } + + @Override + public CellInfoTdscdma[] newArray(int size) { + return new CellInfoTdscdma[size]; + } + }; + + /** @hide */ + protected static CellInfoTdscdma createFromParcelBody(Parcel in) { + return new CellInfoTdscdma(in); + } + + /** + * log + */ + private static void log(String s) { + Rlog.w(LOG_TAG, s); + } +} diff --git a/telephony/java/android/telephony/CellInfoWcdma.java b/telephony/java/android/telephony/CellInfoWcdma.java index 06157022de34..005f3d341ec1 100644 --- a/telephony/java/android/telephony/CellInfoWcdma.java +++ b/telephony/java/android/telephony/CellInfoWcdma.java @@ -20,8 +20,10 @@ import android.os.Parcel; import android.os.Parcelable; import android.telephony.Rlog; +import java.util.Objects; + /** - * Immutable cell information from a point in time. + * A {@link CellInfo} representing a WCDMA cell that provides identity and measurement info. */ public final class CellInfoWcdma extends CellInfo implements Parcelable { @@ -66,7 +68,7 @@ public final class CellInfoWcdma extends CellInfo implements Parcelable { */ @Override public int hashCode() { - return super.hashCode() + mCellIdentityWcdma.hashCode() + mCellSignalStrengthWcdma.hashCode(); + return Objects.hash(super.hashCode(), mCellIdentityWcdma, mCellSignalStrengthWcdma); } @Override diff --git a/telephony/java/android/telephony/CellSignalStrengthCdma.java b/telephony/java/android/telephony/CellSignalStrengthCdma.java index 183f96ddab0b..aa6b207d2f31 100644 --- a/telephony/java/android/telephony/CellSignalStrengthCdma.java +++ b/telephony/java/android/telephony/CellSignalStrengthCdma.java @@ -104,7 +104,10 @@ public final class CellSignalStrengthCdma extends CellSignalStrength implements } /** - * Get signal level as an int from 0..4 + * Retrieve an abstract level value for the overall signal strength. + * + * @return a single integer from 0 to 4 representing the general signal quality. + * 0 represents very poor signal strength while 4 represents a very strong signal strength. */ @Override public int getLevel() { diff --git a/telephony/java/android/telephony/CellSignalStrengthGsm.java b/telephony/java/android/telephony/CellSignalStrengthGsm.java index 8687cd1c454b..cff159b991c0 100644 --- a/telephony/java/android/telephony/CellSignalStrengthGsm.java +++ b/telephony/java/android/telephony/CellSignalStrengthGsm.java @@ -82,7 +82,10 @@ public final class CellSignalStrengthGsm extends CellSignalStrength implements P } /** - * Get signal level as an int from 0..4 + * Retrieve an abstract level value for the overall signal strength. + * + * @return a single integer from 0 to 4 representing the general signal quality. + * 0 represents very poor signal strength while 4 represents a very strong signal strength. */ @Override public int getLevel() { diff --git a/telephony/java/android/telephony/CellSignalStrengthLte.java b/telephony/java/android/telephony/CellSignalStrengthLte.java index 2b6928e2e4f1..2f059f412b64 100644 --- a/telephony/java/android/telephony/CellSignalStrengthLte.java +++ b/telephony/java/android/telephony/CellSignalStrengthLte.java @@ -86,7 +86,10 @@ public final class CellSignalStrengthLte extends CellSignalStrength implements P } /** - * Get signal level as an int from 0..4 + * Retrieve an abstract level value for the overall signal strength. + * + * @return a single integer from 0 to 4 representing the general signal quality. + * 0 represents very poor signal strength while 4 represents a very strong signal strength. */ @Override public int getLevel() { diff --git a/telephony/java/android/telephony/CellSignalStrengthTdscdma.java b/telephony/java/android/telephony/CellSignalStrengthTdscdma.java new file mode 100644 index 000000000000..41859a3e96d9 --- /dev/null +++ b/telephony/java/android/telephony/CellSignalStrengthTdscdma.java @@ -0,0 +1,228 @@ +/* + * 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.telephony; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +/** + * Tdscdma signal strength related information. + * + * @hide + */ +public final class CellSignalStrengthTdscdma extends CellSignalStrength implements Parcelable { + + private static final String LOG_TAG = "CellSignalStrengthTdscdma"; + private static final boolean DBG = false; + + private static final int TDSCDMA_SIGNAL_STRENGTH_GREAT = 12; + private static final int TDSCDMA_SIGNAL_STRENGTH_GOOD = 8; + private static final int TDSCDMA_SIGNAL_STRENGTH_MODERATE = 5; + + private int mSignalStrength; // in ASU; Valid values are (0-31, 99) as defined in TS 27.007 8.5 + // or Integer.MAX_VALUE if unknown + private int mBitErrorRate; // bit error rate (0-7, 99) as defined in TS 27.007 8.5 or + // Integer.MAX_VALUE if unknown + private int mRscp; // Pilot power (0-96, 255) as defined in TS 27.007 8.69 or Integer.MAX_VALUE + // if unknown + + /** @hide */ + public CellSignalStrengthTdscdma() { + setDefaultValues(); + } + + /** @hide */ + public CellSignalStrengthTdscdma(int ss, int ber, int rscp) { + mSignalStrength = ss; + mBitErrorRate = ber; + mRscp = rscp; + } + + /** @hide */ + public CellSignalStrengthTdscdma(CellSignalStrengthTdscdma s) { + copyFrom(s); + } + + /** @hide */ + protected void copyFrom(CellSignalStrengthTdscdma s) { + mSignalStrength = s.mSignalStrength; + mBitErrorRate = s.mBitErrorRate; + mRscp = s.mRscp; + } + + /** @hide */ + @Override + public CellSignalStrengthTdscdma copy() { + return new CellSignalStrengthTdscdma(this); + } + + /** @hide */ + @Override + public void setDefaultValues() { + mSignalStrength = Integer.MAX_VALUE; + mBitErrorRate = Integer.MAX_VALUE; + mRscp = Integer.MAX_VALUE; + } + + /** + * Retrieve an abstract level value for the overall signal strength. + * + * @return a single integer from 0 to 4 representing the general signal quality. + * 0 represents very poor signal strength while 4 represents a very strong signal strength. + */ + @Override + public int getLevel() { + int level; + + // ASU ranges from 0 to 31 - TS 27.007 Sec 8.5 + // asu = 0 (-113dB or less) is very weak + // signal, its better to show 0 bars to the user in such cases. + // asu = 99 is a special case, where the signal strength is unknown. + int asu = mSignalStrength; + if (asu <= 2 || asu == 99) { + level = SIGNAL_STRENGTH_NONE_OR_UNKNOWN; + } else if (asu >= TDSCDMA_SIGNAL_STRENGTH_GREAT) { + level = SIGNAL_STRENGTH_GREAT; + } else if (asu >= TDSCDMA_SIGNAL_STRENGTH_GOOD) { + level = SIGNAL_STRENGTH_GOOD; + } else if (asu >= TDSCDMA_SIGNAL_STRENGTH_MODERATE) { + level = SIGNAL_STRENGTH_MODERATE; + } else { + level = SIGNAL_STRENGTH_POOR; + } + if (DBG) log("getLevel=" + level); + return level; + } + + /** + * Get the signal strength as dBm + */ + @Override + public int getDbm() { + int dBm; + + int level = mSignalStrength; + int asu = (level == 99 ? Integer.MAX_VALUE : level); + if (asu != Integer.MAX_VALUE) { + dBm = -113 + (2 * asu); + } else { + dBm = Integer.MAX_VALUE; + } + if (DBG) log("getDbm=" + dBm); + return dBm; + } + + /** + * Get the signal level as an asu value between 0..31, 99 is unknown + * Asu is calculated based on 3GPP RSRP. Refer to 3GPP 27.007 (Ver 10.3.0) Sec 8.69 + */ + @Override + public int getAsuLevel() { + // ASU ranges from 0 to 31 - TS 27.007 Sec 8.5 + // asu = 0 (-113dB or less) is very weak + // signal, its better to show 0 bars to the user in such cases. + // asu = 99 is a special case, where the signal strength is unknown. + int level = mSignalStrength; + if (DBG) log("getAsuLevel=" + level); + return level; + } + + @Override + public int hashCode() { + return Objects.hash(mSignalStrength, mBitErrorRate); + } + + @Override + public boolean equals(Object o) { + CellSignalStrengthTdscdma s; + + try { + s = (CellSignalStrengthTdscdma) o; + } catch (ClassCastException ex) { + return false; + } + + if (o == null) { + return false; + } + + return mSignalStrength == s.mSignalStrength + && mBitErrorRate == s.mBitErrorRate + && mRscp == s.mRscp; + } + + /** + * @return string representation. + */ + @Override + public String toString() { + return "CellSignalStrengthTdscdma:" + + " ss=" + mSignalStrength + + " ber=" + mBitErrorRate + + " rscp=" + mRscp; + } + + /** Implement the Parcelable interface */ + @Override + public void writeToParcel(Parcel dest, int flags) { + if (DBG) log("writeToParcel(Parcel, int): " + toString()); + dest.writeInt(mSignalStrength); + dest.writeInt(mBitErrorRate); + dest.writeInt(mRscp); + } + + /** + * Construct a SignalStrength object from the given parcel + * where the token is already been processed. + */ + private CellSignalStrengthTdscdma(Parcel in) { + mSignalStrength = in.readInt(); + mBitErrorRate = in.readInt(); + mRscp = in.readInt(); + if (DBG) log("CellSignalStrengthTdscdma(Parcel): " + toString()); + } + + /** Implement the Parcelable interface */ + @Override + public int describeContents() { + return 0; + } + + /** Implement the Parcelable interface */ + @SuppressWarnings("hiding") + public static final Parcelable.Creator<CellSignalStrengthTdscdma> CREATOR = + new Parcelable.Creator<CellSignalStrengthTdscdma>() { + @Override + public CellSignalStrengthTdscdma createFromParcel(Parcel in) { + return new CellSignalStrengthTdscdma(in); + } + + @Override + public CellSignalStrengthTdscdma[] newArray(int size) { + return new CellSignalStrengthTdscdma[size]; + } + }; + + /** + * log + */ + private static void log(String s) { + Rlog.w(LOG_TAG, s); + } +} diff --git a/telephony/java/android/telephony/CellSignalStrengthWcdma.java b/telephony/java/android/telephony/CellSignalStrengthWcdma.java index dd32a960db91..21cf0be96931 100644 --- a/telephony/java/android/telephony/CellSignalStrengthWcdma.java +++ b/telephony/java/android/telephony/CellSignalStrengthWcdma.java @@ -35,7 +35,13 @@ public final class CellSignalStrengthWcdma extends CellSignalStrength implements private static final int WCDMA_SIGNAL_STRENGTH_MODERATE = 5; private int mSignalStrength; // in ASU; Valid values are (0-31, 99) as defined in TS 27.007 8.5 - private int mBitErrorRate; // bit error rate (0-7, 99) as defined in TS 27.007 8.5 + // or Integer.MAX_VALUE if unknown + private int mBitErrorRate; // bit error rate (0-7, 99) as defined in TS 27.007 8.5 or + // Integer.MAX_VALUE if unknown + private int mRscp; // bit error rate (0-96, 255) as defined in TS 27.007 8.69 or + // Integer.MAX_VALUE if unknown + private int mEcNo; // signal to noise radio (0-49, 255) as defined in TS 27.007 8.69 or + // Integer.MAX_VALUE if unknown /** @hide */ public CellSignalStrengthWcdma() { @@ -43,9 +49,11 @@ public final class CellSignalStrengthWcdma extends CellSignalStrength implements } /** @hide */ - public CellSignalStrengthWcdma(int ss, int ber) { + public CellSignalStrengthWcdma(int ss, int ber, int rscp, int ecno) { mSignalStrength = ss; mBitErrorRate = ber; + mRscp = rscp; + mEcNo = ecno; } /** @hide */ @@ -57,6 +65,8 @@ public final class CellSignalStrengthWcdma extends CellSignalStrength implements protected void copyFrom(CellSignalStrengthWcdma s) { mSignalStrength = s.mSignalStrength; mBitErrorRate = s.mBitErrorRate; + mRscp = s.mRscp; + mEcNo = s.mEcNo; } /** @hide */ @@ -70,10 +80,15 @@ public final class CellSignalStrengthWcdma extends CellSignalStrength implements public void setDefaultValues() { mSignalStrength = Integer.MAX_VALUE; mBitErrorRate = Integer.MAX_VALUE; + mRscp = Integer.MAX_VALUE; + mEcNo = Integer.MAX_VALUE; } /** - * Get signal level as an int from 0..4 + * Retrieve an abstract level value for the overall signal strength. + * + * @return a single integer from 0 to 4 representing the general signal quality. + * 0 represents very poor signal strength while 4 represents a very strong signal strength. */ @Override public int getLevel() { @@ -145,7 +160,10 @@ public final class CellSignalStrengthWcdma extends CellSignalStrength implements return false; } - return mSignalStrength == s.mSignalStrength && mBitErrorRate == s.mBitErrorRate; + return mSignalStrength == s.mSignalStrength + && mBitErrorRate == s.mBitErrorRate + && mRscp == s.mRscp + && mEcNo == s.mEcNo; } /** @@ -155,7 +173,9 @@ public final class CellSignalStrengthWcdma extends CellSignalStrength implements public String toString() { return "CellSignalStrengthWcdma:" + " ss=" + mSignalStrength - + " ber=" + mBitErrorRate; + + " ber=" + mBitErrorRate + + " rscp=" + mRscp + + " ecno=" + mEcNo; } /** Implement the Parcelable interface */ @@ -164,6 +184,8 @@ public final class CellSignalStrengthWcdma extends CellSignalStrength implements if (DBG) log("writeToParcel(Parcel, int): " + toString()); dest.writeInt(mSignalStrength); dest.writeInt(mBitErrorRate); + dest.writeInt(mRscp); + dest.writeInt(mEcNo); } /** @@ -173,6 +195,8 @@ public final class CellSignalStrengthWcdma extends CellSignalStrength implements private CellSignalStrengthWcdma(Parcel in) { mSignalStrength = in.readInt(); mBitErrorRate = in.readInt(); + mRscp = in.readInt(); + mEcNo = in.readInt(); if (DBG) log("CellSignalStrengthWcdma(Parcel): " + toString()); } diff --git a/telephony/java/android/telephony/NetworkRegistrationState.java b/telephony/java/android/telephony/NetworkRegistrationState.java index bba779d0c175..e881549489a3 100644 --- a/telephony/java/android/telephony/NetworkRegistrationState.java +++ b/telephony/java/android/telephony/NetworkRegistrationState.java @@ -85,12 +85,12 @@ public class NetworkRegistrationState implements Parcelable { public static final int SERVICE_TYPE_VIDEO = 4; public static final int SERVICE_TYPE_EMERGENCY = 5; - /** {@link AccessNetworkConstants.TransportType}*/ - private final int mTransportType; - @Domain private final int mDomain; + /** {@link AccessNetworkConstants.TransportType}*/ + private final int mTransportType; + @RegState private final int mRegState; @@ -112,19 +112,19 @@ public class NetworkRegistrationState implements Parcelable { private DataSpecificRegistrationStates mDataSpecificStates; /** - * @param transportType Transport type. Must be {@link AccessNetworkConstants.TransportType} * @param domain Network domain. Must be DOMAIN_CS or DOMAIN_PS. + * @param transportType Transport type. Must be {@link AccessNetworkConstants.TransportType} * @param regState Network registration state. * @param accessNetworkTechnology See TelephonyManager NETWORK_TYPE_XXXX. * @param reasonForDenial Reason for denial if the registration state is DENIED. * @param availableServices The supported service. * @param cellIdentity The identity representing a unique cell */ - public NetworkRegistrationState(int transportType, int domain, int regState, + public NetworkRegistrationState(int domain, int transportType, int regState, int accessNetworkTechnology, int reasonForDenial, boolean emergencyOnly, int[] availableServices, @Nullable CellIdentity cellIdentity) { - mTransportType = transportType; mDomain = domain; + mTransportType = transportType; mRegState = regState; mAccessNetworkTechnology = accessNetworkTechnology; mReasonForDenial = reasonForDenial; @@ -137,11 +137,11 @@ public class NetworkRegistrationState implements Parcelable { * Constructor for voice network registration states. * @hide */ - public NetworkRegistrationState(int transportType, int domain, int regState, + public NetworkRegistrationState(int domain, int transportType, int regState, int accessNetworkTechnology, int reasonForDenial, boolean emergencyOnly, int[] availableServices, @Nullable CellIdentity cellIdentity, boolean cssSupported, int roamingIndicator, int systemIsInPrl, int defaultRoamingIndicator) { - this(transportType, domain, regState, accessNetworkTechnology, + this(domain, transportType, regState, accessNetworkTechnology, reasonForDenial, emergencyOnly, availableServices, cellIdentity); mVoiceSpecificStates = new VoiceSpecificRegistrationStates(cssSupported, roamingIndicator, @@ -152,18 +152,18 @@ public class NetworkRegistrationState implements Parcelable { * Constructor for data network registration states. * @hide */ - public NetworkRegistrationState(int transportType, int domain, int regState, + public NetworkRegistrationState(int domain, int transportType, int regState, int accessNetworkTechnology, int reasonForDenial, boolean emergencyOnly, int[] availableServices, @Nullable CellIdentity cellIdentity, int maxDataCalls) { - this(transportType, domain, regState, accessNetworkTechnology, + this(domain, transportType, regState, accessNetworkTechnology, reasonForDenial, emergencyOnly, availableServices, cellIdentity); mDataSpecificStates = new DataSpecificRegistrationStates(maxDataCalls); } protected NetworkRegistrationState(Parcel source) { - mTransportType = source.readInt(); mDomain = source.readInt(); + mTransportType = source.readInt(); mRegState = source.readInt(); mAccessNetworkTechnology = source.readInt(); mReasonForDenial = source.readInt(); @@ -260,8 +260,8 @@ public class NetworkRegistrationState implements Parcelable { @Override public String toString() { return new StringBuilder("NetworkRegistrationState{") - .append("transportType=").append(mTransportType) .append(" domain=").append((mDomain == DOMAIN_CS) ? "CS" : "PS") + .append("transportType=").append(mTransportType) .append(" regState=").append(regStateToString(mRegState)) .append(" accessNetworkTechnology=") .append(TelephonyManager.getNetworkTypeName(mAccessNetworkTechnology)) @@ -290,8 +290,8 @@ public class NetworkRegistrationState implements Parcelable { } NetworkRegistrationState other = (NetworkRegistrationState) o; - return mTransportType == other.mTransportType - && mDomain == other.mDomain + return mDomain == other.mDomain + && mTransportType == other.mTransportType && mRegState == other.mRegState && mAccessNetworkTechnology == other.mAccessNetworkTechnology && mReasonForDenial == other.mReasonForDenial @@ -305,8 +305,8 @@ public class NetworkRegistrationState implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(mTransportType); dest.writeInt(mDomain); + dest.writeInt(mTransportType); dest.writeInt(mRegState); dest.writeInt(mAccessNetworkTechnology); dest.writeInt(mReasonForDenial); diff --git a/telephony/java/android/telephony/ServiceState.java b/telephony/java/android/telephony/ServiceState.java index ae999c31a42a..9e8529e20a7b 100644 --- a/telephony/java/android/telephony/ServiceState.java +++ b/telephony/java/android/telephony/ServiceState.java @@ -1569,13 +1569,14 @@ public class ServiceState implements Parcelable { /** * Get the network registration states with given transport type and domain. * + * @param domain The network domain. Must be {@link NetworkRegistrationState#DOMAIN_CS} or + * {@link NetworkRegistrationState#DOMAIN_PS}. * @param transportType The transport type. See {@link AccessNetworkConstants.TransportType} - * @param domain The network domain. Must be DOMAIN_CS or DOMAIN_PS. * @return The matching NetworkRegistrationState. * @hide */ @SystemApi - public NetworkRegistrationState getNetworkRegistrationStates(int transportType, int domain) { + public NetworkRegistrationState getNetworkRegistrationStates(int domain, int transportType) { synchronized (mNetworkRegistrationStates) { for (NetworkRegistrationState networkRegistrationState : mNetworkRegistrationStates) { if (networkRegistrationState.getTransportType() == transportType diff --git a/telephony/java/android/telephony/SubscriptionInfo.java b/telephony/java/android/telephony/SubscriptionInfo.java index 936505ca407d..d76e39b83801 100644 --- a/telephony/java/android/telephony/SubscriptionInfo.java +++ b/telephony/java/android/telephony/SubscriptionInfo.java @@ -33,9 +33,9 @@ import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.util.DisplayMetrics; +import android.util.Log; import java.util.Arrays; -import java.util.ArrayList; import java.util.List; /** @@ -105,12 +105,12 @@ public class SubscriptionInfo implements Parcelable { /** * Mobile Country Code */ - private int mMcc; + private String mMcc; /** * Mobile Network Code */ - private int mMnc; + private String mMnc; /** * ISO Country code for the subscription's provider @@ -138,11 +138,11 @@ public class SubscriptionInfo implements Parcelable { * @hide */ public SubscriptionInfo(int id, String iccId, int simSlotIndex, CharSequence displayName, - CharSequence carrierName, int nameSource, int iconTint, String number, int roaming, - Bitmap icon, int mcc, int mnc, String countryIso) { + CharSequence carrierName, int nameSource, int iconTint, String number, int roaming, + Bitmap icon, String mcc, String mnc, String countryIso) { this(id, iccId, simSlotIndex, displayName, carrierName, nameSource, iconTint, number, - roaming, icon, mcc, mnc, countryIso, false /* isEmbedded */, - null /* accessRules */, null /* accessRules */); + roaming, icon, mcc, mnc, countryIso, false /* isEmbedded */, + null /* accessRules */, null /* accessRules */); } /** @@ -150,7 +150,7 @@ public class SubscriptionInfo implements Parcelable { */ public SubscriptionInfo(int id, String iccId, int simSlotIndex, CharSequence displayName, CharSequence carrierName, int nameSource, int iconTint, String number, int roaming, - Bitmap icon, int mcc, int mnc, String countryIso, boolean isEmbedded, + Bitmap icon, String mcc, String mnc, String countryIso, boolean isEmbedded, @Nullable UiccAccessRule[] accessRules) { this(id, iccId, simSlotIndex, displayName, carrierName, nameSource, iconTint, number, roaming, icon, mcc, mnc, countryIso, isEmbedded, accessRules, null /* cardId */); @@ -161,7 +161,7 @@ public class SubscriptionInfo implements Parcelable { */ public SubscriptionInfo(int id, String iccId, int simSlotIndex, CharSequence displayName, CharSequence carrierName, int nameSource, int iconTint, String number, int roaming, - Bitmap icon, int mcc, int mnc, String countryIso, boolean isEmbedded, + Bitmap icon, String mcc, String mnc, String countryIso, boolean isEmbedded, @Nullable UiccAccessRule[] accessRules, String cardId) { this.mId = id; this.mIccId = iccId; @@ -316,15 +316,43 @@ public class SubscriptionInfo implements Parcelable { /** * @return the MCC. + * @deprecated Use {@link #getMccString()} instead. */ + @Deprecated public int getMcc() { - return this.mMcc; + try { + return this.mMcc == null ? 0 : Integer.valueOf(this.mMcc); + } catch (NumberFormatException e) { + Log.w(SubscriptionInfo.class.getSimpleName(), "MCC string is not a number"); + return 0; + } } /** * @return the MNC. + * @deprecated Use {@link #getMncString()} instead. */ + @Deprecated public int getMnc() { + try { + return this.mMnc == null ? 0 : Integer.valueOf(this.mMnc); + } catch (NumberFormatException e) { + Log.w(SubscriptionInfo.class.getSimpleName(), "MNC string is not a number"); + return 0; + } + } + + /** + * @return The MCC, as a string. + */ + public String getMccString() { + return this.mMcc; + } + + /** + * @return The MNC, as a string. + */ + public String getMncString() { return this.mMnc; } @@ -425,8 +453,8 @@ public class SubscriptionInfo implements Parcelable { int iconTint = source.readInt(); String number = source.readString(); int dataRoaming = source.readInt(); - int mcc = source.readInt(); - int mnc = source.readInt(); + String mcc = source.readString(); + String mnc = source.readString(); String countryIso = source.readString(); Bitmap iconBitmap = Bitmap.CREATOR.createFromParcel(source); boolean isEmbedded = source.readBoolean(); @@ -455,8 +483,8 @@ public class SubscriptionInfo implements Parcelable { dest.writeInt(mIconTint); dest.writeString(mNumber); dest.writeInt(mDataRoaming); - dest.writeInt(mMcc); - dest.writeInt(mMnc); + dest.writeString(mMcc); + dest.writeString(mMnc); dest.writeString(mCountryIso); mIconBitmap.writeToParcel(dest, flags); dest.writeBoolean(mIsEmbedded); diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java index ece646ca7b2c..17e7c49f6289 100644 --- a/telephony/java/android/telephony/SubscriptionManager.java +++ b/telephony/java/android/telephony/SubscriptionManager.java @@ -253,6 +253,20 @@ public class SubscriptionManager { public static final int SIM_PROVISIONED = 0; /** + * TelephonyProvider column name for the MCC associated with a SIM, stored as a string. + * <P>Type: TEXT (String)</P> + * @hide + */ + public static final String MCC_STRING = "mcc_string"; + + /** + * TelephonyProvider column name for the MNC associated with a SIM, stored as a string. + * <P>Type: TEXT (String)</P> + * @hide + */ + public static final String MNC_STRING = "mnc_string"; + + /** * TelephonyProvider column name for the MCC associated with a SIM. * <P>Type: INTEGER (int)</P> * @hide diff --git a/test-base/Android.bp b/test-base/Android.bp index a0e39856e9c7..d25b47727c0b 100644 --- a/test-base/Android.bp +++ b/test-base/Android.bp @@ -21,6 +21,7 @@ // Also contains the com.android.internal.util.Predicate[s] classes. java_library { name: "android.test.base", + installable: true, srcs: ["src/**/*.java"], @@ -42,6 +43,7 @@ java_library { // Also contains the com.android.internal.util.Predicate[s] classes. java_library { name: "legacy-test", + installable: true, sdk_version: "current", static_libs: ["android.test.base"], @@ -115,4 +117,5 @@ java_library_static { }, }, sdk_version: "current", + compile_dex: true, } diff --git a/test-mock/Android.bp b/test-mock/Android.bp index 51fa86bacc75..8d3faaef9f6b 100644 --- a/test-mock/Android.bp +++ b/test-mock/Android.bp @@ -18,6 +18,7 @@ // =================================== java_library { name: "android.test.mock", + installable: true, java_version: "1.8", srcs: ["src/**/*.java"], @@ -91,6 +92,7 @@ java_library_static { enabled: false, }, }, + compile_dex: true, } java_library_static { @@ -104,4 +106,5 @@ java_library_static { enabled: false, }, }, + compile_dex: true, } diff --git a/test-runner/Android.bp b/test-runner/Android.bp index b50ba3b58275..2caa6c45f16b 100644 --- a/test-runner/Android.bp +++ b/test-runner/Android.bp @@ -18,6 +18,7 @@ // ===================================== java_library { name: "android.test.runner", + installable: true, // Needs to be consistent with the repackaged version of this make target. java_version: "1.8", @@ -120,4 +121,5 @@ java_library_static { }, }, sdk_version: "current", + compile_dex: true, } diff --git a/vr/Android.bp b/vr/Android.bp index b5904d6f35ab..775ec968f59d 100644 --- a/vr/Android.bp +++ b/vr/Android.bp @@ -26,6 +26,7 @@ cc_library_shared { // Java platform library for vr stuff. java_library { name: "com.google.vr.platform", + installable: true, owner: "google", required: [ "libdvr_loader", |